from typing import NamedTuple, Optional, TYPE_CHECKING from BaseClasses import Location, Region from worlds.generic.Rules import set_rule from .Levels import Level, LocationType from .Names import ItemName if TYPE_CHECKING: from . import CelesteOpenWorld else: CelesteOpenWorld = object celeste_base_id: int = 0xCA10000 class CelesteLocation(Location): game = "Celeste" class CelesteLocationData(NamedTuple): region: str address: Optional[int] = None checkpoint_location_data_table: dict[str, CelesteLocationData] = {} key_location_data_table: dict[str, CelesteLocationData] = {} location_id_offsets: dict[LocationType, int | None] = { LocationType.strawberry: celeste_base_id, LocationType.golden_strawberry: celeste_base_id + 0x1000, LocationType.cassette: celeste_base_id + 0x2000, LocationType.car: celeste_base_id + 0x2A00, LocationType.crystal_heart: celeste_base_id + 0x3000, LocationType.checkpoint: celeste_base_id + 0x4000, LocationType.level_clear: celeste_base_id + 0x5000, LocationType.key: celeste_base_id + 0x6000, LocationType.gem: celeste_base_id + 0x6A00, LocationType.binoculars: celeste_base_id + 0x7000, LocationType.room_enter: celeste_base_id + 0x8000, LocationType.clutter: None, } def generate_location_table() -> dict[str, int]: from .Levels import Level, LocationType, load_logic_data level_data: dict[str, Level] = load_logic_data() location_table = {} location_counts: dict[LocationType, int] = { LocationType.strawberry: 0, LocationType.golden_strawberry: 0, LocationType.cassette: 0, LocationType.car: 0, LocationType.crystal_heart: 0, LocationType.checkpoint: 0, LocationType.level_clear: 0, LocationType.key: 0, LocationType.gem: 0, LocationType.binoculars: 0, LocationType.room_enter: 0, } for _, level in level_data.items(): for room in level.rooms: if room.name != "10b_GOAL": location_table[room.display_name] = location_id_offsets[LocationType.room_enter] + location_counts[LocationType.room_enter] location_counts[LocationType.room_enter] += 1 if room.checkpoint is not None and room.checkpoint != "Start": checkpoint_id: int = location_id_offsets[LocationType.checkpoint] + location_counts[LocationType.checkpoint] checkpoint_name: str = level.display_name + " - " + room.checkpoint location_table[checkpoint_name] = checkpoint_id location_counts[LocationType.checkpoint] += 1 checkpoint_location_data_table[checkpoint_name] = CelesteLocationData(level.display_name, checkpoint_id) from .Items import add_checkpoint_to_table add_checkpoint_to_table(checkpoint_id, checkpoint_name) for region in room.regions: for location in region.locations: if location_id_offsets[location.loc_type] is not None: location_id = location_id_offsets[location.loc_type] + location_counts[location.loc_type] location_table[location.display_name] = location_id location_counts[location.loc_type] += 1 if location.loc_type == LocationType.key: from .Items import add_key_to_table add_key_to_table(location_id, location.display_name) if location.loc_type == LocationType.gem: from .Items import add_gem_to_table add_gem_to_table(location_id, location.display_name) return location_table def create_regions_and_locations(world: CelesteOpenWorld): menu_region = Region("Menu", world.player, world.multiworld) world.multiworld.regions.append(menu_region) world.active_checkpoint_names: list[str] = [] world.goal_checkpoint_names: dict[str, str] = dict() world.active_key_names: list[str] = [] world.active_gem_names: list[str] = [] world.active_clutter_names: list[str] = [] for _, level in world.level_data.items(): if level.name not in world.active_levels: continue for room in level.rooms: room_region = Region(room.name + "_room", world.player, world.multiworld) world.multiworld.regions.append(room_region) for pre_region in room.regions: region = Region(pre_region.name, world.player, world.multiworld) world.multiworld.regions.append(region) for level_location in pre_region.locations: if level_location.loc_type == LocationType.golden_strawberry: if level_location.display_name == "Farewell - Golden Strawberry": if not world.options.goal_area == "farewell_golden": continue elif not world.options.include_goldens: continue if level_location.loc_type == LocationType.car and not world.options.carsanity: continue if level_location.loc_type == LocationType.binoculars and not world.options.binosanity: continue if level_location.loc_type == LocationType.key: world.active_key_names.append(level_location.display_name) if level_location.loc_type == LocationType.gem: world.active_gem_names.append(level_location.display_name) location_rule = None if len(level_location.possible_access) == 1: only_access = level_location.possible_access[0] if len(only_access) == 1: only_item = level_location.possible_access[0][0] def location_rule_func(state, only_item=only_item): return state.has(only_item, world.player) location_rule = location_rule_func else: def location_rule_func(state, only_access=only_access): return state.has_all(only_access, world.player) location_rule = location_rule_func elif len(level_location.possible_access) > 0: def location_rule_func(state, level_location=level_location): for sublist in level_location.possible_access: if state.has_all(sublist, world.player): return True return False location_rule = location_rule_func if level_location.loc_type == LocationType.clutter: world.active_clutter_names.append(level_location.display_name) location = CelesteLocation(world.player, level_location.display_name, None, region) if location_rule is not None: set_rule(location, location_rule) region.locations.append(location) continue location = CelesteLocation(world.player, level_location.display_name, world.location_name_to_id[level_location.display_name], region) if location_rule is not None: set_rule(location, location_rule) region.locations.append(location) for pre_region in room.regions: region = world.get_region(pre_region.name) for connection in pre_region.connections: connection_rule = None if len(connection.possible_access) == 1: only_access = connection.possible_access[0] if len(only_access) == 1: only_item = connection.possible_access[0][0] def connection_rule_func(state, only_item=only_item): return state.has(only_item, world.player) connection_rule = connection_rule_func else: def connection_rule_func(state, only_access=only_access): return state.has_all(only_access, world.player) connection_rule = connection_rule_func elif len(connection.possible_access) > 0: def connection_rule_func(state, connection=connection): for sublist in connection.possible_access: if state.has_all(sublist, world.player): return True return False connection_rule = connection_rule_func if connection_rule is None: region.add_exits([connection.destination_name]) else: region.add_exits([connection.destination_name], {connection.destination_name: connection_rule}) region.add_exits([room_region.name]) if room.checkpoint != None: if room.checkpoint == "Start": if world.options.lock_goal_area and (level.name == world.goal_area or (level.name[:2] == world.goal_area[:2] == "10")): world.goal_start_region: str = room.checkpoint_region elif level.name == "8a": world.epilogue_start_region: str = room.checkpoint_region else: menu_region.add_exits([room.checkpoint_region]) else: checkpoint_location_name = level.display_name + " - " + room.checkpoint world.active_checkpoint_names.append(checkpoint_location_name) checkpoint_rule = lambda state, checkpoint_location_name=checkpoint_location_name: state.has(checkpoint_location_name, world.player) room_region.add_locations({ checkpoint_location_name: world.location_name_to_id[checkpoint_location_name] }, CelesteLocation) if world.options.lock_goal_area and (level.name == world.goal_area or (level.name[:2] == world.goal_area[:2] == "10")): world.goal_checkpoint_names[room.checkpoint_region] = checkpoint_location_name else: menu_region.add_exits([room.checkpoint_region], {room.checkpoint_region: checkpoint_rule}) if world.options.roomsanity: if room.name != "10b_GOAL": room_location_name = room.display_name room_region.add_locations({ room_location_name: world.location_name_to_id[room_location_name] }, CelesteLocation) for room_connection in level.room_connections: source_region = world.get_region(room_connection.source.name) source_region.add_exits([room_connection.dest.name]) if room_connection.two_way: dest_region = world.get_region(room_connection.dest.name) dest_region.add_exits([room_connection.source.name]) if level.name == "10b": # Manually connect the two parts of Farewell source_region = world.get_region("10a_e-08_east") source_region.add_exits(["10b_f-door_west"]) if level.name == "10c": # Manually connect the Golden room of Farewell golden_items: list[str] = [ItemName.traffic_blocks, ItemName.dash_refills, ItemName.double_dash_refills, ItemName.dream_blocks, ItemName.swap_blocks, ItemName.move_blocks, ItemName.blue_boosters, ItemName.springs, ItemName.feathers, ItemName.coins, ItemName.red_boosters, ItemName.kevin_blocks, ItemName.core_blocks, ItemName.fire_ice_balls, ItemName.badeline_boosters, ItemName.bird, ItemName.breaker_boxes, ItemName.pufferfish, ItemName.jellyfish, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks, ItemName.yellow_cassette_blocks, ItemName.green_cassette_blocks] golden_rule = lambda state: state.has_all(golden_items, world.player) source_region_end = world.get_region("10b_j-19_top") source_region_end.add_exits(["10c_end-golden_bottom"], {"10c_end-golden_bottom": golden_rule}) source_region_moon = world.get_region("10b_j-16_east") source_region_moon.add_exits(["10c_end-golden_bottom"], {"10c_end-golden_bottom": golden_rule}) source_region_golden = world.get_region("10c_end-golden_top") source_region_golden.add_exits(["10b_GOAL_main"]) location_data_table: dict[str, int] = generate_location_table() def generate_location_groups() -> dict[str, list[str]]: from .Levels import Level, LocationType, load_logic_data level_data: dict[str, Level] = load_logic_data() location_groups: dict[str, list[str]] = { "Strawberries": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.strawberry] and id < location_id_offsets[LocationType.golden_strawberry]], "Golden Strawberries": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.golden_strawberry] and id < location_id_offsets[LocationType.cassette]], "Cassettes": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.cassette] and id < location_id_offsets[LocationType.car]], "Cars": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.car] and id < location_id_offsets[LocationType.crystal_heart]], "Crystal Hearts": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.crystal_heart] and id < location_id_offsets[LocationType.checkpoint]], "Checkpoints": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.checkpoint] and id < location_id_offsets[LocationType.level_clear]], "Level Clears": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.level_clear] and id < location_id_offsets[LocationType.key]], "Keys": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.key] and id < location_id_offsets[LocationType.gem]], "Gems": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.gem] and id < location_id_offsets[LocationType.binoculars]], "Binoculars": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.binoculars] and id < location_id_offsets[LocationType.room_enter]], "Rooms": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.room_enter]], } for level in level_data.values(): location_groups.update({level.display_name: [loc_name for loc_name, id in location_data_table.items() if level.display_name in loc_name]}) return location_groups