z_mirror/RecipeBook

git clone https://git.raiguard.me/z_mirror/RecipeBook

Caleb HeuerAdd spent result9eaa7d2

trunk
18.5 KiB555 linesraw
1local flib_dictionary = require("__flib__.dictionary")
2local flib_format = require("__flib__.format")
3local flib_gui = require("__flib__.gui")
4local flib_math = require("__flib__.math")
5local util = require("scripts.util")
6
7local gui_util = require("scripts.gui.util")
8local researched = require("scripts.database.researched")
9
10local rocket_lift_weight = prototypes.utility_constants.rocket_lift_weight --[[@as double]]
11
12--- @class InfoDescription
13--- @field context MainGuiContext
14--- @field callback flib.GuiElemHandler
15--- @field frame LuaGuiElement
16--- @field has_content boolean
17local info_description = {}
18local mt = { __index = info_description }
19script.register_metatable("info_description", mt)
20
21--- @param parent LuaGuiElement
22--- @param context MainGuiContext
23--- @param callback flib.GuiElemHandler
24--- @return InfoDescription
25function info_description.new(parent, context, callback)
26  local frame = parent.add({
27    type = "frame",
28    style = "deep_frame_in_shallow_frame_for_description",
29    horizontal_scroll_policy = "never",
30    direction = "vertical",
31  })
32  local self = {
33    context = context,
34    frame = frame,
35    callback = callback,
36    has_content = false,
37  }
38  setmetatable(self, mt)
39  return self
40end
41
42--- @param prototype GenericPrototype
43--- @return string
44local function get_prototype_type(prototype)
45  local object_name = prototype.object_name
46  if
47    object_name == "LuaEquipmentPrototype"
48    or object_name == "LuaEntityPrototype"
49    or object_name == "LuaItemPrototype"
50  then
51    return prototype.type
52  end
53  return util.object_name_to_type[object_name]
54end
55
56--- @return LocalisedString
57local function mod_name_string(internal_name)
58  return { "?", { "mod-name." .. internal_name }, internal_name }
59end
60
61function info_description:add_separator()
62  self.new_section = true
63end
64
65--- @private
66--- @param args LuaGuiElement.add_param
67--- @return LuaGuiElement
68function info_description:add_internal(args)
69  if self.new_section then
70    self.new_section = false
71    if #self.frame.children_names > 0 then
72      self.frame.add({ type = "line", style = "rb_description_line", direction = "horizontal" })
73    end
74  end
75  return self.frame.add(args)
76end
77
78--- @param icon SpritePath
79--- @param caption LocalisedString
80--- @param id DatabaseID?
81function info_description:add_category_header(icon, caption, id)
82  if id then
83    local flow = self:add_internal({ type = "flow" })
84    flow.style.vertical_align = "center"
85    flow.add({
86      type = "label",
87      style = "tooltip_heading_label_category",
88      caption = { "", "[img=" .. icon .. "]  ", caption },
89    })
90    flow.add({
91      type = "button",
92      style = "rb_description_heading_id_button",
93      caption = { "", "[img=" .. id.type .. "/" .. id.name .. "]  ", util.get_prototype(id).localised_name }, --- @diagnostic disable-line:assign-type-mismatch
94      elem_tooltip = { type = id.type, name = id.name },
95      tags = flib_gui.format_handlers(
96        { [defines.events.on_gui_click] = self.callback },
97        { rb_id = { type = id.type, name = id.name } }
98      ),
99    })
100    flow.style.top_margin = -4
101    flow.style.bottom_margin = -4
102  else
103    self:add_internal({
104      type = "label",
105      style = "tooltip_heading_label_category",
106      caption = { "", "[img=" .. icon .. "] ", caption },
107    })
108  end
109  local line = self:add_internal({ type = "line", style = "tooltip_category_line" })
110  line.style.left_margin = -8
111  line.style.right_margin = -8
112end
113
114local mod_name_separator = " › "
115
116--- @param prototype GenericPrototype
117function info_description:add_prototype_history(prototype)
118  local history = prototypes.get_history(get_prototype_type(prototype), prototype.name)
119  if history.created ~= "base" or #history.changed > 0 then
120    --- @type LocalisedString
121    local output = { "", mod_name_string(history.created) }
122    local x = 2
123    for _, changed in pairs(history.changed) do
124      local mod_name = mod_name_string(changed)
125      x = x + 2
126      if x > 20 then
127        x = 4
128        output = { "", output, mod_name_separator, mod_name }
129      else
130        output[#output + 1] = mod_name_separator
131        output[#output + 1] = mod_name
132      end
133    end
134    self:add_internal({ type = "label", style = "info_label", caption = output })
135    self:add_separator()
136  end
137end
138
139--- @param prototype GenericPrototype
140function info_description:add_description(prototype)
141  local descriptions = flib_dictionary.get(self.context.player.index, "description") or {}
142  local path = util.get_path(prototype)
143  local description = descriptions[path]
144  if description then
145    local label = self:add_internal({ type = "label", caption = description })
146    label.style.single_line = false
147    self:add_separator()
148  end
149end
150
151--- @param label LocalisedString
152--- @param value LocalisedString
153--- @return LuaGuiElement
154function info_description:add_generic_row(label, value)
155  local flow = self:add_internal({ type = "flow" })
156  flow.add({ type = "label", style = "semibold_caption_label", caption = { "", label, ":" } })
157  flow.add({ type = "label", caption = value })
158  return flow
159end
160
161--- @param label LocalisedString
162--- @param id DatabaseID
163--- @param force_index uint
164--- @return LuaGuiElement
165function info_description:add_id_row(label, id, force_index)
166  local flow = self:add_internal({ type = "flow" })
167  flow.style.vertical_align = "center"
168  flow.add({ type = "label", style = "semibold_caption_label", caption = { "", label, ":" } })
169
170  local id_prototype = util.get_prototype(id)
171  local style = "rb_description_id_button"
172  if id_prototype.hidden_in_factoriopedia then
173    style = "rb_description_id_button_hidden"
174  elseif not researched.is(id_prototype, force_index) then
175    style = "rb_description_id_button_unresearched"
176  end
177
178  flow.add({
179    type = "button",
180    style = style,
181    caption = { "", "[img=" .. id.type .. "/" .. id.name .. "]  ", gui_util.format_caption(id) },
182    elem_tooltip = { type = id.type, name = id.name },
183    tags = flib_gui.format_handlers(
184      { [defines.events.on_gui_click] = self.callback },
185      { rb_id = { type = id.type, name = id.name } }
186    ),
187  })
188  return flow
189end
190
191--- @param recipe LuaRecipePrototype
192function info_description:add_recipe_properties(recipe)
193  local pollution = recipe.emissions_multiplier
194  if pollution ~= 1 then
195    self:add_generic_row({ "description.recipe-pollution" }, flib_format.number(pollution * 100, false, 0) .. "%")
196  end
197end
198
199--- @param item LuaItemPrototype
200function info_description:add_item_properties(item)
201  local fuel_category = item.fuel_category
202  if fuel_category then
203    self:add_internal({
204      type = "label",
205      style = "semibold_caption_label",
206      caption = prototypes.fuel_category[fuel_category].localised_name, --- @diagnostic disable-line:assign-type-mismatch
207    })
208  end
209
210  local fuel_value = item.fuel_value
211  if fuel_value > 0 then
212    self:add_generic_row(
213      { "description.fuel-value" },
214      { "", flib_format.number(flib_math.round(fuel_value, 0.01), true), { "si-unit-symbol-joule" } }
215    )
216  end
217
218  local fuel_pollution = item.fuel_emissions_multiplier
219  if fuel_pollution ~= 1 then
220    self:add_generic_row(
221      { "description.fuel-pollution" },
222      { "format-percent", flib_format.number(flib_math.round(fuel_pollution * 100, 0.01), true) }
223    )
224  end
225
226  local fuel_acceleration_multiplier = item.fuel_acceleration_multiplier
227  if fuel_acceleration_multiplier ~= 1 then
228    self:add_generic_row(
229      { "description.fuel-acceleration" },
230      { "format-percent", flib_format.number(flib_math.round(fuel_acceleration_multiplier * 100, 0.01), true) }
231    )
232  end
233
234  local fuel_top_speed_multiplier = item.fuel_top_speed_multiplier
235  if fuel_top_speed_multiplier ~= 1 then
236    self:add_generic_row(
237      { "description.fuel-top-speed" },
238      { "format-percent", flib_format.number(flib_math.round(fuel_top_speed_multiplier * 100, 0.01), true) }
239    )
240  end
241
242  local burnt_result = item.burnt_result
243  if burnt_result then
244    self:add_id_row(
245      { "description.spent-result" },
246      { type = "item", name = burnt_result.name },
247      self.context.player.force_index
248    )
249  end
250
251  local module_effects = item.module_effects
252  if module_effects then
253    for key, value in pairs(module_effects) do
254      if key == "quality" and #prototypes.quality == 2 then -- "Normal" and "unknown"
255        goto continue
256      end
257      local caption = flib_format.number(value * 100, false, 2) .. "%"
258      if value > 0 then
259        caption = "+" .. caption
260      end
261      self:add_generic_row({ "description." .. key .. "-bonus" }, caption)
262      ::continue::
263    end
264  end
265
266  local stack_size = item.stack_size
267  if stack_size > 0 then
268    self:add_generic_row({ "description.stack-size" }, flib_format.number(stack_size, true))
269  end
270
271  if script.feature_flags.space_travel and item.weight > 0 then
272    local rocket_capacity = math.floor(rocket_lift_weight / item.weight)
273    if rocket_capacity > 0 then
274      --- @type LocalisedString
275      local caption = { "description.rocket-capacity-value-short", util.format_number(rocket_capacity) }
276      if stack_size > 0 and rocket_capacity ~= stack_size then
277        local stack_count = rocket_capacity / stack_size
278        if stack_count >= 0.1 then
279          caption[1] = "description.rocket-capacity-value"
280          caption[#caption + 1] = stack_count
281          caption[#caption + 1] = flib_format.number(flib_math.round(stack_count, 0.1))
282        end
283      end
284      self:add_generic_row({ "description.rocket-capacity" }, caption)
285    else
286      self:add_generic_row({ "description.rocket-capacity" }, { "description.too-heavy-for-rocket" })
287    end
288  end
289end
290
291--- @param fluid LuaFluidPrototype
292function info_description:add_fluid_properties(fluid)
293  local fuel_value = fluid.fuel_value
294  if fuel_value > 0 then
295    self:add_generic_row(
296      { "description.fuel-value" },
297      { "", flib_format.number(flib_math.round(fuel_value, 0.01), true), { "si-unit-symbol-joule" } }
298    )
299  end
300
301  local min_temperature = flib_math.round(fluid.default_temperature, 0.01)
302  local max_temperature = flib_math.round(fluid.max_temperature, 0.01)
303
304  --- @param temperature double
305  local function format_temperature(temperature)
306    local output = flib_format.number(temperature, true)
307    if temperature < 1000 then
308      output = output .. " "
309    end
310    return { "", output, { "si-unit-degree-celsius" } }
311  end
312
313  if min_temperature ~= max_temperature then
314    self:add_generic_row({ "description.minimum-temperature" }, format_temperature(min_temperature))
315    self:add_generic_row({ "description.maximum-temperature" }, format_temperature(max_temperature))
316    self:add_generic_row({ "description.heat-capacity" }, {
317      "",
318      flib_format.number(flib_math.round(fluid.heat_capacity, 0.01), true),
319      " ",
320      { "si-unit-symbol-joule" },
321      "/",
322      { "si-unit-degree-celsius" },
323    })
324  end
325end
326
327local container_types = {
328  ["container"] = true,
329  ["logistic-container"] = true,
330  ["infinity-container"] = true,
331  ["cargo-wagon"] = true,
332}
333
334--- @param entity LuaEntityPrototype
335function info_description:add_entity_properties(entity)
336  local max_underground_distance = entity.max_underground_distance
337  if max_underground_distance then
338    self:add_generic_row(
339      { "description.maximum-length" },
340      flib_format.number(flib_math.round(max_underground_distance, 0.01), true)
341    )
342  end
343
344  local belt_speed = entity.belt_speed
345  if belt_speed then
346    self:add_generic_row({ "description.belt-speed" }, {
347      "",
348      flib_format.number(flib_math.round(belt_speed * 8 * 60, 0.01), true),
349      " ",
350      { "description.belt-items" },
351      { "per-second-suffix" },
352    })
353  end
354
355  if container_types[entity.type] then
356    local storage_size = entity.get_inventory_size(defines.inventory.chest)
357    if storage_size then
358      self:add_generic_row({ "description.storage-size" }, flib_format.number(storage_size, true))
359    end
360  end
361
362  if entity.type == "storage-tank" or entity.type == "fluid-wagon" then
363    self:add_generic_row({ "description.fluid-capacity" }, flib_format.number(entity.fluid_capacity, true))
364  end
365
366  local rotation_speed = entity.get_inserter_rotation_speed() -- TODO: Quality
367  if rotation_speed then
368    self:add_generic_row(
369      { "description.rotation-speed" },
370      { "", { "format-degrees", flib_format.number(rotation_speed * 360 * 60, false, 0) }, { "per-second-suffix" } }
371    )
372  end
373
374  if entity.type == "inserter" then
375    local force_bonus = 0
376    local force = self.context.player.force
377    if entity.bulk then
378      force_bonus = force.bulk_inserter_capacity_bonus
379    else
380      force_bonus = force.inserter_stack_size_bonus
381    end
382    local stack_size = 1 + entity.inserter_stack_size_bonus --[[@as uint]]
383    local label = flib_format.number(stack_size)
384    if force_bonus ~= 0 then
385      label = label .. " + " .. flib_format.number(force_bonus)
386    end
387    self:add_generic_row({ "description.hand-stack-size" }, label)
388    if entity.filter_count > 0 then
389      self:add_internal({
390        type = "label",
391        style = "semibold_caption_label",
392        caption = { "description.can-filter-items" },
393      })
394    end
395  end
396
397  if entity.type == "electric-pole" then
398    self:add_generic_row({ "description.wire-reach" }, flib_format.number(entity.get_max_wire_distance())) -- TODO: Quality
399    local supply_area = flib_format.number(entity.get_supply_area_distance() * 2) -- TODO: Quality
400    self:add_generic_row({ "description.supply-area" }, supply_area .. "×" .. supply_area)
401  end
402
403  if entity.type == "pump" or entity.type == "offshore-pump" then
404    local pumping_speed = entity.pumping_speed
405    if pumping_speed then
406      self:add_generic_row(
407        { "description.pumping-speed" },
408        { "", flib_format.number(pumping_speed * 60, true), { "per-second-suffix" } }
409      )
410    end
411  end
412
413  if entity.type == "resource" then
414    local mineable_properties = entity.mineable_properties
415    local fluid_name = mineable_properties.required_fluid
416    if fluid_name then
417      local fluid_id = { type = "fluid", name = fluid_name, amount = mineable_properties.fluid_amount / 10 }
418      if fluid_id then
419        self:add_id_row({ "description.rb-mining-fluid" }, fluid_id, self.context.player.force_index)
420      end
421    end
422  end
423
424  local burner = entity.burner_prototype
425  if burner then
426    for pollutant_name, pollution in pairs(burner.emissions_per_joule) do
427      if pollution ~= 0 then
428        self:add_generic_row(
429          prototypes.airborne_pollutant[pollutant_name].localised_name, --- @diagnostic disable-line:param-type-mismatch
430          {
431            "",
432            flib_format.number(pollution * entity.get_max_energy_usage() * 60 * 60, false), -- TODO: Quality
433            { "per-minute-suffix" },
434          }
435        )
436      end
437    end
438  end
439
440  local electric_energy_source_prototype = entity.electric_energy_source_prototype
441  if electric_energy_source_prototype then
442    for pollutant_name, pollution in pairs(electric_energy_source_prototype.emissions_per_joule) do
443      if pollution ~= 0 then
444        self:add_generic_row(
445          prototypes.airborne_pollutant[pollutant_name].localised_name, --- @diagnostic disable-line:param-type-mismatch
446          {
447            "",
448            flib_format.number(pollution * entity.get_max_energy_usage() * 60 * 60, false), -- TODO: Quality
449            { "per-minute-suffix" },
450          }
451        )
452      end
453    end
454  end
455
456  if entity.type == "rocket-silo" then
457    self:add_generic_row({ "description.rb-rocket-parts-required" }, util.format_number(entity.rocket_parts_required))
458  end
459
460  if entity.type == "mining-drill" then
461    self:add_generic_row(
462      { "description.mining-speed" },
463      { "", util.format_number(entity.mining_speed), { "per-second-suffix" } }
464    )
465    local mining_radius = util.format_number(math.ceil(entity.mining_drill_radius * 2))
466    self:add_generic_row({ "description.mining-area" }, { "", mining_radius, "×", mining_radius })
467  end
468end
469
470--- @param id DatabaseID
471function info_description:add_consumption(id)
472  self:add_category_header(gui_util.get_tooltip_category_sprite(id, "consumes"), { "tooltip-category.consumes" }, id)
473  assert(id.amount)
474  self:add_generic_row(
475    { "description.energy-consumption" },
476    { "", flib_format.number(id.amount), { "per-second-suffix" } }
477  )
478  if id.minimum_temperature then
479    self:add_generic_row(
480      { "description.minimum-temperature" },
481      { "", flib_format.number(id.minimum_temperature), " ", { "si-unit-degree-celsius" } }
482    )
483  end
484  if id.maximum_temperature then
485    self:add_generic_row(
486      { "description.maximum-temperature" },
487      { "", flib_format.number(id.maximum_temperature), " ", { "si-unit-degree-celsius" } }
488    )
489  end
490end
491
492--- @param id DatabaseID
493function info_description:add_production(id)
494  self:add_category_header(gui_util.get_tooltip_category_sprite(id, "generates"), { "tooltip-category.generates" }, id)
495  assert(id.amount)
496  self:add_generic_row({ "description.fluid-output" }, { "", flib_format.number(id.amount), { "per-second-suffix" } })
497  if id.temperature then
498    self:add_generic_row(
499      { "description.temperature" },
500      { "", flib_format.number(id.temperature), " ", { "si-unit-degree-celsius" } }
501    )
502  end
503  if id.minimum_temperature then
504    self:add_generic_row(
505      { "description.minimum-temperature" },
506      { "", flib_format.number(id.minimum_temperature), " ", { "si-unit-degree-celsius" } }
507    )
508  end
509  if id.maximum_temperature then
510    self:add_generic_row(
511      { "description.maximum-temperature" },
512      { "", flib_format.number(id.maximum_temperature), " ", { "si-unit-degree-celsius" } }
513    )
514  end
515end
516
517--- @param entity LuaEntityPrototype
518function info_description:add_vehicle_properties(entity)
519  if not gui_util.vehicles[entity.type] then
520    return
521  end
522
523  self:add_category_header("tooltip-category-vehicle", { "tooltip-category.vehicle" })
524  if not string.find(entity.type, "wagon") then
525    local max_speed = entity.speed
526    if max_speed then
527      self:add_generic_row(
528        { "description.max-speed" },
529        { "si-unit-kilometer-per-hour", flib_format.number(max_speed * 60 * 60 * 60 / 1000, false, 0) }
530      )
531    end
532    -- TODO: Add read for vehicle max acceleration power
533    --   local burner = entity.burner_prototype
534    --   if burner then
535    --   end
536    --   if acceleration_power then
537    --     self:add_generic_row(
538    --       { "description.max-speed" },
539    --       { "", flib_format.number(acceleration_power * 60 * 60 * 60 / 1000, false, 0), { "si-unit-kilometer-per-hour" } }
540    --     )
541    --   end
542  end
543  local weight = entity.weight
544  if weight then
545    self:add_generic_row({ "description.weight" }, flib_format.number(weight))
546  end
547end
548
549function info_description:finalize()
550  if #self.frame.children_names == 0 then
551    self.frame.destroy()
552  end
553end
554
555return info_description