├── .gitignore ├── graphics ├── null.png ├── shock.png ├── emitter.png ├── biohazard.png ├── emitter-icon.png ├── micro-emitter-icon.png └── overlay │ ├── 20_red_overlay.png │ ├── 22_red_overlay.png │ ├── 24_red_overlay.png │ ├── 25_red_overlay.png │ ├── 26_red_overlay.png │ ├── 28_red_overlay.png │ ├── 30_red_overlay.png │ ├── 32_red_overlay.png │ ├── 34_red_overlay.png │ ├── 35_red_overlay.png │ ├── 36_red_overlay.png │ ├── 38_red_overlay.png │ ├── 40_red_overlay.png │ ├── 42_red_overlay.png │ ├── 44_red_overlay.png │ ├── 45_red_overlay.png │ ├── 46_red_overlay.png │ ├── 48_red_overlay.png │ ├── 50_red_overlay.png │ ├── 52_red_overlay.png │ ├── 54_red_overlay.png │ ├── 55_red_overlay.png │ ├── 56_red_overlay.png │ ├── 58_red_overlay.png │ ├── 60_red_overlay.png │ ├── 62_red_overlay.png │ ├── 64_red_overlay.png │ ├── 65_red_overlay.png │ ├── 66_red_overlay.png │ ├── 68_red_overlay.png │ ├── 70_red_overlay.png │ ├── 72_red_overlay.png │ ├── 74_red_overlay.png │ ├── 75_red_overlay.png │ ├── 76_red_overlay.png │ ├── 78_red_overlay.png │ └── 80_red_overlay.png ├── migrations ├── Misanthrope_0.0.6.lua ├── Misanthrope_0.2.0.lua └── Misanthrope_0.2.5.lua ├── prototypes ├── style │ └── frame.lua ├── equipment │ └── equipment.lua ├── entity │ ├── spawner.lua │ └── emitter.lua ├── item │ └── emitter.lua ├── recipes │ └── emitter.lua └── technology │ └── alien_defense.lua ├── data.lua ├── circle.yml ├── README.md ├── remote.lua ├── libs ├── biter │ ├── ai │ │ ├── no_op.lua │ │ ├── donate_currency.lua │ │ ├── save_currency.lua │ │ ├── abandon_hive.lua │ │ ├── alert.lua │ │ ├── assist_ally.lua │ │ ├── attacked_recently.lua │ │ ├── grow_hive.lua │ │ ├── build_worm.lua │ │ ├── harrassment.lua │ │ ├── identify_targets.lua │ │ └── attack_area.lua │ ├── biter.lua │ ├── random_name.lua │ ├── overwatch.lua │ └── overmind.lua ├── region │ ├── biter_scents.lua │ ├── region_coords.lua │ └── chunk_value.lua ├── pathfinding_engine.lua ├── biter_targets.lua ├── pathfinder_demo.lua ├── developer_mode.lua ├── EvoGUI.lua ├── circular_buffer.lua ├── map_settings.lua ├── world.lua ├── pathfinder.lua └── harpa.lua ├── info.json ├── stdlib ├── core.lua ├── time.lua ├── entity │ ├── inventory.lua │ └── entity.lua ├── game.lua ├── data │ ├── data.lua │ └── recipe.lua ├── string.lua ├── surface.lua ├── log │ └── logger.lua ├── area │ ├── chunk.lua │ ├── tile.lua │ ├── position.lua │ └── area.lua ├── event │ └── event.lua ├── gui │ └── gui.lua └── table.lua ├── LICENSE ├── Makefile ├── control.lua ├── locale └── en │ └── misanthrope.cfg └── data-updates.lua /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build/* 3 | factorio_mods 4 | -------------------------------------------------------------------------------- /graphics/null.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/null.png -------------------------------------------------------------------------------- /graphics/shock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/shock.png -------------------------------------------------------------------------------- /graphics/emitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/emitter.png -------------------------------------------------------------------------------- /graphics/biohazard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/biohazard.png -------------------------------------------------------------------------------- /graphics/emitter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/emitter-icon.png -------------------------------------------------------------------------------- /graphics/micro-emitter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/micro-emitter-icon.png -------------------------------------------------------------------------------- /graphics/overlay/20_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/20_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/22_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/22_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/24_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/24_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/25_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/25_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/26_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/26_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/28_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/28_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/30_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/30_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/32_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/32_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/34_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/34_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/35_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/35_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/36_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/36_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/38_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/38_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/40_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/40_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/42_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/42_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/44_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/44_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/45_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/45_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/46_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/46_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/48_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/48_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/50_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/50_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/52_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/52_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/54_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/54_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/55_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/55_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/56_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/56_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/58_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/58_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/60_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/60_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/62_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/62_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/64_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/64_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/65_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/65_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/66_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/66_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/68_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/68_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/70_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/70_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/72_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/72_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/74_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/74_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/75_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/75_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/76_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/76_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/78_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/78_red_overlay.png -------------------------------------------------------------------------------- /graphics/overlay/80_red_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Afforess/Misanthrope/HEAD/graphics/overlay/80_red_overlay.png -------------------------------------------------------------------------------- /migrations/Misanthrope_0.0.6.lua: -------------------------------------------------------------------------------- 1 | for i, force in pairs(game.forces) do 2 | force.reset_technologies() 3 | force.reset_recipes() 4 | end 5 | -------------------------------------------------------------------------------- /migrations/Misanthrope_0.2.0.lua: -------------------------------------------------------------------------------- 1 | for i, force in pairs(game.forces) do 2 | force.reset_technologies() 3 | force.reset_recipes() 4 | end 5 | -------------------------------------------------------------------------------- /migrations/Misanthrope_0.2.5.lua: -------------------------------------------------------------------------------- 1 | for i, force in pairs(game.forces) do 2 | force.reset_technologies() 3 | force.reset_recipes() 4 | end 5 | -------------------------------------------------------------------------------- /prototypes/style/frame.lua: -------------------------------------------------------------------------------- 1 | data:extend({ 2 | misanthrope_wide_naked_frame_style = 3 | { 4 | type = "frame_style", 5 | parent = "naked_frame_style", 6 | minimal_width = 256, 7 | maximal_width = 256 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /data.lua: -------------------------------------------------------------------------------- 1 | require("prototypes.equipment.equipment") 2 | require("prototypes.item.emitter") 3 | require("prototypes.entity.emitter") 4 | require("prototypes.entity.spawner") 5 | require("prototypes.recipes.emitter") 6 | require("prototypes.technology.alien_defense") 7 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - sudo apt-get update; sudo apt-get install lua5.2 4 | test: 5 | override: 6 | - make 7 | 8 | 9 | deployment: 10 | release-candidate: 11 | branch: master 12 | commands: 13 | - mv build/*.zip $CIRCLE_ARTIFACTS 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Repository for the Factorio Misanthrope mod. 2 | 3 | Description 4 | =========== 5 | Alters biter expansion mechanics, adds vastly improved biter AI behavior 6 | 7 | Mod compatibility 8 | ================= 9 | No known incompatibilities 10 | 11 | Contributing 12 | ============ 13 | Contributions are welcome. 14 | -------------------------------------------------------------------------------- /remote.lua: -------------------------------------------------------------------------------- 1 | require 'libs/developer_mode' 2 | 3 | remote.add_interface("misanthrope", { 4 | developer_mode = function() 5 | for _, player in pairs(game.players) do 6 | if player.valid and player.connected then 7 | DeveloperMode.setup(player) 8 | end 9 | end 10 | end 11 | }) 12 | -------------------------------------------------------------------------------- /libs/biter/ai/no_op.lua: -------------------------------------------------------------------------------- 1 | 2 | local NoOp = {} 3 | 4 | function NoOp.tick(base, data) 5 | return true 6 | end 7 | 8 | function NoOp.is_expired(base, data) 9 | return game.tick > data.end_tick 10 | end 11 | 12 | function NoOp.initialize(base, data) 13 | data.end_tick = game.tick + Time.MINUTE * 2 14 | end 15 | 16 | return NoOp 17 | -------------------------------------------------------------------------------- /libs/biter/ai/donate_currency.lua: -------------------------------------------------------------------------------- 1 | require 'libs/pathfinding_engine' 2 | 3 | local DonateCurrency = {} 4 | 5 | function DonateCurrency.tick(base, data) 6 | if global.overmind then 7 | global.overmind.currency = global.overmind.currency + 50 + (50 * #base:all_hives()) 8 | end 9 | 10 | return false 11 | end 12 | 13 | return DonateCurrency 14 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Misanthrope", 3 | "version": "{{VERSION}}", 4 | "title": "Misanthrope", 5 | "author": "Afforess", 6 | "contact": "afforess@gmail.com", 7 | "homepage": "https://github.com/Afforess/Misanthrope", 8 | "description": "Vastly improved biter AI behavior, tweaks biter expansion mechanics. Memento mori.", 9 | "factorio_version": "0.14", 10 | "dependencies": ["? EvoGUI", "? marathon"] 11 | } 12 | -------------------------------------------------------------------------------- /stdlib/core.lua: -------------------------------------------------------------------------------- 1 | --- Core module 2 | -- @module Core 3 | 4 | --- Errors if the variable evaluates to false, with an optional msg 5 | -- @param var variable to evaluate 6 | -- @param msg (optional) message 7 | function fail_if_missing(var, msg) 8 | if not var then 9 | if msg then 10 | error(msg, 3) 11 | else 12 | error("Missing value", 3) 13 | end 14 | end 15 | return false 16 | end 17 | -------------------------------------------------------------------------------- /libs/biter/ai/save_currency.lua: -------------------------------------------------------------------------------- 1 | require 'libs/pathfinding_engine' 2 | 3 | local SaveCurrency = {} 4 | 5 | function SaveCurrency.tick(base, data) 6 | local save_amt = math.floor(base:get_currency(false) / 4) 7 | if save_amt > 0 then 8 | base.currency.amt = base.currency.amt - save_amt 9 | base.currency.savings = base.currency.savings + save_amt 10 | end 11 | 12 | return false 13 | end 14 | 15 | return SaveCurrency 16 | -------------------------------------------------------------------------------- /stdlib/time.lua: -------------------------------------------------------------------------------- 1 | --- Time module 2 | -- @module Time 3 | 4 | Time = {} 5 | 6 | --- @field the number of factorio ticks in a second 7 | Time.SECOND = 60 8 | 9 | --- @field the number of factorio ticks in a minute 10 | Time.MINUTE = Time.SECOND * 60 11 | 12 | --- @field the number of factorio ticks in an hour 13 | Time.HOUR = Time.MINUTE * 60 14 | 15 | --- @field the number of factorio ticks in a day 16 | Time.DAY = Time.MINUTE * 60 17 | 18 | --- @field the number of factorio ticks in a week 19 | Time.WEEK = Time.DAY * 7 20 | -------------------------------------------------------------------------------- /prototypes/equipment/equipment.lua: -------------------------------------------------------------------------------- 1 | data:extend( 2 | { 3 | { 4 | type = "movement-bonus-equipment", 5 | name = "micro-biter-emitter", 6 | sprite = 7 | { 8 | filename = "__Misanthrope__/graphics/micro-emitter-icon.png", 9 | width = 32, 10 | height = 32, 11 | priority = "medium" 12 | }, 13 | shape = 14 | { 15 | width = 1, 16 | height = 1, 17 | type = "full" 18 | }, 19 | energy_source = 20 | { 21 | type = "electric", 22 | usage_priority = "secondary-input" 23 | }, 24 | energy_consumption = "50kW", 25 | movement_bonus = 0, 26 | categories = {"armor"} 27 | } 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /prototypes/entity/spawner.lua: -------------------------------------------------------------------------------- 1 | data:extend({ 2 | { 3 | type = "tree", 4 | name = "spawner-damaged", 5 | icon = "__Misanthrope__/graphics/null.png", 6 | flags = {"placeable-neutral", "not-on-map", "placeable-off-grid"}, 7 | subgroup = "remnants", 8 | order = "a[remnants]", 9 | max_health = 1, 10 | selection_box = {{-0.0, -0.0}, {0.0, 0.0}}, 11 | collision_box = {{-0.0, -0.0}, {0.0, 0.0}}, 12 | collision_mask = {"object-layer"}, 13 | pictures = 14 | { 15 | { 16 | filename = "__Misanthrope__/graphics/null.png", 17 | width = 32, 18 | height = 32, 19 | } 20 | } 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /prototypes/item/emitter.lua: -------------------------------------------------------------------------------- 1 | data:extend({ 2 | { 3 | type = "item", 4 | name = "biter-emitter", 5 | icon = "__Misanthrope__/graphics/emitter-icon.png", 6 | flags = {"goes-to-quickbar"}, 7 | subgroup = "defensive-structure", 8 | order = "c-c", 9 | place_result = "biter-emitter", 10 | enable = false, 11 | stack_size = 4 12 | }, 13 | { 14 | type = "item", 15 | name = "micro-biter-emitter", 16 | icon = "__Misanthrope__/graphics/micro-emitter-icon.png", 17 | placed_as_equipment_result = "micro-biter-emitter", 18 | flags = {"goes-to-main-inventory"}, 19 | subgroup = "equipment", 20 | order = "e[robotics]-a[micro-biter-emitter]", 21 | enable = false, 22 | stack_size = 1 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Afforess 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /prototypes/recipes/emitter.lua: -------------------------------------------------------------------------------- 1 | data:extend( 2 | { 3 | { 4 | type = "recipe", 5 | name = "biter-emitter", 6 | energy_required = 6, 7 | enabled = false, 8 | ingredients = 9 | { 10 | {"iron-gear-wheel", 5}, 11 | {"steel-plate", 10}, 12 | {"advanced-circuit", 4}, 13 | }, 14 | result = "biter-emitter" 15 | }, 16 | { 17 | type = "recipe", 18 | name = "micro-biter-emitter", 19 | energy_required = 2, 20 | enabled = false, 21 | ingredients = 22 | { 23 | {"biter-emitter", 1}, 24 | {"processing-unit", 10}, 25 | {"steel-plate", 10}, 26 | }, 27 | result = "micro-biter-emitter" 28 | } 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /stdlib/entity/inventory.lua: -------------------------------------------------------------------------------- 1 | --- Inventory module 2 | -- @module Inventory 3 | 4 | Inventory = {} 5 | 6 | require 'stdlib/core' 7 | 8 | --- Copies an inventory contents to a destination inventory 9 | -- @param src source inventory to copy from 10 | -- @param dest destination inventory, to copy to 11 | -- @return an array of SimpleItemStacks of left over items that could not be copied. 12 | function Inventory.copy_inventory(src, dest) 13 | fail_if_missing(src, "missing source inventory") 14 | fail_if_missing(dest, "missing destination inventory") 15 | 16 | local contents = src.get_contents() 17 | local left_over = {} 18 | for n, c in pairs(contents) do 19 | local inserted = dest.insert({name=n, count=c}) 20 | local amt_not_inserted = c - inserted 21 | if amt_not_inserted > 0 then 22 | table.insert(left_over, { name = n, count = amt_not_inserted }) 23 | end 24 | end 25 | return left_over 26 | end 27 | 28 | return Inventory 29 | -------------------------------------------------------------------------------- /libs/biter/ai/abandon_hive.lua: -------------------------------------------------------------------------------- 1 | 2 | local AbandonHive = {stages = {}} 3 | local Log = function(str, ...) BiterBase.LogAI("[AbandonHive] " .. str, ...) end 4 | 5 | function AbandonHive.tick(base, data) 6 | if global.overmind then 7 | global.overmind.currency = global.overmind.currency + 15000 + (10000 * #base:all_hives()) 8 | end 9 | for i = #base.hives, 1, -1 do 10 | if base.hives[i] and base.hives[i].valid then 11 | base.hives[i].destroy() 12 | end 13 | end 14 | base.hives = {} 15 | for i = #base.worms, 1, -1 do 16 | if base.worms[i] and base.worms[i].valid then 17 | base.worms[i].destroy() 18 | end 19 | end 20 | base.worms = {} 21 | if base.queen.valid then 22 | base.queen.destroy() 23 | end 24 | if game.evolution_factor < 0.8 then 25 | game.evolution_factor = math.min(1, game.evolution_factor + 0.0001) 26 | end 27 | base.valid = false 28 | 29 | return true 30 | end 31 | 32 | return AbandonHive 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME := Misanthrope 2 | VERSION_STRING := 0.7.1 3 | 4 | OUTPUT_NAME := $(PACKAGE_NAME)_$(VERSION_STRING) 5 | OUTPUT_DIR := build/$(OUTPUT_NAME) 6 | 7 | PKG_COPY := $(wildcard *.md) graphics locale 8 | 9 | SED_FILES := $(shell find . -iname '*.json' -type f -not -path "./build/*") $(shell find . -iname '*.lua' -type f -not -path "./build/*") 10 | OUT_FILES := $(SED_FILES:%=$(OUTPUT_DIR)/%) 11 | 12 | SED_EXPRS := -e 's/{{MOD_NAME}}/$(PACKAGE_NAME)/g' 13 | SED_EXPRS += -e 's/{{VERSION}}/$(VERSION_STRING)/g' 14 | 15 | all: clean package install_mod 16 | 17 | package-copy: $(PKG_DIRS) $(PKG_FILES) 18 | mkdir -p $(OUTPUT_DIR) 19 | ifneq ($(PKG_COPY),) 20 | cp -r $(PKG_COPY) build/$(OUTPUT_NAME) 21 | endif 22 | 23 | $(OUTPUT_DIR)/%.lua: %.lua 24 | @mkdir -p $(@D) 25 | @sed $(SED_EXPRS) $< > $@ 26 | luac -p $@ 27 | 28 | $(OUTPUT_DIR)/%: % 29 | mkdir -p $(@D) 30 | sed $(SED_EXPRS) $< > $@ 31 | 32 | package: package-copy $(OUT_FILES) 33 | cd build && zip -r $(OUTPUT_NAME).zip $(OUTPUT_NAME) 34 | 35 | clean: 36 | rm -rf build/ 37 | 38 | install_mod: 39 | if [ -L factorio_mods ] ; \ 40 | then \ 41 | cp -R build/$(OUTPUT_NAME) factorio_mods ; \ 42 | fi; 43 | -------------------------------------------------------------------------------- /libs/biter/ai/alert.lua: -------------------------------------------------------------------------------- 1 | 2 | local Alert = {} 3 | local Log = function(str, base, ...) BiterBase.Logger.log(string.format("[Alert] - (" .. base.name .. "): " .. str, ...)) end 4 | 5 | function Alert.tick(base, data) 6 | if #base:get_entities() < 15 then 7 | local surface = base.queen.surface 8 | 9 | local biters = base:get_prev_entities() 10 | for _, hive in pairs(base:all_hives()) do 11 | table.insert(biters, Biters.spawn_biter(base, surface, hive)) 12 | end 13 | 14 | if #biters > 0 then 15 | local unit_group = BiterBase.create_unit_group(base, {position = biters[1].position, force = 'enemy'}) 16 | for _, biter in pairs(biters) do 17 | unit_group.add_member(biter) 18 | end 19 | -- normal wander behavior 20 | unit_group.set_command({type = defines.command.attack_area, destination = base.queen.position, radius = 16}) 21 | unit_group.start_moving() 22 | end 23 | end 24 | return true 25 | end 26 | 27 | function Alert.is_expired(base, data) 28 | return data.alerted_at + Time.MINUTE * 5 < game.tick 29 | end 30 | 31 | return Alert 32 | -------------------------------------------------------------------------------- /libs/region/biter_scents.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | require 'stdlib/area/position' 3 | require 'stdlib/area/area' 4 | require 'stdlib/area/tile' 5 | 6 | -- Event.register(defines.events.on_entity_died, function(event) 7 | -- local entity = event.entity 8 | -- local max_health = entity.prototype.max_health 9 | -- local position = Tile.from_position(entity.position) 10 | -- local surface = entity.surface 11 | -- 12 | -- local radius = math.floor(math.min(32, math.max(5, math.sqrt(max_health)))) 13 | -- local area = Position.expand_to_area(position, radius) 14 | -- for x, y in Area.iterate(area) do 15 | -- local pos = {x = x, y = y} 16 | -- local dist_squared = Position.distance_squared(position, pos) 17 | -- local delta = math.min(max_health, math.floor(max_health / math.pow(dist_squared, 0.25))) 18 | -- if delta > 1 then 19 | -- local data = Tile.get_data(surface, pos, {}) 20 | -- if not data.scent then 21 | -- data.scent = delta 22 | -- else 23 | -- data.scent = data.scent + delta 24 | -- end 25 | -- end 26 | -- end 27 | -- end) 28 | -------------------------------------------------------------------------------- /control.lua: -------------------------------------------------------------------------------- 1 | DEBUG_MODE = false 2 | UNIT_GROUP_EVENT_ID = script.generate_event_name() 3 | local wrapper = function(wrap) 4 | return function(msg, options) 5 | if not options or not options.comments then 6 | if options then 7 | options.comment = false 8 | return wrap(msg, options) 9 | else 10 | return wrap(msg, {comment = false}) 11 | end 12 | end 13 | return wrap(msg, options) 14 | end 15 | end 16 | 17 | if DEBUG_MODE then 18 | string.line = wrapper(serpent.line) 19 | string.block = wrapper(serpent.line) 20 | string.dump = wrapper(serpent.line) 21 | else 22 | string.line = function() return 'debug mode disabled' end 23 | string.block = function() return 'debug mode disabled' end 24 | string.dump = function() return 'debug mode disabled' end 25 | end 26 | 27 | require 'stdlib/log/logger' 28 | require 'stdlib/time' 29 | require 'stdlib/area/chunk' 30 | require 'remote' 31 | require 'libs/EvoGUI' 32 | require 'libs/world' 33 | require 'libs/map_settings' 34 | require 'libs/harpa' 35 | -- require 'libs/region/biter_scents' 36 | -- require 'libs/region/chunk_value' 37 | 38 | LOGGER = Logger.new("Misanthrope", "main", DEBUG_MODE) 39 | 40 | function Chunk.to_string(chunk_pos) 41 | return "{x = " .. chunk_pos.x .. ", y = " .. chunk_pos.y .. "}" 42 | end 43 | -------------------------------------------------------------------------------- /libs/region/region_coords.lua: -------------------------------------------------------------------------------- 1 | region_coords = {} 2 | region_coords.__index = region_coords 3 | 4 | function region_coords.get_chunk_x(region_data) 5 | if region_data.x < 0 then 6 | return bit32.lshift(region_data.x, 2) - MAX_UINT 7 | end 8 | return bit32.lshift(region_data.x, 2) 9 | end 10 | 11 | function region_coords.get_chunk_y(region_data) 12 | if region_data.y < 0 then 13 | return bit32.lshift(region_data.y, 2) - MAX_UINT 14 | end 15 | return bit32.lshift(region_data.y, 2) 16 | end 17 | 18 | function region_coords.get_lower_pos_x(region_data) 19 | if region_data.x < 0 then 20 | return bit32.lshift(region_data.x, 7) - MAX_UINT 21 | end 22 | return bit32.lshift(region_data.x, 7) 23 | end 24 | 25 | function region_coords.get_lower_pos_y(region_data) 26 | if region_data.y < 0 then 27 | return bit32.lshift(region_data.y, 7) - MAX_UINT 28 | end 29 | return bit32.lshift(region_data.y, 7) 30 | end 31 | 32 | function region_coords.get_upper_pos_x(region_data) 33 | if (1 + region_data.x) < 0 then 34 | return bit32.lshift(1 + region_data.x, 7) - MAX_UINT 35 | end 36 | return bit32.lshift(1 + region_data.x, 7) 37 | end 38 | 39 | function region_coords.get_upper_pos_y(region_data) 40 | if (1 + region_data.y) < 0 then 41 | return bit32.lshift(1 + region_data.y, 7) - MAX_UINT 42 | end 43 | return bit32.lshift(1 + region_data.y, 7) 44 | end 45 | -------------------------------------------------------------------------------- /libs/pathfinding_engine.lua: -------------------------------------------------------------------------------- 1 | require 'libs/pathfinder' 2 | 3 | PathfindingEngine = {} 4 | 5 | function PathfindingEngine.request_path(surface, start_pos, end_pos, max_iterations) 6 | if not global.pathfinding_count then global.pathfinding_count = 0 end 7 | if not global.pathfinding_requests then global.pathfinding_requests = {} end 8 | 9 | local request = pathfinder.partial_a_star(surface, start_pos, end_pos, 1, max_iterations) 10 | global.pathfinding_count = global.pathfinding_count + 1 11 | global.pathfinding_requests[global.pathfinding_count] = request 12 | 13 | return global.pathfinding_count 14 | end 15 | 16 | function PathfindingEngine.is_path_complete(request_id) 17 | return global.pathfinding_requests[request_id].completed 18 | end 19 | 20 | function PathfindingEngine.retreive_path(request_id) 21 | local result = global.pathfinding_requests[request_id] 22 | global.pathfinding_requests[request_id] = nil 23 | 24 | return result 25 | end 26 | 27 | Event.register(defines.events.on_tick, function(event) 28 | if global.pathfinding_requests and event.tick % 3 == 0 then 29 | local iterations = 0 30 | for i, request in pairs(global.pathfinding_requests) do 31 | if not request.completed then 32 | global.pathfinding_requests[i] = pathfinder.resume_a_star(request, 3) 33 | iterations = iterations + 1 34 | if iterations >= 5 then 35 | break 36 | end 37 | end 38 | end 39 | end 40 | end) 41 | -------------------------------------------------------------------------------- /prototypes/technology/alien_defense.lua: -------------------------------------------------------------------------------- 1 | data:extend( 2 | { 3 | { 4 | type = "technology", 5 | name = "alien_defense", 6 | icon = "__Misanthrope__/graphics/biohazard.png", 7 | icon_size = 128, 8 | prerequisites = {"military-3", "turrets"}, 9 | effects = 10 | { 11 | { 12 | type = "unlock-recipe", 13 | recipe = "biter-emitter" 14 | } 15 | }, 16 | unit = 17 | { 18 | count = 100, 19 | ingredients = 20 | { 21 | {"science-pack-1", 2}, 22 | {"science-pack-2", 1}, 23 | {"science-pack-3", 1} 24 | }, 25 | time = 50 26 | }, 27 | upgrade = true, 28 | order = "e-l-a" 29 | }, 30 | { 31 | type = "technology", 32 | name = "alien_defense-2", 33 | icon = "__Misanthrope__/graphics/biohazard.png", 34 | icon_size = 128, 35 | prerequisites = {"alien_defense"}, 36 | effects = 37 | { 38 | { 39 | type = "unlock-recipe", 40 | recipe = "micro-biter-emitter" 41 | } 42 | }, 43 | unit = 44 | { 45 | count = 50, 46 | ingredients = 47 | { 48 | {"science-pack-1", 4}, 49 | {"science-pack-2", 4}, 50 | {"science-pack-3", 2}, 51 | {"alien-science-pack", 1} 52 | }, 53 | time = 50 54 | }, 55 | upgrade = true, 56 | order = "e-l-a" 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /libs/biter/biter.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/string' 2 | 3 | Biters = {} 4 | 5 | function Biters.spawn_biter(base, surface, spawner) 6 | if spawner and spawner.valid then 7 | for _, unit_name in pairs(Biters.valid_units(spawner)) do 8 | local odds = 100 * Biters.unit_odds(unit_name) 9 | if odds > 0 and odds > math.random(100) then 10 | local spawn_pos = surface.find_non_colliding_position(unit_name, spawner.position, 6, 0.5) 11 | if spawn_pos then 12 | return BiterBase.create_entity(base, surface, {name = unit_name, position = spawn_pos, force = spawner.force}) 13 | end 14 | end 15 | end 16 | end 17 | return nil 18 | end 19 | 20 | function Biters.unit_odds(name) 21 | local evo_factor = game.evolution_factor 22 | if name:contains('behemoth') and evo_factor > 0.7 then 23 | return (evo_factor - 0.7) * 2 24 | end 25 | if name:contains('big') and evo_factor > 0.4 then 26 | return (evo_factor - 0.4) * 1.3 27 | end 28 | if name:contains('medium') and evo_factor > 0.25 then 29 | return math.min(0.5, evo_factor - 0.25) 30 | end 31 | if name == 'small-spitter' and evo_factor > 0.15 then 32 | return 0.75 33 | end 34 | if name == 'small-biter' then 35 | return 1 36 | end 37 | return 0 38 | end 39 | 40 | function Biters.valid_units(spawner) 41 | if spawner.name == 'spitter-spawner' then 42 | return {'behemoth-spitter', 'big-spitter', 'medium-spitter', 'small-spitter', 'small-biter'} 43 | end 44 | return {'behemoth-biter', 'big-biter', 'medium-biter', 'small-biter'} 45 | end 46 | -------------------------------------------------------------------------------- /libs/biter_targets.lua: -------------------------------------------------------------------------------- 1 | require 'libs/biter/biter' 2 | 3 | Biters.targets = {} 4 | 5 | table.insert(Biters.targets, {name = "big-electric-pole", value = 300, min_evolution = 0.9}) 6 | table.insert(Biters.targets, {name = "medium-electric-pole", value = 100, min_evolution = 0.7}) 7 | table.insert(Biters.targets, {name = "small-electric-pole", value = 60, min_evolution = 0.5}) 8 | 9 | table.insert(Biters.targets, {type = "roboport", value = 500, min_evolution = 0}) 10 | table.insert(Biters.targets, {type = "radar", value = 500, min_evolution = 0}) 11 | table.insert(Biters.targets, {type = "pipe", value = 10, min_evolution = 0}) 12 | table.insert(Biters.targets, {name = "pipe-to-ground", value = 50, min_evolution = 0}) 13 | 14 | table.insert(Biters.targets, {type = "transport-belt", value = 20, min_evolution = 0}) 15 | table.insert(Biters.targets, {type = "offshore-pump", value = 20, min_evolution = 0}) 16 | table.insert(Biters.targets, {type = "storage-tank", value = 20, min_evolution = 0}) 17 | 18 | table.insert(Biters.targets, {type = "solar-panel", value = 100, min_evolution = 0.5}) 19 | table.insert(Biters.targets, {type = "boiler", value = 25, min_evolution = 0.3}) 20 | 21 | function Biters.entity_value(entity) 22 | local entity_name = entity.name 23 | local entity_type = entity.type 24 | local evo_factor = game.evolution_factor 25 | for i = 1, #Biters.targets do 26 | local target_data = Biters.targets[i] 27 | if evo_factor > target_data.min_evolution then 28 | if target_data.name == entity_name then 29 | return target_data.value 30 | elseif target_data.type == entity_type then 31 | return target_data.value 32 | end 33 | end 34 | end 35 | return -1 36 | end 37 | -------------------------------------------------------------------------------- /libs/biter/ai/assist_ally.lua: -------------------------------------------------------------------------------- 1 | 2 | local AssistAlly = {} 3 | local Log = function(str, ...) BiterBase.LogAI("[AssistAlly] " .. str, ...) end 4 | 5 | function AssistAlly.tick(base, data) 6 | if #base:get_entities() < 75 then 7 | local surface = base.queen.surface 8 | 9 | local biters = base:get_prev_entities() 10 | for _, hive in pairs(base:all_hives()) do 11 | table.insert(biters, Biters.spawn_biter(base, surface, hive)) 12 | end 13 | 14 | if #biters > 0 then 15 | local closest_player = World.closest_player_character(surface, data.ally_base.queen.position, 56) 16 | if closest_player then 17 | data.idle_units = table.filter(data.idle_units, Game.VALID_FILTER) 18 | table.each(data.idle_units, function(biter) 19 | biter.set_command({type = defines.command.attack, target = closest_player}) 20 | end) 21 | data.idle_units = {} 22 | for _, biter in pairs(biters) do 23 | biter.set_command({type = defines.command.attack, target = closest_player}) 24 | end 25 | else 26 | for _, biter in pairs(biters) do 27 | biter.set_command({type = defines.command.attack_area, destination = data.ally_base.queen.position, radius = 25}) 28 | table.insert(data.idle_units, biter) 29 | end 30 | end 31 | end 32 | end 33 | return true 34 | end 35 | 36 | function AssistAlly.initialize(base, data) 37 | data.idle_units = {} 38 | end 39 | 40 | function AssistAlly.is_expired(base, data) 41 | return not data.ally_base.valid or not data.ally_base.queen.valid or data.ally_base.last_attacked + (Time.MINUTE * 3) < game.tick 42 | end 43 | 44 | return AssistAlly 45 | -------------------------------------------------------------------------------- /libs/biter/ai/attacked_recently.lua: -------------------------------------------------------------------------------- 1 | 2 | local AttackedRecently = {} 3 | local Log = function(str, ...) BiterBase.LogAI("[AttackedRecently] " .. str, ...) end 4 | 5 | function AttackedRecently.tick(base, data) 6 | if #base:get_entities() < 30 then 7 | local surface = base.queen.surface 8 | 9 | local biters = base:get_prev_entities() 10 | for _, hive in pairs(base:all_hives()) do 11 | table.insert(biters, Biters.spawn_biter(base, surface, hive)) 12 | end 13 | 14 | if #biters > 0 then 15 | local closest_player = World.closest_player_character(surface, base.queen.position, 56) 16 | local unit_group = BiterBase.create_unit_group(base, {position = biters[1].position, force = 'enemy'}) 17 | for _, biter in pairs(biters) do 18 | unit_group.add_member(biter) 19 | end 20 | if closest_player then 21 | unit_group.set_command({type = defines.command.attack, target = closest_player}) 22 | if data.idle_unit_groups then 23 | table.each(table.filter(data.idle_unit_groups, Game.VALID_FILTER), function(unit_group) 24 | unit_group.set_command({type = defines.command.attack, target = closest_player}) 25 | end) 26 | data.idle_unit_groups = nil 27 | end 28 | else 29 | unit_group.set_command({type = defines.command.attack_area, destination = base.queen.position, radius = 20}) 30 | if not data.idle_unit_groups then data.idle_unit_groups = {} end 31 | table.insert(data.idle_unit_groups, unit_group) 32 | end 33 | unit_group.start_moving() 34 | end 35 | end 36 | return true 37 | end 38 | 39 | function AttackedRecently.is_expired(base, data) 40 | return base.last_attacked + (Time.MINUTE * 3) < game.tick 41 | end 42 | 43 | return AttackedRecently 44 | -------------------------------------------------------------------------------- /stdlib/game.lua: -------------------------------------------------------------------------------- 1 | --- Game module 2 | -- @module Game 3 | 4 | Game = {} 5 | Game.VALID_FILTER = function(v) 6 | return v.valid 7 | end 8 | 9 | --- Messages all players currently connected to the game 10 | -- @param msg message to send to players 11 | -- @param condition (optional) optional condition to be true for the player to be messaged 12 | -- @return the number of players who received the message 13 | function Game.print_all(msg, condition) 14 | local num = 0 15 | for _, player in pairs(game.players) do 16 | if player.valid and player.connected then 17 | if condition == nil or select(2, pcall(condition, player)) then 18 | player.print(msg) 19 | num = num + 1 20 | end 21 | end 22 | end 23 | return num 24 | end 25 | 26 | --- Messages all players with the given force connected to the game 27 | -- @param force (may be force name string, or force object) the players with the given force to message 28 | -- @param msg message to send to players 29 | -- @return the number of players who received the message 30 | function Game.print_force(force, msg) 31 | local force_name 32 | if type(force) == "string" then 33 | force_name = force 34 | else 35 | force_name = force.name 36 | end 37 | return Game.print_all(msg, function(player) 38 | return player.force.name == force_name 39 | end) 40 | end 41 | 42 | --- Messages all players with the given surface connected to the game 43 | -- @param surface the players with the given surface to message 44 | -- @param msg message to send to players 45 | -- @return the number of players who received the message 46 | function Game.print_surface(surface, msg) 47 | local surface_name 48 | if type(surface) == "string" then 49 | surface_name = surface 50 | else 51 | surface_name = surface.name 52 | end 53 | return Game.print_all(msg, function(player) 54 | return player.surface.name == surface_name 55 | end) 56 | end 57 | 58 | return Game 59 | -------------------------------------------------------------------------------- /libs/biter/ai/grow_hive.lua: -------------------------------------------------------------------------------- 1 | 2 | local GrowHive = {stages = {}} 3 | local Log = function(str, ...) BiterBase.LogAI("[GrowHive] " .. str, ...) end 4 | 5 | GrowHive.stages.clear_trees = function(base, data) 6 | local surface = base.queen.surface 7 | local pos = base.queen.position 8 | table.each(surface.find_entities_filtered({area = Position.expand_to_area(pos, data.search_distance), type = 'tree'}), function(entity) 9 | entity.destroy() 10 | end) 11 | return 'build_hive' 12 | end 13 | 14 | GrowHive.stages.build_hive = function(base, data) 15 | local surface = base.queen.surface 16 | local pos = base.queen.position 17 | local entity_pos = surface.find_non_colliding_position(data.hive_type, pos, data.search_distance, 0.5) 18 | if entity_pos and Position.distance(pos, entity_pos) <= data.search_distance then 19 | local hive = surface.create_entity({name = data.hive_type, position = entity_pos, direction = math.random(7), force = base.queen.force}) 20 | table.insert(base.hives, hive) 21 | Log("Successfully spawned a new hive at %s", base, string.line(hive.position)) 22 | game.evolution_factor = math.max(0, game.evolution_factor - 0.0001) 23 | return 'success' 24 | end 25 | 26 | data.search_distance = data.search_distance + 1 27 | return 'clear_trees' 28 | end 29 | 30 | function GrowHive.tick(base, data) 31 | if not data.stage then 32 | data.stage = 'clear_trees' 33 | end 34 | local prev_stage = data.stage 35 | data.stage = GrowHive.stages[data.stage](base, data) 36 | if prev_stage ~= data.stage then 37 | Log("Updating stage from %s to %s", base, prev_stage, data.stage) 38 | end 39 | return true 40 | end 41 | 42 | function GrowHive.is_expired(base, data) 43 | return data.search_distance > 12 or data.stage == 'success' 44 | end 45 | 46 | function GrowHive.initialize(base, data) 47 | data.search_distance = 3 48 | if math.random(100) > 33 then 49 | data.hive_type = 'biter-spawner' 50 | else 51 | data.hive_type = 'spitter-spawner' 52 | end 53 | end 54 | 55 | return GrowHive 56 | -------------------------------------------------------------------------------- /libs/biter/ai/build_worm.lua: -------------------------------------------------------------------------------- 1 | 2 | local BuildWorm = {stages = {}} 3 | local Log = function(str, ...) BiterBase.LogAI("[BuildWorm] " .. str, ...) end 4 | 5 | BuildWorm.stages.clear_trees = function(base, data) 6 | local surface = base.queen.surface 7 | local pos = base.queen.position 8 | table.each(surface.find_entities_filtered({area = Position.expand_to_area(pos, data.search_distance), type = 'tree'}), function(entity) 9 | entity.destroy() 10 | end) 11 | return 'build_worm' 12 | end 13 | 14 | BuildWorm.stages.build_worm = function(base, data) 15 | local surface = base.queen.surface 16 | local pos = base.queen.position 17 | local entity_pos = surface.find_non_colliding_position(data.worm_type, pos, data.search_distance, 0.5) 18 | if entity_pos and Position.distance(pos, entity_pos) <= data.search_distance then 19 | local worm = surface.create_entity({name = data.worm_type, position = entity_pos, force = base.queen.force}) 20 | table.insert(base.worms, worm) 21 | Log("Successfully spawned a new worm at %s", base, string.line(worm.position)) 22 | game.evolution_factor = math.max(0, game.evolution_factor - 0.00001) 23 | return 'success' 24 | end 25 | 26 | data.search_distance = data.search_distance + 1 27 | return 'clear_trees' 28 | end 29 | 30 | function BuildWorm.tick(base, data) 31 | if not data.stage then 32 | data.stage = 'clear_trees' 33 | end 34 | local prev_stage = data.stage 35 | data.stage = BuildWorm.stages[data.stage](base, data) 36 | if prev_stage ~= data.stage then 37 | Log("Updating stage from %s to %s", base, prev_stage, data.stage) 38 | end 39 | return true 40 | end 41 | 42 | function BuildWorm.is_expired(base, data) 43 | return data.search_distance > 14 or data.stage == 'success' 44 | end 45 | 46 | function BuildWorm.initialize(base, data) 47 | data.search_distance = 2 48 | if game.evolution_factor > 0.66 and math.random(100) > 66 then 49 | data.worm_type = 'big-worm-turret' 50 | elseif game.evolution_factor > 0.4 and math.random(100) > 40 then 51 | data.worm_type = 'medium-worm-turret' 52 | else 53 | data.worm_type = 'small-worm-turret' 54 | end 55 | end 56 | 57 | return BuildWorm 58 | -------------------------------------------------------------------------------- /locale/en/misanthrope.cfg: -------------------------------------------------------------------------------- 1 | gui.misanthrope.harpa = HARPA Logic 2 | gui.misanthrope.debug_logging = Debug Logging 3 | gui.misanthrope.biter_ai = Biter AI 4 | gui.misanthrope.biter_overmind = Overmind AI 5 | gui.misanthrope.player_scent = Player Scent AI 6 | gui.misanthrope.close = Close 7 | gui.misanthrope.disable_developer_mode = Disable Developer Mode 8 | gui.misanthrope.developer_mode = Dev Mode 9 | 10 | [entity-name] 11 | power-short=Power short 12 | biter-emitter=HARPA Emitter 13 | 14 | 80_red_overlay=HARPA Emitter Range 15 | 78_red_overlay=HARPA Emitter Range 16 | 76_red_overlay=HARPA Emitter Range 17 | 74_red_overlay=HARPA Emitter Range 18 | 72_red_overlay=HARPA Emitter Range 19 | 70_red_overlay=HARPA Emitter Range 20 | 68_red_overlay=HARPA Emitter Range 21 | 66_red_overlay=HARPA Emitter Range 22 | 64_red_overlay=HARPA Emitter Range 23 | 62_red_overlay=HARPA Emitter Range 24 | 60_red_overlay=HARPA Emitter Range 25 | 58_red_overlay=HARPA Emitter Range 26 | 56_red_overlay=HARPA Emitter Range 27 | 54_red_overlay=HARPA Emitter Range 28 | 52_red_overlay=HARPA Emitter Range 29 | 50_red_overlay=HARPA Emitter Range 30 | 48_red_overlay=HARPA Emitter Range 31 | 46_red_overlay=HARPA Emitter Range 32 | 44_red_overlay=HARPA Emitter Range 33 | 42_red_overlay=HARPA Emitter Range 34 | 40_red_overlay=HARPA Emitter Range 35 | 38_red_overlay=HARPA Emitter Range 36 | 36_red_overlay=HARPA Emitter Range 37 | 34_red_overlay=HARPA Emitter Range 38 | 32_red_overlay=HARPA Emitter Range 39 | 30_red_overlay=HARPA Emitter Range 40 | 28_red_overlay=HARPA Emitter Range 41 | 26_red_overlay=HARPA Emitter Range 42 | 24_red_overlay=HARPA Emitter Range 43 | 22_red_overlay=HARPA Emitter Range 44 | 20_red_overlay=HARPA Emitter Range 45 | 46 | [item-name] 47 | biter-emitter=HARPA Emitter 48 | micro-biter-emitter=Micro HARPA 49 | 50 | [equipment-name] 51 | micro-biter-emitter=Micro HARPA 52 | 53 | [item-description] 54 | biter-emitter=High Amplitude Radio Pulse Antenna. Discourages biters from a 60x60 area. Prevents biter bases in area. 55 | micro-biter-emitter=Micro High Amplitude Radio Pulse Antenna. 16x16 range. Can be inserted into armor. 56 | 57 | [technology-name] 58 | alien_defense=Alien defense 59 | 60 | [technology-description] 61 | alien_defense=Advanced defensive capabilities to reduce the threat from biters. 62 | -------------------------------------------------------------------------------- /prototypes/entity/emitter.lua: -------------------------------------------------------------------------------- 1 | function color_overlay(color_name, opacity) 2 | return { 3 | type = "container", 4 | name = opacity .. "_" .. color_name .."_overlay", 5 | flags = {"placeable-neutral", "player-creation", "not-repairable"}, 6 | icon = "__Misanthrope__/graphics/overlay/" .. opacity .. "_" .. color_name .. "_overlay.png", 7 | max_health = 1, 8 | order = 'z', 9 | collision_mask = {"resource-layer"}, 10 | collision_box = {{-0.35, -0.35}, {0.35, 0.35}}, 11 | selection_box = {{-0.5, -0.5}, {0.5, 0.5}}, 12 | inventory_size = 1, 13 | picture = 14 | { 15 | filename = "__Misanthrope__/graphics/overlay/" .. opacity .. "_" .. color_name .. "_overlay.png", 16 | priority = "extra-high", 17 | width = 32, 18 | height = 32, 19 | shift = {0.0, 0.0} 20 | } 21 | } 22 | end 23 | 24 | local overlays = {} 25 | for i = 20, 80, 2 do 26 | table.insert(overlays, color_overlay("red", i)) 27 | end 28 | data:extend(overlays) 29 | 30 | data:extend({ 31 | { 32 | type = "radar", 33 | name = "biter-emitter", 34 | icon = "__Misanthrope__/graphics/emitter-icon.png", 35 | flags = {"placeable-neutral", "placeable-player", "player-creation"}, 36 | minable = {hardness = 0.5, mining_time = 0.5, result = "biter-emitter"}, 37 | max_health = 250, 38 | corpse = "small-remnants", 39 | dying_explosion = "medium-explosion", 40 | collision_box = {{-0.29, -0.29}, {0.29, 0.29}}, 41 | selection_box = {{-0.5, -0.5}, {0.5, 0.5}}, 42 | energy_per_sector = "30MJ", 43 | max_distance_of_nearby_sector_revealed = 1, 44 | max_distance_of_sector_revealed = 1, 45 | energy_per_nearby_scan = "250kJ", 46 | energy_source = 47 | { 48 | type = "electric", 49 | usage_priority = "secondary-input" 50 | }, 51 | energy_usage = "500kW", 52 | pictures = 53 | { 54 | filename = "__Misanthrope__/graphics/emitter.png", 55 | priority = "high", 56 | width = 60, 57 | height = 60, 58 | axially_symmetrical = false, 59 | apply_projection = false, 60 | direction_count = 25, 61 | line_length = 5, 62 | shift = {0.425, -0.5}, 63 | }, 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /stdlib/data/data.lua: -------------------------------------------------------------------------------- 1 | --- Data module 2 | -- @module Data 3 | 4 | require 'stdlib/core' 5 | require 'stdlib/string' 6 | require 'stdlib/table' 7 | 8 | Data = {} 9 | 10 | --- Selects all data values where the key matches the selector pattern. 11 | -- The selector pattern is divided into groups. The pattern should have a colon character `:` to denote the selection for each group. 12 | --
The first group is for the class of the data type (item, recipe, entity-type, etc) 13 | --
The second group is for the name of the data element, and is optional. If missing, all elements matching prior groups are returned. 14 | --

For more granular selectors, see other modules, such as Recipe.select. 15 | -- @usage Data.select('recipe') -- returns a table with all recipes 16 | -- @usage Data.select('recipe:steel.*') -- returns a table with all recipes whose name matches 'steel.*' 17 | -- @param pattern to search with 18 | -- @return table containing the elements matching the selector pattern, or an empty table if there was no matches 19 | function Data.select(pattern) 20 | fail_if_missing(pattern, "missing pattern argument") 21 | 22 | local parts = string.split(pattern, ":") 23 | local category_pattern = table.first(parts) 24 | local results = {} 25 | for category, values in pairs(data.raw) do 26 | if string.match(category, category_pattern) then 27 | local element_pattern = #parts > 1 and parts[2] or '.*' 28 | -- escape the '-' in names 29 | element_pattern = string.gsub(element_pattern, "%-", "%%-") 30 | for element_name, element in pairs(values) do 31 | if string.match(element_name, element_pattern) then 32 | table.insert(results, element) 33 | end 34 | end 35 | end 36 | end 37 | setmetatable(results, Data._select_metatable.new(results)) 38 | return results 39 | end 40 | 41 | -- this metatable is set on recipes, to control access to ingredients and results 42 | Data._select_metatable = {} 43 | Data._select_metatable.new = function(selection) 44 | local self = { } 45 | self.__index = function(tbl, key) 46 | if key == 'apply' then 47 | return function(k, v) 48 | table.each(tbl, function(obj) 49 | obj[k] = v 50 | end) 51 | return tbl 52 | end 53 | end 54 | end 55 | self.__newindex = function(tbl, key, value) 56 | table.each(tbl, function(obj) 57 | obj[key] = value 58 | end) 59 | end 60 | 61 | return self 62 | end 63 | -------------------------------------------------------------------------------- /data-updates.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/string' 2 | 3 | for _, prototype in pairs(data.raw["unit-spawner"]) do 4 | prototype.pollution_absorbtion_absolute = prototype.pollution_absorbtion_absolute / 10 5 | prototype.pollution_absorbtion_absolute = prototype.pollution_absorbtion_proportional / 5 6 | -- prototype.max_count_of_owned_units = 0 7 | -- prototype.max_friends_around_to_spawn = 0 8 | -- prototype.spawning_cooldown = {9999999999,99999999999} 9 | 10 | prototype.max_count_of_owned_units = math.floor(prototype.max_count_of_owned_units * 3 / 2) 11 | prototype.max_friends_around_to_spawn = math.floor(prototype.max_friends_around_to_spawn * 3 / 2) 12 | 13 | prototype.attack_reaction = { 14 | { 15 | range = 50, 16 | action = 17 | { 18 | type = "direct", 19 | action_delivery = 20 | { 21 | type = "instant", 22 | source_effects = 23 | { 24 | { 25 | type = "create-entity", 26 | entity_name = "spawner-damaged", 27 | trigger_created_entity = "true" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | end 35 | 36 | data.raw["unit"]["small-biter"].pollution_to_join_attack = 50 37 | data.raw["unit"]["medium-biter"].pollution_to_join_attack = 150 38 | data.raw["unit"]["big-biter"].pollution_to_join_attack = 300 39 | data.raw["unit"]["behemoth-biter"].pollution_to_join_attack = 2000 40 | data.raw["unit"]["small-spitter"].pollution_to_join_attack = 50 41 | data.raw["unit"]["medium-spitter"].pollution_to_join_attack = 100 42 | data.raw["unit"]["big-spitter"].pollution_to_join_attack = 200 43 | data.raw["unit"]["behemoth-spitter"].pollution_to_join_attack = 1000 44 | 45 | for key, prototype_type in pairs(data.raw) do 46 | for name, prototype in pairs(prototype_type) do 47 | if prototype.energy_source then 48 | if prototype.energy_source.emissions and prototype.energy_source.emissions > 0.001 then 49 | local multiplier = 7 50 | if name:contains('assembling-machine') and not name == 'assembling-machine-1' then 51 | multiplier = 10 52 | end 53 | if marathon then 54 | multiplier = multiplier / 5 55 | end 56 | prototype.energy_source.emissions = prototype.energy_source.emissions * multiplier 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /stdlib/string.lua: -------------------------------------------------------------------------------- 1 | --- String module 2 | -- @module string 3 | 4 | --- Returns a copy of the string with any leading or trailing whitespace from the string removed. 5 | -- @param s the string to remove leading or trailing whitespace from 6 | -- @return a copy of the string without leading or trailing whitespace 7 | function string.trim(s) 8 | return (s:gsub("^%s*(.-)%s*$", "%1")) 9 | end 10 | 11 | --- Tests if a string starts with a given substring 12 | -- @param s the string to check for the start substring 13 | -- @param start the substring to test for 14 | -- @return true if the start substring was found in the string 15 | function string.starts_with(s, start) 16 | return string.find(s, start, 1, true) == 1 17 | end 18 | 19 | --- Tests if a string ends with a given substring 20 | -- @param s the string to check for the end substring 21 | -- @param ends the substring to test for 22 | -- @return true if the end substring was found in the string 23 | function string.ends_with(s, ends) 24 | return #s >= #ends and string.find(s, ends, #s - #ends + 1, true) and true or false 25 | end 26 | 27 | --- Tests if a string contains a given substring 28 | -- @param s the string to check for the substring 29 | -- @param ends the substring to test for 30 | -- @return true if the substring was found in the string 31 | function string.contains(s, ends) 32 | return s and string.find(s, ends) ~= nil 33 | end 34 | 35 | --- Tests whether a string is empty 36 | -- @param s the string to test 37 | function string.is_empty(s) 38 | return s == nil or s == '' 39 | end 40 | 41 | --- Splits a string into a table 42 | --

43 | -- Note: Empty split substrings are not included in the resulting table. 44 | -- For example, string.split('foo.bar...', '.', false) results in the table {'foo', 'bar'} 45 | -- @param s the string to split 46 | -- @param sep (optional) the separator to use. The period character, `.`, is the default separator. 47 | -- @param pattern whether to interpret the separator as a lua pattern or plaintext for the string split 48 | -- @return the table 49 | function string.split(s, sep, pattern) 50 | local sep, fields = sep or ".", {} 51 | sep = sep ~= "" and sep or "." 52 | sep = not pattern and string.gsub(sep, "([^%w])", "%%%1") or sep 53 | 54 | local fields = {} 55 | local start_idx, end_idx = string.find(s, sep) 56 | local last_find = 1 57 | local len = string.len(sep) 58 | while start_idx do 59 | local substr = string.sub(s, last_find, start_idx - 1) 60 | if string.len(substr) > 0 then 61 | table.insert(fields, string.sub(s, last_find, start_idx - 1)) 62 | end 63 | last_find = end_idx + 1 64 | start_idx, end_idx = string.find(s, sep, end_idx + 1) 65 | end 66 | local substr = string.sub(s, last_find) 67 | if string.len(substr) > 0 then 68 | table.insert(fields, string.sub(s, last_find)) 69 | end 70 | return fields 71 | end 72 | -------------------------------------------------------------------------------- /libs/pathfinder_demo.lua: -------------------------------------------------------------------------------- 1 | require 'libs/pathfinder' 2 | 3 | pathfinder_demo = {} 4 | pathfinder_demo.__index = pathfinder_demo 5 | 6 | function pathfinder_demo.tick() 7 | if game.tick % 30 ~= 0 then 8 | return 9 | end 10 | if global.pathfinding_demo then 11 | local demo_data = global.pathfinding_demo 12 | if demo_data.ticks_remaining ~= nil then 13 | LOGGER.log("Counting down demo ticks: " .. demo_data.ticks_remaining) 14 | demo_data.ticks_remaining = demo_data.ticks_remaining - 1 15 | if demo_data.ticks_remaining <= 0 then 16 | for _, entity in pairs(demo_data.entities) do 17 | if entity.valid then 18 | entity.destroy() 19 | end 20 | end 21 | global.pathfinding_demo = nil 22 | end 23 | elseif demo_data.started then 24 | global.pathfinding_demo.data = pathfinder.resume_a_star(global.pathfinding_demo.data, 10) 25 | local pathfinding_data = global.pathfinding_demo.data 26 | --LOGGER.log("Pathfinding Data: \n" .. string.block(pathfinding_data, {comments = false})) 27 | 28 | if pathfinding_data.completed then 29 | demo_data.ticks_remaining = 200 30 | if pathfinding_data.path then 31 | for _, position in ipairs(pathfinding_data.path) do 32 | local overlay_entity = global.pathfinding_demo.surface.create_entity({name = "rm_overlay", force = game.forces.neutral, position = position }) 33 | overlay_entity.minable = false 34 | overlay_entity.destructible = false 35 | overlay_entity.operable = false 36 | table.insert(demo_data.entities, overlay_entity) 37 | end 38 | end 39 | else 40 | for _, position in ipairs(pathfinding_data.open_set) do 41 | if pathfinding_data.surface.count_entities_filtered({name = "30_red_overlay", area = {left_top = position, right_bottom = {x = position.x + 0.99, y = position.y + 0.99}} }) == 0 then 42 | local overlay_entity = pathfinding_data.surface.create_entity({name = "30_red_overlay", force = game.forces.neutral, position = position }) 43 | overlay_entity.minable = false 44 | overlay_entity.destructible = false 45 | overlay_entity.operable = false 46 | table.insert(demo_data.entities, overlay_entity) 47 | end 48 | end 49 | end 50 | else 51 | LOGGER.log("Starting demo: " .. serpent.line(demo_data)) 52 | demo_data.data = pathfinder.partial_a_star(demo_data.surface, demo_data.start_pos, demo_data.goal_pos, 1) 53 | demo_data.started = true 54 | demo_data.entities = {} 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /libs/biter/ai/harrassment.lua: -------------------------------------------------------------------------------- 1 | 2 | local Harrassment = {stages = {}} 3 | local Log = function(str, ...) BiterBase.LogAI("[Harrassment] " .. str, ...) end 4 | 5 | Harrassment.search_radius = 14 6 | Harrassment.search_queue = {} 7 | for dx, dy in Area.spiral_iterate(Position.expand_to_area({0, 0}, Harrassment.search_radius)) do 8 | table.insert(Harrassment.search_queue, {x = dx, y = dy}) 9 | end 10 | Harrassment.search_queue_size = #Harrassment.search_queue 11 | 12 | function Harrassment.search_queue_chunk(base, data) 13 | local idx = data.search_idx 14 | local center = data.start_chunk 15 | local delta_pos = Harrassment.search_queue[idx] 16 | return { x = center.x + delta_pos.x, y = center.y + delta_pos.y } 17 | end 18 | 19 | Harrassment.stages.attacking = function(base, data) 20 | return 'attacking' 21 | end 22 | 23 | Harrassment.stages.spawning = function(base, data) 24 | local command = {type = defines.command.attack_area, destination = data.target_pos, radius = 8} 25 | local surface = base.queen.surface 26 | for _, hive in pairs(base:all_hives()) do 27 | local biter = Biters.spawn_biter(base, surface, hive) 28 | if biter then 29 | biter.set_command(command) 30 | end 31 | end 32 | base.entities = table.filter(base.entities, Game.VALID_FILTER) 33 | return 'spawning' 34 | end 35 | 36 | Harrassment.stages.search = function(base, data) 37 | if data.search_idx > Harrassment.search_queue_size then 38 | if not data.worst_candidate.chunk_pos then 39 | return 'fail' 40 | end 41 | data.end_tick = game.tick + (Time.MINUTE * math.random(3,7)) 42 | data.target_pos = Area.center(Chunk.to_area(data.worst_candidate.chunk_pos)) 43 | return 'spawning' 44 | end 45 | local chunk_pos = Harrassment.search_queue_chunk(base, data) 46 | 47 | local chunk_value = World.get_chunk_value(base.queen.surface, chunk_pos) 48 | if chunk_value < 0 then 49 | local dist = Position.manhattan_distance(chunk_pos, data.start_chunk) 50 | 51 | local value = (chunk_value * chunk_value) / ((1 + dist) * (1 + dist)) 52 | if data.worst_candidate.value == nil or data.worst_candidate.value < value then 53 | data.worst_candidate = { chunk_pos = chunk_pos, value = math.floor(value) } 54 | end 55 | end 56 | 57 | data.search_idx = data.search_idx + 1 58 | return 'search' 59 | end 60 | 61 | Harrassment.stages.setup = function(base, data) 62 | data.search_idx = 1 63 | data.start_chunk = Chunk.from_position(base.queen.position) 64 | data.worst_candidate = { chunk_pos = nil, value = nil } 65 | return 'search' 66 | end 67 | 68 | function Harrassment.tick(base, data) 69 | if not data.stage then 70 | data.stage = 'setup' 71 | end 72 | local prev_stage = data.stage 73 | data.stage = Harrassment.stages[data.stage](base, data) 74 | if prev_stage ~= data.stage then 75 | Log("Updating stage from %s to %s", base, prev_stage, data.stage) 76 | end 77 | return true 78 | end 79 | 80 | function Harrassment.is_expired(base, data) 81 | if data.stage == 'fail' or data.stage == 'success' then 82 | return true 83 | end 84 | if data.end_tick then 85 | return game.tick > data.end_tick 86 | end 87 | return false 88 | end 89 | 90 | return Harrassment 91 | -------------------------------------------------------------------------------- /stdlib/surface.lua: -------------------------------------------------------------------------------- 1 | --- Surface module 2 | -- @module Surface 3 | 4 | require 'stdlib/core' 5 | 6 | Surface = {} 7 | 8 | --- Flexible, safe lookup function for surfaces.

9 | -- May be given a string, the name of a surface, or may be given a table with surface names, 10 | -- may be given the a surface object, or may be given a table of surface objects, or 11 | -- may be given nil.

12 | -- Returns an array of surface objects of all valid, existing surfaces 13 | -- If a surface does not exist for the surface, it is ignored, if no surfaces 14 | -- are given, an empty array is returned. 15 | -- @param surface to lookup 16 | -- @return the list of surfaces looked up 17 | function Surface.lookup(surface) 18 | if not surface then 19 | return {} 20 | end 21 | if type(surface) == 'string' then 22 | if game.surfaces[surface] then 23 | return {game.surfaces[surface]} 24 | end 25 | return {} 26 | end 27 | local result = {} 28 | for _, surface_item in pairs(surface) do 29 | if type(surface_item) == 'string' then 30 | if game.surfaces[surface_item] then 31 | table.insert(result, game.surfaces[surface_item]) 32 | end 33 | elseif type(surface_item) == 'table' and surface_item['__self'] then 34 | table.insert(result, surface_item) 35 | end 36 | end 37 | return result 38 | end 39 | 40 | --- Given search criteria, a table that contains a name or type of entity to search for, 41 | -- and optionally surface or force, searches all loaded chunks for the entities that 42 | -- match the critera. 43 | -- @usage 44 | ----Surface.final_all_entities({ type = 'unit', surface = 'nauvis' }) --returns a list containing all unit entities on the nauvis surface 45 | -- @param search_criteria a table of criteria. Must contain either the name or type or force of an entity. May contain surface or force. 46 | -- @return an array of all entities that matched the criteria 47 | function Surface.find_all_entities(search_criteria) 48 | fail_if_missing(search_criteria, "missing search_criteria argument") 49 | if search_criteria.name == nil and search_criteria.type == nil and search_criteria.force == nil then 50 | error("Missing search criteria field: name or type or force of entity", 2) 51 | end 52 | 53 | local surface_list = Surface.lookup(search_criteria.surface) 54 | if search_criteria.surface == nil then 55 | surface_list = game.surfaces 56 | end 57 | 58 | local result = {} 59 | 60 | for _, surface in pairs(surface_list) do 61 | -- TODO: this chunk iteration is no longer nessecary in Factorio 13.10+ 62 | -- see https://forums.factorio.com/viewtopic.php?f=3&t=29612 for details 63 | for chunk in surface.get_chunks() do 64 | local entities = surface.find_entities_filtered( 65 | { 66 | area = { left_top = { x = chunk.x * 32, y = chunk.y * 32 }, right_bottom = {x = (chunk.x + 1) * 32, y = (chunk.y + 1) * 32}}, 67 | name = search_criteria.name, 68 | type = search_criteria.type, 69 | force = search_criteria.force 70 | }) 71 | for _, entity in pairs(entities) do 72 | table.insert(result, entity) 73 | end 74 | end 75 | end 76 | 77 | return result 78 | end 79 | 80 | return Surface 81 | -------------------------------------------------------------------------------- /libs/developer_mode.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/gui/gui' 2 | 3 | DeveloperMode = {} 4 | 5 | function DeveloperMode.setup(player) 6 | local top_gui = player.gui.top 7 | if not top_gui['misanthrope_debug_toggle'] then 8 | top_gui.add({type = 'button', name = 'misanthrope_debug_toggle', style = 'button_style', caption = {'gui.misanthrope.developer_mode'}}) 9 | end 10 | end 11 | 12 | function DeveloperMode.close(player) 13 | local top_gui = player.gui.top 14 | if top_gui['misanthrope_debug_toggle'] then 15 | top_gui['misanthrope_debug_toggle'].destroy() 16 | end 17 | end 18 | 19 | function DeveloperMode.get_gui_setting(setting_name, default_val) 20 | if not global.settings then return default_val end 21 | if global.settings[setting_name] then 22 | return global.settings[setting_name] 23 | end 24 | return default_val 25 | end 26 | 27 | function DeveloperMode.set_gui_setting(setting_name, val) 28 | if not global.settings then global.settings = {} end 29 | global.settings[setting_name] = val 30 | end 31 | 32 | Gui.on_click('misanthrope_debug_toggle', function(event) 33 | local player = game.players[event.player_index] 34 | local center_gui = player.gui.center 35 | if not center_gui["misanthrope_frame"] then 36 | local frame = center_gui.add({type = 'frame', name = 'misanthrope_frame', direction = 'vertical'}) 37 | local settings = {debug_logging = DEBUG_MODE, biter_overmind = true, harpa = true, player_scent = true} 38 | for setting_name, default_val in pairs(settings) do 39 | local item_frame = frame.add({type = 'frame', name = setting_name .. 'frame', style = 'misanthrope_wide_naked_frame_style', direction = 'horizontal'}) 40 | item_frame.add({type = 'label', name = setting_name .. 'frame', caption = {'gui.misanthrope.' .. setting_name}}) 41 | item_frame.add({type = 'checkbox', name = setting_name .. 'checkbox', state = DeveloperMode.get_gui_setting(setting_name, default_val)}) 42 | end 43 | local biter_frame = frame.add({type = 'frame', name = 'biter_ai_frame', style = 'frame_in_right_container_style', direction = 'vertical'}) 44 | 45 | frame.add({type = 'button', name = 'close_misanthrope_frame', caption = {'gui.misanthrope.close'}}) 46 | frame.add({type = 'button', name = 'disable_developer_mode', caption = {'gui.misanthrope.disable_developer_mode'}}) 47 | end 48 | end) 49 | 50 | Gui.on_click('close_misanthrope_frame', function(event) 51 | event.element.parent.destroy() 52 | end) 53 | 54 | Gui.on_click('disable_developer_mode', function(event) 55 | local player = game.players[event.player_index] 56 | DeveloperMode.close(player) 57 | event.element.parent.destroy() 58 | end) 59 | 60 | local checkbox_func = function(event) 61 | local element = event.element 62 | local state = element.state 63 | local setting_name = element.name:sub(0, element.name:len() - 9) 64 | if state then 65 | element.state = false 66 | DeveloperMode.set_gui_setting(setting_name, false) 67 | else 68 | element.state = true 69 | DeveloperMode.set_gui_setting(setting_name, true) 70 | end 71 | World.Logger.log("Misanthrope settings: " .. string.block(global.settings)) 72 | end 73 | 74 | Gui.on_click('biter_ai_checkbox', checkbox_func) 75 | Gui.on_click('biter_overmind_checkbox', checkbox_func) 76 | Gui.on_click('harpa_checkbox', checkbox_func) 77 | Gui.on_click('player_scent_checkbox', checkbox_func) 78 | -------------------------------------------------------------------------------- /libs/region/chunk_value.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | require 'stdlib/area/position' 3 | require 'stdlib/area/chunk' 4 | require 'stdlib/area/tile' 5 | require 'libs/biter_targets' 6 | 7 | local Log = function(str, ...) World.Logger.log(string.format(str, ...)) end 8 | 9 | Event.register({defines.events.on_entity_built, defines.events.on_robot_built_entity}, function(event) 10 | local value, adj_value = World.entity_value(event.entity) 11 | 12 | if value ~= 0 then 13 | local chunk = Chunk.from_position(event.entity.position) 14 | World.change_chunk_value(event.entity.surface, chunk, value, adj_value) 15 | end 16 | end) 17 | 18 | Event.register({defines.events.on_entity_died, defines.events.on_preplayer_mined_item, defines.events.on_robot_pre_mined}, function(event) 19 | local value, adj_value = World.entity_value(event.entity) 20 | 21 | if value ~= 0 then 22 | local chunk = Chunk.from_position(event.entity.position) 23 | World.change_chunk_value(event.entity.surface, chunk, value * -1, adj_value * -1) 24 | end 25 | end) 26 | 27 | function World.entity_value(entity) 28 | if not entity or not entity.valid then 29 | return 0 30 | end 31 | local entity_name = entity.name 32 | local value = 0 33 | local adj_value = 0 34 | local biter_value = Biters.entity_value(entity) 35 | if entity.type:contains('turret') then 36 | value = -1 * game.entity_prototypes[entity_name].max_health 37 | adj_value = value / 2 38 | elseif biter_value > 0 then 39 | value = biter_value 40 | elseif entity.type:contains('container') then 41 | value = game.entity_prototypes[entity_name].max_health / 3 42 | end 43 | if value ~= 0 then 44 | Log("Entity %s value is %d", entity.name, value) 45 | end 46 | return math.floor(value), math.floor(adj_value) 47 | end 48 | 49 | function World.chunk_index(chunk_pos) 50 | return bit32.bor(bit32.lshift(bit32.band(chunk_pos.x, 0xFFFF), 16), bit32.band(chunk_pos.y, 0xFFFF)) 51 | end 52 | 53 | function World.recalculate_chunk_values(reset) 54 | if not global.chunk_values then global.chunk_values = {} end 55 | if reset then 56 | global.chunk_values = {} 57 | end 58 | local all_entities = Surface.find_all_entities({force = game.forces.player}) 59 | Log("Total number of player entities: %d", #all_entities) 60 | local entity_prototypes = game.entity_prototypes 61 | for _, entity in pairs(all_entities) do 62 | local value, adj_value = World.entity_value(entity) 63 | if value ~= 0 then 64 | local chunk = Chunk.from_position(entity.position) 65 | World.change_chunk_value(entity.surface, chunk, value, adj_value) 66 | end 67 | end 68 | end 69 | 70 | function World.get_chunk_value(surface, chunk) 71 | if not global.chunk_values then return 0 end 72 | local idx = World.chunk_index(chunk) 73 | local chunk_value = global.chunk_values[idx] 74 | if chunk_value then 75 | return chunk_value 76 | end 77 | return 0 78 | end 79 | 80 | function World.change_chunk_value(surface, chunk, value, adj_value) 81 | if not global.chunk_values then global.chunk_values = {} end 82 | 83 | local idx = World.chunk_index(chunk) 84 | local chunk_value = global.chunk_values[idx] 85 | if chunk_value then 86 | global.chunk_values[idx] = math.floor(chunk_value + value) 87 | else 88 | global.chunk_values[idx] = math.floor(value) 89 | end 90 | if adj_value ~= 0 then 91 | for x, y in Area.iterate(Position.expand_to_area(chunk, 1)) do 92 | if x ~= chunk.x and y ~= chunk.y then 93 | World.change_chunk_value(surface, {x = x, y = y}, adj_value, 0) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /libs/biter/random_name.lua: -------------------------------------------------------------------------------- 1 | 2 | RandomName = {} 3 | 4 | function RandomName.get_random_vowel() 5 | local r = math.random(38100) 6 | if (r < 8167) then return 'a' end 7 | if (r < 20869) then return 'e' end 8 | if (r < 27835) then return 'i' end 9 | if (r < 35342) then return 'o' end 10 | return 'u' 11 | end 12 | 13 | function RandomName.get_random_consonant() 14 | local r = math.random(34550) * 2 15 | 16 | if (r < 1492) then return 'b' end 17 | if (r < 4274) then return 'c' end 18 | if (r < 8527) then return 'd' end 19 | if (r < 10755) then return 'f' end 20 | if (r < 12770) then return 'g' end 21 | if (r < 18864) then return 'h' end 22 | if (r < 19017) then return 'j' end 23 | if (r < 19789) then return 'k' end 24 | if (r < 23814) then return 'l' end 25 | if (r < 26220) then return 'm' end 26 | if (r < 32969) then return 'n' end 27 | if (r < 34898) then return 'p' end 28 | if (r < 34993) then return 'q' end 29 | if (r < 40980) then return 'r' end 30 | if (r < 47307) then return 's' end 31 | if (r < 56363) then return 't' end 32 | if (r < 57341) then return 'v' end 33 | if (r < 59701) then return 'w' end 34 | if (r < 59851) then return 'x' end 35 | if (r < 61825) then return 'y' end 36 | return 'z' 37 | end 38 | 39 | function RandomName.generate_random_word(max_length) 40 | local str = "" 41 | local length = math.max(4, math.random(max_length)) 42 | for i = 1, length do 43 | local letter = "" 44 | local r = math.random(1000) 45 | if i == 1 then r = r * 2 end 46 | if (r < 381) then 47 | letter = RandomName.get_random_vowel() 48 | else 49 | letter = RandomName.get_random_consonant() 50 | end 51 | if (i == 1) then 52 | letter = letter:upper() 53 | end 54 | str = str .. letter 55 | end 56 | return str 57 | end 58 | 59 | function RandomName.is_valid_name(name) 60 | local consonant_count = 0 61 | local vowel_count = 0 62 | local vowel_streak = 0 63 | local consonant_streak = 0 64 | 65 | name = name:lower() 66 | for i = 1, #name do 67 | local ch = name:sub(i,i) 68 | if (ch == 'a' or ch == 'e' or ch == 'i' or ch == 'o' or ch == 'u') then 69 | vowel_count = vowel_count + 1 70 | vowel_streak = vowel_streak + 1 71 | consonant_streak = 0 72 | else 73 | consonant_count = consonant_count + 1 74 | consonant_streak = consonant_streak + 1 75 | vowel_streak = 0 76 | end 77 | if consonant_streak > 3 or vowel_streak > 4 then 78 | return false 79 | end 80 | end 81 | --More than 75% of the word is vowels 82 | if ((vowel_count * 100 / math.max(1, vowel_count + consonant_count)) >= 75) then 83 | return false 84 | end 85 | --More than 70% of the word is consonants 86 | if ((consonant_count * 100 / math.max(1, vowel_count + consonant_count)) >= 70) then 87 | return false 88 | end 89 | return true 90 | end 91 | 92 | function RandomName.get_random_name(max_length) 93 | local random_name = "" 94 | local tries = 3 95 | max_length = math.max(4, max_length) 96 | while(tries > 0) do 97 | local name = RandomName.generate_random_word(max_length) 98 | if RandomName.is_valid_name(name) then 99 | --first word is always valid 100 | if #random_name == 0 then 101 | random_name = name 102 | else 103 | if (#random_name + #name <= max_length) then 104 | random_name = random_name .. " " .. name 105 | else 106 | tries = tries - 1 107 | end 108 | end 109 | end 110 | end 111 | return random_name; 112 | end 113 | -------------------------------------------------------------------------------- /libs/biter/ai/identify_targets.lua: -------------------------------------------------------------------------------- 1 | require 'libs/pathfinding_engine' 2 | 3 | local IdentifyTargets = {stages = {}} 4 | local Log = function(str, ...) BiterBase.LogAI("[IdentifyTargets] " .. str, ...) end 5 | 6 | IdentifyTargets.search_radius = 40 7 | IdentifyTargets.search_queue = {} 8 | for dx, dy in Area.spiral_iterate(Position.expand_to_area({0, 0}, IdentifyTargets.search_radius)) do 9 | table.insert(IdentifyTargets.search_queue, {x = dx, y = dy}) 10 | end 11 | IdentifyTargets.search_queue_size = #IdentifyTargets.search_queue 12 | 13 | function IdentifyTargets.search_queue_chunk(data) 14 | local idx = data.search_idx 15 | local center = data.start_chunk 16 | local delta_pos = IdentifyTargets.search_queue[idx] 17 | return { x = center.x + delta_pos.x, y = center.y + delta_pos.y } 18 | end 19 | 20 | IdentifyTargets.stages.setup = function(base, data) 21 | local chunk_pos = Chunk.from_position(base.queen.position) 22 | data.start_chunk = chunk_pos 23 | data.search_idx = 1 24 | data.candidates = {} 25 | data.path_finding = { idx = 1, path_id = -1} 26 | data.completed = false 27 | return 'search' 28 | end 29 | 30 | IdentifyTargets.stages.search = function(base, data) 31 | if data.search_idx > IdentifyTargets.search_queue_size then 32 | return 'sort' 33 | end 34 | local chunk_pos = IdentifyTargets.search_queue_chunk(data) 35 | 36 | local chunk_value = World.get_chunk_value(base.queen.surface, chunk_pos) 37 | if chunk_value > 0 then 38 | local dist = Position.manhattan_distance(chunk_pos, data.start_chunk) 39 | 40 | value = (chunk_value * chunk_value) / ((1 + dist) * (1 + dist)) 41 | table.insert(data.candidates, { chunk_pos = chunk_pos, value = math.floor(value)}) 42 | end 43 | 44 | data.search_idx = data.search_idx + 1 45 | return 'search' 46 | end 47 | 48 | IdentifyTargets.stages.sort = function(base, data) 49 | if #data.candidates == 0 then 50 | Log("No candidates, unable to identify any targets.", base) 51 | base.targets = { candidates = {}, tick = game.tick } 52 | return 'fail' 53 | end 54 | table.sort(data.candidates, function(a, b) 55 | return b.value < a.value 56 | end) 57 | 58 | Log("All candidates: %s", base, string.block(data.candidates)) 59 | data.candidates = table.filter(data.candidates, function(candidate) 60 | return candidate.value > 100 61 | end) 62 | if #data.candidates == 0 then 63 | Log("No candidates, unable to identify any valuable targets.", base) 64 | base.targets = { candidates = {}, tick = game.tick } 65 | return 'fail' 66 | end 67 | Log("Filtered candidates: %s", base, string.block(data.candidates)) 68 | local max_candidates = math.min(20, #data.candidates) 69 | local base_candidates = {} 70 | for i = 1, max_candidates do 71 | base_candidates[i] = data.candidates[i].chunk_pos 72 | end 73 | 74 | base.targets = { candidates = base_candidates, tick = game.tick } 75 | return 'success' 76 | end 77 | 78 | function IdentifyTargets.tick(base, data) 79 | if not data.stage then 80 | data.stage = 'setup' 81 | end 82 | local prev_stage = data.stage 83 | data.stage = IdentifyTargets.stages[data.stage](base, data) 84 | if prev_stage ~= data.stage then 85 | Log("Updating stage from %s to %s", base, prev_stage, data.stage) 86 | end 87 | return true 88 | end 89 | 90 | function IdentifyTargets.is_expired(base, data) 91 | if data.stage == 'fail' then 92 | Log("Failed to identify any targets", base) 93 | return true 94 | elseif data.stage == 'success' then 95 | Log("Successfully found a target!", base) 96 | return true 97 | end 98 | return false 99 | end 100 | 101 | return IdentifyTargets 102 | -------------------------------------------------------------------------------- /stdlib/entity/entity.lua: -------------------------------------------------------------------------------- 1 | --- Entity module 2 | -- @module Entity 3 | 4 | require 'stdlib/core' 5 | require 'stdlib/surface' 6 | require 'stdlib/area/area' 7 | 8 | Entity = {} 9 | 10 | --- Converts an entity and its selection_box to the area around it 11 | -- @param entity to convert to an area 12 | -- @return area that entity selection_box is valid for 13 | function Entity.to_selection_area(entity) 14 | fail_if_missing(entity, "missing entity argument") 15 | 16 | local pos = entity.position 17 | local bb = entity.prototype.selection_box 18 | return Area.offset(bb, pos) 19 | end 20 | 21 | --- Converts an entity and its selection_box to the area around it 22 | -- @param entity to convert to an area 23 | -- @return area that entity selection_box is valid for 24 | function Entity.to_collision_area(entity) 25 | fail_if_missing(entity, "missing entity argument") 26 | 27 | local pos = entity.position 28 | local bb = entity.prototype.collision_box 29 | return Area.offset(bb, pos) 30 | end 31 | 32 | --- Tests whether an entity has access to the field 33 | -- @param entity to test field access 34 | -- @param field_name that should be tested for 35 | -- @return true if the entity has access to the field, false if the entity threw an exception accessing the field 36 | function Entity.has(entity, field_name) 37 | fail_if_missing(entity, "missing entity argument") 38 | fail_if_missing(field_name, "missing field name argument") 39 | 40 | local status = pcall(function() return entity[field_name]; end) 41 | return status 42 | end 43 | 44 | --- Gets user data from the entity, stored in a mod's global data. 45 | ---

The data will persist between loads, and will be removed for an entity when it becomes invalid

46 | -- @param entity the entity to look up data for 47 | -- @return the data, or nil if no data exists for the entity 48 | function Entity.get_data(entity) 49 | fail_if_missing(entity, "missing entity argument") 50 | if not global._entity_data then return nil end 51 | 52 | local entity_name = entity.name 53 | if not global._entity_data[entity_name] then return nil end 54 | local entity_category = global._entity_data[entity_name] 55 | for _, entity_data in pairs(entity_category) do 56 | if entity_data.entity == entity then 57 | return entity_data.data 58 | end 59 | end 60 | return nil 61 | end 62 | 63 | --- Sets user data on the entity, stored in a mod's global data. 64 | ---

The data will persist between loads, and will be removed for an entity when it becomes invalid

65 | -- @param entity the entity to set data for 66 | -- @param data the data to set, or nil to delete the data associated with the entity 67 | -- @return the previous data associated with the entity, or nil if the entity had no previous data 68 | function Entity.set_data(entity, data) 69 | fail_if_missing(entity, "missing entity argument") 70 | 71 | if not global._entity_data then global._entity_data = {} end 72 | 73 | local entity_name = entity.name 74 | if not global._entity_data[entity_name] then 75 | global._entity_data[entity_name] = { pos_data = {}, data = {} } 76 | end 77 | 78 | local entity_category = global._entity_data[entity_name] 79 | 80 | for i = #entity_category, 1, -1 do 81 | local entity_data = entity_category[i] 82 | if not entity_data.entity.valid then 83 | table.remove(entity_category, i) 84 | end 85 | if entity_data.entity == entity then 86 | local prev = entity_data.data 87 | if data then 88 | entity_data.data = data 89 | else 90 | table.remove(entity_category, i) 91 | end 92 | return prev 93 | end 94 | end 95 | 96 | table.insert(entity_category, { entity = entity, data = data }) 97 | return nil 98 | end 99 | 100 | return Entity 101 | -------------------------------------------------------------------------------- /libs/EvoGUI.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | 3 | EvoGUI = {} 4 | 5 | function EvoGUI.create_evolution_rate_text() 6 | local diff = game.evolution_factor - global.exponential_moving_average 7 | -- percentage is decimal * 100, * 60 for per minute value 8 | local evo_rate_per_min = math.abs(diff * 100 * 60) 9 | 10 | -- this nonsense is because string.format(%.3f) is not safe in MP across platforms, but integer math is 11 | local whole_number = math.floor(evo_rate_per_min) 12 | local fractional_component = math.floor((evo_rate_per_min - whole_number) * 1000) 13 | local text = string.format("%d.%04d%%", whole_number, fractional_component) 14 | if diff > 0 then 15 | return "Evolution Rate: +" .. text .. " / min" 16 | else 17 | return "Evolution Rate: -" .. text .. " / min" 18 | end 19 | end 20 | 21 | function EvoGUI.create_biter_scent_text() 22 | local player = game.players[1] 23 | if player and player.valid and player.connected then 24 | local character = player.character 25 | if character and character.valid then 26 | local pos = character.position 27 | local data = Tile.get_data(character.surface, Tile.from_position(pos)) 28 | if data and data.scent then 29 | return "Biter Scent: " .. data.scent 30 | end 31 | end 32 | end 33 | return "Biter Scent: 0" 34 | end 35 | 36 | function EvoGUI.create_chunk_value_text() 37 | local player = game.players[1] 38 | if player and player.valid and player.connected then 39 | local character = player.character 40 | if character and character.valid then 41 | local pos = character.position 42 | return "Chunk Value: " .. World.get_chunk_value(character.surface, Chunk.from_position(pos)) 43 | end 44 | end 45 | return "Chunk Value: 0" 46 | end 47 | 48 | function EvoGUI.create_evolution_rate_color() 49 | local diff = game.evolution_factor - global.exponential_moving_average 50 | 51 | if diff > 0 then 52 | local red = (100 * 255 * diff) / 0.0035 53 | return { r = math.max(0, math.min(255, math.floor( red ))), g = math.max(0, math.min(255, math.floor( 255 - red ))), b = 0 } 54 | else 55 | return { r = 0, g = 255, b = 0 } 56 | end 57 | end 58 | 59 | function EvoGUI.setup() 60 | if remote.interfaces.EvoGUI and remote.interfaces.EvoGUI.create_remote_sensor then 61 | global.evo_gui.detected = true 62 | 63 | remote.call("EvoGUI", "create_remote_sensor", { 64 | mod_name = "Misanthrope", 65 | name = "evolution_rate", 66 | text = "Evolution Rate:", 67 | caption = "Evolution Rate" 68 | }) 69 | EvoGUI.update_gui() 70 | end 71 | end 72 | 73 | Event.register(defines.events.on_tick, function(event) 74 | if not global.evo_gui then global.evo_gui = {} end 75 | if not global.exponential_moving_average then 76 | global.exponential_moving_average = game.evolution_factor 77 | end 78 | 79 | if not global.evo_gui.detected then 80 | EvoGUI.setup() 81 | if remote.interfaces.EvoGUI and remote.interfaces.EvoGUI.remove_remote_sensor and remote.interfaces.EvoGUI.has_remote_sensor then 82 | if remote.call("EvoGUI", "has_remote_sensor", "evolution_state") then 83 | remote.call("EvoGUI", "remove_remote_sensor", "evolution_state") 84 | end 85 | end 86 | end 87 | 88 | if global.evo_gui.detected and event.tick % 20 == 0 then 89 | if remote.interfaces.EvoGUI then 90 | EvoGUI.update_gui() 91 | global.exponential_moving_average = global.exponential_moving_average + (0.8 * (game.evolution_factor - global.exponential_moving_average)) 92 | end 93 | end 94 | end) 95 | 96 | function EvoGUI.update_gui() 97 | remote.call("EvoGUI", "update_remote_sensor", "evolution_rate", EvoGUI.create_evolution_rate_text(), EvoGUI.create_evolution_rate_color()) 98 | end 99 | 100 | return EvoGUI 101 | -------------------------------------------------------------------------------- /libs/circular_buffer.lua: -------------------------------------------------------------------------------- 1 | circular_buffer = {} 2 | circular_buffer.__index = circular_buffer 3 | 4 | function circular_buffer.new() 5 | return {start_index = 1, end_index = 0, internal_table = {}, count = 0, dead_count = 0} 6 | end 7 | 8 | function circular_buffer.reset(list) 9 | list.start_index = 1 10 | list.end_index = 0 11 | list.internal_table = {} 12 | list.count = 0 13 | list.dead_count = 0 14 | end 15 | 16 | function circular_buffer.append(list, item) 17 | if list.dead_count > 10000 then 18 | local old_table = list.internal_table 19 | local old_start = list.start_index 20 | local old_end = list.end_index 21 | circular_buffer.reset(list) 22 | for i = old_start, old_end do 23 | if old_table[i] ~= nil then 24 | circular_buffer.append(list, old_table[i].value) 25 | end 26 | end 27 | end 28 | 29 | local index = list.end_index + 1 30 | local node = {value = item, index = index} 31 | list.internal_table[index] = node 32 | list.count = list.count + 1 33 | list.end_index = index 34 | end 35 | 36 | function circular_buffer.pop(list) 37 | if list.count == 0 then 38 | if list.dead_count > 0 then 39 | circular_buffer.reset(list) 40 | end 41 | return nil 42 | elseif list.count == 1 then 43 | local node = list.internal_table[list.start_index] 44 | circular_buffer.reset(list) 45 | return node.value 46 | else 47 | local node = list.internal_table[list.start_index] 48 | list.internal_table[list.start_index] = nil 49 | for i = list.start_index + 1, list.end_index do 50 | if list.internal_table[i] ~= nil then 51 | list.start_index = i 52 | break 53 | end 54 | end 55 | list.count = list.count - 1 56 | list.dead_count = list.dead_count + 1 57 | return node.value 58 | end 59 | end 60 | 61 | function circular_buffer.remove(list, node) 62 | if list.count == 1 then 63 | circular_buffer.reset(list) 64 | else 65 | list.internal_table[node.index] = nil 66 | list.count = list.count - 1 67 | list.dead_count = list.dead_count + 1 68 | -- find a new start index if we just erased it 69 | if node.index == list.start_index then 70 | for i = list.start_index + 1, list.end_index do 71 | if list.internal_table[i] ~= nil then 72 | list.start_index = i 73 | break 74 | end 75 | end 76 | -- find a new end index if we just erased it 77 | elseif node.index == list.end_index then 78 | for i = list.end_index, list.start_index, -1 do 79 | if list.internal_table[i] ~= nil then 80 | list.end_index = i 81 | break 82 | end 83 | end 84 | end 85 | end 86 | end 87 | 88 | function circular_buffer.iterator(list) 89 | local iterator = {list = list, current_index = list.start_index, _next = list.count > 0} 90 | function iterator.next() 91 | if iterator.has_next() then 92 | local node = iterator.next_node() 93 | if node then 94 | return node.value 95 | end 96 | end 97 | return nil 98 | end 99 | 100 | function iterator.next_node() 101 | local cur = iterator.current_index 102 | local any_next = iterator._next 103 | iterator._next = false 104 | for i = cur + 1, iterator.list.end_index do 105 | if iterator.list.internal_table[i] ~= nil then 106 | iterator.current_index = i 107 | iterator._next = true 108 | break 109 | end 110 | end 111 | if any_next then 112 | return iterator.list.internal_table[cur] 113 | else 114 | return nil 115 | end 116 | end 117 | 118 | function iterator.has_next() 119 | return iterator._next 120 | end 121 | return iterator 122 | end 123 | -------------------------------------------------------------------------------- /libs/biter/ai/attack_area.lua: -------------------------------------------------------------------------------- 1 | 2 | local AttackArea = {stages = {}} 3 | local Log = function(str, ...) BiterBase.LogAI("[AttackArea] " .. str, ...) end 4 | 5 | AttackArea.stages.attacking = function(base, data) 6 | if not data.attack_group.valid then 7 | local entities = base:get_entities() 8 | Log("Unit group invalid, valid entities: %d", base, #entities) 9 | 10 | if #entities == 0 then 11 | return 'fail' 12 | end 13 | local command = {type = defines.command.attack_area, destination = data.attack_target, radius = 18} 14 | local unit_group = BiterBase.create_unit_group(base, {position = entities[1].position, force = 'enemy'}) 15 | for _, biter in pairs(entities) do 16 | if biter.unit_group and biter.unit_group.valid then 17 | biter.unit_group.destroy() 18 | end 19 | unit_group.add_member(biter) 20 | end 21 | unit_group.set_command(command) 22 | unit_group.start_moving() 23 | data.attack_group = unit_group 24 | end 25 | return 'attacking' 26 | end 27 | 28 | AttackArea.stages.spawning = function(base, data) 29 | local surface = base.queen.surface 30 | 31 | local biters = base:get_prev_entities() 32 | for _, hive in pairs(base:all_hives()) do 33 | table.insert(biters, Biters.spawn_biter(base, surface, hive)) 34 | end 35 | if #biters > 0 then 36 | local unit_group = BiterBase.create_unit_group(base, {position = biters[1].position, force = 'enemy'}) 37 | for _, biter in pairs(biters) do 38 | unit_group.add_member(biter) 39 | end 40 | unit_group.set_command({type = defines.command.attack_area, destination = base.queen.position, radius = 8}) 41 | unit_group.start_moving() 42 | end 43 | 44 | if #base:get_entities() > data.attack_group_size then 45 | return 'plan_attack' 46 | end 47 | return 'spawning' 48 | end 49 | 50 | AttackArea.stages.plan_attack = function(base, data) 51 | local candidates = base.targets.candidates 52 | if #candidates == 0 then 53 | base.targets = nil 54 | return 'fail' 55 | end 56 | local idx = math.random(#candidates) 57 | local chunk_pos = table.remove(candidates, idx) 58 | Log("Attack candidate: %s", base, Chunk.to_string(chunk_pos)) 59 | if #candidates == 0 then 60 | base.targets = nil 61 | end 62 | 63 | local end_pos = Area.center(Chunk.to_area(chunk_pos)) 64 | data.attack_target = end_pos 65 | local command = {type = defines.command.attack_area, destination = end_pos, radius = 18} 66 | local entities = base:get_entities() 67 | 68 | local unit_group = BiterBase.create_unit_group(base, {position = entities[1].position, force = 'enemy'}) 69 | for _, biter in pairs(entities) do 70 | if biter.unit_group and biter.unit_group.valid then 71 | biter.unit_group.destroy() 72 | end 73 | unit_group.add_member(biter) 74 | end 75 | unit_group.set_command(command) 76 | unit_group.start_moving() 77 | 78 | data.attack_group = unit_group 79 | data.attack_tick = game.tick 80 | return 'attacking' 81 | end 82 | 83 | function AttackArea.tick(base, data) 84 | if not data.stage then 85 | data.stage = 'spawning' 86 | end 87 | local prev_stage = data.stage 88 | data.stage = AttackArea.stages[data.stage](base, data) 89 | if prev_stage ~= data.stage then 90 | Log("Updating stage from %s to %s", base, prev_stage, data.stage) 91 | end 92 | return true 93 | end 94 | 95 | function AttackArea.initialize(base, data) 96 | data.attack_group_size = math.floor(10 + game.evolution_factor / 0.025) 97 | if base:get_currency(false) > BiterBase.plans.attack_area.cost * 2 then 98 | base:spend_currency(BiterBase.plans.attack_area.cost) 99 | data.attack_group_size = data.attack_group_size + math.floor(15 + game.evolution_factor / 0.02) 100 | end 101 | Log("Attack group size: %d", base, data.attack_group_size) 102 | end 103 | 104 | function AttackArea.is_expired(base, data) 105 | if data.stage == 'fail' or data.stage == 'success' then 106 | return true 107 | end 108 | return data.attack_group and ( --[[not data.attack_group.valid or --]] game.tick > data.attack_tick + Time.MINUTE * 6) 109 | end 110 | 111 | return AttackArea 112 | -------------------------------------------------------------------------------- /stdlib/log/logger.lua: -------------------------------------------------------------------------------- 1 | --- Logger module 2 | -- @module Logger 3 | 4 | Logger = {} 5 | 6 | --- Creates a new logger object.

7 | -- In debug mode, the logger writes immediately. Otherwise the loggers buffers lines. 8 | -- The logger flushes after 60 seconds has elapsed since the last message. 9 | --

10 | -- When loggers are created, a table of options may be specified. The valid options are: 11 | -- 12 | -- log_ticks -- whether to include the game tick timestamp in logs. Defaults to false. 13 | -- file_extension -- a string that overides the default 'log' file extension. 14 | -- force_append -- each time a logger is created, it will always append, instead of 15 | -- -- the default behavior, which is to write out a new file, then append 16 | -- 17 | -- 18 | -- @usage 19 | --LOGGER = Logger.new('cool_mod_name') 20 | --LOGGER.log("this msg will be logged!") 21 | -- 22 | -- @usage 23 | --Logger.new('cool_mod_name', 'test', true) 24 | --LOGGER.log("this msg will be logged and written immediately in test.log!") 25 | -- 26 | -- @usage 27 | --LOGGER = Logger.new('cool_mod_name', 'test', true, { file_extension = data }) 28 | --LOGGER.log("this msg will be logged and written immediately in test.data!") 29 | -- 30 | -- @param mod_name [required] the name of the mod to create the logger for 31 | -- @param log_name (optional, default: 'main') the name of the logger 32 | -- @param debug_mode (optional, default: false) toggles the debug state of logger. 33 | -- @param options (optional) table with optional arguments 34 | -- @return the logger instance 35 | function Logger.new(mod_name, log_name, debug_mode, options) 36 | if not mod_name then 37 | error("Logger must be given a mod_name as the first argument") 38 | end 39 | if not log_name then 40 | log_name = "main" 41 | end 42 | if not options then 43 | options = {} 44 | end 45 | local Logger = {mod_name = mod_name, log_name = log_name, debug_mode = debug_mode, buffer = {}, last_written = 0, ever_written = false} 46 | 47 | --- Logger options 48 | Logger.options = { 49 | log_ticks = options.log_ticks or false, -- whether to add the ticks in the timestamp, default false 50 | file_extension = options.file_extension or 'log', -- extension of the file, default: log 51 | force_append = options.force_append or false, -- append the file on first write, default: false 52 | } 53 | Logger.file_name = 'logs/' .. Logger.mod_name .. '/' .. Logger.log_name .. '.' .. Logger.options.file_extension 54 | Logger.ever_written = Logger.options.force_append 55 | 56 | --- Logs a message 57 | -- @param msg a string, the message to log 58 | -- @return true if the message was written, false if it was queued for a later write 59 | function Logger.log(msg) 60 | local format = string.format 61 | if _G.game then 62 | local tick = game.tick 63 | local floor = math.floor 64 | local time_s = floor(tick/60) 65 | local time_minutes = floor(time_s/60) 66 | local time_hours = floor(time_minutes/60) 67 | 68 | if Logger.options.log_ticks then 69 | table.insert(Logger.buffer, format("%02d:%02d:%02d.%02d: %s\n", time_hours, time_minutes % 60, time_s % 60, tick - time_s*60, msg)) 70 | else 71 | table.insert(Logger.buffer, format("%02d:%02d:%02d: %s\n", time_hours, time_minutes % 60, time_s % 60, msg)) 72 | end 73 | 74 | -- write the log every minute 75 | if (Logger.debug_mode or (tick - Logger.last_written) > 3600) then 76 | return Logger.write() 77 | end 78 | else 79 | table.insert(Logger.buffer, format("00:00:00: %s\n", msg)) 80 | end 81 | return false 82 | end 83 | 84 | --- Writes out all buffered messages immediately 85 | -- @return true if there any messages were written, false if not 86 | function Logger.write() 87 | if _G.game then 88 | Logger.last_written = game.tick 89 | game.write_file(Logger.file_name, table.concat(Logger.buffer), Logger.ever_written) 90 | Logger.buffer = {} 91 | Logger.ever_written = true 92 | return true 93 | end 94 | return false 95 | end 96 | 97 | return Logger 98 | end 99 | 100 | return Logger 101 | -------------------------------------------------------------------------------- /stdlib/area/chunk.lua: -------------------------------------------------------------------------------- 1 | --- Chunk module 2 | ---

A chunk represents a 32x32 area of a surface in factorio.

3 | -- @module Chunk 4 | 5 | require 'stdlib/core' 6 | require 'stdlib/area/position' 7 | 8 | Chunk = {} 9 | MAX_UINT = 4294967296 10 | 11 | --- Calculates the chunk coordinates for the tile position given 12 | -- @param position to calculate the chunk for 13 | -- @return the chunk position as a table 14 | -- @usage 15 | ----local chunk_x = Chunk.from_position(pos).x 16 | function Chunk.from_position(position) 17 | position = Position.to_table(position) 18 | local x = math.floor(position.x) 19 | local y = math.floor(position.y) 20 | local chunk_x = bit32.arshift(x, 5) 21 | if x < 0 then 22 | chunk_x = chunk_x - MAX_UINT 23 | end 24 | local chunk_y = bit32.arshift(y, 5) 25 | if y < 0 then 26 | chunk_y = chunk_y - MAX_UINT 27 | end 28 | return {x = chunk_x, y = chunk_y} 29 | end 30 | 31 | --- Converts a chunk to the area it contains 32 | -- @param chunk_pos to convert to an area 33 | -- @return area that chunk is valid for 34 | function Chunk.to_area(chunk_pos) 35 | fail_if_missing(chunk_pos, "missing chunk_pos argument") 36 | chunk_pos = Position.to_table(chunk_pos) 37 | 38 | local left_top = { x = chunk_pos.x * 32, y = chunk_pos.y * 32 } 39 | return { left_top = left_top, right_bottom = Position.offset(left_top, 32, 32) } 40 | end 41 | 42 | --- Gets user data from the chunk, stored in a mod's global data. 43 | ---

The data will persist between loads

44 | -- @param surface the surface to look up data for 45 | -- @param chunk_pos the chunk coordinates to look up data for 46 | -- @param default_value (optional) to set and return if no data exists 47 | -- @return the data, or nil if no data exists for the chunk 48 | function Chunk.get_data(surface, chunk_pos, default_value) 49 | fail_if_missing(surface, "missing surface argument") 50 | fail_if_missing(chunk_pos, "missing chunk_pos argument") 51 | if not global._chunk_data then 52 | if not default_value then return nil end 53 | global._chunk_data = {} 54 | end 55 | 56 | local idx = Chunk.get_index(surface, chunk_pos) 57 | local val = global._chunk_data[idx] 58 | if not val then 59 | global._chunk_data[idx] = default_value 60 | val = default_value 61 | end 62 | 63 | return val, idx 64 | end 65 | 66 | --- Sets user data on the chunk, stored in a mod's global data. 67 | ---

The data will persist between loads

68 | -- @param surface the surface to look up data for 69 | -- @param chunk_pos the chunk coordinates to look up data for 70 | -- @param data the data to set (or nil to erase the data for the chunk) 71 | -- @return the previous data associated with the chunk, or nil if the chunk had no previous data 72 | function Chunk.set_data(surface, chunk_pos, data) 73 | fail_if_missing(surface, "missing surface argument") 74 | fail_if_missing(chunk_pos, "missing chunk_pos argument") 75 | if not global._chunk_data then global._chunk_data = {} end 76 | 77 | local idx = Chunk.get_index(surface, chunk_pos) 78 | local prev = global._chunk_data[idx] 79 | global._chunk_data[idx] = data 80 | 81 | return prev 82 | end 83 | 84 | --- Calculates and returns a stable, deterministic, unique integer id for the given chunk_pos 85 | ---

The id will not change once calculated

86 | -- @param surface the chunk is on 87 | -- @param chunk_pos of the chunk 88 | function Chunk.get_index(surface, chunk_pos) 89 | fail_if_missing(surface, "missing surface argument") 90 | fail_if_missing(chunk_pos, "missing chunk_pos argument") 91 | if not global._next_chunk_index then global._next_chunk_index = 0 end 92 | if not global._chunk_indexes then global._chunk_indexes = {} end 93 | 94 | if type(surface) == "string" then 95 | surface = game.surfaces[surface] 96 | end 97 | local surface_idx = surface.index 98 | if not global._chunk_indexes[surface_idx] then global._chunk_indexes[surface_idx] = {} end 99 | 100 | local surface_chunks = global._chunk_indexes[surface_idx] 101 | if not surface_chunks[chunk_pos.x] then surface_chunks[chunk_pos.x] = {} end 102 | if not surface_chunks[chunk_pos.x][chunk_pos.y] then 103 | surface_chunks[chunk_pos.x][chunk_pos.y] = global._next_chunk_index 104 | global._next_chunk_index = global._next_chunk_index + 1 105 | end 106 | 107 | return surface_chunks[chunk_pos.x][chunk_pos.y] 108 | end 109 | -------------------------------------------------------------------------------- /stdlib/event/event.lua: -------------------------------------------------------------------------------- 1 | --- Event module 2 | -- @module Event 3 | 4 | require 'stdlib/core' 5 | require 'stdlib/game' 6 | 7 | Event = { 8 | _registry = {}, 9 | core_events = { 10 | init = -1, 11 | load = -2, 12 | configuration_changed = -3, 13 | _register = function(id) 14 | if id == Event.core_events.init then 15 | script.on_init(function() 16 | Event.dispatch({name = Event.core_events.init, tick = game.tick}) 17 | end) 18 | elseif id == Event.core_events.load then 19 | script.on_load(function() 20 | Event.dispatch({name = Event.core_events.load, tick = -1}) 21 | end) 22 | elseif id == Event.core_events.configuration_changed then 23 | script.on_configuration_changed(function(data) 24 | Event.dispatch({name = Event.core_events.configuration_changed, tick = game.tick, data = data}) 25 | end) 26 | end 27 | end 28 | } 29 | } 30 | 31 | --- Registers a function for a given event 32 | -- @param event or array containing events to register 33 | -- @param handler Function to call when event is triggered 34 | -- @return #Event 35 | function Event.register(event, handler) 36 | fail_if_missing(event, "missing event argument") 37 | 38 | if type(event) == "number" then 39 | event = {event} 40 | end 41 | 42 | for _, event_id in pairs(event) do 43 | fail_if_missing(event_id, "missing event id") 44 | if handler == nil then 45 | Event._registry[event_id] = nil 46 | script.on_event(event_id, nil) 47 | else 48 | if not Event._registry[event_id] then 49 | Event._registry[event_id] = {} 50 | 51 | if event_id >= 0 then 52 | script.on_event(event_id, Event.dispatch) 53 | else 54 | Event.core_events._register(event_id) 55 | end 56 | end 57 | table.insert(Event._registry[event_id], handler) 58 | end 59 | end 60 | return Event 61 | end 62 | 63 | --- Calls the registerd handlers 64 | -- @param event LuaEvent as created by game.raise_event 65 | function Event.dispatch(event) 66 | fail_if_missing(event, "missing event argument") 67 | 68 | if Event._registry[event.name] then 69 | for _, handler in pairs(Event._registry[event.name]) do 70 | local metatbl = { __index = function(tbl, key) if key == '_handler' then return handler else return rawget(tbl, key) end end } 71 | setmetatable(event, metatbl) 72 | local success, err = pcall(handler, event) 73 | if not success then 74 | -- may be nil in on_load 75 | if _G.game then 76 | if Game.print_all(err) == 0 then 77 | -- no players received the message, force a real error so someone notices 78 | error(err) 79 | end 80 | else 81 | -- no way to handle errors cleanly when the game is not up 82 | error(err) 83 | end 84 | return 85 | end 86 | if err then 87 | return 88 | end 89 | end 90 | end 91 | end 92 | 93 | --- Removes the handler from the event 94 | -- @param event event or array containing events to remove the handler 95 | -- @param handler to remove 96 | -- @return #Event 97 | function Event.remove(event, handler) 98 | fail_if_missing(event, "missing event argument") 99 | fail_if_missing(handler, "missing handler argument") 100 | 101 | if type(event) == "number" then 102 | event = {event} 103 | end 104 | 105 | for _, event_id in pairs(event) do 106 | fail_if_missing(event_id, "missing event id") 107 | if Event._registry[event_id] then 108 | for i=#Event._registry[event_id], 1, -1 do 109 | if Event._registry[event_id][i] == handler then 110 | table.remove(Event._registry[event_id], i) 111 | end 112 | end 113 | if #Event._registry[event_id] == 0 then 114 | Event._registry[event_id] = nil 115 | script.on_event(event_id, nil) 116 | end 117 | end 118 | end 119 | return Event 120 | end 121 | 122 | return Event 123 | -------------------------------------------------------------------------------- /libs/map_settings.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | require 'stdlib/table' 3 | require 'stdlib/string' 4 | 5 | Event.register(defines.events.on_tick, function(event) 6 | if not global.evo_modifier then 7 | global.evo_modifier = 0 8 | end 9 | -- enforce map settings 10 | if event.tick % 3600 == 0 then 11 | local map_settings = game.map_settings 12 | -- truncate to 3 digits (ex: 0.756) 13 | local research_progress = math.floor(Evolution.research_progress() * 1000) / 1000 14 | map_settings.steering.moving.separation_force = 0.005 15 | map_settings.steering.moving.separation_factor = 1 16 | 17 | -- cause pollution to spread farther 18 | map_settings.pollution.diffusion_ratio = math.max(0.03, 0.1 * research_progress) 19 | map_settings.pollution.min_to_diffuse = 10 20 | map_settings.pollution.expected_max_per_chunk = 6000 21 | 22 | map_settings.enemy_evolution.enabled = true 23 | map_settings.enemy_evolution.time_factor = math.min(0.000001, 0.000032 * research_progress) 24 | map_settings.enemy_evolution.pollution_factor = 0.000008 * research_progress 25 | map_settings.enemy_evolution.destroy_factor = -0.002 26 | 27 | local evo_factor = game.evolution_factor 28 | if evo_factor < 0 then 29 | game.evolution_factor = 0 30 | evo_factor = 0 31 | end 32 | global.evo_modifier = 0 33 | if evo_factor > 0.4 and not Evolution.is_any_laser_turrets_researched() then 34 | global.evo_modifier = ((evo_factor - 0.4) / 6) / 60 35 | elseif evo_factor > 0.1 and not Evolution.is_any_turrets_researched() then 36 | global.evo_modifier = ((evo_factor - 0.1) / 2) / 60 37 | end 38 | end 39 | if event.tick % 60 == 0 then 40 | if global.evo_modifier > 0 then 41 | game.evolution_factor = game.evolution_factor - global.evo_modifier 42 | end 43 | end 44 | end) 45 | 46 | Evolution = {} 47 | function Evolution.player_forces() 48 | return table.filter(game.forces, function(force, name) return name ~= 'neutral' and name ~= 'enemy' end) 49 | end 50 | 51 | function Evolution.research_progress() 52 | local researched = 0 53 | local total = 0 54 | table.each(Evolution.player_forces(), function(force, name) 55 | for tech_name, tech in pairs(force.technologies) do 56 | total = total + 1 57 | if tech.researched then 58 | researched = researched + 1 59 | end 60 | end 61 | end) 62 | if total > 0 then 63 | return researched / total 64 | end 65 | return 0 66 | end 67 | 68 | function table.is_empty(tbl) 69 | return next(tbl) ~= nil 70 | end 71 | 72 | function Evolution.is_any_laser_turrets_researched() 73 | return table.is_empty(table.filter(Evolution.player_forces(), function(force) 74 | return table.is_empty(Evolution.find_all_buildable_laser_turrets(force)) 75 | end)) 76 | end 77 | 78 | function Evolution.is_any_turrets_researched() 79 | return table.is_empty(table.filter(Evolution.player_forces(), function(force) 80 | return table.is_empty(Evolution.find_all_buildable_turrets(force)) 81 | end)) 82 | end 83 | 84 | function Evolution.find_all_buildable_laser_turrets(force) 85 | return table.filter(Evolution.find_all_buildable_turrets(force), function(prototype, name) 86 | return name and name:contains('laser') 87 | end) 88 | end 89 | 90 | -- cache for a value that only changes once per game load anyway 91 | Evolution._find_all_buildable_turrets = nil 92 | function Evolution.find_all_buildable_turrets(force) 93 | if not Evolution.__find_all_buildable_turrets then 94 | Evolution.__find_all_buildable_turrets = {} 95 | end 96 | local force_name = force.name 97 | if not Evolution.__find_all_buildable_turrets[force_name] then 98 | Evolution.__find_all_buildable_turrets[force_name] = Evolution._find_all_buildable_turrets(force) 99 | end 100 | return Evolution.__find_all_buildable_turrets[force_name] 101 | end 102 | 103 | function Evolution._find_all_buildable_turrets(force) 104 | local recipes = force.recipes 105 | return table.filter(Evolution.find_all_turret_prototypes(), function(prototype, name) 106 | return recipes[name] and recipes[name].enabled 107 | end) 108 | end 109 | 110 | function Evolution.find_all_turret_prototypes() 111 | local prototypes = {} 112 | table.each(table.filter(game.entity_prototypes, function(prototype) 113 | return Entity.has(prototype, "turret_range") and prototype.turret_range > 0 114 | end), function(prototype) 115 | prototypes[prototype.name] = prototype 116 | end) 117 | return prototypes 118 | end 119 | -------------------------------------------------------------------------------- /libs/world.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | require 'stdlib/log/logger' 3 | require 'stdlib/entity/entity' 4 | require 'stdlib/surface' 5 | require 'stdlib/table' 6 | 7 | -- require 'libs/biter/base' 8 | 9 | World = {} 10 | World.version = 70 11 | World.Logger = Logger.new("Misanthrope", "world", DEBUG_MODE) 12 | local Log = function(str, ...) World.Logger.log(string.format(str, ...)) end 13 | 14 | function World.setup() 15 | if not global.mod_version then 16 | -- goodbye fair world 17 | local old_global = global 18 | global = {} 19 | global.mod_version = 0 20 | 21 | -- Harpa.migrate(old_global) 22 | -- World.recalculate_chunk_values() 23 | end 24 | if World.version ~= global.mod_version then 25 | --World.migrate(global.mod_version, World.version) 26 | global.mod_version = World.version 27 | end 28 | end 29 | 30 | function World.migrate(old_version, new_version) 31 | Log("Migrating world data from {%s} to {%s}...", old_version, new_version) 32 | if old_version < 60 then 33 | local old_global = global 34 | global = {} 35 | global.mod_version = 60 36 | 37 | Harpa.migrate(old_global) 38 | World.recalculate_chunk_values() 39 | global.bases = {} 40 | for _, spawner in pairs(Surface.find_all_entities({ type = 'unit-spawner', surface = 'nauvis' })) do 41 | -- may already be dead if it was discovered and killed 42 | if spawner.valid then 43 | local data = Entity.get_data(spawner) 44 | if not data or not data.base then 45 | BiterBase.discover(spawner) 46 | end 47 | end 48 | end 49 | end 50 | if old_version < 67 then 51 | global.mod_version = 67 52 | global.tick_schedule = {} 53 | global.bases = table.each(table.filter(global.bases, Game.VALID_FILTER), function(base) 54 | if base.next_tick < game.tick then 55 | base.next_tick = game.tick + 60 56 | end 57 | if not global.tick_schedule[base.next_tick] then 58 | global.tick_schedule[base.next_tick] = {} 59 | end 60 | table.insert(global.tick_schedule[base.next_tick], base) 61 | end) 62 | end 63 | if old_version < 69 then 64 | global.mod_version = 69 65 | global._chunk_indexes = nil 66 | global._chunk_data = nil 67 | end 68 | if old_version < 70 then 69 | global.mod_version = 70 70 | if global.overmind then 71 | global.overmind.last_evo_boost = 0 72 | end 73 | global.bases = table.each(table.filter(global.bases, Game.VALID_FILTER), function(base) 74 | if base.targets then 75 | local max_candidates = math.min(20, #base.targets.candidates) 76 | local base_candidates = {} 77 | for i = 1, max_candidates do 78 | base_candidates[i] = base.targets.candidates[i].chunk_pos 79 | end 80 | base.targets.candidates = base_candidates 81 | end 82 | end) 83 | end 84 | end 85 | 86 | function World.all_characters(surface) 87 | local characters = {} 88 | for idx, player in pairs(game.players) do 89 | if player.valid and player.connected then 90 | local character = player.character 91 | if character and character.valid and (surface == nil or character.surface == surface) then 92 | characters[idx] = character 93 | end 94 | end 95 | end 96 | return characters 97 | end 98 | 99 | function World.closest_player_character(surface, pos, dist) 100 | local closest_char = nil 101 | local closest = dist * dist 102 | for _, character in pairs(World.all_characters(surface)) do 103 | local dist_squared = Position.distance_squared(pos, character.position) 104 | if dist_squared < closest then 105 | closest_char = character 106 | closest = dist_squared 107 | end 108 | end 109 | return closest_char 110 | end 111 | 112 | function World.get_base_at(surface, chunk) 113 | local area = Chunk.to_area(chunk) 114 | if not global.bases then return nil end 115 | for _, base in pairs(global.bases) do 116 | if base.valid and base.queen.valid and base.queen.surface == surface then 117 | if Area.inside(area, base.queen.position) then 118 | return base 119 | end 120 | end 121 | end 122 | return nil 123 | end 124 | 125 | Event.register({Event.core_events.init, Event.core_events.configuration_changed}, function(event) 126 | Log("Setting up world...") 127 | World.setup() 128 | Log("World setup complete.") 129 | end) 130 | -------------------------------------------------------------------------------- /stdlib/area/tile.lua: -------------------------------------------------------------------------------- 1 | --- Tile module 2 | ---

A tile represents a 1x1 area on a surface in factorio 3 | -- @module Tile 4 | 5 | require 'stdlib/core' 6 | require 'stdlib/area/position' 7 | require 'stdlib/area/chunk' 8 | 9 | Tile = {} 10 | MAX_UINT = 4294967296 11 | 12 | --- Calculates the tile coordinates for the position given 13 | -- @param position to calculate the tile for 14 | -- @return the tile position 15 | function Tile.from_position(position) 16 | position = Position.to_table(position) 17 | return {x = math.floor(position.x), y = math.floor(position.y)} 18 | end 19 | 20 | --- Converts a tile position to the area it contains 21 | -- @param tile_pos to convert to an area 22 | -- @return area that tile is valid for 23 | function Tile.to_area(tile_pos) 24 | fail_if_missing(tile_pos, "missing tile_pos argument") 25 | tile_pos = Tile.from_position(tile_pos) 26 | 27 | return { left_top = tile_pos, right_bottom = Position.offset(tile_pos, 1, 1) } 28 | end 29 | 30 | --- Creates a list of tile positions for all adjacent tiles (N, E, S, W) or (N, NE, E, SE, S, SW, W, NW) if diagonal is true 31 | -- @param surface to examine for adjacent tiles 32 | -- @param position the center tile position, to search around 33 | -- @param diagonal (optional: defaults to false) whether to include diagonal tiles 34 | -- @param tile_name (optional) whether to restrict adjacent tiles to one particular tile name (e.g 'water-tile') 35 | -- @return list of tile positions adjacent to the given position 36 | function Tile.adjacent(surface, position, diagonal, tile_name) 37 | fail_if_missing(surface, "missing surface argument") 38 | fail_if_missing(position, "missing position argument") 39 | 40 | local offsets = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}} 41 | if diagonal then 42 | offsets = {{0, 1}, {1, 1}, {1, 0}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}} 43 | end 44 | local adjacent_tiles = {} 45 | for _, offset in pairs(offsets) do 46 | local adj_pos = Position.add(position, offset) 47 | if tile_name then 48 | local tile = surface.get_tile(adj_pos.x, adj_pos.y) 49 | if tile and tile.name == tile_name then 50 | table.insert(adjacent_tiles, adj_pos) 51 | end 52 | else 53 | table.insert(adjacent_tiles, adj_pos) 54 | end 55 | end 56 | return adjacent_tiles 57 | end 58 | 59 | --- Gets user data from the tile, stored in a mod's global data. 60 | ---

The data will persist between loads

61 | -- @param surface the surface to look up data for 62 | -- @param tile_pos the tile coordinates to look up data for 63 | -- @param default_value (optional) to set and return if no data exists 64 | -- @return the data, or nil if no data exists for the chunk 65 | function Tile.get_data(surface, tile_pos, default_value) 66 | fail_if_missing(surface, "missing surface argument") 67 | fail_if_missing(tile_pos, "missing tile_pos argument") 68 | if not global._tile_data then 69 | if not default_value then return nil end 70 | global._tile_data = {} 71 | end 72 | local chunk_idx = Chunk.get_index(surface, Chunk.from_position(tile_pos)) 73 | if not global._tile_data[chunk_idx] then 74 | if not default_value then return nil end 75 | global._tile_data[chunk_idx] = {} 76 | end 77 | 78 | local chunk_tiles = global._tile_data[chunk_idx] 79 | if not chunk_tiles then return nil end 80 | 81 | local idx = Tile.get_index(tile_pos) 82 | local val = chunk_tiles[idx] 83 | if not val then 84 | chunk_tiles[idx] = default_value 85 | val = default_value 86 | end 87 | 88 | return val 89 | end 90 | 91 | --- Sets user data on the tile, stored in a mod's global data. 92 | ---

The data will persist between loads

93 | -- @param surface the surface to look up data for 94 | -- @param tile_pos the chunk coordinates to look up data for 95 | -- @param data the data to set (or nil to erase the data for the tile) 96 | -- @return the previous data associated with the tile, or nil if the tile had no previous data 97 | function Tile.set_data(surface, tile_pos, data) 98 | fail_if_missing(surface, "missing surface argument") 99 | fail_if_missing(tile_pos, "missing tile_pos argument") 100 | if not global._tile_data then global._tile_data = {} end 101 | 102 | local chunk_idx = Chunk.get_index(surface, Chunk.from_position(tile_pos)) 103 | if not global._tile_data[chunk_idx] then global._tile_data[chunk_idx] = {} end 104 | 105 | local chunk_tiles = global._tile_data[chunk_idx] 106 | local idx = Tile.get_index(tile_pos) 107 | local prev = chunk_tiles[idx] 108 | chunk_tiles[idx] = data 109 | 110 | return prev 111 | end 112 | 113 | --- Calculates and returns a stable, deterministic integer id for the given tile_pos 114 | ---

Tile id will not change once calculated

115 | ---

Tile ids are only unique for the chunk they are in, they may repeat across a surface.

116 | -- @param tile_pos 117 | -- @return the tile index 118 | function Tile.get_index(tile_pos) 119 | fail_if_missing(tile_pos, "missing tile_pos argument") 120 | return bit32.band(bit32.bor(bit32.lshift(bit32.band(tile_pos.x, 0x1F), 5), bit32.band(tile_pos.y, 0x1F)), 0x3FF) 121 | end 122 | -------------------------------------------------------------------------------- /stdlib/data/recipe.lua: -------------------------------------------------------------------------------- 1 | --- Recipe module 2 | -- @module Recipe 3 | 4 | require 'stdlib/data/data' 5 | 6 | Recipe = {} 7 | 8 | --- Selects all recipe values where the key matches the selector pattern. 9 | -- The selector pattern is divided into groups. The pattern should have a colon character `:` to denote the selection for each group. 10 | --
The first group is for the name of the recipe element 11 | --
The second group is for the name of keys inside of the recipe element, and is optional. If missing, all elements matching prior groups are returned. 12 | --
The third group is for the name of values inside of the recipe element, and is optional. If missing, all elements matching prior groups are returned. 13 | --

Selectors without a colon `:` separator are assumed to select all values in the first group. 14 | -- @usage Recipe.select('.*') -- returns a table with all recipes, equivalent to Data.select('recipe:.*') 15 | -- @usage Data.select('steel.*') -- returns a table with all recipes whose name matches 'steel.*' 16 | -- @usage Data.select('steel.*:ingredients') -- returns a table with all ingredients from all recipes whose name matches 'steel.*' 17 | -- @usage Data.select('steel.*:ingredients:iron-plate') -- returns a table with all iron-plate ingredient objects, from all recipes whose name matches 'steel.*' 18 | -- @param pattern to search with 19 | -- @return table containing the elements matching the selector pattern, or an empty table if there was no matches 20 | function Recipe.select(pattern) 21 | fail_if_missing(pattern, "missing pattern argument") 22 | 23 | local results = {} 24 | local parts = string.split(pattern, ":") 25 | local inner_field_pattern = #parts > 1 and parts[2] or nil 26 | 27 | if inner_field_pattern then 28 | -- Data.select --> { { recipe }, { recipe } } 29 | for _, recipe in pairs(Data.select('recipe:' .. pattern)) do 30 | for field_key, field_value in pairs(recipe) do 31 | -- field_key --> ingredients, field_value --> { { 'copper-ore', 1} } 32 | if string.match(field_key, inner_field_pattern) then 33 | local contents_field_pattern = #parts > 2 and parts[3] or nil 34 | if contents_field_pattern then 35 | -- escape the '-' in names 36 | contents_field_pattern = string.gsub(contents_field_pattern, "%-", "%%-") 37 | 38 | -- ex: field_value --> { { 'copper-ore', 1} } 39 | for _, content_value in pairs(field_value) do 40 | -- ex: content_value --> { 'copper-ore', 1} 41 | for _, content in pairs(content_value) do 42 | -- ex: content --> 'copper-ore', 1 43 | if string.match(content, contents_field_pattern) then 44 | Recipe.format_items({recipe}) 45 | table.insert(results, content_value) 46 | end 47 | end 48 | end 49 | else 50 | Recipe.format_items({recipe}) 51 | table.insert(results, field_value) 52 | end 53 | end 54 | end 55 | end 56 | else 57 | return Recipe.format_items(Data.select('recipe:' .. pattern)) 58 | end 59 | setmetatable(results, Data._select_metatable.new(results)) 60 | return results 61 | end 62 | 63 | -- this metatable is set on recipes, to control access to ingredients and results 64 | Recipe._item_metatable = {} 65 | Recipe._item_metatable.new = function(item) 66 | local self = { } 67 | self.__index = function(tbl, key) 68 | if type(key) == 'number' then 69 | local keys = { 'name', 'amount' } 70 | local val = rawget(tbl, keys[key]) 71 | -- amount defaults to one 72 | if not val and keys[key] == 'amount' then 73 | return 1 74 | end 75 | return val 76 | elseif type(key) == 'string' then 77 | local keys = { name = 1, amount = 2 } 78 | local val = rawget(tbl, keys[key]) 79 | -- amount defaults to one 80 | if not val and key == 'amount' then 81 | return 1 82 | end 83 | return val 84 | end 85 | return rawget(tbl, key) 86 | end 87 | 88 | self.__newindex = function(tbl, key, value) 89 | if type(key) == 'number' and #tbl == 0 then 90 | local keys = { 'name', 'amount' } 91 | rawset(tbl, keys[key], value) 92 | elseif type(key) == 'string' and #tbl > 0 then 93 | local keys = { name = 1, amount = 2 } 94 | rawset(tbl, keys[key], value) 95 | else 96 | return rawset(tbl, key, value) 97 | end 98 | end 99 | 100 | return self 101 | end 102 | 103 | function Recipe.format_items(recipes) 104 | recipes = recipes or data.raw.recipe 105 | table.each(recipes, function(recipe, recipe_name) 106 | if recipe.ingredients and type(recipe.ingredients) == 'table' then 107 | table.each(recipe.ingredients, function(ingredient) setmetatable(ingredient, Recipe._item_metatable.new(ingredient)) end) 108 | end 109 | if recipe.results and type(recipe.results) == 'table' then 110 | table.each(recipe.results, function(result) setmetatable(result, Recipe._item_metatable.new(result)) end) 111 | end 112 | end) 113 | return recipes 114 | end 115 | -------------------------------------------------------------------------------- /stdlib/gui/gui.lua: -------------------------------------------------------------------------------- 1 | --- Gui module 2 | -- @module Gui 3 | 4 | require 'stdlib/event/event' 5 | 6 | Gui = {} 7 | -- Factorio's gui events are so monolithic we need a special event system for it. 8 | Gui.Event = { 9 | _registry = {}, 10 | _dispatch = {} 11 | } 12 | 13 | --- Registers a function for a given event and matching gui element pattern 14 | -- @param event Valid values are defines.event.on_gui_* 15 | -- @param gui_element_pattern the name or string regular expression to match the gui element 16 | -- @param handler Function to call when event is triggered 17 | -- @return #Gui.Event 18 | function Gui.Event.register(event, gui_element_pattern, handler) 19 | fail_if_missing(event, "missing event name argument") 20 | fail_if_missing(gui_element_pattern, "missing gui name or pattern argument") 21 | 22 | if type(gui_element_pattern) ~= "string" then 23 | error("gui_element_pattern argument must be a string") 24 | end 25 | 26 | if handler == nil then 27 | Gui.Event.remove(event, gui_element_pattern) 28 | return Gui.Event 29 | end 30 | 31 | if not Gui.Event._registry[event] then 32 | Gui.Event._registry[event] = {} 33 | end 34 | Gui.Event._registry[event][gui_element_pattern] = handler 35 | 36 | -- Use custom Gui event dispatcher to pass off the event to the correct sub-handler 37 | if not Gui.Event._dispatch[event] then 38 | Event.register(event, Gui.Event.dispatch) 39 | Gui.Event._dispatch[event] = true 40 | end 41 | 42 | return Gui.Event 43 | end 44 | 45 | --- Calls the registered handlers 46 | -- @param event LuaEvent as created by game.raise_event 47 | function Gui.Event.dispatch(event) 48 | fail_if_missing(event, "missing event argument") 49 | 50 | local gui_element = event.element 51 | if gui_element and gui_element.valid then 52 | local gui_element_name = gui_element.name; 53 | local gui_element_state = nil; 54 | local gui_element_text = nil; 55 | 56 | if event.name == defines.events.on_gui_checked_state_changed then 57 | gui_element_state = gui_element.state 58 | end 59 | 60 | if event.name == defines.events.on_gui_text_changed then 61 | gui_element_text = gui_element.text 62 | end 63 | 64 | for gui_element_pattern, handler in pairs(Gui.Event._registry[event.name]) do 65 | local match_str = string.match(gui_element_name, gui_element_pattern) 66 | if match_str ~= nil then 67 | local new_event = { tick = event.tick, name = event.name, _handler = handler, match = match_str, element = gui_element, state=gui_element_state, text=gui_element_text, player_index = event.player_index , _event = event} 68 | local success, err = pcall(handler, new_event) 69 | if not success then 70 | Game.print_all(err) 71 | end 72 | end 73 | end 74 | end 75 | end 76 | 77 | --- Removes the handler with matching gui element pattern from the event 78 | -- @param event Valid values are defines.event.on_gui_* 79 | -- @param gui_element_pattern the name or string regular expression to remove the handler for 80 | -- @return #Gui.Event 81 | function Gui.Event.remove(event, gui_element_pattern) 82 | fail_if_missing(event, "missing event argument") 83 | fail_if_missing(gui_element_pattern, "missing gui_element_pattern argument") 84 | 85 | if type(gui_element_pattern) ~= "string" then 86 | error("gui_element_pattern argument must be a string") 87 | end 88 | 89 | local function tablelength(T) 90 | local count = 0 91 | for _ in pairs(T) do count = count + 1 end 92 | return count 93 | end 94 | 95 | if Gui.Event._registry[event] then 96 | if Gui.Event._registry[event][gui_element_pattern] then 97 | Gui.Event._registry[event][gui_element_pattern] = nil 98 | end 99 | if tablelength(Gui.Event._registry[event]) == 0 then 100 | Event.remove(event, Gui.Event.dispatch) 101 | Gui.Event._registry[event] = nil 102 | Gui.Event._dispatch[event] = false 103 | end 104 | end 105 | return Gui.Event 106 | end 107 | 108 | --- Registers a function for a given gui element name or pattern when the element is clicked 109 | -- @param gui_element_pattern the name or string regular expression to match the gui element 110 | -- @param handler Function to call when gui element is clicked 111 | -- @return #Gui 112 | function Gui.on_click(gui_element_pattern, handler) 113 | Gui.Event.register(defines.events.on_gui_click, gui_element_pattern, handler) 114 | return Gui 115 | end 116 | 117 | --- Registers a function for a given gui element name or pattern when the element checked state changes 118 | -- @param gui_element_pattern the name or string regular expression to match the gui element 119 | -- @param handler Function to call when gui element checked state changes 120 | -- @return #Gui 121 | function Gui.on_checked_state_changed(gui_element_pattern, handler) 122 | Gui.Event.register(defines.events.on_gui_checked_state_changed, gui_element_pattern, handler) 123 | return Gui 124 | end 125 | 126 | --- Registers a function for a given gui element name or pattern when the element text changes 127 | -- @param gui_element_pattern the name or string regular expression to match the gui element 128 | -- @param handler Function to call when gui element text changes 129 | -- @return #Gui 130 | function Gui.on_text_changed(gui_element_pattern, handler) 131 | Gui.Event.register(defines.events.on_gui_text_changed, gui_element_pattern, handler) 132 | return Gui 133 | end 134 | -------------------------------------------------------------------------------- /libs/biter/overwatch.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | require 'stdlib/log/logger' 3 | require 'stdlib/entity/entity' 4 | require 'stdlib/area/position' 5 | require 'stdlib/area/area' 6 | require 'stdlib/table' 7 | require 'stdlib/game' 8 | 9 | Overwatch = {stages = {}, tick_rates = {}} 10 | Overwatch.Logger = Logger.new("Misanthrope", "overwatch", false) 11 | local Log = function(str, ...) Overwatch.Logger.log(string.format(str, ...)) end 12 | 13 | Event.register(defines.events.on_tick, function(event) 14 | if not global.overwatch then 15 | global.overwatch = { tick_rate = 600, stage = 'setup', data = {}, chunks = {}, valuable_chunks = {}, surface = game.surfaces.nauvis} 16 | end 17 | if not (event.tick % global.overwatch.tick_rate == 0) then return end 18 | 19 | local prev_stage = global.overwatch.stage 20 | local data = global.overwatch.data 21 | local stage = Overwatch.stages[prev_stage](data) 22 | if data.reset then 23 | global.overwatch.data = {} 24 | end 25 | global.overwatch.stage = stage 26 | if prev_stage ~= stage then 27 | Log("Updating stage from %s to %s", prev_stage, stage) 28 | global.overwatch.tick_rate = Overwatch.tick_rates[stage] 29 | end 30 | end) 31 | 32 | Overwatch.tick_rates.setup = Time.SECOND * 10 33 | Overwatch.stages.setup = function(data) 34 | local chunks = global.overwatch.chunks 35 | for chunk in global.overwatch.surface.get_chunks() do 36 | table.insert(chunks, chunk) 37 | end 38 | Log("Found %d chunks to scan", #chunks) 39 | return 'scan_chunk' 40 | end 41 | 42 | Overwatch.tick_rates.decide = Time.SECOND * 10 43 | Overwatch.stages.decide = function(data) 44 | if #global.overwatch.valuable_chunks > 100 then 45 | return 'decide' 46 | end 47 | return 'setup' 48 | end 49 | 50 | Overwatch.tick_rates.scan_chunk = 120 51 | Overwatch.stages.scan_chunk = function(data) 52 | if #global.overwatch.chunks == 0 then 53 | return 'decide' 54 | end 55 | if #global.overwatch.chunks % 100 == 0 then 56 | Log("Currently %d chunks in queue to be scanned", #global.overwatch.chunks) 57 | end 58 | local chunk = table.remove(global.overwatch.chunks, math.random(1, #global.overwatch.chunks)) 59 | local surface = global.overwatch.surface 60 | 61 | local area = Chunk.to_area(chunk) 62 | local chunk_center = Area.center(area) 63 | 64 | if surface.count_entities_filtered({type = 'unit-spawner', area = Area.expand(area, 64), force = game.forces.enemy}) > 0 then 65 | Log("Chunk %s had biter spawners within 2 chunks", Chunk.to_string(chunk)) 66 | return 'scan_chunk' 67 | end 68 | 69 | local pos = surface.find_non_colliding_position('biter-spawner', chunk_center, 16, 1) 70 | if not pos or not Area.inside(area, pos) then 71 | local pos = surface.find_non_colliding_position('medium-biter', chunk_center, 16, 1) 72 | if not pos or not Area.inside(area, pos) then 73 | Log("Chunk %s had no suitable location for spawner or biter", Chunk.to_string(chunk)) 74 | return 'scan_chunk' 75 | else 76 | Log("Chunk %s had no suitable location for spawner, but may support spawning biters", Chunk.to_string(chunk)) 77 | data.chunk = chunk 78 | data.adjacent = {} 79 | data.spawn = false 80 | data.nearby_bases = 0 81 | data.value = 0 82 | data.best = { value = 0, chunk = nil } 83 | for x, y in Area.iterate(Position.expand_to_area(chunk, 7)) do 84 | table.insert(data.adjacent, {x = x, y = y}) 85 | end 86 | return 'analyze_base' 87 | end 88 | end 89 | Log("Chunk %s had a suitable location for spawner", Chunk.to_string(chunk)) 90 | data.chunk = chunk 91 | data.adjacent = {} 92 | data.spawn = true 93 | data.nearby_bases = 0 94 | data.value = 0 95 | data.best = { value = 0, chunk = nil } 96 | for x, y in Area.iterate(Position.expand_to_area(chunk, 15)) do 97 | table.insert(data.adjacent, {x = x, y = y}) 98 | end 99 | 100 | return 'analyze_base' 101 | end 102 | 103 | Overwatch.tick_rates.evaluate_base = 30 104 | Overwatch.stages.evaluate_base = function(data) 105 | local value = math.floor(data.value) 106 | local nearby_bases = data.nearby_bases 107 | value = (value * 6) / (1 + nearby_bases) 108 | 109 | local surface = global.overwatch.surface 110 | local area = Chunk.to_area(data.chunk) 111 | local player_entities = surface.count_entities_filtered({area = Area.expand(area, 32 * 5), force = game.forces.player}) 112 | if player_entities > 0 then 113 | value = value / (math.sqrt(player_entities)) 114 | end 115 | 116 | value = math.floor(value) 117 | if value > 1000 then 118 | Log("Finished evaluating chunk %s, its value is %d", Chunk.to_string(data.chunk), value) 119 | table.insert(global.overwatch.valuable_chunks, { chunk = data.chunk, value = value, spawn = data.spawn, best_target = data.best }) 120 | else 121 | Log("Finished evaluating chunk %s, value too low, its value is %d", Chunk.to_string(data.chunk), value) 122 | end 123 | data.reset = true 124 | return 'scan_chunk' 125 | end 126 | 127 | Overwatch.tick_rates.analyze_base = 10 128 | Overwatch.stages.analyze_base = function(data) 129 | -- finished evaluation 130 | if #data.adjacent == 0 then 131 | return 'evaluate_base' 132 | end 133 | 134 | local surface = global.overwatch.surface 135 | local adjacent_chunks = data.adjacent 136 | local limit = math.max(1, #adjacent_chunks - 25) 137 | for i = #adjacent_chunks, limit, -1 do 138 | local adj_chunk = adjacent_chunks[i] 139 | adjacent_chunks[i] = nil 140 | if adj_chunk then 141 | local chunk_value = World.get_chunk_value(surface, adj_chunk) 142 | if chunk_value ~= 0 then 143 | -- count negative value as positive, as it represents hardened player structures 144 | data.value = data.value + math.abs(chunk_value) 145 | 146 | if chunk_value > data.best.value then 147 | data.best.value = chunk_value 148 | data.best.chunk = adj_chunk 149 | end 150 | end 151 | end 152 | end 153 | return 'analyze_base' 154 | end 155 | -------------------------------------------------------------------------------- /stdlib/area/position.lua: -------------------------------------------------------------------------------- 1 | --- Position module 2 | -- @module Position 3 | 4 | Position = {} 5 | 6 | require 'stdlib/core' 7 | 8 | --- Creates a position that is offset by x,y coordinate pair 9 | -- @param pos the position to offset 10 | -- @param x the amount to offset the position in the x direction 11 | -- @param y the amount to offset the position in the y direction 12 | -- @return a new position, offset by the x,y coordinates 13 | function Position.offset(pos, x, y) 14 | fail_if_missing(pos, "missing position argument") 15 | fail_if_missing(x, "missing x-coordinate value") 16 | fail_if_missing(y, "missing y-coordinate value") 17 | 18 | if #pos == 2 then 19 | return { x = pos[1] + x, y = pos[2] + y } 20 | else 21 | return { x = pos.x + x, y = pos.y + y } 22 | end 23 | end 24 | 25 | --- Adds 2 positions 26 | -- @param pos1 the first position 27 | -- @param pos2 the second position 28 | -- @return a new position 29 | function Position.add(pos1, pos2) 30 | fail_if_missing(pos1, "missing first position argument") 31 | fail_if_missing(pos2, "missing second position argument") 32 | 33 | pos1 = Position.to_table(pos1) 34 | pos2 = Position.to_table(pos2) 35 | return { x = pos1.x + pos2.x, y = pos1.y + pos2.y} 36 | end 37 | 38 | --- Subtracts 2 positions 39 | -- @param pos1 the first position 40 | -- @param pos2 the second position 41 | -- @return a new position 42 | function Position.subtract(pos1, pos2) 43 | fail_if_missing(pos1, "missing first position argument") 44 | fail_if_missing(pos2, "missing second position argument") 45 | 46 | pos1 = Position.to_table(pos1) 47 | pos2 = Position.to_table(pos2) 48 | return { x = pos1.x - pos2.x, y = pos1.y - pos2.y } 49 | end 50 | 51 | --- Translates a position in the given direction 52 | -- @param pos the position to translate 53 | -- @param direction in which direction to translate (see defines.direction) 54 | -- @param distance distance of the translation 55 | -- @return the translated position 56 | function Position.translate(pos, direction, distance) 57 | fail_if_missing(pos, "missing position argument") 58 | fail_if_missing(direction, "missing direction argument") 59 | fail_if_missing(distance, "missing distance argument") 60 | 61 | pos = Position.to_table(pos) 62 | 63 | if direction == defines.direction.north then 64 | return { x = pos.x, y = pos.y - distance } 65 | elseif direction == defines.direction.northeast then 66 | return { x = pos.x + distance, y = pos.y - distance } 67 | elseif direction == defines.direction.east then 68 | return { x = pos.x + distance, y = pos.y } 69 | elseif direction == defines.direction.southeast then 70 | return { x = pos.x + distance, y = pos.y + distance } 71 | elseif direction == defines.direction.south then 72 | return { x = pos.x, y = pos.y + distance } 73 | elseif direction == defines.direction.southwest then 74 | return { x = pos.x - distance, y = pos.y + distance } 75 | elseif direction == defines.direction.west then 76 | return { x = pos.x - distance, y = pos.y } 77 | elseif direction == defines.direction.northwest then 78 | return { x = pos.x - distance, y = pos.y - distance } 79 | end 80 | end 81 | 82 | --- Expands a position to a square area 83 | -- @param pos the position to expand into an area 84 | -- @param radius half the side length of the area 85 | -- @return a bounding box 86 | function Position.expand_to_area(pos, radius) 87 | fail_if_missing(pos, "missing position argument") 88 | fail_if_missing(radius, "missing radius argument") 89 | 90 | if #pos == 2 then 91 | return { left_top = { x = pos[1] - radius, y = pos[2] - radius }, right_bottom = { x = pos[1] + radius, y = pos[2] + radius } } 92 | end 93 | return { left_top = { x = pos.x - radius, y = pos.y - radius}, right_bottom = { x = pos.x + radius, y = pos.y + radius } } 94 | end 95 | 96 | --- Calculates the Euclidean distance squared between two positions, useful when sqrt is not needed 97 | -- @param pos1 the first position 98 | -- @param pos2 the second position 99 | -- @return the square of the Euclidean distance 100 | function Position.distance_squared(pos1, pos2) 101 | fail_if_missing(pos1, "missing first position argument") 102 | fail_if_missing(pos2, "missing second position argument") 103 | 104 | pos1 = Position.to_table(pos1) 105 | pos2 = Position.to_table(pos2) 106 | local axbx = pos1.x - pos2.x 107 | local ayby = pos1.y - pos2.y 108 | return axbx * axbx + ayby * ayby 109 | end 110 | 111 | --- Calculates the Euclidean distance between two positions 112 | -- @param pos1 the first position 113 | -- @param pos2 the second position 114 | -- @return the square of the Euclidean distance 115 | function Position.distance(pos1, pos2) 116 | fail_if_missing(pos1, "missing first position argument") 117 | fail_if_missing(pos2, "missing second position argument") 118 | 119 | return math.sqrt(Position.distance_squared(pos1, pos2)) 120 | end 121 | 122 | --- Calculates the manhatten distance between two positions 123 | -- @param pos1 the first position 124 | -- @param pos2 the second position 125 | -- @return the square of the Euclidean distance 126 | function Position.manhattan_distance(pos1, pos2) 127 | fail_if_missing(pos1, "missing first position argument") 128 | fail_if_missing(pos2, "missing second position argument") 129 | pos1 = Position.to_table(pos1) 130 | pos2 = Position.to_table(pos2) 131 | 132 | return math.abs(pos2.x - pos1.x) + math.abs(pos2.y - pos1.y) 133 | end 134 | 135 | -- see: https://en.wikipedia.org/wiki/Machine_epsilon 136 | Position._epsilon = 1.19e-07 137 | 138 | --- Whether 2 positions are equal 139 | -- @param pos1 the first position 140 | -- @param pos2 the second position 141 | -- @return true if positions are equal 142 | function Position.equals(pos1, pos2) 143 | if not pos1 or not pos2 then return false end 144 | -- optimize for a shallow equality check first 145 | if pos1 == pos2 then return true end 146 | 147 | local epsilon = Position._epsilon 148 | local abs = math.abs 149 | if #pos1 == 2 and #pos2 == 2 then 150 | return abs(pos1[1] - pos2[1]) < epsilon and abs(pos1[2] - pos2[2]) < epsilon 151 | elseif #pos1 == 2 and #pos2 == 0 then 152 | return abs(pos1[1] - pos2.x) < epsilon and abs(pos1[2] - pos2.y) < epsilon 153 | elseif #pos1 == 0 and #pos2 == 2 then 154 | return abs(pos1.x - pos2[1]) < epsilon and abs(pos1.y - pos2[2]) < epsilon 155 | elseif #pos1 == 0 and #pos2 == 0 then 156 | return abs(pos1.x - pos2.x) < epsilon and abs(pos1.y - pos2.y) < epsilon 157 | end 158 | 159 | return false 160 | end 161 | 162 | --- Converts a position in the array format to a position in the table format 163 | -- @param pos_arr the position to convert 164 | -- @return a converted position, { x = pos_arr[1], y = pos_arr[2] } 165 | function Position.to_table(pos_arr) 166 | fail_if_missing(pos_arr, "missing position argument") 167 | 168 | if #pos_arr == 2 then 169 | return { x = pos_arr[1], y = pos_arr[2] } 170 | end 171 | return pos_arr 172 | end 173 | 174 | --- Converts a position to a string 175 | -- @param pos the position to convert 176 | -- @return string representation of pos 177 | function Position.tostring(pos) 178 | fail_if_missing(pos, "missing position argument") 179 | if #pos == 2 then 180 | return "Position {x = " .. pos[1] .. ", y = " .. pos[2] .. "}" 181 | else 182 | return "Position {x = " .. pos.x .. ", y = " .. pos.y .. "}" 183 | end 184 | end 185 | 186 | return Position 187 | -------------------------------------------------------------------------------- /stdlib/area/area.lua: -------------------------------------------------------------------------------- 1 | --- Area module 2 | -- @module Area 3 | 4 | require 'stdlib/core' 5 | require 'stdlib/area/position' 6 | 7 | Area = {} 8 | 9 | --- Returns the size of the space contained in the 2d area 10 | -- @param area the area 11 | -- @return size of the area 12 | function Area.area(area) 13 | fail_if_missing(area, "missing area value") 14 | area = Area.to_table(area) 15 | 16 | local left_top = Position.to_table(area.left_top) 17 | local right_bottom = Position.to_table(area.right_bottom) 18 | 19 | local dx = math.abs(left_top.x - right_bottom.x) 20 | local dy = math.abs(left_top.y - right_bottom.y) 21 | return dx * dy 22 | end 23 | 24 | --- Tests if a position {x, y} is inside (inclusive) of area 25 | -- @param area the area 26 | -- @param pos the position to check 27 | -- @return true if the position is inside of the area 28 | function Area.inside(area, pos) 29 | fail_if_missing(pos, "missing pos value") 30 | fail_if_missing(area, "missing area value") 31 | pos = Position.to_table(pos) 32 | area = Area.to_table(area) 33 | 34 | local left_top = Position.to_table(area.left_top) 35 | local right_bottom = Position.to_table(area.right_bottom) 36 | return pos.x >= left_top.x and pos.y >= left_top.y and pos.x <= right_bottom.x and pos.y <= right_bottom.y 37 | end 38 | 39 | --- Shrinks the size of an area by the given amount 40 | -- @param area the area 41 | -- @param amount to shrink each edge of the area inwards by 42 | -- @return the shrunk area 43 | function Area.shrink(area, amount) 44 | fail_if_missing(area, "missing area value") 45 | fail_if_missing(amount, "missing amount value") 46 | if amount < 0 then error("Can not shrunk area by a negative amount (see Area.expand)!", 2) end 47 | area = Area.to_table(area) 48 | 49 | local left_top = Position.to_table(area.left_top) 50 | local right_bottom = Position.to_table(area.right_bottom) 51 | return {left_top = {x = left_top.x + amount, y = left_top.y + amount}, right_bottom = {x = right_bottom.x - amount, y = right_bottom.y - amount}} 52 | end 53 | 54 | --- Expands the size of an area by the given amount 55 | -- @param area the area 56 | -- @param amount to expand each edge of the area outwards by 57 | -- @return the expanded area 58 | function Area.expand(area, amount) 59 | fail_if_missing(area, "missing area value") 60 | fail_if_missing(amount, "missing amount value") 61 | if amount < 0 then error("Can not expand area by a negative amount (see Area.shrink)!", 2) end 62 | area = Area.to_table(area) 63 | 64 | local left_top = Position.to_table(area.left_top) 65 | local right_bottom = Position.to_table(area.right_bottom) 66 | return {left_top = {x = left_top.x - amount, y = left_top.y - amount}, right_bottom = {x = right_bottom.x + amount, y = right_bottom.y + amount}} 67 | end 68 | 69 | --- Calculates the center of the area and returns the position 70 | -- @param area the area 71 | -- @return area to find the center for 72 | function Area.center(area) 73 | fail_if_missing(area, "missing area value") 74 | area = Area.to_table(area) 75 | 76 | local dist_x = area.right_bottom.x - area.left_top.x 77 | local dist_y = area.right_bottom.y - area.left_top.y 78 | 79 | return {x = area.left_top.x + (dist_x / 2), y = area.left_top.y + (dist_y / 2)} 80 | end 81 | 82 | --- Offsets the area by the {x, y} values 83 | -- @param area the area 84 | -- @param pos the {x, y} amount to offset the area 85 | -- @return offset area by the position values 86 | function Area.offset(area, pos) 87 | fail_if_missing(area, "missing area value") 88 | fail_if_missing(pos, "missing pos value") 89 | area = Area.to_table(area) 90 | 91 | return {left_top = Position.add(area.left_top, pos), right_bottom = Position.add(area.right_bottom, pos)} 92 | end 93 | 94 | --- Converts an area to the integer representation, by taking the floor of the left_top and the ceiling of the right_bottom 95 | -- @param area the area 96 | -- @return the rounded integer representation 97 | function Area.round_to_integer(area) 98 | fail_if_missing(area, "missing area value") 99 | area = Area.to_table(area) 100 | 101 | local left_top = Position.to_table(area.left_top) 102 | local right_bottom = Position.to_table(area.right_bottom) 103 | return {left_top = {x = math.floor(left_top.x), y = math.floor(left_top.y)}, 104 | right_bottom = {x = math.ceil(right_bottom.x), y = math.ceil(right_bottom.y)}} 105 | end 106 | 107 | --- Iterates an area. 108 | -- @usage 109 | ---for x,y in Area.iterate({{0, -5}, {3, -3}}) do 110 | -----... 111 | ---end 112 | -- @param area the area 113 | -- @return iterator 114 | function Area.iterate(area) 115 | fail_if_missing(area, "missing area value") 116 | 117 | local iterator = {idx = 0} 118 | function iterator.iterate(area) 119 | local rx = area.right_bottom.x - area.left_top.x + 1 120 | local dx = iterator.idx % rx 121 | local dy = math.floor(iterator.idx / rx) 122 | iterator.idx = iterator.idx + 1 123 | if (area.left_top.y + dy) > area.right_bottom.y then 124 | return 125 | end 126 | return (area.left_top.x + dx), (area.left_top.y + dy) 127 | end 128 | return iterator.iterate, Area.to_table(area), 0 129 | end 130 | 131 | --- Iterates an area in a spiral inner-most to outer-most fashion. 132 | ---

Example:

133 | ---
134 | ---for x, y in Area.spiral_iterate({{-2, -1}, {2, 1}}) do
135 | ----  print("(" .. x .. ", " .. y .. ")")
136 | ---end
137 | --- prints: (0, 0) (1, 0) (1, 1) (0, 1) (-1, 1) (-1, 0) (-1, -1) (0, -1) (1, -1) (2, -1) (2, 0) (2, 1) (-2, 1) (-2, 0) (-2, -1)
138 | ---
139 | -- iterates in the order depicted:
140 | -- ![](http://i.imgur.com/EwfO0Es.png) 141 | -- @param area the area 142 | -- @return iterator 143 | function Area.spiral_iterate(area) 144 | fail_if_missing(area, "missing area value") 145 | area = Area.to_table(area) 146 | 147 | local rx = area.right_bottom.x - area.left_top.x + 1 148 | local ry = area.right_bottom.y - area.left_top.y + 1 149 | local half_x = math.floor(rx / 2) 150 | local half_y = math.floor(ry / 2) 151 | local center_x = area.left_top.x + half_x 152 | local center_y = area.left_top.y + half_y 153 | 154 | local x = 0 155 | local y = 0 156 | local dx = 0 157 | local dy = -1 158 | local iterator = {list = {}, idx = 1} 159 | for i = 1, math.max(rx, ry) * math.max(rx, ry) do 160 | if -(half_x) <= x and x <= half_x and -(half_y) <= y and y <= half_y then 161 | table.insert(iterator.list, {x, y}) 162 | end 163 | if x == y or (x < 0 and x == -y) or (x > 0 and x == 1 - y) then 164 | local temp = dx 165 | dx = -(dy) 166 | dy = temp 167 | end 168 | x = x + dx 169 | y = y + dy 170 | end 171 | 172 | function iterator.iterate(area) 173 | if #iterator.list < iterator.idx then return end 174 | local x, y = unpack(iterator.list[iterator.idx]) 175 | iterator.idx = iterator.idx + 1 176 | 177 | return (center_x + x), (center_y + y) 178 | end 179 | return iterator.iterate, Area.to_table(area), 0 180 | end 181 | 182 | --- Converts an area in the array format to an array in the table format 183 | -- @param area_arr the area to convert 184 | -- @return a converted position, { x = pos_arr[1], y = pos_arr[2] } 185 | function Area.to_table(area_arr) 186 | fail_if_missing(area_arr, "missing area value") 187 | if #area_arr == 2 then 188 | return { left_top = Position.to_table(area_arr[1]), right_bottom = Position.to_table(area_arr[2]) } 189 | end 190 | return area_arr 191 | end 192 | 193 | return Area 194 | -------------------------------------------------------------------------------- /libs/pathfinder.lua: -------------------------------------------------------------------------------- 1 | -- Adapted from: https://github.com/lattejed/a-star-lua/blob/master/a-star.lua 2 | 3 | -- ====================================================================== 4 | -- Copyright (c) 2012 RapidFire Studio Limited 5 | -- All Rights Reserved. 6 | -- http://www.rapidfirestudio.com 7 | 8 | -- Permission is hereby granted, free of charge, to any person obtaining 9 | -- a copy of this software and associated documentation files (the 10 | -- "Software"), to deal in the Software without restriction, including 11 | -- without limitation the rights to use, copy, modify, merge, publish, 12 | -- distribute, sublicense, and/or sell copies of the Software, and to 13 | -- permit persons to whom the Software is furnished to do so, subject to 14 | -- the following conditions: 15 | 16 | -- The above copyright notice and this permission notice shall be 17 | -- included in all copies or substantial portions of the Software. 18 | 19 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -- ====================================================================== 27 | 28 | require 'stdlib/area/tile' 29 | 30 | pathfinder = {} 31 | pathfinder.__index = pathfinder 32 | 33 | -- Partially search for a path on the given surface between the start_pos and goal_pos 34 | -- If the search completes, the path object will be inside of the returned table { completed = true, path = { ... }} 35 | -- If the search is not yet completed, the returned table will be { completed = false, ... } 36 | -- Pathfinding can be resumed with pathfinder.resume_a_star 37 | function pathfinder.partial_a_star(surface, start_pos, goal_pos, max_iterations, max_total_iterations) 38 | local start_tile = Tile.from_position(start_pos) 39 | local goal_tile = Tile.from_position(goal_pos) 40 | 41 | local closed_set = {} 42 | local open_set = {} 43 | open_set[pathfinder.node_key(start_tile)] = start_tile 44 | local came_from = {} 45 | 46 | local g_score = {} 47 | local f_score = {} 48 | g_score[pathfinder.node_key(start_tile)] = 0 49 | f_score[pathfinder.node_key(start_tile)] = pathfinder.heuristic_cost_estimate(start_tile, goal_tile) 50 | 51 | local pathfinding_data = 52 | { 53 | surface = surface, 54 | start_pos = start_tile, 55 | goal_pos = goal_tile, 56 | closed_set = closed_set, 57 | open_set = open_set, 58 | came_from = came_from, 59 | g_score = g_score, 60 | f_score = f_score, 61 | iterations = 0, 62 | max_total_iterations = max_total_iterations, 63 | completed = false 64 | } 65 | return pathfinder.resume_a_star(pathfinding_data, max_iterations) 66 | end 67 | 68 | -- Resumes an uncomplete pathfinding search, given the partially completed data and max iterations 69 | function pathfinder.resume_a_star(pathfinding_data, max_iterations) 70 | for i = 1, max_iterations do 71 | local result = pathfinder.step_a_star(pathfinding_data) 72 | if pathfinding_data.completed then 73 | return { completed = true, path = result } 74 | end 75 | end 76 | return pathfinding_data 77 | end 78 | 79 | -- Find a complete path on the given surface between the start_pos and goal_pos 80 | function pathfinder.a_star(surface, start_pos, goal_pos, max_total_iterations) 81 | local start_tile = Tile.from_position(start_pos) 82 | local goal_tile = Tile.from_position(goal_pos) 83 | 84 | local closed_set = {} 85 | local open_set = {} 86 | open_set[pathfinder.node_key(start_tile)] = start_tile 87 | local came_from = {} 88 | 89 | local g_score = {} 90 | local f_score = {} 91 | g_score[pathfinder.node_key(start_tile)] = 0 92 | f_score[pathfinder.node_key(start_tile)] = pathfinder.heuristic_cost_estimate(start_tile, goal_tile) 93 | 94 | local pathfinding_data = 95 | { 96 | surface = surface, 97 | start_pos = start_tile, 98 | goal_pos = goal_tile, 99 | closed_set = closed_set, 100 | open_set = open_set, 101 | came_from = came_from, 102 | g_score = g_score, 103 | f_score = f_score, 104 | iterations = 0, 105 | max_total_iterations = max_total_iterations, 106 | completed = false 107 | } 108 | while not pathfinding_data.completed do 109 | local result = pathfinder.step_a_star(pathfinding_data) 110 | if pathfinding_data.completed then 111 | return result 112 | end 113 | end 114 | return nil 115 | end 116 | 117 | function pathfinder.step_a_star(data) 118 | if data.iterations > data.max_total_iterations then 119 | World.Logger.log(string.format("step_a_star failed, %d iterations exceeded max iterations (%d)", data.iterations, data.max_total_iterations)) 120 | --World.Logger.log("step_a_star state: " .. string.block(data)) 121 | data.completed = true 122 | return nil 123 | end 124 | data.iterations = data.iterations + 1 125 | 126 | local current = pathfinder.lowest_f_score(data.open_set, data.f_score) 127 | if not current then 128 | World.Logger.log(string.format("step_a_star failed, %d iterations found no path to target", data.iterations)) 129 | --World.Logger.log("step_a_star state: " .. string.block(data)) 130 | data.completed = true 131 | return nil 132 | end 133 | 134 | if current.x == data.goal_pos.x and current.y == data.goal_pos.y then 135 | local path = pathfinder.unwind_path({}, data.came_from, data.goal_pos) 136 | table.insert(path, data.goal_pos) 137 | data.completed = true 138 | World.Logger.log(string.format("step_a_star completed, %d iterations ", data.iterations)) 139 | return path 140 | end 141 | 142 | local current_key = pathfinder.node_key(current) 143 | data.open_set[current_key] = nil 144 | data.closed_set[current_key] = true 145 | 146 | local neighbors = pathfinder.neighbor_nodes(data.surface, current) 147 | for _, neighbor in pairs(neighbors) do 148 | local key = pathfinder.node_key(neighbor) 149 | if not data.closed_set[key] then 150 | local tentative_g_score = data.g_score[current_key] + pathfinder.heuristic_cost_estimate(current, neighbor) 151 | 152 | local neighbor_key = pathfinder.node_key(neighbor) 153 | if not data.open_set[key] or tentative_g_score < data.g_score[neighbor_key] then 154 | data.came_from[neighbor_key] = current 155 | data.g_score[neighbor_key] = tentative_g_score 156 | data.f_score[neighbor_key] = data.g_score[neighbor_key] + pathfinder.heuristic_cost_estimate(neighbor, data.goal_pos) 157 | if not data.open_set[key] then 158 | data.open_set[key] = neighbor 159 | end 160 | end 161 | end 162 | end 163 | end 164 | 165 | function pathfinder.node_key(pos) 166 | local key = bit32.bor(bit32.lshift(bit32.band(pos.x, 0xFFFF), 16), bit32.band(pos.y, 0xFFFF)) 167 | --World.Logger.log(string.format("%s - %d", serpent.line(pos), key)) 168 | return key 169 | end 170 | 171 | function pathfinder.heuristic_cost_estimate(nodeA, nodeB) 172 | return math.abs(nodeB.x - nodeA.x) + math.abs(nodeB.y - nodeA.y) 173 | end 174 | 175 | function pathfinder.neighbor_nodes(surface, center_node) 176 | local neighbors = {} 177 | local adjacent = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}} 178 | for _, tuple in pairs(adjacent) do 179 | if not string.find(surface.get_tile(center_node.x + tuple[1], center_node.y + tuple[2]).name, "water", 1, true) then 180 | table.insert(neighbors, {x = center_node.x + tuple[1], y = center_node.y + tuple[2]}) 181 | end 182 | end 183 | return neighbors 184 | end 185 | 186 | function pathfinder.unwind_path(flat_path, map, current_node) 187 | local map_value = map[pathfinder.node_key(current_node)] 188 | if map_value then 189 | table.insert(flat_path, 1, map_value) 190 | return pathfinder.unwind_path(flat_path, map, map_value) 191 | else 192 | return flat_path 193 | end 194 | end 195 | 196 | function pathfinder.lowest_f_score(set, f_score) 197 | local lowest, best_node = nil, nil 198 | for key, node in pairs(set) do 199 | local score = f_score[key] 200 | if lowest == nil or score < lowest then 201 | lowest, best_node = score, node 202 | end 203 | end 204 | return best_node 205 | end 206 | -------------------------------------------------------------------------------- /stdlib/table.lua: -------------------------------------------------------------------------------- 1 | --- Table module 2 | -- @module table 3 | 4 | --- Given a mapping function, creates a transformed copy of the table 5 | --- by calling the function for each element in the table, and using 6 | --- the result as the new value for the key. Passes the index as second argument to the function. 7 | --- @usage a= { 1, 2, 3, 4, 5} 8 | ---table.map(a, function(v) return v * 10 end) --produces: { 10, 20, 30, 40, 50 } 9 | --- @usage a = {1, 2, 3, 4, 5} 10 | ---table.map(a, function(v, k, x) return v * k + x end, 100) --produces { 101, 104, 109, 116, 125} 11 | -- @param tbl to be mapped to the transform 12 | -- @param func to transform values 13 | -- @param[opt] ... additional arguments passed to the function 14 | -- @return a new table containing the keys and mapped values 15 | function table.map(tbl, func, ...) 16 | local newtbl = {} 17 | for i, v in pairs(tbl) do 18 | newtbl[i] = func(v, i, ...) 19 | end 20 | return newtbl 21 | end 22 | 23 | --- Given a filter function, creates a filtered copy of the table 24 | --- by calling the function for each element in the table, and 25 | --- filtering out any key-value pairs for non-true results. Passes the index as second argument to the function. 26 | --- @usage a= { 1, 2, 3, 4, 5} 27 | ---table.filter(a, function(v) return v % 2 == 0 end) --produces: { 2, 4 } 28 | --- @usage a = {1, 2, 3, 4, 5} 29 | ---table.filter(a, function(v, k, x) return k % 2 == 1 end) --produces: { 1, 3, 5 } 30 | -- @param tbl to be filtered 31 | -- @param func to filter values 32 | -- @param[opt] ... additional arguments passed to the function 33 | -- @return a new table containing the filtered key-value pairs 34 | function table.filter(tbl, func, ...) 35 | local newtbl = {} 36 | local insert = #tbl > 0 37 | for k, v in pairs(tbl) do 38 | if func(v, k, ...) then 39 | if insert then table.insert(newtbl, v) 40 | else newtbl[k] = v end 41 | end 42 | end 43 | return newtbl 44 | end 45 | 46 | --- Given a function, apply it to each element in the table. Passes the index as second argument to the function. 47 | --

Iteration is aborted if the applied function returns true for any element during iteration. 48 | -- @param tbl to be iterated 49 | -- @param func to apply to values 50 | -- @param[opt] ... additional arguments passed to the function 51 | -- @return the given table 52 | function table.each(tbl, func, ...) 53 | for k, v in pairs(tbl) do 54 | if func(v, k, ...) then 55 | break 56 | end 57 | end 58 | return tbl 59 | end 60 | 61 | --- Returns a new table that is a one-dimensional flattening of this array (recursively). 62 | -- For every element that is an table, extract its elements into the new array. 63 | -- The optional level argument determines the level of recursion to flatten. 64 | --

Note: does not flatten associative elements, only arrays 65 | -- @param tbl to be flattened 66 | -- @param level (optional) recursive levels, or no limit to recursion if not supplied 67 | -- @return a new table that represents the flattened contents of the given table 68 | function table.flatten(tbl, level) 69 | local flattened = {} 70 | table.each(tbl, function(value) 71 | if type(value) == "table" and #value > 0 then 72 | if level then 73 | if level > 0 then 74 | table.merge(flattened, table.flatten(value, level - 1), true) 75 | else 76 | table.insert(flattened, value) 77 | end 78 | else 79 | table.merge(flattened, table.flatten(value), true) 80 | end 81 | else 82 | table.insert(flattened, value) 83 | end 84 | end) 85 | return flattened 86 | end 87 | 88 | --- Given an array, returns the first element or nil if no element exists 89 | -- @param tbl the array 90 | -- @return the first element 91 | function table.first(tbl) 92 | return tbl[1] 93 | end 94 | 95 | --- Given an array, returns the last element or nil if no elements exist 96 | -- @param tbl the array 97 | -- @return the last element 98 | function table.last(tbl) 99 | local size = #tbl 100 | if size == 0 then return nil end 101 | return tbl[size] 102 | end 103 | 104 | --- Merges 2 tables, values from first get overwritten by second 105 | --- @usage function some_func(x, y, args) 106 | -- args = table.merge({option1=false}, args) 107 | -- if opts.option1 == true then return x else return y end 108 | -- end 109 | -- some_func(1,2) --returns 2 110 | -- some_func(1,2,{option1=true}) --returns 1 111 | -- @param tblA first table 112 | -- @param tblB second table 113 | -- @param array_merge (optional: false) whether to merge the tables as arrays, or associatively 114 | -- @return tblA with merged values from tblB 115 | function table.merge(tblA, tblB, array_merge) 116 | if not tblB then 117 | return tblA 118 | end 119 | if array_merge then 120 | for _, v in pairs(tblB) do 121 | table.insert(tblA, v) 122 | end 123 | 124 | else 125 | for k, v in pairs(tblB) do 126 | tblA[k] = v 127 | end 128 | end 129 | return tblA 130 | end 131 | 132 | -- copied from factorio/data/core/luablib/util.lua 133 | 134 | --- Creates a deep copy of table, not coyping Factorio objects 135 | -- @param object the table to copy 136 | -- @return a copy of the table 137 | function table.deepcopy(object) 138 | local lookup_table = {} 139 | local function _copy(object) 140 | if type(object) ~= "table" then 141 | return object 142 | elseif object.__self then 143 | return object 144 | elseif lookup_table[object] then 145 | return lookup_table[object] 146 | end 147 | local new_table = {} 148 | lookup_table[object] = new_table 149 | for index, value in pairs(object) do 150 | new_table[_copy(index)] = _copy(value) 151 | end 152 | return setmetatable(new_table, getmetatable(object)) 153 | end 154 | return _copy(object) 155 | end 156 | 157 | --- Returns a copy of all of the values in the table 158 | -- @param tbl the table to copy the keys from, or an empty table if the tbl is nil 159 | -- @param sorted (optional) whether to sort the keys (slower) or keep the random order from pairs() 160 | -- @param as_string (optional) whether to try and parse the values as strings, or leave them as their existing type 161 | -- @return an array with a copy of all the values in the table 162 | function table.values(tbl,sorted,as_string) 163 | if not tbl then return {} end 164 | local valueset = {} 165 | local n = 0 166 | if as_string == true then --checking as_string /before/ looping is faster 167 | for _,v in pairs(tbl) do n = n+1 ; valueset[n] = tostring(v) end 168 | else 169 | for _,v in pairs(tbl) do n = n+1 ; valueset[n] = v end 170 | end 171 | if sorted == true then 172 | table.sort(valueset, function(x,y) --sorts tables with mixed index types. 173 | local tx = type(x) == 'number' 174 | local ty = type(y) == 'number' 175 | if tx == ty then 176 | return x < y and true or false --similar type can be compared 177 | elseif tx == true then 178 | return true --only x is a number and goes first 179 | else 180 | return false --only y is a number and goes first 181 | end 182 | end) 183 | end 184 | return valueset 185 | end 186 | 187 | --- Returns a copy of all of the keys in the table 188 | -- @param tbl the table to copy the keys from, or an empty table if the tbl is nil 189 | -- @param sorted (optional) whether to sort the keys (slower) or keep the random order from pairs() 190 | -- @param as_string (optional) whether to try and parse the keys as strings, or leave them as their existing type 191 | -- @return an array with a copy of all the keys in the table 192 | function table.keys(tbl,sorted,as_string) 193 | if not tbl then return {} end 194 | local keyset = {} 195 | local n = 0 196 | if as_string == true then --checking as_string /before/ looping is faster 197 | for k,_ in pairs(tbl) do n = n+1 ; keyset[n] = tostring(k) end 198 | else 199 | for k,_ in pairs(tbl) do n = n+1 ; keyset[n] = k end 200 | end 201 | if sorted == true then 202 | table.sort(keyset, function(x,y) --sorts tables with mixed index types. 203 | local tx = type(x) == 'number' 204 | local ty = type(y) == 'number' 205 | if tx == ty then 206 | return x < y and true or false --similar type can be compared 207 | elseif tx == true then 208 | return true --only x is a number and goes first 209 | else 210 | return false --only y is a number and goes first 211 | end 212 | end) 213 | end 214 | return keyset 215 | end 216 | 217 | --- Removes keys from a table (sets them to nil) 218 | -- @usage local a = {1, 2, 3, 4} 219 | --table.remove_keys(a, {1,3} --returns {nil, 2, nil, 4} 220 | -- @usage local b = {k1 = 1, k2 = 'foo', old_key = 'bar'} 221 | --table.remove_keys(b, {'old_key'}) --returns {k1 = 1, k2 = 'foo'} 222 | -- @param tbl the table to remove the keys from 223 | -- @param keys array with the keys to remove 224 | -- @return tbl without the specified keys 225 | function table.remove_keys(tbl, keys) 226 | for i=1, #keys do 227 | tbl[keys[i]] = nil 228 | end 229 | return tbl 230 | end 231 | -------------------------------------------------------------------------------- /libs/biter/overmind.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | require 'stdlib/log/logger' 3 | require 'stdlib/entity/entity' 4 | require 'stdlib/area/position' 5 | require 'stdlib/area/area' 6 | require 'stdlib/table' 7 | require 'stdlib/game' 8 | 9 | Overmind = {stages = {}, tick_rates = {}} 10 | Overmind.Logger = Logger.new("Misanthrope", "overmind", false) 11 | local Log = function(str, ...) Overmind.Logger.log(string.format(str, ...)) end 12 | 13 | Event.register(defines.events.on_tick, function(event) 14 | if not global.overmind then 15 | global.overmind = { tick_rate = 600, currency = 0, stage = 'decide', data = {}, tracked_entities = {}, last_evo_boost = 0 } 16 | end 17 | if not (event.tick % global.overmind.tick_rate == 0) then return end 18 | 19 | -- accrue a tiny amount of currency due to the passage of time 20 | if event.tick % 600 == 0 then 21 | if not global.bases or #global.bases < 3 then 22 | global.overmind.currency = global.overmind.currency + 100 23 | elseif #global.bases < 50 then 24 | global.overmind.currency = global.overmind.currency + 10 + (50 - #global.bases) 25 | else 26 | global.overmind.currency = global.overmind.currency + 10 27 | end 28 | 29 | if game.evolution_factor < 0.1 then 30 | global.overmind.currency = math.floor(global.overmind.currency / 5) 31 | elseif game.evolution_factor < 0.2 then 32 | global.overmind.currency = math.floor(global.overmind.currency / 2) 33 | end 34 | end 35 | 36 | -- clear out any tracked entities that have expired their max_age 37 | if event.tick % Time.MINUTE == 0 then 38 | table.each(table.filter(global.overmind.tracked_entities, function(entity_data) return entity_data.max_age < event.tick end), function(entity_data) 39 | if entity_data.entity.valid then 40 | entity_data.entity.destroy() 41 | end 42 | end) 43 | global.overmind.tracked_entities = table.filter(global.overmind.tracked_entities, function(entity_data) return entity_data.entity.valid end) 44 | end 45 | 46 | local prev_stage = global.overmind.stage 47 | local data = global.overmind.data 48 | local stage = Overmind.stages[prev_stage](data) 49 | if data.reset then 50 | global.overmind.data = {} 51 | end 52 | global.overmind.stage = stage 53 | if prev_stage ~= stage then 54 | Log("Updating stage from %s to %s", prev_stage, stage) 55 | global.overmind.tick_rate = Overmind.tick_rates[stage] 56 | end 57 | end) 58 | 59 | Overmind.tick_rates.decide = 600 60 | Overmind.stages.decide = function(data) 61 | Log("Overmind currency: %d, Valuable Chunks: %d", math.floor(global.overmind.currency), #global.overwatch.valuable_chunks) 62 | if #global.overwatch.valuable_chunks > 0 then 63 | if global.overmind.currency > 10000 then 64 | 65 | if global.overmind.currency > 100000 then 66 | if math.random(100) < 75 then 67 | Log("Overmind selects spread early and expensive hive spawner") 68 | global.overmind.currency = global.overmind.currency - 25000 69 | return 'fast_spread_spawner' 70 | end 71 | end 72 | 73 | if math.random(100) < 10 then 74 | Log("Overmind selects early biter spawn") 75 | global.overmind.currency = global.overmind.currency - 3000 76 | return 'spawn_biters' 77 | end 78 | 79 | -- Early 80 | if math.random(100) < 33 and game.evolution_factor < 0.33 then 81 | Log("Overmind selects early evolution factor boost") 82 | global.overmind.currency = global.overmind.currency - 10000 83 | data.extra_factor = 0.0000125 84 | data.iterations = 3200 85 | return 'increase_evolution_factor' 86 | end 87 | 88 | if math.random(100) < 33 then 89 | Log("Overmind selects spread hive spawner") 90 | global.overmind.currency = global.overmind.currency - 10000 91 | return 'spread_spawner' 92 | end 93 | 94 | if math.random(100) < 25 and global.overmind.currency > 200000 then 95 | Log("Overmind selects donate currency to poor") 96 | global.overmind.currency = global.overmind.currency - 10000 97 | return 'donate_currency_to_poor' 98 | end 99 | 100 | if math.random(100) < 10 and game.evolution_factor < 0.8 and (global.overmind.last_evo_boost + Time.MINUTE * 30) < game.tick then 101 | Log("Overmind selects late evolution factor boost") 102 | global.overmind.currency = global.overmind.currency - 10000 103 | data.extra_factor = 0.0000125 104 | data.iterations = 3200 105 | return 'increase_evolution_factor' 106 | end 107 | 108 | end 109 | end 110 | return 'decide' 111 | end 112 | 113 | Overmind.tick_rates.donate_currency_to_poor = Time.MINUTE * 1 114 | Overmind.stages.donate_currency_to_poor = function(data) 115 | if not data.iterations then 116 | data.iterations = 1 117 | end 118 | if global.overmind.currency > 10000 then 119 | local start_currency = global.overmind.currency 120 | table.each(table.filter(global.bases, Game.VALID_FILTER), function(base) 121 | if base.currency.amt < 6000 and global.overmind.currency > 1000 then 122 | base.currency.amt = base.currency.amt + 1000 123 | global.overmind.currency = global.overmind.currency - 1000 124 | end 125 | end) 126 | data.iterations = data.iterations + 1 127 | if data.iterations > 10 then 128 | return 'decide' 129 | end 130 | -- successfully found donation targets 131 | if start_currency > global.overmind.currency then 132 | return 'donate_currency_to_poor' 133 | end 134 | end 135 | 136 | return 'decide' 137 | end 138 | 139 | Overmind.tick_rates.increase_evolution_factor = 10 140 | Overmind.stages.increase_evolution_factor = function(data) 141 | if data.iterations > 0 then 142 | data.iterations = data.iterations - 1 143 | game.evolution_factor = game.evolution_factor + data.extra_factor 144 | return 'increase_evolution_factor' 145 | end 146 | 147 | data.reset = true 148 | return 'decide' 149 | end 150 | 151 | Overmind.tick_rates.spawn_biters = Time.MINUTE 152 | Overmind.stages.spawn_biters = function(data) 153 | Log("Attempting to spawn biters, total valuable chunks: %d", #global.overwatch.valuable_chunks) 154 | local spawnable_chunks = table.filter(global.overwatch.valuable_chunks, function(data) return data.best_target ~= nil end) 155 | table.sort(spawnable_chunks, function(a, b) 156 | return b.value < a.value 157 | end) 158 | if #spawnable_chunks == 0 then 159 | return 'decide' 160 | end 161 | 162 | local chunk_data = spawnable_chunks[1] 163 | local chunk = chunk_data.chunk 164 | global.overwatch.valuable_chunks = table.filter(global.overwatch.valuable_chunks, function(data) return data.chunk.x ~= chunk.x and data.chunk.y ~= chunk.y end) 165 | Log("Choose chunk %s to spawn units on, remaining valuable chunks: %d", Chunk.to_string(chunk), #global.overwatch.valuable_chunks) 166 | 167 | local area = Chunk.to_area(chunk) 168 | local chunk_center = Area.center(area) 169 | local surface = global.overwatch.surface 170 | if surface.count_entities_filtered({area = Area.expand(area, 32 * 2), force = game.forces.player}) > 16 then 171 | Log("Chunk %s had > 16 player entities within 2 chunks", Chunk.to_string(chunk)) 172 | return 'decide' 173 | end 174 | if surface.count_entities_filtered({area = Area.expand(area, 32 * 4), force = game.forces.player}) > 100 then 175 | Log("Chunk %s had > 100 player entities within 4 chunks", Chunk.to_string(chunk)) 176 | return 'decide' 177 | end 178 | if surface.count_entities_filtered({type = 'unit-spawner', area = Area.expand(area, 64), force = game.forces.enemy}) > 0 then 179 | Log("Chunk %s had biter spawners within 2 chunks", Chunk.to_string(chunk)) 180 | return 'decide' 181 | end 182 | 183 | local max_age = game.tick + Time.MINUTE * 10 184 | local attack_group_size = math.floor(30 + game.evolution_factor / 0.025) 185 | local tracked_entities = global.overmind.tracked_entities 186 | local biters = {} 187 | local all_units = {'behemoth-biter', 'behemoth-spitter', 'big-biter', 'big-spitter', 'medium-biter', 'medium-spitter', 'small-spitter', 'small-biter'} 188 | local unit_count = 0 189 | for i = 1, attack_group_size do 190 | for _, unit_name in pairs(all_units) do 191 | local odds = 100 * Biters.unit_odds(unit_name) 192 | if odds > 0 and odds > math.random(100) then 193 | local spawn_pos = surface.find_non_colliding_position(unit_name, chunk_center, 12, 0.5) 194 | if spawn_pos then 195 | local entity = surface.create_entity({name = unit_name, position = spawn_pos, force = 'enemy'}) 196 | if entity then 197 | table.insert(biters, entity) 198 | table.insert(tracked_entities, {entity = entity, max_age = max_age}) 199 | unit_count = unit_count + 1 200 | end 201 | end 202 | end 203 | end 204 | end 205 | Log("Spawned %d units at chunk %s, to attack %s", unit_count, Chunk.to_string(chunk), string.line(chunk_data.best_target)) 206 | if #biters > 0 then 207 | local unit_group = surface.create_unit_group({position = biters[1].position, force = 'enemy'}) 208 | for _, biter in pairs(biters) do 209 | unit_group.add_member(biter) 210 | end 211 | local cmd = {type = defines.command.attack_area, destination = chunk_center, radius = 12} 212 | Log("Attack command: %s", string.line(cmd)) 213 | unit_group.set_command(cmd) 214 | unit_group.start_moving() 215 | end 216 | 217 | return 'decide' 218 | end 219 | 220 | Overmind.tick_rates.spread_spawner = Time.MINUTE 221 | Overmind.stages.spread_spawner = function(data) 222 | Log("Attempting to spread a spawner, total valuable chunks: %d", #global.overwatch.valuable_chunks) 223 | local spawnable_chunks = table.filter(global.overwatch.valuable_chunks, function(data) return data.spawn end) 224 | table.sort(spawnable_chunks, function(a, b) 225 | return b.value < a.value 226 | end) 227 | if #spawnable_chunks == 0 then 228 | return 'decide' 229 | end 230 | 231 | local chunk_data = spawnable_chunks[1] 232 | local chunk = chunk_data.chunk 233 | global.overwatch.valuable_chunks = table.filter(global.overwatch.valuable_chunks, function(data) return data.chunk.x ~= chunk.x and data.chunk.y ~= chunk.y end) 234 | Log("Choose chunk %s to spawn a new base, remaining valuable chunks: %d", Chunk.to_string(chunk), #global.overwatch.valuable_chunks) 235 | 236 | local surface = global.overwatch.surface 237 | local area = Chunk.to_area(chunk) 238 | if surface.count_entities_filtered({area = Area.expand(area, 32 * 2), force = game.forces.player}) > 16 then 239 | Log("Chunk %s had > 16 player entities within 2 chunks", Chunk.to_string(chunk)) 240 | return 'decide' 241 | end 242 | if surface.count_entities_filtered({area = Area.expand(area, 32 * 4), force = game.forces.player}) > 100 then 243 | Log("Chunk %s had > 100 player entities within 4 chunks", Chunk.to_string(chunk)) 244 | return 'decide' 245 | end 246 | if surface.count_entities_filtered({type = 'unit-spawner', area = Area.expand(area, 64), force = game.forces.enemy}) > 0 then 247 | Log("Chunk %s had biter spawners within 2 chunks", Chunk.to_string(chunk)) 248 | return 'decide' 249 | end 250 | 251 | local chunk_center = Area.center(area) 252 | local pos = surface.find_non_colliding_position('biter-spawner', chunk_center, 16, 1) 253 | if pos and Area.inside(area, pos) then 254 | local queen = surface.create_entity({name = 'biter-spawner', position = pos, direction = math.random(7)}) 255 | local base = BiterBase.discover(queen) 256 | Log("Successfully spawned a new base: %s", BiterBase.tostring(base)) 257 | else 258 | Log("Unable to spawn new base at chunk %s", Chunk.to_string(chunk)) 259 | end 260 | 261 | return 'decide' 262 | end 263 | 264 | Overmind.tick_rates.fast_spread_spawner = Time.SECOND * 2 265 | Overmind.stages.fast_spread_spawner = Overmind.stages.spread_spawner 266 | -------------------------------------------------------------------------------- /libs/harpa.lua: -------------------------------------------------------------------------------- 1 | require 'stdlib/event/event' 2 | 3 | Harpa = {} 4 | Harpa.Logger = Logger.new("Misanthrope", "harpa", DEBUG_MODE) 5 | 6 | function Harpa.migrate(old_global) 7 | for _, field_name in pairs({"harpa_list", "idle_harpa_list", "unpowered_harpa_list", "biter_ignore_list", "harpa_overlays"}) do 8 | global[field_name] = old_global[field_name] 9 | end 10 | end 11 | 12 | function Harpa.setup() 13 | for _, field_name in pairs({"harpa_list", "idle_harpa_list", "unpowered_harpa_list", "biter_ignore_list", "harpa_overlays", "micro_harpa_players", "idle_micro_harpa_players"}) do 14 | if not global[field_name] then 15 | global[field_name] = {} 16 | end 17 | end 18 | end 19 | 20 | Event.register(defines.events.on_tick, function(event) 21 | Event.remove(defines.events.on_tick, event._handler) 22 | Harpa.setup() 23 | end) 24 | 25 | function Harpa.register(entity, player_idx) 26 | if Harpa.is_powered(entity, nil) then 27 | if player_idx then 28 | Harpa.create_overlay(entity, player_idx) 29 | end 30 | table.insert(global.harpa_list, entity) 31 | else 32 | table.insert(global.unpowered_harpa_list, entity) 33 | end 34 | end 35 | 36 | function Harpa.update_power_grid(position, range, ignore_entity) 37 | local range_squared = range * range 38 | 39 | -- check inactive emitters to see if they gained power 40 | for i = #global.unpowered_harpa_list, 1, -1 do 41 | local harpa = global.unpowered_harpa_list[i] 42 | if harpa.valid then 43 | local harpa_pos = harpa.position 44 | local dist_squared = (position.x - harpa_pos.x) * (position.x - harpa_pos.x) + (position.y - harpa_pos.y) * (position.y - harpa_pos.y) 45 | if range_squared > dist_squared then 46 | if Harpa.is_powered(harpa, ignore_entity) then 47 | table.remove(global.unpowered_harpa_list, i) 48 | table.insert(global.harpa_list, harpa) 49 | end 50 | end 51 | else 52 | table.remove(global.unpowered_harpa_list, i) 53 | end 54 | end 55 | 56 | -- check active emitters to verify they still have power 57 | for i = #global.harpa_list, 1, -1 do 58 | local harpa = global.harpa_list[i] 59 | if harpa.valid then 60 | local harpa_pos = harpa.position 61 | local dist_squared = (position.x - harpa_pos.x) * (position.x - harpa_pos.x) + (position.y - harpa_pos.y) * (position.y - harpa_pos.y) 62 | if range_squared > dist_squared then 63 | if not Harpa.is_powered(harpa, ignore_entity) then 64 | table.remove(global.harpa_list, i) 65 | table.insert(global.unpowered_harpa_list, harpa) 66 | Harpa.disable_overlay(harpa) 67 | end 68 | end 69 | else 70 | table.remove(global.harpa_list, i) 71 | end 72 | end 73 | end 74 | 75 | function Harpa.disable_overlay(entity) 76 | for i = #global.harpa_overlays, 1, -1 do 77 | local overlay = global.harpa_overlays[i] 78 | if overlay.harpa == entity then 79 | overlay.ticks_remaining = -1 80 | end 81 | end 82 | end 83 | 84 | function Harpa.create_overlay(entity, player_idx) 85 | -- only allow 1 active overlay per player (to prevent lag) 86 | for i = #global.harpa_overlays, 1, -1 do 87 | local overlay = global.harpa_overlays[i] 88 | if overlay.player_idx == player_idx then 89 | overlay.ticks_remaining = -1 90 | end 91 | end 92 | local overlay_entity = entity.surface.create_entity({name = "80_red_overlay", force = game.forces.neutral, position = entity.position }) 93 | local overlay = { player_idx = player_idx, harpa = entity, entity_list = {}, radius = 0, ticks_remaining = 15 * 30 + 12 * 60 } 94 | table.insert(overlay.entity_list, overlay_entity) 95 | table.insert(global.harpa_overlays, overlay) 96 | end 97 | 98 | function Harpa.update_overlays() 99 | for i = #global.harpa_overlays, 1, -1 do 100 | local overlay = global.harpa_overlays[i] 101 | if overlay.radius < 30 and overlay.harpa.valid and overlay.ticks_remaining % 15 == 0 then 102 | overlay.radius = overlay.radius + 1 103 | if (overlay.radius % 5 == 0) then 104 | local surface = overlay.harpa.surface 105 | local opacity = 80 - overlay.radius * 2 106 | local position = overlay.harpa.position 107 | for dx = -(overlay.radius), overlay.radius do 108 | Harpa.create_overlay_entity(surface, opacity, {position.x + dx, position.y + overlay.radius}, overlay.entity_list) 109 | Harpa.create_overlay_entity(surface, opacity, {position.x + dx, position.y - overlay.radius}, overlay.entity_list) 110 | end 111 | for dy = -(overlay.radius - 1), overlay.radius - 1 do 112 | Harpa.create_overlay_entity(surface, opacity, {position.x + overlay.radius, position.y + dy}, overlay.entity_list) 113 | Harpa.create_overlay_entity(surface, opacity, {position.x - overlay.radius, position.y - dy}, overlay.entity_list) 114 | end 115 | end 116 | end 117 | overlay.ticks_remaining = overlay.ticks_remaining - 1 118 | if overlay.ticks_remaining <= 0 or not overlay.harpa.valid then 119 | table.remove(global.harpa_overlays, i) 120 | for _, entity in ipairs(overlay.entity_list) do 121 | if entity.valid then 122 | entity.destroy() 123 | end 124 | end 125 | end 126 | end 127 | end 128 | 129 | function Harpa.create_overlay_entity(surface, opacity, position, list) 130 | local overlay_entity = surface.create_entity({name = opacity .. "_red_overlay", force = game.forces.neutral, position = position}) 131 | overlay_entity.minable = false 132 | overlay_entity.destructible = false 133 | overlay_entity.operable = false 134 | table.insert(list, overlay_entity) 135 | end 136 | 137 | function Harpa.check_power(entity, ignore_entity) 138 | if entity.prototype.type == "electric-pole" then 139 | Harpa.update_power_grid(entity.position, 10, ignore_entity) 140 | end 141 | end 142 | 143 | Event.register(defines.events.on_built_entity, function(event) 144 | if event.created_entity.name == "biter-emitter" then 145 | event.created_entity.backer_name = "" 146 | Harpa.register(event.created_entity, event.player_index) 147 | end 148 | Harpa.check_power(event.created_entity, nil) 149 | end) 150 | 151 | Event.register(defines.events.on_robot_built_entity, function(event) 152 | if event.created_entity.name == "biter-emitter" then 153 | event.created_entity.backer_name = "" 154 | Harpa.register(event.created_entity, nil) 155 | end 156 | Harpa.check_power(event.created_entity, nil) 157 | end) 158 | 159 | Event.register(defines.events.on_entity_died, function(event) 160 | local entity = event.entity 161 | Harpa.check_power(entity, entity) 162 | end) 163 | 164 | Event.register(defines.events.on_player_mined_item, function(event) 165 | if event and event.item_stack and event.item_stack.name and game.entity_prototypes[event.item_stack.name] then 166 | if game.entity_prototypes[event.item_stack.name].type == "electric-pole" then 167 | if game.players[event.player_index].character then 168 | Harpa.update_power_grid(game.players[event.player_index].character.position, 10, nil) 169 | else 170 | Harpa.update_power_grid(game.players[event.player_index].position, 10, nil) 171 | end 172 | end 173 | end 174 | end) 175 | 176 | Event.register(defines.events.on_tick, function(event) 177 | Harpa.update_overlays() 178 | 179 | -- check idle emitters less often 180 | if event.tick % 150 == 0 then 181 | for i = #global.idle_harpa_list, 1, -1 do 182 | local harpa = global.idle_harpa_list[i] 183 | if not harpa.valid then 184 | table.remove(global.idle_harpa_list, i) 185 | else 186 | -- validate that emitter is still idle 187 | if not Harpa.is_idle(harpa, 32) then 188 | table.remove(global.idle_harpa_list, i) 189 | table.insert(global.harpa_list, harpa) 190 | end 191 | end 192 | end 193 | end 194 | 195 | for i = #global.harpa_list, 1, -1 do 196 | local harpa = global.harpa_list[i] 197 | if not harpa.valid then 198 | table.remove(global.harpa_list, i) 199 | else 200 | -- check to see if emitter is idle, and we can update it less often 201 | if event.tick % 150 == 0 then 202 | if Harpa.is_idle(harpa, 32) then 203 | table.remove(global.harpa_list, i) 204 | table.insert(global.idle_harpa_list, harpa) 205 | end 206 | end 207 | 208 | Harpa.tick_emitter(harpa, 30) 209 | end 210 | end 211 | Harpa.update_power_armor() 212 | end) 213 | 214 | Event.register({defines.events.on_player_placed_equipment, defines.events.on_player_removed_equipment}, function(event) 215 | local player_index = event.player_index 216 | 217 | -- examine harpa status on the next tick 218 | Event.register(defines.events.on_tick, function(event) 219 | Event.remove(defines.events.on_tick, event._handler) 220 | 221 | local player = game.players[player_index] 222 | if Harpa.has_micro_emitter(player) then 223 | Harpa.track_micro_emitter(player) 224 | end 225 | end) 226 | end) 227 | 228 | function Harpa.track_micro_emitter(player) 229 | -- prevent duplicate entries 230 | for i = #global.micro_harpa_players, 1, -1 do 231 | if (player == global.micro_harpa_players[i]) then 232 | return 233 | end 234 | end 235 | for i = #global.idle_micro_harpa_players, 1, -1 do 236 | if (player == global.idle_micro_harpa_players[i]) then 237 | return 238 | end 239 | end 240 | table.insert(global.micro_harpa_players, player) 241 | end 242 | 243 | function Harpa.has_micro_emitter(player) 244 | if player and player.valid and player.connected then 245 | --local armor = player.get_inventory(defines.inventory.player_armor)[1] 246 | local armor = Harpa.get_player_armor(player) 247 | local equipment_grid = Harpa.get_equipment_grid(armor) 248 | if equipment_grid then 249 | for _, equipment in pairs(equipment_grid.equipment) do 250 | if equipment.name == "micro-biter-emitter" then 251 | return true 252 | end 253 | end 254 | end 255 | end 256 | return false 257 | end 258 | 259 | function Harpa.get_equipment_grid(item) 260 | if not item then 261 | return nil 262 | end 263 | if not item.valid_for_read then 264 | return nil 265 | end 266 | local status, value = pcall(function() return item.grid end) 267 | if status then 268 | return value 269 | end 270 | return nil 271 | end 272 | 273 | function Harpa.get_player_armor(player) 274 | local status, inventory = pcall(player.get_inventory, defines.inventory.player_armor) 275 | if status and inventory then 276 | return inventory[1] 277 | end 278 | return nil 279 | end 280 | 281 | function Harpa.update_power_armor() 282 | local idle_check = game.tick % 120 == 0 283 | 284 | -- check all idle micro emitters, and return active emitters to service 285 | if game.tick % 150 == 0 then 286 | for i = #global.idle_micro_harpa_players, 1, -1 do 287 | local player = global.idle_micro_harpa_players[i] 288 | if Harpa.has_micro_emitter(player) then 289 | if Harpa.is_idle(player, 20) then 290 | -- do nothing, still idle 291 | else 292 | -- return to active status 293 | table.remove(global.idle_micro_harpa_players, i) 294 | table.insert(global.micro_harpa_players, player) 295 | end 296 | else 297 | table.remove(global.idle_micro_harpa_players, i) 298 | end 299 | end 300 | end 301 | -- update all active micro emitters 302 | for i = #global.micro_harpa_players, 1, -1 do 303 | local player = global.micro_harpa_players[i] 304 | if Harpa.has_micro_emitter(player) then 305 | -- only test if HARPA is idle every 120 ticks, it is expensive 306 | if idle_check and Harpa.is_idle(player, 20) then 307 | table.insert(global.idle_micro_harpa_players, player) 308 | table.remove(global.micro_harpa_players, i) 309 | else 310 | Harpa.tick_emitter(player, 16) 311 | end 312 | else 313 | table.remove(global.micro_harpa_players, i) 314 | end 315 | end 316 | end 317 | 318 | -- only a best guess based on nearby electric poles 319 | function Harpa.is_powered(entity, ignore_entity) 320 | local surface = entity.surface 321 | local position = entity.position 322 | local ranges_squared = {}; ranges_squared["small-electric-pole"] = 2.5; ranges_squared["medium-electric-pole"] = 3.5; ranges_squared["big-electric-pole"] = 2; ranges_squared["substation"] = 7 323 | local electric_poles = surface.find_entities_filtered({area = Harpa.area_around(position, 10), type = "electric-pole", force = "player"}) 324 | for i = 1, #electric_poles do 325 | local electric_pole = electric_poles[i] 326 | if electric_pole ~= ignore_entity then 327 | local range = ranges_squared[electric_pole.prototype.name] 328 | 329 | local pole_pos = electric_pole.position 330 | if range ~= nil and Harpa.is_inside_area(Harpa.area_around(pole_pos, range), position) then 331 | return true 332 | end 333 | end 334 | end 335 | return false 336 | end 337 | 338 | function Harpa.is_inside_area(area, position) 339 | return position.x > area.left_top.x and position.y > area.left_top.y and 340 | position.x < area.right_bottom.x and position.y < area.right_bottom.y 341 | end 342 | 343 | function Harpa.area_around(position, distance) 344 | return {left_top = {x = position.x - distance, y = position.y - distance}, 345 | right_bottom = {x = position.x + distance, y = position.y + distance}} 346 | end 347 | 348 | function Harpa.is_idle(entity, radius) 349 | return entity.surface.find_nearest_enemy({position = entity.position, max_distance = radius, force = entity.force}) == nil 350 | end 351 | 352 | -- called every tick... keep it optimized 353 | function Harpa.tick_emitter(entity, radius) 354 | -- using x and y and tick for modulus assures emitters next to each other will scan separate rows 355 | local diameter = radius * 2 356 | local pos = entity.position 357 | local surface = entity.surface 358 | local force = entity.force 359 | local row = ((math.floor(pos.y) + math.floor(pos.x) + game.tick) % diameter) - radius 360 | local area = {left_top = {pos.x - radius, pos.y - row}, right_bottom = {pos.x + radius, pos.y - row + 1}} 361 | local biters = surface.find_entities_filtered({area = area, type = "unit", force = "enemy"}) 362 | 363 | local emitter_area = {left_top = {pos.x - diameter, pos.y - diameter}, right_bottom = {pos.x + diameter, pos.y + diameter}} 364 | for _, biter in ipairs(biters) do 365 | local roll = math.random(0, 100) 366 | local biter_pos = biter.position 367 | -- random chance to 1-shot kill a biter (as long as it is not a behemoth) 368 | if (roll >= 99) and biter.prototype.max_health < 2500 then 369 | biter.damage(biter.prototype.max_health, force) 370 | else 371 | distance = math.sqrt((biter_pos.x - pos.x) * (biter_pos.x - pos.x) + (biter_pos.y - pos.y) * (biter_pos.y - pos.y)) 372 | biter.damage(math.min(100, biter.prototype.max_health / (1 + distance)), force) 373 | end 374 | 375 | -- check if biter is valid (damage may have killed it) 376 | if biter.valid and not Harpa.ignore_biter(biter) then 377 | local command = {} 378 | local ignore_time = 60 * 5 379 | 380 | -- emitter only works on non-behemoth biters 381 | if biter.prototype.max_health < 2500 then 382 | local destination = Harpa.nearest_corner(biter_pos, emitter_area, math.random(1, 10), math.random(1, 10)) 383 | destination = surface.find_non_colliding_position(biter.name, destination, 20, 0.3) 384 | command = {type = defines.command.compound, structure_type = defines.compound_command.logical_and, commands = { 385 | {type = defines.command.go_to_location, distraction = defines.distraction.by_damage, destination = destination}, 386 | {type = defines.command.wander} 387 | }} 388 | else 389 | -- emitter angers behemoth biters into attacking immediately 390 | command = {type = defines.command.attack, target = entity, distraction = defines.distraction.none} 391 | ignore_time = 60 * 60 392 | end 393 | local status, err = pcall(biter.set_command, command) 394 | if not status then 395 | Harpa.Logger.log("Error (" .. string.line(err) .. ") executing biter command command: " .. string.block(command)) 396 | end 397 | table.insert(global.biter_ignore_list, {biter = biter, until_tick = game.tick + ignore_time}) 398 | end 399 | end 400 | 401 | local spawners = surface.find_entities_filtered({area = area, type = "unit-spawner", force = "enemy"}) 402 | for _, spawner in ipairs(spawners) do 403 | spawner.damage(spawner.prototype.max_health / 250, force) 404 | end 405 | local worms = surface.find_entities_filtered({area = area, type = "turret", force = "enemy"}) 406 | for _, worm in ipairs(worms) do 407 | worm.damage(worm.prototype.max_health / 100, force) 408 | end 409 | end 410 | 411 | function Harpa.ignore_biter(entity) 412 | for i = #global.biter_ignore_list, 1, -1 do 413 | local biter_data = global.biter_ignore_list[i] 414 | if not biter_data.biter.valid or game.tick > biter_data.until_tick then 415 | table.remove(global.biter_ignore_list, i) 416 | elseif biter_data.biter == entity then 417 | return true 418 | end 419 | end 420 | return false 421 | end 422 | 423 | function Harpa.nearest_corner(pos, area, rand_x, rand_y) 424 | local dist_left_top = (pos.x - area.left_top[1]) * (pos.x - area.left_top[1]) + (pos.y - area.left_top[2]) * (pos.y - area.left_top[2]) 425 | local dist_right_bottom = (pos.x - area.right_bottom[1]) * (pos.x - area.right_bottom[1]) + (pos.y - area.right_bottom[2]) * (pos.y - area.right_bottom[2]) 426 | if (dist_left_top < dist_right_bottom) then 427 | local dist_right_top = (pos.x - area.right_bottom[1]) * (pos.x - area.right_bottom[1]) + (pos.y - area.left_top[2]) * (pos.y - area.left_top[2]) 428 | if (dist_left_top < dist_right_top) then 429 | return {area.left_top[1] - rand_x, area.left_top[2] - rand_y} 430 | else 431 | return {area.right_bottom[1] + rand_x, area.left_top[2] - rand_y} 432 | end 433 | else 434 | local dist_left_bottom = (pos.x - area.left_top[1]) * (pos.x - area.left_top[1]) + (pos.y - area.right_bottom[2]) * (pos.y - area.right_bottom[2]) 435 | if (dist_right_bottom < dist_left_bottom) then 436 | return {area.right_bottom[1] + rand_x, area.right_bottom[2] + rand_y} 437 | else 438 | return {area.left_top[1] - rand_x, area.right_bottom[2] + rand_y} 439 | end 440 | end 441 | end 442 | 443 | return Harpa 444 | --------------------------------------------------------------------------------