From 53974d568b0d2723e7f1bce9c17f2d2fc5f89f06 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 28 Oct 2022 21:00:06 +0200 Subject: [PATCH] Factorio: revamped location system (#1147) --- Utils.py | 2 +- worlds/factorio/Locations.py | 32 +++ worlds/factorio/Mod.py | 43 ++-- worlds/factorio/Options.py | 41 ++-- worlds/factorio/Shapes.py | 101 ++++----- worlds/factorio/Technologies.py | 4 +- worlds/factorio/__init__.py | 201 +++++++++++------- .../data/mod_template/data-final-fixes.lua | 81 ++----- .../data/mod_template/locale/en/locale.cfg | 20 +- worlds/factorio/data/mod_template/macros.lua | 2 +- 10 files changed, 294 insertions(+), 233 deletions(-) create mode 100644 worlds/factorio/Locations.py diff --git a/Utils.py b/Utils.py index e0c86ddb..447d39d2 100644 --- a/Utils.py +++ b/Utils.py @@ -37,7 +37,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.5" +__version__ = "0.3.6" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py new file mode 100644 index 00000000..1903e589 --- /dev/null +++ b/worlds/factorio/Locations.py @@ -0,0 +1,32 @@ +from typing import Dict, List + +from .Technologies import factorio_base_id, factorio_id +from .Options import MaxSciencePack + +boundary: int = 0xff +total_locations: int = 0xff + +assert total_locations <= boundary +assert factorio_base_id != factorio_id + + +def make_pools() -> Dict[str, List[str]]: + pools: Dict[str, List[str]] = {} + for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1): + max_needed: int = sum(divmod(boundary, i)) + scale: float = boundary / max_needed + prefix: str = f"AP-{i}-" + pools[pack] = [prefix + hex(int(x * scale))[2:].upper().zfill(2) for x in range(1, max_needed + 1)] + return pools + + +location_pools: Dict[str, List[str]] = make_pools() + +location_table: Dict[str, int] = {} +end_id: int = factorio_id +for pool in location_pools.values(): + location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)}) + end_id += len(pool) + +assert end_id - len(location_table) == factorio_id +del pool diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 018816d9..33e3b75a 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -1,23 +1,23 @@ """Outputs a Factorio Mod to facilitate integration with Archipelago""" -import os -import zipfile -from typing import Optional -import threading import json +import os +import shutil +import threading +import zipfile +from typing import Optional, TYPE_CHECKING import jinja2 -import shutil import Utils -import Patch -import worlds.AutoWorld import worlds.Files from . import Options - from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ base_tech_table, tech_to_progressive_lookup, fluids +if TYPE_CHECKING: + from . import Factorio + template_env: Optional[jinja2.Environment] = None data_template: Optional[jinja2.Template] = None @@ -75,7 +75,7 @@ class FactorioModFile(worlds.Files.APContainer): super(FactorioModFile, self).write_contents(opened_zipfile) -def generate_mod(world, output_directory: str): +def generate_mod(world: "Factorio", output_directory: str): player = world.player multiworld = world.world global data_final_template, locale_template, control_template, data_template, settings_template @@ -95,18 +95,10 @@ def generate_mod(world, output_directory: str): control_template = template_env.get_template("control.lua") settings_template = template_env.get_template("settings.lua") # get data for templates - locations = [] - for location in multiworld.get_filled_locations(player): - if location.address: - locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) + locations = [(location, location.item) + for location in world.locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" - tech_cost_scale = {0: 0.1, - 1: 0.25, - 2: 0.5, - 3: 1, - 4: 2, - 5: 5, - 6: 10}[multiworld.tech_cost[player].value] + random = multiworld.slot_seeds[player] def flop_random(low, high, base=None): @@ -120,18 +112,19 @@ def generate_mod(world, output_directory: str): return random.uniform(low, high) template_data = { - "locations": locations, "player_names": multiworld.player_name, "tech_table": tech_table, - "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, + "locations": locations, + "player_names": multiworld.player_name, + "tech_table": tech_table, + "base_tech_table": base_tech_table, + "tech_to_progressive_lookup": tech_to_progressive_lookup, "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), - "tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies, "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_player": player, "starting_items": multiworld.starting_items[player], "recipes": recipes, "random": random, "flop_random": flop_random, - "static_nodes": multiworld.worlds[player].static_nodes, "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, @@ -141,7 +134,7 @@ def generate_mod(world, output_directory: str): "max_science_pack": multiworld.max_science_pack[player].value, "liquids": fluids, "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value + "energy_link": multiworld.energy_link[player].value, } for factorio_option in Options.factorio_options: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 64e03c43..a3580f8f 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -41,17 +41,30 @@ class Goal(Choice): default = 0 -class TechCost(Choice): - """How expensive are the technologies.""" - display_name = "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 TechCost(Range): + range_start = 1 + range_end = 10000 + default = 5 + + +class MinTechCost(TechCost): + """The cheapest a Technology can be in Science Packs.""" + display_name = "Minimum Science Pack Cost" + default = 5 + + +class MaxTechCost(TechCost): + """The most expensive a Technology can be in Science Packs.""" + display_name = "Maximum Science Pack Cost" + default = 500 + + +class TechCostMix(Range): + """Percent chance that a preceding Science Pack is also required. + Chance is rolled per preceding pack.""" + display_name = "Science Pack Cost Mix" + range_end = 100 + default = 70 class Silo(Choice): @@ -168,7 +181,7 @@ class FactorioFreeSampleWhitelist(OptionSet): class TrapCount(Range): - range_end = 4 + range_end = 25 class AttackTrapCount(TrapCount): @@ -343,7 +356,9 @@ factorio_options: typing.Dict[str, type(Option)] = { "max_science_pack": MaxSciencePack, "goal": Goal, "tech_tree_layout": TechTreeLayout, - "tech_cost": TechCost, + "min_tech_cost": MinTechCost, + "max_tech_cost": MaxTechCost, + "tech_cost_mix": TechCostMix, "silo": Silo, "satellite": Satellite, "free_samples": FreeSamples, diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index f42da4d2..4f093c3a 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -1,8 +1,11 @@ -from typing import Dict, List, Set +from typing import Dict, List, Set, TYPE_CHECKING from collections import deque from .Options import TechTreeLayout +if TYPE_CHECKING: + from . import Factorio, FactorioScienceLocation + funnel_layers = {TechTreeLayout.option_small_funnels: 3, TechTreeLayout.option_medium_funnels: 4, TechTreeLayout.option_large_funnels: 5} @@ -12,24 +15,26 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6, TechTreeLayout.option_large_funnels: 15} -def get_shapes(factorio_world) -> Dict[str, List[str]]: +def _sorter(location: "FactorioScienceLocation"): + return location.complexity, location.rel_cost + + +def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: world = factorio_world.world player = factorio_world.player - prerequisites: Dict[str, Set[str]] = {} + prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} layout = world.tech_tree_layout[player].value - custom_technologies = factorio_world.custom_technologies - tech_names: List[str] = list(set(custom_technologies) - world.worlds[player].static_nodes) - tech_names.sort() - world.random.shuffle(tech_names) + locations: List["FactorioScienceLocation"] = sorted(factorio_world.locations, key=lambda loc: loc.name) + world.random.shuffle(locations) if layout == TechTreeLayout.option_single: pass elif layout == TechTreeLayout.option_small_diamonds: slice_size = 4 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) diamond_0, diamond_1, diamond_2, diamond_3 = slice # 0 | @@ -40,10 +45,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_medium_diamonds: slice_size = 9 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -65,10 +70,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_large_diamonds: slice_size = 16 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -101,10 +106,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_small_pyramids: slice_size = 6 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -119,10 +124,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_medium_pyramids: slice_size = 10 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -144,10 +149,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_large_pyramids: slice_size = 15 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -176,17 +181,17 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout in funnel_layers: slice_size = funnel_slice_sizes[layout] - world.random.shuffle(tech_names) + world.random.shuffle(locations) - while len(tech_names) > slice_size: - tech_names = tech_names[slice_size:] - current_tech_names = tech_names[:slice_size] + while len(locations) > slice_size: + locations = locations[slice_size:] + current_locations = locations[:slice_size] layer_size = funnel_layers[layout] previous_slice = [] - current_tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + current_locations.sort(key=_sorter) for layer in range(funnel_layers[layout]): - slice = current_tech_names[:layer_size] - current_tech_names = current_tech_names[layer_size:] + slice = current_locations[:layer_size] + current_locations = current_locations[layer_size:] if previous_slice: for i, tech_name in enumerate(slice): prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2]) @@ -202,10 +207,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: # 15 | # 16 | slice_size = 17 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) prerequisites[slice[1]] = {slice[0]} prerequisites[slice[2]] = {slice[0]} @@ -229,13 +234,13 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]} prerequisites[slice[16]] = {slice[15]} elif layout == TechTreeLayout.option_choices: - tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) - current_choices = deque([tech_names[0]]) - tech_names = tech_names[1:] - while len(tech_names) > 1: + locations.sort(key=_sorter) + current_choices = deque([locations[0]]) + locations = locations[1:] + while len(locations) > 1: source = current_choices.pop() - choices = tech_names[:2] - tech_names = tech_names[2:] + choices = locations[:2] + locations = locations[2:] for choice in choices: prerequisites[choice] = {source} current_choices.extendleft(choices) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index b88cc9b1..09f9fc93 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -36,7 +36,7 @@ technology_table: Dict[str, Technology] = {} always = lambda state: True -class FactorioElement(): +class FactorioElement: name: str def __repr__(self): @@ -98,7 +98,7 @@ class CustomTechnology(Technology): and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) or origin.name == "rocket-silo") self.player = player - if origin.name not in world.worlds[player].static_nodes: + if origin.name not in world.worlds[player].special_nodes: if military_allowed: ingredients.add("military-science-pack") ingredients = list(ingredients) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a01abac7..f1bdce17 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,19 +1,20 @@ +from __future__ import annotations + import collections +import logging import typing -from worlds.AutoWorld import World, WebWorld - from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +from .Mod import generate_mod +from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal +from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ - fluids, stacking_items, valid_ingredients -from .Shapes import get_shapes -from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal - -import logging + fluids, stacking_items, valid_ingredients, progressive_rows +from .Locations import location_pools, location_table class FactorioWeb(WebWorld): @@ -43,89 +44,75 @@ class Factorio(World): research new technologies, and become more efficient in your quest to build a rocket and return home. """ game: str = "Factorio" - static_nodes = {"automation", "logistics", "rocket-silo"} + special_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes: typing.Dict[str, Recipe] + location_pool: typing.List[FactorioScienceLocation] advancement_technologies: typing.Set[str] web = FactorioWeb() item_name_to_id = all_items - location_name_to_id = base_tech_table + # TODO: remove base_tech_table ~ 0.3.7 + location_name_to_id = {**base_tech_table, **location_table} item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 5 - required_client_version = (0, 3, 0) + data_version = 6 + required_client_version = (0, 3, 6) + + ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() + tech_mix: int = 0 + skip_silo: bool = False def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) self.advancement_technologies = set() self.custom_recipes = {} - - def generate_basic(self): - player = self.player - want_progressives = collections.defaultdict(lambda: self.world.progressive[player]. - want_progressives(self.world.random)) - skip_silo = self.world.silo[player].value == Silo.option_spawn - evolution_traps_wanted = self.world.evolution_traps[player].value - attack_traps_wanted = self.world.attack_traps[player].value - traps_wanted = ["Evolution Trap"] * evolution_traps_wanted + ["Attack Trap"] * attack_traps_wanted - self.world.random.shuffle(traps_wanted) - - for tech_name in base_tech_table: - if traps_wanted and tech_name in useless_technologies: - self.world.itempool.append(self.create_item(traps_wanted.pop())) - elif skip_silo and tech_name == "rocket-silo": - pass - else: - progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) - want_progressive = want_progressives[progressive_item_name] - item_name = progressive_item_name if want_progressive else tech_name - tech_item = self.create_item(item_name) - if tech_name in self.static_nodes: - self.world.get_location(tech_name, player).place_locked_item(tech_item) - else: - self.world.itempool.append(tech_item) - - map_basic_settings = self.world.world_gen[player].value["basic"] - if map_basic_settings.get("seed", None) is None: # allow seed 0 - map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint - - # used to be called "sending_visible" - if self.world.tech_tree_information[player] == TechTreeInformation.option_full: - # mark all locations as pre-hinted - self.world.start_location_hints[self.player].value.update(base_tech_table) + self.locations = [] generate_output = generate_mod + def generate_early(self) -> None: + self.world.max_tech_cost[self.player] = max(self.world.max_tech_cost[self.player], + self.world.min_tech_cost[self.player]) + self.tech_mix = self.world.tech_cost_mix[self.player] + self.skip_silo = self.world.silo[self.player].value == Silo.option_spawn + def create_regions(self): player = self.player + random = self.world.random menu = Region("Menu", RegionType.Generic, "Menu", player, self.world) crash = Entrance(player, "Crash Land", menu) menu.exits.append(crash) nauvis = Region("Nauvis", RegionType.Generic, "Nauvis", player, self.world) - skip_silo = self.world.silo[self.player].value == Silo.option_spawn - for tech_name, tech_id in base_tech_table.items(): - if skip_silo and tech_name == "rocket-silo": - continue - tech = Location(player, tech_name, tech_id, nauvis) - nauvis.locations.append(tech) - tech.game = "Factorio" - location = Location(player, "Rocket Launch", None, nauvis) + location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ + self.world.evolution_traps[player].value + self.world.attack_traps[player].value + + location_pool = [] + + for pack in self.world.max_science_pack[self.player].get_allowed_packs(): + location_pool.extend(location_pools[pack]) + location_names = self.world.random.sample(location_pool, location_count) + self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) + for loc_name in location_names] + rand_values = sorted(random.randint(self.world.min_tech_cost[self.player], + self.world.max_tech_cost[self.player]) for _ in self.locations) + for i, location in enumerate(sorted(self.locations, key=lambda loc: loc.rel_cost)): + location.count = rand_values[i] + del rand_values + nauvis.locations.extend(self.locations) + location = FactorioLocation(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) - location.game = "Factorio" event = FactorioItem("Victory", ItemClassification.progression, None, player) - event.game = "Factorio" - self.world.push_item(location, event, False) - location.event = location.locked = True + location.place_locked_item(event) + for ingredient in self.world.max_science_pack[self.player].get_allowed_packs(): - location = Location(player, f"Automate {ingredient}", None, nauvis) - location.game = "Factorio" + location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) - self.world.push_item(location, event, False) - location.event = location.locked = True + location.place_locked_item(event) + crash.connect(nauvis) self.world.regions += [menu, nauvis] @@ -151,17 +138,13 @@ class Factorio(World): location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) - skip_silo = self.world.silo[self.player].value == Silo.option_spawn - for tech_name, technology in self.custom_technologies.items(): - if skip_silo and tech_name == "rocket-silo": - continue - location = world.get_location(tech_name, player) - Rules.set_rule(location, technology.build_rule(player)) - prequisites = shapes.get(tech_name) - if prequisites: - locations = {world.get_location(requisite, player) for requisite in prequisites} - Rules.add_rule(location, lambda state, - locations=locations: all(state.can_reach(loc) for loc in locations)) + for location in self.locations: + Rules.set_rule(location, lambda state, ingredients=location.ingredients: + all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) + prerequisites = shapes.get(location) + if prerequisites: + Rules.add_rule(location, lambda state, locations= + prerequisites: all(state.can_reach(loc) for loc in locations)) silo_recipe = None if self.world.silo[self.player] == Silo.option_spawn: @@ -179,6 +162,48 @@ class Factorio(World): world.completion_condition[player] = lambda state: state.has('Victory', player) + def generate_basic(self): + player = self.player + want_progressives = collections.defaultdict(lambda: self.world.progressive[player]. + want_progressives(self.world.random)) + self.world.itempool.extend(self.create_item("Evolution Trap") for _ in + range(self.world.evolution_traps[player].value)) + self.world.itempool.extend(self.create_item("Attack Trap") for _ in + range(self.world.attack_traps[player].value)) + + cost_sorted_locations = sorted(self.locations, key=lambda location: location.name) + special_index = {"automation": 0, + "logistics": 1, + "rocket-silo": -1} + loc: FactorioScienceLocation + if self.skip_silo: + removed = useless_technologies | {"rocket-silo"} + else: + removed = useless_technologies + for tech_name in base_tech_table: + if tech_name not in removed: + progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) + want_progressive = want_progressives[progressive_item_name] + item_name = progressive_item_name if want_progressive else tech_name + tech_item = self.create_item(item_name) + index = special_index.get(tech_name, None) + if index is None: + self.world.itempool.append(tech_item) + else: + loc = cost_sorted_locations[index] + loc.place_locked_item(tech_item) + loc.revealed = True + + map_basic_settings = self.world.world_gen[player].value["basic"] + if map_basic_settings.get("seed", None) is None: # allow seed 0 + map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint + + if self.world.tech_tree_information[player] == TechTreeInformation.option_full: + # mark all locations as pre-hinted + self.world.start_location_hints[self.player].value.update(base_tech_table) + for loc in self.locations: + loc.revealed = True + def collect_item(self, state, item, remove=False): if item.advancement and item.name in progressive_technology_table: prog_table = progressive_technology_table[item.name].progressive @@ -400,3 +425,33 @@ class Factorio(World): ItemClassification.trap if "Trap" in name else ItemClassification.filler, all_items[name], self.player) return item + + +class FactorioLocation(Location): + game: str = Factorio.game + + +class FactorioScienceLocation(FactorioLocation): + complexity: int + revealed: bool = False + + # Factorio technology properties: + ingredients: typing.Dict[str, int] + count: int + + def __init__(self, player: int, name: str, address: int, parent: Region): + super(FactorioScienceLocation, self).__init__(player, name, address, parent) + # "AP-{Complexity}-{Cost}" + self.complexity = int(self.name[3]) - 1 + self.rel_cost = int(self.name[5:], 16) + + self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} + for complexity in range(self.complexity): + if parent.world.tech_cost_mix[self.player] > parent.world.random.randint(0, 99): + self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 + self.count = parent.world.random.randint(parent.world.min_tech_cost[self.player], + parent.world.max_tech_cost[self.player]) + + @property + def factorio_ingredients(self) -> typing.List[typing.Tuple[str, int]]: + return [(name, count) for name, count in self.ingredients.items()] diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 70bc1eac..3021fd5d 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -1,4 +1,4 @@ -{% from "macros.lua" import dict_to_recipe %} +{% from "macros.lua" import dict_to_recipe, variable_to_lua %} -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { @@ -50,16 +50,8 @@ data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ing {%- endfor %} local technologies = data.raw["technology"] -local original_tech local new_tree_copy -allowed_ingredients = {} -{%- for tech_name, technology in custom_technologies.items() %} -allowed_ingredients["{{ tech_name }}"] = { -{%- for ingredient in technology.ingredients %} -["{{ingredient}}"] = 1, -{%- endfor %} -} -{% endfor %} + local template_tech = table.deepcopy(technologies["automation"]) {#- ensure the copy unlocks nothing #} template_tech.unlocks = {} @@ -87,39 +79,6 @@ template_tech.prerequisites = {} data.raw["recipe"]["rocket-silo"].enabled = true {% endif %} -function prep_copy(new_copy, old_tech) - old_tech.hidden = true - local ingredient_filter = allowed_ingredients[old_tech.name] - if ingredient_filter ~= nil then - if mods["science-not-invited"] then - local weights = { - ["automation-science-pack"] = 0, -- Red science - ["logistic-science-pack"] = 0, -- Green science - ["military-science-pack"] = 0, -- Black science - ["chemical-science-pack"] = 0, -- Blue science - ["production-science-pack"] = 0, -- Purple science - ["utility-science-pack"] = 0, -- Yellow science - ["space-science-pack"] = 0 -- Space science - } - for key, value in pairs(ingredient_filter) do - weights[key] = value - end - SNI.setWeights(weights) - -- Just in case an ingredient is being added to an existing tech. Found the root cause of the 9.223e+18 problem. - -- Turns out science-not-invited was ultimately dividing by zero, due to it being unaware of there being added ingredients. - old_tech.unit.ingredients = add_ingredients(old_tech.unit.ingredients, ingredient_filter) - SNI.sendInvite(old_tech) - -- SCIENCE-not-invited could potentially make tech cost 9.223e+18. - old_tech.unit.count = math.min(100000, old_tech.unit.count) - end - new_copy.unit = table.deepcopy(old_tech.unit) - new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter) - new_copy.unit.ingredients = add_ingredients(new_copy.unit.ingredients, ingredient_filter) - else - new_copy.unit = table.deepcopy(old_tech.unit) - end -end - function set_ap_icon(tech) tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png" tech.icons = nil @@ -198,38 +157,40 @@ end data.raw["ammo"]["artillery-shell"].stack_size = 10 {# each randomized tech gets set to be invisible, with new nodes added that trigger those #} -{%- for original_tech_name, item_name, receiving_player, advancement in locations %} -original_tech = technologies["{{original_tech_name}}"] +{%- for original_tech_name in base_tech_table -%} +technologies["{{ original_tech_name }}"].hidden = true +{% endfor %} +{%- for location, item in locations %} {#- the tech researched by the local player #} new_tree_copy = table.deepcopy(template_tech) -new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} -prep_copy(new_tree_copy, original_tech) -{% if tech_cost_scale != 1 %} -new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) -{% endif %} -{%- if (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in base_tech_table -%} -{#- copy Factorio Technology Icon -#} -copy_factorio_icon(new_tree_copy, "{{ item_name }}") -{%- if original_tech_name == "rocket-silo" and original_tech_name in static_nodes %} +new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #} +new_tree_copy.unit.count = {{ location.count }} +new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }} + +{%- if location.revealed and item.name in base_tech_table -%} +{#- copy Factorio Technology Icon #} +copy_factorio_icon(new_tree_copy, "{{ item.name }}") +{%- if item.name == "rocket-silo" and item.player == location.player %} {%- for ingredient in custom_recipes["rocket-part"].ingredients %} table.insert(new_tree_copy.effects, {type = "nothing", effect_description = "Ingredient {{ loop.index }}: {{ ingredient }}"}) {% endfor -%} {% endif -%} -{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%} -copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}") +{%- elif location.revealed and item.name in progressive_technology_table -%} +copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item.name][0] }}") {%- else -%} {#- use default AP icon if no Factorio graphics exist -#} -{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} +{% if item.advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} {%- endif -%} {#- connect Technology #} -{%- if original_tech_name in tech_tree_layout_prerequisites %} -{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %} -table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-") +{%- if location in tech_tree_layout_prerequisites %} +{%- for prerequisite in tech_tree_layout_prerequisites[location] %} +table.insert(new_tree_copy.prerequisites, "ap-{{ prerequisite.address }}-") {% endfor %} {% endif -%} {#- add new Technology to game #} data:extend{new_tree_copy} {% endfor %} +{#- Recipe Rando #} {% if recipe_time_scale %} {%- for recipe_name, recipe in recipes.items() %} {%- if recipe.category not in ("basic-solid", "basic-fluid") %} diff --git a/worlds/factorio/data/mod_template/locale/en/locale.cfg b/worlds/factorio/data/mod_template/locale/en/locale.cfg index e970dbfa..59dcffcd 100644 --- a/worlds/factorio/data/mod_template/locale/en/locale.cfg +++ b/worlds/factorio/data/mod_template/locale/en/locale.cfg @@ -5,22 +5,22 @@ archipelago=Archipelago archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos. [technology-name] -{% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} -ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} +{% for location, item in locations %} +{%- if location.revealed %} +ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }}) {%- else %} -ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable +ap-{{ location.address }}-= {{location.name}} {%- endif -%} {% endfor %} [technology-description] -{% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. -{%- elif tech_tree_information == 1 and advancement %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}". +{% for location, item in locations %} +{%- if location.revealed %} +ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}. +{%- elif tech_tree_information == 1 and item.advancement %} +ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement. {%- else %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}". +ap-{{ location.address }}-=Researching this technology sends something to someone. {%- endif -%} {% endfor %} diff --git a/worlds/factorio/data/mod_template/macros.lua b/worlds/factorio/data/mod_template/macros.lua index c81ddc5f..1b271031 100644 --- a/worlds/factorio/data/mod_template/macros.lua +++ b/worlds/factorio/data/mod_template/macros.lua @@ -4,7 +4,7 @@ ["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %} {% endfor -%} } -{%- endmacro %} +{% endmacro %} {% macro list_to_lua(list) -%} { {%- for key in list -%}