mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
SM: 20221101 update (#1479)
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
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import random
|
||||
from enum import IntEnum,IntFlag
|
||||
import copy
|
||||
from ..logic.smbool import SMBool
|
||||
from ..rom.rom_patches import RomPatches
|
||||
@@ -11,7 +12,7 @@ colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice']
|
||||
# 1/15 chance to have the door set to grey
|
||||
colorsListGrey = colorsList * 2 + ['grey']
|
||||
|
||||
class Facing:
|
||||
class Facing(IntEnum):
|
||||
Left = 0
|
||||
Right = 1
|
||||
Top = 2
|
||||
@@ -38,9 +39,48 @@ colors2plm = {
|
||||
'ice': plmIce
|
||||
}
|
||||
|
||||
# door color indicators PLMs (flashing on the other side of colored doors)
|
||||
indicatorsDirection = {
|
||||
Facing.Left: Facing.Right,
|
||||
Facing.Right: Facing.Left,
|
||||
Facing.Top: Facing.Bottom,
|
||||
Facing.Bottom: Facing.Top
|
||||
}
|
||||
|
||||
# door facing left - right - top - bottom
|
||||
plmRedIndicator = [0xFBB0, 0xFBB6, 0xFBBC, 0xFBC2]
|
||||
plmGreenIndicator = [0xFBC8, 0xFBCE, 0xFBD4, 0xFBDA]
|
||||
plmYellowIndicator = [0xFBE0, 0xFBE6, 0xFBEC, 0xFBF2]
|
||||
plmGreyIndicator = [0xFBF8, 0xFBFE, 0xFC04, 0xFC0A]
|
||||
plmWaveIndicator = [0xF60B, 0xF611, 0xF617, 0xF61D]
|
||||
plmSpazerIndicator = [0xF63B, 0xF641, 0xF647, 0xF64D]
|
||||
plmPlasmaIndicator = [0xF623, 0xF629, 0xF62F, 0xF635]
|
||||
plmIceIndicator = [0xF653, 0xF659, 0xF65F, 0xF665]
|
||||
|
||||
colors2plmIndicator = {
|
||||
'red': plmRedIndicator,
|
||||
'green': plmGreenIndicator,
|
||||
'yellow': plmYellowIndicator,
|
||||
'grey': plmGreyIndicator,
|
||||
'wave': plmWaveIndicator,
|
||||
'spazer': plmSpazerIndicator,
|
||||
'plasma': plmPlasmaIndicator,
|
||||
'ice': plmIceIndicator
|
||||
}
|
||||
|
||||
class IndicatorFlag(IntFlag):
|
||||
Standard = 1
|
||||
AreaRando = 2
|
||||
DoorRando = 4
|
||||
|
||||
# indicator always there
|
||||
IndicatorAll = IndicatorFlag.Standard | IndicatorFlag.AreaRando | IndicatorFlag.DoorRando
|
||||
# indicator there when not in area rando
|
||||
IndicatorDoor = IndicatorFlag.Standard | IndicatorFlag.DoorRando
|
||||
|
||||
class Door(object):
|
||||
__slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors')
|
||||
def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None):
|
||||
__slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors','indicator')
|
||||
def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None,indicator=0):
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.vanillaColor = vanillaColor
|
||||
@@ -52,6 +92,7 @@ class Door(object):
|
||||
self.id = id
|
||||
# list of forbidden colors
|
||||
self.forbiddenColors = forbiddenColors
|
||||
self.indicator = indicator
|
||||
|
||||
def forceBlue(self):
|
||||
# custom start location, area, patches can force doors to blue
|
||||
@@ -115,21 +156,21 @@ class Door(object):
|
||||
def isRefillSave(self):
|
||||
return self.address is None
|
||||
|
||||
def writeColor(self, rom):
|
||||
def writeColor(self, rom, writeWordFunc):
|
||||
if self.isBlue() or self.isRefillSave():
|
||||
return
|
||||
|
||||
rom.writeWord(colors2plm[self.color][self.facing], self.address)
|
||||
writeWordFunc(colors2plm[self.color][self.facing], self.address)
|
||||
|
||||
# also set plm args high byte to never opened, even during escape
|
||||
if self.color == 'grey':
|
||||
rom.writeByte(0x90, self.address+5)
|
||||
|
||||
def readColor(self, rom):
|
||||
def readColor(self, rom, readWordFunc):
|
||||
if self.forced or self.isRefillSave():
|
||||
return
|
||||
|
||||
plm = rom.readWord(self.address)
|
||||
plm = readWordFunc(self.address)
|
||||
if plm in plmRed:
|
||||
self.setColor('red')
|
||||
elif plm in plmGreen:
|
||||
@@ -147,7 +188,15 @@ class Door(object):
|
||||
elif plm in plmIce:
|
||||
self.setColor('ice')
|
||||
else:
|
||||
raise Exception("Unknown color {} for {}".format(hex(plm), self.name))
|
||||
# we can't read the color, handle as grey door (can happen in race protected seeds)
|
||||
self.setColor('grey')
|
||||
|
||||
# gives the PLM ID for matching indicator door
|
||||
def getIndicatorPLM(self, indicatorFlags):
|
||||
ret = None
|
||||
if (indicatorFlags & self.indicator) != 0 and self.color in colors2plmIndicator:
|
||||
ret = colors2plmIndicator[self.color][indicatorsDirection[self.facing]]
|
||||
return ret
|
||||
|
||||
# for tracker
|
||||
def canHide(self):
|
||||
@@ -179,10 +228,10 @@ class DoorsManager():
|
||||
doorsDict = {}
|
||||
doors = {
|
||||
# crateria
|
||||
'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True),
|
||||
'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll),
|
||||
'LandingSiteTopRight': Door('LandingSiteTopRight', 0x07801e, 'yellow', Facing.Left),
|
||||
'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True),
|
||||
'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True),
|
||||
'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True, indicator=IndicatorDoor),
|
||||
'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True, indicator=IndicatorAll),
|
||||
'FlywayRight': Door('FlywayRight', 0x78420, 'red', Facing.Left),
|
||||
'GreenPiratesShaftBottomRight': Door('GreenPiratesShaftBottomRight', 0x78470, 'red', Facing.Left, canGrey=True),
|
||||
'RedBrinstarElevatorTop': Door('RedBrinstarElevatorTop', 0x78256, 'yellow', Facing.Bottom),
|
||||
@@ -190,34 +239,34 @@ class DoorsManager():
|
||||
# blue brinstar
|
||||
'ConstructionZoneRight': Door('ConstructionZoneRight', 0x78784, 'red', Facing.Left),
|
||||
# green brinstar
|
||||
'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True),
|
||||
'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True),
|
||||
'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True, indicator=IndicatorFlag.DoorRando),
|
||||
'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True, indicator=IndicatorDoor),
|
||||
'MainShaftRight': Door('MainShaftRight', 0x784be, 'red', Facing.Left),
|
||||
'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True),
|
||||
'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True, indicator=IndicatorAll),
|
||||
'EarlySupersRight': Door('EarlySupersRight', 0x78512, 'red', Facing.Left),
|
||||
'EtecoonEnergyTankLeft': Door('EtecoonEnergyTankLeft', 0x787c8, 'green', Facing.Right),
|
||||
# pink brinstar
|
||||
'BigPinkTopRight': Door('BigPinkTopRight', 0x78626, 'red', Facing.Left),
|
||||
'BigPinkRight': Door('BigPinkRight', 0x7861a, 'yellow', Facing.Left),
|
||||
'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True),
|
||||
'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll),
|
||||
'BigPinkBottomLeft': Door('BigPinkBottomLeft', 0x7862c, 'red', Facing.Right),
|
||||
# red brinstar
|
||||
'RedTowerLeft': Door('RedTowerLeft', 0x78866, 'yellow', Facing.Right),
|
||||
'RedBrinstarFirefleaLeft': Door('RedBrinstarFirefleaLeft', 0x7886e, 'red', Facing.Right),
|
||||
'RedTowerElevatorTopLeft': Door('RedTowerElevatorTopLeft', 0x788aa, 'green', Facing.Right),
|
||||
'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right),
|
||||
'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right, indicator=IndicatorAll),
|
||||
'RedTowerElevatorBottomLeft': Door('RedTowerElevatorBottomLeft', 0x788b6, 'green', Facing.Right),
|
||||
'BelowSpazerTopRight': Door('BelowSpazerTopRight', 0x78966, 'green', Facing.Left),
|
||||
# Wrecked ship
|
||||
'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True),
|
||||
'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True),
|
||||
'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top),
|
||||
'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll),
|
||||
'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True, indicator=IndicatorDoor),
|
||||
'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top, indicator=IndicatorFlag.AreaRando),
|
||||
'ElectricDeathRoomTopLeft': Door('ElectricDeathRoomTopLeft', 0x7c32f, 'red', Facing.Right),
|
||||
# Upper Norfair
|
||||
'BusinessCenterTopLeft': Door('BusinessCenterTopLeft', 0x78b00, 'green', Facing.Right),
|
||||
'BusinessCenterBottomLeft': Door('BusinessCenterBottomLeft', 0x78b0c, 'red', Facing.Right),
|
||||
'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True),
|
||||
'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left),
|
||||
'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True, indicator=IndicatorAll),
|
||||
'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left, indicator=IndicatorAll),
|
||||
'BubbleMountainTopRight': Door('BubbleMountainTopRight', 0x78c60, 'green', Facing.Left),
|
||||
'BubbleMountainTopLeft': Door('BubbleMountainTopLeft', 0x78c5a, 'green', Facing.Right),
|
||||
'SpeedBoosterHallRight': Door('SpeedBoosterHallRight', 0x78c7a, 'red', Facing.Left),
|
||||
@@ -229,13 +278,13 @@ class DoorsManager():
|
||||
'PostCrocomireUpperLeft': Door('PostCrocomireUpperLeft', 0x78bf4, 'red', Facing.Right),
|
||||
'PostCrocomireShaftRight': Door('PostCrocomireShaftRight', 0x78c0c, 'red', Facing.Left),
|
||||
# Lower Norfair
|
||||
'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top),
|
||||
'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow']),
|
||||
'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top, indicator=IndicatorFlag.AreaRando),
|
||||
'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow'], indicator=IndicatorFlag.AreaRando),
|
||||
# Maridia
|
||||
'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left),
|
||||
'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left, indicator=IndicatorAll),
|
||||
'FishTankRight': Door('FishTankRight', 0x7c475, 'red', Facing.Left),
|
||||
'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left),
|
||||
'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left),
|
||||
'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left, indicator=IndicatorDoor),
|
||||
'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left, indicator=IndicatorFlag.AreaRando),
|
||||
'PlasmaSparkBottom': Door('PlasmaSparkBottom', 0x7c577, 'green', Facing.Top),
|
||||
'OasisTop': Door('OasisTop', 0x7c5d3, 'green', Facing.Bottom),
|
||||
# refill/save
|
||||
@@ -310,12 +359,12 @@ class DoorsManager():
|
||||
|
||||
# call from rom loader
|
||||
@staticmethod
|
||||
def loadDoorsColor(rom):
|
||||
def loadDoorsColor(rom, readWordFunc):
|
||||
# force to blue some doors depending on patches
|
||||
DoorsManager.setDoorsColor()
|
||||
# for each door store it's color
|
||||
for door in DoorsManager.doors.values():
|
||||
door.readColor(rom)
|
||||
door.readColor(rom, readWordFunc)
|
||||
DoorsManager.debugDoorsColor()
|
||||
|
||||
# tell that we have randomized doors
|
||||
@@ -342,13 +391,24 @@ class DoorsManager():
|
||||
|
||||
# call from rom patcher
|
||||
@staticmethod
|
||||
def writeDoorsColor(rom, doors, player):
|
||||
def writeDoorsColor(rom, doors, player, readWordFunc):
|
||||
for door in DoorsManager.doorsDict[player].values():
|
||||
door.writeColor(rom)
|
||||
door.writeColor(rom, readWordFunc)
|
||||
# also set save/refill doors to blue
|
||||
if door.id is not None:
|
||||
doors.append(door.id)
|
||||
|
||||
# returns a dict {'DoorName': indicatorPlmType }
|
||||
@staticmethod
|
||||
def getIndicatorPLMs(player, indicatorFlags):
|
||||
ret = {}
|
||||
for doorName,door in DoorsManager.doorsDict[player].items():
|
||||
plm = door.getIndicatorPLM(indicatorFlags)
|
||||
if plm is not None:
|
||||
ret[doorName] = plm
|
||||
return ret
|
||||
|
||||
|
||||
# call from web
|
||||
@staticmethod
|
||||
def getAddressesToRead():
|
||||
|
||||
804
worlds/sm/variaRandomizer/utils/objectives.py
Normal file
804
worlds/sm/variaRandomizer/utils/objectives.py
Normal file
@@ -0,0 +1,804 @@
|
||||
import copy
|
||||
import random
|
||||
from ..rom.addresses import Addresses
|
||||
from ..rom.rom import pc_to_snes
|
||||
from ..logic.helpers import Bosses
|
||||
from ..logic.smbool import SMBool
|
||||
from ..logic.logic import Logic
|
||||
from ..graph.location import locationsDict
|
||||
from ..utils.parameters import Knows
|
||||
from ..utils import log
|
||||
import logging
|
||||
|
||||
LOG = log.get('Objectives')
|
||||
|
||||
class Synonyms(object):
|
||||
killSynonyms = [
|
||||
"defeat",
|
||||
"massacre",
|
||||
"slay",
|
||||
"wipe out",
|
||||
"erase",
|
||||
"finish",
|
||||
"destroy",
|
||||
"wreck",
|
||||
"smash",
|
||||
"crush",
|
||||
"end"
|
||||
]
|
||||
alreadyUsed = []
|
||||
@staticmethod
|
||||
def getVerb():
|
||||
verb = random.choice(Synonyms.killSynonyms)
|
||||
while verb in Synonyms.alreadyUsed:
|
||||
verb = random.choice(Synonyms.killSynonyms)
|
||||
Synonyms.alreadyUsed.append(verb)
|
||||
return verb
|
||||
|
||||
class Goal(object):
|
||||
def __init__(self, name, gtype, logicClearFunc, romClearFunc,
|
||||
escapeAccessPoints=None, objCompletedFuncAPs=lambda ap: [ap],
|
||||
exclusion=None, items=None, text=None, introText=None,
|
||||
available=True, expandableList=None, category=None, area=None,
|
||||
conflictFunc=None):
|
||||
self.name = name
|
||||
self.available = available
|
||||
self.clearFunc = logicClearFunc
|
||||
self.objCompletedFuncAPs = objCompletedFuncAPs
|
||||
# SNES addr in bank A1, see objectives.asm
|
||||
self.checkAddr = pc_to_snes(Addresses.getOne("objective[%s]" % romClearFunc)) & 0xffff
|
||||
self.escapeAccessPoints = escapeAccessPoints
|
||||
if self.escapeAccessPoints is None:
|
||||
self.escapeAccessPoints = (1, [])
|
||||
self.rank = -1
|
||||
# possible values:
|
||||
# - boss
|
||||
# - miniboss
|
||||
# - other
|
||||
self.gtype = gtype
|
||||
# example for kill three g4
|
||||
# {
|
||||
# "list": [list of objectives],
|
||||
# "type: "boss",
|
||||
# "limit": 2
|
||||
# }
|
||||
self.exclusion = exclusion
|
||||
if self.exclusion is None:
|
||||
self.exclusion = {"list": []}
|
||||
self.items = items
|
||||
if self.items is None:
|
||||
self.items = []
|
||||
self.text = name if text is None else text
|
||||
self.introText = introText
|
||||
self.useSynonym = text is not None
|
||||
self.expandableList = expandableList
|
||||
if self.expandableList is None:
|
||||
self.expandableList = []
|
||||
self.expandable = len(self.expandableList) > 0
|
||||
self.category = category
|
||||
self.area = area
|
||||
self.conflictFunc = conflictFunc
|
||||
# used by solver/isolver to know if a goal has been completed
|
||||
self.completed = False
|
||||
|
||||
def setRank(self, rank):
|
||||
self.rank = rank
|
||||
|
||||
def canClearGoal(self, smbm, ap=None):
|
||||
# not all objectives require an ap (like limit objectives)
|
||||
return self.clearFunc(smbm, ap)
|
||||
|
||||
def getText(self):
|
||||
out = "{}. ".format(self.rank)
|
||||
if self.useSynonym:
|
||||
out += self.text.format(Synonyms.getVerb())
|
||||
else:
|
||||
out += self.text
|
||||
assert len(out) <= 28, "Goal text '{}' is too long: {}, max 28".format(out, len(out))
|
||||
if self.introText is not None:
|
||||
self.introText = "%d. %s" % (self.rank, self.introText)
|
||||
else:
|
||||
self.introText = out
|
||||
return out
|
||||
|
||||
def getIntroText(self):
|
||||
assert self.introText is not None
|
||||
return self.introText
|
||||
|
||||
def isLimit(self):
|
||||
return "type" in self.exclusion
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def getBossEscapeAccessPoint(boss):
|
||||
return (1, [Bosses.accessPoints[boss]])
|
||||
|
||||
def getG4EscapeAccessPoints(n):
|
||||
return (n, [Bosses.accessPoints[boss] for boss in Bosses.Golden4()])
|
||||
|
||||
def getMiniBossesEscapeAccessPoints(n):
|
||||
return (n, [Bosses.accessPoints[boss] for boss in Bosses.miniBosses()])
|
||||
|
||||
def getAreaEscapeAccessPoints(area):
|
||||
return (1, list({list(loc.AccessFrom.keys())[0] for loc in Logic.locations if loc.GraphArea == area}))
|
||||
|
||||
_goalsList = [
|
||||
Goal("kill kraid", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Kraid'), "kraid_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("Kraid"),
|
||||
exclusion={"list": ["kill all G4", "kill one G4"]},
|
||||
items=["Kraid"],
|
||||
text="{} kraid",
|
||||
category="Bosses"),
|
||||
Goal("kill phantoon", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Phantoon'), "phantoon_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("Phantoon"),
|
||||
exclusion={"list": ["kill all G4", "kill one G4"]},
|
||||
items=["Phantoon"],
|
||||
text="{} phantoon",
|
||||
category="Bosses"),
|
||||
Goal("kill draygon", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Draygon'), "draygon_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("Draygon"),
|
||||
exclusion={"list": ["kill all G4", "kill one G4"]},
|
||||
items=["Draygon"],
|
||||
text="{} draygon",
|
||||
category="Bosses"),
|
||||
Goal("kill ridley", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Ridley'), "ridley_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("Ridley"),
|
||||
exclusion={"list": ["kill all G4", "kill one G4"]},
|
||||
items=["Ridley"],
|
||||
text="{} ridley",
|
||||
category="Bosses"),
|
||||
Goal("kill one G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 1), "boss_1_killed",
|
||||
escapeAccessPoints=getG4EscapeAccessPoints(1),
|
||||
exclusion={"list": ["kill kraid", "kill phantoon", "kill draygon", "kill ridley",
|
||||
"kill all G4", "kill two G4", "kill three G4"],
|
||||
"type": "boss",
|
||||
"limit": 0},
|
||||
text="{} one golden4",
|
||||
category="Bosses"),
|
||||
Goal("kill two G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 2), "boss_2_killed",
|
||||
escapeAccessPoints=getG4EscapeAccessPoints(2),
|
||||
exclusion={"list": ["kill all G4", "kill one G4", "kill three G4"],
|
||||
"type": "boss",
|
||||
"limit": 1},
|
||||
text="{} two golden4",
|
||||
category="Bosses"),
|
||||
Goal("kill three G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 3), "boss_3_killed",
|
||||
escapeAccessPoints=getG4EscapeAccessPoints(3),
|
||||
exclusion={"list": ["kill all G4", "kill one G4", "kill two G4"],
|
||||
"type": "boss",
|
||||
"limit": 2},
|
||||
text="{} three golden4",
|
||||
category="Bosses"),
|
||||
Goal("kill all G4", "other", lambda sm, ap: Bosses.allBossesDead(sm), "all_g4_dead",
|
||||
escapeAccessPoints=getG4EscapeAccessPoints(4),
|
||||
exclusion={"list": ["kill kraid", "kill phantoon", "kill draygon", "kill ridley", "kill one G4", "kill two G4", "kill three G4"]},
|
||||
items=["Kraid", "Phantoon", "Draygon", "Ridley"],
|
||||
text="{} all golden4",
|
||||
expandableList=["kill kraid", "kill phantoon", "kill draygon", "kill ridley"],
|
||||
category="Bosses"),
|
||||
Goal("kill spore spawn", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'SporeSpawn'), "spore_spawn_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("SporeSpawn"),
|
||||
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
|
||||
items=["SporeSpawn"],
|
||||
text="{} spore spawn",
|
||||
category="Minibosses"),
|
||||
Goal("kill botwoon", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'Botwoon'), "botwoon_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("Botwoon"),
|
||||
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
|
||||
items=["Botwoon"],
|
||||
text="{} botwoon",
|
||||
category="Minibosses"),
|
||||
Goal("kill crocomire", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'Crocomire'), "crocomire_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("Crocomire"),
|
||||
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
|
||||
items=["Crocomire"],
|
||||
text="{} crocomire",
|
||||
category="Minibosses"),
|
||||
Goal("kill golden torizo", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'GoldenTorizo'), "golden_torizo_is_dead",
|
||||
escapeAccessPoints=getBossEscapeAccessPoint("GoldenTorizo"),
|
||||
exclusion={"list": ["kill all mini bosses", "kill one miniboss"]},
|
||||
items=["GoldenTorizo"],
|
||||
text="{} golden torizo",
|
||||
category="Minibosses",
|
||||
conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))),
|
||||
Goal("kill one miniboss", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 1), "miniboss_1_killed",
|
||||
escapeAccessPoints=getMiniBossesEscapeAccessPoints(1),
|
||||
exclusion={"list": ["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo",
|
||||
"kill all mini bosses", "kill two minibosses", "kill three minibosses"],
|
||||
"type": "miniboss",
|
||||
"limit": 0},
|
||||
text="{} one miniboss",
|
||||
category="Minibosses"),
|
||||
Goal("kill two minibosses", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 2), "miniboss_2_killed",
|
||||
escapeAccessPoints=getMiniBossesEscapeAccessPoints(2),
|
||||
exclusion={"list": ["kill all mini bosses", "kill one miniboss", "kill three minibosses"],
|
||||
"type": "miniboss",
|
||||
"limit": 1},
|
||||
text="{} two minibosses",
|
||||
category="Minibosses"),
|
||||
Goal("kill three minibosses", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 3), "miniboss_3_killed",
|
||||
escapeAccessPoints=getMiniBossesEscapeAccessPoints(3),
|
||||
exclusion={"list": ["kill all mini bosses", "kill one miniboss", "kill two minibosses"],
|
||||
"type": "miniboss",
|
||||
"limit": 2},
|
||||
text="{} three minibosses",
|
||||
category="Minibosses"),
|
||||
Goal("kill all mini bosses", "other", lambda sm, ap: Bosses.allMiniBossesDead(sm), "all_mini_bosses_dead",
|
||||
escapeAccessPoints=getMiniBossesEscapeAccessPoints(4),
|
||||
exclusion={"list": ["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo",
|
||||
"kill one miniboss", "kill two minibosses", "kill three minibosses"]},
|
||||
items=["SporeSpawn", "Botwoon", "Crocomire", "GoldenTorizo"],
|
||||
text="{} all mini bosses",
|
||||
expandableList=["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo"],
|
||||
category="Minibosses",
|
||||
conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))),
|
||||
# not available in AP
|
||||
#Goal("finish scavenger hunt", "other", lambda sm, ap: SMBool(True), "scavenger_hunt_completed",
|
||||
# exclusion={"list": []}, # will be auto-completed
|
||||
# available=False),
|
||||
Goal("nothing", "other", lambda sm, ap: Objectives.objDict[sm.player].canAccess(sm, ap, "Landing Site"), "nothing_objective",
|
||||
escapeAccessPoints=(1, ["Landing Site"])), # with no objectives at all, escape auto triggers only in crateria
|
||||
Goal("collect 25% items", "items", lambda sm, ap: SMBool(True), "collect_25_items",
|
||||
exclusion={"list": ["collect 50% items", "collect 75% items", "collect 100% items"]},
|
||||
category="Items",
|
||||
introText="collect 25 percent of items"),
|
||||
Goal("collect 50% items", "items", lambda sm, ap: SMBool(True), "collect_50_items",
|
||||
exclusion={"list": ["collect 25% items", "collect 75% items", "collect 100% items"]},
|
||||
category="Items",
|
||||
introText="collect 50 percent of items"),
|
||||
Goal("collect 75% items", "items", lambda sm, ap: SMBool(True), "collect_75_items",
|
||||
exclusion={"list": ["collect 25% items", "collect 50% items", "collect 100% items"]},
|
||||
category="Items",
|
||||
introText="collect 75 percent of items"),
|
||||
Goal("collect 100% items", "items", lambda sm, ap: SMBool(True), "collect_100_items",
|
||||
exclusion={"list": ["collect 25% items", "collect 50% items", "collect 75% items", "collect all upgrades"]},
|
||||
category="Items",
|
||||
introText="collect all items"),
|
||||
Goal("collect all upgrades", "items", lambda sm, ap: SMBool(True), "all_major_items",
|
||||
category="Items"),
|
||||
Goal("clear crateria", "items", lambda sm, ap: SMBool(True), "crateria_cleared",
|
||||
category="Items",
|
||||
area="Crateria"),
|
||||
Goal("clear green brinstar", "items", lambda sm, ap: SMBool(True), "green_brin_cleared",
|
||||
category="Items",
|
||||
area="GreenPinkBrinstar"),
|
||||
Goal("clear red brinstar", "items", lambda sm, ap: SMBool(True), "red_brin_cleared",
|
||||
category="Items",
|
||||
area="RedBrinstar"),
|
||||
Goal("clear wrecked ship", "items", lambda sm, ap: SMBool(True), "ws_cleared",
|
||||
category="Items",
|
||||
area="WreckedShip"),
|
||||
Goal("clear kraid's lair", "items", lambda sm, ap: SMBool(True), "kraid_cleared",
|
||||
category="Items",
|
||||
area="Kraid"),
|
||||
Goal("clear upper norfair", "items", lambda sm, ap: SMBool(True), "upper_norfair_cleared",
|
||||
category="Items",
|
||||
area="Norfair"),
|
||||
Goal("clear croc's lair", "items", lambda sm, ap: SMBool(True), "croc_cleared",
|
||||
category="Items",
|
||||
area="Crocomire"),
|
||||
Goal("clear lower norfair", "items", lambda sm, ap: SMBool(True), "lower_norfair_cleared",
|
||||
category="Items",
|
||||
area="LowerNorfair"),
|
||||
Goal("clear west maridia", "items", lambda sm, ap: SMBool(True), "west_maridia_cleared",
|
||||
category="Items",
|
||||
area="WestMaridia"),
|
||||
Goal("clear east maridia", "items", lambda sm, ap: SMBool(True), "east_maridia_cleared",
|
||||
category="Items",
|
||||
area="EastMaridia"),
|
||||
Goal("tickle the red fish", "other",
|
||||
lambda sm, ap: sm.wand(sm.haveItem('Grapple'), Objectives.objDict[sm.player].canAccess(sm, ap, "Red Fish Room Bottom")),
|
||||
"fish_tickled",
|
||||
escapeAccessPoints=(1, ["Red Fish Room Bottom"]),
|
||||
objCompletedFuncAPs=lambda ap: ["Red Fish Room Bottom"],
|
||||
category="Memes"),
|
||||
Goal("kill the orange geemer", "other",
|
||||
lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Bowling"), # XXX this unnecessarily adds canPassBowling as requirement
|
||||
sm.wor(sm.haveItem('Wave'), sm.canUsePowerBombs())),
|
||||
"orange_geemer",
|
||||
escapeAccessPoints=(1, ["Bowling"]),
|
||||
objCompletedFuncAPs=lambda ap: ["Bowling"],
|
||||
text="{} orange geemer",
|
||||
category="Memes"),
|
||||
Goal("kill shaktool", "other",
|
||||
lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Oasis Bottom"),
|
||||
sm.canTraverseSandPits(),
|
||||
sm.canAccessShaktoolFromPantsRoom()),
|
||||
"shak_dead",
|
||||
escapeAccessPoints=(1, ["Oasis Bottom"]),
|
||||
objCompletedFuncAPs=lambda ap: ["Oasis Bottom"],
|
||||
text="{} shaktool",
|
||||
category="Memes"),
|
||||
Goal("activate chozo robots", "other", lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccessLocation(sm, ap, "Bomb"),
|
||||
Objectives.objDict[sm.player].canAccessLocation(sm, ap, "Gravity Suit"),
|
||||
sm.haveItem("GoldenTorizo"),
|
||||
sm.canPassLowerNorfairChozo()), # graph access implied by GT loc
|
||||
"all_chozo_robots",
|
||||
category="Memes",
|
||||
escapeAccessPoints=(3, ["Landing Site", "Screw Attack Bottom", "Bowling"]),
|
||||
objCompletedFuncAPs=lambda ap: ["Landing Site", "Screw Attack Bottom", "Bowling"],
|
||||
exclusion={"list": ["kill golden torizo"]},
|
||||
conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))),
|
||||
Goal("visit the animals", "other", lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Big Pink"), sm.haveItem("SpeedBooster"), # dachora
|
||||
Objectives.objDict[sm.player].canAccess(sm, ap, "Etecoons Bottom")), # Etecoons
|
||||
"visited_animals",
|
||||
category="Memes",
|
||||
escapeAccessPoints=(2, ["Big Pink", "Etecoons Bottom"]),
|
||||
objCompletedFuncAPs=lambda ap: ["Big Pink", "Etecoons Bottom"]),
|
||||
Goal("kill king cacatac", "other",
|
||||
lambda sm, ap: Objectives.objDict[sm.player].canAccess(sm, ap, 'Bubble Mountain Top'),
|
||||
"king_cac_dead",
|
||||
category="Memes",
|
||||
escapeAccessPoints=(1, ['Bubble Mountain Top']),
|
||||
objCompletedFuncAPs=lambda ap: ['Bubble Mountain Top'])
|
||||
]
|
||||
|
||||
|
||||
_goals = {goal.name:goal for goal in _goalsList}
|
||||
|
||||
def completeGoalData():
|
||||
# "nothing" is incompatible with everything
|
||||
_goals["nothing"].exclusion["list"] = [goal.name for goal in _goalsList]
|
||||
areaGoals = [goal.name for goal in _goalsList if goal.area is not None]
|
||||
# if we need 100% items, don't require "clear area", as it covers those
|
||||
_goals["collect 100% items"].exclusion["list"] += areaGoals[:]
|
||||
# if we have scav hunt, don't require "clear area" (HUD behaviour incompatibility)
|
||||
# not available in AP
|
||||
#_goals["finish scavenger hunt"].exclusion["list"] += areaGoals[:]
|
||||
# remove clear area goals if disabled tourian, as escape can trigger as soon as an area is cleared,
|
||||
# even if ship is not currently reachable
|
||||
for goal in areaGoals:
|
||||
_goals[goal].exclusion['tourian'] = "Disabled"
|
||||
|
||||
completeGoalData()
|
||||
|
||||
class Objectives(object):
|
||||
maxActiveGoals = 5
|
||||
vanillaGoals = ["kill kraid", "kill phantoon", "kill draygon", "kill ridley"]
|
||||
scavHuntGoal = ["finish scavenger hunt"]
|
||||
objDict = {}
|
||||
|
||||
def __init__(self, player=0, tourianRequired=True, randoSettings=None):
|
||||
self.player = player
|
||||
self.activeGoals = []
|
||||
self.nbActiveGoals = 0
|
||||
self.totalItemsCount = 100
|
||||
self.goals = copy.deepcopy(_goals)
|
||||
self.graph = None
|
||||
self._tourianRequired = tourianRequired
|
||||
self.randoSettings = randoSettings
|
||||
Objectives.objDict[player] = self
|
||||
|
||||
@property
|
||||
def tourianRequired(self):
|
||||
assert self._tourianRequired is not None
|
||||
return self._tourianRequired
|
||||
|
||||
def resetGoals(self):
|
||||
self.activeGoals = []
|
||||
self.nbActiveGoals = 0
|
||||
|
||||
def conflict(self, newGoal):
|
||||
if newGoal.exclusion.get('tourian') == "Disabled" and self.tourianRequired == False:
|
||||
LOG.debug("new goal %s conflicts with disabled Tourian" % newGoal.name)
|
||||
return True
|
||||
LOG.debug("check if new goal {} conflicts with existing active goals".format(newGoal.name))
|
||||
count = 0
|
||||
for goal in self.activeGoals:
|
||||
if newGoal.name in goal.exclusion["list"]:
|
||||
LOG.debug("new goal {} in exclusion list of active goal {}".format(newGoal.name, goal.name))
|
||||
return True
|
||||
if goal.name in newGoal.exclusion["list"]:
|
||||
LOG.debug("active goal {} in exclusion list of new goal {}".format(goal.name, newGoal.name))
|
||||
return True
|
||||
# count bosses/minibosses already active if new goal has a limit
|
||||
if newGoal.exclusion.get("type") == goal.gtype:
|
||||
count += 1
|
||||
LOG.debug("new goal limit type: {} same as active goal {}. count: {}".format(newGoal.exclusion["type"], goal.name, count))
|
||||
if count > newGoal.exclusion.get("limit", 0):
|
||||
LOG.debug("new goal {} limit {} is lower than active goals of type: {}".format(newGoal.name, newGoal.exclusion["limit"], newGoal.exclusion["type"]))
|
||||
return True
|
||||
LOG.debug("no direct conflict detected for new goal {}".format(newGoal.name))
|
||||
|
||||
# if at least one active goal has a limit and new goal has the same type of one of the existing limit
|
||||
# check that new goal doesn't exceed the limit
|
||||
for goal in self.activeGoals:
|
||||
goalExclusionType = goal.exclusion.get("type")
|
||||
if goalExclusionType is not None and goalExclusionType == newGoal.gtype:
|
||||
count = 0
|
||||
for lgoal in self.activeGoals:
|
||||
if lgoal.gtype == newGoal.gtype:
|
||||
count += 1
|
||||
# add new goal to the count
|
||||
if count >= goal.exclusion["limit"]:
|
||||
LOG.debug("new Goal {} would excess limit {} of active goal {}".format(newGoal.name, goal.exclusion["limit"], goal.name))
|
||||
return True
|
||||
|
||||
LOG.debug("no backward conflict detected for new goal {}".format(newGoal.name))
|
||||
|
||||
if self.randoSettings is not None and newGoal.conflictFunc is not None:
|
||||
if newGoal.conflictFunc(self.randoSettings, self.player):
|
||||
LOG.debug("new Goal {} is conflicting with rando settings".format(newGoal.name))
|
||||
return True
|
||||
LOG.debug("no conflict with rando settings detected for new goal {}".format(newGoal.name))
|
||||
|
||||
return False
|
||||
|
||||
def addGoal(self, goalName, completed=False):
|
||||
LOG.debug("addGoal: {}".format(goalName))
|
||||
goal = self.goals[goalName]
|
||||
if self.conflict(goal):
|
||||
return
|
||||
self.nbActiveGoals += 1
|
||||
assert self.nbActiveGoals <= self.maxActiveGoals, "Too many active goals"
|
||||
goal.setRank(self.nbActiveGoals)
|
||||
goal.completed = completed
|
||||
self.activeGoals.append(goal)
|
||||
|
||||
def removeGoal(self, goal):
|
||||
self.nbActiveGoals -= 1
|
||||
self.activeGoals.remove(goal)
|
||||
|
||||
def clearGoals(self):
|
||||
self.nbActiveGoals = 0
|
||||
self.activeGoals.clear()
|
||||
|
||||
def isGoalActive(self, goalName):
|
||||
return self.goals[goalName] in self.activeGoals
|
||||
|
||||
# having graph as a global sucks but Objectives instances are all over the place,
|
||||
# goals must access it, and it doesn't change often
|
||||
def setGraph(self, graph, maxDiff):
|
||||
self.graph = graph
|
||||
self.maxDiff = maxDiff
|
||||
for goalName, goal in self.goals.items():
|
||||
if goal.area is not None:
|
||||
goal.escapeAccessPoints = getAreaEscapeAccessPoints(goal.area)
|
||||
|
||||
def canAccess(self, sm, src, dst):
|
||||
return SMBool(self.graph.canAccess(sm, src, dst, self.maxDiff))
|
||||
|
||||
def canAccessLocation(self, sm, ap, locName):
|
||||
loc = locationsDict[locName]
|
||||
availLocs = self.graph.getAvailableLocations([loc], sm, self.maxDiff, ap)
|
||||
return SMBool(loc in availLocs)
|
||||
|
||||
def setVanilla(self):
|
||||
for goal in self.vanillaGoals:
|
||||
self.addGoal(goal)
|
||||
|
||||
def isVanilla(self):
|
||||
# kill G4 and/or scav hunt
|
||||
if len(self.activeGoals) == 1:
|
||||
for goal in self.activeGoals:
|
||||
if goal.name not in self.scavHuntGoal:
|
||||
return False
|
||||
return True
|
||||
elif len(self.activeGoals) == 4:
|
||||
for goal in self.activeGoals:
|
||||
if goal.name not in self.vanillaGoals:
|
||||
return False
|
||||
return True
|
||||
elif len(self.activeGoals) == 5:
|
||||
for goal in self.activeGoals:
|
||||
if goal.name not in self.vanillaGoals + self.scavHuntGoal:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def setScavengerHunt(self):
|
||||
self.addGoal("finish scavenger hunt")
|
||||
|
||||
def updateScavengerEscapeAccess(self, ap):
|
||||
assert self.isGoalActive("finish scavenger hunt")
|
||||
(_, apList) = self.goals['finish scavenger hunt'].escapeAccessPoints
|
||||
apList.append(ap)
|
||||
|
||||
def _replaceEscapeAccessPoints(self, goal, aps):
|
||||
(_, apList) = self.goals[goal].escapeAccessPoints
|
||||
apList.clear()
|
||||
apList += aps
|
||||
|
||||
def updateItemPercentEscapeAccess(self, collectedLocsAccessPoints):
|
||||
for pct in [25,50,75,100]:
|
||||
goal = 'collect %d%% items' % pct
|
||||
self._replaceEscapeAccessPoints(goal, collectedLocsAccessPoints)
|
||||
# not exactly accurate, but player has all upgrades to escape
|
||||
self._replaceEscapeAccessPoints("collect all upgrades", collectedLocsAccessPoints)
|
||||
|
||||
def setScavengerHuntFunc(self, scavClearFunc):
|
||||
self.goals["finish scavenger hunt"].clearFunc = scavClearFunc
|
||||
|
||||
def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None):
|
||||
def getPctFunc(pct, totalItemsCount):
|
||||
def f(sm, ap):
|
||||
nonlocal pct, totalItemsCount
|
||||
return sm.hasItemsPercent(pct, totalItemsCount)
|
||||
return f
|
||||
|
||||
for pct in [25,50,75,100]:
|
||||
goal = 'collect %d%% items' % pct
|
||||
self.goals[goal].clearFunc = getPctFunc(pct, totalItemsCount)
|
||||
if allUpgradeTypes is not None:
|
||||
self.goals["collect all upgrades"].clearFunc = lambda sm, ap: sm.haveItems(allUpgradeTypes)
|
||||
|
||||
def setAreaFuncs(self, funcsByArea):
|
||||
goalsByArea = {goal.area:goal for goalName, goal in self.goals.items()}
|
||||
for area, func in funcsByArea.items():
|
||||
if area in goalsByArea:
|
||||
goalsByArea[area].clearFunc = func
|
||||
|
||||
def setSolverMode(self, solver):
|
||||
self.setScavengerHuntFunc(solver.scavengerHuntComplete)
|
||||
# in rando we know the number of items after randomizing, so set the functions only for the solver
|
||||
self.setItemPercentFuncs(allUpgradeTypes=solver.majorUpgrades)
|
||||
|
||||
def getObjAreaFunc(area):
|
||||
def f(sm, ap):
|
||||
nonlocal solver, area
|
||||
visitedLocs = set([loc.Name for loc in solver.visitedLocations])
|
||||
return SMBool(all(locName in visitedLocs for locName in solver.splitLocsByArea[area]))
|
||||
return f
|
||||
self.setAreaFuncs({area:getObjAreaFunc(area) for area in solver.splitLocsByArea})
|
||||
|
||||
def expandGoals(self):
|
||||
LOG.debug("Active goals:"+str(self.activeGoals))
|
||||
# try to replace 'kill all G4' with the four associated objectives.
|
||||
# we need at least 3 empty objectives out of the max (-1 +4)
|
||||
if self.maxActiveGoals - self.nbActiveGoals < 3:
|
||||
return
|
||||
|
||||
expandable = None
|
||||
for goal in self.activeGoals:
|
||||
if goal.expandable:
|
||||
expandable = goal
|
||||
break
|
||||
|
||||
if expandable is None:
|
||||
return
|
||||
|
||||
LOG.debug("replace {} with {}".format(expandable.name, expandable.expandableList))
|
||||
self.removeGoal(expandable)
|
||||
for name in expandable.expandableList:
|
||||
self.addGoal(name)
|
||||
|
||||
# rebuild ranks
|
||||
for i, goal in enumerate(self.activeGoals, 1):
|
||||
goal.rank = i
|
||||
|
||||
# call from logic
|
||||
def canClearGoals(self, smbm, ap):
|
||||
result = SMBool(True)
|
||||
for goal in self.activeGoals:
|
||||
result = smbm.wand(result, goal.canClearGoal(smbm, ap))
|
||||
return result
|
||||
|
||||
# call from solver
|
||||
def checkGoals(self, smbm, ap):
|
||||
ret = {}
|
||||
|
||||
for goal in self.activeGoals:
|
||||
if goal.completed is True:
|
||||
continue
|
||||
# check if goal can be completed
|
||||
ret[goal.name] = goal.canClearGoal(smbm, ap)
|
||||
|
||||
return ret
|
||||
|
||||
def setGoalCompleted(self, goalName, completed):
|
||||
for goal in self.activeGoals:
|
||||
if goal.name == goalName:
|
||||
goal.completed = completed
|
||||
return
|
||||
assert False, "Can't set goal {} completion to {}, goal not active".format(goalName, completed)
|
||||
|
||||
def allGoalsCompleted(self):
|
||||
for goal in self.activeGoals:
|
||||
if goal.completed is False:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getGoalFromCheckFunction(self, checkFunction):
|
||||
for name, goal in self.goals.items():
|
||||
if goal.checkAddr == checkFunction:
|
||||
return goal
|
||||
assert True, "Goal with check function {} not found".format(hex(checkFunction))
|
||||
|
||||
def getTotalItemsCount(self):
|
||||
return self.totalItemsCount
|
||||
|
||||
# call from web
|
||||
def getAddressesToRead(self):
|
||||
terminator = 1
|
||||
objectiveSize = 2
|
||||
bytesToRead = (self.maxActiveGoals + terminator) * objectiveSize
|
||||
return [Addresses.getOne('objectivesList')+i for i in range(0, bytesToRead+1)] + Addresses.getWeb('totalItems') + Addresses.getWeb("itemsMask") + Addresses.getWeb("beamsMask")
|
||||
|
||||
def getExclusions(self):
|
||||
# to compute exclusions in the front end
|
||||
return {goalName: goal.exclusion for goalName, goal in self.goals.items()}
|
||||
|
||||
def getObjectivesTypes(self):
|
||||
# to compute exclusions in the front end
|
||||
types = {'boss': [], 'miniboss': []}
|
||||
for goalName, goal in self.goals.items():
|
||||
if goal.gtype in types:
|
||||
types[goal.gtype].append(goalName)
|
||||
return types
|
||||
|
||||
def getObjectivesSort(self):
|
||||
return list(self.goals.keys())
|
||||
|
||||
def getObjectivesCategories(self):
|
||||
return {goal.name: goal.category for goal in self.goals.values() if goal.category is not None}
|
||||
|
||||
# call from rando check pool and solver
|
||||
|
||||
def getMandatoryBosses(self):
|
||||
r = [goal.items for goal in self.activeGoals]
|
||||
return [item for items in r for item in items]
|
||||
|
||||
def checkLimitObjectives(self, beatableBosses):
|
||||
# check that there's enough bosses/minibosses for limit objectives
|
||||
from ..logic.smboolmanager import SMBoolManager
|
||||
smbm = SMBoolManager(self.player)
|
||||
smbm.addItems(beatableBosses)
|
||||
for goal in self.activeGoals:
|
||||
if not goal.isLimit():
|
||||
continue
|
||||
if not goal.canClearGoal(smbm):
|
||||
return False
|
||||
return True
|
||||
|
||||
# call from solver
|
||||
def getGoalsList(self):
|
||||
return [goal.name for goal in self.activeGoals]
|
||||
|
||||
# call from interactivesolver
|
||||
def getState(self):
|
||||
return {goal.name: goal.completed for goal in self.activeGoals}
|
||||
|
||||
def setState(self, state):
|
||||
for goalName, completed in state.items():
|
||||
self.addGoal(goalName, completed)
|
||||
|
||||
def resetGoals(self):
|
||||
for goal in self.activeGoals:
|
||||
goal.completed = False
|
||||
|
||||
# call from rando
|
||||
@staticmethod
|
||||
def getAllGoals(removeNothing=False):
|
||||
return [goal.name for goal in _goals.values() if goal.available and (not removeNothing or goal.name != "nothing")]
|
||||
|
||||
# call from rando
|
||||
def setRandom(self, nbGoals, availableGoals):
|
||||
while self.nbActiveGoals < nbGoals and availableGoals:
|
||||
goalName = random.choice(availableGoals)
|
||||
self.addGoal(goalName)
|
||||
availableGoals.remove(goalName)
|
||||
|
||||
# call from solver
|
||||
def readGoals(self, romReader):
|
||||
self.resetGoals()
|
||||
romReader.romFile.seek(Addresses.getOne('objectivesList'))
|
||||
checkFunction = romReader.romFile.readWord()
|
||||
while checkFunction != 0x0000:
|
||||
goal = self.getGoalFromCheckFunction(checkFunction)
|
||||
self.activeGoals.append(goal)
|
||||
checkFunction = romReader.romFile.readWord()
|
||||
|
||||
# read number of available items for items % objectives
|
||||
self.totalItemsCount = romReader.romFile.readByte(Addresses.getOne('totalItems'))
|
||||
|
||||
for goal in self.activeGoals:
|
||||
LOG.debug("active goal: {}".format(goal.name))
|
||||
|
||||
self._tourianRequired = not romReader.patchPresent('Escape_Trigger')
|
||||
LOG.debug("tourianRequired: {}".format(self.tourianRequired))
|
||||
|
||||
# call from rando
|
||||
def writeGoals(self, romFile):
|
||||
# write check functions
|
||||
romFile.seek(Addresses.getOne('objectivesList'))
|
||||
for goal in self.activeGoals:
|
||||
romFile.writeWord(goal.checkAddr)
|
||||
# list terminator
|
||||
romFile.writeWord(0x0000)
|
||||
|
||||
# compute chars
|
||||
char2tile = {
|
||||
'.': 0x4A,
|
||||
'?': 0x4B,
|
||||
'!': 0x4C,
|
||||
' ': 0x00,
|
||||
'%': 0x02,
|
||||
'*': 0x03,
|
||||
'0': 0x04,
|
||||
'a': 0x30,
|
||||
}
|
||||
for i in range(1, ord('z')-ord('a')+1):
|
||||
char2tile[chr(ord('a')+i)] = char2tile['a']+i
|
||||
for i in range(1, ord('9')-ord('0')+1):
|
||||
char2tile[chr(ord('0')+i)] = char2tile['0']+i
|
||||
|
||||
# write text
|
||||
tileSize = 2
|
||||
lineLength = 32 * tileSize
|
||||
firstChar = 3 * tileSize
|
||||
# start at 8th line
|
||||
baseAddr = Addresses.getOne('objectivesText') + lineLength * 8 + firstChar
|
||||
# space between two lines of text
|
||||
space = 3 if self.nbActiveGoals == 5 else 4
|
||||
for i, goal in enumerate(self.activeGoals):
|
||||
addr = baseAddr + i * lineLength * space
|
||||
text = goal.getText()
|
||||
romFile.seek(addr)
|
||||
for c in text:
|
||||
if c not in char2tile:
|
||||
continue
|
||||
romFile.writeWord(0x3800 + char2tile[c])
|
||||
|
||||
# write goal completed positions y in sprites OAM
|
||||
baseY = 0x40
|
||||
addr = Addresses.getOne('objectivesSpritesOAM')
|
||||
spritemapSize = 5 + 2
|
||||
for i, goal in enumerate(self.activeGoals):
|
||||
y = baseY + i * space * 8
|
||||
# sprite center is at 128
|
||||
y = (y - 128) & 0xFF
|
||||
romFile.writeByte(y, addr+4 + i*spritemapSize)
|
||||
|
||||
def writeIntroObjectives(self, rom, tourian):
|
||||
if self.isVanilla() and tourian == "Vanilla":
|
||||
return
|
||||
# objectives or tourian are not vanilla, prepare intro text
|
||||
# two \n for an actual newline
|
||||
text = "MISSION OBJECTIVES\n"
|
||||
for goal in self.activeGoals:
|
||||
text += "\n\n%s" % goal.getIntroText()
|
||||
text += "\n\n\nTOURIAN IS %s\n\n\n" % tourian
|
||||
text += "CHECK OBJECTIVES STATUS IN\n\n"
|
||||
text += "THE PAUSE SCREEN"
|
||||
# actually write text in ROM
|
||||
self._writeIntroText(rom, text.upper())
|
||||
|
||||
def _writeIntroText(self, rom, text, startX=1, startY=2):
|
||||
# for character translation
|
||||
charCodes = {
|
||||
' ': 0xD67D,
|
||||
'.': 0xD75D,
|
||||
'!': 0xD77B,
|
||||
"'": 0xD76F,
|
||||
'0': 0xD721,
|
||||
'A': 0xD685
|
||||
}
|
||||
def addCharRange(start, end, base): # inclusive range
|
||||
for c in range(ord(start), ord(end)+1):
|
||||
offset = c - ord(base)
|
||||
charCodes[chr(c)] = charCodes[base]+offset*6
|
||||
addCharRange('B', 'Z', 'A')
|
||||
addCharRange('1', '9', '0')
|
||||
# actually write chars
|
||||
x, y = startX, startY
|
||||
def writeChar(c, frameDelay=2):
|
||||
nonlocal rom, x, y
|
||||
assert x <= 0x1F and y <= 0x18, "Intro text formatting error (x=0x%x, y=0x%x):\n%s" % (x, y, text)
|
||||
if c == '\n':
|
||||
x = startX
|
||||
y += 1
|
||||
else:
|
||||
assert c in charCodes, "Invalid intro char "+c
|
||||
rom.writeWord(frameDelay)
|
||||
rom.writeByte(x)
|
||||
rom.writeByte(y)
|
||||
rom.writeWord(charCodes[c])
|
||||
x += 1
|
||||
rom.seek(Addresses.getOne('introText'))
|
||||
for c in text:
|
||||
writeChar(c)
|
||||
# write trailer, see intro_text.asm
|
||||
rom.writeWord(0xAE5B)
|
||||
rom.writeWord(0x9698)
|
||||
@@ -46,19 +46,19 @@ text2diff = {
|
||||
|
||||
def diff4solver(difficulty):
|
||||
if difficulty == -1:
|
||||
return "break"
|
||||
return ("break", "break")
|
||||
elif difficulty < medium:
|
||||
return "easy"
|
||||
return ("easy", "easy")
|
||||
elif difficulty < hard:
|
||||
return "medium"
|
||||
return ("medium", "medium")
|
||||
elif difficulty < harder:
|
||||
return "hard"
|
||||
return ("hard", "hard")
|
||||
elif difficulty < hardcore:
|
||||
return "harder"
|
||||
return ("harder", "very hard")
|
||||
elif difficulty < mania:
|
||||
return "hardcore"
|
||||
return ("hardcore", "hardcore")
|
||||
else:
|
||||
return "mania"
|
||||
return ("mania", "mania")
|
||||
|
||||
# allow multiple local repo
|
||||
appDir = str(Path(__file__).parents[4])
|
||||
@@ -120,7 +120,7 @@ class Knows:
|
||||
|
||||
Mockball = SMBool(True, easy, ['Mockball'])
|
||||
desc['Mockball'] = {'display': 'Mockball',
|
||||
'title': 'Morph from runing without loosing momentum to get Early Super and Ice Beam',
|
||||
'title': 'Morph from running without loosing momentum to get Early Super and Ice Beam',
|
||||
'href': 'https://wiki.supermetroid.run/index.php?title=Mockball',
|
||||
'rooms': ['Early Supers Room', 'Ice Beam Gate Room']}
|
||||
|
||||
@@ -161,7 +161,7 @@ class Knows:
|
||||
SpringBallJump = SMBool(True, hard, ['SpringBallJump'])
|
||||
desc['SpringBallJump'] = {'display': 'SpringBall-Jump',
|
||||
'title': 'Do a SpringBall Jump from a jump to Access to Wrecked Ship Etank without anything else, Suitless Maridia navigation',
|
||||
'href': 'https://www.youtube.com/watch?v=8ldQUIgBavw&t=49s',
|
||||
'href': 'https://www.twitch.tv/videos/147442861',
|
||||
'rooms': ['Sponge Bath', 'East Ocean',
|
||||
'Main Street', 'Crab Shaft', 'Pseudo Plasma Spark Room',
|
||||
'Mama Turtle Room', 'The Precious Room', 'Spring Ball Room', 'East Sand Hole',
|
||||
|
||||
@@ -56,7 +56,7 @@ def exists(resource: str):
|
||||
return os.path.exists(resource)
|
||||
|
||||
def isStdPreset(preset):
|
||||
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021']
|
||||
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021', 'Torneio_SGPT3']
|
||||
|
||||
def getPresetDir(preset) -> str:
|
||||
if isStdPreset(preset):
|
||||
@@ -316,6 +316,7 @@ class PresetLoaderDict(PresetLoader):
|
||||
|
||||
def getDefaultMultiValues():
|
||||
from ..graph.graph_utils import GraphUtils
|
||||
from ..utils.objectives import Objectives
|
||||
defaultMultiValues = {
|
||||
'startLocation': GraphUtils.getStartAccessPointNames(),
|
||||
'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'],
|
||||
@@ -323,7 +324,10 @@ def getDefaultMultiValues():
|
||||
'progressionDifficulty': ['easier', 'normal', 'harder'],
|
||||
'morphPlacement': ['early', 'normal'], #['early', 'late', 'normal'],
|
||||
'energyQty': ['ultra sparse', 'sparse', 'medium', 'vanilla'],
|
||||
'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive']
|
||||
'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive'],
|
||||
'areaRandomization': ['off', 'full', 'light'],
|
||||
'objective': Objectives.getAllGoals(removeNothing=True),
|
||||
'tourian': ['Vanilla', 'Fast', 'Disabled']
|
||||
}
|
||||
return defaultMultiValues
|
||||
|
||||
@@ -363,17 +367,16 @@ def loadRandoPreset(world, player, args):
|
||||
args.noVariaTweaks = not world.varia_tweaks[player].value
|
||||
args.maxDifficulty = diffs[world.max_difficulty[player].value]
|
||||
#args.suitsRestriction = world.suits_restriction[player].value
|
||||
#args.hideItems = world.hide_items[player].value
|
||||
args.hideItems = world.hide_items[player].value
|
||||
args.strictMinors = world.strict_minors[player].value
|
||||
args.noLayout = not world.layout_patches[player].value
|
||||
args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value]
|
||||
args.nerfedCharge = world.nerfed_charge[player].value
|
||||
args.area = world.area_randomization[player].value != 0
|
||||
if args.area:
|
||||
args.area = world.area_randomization[player].current_key
|
||||
if args.area != "off":
|
||||
args.areaLayoutBase = not world.area_layout[player].value
|
||||
args.lightArea = world.area_randomization[player].value == 1
|
||||
#args.escapeRando
|
||||
#args.noRemoveEscapeEnemies
|
||||
args.escapeRando = world.escape_rando[player].value
|
||||
args.noRemoveEscapeEnemies = not world.remove_escape_enemies[player].value
|
||||
args.doorsColorsRando = world.doors_colors_rando[player].value
|
||||
args.allowGreyDoors = world.allow_grey_doors[player].value
|
||||
args.bosses = world.boss_randomization[player].value
|
||||
@@ -384,7 +387,12 @@ def loadRandoPreset(world, player, args):
|
||||
if world.fun_suits[player].value:
|
||||
args.superFun.append("Suits")
|
||||
|
||||
ipsPatches = {"spin_jump_restart":"spinjumprestart", "rando_speed":"rando_speed", "elevators_doors_speed":"elevators_doors_speed", "refill_before_save":"refill_before_save"}
|
||||
ipsPatches = { "spin_jump_restart":"spinjumprestart",
|
||||
"rando_speed":"rando_speed",
|
||||
"elevators_speed":"elevators_speed",
|
||||
"fast_doors":"fast_doors",
|
||||
"refill_before_save":"refill_before_save",
|
||||
"relaxed_round_robin_cf":"relaxed_round_robin_cf"}
|
||||
for settingName, patchName in ipsPatches.items():
|
||||
if hasattr(world, settingName) and getattr(world, settingName)[player].value:
|
||||
args.patches.append(patchName + '.ips')
|
||||
@@ -399,7 +407,6 @@ def loadRandoPreset(world, player, args):
|
||||
#args.majorsSplit
|
||||
#args.scavNumLocs
|
||||
#args.scavRandomized
|
||||
#args.scavEscape
|
||||
args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value]
|
||||
#args.progressionDifficulty
|
||||
#args.progressionSpeed
|
||||
@@ -408,6 +415,8 @@ def loadRandoPreset(world, player, args):
|
||||
args.powerBombQty = world.power_bomb_qty[player].value / float(10)
|
||||
args.minorQty = world.minor_qty[player].value
|
||||
args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value]
|
||||
args.objective = world.objective[player].value
|
||||
args.tourian = defaultMultiValues["tourian"][world.tourian[player].value]
|
||||
#args.minimizerN
|
||||
#args.minimizerTourian
|
||||
|
||||
@@ -425,7 +434,6 @@ def getRandomizerDefaultParameters():
|
||||
defaultParams['majorsSplitMultiSelect'] = defaultMultiValues['majorsSplit']
|
||||
defaultParams['scavNumLocs'] = "10"
|
||||
defaultParams['scavRandomized'] = "off"
|
||||
defaultParams['scavEscape'] = "off"
|
||||
defaultParams['startLocation'] = "Landing Site"
|
||||
defaultParams['startLocationMultiSelect'] = defaultMultiValues['startLocation']
|
||||
defaultParams['maxDifficulty'] = 'hardcore'
|
||||
@@ -444,9 +452,13 @@ def getRandomizerDefaultParameters():
|
||||
defaultParams['minorQty'] = "100"
|
||||
defaultParams['energyQty'] = "vanilla"
|
||||
defaultParams['energyQtyMultiSelect'] = defaultMultiValues['energyQty']
|
||||
defaultParams['objectiveRandom'] = "off"
|
||||
defaultParams['nbObjective'] = "4"
|
||||
defaultParams['objective'] = ["kill all G4"]
|
||||
defaultParams['objectiveMultiSelect'] = defaultMultiValues['objective']
|
||||
defaultParams['tourian'] = "Vanilla"
|
||||
defaultParams['areaRandomization'] = "off"
|
||||
defaultParams['areaLayout'] = "off"
|
||||
defaultParams['lightAreaRandomization'] = "off"
|
||||
defaultParams['doorsColorsRando'] = "off"
|
||||
defaultParams['allowGreyDoors'] = "off"
|
||||
defaultParams['escapeRando'] = "off"
|
||||
@@ -454,7 +466,6 @@ def getRandomizerDefaultParameters():
|
||||
defaultParams['bossRandomization'] = "off"
|
||||
defaultParams['minimizer'] = "off"
|
||||
defaultParams['minimizerQty'] = "45"
|
||||
defaultParams['minimizerTourian'] = "off"
|
||||
defaultParams['funCombat'] = "off"
|
||||
defaultParams['funMovement'] = "off"
|
||||
defaultParams['funSuits'] = "off"
|
||||
@@ -463,8 +474,10 @@ def getRandomizerDefaultParameters():
|
||||
defaultParams['gravityBehaviour'] = "Balanced"
|
||||
defaultParams['gravityBehaviourMultiSelect'] = defaultMultiValues['gravityBehaviour']
|
||||
defaultParams['nerfedCharge'] = "off"
|
||||
defaultParams['relaxed_round_robin_cf'] = "off"
|
||||
defaultParams['itemsounds'] = "on"
|
||||
defaultParams['elevators_doors_speed'] = "on"
|
||||
defaultParams['elevators_speed'] = "on"
|
||||
defaultParams['fast_doors'] = "on"
|
||||
defaultParams['spinjumprestart'] = "off"
|
||||
defaultParams['rando_speed'] = "off"
|
||||
defaultParams['Infinite_Space_Jump'] = "off"
|
||||
@@ -496,4 +509,22 @@ def fixEnergy(items):
|
||||
items.append('{}-ETank'.format(maxETank))
|
||||
if maxReserve > 0:
|
||||
items.append('{}-Reserve'.format(maxReserve))
|
||||
|
||||
|
||||
# keep biggest crystal flash
|
||||
cfs = [i for i in items if i.find('CrystalFlash') != -1]
|
||||
if len(cfs) > 1:
|
||||
maxCf = 0
|
||||
for cf in cfs:
|
||||
nCf = int(cf[0:cf.find('-CrystalFlash')])
|
||||
if nCf > maxCf:
|
||||
maxCf = nCf
|
||||
items.remove(cf)
|
||||
items.append('{}-CrystalFlash'.format(maxCf))
|
||||
return items
|
||||
|
||||
def dumpErrorMsg(outFileName, msg):
|
||||
print("DIAG: " + msg)
|
||||
if outFileName is not None:
|
||||
with open(outFileName, 'w') as jsonFile:
|
||||
json.dump({"errorMsg": msg}, jsonFile)
|
||||
@@ -1,3 +1,5 @@
|
||||
import Utils
|
||||
|
||||
# version displayed on the title screen, must be a max 32 chars [a-z0-9.-] string
|
||||
# either 'beta' or 'r.yyyy.mm.dd'
|
||||
displayedVersion = 'beta'
|
||||
displayedVersion = 'r.2022.11.01-ap.' + Utils.__version__
|
||||
|
||||
Reference in New Issue
Block a user