transport.lua 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. -- Movement and passenger delivery are separate native operations with separate receipts.
  2. local AI = require("mission_ember.combat_ai")
  3. local ARRIVAL_SETTLE_MS = 3000
  4. local EXTRA_DROP_DELAY_MS = 3000
  5. -- FNV-1("exit_25"), authored by Harvester action controller 80FE21CE.
  6. -- Empty group requests an exact, unique name lookup in this Harvester's native action table.
  7. local EXIT_ACTION = 0x7B0D3643
  8. local NO_GROUP = 0x811C9DC5
  9. return function(mission, passengers)
  10. local objective = assert(mission.Slot.EMBER_POWERHOUSE_BRIDGE_OBJECTIVE)
  11. local ships = {}
  12. -- A/B cross the bridge deck; C/D are fly-through ships with no bridge passengers.
  13. local manifest = {{1, 3}, {2, 4}, {}, {}}
  14. assert(#passengers.squads == 4 and passengers.reserved)
  15. for index, letter in ipairs({"A", "B", "C", "D"}) do
  16. local prefix = "BRIDGE_CROSSING_DROPSHIP_" .. letter
  17. ships[index] = {
  18. squad = assert(mission.Squad[prefix .. "_SQUAD"]),
  19. sensor = assert(mission.Slot[prefix .. "_SQUAD"]),
  20. member = assert(mission.Slot[prefix .. "_SQUAD_HARVESTER"]),
  21. entry = assert(mission.Slot[prefix .. "_ENTRY_SEQUENCE"]),
  22. exit = assert(mission.Slot[prefix .. "_EXIT_SEQUENCE"]),
  23. cargo = manifest[index],
  24. }
  25. end
  26. local function generation(state) return state:variable("ember.transport.generation") or 1 end
  27. local function timer_name(state, kind, index)
  28. local gen = generation(state)
  29. return "ember.transport." .. kind .. "." .. index .. (gen == 1 and "" or "." .. gen)
  30. end
  31. local function path(context, state, ship, revision)
  32. context:slot(ship.member):play_actor_path{generation = generation(state), revision = revision,
  33. path = context:slot(revision == 1 and ship.entry or ship.exit)}
  34. end
  35. -- Five bits per ship: entry finished, delivery finished, exit requested, retired, unload requested.
  36. local function depart(context, state, index)
  37. local shift = (index - 1) * 5
  38. local flags = state:variable("ember.transport.flags") or 0
  39. if ((flags >> shift) & 31) ~= 19 then return end
  40. local seen = state:variable(passengers.tag .. ".seen") or 0
  41. for _, cargoIndex in ipairs(ships[index].cargo) do
  42. if (seen & (1 << (cargoIndex - 1))) == 0 then return end
  43. end
  44. -- Native delivery is now prompt. Keep a brief authored-position hover after
  45. -- detach completes; native departure action follows the exit path with no timer.
  46. local holds = state:variable("ember.transport.departures") or 0
  47. local scheduled, ready = 1 << (index - 1), 1 << (index + 3)
  48. if (holds & scheduled) == 0 then
  49. context:set_variable("ember.transport.departures", holds | scheduled)
  50. context:start_timer(timer_name(state, "depart", index), 4000)
  51. return
  52. end
  53. if (holds & ready) == 0 then return end
  54. context:set_variable("ember.transport.flags", flags | (4 << shift))
  55. path(context, state, ships[index], 2)
  56. end
  57. local function unload(context, state, index)
  58. local shift = (index - 1) * 5
  59. local flags = state:variable("ember.transport.flags") or 0
  60. local previous = (flags >> shift) & 31
  61. if (previous & 25) ~= 1 then return end
  62. local cargo = {}
  63. for _, cargoIndex in ipairs(ships[index].cargo) do
  64. cargo[#cargo + 1] = context:slot(passengers.squads[cargoIndex].sensor)
  65. end
  66. if #cargo > 0 then
  67. context:slot(ships[index].member):deliver_squads{generation = generation(state), revision = 1, squads = cargo}
  68. context:set_variable("ember.transport.flags", flags | (16 << shift))
  69. else
  70. context:set_variable("ember.transport.flags", flags | (18 << shift))
  71. depart(context, state, index)
  72. end
  73. end
  74. local function prepare(context, state)
  75. if state:variable("ember.transport.prepared") then return end
  76. context:set_variable("ember.transport.prepared", true)
  77. -- Prepare the requests before enabling Ghost. These reserved squads and empty
  78. -- parents create no actors; only the later member entry command starts a ship.
  79. passengers:enter(context, state)
  80. context:set_variable("ember.transport.ai", 0)
  81. for _, ship in ipairs(ships) do
  82. local parent = context:squad(ship.squad)
  83. local counts = parent:counts()
  84. for index = 1, #parent.default_counts do counts:set(index, 0) end
  85. parent:place{counts = counts, mode = context.sdk.squad_modes.replace}
  86. -- Keep zero ordinary spawn requests: the member's path creates the ship.
  87. -- Objective membership lets the native squad evaluate authored behavior.
  88. AI.assign(context, ship.sensor, objective, -1, false)
  89. end
  90. end
  91. return {
  92. reset = function(context, state)
  93. local old = generation(state)
  94. for index, ship in ipairs(ships) do
  95. context:cancel_timer(timer_name(state, "depart", index))
  96. context:cancel_timer(timer_name(state, "unload", index))
  97. context:slot(ship.member):retire_actor{generation = old + 1}
  98. end
  99. context:set_variable("ember.transport.generation", old + 2)
  100. for _, key in ipairs({"prepared", "started", "flags", "departures", "exit_actions", "ai"}) do
  101. context:clear_variable("ember.transport." .. key)
  102. end
  103. end,
  104. prepare = prepare,
  105. start = function(context, state)
  106. if state:variable("ember.transport.started") then return end
  107. prepare(context, state)
  108. context:set_variable("ember.transport.started", true)
  109. context:set_variable("ember.transport.flags", 0)
  110. for _, ship in ipairs(ships) do path(context, state, ship, 1) end
  111. end,
  112. on_path = function(context, state, event)
  113. if not state:variable("ember.transport.started") or event.generation ~= generation(state) then return end
  114. for index, ship in ipairs(ships) do
  115. local member = context:slot(ship.member)
  116. if event.registry_key == member.registry_key and event.slot_type == member.slot_type
  117. and event.slot_index == member.slot_index then
  118. local bits = 0
  119. if event.revision == 1 and event.path_state == 1 then bits = bits | 1 end
  120. if event.delivery_revision == 1 and event.delivery_state == 0 then bits = bits | 2 end
  121. local flags = state:variable("ember.transport.flags") or 0
  122. local previous = (flags >> ((index - 1) * 5)) & 31
  123. local exits = state:variable("ember.transport.exit_actions") or 0
  124. local requested = (exits & (1 << (index - 1))) ~= 0
  125. if not event.dead and event.revision == 2 and event.path_state == 1
  126. and (previous & 12) == 4 and not requested then
  127. context:set_variable("ember.transport.exit_actions", exits | (1 << (index - 1)))
  128. member:play_actor_action{generation = generation(state), revision = 3, group = NO_GROUP, action = EXIT_ACTION}
  129. end
  130. if event.dead or (event.revision == 3 and event.path_state == 1 and requested) then
  131. bits = bits | 8
  132. if (previous & 8) == 0 then member:retire_actor{generation = generation(state) + 1} end
  133. end
  134. context:set_variable("ember.transport.flags", flags | (bits << ((index - 1) * 5)))
  135. if (bits & 1) ~= 0 and (previous & 9) == 0 and not event.dead then
  136. context:start_timer(timer_name(state, "unload", index), ARRIVAL_SETTLE_MS + EXTRA_DROP_DELAY_MS)
  137. end
  138. depart(context, state, index)
  139. return
  140. end
  141. end
  142. end,
  143. on_timer = function(context, state, event)
  144. if not state:variable("ember.transport.started") then return end
  145. for index = 1, #ships do
  146. if event.timer_name == timer_name(state, "depart", index) then
  147. local holds = state:variable("ember.transport.departures") or 0
  148. local scheduled = 1 << (index - 1)
  149. if (holds & scheduled) ~= 0 then
  150. context:set_variable("ember.transport.departures", holds | (1 << (index + 3)))
  151. depart(context, state, index)
  152. end
  153. return
  154. end
  155. if event.timer_name == timer_name(state, "unload", index) then
  156. unload(context, state, index)
  157. return
  158. end
  159. end
  160. end,
  161. on_squad = function(context, state, event)
  162. if not state:variable("ember.transport.prepared") then return end
  163. for index, ship in ipairs(ships) do
  164. local sensor = context:slot(ship.sensor)
  165. if event.registry_key == sensor.registry_key and event.slot_type == sensor.slot_type
  166. and event.slot_index == sensor.slot_index then
  167. AI.update(context, state, "ember.transport.ai", ship.sensor, objective, 14, event, index, false)
  168. end
  169. if state:variable("ember.transport.started") then depart(context, state, index) end
  170. end
  171. end,
  172. }
  173. end