From 7f074781f489ec9dbf76858cd9d5f0b5c20d0dc1 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 12 Apr 2026 12:51:13 +0200 Subject: [PATCH 1/7] xx --- Moose Development/Moose/Core/Menu.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index aacb018a6..550fff9ca 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -869,10 +869,21 @@ do local MenuTable = {} for MenuText, Menu in pairs( self.Menus or {} ) do local tag = Menu.MenuTag or math.random(1,10000) - MenuTable[#MenuTable+1] = {Tag=tag, Enty=Menu} + MenuTable[#MenuTable+1] = {Tag=tag, Entry=Menu} end - table.sort(MenuTable, function (k1, k2) return k1.tag < k2.tag end ) - for _, Menu in pairs( MenuTable ) do + local function SortTable(k1,k2) + if not k1 then + if not k2 then return true else return false end + elseif not k2 then + if not k1 then return true else return false end + else + return (k1.Tag or 15) <= (k2.Tag or 15) + end + return false + end + table.sort(MenuTable, SortTable) + --table.sort(MenuTable, function (k1, k2) return (k1.tag or 15) <= (k2.tag or 15) end ) + for _, Menu in ipairs( MenuTable ) do Menu.Entry:Refresh() end end From dbc752253b4e91e4c75cb87bedcbfb131837ec83 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 12 Apr 2026 12:56:15 +0200 Subject: [PATCH 2/7] #TARS Initial Release --- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/TARS.lua | 1947 ++++++++++++++++++++++++++ Moose Setup/Moose.files | 1 + 3 files changed, 1949 insertions(+) create mode 100644 Moose Development/Moose/Ops/TARS.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 528c86bac..e937c92ab 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -115,6 +115,7 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Squadron.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Target.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyGCICAP.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyA2G.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/TARS.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/ShapeBase.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Circle.lua' ) diff --git a/Moose Development/Moose/Ops/TARS.lua b/Moose Development/Moose/Ops/TARS.lua new file mode 100644 index 000000000..496451169 --- /dev/null +++ b/Moose Development/Moose/Ops/TARS.lua @@ -0,0 +1,1947 @@ +--- **TARS — Tactical Air Recon System** +-- +-- Simulates photo-reconnaissance and visual-observation missions in DCS World. +-- Players fly designated recon aircraft or helicopters, activate the film from +-- the F10 radio menu, overfly enemy units within the sensor's flight envelope, +-- and return to an allied airbase or FARP to trigger a debrief. +-- Detected targets are published as coalition-only F10 map markers and optional +-- scoring credits are awarded. + +--- +-- @module Ops.TARS +-- @author FMD — Fredy +-- @author Applevangelist - Moose migration with documentation, helpers: Claude.AI +-- @image Ops_PlayerRecce.png + +--- +-- @type TARS_SESSION +-- @extends Core.Base#BASE +-- @field #boolean debug Enable debugging in TARS_SESSION. +-- @field Wrapper.Unit#UNIT unit The MOOSE UNIT object for the recon aircraft. +-- @field #string lid Log-line prefix shown in Moose.log entries. +-- @field DCS#Vec3 vec3 World position snapshot taken at session creation. +-- @field #number coa Coalition side (1 = Red, 2 = Blue). +-- @field #string type DCS type name of the aircraft (key into `TARS.parameters`). +-- @field Wrapper.Group#GROUP group MOOSE GROUP the aircraft belongs to. +-- @field #number groupID Numeric group ID. +-- @field #string objectName DCS unit name — used as the registry key. +-- @field #string playerName Human-readable player name as shown in the scoreboard. +-- @field #number playerID Numeric DCS unit ID, used as F10-menu and message key. +-- @field #table ammo Ammo table snapshot from `unit:GetAmmo()`. +-- @field #number time Mission time (seconds) when the session was last refreshed. +-- @field #number category Group category at session creation. +-- @field #boolean capturing `true` while the capture loop is actively running. +-- @field #number duration Remaining film in seconds. +-- @field #table targetList Targets detected this pass, not yet reported. `[unitName] = snap`. +-- @field #number captureCount Total unique targets captured this sortie. +-- @field #boolean loop `true` while the `TARS.CaptureLoop` timer is scheduled. +-- @field #boolean standby `true` when film is paused without ending the session. +-- @field #boolean filmExhausted `true` when `duration` reached zero. +-- @field #boolean sessionEnded `true` after STOP or film exhaustion; awaiting debrief. +-- @field #boolean wasCapturing `true` if capture was active at the moment of touchdown. +-- @field #boolean landingScheduled `true` once the debrief timer has been registered. +-- @field #number lastTakeoffTime Mission time of the last takeoff, used for debouncing. +-- @field #TARS Callback The TARS Callback object for menu functions etc. + +--- +-- ## Tracks all states for a single recon sortie. +-- +-- One instance is created per player per flight when they take off from a +-- validated slot. It holds the remaining film, the list of targets captured +-- this pass, and all flags that drive the capture state-machine. +-- +-- Instances are stored in `TARS.instances[unitName]` and are reset (not +-- destroyed) at the end of each successful debrief so the pilot can fly +-- another sortie in the same slot without rejoining. +-- +-- @field #TARS_SESSION +TARS_SESSION = {} +TARS_SESSION.debug = false + +--- @type TARS +-- @extends Core.Base#BASE +-- @field #string version Semantic version string, e.g. `"v2.1.0"`. +-- @field #string lid Log-line prefix shown in Moose.log entries. +-- @field #string locale Active locale: `"en"` (default), `"de"`, `"fr"`. +-- @field #boolean mooseScoring Enable MOOSE SCORING backend. +-- @field #boolean debug Enable debugging in TARS. +-- @field #number valueScoring Points awarded per detected target (MOOSE path). +-- @field #number landingDelay Seconds after touchdown before landing is confirmed. +-- @field #number debriefDelay Seconds after confirmation before F10 marks appear. +-- @field #number landingDistance Max distance (m) from an allied base/FARP for a valid debrief. +-- @field #number _vAltMin Minimum AGL (m) for visual-recon helicopters. +-- @field #number _vRangeMin Detection radius (m) at `_vAltMin`. +-- @field #number _vAltOpti Optimal AGL (m) for visual-recon helicopters. +-- @field #number _vRangeOpti Detection radius (m) at `_vAltOpti`. +-- @field #number _vAltMax Maximum AGL (m) for visual-recon helicopters. +-- @field #number _vRangeMax Detection radius (m) at `_vAltMax`. +-- @field #boolean filmLimitEnabled Cap the number of detections per sortie. +-- @field #number filmLimitMax Maximum unique detections allowed per sortie. +-- @field #boolean detectUnits Master toggle for detecting DCS Unit objects. +-- @field #table units Sub-toggles `{ air, ground, ship }` for unit categories. +-- @field #boolean detectStatics Master toggle for detecting DCS Static objects. +-- @field #table statics Sub-config for static filtering (`farps`, `captureExceptions`, whitelist). +-- @field #table recoNameFilter `{ enabled=#boolean, keyword=#string }` — restricts TARS menus to matching group names. +-- @field #table targetNameFilter `{ enabled=#boolean, keywords=#table }` — per-coalition keyword lists for target filtering. +-- @field #table reconTypes Map of `[typeName] = true` for all recon-capable DCS type names. +-- @field #table parameters Map of `[typeName] = #TARS.PlatformParams` with per-platform sensor profiles. +-- @field #table allowedAmmo Map of `[weaponDisplayName] = true` for permitted loadout items. +-- @field #table Locale Map of `[messageID] = TEXTANDSOUND` — populated by `TARS_Locale.lua`. +-- @field #table instances Runtime map `[unitName] = #TARS_SESSION` of active sorties. +-- @field #table groundMenus Runtime map `[playerName] = #TARS.MenuData` of open F10 menus. +-- @field #table detectedTargets Lifetime map `[unitName] = #TARS.Snapshot` of all reported targets. +-- @field #table marks `{ blue = {}, red = {} }` — maps `[unitName] = markID`. +-- @field #number redMarkCount Next available mark ID for Red coalition marks. +-- @field #number blueMarkCount Next available mark ID for Blue coalition marks. +-- @field Functional.Scoring#SCORING scoring MOOSE SCORING instance (nil if mooseScoring is false). + +--- +-- ## Simulates photo-reconnaissance and visual-observation missions in DCS World. +-- +-- Players fly designated recon aircraft or helicopters, activate the film from +-- the F10 radio menu, overfly enemy units within the sensor's flight envelope, +-- and return to an allied airbase or FARP to trigger a debrief. +-- Detected targets are published as coalition-only F10 map markers and optional +-- scoring credits are awarded. +-- +-- ## Quick-start +-- The system initialises automatically at the bottom of this file: +-- +-- local locale = "de" -- optional, default is "en" +-- TARS_Instance = TARS:New(locale) +-- +-- Nothing else is required. Adjust the configuration fields at the top of the +-- file (`TARS.filmLimitMax`, `TARS.parameters`, `TARS.allowedAmmo`, …) to fit +-- your mission before loading. +-- +-- ## Player workflow +-- 1. Spawn into a recon-capable slot and open the **F10 › Task TARS** radio menu. +-- 2. Select **TARS validation** on the ground. The system checks your loadout and +-- reports the platform's altitude band, FOV, and available film. +-- The validation item then disappears once approved. +-- 3. Take off. **Start filming / STB & Resume / Stop filming** appear in the menu. +-- 4. Select **TARS mode : Start filming** to begin recording. +-- 5. Fly over enemy units within the sensor's altitude/attitude envelope. +-- Each detected unit is confirmed in the HUD (`+1 Captured target`). +-- 6. Use **TARS mode : STB & Resume** to pause (e.g. to refuel). +-- Film resumes automatically on the next takeoff if the loadout is still valid. +-- 7. Select **TARS mode : Stop filming** or let the film timer expire. +-- 8. Land at an allied airbase or FARP within `TARS.landingDistance` metres. +-- The film controls disappear. After `landingDelay + debriefDelay` seconds the +-- intel marks appear on the F10 map and scoring credits are awarded. +-- After the debrief the **TARS validation** item reappears for the next sortie. +-- +-- ## Localization +-- All player-facing strings are defined in `TARS.Messages` using a table. +-- +-- +-- ## Platforms and Settings +-- +-- Player **group name** filters +-- +-- TARS.recoNameFilter = { enabled=false, keyword="Reco" } -- Only allow groups with this keyword in the name +-- +-- ### Adding a new platform +-- +-- Add an entry to `TARS.reconTypes` and a matching typename and profile to `TARS.parameters`: +-- +-- TARS_Instance.reconTypes["F-16C_50"] = true +-- TARS_Instance.parameters["F-16C_50"] = { +-- minAlt=300, maxAlt=8000, maxRoll=10, maxPitch=15, +-- fov=35, duration=300, offset=math.rad(20), +-- name="F-16C with RECCE pod" +-- } +-- +-- ### Available Platforms are +-- +-- TARS.reconTypes = { +-- ["MiG-21Bis"]=true, ["AJS37"]=true, ["Mirage-F1EE"]=true, +-- ["F-5E-3"]=true, ["F-14A-135-GR"]=true, ["F-14B"]=true, +-- ["F-4E-45MC"]=true, ["P-51D"]=true, ["P-51D-30-NA"]=true, +-- ["SpitfireLFMkIX"]=true, ["FW-190A8"]=true, ["FW-190D9"]=true, +-- ["SA342M"]=true, ["SA342L"]=true, ["UH-1H"]=true, ["OH58D"]=true, +-- ["Mi-8MT"]=true, ["MH-6J"]=true, +-- ["OH-6A"]=true,} +-- +-- ### Allowed weapon types on the platforms are - will be validated by the script +-- +-- TARS.allowedAmmo = { +-- ["AIM-9B"]=true,["AIM-9D"]=true,["AIM-9E"]=true,["AIM-9G"]=true, +-- ["AIM-9H"]=true,["AIM-9J"]=true,["AIM-9L"]=true,["AIM-9M"]=true, +-- ["AIM-9N"]=true,["AIM-9P"]=true,["AIM-9P3"]=true,["AIM-9P5"]=true, +-- ["AIM-9JULI"]=true, +-- ["R-3S"]=true,["R-13M"]=true,["R-13M1"]=true,["R-60"]=true,["R-60M"]=true, +-- ["R550 Magic II"]=true, +-- ["7_62x51"]=true,} +-- +-- ### Film and detection settings +-- +-- TARS.filmLimitEnabled = true +-- TARS.filmLimitMax = 25 -- max 25 captured objects +-- TARS.detectUnits = true -- capture UNIT objects +-- TARS.detectStatics = false -- capture STATIC objects incl. of FARPs +-- +-- ### UNIT Filters +-- +-- TARS.units = { air=false, ground=true, ship=true } +-- +-- ### STATIC Filters +-- +-- TARS.statics = { +-- farps=true, +-- captureExceptions=false, captureExceptionsList={}, -- these are mutually exclusive, either exceptions from the typename list captureExceptionsList **or** +-- captureUnique=false, captureUniqueList={},} -- unique (once only) typenames from the captureUniqueList! +-- +-- +-- ### Other Settings +-- +-- TARS.debug = false +-- TARS.mooseScoring = true -- if true use MOOSE scoring +-- TARS.valueScoring = 100 -- points per detection +-- TARS.landingDelay = 30 -- check valid landing after this many seconds +-- TARS.debriefDelay = 60 -- show debriefing after this many seconds +-- TARS.landingDistance = 2500 -- land closer than this many meters to a friendly base for debrief +-- +-- ## SRS integration +-- All player messages route through `TARS:_MsgUnit()` and `TARS:_MsgCoalition()`. +-- To broadcast over SRS, init the SRS system with `TARS_Instance:SetSRS(...)`, provide the necessary parameters. +-- +-- ## Scoring +-- Two scoring backends are supported: +-- +-- * **MOOSE SCORING** — set `TARS_Instance.mooseScoring = true`. Requires a SCORING object +-- and awards `TARS_Instance.valueScoring` points per target. +-- +-- * **DCSBot** — fallback when mooseScoring is false. Awards `ceil(count/4)` credits +-- via `dcsbot.addUserPoints()` if the DCSBot table is present. +-- +-- ## Mission Scripting integration +-- +-- Moose FSM Style callback functions are available for mission designers. Optionally overwrite with own function. Processed after landing on debriefing analysis: +-- +-- function mytars:OnBeforeDataProcessing(Snapshot) -- provides a #TARS.Snapshot data table for a captured object, function must return true to call the OnAfterDataProcessing() function next. +-- +-- function mytars:OnAfterDataProcessing(Snapshot) -- provides a #TARS.Snapshot data table for a captured object for use in your mission script. +-- +-- @field #TARS +TARS = {} + +--- Platform sensor/camera profile. +-- Stored in `TARS.parameters[typeName]`. +-- @type TARS.PlatformParams +-- @field #number minAlt Minimum AGL altitude (m) for valid detections. +-- @field #number maxAlt Maximum AGL altitude (m) for valid detections. +-- @field #number maxRoll Maximum bank angle (degrees) — camera must be level. +-- @field #number maxPitch Maximum pitch angle (degrees) — camera must be level. +-- @field #number fov Camera half-angle FOV (degrees). Not used for visual-recon helis. +-- @field #number duration Total film per sortie (seconds). +-- @field #number offset Forward look-ahead (radians). +-- @field #string name Human-readable label in player messages. +-- @field #number minRange Detection radius (m) at `minAlt` — visual-recon helis only. +-- @field #number optimalAlt Optimal AGL (m) — triggers the dual-cone model. +-- @field #number optimalRange Detection radius (m) at `optimalAlt`. +-- @field #number maxRange Detection radius (m) at `maxAlt`. + +--- F10 menu registration data. +-- Stored in `TARS.groundMenus[playerName]`. +-- @type TARS.MenuData +-- @field Core.Menu#MENU_GROUP menuHandle Root sub-menu. Call `:Remove()` to destroy. +-- @field Core.Menu#MENU_GROUP_COMMAND itemValidate "TARS validation" — present in GROUND_NEW; removed after validation. +-- @field Core.Menu#MENU_GROUP_COMMAND itemInfo "TARS capture config" — always present. +-- @field Core.Menu#MENU_GROUP_COMMAND itemStart "Start filming" — added on takeoff. +-- @field Core.Menu#MENU_GROUP_COMMAND itemStb "STB & Resume" — added on takeoff. +-- @field Core.Menu#MENU_GROUP_COMMAND itemStop "Stop filming" — added on takeoff. +-- @field #boolean approved `true` after successful ground validation. +-- @field #string unitName DCS unit name. +-- @field #number groupID Numeric group ID. +-- @field #string playerName Player display name. + +--- Frozen target snapshot. +-- @type TARS.Snapshot +-- @field Wrapper.Unit#UNIT unit MOOSE UNIT wrapper or STATIC wrapper. +-- @field DCS#Object dcsObj Raw DCS object reference. +-- @field #number category `Object.Category.*` of the detected object. +-- @field #string type DCS type name. +-- @field #string name DCS unit name (registry key). +-- @field DCS#Vec3 point World position at detection time. +-- @field #number time Mission time of the snapshot. +-- @field #number groupID Group ID (units only). +-- @field #number groupCat Group category (units only). +-- @field #number coa Coalition side (units only). +-- @field #table ammo Ammo table at detection (units only). +-- @field #number life Health 0-100 at detection (units only). +-- @field #string playername Player name who captured the data. + +------------------------------------------------- +-- TODO VERSION & LOCALE +------------------------------------------------- + +--- @field #string version +TARS.version = "v2.2.1" + +--- Active locale. Set before `TARS:New()`. Populated by `TARS_Locale.lua`. +-- @field #string locale +TARS.locale = TARS.locale or "en" + +------------------------------------------------- +-- TODO CONFIGURATION +------------------------------------------------- + +TARS.debug = false +TARS.mooseScoring = true +TARS.valueScoring = 100 +TARS.landingDelay = 30 +TARS.debriefDelay = 60 +TARS.landingDistance = 2500 + +--- @field #number _vAltMin +TARS._vAltMin = 10 +--- @field #number _vRangeMin +TARS._vRangeMin = TARS._vAltMin * 20 -- 200 m +--- @field #number _vAltOpti +TARS._vAltOpti = 500 +--- @field #number _vRangeOpti +TARS._vRangeOpti = TARS._vAltOpti * 5 -- 2500 m +--- @field #number _vAltMax +TARS._vAltMax = 1500 +--- @field #number _vRangeMax +TARS._vRangeMax = TARS._vAltMax * 3 -- 4500 m + +TARS.filmLimitEnabled = true +TARS.filmLimitMax = 25 +TARS.detectUnits = true +TARS.detectStatics = false + +--- @field #table units +TARS.units = { air=false, ground=true, ship=true } + +--- @field #table statics +TARS.statics = { + farps=true, + captureExceptions=false, captureExceptionsList={}, + captureUnique=false, captureUniqueList={}, +} + +--- @field #table recoNameFilter +TARS.recoNameFilter = { enabled=false, keyword="Reco" } + +--- @field #table targetNameFilter +TARS.targetNameFilter = { + enabled = true, + keywords = { + [coalition.side.BLUE] = { "USA" }, + [coalition.side.RED] = { "USSR" }, + }, +} + +------------------------------------------------- +-- TODO RECON-CAPABLE PLATFORMS +------------------------------------------------- + +--- @field #table reconTypes +TARS.reconTypes = { + ["MiG-21Bis"]=true, ["AJS37"]=true, ["Mirage-F1EE"]=true, + ["F-5E-3"]=true, ["F-14A-135-GR"]=true, ["F-14B"]=true, + ["F-4E-45MC"]=true, ["P-51D"]=true, ["P-51D-30-NA"]=true, + ["SpitfireLFMkIX"]=true, ["FW-190A8"]=true, ["FW-190D9"]=true, + ["SA342M"]=true, ["SA342L"]=true, ["UH-1H"]=true, ["OH58D"]=true, + ["Mi-8MT"]=true, ["MH-6J"]=true, + ["OH-6A"]=true, +} + +------------------------------------------------- +-- TODO PER-PLATFORM PARAMETERS +------------------------------------------------- + +--- @field #table parameters +TARS.parameters = {} + +TARS.parameters["F-4E-45MC"] = { minAlt=100, maxAlt=6096, maxRoll=10, maxPitch=15, fov=23, duration=120, offset=math.rad(60), name="RF-4E with KS-87 Forward Oblique Camera" } +TARS.parameters["MiG-21Bis"] = { minAlt=500, maxAlt=5000, maxRoll=10, maxPitch=15, fov=52, duration=140, offset=math.rad(10), name="MiG-21R with Day recce pod" } +TARS.parameters["AJS37"] = { minAlt=15, maxAlt=1524, maxRoll=10, maxPitch=15, fov=25, duration=120, offset=math.rad(10), name="SF 37" } +TARS.parameters["Mirage-F1EE"] = { minAlt=1524, maxAlt=4572, maxRoll=10, maxPitch=15, fov=20, duration=588, offset=math.rad(10), name="Mirage-F1CR with Omera 33" } +TARS.parameters["F-5E-3"] = { minAlt=762, maxAlt=7620, maxRoll=15, maxPitch=15, fov=70, duration=300, offset=math.rad(40), name="F-5E Tigereye" } +TARS.parameters["F-14A-135-GR"] = { minAlt=750, maxAlt=5000, maxRoll=10, maxPitch=20, fov=14, duration=400, offset=math.rad(45), name="F-14A TARPS KS-87D" } +TARS.parameters["F-14B"] = { minAlt=228, maxAlt=1524, maxRoll=10, maxPitch=20, fov=85, duration=80, offset=math.rad(10), name="F-14B TARPS KA-99A" } +TARS.parameters["TF-51D"] = { minAlt=250, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), name="TF-51D Mustang RF-51D Photo Recon" } +TARS.parameters["P-51D"] = { minAlt=250, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), name="P-51D Mustang F-6D Photo Recon" } +TARS.parameters["P-51D-30-NA"] = { minAlt=250, maxAlt=6000, maxRoll=15, maxPitch=15, fov=60, duration=400, offset=math.rad(10), name="P-51D-30 Mustang F-6D Photo Recon" } +TARS.parameters["SpitfireLFMkIX"]= { minAlt=150, maxAlt=5000, maxRoll=15, maxPitch=15, fov=55, duration=350, offset=math.rad(10), name="Spitfire LF Mk IX PR Recon" } +TARS.parameters["FW-190A8"] = { minAlt=200, maxAlt=5500, maxRoll=15, maxPitch=15, fov=60, duration=350, offset=math.rad(10), name="FW-190 A-8 Tactical Recon" } +TARS.parameters["FW-190D9"] = { minAlt=250, maxAlt=6000, maxRoll=15, maxPitch=15, fov=60, duration=350, offset=math.rad(10), name="FW-190 D-9 Tactical Recon" } +TARS.parameters["SA342M"] = { minAlt=20, maxAlt=1000, maxRoll=35, maxPitch=25, fov=18, duration=350, offset=math.rad(10), name="SA342M EO/IR LIGHT RECO" } +TARS.parameters["SA342L"] = { minAlt=20, maxAlt=1000, maxRoll=35, maxPitch=25, fov=18, duration=350, offset=math.rad(10), name="SA342L EO/IR LIGHT RECO" } +TARS.parameters["OH58D"] = { minAlt=30, maxAlt=1200, maxRoll=35, maxPitch=25, fov=12, duration=350, offset=math.rad(12), name="OH-58D MMS EO/IR RECO" } +TARS.parameters["UH-1H"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="UH-1H VISUAL/CREW RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } +TARS.parameters["Mi-8MT"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="Mi-8MT VISUAL/CREW RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } +TARS.parameters["MH-6J"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="MH-6J VISUAL CLOSE RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } +TARS.parameters["OH-6A"] = { maxRoll=50, maxPitch=45, duration=900, offset=math.rad(6), name="OH-6A Cayuse VISUAL CLOSE RECO", minAlt=TARS._vAltMin, minRange=TARS._vRangeMin, optimalAlt=TARS._vAltOpti, optimalRange=TARS._vRangeOpti, maxAlt=TARS._vAltMax, maxRange=TARS._vRangeMax } + +------------------------------------------------- +-- TODO ALLOWED WEAPONS WHITELIST +------------------------------------------------- + +--- @field #table allowedAmmo +TARS.allowedAmmo = { + ["AIM-9B"]=true,["AIM-9D"]=true,["AIM-9E"]=true,["AIM-9G"]=true, + ["AIM-9H"]=true,["AIM-9J"]=true,["AIM-9L"]=true,["AIM-9M"]=true, + ["AIM-9N"]=true,["AIM-9P"]=true,["AIM-9P3"]=true,["AIM-9P5"]=true, + ["AIM-9JULI"]=true, + ["R-3S"]=true,["R-13M"]=true,["R-13M1"]=true,["R-60"]=true,["R-60M"]=true, + ["R550 Magic II"]=true, + ["7_62x51"]=true, +} + +------------------------------------------------- +-- TODO RUNTIME STATE +------------------------------------------------- + +TARS.instances = {} +TARS.groundMenus = {} +TARS.detectedTargets = {} +TARS.marks = { blue={}, red={} } +TARS.redMarkCount = 150000 +TARS.blueMarkCount = 160000 +TARS.scoring = nil + +--- **TARS_Locale — Localization for the Tactical Air Recon System** +-- +-- This file defines all player-facing strings for TARS in English (en), +-- German (de), and French (fr) using the MOOSE `TEXTANDSOUND` class. +-- +-- ## How to use +-- Load this file **after** TARS.lua: +-- +-- dofile(basedir .. "Moose_.lua") +-- dofile(basedir .. "TARS.lua") +-- dofile(basedir .. "TARS_Locale.lua") -- ← this file +-- +-- Then set the desired locale before (or after) calling `TARS:New()`: +-- +-- TARS.locale = "de" -- "en" (default), "de", "fr" +-- TARS_Instance = TARS:New() +-- +-- ## Adding a new language +-- Any key without a translation for the chosen locale automatically falls +-- back to English. +-- +-- ## Strings with format placeholders +-- Entries that contain `%d` or `%s` are passed through `string.format()` +-- inside `TARS:_T()`. Pass the values as extra arguments: +-- +-- self:_MsgUnit( self:_Txt("TARS_FILM_START", self.duration), 5, playerName ) +-- +-- @module TARS_Locale +-- @author FMD — Fredy +-- @author Applevangelist - Moose migration, Claude.AI + +------------------------------------------------- +-- LOCALE CONFIGURATION +------------------------------------------------- + +--- Active locale used by `TARS:_T()`. +-- Set this before `TARS:New()` is called. +-- Supported values: `"en"` (default), `"de"`, `"fr"`. +-- @field #string locale +TARS.locale = TARS.locale or "en" + +------------------------------------------------- +-- TODO LOCALE TABLE +-- Each entry is a TEXTANDSOUND object keyed by a message ID string. +------------------------------------------------- + +--- Map of `[messageID] = TEXTANDSOUND` objects for all player-facing strings. +-- @field #table Locale +--- **TARS.Messages — Localization strings for the Tactical Air Recon System** +-- +-- All player-facing strings are stored in a single `TARS.Messages` table, +-- keyed first by locale (`"en"`, `"de"`, `"fr"`) and then by message ID. +-- +-- ## Resolving a string +-- Use `TARS:_T(id, ...)` anywhere inside TARS methods. +-- The helper looks up `TARS.Messages[TARS.locale][id]`, falls back to `"en"`, +-- and optionally passes extra arguments through `string.format`: +-- +-- self:_MsgUnit( self:_Txt("TARS_FILM_START", self.duration), 5, playerName ) +-- +-- ## Adding a new language +-- Add a new locale block (e.g. `es = { ... }`) following the same keys as `en`. +-- Any missing key automatically falls back to English at runtime. +-- +-- ## Strings with format placeholders +-- `%d` = number, `%s` = string. The number and order of placeholders must +-- match between all languages for a given key. +-- +-- @module TARS_Locale +-- @author FMD — Fredy +-- @author Applevangelist + +------------------------------------------------- +-- LOCALE CONFIGURATION +------------------------------------------------- + +--- Active locale used by `TARS:_T()`. Set before `TARS:New()`. +-- @field #string locale +TARS.locale = "en" + +------------------------------------------------- +-- MESSAGE TABLE +------------------------------------------------- + +--- Nested localization table: `TARS.Messages[locale][messageID] = string`. +-- @field #table Messages +TARS.Messages = { + + -- ========================================================== + -- ENGLISH + -- ========================================================== + en = { + -- Capture state + TARS_FILM_START = "[TARS] Session capture activated. Film remaining: %d seconds.", + TARS_FILM_EXHAUSTED = "[TARS] Film exhausted. Return to base for debrief.", + TARS_FILM_STOP = "[TARS] Session capture ended. Return to base for debrief.", + TARS_FILM_TIME_UP = "[TARS] Film time exhausted. Return to base.", + TARS_FILM_CAP_REACHED = "[TARS] Maximum captures reached (%d). Return to base for debrief.", + TARS_FILM_STB_MANUAL = "[TARS] <>Manual<> Film manual STB.", + TARS_FILM_RESUME_MANUAL = "[TARS] <>Manual<> Film manual resume.", + TARS_FILM_STB_LAND = "[TARS] <>Landing<> Film auto STB.", + TARS_FILM_RESUME_TO = "[TARS] <>TakeOff<> Film auto resume. %d seconds", + TARS_FILM_STB_LOCKED = "[TARS] Film is STB — takeoff to resume.", + TARS_FILM_ALREADY_ACTIVE = "[TARS] Film already active.", + TARS_FILM_NO_CAPTURE = "[TARS] No active film.", + TARS_FILM_NO_CAPTURE_STOP = "[TARS] No active film to stop.", + TARS_CAPTURE_TICK = "FILM DURATION: %d seconds", + TARS_CAPTURE_HIT = "[TARS] +1 Captured target (%d total)", + TARS_CAPTURE_HIT_MAX = "[TARS] +1 Captured target (%d total) / %d max", + + -- Session / debrief + TARS_SESSION_ENDED = "[TARS] Session ended. Return to base for debrief.", + TARS_LAND_VALIDATED = "[TARS] Landing validated, await your debriefing!", + TARS_LAND_VALIDATED_TIME = "[TARS] Targets shown in %d seconds.", + TARS_NOT_AT_BASE = "[TARS] Not on allied base or FARP. Return for debrief.", + TARS_DEBRIEF_TARGETS = "[TARS] %d targets captured. +%d points.", + TARS_DEBRIEF_CREDITS = "You received %d credits for reconnaissance.", + TARS_DEBRIEF_COALITION = "%s gathered intel on %d targets.", + TARS_READY = "[TARS] Ready for recon.", + + -- Validation + TARS_VALID_OK_HDR = "[TARS] Configuration is valid - Ready for takeoff", + TARS_VALID_REFUSED_WPN = "[TARS] Your configuration loadout is not ready. Check your weapons.", + TARS_VALID_REFUSED_AMMO = "Refused : ammo %s", + TARS_VALID_AIRBORNE = "[TARS] Validation only on ground.", + TARS_VALID_RUNNING = "[TARS] Validation already done — film is running.", + TARS_VALID_GROUP_FILTER = "[TARS] Task available for group name >%s< only.", + TARS_VALIDATE_FIRST = "[TARS] Validate first on ground.", + TARS_NO_SESSION = "[TARS] No session actived.", + TARS_CONFIG_CHANGED = "[TARS] Config changed — session ended.", + TARS_LOADOUT_BAD = "[TARS] Loadout not ready. Check your weapons.", + + -- Platform info labels + TARS_PLATFORM_INFO = "[TARS] Platform information", + TARS_PLATFORM_LABEL = "Platform", + TARS_PLATFORM_ALT = "Altitude", + TARS_PLATFORM_FOV = "FOV", + TARS_PLATFORM_FILM = "Film", + + -- F10 menu item labels + TARS_MENU_ROOT = "Task TARS", + TARS_MENU_VALIDATE = "TARS validation", + TARS_MENU_INFO = "TARS my capture config", + TARS_MENU_START = "TARS mode : Start filming", + TARS_MENU_STB = "TARS mode : Standby & Resume", + TARS_MENU_STOP = "TARS mode : Stop filming", + }, + + -- ========================================================== + -- DEUTSCH + -- ========================================================== + de = { + -- Aufnahmestatus + TARS_FILM_START = "[TARS] Aufnahme aktiviert. Verbleibender Film: %d Sekunden.", + TARS_FILM_EXHAUSTED = "[TARS] Film aufgebraucht. Kehren Sie zur Basis für das Briefing zurück.", + TARS_FILM_STOP = "[TARS] Aufnahmesitzung beendet. Kehren Sie zur Basis zurück.", + TARS_FILM_TIME_UP = "[TARS] Filmzeit abgelaufen. Kehren Sie zur Basis zurück.", + TARS_FILM_CAP_REACHED = "[TARS] Maximale Aufnahmen erreicht (%d). Kehren Sie zur Basis zurück.", + TARS_FILM_STB_MANUAL = "[TARS] <>Manuell<> Film manuell auf Standby.", + TARS_FILM_RESUME_MANUAL = "[TARS] <>Manuell<> Film manuell fortgesetzt.", + TARS_FILM_STB_LAND = "[TARS] <>Landung<> Film automatisch auf Standby.", + TARS_FILM_RESUME_TO = "[TARS] <>Start<> Film automatisch fortgesetzt. %d Sekunden.", + TARS_FILM_STB_LOCKED = "[TARS] Film ist auf Standby — starten Sie, um fortzufahren.", + TARS_FILM_ALREADY_ACTIVE = "[TARS] Aufnahme bereits aktiv.", + TARS_FILM_NO_CAPTURE = "[TARS] Keine aktive Aufnahme.", + TARS_FILM_NO_CAPTURE_STOP = "[TARS] Keine aktive Aufnahme zum Stoppen.", + TARS_CAPTURE_TICK = "AUFNAHMEDAUER: %d Sekunden", + TARS_CAPTURE_HIT = "[TARS] +1 Ziel erfasst (%d gesamt)", + TARS_CAPTURE_HIT_MAX = "[TARS] +1 Ziel erfasst (%d gesamt) / %d max", + + -- Sitzung / Briefing + TARS_SESSION_ENDED = "[TARS] Sitzung beendet. Kehren Sie zur Basis für das Briefing zurück.", + TARS_LAND_VALIDATED = "[TARS] Landung erfolgreich, bitte warten Sie auf Ihr Debriefing!", + TARS_LAND_VALIDATED_TIME = "[TARS] Ziele werden in %d Sekunden angezeigt.", + TARS_NOT_AT_BASE = "[TARS] Nicht auf verbündeter Basis oder FARP. Kehren Sie zurück.", + TARS_DEBRIEF_TARGETS = "[TARS] %d Ziele erfasst. +%d Punkte.", + TARS_DEBRIEF_CREDITS = "Sie erhalten %d Credits für die Aufklärung.", + TARS_DEBRIEF_COALITION = "%s hat Informationen über %d Ziele gesammelt.", + TARS_READY = "[TARS] Bereit zur Aufklärung.", + + -- Validierung + TARS_VALID_OK_HDR = "[TARS] Konfiguration gültig - Bereit zum Start", + TARS_VALID_REFUSED_WPN = "[TARS] Ihre Konfiguration ist nicht bereit. Überprüfen Sie Ihre Waffen.", + TARS_VALID_REFUSED_AMMO = "Abgelehnt : Munition %s", + TARS_VALID_AIRBORNE = "[TARS] Validierung nur am Boden möglich.", + TARS_VALID_RUNNING = "[TARS] Validierung bereits erfolgt — Film läuft.", + TARS_VALID_GROUP_FILTER = "[TARS] Aufgabe nur für Gruppenname >%s< verfügbar.", + TARS_VALIDATE_FIRST = "[TARS] Zuerst am Boden validieren.", + TARS_NO_SESSION = "[TARS] Keine aktive Sitzung.", + TARS_CONFIG_CHANGED = "[TARS] Konfiguration geändert — Aufnahme beendet.", + TARS_LOADOUT_BAD = "[TARS] Ausrüstung nicht bereit. Überprüfen Sie Ihre Waffen.", + + -- Plattforminformationen + TARS_PLATFORM_INFO = "[TARS] Plattforminformationen", + TARS_PLATFORM_LABEL = "Plattform", + TARS_PLATFORM_ALT = "Höhe", + TARS_PLATFORM_FOV = "Sichtfeld", + TARS_PLATFORM_FILM = "Film", + + -- F10-Menüeinträge + TARS_MENU_ROOT = "Aufgabe TARS", + TARS_MENU_VALIDATE = "TARS Validierung", + TARS_MENU_INFO = "TARS meine Aufnahmekonfiguration", + TARS_MENU_START = "TARS Modus : Aufnahme starten", + TARS_MENU_STB = "TARS Modus : Standby & Fortsetzen", + TARS_MENU_STOP = "TARS Modus : Aufnahme stoppen", + }, + + -- ========================================================== + -- FRANÇAIS + -- ========================================================== + fr = { + -- État de capture + TARS_FILM_START = "[TARS] Session de capture activée. Film restant : %d seconds", + TARS_FILM_EXHAUSTED = "[TARS] Film épuisé. Retournez à la base pour le compte-rendu.", + TARS_FILM_STOP = "[TARS] Session de capture terminée. Retournez à la base.", + TARS_FILM_TIME_UP = "[TARS] Temps de film épuisé. Retournez à la base.", + TARS_FILM_CAP_REACHED = "[TARS] Nombre maximum de captures atteint (%d). Retournez à la base.", + TARS_FILM_STB_MANUAL = "[TARS] <>Manuel<> Film en STB manuel.", + TARS_FILM_RESUME_MANUAL = "[TARS] <>Manuel<> Reprise manuel du film.", + TARS_FILM_STB_LAND = "[TARS] <>Atterrissage<> Film en STB automatique.", + TARS_FILM_RESUME_TO = "[TARS] <>Décollage<> Reprise automatique du film. %d seconds", + TARS_FILM_STB_LOCKED = "[TARS] Film en STB — décollez pour reprendre.", + TARS_FILM_ALREADY_ACTIVE = "[TARS] Film déjà activé.", + TARS_FILM_NO_CAPTURE = "[TARS] Aucun film activé.", + TARS_FILM_NO_CAPTURE_STOP = "[TARS] Aucun film actif à stopper.", + TARS_CAPTURE_TICK = "DURÉE DU FILM : %d seconds", + TARS_CAPTURE_HIT = "[TARS] +1 Cible capturée (%d au total)", + TARS_CAPTURE_HIT_MAX = "[TARS] +1 Cible capturée (%d au total) / %d max", + + -- Session / compte-rendu + TARS_SESSION_ENDED = "[TARS] Session terminée. Retournez à la base pour le debriefing.", + TARS_LAND_VALIDATED = "[TARS] Atterrissage validé, attendez votre debriefing !", + TARS_LAND_VALIDATED_TIME = "[TARS] Cibles affichées dans %d seconds.", + TARS_NOT_AT_BASE = "[TARS] Vous n'êtes pas sur une base ou FARP alliée. Retourné pour le debriefing.", + TARS_DEBRIEF_TARGETS = "[TARS] %d cibles capturées. +%d points.", + TARS_DEBRIEF_CREDITS = "Vous avez reçu %d crédits pour la reconnaissance.", + TARS_DEBRIEF_COALITION = "%s a recueilli des renseignements sur %d cibles.", + TARS_READY = "[TARS] Prêt pour la reconnaissance.", + + -- Validation + TARS_VALID_OK_HDR = "[TARS] Configuration valide - Prêt au décollage", + TARS_VALID_REFUSED_WPN = "[TARS] Votre configuration n'est pas prête. Vérifiez vos armes.", + TARS_VALID_REFUSED_AMMO = "Refusé : munition %s", + TARS_VALID_AIRBORNE = "[TARS] Validation uniquement au sol.", + TARS_VALID_RUNNING = "[TARS] Validation déjà effectuée — le film tourne.", + TARS_VALID_GROUP_FILTER = "[TARS] Tâche disponible pour le groupe >%s< uniquement.", + TARS_VALIDATE_FIRST = "[TARS] Validez d'abord au sol.", + TARS_NO_SESSION = "[TARS] Aucune session active.", + TARS_CONFIG_CHANGED = "[TARS] Configuration modifiée — session terminée.", + TARS_LOADOUT_BAD = "[TARS] Chargement pas prêt. Vérifiez vos armes.", + + -- Informations plateforme + TARS_PLATFORM_INFO = "[TARS] Informations sur la plateforme", + TARS_PLATFORM_LABEL = "Plateforme", + TARS_PLATFORM_ALT = "Altitude", + TARS_PLATFORM_FOV = "Champ de vision", + TARS_PLATFORM_FILM = "Film", + + -- Entrées menu F10 + TARS_MENU_ROOT = "Mission TARS", + TARS_MENU_VALIDATE = "TARS validation", + TARS_MENU_INFO = "TARS ma config de capture", + TARS_MENU_START = "TARS mode : Démarrer le film", + TARS_MENU_STB = "TARS mode : Standby & Reprise", + TARS_MENU_STOP = "TARS mode : Arrêter le film", + }, +} + + +------------------------------------------------- +-- TODO STATIC HELPERS +------------------------------------------------- + +--- [INTERNAL] Returns the aircraft's signed bank/roll angle in radians. +-- @param Wrapper.Unit#UNIT mooseUnit The recon aircraft. +-- @return #number Roll angle in radians. +function TARS.getRoll(mooseUnit) + return mooseUnit:GetRoll() or 0 +end + +--- [INTERNAL] Returns the aircraft's pitch angle in radians. +-- @param Wrapper.Unit#UNIT mooseUnit The recon aircraft. +-- @return #number Pitch angle in radians. +function TARS.getPitch(mooseUnit) + return mooseUnit:GetPitch() or 0 +end + +--- Maps a 0–100 health value to a human-readable damage status label. +-- @param #number life Health percentage (0–100), or nil. +-- @return #string Damage label. +function TARS.life2text(life) + if life == nil then return "Undefined" + elseif life > 90 then return "No damage" + elseif life > 70 then return "Slightly damage" + elseif life > 40 then return "Damaged" + elseif life > 20 then return "Major damage" + elseif life > 0 then return "Destroyed" + else return "Undefined" + end +end + +------------------------------------------------- +-- TODO TARS_SESSION — constructor + methods +------------------------------------------------- + +--- [INTERNAL] Populates all shared fields from a MOOSE UNIT object. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit The recon aircraft's MOOSE UNIT. +function TARS_SESSION:_SetSharedParams(unit) + self:T(self.lid.."_SetSharedParams") + self.unit = unit + self.vec3 = unit:GetVec3() + self.coa = unit:GetCoalition() + self.type = unit:GetTypeName() + self.group = unit:GetGroup() + self.groupID = unit:GetGroup():GetID() + self.objectName = unit:GetName() + self.playerName = unit:GetPlayerName() + self.playerID = unit:GetID() + self.ammo = unit:GetAmmo() + self.time = timer.getTime() +end + +--- [INTERNAL] Refreshes aircraft references WITHOUT resetting sortie state. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit The recon aircraft's MOOSE UNIT. +function TARS_SESSION:SetObjectParamsLight(unit) + self:T2(self.lid.."SetObjectParamsLight") + self:_SetSharedParams(unit) +end + +--- [INTERNAL] Fully initialises (or resets) all sortie state. +-- After a debrief reset, re-adds the validation menu item (GROUND_NEW state). +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit The recon aircraft's MOOSE UNIT. +-- @return #TARS_SESSION self +function TARS_SESSION:SetObjectParams(unit) + self:T(self.lid.."SetObjectParams") + self:_SetSharedParams(unit) + self.category = unit:GetGroup():GetCategory() + self.capturing = false + self.duration = TARS.parameters[self.type].duration + self.targetList = {} + self.captureCount = 0 + self.loop = false + self.standby = false + self.filmExhausted = false + self.sessionEnded = false + self.wasCapturing = false + self.landingScheduled = false + if self.playerName and TARS.groundMenus[self.playerName] and self.Callback then + self.Callback:_MenuAddValidation(self.playerName) + end + return self +end + +--- [INTERNAL] Creates a new TARS_SESSION for the given MOOSE UNIT. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit The recon aircraft's MOOSE UNIT. +-- @param #TARS Callback The TARS singleton (for callbacks and config access). +-- @return #TARS_SESSION self The newly created session. +function TARS_SESSION:New(unit, Callback) + local self = BASE:Inherit(self, BASE:New()) + self.lid = string.format("TARS_SESSION %s | ", TARS.version) + self.Callback = Callback + self:SetObjectParams(unit) + self:I("TARS_SESSION created — unit=" .. tostring(self.objectName) + .. " type=" .. tostring(self.type)) + return self +end + +--- [INTERNAL] Merges a `FindTargets` result into this session's target list. +-- Notifies the player per new detection using the active locale. +-- @param #TARS_SESSION self +-- @param #table list Map of `[unitName] = Wrapper.Unit#UNIT` from `TARS_SESSION:FindTargets()`. +function TARS_SESSION:AddToTargetList(list) + self:T(self.lid.."AddToTargetList") + for k, v in pairs(list) do + if self.targetList[k] == nil then + self.targetList[k] = self:_FreezeUnit(v) + self.captureCount = (self.captureCount or 0) + 1 + local msg + if TARS.filmLimitEnabled then + msg = self.Callback:_Txt("TARS_CAPTURE_HIT_MAX", + self.captureCount, TARS.filmLimitMax) + else + msg = self.Callback:_Txt("TARS_CAPTURE_HIT", self.captureCount) + end + self.Callback:_MsgUnit(msg, 4, self.playerName) + end + end +end + +--- [INTERNAL] Publishes all captured targets as F10 coalition marks. +-- @param #TARS_SESSION self +-- @return #number count Number of marks placed or updated this debrief. +function TARS_SESSION:ReturnReconTargets() + self:T(self.lid.."ReturnReconTargets") + local count = 0 + for k, v in next, self.targetList do + if v.unit and v.unit:IsAlive() then + local existing = self.Callback.detectedTargets[v.name] + if not existing then + count = count + 1 + self.Callback:OutMark(v, self.coa) + self.Callback.detectedTargets[v.name] = v + self:T("New target: " .. v.type .. "/" .. v.name) + elseif existing.life ~= v.life then + local markID = TARS.marks.blue[v.name] or TARS.marks.red[v.name] + if markID then trigger.action.removeMark(markID) end + count = count + 1 + self.Callback:OutMark(v, self.coa) + self.Callback.detectedTargets[v.name] = v + self:T("Updated " .. v.name .. " life " + .. tostring(existing.life) .. "→" .. tostring(v.life)) + end + end + self.targetList[k] = nil + end + return count +end + +--- [INTERNAL] Starts the 10-second capture loop. +-- @param #TARS_SESSION self +function TARS_SESSION:CaptureData() + self:T(self.lid.."CaptureData") + if self.duration <= 0 then + self.Callback:_MsgUnit( + self.Callback:_Txt("TARS_FILM_EXHAUSTED"), 2, self.playerName) + return + end + self.capturing = true + self.loop = true + self.standby = false + self:I("FILM START — film=" .. self.duration .. "s") + self.Callback:_MsgUnit( + self.Callback:_Txt("TARS_FILM_START", self.duration), 5, self.playerName) + timer.scheduleFunction(TARS_SESSION.CaptureLoop, self, timer.getTime() + 2) +end + +--- [INTERNAL] Removes this session from the global registry. +-- @param #TARS_SESSION self +function TARS_SESSION:Delete() + self:T(self.lid.."Delete") + TARS.instances[self.objectName] = nil +end + +--- [INTERNAL] Returns a unit's current health as a 0–100 percentage. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit +-- @return #number Health 0–100, or nil. +function TARS_SESSION:_NormalizeLife(unit) + if not unit or not unit:IsAlive() then return nil end + local rlife = unit:GetLifeRelative() * 100 + if rlife == -1 then return nil end + return rlife +end + +--- [INTERNAL] Freezes a MOOSE object into a `TARS.Snapshot`. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT _Object MOOSE UNIT or STATIC. +-- @return #TARS.Snapshot snap +function TARS_SESSION:_FreezeUnit(_Object) + self:T(self.lid.."_FreezeUnit") + local snap = {} + snap.unit = _Object + snap.dcsObj = _Object:GetDCSObject() + snap.category = _Object:GetCategory() + snap.type = _Object:GetTypeName() + snap.name = _Object:GetName() + snap.point = _Object:GetVec3() + snap.time = timer.getTime() + if snap.category == Object.Category.UNIT and _Object then + snap.groupID = _Object:GetGroup():GetID() + snap.groupCat = _Object:GetGroup():GetCategory() + snap.coa = _Object:GetCoalition() + snap.ammo = _Object:GetAmmo() + snap.life = self:_NormalizeLife(_Object) + end + snap.playername = self.playerName + return snap +end + +--- [INTERNAL] Calculates the 2-D ground point ahead of the aircraft. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT unit +-- @param #TARS.PlatformParams params +-- @return DCS#Vec2 `{ x, z }` ahead of the aircraft. +function TARS_SESSION:_OffsetCalc(unit, params) + local pos = unit:GetPositionVec3() + local vec3 = unit:GetVec3() + local rad = math.atan2(pos.z, pos.x) + 2 * math.pi -- pos.x = Vorwärts-Vektor + local MSL = land.getHeight({ x = vec3.x, y = vec3.z }) + local alt = vec3.y - MSL + local dist = math.tan(params.offset) * alt + return { x = vec3.x + math.cos(rad) * dist, z = vec3.z + math.sin(rad) * dist } +end + +--- [INTERNAL] Validates a single DCS/MOOSE object against all active filters. +-- Returns true if the object should be added to the target list. +-- @param #TARS_SESSION self +-- @param Wrapper.Unit#UNIT _Object MOOSE UNIT or STATIC. +-- @return #boolean +function TARS_SESSION:_ValidateObjectFound(_Object) + self:I(self.lid.."_ValidateObjectFound " .. tostring(_Object:GetName())) + + if not (_Object and _Object:IsAlive()) then return false end + if _Object:GetCoalition() == self.coa then return false end + + if self.Callback.targetNameFilter.enabled then + local keywords = self.Callback.targetNameFilter.keywords[_Object:GetCoalition()] + local targetName = string.lower(_Object:GetName() or "") + if type(keywords) == "string" then keywords = { keywords } end + local matched = false + for _, kw in pairs(keywords or {}) do + if string.find(targetName, string.lower(kw)) then matched = true; break end + end + if not matched then return false end + end + + local typeName = _Object:GetTypeName() + local typeNameLower = string.lower(typeName) + local objCat = _Object:GetCategory() + + self:I(self.lid.."_ValidateObjectFound Name Filter Passed!") + + if objCat == Object.Category.UNIT then + local desc = _Object:GetDesc() + local unitCat = desc and desc.category + self:I(self.lid.."_ValidateObjectFound Name Category Check "..tostring(unitCat)) + if unitCat == Unit.Category.AIRPLANE or unitCat == Unit.Category.HELICOPTER then + return self.Callback.units.air + elseif unitCat == Unit.Category.GROUND_UNIT then + return self.Callback.units.ground + elseif unitCat == Unit.Category.SHIP then + return self.Callback.units.ship + end + elseif objCat == Object.Category.STATIC or objCat == Object.Category.BASE then + if self.Callback.statics.farps and string.find(typeNameLower, "farp") then + return true + end + if self.Callback.statics.captureUnique then + return self.Callback.statics.captureUniqueList[typeName] == true + elseif self.Callback.statics.captureExceptions then + for _, exName in pairs(self.Callback.statics.captureExceptionsList) do + if string.find(typeNameLower, string.lower(exName)) then return true end + end + end + end + return false +end + +--- [INTERNAL] Main capture tick. Scheduled via `timer.scheduleFunction`. +-- @param #TARS_SESSION self The active session (timer data argument). +-- @return nil +function TARS_SESSION:CaptureLoop() + if not self or not self.loop then return end + + if self.capturing and self.standby then + timer.scheduleFunction(TARS_SESSION.CaptureLoop, self, timer.getTime() + 10) + return + end + + if self.capturing and self.duration > 0 then + self.duration = self.duration - 10 + self.Callback:_MsgUnit( + self.Callback:_Txt("TARS_CAPTURE_TICK", math.max(0, self.duration)), + 9, self.playerName) + self:AddToTargetList(self:FindTargets()) + + if self.Callback.filmLimitEnabled + and self.captureCount >= self.Callback.filmLimitMax then + self.Callback:_MsgUnit( + self.Callback:_Txt("TARS_FILM_CAP_REACHED", self.Callback.filmLimitMax), + 8, self.playerName) + self.Callback:StopCapture(self) + return + end + timer.scheduleFunction(TARS_SESSION.CaptureLoop, self, timer.getTime() + 10) + end + + if self.duration <= 0 and self.loop then + self.loop = false + self.Callback:_MsgUnit( + self.Callback:_Txt("TARS_FILM_TIME_UP"), 8, self.playerName) + self.Callback:StopCapture(self) + end +end + +--- [INTERNAL] Two-segment linear interpolation for visual-recon helicopter range. +-- @param #TARS_SESSION self +-- @param #TARS.PlatformParams params +-- @param #number altitude AGL in metres. +-- @return #number radius Detection sphere radius in metres. +function TARS_SESSION:_CalcVisualRange(params, altitude) + if altitude <= params.minAlt then + return params.minRange + elseif altitude <= params.optimalAlt then + local t = (altitude - params.minAlt) / (params.optimalAlt - params.minAlt) + return params.minRange + t * (params.optimalRange - params.minRange) + elseif altitude <= params.maxAlt then + local t = (altitude - params.optimalAlt) / (params.maxAlt - params.optimalAlt) + return params.optimalRange + t * (params.maxRange - params.optimalRange) + else + return params.maxRange + end +end + +--- [INTERNAL] earches for targets in a sphere ahead of the aircraft. +-- @param #TARS_SESSION self +-- @return #table `[unitName] = Wrapper.Unit#UNIT` +function TARS_SESSION:FindTargets() + local unit = self.unit + local vec3 = unit:GetVec3() + local MSL = land.getHeight({ x = vec3.x, y = vec3.z }) + local alt = vec3.y - MSL + local params = self.Callback.parameters[self.type] + + local roll = math.abs(math.deg(TARS.getRoll(unit))) + local pitch = math.abs(math.deg(TARS.getPitch(unit))) + local isFlat = roll < params.maxRoll and pitch < params.maxPitch + + local radius = params.optimalAlt + and self:_CalcVisualRange(params, alt) + or alt * math.tan(math.rad(params.fov / 2)) + + local offset = self:_OffsetCalc(unit, params) + local coordinate = self.coordinate or COORDINATE:New(offset.x, MSL, offset.z) + coordinate = coordinate:UpdateFromVec3({ x=offset.x, y=MSL, z=offset.z }) + self.coordinate = coordinate + + local debugunitset + if self.debug == true then + self:I(self.lid.."FindTargets Debug SET_UNIT created") + debugunitset = SET_UNIT:New():FilterCategories("ground"):FilterCoalitions("red"):FilterOnce() + end + + local ScannedUnits = self.Callback.detectUnits and coordinate:ScanUnits(radius) or nil + local ScannedStatics = self.Callback.detectStatics and coordinate:ScanStatics(radius) or nil + + local targetList = {} + if alt > params.minAlt and alt < params.maxAlt and isFlat then + for _, u in pairs(ScannedUnits and ScannedUnits.Set or {}) do + if self:_ValidateObjectFound(u) then targetList[u:GetName()] = u end + end + for _, s in pairs(ScannedStatics and ScannedStatics.Set or {}) do + if self:_ValidateObjectFound(s) then targetList[s:GetName()] = s end + end + end + -- TODO Debug + if debugunitset then + for _, u in pairs(debugunitset and debugunitset.Set or {}) do + if self:_ValidateObjectFound(u) then targetList[u:GetName()] = u end + end + end + return targetList +end + +------------------------------------------------- +-- TODO TARS — private helpers +------------------------------------------------- + +--- [INTERNAL] Returns the active TARS_SESSION for a unit name, or nil. +-- @param #TARS self +-- @param #string unitName +-- @return #TARS_SESSION or nil +function TARS:GetInstance(unitName) + local inst = self.instances[unitName] + if inst and inst.unit and inst.unit:IsAlive() then return inst end + self.instances[unitName] = nil + return nil +end + +--- [INTERNAL] Resolves a localized string by message ID. +-- Lookup order: `TARS.Messages[locale][id]` → `TARS.Messages["en"][id]` → raw id. +-- Passes extra arguments through `string.format` when provided. +-- @param #TARS self +-- @param #string id Message ID (key in `TARS.Messages[locale]`). +-- @param ... Optional `string.format` arguments. +-- @return #string Resolved, formatted string. +function TARS:_Txt(id, ...) + local locale = self.locale or "en" + local text = (TARS.Messages[locale] and TARS.Messages[locale][id]) + or (TARS.Messages["en"] and TARS.Messages["en"][id]) + if not text then + BASE:E("TARS:_T — unknown locale key '" .. tostring(id) .. "'") + return tostring(id) + end + if select("#", ...) > 0 then + local ok, result = pcall(string.format, text, ...) + return ok and result or text + end + return text +end + +--- [INTERNAL] Sends a localized MESSAGE to a single unit by player name. +-- @param #TARS self +-- @param #string text Resolved message text. +-- @param #number seconds Display duration in seconds. +-- @param #string playerName Player display name. +-- @param #boolean Silent Do not send via SRS if this is true. +function TARS:_MsgUnit(text, seconds, playerName, Silent) + local unit = CLIENT:FindByPlayerName(playerName) + if unit then + MESSAGE:New(text, seconds, "TARS"):ToUnit(unit) + end + if unit and self.SRS and (not Silent) then + local srsText = string.gsub(text, "^%[TARS%] ?", playerName .. ", ") + srsText = string.gsub(srsText, "[<>]", "") + MESSAGE:New(srsText, seconds, "TARS"):ToSRS() + end +end + +--- [INTERNAL] Sends a localized MESSAGE to an entire coalition. +-- @param #TARS self +-- @param #string text Resolved message text. +-- @param #number seconds Display duration in seconds. +-- @param #number coa Coalition side (1 = Red, 2 = Blue). +function TARS:_MsgCoalition(text, seconds, coa) + MESSAGE:New(text, seconds, "TARS"):ToCoalition(coa) +end + +--- [INTERNAL] Awards credits via DCSBot (if loaded). +-- @param #TARS self +-- @param #string name Player display name. +-- @param #number points Credits to award. +-- @return #boolean `true` if credited. +function TARS:_AddUserPoints(name, points) + if dcsbot and dcsbot.addUserPoints then + dcsbot.addUserPoints(name, points) + self:T(self.lid .. "AddUserPoints +" .. tostring(points) .. " for " .. tostring(name)) + return true + end + return false +end + +--- [INTERNAL] Resolves the MOOSE UNIT for a player from the groundMenus registry. +-- @param #TARS self +-- @param #string playerName +-- @return Wrapper.Unit#UNIT unit or nil. +function TARS:_GetUnitFromPlayerName(playerName) + local data = TARS.groundMenus[playerName] + if not data or not data.unitName then return nil end + return UNIT:FindByName(data.unitName) +end + + +------------------------------------------------- +-- F10 MENU CALLBACKS +------------------------------------------------- + +--- [INTERNAL] F10 callback: ground validation. +-- @param #TARS self +-- @param #string playerName +function TARS:_CbValidate(playerName) + local u = self:_GetUnitFromPlayerName(playerName) + if u then self:CheckTask(u) end +end + +--- [INTERNAL] F10 callback: show platform info. +-- @param #TARS self +-- @param #string playerName +function TARS:_CbInfo(playerName) + local u = self:_GetUnitFromPlayerName(playerName) + if not u then return end + local inst = self:GetInstance(u:GetName()) + if inst then self:ShowPlatformInfo(inst) + else self:_MsgUnit(self:_Txt("TARS_NO_SESSION"), 4, playerName) end +end + +--- [INTERNAL] F10 callback: start capture. +-- @param #TARS self +-- @param #string playerName +function TARS:_CbStart(playerName) + local u = self:_GetUnitFromPlayerName(playerName) + if not u then return end + local inst = self:GetInstance(u:GetName()) + if inst then self:Control(inst) + else self:_MsgUnit(self:_Txt("TARS_VALIDATE_FIRST"), 5, playerName) end +end + +--- [INTERNAL] F10 callback: toggle STB. +-- @param #TARS self +-- @param #string playerName +function TARS:_CbStb(playerName) + local u = self:_GetUnitFromPlayerName(playerName) + if not u then return end + local inst = self:GetInstance(u:GetName()) + if inst then self:StandbyCapture(inst) + else self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE"), 4, playerName) end +end + +--- [INTERNAL] F10 callback: stop capture. +-- @param #TARS self +-- @param #string playerName +function TARS:_CbStop(playerName) + local u = self:_GetUnitFromPlayerName(playerName) + if not u then return end + local inst = self:GetInstance(u:GetName()) + if inst then self:StopCapture(inst) + else self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE"), 4, playerName) end +end + +------------------------------------------------- +-- TODO PUBLIC METHODS +------------------------------------------------- + +--- [INTERNAL] Creates and registers a new TARS_SESSION. +-- @param #TARS self +-- @param Wrapper.Unit#UNIT unit +-- @return #TARS_SESSION +function TARS:CreateInstance(unit) + self:T(self.lid.."CreateInstance") + local inst = TARS_SESSION:New(unit, self) + self.instances[inst.objectName] = inst + return inst +end + +--- [INTERNAL] Validates the aircraft loadout. +-- @param #TARS self +-- @param Wrapper.Unit#UNIT unit +-- @return #boolean reconOk +-- @return #string refusedWeapon or nil +function TARS:CheckIfRecon(unit) + if not unit then return false end + local typeName = unit:GetTypeName() + if not TARS.reconTypes[typeName] then return false end + + if TARS.recoNameFilter.enabled then + local grp = unit:GetGroup() + local name = (grp and grp:GetName()) or unit:GetName() or "" + if not string.find(string.lower(name), string.lower(TARS.recoNameFilter.keyword)) then + return false + end + end + + if not TARS.parameters[typeName] then return false end + + local ammo = unit:GetAmmo() + if type(ammo) ~= "table" then return true end + + for _, w in ipairs(ammo) do + if w and w.desc then + local name = w.desc.displayName or w.desc.typeName or w.desc.name + if name and not TARS.allowedAmmo[name] then + return false, name + end + end + end + return true +end + +--- [INTERNAL] Validates loadout on the ground and sets the approved flag. +-- @param #TARS self +-- @param Wrapper.Unit#UNIT unit +function TARS:CheckTask(unit) + if not unit or not unit:IsAlive() then return end + local typeName = unit:GetTypeName() + local params = self.parameters[typeName] + + if unit:InAir(false) then + local inst = self:GetInstance(unit:GetName()) + local msg = (inst and inst.capturing) + and self:_Txt("TARS_VALID_RUNNING") + or self:_Txt("TARS_VALID_AIRBORNE") + self:_MsgUnit(msg, 5, unit:GetPlayerName() or unit:GetName()) + return + end + + if self.recoNameFilter.enabled then + local grp = unit:GetGroup() + local groupName = grp and grp:GetName() or "" + if not string.find(string.lower(groupName), string.lower(self.recoNameFilter.keyword)) then + self:_MsgUnit( + self:_Txt("TARS_VALID_GROUP_FILTER", self.recoNameFilter.keyword), + 10, unit:GetPlayerName() or unit:GetName(),true) + return + end + end + + local playerName = unit:GetPlayerName() or unit:GetName() + local reconOk, refusedWeapon = self:CheckIfRecon(unit) + + TARS.groundMenus[playerName] = TARS.groundMenus[playerName] or {} + TARS.groundMenus[playerName].approved = reconOk + TARS.groundMenus[playerName].playerName = playerName + + if reconOk then + self:I("VALIDATE OK — " .. unit:GetName() .. " / " .. tostring(playerName)) + self:_MenuRemoveValidation(playerName) + -- Build the platform info block using localized labels + local msg = self:_Txt("TARS_VALID_OK_HDR") + self:_MsgUnit(msg, 15, playerName) + local msg = "" + .. self:_Txt("TARS_PLATFORM_LABEL") .. " : " .. params.name .. "\n" + .. self:_Txt("TARS_PLATFORM_ALT") .. " : " .. params.minAlt .. "m - " .. params.maxAlt .. "m AGL\n" + .. self:_Txt("TARS_PLATFORM_FOV") .. " : " .. tostring(params.fov or "-") .. "\xc2\xb0\n" + .. self:_Txt("TARS_PLATFORM_FILM") .. " : " .. params.duration .. " expositions" + self:_MsgUnit(msg, 15, playerName,true) + else + self:I("VALIDATE REFUSED — " .. unit:GetName() .. " ammo=" .. tostring(refusedWeapon)) + local msg = self:_Txt("TARS_VALID_REFUSED_WPN") + if refusedWeapon then + msg = msg .. "\n" .. self:_Txt("TARS_VALID_REFUSED_AMMO", refusedWeapon) + end + self:_MsgUnit(msg, 10, playerName,true) + end +end + +--- [INTERNAL] Arms the capture session. +-- @param #TARS self +-- @param #TARS_SESSION instance +function TARS:Control(instance) + if not instance then return end + if instance.sessionEnded then + self:_MsgUnit(self:_Txt("TARS_SESSION_ENDED"), 5, instance.playerName) + return + end + if instance.capturing then + self:_MsgUnit(self:_Txt("TARS_FILM_ALREADY_ACTIVE"), 4, instance.playerName) + return + end + instance:CaptureData() +end + +--- [INTERNAL] Ends the capture session; marks it as awaiting debrief. +-- @param #TARS self +-- @param #TARS_SESSION instance +function TARS:StopCapture(instance) + if not instance then return end + if not instance.capturing then + self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE_STOP"), 4, instance.playerName) + return + end + instance.capturing = false + instance.standby = false + instance.loop = false + instance.sessionEnded = true + instance.filmExhausted = (instance.duration <= 0) + instance:I("FILM STOP — captures=" .. instance.captureCount + .. " filmLeft=" .. instance.duration .. "s") + self:_MsgUnit( + instance.filmExhausted + and self:_Txt("TARS_FILM_EXHAUSTED") + or self:_Txt("TARS_FILM_STOP"), + 8, instance.playerName) +end + +--- [INTERNAL] Toggles film standby on/off. +-- @param #TARS self +-- @param #TARS_SESSION instance +function TARS:StandbyCapture(instance) + if not instance then return end + if not instance.capturing then + self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE"), 4, instance.playerName) + return + end + if instance.standby and instance.wasCapturing then + self:_MsgUnit(self:_Txt("TARS_FILM_STB_LOCKED"), 4, instance.playerName) + return + end + instance.standby = not instance.standby + instance:I("FILM " .. (instance.standby and "STB" or "RESUME")) + self:_MsgUnit( + instance.standby + and self:_Txt("TARS_FILM_STB_MANUAL") + or self:_Txt("TARS_FILM_RESUME_MANUAL"), + 5, instance.playerName) +end + +--- [INTERNAL] Sends platform capabilities as a HUD message. +-- All labels are resolved through the active locale. +-- @param #TARS self +-- @param #TARS_SESSION instance +function TARS:ShowPlatformInfo(instance) + if not instance or not instance.unit or not instance.unit:IsAlive() then return end + local params = TARS.parameters[instance.type] + if not params then return end + local msg = self:_Txt("TARS_PLATFORM_INFO") .. "\n" + .. self:_Txt("TARS_PLATFORM_LABEL") .. " : " .. params.name .. "\n" + .. self:_Txt("TARS_PLATFORM_ALT") .. " : " .. params.minAlt .. "m - " .. params.maxAlt .. "m AGL\n" + .. self:_Txt("TARS_PLATFORM_FOV") .. " : " .. tostring(params.fov or "-") .. "\xc2\xb0\n" + .. self:_Txt("TARS_PLATFORM_FILM") .. " : " .. instance.duration .. " / " .. params.duration .. " expositions" + self:_MsgUnit(msg, 15, instance.playerName, true) +end + +--- [INTERNAL] Places a coalition F10 map marker for a detected target snapshot. +-- @param #TARS self +-- @param #TARS.Snapshot snap +-- @param #number coa Coalition side. +-- @return #number counter Mark ID used. +function TARS:OutMark(snap, coa) + if not snap then return end + local c = COORDINATE:NewFromVec3(snap.point) + local lat, lon = c:GetLLDDM() + local hPa = UTILS.Round(c:GetPressure(), 2) + local inHg = UTILS.Round(hPa * 0.02953, 2) + local text = string.format( + "%.4f, %.4f | %.2f hPa / %.2f inHg\nTYPE: %s STATUS: %s", + lat, lon, hPa, inHg, snap.type, TARS.life2text(snap.life)) + + local markTable = (coa == 1) and self.marks.red or self.marks.blue + local counter = (coa == 1) and self.redMarkCount or self.blueMarkCount + + trigger.action.markToCoalition(counter, text, snap.point, coa, true) + markTable[snap.name] = counter + + if coa == 1 then self.redMarkCount = self.redMarkCount + 1 + else self.blueMarkCount = self.blueMarkCount + 1 end + + local out = true + if self.OnBeforeDataProcessing then out = self:OnBeforeDataProcessing(snap) end + if out == true and self.OnAfterDataProcessing then self:OnAfterDataProcessing(snap) end + + return counter +end + +--- [INTERNAL] Debrief: publishes marks and awards points after a valid landing. +-- @param #TARS self +-- @param #TARS_SESSION instance +function TARS:ProcessLanding(instance) + if not instance or not instance.unit or not instance.unit:IsAlive() then return end + local unit = instance.unit + if unit:InAir(false) or not instance.sessionEnded then return end + + instance.wasCapturing = false + instance.landingScheduled = false + + if not self:IsNearAlliedBase(unit) then + self:_MsgUnit(self:_Txt("TARS_NOT_AT_BASE"), 10, instance.playerName) + return + end + + local count = instance:ReturnReconTargets() + instance:I("DEBRIEF — targets=" .. count .. " player=" .. tostring(instance.playerName)) + + if TARS.mooseScoring and count > 0 and TARS.scoring then + local pts = count * TARS.valueScoring + local mooseUnit = UNIT:FindByName(instance.objectName) + if mooseUnit and mooseUnit:IsAlive() then + TARS.scoring:_AddPlayerFromUnit(mooseUnit) + TARS.scoring:AddGoalScore(mooseUnit, + string.format("RECCE_%s_T%d", instance.objectName, math.floor(timer.getTime())), + string.format("[TARS] %d target(s) captured +%d pts", count, pts), pts) + self:_MsgUnit(self:_Txt("TARS_DEBRIEF_TARGETS", count, pts), 8, instance.playerName, true) + end + else + local pts = math.ceil(count / 4) + self:_AddUserPoints(instance.playerName, pts) + self:_MsgUnit(self:_Txt("TARS_DEBRIEF_CREDITS", pts), 8, instance.playerName) + end + + self:_MsgCoalition( + self:_Txt("TARS_DEBRIEF_COALITION", unit:GetPlayerName(), count), + 8, instance.coa) + + instance:I("SESSION RESET") + instance:SetObjectParams(unit) +end + +--- [INTERNAL] Removes F10 marks for units that no longer exist. +-- @param #TARS self +-- @param #boolean _ +-- @param #number time +-- @return #number time + 120 +function TARS:RemoveUnusedMarks(_, time) + local function sweep(markTable) + for unitName, markID in next, markTable do + local u = UNIT:FindByName(unitName) + if not u or not u:IsAlive() then + trigger.action.removeMark(markID) + markTable[unitName] = nil + self.detectedTargets[unitName] = nil + end + end + end + sweep(self.marks.blue) + sweep(self.marks.red) + return time + 120 +end + +--- [INTERNAL] Returns true if the unit is within landingDistance of any allied base/FARP. +-- @param #TARS self +-- @param Wrapper.Unit#UNIT unit +-- @return #boolean +function TARS:IsNearAlliedBase(unit) + if self.debug then return true end + local pos = unit:GetCoordinate() + local _, distance = pos:GetClosestAirbase(nil, unit:GetCoalition()) + return distance < TARS.landingDistance +end + +------------------------------------------------- +-- TODO DYNAMIC MENU HELPERS +-- +-- State machine: +-- GROUND_NEW [validate + info] +-- │ CheckTask() OK → _MenuRemoveValidation +-- ▼ +-- GROUND_APPROVED [info only] +-- │ _OnEventTakeOff → _MenuAddFilmControls +-- ▼ +-- AIRBORNE [info + start + stb + stop] +-- │ _OnEventLand (sessionEnded) → _MenuRemoveFilmControls +-- ▼ +-- LANDED_DEBRIEF [info only] +-- │ SetObjectParams after debrief → _MenuAddValidation +-- ▼ +-- GROUND_NEW (reset) [validate + info] +------------------------------------------------- + +--- [INTERNAL] Adds the "TARS validation" menu item using the active locale label. +-- @param #TARS self +-- @param #string playerName +function TARS:_MenuAddValidation(playerName) + local d = TARS.groundMenus[playerName] + if not d or not d.menuHandle or d.itemValidate then return end + local grp = d.group + local label = self:_Txt("TARS_MENU_VALIDATE") + d.itemValidate = MENU_GROUP_COMMAND:New(grp, label, d.menuHandle, + TARS._CbValidate, self, playerName) + d.itemValidate.MenuTag = 1 + d.menuHandle:RefreshAndOrderByTag() + self:T(self.lid .. "MENU +validate — " .. tostring(playerName)) +end + +--- [INTERNAL] Removes the "TARS validation" menu item. +-- @param #TARS self +-- @param #string playerName +function TARS:_MenuRemoveValidation(playerName) + local d = TARS.groundMenus[playerName] + if not d or not d.itemValidate then return end + d.itemValidate:Remove() + d.itemValidate = nil + d.menuHandle:RefreshAndOrderByTag() + self:T(self.lid .. "MENU -validate — " .. tostring(playerName)) +end + +--- [INTERNAL] Adds the three film-control items using the active locale labels. +-- @param #TARS self +-- @param #string playerName +function TARS:_MenuAddFilmControls(playerName) + local d = TARS.groundMenus[playerName] + if not d or not d.menuHandle or d.itemStart then return end + local grp = d.group + d.itemStart = MENU_GROUP_COMMAND:New(grp, self:_Txt("TARS_MENU_START"), + d.menuHandle, TARS._CbStart, self, playerName) + d.itemStart.MenuTag = 2 + d.itemStb = MENU_GROUP_COMMAND:New(grp, self:_Txt("TARS_MENU_STB"), + d.menuHandle, TARS._CbStb, self, playerName) + d.itemStb.MenuTag = 3 + d.itemStop = MENU_GROUP_COMMAND:New(grp, self:_Txt("TARS_MENU_STOP"), + d.menuHandle, TARS._CbStop, self, playerName) + d.itemStop.MenuTag = 4 + d.menuHandle:RefreshAndOrderByTag() + self:T(self.lid .. "MENU +film controls — " .. tostring(playerName)) +end + +--- [INTERNAL] Removes the three film-control items. +-- @param #TARS self +-- @param #string playerName +function TARS:_MenuRemoveFilmControls(playerName) + local d = TARS.groundMenus[playerName] + if not d then return end + if d.itemStart then d.itemStart:Remove(); d.itemStart = nil end + if d.itemStb then d.itemStb:Remove(); d.itemStb = nil end + if d.itemStop then d.itemStop:Remove(); d.itemStop = nil end + if d.menuHandle then d.menuHandle:RefreshAndOrderByTag() end + self:T(self.lid .. "MENU -film controls — " .. tostring(playerName)) +end + +--- [INTERNAL] Creates the Task TARS F10 sub-menu and its initial items. +-- Initial state (GROUND_NEW): "TARS validation" + "TARS capture config". +-- @param #TARS self +-- @param Wrapper.Unit#UNIT unit +-- @param #string playerName +function TARS:AddBaseMenu(unit, playerName) + self:I(self.lid .. "AddBaseMenu — " .. unit:GetName() + .. " / " .. tostring(playerName)) + + local typeName = unit:GetTypeName() + if not TARS.reconTypes[typeName] then return end + + local grp = unit:GetGroup() + if not grp then return end + + if TARS.recoNameFilter.enabled then + local groupName = grp:GetName() or "" + if not string.find(string.lower(groupName), + string.lower(TARS.recoNameFilter.keyword)) then + return + end + end + + local groupID = grp:GetID() + local unitName = unit:GetName() + local existing = TARS.groundMenus[playerName] + + if existing and existing.menuHandle then + if existing.groupID ~= groupID then + self:T(self.lid .. "AddBaseMenu — group changed, rebuilding") + existing.menuHandle:Remove() + TARS.groundMenus[playerName] = nil + else + if existing.unitName ~= unitName then + existing.unitName = unitName + existing.approved = false + self:_MenuAddValidation(playerName) + self:_MenuRemoveFilmControls(playerName) + end + return + end + end + + local displayName = unit:GetPlayerName() or tostring(playerName) + -- Menu root label uses locale + local subMenu = MENU_GROUP:New(grp, self:_Txt("TARS_MENU_ROOT") .. " - " .. displayName) + subMenu.MenuTag = -1 + local itemInfo = MENU_GROUP_COMMAND:New(grp, self:_Txt("TARS_MENU_INFO"), + subMenu, TARS._CbInfo, self, playerName) + itemInfo.MenuTag = 0 + + TARS.groundMenus[playerName] = { + menuHandle = subMenu, + itemValidate = nil, + itemInfo = itemInfo, + itemStart = nil, + itemStb = nil, + itemStop = nil, + approved = false, + unitName = unitName, + groupID = groupID, + playerName = displayName, + group = grp, + } + + self:_MenuAddValidation(playerName) + + self:I(self.lid .. "MENU created — " .. tostring(playerName) + .. " group=" .. grp:GetName()) +end + +--- [INTERNAL] Removes the TARS F10 menu for a player. +-- @param #TARS self +-- @param #string playerName +function TARS:RemoveGroundMenu(playerName) + local data = TARS.groundMenus[playerName] + if not data or not data.menuHandle then return end + data.menuHandle:Remove() + self:T(self.lid .. "MENU removed — " .. tostring(playerName)) + TARS.groundMenus[playerName] = nil +end + +------------------------------------------------- +-- TODO EVENT HANDLERS +------------------------------------------------- + +--- [INTERNAL] Handles unit birth. +-- @param #TARS self +-- @param Core.Event#EVENTDATA EventData +function TARS:_OnEventBirth(EventData) + self:T(self.lid .. "OnEventBirth") + local unit = EventData.IniUnit + if not unit then return end + local instance = self:GetInstance(unit:GetName()) + if instance then instance:Delete() end + local playerName = unit:GetPlayerName() + if not playerName then return end + local pName = playerName + timer.scheduleFunction(function() + if unit:IsAlive() then + pcall(function() self:AddBaseMenu(unit, pName) end) + end + end, nil, timer.getTime() + 1) +end + +--- [INTERNAL] Handles engine startup (fallback for pre-loaded slots). +-- @param #TARS self +-- @param Core.Event#EVENTDATA EventData +function TARS:_OnEventEngineStartup(EventData) + local unit = EventData.IniUnit + if not unit or not unit:GetPlayerName() then return end + local pName = unit:GetPlayerName() + timer.scheduleFunction(function() + if unit:IsAlive() then + pcall(function() self:AddBaseMenu(unit, pName) end) + end + end, nil, timer.getTime() + 1) +end + +--- [INTERNAL] Handles unit death. +-- @param #TARS self +-- @param Core.Event#EVENTDATA EventData +function TARS:_OnEventDead(EventData) + local unit = EventData.IniUnit + if not unit then return end + local name = unit:GetName() + local playerName = unit:GetPlayerName() or unit:GetName() + if TARS.groundMenus[playerName] then self:RemoveGroundMenu(playerName) end + if self.detectedTargets[name] then + local markID = self.marks.blue[name] or self.marks.red[name] + if markID then trigger.action.removeMark(markID) end + self.marks.blue[name] = nil + self.marks.red[name] = nil + self.detectedTargets[name] = nil + end +end + +--- [INTERNAL] Handles player leaving a slot. +-- @param #TARS self +-- @param Core.Event#EVENTDATA EventData +function TARS:_OnEventPlayerLeaveUnit(EventData) + local unit = EventData.IniUnit + if not unit then return end + local playerName = unit:GetPlayerName() or unit:GetName() + if TARS.groundMenus[playerName] then self:RemoveGroundMenu(playerName) end +end + +--- [INTERNAL] Handles takeoff events (TakeOff + RunwayTakeOff share this handler). +-- Branch 1: capture active → validate config, auto-resume film. +-- Branch 2: film inactive → check approval, create session, add film menus. +-- @param #TARS self +-- @param Core.Event#EVENTDATA EventData +function TARS:_OnEventTakeOff(EventData) + self:T(self.lid.."_OnEventTakeOff") + local unit = EventData.IniUnit + if not unit then return end + local instance = self:GetInstance(unit:GetName()) + local now = timer.getTime() + + -- Branch 1: auto-resume after ground STB + if instance and instance.capturing then + if instance.lastTakeoffTime and (now - instance.lastTakeoffTime) < 5 then return end + if not instance.wasCapturing then return end + instance.lastTakeoffTime = now + + local reconOk, refused = self:CheckIfRecon(unit) + if not reconOk then + self:StopCapture(instance) + instance.wasCapturing = false + local msg = self:_Txt("TARS_CONFIG_CHANGED") + if refused then + msg = msg .. "\n" .. self:_Txt("TARS_VALID_REFUSED_AMMO", refused) + end + self:_MsgUnit(msg, 10, instance.playerName) + return + end + instance:SetObjectParamsLight(unit) + instance.wasCapturing = false + instance.standby = false + instance:I("FILM AUTO-RESUME — filmLeft=" .. instance.duration .. "s") + local inst = instance + timer.scheduleFunction(function() + if inst.unit:IsAlive() then + self:_MsgUnit( + self:_Txt("TARS_FILM_RESUME_TO", inst.duration), 5, inst.playerName) + end + end, nil, now + 2) + return + end + + -- Branch 2: normal takeoff + if instance and instance.lastTakeoffTime and (now - instance.lastTakeoffTime) < 5 then + return + end + local playerName = unit:GetPlayerName() or unit:GetName() + local groundData = TARS.groundMenus[playerName] + if not (groundData and groundData.approved) then return end + + local reconOk, refused = self:CheckIfRecon(unit) + if not reconOk then + local msg = self:_Txt("TARS_LOADOUT_BAD") + if refused then + msg = msg .. "\n" .. self:_Txt("TARS_VALID_REFUSED_AMMO", refused) + end + if TARS.groundMenus[playerName] then + TARS.groundMenus[playerName].approved = false + end + self:_MsgUnit(msg, 10, playerName) + return + end + + if not instance then + instance = self:CreateInstance(unit) + else + instance:SetObjectParams(unit) + end + instance.lastTakeoffTime = now + + -- AIRBORNE: add film control items + self:_MenuAddFilmControls(playerName) + + local inst = instance + timer.scheduleFunction(function() + if inst.unit:IsAlive() then + self:_MsgUnit(self:_Txt("TARS_READY"), 8, inst.playerName) + end + end, nil, now + 5) +end + +--- [INTERNAL] Handles landing events (Land + RunwayTouch share this handler). +-- Branch 1: capture active → auto standby. +-- Branch 2: session ended → schedule debrief, remove film menus. +-- @param #TARS self +-- @param Core.Event#EVENTDATA EventData +function TARS:_OnEventLand(EventData) + self:T(self.lid.."_OnEventLand") + local unit = EventData.IniUnit + if not unit then return end + local instance = self:GetInstance(unit:GetName()) + + -- Branch 1: auto standby + if instance and instance.capturing then + if instance.wasCapturing then return end + instance.standby = true + instance.wasCapturing = true + instance:I("FILM AUTO-STB — landing") + self:_MsgUnit(self:_Txt("TARS_FILM_STB_LAND"), 5, instance.playerName) + return + end + + -- Branch 2: session ended → schedule debrief + if not (instance and instance.sessionEnded) then return end + if instance.landingScheduled then return end + if not TARS.reconTypes[unit:GetTypeName()] then return end + + if not self:IsNearAlliedBase(unit) then + self:_MsgUnit(self:_Txt("TARS_NOT_AT_BASE"), 10, instance.playerName) + return + end + + instance.landingScheduled = true + local landTime = timer.getTime() + local inst = instance + local msgTime = TARS.debriefDelay*0.98 + + -- LANDED_DEBRIEF: remove film controls + self:_MenuRemoveFilmControls(instance.playerName) + + timer.scheduleFunction(function() + if inst.unit:IsAlive() and not inst.unit:InAir(false) then + self:_MsgUnit(self:_Txt("TARS_LAND_VALIDATED"),10,instance.playerName) + self:_MsgUnit( + self:_Txt("TARS_LAND_VALIDATED_TIME", TARS.debriefDelay), + msgTime, inst.playerName) + inst:I("Landing validated — debrief in " .. TARS.debriefDelay .. "s") + end + end, nil, landTime + TARS.landingDelay) + + timer.scheduleFunction(function() + self:ProcessLanding(inst) + end, nil, landTime + TARS.landingDelay + TARS.debriefDelay) +end + +------------------------------------------------- +-- TODO CONSTRUCTOR +------------------------------------------------- + +--- Creates the TARS singleton and wires up all event handlers. +-- @param #TARS self +-- @param #string locale (optional) Set locale for text output, defaults to "en". "fr" and "de" available out-of-the-box. +-- @return #TARS self +function TARS:New(locale) + local self = BASE:Inherit(self, BASE:New()) + self.lid = "TARS " .. TARS.version .. " | " + + if TARS.mooseScoring then + TARS.scoring = SCORING:New("TARS Scoring") + end + + self:HandleEvent(EVENTS.Birth, self._OnEventBirth) + self:HandleEvent(EVENTS.EngineStartup, self._OnEventEngineStartup) + self:HandleEvent(EVENTS.Dead, self._OnEventDead) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._OnEventPlayerLeaveUnit) + self:HandleEvent(EVENTS.Takeoff, self._OnEventTakeOff) + self:HandleEvent(EVENTS.RunwayTakeoff, self._OnEventTakeOff) + self:HandleEvent(EVENTS.Land, self._OnEventLand) + self:HandleEvent(EVENTS.RunwayTouch, self._OnEventLand) + + timer.scheduleFunction( + function(_, t) return self:RemoveUnusedMarks(nil, t) end, + nil, timer.getTime() + 20) + + self.locale = locale or self.locale + self:I(self.lid .. "initialised. Locale: " .. tostring(self.locale)) + return self +end + +--- Configure SRS radio output. +-- @param #TARS self +-- @param #string Path (Optional) Path to SRS (or nil to use MSRS default) +-- @param #number Frequency MHz, e.g. 251 +-- @param #number Modulation radio.modulation.AM or FM (default AM) +-- @param #string Culture (Optional) BCP-47 culture string, e.g. "ru-RU" +-- @param #string Gender (Optional) "male" or "female". Usually not used when using a specific voice. +-- @param #string Voice MSRS voice constant; do not forget to adjust voice to your locale! +-- @param #number Coalition MSRS Coalition, e.g. coalition.side.BLUE. +-- @param #number Port (Optional) SRS port (default 5002) +-- @param #number Speed (Optional) Speech speed (or nil to use MSRS default) +-- @param #string Provider (Optional) Provider, e.g. MSRS.Provider.GOOGLE (or nil to use MSRS default) +-- @param #string Backend (Optional) Backend, e.g. MSRS.Backend.HOUND (or nil to use MSRS default) +-- @param #number Speaker (Optional, HOUND/PIPER only!) Speaker number, e.g. 11 for Speaker "318 (11)" +-- @return #TARS self +function TARS:SetSRS(Path,Frequency,Modulation,Culture,Gender,Voice,Coalition,Port,Speed,Provider,Backend,Speaker) + self:T(self.lid.."SetSRS") + MESSAGE.SetMSRS(Path,Port,nil,Frequency,Modulation,Gender,Culture,Voice,Coalition,nil,"TARS",nil,Backend,Provider,Speaker) + if Speed then + _MESSAGESRS.MSRS.speed = Speed + end + self.SRS = true + return self +end + + +--- Set SRS Voice Speaker for Hound/Piper +--@param #TARS self +--@param #number Speaker Speaker number, e.g. 11 for Speaker "318 (11)" +--@return #TARS self +function TARS:SetSRSPiperSpeaker(Speaker) + self:T(self.lid.."SetSRSPiperSpeaker "..tostring(Speaker)) + self.SRSSpeaker = Speaker + return self +end + +--- Moose FSM Style callback function for mission designers. Optionally overwrite with own function. Processed after landing on debriefing analysis. Use for pre-processing. +-- @param #TARS self +-- @param #TARS.Snapshot TargetSnap Table of data of a **single** found object in the last session. +-- @return #boolean returnvalue If false, then `TARS:OnAfterDataProcessing` will NOT be called. +function TARS:OnBeforeDataProcessing(TargetSnap) + return true +end + +--- Moose FSM Style callback function for mission designers. Optionally overwrite with own function. Processed after landing on debriefing analysis. +-- @param #TARS self +-- @param #TARS.Snapshot TargetSnap Table of data of a **single** found object in the last session. +-- @return #TARS self +function TARS:OnAfterDataProcessing(TargetSnap) + return self +end + diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 2e4c57bc5..085aea6eb 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -112,6 +112,7 @@ Ops/OpsZone.lua Ops/ArmyGroup.lua Ops/OpsTransport.lua Ops/Target.lua +Ops/TARS.lua Sound/UserSound.lua Sound/SoundOutput.lua From 6223b8e1a73fac84e3b4874a060fc98beb124e1b Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 12 Apr 2026 13:12:58 +0200 Subject: [PATCH 3/7] #TARS - docu fixes --- Moose Development/Moose/Ops/TARS.lua | 79 ++-------------------------- 1 file changed, 3 insertions(+), 76 deletions(-) diff --git a/Moose Development/Moose/Ops/TARS.lua b/Moose Development/Moose/Ops/TARS.lua index 496451169..25f908682 100644 --- a/Moose Development/Moose/Ops/TARS.lua +++ b/Moose Development/Moose/Ops/TARS.lua @@ -86,7 +86,6 @@ TARS_SESSION.debug = false -- @field #table reconTypes Map of `[typeName] = true` for all recon-capable DCS type names. -- @field #table parameters Map of `[typeName] = #TARS.PlatformParams` with per-platform sensor profiles. -- @field #table allowedAmmo Map of `[weaponDisplayName] = true` for permitted loadout items. --- @field #table Locale Map of `[messageID] = TEXTANDSOUND` — populated by `TARS_Locale.lua`. -- @field #table instances Runtime map `[unitName] = #TARS_SESSION` of active sorties. -- @field #table groundMenus Runtime map `[playerName] = #TARS.MenuData` of open F10 menus. -- @field #table detectedTargets Lifetime map `[unitName] = #TARS.Snapshot` of all reported targets. @@ -279,7 +278,7 @@ TARS = {} --- @field #string version TARS.version = "v2.2.1" ---- Active locale. Set before `TARS:New()`. Populated by `TARS_Locale.lua`. +--- Active locale. -- @field #string locale TARS.locale = TARS.locale or "en" @@ -404,78 +403,6 @@ TARS.redMarkCount = 150000 TARS.blueMarkCount = 160000 TARS.scoring = nil ---- **TARS_Locale — Localization for the Tactical Air Recon System** --- --- This file defines all player-facing strings for TARS in English (en), --- German (de), and French (fr) using the MOOSE `TEXTANDSOUND` class. --- --- ## How to use --- Load this file **after** TARS.lua: --- --- dofile(basedir .. "Moose_.lua") --- dofile(basedir .. "TARS.lua") --- dofile(basedir .. "TARS_Locale.lua") -- ← this file --- --- Then set the desired locale before (or after) calling `TARS:New()`: --- --- TARS.locale = "de" -- "en" (default), "de", "fr" --- TARS_Instance = TARS:New() --- --- ## Adding a new language --- Any key without a translation for the chosen locale automatically falls --- back to English. --- --- ## Strings with format placeholders --- Entries that contain `%d` or `%s` are passed through `string.format()` --- inside `TARS:_T()`. Pass the values as extra arguments: --- --- self:_MsgUnit( self:_Txt("TARS_FILM_START", self.duration), 5, playerName ) --- --- @module TARS_Locale --- @author FMD — Fredy --- @author Applevangelist - Moose migration, Claude.AI - -------------------------------------------------- --- LOCALE CONFIGURATION -------------------------------------------------- - ---- Active locale used by `TARS:_T()`. --- Set this before `TARS:New()` is called. --- Supported values: `"en"` (default), `"de"`, `"fr"`. --- @field #string locale -TARS.locale = TARS.locale or "en" - -------------------------------------------------- --- TODO LOCALE TABLE --- Each entry is a TEXTANDSOUND object keyed by a message ID string. -------------------------------------------------- - ---- Map of `[messageID] = TEXTANDSOUND` objects for all player-facing strings. --- @field #table Locale ---- **TARS.Messages — Localization strings for the Tactical Air Recon System** --- --- All player-facing strings are stored in a single `TARS.Messages` table, --- keyed first by locale (`"en"`, `"de"`, `"fr"`) and then by message ID. --- --- ## Resolving a string --- Use `TARS:_T(id, ...)` anywhere inside TARS methods. --- The helper looks up `TARS.Messages[TARS.locale][id]`, falls back to `"en"`, --- and optionally passes extra arguments through `string.format`: --- --- self:_MsgUnit( self:_Txt("TARS_FILM_START", self.duration), 5, playerName ) --- --- ## Adding a new language --- Add a new locale block (e.g. `es = { ... }`) following the same keys as `en`. --- Any missing key automatically falls back to English at runtime. --- --- ## Strings with format placeholders --- `%d` = number, `%s` = string. The number and order of placeholders must --- match between all languages for a given key. --- --- @module TARS_Locale --- @author FMD — Fredy --- @author Applevangelist - ------------------------------------------------- -- LOCALE CONFIGURATION ------------------------------------------------- @@ -894,9 +821,9 @@ end -- @param #TARS.PlatformParams params -- @return DCS#Vec2 `{ x, z }` ahead of the aircraft. function TARS_SESSION:_OffsetCalc(unit, params) - local pos = unit:GetPositionVec3() + local pos = unit:GetPosition() local vec3 = unit:GetVec3() - local rad = math.atan2(pos.z, pos.x) + 2 * math.pi -- pos.x = Vorwärts-Vektor + local rad = math.atan2(pos.x.z, pos.x.x) + 2 * math.pi -- pos.x = Vorwärts-Vektor local MSL = land.getHeight({ x = vec3.x, y = vec3.z }) local alt = vec3.y - MSL local dist = math.tan(params.offset) * alt From aebb6ac0db0ec34f381c5ae2ca9b1c61a0b7485a Mon Sep 17 00:00:00 2001 From: Rolln-dev Date: Mon, 13 Apr 2026 19:20:20 -0600 Subject: [PATCH 4/7] [BUGFIX] AIRBOSS:_LSOGrades When points are deducted for a 1-wire pass, we also need to change the grade according to the new points. --- Moose Development/Moose/Ops/Airboss.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5ef27940f..2328464e2 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -12903,6 +12903,14 @@ function AIRBOSS:_LSOgrade( playerData ) -- Circuit edit only take points awary from a 1 wire if there are more than 4 other deviations if playerData.wire == 1 and points >= 3 and N > 4 then points = points -1 + -- We also need to change the grade based on the new points. + if points == 4 then + grade = "OK" + elseif points == 3 then + grade = "(OK)" + elseif points == 2 then + grade = "--" + end end env.info("Returning: " .. grade .. " " .. points .. " " .. G) From 3d10716b1fc2c9a1c88ba9cc0e87b4243bd9e1cf Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:28:39 +0200 Subject: [PATCH 5/7] Update TARS.lua #TARS Documentation addition --- Moose Development/Moose/Ops/TARS.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/TARS.lua b/Moose Development/Moose/Ops/TARS.lua index 25f908682..092e7df06 100644 --- a/Moose Development/Moose/Ops/TARS.lua +++ b/Moose Development/Moose/Ops/TARS.lua @@ -180,11 +180,15 @@ TARS_SESSION.debug = false -- TARS.detectUnits = true -- capture UNIT objects -- TARS.detectStatics = false -- capture STATIC objects incl. of FARPs -- --- ### UNIT Filters +-- ### Target UNIT Filters -- -- TARS.units = { air=false, ground=true, ship=true } +-- +-- ### Target UNIT Name Filters +-- +-- TARS.targetNameFilter = { enabled = true, keywords = { [coalition.side.BLUE] = { "USA" }, [coalition.side.RED] = { "USSR" },},} -- --- ### STATIC Filters +-- ### Target STATIC Filters -- -- TARS.statics = { -- farps=true, From 0b2f07ca6ccdbf4bd281fd3eeb9dac94aa69f375 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:06:57 +0200 Subject: [PATCH 6/7] Update CTLD.lua - **Changed** the pack menu layout so pack actions now open into dedicated `Pack`, `Pack and Load`, and `Pack and Remove` submenus. - **Added** scanning for nearby packable units within each pack submenu. - **Added** direct selection of individual nearby packable units from the pack menus. - **Changed** the pack menu order so detected nearby units appear first, followed by the nearby bulk action and the scan action. - **Added** nearby bulk actions in each pack submenu (`Pack nearby`, `Pack and Load nearby`, `Pack and Remove nearby`). - **Changed** pack menu refresh behavior so nearby packable units are rebuilt after major logistics actions and after build completion. - **Fixed** selected `Pack and Load` so it only loads the crates created by the chosen packed unit. - **Fixed** selected `Pack and Remove` so it only removes the crates created by the chosen packed unit. - **Changed** pre-pack event handling so before-pack callbacks receive the specific group being packed. - **Added** localization for the new pack menu wording in English, German, French, and Spanish. --- Moose Development/Moose/Ops/CTLD.lua | 468 ++++++++++++++++++++++++--- 1 file changed, 430 insertions(+), 38 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 1f9a7aab3..22ef5882f 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1252,6 +1252,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. + -- @param Wrapper.Group#GROUP PackedGroup Group object that is about to be packed. -- @return #CTLD self --- FSM Function OnBeforeTroopsRTB. @@ -1414,7 +1415,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. - -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. + -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. For direct C-130 packing this can also be a table of spawned packed cargo objects. -- @return #CTLD self --- FSM Function OnAfterTroopsRTB. @@ -2566,6 +2567,7 @@ function CTLD:_EventHandler(EventData) local _group = event.IniGroup local _unit = event.IniUnit self:_RefreshLoadCratesMenu(_group, _unit) + self:_RefreshPackMenus(_group, _unit) if self:IsFixedWing(_unit) and self.enableFixedWing then self:_RefreshDropCratesMenu(_group, _unit) end @@ -4160,6 +4162,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress end end self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) return true end @@ -4383,7 +4386,9 @@ function CTLD:_RemoveCratesNearby(_group, _unit) done[n] = true end end + self:_CleanupTrackedCrates(removedIDs) self:_RefreshLoadCratesMenu(_group,_unit) + self:_RefreshPackMenus(_group,_unit) -- Trigger FSM event for removed crates. self:__RemoveCratesNearby(1, _group, _unit, crates) @@ -4639,6 +4644,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group, Unit) self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) -- clean up real world crates self:_CleanupTrackedCrates(crateidsloaded) self:__CratesPickedUp(1, Group, Unit, loaded.Cargo) @@ -5520,6 +5526,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) end self:_CleanUpCrates(cratesNow,build,numberNow) self:_RefreshLoadCratesMenu(Group,Unit) + self:_RefreshPackMenus(Group,Unit) if self.buildtime and self.buildtime > 0 then local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate(),MultiDrop) buildtimer:Start(self.buildtime) @@ -5541,6 +5548,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) end self:_CleanUpCrates(cratesNow,build,numberNow) self:_RefreshLoadCratesMenu(Group,Unit) + self:_RefreshPackMenus(Group,Unit) local off = start + (n-1)*sep local coord = base:Translate(off,lat):GetVec2() local b = { Name=build.Name, Required=build.Required, Template=build.Template, CanBuild=true, Type=build.Type, Coord=coord } @@ -5573,53 +5581,393 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) return self end ---- (Internal) Function to repair nearby vehicles / FOBs +--- (Internal) Function to find nearby packable groups. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit +-- @return #table PackableGroups +-- @return #number Number +function CTLD:_FindPackableGroupsNearby(Group, Unit) + self:T(self.lid .. " _FindPackableGroupsNearby") + local location = Group:GetCoordinate() + if not location then return {}, 0 end + local capabilities = self:_GetUnitCapabilities(Unit) + local innerDist = (capabilities.length and capabilities.length/2) or 15 + local finddist = self.PackDistance or (self.CrateDistance or 35) + local zone = ZONE_RADIUS:New("CTLD_PackableZone", location:GetVec2(), finddist, false) + local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() + local packable = {} -function CTLD:_PackCratesNearby(Group, Unit) - self:T(self.lid .. " _PackCratesNearby") - ----------------------------------------- - -- search for nearest group to player - -- determine if group is packable - -- generate crates and destroy group - ----------------------------------------- - - -- get nearby vehicles - local location = Group:GetCoordinate() -- get coordinate of group using function - local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({ZONE_RADIUS:New("TempZone", location:GetVec2(), self.PackDistance, false)}):FilterOnce() - - local packedAny = false - - -- determine if group is packable - for _, _Group in pairs(nearestGroups.Set) do -- convert #SET_GROUP to a list of Wrapper.Group#GROUP - local didPackThisGroup = false - for _, _Template in pairs(_DATABASE.Templates.Groups) do -- iterate through the database of templates - if string.match(_Group:GetName(), _Template.GroupName) then -- check if the Wrapper.Group#GROUP near the player is in the list of templates by name - for _, _entry in pairs(self.Cargo_Crates) do -- iterate through #CTLD_CARGO - if _entry.Templates[1] == _Template.GroupName then -- check if the #CTLD_CARGO matches the template name - _Group:Destroy() - self:_GetCrates(Group, Unit, _entry, nil, false, true) -- spawn the appropriate crates near the player - self:_RefreshLoadCratesMenu(Group,Unit) -- call the refresher to show the crates in the menu - self:__CratesPacked(1,Group,Unit,_entry) - packedAny = true - didPackThisGroup = true - break + for _, gr in pairs(nearestGroups.Set) do + if gr and gr:GetName() ~= Group:GetName() then + local gc = gr:GetCoordinate() + if gc then + local dist = location:Get2DDistance(gc) + if dist > innerDist and dist <= finddist then + local generic = self:GetGenericCargoObjectFromGroupName(gr:GetName()) + local cargo = generic and self:_FindCratesCargoObject(generic:GetName() or generic.Name) or nil + if cargo then + local display = self:_GetCargoDisplayName(cargo) + packable[#packable + 1] = { + group = gr, + groupName = gr:GetName(), + cargo = cargo, + distance = dist, + display = display, + } end end end - if didPackThisGroup then break end + end + end + + table.sort(packable, function(a, b) + if a.distance ~= b.distance then + return a.distance < b.distance + end + return a.groupName < b.groupName + end) + + return packable, #packable +end + +--- (Internal) Function to pack a selected nearby group into crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param Wrapper.Group#GROUP TargetGroup +-- @param #boolean EmitPackedEvent +-- @param #boolean SkipMenuRefresh +-- @return #table PackedCargo +-- @return #CTLD_CARGO CargoEntry +-- @return #boolean Success +function CTLD:_PackSingleGroupToCrates(Group, Unit, TargetGroup, EmitPackedEvent, SkipMenuRefresh) + self:T(self.lid .. " _PackSingleGroupToCrates") + local generic = self:GetGenericCargoObjectFromGroupName(TargetGroup:GetName()) + local cargoEntry = generic and self:_FindCratesCargoObject(generic:GetName() or generic.Name) or nil + if not cargoEntry then + return nil, nil, false + end + + local from = self.current + local to = self.current + local emitPackedEvent = EmitPackedEvent ~= false + + if emitPackedEvent then + local packParams = { from, "CratesPacked", to, Group, Unit, cargoEntry, TargetGroup } + if self:_call_handler("onbefore", "CratesPacked", packParams, "CratesPacked") == false then + return nil, cargoEntry, false + end + if self:_call_handler("OnBefore", "CratesPacked", packParams, "CratesPacked") == false then + return nil, cargoEntry, false + end + end + + TargetGroup:Destroy() + self.Spawned_Cargo = self.Spawned_Cargo or {} + local spawnedCountBefore = #self.Spawned_Cargo + local ok = self:_GetCrates(Group, Unit, cargoEntry, nil, false, true) + if not ok then + if not SkipMenuRefresh then + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) + end + return nil, cargoEntry, false + end + + local packedCargo = {} + for idx = spawnedCountBefore + 1, #self.Spawned_Cargo do + local cargo = self.Spawned_Cargo[idx] + if cargo then + if self.UseC130LoadAndUnload and self:IsC130J(Unit) then + cargo:SetWasDropped(true, true) + end + packedCargo[#packedCargo + 1] = cargo + end + end + + if not SkipMenuRefresh then + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) + end + + if emitPackedEvent then + local eventCargo = cargoEntry + if self.UseC130LoadAndUnload and self:IsC130J(Unit) and #packedCargo > 0 then + eventCargo = packedCargo + end + local packParams = { from, "CratesPacked", to, Group, Unit, eventCargo } + self:_call_handler("onafter", "CratesPacked", packParams, "CratesPacked") + self:_call_handler("OnAfter", "CratesPacked", packParams, "CratesPacked") + end + + return packedCargo, cargoEntry, true +end + +--- (Internal) Function to load the exact crates created by a selected pack action. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #table crateIds +-- @param #string cargoName +-- @return #CTLD self +function CTLD:_LoadPackedCratesByIds(Group, Unit, crateIds, cargoName) + self:T(self.lid .. " _LoadPackedCratesByIds cargoName=" .. (cargoName or "nil")) + local grounded = not self:IsUnitInAir(Unit) + local hover = self:CanHoverLoad(Unit) + if not grounded and not hover then + local msg = self.gettext:GetEntry("MUST_LAND_OR_HOVER_CRATES",self.locale) + self:_SendMessage(msg, 10, false, Group) + return self + end + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + local msg = self.gettext:GetEntry("OPEN_DOORS_LOAD_CARGO",self.locale) + self:_SendMessage(msg, 10, false, Group) + return self + end + + local idLookup = {} + for _, id in pairs(crateIds or {}) do + idLookup[id] = true + end + + local matchingCrates = {} + local finddist = self.CrateDistance or 35 + local location = Group:GetCoordinate() + for _, crateObj in pairs(self.Spawned_Cargo or {}) do + if crateObj and idLookup[crateObj:GetID()] then + local pos = crateObj:GetPositionable() + if pos and pos:IsAlive() then + local dist = location:Get2DDistance(pos:GetCoordinate()) + if dist <= finddist then + matchingCrates[#matchingCrates + 1] = crateObj + end + end + end + end + + if #matchingCrates == 0 then + local msg = self.gettext:GetEntry("NO_NAMED_CRATES_IN_RANGE",self.locale) + msg = string.format(msg, cargoName or "selection") + self:_SendMessage(msg, 10, false, Group) + self:_RefreshPackMenus(Group, Unit) + return self + end + + table.sort(matchingCrates, function(a, b) return a:GetID() < b:GetID() end) + local needed = matchingCrates[1]:GetCratesNeeded() or 1 + local unitName = Unit:GetName() + local loadedData = self.Loaded_Cargo[unitName] or { Troopsloaded = 0, Cratesloaded = 0, Cargo = {} } + local capabilities = self:_GetUnitCapabilities(Unit) + local capacity = capabilities.cratelimit or 0 + if loadedData.Cratesloaded >= capacity then + local msg = self.gettext:GetEntry("NO_MORE_CAPACITY",self.locale) + self:_SendMessage(msg, 10, false, Group) + self:_RefreshPackMenus(Group, Unit) + return self + end + + local spaceLeft = capacity - loadedData.Cratesloaded + local toLoad = math.min(#matchingCrates, needed, spaceLeft) + if toLoad < 1 then + local msg = self.gettext:GetEntry("CANNOT_LOAD_NONE_OR_FULL",self.locale) + self:_SendMessage(msg, 10, false, Group) + self:_RefreshPackMenus(Group, Unit) + return self + end + + local crateIDsLoaded = {} + for i = 1, toLoad do + local crate = matchingCrates[i] + crate:SetHasMoved(true) + crate:SetWasDropped(false) + table.insert(loadedData.Cargo, crate) + loadedData.Cratesloaded = loadedData.Cratesloaded + 1 + local stObj = crate:GetPositionable() + if stObj and stObj:IsAlive() then + stObj:Destroy(false) + end + crateIDsLoaded[#crateIDsLoaded + 1] = crate:GetID() + end + + self.Loaded_Cargo[unitName] = loadedData + self:_UpdateUnitCargoMass(Unit) + self:_CleanupTrackedCrates(crateIDsLoaded) + + local loadedHere = toLoad + local displayName = cargoName or (matchingCrates[1]:GetName() or "selection") + if loadedHere < needed and loadedData.Cratesloaded >= capacity then + local msg = self.gettext:GetEntry("LOADED_PARTIAL_LIMIT",self.locale) + msg = string.format(msg, loadedHere, needed, displayName) + self:_SendMessage(msg, 10, false, Group) + else + local fullSets = math.floor(loadedHere / needed) + local leftover = loadedHere % needed + if needed > 1 then + if fullSets > 0 and leftover == 0 then + local msg = self.gettext:GetEntry("LOADED_FULL",self.locale) + msg = string.format(msg, fullSets, displayName) + self:_SendMessage(msg, 10, false, Group) + elseif fullSets > 0 and leftover > 0 then + local msg = self.gettext:GetEntry("LOADED_SETS_LEFTOVER",self.locale) + msg = string.format(msg, fullSets, displayName, leftover) + self:_SendMessage(msg, 10, false, Group) + else + local msg = self.gettext:GetEntry("LOADED_PARTIAL",self.locale) + msg = string.format(msg, loadedHere, needed, displayName) + self:_SendMessage(msg, 15, false, Group) + end + else + local msg = self.gettext:GetEntry("LOADED_SETS",self.locale) + msg = string.format(msg, loadedHere, displayName) + self:_SendMessage(msg, 10, false, Group) + end + end + + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshDropCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) + if cargoName then + self:_RefreshCrateQuantityMenus(Group, Unit, self:_FindCratesCargoObject(cargoName)) + end + return self +end + +--- (Internal) Function to remove the exact crates created by a selected pack action. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #table crateIds +-- @return #CTLD self +function CTLD:_RemovePackedCratesByIds(Group, Unit, crateIds) + self:T(self.lid .. " _RemovePackedCratesByIds") + local idLookup = {} + for _, id in pairs(crateIds or {}) do + idLookup[id] = true + end + + local crates = {} + local finddist = self.CrateDistance or 35 + local location = Group:GetCoordinate() + for _, entry in pairs(self.Spawned_Cargo or {}) do + if entry and idLookup[entry:GetID()] then + local pos = entry:GetPositionable() + if pos and pos:IsAlive() then + local dist = location:Get2DDistance(pos:GetCoordinate()) + if dist <= finddist then + crates[#crates + 1] = entry + end + end + end + end + + if #crates == 0 then + local msg = self.gettext:GetEntry("NOTHING_TO_REMOVE",self.locale) + self:_SendMessage(msg, 10, false, Group) + self:_RefreshPackMenus(Group, Unit) + return self + end + + local text = REPORT:New(self.gettext:GetEntry("REPORT_REMOVING_CRATES",self.locale)) + text:Add("------------------------------------------------------------") + local removedIDs = {} + for _, entry in pairs(crates) do + local name = entry:GetName() or "none" + text:Add(string.format(self.gettext:GetEntry("REPORT_ROW_CRATE_REMOVED",self.locale), name, entry.PerCrateMass)) + local pos = entry:GetPositionable() + if pos then + entry.coordinate = pos:GetCoordinate() + pos:Destroy(false) + end + removedIDs[#removedIDs + 1] = entry:GetID() + end + text:Add("------------------------------------------------------------") + self:_SendMessage(text:Text(), 30, true, Group, true) + + local done = {} + for _, e in pairs(crates) do + local n = e:GetName() or "none" + if not done[n] then + local object = self:_FindCratesCargoObject(n) + if object then self:_RefreshCrateQuantityMenus(Group, Unit, object) end + done[n] = true + end + end + + self:_CleanupTrackedCrates(removedIDs) + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) + self:__RemoveCratesNearby(1, Group, Unit, crates) + return self +end + +--- (Internal) Function to pack a selected nearby group. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #string TargetGroupName +-- @param #string Mode +-- @return #boolean Success +function CTLD:_PackSelectedGroupAction(Group, Unit, TargetGroupName, Mode) + self:T(self.lid .. " _PackSelectedGroupAction") + local targetGroup = GROUP:FindByName(TargetGroupName) + if not targetGroup or not targetGroup:IsAlive() then + local msg = self.gettext:GetEntry("NOTHING_TO_PACK",self.locale) + self:_SendMessage(msg, 10, false, Group) + self:_RefreshPackMenus(Group, Unit) + return false + end + + local emitPackedEvent = Mode == "pack" + local packedCargo, cargoEntry, ok = self:_PackSingleGroupToCrates(Group, Unit, targetGroup, emitPackedEvent) + if not ok then + self:_RefreshPackMenus(Group, Unit) + return false + end + + if Mode == "load" or Mode == "remove" then + local crateIds = {} + for _, cargo in ipairs(packedCargo or {}) do + crateIds[#crateIds + 1] = cargo:GetID() + end + local cargoName = cargoEntry and (cargoEntry:GetName() or cargoEntry.Name) or nil + if Mode == "load" then + timer.scheduleFunction(function() self:_LoadPackedCratesByIds(Group, Unit, crateIds, cargoName) end, {}, timer.getTime() + 1) + else + timer.scheduleFunction(function() self:_RemovePackedCratesByIds(Group, Unit, crateIds) end, {}, timer.getTime() + 1) + end + end + + return true +end + +--- (Internal) Function to pack nearby vehicles / FOBs. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean EmitPackedEvent If false, suppress CratesPacked callbacks. +-- @return #boolean Success +function CTLD:_PackCratesNearby(Group, Unit, EmitPackedEvent) + self:T(self.lid .. " _PackCratesNearby") + local packableGroups = self:_FindPackableGroupsNearby(Group, Unit) + local packedAny = false + local emitPackedEvent = EmitPackedEvent ~= false + + for _, entry in ipairs(packableGroups) do + local _, _, ok = self:_PackSingleGroupToCrates(Group, Unit, entry.group, emitPackedEvent, true) + if ok then + packedAny = true end end if not packedAny then local msg = self.gettext:GetEntry("NOTHING_TO_PACK",self.locale) self:_SendMessage(msg, 10, false, Group) - --self:_SendMessage("Nothing to pack at this distance pilot!",10,false,Group) return false end + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) return true end @@ -5775,6 +6123,8 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) end end -- template loop + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) else self:T(self.lid.."Group KIA while building!") end @@ -5898,7 +6248,7 @@ function CTLD:_PackAndLoad(Group,Unit) --self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group) return self end - if not self:_PackCratesNearby(Group,Unit) then + if not self:_PackCratesNearby(Group,Unit,false) then return self end timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1) @@ -6596,10 +6946,13 @@ function CTLD:_RefreshF10Menus() MENU_GROUP_COMMAND:New(_group, self.gettext:GetEntry("MENU_REMOVE_CRATES_NEARBY",self.locale), removecratesmenu, self._RemoveCratesNearby, self, _group, _unit) if self.onestepmenu then - local mPack=MENU_GROUP:New(_group,self.gettext:GetEntry("MENU_PACK_CRATES",self.locale),topcrates) - MENU_GROUP_COMMAND:New(_group,self.gettext:GetEntry("MENU_PACK",self.locale),mPack,self._PackCratesNearby,self,_group,_unit) - MENU_GROUP_COMMAND:New(_group,self.gettext:GetEntry("MENU_PACK_AND_LOAD",self.locale),mPack,self._PackAndLoad,self,_group,_unit) - MENU_GROUP_COMMAND:New(_group,self.gettext:GetEntry("MENU_PACK_AND_REMOVE",self.locale),mPack,self._PackAndRemove,self,_group,_unit) + topcrates.PackRootMenu = MENU_GROUP:New(_group, self.gettext:GetEntry("MENU_PACK",self.locale), topcrates) + topcrates.PackMenu = MENU_GROUP:New(_group, self.gettext:GetEntry("MENU_PACK",self.locale), topcrates.PackRootMenu) + local showPackAndLoad = not (self.UseC130LoadAndUnload and self:IsC130J(_unit)) + if showPackAndLoad then + topcrates.PackAndLoadMenu = MENU_GROUP:New(_group, self.gettext:GetEntry("MENU_PACK_AND_LOAD",self.locale), topcrates.PackRootMenu) + end + topcrates.PackAndRemoveMenu = MENU_GROUP:New(_group, self.gettext:GetEntry("MENU_PACK_AND_REMOVE",self.locale), topcrates.PackRootMenu) MENU_GROUP_COMMAND:New(_group, self.gettext:GetEntry("MENU_LIST_CRATES_NEARBY",self.locale), topcrates, self._ListCratesNearby, self, _group, _unit) else MENU_GROUP_COMMAND:New(_group, self.gettext:GetEntry("MENU_PACK_CRATES",self.locale), topcrates, self._PackCratesNearby, self, _group, _unit) @@ -6694,6 +7047,7 @@ function CTLD:_RefreshF10Menus() -- Mark we built the menu self.MenusDone[_unitName] = true self:_RefreshLoadCratesMenu(_group,_unit) + self:_RefreshPackMenus(_group,_unit) self:_RefreshDropCratesMenu(_group,_unit) if firstBuild then menucount=menucount+1 end if firstBuild and not self.showstockinmenuitems then self:_RefreshQuantityMenusForGroup(_group,_unit) end @@ -6754,7 +7108,43 @@ function CTLD:_RefreshLoadCratesMenu(Group,Unit) end end end - + +--- (Internal) Function to refresh the menu for pack actions. Triggered from land/build/pack and more. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group The calling group. +-- @param Wrapper.Unit#UNIT Unit The calling unit. +-- @return #CTLD self +function CTLD:_RefreshPackMenus(Group,Unit) + if not self.onestepmenu then return end + if not Group.CTLDTopmenu then return end + local topCrates = Group.MyTopCratesMenu + if not topCrates then return end + if not topCrates.PackRootMenu and not topCrates.PackMenu and not topCrates.PackAndLoadMenu and not topCrates.PackAndRemoveMenu then return end + + local packableGroups, n = self:_FindPackableGroupsNearby(Group,Unit) + + local function refreshPackMenu(menu, mode, allKey, bulkFunc) + if not menu then return end + menu:RemoveSubMenus() + + if n > 0 then + for idx, entry in ipairs(packableGroups) do + local label = string.format("%d. %s (%dm)", idx, entry.display or entry.groupName, math.floor((entry.distance or 0) + 0.5)) + MENU_GROUP_COMMAND:New(Group, label, menu, self._PackSelectedGroupAction, self, Group, Unit, entry.groupName, mode) + end + end + + MENU_GROUP_COMMAND:New(Group, self.gettext:GetEntry(allKey,self.locale), menu, bulkFunc, self, Group, Unit) + MENU_GROUP_COMMAND:New(Group, self.gettext:GetEntry("MENU_SCAN_PACKABLE_UNITS",self.locale), menu, self._RefreshPackMenus, self, Group, Unit) + end + + refreshPackMenu(topCrates.PackMenu, "pack", "MENU_PACK_ALL", self._PackCratesNearby) + if topCrates.PackAndLoadMenu then + refreshPackMenu(topCrates.PackAndLoadMenu, "load", "MENU_PACK_AND_LOAD_ALL", self._PackAndLoad) + end + refreshPackMenu(topCrates.PackAndRemoveMenu, "remove", "MENU_PACK_AND_REMOVE_ALL", self._PackAndRemove) +end + --- -- Loads exactly `CratesNeeded` crates for one cargoName in range. @@ -6911,6 +7301,7 @@ function CTLD:_LoadSingleCrateSet(Group, Unit, cargoName, details) self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshDropCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) self:_RefreshCrateQuantityMenus(Group, Unit, self:_FindCratesCargoObject(cargoName)) if batch and batch.cname == cargoName then @@ -7081,6 +7472,7 @@ end self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group, Unit) self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) self:_RefreshCrateQuantityMenus(Group, Unit, nil) return self end From 347783d4b7bc0bae2edf965bcf633f9982136155 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:08:45 +0200 Subject: [PATCH 7/7] Update CTLD_Localization.lua MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- -- @field Messages CTLD.Messages = { EN = { -- ============================================================ -- Crate / Cargo Loading -- ============================================================ CRATE_LOADED_GROUNDCREW = "Crate %s loaded by ground crew!", CRATE_UNLOADED_GROUNDCREW = "Crate %s unloaded by ground crew!", CRATE_LOADED_ID = "Crate ID %d for %s loaded!", LOADED_FULL = "Loaded %d %s.", LOADED_SETS_LEFTOVER = "Loaded %d %s(s), with %d leftover crate(s).", LOADED_SETS = "Loaded %d %s(s).", LOADED_PARTIAL = "Loaded only %d/%d crate(s) of %s.", LOADED_PARTIAL_LIMIT = "Loaded only %d/%d crate(s) of %s. Cargo limit is now reached!", LOADED_BATCH = "Loaded %d %s.", LOADED_BATCH_PARTIAL = "Some sets could not be fully loaded.", -- ============================================================ -- Dropping / Unloading -- ============================================================ DROPPED_FULL = "Dropped %d %s.", DROPPED_SETS_LEFTOVER = "Dropped %d %s(s), with %d leftover crate(s).", DROPPED_SETS = "Dropped %d %s(s).", DROPPED_PARTIAL = "Dropped %d/%d crate(s) of %s.", DROPPED_INTO_ACTION = "Dropped %s into action!", DROPPED_BEACON = "Dropped %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED = "%d crates for %s have been positioned near you!", CRATES_DROPPED = "%d crates for %s have been dropped!", -- ============================================================ -- Troops -- ============================================================ BOARDED = "%s boarded!", BOARDING = "%s boarding!", TROOPS_RETURNED = "Troops have returned to base!", -- ============================================================ -- Deployment -- ============================================================ DEPLOYED_NEAR_YOU = "%s have been deployed near you!", UNITS_REMOVED = "%s have been removed", -- ============================================================ -- Build / Repair -- ============================================================ BUILD_STARTED = "Build started, ready in %d seconds!", REPAIR_STARTED = "Repair started using %s taking %d secs", NO_UNIT_TO_REPAIR = "No unit close enough to repair!", CANT_REPAIR_WITH = "Can't repair this unit with %s", CRATES_MOVE_BEFORE_BUILD = "*** Crates need to be moved before building!", -- ============================================================ -- Errors - Chopper / Weight / Capacity -- ============================================================ CHOPPER_CANNOT_CARRY = "Sorry this chopper cannot carry crates!", TOO_HEAVY = "Sorry, that's too heavy to load!", FULLY_LOADED = "Sorry, we are fully loaded!", CRAMMED = "Sorry, we're crammed already!", NO_CAPACITY_NOW = "No capacity to load more now!", NO_MORE_CAPACITY = "No more capacity to load crates!", CANNOT_LOAD_NONE_OR_FULL = "Cannot load crates: either none found or no capacity left.", -- ============================================================ -- Errors - Position -- ============================================================ NEED_TO_LAND_OR_HOVER_LOAD = "You need to land or hover in position to load!", HOVER_OVER_CRATES = "Hover over the crates to pick them up!", LAND_OR_HOVER_OVER_CRATES = "Land or hover over the crates to pick them up!", MUST_LAND_OR_HOVER_CRATES = "You must land or hover to load crates!", NEED_TO_LAND_BUILD = "You need to land / stop to build something, Pilot!", NOT_CLOSE_ENOUGH_LOGISTICS = "You are not close enough to a logistics zone!", NOT_CLOSE_ENOUGH_DROP = "You are not close enough to a drop zone!", NOT_CLOSE_ENOUGH_ZONE_NM = "Negative, need to be closer than %dnm to a zone!", CANNOT_BUILD_LOADING_AREA = "You cannot build in a loading area, Pilot!", -- ============================================================ -- Errors - Doors -- ============================================================ OPEN_DOORS_LOAD_CARGO = "You need to open the door(s) to load cargo!", OPEN_DOORS_LOAD_TROOPS = "You need to open the door(s) to load troops!", OPEN_DOORS_EXTRACT_TROOPS = "You need to open the door(s) to extract troops!", OPEN_DOORS_UNLOAD_TROOPS = "You need to open the door(s) to unload troops!", OPEN_DOORS_DROP_CARGO = "You need to open the door(s) to drop cargo!", -- ============================================================ -- Errors - Stock / Availability -- ============================================================ ALL_GONE = "Sorry, all %s are gone!", RAN_OUT_OF = "Sorry, we ran out of %s", CARGO_NOT_AVAILABLE_ZONE = "The requested cargo is not available in this zone!", ENOUGH_CRATES_NEARBY = "There are enough crates nearby already! Take care of those first!", NO_CRATES_WITHIN = "No (loadable) crates within %d meters!", NO_CRATES_WITHIN_PLAIN = "No crates within %d meters!", NO_CRATES_IN_RANGE = "No crates found in range!", NO_NAMED_CRATES_IN_RANGE = "No \"%s\" crates found in range!", NO_LOADABLE_CRATES = "Sorry, no loadable crates nearby or max cargo weight reached!", NO_UNITS_TO_EXTRACT = "No units close enough to extract!", NO_UNIT_CONFIG = "No unit configuration found for %s", CANT_ONBOARD = "Can't onboard %s", TOO_MANY_UNITS_NEARBY = "You already have %d units nearby!", NO_CRATE_GROUPS = "No crate groups found for this unit!", NO_CRATE_SET = "No crate set found or index invalid!", NO_CRATE_IN_SET = "No crate found in that set!", NO_TROOP_CHUNK = "No troop cargo chunk found for ID %d!", TROOP_CHUNK_EMPTY = "Troop chunk is empty for ID %d!", -- ============================================================ -- Nothing loaded / in stock -- ============================================================ NOTHING_LOADED = "Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs", NOTHING_LOADED_AIRDROP = "Nothing loaded or not within airdrop parameters!", NOTHING_LOADED_HOVER = "Nothing loaded or not hovering within parameters!", NOTHING_IN_STOCK = "Nothing in stock!", NOTHING_TO_PACK = "Nothing to pack at this distance pilot!", NOTHING_TO_REMOVE = "Nothing to remove at this distance pilot!", -- ============================================================ -- Zone / Info -- ============================================================ ROGER_ZONE = "Roger, %s zone %s!", -- ============================================================ -- Report: Hover / Flight Parameters -- ============================================================ HOVER_PARAMS_METRIC = "Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", HOVER_PARAMS_IMPERIAL = "Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s", FLIGHT_PARAMS_IMPERIAL = "Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", FLIGHT_PARAMS_METRIC = "Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", -- ============================================================ -- Report Titles (REPORT:New()) -- ============================================================ REPORT_CRATES_FOUND = "Crates Found Nearby:", REPORT_REMOVING_CRATES = "Removing Crates Found Nearby:", REPORT_TRANSPORT_CHECKOUT = "Transport Checkout Sheet", REPORT_INVENTORY = "Inventory Sheet", REPORT_BUILD_CHECKLIST = "Checklist Buildable Crates", REPORT_REPAIR_CHECKLIST = "Checklist Repairs", REPORT_BEACONS = "Active Zone Beacons", -- ============================================================ -- Report Section Headers (report:Add()) -- ============================================================ REPORT_SECTION_TROOPS = " -- TROOPS --", REPORT_SECTION_CRATES = " -- CRATES --", REPORT_SECTION_CRATES_GC = " -- CRATES loaded via Ground Crew --", REPORT_SECTION_NONE = " N O N E", REPORT_SECTION_NONE_ALT = " --- None found! ---", REPORT_SECTION_NONE_REPAIR = " --- None Found ---", REPORT_GC_LOADABLE_HINT = "Probably ground crew loadable (F8)", REPORT_TOTAL_MASS = "Total Mass: %s kg. Loadable: %s kg.", REPORT_TROOPS_CRATES_COUNT = "Troops: %d(%d), Crates: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT = "Troops: %d, Cratetypes: %d", -- ============================================================ -- Report Row Templates (per-item lines in reports) -- ============================================================ REPORT_ROW_TROOP = "Troop: %s size %d", REPORT_ROW_CRATE = "Crate: %s %d/%d", REPORT_ROW_CRATE_SIZE1 = "Crate: %s size 1", REPORT_ROW_GC_CRATE = "GC loaded Crate: %s size 1", REPORT_ROW_DROPPED_CRATE = "Dropped crate for %s, %dkg", REPORT_ROW_CRATE_KG = "Crate for %s, %dkg", REPORT_ROW_CRATE_REMOVED = "Crate for %s, %dkg removed", REPORT_ROW_UNIT_STOCK = "Unit: %s | Soldiers: %d | Stock: %s", REPORT_ROW_TYPE_CRATE_STOCK = "Type: %s | Crates per Set: %d | Stock: %s", REPORT_ROW_TYPE_STOCK = "Type: %s | Stock: %s", REPORT_ROW_BUILD_CHECK = "Type: %s | Required %d | Found %d | Can Build %s", REPORT_ROW_REPAIR_CHECK = "Type: %s | Required %d | Found %d | Can Repair %s", REPORT_ROW_BEACON = " %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", -- ============================================================ -- Weight / Crate limit tokens -- ============================================================ WEIGHT_LIMIT = "Weight limit reached", CRATE_LIMIT = "Crate limit reached", -- ============================================================ -- Menu labels - Top level -- ============================================================ MENU_CTLD = "CTLD", MENU_MANAGE_TROOPS = "Manage Troops", MENU_MANAGE_CRATES = "Manage Crates", MENU_MANAGE_UNITS = "Manage Units", -- ============================================================ -- Menu labels - Troops -- ============================================================ MENU_LOAD_TROOPS = "Load troops", MENU_DROP_TROOPS = "Drop Troops", MENU_DROP_ALL_TROOPS = "Drop ALL troops", MENU_EXTRACT_TROOPS = "Extract troops", MENU_DROP_N_TROOPS = "Drop (%d) %s", -- ============================================================ -- Menu labels - Crates: Get -- ============================================================ MENU_GET_CRATES = "Get Crates", MENU_GET = "Get", MENU_GET_AND_LOAD = "Get and Load", MENU_GET_ANYWAY = "Get anyway", MENU_PARTIALLY_LOAD = "Partially load", MENU_OUT_OF_STOCK = "Out of stock", MENU_TROOP_LIMIT = "Troop limit reached", -- ============================================================ -- Menu labels - Crates: Load -- ============================================================ MENU_LOAD_CRATES = "Load Crates", MENU_LOAD_ALL = "Load ALL", MENU_SHOW_LOADABLE_CRATES = "Show loadable crates", MENU_NO_CRATES_FOUND_RESCAN = "No crates found! Rescan?", MENU_USE_C130_LOAD = "Use C-130 Load system", MENU_LOAD_SINGLE = "Load", -- ============================================================ -- Menu labels - Crates: Drop -- ============================================================ MENU_DROP_CRATES = "Drop Crates", MENU_DROP_ALL_CRATES = "Drop ALL crates", MENU_DROP = "Drop", MENU_DROP_AND_BUILD = "Drop and build", MENU_DROP_N_SETS = "Drop %d Set%s", MENU_NO_CRATES_TO_DROP = "No crates to drop!", -- ============================================================ -- Menu labels - Crates: Build / Repair / Pack / Remove -- ============================================================ MENU_BUILD_CRATES = "Build crates", MENU_REPAIR = "Repair", MENU_PACK_CRATES = "Pack crates", MENU_PACK = "Pack", MENU_SCAN_PACKABLE_UNITS = "Scan packable units nearby", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN = "No packable units found! Rescan?", MENU_PACK_ALL = "Pack nearby", MENU_PACK_AND_LOAD = "Pack and Load", MENU_PACK_AND_LOAD_ALL = "Pack and Load nearby", MENU_PACK_AND_REMOVE = "Pack and Remove", MENU_PACK_AND_REMOVE_ALL = "Pack and Remove nearby", MENU_REMOVE_CRATES = "Remove crates", MENU_REMOVE_CRATES_NEARBY = "Remove crates nearby", MENU_LIST_CRATES_NEARBY = "List crates nearby", MENU_CRATES_NEEDED = "%d crate%s %s (%dkg)", -- ============================================================ -- Menu labels - Units (C-130) -- ============================================================ MENU_GET_UNITS = "Get Units", MENU_REMOVE_UNITS_NEARBY = "Remove units nearby", -- ============================================================ -- Menu labels - Info / Cargo -- ============================================================ MENU_LIST_BOARDED_CARGO = "List boarded cargo", MENU_INVENTORY = "Inventory", MENU_LIST_ZONE_BEACONS = "List active zone beacons", -- ============================================================ -- Menu labels - Smokes / Flares / Beacons -- ============================================================ MENU_SMOKES_FLARES_BEACONS = "Smokes, Flares, Beacons", MENU_SMOKE_ZONES_NEARBY = "Smoke zones nearby", MENU_DROP_SMOKE_NOW = "Drop smoke now", MENU_RED_SMOKE = "Red smoke", MENU_BLUE_SMOKE = "Blue smoke", MENU_GREEN_SMOKE = "Green smoke", MENU_ORANGE_SMOKE = "Orange smoke", MENU_WHITE_SMOKE = "White smoke", MENU_FLARE_ZONES_NEARBY = "Flare zones nearby", MENU_FIRE_FLARE_NOW = "Fire flare now", MENU_DROP_BEACON_NOW = "Drop beacon now", -- ============================================================ -- Menu labels - Parameters -- ============================================================ MENU_SHOW_FLIGHT_PARAMS = "Show flight parameters", MENU_SHOW_HOVER_PARAMS = "Show hover parameters", STOCK_NONE = "none", STOCK_UNLIMITED = "unlimited", BUILD_YES = "YES", BUILD_NO = "NO", }, DE = { -- ============================================================ -- Kiste / Fracht laden -- ============================================================ CRATE_LOADED_GROUNDCREW = "Kiste %s vom Bodenpersonal geladen!", CRATE_UNLOADED_GROUNDCREW = "Kiste %s vom Bodenpersonal entladen!", CRATE_LOADED_ID = "Kiste ID %d für %s geladen!", LOADED_FULL = "%d %s geladen.", LOADED_SETS_LEFTOVER = "%d %s geladen, %d Kiste(n) übrig.", LOADED_SETS = "%d %s geladen.", LOADED_PARTIAL = "Nur %d/%d Kiste(n) von %s geladen.", LOADED_PARTIAL_LIMIT = "Nur %d/%d Kiste(n) von %s geladen. Frachtlimit erreicht!", LOADED_BATCH = "%d %s geladen.", LOADED_BATCH_PARTIAL = "Einige Sets konnten nicht vollständig geladen werden.", -- ============================================================ -- Abwerfen / Entladen -- ============================================================ DROPPED_FULL = "%d %s abgeworfen.", DROPPED_SETS_LEFTOVER = "%d %s abgeworfen, %d Kiste(n) übrig.", DROPPED_SETS = "%d %s abgeworfen.", DROPPED_PARTIAL = "%d/%d Kiste(n) von %s abgeworfen.", DROPPED_INTO_ACTION = "%s im Einsatz abgesetzt!", DROPPED_BEACON = "%s abgesetzt | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED = "%d Kisten für %s in Ihrer Nähe positioniert!", CRATES_DROPPED = "%d Kisten für %s abgeworfen!", -- ============================================================ -- Truppen -- ============================================================ BOARDED = "%s eingestiegen!", BOARDING = "%s steigt ein!", TROOPS_RETURNED = "Truppen zur Basis zurückgekehrt!", -- ============================================================ -- Einsatz -- ============================================================ DEPLOYED_NEAR_YOU = "%s in Ihrer Nähe eingesetzt!", UNITS_REMOVED = "%s entfernt", -- ============================================================ -- Bauen / Reparieren -- ============================================================ BUILD_STARTED = "Bau gestartet, fertig in %d Sekunden!", REPAIR_STARTED = "Reparatur mit %s gestartet, dauert %d Sek.", NO_UNIT_TO_REPAIR = "Keine Einheit in Reichweite zum Reparieren!", CANT_REPAIR_WITH = "Diese Einheit kann nicht mit %s repariert werden", CRATES_MOVE_BEFORE_BUILD = "*** Kisten müssen vor dem Bau verschoben werden!", -- ============================================================ -- Fehler - Hubschrauber / Gewicht / Kapazität -- ============================================================ CHOPPER_CANNOT_CARRY = "Dieser Hubschrauber kann keine Kisten transportieren!", TOO_HEAVY = "Entschuldigung, das ist zu schwer zum Laden!", FULLY_LOADED = "Entschuldigung, wir sind voll beladen!", CRAMMED = "Entschuldigung, wir sind bereits voll besetzt!", NO_CAPACITY_NOW = "Aktuell keine Ladekapazität mehr vorhanden!", NO_MORE_CAPACITY = "Keine Kapazität mehr für weitere Kisten!", CANNOT_LOAD_NONE_OR_FULL = "Laden nicht möglich: keine Kisten gefunden oder Kapazität erschöpft.", -- ============================================================ -- Fehler - Position -- ============================================================ NEED_TO_LAND_OR_HOVER_LOAD = "Bitte landen oder schweben Sie zum Laden!", HOVER_OVER_CRATES = "Schweben Sie über die Kisten, um sie aufzunehmen!", LAND_OR_HOVER_OVER_CRATES = "Landen oder schweben Sie über die Kisten, um sie aufzunehmen!", MUST_LAND_OR_HOVER_CRATES = "Sie müssen landen oder schweben, um Kisten zu laden!", NEED_TO_LAND_BUILD = "Sie müssen landen / anhalten, um etwas zu bauen, Pilot!", NOT_CLOSE_ENOUGH_LOGISTICS = "Sie sind nicht nah genug an einer Logistikzone!", NOT_CLOSE_ENOUGH_DROP = "Sie sind nicht nah genug an einer Abwurfzone!", NOT_CLOSE_ENOUGH_ZONE_NM = "Negativ, Sie müssen näher als %d Seemeilen an einer Zone sein!", CANNOT_BUILD_LOADING_AREA = "In einem Ladebereich kann nicht gebaut werden, Pilot!", -- ============================================================ -- Fehler - Türen -- ============================================================ OPEN_DOORS_LOAD_CARGO = "Bitte öffnen Sie die Tür(en) zum Laden von Fracht!", OPEN_DOORS_LOAD_TROOPS = "Bitte öffnen Sie die Tür(en) zum Einladen von Truppen!", OPEN_DOORS_EXTRACT_TROOPS = "Bitte öffnen Sie die Tür(en) zum Aussteigen der Truppen!", OPEN_DOORS_UNLOAD_TROOPS = "Bitte öffnen Sie die Tür(en) zum Entladen der Truppen!", OPEN_DOORS_DROP_CARGO = "Bitte öffnen Sie die Tür(en) zum Abwerfen der Fracht!", -- ============================================================ -- Fehler - Bestand / Verfügbarkeit -- ============================================================ ALL_GONE = "Entschuldigung, alle %s sind vergriffen!", RAN_OUT_OF = "Entschuldigung, %s ist nicht mehr vorrätig", CARGO_NOT_AVAILABLE_ZONE = "Die angeforderte Fracht ist in dieser Zone nicht verfügbar!", ENOUGH_CRATES_NEARBY = "Es sind bereits genügend Kisten in der Nähe! Bitte zuerst um diese kümmern!", NO_CRATES_WITHIN = "Keine (ladbaren) Kisten in %d Metern Umkreis!", NO_CRATES_WITHIN_PLAIN = "Keine Kisten in %d Metern Umkreis!", NO_CRATES_IN_RANGE = "Keine Kisten in Reichweite gefunden!", NO_NAMED_CRATES_IN_RANGE = "Keine \"%s\"-Kisten in Reichweite gefunden!", NO_LOADABLE_CRATES = "Entschuldigung, keine ladbaren Kisten in der Nähe oder maximales Frachtgewicht erreicht!", NO_UNITS_TO_EXTRACT = "Keine Einheiten nah genug zum Aussteigen!", NO_UNIT_CONFIG = "Keine Einheitenkonfiguration für %s gefunden", CANT_ONBOARD = "%s kann nicht eingeladen werden", TOO_MANY_UNITS_NEARBY = "Sie haben bereits %d Einheiten in der Nähe!", NO_CRATE_GROUPS = "Keine Kistengruppen für diese Einheit gefunden!", NO_CRATE_SET = "Kein Kistenset gefunden oder Index ungültig!", NO_CRATE_IN_SET = "Keine Kiste in diesem Set gefunden!", NO_TROOP_CHUNK = "Kein Truppenfracht-Block für ID %d gefunden!", TROOP_CHUNK_EMPTY = "Truppenfracht-Block für ID %d ist leer!", -- ============================================================ -- Nichts geladen / kein Bestand -- ============================================================ NOTHING_LOADED = "Nichts geladen!\nTruppenlimit: %d | Kistenlimit: %d | Gewichtslimit: %d kg", NOTHING_LOADED_AIRDROP = "Nichts geladen oder nicht innerhalb der Abwurfparameter!", NOTHING_LOADED_HOVER = "Nichts geladen oder Schwebeparameter nicht erfüllt!", NOTHING_IN_STOCK = "Nichts vorrätig!", NOTHING_TO_PACK = "Nichts in dieser Entfernung zum Verpacken, Pilot!", NOTHING_TO_REMOVE = "Nichts in dieser Entfernung zum Entfernen, Pilot!", -- ============================================================ -- Zone / Info -- ============================================================ ROGER_ZONE = "Verstanden, %s Zone %s!", -- ============================================================ -- Report: Schwebe- / Flugparameter -- ============================================================ HOVER_PARAMS_METRIC = "Schwebeparameter (Autoladen/Abwurf):\n - Min. Höhe %dm \n - Max. Höhe %dm \n - Max. Geschwindigkeit 2m/s \n - Im Parameter: %s", HOVER_PARAMS_IMPERIAL = "Schwebeparameter (Autoladen/Abwurf):\n - Min. Höhe %dft \n - Max. Höhe %dft \n - Max. Geschwindigkeit 6ft/s \n - Im Parameter: %s", FLIGHT_PARAMS_IMPERIAL = "Flugparameter (Luftabwurf):\n - Min. Höhe %dft \n - Max. Höhe %dft \n - Im Parameter: %s", FLIGHT_PARAMS_METRIC = "Flugparameter (Luftabwurf):\n - Min. Höhe %dm \n - Max. Höhe %dm \n - Im Parameter: %s", -- ============================================================ -- Report-Titel -- ============================================================ REPORT_CRATES_FOUND = "Kisten in der Nähe:", REPORT_REMOVING_CRATES = "Entferne Kisten in der Nähe:", REPORT_TRANSPORT_CHECKOUT = "Transport-Checkliste", REPORT_INVENTORY = "Inventarliste", REPORT_BUILD_CHECKLIST = "Checkliste baubare Kisten", REPORT_REPAIR_CHECKLIST = "Checkliste Reparaturen", REPORT_BEACONS = "Aktive Zonenfeuer", -- ============================================================ -- Report-Sektionskopfzeilen -- ============================================================ REPORT_SECTION_TROOPS = " -- TRUPPEN --", REPORT_SECTION_CRATES = " -- KISTEN --", REPORT_SECTION_CRATES_GC = " -- KISTEN via Bodenpersonal geladen --", REPORT_SECTION_NONE = " K E I N E", REPORT_SECTION_NONE_ALT = " --- Keine gefunden! ---", REPORT_SECTION_NONE_REPAIR = " --- Keine gefunden ---", REPORT_GC_LOADABLE_HINT = "Wahrscheinlich durch Bodenpersonal ladbar (F8)", REPORT_TOTAL_MASS = "Gesamtgewicht: %s kg. Ladbar: %s kg.", REPORT_TROOPS_CRATES_COUNT = "Truppen: %d(%d), Kisten: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT = "Truppen: %d, Kistentypen: %d", -- ============================================================ -- Report-Zeilenvorlagen -- ============================================================ REPORT_ROW_TROOP = "Truppe: %s Größe %d", REPORT_ROW_CRATE = "Kiste: %s %d/%d", REPORT_ROW_CRATE_SIZE1 = "Kiste: %s Größe 1", REPORT_ROW_GC_CRATE = "Bodenpersonal-Kiste: %s Größe 1", REPORT_ROW_DROPPED_CRATE = "Abgeworfene Kiste für %s, %dkg", REPORT_ROW_CRATE_KG = "Kiste für %s, %dkg", REPORT_ROW_CRATE_REMOVED = "Kiste für %s, %dkg entfernt", REPORT_ROW_UNIT_STOCK = "Einheit: %s | Soldaten: %d | Bestand: %s", REPORT_ROW_TYPE_CRATE_STOCK = "Typ: %s | Kisten pro Set: %d | Bestand: %s", REPORT_ROW_TYPE_STOCK = "Typ: %s | Bestand: %s", REPORT_ROW_BUILD_CHECK = "Typ: %s | Benötigt: %d | Gefunden: %d | Baubar: %s", REPORT_ROW_REPAIR_CHECK = "Typ: %s | Benötigt: %d | Gefunden: %d | Reparierbar: %s", REPORT_ROW_BEACON = " %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", -- ============================================================ -- Gewichts- / Kistenlimit-Token -- ============================================================ WEIGHT_LIMIT = "Gewichtslimit erreicht", CRATE_LIMIT = "Kistenlimit erreicht", -- ============================================================ -- Menübezeichnungen - Obere Ebene -- ============================================================ MENU_CTLD = "CTLD", MENU_MANAGE_TROOPS = "Truppen verwalten", MENU_MANAGE_CRATES = "Kisten verwalten", MENU_MANAGE_UNITS = "Einheiten verwalten", -- ============================================================ -- Menübezeichnungen - Truppen -- ============================================================ MENU_LOAD_TROOPS = "Truppen einladen", MENU_DROP_TROOPS = "Truppen absetzen", MENU_DROP_ALL_TROOPS = "ALLE Truppen absetzen", MENU_EXTRACT_TROOPS = "Truppen aufnehmen", MENU_DROP_N_TROOPS = "(%d) %s absetzen", -- ============================================================ -- Menübezeichnungen - Kisten: Holen -- ============================================================ MENU_GET_CRATES = "Kisten holen", MENU_GET = "Holen", MENU_GET_AND_LOAD = "Holen und laden", MENU_GET_ANYWAY = "Trotzdem holen", MENU_PARTIALLY_LOAD = "Teilweise laden", MENU_OUT_OF_STOCK = "Nicht vorrätig", MENU_TROOP_LIMIT = "Truppenlimit erreicht", -- ============================================================ -- Menübezeichnungen - Kisten: Laden -- ============================================================ MENU_LOAD_CRATES = "Kisten laden", MENU_LOAD_ALL = "ALLE laden", MENU_SHOW_LOADABLE_CRATES = "Ladbare Kisten anzeigen", MENU_NO_CRATES_FOUND_RESCAN = "Keine Kisten gefunden! Neu scannen?", MENU_USE_C130_LOAD = "C-130-Ladesystem verwenden", MENU_LOAD_SINGLE = "Lade", -- ============================================================ -- Menübezeichnungen - Kisten: Abwerfen -- ============================================================ MENU_DROP_CRATES = "Kisten abwerfen", MENU_DROP_ALL_CRATES = "ALLE Kisten abwerfen", MENU_DROP = "Abwerfen", MENU_DROP_AND_BUILD = "Abwerfen und bauen", MENU_DROP_N_SETS = "%d Set%s abwerfen", MENU_NO_CRATES_TO_DROP = "Keine Kisten zum Abwerfen!", -- ============================================================ -- Menübezeichnungen - Kisten: Bauen / Reparieren / Packen / Entfernen -- ============================================================ MENU_BUILD_CRATES = "Kisten bauen", MENU_REPAIR = "Reparieren", MENU_PACK_CRATES = "Kisten packen", MENU_PACK = "Packen", MENU_SCAN_PACKABLE_UNITS = "Packbare Einheiten in der Nähe scannen", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN = "Keine packbaren Einheiten gefunden! Neu scannen?", MENU_PACK_ALL = "In der Nähe packen", MENU_PACK_AND_LOAD = "Packen und laden", MENU_PACK_AND_LOAD_ALL = "In der Nähe packen und laden", MENU_PACK_AND_REMOVE = "Packen und entfernen", MENU_PACK_AND_REMOVE_ALL = "In der Nähe packen und entfernen", MENU_REMOVE_CRATES = "Kisten entfernen", MENU_REMOVE_CRATES_NEARBY = "Nahe Kisten entfernen", MENU_LIST_CRATES_NEARBY = "Nahe Kisten auflisten", MENU_CRATES_NEEDED = "%d Kiste%s %s (%dkg)", -- ============================================================ -- Menübezeichnungen - Einheiten (C-130) -- ============================================================ MENU_GET_UNITS = "Einheiten holen", MENU_REMOVE_UNITS_NEARBY = "Nahe Einheiten entfernen", -- ============================================================ -- Menübezeichnungen - Info / Fracht -- ============================================================ MENU_LIST_BOARDED_CARGO = "Geladene Fracht anzeigen", MENU_INVENTORY = "Inventar", MENU_LIST_ZONE_BEACONS = "Aktive Zonenfeuer anzeigen", -- ============================================================ -- Menübezeichnungen - Rauch / Leuchtfeuer / Baken -- ============================================================ MENU_SMOKES_FLARES_BEACONS = "Rauch, Leuchtfeuer, Baken", MENU_SMOKE_ZONES_NEARBY = "Nahe Zonen einrauchen", MENU_DROP_SMOKE_NOW = "Rauch jetzt setzen", MENU_RED_SMOKE = "Roter Rauch", MENU_BLUE_SMOKE = "Blauer Rauch", MENU_GREEN_SMOKE = "Grüner Rauch", MENU_ORANGE_SMOKE = "Oranger Rauch", MENU_WHITE_SMOKE = "Weißer Rauch", MENU_FLARE_ZONES_NEARBY = "Nahe Zonen befeuern", MENU_FIRE_FLARE_NOW = "Leuchtfeuer jetzt abfeuern", MENU_DROP_BEACON_NOW = "Bake jetzt setzen", -- ============================================================ -- Menübezeichnungen - Parameter -- ============================================================ MENU_SHOW_FLIGHT_PARAMS = "Flugparameter anzeigen", MENU_SHOW_HOVER_PARAMS = "Schwebeparameter anzeigen", STOCK_NONE = "keiner", STOCK_UNLIMITED = "unbegrenzt", BUILD_YES = "JA", BUILD_NO = "NEIN", }, FR = { --- ============================================================ -- Chargement caisse / fret -- ============================================================ CRATE_LOADED_GROUNDCREW = "Caisse(s) %s chargée(s) par l'équipe au sol !", CRATE_UNLOADED_GROUNDCREW = "Caisse(s) %s déchargée(s) par l'équipe au sol !", CRATE_LOADED_ID = "Caisse(s) ID %d pour %s chargée(s) !", LOADED_FULL = "%d %s chargé(s).", LOADED_SETS_LEFTOVER = "%d %s chargé(s), %d caisse(s) restante(s).", LOADED_SETS = "%d %s chargé(s).", LOADED_PARTIAL = "Seulement %d/%d caisse(s) de %s chargée(s).", LOADED_PARTIAL_LIMIT = "Seulement %d/%d caisse(s) de %s chargée(s). Limite de fret atteinte !", LOADED_BATCH = "%d %s chargé(s).", LOADED_BATCH_PARTIAL = "Certains ensembles n'ont pas pu être complètement chargés.", -- ============================================================ -- Largage / Déchargement -- ============================================================ DROPPED_FULL = "%d %s largué(s).", DROPPED_SETS_LEFTOVER = "%d %s largué(s), %d caisse(s) restante(s).", DROPPED_SETS = "%d %s largué(s).", DROPPED_PARTIAL = "%d/%d caisse(s) de %s larguée(s).", DROPPED_INTO_ACTION = "%s engagé(s) en action !", DROPPED_BEACON = "%s largué | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED = "%d caisses pour %s positionnées près de vous !", CRATES_DROPPED = "%d caisses pour %s larguées !", -- ============================================================ -- Troupes -- ============================================================ BOARDED = "%s embarqué(s) !", BOARDING = "%s en cours d'embarquement !", TROOPS_RETURNED = "Les troupes sont retournées à la base !", -- ============================================================ -- Déploiement -- ============================================================ DEPLOYED_NEAR_YOU = "%s déployé(s) près de vous !", UNITS_REMOVED = "%s supprimé(s)", -- ============================================================ -- Construction / Réparation -- ============================================================ BUILD_STARTED = "Construction démarrée, prête dans %d secondes !", REPAIR_STARTED = "Réparation démarrée avec %s, durée %d sec.", NO_UNIT_TO_REPAIR = "Aucune unité(s) assez proche pour être réparée(s) !", CANT_REPAIR_WITH = "Impossible de réparer cette unité avec %s", CRATES_MOVE_BEFORE_BUILD = "*** Les caisses doivent être déplacées avant la construction !", -- ============================================================ -- Erreurs - Hélicoptère / Poids / Capacité -- ============================================================ CHOPPER_CANNOT_CARRY = "Cet hélicoptère ne peut pas transporter de caisses !", TOO_HEAVY = "Désolé, c'est trop lourd à charger !", FULLY_LOADED = "Désolé, capacité maximale atteinte !", CRAMMED = "Désolé, nous sommes déjà au complet !", NO_CAPACITY_NOW = "Aucune capacité de chargement disponible pour le moment !", NO_MORE_CAPACITY = "Plus de capacité pour charger des caisses !", CANNOT_LOAD_NONE_OR_FULL = "Chargement impossible : aucune caisse trouvée ou capacité épuisée.", -- ============================================================ -- Erreurs - Position -- ============================================================ NEED_TO_LAND_OR_HOVER_LOAD = "Vous devez atterrir ou rester en vol stationnaire pour charger !", HOVER_OVER_CRATES = "Survolez les caisses en stationnaire pour les récupérer !", LAND_OR_HOVER_OVER_CRATES = "Atterrissez ou survolez les caisses en stationnaire pour les récupérer !", MUST_LAND_OR_HOVER_CRATES = "Vous devez atterrir ou rester en stationnaire pour charger les caisses !", NEED_TO_LAND_BUILD = "Vous devez atterrir / vous arrêter pour construire quelque chose, Pilote !", NOT_CLOSE_ENOUGH_LOGISTICS = "Vous n'êtes pas assez proche d'une zone logistique !", NOT_CLOSE_ENOUGH_DROP = "Vous n'êtes pas assez proche d'une zone de largage !", NOT_CLOSE_ENOUGH_ZONE_NM = "Négatif, vous devez être à moins de %d nm d'une zone !", CANNOT_BUILD_LOADING_AREA = "Vous ne pouvez pas construire dans une zone de chargement, Pilote !", -- ============================================================ -- Erreurs - Portes -- ============================================================ OPEN_DOORS_LOAD_CARGO = "Vous devez ouvrir la/les porte(s) pour charger du fret !", OPEN_DOORS_LOAD_TROOPS = "Vous devez ouvrir la/les porte(s) pour embarquer des troupes !", OPEN_DOORS_EXTRACT_TROOPS = "Vous devez ouvrir la/les porte(s) pour extraire des troupes !", OPEN_DOORS_UNLOAD_TROOPS = "Vous devez ouvrir la/les porte(s) pour débarquer des troupes !", OPEN_DOORS_DROP_CARGO = "Vous devez ouvrir la/les porte(s) pour larguer du fret !", -- ============================================================ -- Erreurs - Stock / Disponibilité -- ============================================================ ALL_GONE = "Désolé, tous les %s sont épuisés !", RAN_OUT_OF = "Désolé, nous n'avons plus de %s !", CARGO_NOT_AVAILABLE_ZONE = "Le fret demandé n'est pas disponible dans cette zone !", ENOUGH_CRATES_NEARBY = "Il y a déjà suffisamment de caisses à proximité ! Occupez-vous d'abord de celles-ci !", NO_CRATES_WITHIN = "Aucune caisse (chargeable) dans un rayon de %d mètres !", NO_CRATES_WITHIN_PLAIN = "Aucune caisse dans un rayon de %d mètres !", NO_CRATES_IN_RANGE = "Aucune caisse trouvée à portée !", NO_NAMED_CRATES_IN_RANGE = "Aucune caisse \"%s\" trouvée à portée !", NO_LOADABLE_CRATES = "Désolé, aucune caisse chargeable à proximité ou poids maximum atteint !", NO_UNITS_TO_EXTRACT = "Aucune unité assez proche pour être extraite !", NO_UNIT_CONFIG = "Aucune configuration d'unité trouvée pour %s", CANT_ONBOARD = "Impossible d'embarquer %s", TOO_MANY_UNITS_NEARBY = "Vous avez déjà %d unités à proximité !", NO_CRATE_GROUPS = "Aucun groupe de caisses trouvé pour cette unité !", NO_CRATE_SET = "Aucun ensemble de caisses trouvé ou index invalide !", NO_CRATE_IN_SET = "Aucune caisse trouvée dans cet ensemble !", NO_TROOP_CHUNK = "Aucun bloc de fret de troupes trouvé pour l'ID %d !", TROOP_CHUNK_EMPTY = "Le bloc de fret de troupes pour l'ID %d est vide !", -- ============================================================ -- Rien de chargé / en stock -- ============================================================ NOTHING_LOADED = "Rien de chargé !\nLimite de troupes : %d | Limite de caisses : %d | Limite en poids : %d kg", NOTHING_LOADED_AIRDROP = "Rien de chargé ou paramètres de largage non respectés !", NOTHING_LOADED_HOVER = "Rien de chargé ou paramètres de vol stationnaire non respectés !", NOTHING_IN_STOCK = "Rien en stock !", NOTHING_TO_PACK = "Rien à charger à cette distance, Pilote !", NOTHING_TO_REMOVE = "Rien à retirer à cette distance, Pilote !", -- ============================================================ -- Zone / Info -- ============================================================ ROGER_ZONE = "Compris, zone %s %s !", -- ============================================================ -- Rapport : Paramètres stationnaire / vol -- ============================================================ HOVER_PARAMS_METRIC = "Paramètres stationnaires (autochargement/largage) :\n - Hauteur min. %dm \n - Hauteur max. %dm \n - Vitesse max. 2m/s \n - Dans les paramètres : %s", HOVER_PARAMS_IMPERIAL = "Paramètres stationnaires (autochargement/largage) :\n - Hauteur min. %dft \n - Hauteur max. %dft \n - Vitesse max. 6ft/s \n - Dans les paramètres : %s", FLIGHT_PARAMS_IMPERIAL = "Paramètres de vol (largage aérien) :\n - Hauteur min. %dft \n - Hauteur max. %dft \n - Dans les paramètres : %s", FLIGHT_PARAMS_METRIC = "Paramètres de vol (largage aérien) :\n - Hauteur min. %dm \n - Hauteur max. %dm \n - Dans les paramètres : %s", -- ============================================================ -- Titres de rapport -- ============================================================ REPORT_CRATES_FOUND = "Caisses trouvées à proximité :", REPORT_REMOVING_CRATES = "Suppression des caisses à proximité :", REPORT_TRANSPORT_CHECKOUT = "Fiche de contrôle transport", REPORT_INVENTORY = "Fiche d'inventaire", REPORT_BUILD_CHECKLIST = "Checklist caisses constructibles", REPORT_REPAIR_CHECKLIST = "Checklist réparations", REPORT_BEACONS = "Balises de zone actives", -- ============================================================ -- En-têtes de sections de rapport -- ============================================================ REPORT_SECTION_TROOPS = " -- TROUPES --", REPORT_SECTION_CRATES = " -- CAISSES --", REPORT_SECTION_CRATES_GC = " -- CAISSES chargées via équipe au sol --", REPORT_SECTION_NONE = " A U C U N", REPORT_SECTION_NONE_ALT = " --- Aucun trouvé ! ---", REPORT_SECTION_NONE_REPAIR = " --- Aucun trouvé ---", REPORT_GC_LOADABLE_HINT = "Probablement chargeable via l’équipe au sol (F8)", REPORT_TOTAL_MASS = "Masse totale : %s kg. Chargeable : %s kg.", REPORT_TROOPS_CRATES_COUNT = "Troupes : %d(%d), Caisses : %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT = "Troupes : %d, Types de caisses : %d", -- ============================================================ -- Modèles de lignes de rapport -- ============================================================ REPORT_ROW_TROOP = "Troupe : %s taille %d", REPORT_ROW_CRATE = "Caisse : %s %d/%d", REPORT_ROW_CRATE_SIZE1 = "Caisse : %s taille 1", REPORT_ROW_GC_CRATE = "Caisses chargées par l'équipe au sol : %s taille 1", REPORT_ROW_DROPPED_CRATE = "Caisses larguées pour %s, %dkg", REPORT_ROW_CRATE_KG = "Caisses pour %s, %dkg", REPORT_ROW_CRATE_REMOVED = "Caisses pour %s, %dkg retirées", REPORT_ROW_UNIT_STOCK = "Unités : %s | Soldats : %d | Stock : %s", REPORT_ROW_TYPE_CRATE_STOCK = "Type : %s | Caisses par ensemble : %d | Stock : %s", REPORT_ROW_TYPE_STOCK = "Type : %s | Stock : %s", REPORT_ROW_BUILD_CHECK = "Type : %s | Requis : %d | Trouvé : %d | Constructible : %s", REPORT_ROW_REPAIR_CHECK = "Type : %s | Requis : %d | Trouvé : %d | Réparable : %s", REPORT_ROW_BEACON = " %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", -- ============================================================ -- Tokens limite poids / caisses -- ============================================================ WEIGHT_LIMIT = "Limite de poids atteinte", CRATE_LIMIT = "Limite de caisses atteinte", -- ============================================================ -- Libellés de menu - Niveau supérieur -- ============================================================ MENU_CTLD = "CTLD", MENU_MANAGE_TROOPS = "Gérer les troupes", MENU_MANAGE_CRATES = "Gérer les caisses", MENU_MANAGE_UNITS = "Gérer les unités", -- ============================================================ -- Libellés de menu - Troupes -- ============================================================ MENU_LOAD_TROOPS = "Embarquer troupes", MENU_DROP_TROOPS = "Déposer troupes", MENU_DROP_ALL_TROOPS = "Déposer TOUTES les troupes", MENU_EXTRACT_TROOPS = "Extraire troupes", MENU_DROP_N_TROOPS = "Déposer (%d) %s", -- ============================================================ -- Libellés de menu - Caisses : Récupérer -- ============================================================ MENU_GET_CRATES = "Récupérer caisses", MENU_GET = "Récupérer", MENU_GET_AND_LOAD = "Récupérer et charger", MENU_GET_ANYWAY = "Récupérer quand même", MENU_PARTIALLY_LOAD = "Chargement partiel", MENU_OUT_OF_STOCK = "Rupture de stock", MENU_TROOP_LIMIT = "Limite de troupes atteinte", -- ============================================================ -- Libellés de menu - Caisses : Charger -- ============================================================ MENU_LOAD_CRATES = "Charger caisses", MENU_LOAD_ALL = "Tout charger", MENU_SHOW_LOADABLE_CRATES = "Afficher caisses chargeables", MENU_NO_CRATES_FOUND_RESCAN = "Aucune caisse trouvée ! Rescanner ?", MENU_USE_C130_LOAD = "Utiliser le système de chargement C-130", MENU_LOAD_SINGLE = "Charger", -- ============================================================ -- Libellés de menu - Caisses : Larguer -- ============================================================ MENU_DROP_CRATES = "Larguer caisses", MENU_DROP_ALL_CRATES = "Larguer TOUTES les caisses", MENU_DROP = "Larguer", MENU_DROP_AND_BUILD = "Larguer et construire", MENU_DROP_N_SETS = "Larguer %d ensemble%s", MENU_NO_CRATES_TO_DROP = "Aucune caisse à larguer !", -- ============================================================ -- Libellés de menu - Caisses : Construire / Réparer / Emballer / Retirer -- ============================================================ MENU_BUILD_CRATES = "Construire caisses", MENU_REPAIR = "Réparer", MENU_PACK_CRATES = "Emballer caisses", MENU_PACK = "Emballer", MENU_SCAN_PACKABLE_UNITS = "Scanner unités emballables à proximité", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN = "Aucune unité emballable trouvée ! Rescanner ?", MENU_PACK_ALL = "Emballer à proximité", MENU_PACK_AND_LOAD = "Emballer et charger", MENU_PACK_AND_LOAD_ALL = "Emballer et charger à proximité", MENU_PACK_AND_REMOVE = "Emballer et retirer", MENU_PACK_AND_REMOVE_ALL = "Emballer et retirer à proximité", MENU_REMOVE_CRATES = "Retirer caisses", MENU_REMOVE_CRATES_NEARBY = "Retirer caisses proches", MENU_LIST_CRATES_NEARBY = "Lister caisses proches", MENU_CRATES_NEEDED = "%d caisse%s %s (%dkg)", -- ============================================================ -- Libellés de menu - Unités (C-130) -- ============================================================ MENU_GET_UNITS = "Récupérer unités", MENU_REMOVE_UNITS_NEARBY = "Retirer les unités proches", -- ============================================================ -- Libellés de menu - Info / Fret -- ============================================================ MENU_LIST_BOARDED_CARGO = "Lister le fret embarqué", MENU_INVENTORY = "Inventaire", MENU_LIST_ZONE_BEACONS = "Lister les balises de zones actives", -- ============================================================ -- Libellés de menu - Fumigènes / Fusées / Balises -- ============================================================ MENU_SMOKES_FLARES_BEACONS = "Fumigènes, Fusées, Balises", MENU_SMOKE_ZONES_NEARBY = "Fumigène sur les zones proches", MENU_DROP_SMOKE_NOW = "Poser fumigène maintenant", MENU_RED_SMOKE = "Fumigène rouge", MENU_BLUE_SMOKE = "Fumigène bleu", MENU_GREEN_SMOKE = "Fumigène vert", MENU_ORANGE_SMOKE = "Fumigène orange", MENU_WHITE_SMOKE = "Fumigène blanc", MENU_FLARE_ZONES_NEARBY = "Baliser zones proches", MENU_FIRE_FLARE_NOW = "Tirer une fusée maintenant", MENU_DROP_BEACON_NOW = "Poser une balise maintenant", -- ============================================================ -- Libellés de menu - Paramètres -- ============================================================ MENU_SHOW_FLIGHT_PARAMS = "Afficher paramètres de vol", MENU_SHOW_HOVER_PARAMS = "Afficher les paramètres stationnaire", STOCK_NONE = "aucun", STOCK_UNLIMITED = "illimité", BUILD_YES = "OUI", BUILD_NO = "NON", }, ES={ CRATE_LOADED_GROUNDCREW="Contenedor %s cargado por el quipo de tierra.", CRATE_UNLOADED_GROUNDCREW="Contenedor %s descargado por el quipo de tierra.", CRATE_LOADED_ID="Contenedor ID %d para %s cargado.", LOADED_FULL="Cargado %d %s.", LOADED_SETS_LEFTOVER="Cargado %d %s(s), de %d contenedor(es) restante(s).", LOADED_SETS="Cargado %d %s(s).", LOADED_PARTIAL="Cargado sólo %d/%d contenedor(es) de %s.", LOADED_PARTIAL_LIMIT="Cargado sólo %d/%d contenedor(es) de %s. Límite de carga alcanzado.", LOADED_BATCH="Cargado %d %s.", LOADED_BATCH_PARTIAL="Some sets could not be fully loaded.", DROPPED_FULL="Entregado %d %s.", DROPPED_SETS_LEFTOVER="Entregado %d %s(s), de %d conenedor(es) restante(s).", DROPPED_SETS="Entregado %d %s(s).", DROPPED_PARTIAL="Entregado %d/%d contenedor(es) de %s.", DROPPED_INTO_ACTION="¡Soltados %s en acción!", DROPPED_BEACON="Entregado %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d contenedores para %s servidos cerca de ti.", CRATES_DROPPED="%d contenedores para %s han sido entregados.", BOARDED="¡%s a bordo!", BOARDING="¡%s entrando!", TROOPS_RETURNED="¡Las tropas han vuelto a la base!", DEPLOYED_NEAR_YOU="%s han sido servidas cerca de tí.", UNITS_REMOVED="%s ha sido eliminado", BUILD_STARTED="Construcción comenzada, listo en %d segundos.", REPAIR_STARTED="Reparación comenzaza usando %s, tardará %d segundos.", NO_UNIT_TO_REPAIR="No hay unidades cercanas que necesiten reparación.", CANT_REPAIR_WITH="No se puede reparar esta unidad con %s", CRATES_MOVE_BEFORE_BUILD="*** Los contenedores deben ser movidos antes de construirse.", CHOPPER_CANNOT_CARRY="Lo siento, este helicóptero no puede transportar contenedores.", TOO_HEAVY="Lo siento, esta carga es muy pesada.", FULLY_LOADED="Lo siento, vamos hasta arriba.", CRAMMED="Lo siento, vamos a tope.", NO_CAPACITY_NOW="No queda capacidad para cargar más.", NO_MORE_CAPACITY="No queda capacidad para cargar más contenedores.", CANNOT_LOAD_NONE_OR_FULL="No se pueden cargar contenedores: n hay o no queda capacidad.", NEED_TO_LAND_OR_HOVER_LOAD="Necesitas aterrizar o mantenerte en estacionario para cargar.", HOVER_OVER_CRATES="Mantente en estacionario sobre el contenedor para recogerlo.", LAND_OR_HOVER_OVER_CRATES="Aterriza o mantente en estacionario para recoger el contenedor.", MUST_LAND_OR_HOVER_CRATES="Necesitas aterrizar o mantenerte en eestacionario para cargar contenedores.", NEED_TO_LAND_BUILD="Necesitas aterrizar/parar para construir algo.", NOT_CLOSE_ENOUGH_LOGISTICS="No estás cerca de una zona de logística.", NOT_CLOSE_ENOUGH_DROP="No estás en una zona de entrega.", NOT_CLOSE_ENOUGH_ZONE_NM="Negativo, tienes que estar a menos de %dnm de la zona.", CANNOT_BUILD_LOADING_AREA="No puedes construir en la zona de carga.", OPEN_DOORS_LOAD_CARGO="Necesitas abrir las puertas para cargar.", OPEN_DOORS_LOAD_TROOPS="Necesitas abrir las puertas para que embarquen las tropas.", OPEN_DOORS_EXTRACT_TROOPS="Necesitas abrir las puertas para poder sacar a las tropas de aquí.", OPEN_DOORS_UNLOAD_TROOPS="Necesitas abrir las puertas para que desembarquen las tropas.", OPEN_DOORS_DROP_CARGO="Necesitas abrir las puertas para descargar la carga.", ALL_GONE="Lo siento, todos %s se han servido.", RAN_OUT_OF="Lo siento, nos hemos quedad sin %s", CARGO_NOT_AVAILABLE_ZONE="La carga solicitada no está disponible en esta zona.", ENOUGH_CRATES_NEARBY="Hay contenedores cerca de ti listas. Encárgate primero de ellos.", NO_CRATES_WITHIN="No ha contenedores (cargables) en %d metros.", NO_CRATES_WITHIN_PLAIN="No hay contenedores en %d metros.", NO_CRATES_IN_RANGE="No se han encontrado contenedores en rango.", NO_NAMED_CRATES_IN_RANGE="No se han encontrado \"%s\" conenedores en rango.", NO_LOADABLE_CRATES="Lo siento, no hay contenedores cercanos o se ha alcanzado el peso máximo.", NO_UNITS_TO_EXTRACT="No hay unidades cercanas para extracción.", NO_UNIT_CONFIG="No se ha encontrado configuración de unidad para %s", CANT_ONBOARD="No puede subir %s", TOO_MANY_UNITS_NEARBY="Ya tienes %d unidades próximas.", NO_CRATE_GROUPS="No se han encontrado grupos de contendeores para esta unidad.", NO_CRATE_SET="No se ha encontrado contenedor o su index es inválido.", NO_CRATE_IN_SET="No se ha encontrado contenedor para este set.", NO_TROOP_CHUNK="No se han encontrado tropas para el id %d!", TROOP_CHUNK_EMPTY="Troop chunk is empty for ID %d!", NOTHING_LOADED="Nada cargado.\nLímite tropas: %d | Límite contenedores: %d | Peso límite: %d kg.", NOTHING_LOADED_AIRDROP="Nada cargado o no estás en parámetros de lanzamiento aéreo.", NOTHING_LOADED_HOVER="Nada cargado o no estás en parámetros de estacionario.", NOTHING_IN_STOCK="¡Nada en stock!", NOTHING_TO_PACK="Nada para empaquetar a esta distancia.", NOTHING_TO_REMOVE="Nada para eliminar a esta distancia.", ROGER_ZONE="Recibido, %s cona %s!", HOVER_PARAMS_METRIC="Parámetros en estacionario (autocarga/suelta):\n - Altura mínima %dm \n - Altura máxima %dm \n - Velocidad máxima 2mps \n - En parámetros: %s", HOVER_PARAMS_IMPERIAL="Parámetros en estacionario (autocarga/suelta):\n - Altura mínima %dft \n - Altura máxima %dft \n - Velocidad máxima 6ftps \n - En parámetros: %s", FLIGHT_PARAMS_IMPERIAL="Parámetros vuelo (lanzamiento aéreo):\n - Altura mínima %dft \n - Altura máxima %dft \n - En parámetros: %s", FLIGHT_PARAMS_METRIC="Parámetros vuelo (lanzamiento aéreo):\n - Altura mínima %dm \n - Altura máxima %dm \n - En parámetros: %s", REPORT_CRATES_FOUND="Contenedores encontrados cerca:", REPORT_REMOVING_CRATES="Contenedores eliminados cerca:", REPORT_TRANSPORT_CHECKOUT="Informe de transporte", REPORT_INVENTORY="Inventario", REPORT_BUILD_CHECKLIST="Contenedores construibles", REPORT_REPAIR_CHECKLIST="Reparaciones", REPORT_BEACONS="Active Zone Beacons", REPORT_SECTION_TROOPS=" -- TROPAS --", REPORT_SECTION_CRATES=" -- CONTENEDORES --", REPORT_SECTION_CRATES_GC=" -- Contenedores cargados por equipo de tierra --", REPORT_SECTION_NONE=" N A D A", REPORT_SECTION_NONE_ALT=" --- Nada encontrado ---", REPORT_SECTION_NONE_REPAIR=" --- Nada encontrado ---", REPORT_GC_LOADABLE_HINT="Probablemente cargable por el equipo de tierra (F8)", REPORT_TOTAL_MASS="Peso total: %s kg. Cargable: %s kg.", REPORT_TROOPS_CRATES_COUNT="Tropas: %d(%d), Contenedores: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Tropas: %d, Tipos contenedores: %d", REPORT_ROW_TROOP="Tropas: %s tamaño %d", REPORT_ROW_CRATE="Contenedores: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Contenedores: %s tamaño 1", REPORT_ROW_GC_CRATE="Contenedores cargados: %s tamaño 1", REPORT_ROW_DROPPED_CRATE="Entregado contenedor para %s, %dkg", REPORT_ROW_CRATE_KG="Contenedor para %s, %dkg", REPORT_ROW_CRATE_REMOVED="Contenedor para %s, %dkg eliminado", REPORT_ROW_UNIT_STOCK="Unidad: %s | Soldados: %d | Stock: %s", REPORT_ROW_TYPE_CRATE_STOCK="Tipo: %s | Contenedores por set: %d | Stock: %s", REPORT_ROW_TYPE_STOCK="Tipo: %s | Stock: %s", REPORT_ROW_BUILD_CHECK="Tipo: %s | Rquiere %d | Encontrados %d | Construible %s", REPORT_ROW_REPAIR_CHECK="Tipo: %s | Requiere %d | Encontrados %d | Reparable %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Alcanzado límite de pesoWeight limit reached", CRATE_LIMIT="Alcanzado límite contenedores", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Gestionar tropas", MENU_MANAGE_CRATES="Gestionar contenedores", MENU_MANAGE_UNITS="Gestionar unidades", MENU_LOAD_TROOPS="Cargar tropas", MENU_DROP_TROOPS="Entregar tropas", MENU_DROP_ALL_TROOPS="Entregar TODAS tropas", MENU_EXTRACT_TROOPS="Extraer tropas", MENU_DROP_N_TROOPS="Soltar (%d) %s", MENU_GET_CRATES="Solicitar contenedores", MENU_GET="Solicitar", MENU_GET_AND_LOAD="Solicitar y cargar", MENU_GET_ANYWAY="Solicitar de todas formas", MENU_PARTIALLY_LOAD="Carga parcial", MENU_OUT_OF_STOCK="Sin stock", MENU_TROOP_LIMIT="Limite de tropas alcanzado", MENU_LOAD_CRATES="Cargar contenedores", MENU_LOAD_ALL="Cargar TODO", MENU_SHOW_LOADABLE_CRATES="Mostrar contenedores carbables", MENU_NO_CRATES_FOUND_RESCAN="Contenedores no encontrados, ¿buscar?", MENU_USE_C130_LOAD="Usar sistmea de carga del C-130", MENU_LOAD_SINGLE="Cargar", MENU_DROP_CRATES="Soltar cargas", MENU_DROP_ALL_CRATES="Soltar TODAS cargas", MENU_DROP="Soltar", MENU_DROP_AND_BUILD="Soltar y constuir", MENU_DROP_N_SETS="Soltar %d Set %s", MENU_NO_CRATES_TO_DROP="No hay cargas para soltar", MENU_BUILD_CRATES="Construir contenedores", MENU_REPAIR="Reparar", MENU_PACK_CRATES="Empaquetar cargas", MENU_PACK="Empaquetar", MENU_SCAN_PACKABLE_UNITS="Buscar unidades empaquetables cercanas", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="No se encontraron unidades empaquetables. ¿Buscar de nuevo?", MENU_PACK_ALL="Empaquetar cercanas", MENU_PACK_AND_LOAD="Empaquetar y cargar", MENU_PACK_AND_LOAD_ALL="Empaquetar y cargar cercanas", MENU_PACK_AND_REMOVE="Empaquetar y eliminar", MENU_PACK_AND_REMOVE_ALL="Empaquetar y eliminar cercanas", MENU_REMOVE_CRATES="Eliminar cargas", MENU_REMOVE_CRATES_NEARBY="Eliminar cargas cercanas", MENU_LIST_CRATES_NEARBY="Listar cargas cercanas", MENU_CRATES_NEEDED="%d contenedor%s %s (%dkg)", MENU_GET_UNITS="Obtener unidades", MENU_REMOVE_UNITS_NEARBY="Eliminar unidades cercanas", MENU_LIST_BOARDED_CARGO="Lista de cargas a bordo", MENU_INVENTORY="Inventario", MENU_LIST_ZONE_BEACONS="Lista de balizas activas", MENU_SMOKES_FLARES_BEACONS="Humos, Bengalas, Balizas", MENU_SMOKE_ZONES_NEARBY="Humo en zonas cercanas", MENU_DROP_SMOKE_NOW="Lanzar humo ahora", MENU_RED_SMOKE="Humo rojo", MENU_BLUE_SMOKE="Humo azul", MENU_GREEN_SMOKE="Humo verde", MENU_ORANGE_SMOKE="Humo naranja", MENU_WHITE_SMOKE="Humo blanco", MENU_FLARE_ZONES_NEARBY="Bengalas en zonas cercanas", MENU_FIRE_FLARE_NOW="Disparar bengala ahora", MENU_DROP_BEACON_NOW="Soltar baliza ahora", MENU_SHOW_FLIGHT_PARAMS="Mostrar parámetros de vuelo", MENU_SHOW_HOVER_PARAMS="Mostrar parámetros estacionario", STOCK_NONE="Nada", STOCK_UNLIMITED="ilimitado", BUILD_YES="SI", BUILD_NO="NO", }, } --- .../Moose/Ops/CTLD_Localization.lua | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CTLD_Localization.lua b/Moose Development/Moose/Ops/CTLD_Localization.lua index b6ef7f455..4138b7ee8 100644 --- a/Moose Development/Moose/Ops/CTLD_Localization.lua +++ b/Moose Development/Moose/Ops/CTLD_Localization.lua @@ -210,8 +210,13 @@ CTLD.Messages = { MENU_REPAIR = "Repair", MENU_PACK_CRATES = "Pack crates", MENU_PACK = "Pack", + MENU_SCAN_PACKABLE_UNITS = "Scan packable units nearby", + MENU_NO_PACKABLE_UNITS_FOUND_RESCAN = "No packable units found! Rescan?", + MENU_PACK_ALL = "Pack nearby", MENU_PACK_AND_LOAD = "Pack and Load", + MENU_PACK_AND_LOAD_ALL = "Pack and Load nearby", MENU_PACK_AND_REMOVE = "Pack and Remove", + MENU_PACK_AND_REMOVE_ALL = "Pack and Remove nearby", MENU_REMOVE_CRATES = "Remove crates", MENU_REMOVE_CRATES_NEARBY = "Remove crates nearby", MENU_LIST_CRATES_NEARBY = "List crates nearby", @@ -460,8 +465,13 @@ CTLD.Messages = { MENU_REPAIR = "Reparieren", MENU_PACK_CRATES = "Kisten packen", MENU_PACK = "Packen", + MENU_SCAN_PACKABLE_UNITS = "Packbare Einheiten in der Nähe scannen", + MENU_NO_PACKABLE_UNITS_FOUND_RESCAN = "Keine packbaren Einheiten gefunden! Neu scannen?", + MENU_PACK_ALL = "In der Nähe packen", MENU_PACK_AND_LOAD = "Packen und laden", + MENU_PACK_AND_LOAD_ALL = "In der Nähe packen und laden", MENU_PACK_AND_REMOVE = "Packen und entfernen", + MENU_PACK_AND_REMOVE_ALL = "In der Nähe packen und entfernen", MENU_REMOVE_CRATES = "Kisten entfernen", MENU_REMOVE_CRATES_NEARBY = "Nahe Kisten entfernen", MENU_LIST_CRATES_NEARBY = "Nahe Kisten auflisten", @@ -710,8 +720,13 @@ FR = { MENU_REPAIR = "Réparer", MENU_PACK_CRATES = "Emballer caisses", MENU_PACK = "Emballer", + MENU_SCAN_PACKABLE_UNITS = "Scanner unités emballables à proximité", + MENU_NO_PACKABLE_UNITS_FOUND_RESCAN = "Aucune unité emballable trouvée ! Rescanner ?", + MENU_PACK_ALL = "Emballer à proximité", MENU_PACK_AND_LOAD = "Emballer et charger", + MENU_PACK_AND_LOAD_ALL = "Emballer et charger à proximité", MENU_PACK_AND_REMOVE = "Emballer et retirer", + MENU_PACK_AND_REMOVE_ALL = "Emballer et retirer à proximité", MENU_REMOVE_CRATES = "Retirer caisses", MENU_REMOVE_CRATES_NEARBY = "Retirer caisses proches", MENU_LIST_CRATES_NEARBY = "Lister caisses proches", @@ -894,8 +909,13 @@ FR = { MENU_REPAIR="Reparar", MENU_PACK_CRATES="Empaquetar cargas", MENU_PACK="Empaquetar", + MENU_SCAN_PACKABLE_UNITS="Buscar unidades empaquetables cercanas", + MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="No se encontraron unidades empaquetables. ¿Buscar de nuevo?", + MENU_PACK_ALL="Empaquetar cercanas", MENU_PACK_AND_LOAD="Empaquetar y cargar", + MENU_PACK_AND_LOAD_ALL="Empaquetar y cargar cercanas", MENU_PACK_AND_REMOVE="Empaquetar y eliminar", + MENU_PACK_AND_REMOVE_ALL="Empaquetar y eliminar cercanas", MENU_REMOVE_CRATES="Eliminar cargas", MENU_REMOVE_CRATES_NEARBY="Eliminar cargas cercanas", MENU_LIST_CRATES_NEARBY="Listar cargas cercanas", @@ -924,4 +944,4 @@ FR = { BUILD_NO="NO", }, } - \ No newline at end of file +