apex.lua 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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. They were never armed, so the sequence
  22. -- had nothing to advance it and everything the scene did happened at the deposit.
  23. local explosion_triggers = {}
  24. for _, set in ipairs({"A", "B", "C", "D"}) do
  25. explosion_triggers[#explosion_triggers + 1] =
  26. "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_EXPLOSION_SET_" .. set .. "_PLAYER_TRIGGER"
  27. end
  28. local function phase(s) return s:variable("ember.apex.phase") or 0 end
  29. local function set(c, p) c:set_variable("ember.apex.phase", p) end
  30. local function generation(s) return s:variable("ember.apex.generation") or 1 end
  31. local function dead(s, target) return s:variable("ember.apex.dead." .. target) == true end
  32. local function vent_timer(s) return "ember.apex.vents." .. generation(s) end
  33. local function lane(c, name, transition, snap)
  34. a.slot(c, name):transition{transition = c.sdk.device_transitions[transition], snap = snap or false}
  35. end
  36. local function unlock(c, name)
  37. lane(c, name, "unlock"); lane(c, name, "power_on")
  38. end
  39. local function bridges(c, extended, snap)
  40. -- As with the landing bridge, the authored position-0 pose is extended.
  41. for _, side in ipairs(sides) do
  42. a.device(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE", not extended, snap)
  43. end
  44. end
  45. local function coffin_doors(c, open, snap)
  46. for _, side in ipairs(sides) do
  47. a.device(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", open, snap)
  48. a.device(c, "REACTOR_COFFIN_LIGHT_" .. side .. "_DEVICE", open, snap)
  49. lane(c, "REACTOR_COFFIN_LIGHT_" .. side .. "_DEVICE", open and "power_on" or "power_off", snap)
  50. end
  51. a.device(c, "REACTOR_SHIELD_DEVICE", open, snap)
  52. end
  53. -- The three placed ring objects ARE the weapon's structure and stay active for the whole
  54. -- mission: deactivating them removes the beam and everything built around it, rather than
  55. -- dimming it. The two devices are its lanes, so the beam has three authored states:
  56. -- off -- present but dead: powered off, the pose the weapon holds once the cell is in.
  57. -- idle -- powered and running, the beam's baseline between exposures.
  58. -- surge -- powered and driven open, the intensification before the shutters expose a target.
  59. local ring_objects = {"SPECOPS_APEX_RING_LASER_OBJECT", "SPECOPS_APEX_RING_CORE_OBJECT",
  60. "SPECOPS_APEX_RING_RING_OBJECT"}
  61. local BEAM_OFF, BEAM_IDLE, BEAM_SURGE = 0, 1, 2
  62. local function beam(c, s, mode, snap)
  63. -- Idempotent: the cycle re-asserts the same pose every step, and each redundant
  64. -- publication would spend intents from the callback's native budget.
  65. if s:variable("ember.apex.beam") == mode then return end
  66. c:set_variable("ember.apex.beam", mode)
  67. local powered = mode ~= BEAM_OFF
  68. for _, name in ipairs({"SPECOPS_APEX_RING_LASER_DEVICE", "SPECOPS_APEX_RING_RING_DEVICE"}) do
  69. a.device(c, name, mode == BEAM_SURGE, snap)
  70. -- FX power is independent of mechanical position, so the weapon can stay
  71. -- installed and lit, installed and dark, or driven wide open.
  72. lane(c, name, powered and "power_on" or "power_off", snap)
  73. end
  74. end
  75. -- `REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON` and `FOUNDRY_THERMAL_DOT_HOP_ON` both reference
  76. -- effect resource 80C1D9E0 -- the burn already working in the Foundry. The hot-pipe and
  77. -- rail-top hop-ons carry 80B82484 and 80C1D389 instead, which is why contact read as a
  78. -- shock rather than a scorch. Apex drives the one authored scorch for both hazards and
  79. -- swaps only its filter, so the climb and the escape share the same effect. A new revision
  80. -- removes the previous attachment (native 9EF8A0/9F1F10) before attaching the new filter.
  81. local function rail_filter(c) return {players = true, inside = a.slot(c, "SLOT_019E")} end
  82. local function hazards(c, s, mode)
  83. if mode == "climb" then
  84. -- The five narrow authored pipe volumes on the way up to the deposit; their heights
  85. -- track the climb the mother-brain dialogue volumes walk through, from z~172 at
  86. -- x=-395 up to z~186 at x=-460. Slot 7 is a broad kill volume well below the
  87. -- walkable route and is deliberately not a pipe.
  88. --
  89. -- The filter is the one that lives in the same object as those volumes. Pointing the
  90. -- apex-object filter at globals-object volumes published cleanly but attached
  91. -- nothing; this is the pairing that demonstrably delivered contact damage before.
  92. a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
  93. "REACTOR_MOTHER_BRAIN_HOT_PIPES_OBJECT_FILTER_80B3C09F",
  94. {players = true, inside_any = {
  95. a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_02_TRIGGER_VOLUME"),
  96. a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_03_TRIGGER_VOLUME"),
  97. a.slot(c, "SLOT_0005_80B3C09F"), a.slot(c, "SLOT_0006_80B3C09F"),
  98. a.slot(c, "SLOT_0008_80B3C09F")}}, true)
  99. elseif mode == "escape" then
  100. a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
  101. "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filter(c), true)
  102. else
  103. a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
  104. "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filter(c), false)
  105. end
  106. end
  107. -- Activate the authored explosion scene once, then arm its own four progress triggers
  108. -- alongside the escape dialogue volumes so each section detonates as the player reaches it.
  109. local function arm_escape(c)
  110. a.scene(c, "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_TRIGGERED_EXPLOSIONS_SCENE")
  111. for _, name in ipairs(explosion_triggers) do a.slot(c, name):fire_trigger{} end
  112. for _, name in ipairs(escape_triggers) do a.slot(c, name):fire_trigger{} end
  113. end
  114. local function doors(c, side, open, snap)
  115. for _, part in ipairs({"DOOR_A", "DOOR_B", "LIGHT_A", "LIGHT_B", "TARGET"}) do
  116. local name = "REACTOR_CLAMSHELL_" .. side .. "_" .. part .. "_DEVICE"
  117. a.device(c, name, open, snap)
  118. if part == "LIGHT_A" or part == "LIGHT_B" or part == "TARGET" then
  119. a.slot(c, name):transition{transition = open and c.sdk.device_transitions.power_on
  120. or c.sdk.device_transitions.power_off, snap = snap or false}
  121. end
  122. end
  123. end
  124. local function target(c, s, name)
  125. a.slot(c, name .. "_TARGET_OBJECT"):set_interactable_object{generation = generation(s)}
  126. -- Bind on the object's presence receipt: Auth delivery and entity creation
  127. -- happen on different ticks. A missing/unloaded object is not a kill.
  128. end
  129. -- One encounter clock survives the clamshell -> coffin transition. Durations are
  130. -- reconstructed from the reference's warning, exposure and recovery windows.
  131. local function cycle(c, s, step, snap)
  132. c:set_variable("ember.apex.vent_step", step)
  133. c:set_variable("ember.apex.vents_open", step == "open")
  134. if step == "warning" then
  135. beam(c, s, BEAM_SURGE)
  136. if phase(s) == 3 then
  137. for _, side in ipairs(sides) do
  138. if not dead(s, side) then a.slot(c, "REACTOR_CLAMSHELL_" .. side .. "_ALARM_SEQUENCE"):play_sequence{} end
  139. end
  140. else a.slot(c, "REACTOR_COFFIN_ALARM_SEQUENCE"):play_sequence{} end
  141. else
  142. if step == "closed" then beam(c, s, BEAM_IDLE, snap) end
  143. if phase(s) == 3 then
  144. for _, side in ipairs(sides) do if not dead(s, side) then doors(c, side, step == "open", snap) end end
  145. elseif phase(s) == 4 then
  146. coffin_doors(c, step == "open", snap)
  147. if step == "open" then a.cue(c, s, 46) end
  148. end
  149. end
  150. c:start_timer(vent_timer(s), ({closed = 14000, warning = 6000, open = 10000})[step])
  151. end
  152. local function initialize_devices(c, s)
  153. if s:variable("ember.apex.devices_ready") then return end
  154. c:set_variable("ember.apex.devices_ready", true)
  155. unlock(c, "SECURITY_DOOR_DEVICE")
  156. for _, side in ipairs(sides) do
  157. for _, part in ipairs({"DOOR_A", "DOOR_B"}) do unlock(c, "REACTOR_CLAMSHELL_" .. side .. "_" .. part .. "_DEVICE") end
  158. unlock(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE")
  159. lane(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", "lock")
  160. end
  161. lane(c, "MOTHER_BRAIN_DOOR_DEVICE", "lock")
  162. unlock(c, "SPECOPS_APEX_RING_LASER_DEVICE")
  163. unlock(c, "SPECOPS_APEX_RING_RING_DEVICE")
  164. beam(c, s, BEAM_IDLE, true)
  165. end
  166. local function start_reactor(c, s)
  167. if phase(s) >= 3 then return end
  168. initialize_devices(c, s)
  169. set(c, 3)
  170. a.checkpoint(c, 0, 0xDF59C25C, "reactor")
  171. a.darkness(c, s, true)
  172. a.device(c, "SECURITY_DOOR_DEVICE", true)
  173. for _, side in ipairs(sides) do target(c, s, "REACTOR_CLAMSHELL_" .. side) end
  174. a.spawn(c, s, {"reactor_east_entry", "reactor_west_entry"})
  175. a.cue(c, s, 40); cycle(c, s, "closed", true)
  176. c:start_timer("ember.apex.explain." .. generation(s), 9000)
  177. end
  178. local carry = require("mission_ember.carry")(a, "apex", "MOTHER_BRAIN_CARRY_OBJECT", "MOTHER_BRAIN_INTERACT_OBJECT",
  179. function(c, s) a.cue(c, s, 50); A.guidance(c, s) end,
  180. function(c, s)
  181. if phase(s) ~= 5 then return end
  182. set(c, 6)
  183. c:start_timer("ember.apex.hazards", 1)
  184. a.checkpoint(c, 0, 0x45920385, "escape")
  185. a.device(c, "MOTHER_BRAIN_CONSOLE_DEVICE", true)
  186. a.device(c, "MOTHER_BRAIN_ENGINE_LEFT_DEVICE", true)
  187. a.device(c, "MOTHER_BRAIN_ENGINE_RIGHT_DEVICE", true)
  188. a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", true)
  189. a.objects(c, {"REACTOR_GETAWAY_SHIP_OBJECT", "SUNBURN_DAMAGE_OBJECT"}, true)
  190. -- The weapon is dead once the cell is in: powered off but still installed, so
  191. -- the beam and everything built around it stay in the world. The previous code
  192. -- opened both devices here, which left it running through the whole escape.
  193. beam(c, s, BEAM_OFF, true)
  194. c:cancel_timer(vent_timer(s))
  195. a.scene(c, "MOTHER_BRAIN_HOLE_EXPLOSION_SCENE")
  196. arm_escape(c)
  197. a.cue(c, s, 51); A.guidance(c, s)
  198. end)
  199. function A.enter(c, s)
  200. if s:variable("ember.apex.restart_pending") then
  201. c:clear_variable("ember.apex.restart_pending")
  202. start_reactor(c, s); A.guidance(c, s); return
  203. end
  204. if phase(s) > 0 then return end
  205. set(c, 1)
  206. c:start_timer("ember.apex.setup." .. generation(s), 1)
  207. a.device(c, "ACCESS_DOOR_INNER_DEVICE", true)
  208. a.device(c, "ACCESS_DOOR_OUTER_DEVICE", true)
  209. a.device(c, "SECURITY_DOOR_DEVICE", false, true)
  210. a.slot(c, "SECURITY_PLACED_INTERCEPTOR_OBJECT"):set_interactable_object{generation = 1}
  211. -- The weapon's structure is placed once and stays for the whole mission; `beam()`
  212. -- only changes its power and drive.
  213. a.objects(c, ring_objects, true)
  214. for _, side in ipairs(sides) do
  215. doors(c, side, false, true)
  216. a.device(c, "CLAMSHELL_PIPES_" .. side .. "_DEVICE", false, true)
  217. a.device(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE", true, true)
  218. a.device(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", false, true)
  219. end
  220. a.device(c, "REACTOR_SHIELD_DEVICE", false, true)
  221. a.device(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE", false, true)
  222. a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", false, true)
  223. for _, name in ipairs(triggers) do a.slot(c, name):fire_trigger{} end
  224. a.spawn(c, s, {"access1"}); A.guidance(c, s)
  225. end
  226. function A.trigger(c, s, e)
  227. if phase(s) <= 2 then
  228. if a.matches(c, e, "ACCESS_JUMP_TWO_PLAYER_TRIGGER") then a.spawn(c, s, {"access2"}) end
  229. if a.matches(c, e, "SECURITY_CENTER_PLAYER_TRIGGER") or a.matches(c, e, "SECURITY_LEDGE_PLAYER_TRIGGER")
  230. or a.matches(c, e, "SECURITY_DOOR_PLAYER_TRIGGER") then
  231. set(c, 2); a.spawn(c, s, access)
  232. a.device(c, "DISPENSER_MONSTER_CLOSET_LIGHT_DEVICE", true)
  233. if not s:variable("ember.apex.dispenser") then
  234. c:set_variable("ember.apex.dispenser", true)
  235. a.slot(c, "DISPENSER_MONSTER_CLOSET_SEQUENCE"):play_sequence{}
  236. end
  237. a.cue(c, s, 38)
  238. end
  239. end
  240. if a.matches(c, e, "APEX_DIRECTIVE_REACTOR_GOTO_PLAYER_TRIGGER")
  241. or a.matches(c, e, "APEX_REACTOR_CLAMSHELL_001_DIALOG_START_PLAYER_TRIGGER")
  242. or a.matches(c, e, "REACTOR_ARM_EAST_ENTRANCE_PLAYER_TRIGGER") then start_reactor(c, s) end
  243. -- The explanatory cue follows the arrival line, even if its trigger was crossed in the same tick.
  244. if phase(s) == 5 and a.matches(c, e, "DIALOG_APEX_MOTHER_BRAIN_001_PLAYER_TRIGGER") then a.cue(c, s, 50) end
  245. if phase(s) == 6 then
  246. if a.matches(c, e, "APEX_MOTHER_BRAIN_006_DIALOG_PLAYER_TRIGGER") then a.cue(c, s, 52) end
  247. if a.matches(c, e, "APEX_MOTHER_BRAIN_007_DIALOG_PLAYER_TRIGGER") then a.cue(c, s, 53) end
  248. if a.matches(c, e, "APEX_MOTHER_BRAIN_008_DIALOG_PLAYER_TRIGGER") then a.cue(c, s, 54) end
  249. if a.matches(c, e, "APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_PLAYER_TRIGGER") then
  250. set(c, 7); a.darkness(c, s, false)
  251. hazards(c, s, nil)
  252. c:cancel_timer("ember.apex.hazards")
  253. c:cancel_timer(vent_timer(s))
  254. ending.start(c, s)
  255. end
  256. end
  257. A.guidance(c, s)
  258. end
  259. function A.cleared(c, s, name)
  260. if name == "access1" and phase(s) <= 2 then a.spawn(c, s, {"access2"}) end
  261. if name == "electron_controllers" and phase(s) <= 2 then
  262. unlock(c, "SECURITY_DOOR_DEVICE")
  263. a.device(c, "SECURITY_DOOR_DEVICE", true)
  264. end
  265. if phase(s) == 3 or phase(s) == 4 then
  266. for _, side in ipairs({"east", "west"}) do
  267. if name == "reactor_" .. side .. "_entry" then a.spawn(c, s, {"reactor_" .. side .. "_reinforce"}) end
  268. if name == "reactor_" .. side .. "_reinforce" then a.spawn(c, s, {"reactor_" .. side .. "_final"}) end
  269. end
  270. end
  271. A.guidance(c, s)
  272. end
  273. local function destroyed(c, s, which)
  274. if dead(s, which) or (which == "COFFIN" and phase(s) ~= 4)
  275. or (which ~= "COFFIN" and phase(s) ~= 3) then return end
  276. c:set_variable("ember.apex.dead." .. which, true)
  277. if which ~= "COFFIN" then
  278. doors(c, which, true)
  279. a.scene(c, "APEX_REACTOR_CLAMSHELL_" .. which .. "_DESTROYED_SCENE")
  280. a.device(c, "CLAMSHELL_PIPES_" .. which .. "_DEVICE", true)
  281. a.cue(c, s, dead(s, "EAST") and dead(s, "WEST") and 44 or 43)
  282. if dead(s, "EAST") and dead(s, "WEST") then
  283. set(c, 4) -- Keep the current warning/exposure deadline for the central reactor.
  284. -- Remaining authored waves stay alive and retain their objectives.
  285. a.spawn(c, s, {"reactor_east_reinforce", "reactor_west_reinforce", "reactor_east_final", "reactor_west_final"})
  286. c:start_timer("ember.apex.core." .. generation(s), 1)
  287. end
  288. else
  289. set(c, 5)
  290. c:cancel_timer(vent_timer(s)); c:cancel_timer("ember.apex.explain." .. generation(s))
  291. coffin_doors(c, true)
  292. -- The weapon keeps running until the cell goes in; the exposure cycle is over,
  293. -- so it settles back to its baseline rather than holding the surge.
  294. beam(c, s, BEAM_IDLE)
  295. -- Arm the climb scorch now: the pipes burn while the cell is carried up.
  296. c:start_timer("ember.apex.hazards", 1)
  297. unlock(c, "MOTHER_BRAIN_DOOR_DEVICE")
  298. unlock(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE")
  299. a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", true)
  300. a.device(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE", true)
  301. carry.start(c, s); a.cue(c, s, 48)
  302. end
  303. A.guidance(c, s)
  304. end
  305. function A.damage(c, s, e)
  306. if e.revision ~= generation(s) or e.health == nil or e.health ~= e.health or e.health < 0 then return end
  307. local which
  308. for _, side in ipairs(sides) do if a.matches(c, e, "REACTOR_CLAMSHELL_" .. side .. "_TARGET_DAMAGE") then which = side end end
  309. if a.matches(c, e, "REACTOR_COFFIN_TARGET_DAMAGE") then which = "COFFIN" end
  310. if not which or dead(s, which) or (which == "COFFIN" and phase(s) ~= 4)
  311. or (which ~= "COFFIN" and phase(s) ~= 3) then return end
  312. local seen = "ember.apex.healthy." .. which
  313. if e.health > 0 then c:set_variable(seen, true); return end
  314. if not s:variable(seen) then return end -- Absent/unloaded targets are never destruction receipts.
  315. destroyed(c, s, which)
  316. end
  317. function A.timer(c, s, e)
  318. if e.timer_name == "ember.apex.hazards" then
  319. -- The climb pipes burn while the cell is being carried up (phase 5), well before
  320. -- the deposit; the rail top burns during the escape (phase 6). Any earlier phase
  321. -- owns no hazard, so this is also how a checkpoint reset detaches one.
  322. if s:variable("ember.region") == 0 then
  323. local p = phase(s)
  324. if p == 5 then hazards(c, s, "climb")
  325. elseif p == 6 then hazards(c, s, "escape")
  326. else hazards(c, s, nil) end
  327. end
  328. return true
  329. end
  330. if carry.timer(c, s, e) then return true end
  331. if e.timer_name == "ember.apex.setup." .. generation(s) then
  332. if s:variable("ember.region") == 0 then initialize_devices(c, s) end
  333. return true
  334. end
  335. if e.timer_name == "ember.apex.explain." .. generation(s) then
  336. if phase(s) == 3 and s:variable("ember.region") == 0 then a.cue(c, s, 41); A.guidance(c, s) end
  337. return true
  338. end
  339. if e.timer_name == vent_timer(s) then
  340. if (phase(s) == 3 or phase(s) == 4) and s:variable("ember.region") == 0 then
  341. cycle(c, s, ({closed = "warning", warning = "open", open = "closed"})[s:variable("ember.apex.vent_step")] or "closed")
  342. A.guidance(c, s)
  343. end
  344. return true
  345. end
  346. if e.timer_name == "ember.apex.core." .. generation(s) and phase(s) == 4 then
  347. if s:variable("ember.region") ~= 0 or s:variable("ember.apex.core_ready") then return true end
  348. c:set_variable("ember.apex.core_ready", true)
  349. bridges(c, true)
  350. for _, side in ipairs(sides) do unlock(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE") end
  351. unlock(c, "REACTOR_SHIELD_DEVICE")
  352. coffin_doors(c, s:variable("ember.apex.vents_open") == true)
  353. target(c, s, "REACTOR_COFFIN")
  354. a.spawn(c, s, {"coffin_east", "coffin_west", "coffin_interior"})
  355. A.guidance(c, s); return true
  356. end
  357. return false
  358. end
  359. function A.guidance(c, s)
  360. if s:variable("ember.region") ~= 0 then return end
  361. local p = phase(s)
  362. if p == 1 then a.directive(c, s, "4E4862BB", "SECURITY_INTERCEPTOR_NAV_POINT")
  363. elseif p == 2 then a.directive(c, s, s:variable("ember.apex.interceptor_boarded") and "4E4862BB" or "3CBFC90B",
  364. s:variable("ember.apex.interceptor_boarded") and "APEX_DIRECTIVE_REACTOR_CLAMSHELL_GOTO_NAV_POINT" or "SECURITY_INTERCEPTOR_NAV_POINT")
  365. elseif p == 3 then a.directive(c, s, "26C3C19D", "APEX_DIRECTIVE_REACTOR_CLAMSHELL_GOTO_NAV_POINT")
  366. elseif p == 4 then a.directive(c, s, "61D2B286", "APEX_DIRECTIVE_REACTOR_COFFIN_TARGET_NAV_POINT")
  367. elseif p == 5 then a.directive(c, s, "4746660F",
  368. carry.held(s) and "EMBER_DIRECTIVE_REACTOR_MOTHER_BRAIN_DELIVERY_NAV_POINT" or "APEX_WEAPON_NAV_POINT")
  369. elseif p == 6 then a.directive(c, s, "40FC40AD", "APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_NAV_POINT") end
  370. end
  371. function A.reset(c, s, name)
  372. if name == "escape" then
  373. c:start_timer("ember.apex.hazards", 1)
  374. set(c, 6)
  375. -- The weapon is already dead at this checkpoint: restoring it must leave the
  376. -- beam off and re-arm the escape's own progress triggers.
  377. beam(c, s, BEAM_OFF, true)
  378. arm_escape(c)
  379. a.darkness(c, s, true)
  380. else
  381. c:cancel_timer(vent_timer(s)); c:cancel_timer("ember.apex.core." .. generation(s))
  382. c:cancel_timer("ember.apex.explain." .. generation(s)); c:cancel_timer("ember.apex.setup." .. generation(s))
  383. a.reset(c, reactor); carry.reset(c, s)
  384. c:set_variable("ember.apex.generation", generation(s) + 2)
  385. c:clear_variable("ember.apex.core_ready")
  386. for _, side in ipairs({"EAST", "WEST", "COFFIN"}) do
  387. c:clear_variable("ember.apex.dead." .. side); c:clear_variable("ember.apex.healthy." .. side)
  388. end
  389. for _, name in ipairs({"EMBER_APEX_REACTOR_CLAMSHELL_EAST_OBJECTIVE", "EMBER_APEX_REACTOR_CLAMSHELL_WEST_OBJECTIVE",
  390. "EMBER_APEX_REACTOR_COFFIN_OBJECTIVE"}) do a.slot(c, name):reset_objectives{} end
  391. a.objects(c, {"REACTOR_COFFIN_TARGET_OBJECT"}, false)
  392. for _, side in ipairs(sides) do
  393. a.device(c, "CLAMSHELL_TO_COFFIN_" .. side .. "_BRIDGE_DEVICE", true, true)
  394. a.device(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", false, true)
  395. lane(c, "REACTOR_COFFIN_DOOR_" .. side .. "_DEVICE", "lock")
  396. end
  397. lane(c, "MOTHER_BRAIN_DOOR_DEVICE", "lock")
  398. a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", false, true)
  399. a.device(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE", false, true)
  400. a.device(c, "REACTOR_SHIELD_DEVICE", false, true)
  401. -- Detaching the hazard has its own callback: retiring 45 squads already spends
  402. -- most of this one's native intent budget.
  403. c:start_timer("ember.apex.hazards", 1)
  404. set(c, 2)
  405. -- Publish the reset now; repopulate on the first playable client report
  406. -- after the wipe handshake. A one-millisecond timer can expire during the fade.
  407. c:set_variable("ember.apex.restart_pending", true)
  408. end
  409. c:clear_variable("ember.r.guidance")
  410. end
  411. function A.resume(c, s)
  412. carry.resume(c, s)
  413. if phase(s) == 4 and not s:variable("ember.apex.core_ready") then
  414. c:start_timer("ember.apex.core." .. generation(s), 1)
  415. end
  416. a.darkness(c, s, phase(s) >= 3 and phase(s) <= 6)
  417. -- Streaming back into the area re-attaches the hazard this phase owns.
  418. if phase(s) == 5 or phase(s) == 6 then c:start_timer("ember.apex.hazards", 1) end
  419. if phase(s) == 3 or phase(s) == 4 then
  420. cycle(c, s, "closed", true)
  421. if phase(s) == 3 and not s:variable("ember.r.cue.41") then c:start_timer("ember.apex.explain." .. generation(s), 9000) end
  422. end
  423. end
  424. function A.interaction(c, s, e)
  425. if a.matches(c, e, "SECURITY_PLACED_INTERCEPTOR_OBJECT") and e.generation == 1
  426. and not s:variable("ember.apex.interceptor_boarded") then
  427. c:set_variable("ember.apex.interceptor_boarded", true)
  428. end
  429. carry.interaction(c, s, e)
  430. A.guidance(c, s)
  431. end
  432. function A.object(c, s, e)
  433. carry.state(c, s, e)
  434. if e.generation == generation(s) then
  435. for _, which in ipairs({"EAST", "WEST", "COFFIN"}) do
  436. local prefix = which == "COFFIN" and "REACTOR_COFFIN" or ("REACTOR_CLAMSHELL_" .. which)
  437. if a.matches(c, e, prefix .. "_TARGET_OBJECT") and not dead(s, which)
  438. and ((which == "COFFIN" and phase(s) == 4) or (which ~= "COFFIN" and phase(s) == 3)) then
  439. local seen = "ember.apex.healthy." .. which
  440. if e.present and e.alive then
  441. if not s:variable(seen) then
  442. c:set_variable(seen, true)
  443. a.slot(c, prefix .. "_TARGET_DAMAGE"):watch_damage{
  444. target = a.slot(c, prefix .. "_TARGET_OBJECT"), revision = generation(s)}
  445. end
  446. elseif e.present and e.alive == false and s:variable(seen) then
  447. destroyed(c, s, which)
  448. end
  449. end
  450. end
  451. end
  452. A.guidance(c, s)
  453. end
  454. return A
  455. end