Skip to content

Commit

Permalink
Factorio: content update
Browse files Browse the repository at this point in the history
Energy Link:
  * Transfer and Storage increased by 10X
  * Cost of building increased by roughly 10X
  * This is a way to address their effect on performance, as users now need a tenth of them to get the same throughput, it also differentiates them from Accumulators
5 new Traps:
  * Teleport Trap
  * Grenade Trap
  * Cluster Grenade Trap
  * Artillery Trap
  * Atomic Rocket Trap
When max science is lower than min science, the two are now swapped.
Max Evolution Trap count was changed from 25 -> 10.
New option: Ingredients Offset
  * When creating random recipes, use this many more or less ingredients in the new recipe.
  • Loading branch information
Berserker66 committed Mar 15, 2023
1 parent 5d6184f commit d825576
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 82 deletions.
1 change: 1 addition & 0 deletions worlds/factorio/Mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def flop_random(low, high, base=None):
"goal": multiworld.goal[player].value,
"energy_link": multiworld.energy_link[player].value,
"useless_technologies": useless_technologies,
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
}

for factorio_option in Options.factorio_options:
Expand Down
59 changes: 57 additions & 2 deletions worlds/factorio/Options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
import typing
import datetime

from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle
from schema import Schema, Optional, And, Or
Expand Down Expand Up @@ -197,6 +198,14 @@ class RecipeIngredients(Choice):
option_science_pack = 1


class RecipeIngredientsOffset(Range):
"""When randomizing ingredients, remove or add this many "slots" of items.
For example, at -1 a randomized Automation Science Pack will only require 1 ingredient, instead of 2."""
display_name = "Randomized Recipe Ingredients Offset"
range_start = -1
range_end = 5


class FactorioStartItems(ItemDict):
"""Mapping of Factorio internal item-name to amount granted on start."""
display_name = "Starting Items"
Expand All @@ -223,9 +232,36 @@ class AttackTrapCount(TrapCount):
display_name = "Attack Traps"


class TeleportTrapCount(TrapCount):
"""Trap items that when received trigger a random teleport."""
display_name = "Teleport Traps"


class GrenadeTrapCount(TrapCount):
"""Trap items that when received trigger a grenade explosion on each player."""
display_name = "Grenade Traps"


class ClusterGrenadeTrapCount(TrapCount):
"""Trap items that when received trigger a cluster grenade explosion on each player."""
display_name = "Cluster Grenade Traps"


class ArtilleryTrapCount(TrapCount):
"""Trap items that when received trigger an artillery shell on each player."""
display_name = "Artillery Traps"


class AtomicRocketTrapCount(TrapCount):
"""Trap items that when received trigger an atomic rocket explosion on each player.
Warning: there is no warning. The launch is instantaneous."""
display_name = "Atomic Rocket Traps"


class EvolutionTrapCount(TrapCount):
"""Trap items that when received increase the enemy evolution."""
display_name = "Evolution Traps"
range_end = 10


class EvolutionTrapIncrease(Range):
Expand Down Expand Up @@ -404,12 +440,31 @@ class EnergyLink(Toggle):
"free_sample_whitelist": FactorioFreeSampleWhitelist,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"recipe_ingredients_offset": RecipeIngredientsOffset,
"imported_blueprints": ImportedBlueprint,
"world_gen": FactorioWorldGen,
"progressive": Progressive,
"evolution_traps": EvolutionTrapCount,
"teleport_traps": TeleportTrapCount,
"grenade_traps": GrenadeTrapCount,
"cluster_grenade_traps": ClusterGrenadeTrapCount,
"artillery_traps": ArtilleryTrapCount,
"atomic_rocket_traps": AtomicRocketTrapCount,
"attack_traps": AttackTrapCount,
"evolution_traps": EvolutionTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
"death_link": DeathLink,
"energy_link": EnergyLink
"energy_link": EnergyLink,
}

# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
if datetime.datetime.today().month == 4:

class ChunkShuffle(Toggle):
"""Entrance Randomizer."""
display_name = "Chunk Shuffle"


if datetime.datetime.today().day > 1:
ChunkShuffle.__doc__ += """
2023 April Fool's option. Shuffles chunk border transitions."""
factorio_options["chunk_shuffle"] = ChunkShuffle
82 changes: 56 additions & 26 deletions worlds/factorio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class FactorioItem(Item):
all_items = tech_table.copy()
all_items["Attack Trap"] = factorio_base_id - 1
all_items["Evolution Trap"] = factorio_base_id - 2
all_items["Teleport Trap"] = factorio_base_id - 3
all_items["Grenade Trap"] = factorio_base_id - 4
all_items["Cluster Grenade Trap"] = factorio_base_id - 5
all_items["Artillery Trap"] = factorio_base_id - 6
all_items["Atomic Rocket Trap"] = factorio_base_id - 7


class Factorio(World):
Expand All @@ -43,7 +48,7 @@ class Factorio(World):
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""
game: str = "Factorio"
game = "Factorio"
special_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes: typing.Dict[str, Recipe]
location_pool: typing.List[FactorioScienceLocation]
Expand All @@ -52,12 +57,11 @@ class Factorio(World):
web = FactorioWeb()

item_name_to_id = all_items
# TODO: remove base_tech_table ~ 0.3.7
location_name_to_id = {**base_tech_table, **location_table}
location_name_to_id = location_table
item_name_groups = {
"Progressive": set(progressive_tech_table.keys()),
}
data_version = 6
data_version = 7
required_client_version = (0, 3, 6)

ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
Expand All @@ -73,8 +77,10 @@ def __init__(self, world, player: int):
generate_output = generate_mod

def generate_early(self) -> None:
self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player],
self.multiworld.min_tech_cost[self.player])
# if max < min, then swap max and min
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn

Expand All @@ -87,14 +93,25 @@ def create_regions(self):
nauvis = Region("Nauvis", player, self.multiworld)

location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.multiworld.evolution_traps[player].value + self.multiworld.attack_traps[player].value
self.multiworld.evolution_traps[player] + \
self.multiworld.attack_traps[player] + \
self.multiworld.teleport_traps[player] + \
self.multiworld.grenade_traps[player] + \
self.multiworld.cluster_grenade_traps[player] + \
self.multiworld.atomic_rocket_traps[player] + \
self.multiworld.artillery_traps[player]

location_pool = []

for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
location_pool.extend(location_pools[pack])
try:
location_names = self.multiworld.random.sample(location_pool, location_count)
except ValueError as e:
# should be "ValueError: Sample larger than population or is negative"
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
f"or increase the location count (higher max science pack). (Player {self.player})") from e

location_names = self.multiworld.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]
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
Expand Down Expand Up @@ -132,6 +149,14 @@ def sorter(loc: FactorioScienceLocation):
crash.connect(nauvis)
self.multiworld.regions += [menu, nauvis]

def create_items(self) -> None:
player = self.player
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.multiworld,
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))

def set_rules(self):
world = self.multiworld
player = self.player
Expand Down Expand Up @@ -184,10 +209,6 @@ def generate_basic(self):
player = self.player
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
want_progressives(self.multiworld.random))
self.multiworld.itempool.extend(self.create_item("Evolution Trap") for _ in
range(self.multiworld.evolution_traps[player].value))
self.multiworld.itempool.extend(self.create_item("Attack Trap") for _ in
range(self.multiworld.attack_traps[player].value))

cost_sorted_locations = sorted(self.locations, key=lambda location: location.name)
special_index = {"automation": 0,
Expand Down Expand Up @@ -265,10 +286,11 @@ def get_category(category: str, liquids: int) -> str:
2: "chemistry"}
return categories.get(liquids, category)

def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2) -> Recipe:
def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2,
ingredients_offset: int = 0) -> Recipe:
new_ingredients = {}
liquids_used = 0
for _ in original.ingredients:
for _ in range(len(original.ingredients) + ingredients_offset):
new_ingredient = pool.pop()
if new_ingredient in fluids:
while liquids_used == allow_liquids and new_ingredient in fluids:
Expand All @@ -282,7 +304,7 @@ def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2
original.products, original.energy)

def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: float = 1,
allow_liquids: int = 2) -> Recipe:
allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe:
"""Generate a recipe from pool with time and cost similar to original * factor"""
new_ingredients = {}
# have to first sort for determinism, while filtering out non-stacking items
Expand All @@ -291,7 +313,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor:
self.multiworld.random.shuffle(pool)
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
target_energy = original.total_energy * factor
target_num_ingredients = len(original.ingredients)
target_num_ingredients = len(original.ingredients) + ingredients_offset
remaining_raw = target_raw
remaining_energy = target_energy
remaining_num_ingredients = target_num_ingredients
Expand Down Expand Up @@ -382,12 +404,13 @@ def set_custom_technologies(self):
return custom_technologies

def set_custom_recipes(self):
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
self.multiworld.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x]: 10 for x in range(3)},
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
original_rocket_part.products,
original_rocket_part.energy)}

Expand All @@ -397,7 +420,8 @@ def set_custom_recipes(self):
valid_pool += sorted(science_pack_pools[pack])
self.multiworld.random.shuffle(valid_pool)
if pack in recipes: # skips over space science pack
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool)
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset=
ingredients_offset)
self.custom_recipes[pack] = new_recipe

if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
Expand All @@ -407,21 +431,27 @@ def set_custom_recipes(self):
valid_pool |= science_pack_pools[pack]

if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7)
new_recipe = self.make_balanced_recipe(
recipes["rocket-silo"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
ingredients_offset=ingredients_offset)
self.custom_recipes["rocket-silo"] = new_recipe

if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7)
new_recipe = self.make_balanced_recipe(
recipes["satellite"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
ingredients_offset=ingredients_offset)
self.custom_recipes["satellite"] = new_recipe
bridge = "ap-energy-bridge"
new_recipe = self.make_quick_recipe(
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1},
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
"replace_4": 1, "replace_5": 1, "replace_6": 1},
{bridge: 1}, 10),
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]))
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
ingredients_offset=ingredients_offset)
for ingredient_name in new_recipe.ingredients:
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(10, 100)
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
self.custom_recipes[bridge] = new_recipe

needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
Expand Down Expand Up @@ -452,7 +482,7 @@ def create_item(self, name: str) -> FactorioItem:
tech_table[name], self.player)

item = FactorioItem(name,
ItemClassification.trap if "Trap" in name else ItemClassification.filler,
ItemClassification.trap if name.endswith("Trap") else ItemClassification.filler,
all_items[name], self.player)
return item

Expand Down
3 changes: 2 additions & 1 deletion worlds/factorio/data/mod/LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

The MIT License (MIT)

Copyright (c) 2021 Berserker55 and Dewiniaid
Copyright (c) 2023 Berserker55
Copyright (c) 2021 Dewiniaid

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
44 changes: 15 additions & 29 deletions worlds/factorio/data/mod/lib.lua
Original file line number Diff line number Diff line change
@@ -1,32 +1,3 @@
function filter_ingredients(ingredients, ingredient_filter)
local new_ingredient_list = {}
for _, ingredient_table in pairs(ingredients) do
if ingredient_filter[ingredient_table[1]] then -- name of ingredient_table
table.insert(new_ingredient_list, ingredient_table)
end
end

return new_ingredient_list
end

function add_ingredients(ingredients, added_ingredients)
local new_ingredient_list = table.deepcopy(ingredients)
for new_ingredient, count in pairs(added_ingredients) do
local found = false
for _, old_ingredient in pairs(ingredients) do
if old_ingredient[1] == new_ingredient then
found = true
break
end
end
if not found then
table.insert(new_ingredient_list, {new_ingredient, count})
end
end

return new_ingredient_list
end

function get_any_stack_size(name)
local item = game.item_prototypes[name]
if item ~= nil then
Expand All @@ -50,4 +21,19 @@ function split(s, sep)
string.gsub(s, pattern, function(c) fields[#fields + 1] = c end)

return fields
end

function random_offset_position(position, offset)
return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)}
end

function fire_entity_at_players(entity_name, speed)
for _, player in ipairs(game.forces["player"].players) do
current_character = player.character
if current_character ~= nil then
current_character.surface.create_entity{name=entity_name,
position=random_offset_position(current_character.position, 128),
target=current_character, speed=speed}
end
end
end
Loading

0 comments on commit d825576

Please sign in to comment.