mesecraft/mods/WORLD/settlements/bookgen.lua

270 lines
8.9 KiB
Lua

local modpath_default = minetest.get_modpath("default")
if not (minetest.settings:get_bool("settlements_generate_books", true) and modpath_default) then
return
end
-- internationalization boilerplate
local S, NS = settlements.S, settlements.NS
-- values taken from default's craftitems.lua
local max_text_size = 10000
local max_title_size = 80
local short_title_size = 35
local lpp = 14
local generate_book = function(title, owner, text)
local book = ItemStack("default:book_written")
local meta = book:get_meta()
meta:set_string("title", title:sub(1, max_title_size))
meta:set_string("owner", owner)
local short_title = title
-- Don't bother trimming the title if the trailing dots would make it longer
if #short_title > short_title_size + 3 then
short_title = short_title:sub(1, short_title_size) .. "..."
end
meta:set_string("description", S("@1 by @2", short_title, owner))
text = text:sub(1, max_text_size)
text = text:gsub("\r\n", "\n"):gsub("\r", "\n")
meta:set_string("text", text)
meta:set_int("page", 1)
meta:set_int("page_max", math.ceil((#text:gsub("[^\n]", "") + 1) / lpp))
return book
end
----------------------------------------------------------------------------------------------------------------
local half_map_chunk_size = settlements.half_map_chunk_size
local bookshelf_def = minetest.registered_items["default:bookshelf"]
local bookshelf_on_construct
if bookshelf_def then
bookshelf_on_construct = bookshelf_def.on_construct
end
minetest.register_abm({
label = "Settlement book authoring",
nodenames = {"default:bookshelf"},
interval = 60, -- Operation interval in seconds
chance = 1440, -- Chance of triggering `action` per-node per-interval is 1.0 / this value
catch_up = true,
-- If true, catch-up behaviour is enabled: The `chance` value is
-- temporarily reduced when returning to an area to simulate time lost
-- by the area being unattended. Note that the `chance` value can often
-- be reduced to 1.
action = function(pos, node, active_object_count, active_object_count_wider)
local inv = minetest.get_inventory( {type="node", pos=pos} )
-- Can we fit a book?
if not inv or not inv:room_for_item("books", "default:book_written") then
return
end
-- find any settlements within the shelf's mapchunk
-- There's probably only ever going to be one, but might as well do a closeness check to be on the safe side.
local min_edge = vector.subtract(pos, half_map_chunk_size)
local max_edge = vector.add(pos, half_map_chunk_size)
local settlement_list = named_waypoints.get_waypoints_in_area("settlements", min_edge, max_edge)
local closest_settlement
for _, settlement in pairs(settlement_list) do
local target_pos = settlement.pos
if not closest_settlement or vector.distance(pos, target_pos) < vector.distance(pos, closest_settlement.pos) then
closest_settlement = settlement
end
end
if not closest_settlement then
return
end
-- Get the settlement def and, if it generate books, generate one
local data = closest_settlement.data
local town_name = data.name
local town_type = data.settlement_type
local town_def = settlements.registered_settlements[town_type]
if town_def and town_def.generate_book then
local book = town_def.generate_book(closest_settlement.pos, town_name)
if book then
inv:add_item("books", book)
if bookshelf_on_construct then
bookshelf_on_construct(pos) -- this should safely update the bookshelf's infotext without disturbing its contents
end
end
end
end,
})
---------------------------------------------------------------------------
-- Commoditymarket ledgers
if minetest.get_modpath("commoditymarket") then
--{item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
local log_to_string = function(log_entry, market)
local anonymous = market.def.anonymous
local purchaser = log_entry.purchaser
local seller = log_entry.seller
local purchaser_name
if purchaser == seller then
purchaser_name = S("themself")
elseif anonymous then
purchaser_name = S("someone")
else
purchaser_name = purchaser.name
end
local seller_name
if anonymous then
seller_name = S("someone")
else
seller_name = seller.name
end
local itemname = log_entry.item
local item_def = minetest.registered_items[log_entry.item]
if item_def then
itemname = item_def.description:gsub("\n", " ")
end
return S("On day @1 @2 sold @3 @4 to @5 at @6@7 each for a total of @6@8.",
math.ceil(log_entry.timestamp/86400), seller_name, log_entry.quantity, itemname,
purchaser_name, market.def.currency_symbol, log_entry.price, log_entry.quantity*log_entry.price)
end
local get_log_strings = function(market, quantity)
local accounts = market.player_accounts
local all_logs = {}
for player_name, account in pairs(accounts) do
for _, log_entry in pairs(account.log) do
table.insert(all_logs, log_entry)
end
end
if #all_logs == 0 then
return
end
table.sort(all_logs, function(log1, log2) return log1.timestamp > log2.timestamp end)
local start_range = math.max(#all_logs, #all_logs - quantity)
local start_point = math.random(start_range)
local end_point = math.min(start_point+quantity, #all_logs)
local out = {}
local last_timestamp = all_logs[end_point].timestamp
for i = start_point, end_point do
table.insert(out, log_to_string(all_logs[i], market))
end
return out, last_timestamp
end
settlements.generate_ledger = function(market_name, town_name)
local market = commoditymarket.registered_markets[market_name]
if not market then
return
end
local strings, last_timestamp = get_log_strings(market, math.random(5,15))
if not strings then
return
end
strings = table.concat(strings, "\n")
local day = math.ceil(last_timestamp/86400)
local title = S("@1 Ledger on Day @2", market.def.description, day)
local author = S("@1's market clerk", town_name)
return generate_book(title, author, strings)
end
end
--------------------------------------------------------------------------------
-- Travel guides
-- returns {pos, data}
local half_map_chunk_size = settlements.half_map_chunk_size
local get_random_settlement_within_range = function(pos, range_max, range_min)
range_min = range_min or half_map_chunk_size -- If no minimum provided, at least don't look within your own chunk
if range_max <= range_min then
return
end
local min_edge = vector.subtract(pos, range_max)
local max_edge = vector.add(pos, range_max)
local settlement_list = named_waypoints.get_waypoints_in_area("settlements", min_edge, max_edge)
local settlements_within_range = {}
for _, settlement in pairs(settlement_list) do
local distance = vector.distance(pos, settlement.pos)
if distance < range_max and distance > range_min then
table.insert(settlements_within_range, settlement)
end
end
if #settlements_within_range == 0 then
return
end
local target = settlements_within_range[math.random(#settlements_within_range)]
return target
end
local compass_dirs = {
[0] = S("west"),
S("west-southwest"),
S("southwest"),
S("south-southwest"),
S("south"),
S("south-southeast"),
S("southeast"),
S("east-southeast"),
S("east"),
S("east-northeast"),
S("northeast"),
S("north-northeast"),
S("north"),
S("north-northwest"),
S("northwest"),
S("west-northwest"),
S("west"),
}
local increment = 2*math.pi/#compass_dirs -- Divide the circle up into pieces
local reframe = math.pi - increment/2 -- Adjust the angle to run through a range divisible into indexes
local compass_direction = function(p1, p2)
local dir = vector.subtract(p2, p1)
local angle = math.atan2(dir.z, dir.x);
angle = angle + reframe
angle = math.ceil(angle / increment)
return compass_dirs[angle]
end
local get_altitude = function(pos)
if pos.y > 100 then
return S("highlands")
elseif pos.y > 20 then
return S("midlands")
elseif pos.y > 0 then
return S("lowlands")
else
-- TODO: need to update this system once there are underground settlements
return S("waters")
end
end
settlements.generate_travel_guide = function(source_pos, source_name)
local range = math.random(settlements.min_dist_settlements*2, settlements.min_dist_settlements*5)
local target = get_random_settlement_within_range(source_pos, range)
if not target then
return
end
local target_name = target.data.name
local target_type = target.data.settlement_type
local target_desc = S("settlement")
if target_type then
local def = settlements.registered_settlements[target_type]
if def and def.description then
target_desc = def.description
end
end
local title = S("A travel guide to @1", target_name)
local author = S("a resident of @1", source_name)
local dir = compass_direction(source_pos, target.pos)
local distance = vector.distance(source_pos, target.pos)
local kilometers = string.format("%.1f", distance/1000)
local altitude = get_altitude(target.pos)
local text = S("In the @1 @2 kilometers to the @3 of @4 lies the @5 of @6.", altitude, kilometers, dir, source_name, target_desc, target_name)
return generate_book(title, author, text)
end