807 lines
34 KiB
Python
807 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, container=None):
|
|
def getPctFunc(total_needed, container):
|
|
def f(sm, ap):
|
|
nonlocal total_needed, container
|
|
locs_checked = len(container.getUsedLocs(lambda loc: True))
|
|
return SMBool(locs_checked >= total_needed)
|
|
return f
|
|
|
|
# AP: now based on location checks instead of local item
|
|
for pct in [25,50,75,100]:
|
|
goal = 'collect %d%% items' % pct
|
|
self.goals[goal].clearFunc = getPctFunc(totalItemsCount * pct / 100, container)
|
|
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)
|