 cb634fa8d4
			
		
	
	cb634fa8d4
	
	
	
		
			
			- fixed wrong condition in Collect to assign lastAP - fixed possible infinite loop in generating output when many SM worlds are present - fixed new VARIA code that changed a list used for every SM worlds and would throw if many SM worlds uses Aea rando and not AreaLayout
		
			
				
	
	
		
			805 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			805 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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])
 | |
|         Synonyms.alreadyUsed = []
 | |
|         # 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)
 |