From 5054971af5553aa9efb6bb8f889955031ed3c6d8 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Wed, 11 Mar 2026 23:54:06 -0600 Subject: [PATCH 1/3] add botanics amounts to codegen --- worlds/crosscode/codegen/gen.py | 3 +++ worlds/crosscode/codegen/lists.py | 11 ++++++++ worlds/crosscode/regions.py | 26 ++++++++++++++++--- .../crosscode/templates/regions.template.py | 6 +++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/worlds/crosscode/codegen/gen.py b/worlds/crosscode/codegen/gen.py index e193f4d2d87e..dedabd855579 100644 --- a/worlds/crosscode/codegen/gen.py +++ b/worlds/crosscode/codegen/gen.py @@ -171,9 +171,12 @@ def generate_python_file_regions(self): """ template = self.environment.get_template("regions.template.py") + print(self.lists.region_botanics_amounts) + regions_complete = template.render( modes_string=", ".join([f'"{x}"' for x in self.ctx.rando_data["modes"]]), region_packs=self.regions_data, + region_botanics_amounts={key: val.items() for key, val in self.lists.region_botanics_amounts.items()}, **self.common_args ) diff --git a/worlds/crosscode/codegen/lists.py b/worlds/crosscode/codegen/lists.py index ddccd6e7d447..618a8a759ebf 100644 --- a/worlds/crosscode/codegen/lists.py +++ b/worlds/crosscode/codegen/lists.py @@ -65,6 +65,8 @@ class ListInfo: shop_unlock_by_shop_and_id: dict[tuple[str, int], ItemData] global_slot_region_conditions_list: dict[str, list[Condition]] + region_botanics_amounts: dict[str, dict[str, int]] # { mode => { region => number of plants } } + progressive_chains: dict[str, ProgressiveItemChain] progressive_items: dict[str, ItemData] @@ -107,6 +109,8 @@ def __init__(self, ctx: Context): self.shop_unlock_by_shop_and_id = {} self.global_slot_region_conditions_list = {} + self.region_botanics_amounts = defaultdict(lambda: defaultdict(lambda: 0)) + self.json_parser = JsonParser(self.ctx) self.json_parser.single_items_dict = self.single_items_dict self.json_parser.items_dict = self.items_dict @@ -153,6 +157,8 @@ def build(self): self.__add_progressive_chains(file["progressiveChains"]) + self.__add_botanics(file["botanics"]) + self.__add_vars(self.ctx.rando_data["vars"]) def __get_cached_location_id(self, name: str) -> typing.Optional[int]: @@ -463,6 +469,11 @@ def __add_item_pool_list(self, raw: dict[str, list[dict[str, typing.Any]]]): for name, pool in raw.items(): self.__add_item_pool(name, pool) + def __add_botanics(self, raw: dict[str, dict[str, typing.Any]]): + for plant in raw.values(): + for mode, region in plant["region"].items(): + self.region_botanics_amounts[mode][region] += 1 + def __add_reward(self, reward: list[dict[str, typing.Any]]) -> ItemData: """ Ensure an item reward is in the list of items. diff --git a/worlds/crosscode/regions.py b/worlds/crosscode/regions.py index 0db851232b26..7723bb7a1264 100644 --- a/worlds/crosscode/regions.py +++ b/worlds/crosscode/regions.py @@ -123,12 +123,12 @@ 'open7.8', 'open8', 'open9', - 'open10', - 'open10.Infested', - 'open10.Grove', 'open10.Left', - 'open10.Mid', + 'open10.Grove', + 'open10.Infested', + 'open10', 'open10.Right', + 'open10.Mid', 'open11', 'open13.1', 'open13.2', @@ -202,4 +202,22 @@ } ), +} + +region_botanics_amounts: dict[str, dict[str, int]] = { + "open": { + 'open3': 20, + 'open4.4': 6, + 'open5': 18, + 'open8': 7, + 'open10': 5, + 'open10.Mid': 1, + 'open10.Left': 6, + 'open10.Right': 2, + 'open10.Infested': 1, + 'open16': 9, + 'open20': 1, + 'open11': 1, + } + } \ No newline at end of file diff --git a/worlds/crosscode/templates/regions.template.py b/worlds/crosscode/templates/regions.template.py index 733dfb6c72c3..993cc610d2cb 100644 --- a/worlds/crosscode/templates/regions.template.py +++ b/worlds/crosscode/templates/regions.template.py @@ -20,3 +20,9 @@ ), {% endfor %} } + +region_botanics_amounts: dict[str, dict[str, int]] = { + {% for mode, amounts in region_botanics_amounts.items() -%} + "{{mode}}": {{ amounts | emit_dict("constant", "constant") | indent(4) }} + {% endfor %} +} From a70dbc77f4fa528680dc657a25b392fe8930ad41 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Thu, 12 Mar 2026 00:20:39 -0600 Subject: [PATCH 2/3] add botanics condition class --- worlds/crosscode/types/condition.py | 14 ++++++++++++++ worlds/crosscode/world.py | 1 + worlds/crosscode/world_data.py | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index 652b8dbc1997..5670bc331224 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -3,6 +3,7 @@ from dataclasses import field, dataclass from BaseClasses import CollectionState +from .world import WorldData from ..options import ShopReceiveMode from .items import ItemData @@ -18,6 +19,7 @@ class LogicDict(typing.TypedDict): shop_unlock_by_id: dict[int, ItemData] shop_unlock_by_shop: dict[str, ItemData] shop_unlock_by_shop_and_id: dict[tuple[str, int], ItemData] + world_data: WorldData class Condition(abc.ABC): @abc.abstractmethod @@ -165,6 +167,18 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a return state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].name, player) return True +@dataclass +class BotanicsCompletionCondition(Condition): + amount: int + + def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: + collected = sum( + state.has(item, player) + for item in args["world_data"].region_botanics_amounts[args["mode"]] + ) + + return collected >= self.amount + class NeverCondition(Condition): def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: return False diff --git a/worlds/crosscode/world.py b/worlds/crosscode/world.py index 7680a822ac0b..f2cafbbda11e 100644 --- a/worlds/crosscode/world.py +++ b/worlds/crosscode/world.py @@ -405,6 +405,7 @@ def generate_early(self): "shop_unlock_by_id": self.world_data.shop_unlock_by_id, "shop_unlock_by_shop": self.world_data.shop_unlock_by_shop, "shop_unlock_by_shop_and_id": self.world_data.shop_unlock_by_shop_and_id, + "world_data": self.world_data, } # Universal Tracker support diff --git a/worlds/crosscode/world_data.py b/worlds/crosscode/world_data.py index fd69a209f941..3a14b7db00c2 100644 --- a/worlds/crosscode/world_data.py +++ b/worlds/crosscode/world_data.py @@ -4,7 +4,7 @@ from .common import BASE_ID, DATA_VERSION from .types.world import WorldData -from .regions import region_packs +from .regions import region_packs, region_botanics_amounts from .items import single_items_dict, items_dict, items_by_full_name, keyring_items from .shops import shop_dict, per_shop_locations, global_shop_locations, shop_unlock_by_id, shop_unlock_by_shop, \ shop_unlock_by_shop_and_id @@ -17,6 +17,7 @@ data_version=DATA_VERSION, base_id=BASE_ID, region_packs=region_packs, + region_botanics_amounts=region_botanics_amounts, locations_dict=locations_dict, events_dict=events_dict, locked_locations=locked_locations, From 97ff6cd13c954572059b6d44698e3f9608aaa56a Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Thu, 12 Mar 2026 01:02:36 -0600 Subject: [PATCH 3/3] add parse support, regen, rewrite --- worlds/crosscode/codegen/gen.py | 2 -- worlds/crosscode/codegen/parse.py | 11 +++++++++++ worlds/crosscode/locations.py | 10 +++++----- worlds/crosscode/regions.py | 8 ++++---- worlds/crosscode/types/condition.py | 13 +++++++------ worlds/crosscode/types/world.py | 1 + worlds/crosscode/world.py | 2 +- 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/worlds/crosscode/codegen/gen.py b/worlds/crosscode/codegen/gen.py index dedabd855579..d698940de815 100644 --- a/worlds/crosscode/codegen/gen.py +++ b/worlds/crosscode/codegen/gen.py @@ -171,8 +171,6 @@ def generate_python_file_regions(self): """ template = self.environment.get_template("regions.template.py") - print(self.lists.region_botanics_amounts) - regions_complete = template.render( modes_string=", ".join([f'"{x}"' for x in self.ctx.rando_data["modes"]]), region_packs=self.regions_data, diff --git a/worlds/crosscode/codegen/parse.py b/worlds/crosscode/codegen/parse.py index a893be6813cf..9eb663b1ef54 100644 --- a/worlds/crosscode/codegen/parse.py +++ b/worlds/crosscode/codegen/parse.py @@ -137,6 +137,17 @@ def parse_condition(self, raw: list[typing.Any]) -> list[Condition]: f"expected 2 arguments, not {num_args}" ) + elif cond[0] == "botanics": + if num_args == 1: + result.append(BotanicsCompletionCondition(cond[1])) + else: + raise JsonParserError( + raw, + cond, + "botanics completion condition", + f"expected 1 arguments, not {num_args}" + ) + elif cond[0] == "never": result.append(NeverCondition()) diff --git a/worlds/crosscode/locations.py b/worlds/crosscode/locations.py index 9c4702d56eed..401ff184616d 100644 --- a/worlds/crosscode/locations.py +++ b/worlds/crosscode/locations.py @@ -379,9 +379,9 @@ LocationData(code=3235824367, name="Krys'kajo SP Upgrade", area='tree-dng', access=AccessInfo(region={'linear': '30', 'open': 'open15.3'})), LocationData(code=3235824790, name='Apollo Duel East Pass', area='jungle', access=AccessInfo(region={'linear': '30', 'open': 'open10'})), LocationData(code=3235824791, name='Apollo Duel West Pass', area='jungle', access=AccessInfo(region={'linear': '30', 'open': 'open10'})), - LocationData(code=3235824374, name='Talatu Bergen 25%', area='bergen', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'}, cond=[LocationCondition(location_name='Talatu Introductions'), ItemCondition(item_name='Disc of Flora', amount=1)])), - LocationData(code=3235824375, name="Talatu Ba'kii 50%", area='heat-village', metadata={'quest': True}, access=AccessInfo(region={'linear': '11', 'open': 'open5'}, cond=[LocationCondition(location_name='Talatu Bergen 25%')])), - LocationData(code=3235824376, name='Talatu Basin 75%', area='jungle-city', metadata={'quest': True}, access=AccessInfo(region={'linear': '23', 'open': 'open10'}, cond=[LocationCondition(location_name="Talatu Ba'kii 50%")])), + LocationData(code=3235824374, name='Talatu Bergen 25%', area='bergen', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'}, cond=[LocationCondition(location_name='Talatu Introductions'), ItemCondition(item_name='Disc of Flora', amount=1), BotanicsCompletionCondition(amount=20)])), + LocationData(code=3235824375, name="Talatu Ba'kii 50%", area='heat-village', metadata={'quest': True}, access=AccessInfo(region={'linear': '11', 'open': 'open5'}, cond=[LocationCondition(location_name='Talatu Bergen 25%'), BotanicsCompletionCondition(amount=39)])), + LocationData(code=3235824376, name='Talatu Basin 75%', area='jungle-city', metadata={'quest': True}, access=AccessInfo(region={'linear': '23', 'open': 'open10'}, cond=[LocationCondition(location_name="Talatu Ba'kii 50%"), BotanicsCompletionCondition(amount=58)])), LocationData(code=3235824368, name='Heat Pedestal', area='cold-dng', access=AccessInfo(region={'linear': '8', 'open': 'open4.5'})), LocationData(code=3235824369, name='Cold Pedestal', area='heat-dng', access=AccessInfo(region={'linear': '16', 'open': 'open7.4'})), LocationData(code=3235824370, name='Shock Pedestal', area='wave-dng', access=AccessInfo(region={'linear': '27', 'open': 'open14.5'})), @@ -401,7 +401,7 @@ LocationData(code=3235824386, name='Round and Round - Reward 1', area='autumn-area', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), LocationData(code=3235824387, name='Round and Round - Reward 2', area='autumn-area', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), LocationData(code=3235824388, name='Round and Round - Reward 3', area='autumn-area', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), - LocationData(code=3235824389, name='Crocus Pocus', area='autumn-area', metadata={'quest': True}, access=AccessInfo(region={'linear': '31', 'open': 'open3'}, cond=[ItemCondition(item_name='Disc of Flora', amount=1), ItemCondition(item_name='Heat', amount=1), LocationCondition(location_name='Talatu Bergen 25%'), LocationCondition(location_name="Talatu Ba'kii 50%"), LocationCondition(location_name='Talatu Basin 75%'), RegionCondition(target_mode='open', region_name='open4.1'), RegionCondition(target_mode='open', region_name='open5'), RegionCondition(target_mode='open', region_name='open9'), RegionCondition(target_mode='open', region_name='open10'), RegionCondition(target_mode='open', region_name='open16'), RegionCondition(target_mode='open', region_name='open20')])), + LocationData(code=3235824389, name='Crocus Pocus', area='autumn-area', metadata={'quest': True}, access=AccessInfo(region={'linear': '31', 'open': 'open3'}, cond=[ItemCondition(item_name='Disc of Flora', amount=1), LocationCondition(location_name='Talatu Basin 75%'), BotanicsCompletionCondition(amount=77)])), LocationData(code=3235824390, name='The Observatory', area='autumn-area', metadata={'quest': True}, access=AccessInfo(region={'linear': '31', 'open': 'open18'}, cond=[QuestCondition(quest_name='A Promise is a Promise 5'), ItemCondition(item_name='Mine Pass', amount=1)])), LocationData(code=3235824391, name='Bergen Trailblazing', area='bergen-trails', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'}, cond=[QuestCondition(quest_name='Bergen Trailblazing Collect'), QuestCondition(quest_name='Bergen Trailblazing Defeat'), QuestCondition(quest_name='Bergen Trailblazing Landmarks'), QuestCondition(quest_name='Bergen Trailblazing Data Probe')])), LocationData(code=3235824392, name='Bergen Trailblazing Collect', area='bergen-trails', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), @@ -814,7 +814,7 @@ LocationData(code=None, name="Autumn's Rise Landmarks (Event)", metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), LocationData(code=None, name="Autumn's Rise Data Probe (Event)", metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), LocationData(code=None, name='Round and Round (Event)', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), - LocationData(code=None, name='Crocus Pocus (Event)', metadata={'quest': True}, access=AccessInfo(region={'linear': '31', 'open': 'open3'}, cond=[ItemCondition(item_name='Disc of Flora', amount=1), ItemCondition(item_name='Heat', amount=1), LocationCondition(location_name='Talatu Bergen 25%'), LocationCondition(location_name="Talatu Ba'kii 50%"), LocationCondition(location_name='Talatu Basin 75%'), RegionCondition(target_mode='open', region_name='open4.1'), RegionCondition(target_mode='open', region_name='open5'), RegionCondition(target_mode='open', region_name='open9'), RegionCondition(target_mode='open', region_name='open10'), RegionCondition(target_mode='open', region_name='open16'), RegionCondition(target_mode='open', region_name='open20')])), + LocationData(code=None, name='Crocus Pocus (Event)', metadata={'quest': True}, access=AccessInfo(region={'linear': '31', 'open': 'open3'}, cond=[ItemCondition(item_name='Disc of Flora', amount=1), LocationCondition(location_name='Talatu Basin 75%'), BotanicsCompletionCondition(amount=77)])), LocationData(code=None, name='The Observatory (Event)', metadata={'quest': True}, access=AccessInfo(region={'linear': '31', 'open': 'open18'}, cond=[QuestCondition(quest_name='A Promise is a Promise 5'), ItemCondition(item_name='Mine Pass', amount=1)])), LocationData(code=None, name='Bergen Trailblazing (Event)', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'}, cond=[QuestCondition(quest_name='Bergen Trailblazing Collect'), QuestCondition(quest_name='Bergen Trailblazing Defeat'), QuestCondition(quest_name='Bergen Trailblazing Landmarks'), QuestCondition(quest_name='Bergen Trailblazing Data Probe')])), LocationData(code=None, name='Bergen Trailblazing Collect (Event)', metadata={'quest': True}, access=AccessInfo(region={'linear': '3', 'open': 'open3'})), diff --git a/worlds/crosscode/regions.py b/worlds/crosscode/regions.py index 7723bb7a1264..79eac415772d 100644 --- a/worlds/crosscode/regions.py +++ b/worlds/crosscode/regions.py @@ -123,12 +123,12 @@ 'open7.8', 'open8', 'open9', - 'open10.Left', - 'open10.Grove', + 'open10.Mid', + 'open10.Right', 'open10.Infested', 'open10', - 'open10.Right', - 'open10.Mid', + 'open10.Grove', + 'open10.Left', 'open11', 'open13.1', 'open13.2', diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index 5670bc331224..6145c1beec28 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -3,7 +3,6 @@ from dataclasses import field, dataclass from BaseClasses import CollectionState -from .world import WorldData from ..options import ShopReceiveMode from .items import ItemData @@ -19,7 +18,7 @@ class LogicDict(typing.TypedDict): shop_unlock_by_id: dict[int, ItemData] shop_unlock_by_shop: dict[str, ItemData] shop_unlock_by_shop_and_id: dict[tuple[str, int], ItemData] - world_data: WorldData + region_botanics_amounts: dict[str, int] class Condition(abc.ABC): @abc.abstractmethod @@ -172,10 +171,11 @@ class BotanicsCompletionCondition(Condition): amount: int def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool: - collected = sum( - state.has(item, player) - for item in args["world_data"].region_botanics_amounts[args["mode"]] - ) + collected = sum([ + amount + for region, amount in args["region_botanics_amounts"].items() + if state.can_reach_region(region, player) + ]) return collected >= self.amount @@ -196,5 +196,6 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a "VariableEntryCondition", "ChestKeyCondition", "ShopSlotCondition", + "BotanicsCompletionCondition", "NeverCondition" ] diff --git a/worlds/crosscode/types/world.py b/worlds/crosscode/types/world.py index 5a9924b3b4f8..c48b54c9a006 100644 --- a/worlds/crosscode/types/world.py +++ b/worlds/crosscode/types/world.py @@ -14,6 +14,7 @@ class WorldData: # regions.py region_packs: dict[str, RegionsData] modes: list[str] = field(init=False) + region_botanics_amounts: dict[str, dict[str, int]] # locations.py locations_dict: dict[str, LocationData] diff --git a/worlds/crosscode/world.py b/worlds/crosscode/world.py index f2cafbbda11e..d7e4f654f085 100644 --- a/worlds/crosscode/world.py +++ b/worlds/crosscode/world.py @@ -405,7 +405,7 @@ def generate_early(self): "shop_unlock_by_id": self.world_data.shop_unlock_by_id, "shop_unlock_by_shop": self.world_data.shop_unlock_by_shop, "shop_unlock_by_shop_and_id": self.world_data.shop_unlock_by_shop_and_id, - "world_data": self.world_data, + "region_botanics_amounts": self.world_data.region_botanics_amounts[self.logic_mode], } # Universal Tracker support