diff --git a/maps/fish_defender/main.lua b/maps/fish_defender/main.lua index 6f5b9a98..2e116aa7 100644 --- a/maps/fish_defender/main.lua +++ b/maps/fish_defender/main.lua @@ -883,7 +883,9 @@ local function on_player_joined_game(event) is_game_lost() end - if game.tick < 5 then game.forces.player.chart(game.surfaces["fish_defender"], {{-256, -512},{768, 512}}) end + if global.charting_done then return end + game.forces.player.chart(game.surfaces["fish_defender"], {{-256, -512},{768, 512}}) + global.charting_done = true end local function on_built_entity(event) diff --git a/maps/fish_defender/terrain.lua b/maps/fish_defender/terrain.lua index 4b2a1586..725f77c4 100644 --- a/maps/fish_defender/terrain.lua +++ b/maps/fish_defender/terrain.lua @@ -302,6 +302,11 @@ local function process_chunk(left_top) end end end + + if game.tick == 0 then return end + if game.forces.player.is_chunk_charted(surface, {left_top.x / 32, left_top.y / 32}) then + game.forces.player.chart(surface, {{left_top.x, left_top.y},{left_top.x + 31, left_top.y + 31}}) + end end local function process_chunk_queue() @@ -324,5 +329,5 @@ local function on_chunk_generated(event) end local event = require 'utils.event' -event.on_nth_tick(16, process_chunk_queue) +event.on_nth_tick(30, process_chunk_queue) event.add(defines.events.on_chunk_generated, on_chunk_generated) \ No newline at end of file diff --git a/maps/fish_defender_v1/boss_biters.lua b/maps/fish_defender_v1/boss_biters.lua new file mode 100644 index 00000000..650ed730 --- /dev/null +++ b/maps/fish_defender_v1/boss_biters.lua @@ -0,0 +1,74 @@ +local boss_biter = {} +local math_random = math.random +local radius = 6 +local targets = {} +local acid_splashes = { + ["big-biter"] = "acid-stream-worm-medium", + ["behemoth-biter"] = "acid-stream-worm-big" +} +local acid_lines = { + ["big-spitter"] = "acid-stream-spitter-medium", + ["behemoth-spitter"] = "acid-stream-spitter-big" +} +for x = radius * -1, radius, 1 do + for y = radius * -1, radius, 1 do + if math.sqrt(x^2 + y^2) <= radius then + targets[#targets + 1] = {x = x, y = y} + end + end +end + +local function acid_nova(event) + for _ = 1, math.random(20, 40) do + local i = math.random(1, #targets) + event.entity.surface.create_entity({ + name = acid_splashes[event.entity.name], + position = event.entity.position, + force = event.entity.force.name, + source = event.entity.position, + target = {x = event.entity.position.x + targets[i].x, y = event.entity.position.y + targets[i].y}, + max_range = radius, + speed = 0.001 + }) + end +end + +boss_biter.died = function(event) + if acid_splashes[event.entity.name] then acid_nova(event) end + if global.acid_lines_delay[event.entity.unit_number] then global.acid_lines_delay[event.entity.unit_number] = nil end + global.boss_biters[event.entity.unit_number] = nil +end + +local function acid_line(surface, name, source, target) + local distance = math.sqrt((source.x - target.x) ^ 2 + (source.y - target.y) ^ 2) + local modifier = {(target.x - source.x) / distance, (target.y - source.y) / distance} + + local position = {source.x, source.y} + + for i = 1, distance * 1.5, 1 do + if math_random(1,2) ~= 1 then + surface.create_entity({ + name = name, + position = source, + force = "enemy", + source = source, + target = position, + max_range = 25, + speed = 1 + }) + end + position = {position[1] + modifier[1], position[2] + modifier[2]} + end +end + +boss_biter.damaged_entity = function(event) + if acid_lines[event.cause.name] then + if not global.acid_lines_delay[event.cause.unit_number] then global.acid_lines_delay[event.cause.unit_number] = 0 end + if global.acid_lines_delay[event.cause.unit_number] < game.tick then + acid_line(event.cause.surface, acid_lines[event.cause.name], event.cause.position, event.entity.position) + global.acid_lines_delay[event.cause.unit_number] = game.tick + 180 + end + end +end + +return boss_biter \ No newline at end of file diff --git a/maps/fish_defender_v1/bouncy_shells.lua b/maps/fish_defender_v1/bouncy_shells.lua new file mode 100644 index 00000000..53ad988a --- /dev/null +++ b/maps/fish_defender_v1/bouncy_shells.lua @@ -0,0 +1,63 @@ +local radius = 9 +local math_random = math.random +local math_sqrt = math.sqrt + +local ammo_to_projectile_translation = { + ["shotgun-shell"] = "shotgun-pellet", + ["piercing-shotgun-shell"] = "piercing-shotgun-pellet" +} + +local function create_projectile(surface, position, target, name) + surface.create_entity({ + name = name, + position = position, + force = force, + source = position, + target = target, + max_range = 16, + speed = 0.3 + }) +end + +local function bounce(surface, position, ammo) + if math_random(1,3) ~= 1 then return end + local valid_entities = {} + for _, e in pairs(surface.find_entities_filtered({area = {{position.x - radius, position.y - radius},{position.x + radius, position.y + radius}}})) do + if e.health then + if e.force.name ~= "player" then + --local distance_from_center = math_sqrt((e.position.x - position.x) ^ 2 + (e.position.y - position.y) ^ 2) + --if distance_from_center <= radius then + valid_entities[#valid_entities + 1] = e + --end + end + end + end + + if not valid_entities[1] then return end + + for c = 1, math_random(3,6), 1 do + create_projectile(surface, position, valid_entities[math_random(1, #valid_entities)].position, ammo) + end +end + +local function bouncy_shells(event) + if event.damage_type.name ~= "physical" then return false end + local player = event.cause + if player.shooting_state.state == defines.shooting.not_shooting then return false end + local selected_weapon = player.get_inventory(defines.inventory.character_guns)[player.selected_gun_index] + if selected_weapon.name ~= "combat-shotgun" and selected_weapon.name ~= "shotgun" then return false end + + local selected_ammo = player.get_inventory(defines.inventory.character_ammo)[player.selected_gun_index] + if not selected_ammo then return end + if not ammo_to_projectile_translation[selected_ammo.name] then return end + + bounce( + player.surface, + event.entity.position, + ammo_to_projectile_translation[selected_ammo.name] + ) +end + +return bouncy_shells + + diff --git a/maps/fish_defender/changelog.txt b/maps/fish_defender_v1/changelog.txt similarity index 100% rename from maps/fish_defender/changelog.txt rename to maps/fish_defender_v1/changelog.txt diff --git a/maps/fish_defender_v1/crumbly_walls.lua b/maps/fish_defender_v1/crumbly_walls.lua new file mode 100644 index 00000000..bc39db91 --- /dev/null +++ b/maps/fish_defender_v1/crumbly_walls.lua @@ -0,0 +1,15 @@ +local event = require 'utils.event' +local math_random = math.random + +local rock_raffle = {"sand-rock-big", "rock-big", "rock-big", "rock-big", "rock-huge"} + +local function on_entity_died(event) + if not global.crumbly_walls_unlocked then return end + local entity = event.entity + if not entity.valid then return end + if entity.name ~= "stone-wall" then return end + if math_random(1,4) == 1 then return end + entity.surface.create_entity({name = rock_raffle[math_random(1, #rock_raffle)], position = entity.position, force = "player"}) +end + +event.add(defines.events.on_entity_died, on_entity_died) diff --git a/maps/fish_defender_v1/explosive_gun_bullets.lua b/maps/fish_defender_v1/explosive_gun_bullets.lua new file mode 100644 index 00000000..09932da2 --- /dev/null +++ b/maps/fish_defender_v1/explosive_gun_bullets.lua @@ -0,0 +1,36 @@ +local radius = 3 + +local function splash_damage(surface, position, final_damage_amount) + local damage = math.random(math.floor(final_damage_amount * 3), math.floor(final_damage_amount * 4)) + for _, e in pairs(surface.find_entities_filtered({area = {{position.x - radius, position.y - radius},{position.x + radius, position.y + radius}}})) do + if e.valid and e.health then + local distance_from_center = math.sqrt((e.position.x - position.x) ^ 2 + (e.position.y - position.y) ^ 2) + if distance_from_center <= radius then + local damage_distance_modifier = 1 - distance_from_center / radius + if damage > 0 then + if math.random(1, 3) == 1 then surface.create_entity({name = "explosion", position = e.position}) end + e.damage(damage * damage_distance_modifier, "player", "explosion") + end + end + end + end +end + +local function explosive_bullets(event) + if math.random(1, 3) ~= 1 then return false end + if event.damage_type.name ~= "physical" then return false end + local player = event.cause + if player.shooting_state.state == defines.shooting.not_shooting then return false end + local selected_weapon = player.get_inventory(defines.inventory.character_guns)[player.selected_gun_index] + if selected_weapon.name ~= "submachine-gun" and selected_weapon.name ~= "pistol" then return false end + + player.surface.create_entity({name = "explosion", position = event.entity.position}) + + splash_damage( + player.surface, + event.entity.position, + event.final_damage_amount + ) +end + +return explosive_bullets diff --git a/maps/fish_defender_v1/fish_defender.lua b/maps/fish_defender_v1/fish_defender.lua new file mode 100644 index 00000000..e5cf0a56 --- /dev/null +++ b/maps/fish_defender_v1/fish_defender.lua @@ -0,0 +1,1453 @@ +-- fish defender -- by mewmew -- + +require "maps.fish_defender.map_intro" +require "maps.fish_defender.market" +require "maps.fish_defender.shotgun_buff" +require "maps.fish_defender.on_entity_damaged" + +require "modules.rocket_launch_always_yields_science" +require "modules.launch_fish_to_win" +require "modules.biters_yield_coins" +require "modules.dynamic_landfill" +require "modules.dangerous_goods" +require "modules.custom_death_messages" +require "modules.biter_evasion_hp_increaser" + +local event = require 'utils.event' +local boss_biter = require "maps.fish_defender.boss_biters" +require "functions.boss_unit" +local map_functions = require "tools.map_functions" +local math_random = math.random +local insert = table.insert +local enable_start_grace_period = true + +local biter_count_limit = 1024 --maximum biters on the east side of the map, next wave will be delayed if the maximum has been reached +local boss_waves = { + [50] = {{name = "big-biter", count = 3}}, + [100] = {{name = "behemoth-biter", count = 1}}, + [150] = {{name = "behemoth-spitter", count = 4}, {name = "big-spitter", count = 16}}, + [200] = {{name = "behemoth-biter", count = 4}, {name = "behemoth-spitter", count = 2}, {name = "big-biter", count = 32}}, + [250] = {{name = "behemoth-biter", count = 8}, {name = "behemoth-spitter", count = 4}, {name = "big-spitter", count = 32}}, + [300] = {{name = "behemoth-biter", count = 16}, {name = "behemoth-spitter", count = 8}} +} + +local difficulties_votes = { + [1] = {wave_interval = 5100, amount_modifier = 0.55, strength_modifier = 0.40}, + [2] = {wave_interval = 4500, amount_modifier = 0.75, strength_modifier = 0.65}, + [3] = {wave_interval = 4000, amount_modifier = 0.90, strength_modifier = 0.85}, + [4] = {wave_interval = 3600, amount_modifier = 1.00, strength_modifier = 1.00}, + [5] = {wave_interval = 3200, amount_modifier = 1.10, strength_modifier = 1.25}, + [6] = {wave_interval = 2700, amount_modifier = 1.25, strength_modifier = 1.75}, + [7] = {wave_interval = 2100, amount_modifier = 1.50, strength_modifier = 2.50} +} + +local function shuffle(tbl) + local size = #tbl + for i = size, 1, -1 do + local rand = math.random(size) + tbl[i], tbl[rand] = tbl[rand], tbl[i] + end + return tbl +end + +local function create_wave_gui(player) + if player.gui.top["fish_defense_waves"] then player.gui.top["fish_defense_waves"].destroy() end + local frame = player.gui.top.add({ type = "frame", name = "fish_defense_waves", tooltip = "Click to show map info"}) + frame.style.maximal_height = 38 + + local wave_count = 0 + if global.wave_count then wave_count = global.wave_count end + + if not global.wave_grace_period then + local label = frame.add({ type = "label", caption = "Wave: " .. wave_count }) + label.style.font_color = {r=0.88, g=0.88, b=0.88} + label.style.font = "default-listbox" + label.style.left_padding = 4 + label.style.right_padding = 4 + label.style.minimal_width = 68 + label.style.font_color = {r=0.33, g=0.66, b=0.9} + + local next_level_progress = game.tick % global.wave_interval / global.wave_interval + + local progressbar = frame.add({ type = "progressbar", value = next_level_progress}) + progressbar.style.minimal_width = 120 + progressbar.style.maximal_width = 120 + progressbar.style.top_padding = 10 + else + local time_remaining = math.floor(((global.wave_grace_period - (game.tick % global.wave_grace_period)) / 60) / 60) + if time_remaining <= 0 then + global.wave_grace_period = nil + return + end + + local label = frame.add({ type = "label", caption = "Waves will start in " .. time_remaining .. " minutes."}) + label.style.font_color = {r=0.88, g=0.88, b=0.88} + label.style.font = "default-listbox" + label.style.left_padding = 4 + label.style.right_padding = 4 + label.style.font_color = {r=0.33, g=0.66, b=0.9} + + if not enable_start_grace_period then global.wave_grace_period = nil return end + end +end + +local function show_fd_stats(player) + local gui_id = "fd-stats" + local table_id = gui_id.."table" + + if player.gui.left[gui_id] then + player.gui.left[gui_id].destroy() + end + + local frame = player.gui.left.add { + type = "frame", + name = gui_id + } + local table = frame.add { + type = "table", + name = table_id, + column_count = 2, + } + + local table_header = {"Building", "Placed"..'/'.."Limit"} + for k,v in pairs(table_header) do + local h = table.add { type="label", caption=v } + h.style.font = "heading-2" + end + + for k,v in pairs(global.entity_limits) do + local name = v.str + local placed = v.placed + local limit = v.limit + local entry = {name, placed..'/'..limit} + for k, v in pairs(entry) do + table.add { + type = "label", + caption = v + } + end + end +end + +local function update_fd_stats() + for _, player in pairs(game.connected_players) do + if player.gui.left["fd-stats"] then + show_fd_stats(player) + end + end +end + +local function add_fd_stats_button(player) + local button_id = "fd-stats-button" + if player.gui.top[button_id] then + player.gui.top[button_id].destroy() + end + local button = player.gui.top.add { + type = "sprite-button", + name = button_id, + sprite = "item/submachine-gun" + } +end + +local function on_gui_click(event) + if not event.element.valid then + return + end + if event.element.name ~= "fd-stats-button" then + return + end + local player = game.players[event.player_index] + local frame = player.gui.left["fd-stats"] + if frame == nil then + show_fd_stats(player) + else + frame.destroy() + end +end + +local function on_market_item_purchased(event) + update_fd_stats() +end + +local threat_values = { + ["small_biter"] = 1, + ["medium_biter"] = 3, + ["big_biter"] = 5, + ["behemoth_biter"] = 10, + ["small_spitter"] = 1, + ["medium_spitter"] = 3, + ["big_spitter"] = 5, + ["behemoth_spitter"] = 10 +} + +local function get_biter_initial_pool() + local biter_pool = {} + if global.wave_count > 1750 then + biter_pool = { + {name = "behemoth-biter", threat = threat_values.behemoth_biter, weight = 2}, + {name = "behemoth-spitter", threat = threat_values.behemoth_spitter, weight = 1} + } + return biter_pool + end + if global.wave_count > 1500 then + biter_pool = { + {name = "big-biter", threat = threat_values.big_biter, weight = 1}, + {name = "behemoth-biter", threat = threat_values.behemoth_biter, weight = 2}, + {name = "behemoth-spitter", threat = threat_values.behemoth_spitter, weight = 1} + } + return biter_pool + end + if global.wave_count > 1250 then + biter_pool = { + {name = "big-biter", threat = threat_values.big_biter, weight = 2}, + {name = "behemoth-biter", threat = threat_values.behemoth_biter, weight = 2}, + {name = "behemoth-spitter", threat = threat_values.behemoth_spitter, weight = 1} + } + return biter_pool + end + if global.wave_count > 1000 then + biter_pool = { + {name = "big-biter", threat = threat_values.big_biter, weight = 3}, + {name = "behemoth-biter", threat = threat_values.behemoth_biter, weight = 2}, + {name = "behemoth-spitter", threat = threat_values.behemoth_spitter, weight = 1} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.1 then + biter_pool = { + {name = "small-biter", threat = threat_values.small_biter, weight = 3}, + {name = "small-spitter", threat = threat_values.small_spitter, weight = 1} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.2 then + biter_pool = { + {name = "small-biter", threat = threat_values.small_biter, weight = 10}, + {name = "medium-biter", threat = threat_values.medium_biter, weight = 2}, + {name = "small-spitter", threat = threat_values.small_spitter, weight = 5}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 1} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.3 then + biter_pool = { + {name = "small-biter", threat = threat_values.small_biter, weight = 18}, + {name = "medium-biter", threat = threat_values.medium_biter, weight = 6}, + {name = "small-spitter", threat = threat_values.small_spitter, weight = 8}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 3}, + {name = "big-biter", threat = threat_values.big_biter, weight = 1} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.4 then + biter_pool = { + {name = "small-biter", threat = threat_values.small_biter, weight = 2}, + {name = "medium-biter", threat = threat_values.medium_biter, weight = 8}, + {name = "big-biter", threat = threat_values.big_biter, weight = 2}, + {name = "small-spitter", threat = threat_values.small_spitter, weight = 1}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 4}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 1} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.5 then + biter_pool = { + {name = "small-biter", threat = threat_values.small_biter, weight = 2}, + {name = "medium-biter", threat = threat_values.medium_biter, weight = 4}, + {name = "big-biter", threat = threat_values.big_biter, weight = 8}, + {name = "small-spitter", threat = threat_values.small_spitter, weight = 1}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 2}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 4} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.6 then + biter_pool = { + {name = "medium-biter", threat = threat_values.medium_biter, weight = 4}, + {name = "big-biter", threat = threat_values.big_biter, weight = 8}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 2}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 4} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.7 then + biter_pool = { + {name = "behemoth-biter", threat = threat_values.small_biter, weight = 2}, + {name = "medium-biter", threat = threat_values.medium_biter, weight = 12}, + {name = "big-biter", threat = threat_values.big_biter, weight = 20}, + {name = "behemoth-spitter", threat = threat_values.small_spitter, weight = 1}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 6}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 10} + } + return biter_pool + end + if game.forces.enemy.evolution_factor < 0.8 then + biter_pool = { + {name = "behemoth-biter", threat = threat_values.small_biter, weight = 2}, + {name = "medium-biter", threat = threat_values.medium_biter, weight = 4}, + {name = "big-biter", threat = threat_values.big_biter, weight = 10}, + {name = "behemoth-spitter", threat = threat_values.small_spitter, weight = 1}, + {name = "medium-spitter", threat = threat_values.medium_spitter, weight = 2}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 5} + } + return biter_pool + end + if game.forces.enemy.evolution_factor <= 0.9 then + biter_pool = { + {name = "big-biter", threat = threat_values.big_biter, weight = 12}, + {name = "behemoth-biter", threat = threat_values.behemoth_biter, weight = 2}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 6}, + {name = "behemoth-spitter", threat = threat_values.behemoth_spitter, weight = 1} + } + return biter_pool + end + if game.forces.enemy.evolution_factor <= 1 then + biter_pool = { + {name = "big-biter", threat = threat_values.big_biter, weight = 4}, + {name = "behemoth-biter", threat = threat_values.behemoth_biter, weight = 2}, + {name = "big-spitter", threat = threat_values.big_spitter, weight = 2}, + {name = "behemoth-spitter", threat = threat_values.behemoth_spitter, weight = 1} + } + return biter_pool + end +end + +local function get_biter_pool() + local surface = game.surfaces["fish_defender"] + local biter_pool = get_biter_initial_pool() + local biter_raffle = {} + for _, biter_type in pairs(biter_pool) do + for x = 1, biter_type.weight, 1 do + insert(biter_raffle, {name = biter_type.name, threat = biter_type.threat}) + end + end + return biter_raffle +end + +local function spawn_biter(pos, biter_pool) + if global.attack_wave_threat < 1 then return false end + local surface = game.surfaces["fish_defender"] + biter_pool = shuffle(biter_pool) + global.attack_wave_threat = global.attack_wave_threat - biter_pool[1].threat + local valid_pos = surface.find_non_colliding_position(biter_pool[1].name, pos, 100, 2) + local biter = surface.create_entity({name = biter_pool[1].name, position = valid_pos}) + return biter +end + +local attack_group_count_thresholds = { + {0, 1}, + {50, 2}, + {100, 3}, + {150, 4}, + {200, 5}, + {1000, 6}, + {2000, 7}, + {3000, 8} + } + +local function get_number_of_attack_groups() + local n = 1 + for _, entry in pairs(attack_group_count_thresholds) do + if global.wave_count >= entry[1] then + n = entry[2] + end + end + return n +end + +local function clear_corpses(surface) + if not global.wave_count then return end + local chance = 4 + if global.wave_count > 250 then chance = 3 end + if global.wave_count > 500 then chance = 2 end + for _, entity in pairs(surface.find_entities_filtered{type = "corpse"}) do + if math_random(1, chance) == 1 then + entity.destroy() + end + end +end + +local boss_wave_names = { + [50] = "The Big Biter Gang", + [100] = "Biterzilla", + [150] = "The Spitter Squad", + [200] = "The Wall Nibblers", + [250] = "Conveyor Munchers", + [300] = "Furnace Freezers", + [350] = "Cable Chewers", + [400] = "Power Pole Thieves", + [450] = "Assembler Annihilators", + [500] = "Inserter Crunchers", + [550] = "Engineer Eaters", + [600] = "Belt Unbalancers", + [650] = "Turret Devourers", + [700] = "Pipe Perforators", + [750] = "Desync Bros", + [800] = "Ratio Randomizers", + [850] = "Wire Chompers", + [900] = "The Bus Mixers", + [950] = "Roundabout Deadlockers", + [1000] = "Happy Tree Friends", + [1050] = "Uranium Digesters", + [1100] = "Bot Banishers", + [1150] = "Chest Crushers", + [1200] = "Cargo Wagon Scratchers", + [1250] = "Transport Belt Surfers", + [1300] = "Pumpjack Pulverizers", + [1350] = "Radar Ravagers", + [1400] = "Mall Deconstrutors", + [1450] = "Lamp Dimmers", + [1500] = "Roboport Disablers", + [1550] = "Signal Spammers", + [1600] = "Brick Tramplers", + [1650] = "Drill Destroyers", + [1700] = "Gearwheel Grinders", + [1750] = "Silo Seekers", + [1800] = "Circuit Breakers", + [1850] = "Bullet Absorbers", + [1900] = "Oil Guzzlers", + [1950] = "Belt Rotators", + [2000] = "Bluescreen Factor" +} + +local function spawn_boss_units(surface) + if boss_wave_names[global.wave_count] then + game.print("Boss Wave " .. global.wave_count .. " - - " .. boss_wave_names[global.wave_count], {r = 0.8, g = 0.1, b = 0.1}) + else + game.print("Boss Wave " .. global.wave_count, {r = 0.8, g = 0.1, b = 0.1}) + end + + if not boss_waves[global.wave_count] then + boss_waves[global.wave_count] = {{name = "behemoth-biter", count = math.floor(global.wave_count / 16)}, {name = "behemoth-spitter", count = math.floor(global.wave_count / 32)}} + end + + local position = {x = 216, y = 0} + local biter_group = surface.create_unit_group({position = position}) + for _, entry in pairs(boss_waves[global.wave_count]) do + for x = 1, entry.count, 1 do + local pos = surface.find_non_colliding_position(entry.name, position, 64, 3) + if pos then + local biter = surface.create_entity({name = entry.name, position = pos}) + global.boss_biters[biter.unit_number] = biter + add_boss_unit(biter, global.biter_evasion_health_increase_factor * 8 * difficulties_votes[global.difficulty_vote_index].strength_modifier, 0.70) + biter_group.add_member(biter) + end + end + end + biter_group.set_command({ + type = defines.command.compound, + structure_type = defines.compound_command.logical_and, + commands = { + { + type=defines.command.attack_area, + destination={x = 160, y = 0}, + radius=16, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack_area, + destination={x = 128, y = 0}, + radius=16, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack_area, + destination={x = 96, y = 0}, + radius=16, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack_area, + destination={x = 64, y = 0}, + radius=16, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack_area, + destination={x = 32, y = 0}, + radius=16, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack_area, + destination={x = -32, y = 0}, + radius=16, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack, + target=global.market, + distraction=defines.distraction.by_enemy + } + } + }) + biter_group.start_moving() +end + +local function wake_up_the_biters(surface) + if not global.market then return end + + --if not global.wake_up_counter then global.wake_up_counter = 0 end + --global.wake_up_counter = global.wake_up_counter + 1 + --if global.wake_up_counter % 2 == 1 then return end + + --[[ + unit_group = game.player.surface.create_unit_group({position = game.player.selected.position}) + for _, biter in pairs(game.player.surface.find_enemy_units(game.player.selected.position, 96, "player")) do + unit_group.add_member(biter) + end + unit_group.set_command({ + type = defines.command.compound, + structure_type = defines.compound_command.logical_and, + commands = { + { + type=defines.command.attack_area, + destination=global.market.position, + radius=512, + distraction=defines.distraction.by_anything + } + } + }) + unit_group.start_moving() + + game.player.surface.set_multi_command({ + command={ + type=defines.command.attack, + target=global.market, + distraction=defines.distraction.none + }, + unit_count = 128, + force = "enemy", + unit_search_distance=128 + }) + + ]] + + local nearest_player_unit = surface.find_nearest_enemy({position = {x = 256, y = 0}, max_distance=512, force="enemy"}) + if not nearest_player_unit then return end + local target_positions = {} + for y = -80, 80, 4 do + insert(target_positions, {x = nearest_player_unit.position.x, y = y}) + end + target_positions = shuffle(target_positions) + + local units = surface.find_entities_filtered({type = "unit"}) + units = shuffle(units) + local unit_groups = {} + for i = 1, 2, 1 do + if not units[i] then break end + if not units[i].valid then break end + unit_groups[i] = surface.create_unit_group({position = {x = units[i].position.x, y = units[i].position.y}}) + local biters = surface.find_enemy_units(units[i].position, 24, "player") + for _, biter in pairs(biters) do + unit_groups[i].add_member(biter) + end + end + + for i = 1, #unit_groups, 1 do + if unit_groups[i].valid then + if #unit_groups[i].members > 0 then + unit_groups[i].set_command({ + type = defines.command.compound, + structure_type = defines.compound_command.logical_and, + commands = { + { + type=defines.command.attack_area, + destination={target_positions[i].x, target_positions[i].y}, + radius=32, + distraction=defines.distraction.by_anything + }, + { + type=defines.command.attack_area, + destination=global.market.position, + radius=32, + distraction=defines.distraction.by_anything + }, + { + type=defines.command.attack, + target=global.market, + distraction=defines.distraction.by_enemy + } + } + }) + unit_groups[i].start_moving() + else + unit_groups[i].destroy() + end + end + end + + --[[ + surface.set_multi_command({ + command={ + type=defines.command.attack, + target=global.market, + distraction=defines.distraction.by_enemy + }, + unit_count = 16, + force = "enemy", + unit_search_distance=64 + })]] + + surface.set_multi_command({ + command={ + type=defines.command.attack, + target=global.market, + distraction=defines.distraction.none + }, + unit_count = 16, + force = "enemy", + unit_search_distance=24 + }) +end + +local function damage_entity_outside_of_fence(e) + if not e.health then return end + if e.force.name == "neutral" then return end + if e.type == "unit" or e.type == "unit-spawner" then return end + + e.surface.create_entity({name = "water-splash", position = e.position}) + + if e.type == "entity-ghost" then e.destroy() return end + + e.health = e.health - math_random(math.floor(e.prototype.max_health * 0.05), math.floor(e.prototype.max_health * 0.1)) + if e.health <= 0 then e.die("enemy") end +end + +local function biter_attack_wave() + if not global.market then return end + if global.wave_grace_period then return end + local surface = game.surfaces["fish_defender"] + + clear_corpses(surface) + wake_up_the_biters(surface) + + if surface.count_entities_filtered({type = "unit"}) > biter_count_limit then + --game.print("Biter limit reached, wave delayed.", {r = 0.7, g = 0.1, b = 0.1}) + return + end + + if not global.wave_count then + global.wave_count = 1 + else + global.wave_count = global.wave_count + 1 + end + + local m = 0.0015 + if global.difficulty_vote_index then + m = m * difficulties_votes[global.difficulty_vote_index].strength_modifier + end + game.forces.enemy.set_ammo_damage_modifier("melee", global.wave_count * m) + game.forces.enemy.set_ammo_damage_modifier("biological", global.wave_count * m) + global.biter_evasion_health_increase_factor = 1 + (global.wave_count * (m * 2)) + + local m = 4 + if global.difficulty_vote_index then + m = m * difficulties_votes[global.difficulty_vote_index].amount_modifier + end + + if global.wave_count % 50 == 0 then + global.attack_wave_threat = math.floor(global.wave_count * m) + spawn_boss_units(surface) + if global.attack_wave_threat > 10000 then global.attack_wave_threat = 10000 end + else + global.attack_wave_threat = math.floor(global.wave_count * m) + if global.attack_wave_threat > 10000 then global.attack_wave_threat = 10000 end + end + + local evolution = global.wave_count * 0.00125 + if evolution > 1 then evolution = 1 end + game.forces.enemy.evolution_factor = evolution + + if game.forces.enemy.evolution_factor == 1 then + if not global.endgame_modifier then + global.endgame_modifier = 1 + game.print("Endgame enemy evolution reached.", {r = 0.7, g = 0.1, b = 0.1}) + else + global.endgame_modifier = global.endgame_modifier + 1 + end + end + + for _, e in pairs(surface.find_entities_filtered({area = {{160, -256},{360, 256}}})) do + damage_entity_outside_of_fence(e) + end + + local spawn_x = 242 + local target_x = -32 + local group_coords = {} + for a = -80, 80, 16 do + insert(group_coords, {spawn = {x = spawn_x, y = a * 2}, target = {x = target_x, y = a}}) + end + group_coords = shuffle(group_coords) + + local unit_groups = {} + if global.wave_count > 100 and math_random(1, 8) == 1 then + for i = 1, #group_coords, 1 do + unit_groups[i] = surface.create_unit_group({position = group_coords[i].spawn}) + end + else + for i = 1, get_number_of_attack_groups(), 1 do + unit_groups[i] = surface.create_unit_group({position = group_coords[i].spawn}) + end + end + + local biter_pool = get_biter_pool() + while global.attack_wave_threat > 0 do + for i = 1, #unit_groups, 1 do + local biter = spawn_biter(unit_groups[i].position, biter_pool) + if biter then + unit_groups[i].add_member(biter) + else + break + end + end + end + + for i = 1, #unit_groups, 1 do + unit_groups[i].set_command({ + type = defines.command.compound, + structure_type = defines.compound_command.logical_and, + commands = { + { + type=defines.command.attack_area, + destination={group_coords[i].target.x + 192, group_coords[i].target.y}, + radius=32, + distraction=defines.distraction.by_anything + }, + { + type=defines.command.attack_area, + destination={group_coords[i].target.x + 128, group_coords[i].target.y}, + radius=32, + distraction=defines.distraction.by_anything + }, + { + type=defines.command.attack_area, + destination={group_coords[i].target.x + 64, group_coords[i].target.y}, + radius=32, + distraction=defines.distraction.by_anything + }, + { + type=defines.command.attack_area, + destination={group_coords[i].target.x, group_coords[i].target.y}, + radius=32, + distraction=defines.distraction.by_enemy + }, + { + type=defines.command.attack, + target=global.market, + distraction=defines.distraction.by_enemy + } + } + }) + unit_groups[i].start_moving() + end +end + +local function get_sorted_list(column_name, score_list) + for x = 1, #score_list, 1 do + for y = 1, #score_list, 1 do + if not score_list[y + 1] then break end + if score_list[y][column_name] < score_list[y + 1][column_name] then + local key = score_list[y] + score_list[y] = score_list[y + 1] + score_list[y + 1] = key + end + end + end + return score_list +end + +local function get_mvps() + if not global.score["player"] then return false end + local score = global.score["player"] + local score_list = {} + for _, p in pairs(game.players) do + local killscore = 0 + if score.players[p.name].killscore then killscore = score.players[p.name].killscore end + local deaths = 0 + if score.players[p.name].deaths then deaths = score.players[p.name].deaths end + local built_entities = 0 + if score.players[p.name].built_entities then built_entities = score.players[p.name].built_entities end + local mined_entities = 0 + if score.players[p.name].mined_entities then mined_entities = score.players[p.name].mined_entities end + table.insert(score_list, {name = p.name, killscore = killscore, deaths = deaths, built_entities = built_entities, mined_entities = mined_entities}) + end + local mvp = {} + score_list = get_sorted_list("killscore", score_list) + mvp.killscore = {name = score_list[1].name, score = score_list[1].killscore} + score_list = get_sorted_list("deaths", score_list) + mvp.deaths = {name = score_list[1].name, score = score_list[1].deaths} + score_list = get_sorted_list("built_entities", score_list) + mvp.built_entities = {name = score_list[1].name, score = score_list[1].built_entities} + return mvp +end + +local function is_game_lost() + if global.market then return end + + for _, player in pairs(game.connected_players) do + if player.gui.left["fish_defense_game_lost"] then return end + local f = player.gui.left.add({ type = "frame", name = "fish_defense_game_lost", caption = "The fish market was overrun! The biters are having a feast :3", direction = "vertical"}) + f.style.font_color = {r = 0.65, g = 0.1, b = 0.99} + + local t = f.add({type = "table", column_count = 2}) + local l = t.add({type = "label", caption = "Survival Time >> "}) + l.style.font = "default-listbox" + l.style.font_color = {r = 0.22, g = 0.77, b = 0.44} + + if global.market_age >= 216000 then + local l = t.add({type = "label", caption = math.floor(((global.market_age / 60) / 60) / 60) .. " hours " .. math.ceil((global.market_age % 216000 / 60) / 60) .. " minutes"}) + l.style.font = "default-bold" + l.style.font_color = {r=0.33, g=0.66, b=0.9} + else + local l = t.add({type = "label", caption = math.ceil((global.market_age % 216000 / 60) / 60) .. " minutes"}) + l.style.font = "default-bold" + l.style.font_color = {r=0.33, g=0.66, b=0.9} + end + + local mvp = get_mvps() + if mvp then + + local l = t.add({type = "label", caption = "MVP Defender >> "}) + l.style.font = "default-listbox" + l.style.font_color = {r = 0.22, g = 0.77, b = 0.44} + local l = t.add({type = "label", caption = mvp.killscore.name .. " with a score of " .. mvp.killscore.score}) + l.style.font = "default-bold" + l.style.font_color = {r=0.33, g=0.66, b=0.9} + + local l = t.add({type = "label", caption = "MVP Builder >> "}) + l.style.font = "default-listbox" + l.style.font_color = {r = 0.22, g = 0.77, b = 0.44} + local l = t.add({type = "label", caption = mvp.built_entities.name .. " built " .. mvp.built_entities.score .. " things"}) + l.style.font = "default-bold" + l.style.font_color = {r=0.33, g=0.66, b=0.9} + + local l = t.add({type = "label", caption = "MVP Deaths >> "}) + l.style.font = "default-listbox" + l.style.font_color = {r = 0.22, g = 0.77, b = 0.44} + local l = t.add({type = "label", caption = mvp.deaths.name .. " died " .. mvp.deaths.score .. " times"}) + l.style.font = "default-bold" + l.style.font_color = {r=0.33, g=0.66, b=0.9} + + if not global.results_sent then + local result = {} + insert(result, 'MVP Defender: \\n') + insert(result, mvp.killscore.name .. " with a score of " .. mvp.killscore.score .. "\\n" ) + insert(result, '\\n') + insert(result, 'MVP Builder: \\n') + insert(result, mvp.built_entities.name .. " built " .. mvp.built_entities.score .. " things\\n" ) + insert(result, '\\n') + insert(result, 'MVP Deaths: \\n') + insert(result, mvp.deaths.name .. " died " .. mvp.deaths.score .. " times" ) + local message = table.concat(result) + server_commands.to_discord_embed(message) + global.results_sent = true + end + end + + for _, player in pairs(game.connected_players) do + player.play_sound{path="utility/game_lost", volume_modifier=0.75} + end + end + + game.map_settings.enemy_expansion.enabled = true + game.map_settings.enemy_expansion.max_expansion_distance = 15 + game.map_settings.enemy_expansion.settler_group_min_size = 15 + game.map_settings.enemy_expansion.settler_group_max_size = 30 + game.map_settings.enemy_expansion.min_expansion_cooldown = 600 + game.map_settings.enemy_expansion.max_expansion_cooldown = 600 +end + +local function damage_entities_in_radius(surface, position, radius, damage) + local entities_to_damage = surface.find_entities_filtered({area = {{position.x - radius, position.y - radius},{position.x + radius, position.y + radius}}}) + for _, entity in pairs(entities_to_damage) do + if entity.valid then + if entity.health and entity.name ~= "land-mine" then + if entity.force.name ~= "enemy" then + if entity.name == "character" then + entity.damage(damage, "enemy") + else + entity.health = entity.health - damage + if entity.health <= 0 then entity.die("enemy") end + end + end + end + end + end +end + +local function market_kill_visuals() + local m = 32 + local m2 = m * 0.005 + for i = 1, 1024, 1 do + global.market.surface.create_entity({ + name = "branch-particle", + position = global.market.position, + frame_speed = 0.1, + vertical_speed = 0.1, + height = 0.1, + movement = {m2 - (math.random(0, m) * 0.01), m2 - (math.random(0, m) * 0.01)} + }) + end + for x = -5, 5, 0.5 do + for y = -5, 5, 0.5 do + if math_random(1, 2) == 1 then + global.market.surface.create_trivial_smoke({name="smoke-fast", position={global.market.position.x + (x * 0.35), global.market.position.y + (y * 0.35)}}) + end + if math_random(1, 3) == 1 then + global.market.surface.create_trivial_smoke({name="train-smoke", position={global.market.position.x + (x * 0.35), global.market.position.y + (y * 0.35)}}) + end + end + end + global.market.surface.spill_item_stack(global.market.position,{name = "raw-fish", count = 1024}, true) +end + +local biter_splash_damage = { + ["medium-biter"] = {visuals = {"blood-explosion-big", "big-explosion"}, radius = 1.5, damage_min = 50, damage_max = 100, chance = 8}, + ["big-biter"] = {visuals = {"blood-explosion-huge", "ground-explosion"}, radius = 2, damage_min = 75, damage_max = 150, chance = 16}, + ["behemoth-biter"] = {visuals = {"blood-explosion-huge", "big-artillery-explosion"}, radius = 2.5, damage_min = 100, damage_max = 200, chance = 32} +} + +local function on_entity_died(event) + if event.entity.force.name == "enemy" then + local surface = event.entity.surface + + if global.boss_biters[event.entity.unit_number] then boss_biter.died(event) end + + local splash = biter_splash_damage[event.entity.name] + if splash then + if math_random(1, splash.chance) == 1 then + for _, visual in pairs(splash.visuals) do + surface.create_entity({name = visual, position = event.entity.position}) + end + damage_entities_in_radius(surface, event.entity.position, splash.radius, math_random(splash.damage_min, splash.damage_max)) + return + end + end + + if event.entity.name == "behemoth-biter" then + if math_random(1, 16) == 1 then + local p = surface.find_non_colliding_position("big-biter", event.entity.position, 3, 0.5) + if p then surface.create_entity {name = "big-biter", position = p} end + end + for i = 1, math_random(1, 2), 1 do + local p = surface.find_non_colliding_position("medium-biter", event.entity.position, 3, 0.5) + if p then surface.create_entity {name = "medium-biter", position = p} end + end + end + return + end + + if event.entity == global.market then + market_kill_visuals() + global.market = nil + global.market_age = game.tick + is_game_lost() + end + + if global.entity_limits[event.entity.name] then + global.entity_limits[event.entity.name].placed = global.entity_limits[event.entity.name].placed - 1 + update_fd_stats() + end +end + +local function on_player_joined_game(event) + local player = game.players[event.player_index] + + if not global.fish_defense_init_done then + local map_gen_settings = {} + map_gen_settings.water = "0.5" + map_gen_settings.cliff_settings = {cliff_elevation_interval = 16, cliff_elevation_0 = 32} + map_gen_settings.autoplace_controls = { + ["coal"] = {frequency = "3", size = "2", richness = "1"}, + ["stone"] = {frequency = "3", size = "2", richness = "1"}, + ["copper-ore"] = {frequency = "3", size = "2", richness = "1"}, + ["iron-ore"] = {frequency = "3", size = "2", richness = "1"}, + ["uranium-ore"] = {frequency = "2", size = "1", richness = "1"}, + ["crude-oil"] = {frequency = "4", size = "1", richness = "1"}, + ["trees"] = {frequency = "1.5", size = "1.5", richness = "1"}, + ["enemy-base"] = {frequency = "none", size = "none", richness = "none"} + } + game.create_surface("fish_defender", map_gen_settings) + local surface = game.surfaces["fish_defender"] + + local radius = 256 + game.forces.player.chart(surface, {{x = -1 * radius, y = -1 * radius}, {x = radius, y = radius}}) + + game.map_settings.enemy_expansion.enabled = false + game.map_settings.enemy_evolution.destroy_factor = 0 + game.map_settings.enemy_evolution.time_factor = 0 + game.map_settings.enemy_evolution.pollution_factor = 0 + game.map_settings.pollution.enabled = false + + game.forces["player"].technologies["atomic-bomb"].enabled = false + + global.entity_limits = { + ["gun-turret"] = {placed = 1, limit = 1, str = "gun turret", slot_price = 75}, + ["laser-turret"] = {placed = 0, limit = 1, str = "laser turret", slot_price = 300}, + ["artillery-turret"] = {placed = 0, limit = 1, str = "artillery turret", slot_price = 500}, + ["flamethrower-turret"] = {placed = 0, limit = 0, str = "flamethrower turret", slot_price = 50000}, + ["land-mine"] = {placed = 0, limit = 1, str = "mine", slot_price = 1} + } + + game.create_force("decoratives") + game.forces["decoratives"].set_cease_fire("enemy", true) + game.forces["enemy"].set_cease_fire("decoratives", true) + game.forces["player"].set_cease_fire("decoratives", true) + + global.comfylatron_habitat = { + left_top = {x = -1500, y = -1500}, + right_bottom = {x = -80, y = 1500} + } + + global.fish_defense_init_done = true + end + + if player.online_time < 1 then + player.insert({name = "pistol", count = 1}) + --player.insert({name = "iron-axe", count = 1}) + player.insert({name = "raw-fish", count = 3}) + player.insert({name = "firearm-magazine", count = 16}) + player.insert({name = "iron-plate", count = 32}) + if global.show_floating_killscore then global.show_floating_killscore[player.name] = false end + end + + local surface = game.surfaces["fish_defender"] + if player.online_time < 2 and surface.is_chunk_generated({0,0}) then + player.teleport(surface.find_non_colliding_position("character", {-75, 4}, 50, 1), "fish_defender") + else + if player.online_time < 2 then + player.teleport({-50, 0}, "fish_defender") + end + end + + create_wave_gui(player) + add_fd_stats_button(player) + + if game.tick > 900 then + is_game_lost() + end +end + +local function get_replacement_tile(surface) + local tilename = "grass-1" + for x = -160, 160, 1 do + for y = -96, 90, 1 do + local tile = surface.get_tile(x, y) + if tile.name ~= "water" and tile.name ~= "deepwater" then + tilename = tile.name + end + end + end + return tilename +end + +local worm_raffle_table = { + [1] = {"small-worm-turret", "small-worm-turret", "small-worm-turret", "small-worm-turret", "small-worm-turret", "small-worm-turret"}, + [2] = {"small-worm-turret", "small-worm-turret", "small-worm-turret", "small-worm-turret", "small-worm-turret", "medium-worm-turret"}, + [3] = {"small-worm-turret", "small-worm-turret", "small-worm-turret", "small-worm-turret", "medium-worm-turret", "medium-worm-turret"}, + [4] = {"small-worm-turret", "small-worm-turret", "small-worm-turret", "medium-worm-turret", "medium-worm-turret", "medium-worm-turret"}, + [5] = {"small-worm-turret", "small-worm-turret", "medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "big-worm-turret"}, + [6] = {"small-worm-turret", "medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "big-worm-turret"}, + [7] = {"medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "big-worm-turret", "big-worm-turret"}, + [8] = {"medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "big-worm-turret", "big-worm-turret"}, + [9] = {"medium-worm-turret", "medium-worm-turret", "medium-worm-turret", "big-worm-turret", "big-worm-turret", "big-worm-turret"}, + [10] = {"medium-worm-turret", "medium-worm-turret", "big-worm-turret", "big-worm-turret", "big-worm-turret", "big-worm-turret"} + } +local rock_raffle = {"sand-rock-big","sand-rock-big","rock-big","rock-big","rock-big","rock-big","rock-big","rock-big","rock-huge"} + +local function spawn_obstacles(left_top, surface) + if not global.obstacle_start_x then global.obstacle_start_x = math.abs(left_top.x) - 32 end + local current_depth = math.abs(left_top.x) - global.obstacle_start_x + local worm_amount = math.ceil(current_depth / 64) + local i = math.ceil(current_depth / 256) + if i > 10 then i = 10 end + if i < 1 then i = 1 end + local worm_raffle = worm_raffle_table[i] + + local rocks_amount = math.ceil(current_depth / 16) + + local tile_positions = {} + for x = 0, 31, 1 do + for y = 0, 31, 1 do + local pos = {x = left_top.x + x, y = left_top.y + y} + if not surface.get_tile(pos).collides_with("player-layer") then + tile_positions[#tile_positions + 1] = pos + end + end + end + if #tile_positions == 0 then return end + + tile_positions = shuffle(tile_positions) + for _, pos in pairs(tile_positions) do + surface.create_entity({name = worm_raffle[math_random(1, #worm_raffle)], position = pos, force = "enemy"}) + worm_amount = worm_amount - 1 + if worm_amount < 1 then break end + end + + tile_positions = shuffle(tile_positions) + for _, pos in pairs(tile_positions) do + surface.create_entity({name = rock_raffle[math_random(1, #rock_raffle)], position = pos}) + rocks_amount = rocks_amount - 1 + if rocks_amount < 1 then break end + end +end + +local map_height = 96 + +local function on_chunk_generated(event) + local surface = game.surfaces["fish_defender"] + + if not surface then return end + if surface.name ~= event.surface.name then return end + + local area = event.area + local left_top = area.left_top + + if left_top.x <= -196 then + + local search_area = {{left_top.x - 32, left_top.y - 32}, {left_top.x + 32, left_top.y + 32}} + if surface.count_tiles_filtered({name = "water", area = search_area}) == 0 and math_random(1, 64) == 1 then + map_functions.draw_noise_tile_circle({x = left_top.x + math_random(1,30), y = left_top.y + math_random(1,30)}, "water", surface, math_random(6, 12)) + end + + if not global.spawn_ores_generated then + + local spawn_position_x = -76 + + surface.create_entity({name = "electric-beam", position = {160, -96}, source = {160, -96}, target = {160,96}}) + + local tiles = {} + local replacement_tile = get_replacement_tile(surface) + local water_tiles = surface.find_tiles_filtered({name = {"water", "deepwater"}}) + + for _, tile in pairs(water_tiles) do + insert(tiles, {name = replacement_tile, position = {tile.position.x, tile.position.y}}) + end + surface.set_tiles(tiles, true) + + local entities = surface.find_entities_filtered({type = "resource", area = {{-160, -96},{160, 96}}}) + for _, entity in pairs(entities) do + entity.destroy() + end + + local decorative_names = {} + for k,v in pairs(game.decorative_prototypes) do + if v.autoplace_specification then + decorative_names[#decorative_names+1] = k + end + end + for x = -4, 4, 1 do + for y = -3, 3, 1 do + surface.regenerate_decorative(decorative_names, {{x,y}}) + end + end + + local ore_positions = {{x = -128, y = -64},{x = -128, y = -32},{x = -128, y = 32},{x = -128, y = 64},{x = -128, y = 0}} + ore_positions = shuffle(ore_positions) + map_functions.draw_smoothed_out_ore_circle(ore_positions[1], "copper-ore", surface, 15, 2500) + map_functions.draw_smoothed_out_ore_circle(ore_positions[2], "iron-ore", surface, 15, 2500) + map_functions.draw_smoothed_out_ore_circle(ore_positions[3], "coal", surface, 15, 1500) + map_functions.draw_smoothed_out_ore_circle(ore_positions[4], "stone", surface, 15, 1500) + map_functions.draw_noise_tile_circle({x = -96, y = 0}, "water", surface, 16) + map_functions.draw_oil_circle(ore_positions[5], "crude-oil", surface, 8, 200000) + + local pos = surface.find_non_colliding_position("market",{spawn_position_x, 0}, 50, 1) + global.market = place_fish_market(surface, pos) + + local pos = surface.find_non_colliding_position("gun-turret",{spawn_position_x + 5, 1}, 50, 1) + local turret = surface.create_entity({name = "gun-turret", position = pos, force = "player"}) + turret.insert({name = "firearm-magazine", count = 32}) + + for x = -20, 20, 1 do + for y = -20, 20, 1 do + local pos = {x = global.market.position.x + x, y = global.market.position.y + y} + local distance_to_center = math.sqrt(x^2 + y^2) + if distance_to_center > 8 and distance_to_center < 15 then + if math_random(1,3) == 1 and surface.can_place_entity({name = "wooden-chest", position = pos, force = "player"}) then + local chest = surface.create_entity({name = "wooden-chest", position = pos, force = "player"}) + end + end + end + end + + local area = {{x = -160, y = -96}, {x = 160, y = 96}} + for _, tile in pairs(surface.find_tiles_filtered({name = "water", area = area})) do + if math_random(1, 32) == 1 then + surface.create_entity({name = "fish", position = tile.position}) + end + end + + local pos = surface.find_non_colliding_position("character",{spawn_position_x + 1, 4}, 50, 1) + game.forces["player"].set_spawn_position(pos, surface) + for _, player in pairs(game.connected_players) do + local pos = surface.find_non_colliding_position("character",{spawn_position_x + 1, 4}, 50, 1) + player.teleport(pos, surface) + end + + global.spawn_ores_generated = true + end + end + + local tiles = {} + local hourglass_center_piece_length = 64 + + for x = 0, 31, 1 do + for y = 0, 31, 1 do + local pos = {x = left_top.x + x, y = left_top.y + y} + if pos.y >= map_height then + if pos.y > pos.x - hourglass_center_piece_length and pos.x > 0 then + insert(tiles, {name = "out-of-map", position = pos}) + end + if pos.y > (pos.x + hourglass_center_piece_length) * -1 and pos.x <= 0 then + insert(tiles, {name = "out-of-map", position = pos}) + end + end + if pos.y < map_height * -1 then + if pos.y < (pos.x - hourglass_center_piece_length) * -1 and pos.x > 0 then + insert(tiles, {name = "out-of-map", position = pos}) + end + if pos.y < pos.x + hourglass_center_piece_length and pos.x <= 0 then + insert(tiles, {name = "out-of-map", position = pos}) + end + end + end + end + + surface.set_tiles(tiles, false) + + for _, tile in pairs(surface.find_tiles_filtered({name = "water", area = event.area})) do + if math_random(1, 32) == 1 then + surface.create_entity({name = "fish", position = tile.position}) + end + end + + if left_top.x < -2048 then + spawn_obstacles(left_top, surface) + end + + if left_top.x < 0 then return end + + for _, entity in pairs(surface.find_entities_filtered({area = area, type = "cliff"})) do + entity.destroy() + end + + if left_top.x < 160 then return end + + for _, entity in pairs(surface.find_entities_filtered({area = area, type = "tree"})) do + entity.destroy() + end + + for _, entity in pairs(surface.find_entities_filtered({area = area, type = "resource"})) do + surface.create_entity({name = "uranium-ore", position = entity.position, amount = math_random(200, 8000)}) + entity.destroy() + end + + local tiles = {} + + for x = 0, 31, 1 do + for y = 0, 31, 1 do + local pos = {x = left_top.x + x, y = left_top.y + y} + + local tile = surface.get_tile(pos) + if tile.name ~= "out-of-map" then + + if pos.x > 0 then + if pos.x > 320 then + insert(tiles, {name = "out-of-map", position = pos}) + else + local a = 0 + (pos.x - 160) * 0.01 + local b = (pos.x - 160) * 0.035 + local r = (pos.x - 160) * 0.015 + if a > 0.75 then a = 0.75 end + if b > 1 then b = 1 end + if r > 0.6 then r = 0.6 end + rendering.draw_sprite({sprite = "tile/lab-dark-2", target = {pos.x + 0.5, pos.y + 0.5}, surface = surface, tint = {r = r, g = 0, b = b, a = a}, render_layer = "ground"}) + end + + if pos.x > 296 and pos.x < 312 and math_random(1, 128) == 1 then + if surface.can_place_entity({name = "biter-spawner", force = "decoratives", position = pos}) then + local entity + if math_random(1,4) == 1 then + entity = surface.create_entity({name = "spitter-spawner", force = "decoratives", position = pos}) + else + entity = surface.create_entity({name = "biter-spawner", force = "decoratives", position = pos}) + end + entity.active = false + entity.destructible = false + end + end + end + end + end + end + surface.set_tiles(tiles, true) + + local decorative_names = {} + for k,v in pairs(game.decorative_prototypes) do + if v.autoplace_specification then + decorative_names[#decorative_names+1] = k + end + end + surface.regenerate_decorative(decorative_names, {{x=math.floor(event.area.left_top.x/32),y=math.floor(event.area.left_top.y/32)}}) +end + +local function on_built_entity(event) + local entity = event.created_entity + if not entity.valid then return end + if global.entity_limits[entity.name] then + local surface = entity.surface + + if global.entity_limits[entity.name].placed < global.entity_limits[entity.name].limit then + global.entity_limits[entity.name].placed = global.entity_limits[entity.name].placed + 1 + surface.create_entity( + {name = "flying-text", position = entity.position, text = global.entity_limits[entity.name].placed .. " / " .. global.entity_limits[entity.name].limit .. " " .. global.entity_limits[entity.name].str .. "s", color = {r=0.98, g=0.66, b=0.22}} + ) + update_fd_stats() + else + surface.create_entity({name = "flying-text", position = entity.position, text = global.entity_limits[entity.name].str .. " limit reached.", color = {r=0.82, g=0.11, b=0.11}}) + local player = game.players[event.player_index] + player.insert({name = entity.name, count = 1}) + if global.score then + if global.score[player.force.name] then + if global.score[player.force.name].players[player.name] then + global.score[player.force.name].players[player.name].built_entities = global.score[player.force.name].players[player.name].built_entities - 1 + end + end + end + entity.destroy() + end + end +end + +local function on_robot_built_entity(event) + local entity = event.created_entity + if global.entity_limits[entity.name] then + local surface = entity.surface + if global.entity_limits[entity.name].placed < global.entity_limits[entity.name].limit then + global.entity_limits[entity.name].placed = global.entity_limits[entity.name].placed + 1 + surface.create_entity( + {name = "flying-text", position = entity.position, text = global.entity_limits[entity.name].placed .. " / " .. global.entity_limits[entity.name].limit .. " " .. global.entity_limits[entity.name].str .. "s", color = {r=0.98, g=0.66, b=0.22}} + ) + update_fd_stats() + else + surface.create_entity({name = "flying-text", position = entity.position, text = global.entity_limits[entity.name].str .. " limit reached.", color = {r=0.82, g=0.11, b=0.11}}) + local inventory = event.robot.get_inventory(defines.inventory.robot_cargo) + inventory.insert({name = entity.name, count = 1}) + entity.destroy() + end + end +end + +local function on_tick() + if game.tick % 30 == 0 then + if global.market then + for _, player in pairs(game.connected_players) do + if game.surfaces["fish_defender"].peaceful_mode == false then + create_wave_gui(player) + end + end + end + if game.tick % 180 == 0 then + if game.surfaces["fish_defender"] then + game.forces.player.chart(game.surfaces["fish_defender"], {{x = -64, y = -256}, {x = 288, y = 256}}) + if global.difficulty_vote_index then + global.wave_interval = difficulties_votes[global.difficulty_vote_index].wave_interval + end + end + end + + if global.market_age then + if not global.game_restart_timer then + global.game_restart_timer = 10800 + else + if global.game_restart_timer < 0 then return end + global.game_restart_timer = global.game_restart_timer - 30 + end + if global.game_restart_timer % 1800 == 0 then + if global.game_restart_timer > 0 then game.print("Map will restart in " .. global.game_restart_timer / 60 .. " seconds!", { r=0.22, g=0.88, b=0.22}) end + if global.game_restart_timer == 0 then + game.print("Map is restarting!", { r=0.22, g=0.88, b=0.22}) + --game.write_file("commandPipe", ":loadscenario --force", false, 0) + + local message = 'Map is restarting! ' + server_commands.to_discord_bold(table.concat{'*** ', message, ' ***'}) + server_commands.start_scenario('Fish_Defender') + + end + end + end + end + + if game.tick % global.wave_interval == global.wave_interval - 1 then + if game.surfaces["fish_defender"].peaceful_mode == true then return end + biter_attack_wave() + end +end + +local function on_player_changed_position(event) + local player = game.players[event.player_index] + if player.position.x >= 160 then + player.teleport({player.position.x - 1, player.position.y}, game.surfaces["fish_defender"]) + if player.position.y > map_height or player.position.y < map_height * -1 then + player.teleport({player.position.x, 0}, game.surfaces["fish_defender"]) + end + if player.character then + player.character.health = player.character.health - 25 + player.character.surface.create_entity({name = "water-splash", position = player.position}) + if player.character.health <= 0 then player.character.die("enemy") end + end + end +end + +local function on_player_mined_entity(event) + if global.entity_limits[event.entity.name] then + global.entity_limits[event.entity.name].placed = global.entity_limits[event.entity.name].placed - 1 + update_fd_stats() + end +end + +local function on_robot_mined_entity(event) + if global.entity_limits[event.entity.name] then + global.entity_limits[event.entity.name].placed = global.entity_limits[event.entity.name].placed - 1 + update_fd_stats() + end +end + +local function on_research_finished(event) + local research = event.research.name + if research ~= "tanks" then return end + game.forces["player"].technologies["artillery"].researched=true + game.forces.player.recipes["artillery-wagon"].enabled = false +end + +local function on_player_respawned(event) + if not global.market_age then return end + local player = game.players[event.player_index] + player.character.destructible = false +end + +local function on_init(event) + global.wave_interval = 3600 --interval between waves in ticks + global.wave_grace_period = 54000 + global.difficulty_poll_closing_timeout = 54000 + global.boss_biters = {} + global.acid_lines_delay = {} +end + +event.add(defines.events.on_gui_click, on_gui_click) +event.add(defines.events.on_market_item_purchased, on_market_item_purchased) +event.add(defines.events.on_player_respawned, on_player_respawned) +event.add(defines.events.on_built_entity, on_built_entity) +event.add(defines.events.on_chunk_generated, on_chunk_generated) +event.add(defines.events.on_entity_died, on_entity_died) +event.add(defines.events.on_player_changed_position, on_player_changed_position) +event.add(defines.events.on_player_joined_game, on_player_joined_game) +event.add(defines.events.on_player_mined_entity, on_player_mined_entity) +event.add(defines.events.on_research_finished, on_research_finished) +event.add(defines.events.on_robot_built_entity, on_robot_built_entity) +event.add(defines.events.on_robot_mined_entity, on_robot_mined_entity) +event.add(defines.events.on_tick, on_tick) +event.on_init(on_init) + +require "modules.difficulty_vote" \ No newline at end of file diff --git a/maps/fish_defender_v1/flame_boots.lua b/maps/fish_defender_v1/flame_boots.lua new file mode 100644 index 00000000..22e3e2d5 --- /dev/null +++ b/maps/fish_defender_v1/flame_boots.lua @@ -0,0 +1,40 @@ +local event = require 'utils.event' +local math_random = math.random + +local function on_player_changed_position(event) + if not global.flame_boots then return end + local player = game.players[event.player_index] + if not player.character then return end + if player.character.driving then return end + + if not global.flame_boots[player.index] then global.flame_boots[player.index] = {} end + + if not global.flame_boots[player.index].fuel then return end + + if global.flame_boots[player.index].fuel < 0 then + player.print("Your flame boots have worn out.", {r = 0.22, g = 0.77, b = 0.44}) + global.flame_boots[player.index] = {} + return + end + + if global.flame_boots[player.index].fuel % 500 == 0 then player.print("Fuel remaining: " .. global.flame_boots[player.index].fuel, {r = 0.22, g = 0.77, b = 0.44}) end + + if not global.flame_boots[player.index].step_history then global.flame_boots[player.index].step_history = {} end + + local elements = #global.flame_boots[player.index].step_history + + global.flame_boots[player.index].step_history[elements + 1] = {x = player.position.x, y = player.position.y} + + if elements < 50 then return end + + player.surface.create_entity({name = "fire-flame", position = global.flame_boots[player.index].step_history[elements - 2]}) + + global.flame_boots[player.index].fuel = global.flame_boots[player.index].fuel - 1 +end + +local function on_init() + if not global.flame_boots then global.flame_boots = {} end +end + +event.on_init(on_init) +event.add(defines.events.on_player_changed_position, on_player_changed_position) diff --git a/maps/fish_defender_v1/laser_pointer.lua b/maps/fish_defender_v1/laser_pointer.lua new file mode 100644 index 00000000..272a1189 --- /dev/null +++ b/maps/fish_defender_v1/laser_pointer.lua @@ -0,0 +1,31 @@ +local event = require 'utils.event' +local radius = 32 + +local function on_player_used_capsule(event) + if not global.laser_pointer_unlocked then return end + + local player = game.players[event.player_index] + local position = event.position + local used_item = event.item + if used_item.name ~= "artillery-targeting-remote" then return end + + for _, unit in pairs(player.surface.find_enemy_units(position, radius, "player")) do + if math.random(1,2) == 1 then + unit.set_command({ + type = defines.command.go_to_location, + destination = position, + radius = 2, + distraction = defines.distraction.none, + pathfind_flags = { + allow_destroy_friendly_entities = false, + prefer_straight_paths = false, + low_priority = false + } + }) + end + end +end + +event.add(defines.events.on_player_used_capsule, on_player_used_capsule) + + diff --git a/maps/fish_defender_v1/map_intro.lua b/maps/fish_defender_v1/map_intro.lua new file mode 100644 index 00000000..61ad2c64 --- /dev/null +++ b/maps/fish_defender_v1/map_intro.lua @@ -0,0 +1,97 @@ +local event = require 'utils.event' + +local main_caption = " --Fish Defender-- " +local sub_caption = " *blb blubby blub* " +local info = [[ + The biters have catched the scent of fish in the market. + Fend them off as long as possible! + This however will not be an easy task, + since their strength and resistance increases constantly over time. + + Your ultimate goal is to evacuate all the fish to cat planet! + Put them in your rocket's cargo and launch them into space. + Don't worry, you will still get space science. + + The Market will gladly take any coin you might find. + Additional turret slots can be bought at the market. + Several unique upgrades are available too. + + Researching tanks will unlock the artillery technology early. + + Any container bearing dangerous goods, like ammo, grenades or barrels, + causes heavy explosions when it breaks. + Maybe this can be used to our advantage. +]] + +local function create_map_intro_button(player) + if player.gui.top["map_intro_button"] then return end + local b = player.gui.top.add({type = "sprite-button", caption = "?", name = "map_intro_button", tooltip = "Map Info"}) + b.style.font_color = {r=0.11, g=0.8, b=0.44} + b.style.font = "heading-1" + b.style.minimal_height = 38 + b.style.minimal_width = 38 + b.style.top_padding = 2 + b.style.left_padding = 4 + b.style.right_padding = 4 + b.style.bottom_padding = 2 +end + +local function create_map_intro(player) + if player.gui.left["map_intro_frame"] then player.gui.left["map_intro_frame"].destroy() end + local frame = player.gui.left.add {type = "frame", name = "map_intro_frame", direction = "vertical"} + local t = frame.add {type = "table", column_count = 1} + + local tt = t.add {type = "table", column_count = 3} + local l = tt.add {type = "label", caption = main_caption} + l.style.font = "heading-1" + l.style.font_color = {r=0.11, g=0.8, b=0.44} + + local l = tt.add {type = "label", caption = sub_caption} + l.style.font = "heading-2" + l.style.font_color = {r=0.33, g=0.66, b=0.9} + l.style.minimal_width = 320 + + local b = tt.add {type = "button", caption = "X", name = "close_map_intro_frame", align = "right"} + b.style.font = "heading-2" + b.style.minimal_height = 30 + b.style.minimal_width = 30 + b.style.top_padding = 2 + b.style.left_padding = 4 + b.style.right_padding = 4 + b.style.bottom_padding = 2 + + local tt = t.add {type = "table", column_count = 1} + local frame = t.add {type = "frame"} + local l = frame.add {type = "label", caption = info} + l.style.single_line = false + l.style.font = "heading-2" + l.style.font_color = {r=0.75, g=0.8, b=0.8} + l.style.minimal_width = 480 +end + +local function on_gui_click(event) + if not event then return end + if not event.element then return end + if not event.element.valid then return end + local player = game.players[event.element.player_index] + if event.element.name == "close_map_intro_frame" then player.gui.left["map_intro_frame"].destroy() return end + if event.element.name == "map_intro_button" then + if player.gui.left["map_intro_frame"] then + player.gui.left["map_intro_frame"].destroy() + else + create_map_intro(player) + end + return + end +end + +local function on_player_joined_game(event) + local player = game.players[event.player_index] + create_map_intro_button(player) + if player.online_time == 0 then + create_map_intro(player) + end +end + +event.add(defines.events.on_player_joined_game, on_player_joined_game) +event.add(defines.events.on_gui_click, on_gui_click) \ No newline at end of file diff --git a/maps/fish_defender_v1/market.lua b/maps/fish_defender_v1/market.lua new file mode 100644 index 00000000..002df2cf --- /dev/null +++ b/maps/fish_defender_v1/market.lua @@ -0,0 +1,264 @@ +require 'maps.fish_defender.flame_boots' +require 'maps.fish_defender.trapped_capsules' +require 'maps.fish_defender.ultra_mines' +require 'maps.fish_defender.crumbly_walls' +require 'maps.fish_defender.vehicle_nanobots' +require 'maps.fish_defender.laser_pointer' + +local event = require 'utils.event' + +local slot_upgrade_offers = { + [1] = {"gun-turret", "gun turret"}, + [2] = {"laser-turret", "laser turret"}, + [3] = {"artillery-turret", "artillery turret"}, + [4] = {"flamethrower-turret", "flamethrower turret"}, + [5] = {"land-mine", "land mine"} + } + +local special_descriptions = { + ["flame-boots"] = "Flame Boots - Get yourself some hot boots.", + ["explosive-bullets"] = "Unlock Explosive Bullets - Submachine-Gun and Pistol gains a chance to deal splash damage.", + ["bouncy-shells"] = "Unlock Bouncy Shells - Shotgun projectiles may bounce to multiple targets.", + ["trapped-capsules"] = "Unlock Trapped Capsules - Combat robots will send a last deadly projectile to a nearby enemy when killed.", + ["ultra-mines"] = "Unlock Ultra Mines - Careful with these...", + ["railgun-enhancer"] = "Unlock Railgun Enhancer - Turns the railgun into a powerful forking gun.", + ["crumbly-walls"] = "Unlock Crumbly Walls - Fortifications which crumble, may turn into rocks.", + ["vehicle-nanobots"] = "Unlock Vehicle Nanobots - Vehicles repair rapidly while driving.", + ["laser-pointer"] = "Unlock Laser Pointer - The biters are on a quest to slay the red (artillery) dot." +} + +function place_fish_market(surface, position) + local market = surface.create_entity({name = "market", position = position, force = "player"}) + market.minable = false + return market +end + +local function refresh_market_offers() + if not global.market then return end + for i = 1, 100, 1 do + local a = global.market.remove_market_item(1) + if a == false then break end + end + + local str1 = "Gun Turret Slot for " .. tostring(global.entity_limits["gun-turret"].limit * global.entity_limits["gun-turret"].slot_price) + str1 = str1 .. " Coins." + + local str2 = "Laser Turret Slot for " .. tostring(global.entity_limits["laser-turret"].limit * global.entity_limits["laser-turret"].slot_price) + str2 = str2 .. " Coins." + + local str3 = "Artillery Slot for " .. tostring(global.entity_limits["artillery-turret"].limit * global.entity_limits["artillery-turret"].slot_price) + str3 = str3 .. " Coins." + + local current_limit = 1 + if global.entity_limits["flamethrower-turret"].limit ~= 0 then current_limit = current_limit + global.entity_limits["flamethrower-turret"].limit end + local str4 = "Flamethrower Turret Slot for " .. tostring(current_limit * global.entity_limits["flamethrower-turret"].slot_price) + str4 = str4 .. " Coins." + + local str5 = "Landmine Slot for " .. tostring(math.ceil((global.entity_limits["land-mine"].limit / 3) * global.entity_limits["land-mine"].slot_price)) + str5 = str5 .. " Coins." + + local market_items = { + {price = {}, offer = {type = 'nothing', effect_description = str1}}, + {price = {}, offer = {type = 'nothing', effect_description = str2}}, + {price = {}, offer = {type = 'nothing', effect_description = str3}}, + {price = {}, offer = {type = 'nothing', effect_description = str4}}, + {price = {}, offer = {type = 'nothing', effect_description = str5}}, + {price = {{"coin", 5}}, offer = {type = 'give-item', item = "raw-fish", count = 1}}, + {price = {{"coin", 1}}, offer = {type = 'give-item', item = 'wood', count = 8}}, + {price = {{"coin", 8}}, offer = {type = 'give-item', item = 'grenade', count = 1}}, + {price = {{"coin", 32}}, offer = {type = 'give-item', item = 'cluster-grenade', count = 1}}, + {price = {{"coin", 1}}, offer = {type = 'give-item', item = 'land-mine', count = 1}}, + {price = {{"coin", 80}}, offer = {type = 'give-item', item = 'car', count = 1}}, + {price = {{"coin", 1200}}, offer = {type = 'give-item', item = 'tank', count = 1}}, + {price = {{"coin", 3}}, offer = {type = 'give-item', item = 'cannon-shell', count = 1}}, + {price = {{"coin", 7}}, offer = {type = 'give-item', item = 'explosive-cannon-shell', count = 1}}, + {price = {{"coin", 50}}, offer = {type = 'give-item', item = 'gun-turret', count = 1}}, + {price = {{"coin", 300}}, offer = {type = 'give-item', item = 'laser-turret', count = 1}}, + {price = {{"coin", 450}}, offer = {type = 'give-item', item = 'artillery-turret', count = 1}}, + {price = {{"coin", 10}}, offer = {type = 'give-item', item = 'artillery-shell', count = 1}}, + {price = {{"coin", 25}}, offer = {type = 'give-item', item = 'artillery-targeting-remote', count = 1}}, + {price = {{"coin", 1}}, offer = {type = 'give-item', item = 'firearm-magazine', count = 1}}, + {price = {{"coin", 4}}, offer = {type = 'give-item', item = 'piercing-rounds-magazine', count = 1}}, + {price = {{"coin", 2}}, offer = {type = 'give-item', item = 'shotgun-shell', count = 1}}, + {price = {{"coin", 6}}, offer = {type = 'give-item', item = 'piercing-shotgun-shell', count = 1}}, + {price = {{"coin", 30}}, offer = {type = 'give-item', item = "submachine-gun", count = 1}}, + {price = {{"coin", 250}}, offer = {type = 'give-item', item = 'combat-shotgun', count = 1}}, + {price = {{"coin", 450}}, offer = {type = 'give-item', item = 'flamethrower', count = 1}}, + {price = {{"coin", 25}}, offer = {type = 'give-item', item = 'flamethrower-ammo', count = 1}}, + {price = {{"coin", 125}}, offer = {type = 'give-item', item = 'rocket-launcher', count = 1}}, + {price = {{"coin", 2}}, offer = {type = 'give-item', item = 'rocket', count = 1}}, + {price = {{"coin", 7}}, offer = {type = 'give-item', item = 'explosive-rocket', count = 1}}, + {price = {{"coin", 7500}}, offer = {type = 'give-item', item = 'atomic-bomb', count = 1}}, + {price = {{"coin", 325}}, offer = {type = 'give-item', item = 'railgun', count = 1}}, + {price = {{"coin", 8}}, offer = {type = 'give-item', item = 'railgun-dart', count = 1}}, + {price = {{"coin", 40}}, offer = {type = 'give-item', item = 'poison-capsule', count = 1}}, + {price = {{"coin", 4}}, offer = {type = 'give-item', item = 'defender-capsule', count = 1}}, + {price = {{"coin", 10}}, offer = {type = 'give-item', item = 'light-armor', count = 1}}, + {price = {{"coin", 125}}, offer = {type = 'give-item', item = 'heavy-armor', count = 1}}, + {price = {{"coin", 350}}, offer = {type = 'give-item', item = 'modular-armor', count = 1}}, + {price = {{"coin", 1500}}, offer = {type = 'give-item', item = 'power-armor', count = 1}}, + {price = {{"coin", 12000}}, offer = {type = 'give-item', item = 'power-armor-mk2', count = 1}}, + {price = {{"coin", 50}}, offer = {type = 'give-item', item = 'solar-panel-equipment', count = 1}}, + {price = {{"coin", 2250}}, offer = {type = 'give-item', item = 'fusion-reactor-equipment', count = 1}}, + {price = {{"coin", 100}}, offer = {type = 'give-item', item = 'battery-equipment', count = 1}}, + {price = {{"coin", 200}}, offer = {type = 'give-item', item = 'energy-shield-equipment', count = 1}}, + {price = {{"coin", 850}}, offer = {type = 'give-item', item = 'personal-laser-defense-equipment', count = 1}}, + {price = {{"coin", 175}}, offer = {type = 'give-item', item = 'exoskeleton-equipment', count = 1}}, + {price = {{"coin", 125}}, offer = {type = 'give-item', item = 'night-vision-equipment', count = 1}}, + {price = {{"coin", 200}}, offer = {type = 'give-item', item = 'belt-immunity-equipment', count = 1}}, + {price = {{"coin", 250}}, offer = {type = 'give-item', item = 'personal-roboport-equipment', count = 1}}, + {price = {{"coin", 35}}, offer = {type = 'give-item', item = 'construction-robot', count = 1}}, + {price = {{"coin", 25}}, offer = {type = 'give-item', item = 'cliff-explosives', count = 1}}, + {price = {{"coin", 80}}, offer = {type = 'nothing', effect_description = special_descriptions["flame-boots"]}} + } + + for _, item in pairs(market_items) do + global.market.add_market_item(item) + end + + if not global.railgun_enhancer_unlocked then + global.market.add_market_item({price = {{"coin", 1500}}, offer = {type = 'nothing', effect_description = special_descriptions["railgun-enhancer"]}}) + end + if not global.trapped_capsules_unlocked then + global.market.add_market_item({price = {{"coin", 3500}}, offer = {type = 'nothing', effect_description = special_descriptions["trapped-capsules"]}}) + end + if not global.explosive_bullets_unlocked then + global.market.add_market_item({price = {{"coin", 4500}}, offer = {type = 'nothing', effect_description = special_descriptions["explosive-bullets"]}}) + end + if not global.bouncy_shells_unlocked then + global.market.add_market_item({price = {{"coin", 10000}}, offer = {type = 'nothing', effect_description = special_descriptions["bouncy-shells"]}}) + end + if not global.vehicle_nanobots_unlocked then + global.market.add_market_item({price = {{"coin", 15000}}, offer = {type = 'nothing', effect_description = special_descriptions["vehicle-nanobots"]}}) + end + if not global.crumbly_walls_unlocked then + global.market.add_market_item({price = {{"coin", 35000}}, offer = {type = 'nothing', effect_description = special_descriptions["crumbly-walls"]}}) + end + if not global.ultra_mines_unlocked then + global.market.add_market_item({price = {{"coin", 45000}}, offer = {type = 'nothing', effect_description = special_descriptions["ultra-mines"]}}) + end + if not global.laser_pointer_unlocked then + global.market.add_market_item({price = {{"coin", 65000}}, offer = {type = 'nothing', effect_description = special_descriptions["laser-pointer"]}}) + end +end + +local function slot_upgrade(player, offer_index) + local price = global.entity_limits[slot_upgrade_offers[offer_index][1]].limit * global.entity_limits[slot_upgrade_offers[offer_index][1]].slot_price + + local gain = 1 + if offer_index == 5 then + price = math.ceil((global.entity_limits[slot_upgrade_offers[offer_index][1]].limit / 3) * global.entity_limits[slot_upgrade_offers[offer_index][1]].slot_price) + gain = 3 + end + + if slot_upgrade_offers[offer_index][1] == "flamethrower-turret" then + price = (global.entity_limits[slot_upgrade_offers[offer_index][1]].limit + 1) * global.entity_limits[slot_upgrade_offers[offer_index][1]].slot_price + end + + local coins_removed = player.remove_item({name = "coin", count = price}) + if coins_removed ~= price then + if coins_removed > 0 then + player.insert({name = "coin", count = coins_removed}) + end + player.print("Not enough coins.", {r = 0.22, g = 0.77, b = 0.44}) + return false + end + + global.entity_limits[slot_upgrade_offers[offer_index][1]].limit = global.entity_limits[slot_upgrade_offers[offer_index][1]].limit + gain + game.print(player.name .. " has bought a " .. slot_upgrade_offers[offer_index][2] .. " slot for " .. price .. " coins!", {r = 0.22, g = 0.77, b = 0.44}) + server_commands.to_discord_bold(table.concat{player.name .. " has bought a " .. slot_upgrade_offers[offer_index][2] .. " slot for " .. price .. " coins!"}) + refresh_market_offers() +end + +local function on_market_item_purchased(event) + local player = game.players[event.player_index] + local market = event.market + local offer_index = event.offer_index + local offers = market.get_market_items() + local bought_offer = offers[offer_index].offer + if bought_offer.type ~= "nothing" then return end + + if slot_upgrade_offers[offer_index] then + if slot_upgrade(player, offer_index) then return end + end + + if offer_index < 50 then return end + + if bought_offer.effect_description == special_descriptions["flame-boots"] then + game.print(player.name .. " has bought themselves some flame boots.", {r = 0.22, g = 0.77, b = 0.44}) + if not global.flame_boots[player.index].fuel then + global.flame_boots[player.index].fuel = math.random(1500, 3000) + else + global.flame_boots[player.index].fuel = global.flame_boots[player.index].fuel + math.random(1500, 3000) + end + + player.print("Fuel remaining: " .. global.flame_boots[player.index].fuel, {r = 0.22, g = 0.77, b = 0.44}) + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["explosive-bullets"] then + game.print(player.name .. " has unlocked explosive bullets.", {r = 0.22, g = 0.77, b = 0.44}) + global.explosive_bullets_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["bouncy-shells"] then + game.print(player.name .. " has unlocked bouncy shells.", {r = 0.22, g = 0.77, b = 0.44}) + global.bouncy_shells_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["trapped-capsules"] then + game.print(player.name .. " has unlocked trapped capsules!", {r = 0.22, g = 0.77, b = 0.44}) + global.trapped_capsules_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["ultra-mines"] then + game.print(player.name .. " has unlocked ultra mines!", {r = 0.22, g = 0.77, b = 0.44}) + global.ultra_mines_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["laser-pointer"] then + game.print(player.name .. " has unleashed the quest to slay the red dot!", {r = 0.22, g = 0.77, b = 0.44}) + global.laser_pointer_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["railgun-enhancer"] then + game.print(player.name .. " has unlocked the enhanced railgun!", {r = 0.22, g = 0.77, b = 0.44}) + global.railgun_enhancer_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["crumbly-walls"] then + game.print(player.name .. " has unlocked crumbly walls!", {r = 0.22, g = 0.77, b = 0.44}) + global.crumbly_walls_unlocked = true + refresh_market_offers() + return + end + + if bought_offer.effect_description == special_descriptions["vehicle-nanobots"] then + game.print(player.name .. " has unlocked vehicle nanobots!", {r = 0.22, g = 0.77, b = 0.44}) + global.vehicle_nanobots_unlocked = true + refresh_market_offers() + return + end +end + +local function on_gui_opened(event) + if not event.entity then return end + if not event.entity.valid then return end + if event.entity.name == "market" then refresh_market_offers() return end +end + +event.add(defines.events.on_market_item_purchased, on_market_item_purchased) +event.add(defines.events.on_gui_opened, on_gui_opened) \ No newline at end of file diff --git a/maps/fish_defender_v1/on_entity_damaged.lua b/maps/fish_defender_v1/on_entity_damaged.lua new file mode 100644 index 00000000..43e55fed --- /dev/null +++ b/maps/fish_defender_v1/on_entity_damaged.lua @@ -0,0 +1,42 @@ +local event = require 'utils.event' + +local enhance_railgun = require 'maps.fish_defender.railgun_enhancer' +local explosive_bullets = require 'maps.fish_defender.explosive_gun_bullets' +local bouncy_shells = require 'maps.fish_defender.bouncy_shells' +local boss_biter = require "maps.fish_defender.boss_biters" + +local function protect_market(event) + if event.entity.name ~= "market" then return false end + if event.cause then + if event.cause.force.name == "enemy" then return false end + end + event.entity.health = event.entity.health + event.final_damage_amount + return true +end + +local function on_entity_damaged(event) + if not event.entity then return end + if not event.entity.valid then return end + + if protect_market(event) then return end + + if not event.cause then return end + + if event.cause.unit_number then + if global.boss_biters[event.cause.unit_number] then + boss_biter.damaged_entity(event) + end + end + + if event.cause.name ~= "character" then return end + + if enhance_railgun(event) then return end + if global.explosive_bullets_unlocked then + if explosive_bullets(event) then return end + end + if global.bouncy_shells_unlocked then + if bouncy_shells(event) then return end + end +end + +event.add(defines.events.on_entity_damaged, on_entity_damaged) \ No newline at end of file diff --git a/maps/fish_defender_v1/railgun_enhancer.lua b/maps/fish_defender_v1/railgun_enhancer.lua new file mode 100644 index 00000000..685df209 --- /dev/null +++ b/maps/fish_defender_v1/railgun_enhancer.lua @@ -0,0 +1,98 @@ +-- improves the damage of the railgun and adds visual effects -- by mewmew +-- laser turret research will increase it´s damage even further -- + +local damage_min = 400 +local damage_max = 800 +local math_random = math.random +local additional_visual_effects = true + +local biological_target_types = { + ["unit"] = true, + ["player"] = true, + ["turret"] = true, + ["unit-spawner"] = true +} + +local function create_visuals(source_entity, target_entity) + if not source_entity.valid then return end + if not target_entity.valid then return end + if not additional_visual_effects then return end + local surface = target_entity.surface + surface.create_entity({name = "water-splash", position = target_entity.position}) + if biological_target_types[target_entity.type] then + surface.create_entity({name = "blood-explosion-big", position = target_entity.position}) + for x = -8, 8, 1 do + for y = -8, 8, 1 do + if math_random(1, 16) == 1 then + surface.create_entity({name = "blood-fountain", position = {target_entity.position.x + (x * 0.1), target_entity.position.y + (y * 0.1)}}) + surface.create_entity({name = "blood-fountain-big", position = {target_entity.position.x + (x * 0.1), target_entity.position.y + (y * 0.1)}}) + end + end + end + else + if math_random(1, 3) ~= 1 then + surface.create_entity({name = "fire-flame", position = target_entity.position}) + end + for x = -3, 3, 1 do + for y = -3, 3, 1 do + if math_random(1, 3) == 1 then + surface.create_trivial_smoke({name="smoke-fast", position={target_entity.position.x + (x * 0.35), target_entity.position.y + (y * 0.35)}}) + end + if math_random(1, 5) == 1 then + surface.create_trivial_smoke({name="train-smoke", position={target_entity.position.x + (x * 0.35), target_entity.position.y + (y * 0.35)}}) + end + end + end + end +end + +local function do_splash_damage_around_entity(source_entity, player) + if not source_entity.valid then return end + local research_damage_bonus = player.force.get_ammo_damage_modifier("laser-turret") + 1 + local research_splash_radius_bonus = player.force.get_ammo_damage_modifier("laser-turret") * 0.5 + local splash_area = { + {source_entity.position.x - (2.5 + research_splash_radius_bonus), source_entity.position.y - (2.5 + research_splash_radius_bonus)}, + {source_entity.position.x + (2.5 + research_splash_radius_bonus), source_entity.position.y + (2.5 + research_splash_radius_bonus)} + } + local entities = source_entity.surface.find_entities_filtered({area = splash_area}) + for _, entity in pairs(entities) do + if entity.valid then + if entity.health and entity ~= source_entity and entity ~= player then + if additional_visual_effects then + local surface = entity.surface + surface.create_entity({name = "railgun-beam", position = source_entity.position, source = source_entity.position, target = entity.position}) + surface.create_entity({name = "water-splash", position = entity.position}) + if biological_target_types[entity.type] then + surface.create_entity({name = "blood-fountain", position = entity.position}) + end + end + local damage = math_random(math.ceil((damage_min * research_damage_bonus) / 16), math.ceil((damage_max * research_damage_bonus) / 16)) + entity.damage(damage, player.force, "physical") + end + end + end +end + +local function enhance(event) + if not global.railgun_enhancer_unlocked then return end + if event.damage_type.name ~= "physical" then return end + if event.original_damage_amount ~= 100 then return end + + local player = event.cause + if player.shooting_state.state == defines.shooting.not_shooting then return end + local selected_weapon = player.get_inventory(defines.inventory.character_guns)[player.selected_gun_index] + if selected_weapon.name ~= "railgun" then return end + + create_visuals(event.cause, event.entity) + + do_splash_damage_around_entity(event.entity, player) + + event.entity.health = event.entity.health + event.final_damage_amount + + local research_damage_bonus = player.force.get_ammo_damage_modifier("laser-turret") + 1 + local damage = math_random(math.ceil(damage_min * research_damage_bonus), math.ceil(damage_max * research_damage_bonus)) + event.entity.damage(damage, player.force, "physical") + return true +end + +return enhance \ No newline at end of file diff --git a/maps/fish_defender_v1/shotgun_buff.lua b/maps/fish_defender_v1/shotgun_buff.lua new file mode 100644 index 00000000..43e0a4b2 --- /dev/null +++ b/maps/fish_defender_v1/shotgun_buff.lua @@ -0,0 +1,28 @@ +local event = require 'utils.event' +local gain_multiplier = 4 + +local function on_research_finished(event) + local research = event.research + local force_name = research.force.name + + if not global.shotgun_shell_damage_modifier_old[force_name] then global.shotgun_shell_damage_modifier_old[force_name] = game.forces[force_name].get_ammo_damage_modifier("shotgun-shell") - 0.1 end + + if string.sub(research.name, 0, 26) == "physical-projectile-damage" then + local current_damage = game.forces[force_name].get_ammo_damage_modifier("shotgun-shell") + local vanilla_gain = current_damage - global.shotgun_shell_damage_modifier_old[force_name] + local additional_gain = vanilla_gain * (gain_multiplier - 1) + game.forces[force_name].set_ammo_damage_modifier("shotgun-shell", current_damage + additional_gain) + end + + global.shotgun_shell_damage_modifier_old[force_name] = game.forces[force_name].get_ammo_damage_modifier("shotgun-shell") +end + +local function on_init() + game.forces.player.set_ammo_damage_modifier("shotgun-shell", 1) + global.shotgun_shell_damage_modifier_old = {} +end + +event.on_init(on_init) +event.add(defines.events.on_research_finished, on_research_finished) + + diff --git a/maps/fish_defender_v1/trapped_capsules.lua b/maps/fish_defender_v1/trapped_capsules.lua new file mode 100644 index 00000000..c287b768 --- /dev/null +++ b/maps/fish_defender_v1/trapped_capsules.lua @@ -0,0 +1,44 @@ +local event = require 'utils.event' + +local radius = 20 + +local whitelist = { + ["defender"] = "explosive-cannon-projectile", + ["distractor"] = "explosive-uranium-cannon-projectile", + ["destroyer"] = "explosive-uranium-cannon-projectile" +} + +local function on_entity_died(event) + if not global.trapped_capsules_unlocked then return end + + if not event.entity.valid then return end + if not whitelist[event.entity.name] then return end + + local valid_targets = {} + local position = event.entity.position + + for _, e in pairs(event.entity.surface.find_entities_filtered({area = {{position.x - radius, position.y - radius},{position.x + radius, position.y + radius}}, force = "enemy"})) do + if e.health then + local distance_from_center = math.sqrt((e.position.x - position.x) ^ 2 + (e.position.y - position.y) ^ 2) + if distance_from_center <= radius then + valid_targets[#valid_targets + 1] = e + end + end + end + + if not valid_targets[1] then return end + + event.entity.surface.create_entity({ + name = whitelist[event.entity.name], + position = position, + force = "player", + source = position, + target = valid_targets[math.random(1, #valid_targets)].position, + max_range = 20, + speed = 0.1 + }) +end + +event.add(defines.events.on_entity_died, on_entity_died) + + diff --git a/maps/fish_defender_v1/ultra_mines.lua b/maps/fish_defender_v1/ultra_mines.lua new file mode 100644 index 00000000..63e2d402 --- /dev/null +++ b/maps/fish_defender_v1/ultra_mines.lua @@ -0,0 +1,34 @@ +local event = require 'utils.event' +local radius = 8 + +local function damage_entities_around_target(entity, damage) + for _, e in pairs(entity.surface.find_entities_filtered({area = {{entity.position.x - radius, entity.position.y - radius},{entity.position.x + radius, entity.position.y + radius}}})) do + if e.health then + if e.force.name ~= "player" then + local distance_from_center = math.sqrt((e.position.x - entity.position.x) ^ 2 + (e.position.y - entity.position.y) ^ 2) + if distance_from_center <= radius then + e.damage(damage, "player", "explosion") + end + end + end + end +end + +local function on_entity_died(event) + if not global.ultra_mines_unlocked then return end + if not event.entity.valid then return end + if event.entity.name ~= "land-mine" then return end + + event.entity.surface.create_entity({ + name = "big-artillery-explosion", + position = event.entity.position + }) + + local damage = (1 + event.entity.force.get_ammo_damage_modifier("grenade")) * 250 + + damage_entities_around_target(event.entity, damage) +end + +event.add(defines.events.on_entity_died, on_entity_died) + + diff --git a/maps/fish_defender_v1/vehicle_nanobots.lua b/maps/fish_defender_v1/vehicle_nanobots.lua new file mode 100644 index 00000000..bdd5e5e8 --- /dev/null +++ b/maps/fish_defender_v1/vehicle_nanobots.lua @@ -0,0 +1,16 @@ +local event = require 'utils.event' + +local function on_player_changed_position(event) + if not global.vehicle_nanobots_unlocked then return end + local player = game.players[event.player_index] + if not player.character then return end + if not player.character.driving then return end + if not player.vehicle then return end + if not player.vehicle.valid then return end + if player.vehicle.health == player.vehicle.prototype.max_health then return end + player.vehicle.health = player.vehicle.health + player.vehicle.prototype.max_health * 0.005 +end + +event.add(defines.events.on_player_changed_position, on_player_changed_position) + + diff --git a/maps/island_troopers/enemies.lua b/maps/island_troopers/enemies.lua index 6cab18d1..bf683031 100644 --- a/maps/island_troopers/enemies.lua +++ b/maps/island_troopers/enemies.lua @@ -112,12 +112,12 @@ function add_enemies(surface, tiles) if math.random(1, 5) == 1 or is_boss_stage() then local evolution = (global.current_level * 2 * difficulties_votes[global.difficulty_vote_index].strength_modifier) * 0.01 if evolution > 1 then evolution = 1 end - game.forces.enemy.evolution_factor = evolution - local count = math.random(1, math.ceil(global.current_level * 0.05)) - if count > 3 then count = 3 end + game.forces.enemy_spawners.evolution_factor = evolution + local count = math.random(1, math.ceil(global.current_level * 0.10)) + if count > 5 then count = 5 end for k, tile in pairs(tiles) do - if surface.can_place_entity({name = "biter-spawner", position = tile.position, force = "enemy"}) then - surface.create_entity({name = "biter-spawner", position = tile.position, force = "enemy"}) + if surface.can_place_entity({name = "biter-spawner", position = tile.position, force = "enemy_spawners"}) then + surface.create_entity({name = "biter-spawner", position = tile.position, force = "enemy_spawners"}) global.alive_enemies = global.alive_enemies + 1 count = count - 1 if count == 0 then break end diff --git a/maps/island_troopers/main.lua b/maps/island_troopers/main.lua index 9f784c04..7fd40ef0 100644 --- a/maps/island_troopers/main.lua +++ b/maps/island_troopers/main.lua @@ -176,6 +176,10 @@ end local function on_init() + game.create_force("enemy_spawners") + game.forces.enemy_spawners.set_friend("enemy", true) + game.forces.enemy.set_friend("enemy_spawners", true) + local surface = game.surfaces[1] surface.request_to_generate_chunks({x = 0, y = 0}, 16) surface.force_generate_chunk_requests() @@ -223,11 +227,14 @@ local function on_entity_died(event) local entity = event.entity if not entity.valid then return end - if entity.force.name ~= "enemy" then return end - if entity.type == "unit" then - if entity.spawner then return end + if entity.force.name == "enemy_spawners" then + if entity.type == "unit" then return end + global.alive_enemies = global.alive_enemies - 1 + return end + if entity.force.name ~= "enemy" then return end + global.alive_enemies = global.alive_enemies - 1 update_stage_gui() @@ -259,7 +266,7 @@ local gamestate_functions = { local function on_tick() gamestate_functions[global.gamestate]() - if game.tick % 120 == 0 then drift_corpses_toward_beach() end + if game.tick % 150 == 0 then drift_corpses_toward_beach() end end local event = require 'utils.event' diff --git a/maps/island_troopers/terrain.lua b/maps/island_troopers/terrain.lua index 332ef26f..17319cbe 100644 --- a/maps/island_troopers/terrain.lua +++ b/maps/island_troopers/terrain.lua @@ -268,11 +268,14 @@ local function process_tile(surface, position) if position.x < -128 then surface.set_tiles({{name = "out-of-map", position = position}}, true) return end if position.x > 8192 then surface.set_tiles({{name = "out-of-map", position = position}}, true) return end if position.y < 0 then surface.set_tiles({{name = "deepwater", position = position}}, true) return end - if position.y > 32 then surface.set_tiles({{name = "water-green", position = position}}, true) + if position.y > 32 then + surface.set_tiles({{name = "water-green", position = position}}, true) if math.random(1, 4096) == 1 then if math.random(1, 4) == 1 then + surface.set_tiles({{name = "sand-1", position = position}}, true) create_dump_chest(surface, position, false) else + surface.set_tiles({{name = "sand-1", position = position}}, true) create_shopping_chest(surface, position, false) end end