From 7f2977039e97fad0fb5995a3c9170a29c17729c7 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Mon, 29 Dec 2025 13:12:47 -0700 Subject: [PATCH] add goals support --- worlds/crosscode/codegen/ast.py | 26 ++++++++++++++- worlds/crosscode/codegen/parse.py | 32 +++++++++++++------ worlds/crosscode/options.py | 13 ++++++++ worlds/crosscode/regions.py | 23 +++++++------ .../crosscode/templates/regions.template.py | 6 ++-- worlds/crosscode/types/regions.py | 7 +++- worlds/crosscode/world.py | 19 ++++++++--- 7 files changed, 98 insertions(+), 28 deletions(-) diff --git a/worlds/crosscode/codegen/ast.py b/worlds/crosscode/codegen/ast.py index f96d5d05eb98..4250f264fe70 100644 --- a/worlds/crosscode/codegen/ast.py +++ b/worlds/crosscode/codegen/ast.py @@ -7,7 +7,7 @@ from ..types.condition import Condition from ..types.locations import AccessInfo, LocationData -from ..types.regions import RegionConnection +from ..types.regions import Goal, RegionConnection from ..types.items import ItemData, ItemPoolEntry, ProgressiveChainEntry, SingleItemData from ..types.shops import ShopData @@ -294,6 +294,30 @@ def create_expression_region_connection(conn: RegionConnection): return ast_region +def create_expression_goal(goal: Goal): + """ + Create an expression representing a goal. + """ + + ast_goal = ast.Call( + func=ast.Name("Goal"), + args=[], + keywords=[ + ast.keyword( + arg="region", + value=ast.Constant(goal.region) + ), + ast.keyword( + arg="condition", + value=create_expression_condition_list(goal.condition) + ), + ] + ) + + ast.fix_missing_locations(ast_goal) + + return ast_goal + def create_expression_shop(data: ShopData) -> ast.Call: """ Create an expression that represents a shop region. diff --git a/worlds/crosscode/codegen/parse.py b/worlds/crosscode/codegen/parse.py index 657d47828486..a893be6813cf 100644 --- a/worlds/crosscode/codegen/parse.py +++ b/worlds/crosscode/codegen/parse.py @@ -12,7 +12,7 @@ from ..types.items import ItemData, ProgressiveChainEntry, ProgressiveItemChain, ProgressiveItemChainSingle, ProgressiveItemChainMulti, ProgressiveItemSubchain, SingleItemData from ..types.locations import AccessInfo, Condition -from ..types.regions import RegionConnection, RegionsData +from ..types.regions import Goal, RegionConnection, RegionsData from ..types.condition import * class JsonParserError(Exception): @@ -305,7 +305,18 @@ def parse_progressive_chain(self, name: str, raw: dict[str, typing.Any]) -> Prog items=self.__parse_progressive_itemlist(raw_items), ) + def parse_goal(self, raw: dict[str, typing.Any]) -> Goal: + if "region" not in raw: + raise JsonParserError(raw, None, "goal", "goals must have a source region") + region = raw["region"] + + if "condition" not in raw or len(raw["condition"]) == 0: + condition = None + else: + condition = self.parse_condition(raw["condition"]) + + return Goal(region, condition) def parse_region_connection(self, raw: dict[str, typing.Any]) -> RegionConnection: """ @@ -342,13 +353,6 @@ def parse_regions_data(self, raw: dict[str, typing.Any]) -> RegionsData: if not isinstance(start, str): raise JsonParserError(raw, start, "regions data", "starting region must be a string") - if "goal" not in raw: - raise JsonParserError(raw, None, "regions data", "must have goal region") - goal = raw["goal"] - - if not isinstance(goal, str): - raise JsonParserError(raw, goal, "regions data", "goal region must be a string") - exclude = [] if "exclude" in raw: exclude = raw["exclude"] @@ -373,11 +377,21 @@ def parse_regions_data(self, raw: dict[str, typing.Any]) -> RegionsData: connections.append(conn) + if "goals" not in raw: + raise JsonParserError(raw, None, "regions data", "must have goal region") + + if not isinstance(raw["goals"], dict): + raise JsonParserError(raw, None, "regions data", "goals must be a dict") + + goals = {} + for goal_name, goal in raw["goals"].items(): + goals[goal_name] = self.parse_goal(goal) + region_list = list(regions_seen) region_list.sort(key=lambda x: float(x.strip(string.ascii_letters))) - return RegionsData(start, goal, exclude, region_list, connections) + return RegionsData(start, exclude, region_list, connections, goals) def parse_regions_data_list(self, raw: dict[str, dict[str, typing.Any]]) -> dict[str, RegionsData]: """ diff --git a/worlds/crosscode/options.py b/worlds/crosscode/options.py index 876ebe8d230f..4dd614164eef 100644 --- a/worlds/crosscode/options.py +++ b/worlds/crosscode/options.py @@ -20,6 +20,18 @@ # default = 1 +class Goal(Choice): + """ + Determines what must be done to complete the game. + [Creator] Ascend Vermillion Tower and fight the Creator. + [Monkey] Ascend the Grand Krys'kajo and defeat the Son of the East. + """ + display_name = "Goal" + + option_creator = 0 + option_monkey = 1 + default = 0 + class VTShadeLock(Choice): """ If set to a non-None value, creates an in-game barrier at the entrance of Vermillion Tower to prevent extremely @@ -481,6 +493,7 @@ class CrossCodeOptions(PerGameCommonOptions): Options dataclass for CrossCode """ # logic_mode: LogicMode + goal: Goal vt_shade_lock: VTShadeLock vw_meteor_passage: VWMeteorPassage closed_gaia: ClosedGaia diff --git a/worlds/crosscode/regions.py b/worlds/crosscode/regions.py index 6b8a2a6c643f..3714b03ad19f 100644 --- a/worlds/crosscode/regions.py +++ b/worlds/crosscode/regions.py @@ -5,7 +5,7 @@ import typing -from .types.regions import RegionConnection, RegionsData +from .types.regions import Goal, RegionConnection, RegionsData from .types.condition import * modes = [ @@ -18,7 +18,6 @@ region_packs: typing.Dict[str, RegionsData] = { "linear": RegionsData( starting_region = "2", - goal_region = "32", excluded_regions = ['1'], region_list = [ '2', @@ -92,11 +91,13 @@ RegionConnection(region_from='31', region_to='32', cond=[ItemCondition(item_name='Old Dojo Key', amount=1)]), RegionConnection(region_from='32', region_to='33', cond=[ItemCondition(item_name='Meteor Shade', amount=1)]), RegionConnection(region_from='31', region_to='22', cond=None), - ] + ], + goals = { + 'creator': Goal(region='32', condition=None), + } ), "open": RegionsData( starting_region = "open2", - goal_region = "open19", excluded_regions = ['open1'], region_list = [ 'open2', @@ -122,12 +123,12 @@ 'open7.8', 'open8', 'open9', - 'open10.Infested', - 'open10', - 'open10.Grove', 'open10.Left', + 'open10.Grove', + 'open10.Infested', 'open10.Right', 'open10.Mid', + 'open10', 'open11', 'open13.1', 'open13.2', @@ -143,7 +144,6 @@ 'open16.1', 'open17', 'open18', - 'open19', 'open20', ], region_connections = [ @@ -194,8 +194,11 @@ RegionConnection(region_from='open16', region_to='open17', cond=[ItemCondition(item_name='Old Dojo Key', amount=1)]), RegionConnection(region_from='open16', region_to='open16.1', cond=[ItemCondition(item_name='Meteor Shade', amount=1), ItemCondition(item_name='Shock', amount=1)]), RegionConnection(region_from='open16', region_to='open18', cond=[VariableCondition(name='vwPassage')]), - RegionConnection(region_from='open16.1', region_to='open19', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1), VariableCondition(name='vtShadeLock')]), - ] + ], + goals = { + 'creator': Goal(region='open16.1', condition=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1), VariableCondition(name='vtShadeLock')]), + 'monkey': Goal(region='open15.3', condition=None), + } ), } \ No newline at end of file diff --git a/worlds/crosscode/templates/regions.template.py b/worlds/crosscode/templates/regions.template.py index 4ea4920dd4d7..733dfb6c72c3 100644 --- a/worlds/crosscode/templates/regions.template.py +++ b/worlds/crosscode/templates/regions.template.py @@ -2,7 +2,7 @@ import typing -from .types.regions import RegionConnection, RegionsData +from .types.regions import Goal, RegionConnection, RegionsData from .types.condition import * modes = {{ modes | emit_list("constant") }} @@ -13,10 +13,10 @@ {% for mode, r in region_packs.items() -%} "{{mode}}": RegionsData( starting_region = "{{r.starting_region}}", - goal_region = "{{r.goal_region}}", excluded_regions = {{r.excluded_regions}}, region_list = {{r.region_list | emit_list("constant") | indent(8)}}, - region_connections = {{r.region_connections | emit_list("region_connection") | indent(8)}} + region_connections = {{r.region_connections | emit_list("region_connection") | indent(8)}}, + goals = {{r.goals.items() | emit_dict("constant", "goal") | indent(8) }} ), {% endfor %} } diff --git a/worlds/crosscode/types/regions.py b/worlds/crosscode/types/regions.py index 9565c66a7d90..ec95109c4adc 100644 --- a/worlds/crosscode/types/regions.py +++ b/worlds/crosscode/types/regions.py @@ -9,10 +9,15 @@ class RegionConnection: region_to: str cond: typing.Optional[list[Condition]] +@dataclass +class Goal: + region: str + condition: typing.Optional[typing.List[Condition]] + @dataclass class RegionsData: starting_region: str - goal_region: str excluded_regions: typing.List[str] region_list: typing.List[str] region_connections: typing.List[RegionConnection] + goals: typing.Dict[str, Goal] diff --git a/worlds/crosscode/world.py b/worlds/crosscode/world.py index bbb599f2247b..83d3aa5beffd 100644 --- a/worlds/crosscode/world.py +++ b/worlds/crosscode/world.py @@ -500,11 +500,22 @@ def create_regions(self): if self.options.shop_rando: self.create_shops() - goal_region = self.region_dict[self.region_pack.goal_region] - goal = Location(self.player, "The Creator", parent=goal_region) - goal.place_locked_item(Item("Victory", ItemClassification.progression, None, self.player)) + goal_name = self.options.goal.current_key + goal = self.region_pack.goals[goal_name] + goal_region = self.region_dict[goal.region] + goal_location = Location(self.player, "Victory", parent=goal_region) + goal_location.place_locked_item(Item("Victory", ItemClassification.progression, None, self.player)) + add_rule( + goal_location, + condition_satisfied( + self.player, + goal.condition if goal.condition is not None else [], + None, + self.logic_dict + ) + ) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) - goal_region.locations.append(goal) + goal_region.locations.append(goal_location) def create_items(self): exclude = self.multiworld.precollected_items[self.player][:]