327 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			327 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|  | from collections import defaultdict | ||
|  | import math | ||
|  | import os | ||
|  | from typing import Any, Dict, List, Set | ||
|  | 
 | ||
|  | from .ProgressiveDistricts import get_flat_progressive_districts | ||
|  | from worlds.generic.Rules import forbid_item | ||
|  | 
 | ||
|  | 
 | ||
|  | from .Data import ( | ||
|  |     get_boosts_data, | ||
|  |     get_era_required_items_data, | ||
|  | ) | ||
|  | 
 | ||
|  | from .Rules import create_boost_rules | ||
|  | from .Container import ( | ||
|  |     CivVIContainer, | ||
|  |     generate_goody_hut_sql, | ||
|  |     generate_new_items, | ||
|  |     generate_setup_file, | ||
|  |     generate_update_boosts_sql, | ||
|  | ) | ||
|  | from .Enum import CivVICheckType, CivVIHintClassification | ||
|  | from .Items import ( | ||
|  |     BOOSTSANITY_PROGRESSION_ITEMS, | ||
|  |     FILLER_DISTRIBUTION, | ||
|  |     CivVIEvent, | ||
|  |     CivVIItemData, | ||
|  |     FillerItemRarity, | ||
|  |     format_item_name, | ||
|  |     generate_item_table, | ||
|  |     CivVIItem, | ||
|  |     get_item_by_civ_name, | ||
|  |     get_random_filler_by_rarity, | ||
|  | ) | ||
|  | from .Locations import ( | ||
|  |     CivVILocation, | ||
|  |     CivVILocationData, | ||
|  |     EraType, | ||
|  |     generate_era_location_table, | ||
|  |     generate_flat_location_table, | ||
|  | ) | ||
|  | from .Options import CivVIOptions | ||
|  | from .Regions import create_regions | ||
|  | from BaseClasses import Item, ItemClassification, MultiWorld, Tutorial | ||
|  | from worlds.AutoWorld import World, WebWorld | ||
|  | from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess  # type: ignore | ||
|  | 
 | ||
|  | 
 | ||
|  | def run_client(*args: Any): | ||
|  |     print("Running Civ6 Client") | ||
|  |     from .Civ6Client import main  # lazy import | ||
|  | 
 | ||
|  |     launch_subprocess(main, name="Civ6Client") | ||
|  | 
 | ||
|  | 
 | ||
|  | components.append( | ||
|  |     Component( | ||
|  |         "Civ6 Client", | ||
|  |         func=run_client, | ||
|  |         component_type=Type.CLIENT, | ||
|  |         file_identifier=SuffixIdentifier(".apcivvi"), | ||
|  |     ) | ||
|  | ) | ||
|  | 
 | ||
|  | 
 | ||
|  | class CivVIWeb(WebWorld): | ||
|  |     tutorials = [ | ||
|  |         Tutorial( | ||
|  |             "Multiworld Setup Guide", | ||
|  |             "A guide to setting up Civilization VI for MultiWorld.", | ||
|  |             "English", | ||
|  |             "setup_en.md", | ||
|  |             "setup/en", | ||
|  |             ["hesto2"], | ||
|  |         ) | ||
|  |     ] | ||
|  |     theme = "ocean" | ||
|  | 
 | ||
|  | 
 | ||
|  | class CivVIWorld(World): | ||
|  |     """
 | ||
|  |     Civilization VI is a turn-based strategy video game in which one or more players compete alongside computer-controlled opponents to grow their individual civilization from a small tribe to control the entire planet across several periods of development. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     game = "Civilization VI" | ||
|  |     topology_present = False | ||
|  |     options_dataclass = CivVIOptions | ||
|  |     options: CivVIOptions  # type: ignore | ||
|  | 
 | ||
|  |     web = CivVIWeb() | ||
|  | 
 | ||
|  |     item_name_to_id = {item.name: item.code for item in generate_item_table().values()} | ||
|  |     location_name_to_id = { | ||
|  |         location.name: location.code | ||
|  |         for location in generate_flat_location_table().values() | ||
|  |     } | ||
|  | 
 | ||
|  |     item_table: Dict[str, CivVIItemData] = {} | ||
|  |     location_by_era: Dict[str, Dict[str, CivVILocationData]] | ||
|  |     required_client_version = (0, 4, 5) | ||
|  |     location_table: Dict[str, CivVILocationData] | ||
|  |     era_required_non_progressive_items: Dict[EraType, List[str]] | ||
|  |     era_required_progressive_items_counts: Dict[EraType, Dict[str, int]] | ||
|  |     era_required_progressive_era_counts: Dict[EraType, int] | ||
|  |     item_by_civ_name: Dict[str, str] | ||
|  | 
 | ||
|  |     def __init__(self, multiworld: MultiWorld, player: int): | ||
|  |         super().__init__(multiworld, player) | ||
|  |         self.location_by_era = generate_era_location_table() | ||
|  | 
 | ||
|  |         self.location_table: Dict[str, CivVILocationData] = {} | ||
|  |         self.item_table = generate_item_table() | ||
|  | 
 | ||
|  |         self.era_required_non_progressive_items = {} | ||
|  |         self.era_required_progressive_items_counts = {} | ||
|  |         self.era_required_progressive_era_counts = {} | ||
|  | 
 | ||
|  |         for locations in self.location_by_era.values(): | ||
|  |             for location in locations.values(): | ||
|  |                 self.location_table[location.name] = location | ||
|  | 
 | ||
|  |     def generate_early(self) -> None: | ||
|  |         flat_progressive_items = get_flat_progressive_districts() | ||
|  | 
 | ||
|  |         self.item_by_civ_name = { | ||
|  |             item.civ_name: get_item_by_civ_name(item.civ_name, self.item_table).name | ||
|  |             for item in self.item_table.values() | ||
|  |             if item.civ_name | ||
|  |         } | ||
|  | 
 | ||
|  |         previous_era_counts = None | ||
|  |         eras_list = [e.value for e in EraType] | ||
|  |         for era in EraType: | ||
|  |             # Initialize era_required_progressive_era_counts | ||
|  |             era_index = eras_list.index(era.value) | ||
|  |             self.era_required_progressive_era_counts[era] = ( | ||
|  |                 0 | ||
|  |                 if era in {EraType.ERA_FUTURE, EraType.ERA_INFORMATION} | ||
|  |                 else era_index + 1 | ||
|  |             ) | ||
|  | 
 | ||
|  |             # Initialize era_required_progressive_items_counts | ||
|  |             self.era_required_progressive_items_counts[era] = defaultdict(int) | ||
|  | 
 | ||
|  |             if previous_era_counts: | ||
|  |                 self.era_required_progressive_items_counts[era].update( | ||
|  |                     previous_era_counts | ||
|  |                 ) | ||
|  | 
 | ||
|  |             # Initialize era_required_non_progressive_items and add to item counts | ||
|  |             self.era_required_non_progressive_items[era] = [] | ||
|  | 
 | ||
|  |             for item in get_era_required_items_data()[era.value]: | ||
|  |                 if ( | ||
|  |                     item in flat_progressive_items | ||
|  |                     and self.options.progression_style != "none" | ||
|  |                 ): | ||
|  |                     progressive_name = format_item_name(flat_progressive_items[item]) | ||
|  |                     self.era_required_progressive_items_counts[era][ | ||
|  |                         progressive_name | ||
|  |                     ] += 1 | ||
|  |                 else: | ||
|  |                     self.era_required_non_progressive_items[era].append( | ||
|  |                         self.item_by_civ_name[item] | ||
|  |                     ) | ||
|  | 
 | ||
|  |             previous_era_counts = self.era_required_progressive_items_counts[era].copy() | ||
|  | 
 | ||
|  |     def get_filler_item_name(self) -> str: | ||
|  |         return get_random_filler_by_rarity(self, FillerItemRarity.COMMON).name | ||
|  | 
 | ||
|  |     def create_regions(self) -> None: | ||
|  |         create_regions(self) | ||
|  | 
 | ||
|  |     def set_rules(self) -> None: | ||
|  |         if self.options.boostsanity: | ||
|  |             create_boost_rules(self) | ||
|  | 
 | ||
|  |     def create_event(self, event: str): | ||
|  |         return CivVIEvent(event, ItemClassification.progression, None, self.player) | ||
|  | 
 | ||
|  |     def create_item(self, name: str) -> Item: | ||
|  |         item: CivVIItemData = self.item_table[name] | ||
|  |         classification = item.classification | ||
|  |         if self.options.boostsanity: | ||
|  |             if item.civ_name in BOOSTSANITY_PROGRESSION_ITEMS: | ||
|  |                 classification = ItemClassification.progression | ||
|  | 
 | ||
|  |         return CivVIItem(item, self.player, classification) | ||
|  | 
 | ||
|  |     def create_items(self) -> None: | ||
|  |         data = get_era_required_items_data() | ||
|  |         early_items = data[EraType.ERA_ANCIENT.value] | ||
|  |         early_locations = [ | ||
|  |             location | ||
|  |             for location in self.location_table.values() | ||
|  |             if location.era_type == EraType.ERA_ANCIENT.value | ||
|  |         ] | ||
|  |         for item_name, item_data in self.item_table.items(): | ||
|  |             # These item types are handled individually | ||
|  |             if item_data.item_type in [ | ||
|  |                 CivVICheckType.PROGRESSIVE_DISTRICT, | ||
|  |                 CivVICheckType.ERA, | ||
|  |                 CivVICheckType.GOODY, | ||
|  |             ]: | ||
|  |                 continue | ||
|  | 
 | ||
|  |             # If we're using progressive districts, we need to check if we need to create a different item instead | ||
|  |             item_to_create = item_name | ||
|  |             item: CivVIItemData = self.item_table[item_name] | ||
|  |             if self.options.progression_style != "none": | ||
|  |                 if item.progressive_name: | ||
|  |                     item_to_create = self.item_table[item.progressive_name].name | ||
|  | 
 | ||
|  |             self.multiworld.itempool += [self.create_item(item_to_create)] | ||
|  |             if item.civ_name in early_items: | ||
|  |                 self.multiworld.early_items[self.player][item_to_create] = 1 | ||
|  |             elif self.item_table[item_name].era in [ | ||
|  |                 EraType.ERA_ATOMIC, | ||
|  |                 EraType.ERA_INFORMATION, | ||
|  |                 EraType.ERA_FUTURE, | ||
|  |             ]: | ||
|  |                 for location in early_locations: | ||
|  |                     found_location = None | ||
|  |                     try: | ||
|  |                         found_location = self.get_location(location.name) | ||
|  |                         forbid_item(found_location, item_to_create, self.player) | ||
|  |                     except KeyError: | ||
|  |                         pass | ||
|  | 
 | ||
|  |         # Era items | ||
|  |         if self.options.progression_style == "eras_and_districts": | ||
|  |             # Add one less than the total number of eras (start in ancient, don't need to find it) | ||
|  |             for era in EraType: | ||
|  |                 if era.value == "ERA_ANCIENT": | ||
|  |                     continue | ||
|  |                 progressive_era_item = self.item_table.get("Progressive Era") | ||
|  |                 assert progressive_era_item is not None | ||
|  |                 self.multiworld.itempool += [ | ||
|  |                     self.create_item(progressive_era_item.name) | ||
|  |                 ] | ||
|  | 
 | ||
|  |             self.multiworld.early_items[self.player]["Progressive Era"] = 2 | ||
|  | 
 | ||
|  |         num_filler_items = 0 | ||
|  |         # Goody items, create 10 by default if options are enabled | ||
|  |         if self.options.shuffle_goody_hut_rewards: | ||
|  |             num_filler_items += 10 | ||
|  | 
 | ||
|  |         if self.options.boostsanity: | ||
|  |             num_filler_items += len(get_boosts_data()) | ||
|  | 
 | ||
|  |         filler_count = { | ||
|  |             rarity: math.ceil(FILLER_DISTRIBUTION[rarity] * num_filler_items) | ||
|  |             for rarity in FillerItemRarity.__reversed__() | ||
|  |         } | ||
|  |         filler_count[FillerItemRarity.COMMON] -= ( | ||
|  |             sum(filler_count.values()) - num_filler_items | ||
|  |         ) | ||
|  |         self.multiworld.itempool += [ | ||
|  |             self.create_item(get_random_filler_by_rarity(self, rarity).name) | ||
|  |             for rarity, count in filler_count.items() | ||
|  |             for _ in range(count) | ||
|  |         ] | ||
|  | 
 | ||
|  |     def post_fill(self) -> None: | ||
|  |         if not self.options.pre_hint_items.value: | ||
|  |             return | ||
|  | 
 | ||
|  |         def is_hintable_filler_item(item: Item) -> bool: | ||
|  |             return ( | ||
|  |                 item.classification == 0 | ||
|  |                 and CivVIHintClassification.FILLER.value | ||
|  |                 in self.options.pre_hint_items.value | ||
|  |             ) | ||
|  | 
 | ||
|  |         start_location_hints: Set[str] = self.options.start_location_hints.value | ||
|  |         non_filler_flags = [ | ||
|  |             CivVIHintClassification(flag).to_item_classification() | ||
|  |             for flag in self.options.pre_hint_items.value | ||
|  |             if flag != CivVIHintClassification.FILLER.value | ||
|  |         ] | ||
|  |         for location_name, location_data in self.location_table.items(): | ||
|  |             if ( | ||
|  |                 location_data.location_type != CivVICheckType.CIVIC | ||
|  |                 and location_data.location_type != CivVICheckType.TECH | ||
|  |             ): | ||
|  |                 continue | ||
|  | 
 | ||
|  |             location: CivVILocation = self.get_location(location_name)  # type: ignore | ||
|  | 
 | ||
|  |             if location.item and ( | ||
|  |                 is_hintable_filler_item(location.item) | ||
|  |                 or any( | ||
|  |                     flag in location.item.classification for flag in non_filler_flags | ||
|  |                 ) | ||
|  |             ): | ||
|  |                 start_location_hints.add(location_name) | ||
|  | 
 | ||
|  |     def fill_slot_data(self) -> Dict[str, Any]: | ||
|  |         return self.options.as_dict( | ||
|  |             "progression_style", | ||
|  |             "death_link", | ||
|  |             "research_cost_multiplier", | ||
|  |             "death_link_effect", | ||
|  |             "death_link_effect_percent", | ||
|  |         ) | ||
|  | 
 | ||
|  |     def generate_output(self, output_directory: str): | ||
|  |         mod_name = self.multiworld.get_out_file_name_base(self.player) | ||
|  |         mod_dir = os.path.join(output_directory, mod_name) | ||
|  |         mod_files = { | ||
|  |             f"NewItems.xml": generate_new_items(self), | ||
|  |             f"InitOptions.lua": generate_setup_file(self), | ||
|  |             f"GoodyHutOverride.sql": generate_goody_hut_sql(self), | ||
|  |             f"UpdateExistingBoosts.sql": generate_update_boosts_sql(self), | ||
|  |         } | ||
|  |         mod = CivVIContainer( | ||
|  |             mod_files, | ||
|  |             mod_dir, | ||
|  |             output_directory, | ||
|  |             self.player, | ||
|  |             self.multiworld.get_file_safe_player_name(self.player), | ||
|  |         ) | ||
|  |         mod.write() |