from __future__ import annotations import typing from Options import Choice, OptionDict, ItemDict, Option, DefaultOnToggle, Range, DeathLink from schema import Schema, Optional, And, Or # schema helpers FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) LuaBool = Or(bool, And(int, lambda n: n in (0, 1))) class MaxSciencePack(Choice): """Maximum level of science pack required to complete the game.""" displayname = "Maximum Required Science Pack" option_automation_science_pack = 0 option_logistic_science_pack = 1 option_military_science_pack = 2 option_chemical_science_pack = 3 option_production_science_pack = 4 option_utility_science_pack = 5 option_space_science_pack = 6 default = 6 def get_allowed_packs(self): return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \ {"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense @classmethod def get_ordered_science_packs(cls): return [option.replace("_", "-") for option, value in sorted(cls.options.items(), key=lambda pair: pair[1])] def get_max_pack(self): return self.get_ordered_science_packs()[self.value].replace("_", "-") class TechCost(Choice): """How expensive are the technologies.""" displayname = "Technology Cost Scale" option_very_easy = 0 option_easy = 1 option_kind = 2 option_normal = 3 option_hard = 4 option_very_hard = 5 option_insane = 6 default = 3 class Silo(Choice): """Ingredients to craft rocket silo or auto-place if set to spawn.""" displayname = "Rocket Silo" option_vanilla = 0 option_randomize_recipe = 1 option_spawn = 2 default = 0 class Satellite(Choice): """Ingredients to craft satellite.""" displayname = "Satellite" options_vanilla = 0 option_randomize_recipe = 1 default = 0 class FreeSamples(Choice): """Get free items with your technologies.""" displayname = "Free Samples" option_none = 0 option_single_craft = 1 option_half_stack = 2 option_stack = 3 default = 3 class TechTreeLayout(Choice): """Selects how the tech tree nodes are interwoven.""" displayname = "Technology Tree Layout" option_single = 0 option_small_diamonds = 1 option_medium_diamonds = 2 option_large_diamonds = 3 option_small_pyramids = 4 option_medium_pyramids = 5 option_large_pyramids = 6 option_small_funnels = 7 option_medium_funnels = 8 option_large_funnels = 9 default = 0 class TechTreeInformation(Choice): """How much information should be displayed in the tech tree.""" displayname = "Technology Tree Information" option_none = 0 option_advancement = 1 option_full = 2 default = 2 class RecipeTime(Choice): """randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.""" displayname = "Recipe Time" option_vanilla = 0 option_fast = 1 option_normal = 2 option_slow = 4 option_chaos = 5 class Progressive(Choice): displayname = "Progressive Technologies" option_off = 0 option_grouped_random = 1 option_on = 2 alias_false = 0 alias_true = 2 default = 2 def want_progressives(self, random): return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value) class RecipeIngredients(Choice): """Select if rocket, or rocket + science pack ingredients should be random.""" displayname = "Random Recipe Ingredients Level" option_rocket = 0 option_science_pack = 1 class FactorioStartItems(ItemDict): displayname = "Starting Items" verify_item_name = False default = {"burner-mining-drill": 19, "stone-furnace": 19} class TrapCount(Range): range_end = 4 class AttackTrapCount(TrapCount): """Trap items that when received trigger an attack on your base.""" displayname = "Attack Traps" class EvolutionTrapCount(TrapCount): """Trap items that when received increase the enemy evolution.""" displayname = "Evolution Traps" class EvolutionTrapIncrease(Range): displayname = "Evolution Trap % Effect" range_start = 1 default = 10 range_end = 100 class FactorioWorldGen(OptionDict): displayname = "World Generation" # FIXME: do we want default be a rando-optimized default or in-game DS? value: typing.Dict[str, typing.Dict[str, typing.Any]] default = { "terrain_segmentation": 0.5, "water": 1.5, "autoplace_controls": { "coal": {"frequency": 1, "size": 3, "richness": 6}, "copper-ore": {"frequency": 1, "size": 3, "richness": 6}, "crude-oil": {"frequency": 1, "size": 3, "richness": 6}, "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, "iron-ore": {"frequency": 1, "size": 3, "richness": 6}, "stone": {"frequency": 1, "size": 3, "richness": 6}, "trees": {"frequency": 1, "size": 1, "richness": 1}, "uranium-ore": {"frequency": 1, "size": 3, "richness": 6} }, "seed": None, "starting_area": 1, "peaceful_mode": False, "cliff_settings": { "name": "cliff", "cliff_elevation_0": 10, "cliff_elevation_interval": 40, "richness": 1 }, "property_expression_names": { "control-setting:moisture:bias": 0, "control-setting:moisture:frequency:multiplier": 1, "control-setting:aux:bias": 0, "control-setting:aux:frequency:multiplier": 1 }, "pollution": { "enabled": True, "diffusion_ratio": 0.02, "ageing": 1, "enemy_attack_pollution_consumption_modifier": 1, "min_pollution_to_damage_trees": 60, "pollution_restored_per_tree_damage": 10 }, "enemy_evolution": { "enabled": True, "time_factor": 40.0e-7, "destroy_factor": 200.0e-5, "pollution_factor": 9.0e-7 }, "enemy_expansion": { "enabled": True, "max_expansion_distance": 7, "settler_group_min_size": 5, "settler_group_max_size": 20, "min_expansion_cooldown": 14400, "max_expansion_cooldown": 216000 } } schema = Schema({ "basic": { Optional("terrain_segmentation"): FloatRange(0.166, 6), Optional("water"): FloatRange(0.166, 6), Optional("autoplace_controls"): { str: { "frequency": FloatRange(0, 6), "size": FloatRange(0, 6), "richness": FloatRange(0.166, 6) } }, Optional("seed"): Or(None, And(int, lambda n: n >= 0)), Optional("starting_area"): FloatRange(0.166, 6), Optional("peaceful_mode"): LuaBool, Optional("cliff_settings"): { "name": str, "cliff_elevation_0": FloatRange(0, 99), "cliff_elevation_interval": FloatRange(0.066, 241), # 40/frequency "richness": FloatRange(0, 6) }, Optional("property_expression_names"): Schema({ "control-setting:moisture:bias": FloatRange(-0.5, 0.5), "control-setting:moisture:frequency:multiplier": FloatRange(0.166, 6), "control-setting:aux:bias": FloatRange(-0.5, 0.5), "control-setting:aux:frequency:multiplier": FloatRange(0.166, 6) }, ignore_extra_keys=True) }, "advanced": { Optional("pollution"): { Optional("enabled"): LuaBool, Optional("diffusion_ratio"): FloatRange(0, 0.25), Optional("ageing"): FloatRange(0.1, 4), Optional("enemy_attack_pollution_consumption_modifier"): FloatRange(0.1, 4), Optional("min_pollution_to_damage_trees"): FloatRange(0, 9999), Optional("pollution_restored_per_tree_damage"): FloatRange(0, 9999) }, Optional("enemy_evolution"): { Optional("enabled"): LuaBool, Optional("time_factor"): FloatRange(0, 1000e-7), Optional("destroy_factor"): FloatRange(0, 1000e-5), Optional("pollution_factor"): FloatRange(0, 1000e-7), }, Optional("enemy_expansion"): { Optional("enabled"): LuaBool, Optional("max_expansion_distance"): FloatRange(2, 20), Optional("settler_group_min_size"): FloatRange(1, 20), Optional("settler_group_max_size"): FloatRange(1, 50), Optional("min_expansion_cooldown"): FloatRange(3600, 216000), Optional("max_expansion_cooldown"): FloatRange(18000, 648000) } } }) def __init__(self, value: typing.Dict[str, typing.Any]): advanced = {"pollution", "enemy_evolution", "enemy_expansion"} self.value = { "basic": {key: value[key] for key in value.keys() - advanced}, "advanced": {key: value[key] for key in value.keys() & advanced} } # verify min_values <= max_values def optional_min_lte_max(container, min_key, max_key): min_val = container.get(min_key, None) max_val = container.get(max_key, None) if min_val is not None and max_val is not None and min_val > max_val: raise ValueError(f"{min_key} can't be bigger than {max_key}") enemy_expansion = self.value["advanced"].get("enemy_expansion", {}) optional_min_lte_max(enemy_expansion, "settler_group_min_size", "settler_group_max_size") optional_min_lte_max(enemy_expansion, "min_expansion_cooldown", "max_expansion_cooldown") @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> FactorioWorldGen: if type(data) == dict: return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") class ImportedBlueprint(DefaultOnToggle): displayname = "Blueprints" factorio_options: typing.Dict[str, type(Option)] = { "max_science_pack": MaxSciencePack, "tech_tree_layout": TechTreeLayout, "tech_cost": TechCost, "silo": Silo, "satellite": Satellite, "free_samples": FreeSamples, "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, "recipe_time": RecipeTime, "recipe_ingredients": RecipeIngredients, "imported_blueprints": ImportedBlueprint, "world_gen": FactorioWorldGen, "progressive": Progressive, "evolution_traps": EvolutionTrapCount, "attack_traps": AttackTrapCount, "evolution_trap_increase": EvolutionTrapIncrease, "death_link": DeathLink }