z_mirror/Krastorio2

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

Polka37Space age fixes + K2SO compatibility (#724)c13e3614

trunk
19.2 KiB618 linesraw
1local flib_gui = require("__flib__.gui")
2local flib_math = require("__flib__.math")
3local flib_position = require("__flib__.position")
4
5local util = require("scripts.util")
6
7--- @alias IntergalacticTransceiverStatus
8--- | "charging"
9--- | "discharging"
10--- | "not_enough_input"
11--- | "ready"
12
13local defines_to_status = {
14  [defines.entity_status.charging] = "charging",
15  [defines.entity_status.discharging] = "discharging",
16}
17
18--- @type table<IntergalacticTransceiverStatus, { label: LocalisedString, sprite: SpritePath }>
19local status_properties = {
20  charging = {
21    label = { "entity-status.charging" },
22    icon = "utility/status_working",
23  },
24  discharging = {
25    alert_label = { "gui.kr-intergalactic-transceiver-alert-discharging" },
26    label = { "entity-status.discharging" },
27    icon = "utility/status_not_working",
28  },
29  not_enough_input = {
30    alert_label = { "gui.kr-intergalactic-transceiver-alert-not-enough-input" },
31    label = { "entity-status.kr-not-enough-input" },
32    icon = "utility/status_not_working",
33    tooltip = { "entity-status-tooltip.kr-not-enough-input" },
34  },
35  no_power = {
36    alert_label = { "gui.kr-intergalactic-transceiver-alert-no-power" },
37    label = { "entity-status.no-power" },
38    icon = "utility/status_not_working",
39  },
40  ready = {
41    alert_label = { "gui.kr-intergalactic-transceiver-alert-ready" },
42    label = { "entity-status.kr-ready" },
43    icon = "utility/status_working",
44  },
45}
46
47local min_charge_delta = 2.5e8 -- Requires charge rate of 15 GW
48local max_charge_delta = 2e9
49---@type table<string, number>
50local max_energy = {}
51local default_buffer_capacity =
52  prototypes.entity["kr-intergalactic-transceiver"].electric_energy_source_prototype.buffer_capacity
53for _, quality in pairs(prototypes.quality) do
54  max_energy[quality.name] = (default_buffer_capacity * quality.accumulator_capacity_multiplier) - max_charge_delta
55end
56local energy_drain = 50e9
57
58--- @param transceiver_data IntergalacticTransceiverForceData
59local function activate_transceiver(transceiver_data)
60  local entity = transceiver_data.entity
61  local entity_surface = entity.surface
62  local entity_position = entity.position
63  for _, player in pairs(entity.force.connected_players) do
64    if player.surface == entity_surface and flib_position.distance(entity_position, player.position) <= 100 then
65      storage.intergalactic_transceiver.player_in_cutscene[player.index] = true
66      if player.controller_type == defines.controllers.character then
67        player.character.destructible = false
68      end
69      player.set_controller({
70        type = defines.controllers.cutscene,
71        waypoints = {
72          {
73            position = entity.position,
74            zoom = 1.2,
75            transition_time = 70,
76            time_to_wait = 30,
77          },
78          {
79            position = entity.position,
80            zoom = 1.2,
81            transition_time = 0,
82            time_to_wait = 15,
83          },
84          {
85            position = entity.position,
86            zoom = 0.8,
87            transition_time = 130,
88            time_to_wait = 10,
89          },
90          {
91            position = entity.position,
92            zoom = 1.2,
93            transition_time = 200,
94            time_to_wait = 60,
95          },
96        },
97        final_transition_time = 60,
98      })
99    end
100  end
101  storage.intergalactic_transceiver.in_cutscene[entity.force_index] = true
102end
103
104--- @param e EventData.on_cutscene_finished
105local function on_cutscene_finished(e)
106  local player = game.get_player(e.player_index)
107  if not player then
108    return
109  end
110  if not storage.intergalactic_transceiver then
111    return
112  end
113
114  if storage.intergalactic_transceiver.player_in_cutscene[e.player_index] then
115    storage.intergalactic_transceiver.player_in_cutscene[e.player_index] = nil
116    if player.character then
117      player.character.destructible = true
118    end
119  end
120
121  if not storage.intergalactic_transceiver.in_cutscene[player.force.index] then
122    return
123  end
124  storage.intergalactic_transceiver.in_cutscene[player.force_index] = nil
125
126  player.force.technologies["kr-logo"].enabled = true
127
128  if not storage.intergalactic_transceiver.is_victory then
129    return
130  end
131
132  game.enable_galaxy_of_fame_button = true -- Does nothing in vanilla
133  game.set_win_ending_info({
134    title = { "gui.kr-victory-title" },
135    message = { "gui.kr-victory-message" },
136    bullet_points = {
137      { "victory-bullet-point-1" },
138      { "victory-bullet-point-2" },
139      { "victory-bullet-point-3" },
140      { "victory-bullet-point-4" },
141    },
142    final_message = { "gui.kr-victory-final-message" },
143  })
144  game.set_game_state({
145    can_continue = true,
146    game_finished = true,
147    victorious_force = player.force,
148    player_won = true,
149  })
150  player.force.play_sound({ path = "kr-win-joke-voice" })
151end
152
153--- @param e EventData.on_cutscene_waypoint_reached
154local function on_cutscene_waypoint_reached(e)
155  if not storage.intergalactic_transceiver then
156    return
157  end
158  local player = game.get_player(e.player_index)
159  if not player or not storage.intergalactic_transceiver.in_cutscene[player.force_index] then
160    return
161  end
162
163  local transceiver_data = storage.intergalactic_transceiver.forces[player.force_index]
164  if not transceiver_data then
165    return
166  end
167  local entity = transceiver_data.entity
168  if not entity.valid then
169    return
170  end
171
172  if e.waypoint_index == 1 then
173    entity.surface.create_entity({
174      name = "kr-intergalactic-transceiver-wave",
175      force = entity.force,
176      position = entity.position,
177      speed = 0,
178      max_range = 100,
179      target = entity,
180      create_build_effect_smoke = false,
181      raise_built = true,
182    })
183  elseif e.waypoint_index == 2 then
184    local entity_force = entity.force
185    local entity_player = entity.last_user
186    local entity_position = entity.position
187    local entity_quality = entity.quality
188    local entity_surface = entity.surface
189    entity.destroy()
190    transceiver_data.activating = true
191    local new_entity = entity_surface.create_entity({
192      name = "kr-activated-intergalactic-transceiver",
193      position = entity_position,
194      force = entity_force,
195      player = entity_player,
196      quality = entity_quality,
197      create_build_effect_smoke = false,
198      raise_built = true,
199    })
200    transceiver_data.activating = false
201    if not new_entity or not new_entity.valid then
202      return
203    end
204    if entity_quality.name ~= "normal" then
205      new_entity.power_production = new_entity.power_production * entity_quality.accumulator_capacity_multiplier
206      new_entity.electric_buffer_size = new_entity.electric_buffer_size * entity_quality.accumulator_capacity_multiplier
207    end
208    storage.intergalactic_transceiver.forces[entity_force.index] = { entity = new_entity }
209  end
210end
211
212--- @param e EventData.on_gui_click
213local function on_gui_window_closed(e)
214  local self = storage.intergalactic_transceiver.guis[e.player_index]
215  if not self then
216    return
217  end
218  self.elems.kr_intergalactic_transceiver_window.destroy()
219  self.player.opened = nil
220  self.player.play_sound({ path = "entity-close/kr-intergalactic-transceiver" })
221  storage.intergalactic_transceiver.guis[e.player_index] = nil
222end
223
224--- @param entity LuaEntity
225local function close_open_guis(entity)
226  for player_index, self in pairs(storage.intergalactic_transceiver.guis) do
227    if self.entity == entity or not self.entity.valid then
228      on_gui_window_closed({ player_index = player_index })
229    end
230  end
231end
232
233--- @param e EventData.on_gui_click
234local function on_gui_activate_button_clicked(e)
235  local player = game.get_player(e.player_index)
236  if not player then
237    return
238  end
239  local transceiver_data = storage.intergalactic_transceiver.forces[player.force_index]
240  close_open_guis(transceiver_data.entity)
241  activate_transceiver(transceiver_data)
242end
243
244--- @param self IntergalacticTransceiverGuiData
245local function update_gui(self)
246  local charged = flib_math.round(self.entity.energy / max_energy[self.entity.quality.name], 0.01)
247  local transceiver_data = storage.intergalactic_transceiver.forces[self.player.force_index]
248  local props = status_properties[transceiver_data.status]
249  self.elems.status_icon.sprite = props.icon
250  self.elems.status_label.caption = props.label
251  self.elems.status_label.tooltip = props.tooltip
252  self.elems.charge_progressbar.value = charged
253  self.elems.charge_progressbar.caption = math.floor(charged * 100) .. " %"
254  self.elems.activate_button.enabled = charged == 1
255end
256
257--- @param player LuaPlayer
258--- @param entity LuaEntity
259local function create_gui(player, entity)
260  local elems = flib_gui.add(player.gui.screen, {
261    type = "frame",
262    name = "kr_intergalactic_transceiver_window",
263    direction = "vertical",
264    elem_mods = { auto_center = true }, --- @diagnostic disable-line: missing-fields
265    handler = { [defines.events.on_gui_closed] = on_gui_window_closed },
266    {
267      type = "flow",
268      style = "flib_titlebar_flow",
269      drag_target = "kr_intergalactic_transceiver_window",
270      {
271        type = "label",
272        style = "flib_frame_title",
273        caption = { "entity-name.kr-intergalactic-transceiver" },
274        ignored_by_interaction = true,
275      },
276      { type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
277      {
278        type = "sprite-button",
279        style = "frame_action_button",
280        sprite = "utility/close",
281        mouse_button_filter = { "left" },
282        handler = { [defines.events.on_gui_click] = on_gui_window_closed },
283      },
284    },
285    {
286      type = "frame",
287      style = "entity_frame",
288      style_mods = { width = 424 }, --- @diagnostic disable-line: missing-fields
289      direction = "vertical",
290      {
291        type = "flow",
292        style_mods = { vertical_align = "center" },
293        { type = "sprite", name = "status_icon", style = "flib_indicator" },
294        { type = "label", name = "status_label" },
295      },
296      {
297        type = "frame",
298        style = "deep_frame_in_shallow_frame",
299        { type = "entity-preview", style = "wide_entity_button", elem_mods = { entity = entity } },
300      },
301      {
302        type = "progressbar",
303        name = "charge_progressbar",
304        style = "production_progressbar",
305        style_mods = { horizontally_stretchable = true },
306        value = 0,
307      },
308      {
309        type = "button",
310        name = "activate_button",
311        style_mods = { horizontally_stretchable = true },
312        caption = { "gui.kr-intergalactic-transceiver-activate" },
313        enabled = false,
314        handler = { [defines.events.on_gui_click] = on_gui_activate_button_clicked },
315      },
316    },
317  })
318
319  --- @class IntergalacticTransceiverGuiData
320  storage.intergalactic_transceiver.guis[player.index] = {
321    elems = elems,
322    entity = entity,
323    player = player,
324  }
325
326  player.opened = elems.kr_intergalactic_transceiver_window
327  if not game.is_multiplayer() then
328    player.play_sound({ path = "entity-open/kr-intergalactic-transceiver" })
329  end
330end
331
332--- @param entity LuaEntity
333local function on_transceiver_destroyed(entity)
334  local force_index = entity.force.index
335  storage.intergalactic_transceiver.forces[force_index] = nil
336  close_open_guis(entity)
337end
338
339--- @param entity LuaEntity
340local function on_inactive_transceiver_destroyed(entity)
341  storage.intergalactic_transceiver.inactive[entity.unit_number] = nil
342end
343
344--- @param e EntityDestroyedEvent
345local function on_entity_destroyed(e)
346  local entity = e.entity
347  if not entity or not entity.valid then
348    return
349  end
350
351  if entity.name == "kr-intergalactic-transceiver" then
352    on_transceiver_destroyed(entity)
353  elseif entity.name == "kr-inactive-intergalactic-transceiver" then
354    on_inactive_transceiver_destroyed(entity)
355  end
356end
357
358--- @param entity LuaEntity
359local function on_transceiver_built(entity)
360  local existing = storage.intergalactic_transceiver.forces[entity.force.index]
361  if existing then
362    local position = entity.position
363    local force = entity.force
364    local player = entity.last_user
365    local surface = entity.surface
366    entity.destroy()
367
368    local new_entity = surface.create_entity({
369      name = "kr-inactive-intergalactic-transceiver",
370      position = position,
371      force = force,
372      player = player,
373      create_build_effect_smoke = false,
374      raise_built = true,
375    })
376    if new_entity and new_entity.valid then
377      storage.intergalactic_transceiver.inactive[new_entity.unit_number] = new_entity
378    end
379
380    return
381  end
382
383  --- @class IntergalacticTransceiverForceData
384  storage.intergalactic_transceiver.forces[entity.force.index] = {
385    activating = false,
386    entity = entity,
387    last_alert_tick = game.tick,
388    last_energy = entity.energy,
389    status = "empty",
390  }
391end
392
393--- @param entity LuaEntity
394local function on_active_transceiver_built(entity)
395  local existing = storage.intergalactic_transceiver.forces[entity.force.index]
396  if not existing or not existing.activating then
397    local position = entity.position
398    local force = entity.force
399    local player = entity.last_user
400    local surface = entity.surface
401    entity.destroy()
402
403    local new_entity = surface.create_entity({
404      name = "kr-intergalactic-transceiver",
405      position = position,
406      force = force,
407      player = player,
408      create_build_effect_smoke = false,
409      raise_built = true,
410      quality = quality,
411    })
412
413    if new_entity and new_entity.valid then
414      on_transceiver_built(new_entity)
415    end
416  end
417end
418
419--- @param e EntityBuiltEvent
420local function on_entity_built(e)
421  local entity = e.entity or e.destination
422  if not entity or not entity.valid then
423    return
424  end
425
426  if entity.name == "kr-intergalactic-transceiver" then
427    on_transceiver_built(entity)
428  elseif entity.name == "kr-activated-intergalactic-transceiver" then
429    on_active_transceiver_built(entity)
430  end
431end
432
433--- @param data IntergalacticTransceiverForceData
434--- @param entity LuaEntity
435local function update_transceiver(data, entity)
436  if not data.last_energy then
437    return
438  end
439  local current_energy = entity.energy
440  local new_energy = current_energy
441  local difference = current_energy - data.last_energy
442  --- @type IntergalacticTransceiverStatus
443  local status = "charging"
444  -- If we're not receiving enough power
445  if difference < min_charge_delta and current_energy > 0 then
446    -- Drain the transceiver at 3 TJ / sec
447    new_energy = math.max(0, current_energy - energy_drain)
448    -- Update status
449    if new_energy > 0 then
450      status = "discharging"
451    else
452      status = "not_enough_input"
453    end
454  else
455    if current_energy > max_energy[entity.quality.name] then
456      -- XXX: If the energy remains constant, the entity status will change to "normal", so give it a random offset.
457      new_energy = max_energy[entity.quality.name] + math.random(0, 20) * 1000000
458      status = "ready"
459    else
460      local current_status = defines_to_status[entity.status]
461      if status_properties[current_status] then
462        status = current_status
463      end
464    end
465  end
466
467  if new_energy ~= current_energy then
468    entity.energy = new_energy
469  end
470  local last_status = data.status
471  data.last_energy = new_energy
472  data.status = status
473
474  local status_properties = status_properties[status]
475  if status_properties and status_properties.alert_label and game.tick - data.last_alert_tick >= 60 then
476    data.last_alert_tick = game.tick
477    for _, player in pairs(entity.force.players) do
478      player.add_custom_alert(
479        entity,
480        { type = "item", name = "kr-intergalactic-transceiver" },
481        status_properties.alert_label,
482        true
483      )
484      if status == "discharging" and last_status ~= status then
485        player.play_sound({
486          path = "kr-intergalactic-transceiver-discharging-warning",
487        })
488      end
489    end
490  end
491end
492
493--- @param e EventData.on_gui_opened
494local function on_gui_opened(e)
495  if storage.intergalactic_transceiver.guis[e.player_index] then
496    return
497  end
498  if e.gui_type ~= defines.gui_type.entity then
499    return
500  end
501  local entity = e.entity
502  if not entity or entity.name ~= "kr-intergalactic-transceiver" then
503    return
504  end
505  local player = game.get_player(e.player_index)
506  if not player then
507    return
508  end
509  create_gui(player, entity)
510end
511
512local function on_tick()
513  if not storage.intergalactic_transceiver then
514    return
515  end
516
517  local transceivers = storage.intergalactic_transceiver.forces
518  for force_index, data in pairs(transceivers) do
519    local entity = data.entity
520    if entity and entity.valid then
521      update_transceiver(data, entity)
522    else
523      transceivers[force_index] = nil
524    end
525  end
526
527  for _, self in pairs(storage.intergalactic_transceiver.guis) do
528    update_gui(self)
529  end
530end
531
532local function on_180th_tick()
533  if not storage.intergalactic_transceiver then
534    return
535  end
536  for unit_number, entity in pairs(storage.intergalactic_transceiver.inactive) do
537    if entity.valid then
538      util.entity_flying_text(entity, { "message.kr-transceiver-is-inactive" }, { r = 1 })
539    else
540      storage.intergalactic_transceiver.inactive[unit_number] = nil
541    end
542  end
543end
544
545--- @class IntergalacticTransceiverHandler : event_handler
546local intergalactic_transceiver = {}
547
548function intergalactic_transceiver.on_init()
549  -- Can be already initialised through remote interface
550  if storage.intergalactic_transceiver then
551    return
552  end
553  storage.intergalactic_transceiver = {
554    --- @type table<uint, IntergalacticTransceiverForceData>
555    forces = {},
556    --- @type table<uint, IntergalacticTransceiverGuiData>
557    guis = {},
558    -- Force index
559    --- @type table<uint, boolean>
560    in_cutscene = {},
561    -- Player index
562    --- @type table<uint, boolean>
563    player_in_cutscene = {},
564    --- @type table<uint, LuaEntity>
565    inactive = {},
566    is_victory = true,
567  }
568end
569
570intergalactic_transceiver.events = {
571  [defines.events.on_built_entity] = on_entity_built,
572  [defines.events.on_cutscene_cancelled] = on_cutscene_finished,
573  [defines.events.on_cutscene_finished] = on_cutscene_finished,
574  [defines.events.on_cutscene_waypoint_reached] = on_cutscene_waypoint_reached,
575  [defines.events.on_entity_cloned] = on_entity_built,
576  [defines.events.on_entity_died] = on_entity_destroyed,
577  [defines.events.on_gui_opened] = on_gui_opened,
578  [defines.events.on_player_mined_entity] = on_entity_destroyed,
579  [defines.events.on_robot_built_entity] = on_entity_built,
580  [defines.events.on_robot_mined_entity] = on_entity_destroyed,
581  [defines.events.on_tick] = on_tick,
582  [defines.events.script_raised_built] = on_entity_built,
583  [defines.events.script_raised_destroy] = on_entity_destroyed,
584  [defines.events.script_raised_revive] = on_entity_built,
585  [defines.events.on_space_platform_built_entity] = on_entity_built,
586  [defines.events.on_space_platform_mined_entity] = on_entity_destroyed,
587}
588
589intergalactic_transceiver.on_nth_tick = {
590  [180] = on_180th_tick,
591}
592
593-- REMOTE
594
595remote.add_interface("kr-intergalactic-transceiver", {
596  get_is_victory = function()
597    if storage.intergalactic_transceiver then
598      intergalactic_transceiver.on_init()
599    end
600    return storage.intergalactic_transceiver.is_victory
601  end,
602  set_is_victory = function(to_state)
603    if to_state == nil or type(to_state) ~= "boolean" then
604      error("`to_state` must be a boolean")
605    end
606    if not storage.intergalactic_transceiver then
607      intergalactic_transceiver.on_init()
608    end
609    storage.intergalactic_transceiver.is_victory = to_state
610  end,
611})
612
613flib_gui.add_handlers({
614  transceiver_on_window_closed = on_gui_window_closed,
615  transceiver_on_activate_button_clicked = on_gui_activate_button_clicked,
616})
617
618return intergalactic_transceiver