| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- package.path = "scripts/?.lua;" .. package.path
- local roster = require("mission_ember.roster")
- local order = {"catwalk_entry", "catwalk_mid", "pipe", "mercury", "mercury_bonus", "bridge", "dropships", "passengers", "sun", "helipad"}
- local names = {}
- local unique = {}
- for _, group in ipairs(order) do
- for _, name in ipairs(roster[group]) do
- assert(not unique[name], "duplicate authored squad")
- unique[name] = true
- names[#names + 1] = name
- end
- end
- assert(#names == 42)
- local triggers = {
- "CATWALK_015_PERCENT_PLAYER_TRIGGER", "CATWALK_035_PERCENT_PLAYER_TRIGGER_80B3CC33",
- "CATWALK_100_PERCENT_PLAYER_TRIGGER", "PIPE_CROSSING_040_PERCENT_PLAYER_TRIGGER",
- "LANDING_MERCURY_040_PERCENT_PLAYER_TRIGGER",
- "BRIDGE_025_PERCENT_PLAYER_TRIGGER", "BRIDGE_075_PERCENT_PLAYER_TRIGGER", "BRIDGE_100_PERCENT_PLAYER_TRIGGER",
- }
- local mission = {
- Squad = {}, Slot = {M_ENGAGEMENT_SENSOR_80B3CB5E = "engagement", M_DIRECTIVE_SENSOR_80B3C90A = "directive", M_MUSIC_SENSOR_80B3C90A = "music"},
- states = {
- STATE_80B3C09E_0008_0000_80B3C09C = {region_index = 64},
- STATE_80B3C09E_0006_0001_80B3C09A = {region_index = 49},
- },
- DialogueCue = {M_DIALOG_SENSOR_80B3C90A = {CUE_0 = 0, CUE_2 = 2, CUE_3 = 3}},
- Directive = {
- FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_4EA80049 = "follow",
- FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_A70DA4A6 = "clear",
- FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_2700C0C5 = "controls",
- FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_AFBF882A = "cross",
- },
- }
- for _, name in ipairs(names) do
- mission.Squad[name] = name
- mission.Slot[name] = name
- end
- for _, name in ipairs(triggers) do mission.Slot[name] = name end
- local markerNames = {"POWERHOUSE_DIRECTIVE_LANDING_MERCURY_CLEAR_NAV_POINT",
- "POWERHOUSE_DIRECTIVE_LANDING_MERCURY_INTERACT_NAV_POINT", "HELIPAD_SUN_SQUAD_FORMATION_NAV_POINT"}
- for index, name in ipairs(markerNames) do
- mission.Slot[name] = {registry_key = 77, slot_type = 47, slot_index = index}
- end
- local navpoints = {}
- local flights, deliveries, retiredShips = {}, {}, 0
- local exitActions = {}
- local darkness = {}
- local beamLanes = {}
- mission.Slot.HARD_WIPE_GLOBALS = "darkness"
- -- The Almighty's weapon beam is placed in every region that shows it, and its type-23 device
- -- must be unlocked and powered or the beam is left a dead prop.
- mission.Slot.POWERHOUSE_CORE_WEAPON_LASER_BEAM_DEVICE = "powerhouse_beam"
- local timers, timerStarts, now = {}, 0, 0
- for index, letter in ipairs({"A", "B", "C", "D"}) do
- local prefix = "BRIDGE_CROSSING_DROPSHIP_" .. letter
- for _, suffix in ipairs({"_SQUAD_HARVESTER", "_ENTRY_SEQUENCE", "_EXIT_SEQUENCE"}) do
- mission.Slot[prefix .. suffix] = {registry_key = 7, slot_type = suffix == "_SQUAD_HARVESTER" and 2 or 58,
- slot_index = index, play_actor_path = function(_, args) flights[#flights + 1] = args end,
- deliver_squads = function(_, args)
- assert(index <= 2, "off-bridge ships cannot receive troops")
- assert(args.generation % 2 == 1 and args.revision == 1 and #args.squads == 2)
- assert(args.squads[1].slot_index == 33 + index and args.squads[2].slot_index == 35 + index,
- "each bridge ship carries its support and melee squad")
- deliveries[#deliveries + 1] = args
- end,
- play_actor_action = function(_, args)
- assert(args.generation % 2 == 1 and args.revision == 3,
- "exit action must preserve the ship and advance only the command revision")
- assert(args.group == 0x811C9DC5 and args.action == 0x7B0D3643)
- exitActions[#exitActions + 1] = args
- end,
- retire_actor = function(_, args)
- assert(args.generation % 2 == 0, "retirement must advance the native lifecycle"); retiredShips = retiredShips + 1
- end}
- end
- end
- local assignments = {}
- for _, area in ipairs({"CATWALK", "PIPE_CROSSING", "LANDING_MERCURY", "BRIDGE", "LANDING_SUN", "HELIPAD_SUN"}) do
- local key = "EMBER_POWERHOUSE_" .. area .. "_OBJECTIVE"
- mission.Slot[key] = {objective_name = key}
- end
- local armed = {}
- local devices = {
- "POWERHOUSE_BRIDGE_ARM_MERCURY_DEVICE", "POWERHOUSE_BRIDGE_GEAR_MERCURY_BOTTOM_DEVICE",
- "POWERHOUSE_BRIDGE_GEAR_MERCURY_TOP_DEVICE", "POWERHOUSE_BRIDGE_ARM_SUN_DEVICE",
- "POWERHOUSE_BRIDGE_GEAR_SUN_BOTTOM_DEVICE", "POWERHOUSE_BRIDGE_GEAR_SUN_TOP_DEVICE",
- }
- for _, name in ipairs(devices) do mission.Slot[name] = name end
- mission.Slot.EMBER_POWERHOUSE_BRIDGE_OBJECTIVE = "bridge_objective"
- mission.Slot.LANDING_MERCURY_CONSOLE_BUTTON_GHOST_LINK = "ghost"
- mission.Slot.LANDING_MERCURY_DOOR_BUTTON_OBJECT = "lever"
- mission.Slot.POWERHOUSE_LANDING_MERCURY_DOOR_DEVICE = "door"
- mission.Slot.POWERHOUSE_LANDING_MERCURY_DOOR_LEVER_DEVICE = "lever_device"
- mission.Slot.PF_CINEMATIC_BOOKEND_CINEMATIC = "cinematic"
- mission.Slot.M_DIALOG_SENSOR_80B3C90A = "dialogue"
- package.preload.missions = function() return {MISSION_EMBER = "test_mission"} end
- package.preload.test_mission = function() return mission end
- -- Match the production sandbox: unordered iteration is deliberately unavailable.
- pairs = nil
- next = nil
- -- This fixture deliberately supplies only the 42 Powerhouse squads. Later controllers
- -- are exercised separately against the complete generated SDK.
- package.preload["mission_ember.routes"] = function() return function() return {
- client = function() end, dispatch = function() end, squad = function() end, terminated = function() end,
- } end end
- local program = require("mission_ember")
- assert(program.initial_state == mission.states.STATE_80B3C09E_0006_0001_80B3C09A,
- "fresh launches must declare the authored cinematic state")
- local vars, directives, placements, seeds = {}, {}, 0, 0
- local retirements = 0
- local variable_count, peak_variables = 0, 0
- local state = {variable = function(_, key) return vars[key] end}
- local closed, resets, movies, cues = {}, 0, {}, {}
- local ghost, door, extended = {}, {}, 0
- local leverAnimation = {}
- local context = {sdk = {squad_modes = {replace = "replace", reserve = "reserve"}, device_transitions = {close = "close", open = "open", unlock = "unlock", lock = "lock", power_on = "power_on", power_off = "power_off"}}}
- function context:start_timer(name, delay)
- assert(delay == (name:find("depart", 1, true) and 4000 or 6000),
- "unload adds three seconds to the prior three-second settle; departure holds four seconds")
- timers[name] = now + delay
- timerStarts = timerStarts + 1
- end
- local function tick(time)
- now = time
- for _, stage in ipairs({"unload", "depart"}) do
- for index = 1, 4 do
- local name = "ember.transport." .. stage .. "." .. index
- if timers[name] and now >= timers[name] then
- timers[name] = nil
- program.on_event_timer_elapsed(context, state, {timer_name = name})
- end
- end
- end
- end
- function context:set_phase(value) vars.phase = value end
- function context:set_variable(key, value)
- if vars[key] == nil then variable_count = variable_count + 1 end
- peak_variables = math.max(peak_variables, variable_count)
- assert(variable_count <= 512, "mission variable capacity exceeded")
- vars[key] = value
- end
- function context:clear_variable(key)
- if vars[key] ~= nil then variable_count = variable_count - 1 end
- vars[key] = nil
- end
- function context:select_state(entry)
- assert(entry.region_index == 64)
- seeds = seeds + 1
- end
- function context:squad(name)
- assert(mission.Squad[name])
- return {default_counts = {1},
- counts = function() return {set = function(self, index, value) assert(index == 1 and value == 0); self.zero = true end} end,
- place = function(_, args)
- if name:find("DROPSHIP_[ABCD]_SQUAD$") then assert(args.counts.zero) end
- if name:find("DROPSHIP_A_SUPPORT") or name:find("DROPSHIP_A_MELEE") then
- assert(args.mode == "reserve", "cargo cannot use ordinary ground placement")
- else assert(args.mode == "replace") end
- if args.counts and args.counts.zero and not name:find("DROPSHIP_[ABCD]_SQUAD$") then
- retirements = retirements + 1
- else placements = placements + 1 end
- end}
- end
- function context:slot(name)
- if name == "music" then return {set_music_section = function(_, args) assert(args.section >= 0 and args.section <= 28) end} end
- if name == "darkness" then return {set_darkness_zone = function(_, args) darkness[#darkness + 1] = args.enabled end} end
- if type(name) == "table" then return name end
- for index, trigger in ipairs(triggers) do
- if name == trigger then
- return {registry_key = 10, slot_type = 31, slot_index = index,
- fire_trigger = function() armed[index] = (armed[index] or 0) + 1 end}
- end
- end
- if name == "cinematic" then
- return {registry_key = 9, slot_type = 6, slot_index = 0,
- set_cinematic_active = function(_, args) movies[#movies + 1] = args.active end}
- end
- if name == "dialogue" then
- return {play_dialogue_cue = function(_, args) cues[#cues + 1] = args.cue end}
- end
- for index, device in ipairs(devices) do
- if name == device then
- return {registry_key = 13, slot_type = 23, slot_index = index, transition = function(_, args)
- if args.snap then
- assert(args.transition == "open")
- closed[#closed + 1] = name
- else
- assert(args.transition == "close")
- extended = extended + 1
- end
- end}
- end
- end
- if name == "lever" then
- return {registry_key = 12, slot_type = 4, slot_index = 19,
- set_interactable_object = function(_, args) assert(args.generation == 1) end}
- end
- if name == "ghost" then
- return {registry_key = 12, slot_type = 65, slot_index = 60,
- set_ghost_link = function(_, args) ghost[#ghost + 1] = args end}
- end
- if name == "powerhouse_beam" then
- return {transition = function(_, args) beamLanes[#beamLanes + 1] = args.transition end}
- end
- if name == "lever_device" then
- return {transition = function(_, args) leverAnimation[#leverAnimation+1] = args end}
- end
- if name == "door" then
- return {transition = function(_, args) door[#door + 1] = args end}
- end
- if name == "bridge_objective" then
- return {objective_name = "bridge_objective", reset_objectives = function() resets = resets + 1 end}
- end
- if name == "engagement" then
- return {set_engagement_state = function(_, args) assert(args.flags == 0 and args.revision == 1) end}
- end
- if name == "directive" then
- return {set_directive = function(_, args) if directives[#directives] ~= args.directive then directives[#directives + 1] = args.directive end; navpoints[#navpoints + 1] = args.navpoint or false end}
- end
- for index, value in ipairs(names) do
- if name == value then return {registry_key = 7, slot_type = 1, slot_index = index,
- assign_combat_objective = function(_, args)
- assert((args.task_group == -1 or (name:find("DROPSHIP_[ABCD]_SQUAD$") and args.task_group == 1)) and args.revision == 1)
- assert(args.reserved == (name:find("DROPSHIP_A_SUPPORT") ~= nil or name:find("DROPSHIP_A_MELEE") ~= nil),
- "objective updates must preserve the passenger reservation")
- assignments[name] = args.objective
- end} end
- end
- error("unknown slot " .. name)
- end
- local function region(index)
- program.on_event_client_state_changed(context, state, {current_region_index = index})
- end
- local function cleared(first, last)
- for index = first or 12, last or 22 do
- program.on_event_squad_state(context, state, {
- registry_key = 7, slot_type = 1, slot_index = index,
- alive_count = 0, previous_alive_count = 1, removal_flag = true, slot_counts = {1},
- })
- end
- end
- assert(#closed == 0 and resets == 0 and placements == 0)
- program.on_event_client_state_changed(context, state, {region_index = 49})
- assert(#movies == 0, "a requested region is not a held cinematic")
- region(64)
- assert(placements == 0 and #closed == 0 and #cues == 0,
- "playable-region reports cannot start the landing before cinematic completion")
- region(49); region(49)
- assert(#movies == 1 and movies[1] == true and placements == 0)
- program.on_event_cinematic_terminated(context, state, {registry_key = 8, slot_type = 6, slot_index = 0})
- assert(seeds == 0, "unrelated movie termination cannot advance the mission")
- local termination = {registry_key = 9, slot_type = 6, slot_index = 0}
- -- A skip request must match the current movie, and late completion must not spawn twice.
- program.on_event_cinematic_skip_requested(context, state, {registry_key = 8, slot_type = 6, slot_index = 0})
- assert(seeds == 0, "an unrelated skip cannot advance the mission")
- program.on_event_cinematic_skip_requested(context, state, termination)
- program.on_event_cinematic_skip_requested(context, state, termination)
- program.on_event_cinematic_terminated(context, state, termination)
- program.on_event_cinematic_terminated(context, state, termination)
- assert(seeds == 1 and #movies == 2 and movies[2] == false)
- region(40)
- assert(placements == 0 and #closed == 0)
- region(64); region(64)
- assert(placements == 5 and directives[1] == "follow")
- assert(#darkness == 1 and darkness[1] == false, "arrival must leave darkness disabled")
- for index = 1, #triggers do assert(armed[index] == (index < 6 and 1 or nil)) end
- assert(#closed == 6 and resets == 1, "initialize devices only after playable region is held")
- program.on_event_client_state_changed(context, state, {teleport_state = 0})
- assert(#cues == 0)
- region(nil); region(nil)
- assert(#cues == 1 and cues[1] == 0, "arrival dialogue follows spawn settlement once")
- -- Initial zero and unrelated trigger reports must not complete or populate later fights.
- for index = 12, 22 do
- program.on_event_squad_state(context, state, {registry_key = 7, slot_type = 1,
- slot_index = index, alive_count = 0, previous_alive_count = 0})
- end
- assert(#directives == 1)
- local function trigger(index, registry)
- program.on_event_player_trigger(context, state, {
- registry_key = registry or 10, slot_type = 31, slot_index = index,
- })
- end
- trigger(1, 99); assert(placements == 5)
- trigger(1); trigger(1); assert(placements == 9)
- trigger(2); assert(placements == 11)
- trigger(3); assert(placements == 19 and directives[2] == "clear")
- for _, name in ipairs({"BRIDGE_VIGNETTE_SUPPORT_A_SQUAD", "BRIDGE_VIGNETTE_SUPPORT_B_SQUAD"}) do
- assert(assignments[name].objective_name == "bridge_objective",
- "near-side squads retain their authored bridge AI objective")
- end
- trigger(4); trigger(4); assert(#cues == 2 and cues[2] == 2)
- assert(#ghost == 1 and ghost[1].generation == 1 and not ghost[1].enabled)
- assert(#door == 1 and door[1].transition == "close" and door[1].snap)
- assert(#leverAnimation == 1 and leverAnimation[1].transition == "close" and leverAnimation[1].snap)
- trigger(5); trigger(5)
- trigger(8); trigger(6); trigger(7)
- assert(placements == 19, "overlapping bridge receipts must not spawn waves before the scan")
- assert(#door == 1, "proximity cannot open the lever door or spawn its group")
- local function console_complete(generation, progress, active, registry)
- program.on_event_ghost_link_state(context, state, {registry_key = registry or 12,
- slot_type = 65, slot_index = 60, generation = generation, progress = progress, active = active})
- end
- console_complete(2, 1, false)
- assert(extended == 0, "console cannot complete before combat")
- cleared(12,17)
- assert(#directives == 2, "near-side bridge squads must also clear before Ghost becomes usable")
- cleared(18,18); assert(#directives == 2)
- cleared(19,19); cleared()
- assert(#directives == 3 and directives[3] == "controls" and retirements == 8)
- assert(#cues == 3 and cues[3] == 3)
- assert(#closed == 6 and extended == 0, "combat clear cannot move the bridge")
- assert(#ghost == 2 and ghost[2].generation == 2 and ghost[2].enabled)
- assert(not vars["ember.mercury_bonus.phase"], "Crimson Shadow cannot block Ghost interaction")
- local function use_lever(registry, slot_type, slot_index)
- program.on_event_object_interacted(context, state, {
- registry_key = registry or 12, slot_type = slot_type or 4, slot_index = slot_index or 19})
- end
- use_lever(99); use_lever(12,65); use_lever(12,4,20)
- assert(#door == 1 and not vars["ember.mercury_bonus.phase"], "unrelated objects cannot use the lever")
- use_lever(); use_lever()
- assert(#door == 2 and door[2].transition == "open" and not door[2].snap)
- assert(#leverAnimation == 2 and leverAnimation[2].transition == "open" and not leverAnimation[2].snap,
- "lever use must animate the matching authored device once")
- assert(vars["ember.mercury_bonus.phase"] == 1, "lever use starts the optional group exactly once")
- cleared(20,24)
- assert(#directives == 3 and #ghost == 2, "bonus completion cannot replace the bridge instruction")
- console_complete(1, 1, false); console_complete(2, 1, false, 99)
- console_complete(2, .5, false); console_complete(2, 1, true)
- assert(extended == 0, "only matching completed native interaction can extend the bridge")
- assert(#darkness == 1 and not darkness[1], "combat clear and incomplete scans cannot enable darkness")
- -- Prepare reserved cargo and empty parents before Ghost is usable, without starting ships.
- assert(placements == 32 and #flights == 0 and #deliveries == 0)
- program.on_event_squad_state(context, state, {registry_key = 7, slot_type = 1, slot_index = 30,
- objective_revision = 1, task_costs = {2040, 10, 20}})
- assert(vars["ember.transport.ai"] == 2, "Harvester chooses its native minimum-cost task")
- assert(placements == 32 and #flights == 0 and #deliveries == 0,
- "ship objective assignment must preserve empty requests and deferred flight")
- console_complete(2, 1, false); console_complete(2, 1, false)
- assert(extended == 6 and directives[4] == "cross" and vars.phase == 4)
- assert(#darkness == 2 and darkness[2], "console completion enables darkness once with bridge extension")
- assert(placements == 37 and #flights == 4 and #deliveries == 0,
- "bridge activation starts ships and all five far-side hangar squads exactly once")
- for _, group in ipairs({"sun", "helipad"}) do
- assert(vars["ember." .. group .. ".phase"] == 1, "hangar waves cannot wait for the crossing")
- end
- -- Crossing stages the seven deck defenders; Sun and helipad remain one-shot.
- trigger(6); assert(placements == 42)
- trigger(6); assert(placements == 42)
- trigger(7); assert(placements == 42)
- trigger(8); assert(placements == 42)
- trigger(6); trigger(7); assert(placements == 42)
- package.loaded.mission_ember = nil
- program = require("mission_ember")
- region(64); cleared(); trigger(8)
- assert(#darkness == 2 and darkness[2], "crossings and reload cannot restart darkness")
- assert(seeds == 1 and placements == 42 and #directives == 4 and #cues == 3)
- for index = 1, #triggers do assert(armed[index] == 1) end
- assert(#closed == 6 and resets == 1, "transit and reload must not reset the bridge")
- local function flight(index, revision, status, generation, dead, delivery_revision, delivery_state)
- program.on_event_actor_path_state(context, state, {registry_key = 7, slot_type = 2, slot_index = index,
- generation = generation or 1, revision = revision, path_state = status, dead = dead or false,
- delivery_revision = delivery_revision, delivery_state = delivery_state})
- end
- flight(1, 1, 0); flight(1, 1, 1, 2, false, 1, 0)
- assert(#flights == 4 and #deliveries == 0, "initial and stale member reports cannot begin unloading")
- for index = 1, 3 do flight(index, 1, 1, 1, false, 0, 0) end
- assert(#deliveries == 0 and timerStarts == 3)
- flight(1, 1, 1, 1, false, 0, 0)
- assert(timerStarts == 3, "duplicate arrival cannot restart the unload delay")
- tick(5999)
- assert(#deliveries == 0, "hold cargo for six seconds after arrival")
- package.loaded.mission_ember = nil
- program = require("mission_ember")
- region(56)
- assert(darkness[#darkness] == false, "leaving the encounter must release respawn restrictions")
- tick(6000)
- assert(#deliveries == 2 and #flights == 4 and timerStarts == 4, "A/B unload; empty C begins its departure hold")
- program.on_event_timer_elapsed(context, state, {timer_name = "ember.transport.unload.1"})
- assert(#deliveries == 2, "duplicate timer receipt cannot deliver twice")
- flight(4, 1, 0, 1, false, 0, 0)
- assert(timerStarts == 4)
- flight(4, 1, 1, 1, false, 0, 0)
- tick(9999); assert(#flights == 4)
- tick(10000); assert(#flights == 5, "C leaves after its separate four-second departure hold")
- tick(11999); assert(#flights == 5)
- tick(12000); assert(#deliveries == 2 and #flights == 5 and timerStarts == 6)
- for index = 1, 2 do flight(index, 1, 1, 1, false, 1, 1) end
- assert(placements == 42, "manifests must not add ground spawn requests")
- flight(1, 1, 1, 1, false, 1, 0)
- assert(#flights == 5)
- program.on_event_squad_state(context, state, {registry_key = 7, slot_type = 1, slot_index = 34,
- alive_count = 1, previous_alive_count = 0})
- assert(#flights == 5, "A waits for both of its passenger squads")
- program.on_event_squad_state(context, state, {registry_key = 7, slot_type = 1, slot_index = 36,
- alive_count = 1, previous_alive_count = 0})
- assert(#flights == 5 and timerStarts == 7, "A begins its hold after both squads and detach completed")
- for _, index in ipairs({35,37}) do
- program.on_event_squad_state(context, state, {registry_key = 7, slot_type = 1, slot_index = index,
- alive_count = 1, previous_alive_count = 0})
- end
- assert(#flights == 5 and timerStarts == 7, "B still waits for native detach completion")
- flight(2, 1, 1, 1, false, 1, 0)
- assert(#flights == 5 and timerStarts == 8)
- -- Reload and duplicate receipts cannot restart or bypass a hold.
- package.loaded.mission_ember = nil
- program = require("mission_ember")
- flight(2, 1, 1, 1, false, 1, 0)
- assert(timerStarts == 8)
- tick(15999); assert(#flights == 5)
- tick(16000)
- assert(#flights == 8 and flights[5].revision == 2 and flights[5].generation == 1)
- for index = 1,4 do
- program.on_event_timer_elapsed(context,state,{timer_name="ember.transport.depart."..index})
- end
- assert(#flights == 8 and timerStarts == 8, "duplicate departure timers cannot repeat paths")
- for index = 1, 4 do flight(index, 1, 1, 1, false, 1, 0); flight(index, 2, 0) end
- assert(retiredShips == 0, "exit requests cannot retire a ship before it finishes flying")
- for index = 1, 4 do flight(index, 2, 1); flight(index, 2, 1) end
- assert(retiredShips == 0 and #exitActions == 4, "path end starts one native exit action per ship")
- -- Stale path receipts, module reload and running-action receipts cannot retire or restart ships.
- package.loaded.mission_ember = nil
- program = require("mission_ember")
- for index = 1,4 do flight(index, 2, 1); flight(index, 3, 0) end
- assert(retiredShips == 0 and #exitActions == 4)
- for index = 1,4 do flight(index, 3, 1); flight(index, 3, 1) end
- assert(retiredShips == 4 and #exitActions == 4, "native exit completion retires each ship once")
- console_complete(2, 1, false)
- assert(placements == 42 and #flights == 8 and #deliveries == 2,
- "duplicate path and scan receipts cannot respawn ships or passengers")
- region(64); cleared()
- assert(#ghost == 2 and extended == 6)
- do
- local unlocked, powered = false, false
- for _, lane in ipairs(beamLanes) do
- unlocked = unlocked or lane == "unlock"
- powered = powered or lane == "power_on"
- end
- assert(unlocked and powered, "the powerhouse weapon beam device must be enabled, not just placed")
- end
- print("cinematic handoff, 42-squad route, skipped volumes, exact triggers, dialogue and reload checks passed")
- for _, group in ipairs({"catwalk_entry", "catwalk_mid", "pipe", "mercury", "mercury_bonus", "bridge", "passengers", "sun", "helipad"}) do
- for _, name in ipairs(roster[group]) do assert(assignments[name], "missing objective: " .. name) end
- end
- for _, name in ipairs(roster.dropships) do
- assert(assignments[name], "each ship parent needs bridge objective membership")
- end
- print("full-route peak durable variables: " .. peak_variables .. "/512")
- -- Completed native requests are zeroed once. Returning from the refinery, including a
- -- module reload and late trigger receipts, cannot replay any opening side effect.
- cleared(25,29); cleared(34,36); cleared(38,39)
- assert(darkness[#darkness] == true, "one remaining passenger must hold darkness")
- cleared(37,37)
- assert(darkness[#darkness] == false, "the final bridge passenger clears darkness without a load zone")
- assert(vars["ember.helipad.phase"] == 1, "later helipad combat cannot hold bridge darkness")
- program.on_event_squad_state(context,state,{registry_key=7,slot_type=1,slot_index=40,
- alive_count=1,previous_alive_count=0})
- assert(darkness[#darkness] == false, "live helipad enemies cannot reactivate the cleared bridge")
- cleared(40,42)
- assert(vars["ember.catwalk_entry.phase"] == 1, "earlier encounters are outside bridge darkness completion")
- cleared(1,11)
- assert(retirements == 38)
- local darknessCount = #darkness
- local objectiveCount, cueCount, doorCount = #directives, #cues, #door
- local movieCount, flightCount = #movies, #flights
- -- A completed checkpoint may never have crossed these narrow early volumes.
- context:clear_variable("ember.route.PIPE_CROSSING_040_PERCENT_PLAYER_TRIGGER")
- context:clear_variable("ember.route.LANDING_MERCURY_040_PERCENT_PLAYER_TRIGGER")
- region(56)
- package.loaded.mission_ember = nil
- program = require("mission_ember")
- region(49); region(64)
- for index = #triggers, 1, -1 do trigger(index) end
- cleared(1,42)
- console_complete(2,1,false)
- use_lever()
- assert(#darkness == darknessCount, "completed-area return cannot restore restrictions")
- assert(retirements == 38 and placements == 42 and retiredShips == 4)
- assert(#directives == objectiveCount and #cues == cueCount and #door == doorCount and #leverAnimation == 2)
- assert(#movies == movieCount and #flights == flightCount and #closed == 6 and extended == 6)
- print("completed-area return preserves objectives, devices, cinematic and zero native spawn requests")
- -- Guidance follows route progress and never leaves a completed approach pointing backward.
- local landing = require("mission_ember.landing")(mission)
- local function guidance() landing.update_guidance(context,state); return navpoints[#navpoints] end
- vars["ember.bridge.extended"]=false;vars["ember.console.enabled"]=false
- vars["ember.route.progress"]=0;vars["ember.navpoint"]=nil
- assert(guidance().slot_index==1, "cleared approach points toward Mercury")
- vars["ember.landing.phase"]=1
- assert(guidance()==false, "combat removes navigation")
- vars["ember.landing.phase"]=2;vars["ember.console.enabled"]=true
- assert(guidance().slot_index==2, "cleared landing points to Ghost")
- local markers=#navpoints;guidance();assert(#navpoints==markers,"unchanged target is idempotent")
- vars["ember.bridge.extended"]=true;vars["ember.sun.phase"]=1
- assert(guidance()==false)
- vars["ember.sun.phase"]=2
- assert(guidance().slot_index==3,"completed bridge points forward to the helipad")
- vars["ember.route.progress"]=8;assert(guidance()==false,"reaching helipad retires the approach marker")
- -- Reset the bridge and scan again without replaying the completed approach or lever.
- function context:cancel_timer(name) timers[name]=nil end
- local beforeMovies,beforeLever=#movies,#leverAnimation
- local beforeFlights=#flights
- landing.reset_checkpoint(context,state)
- assert(not vars["ember.bridge.extended"] and not vars["ember.darkness.enabled"])
- assert(vars["ember.route.progress"]==5 and vars["ember.console.generation"]==4)
- assert(vars["ember.transport.generation"]==3 and not vars["ember.transport.started"])
- assert(vars["ember.catwalk_entry.phase"]==2 and vars["ember.landing.phase"]==2)
- assert(vars["ember.lever.used"] and #leverAnimation==beforeLever and #movies==beforeMovies)
- assert(ghost[#ghost].enabled and ghost[#ghost].generation==4)
- assert(#closed==12 and #flights==beforeFlights)
- console_complete(2,1,false);assert(#flights==beforeFlights,"prior Ghost completion cannot rescan")
- console_complete(4,1,false);assert(#flights==beforeFlights+4)
- assert(flights[#flights].generation==3 and darkness[#darkness])
- local deliveryCount=#deliveries
- program.on_event_actor_path_state(context,state,{registry_key=7,slot_type=2,slot_index=1,
- generation=1,revision=1,path_state=1})
- program.on_event_timer_elapsed(context,state,{timer_name="ember.transport.unload.1"})
- assert(#deliveries==deliveryCount,"old flight/timer cannot unload next attempt")
- program.on_event_actor_path_state(context,state,{registry_key=7,slot_type=2,slot_index=1,
- generation=3,revision=1,path_state=1})
- assert(timers["ember.transport.unload.1.3"],"next flight has its own unload timer")
- program.on_event_timer_elapsed(context,state,{timer_name="ember.transport.unload.1"})
- assert(#deliveries==deliveryCount)
- program.on_event_timer_elapsed(context,state,{timer_name="ember.transport.unload.1.3"})
- assert(#deliveries==deliveryCount+1 and deliveries[#deliveries].generation==3)
- print("navpoint combat gating, checkpoint rescan and second-flight stale-event checks passed")
|