apex.lua 29 KB

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