Bläddra i källkod

Correct Apex cooling order and remove stacked escape scorch

Millie 3 dagar sedan
förälder
incheckning
f2d690ca6b

+ 14 - 0
docs/mission-ember-apex-cooling-and-scorch.md

@@ -0,0 +1,14 @@
+# Apex cooling, object initialization, and escape scorch
+
+Changes on 2026-09-06:
+
+- End the beam surge before publishing clamshell/coffin cooling-door opening. Keep the existing 6-second surge, 10-second exposure, and 14-second closed intervals.
+- On native laser/ring object presence, reapply power and the current pose. Initial commands can precede entity creation; this is a candidate fix for the dark beam before the first surge, pending visual confirmation. Late presence after deposit must not relight the beam.
+- Start the escape ship device only after its native object-presence receipt. Unlock, power on, snap to closed, then animate to open. Duplicate presence does not restart it. Authored flight playback remains to be verified in game; the script does not synthesize a flight path.
+- Remove the added rail-wide thermal attachment during escape. Deposit already creates SUNBURN_DAMAGE_OBJECT, while the old script additionally attached REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON to the rails. The escape-end trigger removed only that attachment. This matches the report that damage becomes normal after reaching the end. Preserve the native sunburn object and retire the climb attachment on entering escape. No global damage multiplier is changed; exact damage rate still needs playtesting.
+
+Package evidence correction: config 80B3D494 (ship) and 80B3D497 (sunburn) both contain fallback resource 80BFDDC2 at offset 0x538. Their actual first placed entries at offset 0x580 are respectively 80B71228 and 80B82486. Do not use the fallback model to infer ship capabilities. Ship model 80B71228 includes device component 80C70C0E and animation component 80B71226. Device placement identity 42BF7017F2901B45 matches the placed ship. Model component references have 12-byte stride, not 16.
+
+Validation: all five mission Lua suites pass. Route peak: 235 variables, 61 intents per callback, three concurrent timers, 13,000 Lua instructions including the mock. Tests cover cooling command order, beam presence, deferred ship activation, duplicate ship receipts, and disabling the extra escape burn.
+
+Still unresolved: exact native ending-readiness failure and full-screen surge distortion. The diagnostic DLL commit fe45d11 improves visibility but does not itself fix cinematic playback. Do not reinstate the reverted cinematic-region ordinal encoding: it caused Lime.

+ 29 - 12
scripts/mission_ember/apex.lua

@@ -98,8 +98,7 @@ return function(m, a, ending)
     -- `REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON` and `FOUNDRY_THERMAL_DOT_HOP_ON` both reference
     -- effect resource 80C1D9E0 -- the burn already working in the Foundry. The hot-pipe and
     -- rail-top hop-ons carry 80B82484 and 80C1D389 instead, which is why contact read as a
-    -- shock rather than a scorch. Apex drives the one authored scorch for both hazards and
-    -- swaps only its filter, so the climb and the escape share the same effect. A new revision
+    -- shock rather than a scorch. Use the authored scorch for the climb pipes. A new revision
     -- removes the previous attachment (native 9EF8A0/9F1F10) before attaching the new filter.
     local function rail_filter(c) return {players = true, inside = a.slot(c, "SLOT_019E")} end
     local function hazards(c, s, mode)
@@ -120,8 +119,11 @@ return function(m, a, ending)
                     a.slot(c, "SLOT_0005_80B3C09F"), a.slot(c, "SLOT_0006_80B3C09F"),
                     a.slot(c, "SLOT_0008_80B3C09F")}}, true)
         elseif mode == "escape" then
+            -- Escape already owns SUNBURN_DAMAGE_OBJECT. Adding this rail-wide burn on top
+            -- adds damage until the escape-end trigger detaches it. Retire the
+            -- climb attachment here and let the native sunburn object own escape damage.
             a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
-                "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filter(c), true)
+                "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filter(c), false)
         else
             a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
                 "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filter(c), false)
@@ -162,7 +164,8 @@ return function(m, a, ending)
                 end
             else a.slot(c, "REACTOR_COFFIN_ALARM_SEQUENCE"):play_sequence{} end
         else
-            if step == "closed" then beam_surge(c, s, false) end
+            -- Exposure is the cooling window: stop the surge before moving the shutters.
+            beam_surge(c, s, false)
             if phase(s) == 3 then
                 for _, side in ipairs(sides) do if not dead(s, side) then doors(c, side, step == "open", snap) end end
             elseif phase(s) == 4 then
@@ -213,12 +216,9 @@ return function(m, a, ending)
             a.device(c, "MOTHER_BRAIN_ENGINE_LEFT_DEVICE", true)
             a.device(c, "MOTHER_BRAIN_ENGINE_RIGHT_DEVICE", true)
             a.objects(c, {"REACTOR_GETAWAY_SHIP_OBJECT", "SUNBURN_DAMAGE_OBJECT"}, true)
-            -- The escape ship has no authored flight path: the four Harvesters own the only
-            -- type-58 sequences in the mission, so this ship travels on its own device's
-            -- position lane. That device was never unlocked or powered, which is the same
-            -- omission that left the weapon beam inert, so it held its start pose.
-            unlock(c, "REACTOR_GETAWAY_SHIP_DEVICE")
-            a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", true)
+            -- Object publication is not an entity-creation receipt. Start its device from
+            -- A.object once the ship is present, so the initial movement is not lost.
+            c:clear_variable("ember.apex.ship_started")
             -- The weapon is dead once the cell is in: powered off but still installed, so
             -- the beam and everything built around it stay in the world. The previous code
             -- opened both devices here, which left it running through the whole escape.
@@ -347,8 +347,8 @@ return function(m, a, ending)
     function A.timer(c, s, e)
         if e.timer_name == "ember.apex.hazards" then
             -- The climb pipes burn while the cell is being carried up (phase 5), well before
-            -- the deposit; the rail top burns during the escape (phase 6). Any earlier phase
-            -- owns no hazard, so this is also how a checkpoint reset detaches one.
+            -- the deposit. Escape (phase 6) uses its separate native sunburn object, so
+            -- entering it detaches the climb burn. A checkpoint reset also detaches it.
             if s:variable("ember.region") == 0 then
                 local p = phase(s)
                 if p == 5 then hazards(c, s, "climb")
@@ -461,6 +461,23 @@ return function(m, a, ending)
     end
     function A.object(c, s, e)
         carry.state(c, s, e)
+        if e.present and e.alive then
+            for i, name in ipairs({"SPECOPS_APEX_RING_LASER_OBJECT", "SPECOPS_APEX_RING_RING_OBJECT"}) do
+                if a.matches(c, e, name) and s:variable("ember.apex.beam") == true then
+                    -- Reapply the current pose when native creation finishes. The initial
+                    -- setup can precede creation; waiting for the first surge left it dark.
+                    unlock(c, beam_devices[i])
+                    a.device(c, beam_devices[i], s:variable("ember.apex.surge") ~= true, true)
+                end
+            end
+            if a.matches(c, e, "REACTOR_GETAWAY_SHIP_OBJECT") and phase(s) == 6
+                and not s:variable("ember.apex.ship_started") then
+                c:set_variable("ember.apex.ship_started", true)
+                unlock(c, "REACTOR_GETAWAY_SHIP_DEVICE")
+                a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", false, true)
+                a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", true)
+            end
+        end
         if e.generation == generation(s) then
             for _, which in ipairs({"EAST", "WEST", "COFFIN"}) do
                 local prefix = which == "COFFIN" and "REACTOR_COFFIN" or ("REACTOR_CLAMSHELL_" .. which)

+ 36 - 7
tests/mission_ember_routes_test.lua

@@ -204,6 +204,12 @@ region(0)
 assert(vars['ember.r.guidance']:sub(1,8)=='4E4862BB','apex must not show the grinder objective')
 assert(transition('SECURITY_DOOR_DEVICE').transition=='close')
 timer('ember.apex.setup.')
+for _,name in ipairs({'SPECOPS_APEX_RING_LASER_OBJECT','SPECOPS_APEX_RING_RING_OBJECT'}) do
+    call(R.dispatch,'object',c,s,event(name,{generation=1,present=true,alive=true}))
+end
+assert(transition('SPECOPS_APEX_RING_LASER_DEVICE',true).transition=='open')
+assert(transition('SPECOPS_APEX_RING_LASER_DEVICE',true).snap==true)
+assert(vars['ember.apex.beam'] and not vars['ember.apex.surge'],'entry must light the resting beam')
 assert(transition('CLAMSHELL_TO_COFFIN_EAST_BRIDGE_DEVICE', true).transition=='open')
 assert(transition('CLAMSHELL_TO_COFFIN_WEST_BRIDGE_DEVICE', true).transition=='open')
 assert(transition('REACTOR_COFFIN_DOOR_EAST_DEVICE').transition=='lock')
@@ -267,8 +273,23 @@ assert(vars['ember.apex.surge']==true,'the warning must drive the beam')
 assert(transition('SPECOPS_APEX_RING_LASER_DEVICE', true).transition=='close')
 assert(transition('SPECOPS_APEX_RING_RING_DEVICE', true).transition=='close')
 assert(transition('REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE').transition=='close')
+local coolingStart=#calls
 timer('ember.apex.vents.')
 assert(timers['ember.apex.vents.1']==10000)
+assert(vars['ember.apex.surge']==false,'cooling shutters opened during the surge')
+local restored={}
+for i=coolingStart+1,#calls do
+    local row=calls[i]
+    if row[1]=='transition' then
+        for _,name in ipairs({'SPECOPS_APEX_RING_LASER_DEVICE','SPECOPS_APEX_RING_RING_DEVICE'}) do
+            if row[2]==slotDefs[m.Slot[name]].name and row[3].transition=='open' then restored[name]=true end
+        end
+        if row[2]==slotDefs[m.Slot.REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE].name and row[3].transition=='open' then
+            assert(restored.SPECOPS_APEX_RING_LASER_DEVICE and restored.SPECOPS_APEX_RING_RING_DEVICE,
+                'both beam devices must end the surge before opening the cooling shutters')
+        end
+    end
+end
 assert(transition('REACTOR_CLAMSHELL_EAST_LIGHT_A_DEVICE').transition=='power_on')
 for _,side in ipairs({'EAST','WEST'})do
     local n='REACTOR_CLAMSHELL_'..side
@@ -335,7 +356,12 @@ assert(vars['ember.carry.apex.generation']==3 and vars['ember.apex.phase']==5)
 use('MOTHER_BRAIN_CARRY_OBJECT',1);use('MOTHER_BRAIN_INTERACT_OBJECT');assert(vars['ember.apex.phase']==5)
 local depositStart=#calls
 use('MOTHER_BRAIN_CARRY_OBJECT',3);use('MOTHER_BRAIN_INTERACT_OBJECT');assert(vars['ember.apex.phase']==6)
--- The device cannot animate an absent object or one left locked/unpowered.
+-- Publish first; only native object presence starts the movement.
+assert(not vars['ember.apex.ship_started'])
+call(R.dispatch,'object',c,s,event('REACTOR_GETAWAY_SHIP_OBJECT',{generation=1,present=false,alive=false}))
+assert(not vars['ember.apex.ship_started'])
+call(R.dispatch,'object',c,s,event('REACTOR_GETAWAY_SHIP_OBJECT',{generation=1,present=true,alive=true}))
+assert(vars['ember.apex.ship_started'])
 local shipSteps={}
 for i=depositStart+1,#calls do
     local row=calls[i]
@@ -345,19 +371,22 @@ for i=depositStart+1,#calls do
         shipSteps[#shipSteps+1]=row[3].transition
     end
 end
-assert(table.concat(shipSteps,',')=='spawn,unlock,power_on,open', 'escape ship activation order')
+assert(table.concat(shipSteps,',')=='spawn,unlock,power_on,close,open', 'escape ship activation order')
+local shipStartedCalls=#calls
+call(R.dispatch,'object',c,s,event('REACTOR_GETAWAY_SHIP_OBJECT',{generation=1,present=true,alive=true}))
+assert(#calls==shipStartedCalls,'duplicate presence restarted the escape flight')
 assert(vars['ember.carry.apex.done'] and not vars['ember.carry.apex.held'])
 local before=#calls;use('MOTHER_BRAIN_INTERACT_OBJECT');assert(#calls==before)
 call(R.dispatch,'object',c,s,event('MOTHER_BRAIN_CARRY_OBJECT',{generation=3,present=false,alive=false}))
 assert(not timers['ember.carry.recover.apex.3'],'consumed final cell respawned')
--- The escape scorch is the same authored effect the climb used, re-filtered to the rail top.
+-- Escape's native sunburn object must not stack with the scripted climb/rail burn.
 timer('ember.apex.hazards')
-assert(vars['ember.effect.REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON'],'escape must scorch')
-local escapeFilter
+local escapeEffect
 for i=#calls,1,-1 do
-    if calls[i][1]=='set_object_filter' then escapeFilter=calls[i][2];break end
+    if calls[i][1]=='set_mission_effect' then escapeEffect=calls[i][3];break end
 end
-assert(escapeFilter=='aod_reactor_rail_top_object_filter','escape hazard must use the rail top')
+assert(escapeEffect and escapeEffect.enabled==false and escapeEffect.filter==nil,
+    'escape must detach the added burn instead of stacking it with native sunburn')
 -- The weapon is dead once the cell is in: powered off, but still installed. Deactivating the
 -- ring objects would take the beam and its surrounding structure out of the world entirely.
 assert(vars['ember.apex.beam']==false,'the beam must stop firing after the deposit')