diff --git a/SNIClient.py b/SNIClient.py index 64f562b0..2229eadf 100644 --- a/SNIClient.py +++ b/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() diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 16ccb5d2..4950b03a 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -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, diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 005844ea..f60b63d3 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -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) diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index 0f88678f..bcbf1381 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -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 diff --git a/worlds/sm/variaRandomizer/logic/helpers.py b/worlds/sm/variaRandomizer/logic/helpers.py index 72d2e4be..4df46657 100644 --- a/worlds/sm/variaRandomizer/logic/helpers.py +++ b/worlds/sm/variaRandomizer/logic/helpers.py @@ -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) diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index b4ee2918..93e50424 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -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 diff --git a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py index 8ddf2a87..1ab43355 100644 --- a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py +++ b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py @@ -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) diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index d64cb252..402c6299 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -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