1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-03-27 21:48:52 +02:00
RedRafe 7789ac4ec6
Updates/frontier v6 (#1431)
* New terrain gen

* Artillery targeting

* Fix silo positioning

* Frontier v6 tweaks
2024-08-28 17:06:34 +01:00

469 lines
16 KiB
Lua

local Color = require 'resources.color_presets'
local EnemyTurret = require 'features.enemy_turret'
local math = require 'utils.math'
local PriceRaffle = require 'features.price_raffle'
local Table = require 'utils.table'
local Toast = require 'features.gui.toast'
local Token = require 'utils.token'
local Debug = require 'map_gen.maps.frontier.shared.debug'
local Public = require 'map_gen.maps.frontier.shared.core'
local register_on_entity_destroyed = script.register_on_entity_destroyed
local math_ceil = math.ceil
local math_clamp = math.clamp
local math_floor = math.floor
local math_max = math.max
local math_random = math.random
local math_sqrt = math.sqrt
local SECOND = 60
local MINUTE = SECOND * 60
local Enemy = {}
Enemy.target_entity_types = {
['accumulator'] = true,
['assembling-machine'] = true,
['beacon'] = true,
['boiler'] = true,
['furnace'] = true,
['generator'] = true,
['heat-interface'] = true,
['lab'] = true,
['mining-drill'] = true,
['offshore-pump'] = true,
['reactor'] = true,
['roboport'] = true,
['solar-panel'] = true,
}
Enemy.commands = {
move = function(unit_group, position)
local data = Enemy.ai_take_control(unit_group)
if not position then
Enemy.ai_processor(unit_group)
return
end
data.position = position
data.stage = Enemy.stages.move
unit_group.set_command {
type = defines.command.go_to_location,
destination = position,
radius = 3,
distraction = defines.distraction.by_enemy
}
unit_group.start_moving()
if Public.get()._DEBUG_AI then
Debug.print_admins(string.format('AI [id=%d] | cmd: MOVE [gps=%.2f,%.2f,%s]', unit_group.group_number, position.x, position.y, unit_group.surface.name), Color.dark_gray)
end
end,
scout = function(unit_group, position)
local data = Enemy.ai_take_control(unit_group)
if not position then
Enemy.ai_processor(unit_group)
return
end
data.position = position
data.stage = Enemy.stages.scout
unit_group.set_command {
type = defines.command.attack_area,
destination = position,
radius = 15,
distraction = defines.distraction.by_enemy
}
unit_group.start_moving()
if Public.get()._DEBUG_AI then
Debug.print_admins(string.format('AI [id=%d] | cmd: SCOUT [gps=%.2f,%.2f,%s]', unit_group.group_number, position.x, position.y, unit_group.surface.name), Color.dark_gray)
end
end,
attack = function(unit_group, target)
local data = Enemy.ai_take_control(unit_group)
if not (target and target.valid) then
Enemy.ai_processor(unit_group)
return
end
data.target = target
data.stage = Enemy.stages.attack
unit_group.set_command {
type = defines.command.attack_area,--defines.command.attack,
destination = target.position,
radius = 15,
--target = target,
distraction = defines.distraction.by_damage
}
if Public.get()._DEBUG_AI then
Debug.print_admins(string.format('AI [id=%d] | cmd: ATTACK [gps=%.2f,%.2f,%s] (type = %s)', unit_group.group_number, target.position.x, target.position.y, unit_group.surface.name, target.type), Color.dark_gray)
end
end
}
Enemy.stages = {
pending = 1,
move = 2,
scout = 3,
attack = 4,
fail = 5,
}
Enemy.turret_raffle = {
-- sets must have at least 1 valid turret and 1 valid refill for function(x_distance, evolution)
['base'] = {
['gun-turret'] = { weight = 16, min_distance = 0, refill = { 'firearm-magazine', 'piercing-rounds-magazine', 'uranium-rounds-magazine' } },
['flamethrower-turret'] = { weight = 2, min_distance = 800, refill = { 'crude-oil', 'heavy-oil', 'light-oil' } },
['artillery-turret'] = { weight = 16, min_distance = 2800, refill = { 'artillery-shell' } }
},
['Krastorio2'] = {
['gun-turret'] = { weight = 16, min_distance = 0, refill = { 'rifle-magazine', 'armor-piercing-rifle-magazine', 'uranium-rifle-magazine', 'imersite-rifle-magazine' } },
['flamethrower-turret'] = { weight = 2, min_distance = 800, refill = { 'crude-oil', 'heavy-oil', 'light-oil' } },
['kr-railgun-turret'] = { weight = 128, min_distance = 1400, refill = { 'basic-railgun-shell', 'explosion-railgun-shell', 'antimatter-railgun-shell' } },
['kr-rocket-turret'] = { weight = 16, min_distance = 2000, refill = { 'explosive-turret-rocket', 'nuclear-turret-rocket', 'antimatter-turret-rocket' } },
['artillery-turret'] = { weight = 16, min_distance = 2800, refill = { 'artillery-shell', 'nuclear-artillery-shell', 'antimatter-artillery-shell' } },
},
['zombiesextended-core'] = {
['gun-turret'] = { weight = 16, min_distance = 0, refill = { 'firearm-magazine', 'assault-ammo-mk1', 'uranium-rounds-magazine', 'assault-ammo-mk2' } },
['gun-turret-mk1'] = { weight = 64, min_distance = 750, refill = { 'piercing-rounds-magazine', 'assault-ammo-mk1', 'assault-ammo-mk2', 'assault-ammo-mk3' } },
['gun-turret-mk2'] = { weight = 256, min_distance = 1500, refill = { 'assault-ammo-mk1', 'uranium-rounds-magazine', 'assault-ammo-mk2', 'assault-ammo-mk3' } },
['flamethrower-turret'] = { weight = 2, min_distance = 800, refill = { 'crude-oil', 'heavy-oil', 'light-oil' } },
['flamethrower-turret-mk1'] = { weight = 4, min_distance = 1200, refill = { 'crude-oil', 'heavy-oil', 'light-oil' } },
['flamethrower-turret-mk2'] = { weight = 16, min_distance = 1500, refill = { 'crude-oil', 'heavy-oil', 'light-oil' } },
['artillery-turret'] = { weight = 128, min_distance = 2800, refill = { 'artillery-shell' } }
}
}
function Enemy.ai_take_control(unit_group)
local this = Public.get()
if not this.unit_groups[unit_group.group_number] then
this.unit_groups[unit_group.group_number] = {
unit_group = unit_group
}
end
return this.unit_groups[unit_group.group_number]
end
function Enemy.ai_stage_by_distance(posA, posB)
local x_axis = posA.x - posB.x
local y_axis = posA.y - posB.y
local distance = math_sqrt(x_axis * x_axis + y_axis * y_axis)
if distance <= 15 then
return Enemy.stages.attack
elseif distance <= 32 then
return Enemy.stages.scout
else
return Enemy.stages.move
end
end
function Enemy.ai_processor(unit_group, result)
if not (unit_group and unit_group.valid) then
return
end
local this = Public.get()
local data = this.unit_groups[unit_group.group_number]
if not data then
return
end
if data.failed_attempts and data.failed_attempts >= 3 then
this.unit_groups[unit_group.group_number] = nil
return
end
if not result or result == defines.behavior_result.fail or result == defines.behavior_result.deleted then
data.stage = Enemy.stages.pending
end
if result == defines.behavior_result.success and (data.stage and data.stage == Enemy.stages.attack) then
data.stage = Enemy.stages.pending
end
data.stage = data.stage or Enemy.stages.pending
if data.stage == Enemy.stages.pending then
local surface = unit_group.surface
data.target = surface.find_nearest_enemy_entity_with_owner {
position = unit_group.position,
max_distance = this.rocket_step * 4,
force = 'enemy',
}
if not (data.target and data.target.valid) then
this.unit_groups[unit_group.group_number] = nil
return
end
data.position = data.target.position
data.stage = Enemy.ai_stage_by_distance(data.position, unit_group.position)
else
data.stage = data.stage + 1
end
if this._DEBUG_AI then
Debug.print_admins(string.format('AI [id=%d] | status: %d', unit_group.group_number, data.stage), Color.dark_gray)
end
if data.stage == Enemy.stages.move then
Enemy.commands.move(unit_group, data.target)
elseif data.stage == Enemy.stages.scout then
Enemy.commands.scout(unit_group, data.target)
elseif data.stage == Enemy.stages.attack then
Enemy.commands.attack(unit_group, data.target)
else
data.failed_attempts = (data.failed_attempts or 0) + 1
if this._DEBUG_AI then
Debug.print_admins(string.format('AI [id=%d] | FAIL | stage: %d | attempts: %d', unit_group.group_number, data.stage, data.failed_attempts), Color.dark_gray)
end
data.stage, data.position, data.target = nil, nil, nil
Enemy.ai_processor(unit_group, nil)
end
end
function Enemy.spawn_enemy_wave(position)
local surface = Public.surface()
local find_position = surface.find_non_colliding_position
local spawn = surface.create_entity
local current_tick = game.tick
local this = Public.get()
local unit_group = surface.create_unit_group { position = position, force = 'enemy' }
local max_time = math_max(MINUTE, MINUTE * math_ceil(0.5 * (this.rockets_launched ^ 0.5)))
local radius = 20
for _ = 1, 12 do
local name
if this.rockets_launched < 3 then
name = math_random(1, 6) == 1 and 'big-worm-turret' or 'medium-worm-turret'
else
name = math_random(1, 6) == 1 and 'behemoth-worm-turret' or 'big-worm-turret'
end
local about = find_position(name, { x = position.x + math_random(), y = position.y + math_random() }, radius, 0.2)
if about then
local worm = spawn { name = name, position = about, force = 'enemy', move_stuck_players = true }
this.invincible[worm.unit_number] = {
time_to_live = current_tick + math_random(MINUTE, max_time)
}
end
end
radius = 32
for _ = 1, 20 do
local name
if this.rockets_launched < 3 then
name = math_random(1, 3) == 1 and 'big-biter' or 'big-spitter'
else
name = math_random(1, 3) == 1 and 'behemoth-biter' or 'behemoth-spitter'
end
local about = find_position(name, { x = position.x + math_random(), y = position.y + math_random() }, radius, 0.6)
if about then
local unit = spawn { name = name, position = about, force = 'enemy', move_stuck_players = true }
this.invincible[unit.unit_number] = {
time_to_live = current_tick + math_random(MINUTE, max_time)
}
unit_group.add_member(unit)
end
end
if unit_group.valid then
Enemy.ai_take_control(unit_group)
Enemy.ai_processor(unit_group)
end
end
Enemy.spawn_enemy_wave_token = Token.register(Enemy.spawn_enemy_wave)
function Enemy.on_enemy_died(entity)
local uid = entity.unit_number
local this = Public.get()
local data = this.invincible[uid]
if not data then
return
end
if game.tick > data.time_to_live then
this.invincible[uid] = nil
return
end
local new_entity = entity.surface.create_entity {
name = entity.name,
position = entity.position,
force = entity.force,
}
this.invincible[new_entity.unit_number] = {
time_to_live = data.time_to_live,
}
this.invincible[uid] = nil
if new_entity.type == 'unit' then
new_entity.set_command(entity.command)
if entity.unit_group then
entity.unit_group.add_member(new_entity)
end
end
end
function Enemy.on_spawner_died(event)
local entity = event.entity
local this = Public.get()
local budget = this.loot_budget + entity.position.x * 2.75
budget = budget * math_random(25, 175) * 0.01
local player = false
if event.cause and event.cause.type == 'character' then
player = event.cause.player
end
if player and player.valid then
budget = budget + (this.death_contributions[player.name] or 0) * 80
end
if math_random(1, 128) == 1 then budget = budget * 4 end
if math_random(1, 256) == 1 then budget = budget * 4 end
budget = budget * this.loot_richness
local chest = entity.surface.create_entity { name = 'steel-chest', position = entity.position, force = 'player', move_stuck_players = true }
chest.destructible = false
for i = 1, 4 do
local item_stacks = PriceRaffle.roll(math_floor(budget / 3 ) + 1, i*i + math_random(3))
for _, item_stack in pairs(item_stacks) do
chest.insert(item_stack)
end
end
if player then
Toast.toast_player(player, nil, {'frontier.loot_chest'})
end
end
function Enemy.roll_turret(x_distance, evolution)
local set = Enemy.turret_raffle['base']
if script.active_mods['Krastorio2'] then
set = Enemy.turret_raffle['Krastorio2']
end
if script.active_mods['zombiesextended-core'] then
set = Enemy.turret_raffle['zombiesextended-core']
end
local weighted_turrets_table = {}
for name, data in pairs(set) do
if data.min_distance < x_distance then
table.insert(weighted_turrets_table, { name, data.weight })
end
end
local turret = Table.get_random_weighted(weighted_turrets_table)
local refills = set[turret].refill
local refill
if evolution < 0.2 then
refill = refills[1]
elseif evolution < 0.5 then
refill = refills[2] or refills[#refills]
elseif evolution < 0.75 then
refill = refills[3] or refills[#refills]
else
refill = refills[4] or refills[#refills]
end
return turret, refill
end
function Enemy.spawn_turret_outpost(position)
local this = Public.get()
if position.x < this.right_boundary * 32 + this.wall_width then
return
end
local max_chance = math_clamp(0.02 * math_sqrt(position.x), 0.01, 0.04)
if math_random() > max_chance then
return
end
local surface = Public.surface()
if Public.ESCAPE_PLAYER then
for _, player in pairs(surface.find_entities_filtered{type = 'character'}) do
local pos = surface.find_non_colliding_position('character', { position.x -10, position.y }, 5, 0.5)
if pos then
player.teleport(pos, surface)
end
end
end
local evolution = game.forces.enemy.evolution_factor
for _, v in pairs({
{ x = -5, y = 0, direction = defines.direction.west },
{ x = 5, y = 0, direction = defines.direction.east },
{ x = 0, y = 5, direction = defines.direction.south },
{ x = 0, y = -5, direction = defines.direction.north },
}) do
local turret_name, refill_name = Enemy.roll_turret(position.x, evolution)
local pos = surface.find_non_colliding_position(turret_name, { position.x + v.x, position.y + v.y }, 2, 0.5)
if pos then
local turret = surface.create_entity {
name = turret_name,
position = pos,
force = 'enemy',
move_stuck_players = true,
create_build_effect_smoke = true,
direction = v.direction,
}
if turret and turret.valid then
EnemyTurret.register(turret, refill_name)
end
end
end
end
function Enemy.start_tracking(entity)
if not Enemy.target_entity_types[entity.type] then
return
end
if entity.force.name == 'enemy' or entity.force.name == 'neutral' then
return
end
register_on_entity_destroyed(entity)
Public.get().target_entities[entity.unit_number] = entity
end
function Enemy.stop_tracking(entity)
Public.get().target_entities[entity.unit_number] = nil
end
function Enemy.get_target()
return Table.get_random_dictionary_entry(Public.get().target_entities, false)
end
function Enemy.nuclear_explosion(position)
Public.surface().create_entity {
name = 'atomic-rocket',
position = position,
force = 'enemy',
source = position,
target = position,
max_range = 1,
speed = 0.1
}
end
Enemy.nuclear_explosion_token = Token.register(Enemy.nuclear_explosion)
function Enemy.artillery_explosion(data)
local surface = game.get_surface(data.surface_name)
local position = data.position
local r = 20
surface.create_entity {
name = 'artillery-projectile',
position = position,
force = 'enemy',
source = position,
target = { x = position.x + math_random(-r, r) + math_random(), y = position.y + math_random(-r, r) + math_random() },
max_range = 1,
speed = 0.1
}
end
Enemy.artillery_explosion_token = Token.register(Enemy.artillery_explosion)
function Enemy.on_research_finished(technology)
if technology.force.name ~= 'player' then
return
end
game.forces.enemy.technologies[technology.name].researched = true
end
return Enemy