apex.lua 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. -- Light's End: authored access squads, damage-gated reactor, cell delivery and escape.
  2. return function(m, a, ending)
  3. local A = {region = 0}
  4. local sides = {"EAST", "WEST"}
  5. local access = {"access1", "access2", "electron_controllers", "dispenser", "security"}
  6. local reactor = {"reactor_east_entry", "reactor_west_entry", "reactor_east_reinforce", "reactor_west_reinforce",
  7. "reactor_east_final", "reactor_west_final", "coffin_east", "coffin_west", "coffin_interior"}
  8. local triggers = {"ACCESS_DOOR_INNER_PLAYER_TRIGGER", "ACCESS_JUMP_ONE_PLAYER_TRIGGER",
  9. "ACCESS_JUMP_TWO_PLAYER_TRIGGER", "SECURITY_CENTER_PLAYER_TRIGGER", "SECURITY_DOOR_PLAYER_TRIGGER",
  10. "SECURITY_LEDGE_PLAYER_TRIGGER", "APEX_DIRECTIVE_REACTOR_GOTO_PLAYER_TRIGGER",
  11. "APEX_REACTOR_CLAMSHELL_001_DIALOG_START_PLAYER_TRIGGER", "REACTOR_ARM_EAST_ENTRANCE_PLAYER_TRIGGER",
  12. "REACTOR_MOTHER_BRAIN_ENTRY_PLAYER_TRIGGGER", "DIALOG_APEX_SECURITY_001_TEST_PLAYER_TRIGGER",
  13. "DIALOG_APEX_SECURITY_002_PLAYER_TRIGGER", "DIALOG_APEX_MOTHER_BRAIN_001_PLAYER_TRIGGER",
  14. "DIALOG_APEX_MOTHER_BRAIN_002_PLAYER_TRIGGER", "DIALOG_APEX_MOTHER_BRAIN_003_PLAYER_TRIGGER",
  15. "DIALOG_APEX_MOTHER_BRAIN_004_PLAYER_TRIGGER"}
  16. local escape_triggers = {"APEX_MOTHER_BRAIN_005_DIALOG_PLAYER_TRIGGER", "APEX_MOTHER_BRAIN_006_DIALOG_PLAYER_TRIGGER",
  17. "APEX_MOTHER_BRAIN_007_DIALOG_PLAYER_TRIGGER", "APEX_MOTHER_BRAIN_008_DIALOG_PLAYER_TRIGGER",
  18. "APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_PLAYER_TRIGGER"}
  19. -- The authored explosion prefab's four player triggers, west to east along the rails:
  20. -- set A x -448.75..-438.75, B -403.75..-393.75, C -368.75..-358.75, D -323.75..-313.75,
  21. -- all spanning y 2967..3002.5 and z 185..212.5. Their receipts must be forwarded
  22. -- to the scene's external event list; arming the volumes alone cannot do that.
  23. local explosion_triggers = {}
  24. -- FNV-1 event names in 80BEB1CC's five authored event-gate nodes.
  25. local explosion_events = {0x329EB106, 0x633B82E9, 0x15A78938, 0xF9D55A83}
  26. local explosion_scene = "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_TRIGGERED_EXPLOSIONS_SCENE"
  27. for _, set in ipairs({"A", "B", "C", "D"}) do
  28. explosion_triggers[#explosion_triggers + 1] =
  29. "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_EXPLOSION_SET_" .. set .. "_PLAYER_TRIGGER"
  30. end
  31. local function phase(s) return s:variable("ember.apex.phase") or 0 end
  32. local function set(c, p) c:set_variable("ember.apex.phase", p) end
  33. local function generation(s) return s:variable("ember.apex.generation") or 1 end
  34. local function dead(s, target) return s:variable("ember.apex.dead." .. target) == true end
  35. local function vent_timer(s) return "ember.apex.vents." .. generation(s) end
  36. local function lane(c, name, transition, snap)
  37. a.slot(c, name):transition{transition = c.sdk.device_transitions[transition], snap = snap or false}
  38. end
  39. local function unlock(c, name)
  40. lane(c, name, "unlock"); lane(c, name, "power_on")
  41. end
  42. local function bridges(c, extended, snap)
  43. -- As with the landing bridge, the authored position-0 pose is extended.
  44. for _, side in ipairs(sides) do
  45. a.device(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE", not extended, snap)
  46. end
  47. end
  48. local function coffin_doors(c, open, snap)
  49. for _, side in ipairs(sides) do
  50. a.device(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", open, snap)
  51. a.device(c, "REACTOR_COFFIN_LIGHT_" .. side .. "_DEVICE", open, snap)
  52. lane(c, "REACTOR_COFFIN_LIGHT_" .. side .. "_DEVICE", open and "power_on" or "power_off", snap)
  53. end
  54. a.device(c, "REACTOR_SHIELD_DEVICE", open, snap)
  55. end
  56. -- The weapon has two halves and both are driven.
  57. --
  58. -- Objects: the core and ring models carry solid geometry with full LOD tables and are the
  59. -- structure the weapon is built from, so they are placed once and never taken back out --
  60. -- removing them deletes the beam and everything around it. The laser model carries the
  61. -- emitter's effect components and no geometry LODs, so it is the beam itself and its
  62. -- presence is what has been observed to make the beam appear and disappear.
  63. --
  64. -- Devices: `SPECOPS_APEX_RING_LASER_DEVICE` and `..._RING_DEVICE` are the authored lanes of
  65. -- those same placed objects, exactly as the Mercury lever's type-23 device animates the
  66. -- type-4 object it belongs to. Their closed vocabulary is position, power and lock. They are
  67. -- snap-initialized to a baseline and then transitioned without snap, which is the pattern
  68. -- that made the lever animate. Whether they actually report back is now observable:
  69. -- `ring_device_sense` in the log carries their native position and power.
  70. local structure_objects = {"SPECOPS_APEX_RING_CORE_OBJECT", "SPECOPS_APEX_RING_RING_OBJECT"}
  71. local beam_devices = {"SPECOPS_APEX_RING_LASER_DEVICE", "SPECOPS_APEX_RING_RING_DEVICE"}
  72. -- The latest live test establishes the beam's poses: close is normal, open is surge.
  73. -- Keep this mapping separate from the encounter clock and shutter cooling state.
  74. local function beam_pose(c, surging, snap)
  75. for _, name in ipairs(beam_devices) do a.device(c, name, surging, snap) end
  76. end
  77. -- Firing: the beam is present and both of its devices are powered.
  78. local function beam(c, s, firing, snap)
  79. -- Idempotent: redundant publications would spend intents from the callback's budget.
  80. if s:variable("ember.apex.beam") == firing then return end
  81. c:set_variable("ember.apex.beam", firing)
  82. a.objects(c, {"SPECOPS_APEX_RING_LASER_OBJECT"}, firing)
  83. for _, name in ipairs(beam_devices) do
  84. lane(c, name, firing and "power_on" or "power_off", snap)
  85. end
  86. if not firing then
  87. -- Installed but dark: the drive returns to its resting pose with the power.
  88. c:set_variable("ember.apex.surge", false)
  89. beam_pose(c, false, snap)
  90. end
  91. end
  92. -- The surge is the authored drive of a firing beam, so it never removes it. If these
  93. -- devices do move the emitter, this is the lane that shows it.
  94. local function beam_surge(c, s, on)
  95. if s:variable("ember.apex.beam") ~= true or s:variable("ember.apex.surge") == on then return end
  96. c:set_variable("ember.apex.surge", on)
  97. beam_pose(c, on, false)
  98. -- One rising edge owns both the beam drive and its authored alarm. There is no
  99. -- independent pre-roll clock that can drift into the shutter cooling window.
  100. if on then
  101. if phase(s) == 3 then
  102. for _, side in ipairs(sides) do
  103. if not dead(s, side) then a.slot(c, "REACTOR_CLAMSHELL_" .. side .. "_ALARM_SEQUENCE"):play_sequence{} end
  104. end
  105. elseif phase(s) == 4 then a.slot(c, "REACTOR_COFFIN_ALARM_SEQUENCE"):play_sequence{} end
  106. end
  107. end
  108. -- Native slot 43 substitutes the actual sunburn attachment 80B82489 for the
  109. -- Foundry thermal resource. One slot owns climb/escape damage: a filter revision
  110. -- detaches the old child before attaching the new one. Never combine it with the volume object.
  111. local function rail_filter(c) return {players = true, inside = a.slot(c, "SLOT_019E")} end
  112. local function hazards(c, s, mode)
  113. local wanted = mode or "off"
  114. if s:variable("ember.apex.hazard_mode") == wanted then return end
  115. c:set_variable("ember.apex.hazard_mode", wanted)
  116. a.objects(c, {"SUNBURN_DAMAGE_OBJECT"}, false)
  117. if mode == "climb" then
  118. -- The five narrow authored pipe volumes on the way up to the deposit; their heights
  119. -- track the climb the mother-brain dialogue volumes walk through, from z~172 at
  120. -- x=-395 up to z~186 at x=-460. Slot 7 is a broad kill volume well below the
  121. -- walkable route and is deliberately not a pipe.
  122. --
  123. -- The filter is the one that lives in the same object as those volumes. Pointing the
  124. -- apex-object filter at globals-object volumes published cleanly but attached
  125. -- nothing; this is the pairing that demonstrably delivered contact damage before.
  126. a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
  127. "REACTOR_MOTHER_BRAIN_HOT_PIPES_OBJECT_FILTER_80B3C09F",
  128. {players = true, inside_any = {
  129. a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_02_TRIGGER_VOLUME"),
  130. a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_03_TRIGGER_VOLUME"),
  131. a.slot(c, "SLOT_0005_80B3C09F"), a.slot(c, "SLOT_0006_80B3C09F"),
  132. a.slot(c, "SLOT_0008_80B3C09F")}}, true)
  133. else
  134. -- Reuse the same attachment owner on the authored escape rail volume.
  135. a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
  136. "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filter(c), mode == "escape")
  137. end
  138. end
  139. local function publish_explosions(c, s)
  140. local events = {}
  141. for i, event in ipairs(explosion_events) do
  142. if s:variable("ember.apex.explosion." .. i) then events[#events + 1] = event end
  143. end
  144. a.slot(c, explosion_scene):set_scene_events{
  145. generation = s:variable("ember.apex.explosion_generation"), events = events}
  146. end
  147. -- Scene activation alone supplies no event keys. Keep one generation alive and append
  148. -- the authored explosion_set_a_trigger .. explosion_set_d_trigger keys as we pass them.
  149. local function arm_escape(c, s)
  150. c:set_variable("ember.apex.explosion_generation", (s:variable("ember.apex.explosion_generation") or 0) + 1)
  151. for i in ipairs(explosion_events) do c:clear_variable("ember.apex.explosion." .. i) end
  152. publish_explosions(c, s)
  153. for _, name in ipairs(explosion_triggers) do a.slot(c, name):fire_trigger{} end
  154. for _, name in ipairs(escape_triggers) do a.slot(c, name):fire_trigger{} end
  155. end
  156. local function doors(c, side, open, snap)
  157. for _, part in ipairs({"DOOR_A", "DOOR_B", "LIGHT_A", "LIGHT_B", "TARGET"}) do
  158. local name = "REACTOR_CLAMSHELL_" .. side .. "_" .. part .. "_DEVICE"
  159. a.device(c, name, open, snap)
  160. if part == "LIGHT_A" or part == "LIGHT_B" or part == "TARGET" then
  161. a.slot(c, name):transition{transition = open and c.sdk.device_transitions.power_on
  162. or c.sdk.device_transitions.power_off, snap = snap or false}
  163. end
  164. end
  165. end
  166. local function target(c, s, name)
  167. a.slot(c, name .. "_TARGET_OBJECT"):set_interactable_object{generation = generation(s)}
  168. -- Bind on the object's presence receipt: Auth delivery and entity creation
  169. -- happen on different ticks. A missing/unloaded object is not a kill.
  170. end
  171. -- One encounter clock survives the clamshell -> coffin transition. Durations are
  172. -- reconstructed from the reference's warning, exposure and recovery windows.
  173. local function cycle(c, s, step, snap)
  174. c:set_variable("ember.apex.vent_step", step)
  175. c:set_variable("ember.apex.vents_open", step == "open")
  176. if step == "warning" then
  177. beam_surge(c, s, true)
  178. else
  179. -- Exposure is the cooling window: stop the surge before moving the shutters.
  180. beam_surge(c, s, false)
  181. if phase(s) == 3 then
  182. for _, side in ipairs(sides) do if not dead(s, side) then doors(c, side, step == "open", snap) end end
  183. elseif phase(s) == 4 then
  184. coffin_doors(c, step == "open", snap)
  185. if step == "open" then a.cue(c, s, 46) end
  186. end
  187. end
  188. c:start_timer(vent_timer(s), ({closed = 14000, warning = 6000, open = 10000})[step])
  189. end
  190. local function initialize_devices(c, s)
  191. if s:variable("ember.apex.devices_ready") then return end
  192. c:set_variable("ember.apex.devices_ready", true)
  193. a.objects(c, {"SUNBURN_DAMAGE_OBJECT"}, false)
  194. unlock(c, "SECURITY_DOOR_DEVICE")
  195. for _, side in ipairs(sides) do
  196. for _, part in ipairs({"DOOR_A", "DOOR_B"}) do unlock(c, "REACTOR_CLAMSHELL_" .. side .. "_" .. part .. "_DEVICE") end
  197. unlock(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE")
  198. lane(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", "lock")
  199. end
  200. lane(c, "MOTHER_BRAIN_DOOR_DEVICE", "lock")
  201. unlock(c, "SPECOPS_APEX_RING_LASER_DEVICE")
  202. unlock(c, "SPECOPS_APEX_RING_RING_DEVICE")
  203. -- The weapon is firing at the sun from the moment the area comes up. Snap the drive to
  204. -- its baseline here so every later surge is an animated transition, not a jump.
  205. beam(c, s, true, true)
  206. c:set_variable("ember.apex.surge", false)
  207. beam_pose(c, false, true)
  208. end
  209. local function start_reactor(c, s)
  210. if phase(s) >= 3 then return end
  211. initialize_devices(c, s)
  212. set(c, 3)
  213. a.checkpoint(c, 0, 0xDF59C25C, "reactor")
  214. a.darkness(c, s, true)
  215. a.device(c, "SECURITY_DOOR_DEVICE", true)
  216. for _, side in ipairs(sides) do target(c, s, "REACTOR_CLAMSHELL_" .. side) end
  217. a.spawn(c, s, {"reactor_east_entry", "reactor_west_entry"})
  218. a.cue(c, s, 40); cycle(c, s, "closed", true)
  219. c:start_timer("ember.apex.explain." .. generation(s), 9000)
  220. end
  221. local carry = require("mission_ember.carry")(a, "apex", "MOTHER_BRAIN_CARRY_OBJECT", "MOTHER_BRAIN_INTERACT_OBJECT",
  222. function(c, s) a.cue(c, s, 50); A.guidance(c, s) end,
  223. function(c, s)
  224. if phase(s) ~= 5 then return end
  225. set(c, 6)
  226. -- Disable the climb burn and enable only the authored escape hazard.
  227. hazards(c, s, "escape")
  228. a.checkpoint(c, 0, 0x45920385, "escape")
  229. a.device(c, "MOTHER_BRAIN_CONSOLE_DEVICE", true)
  230. a.device(c, "MOTHER_BRAIN_ENGINE_LEFT_DEVICE", true)
  231. a.device(c, "MOTHER_BRAIN_ENGINE_RIGHT_DEVICE", true)
  232. a.objects(c, {"REACTOR_GETAWAY_SHIP_OBJECT"}, true)
  233. -- Object publication is not an entity-creation receipt. Start its device from
  234. -- A.object once the ship is present, so the initial movement is not lost.
  235. c:clear_variable("ember.apex.ship_started")
  236. -- The weapon is dead once the cell is in: powered off but still installed, so
  237. -- the beam and everything built around it stay in the world. The previous code
  238. -- opened both devices here, which left it running through the whole escape.
  239. beam(c, s, false, true)
  240. c:cancel_timer(vent_timer(s))
  241. a.scene(c, "MOTHER_BRAIN_HOLE_EXPLOSION_SCENE")
  242. arm_escape(c, s)
  243. a.cue(c, s, 51); A.guidance(c, s)
  244. end)
  245. function A.enter(c, s)
  246. if s:variable("ember.apex.restart_pending") then
  247. c:clear_variable("ember.apex.restart_pending")
  248. start_reactor(c, s); A.guidance(c, s); return
  249. end
  250. if phase(s) > 0 then return end
  251. set(c, 1)
  252. c:start_timer("ember.apex.setup." .. generation(s), 1)
  253. a.device(c, "ACCESS_DOOR_INNER_DEVICE", true)
  254. a.device(c, "ACCESS_DOOR_OUTER_DEVICE", true)
  255. a.device(c, "SECURITY_DOOR_DEVICE", false, true)
  256. a.slot(c, "SECURITY_PLACED_INTERCEPTOR_OBJECT"):set_interactable_object{generation = 1}
  257. -- The weapon's structure is placed once and is never taken back out of the world.
  258. a.objects(c, structure_objects, true)
  259. for _, side in ipairs(sides) do
  260. doors(c, side, false, true)
  261. a.device(c, "CLAMSHELL_PIPES_" .. side .. "_DEVICE", false, true)
  262. a.device(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE", true, true)
  263. a.device(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", false, true)
  264. end
  265. a.device(c, "REACTOR_SHIELD_DEVICE", false, true)
  266. a.device(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE", false, true)
  267. a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", false, true)
  268. for _, name in ipairs(triggers) do a.slot(c, name):fire_trigger{} end
  269. a.spawn(c, s, {"access1"}); A.guidance(c, s)
  270. end
  271. function A.trigger(c, s, e)
  272. if phase(s) <= 2 then
  273. if a.matches(c, e, "ACCESS_JUMP_TWO_PLAYER_TRIGGER") then a.spawn(c, s, {"access2"}) end
  274. if a.matches(c, e, "SECURITY_CENTER_PLAYER_TRIGGER") or a.matches(c, e, "SECURITY_LEDGE_PLAYER_TRIGGER")
  275. or a.matches(c, e, "SECURITY_DOOR_PLAYER_TRIGGER") then
  276. set(c, 2); a.spawn(c, s, access)
  277. a.device(c, "DISPENSER_MONSTER_CLOSET_LIGHT_DEVICE", true)
  278. if not s:variable("ember.apex.dispenser") then
  279. c:set_variable("ember.apex.dispenser", true)
  280. a.slot(c, "DISPENSER_MONSTER_CLOSET_SEQUENCE"):play_sequence{}
  281. end
  282. a.cue(c, s, 38)
  283. end
  284. end
  285. if a.matches(c, e, "APEX_DIRECTIVE_REACTOR_GOTO_PLAYER_TRIGGER")
  286. or a.matches(c, e, "APEX_REACTOR_CLAMSHELL_001_DIALOG_START_PLAYER_TRIGGER")
  287. or a.matches(c, e, "REACTOR_ARM_EAST_ENTRANCE_PLAYER_TRIGGER") then start_reactor(c, s) end
  288. -- The explanatory cue follows the arrival line, even if its trigger was crossed in the same tick.
  289. if phase(s) == 5 and a.matches(c, e, "DIALOG_APEX_MOTHER_BRAIN_001_PLAYER_TRIGGER") then a.cue(c, s, 50) end
  290. if phase(s) == 6 then
  291. for i, name in ipairs(explosion_triggers) do
  292. if a.matches(c, e, name) and not s:variable("ember.apex.explosion." .. i) then
  293. c:set_variable("ember.apex.explosion." .. i, true)
  294. publish_explosions(c, s)
  295. end
  296. end
  297. if a.matches(c, e, "APEX_MOTHER_BRAIN_006_DIALOG_PLAYER_TRIGGER") then a.cue(c, s, 52) end
  298. if a.matches(c, e, "APEX_MOTHER_BRAIN_007_DIALOG_PLAYER_TRIGGER") then a.cue(c, s, 53) end
  299. if a.matches(c, e, "APEX_MOTHER_BRAIN_008_DIALOG_PLAYER_TRIGGER") then a.cue(c, s, 54) end
  300. if a.matches(c, e, "APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_PLAYER_TRIGGER") then
  301. set(c, 7); a.darkness(c, s, false)
  302. hazards(c, s, nil)
  303. c:cancel_timer("ember.apex.hazards")
  304. c:cancel_timer(vent_timer(s))
  305. ending.start(c, s)
  306. end
  307. end
  308. A.guidance(c, s)
  309. end
  310. function A.cleared(c, s, name)
  311. if name == "access1" and phase(s) <= 2 then a.spawn(c, s, {"access2"}) end
  312. if name == "electron_controllers" and phase(s) <= 2 then
  313. unlock(c, "SECURITY_DOOR_DEVICE")
  314. a.device(c, "SECURITY_DOOR_DEVICE", true)
  315. end
  316. if phase(s) == 3 or phase(s) == 4 then
  317. for _, side in ipairs({"east", "west"}) do
  318. if name == "reactor_" .. side .. "_entry" then a.spawn(c, s, {"reactor_" .. side .. "_reinforce"}) end
  319. if name == "reactor_" .. side .. "_reinforce" then a.spawn(c, s, {"reactor_" .. side .. "_final"}) end
  320. end
  321. end
  322. A.guidance(c, s)
  323. end
  324. local function destroyed(c, s, which)
  325. if dead(s, which) or (which == "COFFIN" and phase(s) ~= 4)
  326. or (which ~= "COFFIN" and phase(s) ~= 3) then return end
  327. c:set_variable("ember.apex.dead." .. which, true)
  328. if which ~= "COFFIN" then
  329. doors(c, which, true)
  330. a.scene(c, "APEX_REACTOR_CLAMSHELL_" .. which .. "_DESTROYED_SCENE")
  331. a.device(c, "CLAMSHELL_PIPES_" .. which .. "_DEVICE", true)
  332. a.cue(c, s, dead(s, "EAST") and dead(s, "WEST") and 44 or 43)
  333. if dead(s, "EAST") and dead(s, "WEST") then
  334. set(c, 4) -- Keep the current warning/exposure deadline for the central reactor.
  335. -- Remaining authored waves stay alive and retain their objectives.
  336. a.spawn(c, s, {"reactor_east_reinforce", "reactor_west_reinforce", "reactor_east_final", "reactor_west_final"})
  337. c:start_timer("ember.apex.core." .. generation(s), 1)
  338. end
  339. else
  340. set(c, 5)
  341. c:cancel_timer(vent_timer(s)); c:cancel_timer("ember.apex.explain." .. generation(s))
  342. coffin_doors(c, true)
  343. -- The weapon keeps firing until the cell goes in.
  344. beam(c, s, true)
  345. -- Arm the climb scorch now: the pipes burn while the cell is carried up.
  346. c:start_timer("ember.apex.hazards", 1)
  347. unlock(c, "MOTHER_BRAIN_DOOR_DEVICE")
  348. unlock(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE")
  349. a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", true)
  350. a.device(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE", true)
  351. carry.start(c, s); a.cue(c, s, 48)
  352. end
  353. A.guidance(c, s)
  354. end
  355. function A.damage(c, s, e)
  356. if e.revision ~= generation(s) or e.health == nil or e.health ~= e.health or e.health < 0 then return end
  357. local which
  358. for _, side in ipairs(sides) do if a.matches(c, e, "REACTOR_CLAMSHELL_" .. side .. "_TARGET_DAMAGE") then which = side end end
  359. if a.matches(c, e, "REACTOR_COFFIN_TARGET_DAMAGE") then which = "COFFIN" end
  360. if not which or dead(s, which) or (which == "COFFIN" and phase(s) ~= 4)
  361. or (which ~= "COFFIN" and phase(s) ~= 3) then return end
  362. local seen = "ember.apex.healthy." .. which
  363. if e.health > 0 then c:set_variable(seen, true); return end
  364. if not s:variable(seen) then return end -- Absent/unloaded targets are never destruction receipts.
  365. destroyed(c, s, which)
  366. end
  367. function A.timer(c, s, e)
  368. if e.timer_name == "ember.apex.hazards" then
  369. -- One attachment owner moves from climb pipes (phase 5) to escape rails (phase 6).
  370. -- Entering escape revises the filter and removes attachments from the old filter.
  371. if s:variable("ember.region") == 0 then
  372. local p = phase(s)
  373. if p == 5 then hazards(c, s, "climb")
  374. elseif p == 6 then hazards(c, s, "escape")
  375. else hazards(c, s, nil) end
  376. end
  377. return true
  378. end
  379. if carry.timer(c, s, e) then return true end
  380. if e.timer_name == "ember.apex.setup." .. generation(s) then
  381. if s:variable("ember.region") == 0 then initialize_devices(c, s) end
  382. return true
  383. end
  384. if e.timer_name == "ember.apex.explain." .. generation(s) then
  385. if phase(s) == 3 and s:variable("ember.region") == 0 then a.cue(c, s, 41); A.guidance(c, s) end
  386. return true
  387. end
  388. if e.timer_name == vent_timer(s) then
  389. if (phase(s) == 3 or phase(s) == 4) and s:variable("ember.region") == 0 then
  390. cycle(c, s, ({closed = "warning", warning = "open", open = "closed"})[s:variable("ember.apex.vent_step")] or "closed")
  391. A.guidance(c, s)
  392. end
  393. return true
  394. end
  395. if e.timer_name == "ember.apex.core." .. generation(s) and phase(s) == 4 then
  396. if s:variable("ember.region") ~= 0 or s:variable("ember.apex.core_ready") then return true end
  397. c:set_variable("ember.apex.core_ready", true)
  398. bridges(c, true)
  399. for _, side in ipairs(sides) do unlock(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE") end
  400. unlock(c, "REACTOR_SHIELD_DEVICE")
  401. coffin_doors(c, s:variable("ember.apex.vents_open") == true)
  402. target(c, s, "REACTOR_COFFIN")
  403. a.spawn(c, s, {"coffin_east", "coffin_west", "coffin_interior"})
  404. A.guidance(c, s); return true
  405. end
  406. return false
  407. end
  408. function A.guidance(c, s)
  409. if s:variable("ember.region") ~= 0 then return end
  410. local p = phase(s)
  411. if p == 1 then a.directive(c, s, "4E4862BB", "SECURITY_INTERCEPTOR_NAV_POINT")
  412. elseif p == 2 then a.directive(c, s, s:variable("ember.apex.interceptor_boarded") and "4E4862BB" or "3CBFC90B",
  413. s:variable("ember.apex.interceptor_boarded") and "APEX_DIRECTIVE_REACTOR_CLAMSHELL_GOTO_NAV_POINT" or "SECURITY_INTERCEPTOR_NAV_POINT")
  414. elseif p == 3 then a.directive(c, s, "26C3C19D", "APEX_DIRECTIVE_REACTOR_CLAMSHELL_GOTO_NAV_POINT")
  415. elseif p == 4 then a.directive(c, s, "61D2B286", "APEX_DIRECTIVE_REACTOR_COFFIN_TARGET_NAV_POINT")
  416. elseif p == 5 then a.directive(c, s, "4746660F",
  417. carry.held(s) and "EMBER_DIRECTIVE_REACTOR_MOTHER_BRAIN_DELIVERY_NAV_POINT" or "APEX_WEAPON_NAV_POINT")
  418. elseif p == 6 then a.directive(c, s, "40FC40AD", "APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_NAV_POINT") end
  419. end
  420. function A.reset(c, s, name)
  421. if name == "escape" then
  422. c:start_timer("ember.apex.hazards", 1)
  423. set(c, 6)
  424. -- The weapon is already dead at this checkpoint: restoring it must leave the
  425. -- beam off and re-arm the escape's own progress triggers.
  426. beam(c, s, false, true)
  427. arm_escape(c, s)
  428. a.darkness(c, s, true)
  429. else
  430. c:cancel_timer(vent_timer(s)); c:cancel_timer("ember.apex.core." .. generation(s))
  431. c:cancel_timer("ember.apex.explain." .. generation(s)); c:cancel_timer("ember.apex.setup." .. generation(s))
  432. a.reset(c, reactor); carry.reset(c, s)
  433. c:set_variable("ember.apex.generation", generation(s) + 2)
  434. c:clear_variable("ember.apex.core_ready")
  435. for _, side in ipairs({"EAST", "WEST", "COFFIN"}) do
  436. c:clear_variable("ember.apex.dead." .. side); c:clear_variable("ember.apex.healthy." .. side)
  437. end
  438. for _, name in ipairs({"EMBER_APEX_REACTOR_CLAMSHELL_EAST_OBJECTIVE", "EMBER_APEX_REACTOR_CLAMSHELL_WEST_OBJECTIVE",
  439. "EMBER_APEX_REACTOR_COFFIN_OBJECTIVE"}) do a.slot(c, name):reset_objectives{} end
  440. a.objects(c, {"REACTOR_COFFIN_TARGET_OBJECT"}, false)
  441. for _, side in ipairs(sides) do
  442. a.device(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE", true, true)
  443. a.device(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", false, true)
  444. lane(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", "lock")
  445. end
  446. lane(c, "MOTHER_BRAIN_DOOR_DEVICE", "lock")
  447. a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", false, true)
  448. a.device(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE", false, true)
  449. a.device(c, "REACTOR_SHIELD_DEVICE", false, true)
  450. -- Detaching the hazard has its own callback: retiring 45 squads already spends
  451. -- most of this one's native intent budget.
  452. c:start_timer("ember.apex.hazards", 1)
  453. set(c, 2)
  454. -- Publish the reset now; repopulate on the first playable client report
  455. -- after the wipe handshake. A one-millisecond timer can expire during the fade.
  456. c:set_variable("ember.apex.restart_pending", true)
  457. end
  458. c:clear_variable("ember.r.guidance")
  459. end
  460. function A.resume(c, s)
  461. carry.resume(c, s)
  462. if phase(s) == 4 and not s:variable("ember.apex.core_ready") then
  463. c:start_timer("ember.apex.core." .. generation(s), 1)
  464. end
  465. a.darkness(c, s, phase(s) >= 3 and phase(s) <= 6)
  466. -- Streaming back into the area re-attaches the hazard this phase owns.
  467. if phase(s) == 5 or phase(s) == 6 then c:start_timer("ember.apex.hazards", 1) end
  468. if phase(s) == 3 or phase(s) == 4 then
  469. cycle(c, s, "closed", true)
  470. if phase(s) == 3 and not s:variable("ember.r.cue.41") then c:start_timer("ember.apex.explain." .. generation(s), 9000) end
  471. end
  472. end
  473. function A.interaction(c, s, e)
  474. if a.matches(c, e, "SECURITY_PLACED_INTERCEPTOR_OBJECT") and e.generation == 1
  475. and not s:variable("ember.apex.interceptor_boarded") then
  476. c:set_variable("ember.apex.interceptor_boarded", true)
  477. end
  478. carry.interaction(c, s, e)
  479. A.guidance(c, s)
  480. end
  481. function A.object(c, s, e)
  482. carry.state(c, s, e)
  483. if e.present and e.alive then
  484. if a.matches(c, e, "SPECOPS_APEX_RING_LASER_OBJECT")
  485. and s:variable("ember.apex.beam") == true
  486. and s:variable("ember.apex.beam_primed") ~= e.generation then
  487. c:set_variable("ember.apex.beam_primed", e.generation)
  488. for _, name in ipairs(beam_devices) do unlock(c, name) end
  489. -- Seeking directly to the resting endpoint left the emitter dark until
  490. -- its first animated cycle. Traverse the authored drive once on creation
  491. -- so its effect events run, then leave normal surge timing in charge.
  492. beam_pose(c, true, true)
  493. if s:variable("ember.apex.surge") ~= true then beam_pose(c, false, false) end
  494. end
  495. if a.matches(c, e, "REACTOR_GETAWAY_SHIP_OBJECT") and phase(s) == 6
  496. and not s:variable("ember.apex.ship_started") then
  497. c:set_variable("ember.apex.ship_started", true)
  498. unlock(c, "REACTOR_GETAWAY_SHIP_DEVICE")
  499. a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", false, true)
  500. a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", true)
  501. end
  502. end
  503. if e.generation == generation(s) then
  504. for _, which in ipairs({"EAST", "WEST", "COFFIN"}) do
  505. local prefix = which == "COFFIN" and "REACTOR_COFFIN" or ("REACTOR_CLAMSHELL_" .. which)
  506. if a.matches(c, e, prefix .. "_TARGET_OBJECT") and not dead(s, which)
  507. and ((which == "COFFIN" and phase(s) == 4) or (which ~= "COFFIN" and phase(s) == 3)) then
  508. local seen = "ember.apex.healthy." .. which
  509. if e.present and e.alive then
  510. if not s:variable(seen) then
  511. c:set_variable(seen, true)
  512. a.slot(c, prefix .. "_TARGET_DAMAGE"):watch_damage{
  513. target = a.slot(c, prefix .. "_TARGET_OBJECT"), revision = generation(s)}
  514. end
  515. elseif e.present and e.alive == false and s:variable(seen) then
  516. destroyed(c, s, which)
  517. end
  518. end
  519. end
  520. end
  521. A.guidance(c, s)
  522. end
  523. return A
  524. end