capitalism_game/mods/capitalism/land/api.lua

468 lines
11 KiB
Lua

--- Introduces land plots, and modifies areas to allow companies to own land.
--
-- This mod current uses areas as a backend, which means that a plot = an area.
--
-- @module land
--- Dictionary of valid types
land.valid_types = { commercial = true, residential = true, industrial = true }
local adt = audit("land")
--- Generates a tree representing land ownership hierarchy for a particular owner.
--
-- Each element returned will have a children property, which will be a table
-- of child elements.
--
-- @tparam [table] list A list of owned areas
-- @owner owner Player name or company name
-- @treturn (table,table) root, area by id
function land.get_area_tree(list, owner)
assert(list == nil or type(list) == "table")
assert(owner == nil or type(owner) == "string")
list = list or areas.areas
list = table.copy(list)
local root = {}
local item_by_id = {}
local pending_by_id = {}
if owner then
for i=1, #list do
if not list[i].marked and list[i].owner == owner then
local ptr = i
while ptr and not list[ptr].marked do
list[ptr].marked = true
ptr = list[ptr].parent
end
end
end
end
for i=1, #list do
local area = list[i]
area.id = i
area.children = {}
if owner == nil or list[i].marked then
if not area.parent then
root[#root + 1] = area
if pending_by_id[i] then
area.children = pending_by_id
pending_by_id[i] = nil
end
elseif item_by_id[area.parent] then
local children = item_by_id[area.parent].children
children[#children + 1] = area
else
pending_by_id[area.parent] = pending_by_id[area.parent] or {}
local pending = pending_by_id[area.parent]
pending[#pending + 1] = area
end
item_by_id[i] = area
end
end
return root, item_by_id
end
--- Gets all plots owned by a particular owner
--
-- @owner owner Player name or company name
-- @treturn [table] List of plots
function land.get_all(owner)
local lands = {}
for id, area in pairs(areas.areas) do
if area.land_type and area.owner == owner then
area.id = id
lands[#lands + 1] = area
end
end
return lands
end
--- Gets the owning plot of a particular area
--
-- There is at most one owning area of a particular position; because
-- plots must not overlap, and any child of a plot most be fully contained.
--
-- This function essential returns the lowest plot in the tree at a particular
-- point
--
-- @pos pos
-- @treturn [table]
function land.get_by_pos(pos)
local areas = areas:getAreasAtPos(pos)
local total = 0
for id, area in pairs(areas) do
total = total + 1
if area.parent and areas[area.parent] then
areas[area.parent] = nil
total = total - 1
end
end
assert(total == 0 or total == 1)
local id, first = next(areas)
first.id = id
return first
end
--- Gets a plot by its area ID (as in the mod area)
--
-- @int id
-- @treturn table plot
function land.get_by_area_id(id)
assert(type(id) == "number")
local area = areas.areas[id]
return area and area.land_type and area
end
--- Get zone for a plot
--
-- Gets the base zone for a particular area
function land.get_zone(area)
assert(type(area) == "table")
local zone = area
while zone do
local next = areas.areas[zone.parent]
if next and next.land_type ~= area.land_type then
return zone
end
zone = next
end
return nil
end
--- Transfers a plot between two owners, after checking relevant permissions.
--
-- @int id
-- @owner newowner
-- @player pname
-- @treturn true
-- @error Error message
function land.transfer(id, newowner, pname)
assert(type(id) == "number")
assert(type(newowner) == "string")
assert(type(pname) == "string")
local area = areas.areas[id]
if not area then
return false, "Unable to find area id=" .. id
end
if not area.parent then
return false, "Unable to transfer root areas"
end
local land_admin = minetest.check_player_privs(pname, { land_admin = true })
local comp = company.get_by_name(area.owner)
if not land_admin then
if comp then
local comp_active = company.get_active(pname)
if not comp_active or comp_active.name ~= comp.name then
return false, "You're not currently acting on behalf of " .. comp.title
end
if not comp:check_perm(pname, "TRANSFER_LAND") then
return false, "Missing permission: TRANSFER_LAND"
end
elseif pname ~= area.owner then
return false, "You don't have access to land owned by " .. area.owner
end
end
if not minetest.player_exists(newowner) and
not company.get_by_name(newowner) then
if newowner:sub(1, 2) == "c:" then
return false, "New owner " .. newowner .. " doesn't exist"
else
return false, "New owner " .. newowner .. " doesn't exist (did you forget 'c:'?)"
end
end
adt:post(pname, comp and comp.name, "Transferred area id=" .. id .. " to " .. newowner)
area.owner = newowner
areas:save()
return true, "Transfered area id=" .. id .. " to " .. newowner
end
--- Whether a user can put a plot up for sale, or change the price when already
-- for sale
--
-- @tparam table area
-- @player pname
-- @treturn true
-- @error Error message
function land.can_set_price(area, pname)
if not area or not area.land_type then
return false, "Unable to sell unowned or unclassified (ie: c/i/r) area"
end
local comp = company.get_by_name(area.owner)
if not comp or not comp:is_government() then
return false, "Only the government is currently able to sell land."
end
if not area.parent then
return false, "Root land is not sellable"
end
local comp_active = pname and company.get_active(pname)
if pname and (not comp_active or comp_active.name ~= comp.name) then
return false, "You do not own this area (do you need to change active company?)"
end
if pname and comp and
not comp:check_perm(pname, "SELL_LAND", { area=area }) then
return false, "You do not have permission to sell this area."
end
return true
end
--- Puts a plot up for sale, or changes the price when already for sale
--
-- @tparam table area
-- @player pname
-- @number price
-- @treturn true
-- @error Error message
function land.set_price(area, pname, price)
assert(type(area) == "table")
assert(pname == nil or type(pname) == "string")
assert(type(price) == "number")
if price <= 0 then
return false, "Price must be greater than 0"
end
local suc, msg = land.can_set_price(area, pname)
if not suc then
return suc, msg
end
if pname then
adt:post(pname, company.get_by_name(area.owner),
"Set price for area id=" .. area.id .. " to " .. price)
end
area.land_sale = price
areas:save()
return true
end
--- Calculates a recommended value for the lot, based on the position and more.
--
-- @tparam table area
-- @treturn number value
function land.calc_value(area)
assert(type(area) == "table")
-- Calculate a weighted metric based on volume of area
local pos1, pos2 = vector.sort(area.pos1, area.pos2)
local size = vector.subtract(pos2, pos1)
local value = 200 * (size.x * size.z * 0.33 * size.y)
-- Find distance to zone center
local zone = land.get_zone(area)
if zone then
local zone_center = zone.spawn_point or zone.pos1
local area_center = vector.divide(vector.add(area.pos1, area.pos2), 2)
local dist = vector.sqdist(zone_center, area_center)
value = value + (zone.land_value or 1000000) * 1 / (dist*0.1 + 2)
end
area.land_value = value
return value
end
--- Whether a user can buy a plot
--
-- @tparam table area
-- @player pname
-- @company comp
-- @treturn true
-- @error Error message
function land.can_buy(area, pname, comp)
assert(type(area) == "table")
assert(type(pname) == "string")
if comp and not comp:check_perm(pname, "BUY_LAND", { area = area }) then
return false, "Missing permission: BUY_LAND"
end
if not area.parent then
return false, "Unable to buy root areas"
end
if area.owner == comp.name then
return false, "You already own this land!"
end
local acc = banking.get_by_owner(comp and comp.name or pname)
if not acc then
return false, "You don't have a bank account"
end
if acc.balance < area.land_sale then
return false, "Insufficient funds"
end
return true
end
--- Buy a plot
--
-- @tparam table area
-- @player pname
-- @treturn true
-- @error Error message
function land.buy(area, pname)
assert(type(area) == "table")
assert(type(pname) == "string")
assert(minetest.player_exists(pname))
local comp = company.get_active(pname)
if not land.can_buy(area, pname, comp) then
return false
end
local account = banking.get_by_owner(comp and comp.name or pname)
local owner_account = banking.get_by_owner(area.owner)
assert(account)
assert(owner_account)
if not banking.transfer(pname, account.owner, owner_account.owner, area.land_sale,
"Purchase of land id=" .. area.id) then
return false
end
adt:post(pname, comp,
"Bought land id=" .. area.id .. " from " .. owner_account.owner)
if minetest.get_node(area.land_postpos).name == "land:for_sale" then
minetest.remove_node(area.land_postpos)
minetest.remove_node(vector.add(area.land_postpos, {x=0,y=1,z=0}))
area.land_postpos = nil
end
area.land_sale = nil
area.owner = comp and comp.name or pname
areas:save()
return true
end
--- Whether a user can teleport to a plot
--
-- @tparam table area
-- @player pname
-- @treturn true
-- @error Error message
function land.can_teleport_to(area, pname)
if type(pname) == "userdata" then
pname = pname:get_player_name()
end
assert(type(area) == "table")
assert(type(pname) == "string")
local comp = company.get_active(pname)
local owner = comp and comp.name or pname
if area.owner ~= owner and not area.land_open then
return false, "Not open and is owned by someone else (owner=" ..
area.owner .. ")"
end
if not area.spawn_point then
return false, "No spawn point"
end
return true
end
--- Teleport a user to a plot
--
-- @tparam table area
-- @param player Player userdata
-- @treturn true
-- @error Error message
function land.teleport_to(area, player)
assert(type(area) == "table")
assert(type(player) == "userdata")
local suc, msg = land.can_teleport_to(area, player:get_player_name())
if suc then
player:set_pos(area.spawn_point)
end
if msg then
minetest.log("warning", msg)
end
return suc, msg
end
--- Whether a user can set the spawn of a plot
--
-- @tparam table area
-- @player pname
-- @treturn true
-- @error Error message
function land.can_set_spawn(area, pname)
if type(pname) == "userdata" then
pname = pname:get_player_name()
end
assert(type(area) == "table")
assert(type(pname) == "string")
local comp = company.get_active(pname)
local owner = comp and comp.name or pname
if area.owner ~= owner and not area.land_open then
return false, "Unable to change spawn of land (" ..
area.name .. " [" .. dump(area.id) .. "]), because it is not " ..
"owned by you (owner=" .. area.owner ..
", you you need to switch companies?)"
end
if comp and not comp:check_perm(pname, "CHANGE_SPAWN", { area = area }) then
return false, "Missing permission: CHANGE_SPAWN"
end
return true
end