local util = require("util") local get_walkable_tile = util.get_walkable_tile local mod_gui = require("mod-gui") local config = require("wave_defense_config") local upgrades = require("wave_defense_upgrades") local increment = util.increment local format_number = util.format_number local format_time = util.formattime local insert = table.insert local floor = math.floor local ceil = math.ceil local game_state = { in_round = 1, in_preview = 2, defeat = 3, victory = 4 } local script_data = { config = config, difficulty = config.difficulties.normal, day_number = 1, money = 0, team_upgrades = {}, gui_elements = { preview_frame = {}, day_button = {}, upgrade_frame_button = {}, upgrade_frame = {}, upgrade_table = {}, admin_frame_button = {}, admin_frame = {} }, gui_labels = { money_label = {}, time_label = {}, day_label = {} }, gui_actions = {}, spawners = {}, spawner_distances = {}, spawner_path_requests = {}, state = game_state.in_preview, random = nil, wave_tick = nil, spawn_time = nil, wave_time = nil, path_request_queue = {} } local get_starting_point = function() return {x = 0, y = 0} end local is_player_force = function(force) return force == game.forces.player end local get_preview_size = function() return 32 * 10 end local script_events = { on_round_started = script.generate_event_name() } local power_functions = { default = function(level) return (level ^ 1.15) * 500 * ((#game.connected_players) ^ 0.5) end, hard = function(level) return (level ^ 1.2) * 500 * ((#game.connected_players) ^ 0.75) end } local speed_multiplier_functions = { default = function(level) return (level ^ 0.1) - 0.2 end } local set_daytime_settings = function() local surface = script_data.surface if not (surface and surface.valid) then return end local settings = script_data.difficulty.day_settings for name, value in pairs (settings) do surface[name] = value end end local max_seed = 2^32 - 2 local initial_seed = 2390375328 local players = function(index) return (index and game.get_player(index)) or game.players end function deregister_gui(gui) local player_gui_actions = script_data.gui_actions[gui.player_index] if not player_gui_actions then return end player_gui_actions[gui.index] = nil for k, child in pairs (gui.children) do deregister_gui(child) end end function register_gui_action(gui, param) local gui_actions = script_data.gui_actions local player_gui_actions = gui_actions[gui.player_index] if not player_gui_actions then gui_actions[gui.player_index] = {} player_gui_actions = gui_actions[gui.player_index] end player_gui_actions[gui.index] = param end function init_player_force() for name, upgrade in pairs (get_upgrades()) do script_data.team_upgrades[name] = 0 end local force = game.forces.player force.reset() local surface = script_data.surface if surface and surface.valid then local size = get_preview_size() local starting_point = get_starting_point() force.chart(surface, {{starting_point.x - size, starting_point.y - size},{starting_point.x + (size - 32), starting_point.y + (size - 32)}}) end set_research(force) set_recipes(force) force.disable_research() end function set_tiles_safe(surface, tiles) local grass = get_walkable_tile() local grass_tiles = {} for k, tile in pairs (tiles) do grass_tiles[k] = {position = {x = (tile.position.x or tile.position[1]), y = (tile.position.y or tile.position[2])}, name = grass} end surface.set_tiles(grass_tiles, false) surface.set_tiles(tiles) end local set_up_player = function(player) if not player.connected then return end gui_init(player) if not is_player_force(player.force) then return end if player.ticks_to_respawn then player.ticks_to_respawn = nil end if script_data.state == game_state.in_preview then if player.character then player.character.destroy() end player.spectator = true player.set_controller{type = defines.controllers.god} player.teleport({0,0}, game.surfaces.nauvis) player.create_character() return end if script_data.state == game_state.in_round or script_data.state == game_state.victory then local surface = script_data.surface if player.surface == surface then return end if player.character then player.character.destroy() end local force = game.forces.player local spawn = force.get_spawn_position(surface) player.teleport(spawn, surface) local character = surface.create_entity{name = "character", position = surface.find_non_colliding_position("character", spawn, 0, 1), force = force} player.set_controller{type = defines.controllers.character, character = character} give_respawn_equipment(player) player.print({"wave-defense-intro"}) return end if script_data.state == game_state.defeat then if player.character then player.character.destroy() end local surface = script_data.surface local force = game.forces.player local position = force.get_spawn_position(surface) player.set_controller{type = defines.controllers.spectator} player.teleport(position, surface) return end end function set_up_players() for k, player in pairs (players()) do set_up_player(player) end end local init_enemy_force = function() local force = game.forces.enemy force.reset() force.evolution_factor = script_data.difficulty.starting_evolution_factor end function start_round() game.reset_time_played() local surface = script_data.surface surface.daytime = surface.dawn surface.always_day = false script_data.state = game_state.in_round local tick = game.tick script_data.money = 0 script_data.day_number = 1 --How often waves are sent script_data.wave_time = surface.ticks_per_day --How long waves last script_data.spawn_time = floor(surface.ticks_per_day * (surface.morning - surface.evening)) --First spawn script_data.wave_tick = tick + ceil(surface.ticks_per_day * surface.evening) + ceil((1 - surface.dawn) * surface.ticks_per_day) script_data.dawn_tick = nil script_data.spawn_tick = nil script_data.end_spawn_tick = nil game.print({"start-round-message"}) set_up_players() init_player_force() init_enemy_force() script.raise_event(script_events.on_round_started, {}) for k, player in pairs (players()) do player.clear_recipe_notifications() end end function restart_round() script_data.game_state = game_state.in_preview set_up_players() local seed = script_data.surface.map_gen_settings.seed create_battle_surface(seed) start_round() end local get_random_seed = function() return (32452867 * game.tick) % max_seed end local get_starting_area_size = function() return script_data.difficulty.starting_area_size end local get_base_radius = function() return (32 * (floor(((script_data.surface.get_starting_area_radius() / 32) - 1) / (2 ^ 0.5)))) end function create_battle_surface(seed) local index = 1 local name = "Surface " while game.surfaces[name..index] do index = index + 1 end name = name..index for k, surface in pairs (game.surfaces) do if surface.name ~= "nauvis" then game.delete_surface(surface.name) end end --Must be cleared before the new surface is generated, as these lists are updated on chunk_generated. script_data.spawners = {} script_data.spawner_distances = {} script_data.spawner_path_requests = {} script_data.path_request_queue = {} local settings = script_data.config.map_gen_settings local seed = seed or get_random_seed() script_data.random = game.create_random_generator(seed) settings.seed = seed settings.starting_area = get_starting_area_size() local starting_point = get_starting_point() settings.starting_points = {starting_point} settings.property_expression_names = { elevation = not script_data.config.infinite and "0_17-island" or nil } local surface = game.create_surface(name, settings) local size = get_preview_size() script_data.surface = surface set_daytime_settings() surface.request_to_generate_chunks(starting_point, 1 + ceil(get_base_radius() / 32)) surface.force_generate_chunk_requests() --Must force generate the starting chunks before placing the silo, walls etc. create_silo(starting_point) create_wall(starting_point) create_turrets(starting_point) create_starting_chest(starting_point) game.forces.player.chart(surface, {{starting_point.x - size, starting_point.y - size},{starting_point.x + (size - 32), starting_point.y + (size - 32)}}) for k, player in pairs (players()) do refresh_preview_gui(player) end end function create_silo(starting_point) local force = game.forces.player local surface = script_data.surface local silo_position = {starting_point.x, starting_point.y - 8} local silo_name = "rocket-silo" if not game.entity_prototypes[silo_name] then log("Silo not created as "..silo_name.." is not a valid entity prototype") return end local silo = surface.create_entity{name = silo_name, position = silo_position, force = force, raise_built = true, create_build_effect_smoke = false} if not (silo and silo.valid) then return end rendering.draw_light { sprite = "utility/light_medium", target = silo, surface = silo.surface, scale = 4 } silo.minable = false if silo.supports_backer_name() then silo.backer_name = "" end script_data.silo = silo local tile_name = "concrete" if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end local tiles_2 = {} local box = silo.bounding_box local x1, x2, y1, y2 = floor(box.left_top.x) - 1, floor(box.right_bottom.x) + 1, floor(box.left_top.y) - 1, floor(box.right_bottom.y) + 1 for X = x1, x2 do for Y = y1, y2 do insert(tiles_2, {name = tile_name, position = {X, Y}}) end end for i, entity in pairs(surface.find_entities_filtered({area = {{x1 - 1, y1 - 1},{x2 + 1, y2 + 1}}, force = "neutral"})) do entity.destroy() end set_tiles_safe(surface, tiles_2) end local is_in_map = function(width, height, position) return position.x >= -width and position.x < width and position.y >= -height and position.y < height end function create_wall(starting_point) local force = game.forces.player local surface = script_data.surface local origin = starting_point or force.get_spawn_position(surface) local radius = get_base_radius() + 5 local height = surface.map_gen_settings.height / 2 local width = surface.map_gen_settings.width / 2 local perimeter_top = {} local perimeter_bottom = {} local perimeter_left = {} local perimeter_right = {} local tiles = {} local insert = insert local can_place_entity = surface.can_place_entity for X = -radius, radius - 1 do insert(perimeter_top, {x = origin.x + X, y = origin.y - radius}) insert(perimeter_bottom, {x = origin.x + X, y = origin.y + (radius-1)}) end for Y = -radius, radius - 1 do insert(perimeter_left, {x = origin.x - radius, y = origin.y + Y}) insert(perimeter_right, {x = origin.x + (radius-1), y = origin.y + Y}) end local tile_name = "refined-concrete" if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end local areas = { {{perimeter_top[1].x, perimeter_top[1].y - 1}, {perimeter_top[#perimeter_top].x, perimeter_top[1].y + 3}}, {{perimeter_bottom[1].x, perimeter_bottom[1].y - 3}, {perimeter_bottom[#perimeter_bottom].x, perimeter_bottom[1].y + 1}}, {{perimeter_left[1].x - 1, perimeter_left[1].y}, {perimeter_left[1].x + 3, perimeter_left[#perimeter_left].y}}, {{perimeter_right[1].x - 3, perimeter_right[1].y}, {perimeter_right[1].x + 1, perimeter_right[#perimeter_right].y}} } local find_entities_filtered = surface.find_entities_filtered local destroy_param = {do_cliff_correction = true} for k, area in pairs (areas) do for i, entity in pairs(find_entities_filtered({area = area})) do entity.destroy(destroy_param) end end local wall_name = "stone-wall" local gate_name = "gate" if not game.entity_prototypes[wall_name] then log("Setting walls cancelled as "..wall_name.." is not a valid entity prototype") return end if not game.entity_prototypes[gate_name] then log("Setting walls cancelled as "..gate_name.." is not a valid entity prototype") return end local should_gate = { [12] = true, [13] = true, [14] = true, [15] = true, [16] = true, [17] = true, [18] = true, [19] = true } local create_entity = surface.create_entity for k, position in pairs (perimeter_left) do if is_in_map(width, height, position) and can_place_entity{name = wall_name, position = position, force = force, build_check_type = defines.build_check_type.manual_ghost, forced = true} then if (k ~= 1) and (k ~= #perimeter_left) then insert(tiles, {name = tile_name, position = {position.x + 2, position.y}}) insert(tiles, {name = tile_name, position = {position.x + 1, position.y}}) end if should_gate[position.y % 32] then create_entity{name = gate_name, position = position, direction = 0, force = force, create_build_effect_smoke = false} else create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false} end end end for k, position in pairs (perimeter_right) do if is_in_map(width, height, position) and can_place_entity{name = wall_name, position = position, force = force, build_check_type = defines.build_check_type.manual_ghost, forced = true} then if (k ~= 1) and (k ~= #perimeter_right) then insert(tiles, {name = tile_name, position = {position.x - 2, position.y}}) insert(tiles, {name = tile_name, position = {position.x - 1, position.y}}) end if should_gate[position.y % 32] then create_entity{name = gate_name, position = position, direction = 0, force = force, create_build_effect_smoke = false} else create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false} end end end for k, position in pairs (perimeter_top) do if is_in_map(width, height, position) and can_place_entity{name = wall_name, position = position, force = force, build_check_type = defines.build_check_type.manual_ghost, forced = true} then if (k ~= 1) and (k ~= #perimeter_top) then insert(tiles, {name = tile_name, position = {position.x, position.y + 2}}) insert(tiles, {name = tile_name, position = {position.x, position.y + 1}}) end if should_gate[position.x % 32] then create_entity{name = gate_name, position = position, direction = 2, force = force, create_build_effect_smoke = false} else create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false} end end end for k, position in pairs (perimeter_bottom) do if is_in_map(width, height, position) and can_place_entity{name = wall_name, position = position, force = force, build_check_type = defines.build_check_type.manual_ghost, forced = true} then if (k ~= 1) and (k ~= #perimeter_bottom) then insert(tiles, {name = tile_name, position = {position.x, position.y - 2}}) insert(tiles, {name = tile_name, position = {position.x, position.y - 1}}) end if should_gate[position.x % 32] then create_entity{name = gate_name, position = position, direction = 2, force = force, create_build_effect_smoke = false} else create_entity{name = wall_name, position = position, force = force, create_build_effect_smoke = false} end end end set_tiles_safe(surface, tiles) end function create_turrets(starting_point) local force = game.forces.player local turret_name = "gun-turret" if not game.entity_prototypes[turret_name] then return end local surface = script_data.surface local ammo_name = "firearm-magazine" local direction = defines.direction local surface = script_data.surface local height = surface.map_gen_settings.height / 2 local width = surface.map_gen_settings.width / 2 local origin = starting_point local radius = get_base_radius() - 5 local positions = {} local Xo = origin.x local Yo = origin.y for X = -radius, radius do local Xt = X + Xo if X == -radius then for Y = -radius, radius do local Yt = Y + Yo if (Yt + 16) % 32 ~= 0 and Yt % 8 == 0 then insert(positions, {x = Xo - radius, y = Yt, direction = direction.west}) insert(positions, {x = Xo + radius, y = Yt, direction = direction.east}) end end elseif (Xt + 16) % 32 ~= 0 and Xt % 8 == 0 then insert(positions, {x = Xt, y = Yo - radius, direction = direction.north}) insert(positions, {x = Xt, y = Yo + radius, direction = direction.south}) end end local tiles = {} local tile_name = "hazard-concrete-left" if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end local stack if ammo_name and game.item_prototypes[ammo_name] then stack = {name = ammo_name, count = 50} end local direction_offset = { [direction.north] = {0, -13}, [direction.east] = {13, 0}, [direction.south] = {0, 13}, [direction.west] = {-13, 0} } local find_entities_filtered = surface.find_entities_filtered local neutral = game.forces.neutral local destroy_params = {do_cliff_correction = true} local floor = floor local create_entity = surface.create_entity local can_place_entity = surface.can_place_entity for k, position in pairs (positions) do if is_in_map(width, height, position) and can_place_entity{name = turret_name, position = position, force = force, build_check_type = defines.build_check_type.manual_ghost, forced = true} then local turret = create_entity{name = turret_name, position = position, force = force, direction = position.direction, create_build_effect_smoke = false} local box = turret.bounding_box for k, entity in pairs (find_entities_filtered{area = turret.bounding_box, force = neutral}) do entity.destroy(destroy_params) end if stack then turret.insert(stack) end for x = floor(box.left_top.x), floor(box.right_bottom.x) do for y = floor(box.left_top.y), floor(box.right_bottom.y) do insert(tiles, {name = tile_name, position = {x, y}}) end end end end set_tiles_safe(surface, tiles) end local root_2 = 2 ^ 0.5 function get_chest_offset(n) local offset_x = 0 n = n / 2 if n % 1 == 0.5 then offset_x = -1 n = n + 0.5 end local root = n ^ 0.5 local nearest_root = math.floor(root + 0.5) local upper_root = math.ceil(root) local root_difference = math.abs(nearest_root ^ 2 - n) if nearest_root == upper_root then x = upper_root - root_difference y = nearest_root else x = upper_root y = root_difference end local orientation = 2 * math.pi * (45/360) x = x * root_2 y = y * root_2 local rotated_x = math.floor(0.5 + x * math.cos(orientation) - y * math.sin(orientation)) local rotated_y = math.floor(0.5 + x * math.sin(orientation) + y * math.cos(orientation)) return {x = rotated_x + offset_x, y = rotated_y} end function create_starting_chest(starting_point) local force = game.forces.player local inventory = script_data.difficulty.starting_chest_items if not (table_size(inventory) > 0) then return end local surface = script_data.surface local chest_name = "iron-chest" local prototype = game.entity_prototypes[chest_name] if not prototype then log("Starting chest "..chest_name.." is not a valid entity prototype, picking a new container from prototype list") for name, chest in pairs (game.entity_prototypes) do if chest.type == "container" then chest_name = name prototype = chest break end end end local size = math.ceil(prototype.radius * 2) local origin = {x = starting_point.x, y = starting_point.y} local index = 1 local position = {x = origin.x + get_chest_offset(index).x * size, y = origin.y + get_chest_offset(index).y * size} local chest = surface.create_entity{name = chest_name, position = position, force = force, create_build_effect_smoke = false} for k, v in pairs (surface.find_entities_filtered{force = "neutral", area = chest.bounding_box}) do v.destroy() end local tiles = {} local grass = {} local tile_name = "refined-concrete" if not game.tile_prototypes[tile_name] then tile_name = get_walkable_tile() end insert(tiles, {name = tile_name, position = {x = position.x, y = position.y}}) chest.destructible = false local items = game.item_prototypes for name, count in pairs (inventory) do if items[name] then local count_to_insert = math.ceil(count) local difference = count_to_insert - chest.insert{name = name, count = count_to_insert} while difference > 0 do index = index + 1 position = {x = origin.x + get_chest_offset(index).x * size, y = origin.y + get_chest_offset(index).y * size} chest = surface.create_entity{name = chest_name, position = position, force = force, create_build_effect_smoke = false} for k, v in pairs (surface.find_entities_filtered{force = "neutral", area = chest.bounding_box}) do v.destroy() end insert(tiles, {name = tile_name, position = {x = position.x, y = position.y}}) chest.destructible = false difference = difference - chest.insert{name = name, count = difference} end end end set_tiles_safe(surface, tiles) end local get_ticks_till_dawn = function() local surface = script_data.surface local current_daytime = surface.daytime local dawn = surface.dawn local diff = dawn - current_daytime if diff < 0 then diff = diff + 1 end local ticks = math.ceil(diff * surface.ticks_per_day) return ticks end local make_next_dawn_tick = function() script_data.dawn_tick = game.tick + get_ticks_till_dawn() end local check_dawn = function(tick) if not script_data.dawn_tick or tick < script_data.dawn_tick then return end increment(script_data, "day_number") game.print({"dawn-of-new-day", script_data.day_number}) update_label_list(script_data.gui_labels.day_label, {"current-day", script_data.day_number}) script_data.dawn_tick = nil end function check_next_wave(tick) if not script_data.wave_tick then return end if script_data.wave_tick ~= tick then return end next_wave() end function next_wave() make_next_wave_tick() make_next_spawn_tick() spawn_units() end function wave_end() make_next_dawn_tick() script_data.spawn_tick = nil script_data.end_spawn_tick = nil end local victory_sound = {path = "utility/game_won"} local round_won = function() if script_data.state ~= game_state.in_round then return end game.play_sound(victory_sound) game.print({"you-win", script_data.day_number}) script_data.state = game_state.victory set_up_players() --TODO, maybe popup some ugly GUI with stats etc. end function make_next_spawn_tick() local addition = 8 * 60 script_data.spawn_tick = game.tick + addition end function check_spawn_units(tick) if not script_data.spawn_tick then return end if script_data.end_spawn_tick <= tick then wave_end() return end if script_data.spawn_tick == tick then spawn_units() make_next_spawn_tick() end end function get_wave_spawners() local spawners = script_data.spawner_distances local wave_spawners = {} local count = math.min(#spawners, math.ceil(script_data.random(5, 15) * math.log(1 + script_data.day_number))) for k = count, 1, -1 do local spawn = spawners[k] if (spawn and spawn.entity.valid) then insert(wave_spawners, spawn.entity) else table.remove(spawners, k) end end return wave_spawners end local get_wave_power = function() return power_functions[script_data.difficulty.wave_power_function or "default"](script_data.day_number) end function get_wave_units() local wave = script_data.day_number local prices = script_data.difficulty.unit_prices local units = {} for name, unit_wave in pairs (script_data.difficulty.unit_waves) do if wave >= unit_wave[1] then if not unit_wave[2] or wave <= unit_wave[2] then insert(units, {name = name, amount = floor(((wave - unit_wave[1]) + 1) ^ 1.25), price = prices[name]}) end end end return units end local get_speed_multiplier = function() local level = script_data.day_number if level == 0 then return 0.8 end return speed_multiplier_functions[script_data.difficulty.speed_multiplier_function or "default"](level) end function select_unit(units, power) local roll_max = 1 local available = {} for k, unit in pairs (units) do if unit.price <= power then insert(available, unit) roll_max = roll_max + unit.amount end end local roll_value = script_data.random(roll_max) for k, unit in pairs (available) do roll_value = roll_value - unit.amount if (roll_value < 0) then return unit end end end local random_base_position = function() local random = script_data.random local position = get_starting_point() local radius = get_base_radius() position.x = position.x + random(-radius, radius) position.y = position.y + random(-radius, radius) return position end local group_path_flags = { cache = false, low_priority = false, no_break = true } function spawn_units() local random = script_data.random local surface = script_data.surface local silo = script_data.silo if not (silo and silo.valid) then return end local command = { type = defines.command.compound, structure_type = defines.compound_command.return_last, commands = { { type = defines.command.go_to_location, destination = random_base_position(), distraction = defines.distraction.by_anything, radius = 16, pathfind_flags = group_path_flags }, { type = defines.command.go_to_location, destination_entity = silo, distraction = defines.distraction.by_enemy, radius = get_base_radius() / 2, pathfind_flags = group_path_flags }, { type = defines.command.attack, target = silo, distraction = defines.distraction.by_damage } } } local power = get_wave_power() local some_spawns = get_wave_spawners() local spawns_count = #some_spawns if spawns_count == 0 then return end local units = get_wave_units() local units_length = #units local find_non_colliding_position = surface.find_non_colliding_position local create_entity = surface.create_entity local entities = game.entity_prototypes local speed_multiplier = get_speed_multiplier() local get_spawn_position = function(spawn_position, unit) local origin = {spawn_position[1] + random(-8, 8), spawn_position[2] + random(-8, 8)} local position = find_non_colliding_position(unit.name, origin, 0, 1) return position end local power_per_spawner = power / spawns_count for k, spawner in pairs (some_spawns) do local spawner_power = power_per_spawner local spawn_position = {spawner.position.x + random(-16, 16), spawner.position.y + random(-16, 16)} local group = surface.create_unit_group{position = spawn_position, force = spawner.force} for k, unit in pairs (spawner.units) do unit.release_from_spawner() unit.speed = unit.prototype.speed * speed_multiplier group.add_member(unit) end while true do local unit = select_unit(units, spawner_power) if not unit then break end spawner_power = spawner_power - unit.price local entity = create_entity{name = unit.name, position = get_spawn_position(spawn_position, unit)} entity.speed = entity.prototype.speed * speed_multiplier group.add_member(entity) if spawner_power <= 0 then break end end group.set_command(command) end end function make_next_wave_tick() script_data.end_spawn_tick = game.tick + script_data.spawn_time script_data.wave_tick = game.tick + script_data.wave_time end function time_to_next_wave() if not script_data.wave_tick then return end return format_time(script_data.wave_tick - game.tick) end function time_to_wave_end() if not script_data.end_spawn_tick then return end return format_time(script_data.end_spawn_tick - game.tick) end local lose_sound = {path = "utility/game_lost"} function rocket_died(event) if not (script_data.silo and script_data.silo.valid) then return end local silo = event.entity if silo ~= script_data.silo then return end script_data.state = game_state.defeat script_data.silo = nil set_up_players() game.play_sound(lose_sound) game.print({"you-lose", script_data.day_number}) end local insert_items = util.insert_safe give_respawn_equipment = function(player) if not is_player_force(player.force) then return end local equipment = script_data.difficulty.respawn_items local items = game.item_prototypes local list = {items = {}, armor = false, equipment = {}} for name, count in pairs (equipment) do local item = items[name] if item then if item.type == "armor" then local count = count if not list.armor then list.armor = item end count = count - 1 if count > 0 then list.items[item] = (list.items[item] or 0) + count end elseif item.place_as_equipment_result then list.equipment[item] = (list.equipment[item] or 0) + count else list.items[item] = (list.items[item] or 0) + count end else equipment[name] = nil end end local put_equipment = false if list.armor then local stack = player.get_inventory(defines.inventory.character_armor)[1] stack.set_stack{name = list.armor.name} local grid = stack.grid if grid then put_equipment = true for prototype, count in pairs (list.equipment) do local equipment = prototype.place_as_equipment_result for k = 1, count do local equipment = grid.put{name = equipment.name} if equipment then equipment.energy = equipment.max_energy else player.insert{name = prototype.name} end end end end end if not put_equipment then for prototype, count in pairs (list.equipment) do player.insert{name = prototype.name, count = count} end end for prototype, count in pairs (list.items) do player.insert{name = prototype.name, count = count} end end function refresh_preview_gui(player) local frame = script_data.gui_elements.preview_frame[player.index] if not (frame and frame.valid) then return end deregister_gui(frame) frame.clear() local admin = player.admin local inner = frame.add{type = "frame", style = "inside_deep_frame", direction = "vertical"}.add{type = "flow", direction = "vertical"} inner.style.vertical_spacing = 0 local subheader = inner.add{type = "frame", style = "subheader_frame"} local surface = script_data.surface if not (surface and surface.valid) then return end subheader.style.horizontally_stretchable = true local label = subheader.add{type = "label", caption = {"gui-map-generator.difficulty"}, style = "subheader_caption_label"} --label.style.vertically_stretchable = true label.style.vertical_align = "center" label.style.right_padding = 4 if admin then local config = subheader.add{type = "drop-down"} local count = 1 local index for name, difficulty in pairs (script_data.config.difficulties) do config.add_item{name} if difficulty == script_data.difficulty then index = count end count = count + 1 end config.selected_index = index register_gui_action(config, {type = "difficulty_changed"}) else local key for k, value in pairs (script_data.config.difficulties) do if value == script_data.difficulty then key = k break end end subheader.add{type = "label", caption = {key}, style = "caption_label"} end local line = subheader.add{type = "line", direction = "vertical"} local infinite_checkbox = subheader.add{type = "checkbox", state = script_data.config.infinite, caption = {"infinite-map"}, enabled = admin} register_gui_action(infinite_checkbox, {type = "infinite_checkbox_input"}) local pusher = subheader.add{type = "flow"} pusher.style.horizontally_stretchable = true local seed_flow = subheader.add{type = "flow", direction = "horizontal", style = "player_input_horizontal_flow"} seed_flow.add{type = "label", style = "caption_label", caption = {"gui-map-generator.map-seed"}} if admin then local seed_input = seed_flow.add { type = "textfield", text = surface.map_gen_settings.seed, style = "long_number_textfield", numeric = true, allow_decimal = false, allow_negative = false } register_gui_action(seed_input, {type = "check_seed_input"}) local shuffle_button = seed_flow.add{type = "sprite-button", sprite = "utility/shuffle", style = "tool_button"} register_gui_action(shuffle_button, {type = "shuffle_button"}) else seed_flow.add{type = "label", style = "caption_label", caption = surface.map_gen_settings.seed} end local size = get_preview_size() local max = math.min(size * 2, player.display_resolution.width * 0.8 / player.display_scale, player.display_resolution.height * 0.8 / player.display_scale) local zoom = max / (size * 2) local position = player.force.get_spawn_position(surface) local minimap = inner.add { type = "minimap", surface_index = surface.index, zoom = zoom, force = player.force.name, position = position } minimap.style.natural_width = max minimap.style.natural_height = max local button_flow = frame.add{type = "flow"} button_flow.style.horizontally_stretchable = true button_flow.style.vertical_align = "center" button_flow.style.top_padding = 4 local pusher = button_flow.add{type = "empty-widget", style = "draggable_space_header"} pusher.style.vertically_stretchable = true pusher.style.horizontally_stretchable = true pusher.drag_target = frame local start_round = button_flow.add{type = "button", caption = {"start-round"}, style = "confirm_button", enabled = admin} start_round.style.natural_width = max / 3 register_gui_action(start_round, {type = "start_round"}) end local setup_frame = {type = "frame", caption = {"setup-frame"}, direction = "vertical"} function make_preview_gui(player) local gui = player.gui.screen local frame = script_data.gui_elements.preview_frame[player.index] if not (frame and frame.valid) then frame = gui.add(setup_frame) frame.auto_center = true frame.style.horizontal_align = "right" frame.style.maximal_height = player.display_resolution.height / player.display_scale frame.style.vertically_stretchable = true script_data.gui_elements.preview_frame[player.index] = frame end refresh_preview_gui(player) end local day_button_param = { type = "button", ignored_by_interaction = true, style = mod_gui.button_style } local upgrade_button_param = { type = "button", caption = {"upgrade-button"}, tooltip = {"upgrade-button-tooltip"}, style = mod_gui.button_style } local admin_button_param = { type = "button", caption = {"admin"}, style = mod_gui.button_style } local add_admin_buttons = function(player) if not player.admin then return end local button_flow = mod_gui.get_button_flow(player) local admin_button = button_flow.add(admin_button_param) script_data.gui_elements.admin_frame_button[player.index] = admin_button register_gui_action(admin_button, {type = "admin_button"}) end local add_gui_buttons= function(player) if not is_player_force(player.force) then return end local button_flow = mod_gui.get_button_flow(player) local day_button = script_data.gui_elements.day_button[player.index] if not day_button then day_button = button_flow.add(day_button_param) script_data.gui_elements.day_button[player.index] = day_button end day_button.caption = {"current-day", script_data.day_number} insert(script_data.gui_labels.day_label, day_button) local upgrade_button = script_data.gui_elements.upgrade_frame_button[player.index] if not upgrade_button then upgrade_button = button_flow.add(upgrade_button_param) script_data.gui_elements.upgrade_frame_button[player.index] = upgrade_button register_gui_action(upgrade_button, {type = "upgrade_button"}) end end local delete_game_gui = function(player) local index = player.index for k, gui_list in pairs(script_data.gui_elements) do local element = gui_list[index] if (element and element.valid) then deregister_gui(element) element.destroy() end gui_list[index] = nil end end function gui_init(player) delete_game_gui(player) if script_data.state == game_state.in_preview then make_preview_gui(player) return end if script_data.state == game_state.in_round then add_gui_buttons(player) add_admin_buttons(player) return end if script_data.state == game_state.defeat or script_data.state == game_state.victory then add_admin_buttons(player) return end end local cash_font_color = {r = 0.8, b = 0.5, g = 0.8} local upgrade_frame = {type = "frame", style = mod_gui.frame_style, caption = {"buy-upgrades"}, direction = "vertical"} function toggle_upgrade_frame(player) local frame = script_data.gui_elements.upgrade_frame[player.index] if frame and frame.valid then deregister_gui(frame) frame.destroy() script_data.gui_elements.upgrade_frame[player.index] = nil return end frame = mod_gui.get_frame_flow(player).add(upgrade_frame) script_data.gui_elements.upgrade_frame[player.index] = frame frame.visible = true inner = frame.add{type = "frame", style = "inside_deep_frame", direction = "vertical"} local subheader = inner.add{type = "frame", style = "subheader_frame"} subheader.style.horizontally_stretchable = "true" local label = subheader.add{type = "label", caption = {"force-money"}, style = "subheader_label"} label.style.font = "default-semibold" local cash = subheader.add{type = "label", caption = get_money()} insert(script_data.gui_labels.money_label, cash) cash.style.font_color = {r = 0.8, b = 0.5, g = 0.8} local scroll = inner.add{type = "scroll-pane", style = "scroll_pane_with_dark_background_under_subheader"} scroll.style.padding = 0 scroll.style.maximal_height = (player.display_resolution.height * 0.5) / player.display_scale local upgrade_table = scroll.add{type = "table", column_count = 2} upgrade_table.style.horizontal_spacing = 0 upgrade_table.style.vertical_spacing = 0 script_data.gui_elements.upgrade_table[player.index] = upgrade_table update_upgrade_listing(player) end function update_upgrade_listing(player) local gui = script_data.gui_elements.upgrade_table[player.index] if not (gui and gui.valid) then return end local upgrades = script_data.team_upgrades deregister_gui(gui) gui.clear() for name, upgrade in pairs (get_upgrades()) do local level = upgrades[name] or 0 local sprite = gui.add{type = "sprite-button", name = name, sprite = upgrade.sprite, tooltip = {"purchase"}, style = "slot_sized_button"} sprite.style.minimal_height = 64 + 8 sprite.style.minimal_width = 64 + 8 sprite.style.margin = -1 sprite.number = upgrade.price(level) register_gui_action(sprite, {type = "purchase_button", name = name}) local flow = gui.add{type = "frame", name = name.."_flow", direction = "vertical", style = "subpanel_frame"} flow.style.horizontally_stretchable = true flow.style.vertically_stretchable = true local label = flow.add{type = "label", name = name.."_name", caption = {"", upgrade.caption, " "..upgrade.modifier}} label.style.font = "default-bold" local level = flow.add{type = "label", name = name.."_level", caption = {"upgrade-level", level}} end end function get_upgrades() return upgrades end function get_money() return format_number(script_data.money) end function update_label_list(list, caption) for k, label in pairs (list) do if label.valid then label.caption = caption else list[k] = nil end end end local admin_frame_param = { type = "frame", style = mod_gui.frame_style, caption = {"admin"}, direction = "vertical" } local admin_buttons = { { param = {type = "button", caption = {"end-round"}}, action = {type = "end_round"} }, { param = {type = "button", caption = {"restart-round"}}, action = {type = "restart_round"} }, --[[{ param = {type = "button", caption = "Dev only: Send wave"}, action = {type = "send_wave"} },]] } local toggle_admin_frame = function(player) if not (player and player.valid) then return end local frame = script_data.gui_elements.admin_frame[player.index] if (frame and frame.valid) then deregister_gui(frame) frame.destroy() script_data.gui_elements.admin_frame[player.index] = nil return end local gui = mod_gui.get_frame_flow(player) frame = gui.add(admin_frame_param) frame.style.vertically_stretchable = false frame.style.horizontally_stretchable = false script_data.gui_elements.admin_frame[player.index] = frame local inner = frame.add{type = "frame", direction = "vertical", style = "window_content_frame_deep"} for k, button in pairs (admin_buttons) do local butt = inner.add(button.param) butt.style.horizontally_stretchable = true register_gui_action(butt, button.action) end end local techs_to_disable = { "physical-projectile-damage", "stronger-explosives", "refined-flammables", "energy-weapons-damage", "weapon-shooting-speed", "laser-shooting-speed", "follower-robot-count", "mining-productivity" } function set_research(force) force.research_all_technologies() local tech = force.technologies for k, name in pairs (techs_to_disable) do for i = 1, 20 do local full_name = name.."-"..i if tech[full_name] then tech[full_name].researched = false end end end force.reset_technology_effects() end function set_recipes(force) local recipes = force.recipes local disable = { "automation-science-pack", "logistic-science-pack", "chemical-science-pack", "military-science-pack", "production-science-pack", "utility-science-pack", "lab" } for k, name in pairs (disable) do if recipes[name] then recipes[name].enabled = false else error(name.." is not a valid recipe") end end end local init_map_settings = function() local settings = game.map_settings settings.pollution.enabled = false settings.enemy_expansion.enabled = false --So, when path cache is enabled, negative path cache is also enabled. --The problem is, when a single unit inside a nest can't get to the silo, --He tells all other biters nearby that they also can't get to the silo. --Which causes whole groups of them just to chillout and idle... settings.path_finder.use_path_cache = false --The bases are surrounded by walls --This stops the pathfinder wasting a ton of time trying to go around the walls settings.path_finder.general_entity_collision_penalty = 1 settings.path_finder.general_entity_subsequent_collision_penalty = 1 settings.path_finder.max_steps_worked_per_tick = 1000 settings.path_finder.max_clients_to_accept_any_new_request = 5000 settings.path_finder.ignore_moving_enemy_collision_distance = 0 settings.short_request_max_steps = 1000000 settings.short_request_ratio = 1 settings.max_failed_behavior_count = 2 --settings.steering.moving.force_unit_fuzzy_goto_behavior = true --settings.steering.moving.radius = 6 --settings.steering.moving.separation_force = 0.02 --settings.steering.moving.separation_factor = 8 --settings.steering.default.force_unit_fuzzy_goto_behavior = true --settings.steering.default.radius = 1 --settings.steering.default.separation_force = 0.01 --settings.steering.default.separation_factor = 1 settings.unit_group= { -- pollution triggered group waiting time is a random time between min and max gathering time min_group_gathering_time = 3600, max_group_gathering_time = 10 * 3600, -- after the gathering is finished the group can still wait for late members, -- but it doesn't accept new ones anymore max_wait_time_for_late_members = 2 * 3600, -- limits for group radius (calculated by number of numbers) max_group_radius = 50.0, min_group_radius = 5.0, -- when a member falls behind the group he can speedup up till this much of his regular speed max_member_speedup_when_behind = 2, -- When a member gets ahead of its group, it will slow down to at most this factor of its speed max_member_slowdown_when_ahead = 0.9, -- When members of a group are behind, the entire group will slow down to at most this factor of its max speed max_group_slowdown_factor = 0.9, -- If a member falls behind more than this times the group radius, the group will slow down to max_group_slowdown_factor max_group_member_fallback_factor = 2, -- If a member falls behind more than this time the group radius, it will be removed from the group. member_disown_distance = 50, tick_tolerance_when_member_arrives = 60, -- Maximum number of automatically created unit groups gathering for attack at any time. max_gathering_unit_groups = 30, -- Maximum size of an attack unit group. This only affects automatically-created unit groups; manual groups -- created through the API are unaffected. max_unit_group_size = 200 } end local on_init = function() init_map_settings() game.forces.player.disable_research() game.surfaces.nauvis.always_day = true end local spawner_died = function(event) local spawner = event.entity if not (spawner and spawner.valid) then return end script_data.spawners[spawner.unit_number] = nil end local bounty_color = {r = 0.2, g = 0.8, b = 0.2, a = 0.2} local on_entity_died = function(event) if script_data.state ~= game_state.in_round then return end local died = event.entity if not (died and died.valid) then return end local bounty = script_data.difficulty.bounties[died.name] if bounty and is_player_force(event.force) then local cash = floor(bounty * (script_data.difficulty.bounty_modifier or 1)) increment(script_data, "money", cash) died.surface.create_entity{name = "flying-text", position = died.position, text = "+"..cash, color = bounty_color} update_label_list(script_data.gui_labels.money_label, get_money()) end if died.type == "rocket-silo" then return rocket_died(event) end if died.type == "unit-spawner" then return spawner_died(event) end end local on_rocket_launched = function(event) round_won() end local on_player_joined_game = function(event) local player = players(event.player_index) if not (script_data.surface and script_data.surface.valid) then create_battle_surface(initial_seed) end set_up_player(player) end local on_player_respawned = function(event) give_respawn_equipment(players(event.player_index)) end local is_reasonable_seed = function(string) local number = tonumber(string) if not number then return end if number < 0 or number > max_seed then return end return true end local end_round = function(player) script_data.state = game_state.in_preview script_data.wave_tick = nil script_data.spawn_tick = nil local seed = script_data.surface.map_gen_settings.seed game.delete_surface(script_data.surface) create_battle_surface(script_data.surface.map_gen_settings.seed) set_up_players() end local gui_functions = { upgrade_button = function(event) toggle_upgrade_frame(players(event.player_index)) end, admin_button = function(event) toggle_admin_frame(players(event.player_index)) end, purchase_button = function(event, param) local name = param.name local list = get_upgrades() local upgrades = script_data.team_upgrades local player = players(event.player_index) local upgrade = list[name] if not upgrade then --Maybe some migration, we don't have an upgrade by this name anymore, so, get lost... toggle_upgrade_frame(player) return end local price = upgrade.price(upgrades[name]) if script_data.money < price then player.print({"not-enough-money"}) return end increment(script_data, "money", -price) for k, effect in pairs (upgrade.effect) do effect(player.force) end increment(script_data.team_upgrades, name) player.force.print({"purchased-team-upgrade", player.name, upgrade.caption, upgrades[name]}) for k, player in pairs (game.connected_players) do update_upgrade_listing(player) end update_label_list(script_data.gui_labels.money_label, get_money()) end, shuffle_button = function(event, param) create_battle_surface() end, check_seed_input = function(event, param) local gui = event.element if not (gui and gui.valid) then return end if not is_reasonable_seed(gui.text) then return end gui.style = "long_number_textfield" if event.name == defines.events.on_gui_confirmed then create_battle_surface(tonumber(gui.text)) end end, infinite_checkbox_input = function(event, param) local gui = event.element if not (gui and gui.valid) then return end script_data.config.infinite = gui.state create_battle_surface(script_data.surface.map_gen_settings.seed) end, start_round = function(event, param) start_round() end, send_wave = function(event, param) spawn_units() end, end_round = function(event, param) end_round() end, restart_round = function(event, param) restart_round() end, difficulty_changed = function(event, param) local gui = event.element if not (gui and gui.valid) then return end if not (event.name == defines.events.on_gui_selection_state_changed) then return end local selected = gui.selected_index local index = 1 for name, difficulty in pairs (script_data.config.difficulties) do if index == selected then script_data.difficulty = difficulty break end index = index + 1 end create_battle_surface(script_data.surface.map_gen_settings.seed) end } function generic_gui_event(event) local gui = event.element if not (gui and gui.valid) then return end local player_gui_actions = script_data.gui_actions[gui.player_index] if not player_gui_actions then return end local action = player_gui_actions[gui.index] if not action then return end gui_functions[action.type](event, action) end local chart_base_area = function() if script_data.state ~= game_state.in_round then return end local surface = script_data.surface if not (surface and surface.valid) then return end local force = game.forces.player local origin = force.get_spawn_position(surface) local size = get_base_radius() force.chart(surface, { { origin.x - (size + 32), origin.y - (size + 32) }, { origin.x + size, origin.y + size } }) end local collision_mask = {"colliding-with-tiles-only", "water-tile"} local bounding_box = {{0,0},{0,0}} local flags = { cache = false, low_priority = false, no_break = true } local max_pending_paths = 15 local process_path_queue = function() local queue = script_data.path_request_queue if not queue then return end local requests = script_data.spawner_path_requests local current_count = table_size(requests) for k = 1, (max_pending_paths - current_count) do local unit_number, spawner = next(queue) if not unit_number then return end queue[unit_number] = nil if not (spawner and spawner.valid) then return end local key = spawner.surface.request_path { bounding_box = bounding_box, collision_mask = collision_mask, start = spawner.position, goal = get_starting_point(), radius = get_base_radius(), force = spawner.force, path_resolution_modifier = -1, pathfind_flags = flags } requests[key] = spawner end end local on_tick = function(event) local tick = event.tick if script_data.state == game_state.in_round then check_next_wave(tick) check_spawn_units(tick) check_dawn(tick) process_path_queue() return end if script_data.state == game_state.in_preview then if script_data.surface and script_data.surface.valid then script_data.surface.force_generate_chunk_requests() end end end local oh_no_you_dont = {game_finished = false} local on_player_died = function(event) if not game.is_multiplayer() then game.set_game_state(oh_no_you_dont) end end local request_path_for_spawner = function(spawner) if not (spawner and spawner.valid) then return end local unit_number = spawner.unit_number if script_data.spawners[unit_number] then --Already know a path exists. return end script_data.path_request_queue[unit_number] = spawner end local on_chunk_generated = function(event) local surface = event.surface if not (surface and surface.valid and surface == script_data.surface) then return end for k, spawner in pairs (surface.find_entities_filtered{area = event.area, type = "unit-spawner"}) do request_path_for_spawner(spawner) end end local refresh_player_gui_event = function(event) return gui_init(players(event.player_index)) end local add_remote_interface = function() remote.add_interface("wave_defense", { set_config = function(data) if type(data) ~= "table" then error("Data type for 'set_config' must be a table") end log("Wave defense config set by remote call, can expect script errors after this point.") script_data.config = data end, get_config = function() return script_data.config end, get_events = function() return script_events end }) end local on_script_path_request_finished = function(event) local id = event.id local spawner = script_data.spawner_path_requests[id] if not (spawner and spawner.valid) then return end script_data.spawner_path_requests[id] = nil if event.try_again_later then request_path_for_spawner(spawner) return end if not event.path then --pathing from the spawner to the silo failed, so we don't add it to our list of spawn/kill candidates. return end script_data.spawners[spawner.unit_number] = spawner local path = event.path local distance = #path local spawners = script_data.spawner_distances local inserted = false for k, other_spawner in pairs (spawners) do if distance < other_spawner.distance then insert(spawners, k, {entity = spawner, distance = distance}) inserted = true break end end if not inserted then insert(spawners, {entity = spawner, distance = distance}) end end local on_ai_command_completed = function(event) --Used only for debugging. local unit = script_data.units[event.unit_number] local silo = script_data.silo if not (silo and silo.valid) then return end if unit and unit.valid then unit.ai_settings.path_resolution_modifier = math.min(0, unit.ai_settings.path_resolution_modifier + 1) unit.set_command { type = defines.command.attack, target = silo, distraction = defines.distraction.by_damage } end end local on_pre_player_died = function(event) -- People were complaining about cheesing the death and respawn items. -- So we just remove all the respawn items from them when they die. -- Theoretically, they can put the items in a chest and then die, but this covers the typical case. local player = game.get_player(event.player_index) if not (player and player.valid) then return end if not is_player_force(player.force) then return end local remove_item = player.remove_item for name, count in pairs (script_data.difficulty.respawn_items) do remove_item{name = name, count = count} end end local on_player_changed_force = function(event) local player = players(event.player_index) set_up_player(player) end local on_technology_effects_reset = function(event) local force = event.force if force.name ~= "player" then return end local upgrades = script_data.team_upgrades for name, upgrade in pairs (get_upgrades()) do local count = upgrades[name] or 0 if count > 1 then for k, effect in pairs (upgrade.effect) do for j = 1, count do effect(force) end end end end end local lib = {} lib.events = { [defines.events.on_chunk_generated] = on_chunk_generated, [defines.events.on_entity_died] = on_entity_died, [defines.events.on_gui_click] = generic_gui_event, [defines.events.on_gui_selection_state_changed] = generic_gui_event, [defines.events.on_gui_text_changed] = generic_gui_event, [defines.events.on_gui_confirmed] = generic_gui_event, [defines.events.on_gui_checked_state_changed] = generic_gui_event, [defines.events.on_player_died] = on_player_died, [defines.events.on_pre_player_died] = on_pre_player_died, [defines.events.on_player_demoted] = refresh_player_gui_event, [defines.events.on_player_display_resolution_changed] = refresh_player_gui_event, [defines.events.on_player_display_scale_changed] = refresh_player_gui_event, [defines.events.on_player_promoted] = refresh_player_gui_event, [defines.events.on_player_joined_game] = on_player_joined_game, [defines.events.on_player_changed_force] = on_player_changed_force, [defines.events.on_player_respawned] = on_player_respawned, [defines.events.on_rocket_launched] = on_rocket_launched, [defines.events.on_script_path_request_finished] = on_script_path_request_finished, --[defines.events.on_ai_command_completed] = on_ai_command_completed, [defines.events.on_tick] = on_tick, [defines.events.on_technology_effects_reset] = on_technology_effects_reset } lib.on_nth_tick = { [13] = chart_base_area } lib.on_event = function(event) local action = events[event.name] if not action then return end return action(event) end lib.on_load = function() script_data = global.wave_defense or script_data add_remote_interface() end lib.on_init = function() global.wave_defense = global.wave_defense or script_data on_init() add_remote_interface() end lib.on_configuration_changed = function(data) game.forces.player.reset_technology_effects() for name, upgrade in pairs (get_upgrades()) do script_data.team_upgrades[name] = script_data.team_upgrades[name] or 0 end for k, player in pairs (game.players) do update_upgrade_listing(player) end init_map_settings() set_recipes(game.forces.player) game.forces.player.disable_research() if script_data.surface and script_data.surface.valid then script_data.path_request_queue = {} script_data.spawner_path_requests = {} for k, spawner in pairs (script_data.surface.find_entities_filtered{type = "unit-spawner"}) do request_path_for_spawner(spawner) end end if type(script_data.difficulty.wave_power_function) ~= "string" then script_data.difficulty.wave_power_function = "default" end if type(script_data.difficulty.speed_multiplier_function) ~= "string" then script_data.difficulty.speed_multiplier_function = "default" end end return lib