Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,786 @@
--- Tools for working with bounding boxes.
-- @module Area.Area
-- @usage local Area = require('__stdlib__/stdlib/area/area')
-- @see Area.Position
-- @see Concepts.BoundingBox
-- @see Concepts.Position
local Area = { __class = 'Area', __index = require('__stdlib__/stdlib/core') }
setmetatable(Area, Area)
local Position = require('__stdlib__/stdlib/area/position')
local math = require('__stdlib__/stdlib/utils/math')
local string = require('__stdlib__/stdlib/utils/string')
local abs, floor, max = math.abs, math.floor, math.max
local metatable
--- Constructor Methods
-- @section Constructors
Area.__call = function(_, ...)
local type = type((...))
if type == 'table' then
local t = (...)
if t.left_top and t.right_bottom then
return Area.load(...)
else
return Area.new(...)
end
elseif type == 'string' then
return Area.from_string(...)
else
return Area.construct(...)
end
end
local function new_area(lt, rb, o)
return setmetatable({ left_top = lt, right_bottom = rb, orientation = o }, metatable)
end
--- Converts an area in either array or table format to an area with a metatable.
-- Returns itself if it already has a metatable
-- @tparam Concepts.BoundingBox area the area to convert
-- @treturn Concepts.BoundingBox a converted area
function Area.new(area)
local left_top = Position.new(area.left_top or area[1])
local right_bottom = Position.new(area.right_bottom or area[2] or area[1])
return setmetatable({ left_top = left_top, right_bottom = right_bottom, orientation = area.orientation }, metatable)
end
--- Creates an area from number parameters.
-- @tparam[opt=0] number x1 x-position of left_top, first position
-- @tparam[opt=0] number y1 y-position of left_top, first position
-- @tparam[opt=0] number x2 x-position of right_bottom, second position
-- @tparam[opt=0] number y2 y-position of right_bottom, second position
-- @treturn Concepts.BoundingBox the area in a table format
function Area.construct(...)
local args = type((...)) == 'table' and { select(2, ...) } or { select(1, ...) }
local lt = Position.construct_xy(args[1] or 0, args[2] or 0)
local rb = Position.construct_xy(args[3] or lt.x, args[4] or lt.y)
return setmetatable({ left_top = lt, right_bottom = rb }, metatable)
end
--- Loads the metatable into the passed Area without creating a new one.
-- @tparam Concepts.BoundingBox area the Area to set the metatable onto
-- @treturn Concepts.BoundingBox the Area with metatable attached
function Area.load(area)
area.left_top = Position.load(area.left_top)
area.right_bottom = Position.load(area.right_bottom)
return setmetatable(area, metatable)
end
--- Converts an area string to an area.
-- @tparam string area_string the area to convert
-- @treturn Concepts.BoundingBox
function Area.from_string(area_string)
return Area(load('return ' .. area_string)())
end
--- Converts a string key area to an area.
-- @tparam string area_string the area to convert
-- @treturn Concepts.BoundingBox
function Area.from_key(area_string)
local tab = string.split(area_string, ',', false, tonumber)
local lt = Position.new { x = tab[1], y = tab[2] }
local rb = Position.new { x = tab[3], y = tab[4] }
return new_area(lt, rb)
end
--- Area Methods
-- @section Methods
--- Stores the area for recall later, not deterministic.
-- Only the last area stored is saved.
-- @tparam Concepts.BoundingBox area
function Area.store(area)
rawset(getmetatable(area), '_saved', area)
return area
end
--- Recalls the stored area.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox the stored area
function Area.recall(area)
return rawget(getmetatable(area), '_saved')
end
--- Normalizes the given area.
-- <ul>
-- <li>Swaps the values between `right_bottom.x` & `left_top.x` **IF** `right_bottom.x` < `left_top.x`
-- <li>Swaps the values between `right_bottom.y` & `left_top.y` **IF** `right_bottom.y` < `left_top.y`
-- </ul>
-- @tparam Concepts.BoundingBox area the area to normalize
-- @treturn Concepts.BoundingBox a new normalized area
function Area.normalize(area)
local left_top = Position.new(area.left_top)
local right_bottom = Position.new(area.right_bottom)
if right_bottom.x < left_top.x then left_top.x, right_bottom.x = right_bottom.x, left_top.x end
if right_bottom.y < left_top.y then left_top.y, right_bottom.y = right_bottom.y, left_top.y end
return new_area(left_top, right_bottom, area.orientation)
end
--- Normalize an area in place.
-- @tparam Concepts.BoundingBox area the area to normalize
-- @treturn area The area normalized in place
function Area.normalized(area)
local lt, rb = area.left_top, area.right_bottom
if rb.x < lt.x then lt.x, rb.x = rb.x, lt.x end
if rb.y < lt.y then lt.y, rb.y = rb.y, lt.y end
return area
end
--- Convert area from pixels.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox
function Area.from_pixels(area)
return new_area(Position.from_pixels(area.left_top), Position.from_pixels(area.right_bottom), area.orientation)
end
--- Convert area to pixels.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox
function Area.to_pixels(area)
return new_area(Position.to_pixels(area.left_top), Position.to_pixels(area.right_bottom), area.orientation)
end
--- Rounds an areas points to its closest integer.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox
function Area.round(area)
return new_area(Position.round(area.left_top), Position.round(area.right_bottom), area.orientation)
end
--- Ceils an area by increasing the size of the area outwards
-- @tparam Concepts.BoundingBox area the area to round
-- @treturn Concepts.BoundingBox
function Area.ceil(area)
return new_area(Position.floor(area.left_top), Position.ceil(area.right_bottom), area.orientation)
end
--- Floors an area by decreasing the size of the area inwards.
-- @tparam Concepts.BoundingBox area the area to round
-- @treturn Concepts.BoundingBox
function Area.floor(area)
return new_area(Position.ceil(area.left_top), Position.floor(area.right_bottom), area.orientation)
end
-- When looking for tile center points, look inwards on right bottom
-- when x or y is int. This will keep the area with only the tiles it
-- contains.
local function right_bottom_center(pos)
local x, y
local fx, fy = floor(pos.x), floor(pos.y)
x = fx == pos.x and (fx - 0.5) or (fx + 0.5)
y = fy == pos.y and (fy - 0.5) or (fy + 0.5)
return Position.construct_xy(x, y)
end
--- Gets the center positions of the tiles where the given area's two positions reside.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox the area with its two positions at the center of the tiles in which they reside
function Area.center_points(area)
return new_area(Position.center(area.left_top), right_bottom_center(area.right_bottom), area.orientation)
end
--- add left_bottom and right_top to the area
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox the area with left_bottom and right_top included
function Area.corners(area)
local lt, rb = area.left_top, area.right_bottom
local lb = area.left_bottom or Position.construct_xy(0, 0)
local rt = area.right_top or Position.construct_xy(0, 0)
lb.x, lb.y = lt.x, rb.y
rt.x, rt.y = rb.x, lt.y
area.left_bottom = lb
area.right_top = rt
return area
end
--- Flip an area such that the value of its width becomes its height, and the value of its height becomes its width.
-- @tparam Concepts.BoundingBox area the area to flip
-- @treturn Concepts.BoundingBox the fliped area
function Area.flip(area)
local w, h = Area.dimensions(area)
if w == h then
return area -- no point flipping a square
elseif h > w then
local rad = h / 2 - w / 2
return Area.adjust(area, { rad, -rad })
elseif w > h then
local rad = w / 2 - h / 2
return Area.adjust(area, { -rad, rad })
end
end
--- Return a non zero sized area by expanding if needed
-- @tparam Concepts.BoundingBox area the area to check
-- @tparam number|Concepts.Vector amount the amount to expand
-- @treturn Concepts.BoundingBox the area
function Area.non_zero(area, amount)
amount = amount or 0.01
return Area.size(area) == 0 and Area.expand(area, amount) or area
end
--- Returns the area to the diameter from left_top
-- @tparam Concepts.BoundingBox area
-- @tparam number diameter
-- @treturn Concepts.BoundingBox
function Area.to_diameter(area, diameter)
diameter = diameter or 0.1
return new_area(Position.new(area.left_top), Position.add(area.left_top + diameter))
end
--- Returns the smallest sized area.
-- @tparam Concepts.BoundingBox area
-- @tparam Concepts.BooundingBox area2
-- @treturn Concepts.BoundingBox the smallest area
function Area.min(area, area2)
return (Area.size(Area) <= Area.size(area2) and area) or area2
end
--- Returns the largest sized area.
-- @tparam Concepts.BoundingBox area
-- @tparam Concepts.BooundingBox area2
-- @treturn Concepts.BoundingBox the largest area
function Area.max(area, area2)
return (Area.size(area) >= Area.size(area2) and area) or area2
end
--- Shrinks the area inwards by the given amount.
-- The area shrinks inwards from top-left towards the bottom-right, and from bottom-right towards the top-left.
-- @tparam Concepts.BoundingBox area the area to shrink
-- @tparam number|Concepts.Vector amount the amount to shrink
-- @treturn Concepts.BoundingBox the area reduced by amount
function Area.shrink(area, amount)
return new_area(Position.add(area.left_top, amount), Position.subtract(area.right_bottom, amount))
end
--- Expands the area outwards by the given amount.
-- @tparam Concepts.BoundingBox area the area
-- @tparam number|Concepts.Vector amount to expand each edge of the area outwards by
-- @treturn Concepts.BoundingBox the area expanded by amount
-- @see Area.shrink
function Area.expand(area, amount)
return new_area(Position.subtract(area.left_top, amount), Position.add(area.right_bottom, amount))
end
--- Adjust an area by shrinking or expanding.
-- Imagine pinching & holding with fingers the top-left & bottom-right corners of a 2D box and pulling outwards to expand and pushing inwards to shrink the box.
-- @usage local area = Area.adjust({{-2, -2}, {2, 2}}, {4, -1})
-- -- returns {left_top = {x = -6, y = -1}, right_bottom = {x = 6, y = 1}}
-- @tparam Concepts.BoundingBox area the area to adjust
-- @tparam number|Concepts.Vector amount the vectors to use
-- @treturn Concepts.BoundingBox the adjusted bounding box
function Area.adjust(area, amount)
local vec = Position(amount)
area = Area.new(area)
-- shrink or expand on x vector
if vec.x > 0 then
area = Area.expand(area, { vec.x, 0 })
elseif vec.x < 0 then
area = Area.shrink(area, { abs(vec.x), 0 })
end
-- shrink or expand on y vector
if vec.y > 0 then
area = Area.expand(area, { 0, vec.y })
elseif vec.y < 0 then
area = Area.shrink(area, { 0, abs(vec.y) })
end
return area
end
--- Offsets the area by the `{x, y}` values.
-- @tparam Concepts.BoundingBox area the area to offset
-- @tparam Concepts.Position pos the position to which the area will offset
-- @treturn Concepts.BoundingBox the area offset by the position
function Area.offset(area, pos)
local vec = Position(pos)
return new_area(Position.add(area.left_top, vec), Position.add(area.right_bottom, vec))
end
--- Translates an area in the given direction.
-- @tparam Concepts.BoundingBox area the area to translate
-- @tparam defines.direction direction the direction of translation
-- @tparam number distance the distance of the translation
-- @treturn Concepts.BoundingBox the area translated
function Area.translate(area, direction, distance)
direction = direction or 0
distance = distance or 1
return new_area(Position.translate(area.left_top, direction, distance),
Position.translate(area.right_bottom, direction, distance))
end
--- Set an area to the whole size of the surface.
-- @tparam Concepts.BoundingBox area
-- @tparam LuaSurface surface
-- @treturn Concepts.BoundingBox
function Area.to_surface_size(area, surface)
local w, h = surface.map_gen_settings.width, surface.map_gen_settings.height
area.left_top.x = -(w / 2)
area.right_bottom.x = (w / 2)
area.left_top.y = -(h / 2)
area.right_bottom.y = (h / 2)
return area
end
--- Shrinks an area to the size of the surface if it is bigger.
-- @tparam Concepts.BoundingBox area
-- @tparam LuaSurface surface
-- @treturn Concepts.BoundingBox
function Area.shrink_to_surface_size(area, surface)
local w, h = surface.map_gen_settings.width, surface.map_gen_settings.height
if abs(area.left_top.x) > w / 2 then
area.left_top.x = -(w / 2)
area.right_bottom.x = (w / 2)
end
if abs(area.left_top.y) > w / 2 then
area.left_top.y = -(h / 2)
area.right_bottom.y = (h / 2)
end
return area
end
--- Return the chunk coordinates from an area.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox Chunk position coordinates
function Area.to_chunk_coords(area)
return Area.load {
left_top = { x = floor(area.left_top.x / 32), y = floor(area.left_top.y / 32) },
right_bottom = { x = floor(area.right_bottom.x / 32), y = floor(area.right_bottom.y / 32) }
}
end
--- Position Conversion Functions
-- @section ConversionFunctions
--- Calculates the center of the area and returns the position.
-- @tparam Concepts.BoundingBox area the area
-- @treturn Concepts.Position the center of the area
function Area.center(area)
local dist_x = area.right_bottom.x - area.left_top.x
local dist_y = area.right_bottom.y - area.left_top.y
return Position.construct_xy(area.left_top.x + (dist_x / 2), area.left_top.y + (dist_y / 2))
end
--- Area Functions
-- @section Functions
--- Return a suitable string for using as a table key
-- @tparam Concepts.BoundingBox area
-- @return string
function Area.to_key(area)
return table.concat({ area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y }, ',')
end
--- Converts an area to a string.
-- @tparam Concepts.BoundingBox area the area to convert
-- @treturn string the string representation of the area
function Area.to_string(area)
local left_top = 'left_top = ' .. area.left_top
local right_bottom = 'right_bottom = ' .. area.right_bottom
local orientation = area.orientation and ', ' .. area.orientation or ''
return '{' .. left_top .. ', ' .. right_bottom .. orientation .. '}'
end
--- Converts an area to an ltx, lty / rbx, rby string.
-- @tparam Concepts.BoundingBox area the area to convert
-- @treturn string the string representation of the area
function Area.to_string_xy(area)
return table.concat(area.left_top, ', ') .. ' / ' .. table.concat(area.right_bottom, ', ')
end
--- Is this a non zero sized area
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_zero(area)
return Area.size(area) == 0
end
--- Is the area normalized.
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_normalized(area)
return area.right_bottom.x >= area.left_top.x and area.right_bottom.y >= area.left_top.y
end
--- Is the area non-zero and normalized.
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.valid(area)
return Area.is_normalized(area) and Area.size(area) ~= 0
end
--- Is this a simple area. {{num, num}, {num, num}}
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_simple_area(area)
return Position.is_simple_position(area[1]) and Position.is_simple_position(area[2])
end
--- Is this a complex area {left_top = {x = num, y = num}, right_bottom = {x = num, y = num}}
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_complex_area(area)
return Position.is_complex_position(area.left_top) and Position.is_complex_position(area.right_bottom)
end
--- Is this and area of any kind.
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_area(area)
return Area.is_Area(area) or Area.is_complex_area(area) or Area.is_simple_area(area)
end
--- Does the area have the class attached
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_Area(area)
return getmetatable(area) == metatable
end
--- Unpack an area into a tuple.
-- @tparam Concepts.Boundingbox area
-- @treturn tuple lt.x, lt.y, rb.x, rb.y
function Area.unpack(area)
return area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y, area.orientation
end
--- Unpack an area into a tuple of position tables.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.Position left_top
-- @treturn Concepts.Position right_bottom
function Area.unpack_positions(area)
return area.left_top, area.right_bottom
end
--- Pack an area into an array.
-- @tparam Concepts.BoundingBox area
-- @treturn array
function Area.pack(area)
return { area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y, area.orientation }
end
--- Pack an area into a simple bounding box array
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox simple array
function Area.pack_positions(area)
return { { area.left_top.x, area.left_top.y }, { area.right_bottom.x, area.right_bottom.y } }
end
--- Gets the properties of the given area.
-- This function returns a total of four values that represent the properties of the given area.
-- @tparam Concepts.BoundingBox area the area from which to get the size
-- @treturn number the size of the area &mdash; (width &times; height)
-- @treturn number the width of the area
-- @treturn number the height of the area
-- @treturn number the perimeter of the area &mdash; (2 &times; (width + height))
function Area.size(area)
local width = Area.width(area)
local height = Area.height(area)
local area_size = width * height
local perimeter = (width + width) * 2
return area_size, width, height, perimeter
end
--- Return the rectangle.
-- @tparam Concepts.BoundingBox area
-- @treturn number left_top.x
-- @treturn number left_top.y
-- @treturn number width
-- @treturn number height
function Area.rectangle(area)
return area.left_top.x, area.left_top.y, Area.width(area), Area.height(area)
end
--- The width of the area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
function Area.width(area)
return abs(area.left_top.x - area.right_bottom.x)
end
--- The height of an area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
function Area.height(area)
return abs(area.left_top.y - area.right_bottom.y)
end
--- The dimensions of an area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
-- @treturn number height
function Area.dimensions(area)
return Area.width(area), Area.height(area)
end
--- The Perimiter of an area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
function Area.perimeter(area)
return (Area.width(area) + Area.height(area)) * 2
end
--- Returns true if two areas are the same.
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean true if areas are the same
function Area.equals(area1, area2)
if not (area1 and area2) then return false end
local ori = area1.orientation or 0 == area2.orientation or 0
return ori and area1.left_top == area2.left_top and area1.right_bottom == area2.right_bottom
end
--- Is area1 smaller in size than area2
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean is area1 less than area2 in size
function Area.less_than(area1, area2)
if type(area1) == 'number' then
return area1 < Area.size(area2)
elseif type(area2) == 'number' then
return Area.size(area1) < area2
else
return Area.size(area1) < Area.size(area2)
end
end
--- Is area1 smaller or equal in size to area2.
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean is area1 less than or equal to area2 in size
-- @local
function Area.less_than_eq(area1, area2)
if type(area1) == 'number' then
return area1 <= Area.size(area2)
elseif type(area2) == 'number' then
return Area.size(area1) <= area2
else
return Area.size(area1) <= Area.size(area2)
end
end
--- Does either area overlap/collide with the other area.
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean
function Area.collides(area1, area2)
local x1, y1 = Position.unpack(area1.left_top)
local _, w1, h1 = Area.size(area1)
local x2, y2 = Position.unpack(area2.left_top)
local _, w2, h2 = Area.size(area2)
return not ((x1 > x2 + w2) or (x1 > y2 + h2) or (x2 > x1 + w1) or (y2 > y1 + h1))
end
--- Are the passed positions all located in an area.
-- @tparam Concepts.BoundingBox area the search area
-- @tparam array positions array of Concepts.Position
-- @treturn boolean true if the positions are located in the area
function Area.contains_positions(area, positions)
for _, pos in pairs(positions) do if not Position.inside(pos, area) then return false end end
return true
end
--- Are all passed areas completly inside an area.
-- @tparam Concepts.BoundingBox area
-- @tparam array areas array of Concepts.BoundingBox
-- @treturn boolean
function Area.contains_areas(area, areas)
for _, inner in pairs(areas) do
if not Area.contains_positions(area, { Area.unpack_positions(inner) }) then return false end
end
return true
end
--- Do all passed areas collide with an area.
-- @tparam Concepts.BoundingBox area
-- @tparam array areas array of Concepts.BoundingBox
-- @treturn boolean
function Area.collides_areas(area, areas)
for _, inner in pairs(areas) do if not Area.collides(area, inner) then return false end end
return true
end
--- Area Iterators
-- @section Area Iterators
--- Iterates an area.
-- @usage
-- local area = {{0, -5}, {3, -3}}
-- for x,y in Area.iterate(area) do
-- -- return x, y values
-- end
-- for position in Area.iterate(area, true) do
-- -- returns a position object
-- end
-- -- Iterates from left_top.x to right_bottom.x then goes down y until right_bottom.y
-- @tparam Concepts.BoundingBox area the area to iterate
-- @tparam[opt=false] boolean as_position return a position object
-- @tparam[opt=false] boolean inside only return values that contain the areas tiles
-- @tparam[opt=1] number step size to increment
-- @treturn function an iterator
function Area.iterate(area, as_position, inside, step)
step = step or 1
local x, y = area.left_top.x, area.left_top.y
local max_x = area.right_bottom.x - (inside and 0.001 or 0)
local max_y = area.right_bottom.y - (inside and 0.001 or 0)
local first = true
local function iterator()
if first then
first = false
elseif x <= max_x and x + step <= max_x then
x = x + step
elseif y <= max_y and y + step <= max_y then
x = area.left_top.x
y = y + step
else
return
end
return (as_position and Position.construct_xy(x, y)) or x, (not as_position and y) or nil
end
return iterator
end
--- Iterates the given area in a spiral as depicted below, from innermost to the outermost location.
-- <p>![](http://i.imgur.com/EwfO0Es.png)
-- @usage for x, y in Area.spiral_iterate({{-2, -1}, {2, 1}}) do
-- print('(' .. x .. ', ' .. y .. ')')
-- end
-- prints: (0, 0) (1, 0) (1, 1) (0, 1) (-1, 1) (-1, 0) (-1, -1) (0, -1) (1, -1) (2, -1) (2, 0) (2, 1) (-2, 1) (-2, 0) (-2, -1)
-- @tparam Concepts.BoundingBox area the area on which to perform a spiral iteration
-- @tparam boolean as_position return a position object instead of x, y
-- @treturn function an iterator
function Area.spiral_iterate(area, as_position)
local rx = area.right_bottom.x - area.left_top.x + 1
local ry = area.right_bottom.y - area.left_top.y + 1
local half_x = floor(rx / 2)
local half_y = floor(ry / 2)
local center_x = area.left_top.x + half_x
local center_y = area.left_top.y + half_y
local size = max(rx, ry) ^ 2
local x, y, dx, dy = 0, 0, 0, -1
local positions = {}
local index = 1
for _ = 1, size do
if -(half_x) <= x and x <= half_x and -(half_y) <= y and y <= half_y then
positions[#positions + 1] = { x = x, y = y }
end
if x == y or (x < 0 and x == -y) or (x > 0 and x == 1 - y) then
local temp = dx
dx = -(dy)
dy = temp
end
x = x + dx
y = y + dy
end
local function iterator()
if index > #positions then return end
local pos = positions[index]
index = index + 1
pos.x = pos.x + center_x
pos.y = pos.y + center_y
return (as_position and Position.load(pos)) or pos.x, (not as_position and pos.y) or nil
end
return iterator, area, 0
end
--- Area Arrays
-- @section Area Arrays
function Area.positions(area, inside, step)
local positions = {}
for pos in Area.iterate(area, true, inside, step) do positions[#positions + 1] = pos end
return positions
end
--- Metamethods
-- @section Metamethods
local function __add(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top + area2.left_top
area1.right_bottom = area1.right_bottom + area2.right_bottom
return area1
end
local function __sub(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top - area2.left_top
area1.right_bottom = area1.right_bottom - area2.right_bottom
return area1
end
local function __mul(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top * area2.left_top
area1.right_bottom = area1.right_bottom * area2.right_bottom
return area1
end
local function __div(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top / area2.left_top
area1.right_bottom = area1.right_bottom / area2.right_bottom
return area1
end
local function __mod(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top % area2.left_top
area1.right_bottom = area1.right_bottom % area2.right_bottom
return area1
end
local function __unm(area)
---@diagnostic disable: assign-type-mismatch
area = Area.new(area)
area.left_top = -area.left_top
area.right_bottom = -area.right_bottom
---@diagnostic enable: assign-type-mismatch
return area
end
--- Area tables are returned with these Metamethods attached.
-- @table Metamethods
metatable = {
__class = 'area',
__index = Area, -- If key is not found see if there is one available in the Area module.
__tostring = Area.to_string, -- Will print a string representation of the area.
__concat = _ENV.concat, -- calls tostring on both sides of concat.
__add = __add, -- Will adjust if RHS is vector/position, add offset if RHS is number/area
__sub = __sub, -- Will adjust if RHS is vector/position, sub offset if RHS is number/area
__mul = __mul,
__div = __div,
__mod = __mod,
__unm = __unm,
__eq = Area.equals, -- Is area1 the same as area2.
__lt = Area.less_than, -- Is the size of area1 less than number/area2.
__le = Area.less_than_eq, -- Is the size of area1 less than or equal to number/area2.
__len = Area.size, -- The size of the area.
__call = Area.new, -- Return a new copy.
__debugline = [[<Area>{[}left_top={left_top},right_bottom={right_bottom}{]}]]
}
return Area

View File

@@ -0,0 +1,85 @@
--- For working with chunks.
-- A chunk represents a 32 tile<sup>2</sup> on a surface in Factorio.
-- @module Area.Chunk
-- @usage local Chunk = require('__stdlib__/stdlib/area/chunk')
-- @see Concepts.ChunkPosition
local Chunk = {
__class = 'Chunk',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Chunk, Chunk)
local Game = require('__stdlib__/stdlib/game')
local Position = require('__stdlib__/stdlib/area/position')
local AREA_PATH = '__stdlib__/stdlib/area/area'
Chunk.__call = Position.__call
--- Gets the chunk position of a chunk where the specified position resides.
-- @function Chunk.from_position
-- @see Area.Position.to_chunk_position
Chunk.from_position = Position.to_chunk_position
--- Gets the top_left position from a chunk position.
-- @function Chunk.to_position
-- @see Area.Position.from_chunk_position
Chunk.to_position = Position.from_chunk_position
--Chunk.to_center_position
--Chunk.to_center_tile_position
-- Hackish function, Factorio lua doesn't allow require inside functions because...
local function load_area(area)
local Area = package.loaded[AREA_PATH]
if not Area then
local log = log or function(_msg_) end
log('WARNING: Area for Position not found in package.loaded')
end
return Area and Area.load(area) or area
end
--- Gets the area of a chunk from the specified chunk position.
-- @tparam Concepts.ChunkPosition pos the chunk position
-- @treturn Concepts.BoundingBox the chunk's area
function Chunk.to_area(pos)
local left_top = Chunk.to_position(pos)
local right_bottom = Position.add(left_top, 32, 32)
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Gets the user data that is associated with a chunk.
-- The user data is stored in the global object and it persists between loads.
-- @tparam LuaSurface surface the surface on which the user data is looked up
-- @tparam Concepts.ChunkPosition chunk_pos the chunk position on which the user data is looked up
-- @tparam[opt] Mixed default_value the user data to set for the chunk and returned if the chunk had no user data
-- @treturn ?|nil|Mixed the user data **OR** *nil* if it does not exist for the chunk and if no default_value was set
function Chunk.get_data(surface, chunk_pos, default_value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position(chunk_pos):to_key()
return Game.get_or_set_data('_chunk_data', surface.index, key, false, default_value)
end
Chunk.get = Chunk.get_data
--- Associates the user data to a chunk.
-- The user data will be stored in the global object and it will persist between loads.
-- @tparam LuaSurface surface the surface on which the user data will reside
-- @tparam Concepts.ChunkPosition chunk_pos the chunk position to associate with the user data
-- @tparam ?|nil|Mixed value the user data to set **OR** *nil* to erase the existing user data for the chunk
-- @treturn ?|nil|Mixed the previous user data associated with the chunk **OR** *nil* if the chunk had no previous user data
function Chunk.set_data(surface, chunk_pos, value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position(chunk_pos):to_key()
return Game.get_or_set_data('_chunk_data', surface.index, key, true, value)
end
Chunk.set = Chunk.set_data
return Chunk

View File

@@ -0,0 +1,108 @@
--- Functions for working with directions and orientations.
-- @module Area.Direction
-- @usage local Direction = require('__stdlib__/stdlib/area/direction')
-- @see defines.direction
local Direction = {
__class = 'Direction',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Direction, Direction)
--- defines.direction.north
Direction.north = defines.direction.north
--- defines.direction.east
Direction.east = defines.direction.east
--- defines.direction.west
Direction.west = defines.direction.west
--- defines.direction.south
Direction.south = defines.direction.south
--- defines.direction.northeast
Direction.northeast = defines.direction.northeast
--- defines.direction.northwest
Direction.northwest = defines.direction.northwest
--- defines.direction.southeast
Direction.southeast = defines.direction.southeast
--- defines.direction.southwest
Direction.southwest = defines.direction.southwest
--- Returns the opposite direction
-- @tparam defines.direction direction the direction
-- @treturn defines.direction the opposite direction
function Direction.opposite(direction)
return (direction + 4) % 8
end
--- Returns the next direction.
--> For entities that only support two directions, see @{opposite}.
-- @tparam defines.direction direction the starting direction
-- @tparam[opt=false] boolean eight_way true to get the next direction in 8-way (note: not many prototypes support 8-way)
-- @treturn defines.direction the next direction
function Direction.next(direction, eight_way)
return (direction + (eight_way and 1 or 2)) % 8
end
--- Returns the previous direction.
--> For entities that only support two directions, see @{opposite}.
-- @tparam defines.direction direction the starting direction
-- @tparam[opt=false] boolean eight_way true to get the previous direction in 8-way (note: not many prototypes support 8-way)
-- @treturn defines.direction the next direction
function Direction.previous(direction, eight_way)
return (direction + (eight_way and -1 or -2)) % 8
end
--- Returns an orientation from a direction.
-- @tparam defines.direction direction
-- @treturn float
function Direction.to_orientation(direction)
return direction / 8
end
--- Returns a vector from a direction.
-- @tparam defines.direction direction
-- @tparam[opt = 1] number distance
-- @treturn Position
function Direction.to_vector(direction, distance)
distance = distance or 1
local x, y = 0, 0
if direction == Direction.north then
y = y - distance
elseif direction == Direction.northeast then
x, y = x + distance, y - distance
elseif direction == Direction.east then
x = x + distance
elseif direction == Direction.southeast then
x, y = x + distance, y + distance
elseif direction == Direction.south then
y = y + distance
elseif direction == Direction.southwest then
x, y = x - distance, y + distance
elseif direction == Direction.west then
x = x - distance
elseif direction == Direction.northwest then
x, y = x - distance, y - distance
end
return { x = x, y = y }
end
-- Deprecated
do
local Orientation = require('__stdlib__/stdlib/area/orientation')
Direction.opposite_direction = Direction.opposite
Direction.direction_to_orientation = Direction.to_orientation
function Direction.orientation_to_4way(orientation)
return Orientation.to_direction(orientation)
end
function Direction.orientation_to_8way(orientation)
return Orientation.to_direction(orientation, true)
end
function Direction.next_direction(direction, reverse, eight_way)
return (direction + (eight_way and ((reverse and -1) or 1) or ((reverse and -2) or 2))) % 8
end
end
return Direction

View File

@@ -0,0 +1,55 @@
--- Functions for working with orientations.
-- @module Area.Orientation
-- @usage local Orientation = require('__stdlib__/stdlib/area/orientation')
local Orientation = {
__class = 'Orientation',
__index = require('__stdlib__/stdlib/core'),
}
setmetatable(Orientation, Orientation)
--- north orientation
Orientation.north = defines.direction.north / 8
--- east orientation
Orientation.east = defines.direction.east / 8
--- west orientation
Orientation.west = defines.direction.west / 8
--- south orientation
Orientation.south = defines.direction.south / 8
--- northeast orientation
Orientation.northeast = defines.direction.northeast / 8
--- northwest orientation
Orientation.northwest = defines.direction.northwest / 8
--- southeast orientation
Orientation.southeast = defines.direction.southeast / 8
--- southwest orientation
Orientation.southwest = defines.direction.southwest / 8
local floor = math.floor
--- Returns a 4way or 8way direction from an orientation.
-- @tparam float orientation
-- @tparam[opt=false] boolean eight_way
-- @treturn defines.direction
function Orientation.to_direction(orientation, eight_way)
local ways = eight_way and 8 or 4
local mod = eight_way and 1 or 2
return floor(orientation * ways + 0.5) % ways * mod
end
--- Returns the opposite orientation.
-- @tparam float orientation
-- @treturn float the opposite orientation
function Orientation.opposite(orientation)
return (orientation + 0.5) % 1
end
--- Add two orientations together.
-- @tparam float orientation1
-- @tparam float orientation2
-- @treturn float the orientations added together
function Orientation.add(orientation1, orientation2)
return (orientation1 + orientation2) % 1
end
return Orientation

View File

@@ -0,0 +1,947 @@
--- Tools for working with `<x,y>` coordinates.
-- @module Area.Position
-- @usage local Position = require('__stdlib__/stdlib/area/position')
-- @see Area.Area
-- @see Concepts.Position
-- @see defines.direction
local Position = { __class = 'Position', __index = require('__stdlib__/stdlib/core') }
setmetatable(Position, Position)
local Direction = require('__stdlib__/stdlib/area/direction')
local Orientation = require('__stdlib__/stdlib/area/orientation')
local string = require('__stdlib__/stdlib/utils/string')
local math = require('__stdlib__/stdlib/utils/math')
local floor, abs, atan2, round_to, round = math.floor, math.abs, math.atan2, math.round_to, math.round
local cos, sin, ceil, sqrt, pi, random = math.cos, math.sin, math.ceil, math.sqrt, math.pi, math.random
local deg, acos, max, min, is_number = math.deg, math.acos, math.max, math.min, math.is_number
local split = string.split
local directions = defines.direction
local AREA_PATH = '__stdlib__/stdlib/area/area'
local EPSILON = 1.19e-07
local metatable
--- Constructor Methods
-- @section Constructors
Position.__call = function(_, ...)
local type = type((...))
if type == 'table' then
local t = (...)
if t.x and t.y then
return Position.load(...)
else
return Position.new(...)
end
elseif type == 'string' then
return Position.from_string(...)
else
return Position.construct(...)
end
end
local function new(x, y)
return setmetatable({ x = x, y = y }, metatable)
end
--- Returns a correctly formated position object.
-- @usage Position.new({0, 0}) -- returns {x = 0, y = 0}
-- @tparam Concepts.Position pos the position table or array to convert
-- @treturn Concepts.Position
function Position.new(pos)
return new(pos.x or pos[1] or 0, pos.y or pos[2] or 0)
end
--- Creates a table representing the position from x and y.
-- @tparam number x x-position
-- @tparam number y y-position
-- @treturn Concepts.Position
function Position.construct(...)
-- was self was passed as first argument?
local args = type((...)) == 'table' and { select(2, ...) } or { select(1, ...) }
return new(args[1] or 0, args[2] or args[1] or 0)
end
function Position.construct_xy(x, y)
return new(x, y)
end
--- Update a position in place without returning a new position.
-- @tparam Concepts.Position pos
-- @tparam number x
-- @tparam number y
-- @return Concepts.Position the passed position updated.
function Position.update(pos, x, y)
pos.x, pos.y = x, y
return pos
end
--- Load the metatable into the passed position without creating a new one.
-- Always assumes a valid position is passed
-- @tparam Concepts.Position pos the position to set the metatable onto
-- @treturn Concepts.Position the position with metatable attached
function Position.load(pos)
return setmetatable(pos, metatable)
end
--- Converts a position string to a position.
-- @tparam string pos_string the position to convert
-- @treturn Concepts.Position
function Position.from_string(pos_string)
return Position(load('return ' .. pos_string)())
end
--- Converts a string key position to a position.
-- @tparam string pos_string the position to convert
-- @treturn Concepts.Position
function Position.from_key(pos_string)
local tab = split(pos_string, ',', false, tonumber)
return new(tab[1], tab[2])
end
--- Gets the left top tile position of a chunk from the chunk position.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.from_chunk_position(pos)
local x, y = (floor(pos.x) * 32), (floor(pos.y) * 32)
return new(x, y)
end
--- Convert position from pixels
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position pos
function Position.from_pixels(pos)
local x = pos.x / 32
local y = pos.y / 32
return new(x, y)
end
--- Position Methods
-- @section Methods
--- Addition of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values.
-- @treturn Concepts.Position pos1 with pos2 added
function Position.add(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x + pos2.x, pos1.y + pos2.y)
end
--- Subtraction of two positions..
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 with pos2 subtracted
function Position.subtract(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x - pos2.x, pos1.y - pos2.y)
end
--- Multiplication of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 multiplied by pos2
function Position.multiply(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x * pos2.x, pos1.y * pos2.y)
end
--- Division of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 divided by pos2
function Position.divide(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x / pos2.x, pos1.y / pos2.y)
end
--- Modulo of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 modulo pos2
function Position.mod(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x % pos2.x, pos1.y % pos2.y)
end
--- Return the closest position to the first position.
-- @tparam Concepts.Positions pos1 The position to find the closest too
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position
function Position.closest(pos1, positions)
local x, y = pos1.x, pos1.y
local closest = math.MAXINT32
for _, pos in pairs(positions) do
local distance = Position.distance(pos1, pos)
if distance < closest then
x, y = pos.x, pos.y
closest = distance
end
end
return new(x, y)
end
--- Return the farthest position from the first position.
-- @tparam Concepts.Positions pos1 The position to find the farthest from
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position
function Position.farthest(pos1, positions)
local x, y = pos1.x, pos1.y
local closest = 0
for _, pos in pairs(positions) do
local distance = Position.distance(pos1, pos)
if distance > closest then
x, y = pos.x, pos.y
closest = distance
end
end
return new(x, y)
end
--- The middle of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn Concepts.Position pos1 the middle of two positions
function Position.between(pos1, pos2)
return new((pos1.x + pos2.x) / 2, (pos1.y + pos2.y) / 2)
end
--- The projection point of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn Concepts.Position pos1 projected
function Position.projection(pos1, pos2)
local s = (pos1.x * pos2.x + pos1.y * pos2.y) / (pos2.x * pos2.x + pos2.y * pos2.y)
return new(s * pos2.x, s * pos2.y)
end
--- The reflection point or two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn Concepts.Position pos1 reflected
function Position.reflection(pos1, pos2)
local s = 2 * (pos1.x * pos2.x + pos1.y * pos2.y) / (pos2.x * pos2.x + pos2.y * pos2.y)
return new(s * pos2.x - pos1.x, s * pos2.y - pos1.y)
end
--- Stores the position for recall later, not deterministic.
-- Only the last position stored is saved.
-- @tparam Concepts.Position pos
function Position.store(pos)
rawset(getmetatable(pos), '_saved', pos)
return pos
end
--- Recalls the stored position.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the stored position
function Position.recall(pos)
return rawget(getmetatable(pos), '_saved')
end
--- Normalizes a position by rounding it to 2 decimal places.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position a new normalized position
function Position.normalize(pos)
return new(round_to(pos.x, 2), round_to(pos.y, 2))
end
--- Abs x, y values
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.abs(pos)
return new(abs(pos.x), abs(pos.y))
end
--- Ceil x, y values.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.ceil(pos)
return new(ceil(pos.x), ceil(pos.y))
end
--- Floor x, y values.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.floor(pos)
return new(floor(pos.x), floor(pos.y))
end
local function pos_center(pos)
local x, y
local ceil_x = ceil(pos.x)
local ceil_y = ceil(pos.y)
x = pos.x >= 0 and floor(pos.x) + 0.5 or (ceil_x == pos.x and ceil_x + 0.5 or ceil_x - 0.5)
y = pos.y >= 0 and floor(pos.y) + 0.5 or (ceil_y == pos.y and ceil_y + 0.5 or ceil_y - 0.5)
return x, y
end
--- The center position of the tile where the given position resides.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position A new position at the center of the tile
function Position.center(pos)
return new(pos_center(pos))
end
--- Rounds a positions points to the closest integer.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position A new position rounded
function Position.round(pos)
return new(round(pos.x), round(pos.y))
end
--- Perpendicular position.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position pos
function Position.perpendicular(pos)
return new(-pos.y, pos.x)
end
--- Swap the x and y coordinates.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position A new position with x and y swapped
function Position.swap(pos)
return new(pos.y, pos.x)
end
--- Flip the signs of the position.
-- @tparam Concepts.Position pos
-- @return Concepts.Position A new position with flipped signs
function Position.flip(pos)
return new(-pos.x, -pos.y)
end
Position.unary = Position.flip
--- Flip the x sign.
-- @tparam Concepts.Position pos
-- @return Concepts.Position A new position with flipped sign on the x
function Position.flip_x(pos)
return new(-pos.x, pos.y)
end
--- Flip the y sign.
-- @tparam Concepts.Position pos
-- @return Concepts.Position A new position with flipped sign on the y
function Position.flip_y(pos)
return new(pos.x, -pos.y)
end
--- Lerp position of pos1 and pos2.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @tparam float alpha 0-1 how close to get to position 2
-- @treturn Concepts.Position the lerped position
function Position.lerp(pos1, pos2, alpha)
local x = pos1.x + (pos2.x - pos1.x) * alpha
local y = pos1.y + (pos2.y - pos1.y) * alpha
return new(x, y)
end
--- Trim the position to a length.
-- @tparam Concepts.Position pos
-- @tparam number max_len
function Position.trim(pos, max_len)
local s = max_len * max_len / (pos.x * pos.x + pos.y * pos.y)
s = (s > 1 and 1) or sqrt(s)
return new(pos.x * s, pos.y * s)
end
--- Returns the position along line between source and target, at the distance from target.
-- @tparam Concepts.Position pos1 where the line starts and extends from.
-- @tparam Concepts.Position pos2 where the line ends and is offset back from.
-- @tparam number distance_from_pos2 backwards from pos1 for the new position.
-- @treturn Concepts.Position a point along line between source and target, at requested offset back from target.
function Position.offset_along_line(pos1, pos2, distance_from_pos2)
distance_from_pos2 = distance_from_pos2 or 0
local angle = Position.atan2(pos1, pos2)
local veclength = Position.distance(pos1, pos2) - distance_from_pos2
-- From source_position, project the point along the vector at angle, and veclength
local x = pos1.x + round_to(sin(angle) * veclength, 2)
local y = pos1.y + round_to(cos(angle) * veclength, 2)
return new(x, y)
end
--- Translates a position in the given direction.
-- @tparam Concepts.Position pos the position to translate
-- @tparam defines.direction direction the direction of translation
-- @tparam number distance distance of the translation
-- @treturn Concepts.Position a new translated position
function Position.translate(pos, direction, distance)
direction = direction or 0
distance = distance or 1
return Position.add(pos, Direction.to_vector(direction, distance))
end
--- Return a random offset of a position.
-- @tparam Concepts.Position pos the position to randomize
-- @tparam[opt=0] number minimum the minimum amount to offset
-- @tparam[opt=1] number maximum the maximum amount to offset
-- @tparam[opt=false] boolean random_tile randomize the location on the tile
-- @treturn Concepts.Position a new random offset position
function Position.random(pos, minimum, maximum, random_tile)
local rand_x = random(minimum or 0, maximum or 1)
local rand_y = random(minimum or 0, maximum or 1)
local x = pos.x + (random() >= .5 and -rand_x or rand_x) + (random_tile and random() or 0)
local y = pos.y + (random() >= .5 and -rand_y or rand_y) + (random_tile and random() or 0)
return new(x, y)
end
local function get_array(...)
local array = select(2, ...)
if array then
table.insert(array, (...))
else
array = (...)
end
return array
end
--- Return the average position of the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.average(...)
local positions = get_array(...)
local avg = new(0, 0)
for _, pos in ipairs(positions) do Position.add(avg, pos) end
return Position.divide(avg, #positions)
end
--- Return the minimum position of the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.min(...)
local positions = get_array(...)
local x, y
local len = math.MAXINT32
for _, pos in pairs(positions) do
local cur_len = Position.len(pos)
if cur_len < len then
len = cur_len
x, y = pos.x, pos.y
end
end
return new(x, y)
end
--- Return the maximum position of the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.max(...)
local positions = get_array(...)
local x, y
local len = -math.MAXINT32
for _, pos in pairs(positions) do
local cur_len = Position.len(pos)
if cur_len > len then
len = cur_len
x, y = pos.x, pos.y
end
end
return new(x, y)
end
--- Return a position created from the smallest x, y values in the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.min_xy(...)
local positions = get_array(...)
local x, y = positions[1].x, positions[1].y
for _, pos in pairs(positions) do
x = min(x, pos.x)
y = min(y, pos.y)
end
return new(x, y)
end
--- Return a position created from the largest x, y values in the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.max_xy(...)
local positions = get_array(...)
local x, y = positions[1].x, positions[1].y
for _, pos in pairs(positions) do
x = max(x, pos.x)
y = max(y, pos.y)
end
return new(x, y)
end
--- The intersection of 4 positions.
-- @treturn Concepts.Position a new position
function Position.intersection(pos1_start, pos1_end, pos2_start, pos2_end)
local d = (pos1_start.x - pos1_end.x) * (pos2_start.y - pos2_end.y) - (pos1_start.y - pos1_end.y) *
(pos2_start.x - pos2_end.x)
local a = pos1_start.x * pos1_end.y - pos1_start.y * pos1_end.x
local b = pos2_start.x * pos2_end.y - pos2_start.y * pos2_end.x
local x = (a * (pos2_start.x - pos2_end.x) - (pos1_start.x - pos1_end.x) * b) / d
local y = (a * (pos2_start.y - pos2_end.y) - (pos1_start.y - pos1_end.y) * b) / d
return is_number(x) and is_number(y) and new(x, y) or pos1_start
end
--- Position Mutate Methods
-- @section Mutate Methods
--- Normalizes a position by rounding it to 2 decimal places.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the normalized position mutated
function Position.normalized(pos)
pos.x, pos.y = round_to(pos.x, 2), round_to(pos.y, 2)
return pos
end
--- Abs x, y values
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the absolute position mutated
function Position.absed(pos)
pos.x, pos.y = abs(pos.x), abs(pos.y)
return pos
end
--- Ceil x, y values in place.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the ceiled position mutated
function Position.ceiled(pos)
pos.x, pos.y = ceil(pos.x), ceil(pos.y)
return pos
end
--- Floor x, y values.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the floored position mutated
function Position.floored(pos)
pos.x, pos.y = floor(pos.x), floor(pos.y)
return pos
end
--- The center position of the tile where the given position resides.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the centered position mutated
function Position.centered(pos)
pos.x, pos.y = pos_center(pos)
return pos
end
--- Rounds a positions points to the closest integer.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the rounded position mutated
function Position.rounded(pos)
pos.x, pos.y = round(pos.x), round(pos.y)
return pos
end
--- Swap the x and y coordinates.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the swapped position mutated
function Position.swapped(pos)
pos.x, pos.y = pos.y, pos.x
return pos
end
--- Flip the signs of the position.
-- @tparam Concepts.Position pos
-- @return Concepts.Position the flipped position mutated
function Position.flipped(pos)
pos.x, pos.y = -pos.x, -pos.y
return pos
end
--- Position Conversion Methods
-- @section Position Conversion Methods
-- Test Comment
--- Convert to pixels from position
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position pos
function Position.to_pixels(pos)
local x = pos.x * 32
local y = pos.y * 32
return new(x, y)
end
--- Gets the chunk position of a chunk where the specified position resides.
-- @tparam Concepts.Position pos a position residing somewhere in a chunk
-- @treturn Concepts.ChunkPosition a new chunk position
-- @usage local chunk_x = Position.chunk_position(pos).x
function Position.to_chunk_position(pos)
local x, y = floor(pos.x / 32), floor(pos.y / 32)
return new(x, y)
end
--- Area Conversion Methods
-- @section Area Conversion Methods
-- Hackish function, Factorio lua doesn't allow require inside functions because...
local function load_area(area)
local Area = package.loaded[AREA_PATH]
if not Area then
local log = log or function(_msg_)
end
log('WARNING: Area for Position not found in package.loaded')
end
return Area and Area.load(area) or area
end
--- Expands a position to a square area.
-- @tparam Concepts.Position pos the position to expand into an area
-- @tparam number radius half of the side length of the area
-- @treturn Concepts.BoundingBox the area
function Position.expand_to_area(pos, radius)
radius = radius or 1
local left_top = { x = pos.x - radius, y = pos.y - radius }
local right_bottom = { x = pos.x + radius, y = pos.y + radius }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Expands a position into an area by setting pos to left_top.
-- @tparam Concepts.Position pos
-- @tparam number width
-- @tparam number height
-- @treturn Concepts.BoundingBox
function Position.to_area(pos, width, height)
width = width or 0
height = height or width
local left_top = { x = pos.x, y = pos.y }
local right_bottom = { x = pos.x + width, y = pos.y + height }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Converts a tile position to the @{Concepts.BoundingBox|area} of the tile it is in.
-- @tparam LuaTile.position pos the tile position
-- @treturn Concepts.BoundingBox the area of the tile
function Position.to_tile_area(pos)
local x, y = floor(pos.x), floor(pos.y)
local left_top = { x = x, y = y }
local right_bottom = { x = x + 1, y = y + 1 }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Get the chunk area the specified position is in.
-- @tparam Concepts.Position pos
-- @treturn Concepts.BoundingBox
function Position.to_chunk_area(pos)
local left_top = { x = floor(pos.x / 32) * 32, y = floor(pos.y / 32) * 32 }
local right_bottom = { x = left_top.x + 32, y = left_top.y + 32 }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Get the chunk area for the specified chunk position.
-- @tparam Concepts.ChunkPosition pos
-- @treturn Concepts.BoundingBox The chunks positions area
function Position.chunk_position_to_chunk_area(pos)
local left_top = { x = pos.x * 32, y = pos.y * 32 }
local right_bottom = { left_top.x + 32, left_top.y + 32 }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Position Functions
-- @section Functions
--- Gets the squared length of a position
-- @tparam Concepts.Position pos
-- @treturn number
function Position.len_squared(pos)
return pos.x * pos.x + pos.y * pos.y
end
--- Gets the length of a position
-- @tparam Concepts.Position pos
-- @treturn number
function Position.len(pos)
return (pos.x * pos.x + pos.y * pos.y) ^ 0.5
end
--- Converts a position to a string.
-- @tparam Concepts.Position pos the position to convert
-- @treturn string string representation of the position
function Position.to_string(pos)
return '{x = ' .. pos.x .. ', y = ' .. pos.y .. '}'
end
--- Converts a position to an x, y string.
-- @tparam Concepts.Position pos the position to convert
-- @treturn string
function Position.to_string_xy(pos)
return pos.x .. ', ' .. pos.y
end
--- Converts a position to a string suitable for using as a table index.
-- @tparam Concepts.Position pos the position to convert
-- @treturn string
function Position.to_key(pos)
return pos.x .. ',' .. pos.y
end
--- Unpack a position into a tuple.
-- @tparam Concepts.Position pos the position to unpack
-- @treturn tuple x, y
function Position.unpack(pos)
return pos.x, pos.y
end
--- Packs a position into an array.
-- @tparam Concepts.Position pos the position to pack
-- @treturn array
function Position.pack(pos)
return { pos.x, pos.y }
end
--- Is this position {0, 0}.
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_zero(pos)
return pos.x == 0 and pos.y == 0
end
--- Is a position inside of an area.
-- @tparam Concepts.Position pos The pos to check
-- @tparam Concepts.BoundingBox area The area to check.
-- @treturn boolean Is the position inside of the area.
function Position.inside(pos, area)
local lt = area.left_top
local rb = area.right_bottom
return pos.x >= lt.x and pos.y >= lt.y and pos.x <= rb.x and pos.y <= rb.y
end
--- Is this a simple position. {num, num}
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_simple_position(pos)
return type(pos) == 'table' and type(pos[1]) == 'number' and type(pos[2]) == 'number'
end
--- Is this a complex position. {x = number, y = number}
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_complex_position(pos)
return type(pos) == 'table' and type(pos.x) == 'number' and type(pos.y) == 'number'
end
--- Does the position have the class attached
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_Position(pos)
return getmetatable(pos) == metatable
end
--- Is this any position
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_position(pos)
return Position.is_Position(pos) or Position.is_complex_position(pos) or Position.is_simple_position(pos)
end
--- Return the atan2 of 2 positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.atan2(pos1, pos2)
return atan2(pos2.x - pos1.x, pos2.y - pos1.y)
end
--- The angle between two positions
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.angle(pos1, pos2)
local dist = Position.distance(pos1, pos2)
if dist ~= 0 then
return deg(acos((pos1.x * pos2.x + pos1.y * pos2.y) / dist))
else
return 0
end
end
--- Return the cross product of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.cross(pos1, pos2)
return pos1.x * pos2.y - pos1.y * pos2.x
end
-- Return the dot product of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.dot(pos1, pos2)
return pos1.x * pos2.x + pos1.y * pos2.y
end
--- Tests whether or not the two given positions are equal.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn boolean true if positions are equal
function Position.equals(pos1, pos2)
if not (pos1 and pos2) then return false end
return abs(pos1.x - pos2.x) < EPSILON and abs(pos1.y - pos2.y) < EPSILON
end
--- Is pos1 less than pos2.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn boolean
function Position.less_than(pos1, pos2)
return Position.len(pos1) < Position.len(pos2)
end
--- Is pos1 less than or equal to pos2.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn boolean
function Position.less_than_eq(pos1, pos2)
return Position.len(pos1) <= Position.len(pos2)
end
--- Calculates the Euclidean distance squared between two positions, useful when sqrt is not needed.
-- @tparam Concepts.Position pos1
-- @tparam[opt] Concepts.Position pos2
-- @treturn number the square of the euclidean distance
function Position.distance_squared(pos1, pos2)
local ax_bx = pos1.x - pos2.x
local ay_by = pos1.y - pos2.y
return ax_bx * ax_bx + ay_by * ay_by
end
--- Calculates the Euclidean distance between two positions.
-- @tparam Concepts.Position pos1
-- @tparam[opt={x=0, y=0}] Concepts.Position pos2
-- @treturn number the euclidean distance
function Position.distance(pos1, pos2)
local ax_bx = pos1.x - pos2.x
local ay_by = pos1.y - pos2.y
return (ax_bx * ax_bx + ay_by * ay_by) ^ 0.5
end
--- Calculates the manhatten distance between two positions.
-- @tparam Concepts.Position pos1
-- @tparam[opt] Concepts.Position pos2 the second position
-- @treturn number the manhatten distance
-- @see https://en.wikipedia.org/wiki/Taxicab_geometry Taxicab geometry (manhatten distance)
function Position.manhattan_distance(pos1, pos2)
return abs(pos2.x - pos1.x) + abs(pos2.y - pos1.y)
end
--- Returms the direction to a position using simple delta comparisons.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn defines.direction
function Position.direction_to(pos1, pos2)
local dx = pos1.x - pos2.x
local dy = pos1.y - pos2.y
if dx ~= 0 then
if dy == 0 then
return dx > 0 and directions.west or directions.east
else
local adx, ady = abs(dx), abs(dy)
if adx > ady then
return dx > 0 and directions.north or directions.south
else
return dy > 0 and directions.west or directions.east
end
end
else
return dy > 0 and directions.north or directions.south
end
end
--- Returns the direction to a position.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @tparam boolean eight_way return the eight way direction
-- @treturn defines.direction
function Position.complex_direction_to(pos1, pos2, eight_way)
return Orientation.to_direction(Position.orientation_to(pos1, pos2), eight_way)
end
function Position.orientation_to(pos1, pos2)
return (1 - (Position.atan2(pos1, pos2) / pi)) / 2
end
--- Increment a position each time it is called.
-- This can be used to increment or even decrement a position quickly.
-- <p>Do not store function closures in the global object; use them in the current tick.
-- @usage
-- local next_pos = Position.increment({0,0})
-- for i = 1, 5 do next_pos(0,1) -- returns {x = 0, y = 1} {x = 0, y = 2} {x = 0, y = 3} {x = 0, y = 4} {x = 0, y = 5}
-- @usage
-- local next_pos = Position.increment({0, 0}, 1)
-- next_pos() -- returns {1, 0}
-- next_pos(0, 5) -- returns {1, 5}
-- next_pos(nil, 5) -- returns {2, 10}
-- @usage
-- local next_pos = Position.increment({0, 0}, 0, 1)
-- surface.create_entity{name = 'flying-text', text = 'text', position = next_pos()}
-- surface.create_entity{name = 'flying-text', text = 'text', position = next_pos()} -- creates two flying text entities 1 tile apart
-- @tparam Concepts.Position pos the position to start with
-- @tparam[opt=0] number inc_x optional increment x by this amount
-- @tparam[opt=0] number inc_y optional increment y by this amount
-- @tparam[opt=false] boolean increment_initial Whether the first use should be incremented
-- @treturn function @{increment_closure} a function closure that returns a new incremented position
function Position.increment(pos, inc_x, inc_y, increment_initial)
local x, y = pos.x, pos.y
inc_x, inc_y = inc_x or 0, inc_y or 0
--- A closure which the @{increment} function returns.
-- @function increment_closure
-- > Do not call this directly and do not store this in the global object.
-- @see increment
-- @tparam[opt=0] number new_inc_x
-- @tparam[opt=0] number new_inc_y
-- @treturn Concepts.Position the incremented position
return function(new_inc_x, new_inc_y)
if increment_initial then
x = x + (new_inc_x or inc_x)
y = y + (new_inc_y or inc_y)
else
x = x
y = y
increment_initial = true
end
return new(x, y)
end
end
--- Metamethods
-- @section Metamethods
--- Position tables are returned with these metamethods attached.
-- Methods that return a position will return a NEW position without modifying the passed positions.
-- @table Metamethods
metatable = {
__class = 'position',
__index = Position, -- If key is not found, see if there is one availble in the Position module.
__add = Position.add, -- Adds two position together. Returns a new position.
__sub = Position.subtract, -- Subtracts one position from another. Returns a new position.
__mul = Position.multiply, -- Multiply 2 positions. Returns a new position.
__div = Position.divide, -- Divide 2 positions. Returns a new position.
__mod = Position.mod, -- Modulo of 2 positions. Returns a new position.
__unm = Position.flip, -- Unary Minus of a position. Returns a new position.
__len = Position.len, -- Length of a single position.
__eq = Position.equals, -- Are two positions at the same spot.
__lt = Position.less_than, -- Is position1 less than position2.
__le = Position.less_than_eq, -- Is position1 less than or equal to position2.
__tostring = Position.to_string, -- Returns a string representation of the position
__concat = _ENV.concat, -- calls tostring on both sides of concact.
__call = Position.new, -- copy the position.
__debugline = [[<Position>{[}x={x},y={y}{]}]]
}
return Position

View File

@@ -0,0 +1,155 @@
--- For working with surfaces.
-- Surfaces are the "domain" of the world.
-- @module Area.Surface
-- @usage local Surface = require('__stdlib__/stdlib/area/surface')
-- @see LuaSurface
local Surface = {
__class = 'Surface',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Surface, Surface)
local Is = require('__stdlib__/stdlib/utils/is')
local Area = require('__stdlib__/stdlib/area/area')
--- Flexible and safe lookup function for surfaces.
-- <ul>
-- <li>May be given a single surface name or an array of surface names in @{string}.
-- <li>May be given a single surface object or an array of surface objects in @{LuaSurface}.
-- <li>May also be given a @{nil}.
-- <li>Returns an array of surface objects of all valid and existing surfaces.
-- <li>Returns an empty array if no surfaces are given or if they are not found.
-- </ul>
-- @tparam ?|nil|string|{string,...}|LuaSurface|{LuaSurface,...} surface the surfaces to look up
-- @treturn {nil|LuaSurface,...} an array of all valid surfaces or nil otherwise
function Surface.lookup(surface)
if not surface then
return {}
end
if type(surface) == 'string' or type(surface) == 'number' then
local lookup = game.surfaces[surface]
if lookup then
return { lookup }
end
return {}
end
if type(surface) == 'table' and surface['__self'] then
return Surface.lookup(surface.name)
end
local results = {}
for _, surface_item in pairs(surface) do
if type(surface_item) == 'string' then
if game.surfaces[surface_item] then
table.insert(results, game.surfaces[surface_item])
end
elseif type(surface_item) == 'table' and surface_item['__self'] then
table.insert(results, surface_item)
end
end
return results
end
--- Given a @{search_criteria|search criteria}, find all entities that match the criteria.
-- <ul>
-- <li>If ***search\_criteria.name*** is not supplied, search for entities with any name.
-- <li>If ***search\_criteria.type*** is not supplied, search for entities of any type.
-- <li>If ***search\_criteria.force*** is not supplied, search for entities owned by any force.
-- <li>If ***search\_criteria.surface*** is not supplied, search for entities on all surfaces.
-- <li>If ***search\_criteria.area*** is not supplied, search the entire specified surface.
-- </ul>
-- @usage
-- surface.find_all_entities({ type = 'unit', surface = 'nauvis', area = {{-1000,20},{-153,2214}})
-- -- returns a list containing all unit entities on the nauvis surface in the given area
-- @param search_criteria (<span class="types">@{search_criteria}</span>) a table used to search for entities
-- @treturn {nil|LuaEntity,...} an array of all entities that matched the criteria **OR** *nil* if there were no matches
function Surface.find_all_entities(search_criteria)
Is.Assert.Table(search_criteria, 'missing search_criteria argument')
if search_criteria.name == nil and search_criteria.type == nil and search_criteria.force == nil and search_criteria.area == nil then
error('Missing search criteria field: name or type or force or area of entity', 2)
end
local surface_list = Surface.lookup(search_criteria.surface)
if search_criteria.surface == nil then
surface_list = game.surfaces
end
local results = {}
for _, surface in pairs(surface_list) do
local entities =
surface.find_entities_filtered
{
area = search_criteria.area,
name = search_criteria.name,
type = search_criteria.type,
force = search_criteria.force
}
for _, entity in pairs(entities) do
table.insert(results, entity)
end
end
return results
end
---
-- This table should be passed into @{find_all_entities} function to find entities that match the criteria.
-- @tfield[opt] string name internal name of an entity &mdash; (example: "locomotive")
-- @tfield[opt] string type type of an entity &mdash; (example: "unit")
-- @tfield[opt] string|LuaForce force the force of an entity &mdash; (examples: "neutral", "enemy")
-- @tfield[opt] ?|nil|string|{string,...}|LuaSurface|{LuaSurface,...} surface the surface to search &mdash; (example: "nauvis")
-- @tfield[opt] Concepts.BoundingBox area the area to search
-- @table search_criteria
--- Gets the area which covers the entirety of a given surface.
-- This function is useful if you wish to compare the total number of chunks against the number of chunks within the entire area of a given surface.
-- @tparam LuaSurface surface the surface for which to get the area
-- @treturn Concepts.BoundingBox the area of a given surface
function Surface.get_surface_bounds(surface)
Is.Assert(surface, 'missing surface value')
local x1, y1, x2, y2 = 0, 0, 0, 0
for chunk in surface.get_chunks() do
if chunk.x < x1 then
x1 = chunk.x
elseif chunk.x > x2 then
x2 = chunk.x
end
if chunk.y < y1 then
y1 = chunk.y
elseif chunk.y > y2 then
y2 = chunk.y
end
end
return Area.construct(x1 * 32, y1 * 32, x2 * 32, y2 * 32)
end
--- Sets the daytime transition thresholds on a given surface
-- @tparam LuaSurface surface the surface for which to set the thresholds
-- @tparam number morning daytime to begin transition from dark to light
-- @tparam number dawn daytime to finish transition from dark to light
-- @tparam number dusk daytime to begin transition from light to dark
-- @tparam number evening daytime to finish transition from light to dark
-- @treturn boolean true if the thresholds were set, false if there was an error
-- @return[opt] the raised error, if any
function Surface.set_daytime_thresholds(surface, morning, dawn, dusk, evening)
Is.Assert.Valid(surface, 'missing surface value')
Is.Assert(Is.Float(morning) and Is.Float(dawn) and Is.float(dusk) and Is.Float(evening), 'paramaters must be floats')
return pcall(
function()
surface.dusk = 0
surface.evening = .0000000001
surface.morning = .0000000002
surface.dawn = dawn
surface.morning = morning
surface.evening = evening
surface.dusk = dusk
end
)
end
return Surface

View File

@@ -0,0 +1,90 @@
--- Tools for working with tiles.
-- A tile represents a 1 unit<sup>2</sup> on a surface in Factorio.
-- @module Area.Tile
-- @usage local Tile = require('__stdlib__/stdlib/area/tile')
-- @see LuaTile
local Tile = {
__class = 'Tile',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Tile, Tile)
local Is = require('__stdlib__/stdlib/utils/is')
local Game = require('__stdlib__/stdlib/game')
local Position = require('__stdlib__/stdlib/area/position')
Tile.__call = Position.__call
--- Get the @{LuaTile.position|tile position} of a tile where the given position resides.
-- @function Tile.from_position
-- @see Area.Position.floor
Tile.from_position = Position.floor
--- Converts a tile position to the @{Concepts.BoundingBox|area} of the tile it is in.
-- @function Tile.to_area
-- @see Area.Position.to_tile_area
Tile.to_area = Position.to_tile_area
--- Creates an array of tile positions for all adjacent tiles (N, E, S, W) **OR** (N, NE, E, SE, S, SW, W, NW) if diagonal is set to true.
-- @tparam LuaSurface surface the surface to examine for adjacent tiles
-- @tparam LuaTile.position position the tile position of the origin tile
-- @tparam[opt=false] boolean diagonal whether to include diagonal tiles
-- @tparam[opt] string tile_name whether to restrict adjacent tiles to a particular tile name (example: "water-tile")
-- @treturn {LuaTile.position,...} an array of tile positions of the tiles that are adjacent to the origin tile
function Tile.adjacent(surface, position, diagonal, tile_name)
Is.Assert(surface, 'missing surface argument')
Is.Assert(position, 'missing position argument')
local offsets = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } }
if diagonal then
offsets = { { 0, 1 }, { 1, 1 }, { 1, 0 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 } }
end
local adjacent_tiles = {}
for _, offset in pairs(offsets) do
local adj_pos = Position.add(position, offset)
if tile_name then
local tile = surface.get_tile(adj_pos.x, adj_pos.y)
if tile and tile.name == tile_name then
table.insert(adjacent_tiles, adj_pos)
end
else
table.insert(adjacent_tiles, adj_pos)
end
end
return adjacent_tiles
end
--- Gets the user data that is associated with a tile.
-- The user data is stored in the global object and it persists between loads.
-- @tparam LuaSurface surface the surface on which the user data is looked up
-- @tparam LuaTile.position tile_pos the tile position on which the user data is looked up
-- @tparam[opt] Mixed default_value the user data to set for the tile and returned if it did not have user data
-- @treturn ?|nil|Mixed the user data **OR** *nil* if it does not exist for the tile and no default_value was set
function Tile.get_data(surface, tile_pos, default_value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position.to_key(Position.floor(tile_pos))
return Game.get_or_set_data('_tile_data', surface.index, key, false, default_value)
end
Tile.get = Tile.get_data
--- Associates the user data to a tile.
-- The user data will be stored in the global object and it will persist between loads.
-- @tparam LuaSurface surface the surface on which the user data will reside
-- @tparam LuaTile.position tile_pos the tile position of a tile that will be associated with the user data
-- @tparam ?|nil|Mixed value the user data to set **OR** *nil* to erase the existing user data for the tile
-- @treturn ?|nil|Mixed the previous user data associated with the tile **OR** *nil* if the tile had no previous user data
function Tile.set_data(surface, tile_pos, value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position.to_key(Position.floor(tile_pos))
return Game.get_or_set_data('_tile_data', surface.index, key, true, value)
end
Tile.set = Tile.set_data
return Tile