encounter.lua 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. -- One encounter's progress lives in transactional mission state, never module-local tables.
  2. local AI = require("mission_ember.combat_ai")
  3. local Encounter = {}
  4. Encounter.__index = Encounter
  5. function Encounter.new(tag, squads, cleared, reserved)
  6. assert(#squads > 0 and #squads <= 30, "encounter needs 1..30 squads")
  7. return setmetatable({tag = tag, squads = squads, cleared = cleared,
  8. full = (1 << #squads) - 1, reserved = reserved or false}, Encounter)
  9. end
  10. function Encounter:phase(state)
  11. return state:variable(self.tag .. ".phase") or 0
  12. end
  13. function Encounter:enter(context, state)
  14. if self:phase(state) ~= 0 then
  15. return false
  16. end
  17. -- Resolve everything before staging the first placement. Missing SDK rows are errors.
  18. local handles = {}
  19. for index, squad in ipairs(self.squads) do
  20. handles[index] = context:squad(squad.id)
  21. local count = 0
  22. for _, value in ipairs(handles[index].default_counts) do
  23. assert(value >= 0, "squad has unresolved authored counts: " .. squad.name)
  24. count = count + value
  25. end
  26. assert(count > 0, "encounter squad has no authored members: " .. squad.name)
  27. end
  28. context:set_variable(self.tag .. ".seen", 0)
  29. context:set_variable(self.tag .. ".alive", 0)
  30. context:set_variable(self.tag .. ".phase", 1)
  31. for index, handle in ipairs(handles) do
  32. handle:place{mode = self.reserved and context.sdk.squad_modes.reserve or context.sdk.squad_modes.replace}
  33. local squad = self.squads[index]
  34. if squad.objective then
  35. AI.assign(context, squad.sensor, squad.objective, squad.fixed_task or -1, self.reserved)
  36. local word = (index - 1) // 12
  37. if (index - 1) % 12 == 0 then context:set_variable(self.tag .. ".ai." .. word, 0) end
  38. end
  39. end
  40. return true
  41. end
  42. function Encounter:on_squad_state(context, state, event)
  43. if self:phase(state) ~= 1 then
  44. return
  45. end
  46. local bit, matched
  47. for index, squad in ipairs(self.squads) do
  48. local slot = context:slot(squad.sensor)
  49. if event.registry_key == slot.registry_key and event.slot_type == slot.slot_type
  50. and event.slot_index == slot.slot_index then
  51. bit = 1 << (index - 1)
  52. matched = squad
  53. if squad.objective and squad.fixed_task == nil then
  54. AI.update(context, state, self.tag .. ".ai." .. ((index - 1) // 12),
  55. squad.sensor, squad.objective, squad.task_groups, event, (index - 1) % 12 + 1, self.reserved)
  56. end
  57. break
  58. end
  59. end
  60. if bit == nil or event.alive_count == nil then
  61. return
  62. end
  63. local seen = state:variable(self.tag .. ".seen") or 0
  64. local alive = state:variable(self.tag .. ".alive") or 0
  65. -- Native A9CAA0/4E5C80 publish root ordinal 8 from the squad's state flags.
  66. -- It is also set on real deaths; it does not identify a squad unload.
  67. -- Require evidence of positive population before accepting a zero count.
  68. if event.alive_count > 0 or (event.previous_alive_count or 0) > 0 then
  69. seen = seen | bit
  70. end
  71. -- Sense .11 counts consumed requests (native 4E8660/4E8200), not births.
  72. -- Zero living actors while requests remain is an incomplete placement, not
  73. -- a cleared wave. Keep that squad outstanding until every authored slot is
  74. -- accounted for, including waves whose members arrive over several frames.
  75. local accounted = event.slot_counts ~= nil
  76. for index, requested in ipairs(context:squad(matched.id).default_counts) do
  77. if not event.slot_counts or (event.slot_counts[index] or -1) < requested then
  78. accounted = false
  79. end
  80. end
  81. if event.alive_count > 0 or not accounted then
  82. alive = alive | bit
  83. else
  84. alive = alive & ~bit
  85. end
  86. context:set_variable(self.tag .. ".seen", seen)
  87. context:set_variable(self.tag .. ".alive", alive)
  88. if seen == self.full and alive == 0 then
  89. context:set_variable(self.tag .. ".phase", 2)
  90. -- Retire the native requested counts as well as the Lua encounter. The host
  91. -- republishes retained Auth when regions return; a completed wave must retain
  92. -- zero requests, not its original spawn composition.
  93. for _, squad in ipairs(self.squads) do
  94. local handle = context:squad(squad.id)
  95. local counts = handle:counts()
  96. for index = 1, #handle.default_counts do counts:set(index, 0) end
  97. handle:place{counts = counts,
  98. mode = self.reserved and context.sdk.squad_modes.reserve or context.sdk.squad_modes.replace}
  99. end
  100. self.cleared(context, state)
  101. end
  102. end
  103. -- Called only by a confirmed checkpoint reset, not by region transit or script reload.
  104. function Encounter:reset(context)
  105. for _, squad in ipairs(self.squads) do
  106. local handle = context:squad(squad.id)
  107. local counts = handle:counts()
  108. for index = 1, #handle.default_counts do counts:set(index, 0) end
  109. handle:place{counts = counts,
  110. mode = self.reserved and context.sdk.squad_modes.reserve or context.sdk.squad_modes.replace}
  111. end
  112. context:clear_variable(self.tag .. ".phase")
  113. context:clear_variable(self.tag .. ".seen")
  114. context:clear_variable(self.tag .. ".alive")
  115. end
  116. return Encounter