bookgen.lua 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. local modpath_default = minetest.get_modpath("default")
  2. if not (minetest.settings:get_bool("settlements_generate_books", true) and modpath_default) then
  3. return
  4. end
  5. -- internationalization boilerplate
  6. local S, NS = settlements.S, settlements.NS
  7. -- values taken from default's craftitems.lua
  8. local max_text_size = 10000
  9. local max_title_size = 80
  10. local short_title_size = 35
  11. local lpp = 14
  12. local generate_book = function(title, owner, text)
  13. local book = ItemStack("default:book_written")
  14. local meta = book:get_meta()
  15. meta:set_string("title", title:sub(1, max_title_size))
  16. meta:set_string("owner", owner)
  17. local short_title = title
  18. -- Don't bother trimming the title if the trailing dots would make it longer
  19. if #short_title > short_title_size + 3 then
  20. short_title = short_title:sub(1, short_title_size) .. "..."
  21. end
  22. meta:set_string("description", S("@1 by @2", short_title, owner))
  23. text = text:sub(1, max_text_size)
  24. text = text:gsub("\r\n", "\n"):gsub("\r", "\n")
  25. meta:set_string("text", text)
  26. meta:set_int("page", 1)
  27. meta:set_int("page_max", math.ceil((#text:gsub("[^\n]", "") + 1) / lpp))
  28. return book
  29. end
  30. ----------------------------------------------------------------------------------------------------------------
  31. local half_map_chunk_size = settlements.half_map_chunk_size
  32. local bookshelf_def = minetest.registered_items["default:bookshelf"]
  33. local bookshelf_on_construct
  34. if bookshelf_def then
  35. bookshelf_on_construct = bookshelf_def.on_construct
  36. end
  37. minetest.register_abm({
  38. label = "Settlement book authoring",
  39. nodenames = {"default:bookshelf"},
  40. interval = 60, -- Operation interval in seconds
  41. chance = 1440, -- Chance of triggering `action` per-node per-interval is 1.0 / this value
  42. catch_up = true,
  43. -- If true, catch-up behaviour is enabled: The `chance` value is
  44. -- temporarily reduced when returning to an area to simulate time lost
  45. -- by the area being unattended. Note that the `chance` value can often
  46. -- be reduced to 1.
  47. action = function(pos, node, active_object_count, active_object_count_wider)
  48. local inv = minetest.get_inventory( {type="node", pos=pos} )
  49. -- Can we fit a book?
  50. if not inv or not inv:room_for_item("books", "default:book_written") then
  51. return
  52. end
  53. -- find any settlements within the shelf's mapchunk
  54. -- There's probably only ever going to be one, but might as well do a closeness check to be on the safe side.
  55. local min_edge = vector.subtract(pos, half_map_chunk_size)
  56. local max_edge = vector.add(pos, half_map_chunk_size)
  57. local settlement_list = named_waypoints.get_waypoints_in_area("settlements", min_edge, max_edge)
  58. local closest_settlement
  59. for _, settlement in pairs(settlement_list) do
  60. local target_pos = settlement.pos
  61. if not closest_settlement or vector.distance(pos, target_pos) < vector.distance(pos, closest_settlement.pos) then
  62. closest_settlement = settlement
  63. end
  64. end
  65. if not closest_settlement then
  66. return
  67. end
  68. -- Get the settlement def and, if it generate books, generate one
  69. local data = closest_settlement.data
  70. local town_name = data.name
  71. local town_type = data.settlement_type
  72. local town_def = settlements.registered_settlements[town_type]
  73. if town_def and town_def.generate_book then
  74. local book = town_def.generate_book(closest_settlement.pos, town_name)
  75. if book then
  76. inv:add_item("books", book)
  77. if bookshelf_on_construct then
  78. bookshelf_on_construct(pos) -- this should safely update the bookshelf's infotext without disturbing its contents
  79. end
  80. end
  81. end
  82. end,
  83. })
  84. ---------------------------------------------------------------------------
  85. -- Commoditymarket ledgers
  86. if minetest.get_modpath("commoditymarket") then
  87. --{item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
  88. local log_to_string = function(log_entry, market)
  89. local anonymous = market.def.anonymous
  90. local purchaser = log_entry.purchaser
  91. local seller = log_entry.seller
  92. local purchaser_name
  93. if purchaser == seller then
  94. purchaser_name = S("themself")
  95. elseif anonymous then
  96. purchaser_name = S("someone")
  97. else
  98. purchaser_name = purchaser.name
  99. end
  100. local seller_name
  101. if anonymous then
  102. seller_name = S("someone")
  103. else
  104. seller_name = seller.name
  105. end
  106. local itemname = log_entry.item
  107. local item_def = minetest.registered_items[log_entry.item]
  108. if item_def then
  109. itemname = item_def.description:gsub("\n", " ")
  110. end
  111. return S("On day @1 @2 sold @3 @4 to @5 at @6@7 each for a total of @6@8.",
  112. math.ceil(log_entry.timestamp/86400), seller_name, log_entry.quantity, itemname,
  113. purchaser_name, market.def.currency_symbol, log_entry.price, log_entry.quantity*log_entry.price)
  114. end
  115. local get_log_strings = function(market, quantity)
  116. local accounts = market.player_accounts
  117. local all_logs = {}
  118. for player_name, account in pairs(accounts) do
  119. for _, log_entry in pairs(account.log) do
  120. table.insert(all_logs, log_entry)
  121. end
  122. end
  123. if #all_logs == 0 then
  124. return
  125. end
  126. table.sort(all_logs, function(log1, log2) return log1.timestamp > log2.timestamp end)
  127. local start_range = math.max(#all_logs, #all_logs - quantity)
  128. local start_point = math.random(start_range)
  129. local end_point = math.min(start_point+quantity, #all_logs)
  130. local out = {}
  131. local last_timestamp = all_logs[end_point].timestamp
  132. for i = start_point, end_point do
  133. table.insert(out, log_to_string(all_logs[i], market))
  134. end
  135. return out, last_timestamp
  136. end
  137. settlements.generate_ledger = function(market_name, town_name)
  138. local market = commoditymarket.registered_markets[market_name]
  139. if not market then
  140. return
  141. end
  142. local strings, last_timestamp = get_log_strings(market, math.random(5,15))
  143. if not strings then
  144. return
  145. end
  146. strings = table.concat(strings, "\n")
  147. local day = math.ceil(last_timestamp/86400)
  148. local title = S("@1 Ledger on Day @2", market.def.description, day)
  149. local author = S("@1's market clerk", town_name)
  150. return generate_book(title, author, strings)
  151. end
  152. end
  153. --------------------------------------------------------------------------------
  154. -- Travel guides
  155. -- returns {pos, data}
  156. local half_map_chunk_size = settlements.half_map_chunk_size
  157. local get_random_settlement_within_range = function(pos, range_max, range_min)
  158. range_min = range_min or half_map_chunk_size -- If no minimum provided, at least don't look within your own chunk
  159. if range_max <= range_min then
  160. return
  161. end
  162. local min_edge = vector.subtract(pos, range_max)
  163. local max_edge = vector.add(pos, range_max)
  164. local settlement_list = named_waypoints.get_waypoints_in_area("settlements", min_edge, max_edge)
  165. local settlements_within_range = {}
  166. for _, settlement in pairs(settlement_list) do
  167. local distance = vector.distance(pos, settlement.pos)
  168. if distance < range_max and distance > range_min then
  169. table.insert(settlements_within_range, settlement)
  170. end
  171. end
  172. if #settlements_within_range == 0 then
  173. return
  174. end
  175. local target = settlements_within_range[math.random(#settlements_within_range)]
  176. return target
  177. end
  178. local compass_dirs = {
  179. [0] = S("west"),
  180. S("west-southwest"),
  181. S("southwest"),
  182. S("south-southwest"),
  183. S("south"),
  184. S("south-southeast"),
  185. S("southeast"),
  186. S("east-southeast"),
  187. S("east"),
  188. S("east-northeast"),
  189. S("northeast"),
  190. S("north-northeast"),
  191. S("north"),
  192. S("north-northwest"),
  193. S("northwest"),
  194. S("west-northwest"),
  195. S("west"),
  196. }
  197. local increment = 2*math.pi/#compass_dirs -- Divide the circle up into pieces
  198. local reframe = math.pi - increment/2 -- Adjust the angle to run through a range divisible into indexes
  199. local compass_direction = function(p1, p2)
  200. local dir = vector.subtract(p2, p1)
  201. local angle = math.atan2(dir.z, dir.x);
  202. angle = angle + reframe
  203. angle = math.ceil(angle / increment)
  204. return compass_dirs[angle]
  205. end
  206. local get_altitude = function(pos)
  207. if pos.y > 100 then
  208. return S("highlands")
  209. elseif pos.y > 20 then
  210. return S("midlands")
  211. elseif pos.y > 0 then
  212. return S("lowlands")
  213. else
  214. -- TODO: need to update this system once there are underground settlements
  215. return S("waters")
  216. end
  217. end
  218. settlements.generate_travel_guide = function(source_pos, source_name)
  219. local range = math.random(settlements.min_dist_settlements*2, settlements.min_dist_settlements*5)
  220. local target = get_random_settlement_within_range(source_pos, range)
  221. if not target then
  222. return
  223. end
  224. local target_name = target.data.name
  225. local target_type = target.data.settlement_type
  226. local target_desc = S("settlement")
  227. if target_type then
  228. local def = settlements.registered_settlements[target_type]
  229. if def and def.description then
  230. target_desc = def.description
  231. end
  232. end
  233. local title = S("A travel guide to @1", target_name)
  234. local author = S("a resident of @1", source_name)
  235. local dir = compass_direction(source_pos, target.pos)
  236. local distance = vector.distance(source_pos, target.pos)
  237. local kilometers = string.format("%.1f", distance/1000)
  238. local altitude = get_altitude(target.pos)
  239. 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)
  240. return generate_book(title, author, text)
  241. end