local Event = require 'utils.event' local Global = require 'utils.global' local Gui = require 'utils.gui' local Commands = require 'utils.commands' local this = { renders = {}, valid_targets = { 'character', 'tank', 'car', 'locomotive', 'cargo-wagon', 'fluid-wagon', 'artillery-wagon', 'artillery-turret', 'spidertron' }, backup_valid_targets = { 'character', 'tank', 'car', 'locomotive', 'cargo-wagon', 'fluid-wagon', 'artillery-wagon', 'artillery-turret', 'spidertron' } } local Public = {} Public.metatable = { __index = Public } Global.register( this, function (tbl) this = tbl for _, render in pairs(this.renders) do setmetatable(render, Public.metatable) end end ) local sqrt = math.sqrt local random = math.random local remove = table.remove local speed = 0.06 --- Draws a new render. ---@return table|nil function Public:new_render() local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end if self.render_id then self.render_id.destroy() end self.render_id = rendering.draw_sprite { target = self.position, sprite = self.sprite, surface = surface } return self end --- Sets a new target for a given render. ---@return table|nil, table|nil function Public:new_target() local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end local position local entities = surface.find_entities_filtered { type = this.valid_targets, force = 'player' } if entities and #entities > 0 then position = entities[random(#entities)].position end local chunk = surface.get_random_chunk() local random_position = { x = (chunk.x + random()) * 32, y = (chunk.y + random()) * 32 } if not position then entities = surface.find_entities_filtered { type = 'character', force = 'player' } if entities and #entities > 0 then position = entities[random(#entities)].position end if not position then return random_position, random_position end end return position, random_position end --- Subtracts the given positions ---@return table|integer function Public:subtr() if not self.position and self.target_position then return 0 end return { x = self.target_position.x - self.position.x, y = self.target_position.y - self.position.y } end --- Sets the render scale. function Public:set_render_scalar_size() if not self.render_id then return self:validate() end self.render_id.y_scale = 3.5 -- 1.5 self.render_id.x_scale = 7 -- 2 self.render_id.color = { r = 1, g = 0.7, b = 0.7 } end --- Gets a random position. ---@return table function Public:random_position() return { x = self.position.x + (random() - 0.5) * 64, y = self.position.y + (random() - 0.5) * 64 } end --- Sets a random sprite ---@return table function Public:random_sprite() if random(1, 2) == 1 then self.sprite = Gui.beam self:new_render() elseif random(1, 2) == 2 then self.sprite = Gui.beam_2 self:new_render() else self.sprite = Gui.beam_1 self:new_render() end return self end --- Changes the position of a render. ---@param max_abs number ---@param value boolean ---@return table|nil function Public:change_position(max_abs, value) if not self.position or not self.target_position then return end local scalar = 0.9 local subtr = self:subtr() if value then subtr.y = subtr.y / scalar end local multiply = sqrt(subtr.x * subtr.x + subtr.y * subtr.y) if (multiply > max_abs) then local close = max_abs / multiply subtr = { x = subtr.x * close, y = subtr.y * close } end if value then subtr.y = subtr.y * scalar end return { x = self.position.x + subtr.x, y = self.position.y + subtr.y } end --- If a render is stuck, give it a new position. function Public:switch_position() if random() < 0.4 then self.target_position = self:random_position() else local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end local chunk = surface.get_random_chunk() self.target_position = { x = (chunk.x + math.random()) * 32, y = (chunk.y + math.random()) * 32 } end end --- Notifies for a new render function Public:notify_new_beam() if not self.notify then self.notify = true local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end game.print('[color=yellow][Orbital][/color] A new orbital strike has been spotted at: [gps=' .. self.position.x .. ',' .. self.position.y .. ',' .. surface.name .. ']') end end --- Renders a new chart function Public:render_chart() if self.chart and self.chart.valid then self.chart.destroy() end local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end self.chart = game.forces[self.force].add_chart_tag( surface, { icon = { type = 'virtual', name = 'signal-info' }, position = self.position, text = 'Beam' } ) end --- Sets a new position for a render. function Public:set_new_position() self.position = self:change_position(speed, false) if not self.random_pos_set then self.random_pos_set = true self.random_pos_tick = game.tick + 300 end if self.position.x == self.target_position.x and self.position.y == self.target_position.y then self:switch_position() end if self:validate() then self.render_id.target = self.position self:set_render_scalar_size() end end --- Creates fire flame. function Public:render_fire_damage() if random(1, 15) == 1 then local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end surface.create_entity({ name = 'fire-flame', position = { x = self.position.x, y = self.position.y + 5 } }) if random(1, 5) == 1 then surface.create_entity( { name = 'medium-scorchmark', position = { x = self.position.x, y = self.position.y + 5 }, force = 'neutral' } ) end end end --- Damages entities nearby. function Public:damage_entities_nearby() if random(1, 3) == 1 then local surface = game.get_surface(self.surface_id) if not surface or not surface.valid then return end local damage = random(10, 15) local entities = surface.find_entities_filtered( { position = self.position, radius = 20, type = 'simple-entity', invert = true } ) for _, entity in pairs(entities) do if entity.valid then if entity.health then if entity.force.name ~= 'enemy' then entity.damage(damage, 'enemy', 'explosion') end end end end end end --- Validates if a render is valid. ---@return boolean|integer function Public:validate() if not self.render_id then self:new_render() return false end if self.render_id.valid then return true end return false end --- Destroys a render. function Public:destroy_render() if self.render_id and self.render_id.valid then self.render_id.destroy() end return self end --- Destroys a render. function Public:destroy_chart() if self.chart and self.chart.valid then self.chart.destroy() end return self end --- Removes a render. function Public:remove_render() self:destroy_render() self:destroy_chart() remove(this.renders, self.id) return self end function Public:work(tick) if tick < self.ttl then self:render_chart() self:notify_new_beam() self:set_new_position() self:render_fire_damage() self:damage_entities_nearby() if self.random_pos_set and tick > self.random_pos_tick then self:switch_position() self.random_pos_set = nil self.random_pos_tick = nil end else self:remove_render() end end --- Creates a new render. ---@param sprite string ---@param surface LuaSurface ---@param ttl integer|nil ---@param scalar table|nil ---@param delayed number|nil ---@return table function Public.new(sprite, surface, ttl, scalar, delayed) local render = setmetatable({}, Public.metatable) render.surface_id = surface.index local position, random_position = render:new_target() render.position = position render.sprite = sprite render.force = 'player' render.target_position = random_position render.id = #this.renders + 1 if delayed then render.delayed = game.tick + delayed render.ttl = ttl or (game.tick + delayed) + 7200 -- 2 minutes duration else render.ttl = ttl or game.tick + 7200 -- 2 minutes duration render:validate() if not scalar then render:set_render_scalar_size() end end render.ttl = ttl or game.tick + 7200 -- 2 minutes duration this.renders[render.id] = render return render end --- Creates a new defined beam ---@param surface LuaSurface ---@param ttl number|nil function Public.new_beam(surface, ttl) Public.new(Gui.beam, surface, ttl) end --- Defines new targets as valid targets ---@param targets table function Public.new_valid_targets(targets) if targets and type(targets) == 'table' then this.backup_valid_targets = this.valid_targets this.valid_targets = targets else error('New valid targets needs to be of type table', 2) end end --- Defaults the valid targets function Public.reset_valid_targets() this.valid_targets = this.backup_valid_targets end --- Creates a new defined beam with a delayed action ---@param surface LuaSurface ---@param time number function Public.new_beam_delayed(surface, time) Public.new(Gui.beam, surface, nil, nil, time) end Event.add( defines.events.on_tick, function () if #this.renders == 0 then return end local tick = game.tick for id = 1, #this.renders, 1 do local render = this.renders[id] if render then if render.delayed then if tick > render.delayed then render:work(tick) end else render:work(tick) end end end end ) if _DEBUG then Commands.new('laser', 'new laser') :require_admin() :callback( function (player) Public.new_beam_delayed(player.surface, 222) end ) end return Public