@@ -1,760 +0,0 @@
--- **Functional** - Enhanced Warsaw Pact GCI Controller.
--
-- ## Main Features:
--
-- * Guide AI and human pilots in Warsaw Pact Style. GCI Kernel Functions.
-- * Advanced Tactics for Groups.
-- * Many additional events that the mission designer can hook into.
--
-- ===
--
-- ### Author: **Applevangelist**
--
-- ===
-- @module Functional.REDGCI_KERNEL
-- @image Func_RedGCI.png
-- @version 1.0.0
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- REDGCI_KERNEL class
-- @type REDGCI_KERNEL
---
-- # RedGCI — Soviet GCI Doctrine & Player Guide
--
-- ## Philosophy: Централизованное управление (Centralized Control)
--
-- The fundamental difference between Soviet and NATO GCI is **who makes the tactical decision**.
--
-- In NATO doctrine, the GCI controller provides situational awareness — bearing, range, altitude, aspect — and the pilot decides how to prosecute the intercept. The pilot is an autonomous tactician. GCI is an advisor.
--
-- In Soviet doctrine, the GCI controller **directs**. The pilot executes. The controller selects the intercept geometry, assigns the heading, manages the radar, calls weapons free, and coordinates multi-ship tactics. The pilot's job is to fly the numbers and shoot when told. This is not a flaw — it is the system working as designed. Soviet fighter pilots were trained to be precise executors of GCI instructions, not independent tacticians. The ground radar network (PVO) was the brain; the aircraft was the weapon.
--
-- RedGCI models this philosophy faithfully.
--
-- ---
--
-- ## What to Expect as a Player
--
-- ### You will not be asked what you want to do.
--
-- There are no "recommend a vector" calls, no "at your discretion" callouts. The controller tells you your heading, your altitude, and your task. Your acknowledgement is assumed.
--
-- ### The controller manages your radar.
--
-- You do not decide when to turn your radar on. The GCI will tell you when to switch on (`локатор` / `Radar on`). Before that call, you fly cold and silent. This preserves your emissions discipline and prevents the target from getting an early RWR spike.
--
-- ### Weapons free is a controlled event.
--
-- You do not engage until the controller clears you (`цель разрешена` / `WEAPONS FREE`). The controller determines when geometry, range, and aspect are favorable. Shooting early breaks the coordinated intercept and may compromise your wingman's attack.
--
-- ### Radio calls are short and military.
--
-- Soviet GCI brevity is terse by design. Expect calls like:
--
-- - `"Сокол, курс 170, высота 4500."` — vector, altitude
-- - `"Сокол, цель, пара, истребитель. Локатор."` — picture call on commit: count, type, radar on
-- - `"Сокол, захват. Дальность 20. Цель разрешена."` — lock confirmed, range, weapons free
-- - `"Сокол, молодец. Домой."` — good kill, RTB
--
-- There are no "BOGEY DOPE" requests, no "BRAA" calls, no "DECLARE" queries. The controller has already done that work. You fly the vector.
--
-- ---
--
-- ## State Flow — What the GCI is Doing Behind the Scenes
--
-- RedGCI manages a state machine that progresses through six phases. Understanding these phases helps you anticipate what call is coming next.
--
-- ```
-- VECTOR → COMMIT → RADAR_CONTACT → VISUAL → MERGE → (SPLASH / ABORT / RTB)
-- ```
--
-- ### VECTOR
-- The controller has a track. You are being vectored onto an intercept geometry. Your radar is off. The controller is solving a collision course and updating your heading every tick. Altitude calls reflect the intercept geometry — you may be sent below the target (classic Soviet shoot-up doctrine for radar-limited types) or level/above (MiG-29/Su-27 lookdown geometry). Expect heading updates every 10– 15 seconds.
--
-- **What you should do:** Fly the heading. Don't deviate. Don't turn your radar on yet. Speed is expected at 900kph TAS (depending on airframe)
--
-- ### COMMIT
-- Range has closed to approximately 30km. The controller calls the picture: count and type. Your radar comes on. You are now committed to the intercept — turning away is no longer the default option. The controller is building your radar geometry toward a lock.
--
-- **What you should do:** Activate your radar. Acquire the target. Do not fire yet.
--
-- ### RADAR_CONTACT
-- You have radar lock (or the AI has achieved it). The controller confirms lock and calls range. If geometry and range are favorable, weapons free follows immediately. If not — for example if aspect angle is unfavorable for a stern conversion — the controller holds fire and waits for better geometry.
--
-- **What you should do:** Maintain lock. Track the target. Wait for the weapons free call.
--
-- ### VISUAL
-- Range has closed to approximately 5km — visual conditions. Weapons free is automatic at this point. You are now in the merge envelope.
--
-- **What you should do:** Engage.
--
-- ### MERGE
-- Inside 2km. The GCI transitions to merge control: bearing to target, overshoot calls, separation instructions, reattack vectors. At this range the controller cannot see fine-grained geometry — merge calls are based on relative bearing and closure.
--
-- **What you should do:** Fight. Listen for overshoot, separation, and reattack calls.
--
-- ### SPLASH / ABORT / RTB
-- - `SPLASH` — kill confirmed, RTB
-- - `ABORT (THREAT)` — your RWR is spiked or a threat geometry has developed; break off immediately on the given heading
-- - `ABORT (BINGO)` — fuel state critical; break off and return
--
-- ---
--
-- ## Multi-Ship (2v2) Tactics (REDGCI2v2)
--
-- When two fighters are dispatched against a threat, the GCI selects a tactic automatically based on the tactical situation. The tactic is applied at COMMIT — until then, both fighters are vectored together toward the intercept midpoint.
--
-- | Tactic | Description |
-- |--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
-- | **PINCER** | Classic bracket. F1 and F2 split left and right, attacking from opposite angles simultaneously. Forces the target to choose which threat to react to. |
-- | **HIGH-LOW** | Vertical split. One fighter attacks from below (radar up, clean sky background), one from above. Degrades the target's ability to acquire both simultaneously. |
-- | **STAGGER** | BVR timing offset. F1 fires first at long range, F2 follows 8– 13km behind to engage a maneuvering or defending target. |
-- | **TRAIL** | Close trail. F1 is the shooter, F2 is support — ready to engage if F1 overshoots or is defeated. |
-- | **GIRAFFE** | *(Historical — Iraq/Iran War, Mirage F1 vs F-14A)* F1 attacks at normal altitude, binding the AWG-9 radar. F2 flies nap-of-earth |
-- | | (300– 600m AGL) using ground clutter to degrade radar detection, then pulls up and fires from close range. |
--
-- During a tactic split, you may receive a heading that seems unusual — a large lateral offset or an unexpected altitude change. **Trust the vector.** The controller is positioning you for the tactic geometry. The merge point will bring you back onto the target.
--
-- ---
--
-- ## Dispatcher & CAP Flow (REDGCI_DISPATCHER)
--
-- When using the dispatcher layer, the full operational flow is:
--
-- ```
-- Spawn at homeplate
-- → Taxi and takeoff (template-controlled)
-- → Transit to CAP zone
-- → Orbit in assigned zone (radar cold, weapons safe)
-- ↓ INTEL detects threat cluster
-- → "Attention, radar contact. Pair, fighter, 45 kilometers." (all CAP fighters)
-- ↓ Dispatcher assigns pair
-- → "101 102, intercept. Pair, fighter." (dispatched pair)
-- → VECTOR → COMMIT → RADAR_CONTACT → VISUAL → MERGE → SPLASH
-- ↓ Engagement complete
-- → AI: RTB waypoint → land → despawn → respawn after delay
-- → Human: "101, mission complete. RTB, refuel and rearm."
-- ↓ After RespawnDelay
-- → New AI pair spawns into same CAP zone
-- ```
--
-- Human players are dispatched first when available. If a human and AI are both in the CAP pool, the human is always assigned to the next intercept. AI fills gaps. The dispatcher does not send a single fighter if a pair is available — pairing is always preferred.
--
-- ---
--
-- ## Key Differences from NATO GCI at a Glance
--
-- | | Soviet (RedGCI) | NATO |
-- |--------------------|--------------------------------------|------------------------------------|
-- | Tactical decision | Controller | Pilot |
-- | Radar management | Controller-commanded | Pilot-initiated |
-- | Weapons free | Controller-called | Pilot-discretion (after WF) |
-- | Heading calls | Prescriptive | Advisory |
-- | Brevity style | Terse, military, positional | Standardized (BRAA, DECLARE, etc.) |
-- | Multi-ship tactics | Centrally planned, applied at COMMIT | Mutually briefed, pilot-executed |
-- | Pilot autonomy | Low (by design) | High |
--
-- **The Soviet system is not inferior** — it is optimized for a different kind of pilot and a different operational context. Mass interception of large NATO strike packages over defended Soviet airspace demanded centralized, efficient, high-throughput GCI control. RedGCI brings that experience to DCS.
--
-- @field #REDGCI_KERNEL
REDGCI_KERNEL = { }
REDGCI_KERNEL.version = " 1.0.0 "
-- ─────────────────────────────────────────────────────────────
-- Konstanten (aus gci_types.h)
-- ─────────────────────────────────────────────────────────────
REDGCI_KERNEL.C = {
RANGE_VECTOR_START = 60000.0 ,
RANGE_COMMIT = 30000.0 ,
RANGE_RADAR_FLOOR = 20000.0 ,
RANGE_VISUAL = 5000.0 ,
RANGE_MERGE = 2000.0 ,
WF_RANGE_MAX = 25000.0 ,
ALT_OFFSET_LOOKDOWN = 0.0 ,
ASPECT_NOTCH_MIN = 80.0 ,
ASPECT_NOTCH_MAX = 100.0 ,
ASPECT_REAR_ATTACK = 120.0 ,
MAX_TTI = 600.0 ,
TICK_INTERVAL = 15.0 ,
FUEL_BINGO = 0.25 ,
DELAY_MIN = 3.0 ,
DELAY_MAX = 8.0 ,
DELAY_MERGE_MIN = 2.0 ,
DELAY_MERGE_MAX = 4.0 ,
TACTIC_PINCER = 0 ,
TACTIC_HIGH_LOW = 1 ,
TACTIC_STAGGER = 2 ,
TACTIC_TRAIL = 3 ,
TACTIC_GIRAFFE = 4 , -- Irak/Iran-Doktrin: F1=Decoy normal, F2=Nap-of-Earth im Groundclutter
PURSUIT_COLLISION = 0 ,
PURSUIT_LEAD = 1 ,
PURSUIT_PURE = 2 ,
PURSUIT_NO_SOLUTION = 3 ,
}
-- ─────────────────────────────────────────────────────────────
-- Hilfsfunktionen
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . clamp ( v , lo , hi )
if v < lo then return lo end
if v > hi then return hi end
return v
end
function REDGCI_KERNEL . vec2len ( x , z )
return math.sqrt ( x * x + z * z )
end
function REDGCI_KERNEL . bearing ( dx , dz )
local b = math.deg ( math.atan2 ( dx , dz ) )
return b < 0.0 and b + 360.0 or b
end
function REDGCI_KERNEL . randDelay ( lo , hi )
return lo + UTILS.LCGRandom ( ) * ( hi - lo )
end
-- ─────────────────────────────────────────────────────────────
-- Aspect Angle (0=Nose-on, 90=Beam, 180=Tail)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . aspectAngle ( target , observer )
local dx = observer.x - target.x
local dz = observer.z - target.z
local range = REDGCI_KERNEL.vec2len ( dx , dz )
if range < 1.0 then return 0.0 end
local nx = dx / range
local nz = dz / range
local spd = ( target.speed or 1.0 ) + 1e-6
local tvx = ( target.vx or 0.0 ) / spd
local tvz = ( target.vz or 0.0 ) / spd
local dot = REDGCI_KERNEL.clamp ( tvx * nx + tvz * nz , - 1.0 , 1.0 )
return math.deg ( math.acos ( dot ) )
end
-- ─────────────────────────────────────────────────────────────
-- Closure Rate (positiv = Annäherung)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . closureRate ( f , t )
local dx = t.x - f.x
local dz = t.z - f.z
local range = REDGCI_KERNEL.vec2len ( dx , dz )
if range < 1.0 then return 0.0 end
local nx = dx / range
local nz = dz / range
local dvx = ( t.vx or 0.0 ) - ( f.vx or 0.0 )
local dvz = ( t.vz or 0.0 ) - ( f.vz or 0.0 )
return - ( dvx * nx + dvz * nz )
end
-- ─────────────────────────────────────────────────────────────
-- _solveCollision (intern, GCI-Koordinaten)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . _solveCollision ( f , t )
local C = REDGCI_KERNEL.C
local dx = t.x - f.x
local dz = t.z - f.z
local vtx = t.vx or 0.0
local vtz = t.vz or 0.0
local vf = f.speed or 1.0
local a = vtx * vtx + vtz * vtz - vf * vf
local b = 2.0 * ( dx * vtx + dz * vtz )
local c = dx * dx + dz * dz
local sol_t = - 1.0
if math.abs ( a ) < 1.0 then
if math.abs ( b ) > 0.01 then
sol_t = - c / b
else
return false
end
else
local disc = b * b - 4.0 * a * c
if disc < 0.0 then return false end
local sq = math.sqrt ( disc )
local t1 = ( - b - sq ) / ( 2.0 * a )
local t2 = ( - b + sq ) / ( 2.0 * a )
if t1 > 0.0 and t2 > 0.0 then sol_t = math.min ( t1 , t2 )
elseif t1 > 0.0 then sol_t = t1
elseif t2 > 0.0 then sol_t = t2
else return false
end
end
if sol_t < 0.0 or sol_t > C.MAX_TTI then return false end
local ip = {
x = t.x + vtx * sol_t ,
z = t.z + vtz * sol_t ,
y = math.max ( t.y + C.ALT_OFFSET_LOOKDOWN , 300.0 ) ,
}
local hdg = REDGCI_KERNEL.bearing ( ip.x - f.x , ip.z - f.z )
return true , hdg , sol_t , ip
end
-- ─────────────────────────────────────────────────────────────
-- _solveLead (intern, GCI-Koordinaten)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . _solveLead ( f , t )
local C = REDGCI_KERNEL.C
local dx = t.x - f.x
local dz = t.z - f.z
local range = REDGCI_KERNEL.vec2len ( dx , dz )
if range < 1.0 then return 0.0 , 0.0 end
local base_bearing = REDGCI_KERNEL.bearing ( dx , dz )
local aspect_rad = math.rad ( REDGCI_KERNEL.aspectAngle ( t , f ) )
local speed_ratio = ( t.speed or 1.0 ) / ( ( f.speed or 1.0 ) + 1e-6 )
local sin_lead = REDGCI_KERNEL.clamp ( speed_ratio * math.sin ( aspect_rad ) , - 1.0 , 1.0 )
local lead_deg = math.deg ( math.asin ( sin_lead ) )
local hdg
if math.abs ( lead_deg ) < 45.0 then
hdg = ( base_bearing + lead_deg + 360.0 ) % 360.0
else
hdg = base_bearing
end
local closing = REDGCI_KERNEL.closureRate ( f , t )
if closing < 50.0 then closing = 50.0 end
local tti = math.min ( range / closing , C.MAX_TTI )
return hdg , tti
end
-- ─────────────────────────────────────────────────────────────
-- computeIntercept (GCI-Koordinaten intern)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . computeIntercept ( fighter , target )
local C = REDGCI_KERNEL.C
local dx = target.x - fighter.x
local dz = target.z - fighter.z
local sol = {
heading_deg = 0.0 ,
time_to_intercept = C.MAX_TTI ,
intercept_point = { x = target.x , z = target.z , y = target.y } ,
target_alt = target.y ,
solution_found = false ,
aspect_angle = REDGCI_KERNEL.aspectAngle ( target , fighter ) ,
range = REDGCI_KERNEL.vec2len ( dx , dz ) ,
mode = C.PURSUIT_NO_SOLUTION ,
weapons_free = false ,
}
local closure = REDGCI_KERNEL.closureRate ( fighter , target )
local projected_range = sol.range - closure * C.TICK_INTERVAL
sol.weapons_free = ( sol.range < C.WF_RANGE_MAX ) or
( closure > 0.0 and projected_range < C.WF_RANGE_MAX )
local ok , hdg , tti , ip = REDGCI_KERNEL._solveCollision ( fighter , target )
if ok then
sol.heading_deg = hdg
sol.time_to_intercept = tti
sol.intercept_point = ip
sol.solution_found = true
sol.mode = C.PURSUIT_COLLISION
return sol
end
local lhdg , ltti = REDGCI_KERNEL._solveLead ( fighter , target )
sol.heading_deg = lhdg
sol.time_to_intercept = ltti
sol.solution_found = true
sol.mode = C.PURSUIT_LEAD
sol.intercept_point = {
x = target.x + ( target.vx or 0.0 ) * ltti ,
z = target.z + ( target.vz or 0.0 ) * ltti ,
y = math.max ( target.y + C.ALT_OFFSET_LOOKDOWN , 300.0 ) ,
}
return sol
end
-- ─────────────────────────────────────────────────────────────
-- computeInterceptDCS (DCS-Koordinaten Ein/Aus)
-- Flat-Rückgabe kompatibel mit alter C-API
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . computeInterceptDCS ( f , t )
local f_gci = { x = f.z , z = f.x , y = f.y , vx = f.vz , vz = f.vx , vy = f.vy , speed = f.spd }
local t_gci = { x = t.z , z = t.x , y = t.y , vx = t.vz , vz = t.vx , vy = t.vy , speed = t.spd }
local sol = REDGCI_KERNEL.computeIntercept ( f_gci , t_gci )
-- GCI→DCS: ip.z(Nord)→DCS.x, ip.x(Ost)→DCS.z
return sol.heading_deg ,
sol.time_to_intercept ,
sol.mode ,
sol.weapons_free ,
sol.range ,
sol.aspect_angle ,
sol.intercept_point . z ,
sol.intercept_point . x ,
sol.intercept_point . y ,
sol.target_alt
end
-- ─────────────────────────────────────────────────────────────
-- computeSplit (GCI-Koordinaten intern)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . computeSplit ( f1 , f2 , tgt , tactic , variation )
local C = REDGCI_KERNEL.C
variation = REDGCI_KERNEL.clamp ( variation or 0.5 , 0.0 , 1.0 )
tactic = tactic or C.TACTIC_PINCER
local mid_x = ( f1.x + f2.x ) * 0.5
local mid_z = ( f1.z + f2.z ) * 0.5
local dx = tgt.x - mid_x
local dz = tgt.z - mid_z
local rng = REDGCI_KERNEL.vec2len ( dx , dz )
if rng < 1.0 then rng = 1.0 end
local ax = dx / rng
local az = dz / rng
local px = - az
local pz = ax
local plan = {
tactic = tactic ,
wp_f1 = { x = 0 , z = 0 , y = 0 } ,
wp_f2 = { x = 0 , z = 0 , y = 0 } ,
merge_f1 = { x = 0 , z = 0 , y = 0 } ,
merge_f2 = { x = 0 , z = 0 , y = 0 } ,
}
local function clamp_alt ( pt )
if pt.y < 300.0 then pt.y = 300.0 end
end
local function clamp_alt_noe ( pt )
if pt.y < 150.0 then pt.y = 150.0 end
end
-- GIRAFFE F2 fliegt NOE — separater clamp mit niedrigerer Mindesthöhe
local function clamp_alt_noe ( pt )
if pt.y < 150.0 then pt.y = 150.0 end
end
if tactic == C.TACTIC_PINCER then
local spread = 12000.0 + variation * 5000.0
local approach = rng * 0.45
local merge_off = spread * 0.30
plan.wp_f1 = { x = mid_x + ax * approach + px * spread , z = mid_z + az * approach + pz * spread , y = tgt.y }
plan.wp_f2 = { x = mid_x + ax * approach - px * spread , z = mid_z + az * approach - pz * spread , y = tgt.y }
plan.merge_f1 = { x = tgt.x - ax * 3000 + px * merge_off , z = tgt.z - az * 3000 + pz * merge_off , y = tgt.y }
plan.merge_f2 = { x = tgt.x - ax * 3000 - px * merge_off , z = tgt.z - az * 3000 - pz * merge_off , y = tgt.y }
elseif tactic == C.TACTIC_HIGH_LOW then
local vert = 3000.0 + variation * 1500.0
local approach = rng * 0.50
local side_off = 2000.0
plan.wp_f1 = { x = mid_x + ax * approach + px * side_off , z = mid_z + az * approach + pz * side_off , y = tgt.y - 500.0 }
plan.wp_f2 = { x = mid_x + ax * approach - px * side_off , z = mid_z + az * approach - pz * side_off , y = tgt.y + vert }
plan.merge_f1 = { x = tgt.x - ax * 3000 + px * side_off , z = tgt.z - az * 3000 + pz * side_off , y = plan.wp_f1 . y }
plan.merge_f2 = { x = tgt.x - ax * 3000 - px * side_off , z = tgt.z - az * 3000 - pz * side_off , y = plan.wp_f2 . y }
elseif tactic == C.TACTIC_STAGGER then
local lag = 8000.0 + variation * 3000.0
local lead_dist = rng * 0.85
plan.wp_f1 = { x = mid_x + ax * lead_dist , z = mid_z + az * lead_dist , y = tgt.y }
plan.wp_f2 = { x = mid_x + ax * ( lead_dist - lag ) , z = mid_z + az * ( lead_dist - lag ) , y = tgt.y }
plan.merge_f1 = { x = plan.wp_f1 . x , z = plan.wp_f1 . z , y = plan.wp_f1 . y }
plan.merge_f2 = { x = plan.wp_f2 . x , z = plan.wp_f2 . z , y = plan.wp_f2 . y }
elseif tactic == C.TACTIC_TRAIL then
local lag = 3000.0 + variation * 2000.0
local lead_dist = rng * 0.85
local side_off = 500.0
plan.wp_f1 = { x = mid_x + ax * lead_dist , z = mid_z + az * lead_dist , y = tgt.y }
plan.wp_f2 = { x = mid_x + ax * ( lead_dist - lag ) + px * side_off , z = mid_z + az * ( lead_dist - lag ) + pz * side_off , y = tgt.y }
plan.merge_f1 = { x = plan.wp_f1 . x , z = plan.wp_f1 . z , y = plan.wp_f1 . y }
plan.merge_f2 = { x = plan.wp_f2 . x , z = plan.wp_f2 . z , y = plan.wp_f2 . y }
elseif tactic == C.TACTIC_GIRAFFE then
-- ── GIRAFFE: Irak/Iran-Doktrin (Mirage F1 vs F-14A) ──────────────
--
-- F1 = Decoy: normale Höhe, direkter Anflug, bindet AWG-9 Radar.
-- Zieht Phoenixe auf sich — gibt F2 Zeit für Pull-up.
--
-- F2 = Killer: Nap-of-Earth (~300-600m AGL/MSL) weit seitlich versetzt.
-- Nutzt Groundclutter um AWG-9 Look-Down zu degradieren.
-- Nähert sich bis 10km vor Ziel, dann Pull-up und Schuss.
-- Seitlicher Versatz ~8-12km damit F2 nicht im Radarsektor
-- des AWG-9 liegt während F1 den Lock hält.
--
-- Historisch: Iran-Irak Krieg 1982-88. Irakische Mirage F1EQ nutzten
-- dieses Profil gegen F-14A/AIM-54 Phoenix — der AWG-9 hatte trotz
-- Look-Down-Fähigkeit bei sehr tiefen Zielen über unebenen Terrain
-- (Zagros-Berge, Khuzestan-Ebene) erhöhte Clutter-Probleme.
--
-- Höhen: neutral (0.0) — Lua addiert AltOffset + terrain-awareness.
-- Mindesthöhe 300m MSL wird am Ende geclampt.
local noe_alt = 300.0 + variation * 300.0 -- 300-600m MSL (Nap-of-Earth)
local side_off = 8000.0 + variation * 4000.0 -- 8-12km seitlich (aus AWG-9 Sektor)
local approach = rng * 0.60 -- F1 direkter Anflug, 60% des Weges
local noe_dist = rng * 0.75 -- F2 nähert sich weiter ran vor Pull-up
plan.wp_f1 = { x = mid_x + ax * approach + px * 2000 , z = mid_z + az * approach + pz * 2000 , y = tgt.y }
plan.merge_f1 = { x = tgt.x - ax * 5000 + px * 2000 , z = tgt.z - az * 5000 + pz * 2000 , y = tgt.y }
plan.wp_f2 = { x = mid_x + ax * noe_dist - px * side_off , z = mid_z + az * noe_dist - pz * side_off , y = noe_alt , absolute_alt = true }
plan.merge_f2 = { x = tgt.x - ax * 10000 - px * side_off * 0.3 , z = tgt.z - az * 10000 - pz * side_off * 0.3 , y = tgt.y }
else
plan.wp_f1 = { x = tgt.x , z = tgt.z , y = tgt.y }
plan.wp_f2 = { x = tgt.x , z = tgt.z , y = tgt.y }
plan.merge_f1 = { x = tgt.x , z = tgt.z , y = tgt.y }
plan.merge_f2 = { x = tgt.x , z = tgt.z , y = tgt.y }
end
clamp_alt ( plan.wp_f1 )
if tactic == C.TACTIC_GIRAFFE then
clamp_alt_noe ( plan.wp_f2 )
else
clamp_alt ( plan.wp_f2 )
end
clamp_alt ( plan.merge_f1 )
clamp_alt ( plan.merge_f2 )
return plan
end
-- ─────────────────────────────────────────────────────────────
-- computeSplitDCS (DCS-Koordinaten Ein/Aus)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . computeSplitDCS ( f1 , f2 , tgt , tactic , variation )
local function dcs2gci ( u )
return { x = u.z , z = u.x , y = u.y , vx = u.vz or 0 , vz = u.vx or 0 , speed = u.spd or 0 }
end
local function gci2dcs ( pt )
return { x = pt.z , z = pt.x , y = pt.y , absolute_alt = pt.absolute_alt }
end
local tgt_gci = { x = tgt.z , z = tgt.x , y = tgt.y }
local plan = REDGCI_KERNEL.computeSplit (
dcs2gci ( f1 ) , dcs2gci ( f2 ) , tgt_gci , tactic , variation )
plan.wp_f1 = gci2dcs ( plan.wp_f1 )
plan.wp_f2 = gci2dcs ( plan.wp_f2 )
plan.merge_f1 = gci2dcs ( plan.merge_f1 )
plan.merge_f2 = gci2dcs ( plan.merge_f2 )
return plan
end
-- ─────────────────────────────────────────────────────────────
-- buildTransmission (Port von gci_build_transmission)
--
-- ctx/prev Felder: state, prev_state, ticks_in_state, range,
-- aspect_angle, closure_rate, altitude_delta, fuel_fraction
-- sol Felder: heading_deg, time_to_intercept, target_alt,
-- range, aspect_angle, weapons_free
-- Rückgabe: { token_str, delay_sec, weapons_free, silence }
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . buildTransmission ( ctx , prev , sol )
local C = REDGCI_KERNEL.C
local tx = {
token_str = " " ,
delay_sec = REDGCI_KERNEL.clamp (
REDGCI_KERNEL.randDelay ( C.DELAY_MIN , C.DELAY_MAX ) , 3.0 , 8.0 ) ,
weapons_free = sol.weapons_free ,
silence = false ,
priority = 50 ,
}
local hdg_i = math.floor ( ( sol.heading_deg or 0 ) + 0.5 )
local alt_i = math.floor ( ( sol.target_alt or 0 ) / 100.0 + 0.5 ) * 100
local rng_km = math.floor ( ( ctx.range or 0 ) / 1000.0 + 0.5 )
local aspect_i = math.floor ( ( ctx.aspect_angle or 0 ) + 0.5 )
local delay = tx.delay_sec
local state = ctx.state or " VECTOR "
local prev_state = prev.state or " VECTOR "
local ticks = ctx.ticks_in_state or 0
local function emit ( fmt , ... )
tx.token_str = string.format ( fmt , ... )
end
if state == " VECTOR " then
if prev_state ~= " VECTOR " then
local tti_m = math.floor ( ( sol.time_to_intercept or 0 ) / 60.0 )
local tti_s = math.floor ( sol.time_to_intercept or 0 ) % 60
if tti_m > 0 then
emit ( " VECTOR_WITH_TTI|hdg=%d|alt=%d|rng=%d|tti_m=%d|tti_s=%d|delay=%.1f " ,
hdg_i , alt_i , rng_km , tti_m , tti_s , delay )
else
emit ( " VECTOR|hdg=%d|alt=%d|rng=%d|delay=%.1f " ,
hdg_i , alt_i , rng_km , delay )
end
else
local hdg_delta = math.abs ( ( sol.heading_deg or 0 ) - ( ctx.aspect_angle or 0 ) )
if hdg_delta < 5.0 and ticks > 3 then
tx.silence = true
else
emit ( " VECTOR|hdg=%d|alt=%d|rng=%d|delay=%.1f " ,
hdg_i , alt_i , rng_km , delay )
end
end
elseif state == " COMMIT " then
if prev_state ~= " COMMIT " then
tx.priority = 100
emit ( " COMMIT_FIRST|hdg=%d|alt=%d|rng=%d|aspect=%d|delay=%.1f " ,
hdg_i , alt_i , rng_km , aspect_i , delay )
elseif ticks == 6 then
emit ( " COMMIT_NO_LOCK|hdg=%d|rng=%d|aspect=%d|delay=%.1f " ,
hdg_i , rng_km , aspect_i , delay )
elseif ticks > 8 then
emit ( " COMMIT_NUDGE|hdg=%d|aspect=%d|delay=%.1f " ,
hdg_i , aspect_i , delay )
else
tx.silence = true
end
elseif state == " RADAR_CONTACT " then
if prev_state ~= " RADAR_CONTACT " then
if sol.weapons_free then
tx.priority = 100
emit ( " RADAR_LOCK_WF|rng=%d|delay=%.1f " , rng_km , delay )
tx.weapons_free = true
else
emit ( " RADAR_LOCK_HOLD|rng=%d|delay=%.1f " , rng_km , delay )
end
elseif not sol.weapons_free
and ( ctx.range or 0 ) < C.WF_RANGE_MAX
and ( ctx.aspect_angle or 0 ) > C.ASPECT_REAR_ATTACK
and prev_state == " RADAR_CONTACT " then
emit ( " RADAR_WF_NOW|rng=%d|delay=%.1f " , rng_km , delay )
tx.priority = 100
tx.weapons_free = true
else
tx.silence = true
end
elseif state == " VISUAL " then
if prev_state ~= " VISUAL " then
emit ( " VISUAL_CONFIRM|rng=%d|delay=%.1f " , rng_km , delay )
tx.weapons_free = true
else
tx.silence = true
end
elseif state == " NOTCH " then
if prev_state ~= " NOTCH " then
emit ( " NOTCH_ENTRY|delay=1.5 " )
elseif ticks % 8 == 0 then
emit ( " NOTCH_UPDATE|rng=%d|aspect=%d|delay=%.1f " ,
rng_km , aspect_i , delay )
else
tx.silence = true
end
elseif state == " ABORT " then
tx.delay_sec = 1.5
if ( ctx.fuel_fraction or 1.0 ) < C.FUEL_BINGO then
emit ( " ABORT_BINGO|hdg=%d|delay=1.5 " , hdg_i )
else
emit ( " ABORT_THREAT|hdg=%d|delay=1.5 " , hdg_i )
end
else
tx.silence = true
end
return tx
end
-- ─────────────────────────────────────────────────────────────
-- mergeTransition (Port von gci_merge_transition)
--
-- ctx Felder: phase, ticks_in_phase, range, bearing_to_target,
-- closure_rate, altitude_delta, pass_count, radar_lost
-- Rückgabe: phase string
-- "ENTRY"|"OVERSHOOT"|"SEPARATION"|"REATTACK"|"LOST"|"SPLASH"
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . mergeTransition ( ctx , prev )
if ctx.radar_lost then return " LOST " end
if ( ctx.closure_rate or 0 ) < - 30.0 and ( ctx.range or 0 ) > 3000.0 then
return " SEPARATION "
end
local brg = ctx.bearing_to_target or 0
if brg > 100.0 and brg < 260.0 and ( ctx.range or 0 ) < 5000.0 then
return " OVERSHOOT "
end
if ctx.phase == " SEPARATION " and ( ctx.pass_count or 0 ) < 3 then
return " REATTACK "
end
return ctx.phase
end
-- ─────────────────────────────────────────────────────────────
-- buildMergeTransmission (Port von gci_build_merge_transmission)
-- ─────────────────────────────────────────────────────────────
function REDGCI_KERNEL . buildMergeTransmission ( ctx , prev )
local C = REDGCI_KERNEL.C
local tx = {
token_str = " " ,
delay_sec = REDGCI_KERNEL.clamp (
REDGCI_KERNEL.randDelay ( C.DELAY_MERGE_MIN , C.DELAY_MERGE_MAX ) , 2.0 , 5.0 ) ,
weapons_free = false ,
silence = false ,
priority = 50 ,
}
local brg = math.floor ( ( ctx.bearing_to_target or 0 ) + 0.5 )
local rng_km = math.floor ( ( ctx.range or 0 ) / 1000.0 + 0.5 )
local delay = tx.delay_sec
local phase = ctx.phase or " ENTRY "
local prev_phase = prev.phase or " ENTRY "
local ticks = ctx.ticks_in_phase or 0
local dir_rl = ( ( ctx.bearing_to_target or 0 ) < 180.0 ) and " right " or " left "
local alt_d = ctx.altitude_delta or 0
local alt_rel = " "
if alt_d > 400.0 then alt_rel = " low "
elseif alt_d < - 400.0 then alt_rel = " high "
end
local function emit ( fmt , ... )
tx.token_str = string.format ( fmt , ... )
end
if phase == " ENTRY " then
tx.priority = 100
emit ( " MERGE_ENTRY|brg=%d|dir_rl=%s|delay=%.1f " , brg , dir_rl , delay )
elseif phase == " OVERSHOOT " then
emit ( " MERGE_OVERSHOOT|brg=%d|dir_rl=%s|alt_rel=%s|delay=%.1f " ,
brg , dir_rl , alt_rel , delay )
elseif phase == " SEPARATION " then
if ( ctx.pass_count or 0 ) < 3 then
emit ( " MERGE_REATTACK|brg=%d|rng=%d|delay=%.1f " , brg , rng_km , delay )
else
emit ( " ABORT_THREAT|hdg=%d|delay=%.1f " , brg , delay )
end
elseif phase == " REATTACK " then
if prev_phase ~= " REATTACK " or ticks % 3 == 0 then
emit ( " MERGE_REATTACK|brg=%d|rng=%d|delay=%.1f " , brg , rng_km , delay )
else
tx.silence = true
end
elseif phase == " LOST " then
if prev_phase ~= " LOST " or ticks == 4 then
emit ( " MERGE_LOST|brg=%d|rng=%d|delay=%.1f " , brg , rng_km , delay )
else
tx.silence = true
end
elseif phase == " SPLASH " then
tx.delay_sec = 1.5
tx.priority = 100
emit ( " MERGE_SPLASH|delay=1.5 " )
else
tx.silence = true
end
return tx
end
-------------------------------------------------------------------------------
-- END of Class
-------------------------------------------------------------------------------