mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00

* 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>
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()
|