From 3d79f25df2c0a82b9a59c07aaaea8be7df7da463 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:59:25 +0200 Subject: [PATCH] feat(metal-farp): progressive animated construction scene + unit tests (#19) - Rewrites the 9-step scene into a 13-step animated sequence (~45 s total) - Trucks and tent appear first; Tower Crane spawns at t+5.5 s - FG_small_Helipad_Under_Construction animates construction from t+10.5 s to t+30.5 s - FG_small_Helipad revealed at t+30.5 s (critical); crane removed at t+35.5 s - Equipment and warehouse follow; warehouse stocked only once pad is live - Cross-step object references stored in scene._params (_craneObj, _underConstObj) - Registers Tower Crane and FG_small_Helipad_Under_Construction via registerIfAbsent - Adds FG_small_Helipad_Under_Construction to modTypes (same zip, no new modUrls) - Creates plugins/metal-farp/tests/metal_farp_spec.lua with 6 Busted assertions Co-Authored-By: Claude Sonnet 4.6 --- .../metal-farp/src/CTLD_metalFarpScene.lua | 179 ++++++++++++++---- plugins/metal-farp/tests/metal_farp_spec.lua | 65 +++++++ 2 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 plugins/metal-farp/tests/metal_farp_spec.lua diff --git a/plugins/metal-farp/src/CTLD_metalFarpScene.lua b/plugins/metal-farp/src/CTLD_metalFarpScene.lua index 84eba1e..a898625 100644 --- a/plugins/metal-farp/src/CTLD_metalFarpScene.lua +++ b/plugins/metal-farp/src/CTLD_metalFarpScene.lua @@ -4,18 +4,23 @@ -- FG_small_Helipad mod (visible metallic helipad platform). -- -- Requires the FG_small_Helipad mod to be installed on all clients. --- probeSkip=true is set on the registry entry — the mod cannot be validated at runtime --- (DCS getDesc().life == 0 whether the mod is installed or not). +-- probeSkip=true is set on the registry entries for mod types — DCS getDesc().life == 0 +-- whether the mod is installed or not. -- -- Layout (all offsets from trigger unit position): --- FG_small_Helipad heliport — 58 m ahead of trigger unit --- Fuel truck — 35 m / 8° heading 90° (t+5 s) --- Repair truck — 35 m / 11° heading 90° (t+5 s) --- Tent — 35 m / 10° heading 90° (t+5.5 s) --- Ammo cargo — 75 m / 346° (t+10 s) --- M92 light panel — 75 m / 355° alt+4 m (t+15 s) --- Windsock — 73 m / 346° (t+15 s) --- Warehouse stocking — 10 000 L × 4 fuel types (t+20 s) +-- Fuel truck — 0 s +-- Repair truck — 0 s +-- Tent — 0.5 s +-- Tower Crane — 5.5 s (~59 m / 10°, 10 m right of pad) +-- FG_small_Helipad_Under_Construction — 10.5 s (58 m / 0°) +-- [destroy Under_Construction] — 30.5 s +-- FG_small_Helipad heliport — 30.5 s (58 m / 0°) +-- [destroy Tower Crane] — 35.5 s +-- Ammo cargo — 35.5 s +-- M92 light panel — 40 s alt+4 m +-- Windsock — 40 s +-- Carrier Seaman — 40 s +-- Warehouse stocking — 45 s 10 000 L × 4 fuel types -- -- Dependencies: CTLDObjectRegistry, CTLDSceneManager, CTLDUtils -- ==================================================================================================== @@ -58,6 +63,25 @@ CTLDObjectRegistry.registerIfAbsent("FG_small_Helipad", { probeSkip = true, }) +-- Visual-only under-construction variant of FG_small_Helipad, used during the animation phase. +-- Ships in the same mod zip as FG_small_Helipad — no additional modUrls entry needed. +CTLDObjectRegistry.registerIfAbsent("FG_small_Helipad_Under_Construction", { + groupType = "STATIC", + namePrefix = "FARP_Helipad_UC", + type = "FG_small_Helipad_Under_Construction", + category = "Fortifications", + probeSkip = true, +}) + +-- Stock DCS infrastructure object used as a construction prop during the animation. +CTLDObjectRegistry.registerIfAbsent("Tower Crane", { + groupType = "STATIC", + namePrefix = "TowerCrane", + type = "Tower Crane", + category = "Structures", + shape_name = "TowerCrane_01", +}) + CTLDObjectRegistry.registerIfAbsent("Fuel_Truck", { groupType = "GROUND", namePrefix = "Fuel_Truck_Grp", @@ -141,7 +165,8 @@ metalFarpScene.name = "Metal FARP" metalFarpScene.requiresMod = "FG_small_Helipad" -- human-readable required-mod label (docs/catalogue) -- Non-stock (mod) DCS types this scene spawns. Added to the known set by the design-time -- asset hard-gate (datamine ∪ modTypes) so validation still catches typos in every stock type. -metalFarpScene.modTypes = { "FG_small_Helipad" } +-- FG_small_Helipad_Under_Construction ships in the same zip as FG_small_Helipad. +metalFarpScene.modTypes = { "FG_small_Helipad", "FG_small_Helipad_Under_Construction" } -- Minimum CTLD version providing the plugin-scene machinery (load-position-independent menus, -- requiresCtld check). CTLD warns at load if it is older. metalFarpScene.requiresCtld = "2.0.0" @@ -159,83 +184,153 @@ metalFarpScene.crate = { metalFarpScene.steps = { -- ---------------------------------------------------------------- - -- Step 1: FG_small_Helipad heliport (delay=0). - -- Spawned 50 m ahead of the trigger unit to avoid overlapping it. - -- Saves the spawned airbase name for the warehouse-stocking step. - -- critical=true: if the mod is absent the helipad cannot spawn; abort the whole scene - -- rather than deploying trucks and a tent with no landing pad. + -- Step 1: Fuel truck — right side under tent (t0 + 0 s). -- ---------------------------------------------------------------- { - polar = { distance = 58, angle = 0 }, + polar = { distance = 60, angle = 342.5 }, + delayAfterPreviousStep = 0, + relativeHeadingInDegrees = 90, + relativeAltitudeInMeters = 0, + registryKey = "Fuel_Truck", + }, + + -- ---------------------------------------------------------------- + -- Step 2: Repair truck — left side under tent (t0 + 0 s). + -- ---------------------------------------------------------------- + { + polar = { distance = 61, angle = 340.5 }, delayAfterPreviousStep = 0, + relativeHeadingInDegrees = 90, + relativeAltitudeInMeters = 0, + registryKey = "repare_Truck", + }, + + -- ---------------------------------------------------------------- + -- Step 3: Tent — over both trucks (t0 + 0.5 s). + -- ---------------------------------------------------------------- + { + polar = { distance = 61, angle = 341 }, + delayAfterPreviousStep = 0.5, + relativeHeadingInDegrees = 90, + relativeAltitudeInMeters = 0, + registryKey = "FARP_Tent", + }, + + -- ---------------------------------------------------------------- + -- Step 4: Tower Crane — construction prop, ~10 m right of pad + -- as seen from the cockpit (t0 + 5.5 s). + -- Reference saved in scene._params for later destruction. + -- ---------------------------------------------------------------- + { + polar = { distance = 59, angle = 10 }, + delayAfterPreviousStep = 5, relativeHeadingInDegrees = 0, relativeAltitudeInMeters = 0, - registryKey = "FG_small_Helipad", - critical = true, + registryKey = "Tower Crane", func = function(ctx) - if not ctx.spawnedObj then return false end - ctx.scene._params.farpName = ctx.spawnedObj:getName() + if ctx.spawnedObj then + ctx.scene._params._craneObj = ctx.spawnedObj + end return true end, }, -- ---------------------------------------------------------------- - -- Step 2: Fuel truck — right side under tent (t0 + 5 s). + -- Step 5: FG_small_Helipad_Under_Construction — visual construction + -- phase, same footprint as the final pad (t0 + 10.5 s). + -- Reference saved in scene._params for later destruction. -- ---------------------------------------------------------------- { - polar = { distance = 60, angle = 342.5 }, + polar = { distance = 58, angle = 0 }, delayAfterPreviousStep = 5, - relativeHeadingInDegrees = 90, + relativeHeadingInDegrees = 0, relativeAltitudeInMeters = 0, - registryKey = "Fuel_Truck", + registryKey = "FG_small_Helipad_Under_Construction", + func = function(ctx) + if ctx.spawnedObj then + ctx.scene._params._underConstObj = ctx.spawnedObj + end + return true + end, }, -- ---------------------------------------------------------------- - -- Step 3: Repair truck — left side under tent (t0 + 5 s). + -- Step 6: Destroy FG_small_Helipad_Under_Construction — end of + -- construction animation (t0 + 30.5 s). No delay after this step. -- ---------------------------------------------------------------- { - polar = { distance = 61, angle = 340.5 }, + delayAfterPreviousStep = 20, + func = function(ctx) + local obj = ctx.scene._params._underConstObj + if obj and Object.isExist(obj) then + obj:destroy() + end + ctx.scene._params._underConstObj = nil + return true + end, + }, + + -- ---------------------------------------------------------------- + -- Step 7: FG_small_Helipad heliport — finished pad revealed + -- immediately after construction (t0 + 30.5 s). + -- critical=true: if the mod is absent the helipad cannot spawn; + -- abort the whole scene rather than deploying equipment with no pad. + -- Saves the spawned airbase name for the warehouse-stocking step. + -- ---------------------------------------------------------------- + { + polar = { distance = 58, angle = 0 }, delayAfterPreviousStep = 0, - relativeHeadingInDegrees = 90, + relativeHeadingInDegrees = 0, relativeAltitudeInMeters = 0, - registryKey = "repare_Truck", + registryKey = "FG_small_Helipad", + critical = true, + func = function(ctx) + if not ctx.spawnedObj then return false end + ctx.scene._params.farpName = ctx.spawnedObj:getName() + return true + end, }, -- ---------------------------------------------------------------- - -- Step 4: Tent — over both trucks (t0 + 5.5 s). + -- Step 8: Destroy Tower Crane — construction prop removed 5 s + -- after the finished pad appears (t0 + 35.5 s). -- ---------------------------------------------------------------- { - polar = { distance = 61, angle = 341 }, - delayAfterPreviousStep = 0.5, - relativeHeadingInDegrees = 90, - relativeAltitudeInMeters = 0, - registryKey = "FARP_Tent", + delayAfterPreviousStep = 5, + func = function(ctx) + local obj = ctx.scene._params._craneObj + if obj and Object.isExist(obj) then + obj:destroy() + end + ctx.scene._params._craneObj = nil + return true + end, }, -- ---------------------------------------------------------------- - -- Step 5: Ammo cargo (t0 + 10 s). + -- Step 9: Ammo cargo (t0 + 35.5 s). -- ---------------------------------------------------------------- { polar = { distance = 75, angle = 346 }, - delayAfterPreviousStep = 4.5, + delayAfterPreviousStep = 0, relativeHeadingInDegrees = 0, relativeAltitudeInMeters = 0, registryKey = "ammo_cargo", }, -- ---------------------------------------------------------------- - -- Step 6: M92 light panel at tent height (t0 + 15 s). + -- Step 10: M92 light panel at tent height (t0 + 40 s). -- ---------------------------------------------------------------- { polar = { distance = 75, angle = 355 }, - delayAfterPreviousStep = 5, + delayAfterPreviousStep = 4.5, relativeHeadingInDegrees = 310, relativeAltitudeInMeters = 4, registryKey = "NF-2_LightOn", }, -- ---------------------------------------------------------------- - -- Step 7: Windsock near the light, same timing (t0 + 15 s). + -- Step 11: Windsock near the light, same timing (t0 + 40 s). -- ---------------------------------------------------------------- { polar = { distance = 73, angle = 346 }, @@ -246,7 +341,7 @@ metalFarpScene.steps = { }, -- ---------------------------------------------------------------- - -- Step 8: Carrier Seaman on the helipad (t0 + 15 s). + -- Step 12: Carrier Seaman on the helipad (t0 + 40 s). -- ---------------------------------------------------------------- { polar = { distance = 67, angle = 2 }, @@ -257,7 +352,7 @@ metalFarpScene.steps = { }, -- ---------------------------------------------------------------- - -- Step 9: Stock warehouse + completion message (t0 + 20 s). + -- Step 13: Stock warehouse + completion message (t0 + 45 s). -- Fills all fuel types so aircraft can refuel/rearm at this point. -- ---------------------------------------------------------------- { diff --git a/plugins/metal-farp/tests/metal_farp_spec.lua b/plugins/metal-farp/tests/metal_farp_spec.lua new file mode 100644 index 0000000..d84a8f4 --- /dev/null +++ b/plugins/metal-farp/tests/metal_farp_spec.lua @@ -0,0 +1,65 @@ +---@diagnostic disable +-- plugins/metal-farp/tests/metal_farp_spec.lua +-- Smoke tests for the Metal FARP scene: verifies registration, crate declaration, +-- modTypes contents, step count, and critical flag on the helipad spawn step. +-- ============================================================ + +local ROOT = debug.getinfo(1, "S").source:match("^@(.+)plugins[\\/]") or "" + +describe("metal-farp plugin scene", function() + + setup(function() + dofile(ROOT .. "plugins/metal-farp/src/CTLD_metalFarpScene.lua") + end) + + it("registers the Metal FARP scene model", function() + assert.is_true(CTLDSceneManager.getInstance():isSceneEnabled("Metal FARP")) + end) + + it("declares a crate for the Request Equipment menu", function() + local model = CTLDSceneManager.getInstance():getModel("Metal FARP") + assert.is_not_nil(model.crate) + assert.is_not_nil(model.crate.weight) + end) + + it("declares requiresCtld", function() + local model = CTLDSceneManager.getInstance():getModel("Metal FARP") + assert.equals("2.0.0", model.requiresCtld) + end) + + it("lists both mod types including the under-construction variant", function() + local model = CTLDSceneManager.getInstance():getModel("Metal FARP") + local modTypeSet = {} + for _, t in ipairs(model.modTypes) do modTypeSet[t] = true end + assert.is_true(modTypeSet["FG_small_Helipad"], + "modTypes must contain FG_small_Helipad") + assert.is_true(modTypeSet["FG_small_Helipad_Under_Construction"], + "modTypes must contain FG_small_Helipad_Under_Construction") + end) + + it("has exactly 13 steps", function() + local model = CTLDSceneManager.getInstance():getModel("Metal FARP") + assert.equals(13, #model.steps) + end) + + it("marks the FG_small_Helipad spawn step as critical", function() + local model = CTLDSceneManager.getInstance():getModel("Metal FARP") + local found = false + for _, step in ipairs(model.steps) do + if step.registryKey == "FG_small_Helipad" then + assert.is_true(step.critical, + "FG_small_Helipad step must have critical=true") + found = true + end + end + assert.is_true(found, "A step with registryKey FG_small_Helipad must exist") + end) + + it("Tower Crane is NOT in modTypes (it is a stock DCS object)", function() + local model = CTLDSceneManager.getInstance():getModel("Metal FARP") + for _, t in ipairs(model.modTypes) do + assert.is_not_equal("Tower Crane", t) + end + end) + +end)