| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- -- One encounter's progress lives in transactional mission state, never module-local tables.
- local AI = require("mission_ember.combat_ai")
- local Encounter = {}
- Encounter.__index = Encounter
- function Encounter.new(tag, squads, cleared, reserved)
- assert(#squads > 0 and #squads <= 30, "encounter needs 1..30 squads")
- return setmetatable({tag = tag, squads = squads, cleared = cleared,
- full = (1 << #squads) - 1, reserved = reserved or false}, Encounter)
- end
- function Encounter:phase(state)
- return state:variable(self.tag .. ".phase") or 0
- end
- function Encounter:enter(context, state)
- if self:phase(state) ~= 0 then
- return false
- end
- -- Resolve everything before staging the first placement. Missing SDK rows are errors.
- local handles = {}
- for index, squad in ipairs(self.squads) do
- handles[index] = context:squad(squad.id)
- local count = 0
- for _, value in ipairs(handles[index].default_counts) do
- assert(value >= 0, "squad has unresolved authored counts: " .. squad.name)
- count = count + value
- end
- assert(count > 0, "encounter squad has no authored members: " .. squad.name)
- end
- context:set_variable(self.tag .. ".seen", 0)
- context:set_variable(self.tag .. ".alive", 0)
- context:set_variable(self.tag .. ".phase", 1)
- for index, handle in ipairs(handles) do
- handle:place{mode = self.reserved and context.sdk.squad_modes.reserve or context.sdk.squad_modes.replace}
- local squad = self.squads[index]
- if squad.objective then
- AI.assign(context, squad.sensor, squad.objective, squad.fixed_task or -1, self.reserved)
- local word = (index - 1) // 12
- if (index - 1) % 12 == 0 then context:set_variable(self.tag .. ".ai." .. word, 0) end
- end
- end
- return true
- end
- function Encounter:on_squad_state(context, state, event)
- if self:phase(state) ~= 1 then
- return
- end
- local bit, matched
- for index, squad in ipairs(self.squads) do
- local slot = context:slot(squad.sensor)
- if event.registry_key == slot.registry_key and event.slot_type == slot.slot_type
- and event.slot_index == slot.slot_index then
- bit = 1 << (index - 1)
- matched = squad
- if squad.objective and squad.fixed_task == nil then
- AI.update(context, state, self.tag .. ".ai." .. ((index - 1) // 12),
- squad.sensor, squad.objective, squad.task_groups, event, (index - 1) % 12 + 1, self.reserved)
- end
- break
- end
- end
- if bit == nil or event.alive_count == nil then
- return
- end
- local seen = state:variable(self.tag .. ".seen") or 0
- local alive = state:variable(self.tag .. ".alive") or 0
- -- Native A9CAA0/4E5C80 publish root ordinal 8 from the squad's state flags.
- -- It is also set on real deaths; it does not identify a squad unload.
- -- Require evidence of positive population before accepting a zero count.
- if event.alive_count > 0 or (event.previous_alive_count or 0) > 0 then
- seen = seen | bit
- end
- -- Sense .11 counts consumed requests (native 4E8660/4E8200), not births.
- -- Zero living actors while requests remain is an incomplete placement, not
- -- a cleared wave. Keep that squad outstanding until every authored slot is
- -- accounted for, including waves whose members arrive over several frames.
- local accounted = event.slot_counts ~= nil
- for index, requested in ipairs(context:squad(matched.id).default_counts) do
- if not event.slot_counts or (event.slot_counts[index] or -1) < requested then
- accounted = false
- end
- end
- if event.alive_count > 0 or not accounted then
- alive = alive | bit
- else
- alive = alive & ~bit
- end
- context:set_variable(self.tag .. ".seen", seen)
- context:set_variable(self.tag .. ".alive", alive)
- if seen == self.full and alive == 0 then
- context:set_variable(self.tag .. ".phase", 2)
- -- Retire the native requested counts as well as the Lua encounter. The host
- -- republishes retained Auth when regions return; a completed wave must retain
- -- zero requests, not its original spawn composition.
- for _, squad in ipairs(self.squads) do
- local handle = context:squad(squad.id)
- local counts = handle:counts()
- for index = 1, #handle.default_counts do counts:set(index, 0) end
- handle:place{counts = counts,
- mode = self.reserved and context.sdk.squad_modes.reserve or context.sdk.squad_modes.replace}
- end
- self.cleared(context, state)
- end
- end
- -- Called only by a confirmed checkpoint reset, not by region transit or script reload.
- function Encounter:reset(context)
- for _, squad in ipairs(self.squads) do
- local handle = context:squad(squad.id)
- local counts = handle:counts()
- for index = 1, #handle.default_counts do counts:set(index, 0) end
- handle:place{counts = counts,
- mode = self.reserved and context.sdk.squad_modes.reserve or context.sdk.squad_modes.replace}
- end
- context:clear_variable(self.tag .. ".phase")
- context:clear_variable(self.tag .. ".seen")
- context:clear_variable(self.tag .. ".alive")
- end
- return Encounter
|