Archipelago/worlds/factorio/Options.py

305 lines
11 KiB
Python

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 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,
"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
}