route_support.lua 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. -- Shared, transactional helpers for the later mission. No progress is kept in module locals.
  2. local Encounter = require("mission_ember.encounter")
  3. return function(mission, on_clear)
  4. local api = {groups = {}}
  5. local music = require("mission_ember.music")(mission)
  6. local directives = {
  7. ["4746660F"] = mission.Directive.DELIVER_THE_FINAL_BLOW_TO_THE_ALMIGHTY_S_WEAPON_SYSTEMS,
  8. ["40FC40AD"] = mission.Directive.ESCAPE_THE_ALMIGHTY,
  9. ["03285502"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS,
  10. ["127B96D6"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_127B96D6,
  11. ["2700C0C5"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_2700C0C5,
  12. ["3CBFC90B"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_3CBFC90B,
  13. ["4E4862BB"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_4E4862BB,
  14. ["4EA80049"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_4EA80049,
  15. ["57050F62"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_57050F62,
  16. ["591B1D88"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_591B1D88,
  17. ["62E3AEFB"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_62E3AEFB,
  18. ["65D5979D"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_65D5979D,
  19. ["6D8E0897"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_6D8E0897,
  20. ["7FF69D75"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_7FF69D75,
  21. ["883C188D"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_883C188D,
  22. ["A70DA4A6"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_A70DA4A6,
  23. ["AFBF882A"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_AFBF882A,
  24. ["BAD7D583"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_BAD7D583,
  25. ["D496059B"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_D496059B,
  26. ["DF93A91C"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_DF93A91C,
  27. ["ECECF63D"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_ECECF63D,
  28. ["FF7AB219"] = mission.Directive.FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_FF7AB219,
  29. ["26C3C19D"] = mission.Directive.SABOTAGE_THE_ALMIGHTY_S_WEAPON_SYSTEMS,
  30. ["61D2B286"] = mission.Directive.SABOTAGE_THE_ALMIGHTY_S_WEAPON_SYSTEMS_61D2B286,
  31. ["6C7F39DC"] = mission.Directive.SABOTAGE_THE_ALMIGHTY_S_WEAPON_SYSTEMS_6C7F39DC,
  32. ["9DD48A8A"] = mission.Directive.SABOTAGE_THE_ALMIGHTY_S_WEAPON_SYSTEMS_9DD48A8A,
  33. }
  34. local roster = require("mission_ember.route_roster")
  35. for _, row in ipairs(roster) do
  36. local squads = {}
  37. for index, name in ipairs(row.squads) do
  38. squads[#squads + 1] = {name = name, id = assert(mission.Squad[name], name),
  39. sensor = assert(mission.Slot[name], name), objective = assert(mission.Slot[row.objective], row.objective),
  40. task_groups = row.task_groups, fixed_task = row.fixed_tasks and row.fixed_tasks[index]}
  41. end
  42. api.groups[row.name] = Encounter.new("ember.r." .. row.name, squads,
  43. function(c, s) on_clear(c, s, row.name) end)
  44. end
  45. function api.slot(c, name) return c:slot(assert(mission.Slot[name], "missing Ember slot: " .. name)) end
  46. function api.matches(c, event, name)
  47. local slot = api.slot(c, name)
  48. return event.registry_key == slot.registry_key and event.slot_type == slot.slot_type
  49. and event.slot_index == slot.slot_index
  50. end
  51. function api.spawn(c, s, names)
  52. for _, name in ipairs(names) do assert(api.groups[name], name):enter(c, s) end
  53. end
  54. function api.clear(s, names)
  55. for _, name in ipairs(names) do if api.groups[name]:phase(s) ~= 2 then return false end end
  56. return true
  57. end
  58. function api.combat(s, names)
  59. for _, name in ipairs(names) do if api.groups[name]:phase(s) == 1 then return true end end
  60. return false
  61. end
  62. function api.reset(c, names)
  63. for _, name in ipairs(names) do api.groups[name]:reset(c) end
  64. end
  65. function api.squad(c, s, event)
  66. for _, row in ipairs(roster) do api.groups[row.name]:on_squad_state(c, s, event) end
  67. end
  68. function api.device(c, name, open, snap)
  69. api.slot(c, name):transition{transition = open and c.sdk.device_transitions.open or c.sdk.device_transitions.close,
  70. snap = snap or false}
  71. end
  72. -- Position is only one of a type-23's three lanes; power and lock are the others, and a
  73. -- device that is never unlocked and powered stays inert however its position is driven.
  74. function api.lane(c, name, transition, snap)
  75. api.slot(c, name):transition{transition = c.sdk.device_transitions[transition], snap = snap or false}
  76. end
  77. -- The Almighty's weapon beam is an object AND the device that drives it. Placing the object
  78. -- alone leaves a dead prop, so every region that shows the beam enables its device too.
  79. function api.enable_device(c, name)
  80. api.lane(c, name, "unlock"); api.lane(c, name, "power_on")
  81. end
  82. function api.objects(c, names, active)
  83. -- Each is an exact type-4 entry at its package transform, including native components.
  84. for _, name in ipairs(names) do api.slot(c, name):set_object_active{active = active} end
  85. end
  86. function api.cue(c, s, index)
  87. local key = "ember.r.cue." .. index
  88. if s:variable(key) then return end
  89. c:set_variable(key, true)
  90. api.slot(c, "M_DIALOG_SENSOR_80B3C90A"):play_dialogue_cue{
  91. cue = assert(mission.DialogueCue.M_DIALOG_SENSOR_80B3C90A["CUE_" .. index])}
  92. end
  93. function api.scene(c, name) c:scene(assert(mission.Scene[name], name)):activate{} end
  94. function api.darkness(c, s, enabled)
  95. if s:variable("ember.darkness.enabled") == enabled then return end
  96. c:set_variable("ember.darkness.enabled", enabled)
  97. api.slot(c, "HARD_WIPE_GLOBALS"):set_darkness_zone{enabled = enabled}
  98. end
  99. function api.effect(c, s, effect, filter, selection, enabled)
  100. local key = "ember.effect." .. effect
  101. local revision = (s:variable(key) or 0) + 1
  102. c:set_variable(key, revision)
  103. if enabled then api.slot(c, filter):set_object_filter(selection) end
  104. api.slot(c, effect):set_mission_effect{filter = enabled and api.slot(c, filter) or nil,
  105. enabled = enabled, revision = revision}
  106. end
  107. function api.checkpoint(c, region, hash, name)
  108. c:set_variable("ember.checkpoint.region", region)
  109. c:set_variable("ember.checkpoint.hash", hash)
  110. c:set_variable("ember.checkpoint.name", name)
  111. end
  112. -- Each authored directive carries both HUD lines: `title` is the main objective and
  113. -- `description` the sub-objective, so the banner advances by selecting a new element.
  114. -- Native type-68 state 0 ENTERS the directive, and the Lua surface always publishes it,
  115. -- so every republication replays the banner's entry animation. Republishing whenever the
  116. -- navpoint changed therefore made the objective flash on each combat transition while its
  117. -- text stayed put. The directive is now keyed on its element alone and carries one stable
  118. -- navpoint, so the body changes only at a real milestone and enters exactly once there.
  119. function api.directive(c, s, hash, marker, combat)
  120. local rank = ({[56] = 1, [40] = 2, [0] = 3})[s:variable("ember.region")] or 0
  121. if rank < (s:variable("ember.r.furthest") or 0) then return end
  122. music.update(c, s)
  123. local shown = marker or ""
  124. local signature = hash .. ":" .. shown
  125. if s:variable("ember.r.guidance") == signature then return end
  126. local binding = assert(directives[hash], "missing authored directive " .. hash)
  127. local region = s:variable("ember.region")
  128. local engagement = ({[0] = "M_ENGAGEMENT_SENSOR_80B3C21C",
  129. [40] = "M_ENGAGEMENT_SENSOR_80B3C22D", [56] = "M_ENGAGEMENT_SENSOR_80B3C8F7"})[region]
  130. local audience = engagement and api.slot(c, engagement)
  131. if audience and not s:variable("ember.hud.audience." .. region) then
  132. -- Native 9F2A00 includes active players when flag bit0 is clear.
  133. audience:set_engagement_state{flags = 0, revision = 1}
  134. c:set_variable("ember.hud.audience." .. region, true)
  135. end
  136. api.slot(c, "M_DIRECTIVE_SENSOR_80B3C90A"):set_directive{
  137. directive = binding, audience = audience, navpoint = shown ~= "" and api.slot(c, shown) or nil}
  138. c:set_variable("ember.r.guidance", signature)
  139. end
  140. return api
  141. end