SM update (#147)
* fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options
This commit is contained in:
parent
f673dfb7cf
commit
83cfd6ec05
13
SNIClient.py
13
SNIClient.py
|
@ -175,15 +175,22 @@ async def deathlink_kill_player(ctx: Context):
|
|||
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
|
||||
elif ctx.game == GAME_SM:
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
|
||||
if not ctx.death_link_allow_survive:
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
gamemode = None
|
||||
if ctx.game == GAME_ALTTP:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if not gamemode or gamemode[0] in DEATH_MODES:
|
||||
ctx.death_state = DeathState.dead
|
||||
elif ctx.game == GAME_SM:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if not gamemode or gamemode[0] in (DEATH_MODES if ctx.game == GAME_ALTTP else SM_DEATH_MODES):
|
||||
ctx.death_state = DeathState.dead
|
||||
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
|
||||
if health is not None:
|
||||
health = health[0] | (health[1] << 8)
|
||||
if not gamemode or gamemode[0] in SM_DEATH_MODES or (ctx.death_link_allow_survive and health is not None and health > 0):
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
|
||||
|
@ -884,6 +891,7 @@ async def game_watcher(ctx: Context):
|
|||
|
||||
if not ctx.rom:
|
||||
ctx.finished_game = False
|
||||
ctx.death_link_allow_survive = False
|
||||
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
|
||||
if game_name is None:
|
||||
continue
|
||||
|
@ -900,6 +908,7 @@ async def game_watcher(ctx: Context):
|
|||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
|
||||
SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
if death_link:
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
||||
ctx.locations_checked = set()
|
||||
|
|
|
@ -40,6 +40,14 @@ class StartLocation(Choice):
|
|||
option_Golden_Four = 14
|
||||
default = 1
|
||||
|
||||
class DeathLinkSurvive(Choice):
|
||||
"""When DeathLink is enabled and someone dies, you can survive with (enable_survive) if you have non-empty reserve tank."""
|
||||
displayname = "Death Link Survive"
|
||||
option_disable = 0
|
||||
option_enable = 1
|
||||
option_enable_survive = 3
|
||||
default = 0
|
||||
|
||||
class MaxDifficulty(Choice):
|
||||
displayname = "Maximum Difficulty"
|
||||
option_easy = 0
|
||||
|
@ -57,9 +65,6 @@ class MorphPlacement(Choice):
|
|||
option_normal = 1
|
||||
default = 0
|
||||
|
||||
class SuitsRestriction(DefaultOnToggle):
|
||||
displayname = "Suits Restriction"
|
||||
|
||||
class StrictMinors(Toggle):
|
||||
displayname = "Strict Minors"
|
||||
|
||||
|
@ -117,12 +122,15 @@ class BossRandomization(Toggle):
|
|||
displayname = "Boss Randomization"
|
||||
|
||||
class FunCombat(Toggle):
|
||||
"""if used, might force 'items' accessibility"""
|
||||
displayname = "Fun Combat"
|
||||
|
||||
class FunMovement(Toggle):
|
||||
"""if used, might force 'items' accessibility"""
|
||||
displayname = "Fun Movement"
|
||||
|
||||
class FunSuits(Toggle):
|
||||
"""if used, might force 'items' accessibility"""
|
||||
displayname = "Fun Suits"
|
||||
|
||||
class LayoutPatches(DefaultOnToggle):
|
||||
|
@ -188,7 +196,7 @@ sm_options: typing.Dict[str, type(Option)] = {
|
|||
"start_inventory_removes_from_pool": StartItemsRemovesFromPool,
|
||||
"preset": Preset,
|
||||
"start_location": StartLocation,
|
||||
"death_link": DeathLink,
|
||||
"death_link_survive": DeathLinkSurvive,
|
||||
#"majors_split": "Full",
|
||||
#"scav_num_locs": "10",
|
||||
#"scav_randomized": "off",
|
||||
|
@ -197,7 +205,7 @@ sm_options: typing.Dict[str, type(Option)] = {
|
|||
#"progression_speed": "medium",
|
||||
#"progression_difficulty": "normal",
|
||||
"morph_placement": MorphPlacement,
|
||||
"suits_restriction": SuitsRestriction,
|
||||
#"suits_restriction": SuitsRestriction,
|
||||
#"hide_items": "off",
|
||||
"strict_minors": StrictMinors,
|
||||
"missile_qty": MissileQty,
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
import copy
|
||||
import os
|
||||
import threading
|
||||
from typing import Set
|
||||
from typing import Set, List
|
||||
|
||||
logger = logging.getLogger("Super Metroid")
|
||||
|
||||
|
@ -59,7 +59,7 @@ class SMWorld(World):
|
|||
|
||||
def sm_init(self, parent: MultiWorld):
|
||||
if (hasattr(parent, "state")): # for unit tests where MultiWorld is instanciated before worlds
|
||||
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff) for player in parent.get_game_players("Super Metroid")}
|
||||
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff, parent.state.smbm[player].onlyBossLeft) for player in parent.get_game_players("Super Metroid")}
|
||||
orig_init(self, parent)
|
||||
|
||||
|
||||
|
@ -88,6 +88,10 @@ class SMWorld(World):
|
|||
|
||||
if (self.variaRando.args.morphPlacement == "early"):
|
||||
self.world.local_items[self.player].value.add('Morph')
|
||||
|
||||
if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0):
|
||||
self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("items")
|
||||
logger.warning(f"accessibility forced to 'items' for player {self.world.get_player_name(self.player)} because of 'fun' settings")
|
||||
|
||||
def generate_basic(self):
|
||||
itemPool = self.variaRando.container.itemPool
|
||||
|
@ -274,7 +278,7 @@ class SMWorld(World):
|
|||
|
||||
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
|
||||
|
||||
deathLink = {0x277f04: [int(self.world.death_link[self.player])]}
|
||||
deathLink = {0x277f04: [self.world.death_link_survive[self.player].value]}
|
||||
|
||||
playerNames = {}
|
||||
playerNameIDMap = {}
|
||||
|
@ -476,17 +480,6 @@ class SMWorld(World):
|
|||
item.player != self.player or
|
||||
item.name != "Morph Ball"]
|
||||
|
||||
def post_fill(self):
|
||||
# increase maxDifficulty if only bosses is too difficult to beat game
|
||||
new_state = CollectionState(self.world)
|
||||
for item in self.world.itempool:
|
||||
if item.player == self.player:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events()
|
||||
if (any(not self.world.get_location(bossLoc, self.player).can_reach(new_state) for bossLoc in self.locked_items)):
|
||||
if (self.variaRando.randoExec.setup.services.onlyBossesLeft(self.variaRando.randoExec.setup.startAP, self.variaRando.randoExec.setup.container)):
|
||||
self.world.state.smbm[self.player].maxDiff = infinity
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
|
@ -494,6 +487,22 @@ class SMWorld(World):
|
|||
progitempool.sort(
|
||||
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
|
||||
|
||||
def post_fill(self):
|
||||
new_state = CollectionState(self.world)
|
||||
progitempool = []
|
||||
for item in self.world.itempool:
|
||||
if item.player == self.player and item.advancement:
|
||||
progitempool.append(item)
|
||||
|
||||
for item in progitempool:
|
||||
new_state.collect(item, True)
|
||||
|
||||
bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain']
|
||||
for bossLoc in bossesLoc:
|
||||
if (not self.world.get_location(bossLoc, self.player).can_reach(new_state)):
|
||||
self.world.state.smbm[self.player].onlyBossLeft = True
|
||||
break
|
||||
|
||||
def create_locations(self, player: int):
|
||||
for name, id in locations_lookup_name_to_id.items():
|
||||
self.locations[name] = SMLocation(player, name, id)
|
||||
|
|
|
@ -134,9 +134,9 @@ class AccessGraph(object):
|
|||
|
||||
def printGraph(self):
|
||||
if self.log.getEffectiveLevel() == logging.DEBUG:
|
||||
self.log("Area graph:")
|
||||
self.log.debug("Area graph:")
|
||||
for s, d in self.InterAreaTransitions:
|
||||
self.log("{} -> {}".format(s.Name, d.Name))
|
||||
self.log.debug("{} -> {}".format(s.Name, d.Name))
|
||||
|
||||
def addAccessPoint(self, ap):
|
||||
ap.distance = 0
|
||||
|
|
|
@ -566,6 +566,8 @@ class Helpers(object):
|
|||
# print('RIDLEY', ammoMargin, secs)
|
||||
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
|
||||
Settings.bossesDifficulty['Ridley'])
|
||||
if (sm.onlyBossLeft):
|
||||
diff = 1
|
||||
if diff < 0:
|
||||
return smboolFalse
|
||||
else:
|
||||
|
@ -580,6 +582,8 @@ class Helpers(object):
|
|||
#print('KRAID True ', ammoMargin, secs)
|
||||
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
|
||||
Settings.bossesDifficulty['Kraid'])
|
||||
if (sm.onlyBossLeft):
|
||||
diff = 1
|
||||
if diff < 0:
|
||||
return smboolFalse
|
||||
|
||||
|
@ -621,6 +625,8 @@ class Helpers(object):
|
|||
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
|
||||
|
@ -661,6 +667,8 @@ class Helpers(object):
|
|||
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,
|
||||
|
@ -707,6 +715,8 @@ class Helpers(object):
|
|||
# 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)
|
||||
|
|
|
@ -13,12 +13,13 @@ class SMBoolManager(object):
|
|||
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4()
|
||||
countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve']
|
||||
|
||||
def __init__(self, player=0, maxDiff=sys.maxsize):
|
||||
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False):
|
||||
self._items = { }
|
||||
self._counts = { }
|
||||
|
||||
self.player = player
|
||||
self.maxDiff = maxDiff
|
||||
self.onlyBossLeft = onlyBossLeft
|
||||
|
||||
# cache related
|
||||
self.cacheKey = 0
|
||||
|
|
|
@ -76,7 +76,7 @@ class ItemLocContainer(object):
|
|||
locs = copy.copy(self.unusedLocations)
|
||||
# we don't copy restriction state on purpose: it depends on
|
||||
# outside context we don't want to bring to the copy
|
||||
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff),
|
||||
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft),
|
||||
self.itemPoolBackup[:] if self.itemPoolBackup != None else self.itemPool[:],
|
||||
locs)
|
||||
ret.currentItems = self.currentItems[:]
|
||||
|
@ -103,7 +103,7 @@ class ItemLocContainer(object):
|
|||
# transfer collected items/locations to another container
|
||||
def transferCollected(self, dest):
|
||||
dest.currentItems = self.currentItems[:]
|
||||
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff)
|
||||
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft)
|
||||
dest.sm.addItems([item.Type for item in dest.currentItems])
|
||||
dest.itemLocations = copy.copy(self.itemLocations)
|
||||
dest.unrestrictedItems = copy.copy(self.unrestrictedItems)
|
||||
|
|
|
@ -311,7 +311,7 @@ def loadRandoPreset(world, player, args):
|
|||
args.animals = world.animals[player].value
|
||||
args.noVariaTweaks = not world.varia_tweaks[player].value
|
||||
args.maxDifficulty = diffs[world.max_difficulty[player].value]
|
||||
args.suitsRestriction = world.suits_restriction[player].value
|
||||
#args.suitsRestriction = world.suits_restriction[player].value
|
||||
#args.hideItems = world.hide_items[player].value
|
||||
args.strictMinors = world.strict_minors[player].value
|
||||
args.noLayout = not world.layout_patches[player].value
|
||||
|
|
Loading…
Reference in New Issue