├── .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: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 | --------------------------------------------------------------------------------