local Price_raffle = require 'maps.expanse.price_raffle' local BiterRaffle = require 'utils.functions.biter_raffle' local SpaceMissions = require 'maps.expanse.space_missions' local Task = require 'utils.task' local Token = require 'utils.token' local Public = {} local ores = { 'copper-ore', 'iron-ore', 'stone', 'coal' } local price_modifiers = { ['unit-spawner'] = -256, ['unit'] = -16, ['turret'] = -128, ['tree'] = -8, ['simple-entity'] = 2, ['cliff'] = -128, ['water'] = -5, ['water-green'] = -5, ['deepwater'] = -5, ['deepwater-green'] = -5, ['water-mud'] = -6, ['water-shallow'] = -6 } local qualities = { ['normal'] = 0, ['uncommon'] = 1, ['rare'] = 2, ['epic'] = 3, ['legendary'] = 5 } --- Some mods like to destroy the infini tree. --- So we solve it by delaying the creation. local delay_infini_tree_token = Token.register( function (event) local surface = event.surface local position = event.position local newtree = surface.create_entity({ name = 'tree-0' .. math.random(1, 9), position = position }) event.expanse.tree = script.register_on_object_destroyed(newtree) end ) local function reward_tokens(expanse, entity) local chance = expanse.token_chance % 1 local count = math.floor(expanse.token_chance) if chance > 0 then chance = math.floor(chance * 1000) if math.random(1, 1000) <= chance then entity.surface.spill_item_stack({ position = entity.position, stack = { name = 'coin', count = 1 }, enable_looted = true, allow_belts = false }) end end if count > 0 then for _ = 1, count, 1 do entity.surface.spill_item_stack({ position = entity.position, stack = { name = 'coin', count = 1 }, enable_looted = true, allow_belts = false }) end end end local function get_cell_value(expanse, left_top) local square_size = expanse.square_size local value = square_size ^ 2 value = value * 8 local source_surface = game.surfaces[expanse.source_surface] local area = { { left_top.x, left_top.y }, { left_top.x + square_size, left_top.y + square_size } } local entities = source_surface.find_entities(area) local tiles = source_surface.find_tiles_filtered({ area = area }) for _, tile in pairs(tiles) do if price_modifiers[tile.name] then value = value + price_modifiers[tile.name] end end for _, entity in pairs(entities) do if price_modifiers[entity.type] then value = value + price_modifiers[entity.type] end end local distance = math.sqrt(left_top.x ^ 2 + left_top.y ^ 2) value = value * ((distance ^ 1.15) * expanse.price_distance_modifier) local ore_modifier = distance * (expanse.price_distance_modifier / 20) if ore_modifier > expanse.max_ore_price_modifier then ore_modifier = expanse.max_ore_price_modifier end for _, entity in pairs(entities) do if entity.type == 'resource' then if entity.prototype.resource_category == 'basic-fluid' then value = value + (entity.amount * ore_modifier * 0.01) else value = value + (entity.amount * ore_modifier) end end end value = math.floor(value) if value < 16 then value = 16 end return value end local function get_left_top(expanse, position) local vectors = { { -1, 0 }, { 1, 0 }, { 0, 1 }, { 0, -1 } } table.shuffle_table(vectors) local surface = game.surfaces[expanse.active_surface_index] for _, v in pairs(vectors) do local tile = surface.get_tile(position.x + v[1], position.y + v[2]) if tile.name == 'out-of-map' then local left_top = tile.position left_top.x = left_top.x - left_top.x % expanse.square_size left_top.y = left_top.y - left_top.y % expanse.square_size if not expanse.grid[tostring(left_top.x .. '_' .. left_top.y)] then return left_top end end end return false end local function is_container_position_valid(expanse, position) if game.tick == 0 then return true end local left_top = get_left_top(expanse, position) if not left_top then return false end if game.surfaces[expanse.active_surface_index].count_entities_filtered( { name = 'requester-chest', force = 'neutral', area = { { left_top.x - 1, left_top.y - 1 }, { left_top.x + expanse.square_size + 1, left_top.y + expanse.square_size + 1 } } } ) > 0 then return false end return true end local function create_costs_render(entity, name, offset, quality) local id = rendering.draw_sprite { sprite = 'virtual-signal/signal-grey', surface = entity.surface, target = { entity = entity, offset = { offset, -1.5 } }, x_scale = 1.1, y_scale = 1.1, render_layer = '190', only_in_alt_mode = true } local id2 = rendering.draw_sprite { sprite = 'item/' .. name, surface = entity.surface, target = { entity = entity, offset = { offset, -1.5 } }, x_scale = 0.75, y_scale = 0.75, render_layer = '191', only_in_alt_mode = true } local Q = script.active_mods['quality'] local id3 = rendering.draw_sprite { sprite = 'quality/' .. quality, surface = entity.surface, target = { entity = entity, offset = { offset - 0.25, -1.25 } }, x_scale = 0.25, y_scale = 0.25, render_layer = Q and '192' or '189', only_in_alt_mode = true } return { id, id2, id3} end local function remove_one_render(container, key) if container.price[key].render[1].valid then container.price[key].render[1].destroy() end if container.price[key].render[2].valid then container.price[key].render[2].destroy() end if container.price[key].render[3].valid then container.price[key].render[3].destroy() end end local function remove_old_renders(container) for key, _ in pairs(container.price) do remove_one_render(container, key) end end function Public.spawn_units(spawner) local evolution = game.forces.enemy.get_evolution_factor(spawner.surface) local position = spawner.position for i = 1, 4 + math.floor(8 * evolution), 1 do local biter_roll = BiterRaffle.roll('mixed', evolution) local free_pos = spawner.surface.find_non_colliding_position(biter_roll, { x = position.x + math.random(-8, 8), y = position.y + math.random(-8, 8) }, 12, 0.05) spawner.surface.create_entity({ name = biter_roll, position = free_pos or position, force = 'enemy' }) end end function Public.get_item_tooltip(name, quality, include_value) local value = include_value and Price_raffle.get_item_worth(name, qualities[quality]) or '' local desc = include_value and 'expanse.stats_item_tooltip' or 'expanse.stats_item_tooltip_nv' if quality == 'normal' then return { desc, prototypes.item[name].localised_name, '', value } end return { desc, prototypes.item[name].localised_name, {'expanse.stats_quality', prototypes.quality[quality].localised_name or ''}, value } end function Public.invasion_numbers(expanse) local evo = game.forces.enemy.get_evolution_factor(game.surfaces[expanse.active_surface_index]) return { candidates = 3 + math.floor(evo * 10), groups = 1 + math.floor(evo * 4) } end function Public.invasion_warn(event) local seconds = (120 * 60 - (event.delay or 0)) / 60 game.print({ 'expanse.biters_invasion_warning', seconds, event.size }, { r = 0.88, g = 0.22, b = 0.22 }) end function Public.invasion_detonate(event) local surface = event.surface local position = event.position local entities_close = surface.find_entities_filtered { position = position, radius = 8 } for _, entity in pairs(entities_close) do if entity.valid then entity.die('enemy') end end local entities_nearby = surface.find_entities_filtered { position = position, radius = 16 } for _, entity in pairs(entities_nearby) do if entity.valid and entity.is_entity_with_health then entity.damage(entity.max_health * 0.75, 'enemy') end end surface.create_entity({ name = 'nuke-explosion', position = position }) end function Public.invasion_trigger(event) local surface = event.surface local position = event.position local round = event.round local evolution = game.forces.enemy.get_evolution_factor(surface) local biters = {} for i = 1, 5 + math.floor(30 * evolution) + round * 5, 1 do local biter_roll = BiterRaffle.roll('mixed', evolution) local free_pos = surface.find_non_colliding_position(biter_roll, { x = position.x + math.random(-8, 8), y = position.y + math.random(-8, 8) }, 12, 0.05) biters[#biters + 1] = surface.create_entity({ name = biter_roll, position = free_pos or position, force = 'enemy' }) end local group = surface.create_unit_group { position = position, force = 'enemy' } for _, biter in pairs(biters) do group.add_member(biter) end group.set_command({ type = defines.command.attack_area, destination = position, radius = 80, distraction = defines.distraction.by_anything }) group.start_moving() local worm_roll = BiterRaffle.roll('worm', evolution) for i = 1, 3 + math.floor(7 * evolution), 1 do local worm_pos = surface.find_non_colliding_position(worm_roll, { x = position.x + math.random(-12, 12), y = position.y + math.random(-12, 12) }, 12, 0.1) if worm_pos then surface.create_entity({ name = worm_roll, position = worm_pos, force = 'enemy' }) end end local nest = { 'biter-spawner', 'biter-spawner', 'biter-spawner', 'spitter-spawner' } local nest_roll = nest[math.random(1, 4)] local nest_pos = surface.find_non_colliding_position(nest_roll, position, 12, 0.1) if nest_pos then surface.create_entity({ name = nest_roll, position = nest_pos, force = 'enemy' }) end end local function schedule_detonation(expanse, surface, position) table.insert(expanse.schedule, { tick = game.tick + 120 * 60, event = 'invasion_detonate', parameters = { surface = surface, position = position } }) end local function schedule_warning(expanse, size, delay) table.insert(expanse.schedule, { tick = game.tick + 2 * 60 + delay, event = 'invasion_warn', parameters = { size = size, delay = delay } }) end local function schedule_biters(expanse, surface, position, delay, round) table.insert(expanse.schedule, { tick = game.tick + delay + 120 * 60, event = 'invasion_trigger', parameters = { surface = surface, position = position, round = round } }) end local function plan_invasion(expanse, invasion_numbers) local candidates = expanse.invasion_candidates table.shuffle_table(candidates) schedule_warning(expanse, invasion_numbers.groups, 0) schedule_warning(expanse, invasion_numbers.groups, 60 * 60) schedule_warning(expanse, invasion_numbers.groups, 90 * 60) local rounds = 4 + math.random(1, 8) for i = 1, invasion_numbers.groups, 1 do local surface_index = candidates[i].surface_index if not surface_index then break end local surface = game.get_surface(surface_index) local position = candidates[i].position schedule_detonation(expanse, surface, position) for ii = 1, rounds, 1 do schedule_biters(expanse, surface, position, 120 + (ii - 1) * 300, ii) end candidates[i].render.time_to_live = (122 * 60 + rounds * 300) end for j = invasion_numbers.groups + 1, #candidates, 1 do candidates[j].render.time_to_live = (122 * 60) end expanse.invasion_candidates = {} end function Public.check_invasion(expanse) local invasion_numbers = Public.invasion_numbers(expanse) if #expanse.invasion_candidates >= invasion_numbers.candidates then plan_invasion(expanse, invasion_numbers) end end local function calculate_tier(expanse, left_top) local distances = { 30, 60, 90, 150, 210, 300 } local tier = 5 local distance = math.sqrt(left_top.x ^ 2 + left_top.y ^ 2) for i = 1, #distances - 1, 1 do if distance < distances[i] then tier = i break end end if script.active_mods['space-age'] then local biomes = { 6, --vulcanus 7, --fulgora 8, --gleba 9, --aquilo } if distance > distances[5] and distance < distances[6] then if left_top.x >= 0 and left_top.y >= 0 then tier = biomes[(expanse.biome_offset + 1) % 4 + 1] elseif left_top.x >= 0 and left_top.y < 0 then tier = biomes[(expanse.biome_offset + 2) % 4 + 1] elseif left_top.x < 0 and left_top.y >= 0 then tier = biomes[(expanse.biome_offset + 3) % 4 + 1] elseif left_top.x < 0 and left_top.y < 0 then tier = biomes[(expanse.biome_offset + 4) % 4 + 1] end elseif distance >= distances[6] then tier = 10 end end return tier, distance end function Public.expand(expanse, left_top) expanse.grid[tostring(left_top.x .. '_' .. left_top.y)] = true local tier = calculate_tier(expanse, left_top) local square_size = expanse.square_size -- if tier == 7 then -- expanse.lightning_tiles[#expanse.lightning_tiles + 1] = {x = left_top.x + square_size / 2, y = left_top.y + square_size / 2} -- end local source_surface = game.surfaces[expanse.source_surface] if not source_surface then return end source_surface.request_to_generate_chunks(left_top, 2) source_surface.force_generate_chunk_requests() local area = { { left_top.x, left_top.y }, { left_top.x + square_size, left_top.y + square_size } } local surface = game.surfaces[expanse.active_surface_index] source_surface.clone_area( { source_area = area, destination_area = area, destination_surface = surface, clone_tiles = true, clone_entities = game.tick ~= expanse.reset_tick, clone_decoratives = game.tick ~= expanse.reset_tick and (tier < 6 or tier > 9), clear_destination_entities = false, clear_destination_decoratives = true, expand_map = true } ) local positions = { { x = left_top.x + math.random(1, square_size - 2), y = left_top.y }, { x = left_top.x, y = left_top.y + math.random(1, square_size - 2) }, { x = left_top.x + math.random(1, square_size - 2), y = left_top.y + (square_size - 1) }, { x = left_top.x + (square_size - 1), y = left_top.y + math.random(1, square_size - 2) } } for _, position in pairs(positions) do if is_container_position_valid(expanse, position) then local e = surface.create_entity({ name = 'requester-chest', position = position, force = 'neutral' }) e.destructible = false e.minable = false end end local custom_tier = tier if tier == 10 then if expanse.tiered_specials[tier].specials == 0 or math.random(1, 20) ~= 1 then custom_tier = math.random(6, 9) end end SpaceMissions.convert_tiles(surface, left_top, custom_tier, square_size) SpaceMissions.convert_entities(surface, left_top, custom_tier, square_size) SpaceMissions.convert_decoratives(surface, left_top, custom_tier, square_size) SpaceMissions.place_special_tiered_object(expanse, tier, surface, left_top, custom_tier) if game.tick == expanse.reset_tick then local a = math.floor(expanse.square_size * 0.5) for x = 1, 3, 1 do for y = 1, 3, 1 do surface.set_tiles({ { name = 'water', position = { a + x + 2, a + y + 2 } } }, true) end end surface.create_entity({ name = 'crude-oil', position = { a - 4, a - 4 }, amount = 1500000 }) Task.set_timeout_in_ticks(30, delay_infini_tree_token, { surface = surface, position = { a - 4, a + 4 }, expanse = expanse }) surface.create_entity({ name = 'big-rock', position = { a + 4, a - 4 } }) surface.spill_item_stack({ position = { a, a + 2 }, stack = { name = 'coin', count = 1 }, enable_looted = false, allow_belts = false }) surface.spill_item_stack({ position = { a + 0.5, a + 2.5 }, stack = { name = 'coin', count = 1 }, enable_looted = false, allow_belts = false }) surface.spill_item_stack({ position = { a - 0.5, a + 2.5 }, stack = { name = 'coin', count = 1 }, enable_looted = false, allow_belts = false }) for x = 0, square_size, 1 do for y = 0, square_size, 1 do if surface.can_place_entity({ name = 'wooden-chest', position = { x, y } }) and surface.can_place_entity({ name = 'coal', position = { x, y }, amount = 1 }) then surface.create_entity({ name = ores[(x + y) % 4 + 1], position = { x, y }, amount = 1500 }) end end end end end local function get_tier_locker(tier) local SA = script.active_mods['space-age'] local lockers = { [5] = 'space-science-pack', [6] = SA and 'metallurgic-science-pack' or nil, [7] = SA and 'electromagnetic-science-pack' or nil, [8] = SA and 'agricultural-science-pack' or nil, [9] = SA and 'cryogenic-science-pack' or nil, [10] = SA and 'promethium-science-pack' or nil } return lockers[tier] end local function init_container(expanse, entity, budget) local left_top = get_left_top(expanse, entity.position) if not left_top then return end local tier_multi = { [1] = 1, [2] = 1, [3] = 1.5, [4] = 1.5, [5] = 2, [6] = 1, [7] = 1, [8] = 1, [9] = 1, [10] = 3 } local tier, distance = calculate_tier(expanse, left_top) local cell_value = budget or get_cell_value(expanse, left_top) * tier_multi[tier] local item_stacks = {} local locker = get_tier_locker(tier) if locker then item_stacks[locker] = {['normal'] = 10} end local roll_count = 3 for _ = 1, roll_count, 1 do for _, stack in pairs(Price_raffle.roll(math.floor(cell_value / roll_count), 3, tier, nil)) do if not item_stacks[stack.name] then item_stacks[stack.name] = {[stack.quality] = stack.count} elseif not item_stacks[stack.name][stack.quality] then item_stacks[stack.name][stack.quality] = stack.count else item_stacks[stack.name][stack.quality] = item_stacks[stack.name][stack.quality] + stack.count end end end local price = {} local offset = -3 for name, stack in pairs(item_stacks) do for quality, count in pairs(stack) do table.insert(price, { name = name, count = count, quality = quality, render = create_costs_render(entity, name, offset, quality) }) offset = offset + 1 end end if _DEBUG then game.print('distance: ' .. distance .. ', tier: ' .. tier .. ', value: ' .. cell_value) end local containers = expanse.containers containers[entity.unit_number] = { entity = entity, left_top = left_top, price = price } end local function get_remaining_budget(container) local budget = 0 for _, item_stack in pairs(container.price) do budget = budget + (item_stack.count * Price_raffle.get_item_worth(item_stack.name, qualities[item_stack.quality])) end return budget end function Public.make_key(name, quality) return name .. '|' .. quality end function Public.split_key(key) return string.match(key, '^(.-)|(.+)$') end function Public.set_container(expanse, entity) if entity.name ~= 'requester-chest' then return end if not expanse.containers[entity.unit_number] then init_container(expanse, entity) end local container = expanse.containers[entity.unit_number] if not container or not container.entity or not container.entity.valid then expanse.containers[entity.unit_number] = nil return end local inventory = container.entity.get_inventory(defines.inventory.chest) local trash_inventory = container.entity.get_inventory(defines.inventory.logistic_container_trash) if not inventory.is_empty() then if inventory.get_item_count('coin') > 0 then local count_removed = inventory.remove({ name = 'coin', count = 1 }) if count_removed > 0 then expanse.cost_stats[Public.make_key('coin', 'normal')] = (expanse.cost_stats[Public.make_key('coin', 'normal')] or 0) + count_removed script.raise_event(expanse.events.gui_update, { item = 'coin', quality = 'normal' }) remove_old_renders(container) init_container(expanse, entity, get_remaining_budget(container)) container = expanse.containers[entity.unit_number] game.print({ 'expanse.chest_reset', { 'expanse.gps', math.floor(entity.position.x), math.floor(entity.position.y), game.surfaces[expanse.active_surface_index].name } }) end end if _DEBUG and inventory.get_item_count('infinity-chest') > 0 then remove_old_renders(container) container.price = {} end end for key, item_stack in pairs(container.price) do local name = item_stack.name local quality = item_stack.quality local count_removed = inventory.remove({ name = name, count = item_stack.count, quality = item_stack.quality}) container.price[key].count = container.price[key].count - count_removed expanse.cost_stats[Public.make_key(name, quality)] = (expanse.cost_stats[Public.make_key(name, quality)] or 0) + count_removed script.raise_event(expanse.events.gui_update, { item = name, quality = quality}) if container.price[key].count <= 0 then remove_one_render(container, key) table.remove(container.price, key) end end if #container.price == 0 then Public.expand(expanse, container.left_top) local a = math.floor(expanse.square_size * 0.5) local expansion_position = { x = expanse.containers[entity.unit_number].left_top.x + a, y = expanse.containers[entity.unit_number].left_top.y + a } expanse.containers[entity.unit_number] = nil for _, inv in pairs({inventory, trash_inventory}) do if not inv.is_empty() then for index = 1, #inv, 1 do local slot = inv[index] if slot.valid_for_read then entity.surface.spill_item_stack({ position = entity.position, stack = slot, enable_looted = true, allow_belts = false }) end end end end reward_tokens(expanse, entity) entity.destructible = true entity.die() return expansion_position end local logi = container.entity.get_logistic_point(0) for i = 1, 256, 1 do if logi.get_section(i) then logi.remove_section(i) end end logi.add_section() local section = logi.get_section(1) for slot = 1, #container.price, 1 do if #container.price >= slot then local item = container.price[slot] section.set_slot(slot, { value = {name = item.name, quality = item.quality, comparator = '='}, min = item.count, import_from = 'nauvis' }) end end end function Public.chest_value(expanse, player) if not player or not player.valid then return end local position = player.position local chests = player.surface.find_entities_filtered({ name = 'requester-chest', force = 'neutral', position = position, radius = 10}) for _, chest in pairs(chests) do local container = expanse.containers[chest.unit_number] local value = container and get_remaining_budget(container) or 0 player.print({'expanse.chest_value', value, {'expanse.gps', chest.position.x, chest.position.y, chest.surface.name}}) end end return Public