mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
- 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)
|