Files
Grinch-AP/worlds/civ_6/__init__.py

327 lines
12 KiB
Python
Raw Normal View History

New Game Implementation: Civilization VI (#3736) * Init * remove submodule * Init * Update docs * Fix tests * Update to use apcivvi * Update Readme and codeowners * Minor changes * Remove .value from options (except starting hint) * Minor updates * remove unnecessary property * Cleanup Rules and Region * Fix output file generation * Implement feedback * Remove 'AP' tag and fix issue with format strings and using same quotes * Update worlds/civ_6/__init__.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Minor docs changes * minor updates * Small rework of create items * Minor updates * Remove unused variable * Move client to Launcher Components with rest of similar clients * Revert "Move client to Launcher Components with rest of similar clients" This reverts commit f9fd5df9fdf19eaf4f1de54e21e3c33a74f02364. * modify component * Fix generation issues * Fix tests * Minor change * Add improvement and test case * Minor options changes * . * Preliminary Review * Fix failing test due to slot data serialization * Format json * Remove exclude missable boosts * Update options (update goody hut text, make research multiplier a range) * Update docs punctuation and slot data init * Move priority/excluded locations into options * Implement docs PR feedback * PR Feedback for options * PR feedback misc * Update location classification and fix client type * Fix typings * Update research cost multiplier * Remove unnecessary location priority code * Remove extrenous use of items() * WIP PR Feedback * WIP PR Feedback * Add victory event * Add option set for death link effect * PR improvements * Update post fill hint to support items with multiple classifications * remove unnecessary len * Move location exclusion logic * Update test to use set instead of accidental dict * Update docs around progressive eras and boost locations * Update docs for options to be more readable * Fix issue with filler items and prehints * Update filler_data to be static * Update links in docs * Minor updates and PR feedback * Update boosts data * Update era required items * Update existing techs * Update existing techs * move boost data class * Update reward data * Update prereq data * Update new items and progressive districts * Remove unused code * Make filler item name func more efficient * Update death link text * Move Civ6 to the end of readme * Fix bug with hidden locations and location.name * Partial PR Feedback Implementation * Format changes * Minor review feedback * Modify access rules to use list created in generate_early * Modify boost rules to precalculate requirements * Remove option checks from access rules * Fix issue with pre initialized dicts * Add inno setup for civ6 client * Update inno_setup.iss --------- Co-authored-by: Scipio Wright <scipiowright@gmail.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-03-10 07:53:26 -06:00
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()