mirror of
https://github.com/iTracerFacer/Moose_DualCoalitionZoneCapture.git
synced 2026-07-16 14:32:31 +00:00
3750 lines
183 KiB
Lua
3750 lines
183 KiB
Lua
---@diagnostic disable: undefined-field, undefined-global
|
|
---@diagnostic disable: undefined-global, lowercase-global
|
|
-- WWII Pilot Intuition System using Moose Framework
|
|
--
|
|
-- Description:
|
|
-- This script simulates a WWII-era pilot intuition system for DCS World missions. It enhances immersion by providing realistic reconnaissance capabilities,
|
|
-- alerting pilots to nearby air and ground threats through voice-like messages and optional markers, mimicking how pilots spotted targets in WWII without modern aids.
|
|
--
|
|
-- Purpose:
|
|
-- To bridge the gap between modern DCS gameplay and historical WWII aviation, where situational awareness relied on visual scanning, formation flying, and intuition.
|
|
-- It helps players maintain awareness of their surroundings in dense or chaotic environments, promoting better tactics and immersion.
|
|
--
|
|
-- Use Case:
|
|
-- Ideal for WWII-themed missions, dogfights, or ground attack scenarios. Players can fly without relying on radar or labels, using the system to spot bandits,
|
|
-- ground units, and receive formation feedback. It's particularly useful in multiplayer servers for coordinated flights or solo play for enhanced challenge.
|
|
|
|
-- Features:
|
|
-- - **Dynamic Detection Ranges**: Detection ranges automatically adjust based on formation flying and environmental conditions. Flying in tight formations with wingmen significantly boosts spotting distances for both air and ground targets. For example, a solo pilot might detect air targets at 5km, but in a 2-ship formation, this increases to 10km, and in a 4-ship formation to 15km. Environmental factors like night reduce ranges (e.g., 50% at night), while bad weather can further decrease visibility. This simulates historical WWII reconnaissance where formation integrity was crucial for situational awareness.
|
|
-- - Line of Sight checks to ensure targets are only reported when visible.
|
|
-- - Realistic messaging simulating pilot radio calls for spotting bandits and ground threats.
|
|
-- - Formation integrity monitoring with alerts for wingmen presence.
|
|
-- - Dogfight assistance with alerts for merging bandits, tail warnings, head-on threats, and more.
|
|
-- - Optional visual markers (smoke, flares) for spotted targets, with selectable colors.
|
|
-- - Independent toggles for air target scanning, both globally (mission-wide) and per-player.
|
|
-- - On-demand ground target scanning with selective marking: scan nearby targets, review list, and choose which one to mark.
|
|
-- - Configurable settings via F10 menu for players to customize their experience.
|
|
-- - Scheduled summaries for players preferring less frequent updates.
|
|
-- - Per Player customization of dogfight assistance, marker types, and scanning preferences and message frequency.
|
|
-- - Per Player Multi Language Support: English, German, French, Spanish, Russian.
|
|
|
|
--
|
|
-- Setup Instructions:
|
|
-- 1. Ensure the Moose framework is installed and loaded in your DCS mission (download from https://flightcontrol-master.github.io/MOOSE_DOCS/).
|
|
-- 2. Add this script to your mission via the DCS Mission Editor: Go to Triggers > Mission Start > Do Script File, and select this Lua file.
|
|
-- 3. Optionally, customize the PILOT_INTUITION_CONFIG table below to adjust ranges, multipliers, and behaviors to fit your mission.
|
|
-- 4. The system initializes automatically on mission start. Players will see a welcome message and can access settings via the F10 menu.
|
|
-- 5. For best results, test in a mission with active AI units or other players to verify detection ranges and messaging.
|
|
|
|
-- Logging system (0=NONE, 1=ERROR, 2=INFO, 3=DEBUG, 4=TRACE)
|
|
PILOT_INTUITION_LOG_LEVEL = 1 -- Default to INFO level
|
|
|
|
local function PILog(level, message)
|
|
if level <= PILOT_INTUITION_LOG_LEVEL then
|
|
env.info(message)
|
|
end
|
|
end
|
|
|
|
-- Log level constants
|
|
local LOG_NONE = 0
|
|
local LOG_ERROR = 1
|
|
local LOG_INFO = 2
|
|
local LOG_DEBUG = 3
|
|
local LOG_TRACE = 4
|
|
|
|
-- Global configuration table for settings
|
|
PILOT_INTUITION_CONFIG = {
|
|
airDetectionRange = 8000, -- Meters (base range for air targets) (can be modified by formation and environment)
|
|
groundDetectionRange = 5000, -- Meters (base range for ground targets) (can be modified by formation and environment)
|
|
messageCooldown = 10, -- Seconds between messages
|
|
markerType = "smoke_red", -- Options: "smoke_red", "smoke_green", "smoke_blue", "smoke_white", "flare_red", "flare_green", "flare_white", "none"
|
|
markerDuration = 300, -- Seconds for marker visibility (dcs default can't be changed, so this is just for reference)
|
|
threatHotRange = 1000, -- Meters for "hot" threat
|
|
threatColdRange = 5000, -- Meters for "cold" threat
|
|
scanInterval = 5, -- Seconds between scans
|
|
mergeRange = 500, -- Meters for merge detection
|
|
highMergeAltitude = 500, -- Meters altitude difference for "high merge"
|
|
lowMergeAltitude = 500, -- Meters altitude difference for "low merge"
|
|
tailWarningRange = 1000, -- Meters for tail warnings
|
|
headOnRange = 1000, -- Meters for head-on detection
|
|
beamRange = 1500, -- Meters for beam aspect detection
|
|
separatingRange = 2000, -- Meters - if opening past this after merge, call "separating"
|
|
multipleBanditsRange = 2000, -- Meters for multiple bandits detection
|
|
multipleBanditsWarningCooldown = 300, -- Seconds between "Multiple bandits in vicinity!" warnings (5 minutes)
|
|
maxThreatDisplay = 3, -- Maximum number of threats to display in multi-bandit tactical picture (most threatening first)
|
|
combatIntensityThreshold = 3, -- Number of bandits to trigger "high intensity" mode (increases cooldowns)
|
|
combatIntensityCooldownMultiplier = 1.5, -- Multiply dogfight assist cooldown by this during high intensity combat
|
|
suppressFormationInCombat = true, -- Suppress formation join/leave messages when engaged with bandits
|
|
formationRange = 1000, -- Meters for considering players in formation
|
|
minFormationWingmen = 1, -- Minimum wingmen for formation integrity warnings
|
|
formationMessageCooldown = 60, -- Seconds cooldown for formation join/leave messages (prevents spam)
|
|
nightDetectionMultiplier = 0.5, -- Detection range multiplier at night
|
|
badWeatherMultiplier = 0.7, -- Detection range multiplier in bad weather
|
|
dogfightAssistEnabled = true, -- Enable dogfight assistance by default
|
|
dogfightMessageCooldown = 3, -- Seconds between dogfight assist messages
|
|
criticalSpeedThreshold = 150, -- Knots - warn if speed drops below this in dogfight
|
|
altitudeDeltaThreshold = 300, -- Meters - significant altitude difference for callouts
|
|
highClosureRate = 100, -- M/s - warn if closure rate exceeds this
|
|
positionChangeThreshold = 45, -- Degrees - trigger update if bandit moves this much
|
|
maxMultiplier = 8, -- Max detection multiplier to prevent excessive ranges
|
|
summaryInterval = 120, -- Seconds between scheduled summaries (0 to disable)
|
|
summaryCooldown = 2, -- Minimum seconds between on-demand summaries
|
|
activeMessaging = true, -- Enable live alerts; false for on-demand only
|
|
showGlobalMenu = true, -- Enable global settings menu for players
|
|
showWelcomeMessage = true, -- Show welcome message to new players
|
|
enableCloseFlyingCompliments = true, -- Enable compliments for close flying
|
|
complimentRange = 75, -- Meters for close flying compliment
|
|
headOnWarningRange = 150, -- Meters for head-on warning
|
|
closeFlyingMessageCooldown = 26, -- Seconds between close flying messages
|
|
enableAirScanning = true, -- Enable scanning for air targets
|
|
enableGroundScanning = false, -- Enable scanning for ground targets
|
|
illuminationCooldown = 30, -- Seconds between illumination flare drops (simulates reload time)
|
|
illuminationAltitude = 500, -- Meters - altitude offset above target for illumination flares
|
|
illuminationFlaresDefault = 3, -- Number of illumination flares per sortie
|
|
countAIWingmen = true, -- Count AI units in same group as wingmen for formation bonus (false = players only count as wingmen)
|
|
aiWingmenMultiplier = 1.0, -- Multiplier for AI wingmen (0.5 = half credit, 1.0 = full credit)
|
|
distanceUnit = "mi", -- Default distance unit: "km" for kilometers, "mi" for miles (nautical miles)
|
|
defaultLanguage = "EN", -- Default language: "EN", "DE", "FR", "ES", "RU"
|
|
}
|
|
|
|
-- Multilingual message and menu tables
|
|
-- Each language contains all messages and menu text
|
|
PILOT_INTUITION_LANGUAGES = {
|
|
EN = {
|
|
-- Messages
|
|
welcome = {
|
|
"Welcome to WWII Pilot Intuition! This system simulates pilot reconnaissance for spotting air and ground targets. Use F10 menu for settings.",
|
|
"Greetings, pilot! WWII Pilot Intuition is active. It helps you spot bandits and ground threats. Check F10 for options.",
|
|
"Pilot Intuition engaged! Simulate WWII-era reconnaissance. F10 menu for controls.",
|
|
},
|
|
formationJoin = {
|
|
"You've joined flight with %s - air detection increased to %.0f%s, ground to %.0f%s.",
|
|
"%s is now flying off your wing - detection ranges boosted to %.0f%s air, %.0f%s ground.",
|
|
"Welcome aboard, %s! Formation tightens detection to %.0f%s for air, %.0f%s for ground.",
|
|
"%s joins the formation - eyes sharper now, %.0f%s air, %.0f%s ground range.",
|
|
},
|
|
formationLeave = {
|
|
"%s left formation - air detection reduced to %.0f%s, ground to %.0f%s.",
|
|
"%s is outa here - detection drops to %.0f%s air, %.0f%s ground.",
|
|
"Formation broken by %s - ranges now %.0f%s air, %.0f%s ground.",
|
|
"%s has peeled off - back to solo detection: %.0f%s air, %.0f%s ground.",
|
|
},
|
|
formationIntegrityLow = {
|
|
"Formation integrity low! Tighten up.",
|
|
"Form up, pilots! We're spread too thin.",
|
|
"Close ranks! Formation integrity compromised.",
|
|
"Get back in formation, lads! We're vulnerable.",
|
|
},
|
|
airTargetDetected = {
|
|
"Bandit %s at %.0f degrees, %.1f %s, angels %.0f (%s)!",
|
|
"Enemy aircraft %s: %.0f degrees, %.1f %s, altitude %.0f (%s).",
|
|
"Bogey %s at %.0f o'clock, %.1f %s out, angels %.0f (%s).",
|
|
"Hostile contact %s: %.0f degrees, %.1f %s, %.0f angels (%s).",
|
|
"Bandit inbound %s: %.0f degrees, %.1f %s, angels %.0f (%s).",
|
|
},
|
|
groundTargetDetected = {
|
|
"%s contact: %s %s at %.0f degrees, %.1f %s.",
|
|
"Ground threat: %s %s %s spotted at %.0f degrees, %.1f %s.",
|
|
"%s units detected: %s %s, %.0f degrees, %.1f %s.",
|
|
"Enemy ground: %s %s %s at bearing %.0f, %.1f %s away.",
|
|
},
|
|
dogfightEngaged = {
|
|
"Engaged!",
|
|
"Tally ho! Dogfight started.",
|
|
"Bandit engaged! Fight's on.",
|
|
"Dogfight! Guns hot.",
|
|
},
|
|
dogfightConcluded = {
|
|
"Dogfight concluded.",
|
|
"Fight's over. Clear.",
|
|
"Dogfight ended. Stand down.",
|
|
"Engagement terminated.",
|
|
},
|
|
underFire = {
|
|
"Under fire! Break %s!%s",
|
|
"Taking hits! Evade %s!%s",
|
|
"Shots fired! Turn %s now!%s",
|
|
"Incoming! Break %s!%s",
|
|
},
|
|
closeFlyingCompliment = {
|
|
"Nice flying! Tip-to-tip formation.",
|
|
"Hey, nice! That's some close flying.",
|
|
"Great job! That's close!",
|
|
"Easy does it there tiger, I don't know you that well!",
|
|
"Impressive! Wingtip to wingtip.",
|
|
"Smooth moves! Close quarters flying.",
|
|
"Damn, that's tight! Good flying.",
|
|
"Holy shit, that's close! Nice one.",
|
|
"Whew, that was close! Good stick work.",
|
|
"You're glued to my wing! Excellent flying.",
|
|
"Tight as a drum! Keep it up.",
|
|
"Helluva formation! Tip to tip.",
|
|
"Smooth as silk! Close flying there.",
|
|
"Damn, pilot! That's some precision.",
|
|
"Whoa, easy on the throttle! Nice and close.",
|
|
"Impressive control! Wingtip distance.",
|
|
"You're right on my six... wait, formation! Good job.",
|
|
"Tight formation! That's how it's done.",
|
|
"Holy cow, that's close! Well done.",
|
|
"Nice touch! Close quarters.",
|
|
"You're flying like a pro! Tight formation.",
|
|
"Smooth operator! Tip-to-tip.",
|
|
"Damn fine flying! Close as can be.",
|
|
"Whoa there! That's some tight flying.",
|
|
"Impressive! You're practically in my cockpit.",
|
|
"Smooth sailing! Close formation.",
|
|
"Hell yeah, that's tight! Good work.",
|
|
"Easy tiger, but damn good flying.",
|
|
"You're a formation expert! Tip-to-tip.",
|
|
"Smooth moves, pilot! Close quarters.",
|
|
},
|
|
headOnWarning = {
|
|
"Whew! That was close!",
|
|
"Whoa! Nearly a head-on!",
|
|
"Close call! Watch that pass.",
|
|
"Damn, that was tight! Be careful.",
|
|
"Holy crap, almost collided!",
|
|
},
|
|
markerSet = {
|
|
"Marker set to %s.",
|
|
"Markers now %s.",
|
|
"Marker type changed to %s.",
|
|
},
|
|
dogfightAssistToggle = {
|
|
"Dogfight assist %s.",
|
|
"Dogfight assistance %s.",
|
|
},
|
|
activeMessagingToggle = {
|
|
"Active messaging %s.",
|
|
"Live alerts %s.",
|
|
},
|
|
airScanningToggle = {
|
|
"Air scanning %s.",
|
|
"Air detection %s.",
|
|
},
|
|
groundScanningToggle = {
|
|
"Ground scanning %s.",
|
|
"Ground detection %s.",
|
|
},
|
|
alertFrequencyToggle = {
|
|
"Alert frequency set to %s.",
|
|
"Alerts now %s.",
|
|
},
|
|
summaryCooldown = {
|
|
"Summary on cooldown.",
|
|
"Wait a bit for another summary.",
|
|
},
|
|
noThreats = {
|
|
"No active threats.",
|
|
"All clear.",
|
|
"Situation normal.",
|
|
},
|
|
-- Additional hardcoded messages that need translation
|
|
systemActive = "WWII Pilot Intuition active! Use F10 menu for settings.",
|
|
logLevelSet = "Log level set to: %s",
|
|
distanceUnitsSet = "Distance units set to %s.",
|
|
targetNotAvailable = "Target %d not available.",
|
|
markedTarget = "Marked Target %d.",
|
|
noIlluminationFlares = "No illumination flares remaining. Land at a friendly airbase to rearm.",
|
|
illuminationNotReady = "Illumination not ready. Wait %d seconds.",
|
|
cannotDeterminePosition = "Cannot determine position for illumination drop.",
|
|
cannotDetermineAltitude = "Cannot determine altitude for illumination drop.",
|
|
illuminationDropped = "Illumination flare dropped at your position. (%d remaining)",
|
|
cannotDetermineTargetPosition = "Cannot determine target position for illumination drop.",
|
|
errorCouldNotDeterminePosition = "Error: Could not determine your position.",
|
|
illuminationDroppedOnTarget = "Illumination flare dropped on Target %d (%.0f°, %.1f%s). (%d remaining)",
|
|
illuminationRearmed = "Illumination flares rearmed: %d available.",
|
|
noGroundTargets = "No enemy ground targets detected in range.",
|
|
selectTargetFromMenu = "Select a target from the menu to mark it.",
|
|
targetInfo = "Target %d: %s %s %s, Bearing %.0f, Range %.1f %s",
|
|
-- State words
|
|
enabled = "enabled",
|
|
disabled = "disabled",
|
|
hot = "hot",
|
|
cold = "cold",
|
|
closing = "closing",
|
|
opening = "opening",
|
|
left = "left",
|
|
right = "right",
|
|
above = "above",
|
|
below = "below",
|
|
high = "high",
|
|
low = "low",
|
|
normal = "normal",
|
|
quiet = "quiet",
|
|
verbose = "verbose",
|
|
-- Menu text
|
|
menu = {
|
|
mainTitle = "WWII Pilot Intuition",
|
|
dogfightAssist = "Dogfight Assist",
|
|
enable = "Enable",
|
|
disable = "Disable",
|
|
markerType = "Marker Type",
|
|
smoke = "Smoke",
|
|
flare = "Flare",
|
|
red = "Red",
|
|
green = "Green",
|
|
blue = "Blue",
|
|
white = "White",
|
|
none = "None",
|
|
airScanning = "Air Scanning",
|
|
groundScanning = "Ground Scanning",
|
|
groundTargeting = "Ground Targeting",
|
|
scanForTargets = "Scan for Targets",
|
|
markTarget = "Mark Target",
|
|
target = "Target %d",
|
|
illumination = "Illumination (%d left)",
|
|
dropAtMyPosition = "Drop at My Position",
|
|
dropOnTarget = "Drop on Target",
|
|
alertFrequency = "Alert Frequency",
|
|
normalFreq = "Normal",
|
|
quietFreq = "Quiet",
|
|
verboseFreq = "Verbose",
|
|
summary = "Summary",
|
|
brief = "Brief",
|
|
detailed = "Detailed",
|
|
settingsAndGuides = "Settings & Player Guides",
|
|
distanceUnits = "Distance Units",
|
|
milesNautical = "Miles (Nautical)",
|
|
kilometers = "Kilometers",
|
|
playerGuide = "Player Guide",
|
|
systemOverview = "System Overview",
|
|
detectionRanges = "Detection Ranges",
|
|
dogfightAssistHelp = "Dogfight Assist Help",
|
|
groundTargetingHelp = "Ground Targeting Help",
|
|
formationTips = "Formation Tips",
|
|
illuminationHelp = "Illumination Help",
|
|
logLevel = "Log Level",
|
|
language = "Language / Sprache / Langue",
|
|
english = "English",
|
|
german = "Deutsch (German)",
|
|
french = "Français (French)",
|
|
spanish = "Español (Spanish)",
|
|
russian = "Русский (Russian)",
|
|
}
|
|
},
|
|
-- German language
|
|
DE = {
|
|
welcome = {
|
|
"Willkommen bei WWII Piloten-Intuition! Dieses System simuliert Piloten-Aufklärung zum Erkennen von Luft- und Bodenzielen. Verwenden Sie das F10-Menü für Einstellungen.",
|
|
"Grüße, Pilot! WWII Piloten-Intuition ist aktiv. Es hilft Ihnen, Banditen und Bodenbedrohungen zu erkennen. Prüfen Sie F10 für Optionen.",
|
|
"Piloten-Intuition aktiviert! Simulieren Sie Aufklärung aus der WWII-Ära. F10-Menü für Steuerung.",
|
|
},
|
|
formationJoin = {
|
|
"Sie haben sich dem Flug mit %s angeschlossen - Lufterkennung erhöht auf %.0f%s, Boden auf %.0f%s.",
|
|
"%s fliegt jetzt an Ihrem Flügel - Erkennungsreichweiten erhöht auf %.0f%s Luft, %.0f%s Boden.",
|
|
"Willkommen an Bord, %s! Formation verbessert Erkennung auf %.0f%s für Luft, %.0f%s für Boden.",
|
|
"%s schließt sich der Formation an - Augen sind schärfer jetzt, %.0f%s Luft, %.0f%s Boden Reichweite.",
|
|
},
|
|
formationLeave = {
|
|
"%s hat Formation verlassen - Lufterkennung reduziert auf %.0f%s, Boden auf %.0f%s.",
|
|
"%s ist weg - Erkennung fällt auf %.0f%s Luft, %.0f%s Boden.",
|
|
"Formation durch %s aufgelöst - Reichweiten jetzt %.0f%s Luft, %.0f%s Boden.",
|
|
"%s hat sich abgesetzt - zurück zur Solo-Erkennung: %.0f%s Luft, %.0f%s Boden.",
|
|
},
|
|
formationIntegrityLow = {
|
|
"Formationsintegrität niedrig! Zusammenrücken.",
|
|
"Formiert euch, Piloten! Wir sind zu weit auseinander.",
|
|
"Reihen schließen! Formationsintegrität gefährdet.",
|
|
"Zurück in Formation, Leute! Wir sind verwundbar.",
|
|
},
|
|
airTargetDetected = {
|
|
"Bandit %s bei %.0f Grad, %.1f %s, Engel %.0f (%s)!",
|
|
"Feindflugzeug %s: %.0f Grad, %.1f %s, Höhe %.0f (%s).",
|
|
"Bogey %s bei %.0f Uhr, %.1f %s entfernt, Engel %.0f (%s).",
|
|
"Feindkontakt %s: %.0f Grad, %.1f %s, %.0f Engel (%s).",
|
|
"Bandit im Anflug %s: %.0f Grad, %.1f %s, Engel %.0f (%s).",
|
|
},
|
|
groundTargetDetected = {
|
|
"%s Kontakt: %s %s bei %.0f Grad, %.1f %s.",
|
|
"Bodenbedrohung: %s %s %s gesichtet bei %.0f Grad, %.1f %s.",
|
|
"%s Einheiten entdeckt: %s %s, %.0f Grad, %.1f %s.",
|
|
"Feindlicher Boden: %s %s %s bei Peilung %.0f, %.1f %s entfernt.",
|
|
},
|
|
dogfightEngaged = {
|
|
"Im Gefecht!",
|
|
"Tally ho! Luftkampf begonnen.",
|
|
"Bandit im Gefecht! Kampf läuft.",
|
|
"Luftkampf! Waffen scharf.",
|
|
},
|
|
dogfightConcluded = {
|
|
"Luftkampf beendet.",
|
|
"Kampf vorbei. Klar.",
|
|
"Luftkampf beendet. Entspannen.",
|
|
"Gefecht beendet.",
|
|
},
|
|
underFire = {
|
|
"Unter Beschuss! Ausweichen %s!%s",
|
|
"Treffer einsteckend! Ausweichen %s!%s",
|
|
"Schüsse abgefeuert! Wende %s jetzt!%s",
|
|
"Eingehend! Ausweichen %s!%s",
|
|
},
|
|
closeFlyingCompliment = {
|
|
"Schönes Fliegen! Spitze-zu-Spitze-Formation.",
|
|
"Hey, schön! Das ist enges Fliegen.",
|
|
"Großartige Arbeit! Das ist nah!",
|
|
"Ruhig, Tiger, ich kenne dich nicht so gut!",
|
|
"Beeindruckend! Flügelspitze zu Flügelspitze.",
|
|
"Geschmeidige Bewegungen! Nahkampffliegen.",
|
|
"Verdammt, das ist eng! Gutes Fliegen.",
|
|
"Heilige Scheiße, das ist nah! Gut gemacht.",
|
|
"Puh, das war knapp! Gute Steuerung.",
|
|
"Du klebst an meinem Flügel! Exzellentes Fliegen.",
|
|
},
|
|
headOnWarning = {
|
|
"Puh! Das war knapp!",
|
|
"Whoa! Fast frontal!",
|
|
"Knapper Anruf! Pass auf beim Überholen.",
|
|
"Verdammt, das war eng! Sei vorsichtig.",
|
|
"Heiliger Mist, fast kollidiert!",
|
|
},
|
|
markerSet = {
|
|
"Markierung auf %s gesetzt.",
|
|
"Markierungen jetzt %s.",
|
|
"Markierungstyp geändert auf %s.",
|
|
},
|
|
dogfightAssistToggle = {
|
|
"Luftkampf-Hilfe %s.",
|
|
"Luftkampf-Unterstützung %s.",
|
|
},
|
|
activeMessagingToggle = {
|
|
"Aktive Benachrichtigungen %s.",
|
|
"Live-Warnungen %s.",
|
|
},
|
|
airScanningToggle = {
|
|
"Luftscan %s.",
|
|
"Lufterkennung %s.",
|
|
},
|
|
groundScanningToggle = {
|
|
"Bodenscan %s.",
|
|
"Bodenerkennung %s.",
|
|
},
|
|
alertFrequencyToggle = {
|
|
"Warnfrequenz auf %s eingestellt.",
|
|
"Warnungen jetzt %s.",
|
|
},
|
|
summaryCooldown = {
|
|
"Zusammenfassung in Abklingzeit.",
|
|
"Warten Sie etwas für eine weitere Zusammenfassung.",
|
|
},
|
|
noThreats = {
|
|
"Keine aktiven Bedrohungen.",
|
|
"Alles klar.",
|
|
"Situation normal.",
|
|
},
|
|
systemActive = "WWII Piloten-Intuition aktiv! Verwenden Sie das F10-Menü für Einstellungen.",
|
|
logLevelSet = "Log-Level auf %s gesetzt",
|
|
distanceUnitsSet = "Entfernungseinheiten auf %s eingestellt.",
|
|
targetNotAvailable = "Ziel %d nicht verfügbar.",
|
|
markedTarget = "Ziel %d markiert.",
|
|
noIlluminationFlares = "Keine Leuchtraketen mehr. Landen Sie auf einem befreundeten Flugplatz zum Auftanken.",
|
|
illuminationNotReady = "Beleuchtung nicht bereit. Warten Sie %d Sekunden.",
|
|
cannotDeterminePosition = "Kann Position für Leuchtraketen-Abwurf nicht bestimmen.",
|
|
cannotDetermineAltitude = "Kann Höhe für Leuchtraketen-Abwurf nicht bestimmen.",
|
|
illuminationDropped = "Leuchtrakete an Ihrer Position abgeworfen. (%d verbleibend)",
|
|
cannotDetermineTargetPosition = "Kann Zielposition für Leuchtraketen-Abwurf nicht bestimmen.",
|
|
errorCouldNotDeterminePosition = "Fehler: Konnte Ihre Position nicht bestimmen.",
|
|
illuminationDroppedOnTarget = "Leuchtrakete auf Ziel %d abgeworfen (%.0f°, %.1f%s). (%d verbleibend)",
|
|
illuminationRearmed = "Leuchtraketen aufgefüllt: %d verfügbar.",
|
|
noGroundTargets = "Keine feindlichen Bodenziele in Reichweite erkannt.",
|
|
selectTargetFromMenu = "Wählen Sie ein Ziel aus dem Menü zum Markieren.",
|
|
targetInfo = "Ziel %d: %s %s %s, Peilung %.0f, Reichweite %.1f %s",
|
|
enabled = "aktiviert",
|
|
disabled = "deaktiviert",
|
|
hot = "heiß",
|
|
cold = "kalt",
|
|
closing = "schließend",
|
|
opening = "öffnend",
|
|
left = "links",
|
|
right = "rechts",
|
|
above = "oben",
|
|
below = "unten",
|
|
high = "hoch",
|
|
low = "niedrig",
|
|
normal = "normal",
|
|
quiet = "ruhig",
|
|
verbose = "ausführlich",
|
|
menu = {
|
|
mainTitle = "WWII Piloten-Intuition",
|
|
dogfightAssist = "Luftkampf-Hilfe",
|
|
enable = "Aktivieren",
|
|
disable = "Deaktivieren",
|
|
markerType = "Markierungstyp",
|
|
smoke = "Rauch",
|
|
flare = "Leuchtrakete",
|
|
red = "Rot",
|
|
green = "Grün",
|
|
blue = "Blau",
|
|
white = "Weiß",
|
|
none = "Keine",
|
|
airScanning = "Luftscan",
|
|
groundScanning = "Bodenscan",
|
|
groundTargeting = "Bodenziel-Erfassung",
|
|
scanForTargets = "Nach Zielen scannen",
|
|
markTarget = "Ziel markieren",
|
|
target = "Ziel %d",
|
|
illumination = "Beleuchtung (%d übrig)",
|
|
dropAtMyPosition = "An meiner Position abwerfen",
|
|
dropOnTarget = "Auf Ziel abwerfen",
|
|
alertFrequency = "Warnfrequenz",
|
|
normalFreq = "Normal",
|
|
quietFreq = "Ruhig",
|
|
verboseFreq = "Ausführlich",
|
|
summary = "Zusammenfassung",
|
|
brief = "Kurz",
|
|
detailed = "Detailliert",
|
|
settingsAndGuides = "Einstellungen & Spielerführer",
|
|
distanceUnits = "Entfernungseinheiten",
|
|
milesNautical = "Meilen (Nautisch)",
|
|
kilometers = "Kilometer",
|
|
playerGuide = "Spielerführer",
|
|
systemOverview = "Systemübersicht",
|
|
detectionRanges = "Erkennungsreichweiten",
|
|
dogfightAssistHelp = "Luftkampf-Hilfe Anleitung",
|
|
groundTargetingHelp = "Bodenziel-Erfassung Anleitung",
|
|
formationTips = "Formationstipps",
|
|
illuminationHelp = "Beleuchtungs-Hilfe",
|
|
logLevel = "Log-Level",
|
|
language = "Language / Sprache / Langue",
|
|
english = "English (Englisch)",
|
|
german = "Deutsch (German)",
|
|
french = "Français (Französisch)",
|
|
spanish = "Español (Spanisch)",
|
|
russian = "Русский (Russisch)",
|
|
}
|
|
},
|
|
-- French language
|
|
FR = {
|
|
welcome = {
|
|
"Bienvenue dans WWII Intuition du Pilote! Ce système simule la reconnaissance du pilote pour repérer les cibles aériennes et terrestres. Utilisez le menu F10 pour les paramètres.",
|
|
"Salutations, pilote! WWII Intuition du Pilote est actif. Il vous aide à repérer les bandits et les menaces au sol. Consultez F10 pour les options.",
|
|
"Intuition du pilote engagée! Simulez la reconnaissance de l'ère WWII. Menu F10 pour les commandes.",
|
|
},
|
|
formationJoin = {
|
|
"Vous avez rejoint le vol avec %s - détection aérienne augmentée à %.0f%s, sol à %.0f%s.",
|
|
"%s vole maintenant sur votre aile - portées de détection augmentées à %.0f%s air, %.0f%s sol.",
|
|
"Bienvenue à bord, %s! La formation améliore la détection à %.0f%s pour l'air, %.0f%s pour le sol.",
|
|
"%s rejoint la formation - les yeux sont plus aiguisés maintenant, %.0f%s air, %.0f%s sol portée.",
|
|
},
|
|
formationLeave = {
|
|
"%s a quitté la formation - détection aérienne réduite à %.0f%s, sol à %.0f%s.",
|
|
"%s est parti - détection chute à %.0f%s air, %.0f%s sol.",
|
|
"Formation rompue par %s - portées maintenant %.0f%s air, %.0f%s sol.",
|
|
"%s s'est détaché - retour à la détection solo: %.0f%s air, %.0f%s sol.",
|
|
},
|
|
formationIntegrityLow = {
|
|
"Intégrité de formation faible! Resserrez-vous.",
|
|
"Formez-vous, pilotes! Nous sommes trop dispersés.",
|
|
"Serrez les rangs! Intégrité de formation compromise.",
|
|
"Revenez en formation, les gars! Nous sommes vulnérables.",
|
|
},
|
|
airTargetDetected = {
|
|
"Bandit %s à %.0f degrés, %.1f %s, anges %.0f (%s)!",
|
|
"Avion ennemi %s: %.0f degrés, %.1f %s, altitude %.0f (%s).",
|
|
"Bogey %s à %.0f heures, %.1f %s, anges %.0f (%s).",
|
|
"Contact hostile %s: %.0f degrés, %.1f %s, %.0f anges (%s).",
|
|
"Bandit entrant %s: %.0f degrés, %.1f %s, anges %.0f (%s).",
|
|
},
|
|
groundTargetDetected = {
|
|
"%s contact: %s %s à %.0f degrés, %.1f %s.",
|
|
"Menace au sol: %s %s %s repéré à %.0f degrés, %.1f %s.",
|
|
"%s unités détectées: %s %s, %.0f degrés, %.1f %s.",
|
|
"Sol ennemi: %s %s %s au relèvement %.0f, %.1f %s de distance.",
|
|
},
|
|
dogfightEngaged = {
|
|
"Engagé!",
|
|
"Tally ho! Combat aérien commencé.",
|
|
"Bandit engagé! Le combat est lancé.",
|
|
"Combat aérien! Armes chaudes.",
|
|
},
|
|
dogfightConcluded = {
|
|
"Combat aérien terminé.",
|
|
"Combat terminé. Dégagé.",
|
|
"Combat aérien terminé. Repos.",
|
|
"Engagement terminé.",
|
|
},
|
|
underFire = {
|
|
"Sous le feu! Rompez %s!%s",
|
|
"Prend des coups! Évitez %s!%s",
|
|
"Coups tirés! Tournez %s maintenant!%s",
|
|
"Entrant! Rompez %s!%s",
|
|
},
|
|
closeFlyingCompliment = {
|
|
"Beau vol! Formation bout à bout.",
|
|
"Hey, sympa! C'est du vol serré.",
|
|
"Excellent travail! C'est proche!",
|
|
"Doucement, tigre, je ne te connais pas si bien!",
|
|
"Impressionnant! Bout d'aile à bout d'aile.",
|
|
"Mouvements fluides! Vol en quartiers serrés.",
|
|
"Merde, c'est serré! Bon vol.",
|
|
"Putain, c'est proche! Bien joué.",
|
|
"Ouf, c'était proche! Bon maniement du manche.",
|
|
"Tu es collé à mon aile! Excellent vol.",
|
|
},
|
|
headOnWarning = {
|
|
"Ouf! C'était proche!",
|
|
"Whoa! Presque en face!",
|
|
"Appel serré! Attention à ce passage.",
|
|
"Merde, c'était serré! Soyez prudent.",
|
|
"Sainte merde, presque entré en collision!",
|
|
},
|
|
markerSet = {
|
|
"Marqueur défini sur %s.",
|
|
"Marqueurs maintenant %s.",
|
|
"Type de marqueur changé en %s.",
|
|
},
|
|
dogfightAssistToggle = {
|
|
"Assistance combat aérien %s.",
|
|
"Assistance combat aérien %s.",
|
|
},
|
|
activeMessagingToggle = {
|
|
"Messagerie active %s.",
|
|
"Alertes en direct %s.",
|
|
},
|
|
airScanningToggle = {
|
|
"Scan aérien %s.",
|
|
"Détection aérienne %s.",
|
|
},
|
|
groundScanningToggle = {
|
|
"Scan au sol %s.",
|
|
"Détection au sol %s.",
|
|
},
|
|
alertFrequencyToggle = {
|
|
"Fréquence d'alerte définie sur %s.",
|
|
"Alertes maintenant %s.",
|
|
},
|
|
summaryCooldown = {
|
|
"Résumé en temps de recharge.",
|
|
"Attendez un peu pour un autre résumé.",
|
|
},
|
|
noThreats = {
|
|
"Aucune menace active.",
|
|
"Tout est clair.",
|
|
"Situation normale.",
|
|
},
|
|
systemActive = "WWII Intuition du Pilote actif! Utilisez le menu F10 pour les paramètres.",
|
|
logLevelSet = "Niveau de log défini sur: %s",
|
|
distanceUnitsSet = "Unités de distance définies sur %s.",
|
|
targetNotAvailable = "Cible %d non disponible.",
|
|
markedTarget = "Cible %d marquée.",
|
|
noIlluminationFlares = "Plus de fusées éclairantes. Atterrissez sur une base aérienne amie pour vous réarmer.",
|
|
illuminationNotReady = "Illumination pas prête. Attendez %d secondes.",
|
|
cannotDeterminePosition = "Impossible de déterminer la position pour le largage de fusée éclairante.",
|
|
cannotDetermineAltitude = "Impossible de déterminer l'altitude pour le largage de fusée éclairante.",
|
|
illuminationDropped = "Fusée éclairante larguée à votre position. (%d restantes)",
|
|
cannotDetermineTargetPosition = "Impossible de déterminer la position de la cible pour le largage de fusée éclairante.",
|
|
errorCouldNotDeterminePosition = "Erreur: Impossible de déterminer votre position.",
|
|
illuminationDroppedOnTarget = "Fusée éclairante larguée sur cible %d (%.0f°, %.1f%s). (%d restantes)",
|
|
illuminationRearmed = "Fusées éclairantes réarmées: %d disponibles.",
|
|
noGroundTargets = "Aucune cible terrestre ennemie détectée à portée.",
|
|
selectTargetFromMenu = "Sélectionnez une cible dans le menu pour la marquer.",
|
|
targetInfo = "Cible %d: %s %s %s, Relèvement %.0f, Portée %.1f %s",
|
|
enabled = "activé",
|
|
disabled = "désactivé",
|
|
hot = "chaud",
|
|
cold = "froid",
|
|
closing = "rapprochement",
|
|
opening = "éloignement",
|
|
left = "gauche",
|
|
right = "droite",
|
|
above = "au-dessus",
|
|
below = "en-dessous",
|
|
high = "haut",
|
|
low = "bas",
|
|
normal = "normal",
|
|
quiet = "calme",
|
|
verbose = "verbeux",
|
|
menu = {
|
|
mainTitle = "Intuition du Pilote WWII",
|
|
dogfightAssist = "Assistance Combat Aérien",
|
|
enable = "Activer",
|
|
disable = "Désactiver",
|
|
markerType = "Type de Marqueur",
|
|
smoke = "Fumée",
|
|
flare = "Fusée",
|
|
red = "Rouge",
|
|
green = "Vert",
|
|
blue = "Bleu",
|
|
white = "Blanc",
|
|
none = "Aucun",
|
|
airScanning = "Scan Aérien",
|
|
groundScanning = "Scan au Sol",
|
|
groundTargeting = "Ciblage au Sol",
|
|
scanForTargets = "Scanner les Cibles",
|
|
markTarget = "Marquer Cible",
|
|
target = "Cible %d",
|
|
illumination = "Illumination (%d restantes)",
|
|
dropAtMyPosition = "Larguer à ma Position",
|
|
dropOnTarget = "Larguer sur Cible",
|
|
alertFrequency = "Fréquence d'Alerte",
|
|
normalFreq = "Normal",
|
|
quietFreq = "Calme",
|
|
verboseFreq = "Verbeux",
|
|
summary = "Résumé",
|
|
brief = "Bref",
|
|
detailed = "Détaillé",
|
|
settingsAndGuides = "Paramètres & Guides Joueurs",
|
|
distanceUnits = "Unités de Distance",
|
|
milesNautical = "Miles (Nautiques)",
|
|
kilometers = "Kilomètres",
|
|
playerGuide = "Guide du Joueur",
|
|
systemOverview = "Aperçu du Système",
|
|
detectionRanges = "Portées de Détection",
|
|
dogfightAssistHelp = "Aide Combat Aérien",
|
|
groundTargetingHelp = "Aide Ciblage au Sol",
|
|
formationTips = "Conseils Formation",
|
|
illuminationHelp = "Aide Illumination",
|
|
logLevel = "Niveau de Log",
|
|
language = "Language / Sprache / Langue",
|
|
english = "English (Anglais)",
|
|
german = "Deutsch (Allemand)",
|
|
french = "Français (French)",
|
|
spanish = "Español (Espagnol)",
|
|
russian = "Русский (Russe)",
|
|
}
|
|
},
|
|
-- Spanish language
|
|
ES = {
|
|
welcome = {
|
|
"¡Bienvenido a WWII Intuición del Piloto! Este sistema simula el reconocimiento del piloto para detectar objetivos aéreos y terrestres. Use el menú F10 para configuración.",
|
|
"¡Saludos, piloto! WWII Intuición del Piloto está activo. Te ayuda a detectar bandidos y amenazas terrestres. Revisa F10 para opciones.",
|
|
"¡Intuición del piloto activada! Simula reconocimiento de la era WWII. Menú F10 para controles.",
|
|
},
|
|
formationJoin = {
|
|
"Te has unido al vuelo con %s - detección aérea aumentada a %.0f%s, terrestre a %.0f%s.",
|
|
"%s ahora vuela en tu ala - rangos de detección aumentados a %.0f%s aéreo, %.0f%s terrestre.",
|
|
"¡Bienvenido a bordo, %s! La formación mejora detección a %.0f%s para aéreo, %.0f%s para terrestre.",
|
|
"%s se une a la formación - ojos más agudos ahora, %.0f%s aéreo, %.0f%s terrestre rango.",
|
|
},
|
|
formationLeave = {
|
|
"%s dejó la formación - detección aérea reducida a %.0f%s, terrestre a %.0f%s.",
|
|
"%s se fue - detección cae a %.0f%s aéreo, %.0f%s terrestre.",
|
|
"Formación rota por %s - rangos ahora %.0f%s aéreo, %.0f%s terrestre.",
|
|
"%s se ha separado - vuelta a detección solitaria: %.0f%s aéreo, %.0f%s terrestre.",
|
|
},
|
|
formationIntegrityLow = {
|
|
"¡Integridad de formación baja! Aprieten.",
|
|
"¡Formen, pilotos! Estamos demasiado dispersos.",
|
|
"¡Cierren filas! Integridad de formación comprometida.",
|
|
"¡Vuelvan a la formación, muchachos! Somos vulnerables.",
|
|
},
|
|
airTargetDetected = {
|
|
"Bandido %s a %.0f grados, %.1f %s, ángeles %.0f (%s)!",
|
|
"Aeronave enemiga %s: %.0f grados, %.1f %s, altitud %.0f (%s).",
|
|
"Bogey %s a las %.0f en punto, %.1f %s fuera, ángeles %.0f (%s).",
|
|
"Contacto hostil %s: %.0f grados, %.1f %s, %.0f ángeles (%s).",
|
|
"Bandido entrante %s: %.0f grados, %.1f %s, ángeles %.0f (%s).",
|
|
},
|
|
groundTargetDetected = {
|
|
"%s contacto: %s %s a %.0f grados, %.1f %s.",
|
|
"Amenaza terrestre: %s %s %s avistado a %.0f grados, %.1f %s.",
|
|
"%s unidades detectadas: %s %s, %.0f grados, %.1f %s.",
|
|
"Terreno enemigo: %s %s %s en rumbo %.0f, %.1f %s de distancia.",
|
|
},
|
|
dogfightEngaged = {
|
|
"¡Enganchado!",
|
|
"¡Tally ho! Combate aéreo iniciado.",
|
|
"¡Bandido enganchado! La pelea está en marcha.",
|
|
"¡Combate aéreo! Armas calientes.",
|
|
},
|
|
dogfightConcluded = {
|
|
"Combate aéreo concluido.",
|
|
"Pelea terminada. Despejado.",
|
|
"Combate aéreo terminado. Descanso.",
|
|
"Enganche terminado.",
|
|
},
|
|
underFire = {
|
|
"¡Bajo fuego! ¡Rompe %s!%s",
|
|
"¡Recibiendo impactos! ¡Evade %s!%s",
|
|
"¡Disparos! ¡Gira %s ahora!%s",
|
|
"¡Entrante! ¡Rompe %s!%s",
|
|
},
|
|
closeFlyingCompliment = {
|
|
"¡Buen vuelo! Formación punta con punta.",
|
|
"¡Hey, bien! Eso es vuelo cerrado.",
|
|
"¡Excelente trabajo! ¡Qué cerca!",
|
|
"¡Despacio ahí tigre, no te conozco tan bien!",
|
|
"¡Impresionante! Punta de ala con punta de ala.",
|
|
"¡Movimientos suaves! Vuelo en cuartos cerrados.",
|
|
"¡Diablos, qué ajustado! Buen vuelo.",
|
|
"¡Mierda, qué cerca! Bien hecho.",
|
|
"¡Uf, eso estuvo cerca! Buen control del stick.",
|
|
"¡Estás pegado a mi ala! Excelente vuelo.",
|
|
},
|
|
headOnWarning = {
|
|
"¡Uf! ¡Eso estuvo cerca!",
|
|
"¡Whoa! ¡Casi de frente!",
|
|
"¡Llamada cerrada! Cuidado con ese pase.",
|
|
"¡Diablos, eso estuvo ajustado! Ten cuidado.",
|
|
"¡Santo cielo, casi colisionamos!",
|
|
},
|
|
markerSet = {
|
|
"Marcador establecido en %s.",
|
|
"Marcadores ahora %s.",
|
|
"Tipo de marcador cambiado a %s.",
|
|
},
|
|
dogfightAssistToggle = {
|
|
"Asistencia de combate aéreo %s.",
|
|
"Asistencia de combate aéreo %s.",
|
|
},
|
|
activeMessagingToggle = {
|
|
"Mensajería activa %s.",
|
|
"Alertas en vivo %s.",
|
|
},
|
|
airScanningToggle = {
|
|
"Escaneo aéreo %s.",
|
|
"Detección aérea %s.",
|
|
},
|
|
groundScanningToggle = {
|
|
"Escaneo terrestre %s.",
|
|
"Detección terrestre %s.",
|
|
},
|
|
alertFrequencyToggle = {
|
|
"Frecuencia de alerta establecida en %s.",
|
|
"Alertas ahora %s.",
|
|
},
|
|
summaryCooldown = {
|
|
"Resumen en tiempo de reutilización.",
|
|
"Espera un poco para otro resumen.",
|
|
},
|
|
noThreats = {
|
|
"Sin amenazas activas.",
|
|
"Todo despejado.",
|
|
"Situación normal.",
|
|
},
|
|
systemActive = "¡WWII Intuición del Piloto activo! Use el menú F10 para configuración.",
|
|
logLevelSet = "Nivel de registro establecido en: %s",
|
|
distanceUnitsSet = "Unidades de distancia establecidas en %s.",
|
|
targetNotAvailable = "Objetivo %d no disponible.",
|
|
markedTarget = "Objetivo %d marcado.",
|
|
noIlluminationFlares = "No quedan bengalas de iluminación. Aterriza en una base aérea amiga para rearmarte.",
|
|
illuminationNotReady = "Iluminación no lista. Espera %d segundos.",
|
|
cannotDeterminePosition = "No se puede determinar la posición para lanzar bengala de iluminación.",
|
|
cannotDetermineAltitude = "No se puede determinar la altitud para lanzar bengala de iluminación.",
|
|
illuminationDropped = "Bengala de iluminación lanzada en tu posición. (%d restantes)",
|
|
cannotDetermineTargetPosition = "No se puede determinar la posición del objetivo para lanzar bengala de iluminación.",
|
|
errorCouldNotDeterminePosition = "Error: No se pudo determinar tu posición.",
|
|
illuminationDroppedOnTarget = "Bengala de iluminación lanzada en Objetivo %d (%.0f°, %.1f%s). (%d restantes)",
|
|
illuminationRearmed = "Bengalas de iluminación rearmadas: %d disponibles.",
|
|
noGroundTargets = "No se detectaron objetivos terrestres enemigos en rango.",
|
|
selectTargetFromMenu = "Selecciona un objetivo del menú para marcarlo.",
|
|
targetInfo = "Objetivo %d: %s %s %s, Rumbo %.0f, Rango %.1f %s",
|
|
enabled = "activado",
|
|
disabled = "desactivado",
|
|
hot = "caliente",
|
|
cold = "frío",
|
|
closing = "acercándose",
|
|
opening = "alejándose",
|
|
left = "izquierda",
|
|
right = "derecha",
|
|
above = "arriba",
|
|
below = "abajo",
|
|
high = "alto",
|
|
low = "bajo",
|
|
normal = "normal",
|
|
quiet = "silencioso",
|
|
verbose = "detallado",
|
|
menu = {
|
|
mainTitle = "Intuición del Piloto WWII",
|
|
dogfightAssist = "Asistencia Combate Aéreo",
|
|
enable = "Activar",
|
|
disable = "Desactivar",
|
|
markerType = "Tipo de Marcador",
|
|
smoke = "Humo",
|
|
flare = "Bengala",
|
|
red = "Rojo",
|
|
green = "Verde",
|
|
blue = "Azul",
|
|
white = "Blanco",
|
|
none = "Ninguno",
|
|
airScanning = "Escaneo Aéreo",
|
|
groundScanning = "Escaneo Terrestre",
|
|
groundTargeting = "Objetivo Terrestre",
|
|
scanForTargets = "Escanear Objetivos",
|
|
markTarget = "Marcar Objetivo",
|
|
target = "Objetivo %d",
|
|
illumination = "Iluminación (%d restantes)",
|
|
dropAtMyPosition = "Lanzar en Mi Posición",
|
|
dropOnTarget = "Lanzar en Objetivo",
|
|
alertFrequency = "Frecuencia de Alerta",
|
|
normalFreq = "Normal",
|
|
quietFreq = "Silencioso",
|
|
verboseFreq = "Detallado",
|
|
summary = "Resumen",
|
|
brief = "Breve",
|
|
detailed = "Detallado",
|
|
settingsAndGuides = "Configuración y Guías",
|
|
distanceUnits = "Unidades de Distancia",
|
|
milesNautical = "Millas (Náuticas)",
|
|
kilometers = "Kilómetros",
|
|
playerGuide = "Guía del Jugador",
|
|
systemOverview = "Resumen del Sistema",
|
|
detectionRanges = "Rangos de Detección",
|
|
dogfightAssistHelp = "Ayuda Combate Aéreo",
|
|
groundTargetingHelp = "Ayuda Objetivo Terrestre",
|
|
formationTips = "Consejos de Formación",
|
|
illuminationHelp = "Ayuda de Iluminación",
|
|
logLevel = "Nivel de Registro",
|
|
language = "Language / Sprache / Langue",
|
|
english = "English (Inglés)",
|
|
german = "Deutsch (Alemán)",
|
|
french = "Français (Francés)",
|
|
spanish = "Español (Spanish)",
|
|
russian = "Русский (Ruso)",
|
|
}
|
|
},
|
|
-- Russian language
|
|
RU = {
|
|
welcome = {
|
|
"Добро пожаловать в WWII Интуиция Пилота! Эта система имитирует разведку пилота для обнаружения воздушных и наземных целей. Используйте меню F10 для настроек.",
|
|
"Приветствую, пилот! WWII Интуиция Пилота активна. Она поможет вам обнаружить бандитов и наземные угрозы. Проверьте F10 для опций.",
|
|
"Интуиция пилота активирована! Имитируйте разведку эпохи WWII. Меню F10 для управления.",
|
|
},
|
|
formationJoin = {
|
|
"Вы присоединились к полету с %s - обнаружение воздушных целей увеличено до %.0f%s, наземных до %.0f%s.",
|
|
"%s теперь летит на вашем крыле - дальности обнаружения увеличены до %.0f%s воздух, %.0f%s земля.",
|
|
"Добро пожаловать на борт, %s! Формация улучшает обнаружение до %.0f%s для воздуха, %.0f%s для земли.",
|
|
"%s присоединяется к формации - глаза острее теперь, %.0f%s воздух, %.0f%s земля дальность.",
|
|
},
|
|
formationLeave = {
|
|
"%s покинул формацию - обнаружение воздушных целей снижено до %.0f%s, наземных до %.0f%s.",
|
|
"%s ушел - обнаружение падает до %.0f%s воздух, %.0f%s земля.",
|
|
"Формация разорвана %s - дальности теперь %.0f%s воздух, %.0f%s земля.",
|
|
"%s отделился - обратно к одиночному обнаружению: %.0f%s воздух, %.0f%s земля.",
|
|
},
|
|
formationIntegrityLow = {
|
|
"Целостность формации низкая! Сжаться.",
|
|
"Стройтесь, пилоты! Мы слишком разбросаны.",
|
|
"Сомкните ряды! Целостность формации под угрозой.",
|
|
"Вернитесь в формацию, ребята! Мы уязвимы.",
|
|
},
|
|
airTargetDetected = {
|
|
"Бандит %s на %.0f градусов, %.1f %s, ангелы %.0f (%s)!",
|
|
"Вражеский самолет %s: %.0f градусов, %.1f %s, высота %.0f (%s).",
|
|
"Боги %s на %.0f часов, %.1f %s, ангелы %.0f (%s).",
|
|
"Враждебный контакт %s: %.0f градусов, %.1f %s, %.0f ангелов (%s).",
|
|
"Бандит входящий %s: %.0f градусов, %.1f %s, ангелы %.0f (%s).",
|
|
},
|
|
groundTargetDetected = {
|
|
"%s контакт: %s %s на %.0f градусов, %.1f %s.",
|
|
"Наземная угроза: %s %s %s замечен на %.0f градусов, %.1f %s.",
|
|
"%s единицы обнаружены: %s %s, %.0f градусов, %.1f %s.",
|
|
"Вражеская земля: %s %s %s на пеленге %.0f, %.1f %s расстояние.",
|
|
},
|
|
dogfightEngaged = {
|
|
"Вступил!",
|
|
"Талли хо! Воздушный бой начат.",
|
|
"Бандит вступил! Бой идет.",
|
|
"Воздушный бой! Оружие горячо.",
|
|
},
|
|
dogfightConcluded = {
|
|
"Воздушный бой завершен.",
|
|
"Бой окончен. Чисто.",
|
|
"Воздушный бой закончен. Отбой.",
|
|
"Вступление завершено.",
|
|
},
|
|
underFire = {
|
|
"Под огнем! Разрывай %s!%s",
|
|
"Получаем попадания! Уклоняйся %s!%s",
|
|
"Выстрелы! Поверни %s сейчас!%s",
|
|
"Входящий! Разрывай %s!%s",
|
|
},
|
|
closeFlyingCompliment = {
|
|
"Отличный полет! Формация концом к концу.",
|
|
"Эй, хорошо! Это тесный полет.",
|
|
"Отличная работа! Как близко!",
|
|
"Полегче там, тигр, я тебя так хорошо не знаю!",
|
|
"Впечатляюще! Законцовка к законцовке.",
|
|
"Плавные движения! Полет в тесных четвертях.",
|
|
"Черт, как тесно! Хороший полет.",
|
|
"Святые небеса, как близко! Отлично.",
|
|
"Уф, это было близко! Хорошая работа ручкой.",
|
|
"Ты приклеен к моему крылу! Отличный полет.",
|
|
},
|
|
headOnWarning = {
|
|
"Уф! Это было близко!",
|
|
"Воу! Почти лобовое!",
|
|
"Близкий вызов! Осторожнее с этим проходом.",
|
|
"Черт, это было тесно! Будь осторожен.",
|
|
"Святые небеса, почти столкнулись!",
|
|
},
|
|
markerSet = {
|
|
"Маркер установлен на %s.",
|
|
"Маркеры теперь %s.",
|
|
"Тип маркера изменен на %s.",
|
|
},
|
|
dogfightAssistToggle = {
|
|
"Помощь в воздушном бою %s.",
|
|
"Ассистенс воздушного боя %s.",
|
|
},
|
|
activeMessagingToggle = {
|
|
"Активные сообщения %s.",
|
|
"Живые оповещения %s.",
|
|
},
|
|
airScanningToggle = {
|
|
"Воздушное сканирование %s.",
|
|
"Воздушное обнаружение %s.",
|
|
},
|
|
groundScanningToggle = {
|
|
"Наземное сканирование %s.",
|
|
"Наземное обнаружение %s.",
|
|
},
|
|
alertFrequencyToggle = {
|
|
"Частота оповещений установлена на %s.",
|
|
"Оповещения теперь %s.",
|
|
},
|
|
summaryCooldown = {
|
|
"Резюме на перезарядке.",
|
|
"Подожди немного для другого резюме.",
|
|
},
|
|
noThreats = {
|
|
"Нет активных угроз.",
|
|
"Все чисто.",
|
|
"Ситуация нормальная.",
|
|
},
|
|
systemActive = "WWII Интуиция Пилота активна! Используйте меню F10 для настроек.",
|
|
logLevelSet = "Уровень логирования установлен на: %s",
|
|
distanceUnitsSet = "Единицы расстояния установлены на %s.",
|
|
targetNotAvailable = "Цель %d недоступна.",
|
|
markedTarget = "Цель %d отмечена.",
|
|
noIlluminationFlares = "Осветительные ракеты закончились. Приземлитесь на дружественную авиабазу для перевооружения.",
|
|
illuminationNotReady = "Освещение не готово. Подождите %d секунд.",
|
|
cannotDeterminePosition = "Невозможно определить позицию для сброса осветительной ракеты.",
|
|
cannotDetermineAltitude = "Невозможно определить высоту для сброса осветительной ракеты.",
|
|
illuminationDropped = "Осветительная ракета сброшена на вашей позиции. (%d осталось)",
|
|
cannotDetermineTargetPosition = "Невозможно определить позицию цели для сброса осветительной ракеты.",
|
|
errorCouldNotDeterminePosition = "Ошибка: Не удалось определить вашу позицию.",
|
|
illuminationDroppedOnTarget = "Осветительная ракета сброшена на Цель %d (%.0f°, %.1f%s). (%d осталось)",
|
|
illuminationRearmed = "Осветительные ракеты перезаряжены: %d доступно.",
|
|
noGroundTargets = "Вражеские наземные цели в радиусе не обнаружены.",
|
|
selectTargetFromMenu = "Выберите цель из меню, чтобы отметить её.",
|
|
targetInfo = "Цель %d: %s %s %s, Пеленг %.0f, Дальность %.1f %s",
|
|
enabled = "включено",
|
|
disabled = "отключено",
|
|
hot = "горячий",
|
|
cold = "холодный",
|
|
closing = "сближение",
|
|
opening = "расхождение",
|
|
left = "слева",
|
|
right = "справа",
|
|
above = "сверху",
|
|
below = "снизу",
|
|
high = "высокий",
|
|
low = "низкий",
|
|
normal = "нормальный",
|
|
quiet = "тихий",
|
|
verbose = "подробный",
|
|
menu = {
|
|
mainTitle = "Интуиция Пилота WWII",
|
|
dogfightAssist = "Помощь Воздушный Бой",
|
|
enable = "Включить",
|
|
disable = "Отключить",
|
|
markerType = "Тип Маркера",
|
|
smoke = "Дым",
|
|
flare = "Ракета",
|
|
red = "Красный",
|
|
green = "Зеленый",
|
|
blue = "Синий",
|
|
white = "Белый",
|
|
none = "Нет",
|
|
airScanning = "Воздушное Сканирование",
|
|
groundScanning = "Наземное Сканирование",
|
|
groundTargeting = "Наземное Наведение",
|
|
scanForTargets = "Сканировать Цели",
|
|
markTarget = "Отметить Цель",
|
|
target = "Цель %d",
|
|
illumination = "Освещение (%d осталось)",
|
|
dropAtMyPosition = "Сбросить на Моей Позиции",
|
|
dropOnTarget = "Сбросить на Цель",
|
|
alertFrequency = "Частота Оповещений",
|
|
normalFreq = "Нормальная",
|
|
quietFreq = "Тихая",
|
|
verboseFreq = "Подробная",
|
|
summary = "Резюме",
|
|
brief = "Краткое",
|
|
detailed = "Подробное",
|
|
settingsAndGuides = "Настройки и Руководства",
|
|
distanceUnits = "Единицы Расстояния",
|
|
milesNautical = "Мили (Морские)",
|
|
kilometers = "Километры",
|
|
playerGuide = "Руководство Игрока",
|
|
systemOverview = "Обзор Системы",
|
|
detectionRanges = "Дальности Обнаружения",
|
|
dogfightAssistHelp = "Помощь Воздушный Бой",
|
|
groundTargetingHelp = "Помощь Наземное Наведение",
|
|
formationTips = "Советы по Формации",
|
|
illuminationHelp = "Помощь Освещение",
|
|
logLevel = "Уровень Логирования",
|
|
language = "Language / Sprache / Langue",
|
|
english = "English (Английский)",
|
|
german = "Deutsch (Немецкий)",
|
|
french = "Français (Французский)",
|
|
spanish = "Español (Испанский)",
|
|
russian = "Русский (Russian)",
|
|
}
|
|
}
|
|
}
|
|
|
|
-- Backward compatibility: keep old reference for any direct usage
|
|
PILOT_INTUITION_MESSAGES = PILOT_INTUITION_LANGUAGES.EN
|
|
|
|
-- Pilot Intuition Class
|
|
PilotIntuition = {
|
|
ClassName = "PilotIntuition",
|
|
players = {}, -- Table to track per-player data
|
|
trackedGroundTargets = {}, -- Global table to track ground targets
|
|
lastMessageTime = 0,
|
|
menu = nil,
|
|
enabled = true,
|
|
summaryScheduler = nil,
|
|
lastDeepCleanup = 0, -- For periodic deep cleanup
|
|
playerMenus = {}, -- Track created player menus to avoid duplicates
|
|
}
|
|
|
|
-- Helper function to format distance based on player preference
|
|
function PilotIntuition:FormatDistance(distanceMeters, playerKey)
|
|
local distanceUnit = PILOT_INTUITION_CONFIG.distanceUnit
|
|
|
|
-- Check for player-specific preference
|
|
if playerKey and self.players[playerKey] and self.players[playerKey].distanceUnit then
|
|
distanceUnit = self.players[playerKey].distanceUnit
|
|
end
|
|
|
|
if distanceUnit == "mi" then
|
|
-- Convert to nautical miles (1 nautical mile = 1852 meters)
|
|
local distanceNM = distanceMeters / 1852
|
|
return distanceNM, "mi"
|
|
else
|
|
-- Default to kilometers
|
|
local distanceKM = distanceMeters / 1000
|
|
return distanceKM, "km"
|
|
end
|
|
end
|
|
|
|
-- Helper function to get player's language
|
|
function PilotIntuition:GetPlayerLanguage(playerKey)
|
|
if playerKey and self.players[playerKey] and self.players[playerKey].language then
|
|
return self.players[playerKey].language
|
|
end
|
|
return PILOT_INTUITION_CONFIG.defaultLanguage
|
|
end
|
|
|
|
-- Helper function to get translated text (non-array messages)
|
|
function PilotIntuition:GetText(textKey, playerKey)
|
|
local lang = self:GetPlayerLanguage(playerKey)
|
|
local langTable = PILOT_INTUITION_LANGUAGES[lang] or PILOT_INTUITION_LANGUAGES.EN
|
|
|
|
-- Check if it's a nested key (e.g., "menu.mainTitle")
|
|
if string.find(textKey, "%.") then
|
|
local parts = {}
|
|
for part in string.gmatch(textKey, "[^%.]+") do
|
|
table.insert(parts, part)
|
|
end
|
|
|
|
local value = langTable
|
|
for _, part in ipairs(parts) do
|
|
if value and type(value) == "table" then
|
|
value = value[part]
|
|
else
|
|
return textKey -- Fallback to key if not found
|
|
end
|
|
end
|
|
return value or textKey
|
|
end
|
|
|
|
return langTable[textKey] or textKey
|
|
end
|
|
|
|
-- Get random message with player language support
|
|
function PilotIntuition:GetRandomMessage(messageType, params, playerKey)
|
|
local lang = self:GetPlayerLanguage(playerKey)
|
|
local langTable = PILOT_INTUITION_LANGUAGES[lang] or PILOT_INTUITION_LANGUAGES.EN
|
|
local messages = langTable[messageType]
|
|
|
|
if not messages then
|
|
env.info("PilotIntuition: No messages for type " .. tostring(messageType) .. " in language " .. lang)
|
|
return "Message type not found: " .. tostring(messageType)
|
|
end
|
|
|
|
-- Handle both array messages and single string messages
|
|
local msg
|
|
if type(messages) == "table" and #messages > 0 then
|
|
msg = messages[math.random(#messages)]
|
|
elseif type(messages) == "string" then
|
|
msg = messages
|
|
else
|
|
return "Invalid message format for: " .. tostring(messageType)
|
|
end
|
|
|
|
env.info("PilotIntuition: Selected message: " .. msg)
|
|
if params then
|
|
msg = string.format(msg, unpack(params))
|
|
env.info("PilotIntuition: Formatted message: " .. msg)
|
|
end
|
|
return msg
|
|
end
|
|
|
|
function PilotIntuition:New()
|
|
PILog(LOG_INFO, "PilotIntuition: System starting")
|
|
local self = BASE:Inherit(self, BASE:New())
|
|
self.players = {}
|
|
self.trackedGroundTargets = {}
|
|
self.lastMessageTime = timer.getTime()
|
|
self.playerMenus = {} -- Clear cached menus on initialization
|
|
self:SetupMenu()
|
|
self:WireEventHandlers()
|
|
self:StartScheduler()
|
|
PILog(LOG_INFO, "PilotIntuition: System initialized")
|
|
return self
|
|
end
|
|
|
|
-- Helper function to get current unit for a clientName
|
|
function PilotIntuition:GetUnitForClient(clientName)
|
|
if not clientName then
|
|
env.info("PilotIntuition: GetUnitForClient - no clientName provided")
|
|
return nil
|
|
end
|
|
|
|
env.info("PilotIntuition: GetUnitForClient looking for: " .. tostring(clientName))
|
|
|
|
-- Try _DATABASE.CLIENTS first
|
|
if _DATABASE and _DATABASE.CLIENTS and _DATABASE.CLIENTS[clientName] then
|
|
local unitName = _DATABASE.CLIENTS[clientName].UnitName
|
|
env.info("PilotIntuition: Found in _DATABASE.CLIENTS, unitName: " .. tostring(unitName))
|
|
if unitName then
|
|
local unit = UNIT:FindByName(unitName)
|
|
if unit then
|
|
env.info("PilotIntuition: UNIT:FindByName succeeded, IsAlive: " .. tostring(unit:IsAlive()))
|
|
if unit:IsAlive() then
|
|
return unit
|
|
end
|
|
else
|
|
env.info("PilotIntuition: UNIT:FindByName returned nil")
|
|
end
|
|
end
|
|
else
|
|
env.info("PilotIntuition: clientName not found in _DATABASE.CLIENTS")
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Helper function to get player data key from a unit (handles aliasing)
|
|
function PilotIntuition:GetPlayerDataKey(playerUnit)
|
|
if not playerUnit then return nil end
|
|
|
|
local unitName = playerUnit:GetName()
|
|
local playerName = playerUnit:GetPlayerName()
|
|
|
|
-- Try unit name first (clientName from _DATABASE.CLIENTS)
|
|
if self.players[unitName] then
|
|
return unitName
|
|
end
|
|
|
|
-- Try player name second (may be aliased to unit name)
|
|
if playerName and self.players[playerName] then
|
|
return playerName
|
|
end
|
|
|
|
-- Try to find player data by searching _DATABASE.CLIENTS for matching unit
|
|
if _DATABASE and _DATABASE.CLIENTS then
|
|
for clientName, clientData in pairs(_DATABASE.CLIENTS) do
|
|
if clientData and clientData.UnitName == unitName then
|
|
if self.players[clientName] then
|
|
return clientName
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Helper function to get all active player units
|
|
function PilotIntuition:GetActivePlayers()
|
|
local players = {}
|
|
local count = 0
|
|
|
|
-- Method 1: Try DCS coalition.getPlayers()
|
|
for _, coalitionId in pairs({coalition.side.RED, coalition.side.BLUE, coalition.side.NEUTRAL}) do
|
|
local success, coalPlayers = pcall(coalition.getPlayers, coalitionId)
|
|
if success and coalPlayers then
|
|
for _, playerId in pairs(coalPlayers) do
|
|
if playerId and type(playerId) == "string" and playerId ~= "" then
|
|
local success2, unit = pcall(Unit.getByName, playerId)
|
|
if success2 and unit and unit:isExist() and unit:isActive() then
|
|
local unitWrapper = UNIT:Find(unit)
|
|
if unitWrapper then
|
|
local playerName = unitWrapper:GetPlayerName() or unitWrapper:GetName()
|
|
players[playerName] = unitWrapper
|
|
count = count + 1
|
|
env.info("PilotIntuition: Found player via coalition.getPlayers: " .. playerName)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Method 2: Try _DATABASE.CLIENTS
|
|
if _DATABASE and _DATABASE.CLIENTS then
|
|
local clientCount = 0
|
|
for clientName, clientData in pairs(_DATABASE.CLIENTS) do
|
|
clientCount = clientCount + 1
|
|
PILog(LOG_TRACE, "PilotIntuition: Checking _DATABASE.CLIENTS entry #" .. clientCount .. ": " .. tostring(clientName))
|
|
if clientData then
|
|
PILog(LOG_TRACE, " - UnitName: " .. tostring(clientData.UnitName))
|
|
end
|
|
if not players[clientName] and clientData and clientData.UnitName then
|
|
local unit = UNIT:FindByName(clientData.UnitName)
|
|
PILog(LOG_TRACE, " - UNIT:FindByName result: " .. tostring(unit))
|
|
if unit then
|
|
PILog(LOG_TRACE, " - Unit:IsAlive: " .. tostring(unit:IsAlive()))
|
|
end
|
|
if unit and unit:IsAlive() then
|
|
players[clientName] = unit
|
|
count = count + 1
|
|
PILog(LOG_DEBUG, "PilotIntuition: Found player via _DATABASE.CLIENTS: " .. clientName)
|
|
end
|
|
end
|
|
end
|
|
PILog(LOG_DEBUG, "PilotIntuition: _DATABASE.CLIENTS had " .. clientCount .. " total entries")
|
|
else
|
|
PILog(LOG_ERROR, "PilotIntuition: _DATABASE.CLIENTS not available!")
|
|
end
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: GetActivePlayers found " .. count .. " players")
|
|
return players
|
|
end
|
|
|
|
function PilotIntuition:ScanTargets()
|
|
PILog(LOG_DEBUG, "PilotIntuition: ScanTargets called")
|
|
if not self.enabled then
|
|
return
|
|
end
|
|
local startTime = timer.getTime() -- Profiling start
|
|
|
|
local activePlayers = self:GetActivePlayers()
|
|
local activeClients = {}
|
|
local activePlayerNames = {}
|
|
|
|
-- Build active client list from the players we found
|
|
for clientName, unit in pairs(activePlayers) do
|
|
if unit and unit:IsAlive() then
|
|
local client = unit:GetClient()
|
|
local pos = unit:GetCoordinate()
|
|
local actualPlayerName = unit:GetPlayerName() or unit:GetName()
|
|
activeClients[#activeClients + 1] = { client = client, unit = unit, name = clientName, pos = pos, coalition = unit:GetCoalition() }
|
|
activePlayerNames[clientName] = true
|
|
|
|
-- Create player data if it doesn't exist (using clientName as primary key)
|
|
if not self.players[clientName] then
|
|
self.players[clientName] = {
|
|
trackedAirTargets = {},
|
|
lastMessageTime = timer.getTime(),
|
|
lastFormationMessageTime = 3, -- Separate cooldown for formation join/leave messages
|
|
lastDogfightTime = 0, -- For multi-bandit tactical picture cooldown
|
|
wingmenList = {},
|
|
formationWarned = false,
|
|
lastFormationChangeTime = 0,
|
|
lastConclusionTime = 0,
|
|
trackedGroundTargets = {},
|
|
dogfightAssist = PILOT_INTUITION_CONFIG.dogfightAssistEnabled,
|
|
lastDogfightAssistTime = 0,
|
|
markerType = PILOT_INTUITION_CONFIG.markerType,
|
|
lastSpeed = 0,
|
|
primaryTarget = nil,
|
|
lastPrimaryTargetBearing = nil,
|
|
lastSummaryTime = 0,
|
|
hasBeenWelcomed = false,
|
|
lastComplimentTime = 0,
|
|
lastHeadOnWarningTime = 0,
|
|
enableAirScanning = PILOT_INTUITION_CONFIG.enableAirScanning,
|
|
enableGroundScanning = PILOT_INTUITION_CONFIG.enableGroundScanning,
|
|
scannedGroundTargets = {}, -- List of recently scanned ground targets for selection
|
|
cachedWingmen = 0, -- Cached wingmen count
|
|
lastIlluminationTime = 0, -- Cooldown tracking for illumination flares
|
|
illuminationFlares = PILOT_INTUITION_CONFIG.illuminationFlaresDefault, -- Remaining illumination flares
|
|
lastWingmenUpdate = 0,
|
|
previousWingmen = 0, -- For formation change detection
|
|
frequencyMultiplier = 1.0, -- Per-player alert frequency: 1.0=normal, 2.0=quiet, 0.5=verbose
|
|
threateningBandits = {}, -- Reusable table for detected threats
|
|
lastMultipleBanditsWarningTime = 0, -- Cooldown for "Multiple bandits in vicinity!" message
|
|
distanceUnit = PILOT_INTUITION_CONFIG.distanceUnit, -- Player's distance unit preference
|
|
language = PILOT_INTUITION_CONFIG.defaultLanguage, -- Player's language preference
|
|
}
|
|
end
|
|
|
|
-- Menu creation now handled by Birth event only
|
|
end
|
|
end
|
|
|
|
-- If no active players, skip the scan
|
|
if #activeClients == 0 then
|
|
env.info("PilotIntuition: No active players, skipping scan")
|
|
return
|
|
end
|
|
|
|
-- Cache wingmen for each player (updated every scan for simplicity)
|
|
for _, info in ipairs(activeClients) do
|
|
local playerName = info.name
|
|
local playerData = self.players[playerName]
|
|
if playerData then
|
|
playerData.cachedWingmen = 0
|
|
|
|
-- Count other players in formation
|
|
for _, otherInfo in ipairs(activeClients) do
|
|
local u = otherInfo.unit
|
|
if u and u:IsAlive() and u:GetCoalition() == info.unit:GetCoalition() and u:GetName() ~= info.unit:GetName() then
|
|
local dist = info.pos:Get2DDistance(otherInfo.pos)
|
|
if dist <= PILOT_INTUITION_CONFIG.formationRange then
|
|
playerData.cachedWingmen = playerData.cachedWingmen + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Count AI wingmen if enabled (aircraft only, same coalition)
|
|
if PILOT_INTUITION_CONFIG.countAIWingmen then
|
|
PILog(LOG_INFO, "PilotIntuition: Checking AI wingmen for player " .. playerName)
|
|
|
|
-- Get all friendly AIR units in formation range
|
|
local playerCoalition = info.unit:GetCoalition()
|
|
local allFriendlyUnits = coalition.getGroups(playerCoalition, Group.Category.AIRPLANE)
|
|
local aiCount = 0
|
|
|
|
for _, dcsGroup in ipairs(allFriendlyUnits) do
|
|
local mooseGroup = GROUP:Find(dcsGroup)
|
|
if mooseGroup and mooseGroup:IsAlive() then
|
|
local groupUnits = mooseGroup:GetUnits()
|
|
for _, aiUnit in pairs(groupUnits) do
|
|
if aiUnit and aiUnit:IsAlive() and aiUnit:GetName() ~= info.unit:GetName() then
|
|
-- Verify it's an aircraft (double-check since we filtered by category)
|
|
if aiUnit:IsAir() then
|
|
-- Check distance first (cheaper than GetPlayerName)
|
|
local aiCoord = aiUnit:GetCoordinate()
|
|
if aiCoord then
|
|
local dist = info.pos:Get2DDistance(aiCoord)
|
|
if dist <= PILOT_INTUITION_CONFIG.formationRange then
|
|
-- Now check if it's AI (not a player)
|
|
local playerName = aiUnit:GetPlayerName()
|
|
local isAI = not playerName or playerName == ""
|
|
if isAI then
|
|
-- Add AI with multiplier (can be fractional)
|
|
playerData.cachedWingmen = playerData.cachedWingmen + PILOT_INTUITION_CONFIG.aiWingmenMultiplier
|
|
aiCount = aiCount + 1
|
|
PILog(LOG_INFO, "PilotIntuition: Added AI wingman " .. aiUnit:GetName() .. " at " .. math.floor(dist) .. "m, total: " .. playerData.cachedWingmen)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: Total AI wingmen in formation: " .. aiCount)
|
|
end
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: Final wingmen count for " .. playerName .. ": " .. playerData.cachedWingmen)
|
|
playerData.lastWingmenUpdate = timer.getTime()
|
|
end
|
|
end
|
|
|
|
-- Send welcome message to new players
|
|
for _, info in ipairs(activeClients) do
|
|
local playerName = info.name
|
|
if self.players[playerName] and not self.players[playerName].hasBeenWelcomed and PILOT_INTUITION_CONFIG.showWelcomeMessage then
|
|
MESSAGE:New(self:GetRandomMessage("welcome", nil, playerName), 10):ToClient(info.client)
|
|
self.players[playerName].hasBeenWelcomed = true
|
|
end
|
|
end
|
|
|
|
-- Clean up stale player data
|
|
for playerName, _ in pairs(self.players) do
|
|
if not activePlayerNames[playerName] then
|
|
self.players[playerName] = nil
|
|
end
|
|
end
|
|
|
|
-- Build enemy unit lists once per coalition for the scan
|
|
local enemyAirByCoalition = {}
|
|
enemyAirByCoalition[coalition.side.BLUE] = {}
|
|
enemyAirByCoalition[coalition.side.RED] = {}
|
|
PILog(LOG_INFO, "PilotIntuition: Building enemy air unit list from _DATABASE.UNITS...")
|
|
local enemyCount = 0
|
|
|
|
-- Iterate directly through Moose's already-populated database
|
|
for unitName, unitObj in pairs(_DATABASE.UNITS) do
|
|
if unitObj and unitObj:IsAlive() and unitObj:IsAir() then
|
|
local c = unitObj:GetCoalition()
|
|
if c == coalition.side.BLUE or c == coalition.side.RED then
|
|
enemyAirByCoalition[c][#enemyAirByCoalition[c] + 1] = unitObj
|
|
enemyCount = enemyCount + 1
|
|
PILog(LOG_INFO, "PilotIntuition: Found enemy air unit: " .. unitName .. " (coalition " .. c .. ")")
|
|
end
|
|
end
|
|
end
|
|
PILog(LOG_INFO, "PilotIntuition: Total enemy air units found: " .. enemyCount)
|
|
|
|
local enemyGroundByCoalition = {}
|
|
enemyGroundByCoalition[coalition.side.BLUE] = {}
|
|
enemyGroundByCoalition[coalition.side.RED] = {}
|
|
|
|
-- Iterate directly through Moose's already-populated database for ground groups
|
|
for groupName, groupObj in pairs(_DATABASE.GROUPS) do
|
|
if groupObj and groupObj:IsAlive() and groupObj:IsGround() then
|
|
local c = groupObj:GetCoalition()
|
|
if c == coalition.side.BLUE or c == coalition.side.RED then
|
|
enemyGroundByCoalition[c][#enemyGroundByCoalition[c] + 1] = groupObj
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Now scan per active client
|
|
for _, info in ipairs(activeClients) do
|
|
local client = info.client
|
|
local unit = info.unit
|
|
local playerName = info.name
|
|
local playerData = self.players[playerName]
|
|
if playerData and unit and unit:IsAlive() then
|
|
-- Determine enemy lists for this player
|
|
local playerCoal = unit:GetCoalition()
|
|
local enemyCoal = (playerCoal == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
|
|
PILog(LOG_INFO, "PilotIntuition: Player " .. playerName .. " coalition: " .. playerCoal .. ", enemy coalition: " .. enemyCoal)
|
|
PILog(LOG_INFO, "PilotIntuition: Air scanning enabled: " .. tostring(playerData.enableAirScanning))
|
|
PILog(LOG_INFO, "PilotIntuition: Enemy air units for this player: " .. #(enemyAirByCoalition[enemyCoal] or {}))
|
|
if playerData.enableAirScanning then
|
|
self:ScanAirTargetsForPlayer(unit, playerData, client, activeClients, enemyAirByCoalition[enemyCoal])
|
|
end
|
|
if playerData.enableGroundScanning then
|
|
self:ScanGroundTargetsForPlayer(unit, client, activeClients, enemyGroundByCoalition[enemyCoal])
|
|
end
|
|
self:CheckCloseFlyingForPlayer(unit, playerData, client, activeClients)
|
|
end
|
|
end
|
|
|
|
-- Prune dead ground targets globally
|
|
for id, data in pairs(self.trackedGroundTargets) do
|
|
if not data.group:IsAlive() then
|
|
self.trackedGroundTargets[id] = nil
|
|
end
|
|
end
|
|
|
|
-- Periodic deep cleanup every 60 seconds
|
|
local now = timer.getTime()
|
|
if not self.lastDeepCleanup or (now - self.lastDeepCleanup) > 60 then
|
|
self:DeepCleanup()
|
|
self.lastDeepCleanup = now
|
|
end
|
|
|
|
local endTime = timer.getTime() -- Profiling end
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: ScanTargets completed in %.3f seconds", endTime - startTime))
|
|
end
|
|
|
|
function PilotIntuition:DeepCleanup()
|
|
env.info("PilotIntuition: Performing deep cleanup")
|
|
-- Force remove any stale tracked targets
|
|
for playerName, playerData in pairs(self.players) do
|
|
for id, data in pairs(playerData.trackedAirTargets) do
|
|
if not data.unit or not data.unit:IsAlive() then
|
|
playerData.trackedAirTargets[id] = nil
|
|
end
|
|
end
|
|
for id, _ in pairs(playerData.trackedGroundTargets) do
|
|
if not self.trackedGroundTargets[id] or not self.trackedGroundTargets[id].group:IsAlive() then
|
|
playerData.trackedGroundTargets[id] = nil
|
|
end
|
|
end
|
|
end
|
|
-- Clean global ground targets
|
|
for id, data in pairs(self.trackedGroundTargets) do
|
|
if not data.group:IsAlive() then
|
|
self.trackedGroundTargets[id] = nil
|
|
end
|
|
end
|
|
env.info("PilotIntuition: Deep cleanup completed")
|
|
end
|
|
|
|
-- Setup the mission and per-player menus for pilot intuition toggles
|
|
function PilotIntuition:SetupMenu()
|
|
PILog(LOG_INFO, "PilotIntuition: SetupMenu called - creating menus for existing players")
|
|
|
|
-- Create menus for any players already in the mission (e.g., mission editor start)
|
|
SCHEDULER:New(nil, function()
|
|
local activePlayers = self:GetActivePlayers()
|
|
for clientName, unit in pairs(activePlayers) do
|
|
if unit and unit:IsAlive() then
|
|
local group = unit:GetGroup()
|
|
if group then
|
|
local groupName = group:GetName()
|
|
if not self.playerMenus[groupName] then
|
|
PILog(LOG_INFO, "PilotIntuition: Creating menu for existing player group: " .. groupName)
|
|
self.playerMenus[groupName] = self:BuildGroupMenus(group)
|
|
MESSAGE:New(self:GetText("systemActive", group:GetUnit(1):GetPlayerName()), 10):ToGroup(group)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end, {}, 2) -- Wait 2 seconds for mission to fully load
|
|
end
|
|
|
|
-- BuildGroupMenus: Creates full menu tree for a player group (CTLD-style)
|
|
function PilotIntuition:BuildGroupMenus(group)
|
|
PILog(LOG_INFO, "PilotIntuition: BuildGroupMenus called for group: " .. group:GetName())
|
|
|
|
-- Verify group is valid and has units
|
|
if not group or not group:IsAlive() then
|
|
PILog(LOG_ERROR, "PilotIntuition: Cannot create menu - group is nil or not alive")
|
|
return nil
|
|
end
|
|
|
|
-- Get first unit in group for callbacks and language
|
|
local unit = group:GetUnit(1)
|
|
if not unit then
|
|
PILog(LOG_ERROR, "PilotIntuition: WARNING - No unit found in group")
|
|
return nil
|
|
end
|
|
|
|
local playerKey = self:GetPlayerDataKey(unit)
|
|
|
|
-- Create the main menu for this group with translated text
|
|
local playerSubMenu = MENU_GROUP:New(group, self:GetText("menu.mainTitle", playerKey))
|
|
PILog(LOG_INFO, "PilotIntuition: Main menu created for group: " .. group:GetName())
|
|
|
|
-- Dogfight Assist submenu
|
|
local dogfightMenu = MENU_GROUP:New(group, self:GetText("menu.dogfightAssist", playerKey), playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.enable", playerKey), dogfightMenu, function() self:MenuSetPlayerDogfightAssist(unit, true) end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.disable", playerKey), dogfightMenu, function() self:MenuSetPlayerDogfightAssist(unit, false) end)
|
|
|
|
-- Marker Type submenu
|
|
local markerMenu = MENU_GROUP:New(group, self:GetText("menu.markerType", playerKey), playerSubMenu)
|
|
local smokeMenu = MENU_GROUP:New(group, self:GetText("menu.smoke", playerKey), markerMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.red", playerKey), smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_red") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.green", playerKey), smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_green") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.blue", playerKey), smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_blue") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.white", playerKey), smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_white") end)
|
|
local flareMenu = MENU_GROUP:New(group, self:GetText("menu.flare", playerKey), markerMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.red", playerKey), flareMenu, function() self:MenuSetPlayerMarker(unit, "flare_red") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.green", playerKey), flareMenu, function() self:MenuSetPlayerMarker(unit, "flare_green") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.white", playerKey), flareMenu, function() self:MenuSetPlayerMarker(unit, "flare_white") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.none", playerKey), markerMenu, function() self:MenuSetPlayerMarker(unit, "none") end)
|
|
|
|
-- Air scanning submenu
|
|
local airScanMenu = MENU_GROUP:New(group, self:GetText("menu.airScanning", playerKey), playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.enable", playerKey), airScanMenu, function() self:MenuSetPlayerAirScanning(unit, true) end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.disable", playerKey), airScanMenu, function() self:MenuSetPlayerAirScanning(unit, false) end)
|
|
|
|
-- Ground scanning submenu
|
|
local groundScanMenu = MENU_GROUP:New(group, self:GetText("menu.groundScanning", playerKey), playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.enable", playerKey), groundScanMenu, function() self:MenuSetPlayerGroundScanning(unit, true) end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.disable", playerKey), groundScanMenu, function() self:MenuSetPlayerGroundScanning(unit, false) end)
|
|
|
|
-- Ground targeting submenu
|
|
local groundMenu = MENU_GROUP:New(group, self:GetText("menu.groundTargeting", playerKey), playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.scanForTargets", playerKey), groundMenu, function() self:MenuScanGroundTargets(unit) end)
|
|
local markMenu = MENU_GROUP:New(group, self:GetText("menu.markTarget", playerKey), groundMenu)
|
|
for i=1,5 do
|
|
local captureIndex = i
|
|
local targetLabel = string.format(self:GetText("menu.target", playerKey), i)
|
|
MENU_GROUP_COMMAND:New(group, targetLabel, markMenu, function() self:MenuMarkGroundTarget(unit, captureIndex) end)
|
|
end
|
|
|
|
-- Illumination submenu with dynamic count display
|
|
local playerName = unit:GetPlayerName() or unit:GetName()
|
|
local flareCount = (self.players[playerName] and self.players[playerName].illuminationFlares) or PILOT_INTUITION_CONFIG.illuminationFlaresDefault
|
|
local illuLabel = string.format(self:GetText("menu.illumination", playerKey), flareCount)
|
|
local illuMenu = MENU_GROUP:New(group, illuLabel, playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.dropAtMyPosition", playerKey), illuMenu, function() self:MenuDropIlluminationAtPlayer(unit) end)
|
|
local illuTargetMenu = MENU_GROUP:New(group, self:GetText("menu.dropOnTarget", playerKey), illuMenu)
|
|
for i=1,5 do
|
|
local captureIndex = i
|
|
local targetLabel = string.format(self:GetText("menu.target", playerKey), i)
|
|
MENU_GROUP_COMMAND:New(group, targetLabel, illuTargetMenu, function() self:MenuDropIlluminationOnTarget(unit, captureIndex) end)
|
|
end
|
|
|
|
-- Alert frequency submenu
|
|
local freqMenu = MENU_GROUP:New(group, self:GetText("menu.alertFrequency", playerKey), playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.normalFreq", playerKey), freqMenu, function() self:MenuSetPlayerAlertFrequency(unit, "normal") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.quietFreq", playerKey), freqMenu, function() self:MenuSetPlayerAlertFrequency(unit, "quiet") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.verboseFreq", playerKey), freqMenu, function() self:MenuSetPlayerAlertFrequency(unit, "verbose") end)
|
|
|
|
-- Summary submenu
|
|
local summaryMenu = MENU_GROUP:New(group, self:GetText("menu.summary", playerKey), playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.brief", playerKey), summaryMenu, function() self:MenuSendPlayerSummary(unit, "brief") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.detailed", playerKey), summaryMenu, function() self:MenuSendPlayerSummary(unit, "detailed") end)
|
|
|
|
-- Admin Settings submenu (placed last)
|
|
local adminMenu = MENU_GROUP:New(group, self:GetText("menu.settingsAndGuides", playerKey), playerSubMenu)
|
|
|
|
-- Language selection submenu (under admin) - Keep multilingual for accessibility
|
|
local langMenu = MENU_GROUP:New(group, "Language / Sprache / Langue", adminMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.english", playerKey), langMenu, function() self:MenuSetPlayerLanguage(unit, "EN") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.german", playerKey), langMenu, function() self:MenuSetPlayerLanguage(unit, "DE") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.french", playerKey), langMenu, function() self:MenuSetPlayerLanguage(unit, "FR") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.spanish", playerKey), langMenu, function() self:MenuSetPlayerLanguage(unit, "ES") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.russian", playerKey), langMenu, function() self:MenuSetPlayerLanguage(unit, "RU") end)
|
|
|
|
-- Distance units submenu (under admin)
|
|
local distMenu = MENU_GROUP:New(group, self:GetText("menu.distanceUnits", playerKey), adminMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.milesNautical", playerKey), distMenu, function() self:MenuSetPlayerDistanceUnit(unit, "mi") end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.kilometers", playerKey), distMenu, function() self:MenuSetPlayerDistanceUnit(unit, "km") end)
|
|
|
|
-- Player Guide submenu
|
|
local guideMenu = MENU_GROUP:New(group, self:GetText("menu.playerGuide", playerKey), adminMenu)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.systemOverview", playerKey), guideMenu, function() self:ShowGuideOverview(group) end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.detectionRanges", playerKey), guideMenu, function() self:ShowGuideRanges(group) end)
|
|
MENU_GROUP_COMMAND:New(group, self:GetText("menu.formationTips", playerKey), guideMenu, function() self:ShowGuideFormation(group) end)
|
|
MENU_GROUP_COMMAND:New(group, "Environment Effects", guideMenu, function() self:ShowGuideEnvironment(group) end)
|
|
|
|
-- Log level submenu (global setting)
|
|
local logMenu = MENU_GROUP:New(group, self:GetText("menu.logLevel", playerKey), adminMenu)
|
|
MENU_GROUP_COMMAND:New(group, "Error Only", logMenu, function() self:SetLogLevel(1, group) end)
|
|
MENU_GROUP_COMMAND:New(group, "Info (Default)", logMenu, function() self:SetLogLevel(2, group) end)
|
|
MENU_GROUP_COMMAND:New(group, "Debug", logMenu, function() self:SetLogLevel(3, group) end)
|
|
MENU_GROUP_COMMAND:New(group, "Trace (Verbose)", logMenu, function() self:SetLogLevel(4, group) end)
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: Menu created successfully for group " .. group:GetName())
|
|
return playerSubMenu
|
|
end
|
|
|
|
-- Rebuild menu for a player group (used when illumination flare count changes)
|
|
function PilotIntuition:RebuildPlayerMenu(unit)
|
|
if not unit or not unit:IsAlive() then
|
|
PILog(LOG_ERROR, "PilotIntuition: RebuildPlayerMenu - invalid unit")
|
|
return
|
|
end
|
|
|
|
local group = unit:GetGroup()
|
|
if not group then
|
|
PILog(LOG_ERROR, "PilotIntuition: RebuildPlayerMenu - no group found")
|
|
return
|
|
end
|
|
|
|
local groupName = group:GetName()
|
|
PILog(LOG_DEBUG, "PilotIntuition: Rebuilding menu for group: " .. groupName)
|
|
|
|
-- Remove old menu if it exists
|
|
if self.playerMenus[groupName] then
|
|
local oldMenu = self.playerMenus[groupName]
|
|
if oldMenu and oldMenu.Remove then
|
|
oldMenu:Remove()
|
|
PILog(LOG_DEBUG, "PilotIntuition: Removed old menu for group: " .. groupName)
|
|
end
|
|
self.playerMenus[groupName] = nil
|
|
end
|
|
|
|
-- Build new menu
|
|
self.playerMenus[groupName] = self:BuildGroupMenus(group)
|
|
PILog(LOG_INFO, "PilotIntuition: Menu rebuilt successfully for group: " .. groupName)
|
|
end
|
|
|
|
function PilotIntuition:SetLogLevel(level, group)
|
|
PILOT_INTUITION_LOG_LEVEL = level
|
|
local labels = {"None", "Error", "Info", "Debug", "Trace"}
|
|
MESSAGE:New("Log level set to: " .. (labels[level + 1] or "Unknown"), 5):ToGroup(group)
|
|
PILog(LOG_INFO, "PilotIntuition: Log level changed to " .. level)
|
|
end
|
|
|
|
-- Player Guide functions
|
|
function PilotIntuition:ShowGuideOverview(group)
|
|
local msg = "WWII PILOT INTUITION - SYSTEM OVERVIEW\n\n"
|
|
msg = msg .. "This system simulates WWII-era pilot awareness without modern radar or labels.\n\n"
|
|
msg = msg .. "KEY FEATURES:\n"
|
|
msg = msg .. "• Air & Ground target detection based on visual ranges\n"
|
|
msg = msg .. "• Formation flying SIGNIFICANTLY boosts detection ranges\n"
|
|
msg = msg .. "• Dogfight assistance (merge warnings, tail alerts, energy state)\n"
|
|
msg = msg .. "• Optional smoke/flare markers for spotted targets\n"
|
|
msg = msg .. "• Environmental effects (night, weather reduce ranges)\n\n"
|
|
msg = msg .. "Use F10 menu to customize settings per pilot.\n"
|
|
msg = msg .. "Check other guide sections for detailed mechanics."
|
|
MESSAGE:New(msg, 20):ToGroup(group)
|
|
end
|
|
|
|
function PilotIntuition:ShowGuideRanges(group)
|
|
local airRange = PILOT_INTUITION_CONFIG.airDetectionRange
|
|
local groundRange = PILOT_INTUITION_CONFIG.groundDetectionRange
|
|
local maxMult = PILOT_INTUITION_CONFIG.maxMultiplier
|
|
local formRange = PILOT_INTUITION_CONFIG.formationRange
|
|
|
|
local msg = "DETECTION RANGES\n\n"
|
|
msg = msg .. "BASE RANGES (Solo Flight):\n"
|
|
msg = msg .. "• Air Targets: " .. airRange .. "m (~" .. math.floor(airRange / 1852 * 10) / 10 .. "nm)\n"
|
|
msg = msg .. "• Ground Targets: " .. groundRange .. "m (~" .. math.floor(groundRange / 1852 * 10) / 10 .. "nm)\n\n"
|
|
msg = msg .. "FORMATION MULTIPLIERS:\n"
|
|
msg = msg .. "Solo: 1.0x (base range)\n"
|
|
msg = msg .. "2-ship: 2.0x (double range)\n"
|
|
msg = msg .. "3-ship: 3.0x (triple range)\n"
|
|
msg = msg .. "4-ship: 4.0x (quadruple range)\n"
|
|
if maxMult >= 5 then
|
|
msg = msg .. "5-ship: 5.0x\n"
|
|
end
|
|
if maxMult >= 6 then
|
|
msg = msg .. "6+ ship: " .. maxMult .. ".0x (maximum)\n\n"
|
|
else
|
|
msg = msg .. maxMult .. "+ ship: " .. maxMult .. ".0x (maximum)\n\n"
|
|
end
|
|
msg = msg .. "EXAMPLE (Air Detection):\n"
|
|
local solo = math.floor(airRange / 1000)
|
|
local twoship = math.floor(airRange * 2 / 1000)
|
|
local fourship = math.floor(airRange * math.min(4, maxMult) / 1000)
|
|
msg = msg .. "Solo: " .. solo .. "km | 2-ship: " .. twoship .. "km | 4-ship: " .. fourship .. "km\n\n"
|
|
msg = msg .. "Formation = wingmen within " .. formRange .. "m (~" .. math.floor(formRange / 1852 * 10) / 10 .. "nm)"
|
|
MESSAGE:New(msg, 25):ToGroup(group)
|
|
end
|
|
|
|
function PilotIntuition:ShowGuideFormation(group)
|
|
local formRange = PILOT_INTUITION_CONFIG.formationRange
|
|
local maxMult = PILOT_INTUITION_CONFIG.maxMultiplier
|
|
local minWingmen = PILOT_INTUITION_CONFIG.minFormationWingmen
|
|
|
|
local msg = "FORMATION FLYING BENEFITS\n\n"
|
|
msg = msg .. "Formation flying is CRITICAL for situational awareness!\n\n"
|
|
msg = msg .. "FORMATION REQUIREMENTS:\n"
|
|
msg = msg .. "• Wingmen must be within " .. formRange .. "m (~" .. math.floor(formRange / 1852 * 10) / 10 .. "nm)\n"
|
|
msg = msg .. "• Same coalition (friendlies only)\n"
|
|
msg = msg .. "• Aircraft must be alive and player-controlled\n"
|
|
msg = msg .. "• Minimum wingmen for warnings: " .. minWingmen .. "\n\n"
|
|
msg = msg .. "DETECTION BOOST:\n"
|
|
msg = msg .. "Each additional wingman adds 1.0x to your detection range (up to " .. maxMult .. ".0x max).\n\n"
|
|
msg = msg .. "TACTICAL ADVANTAGE:\n"
|
|
local fourShipMult = math.min(4, maxMult)
|
|
msg = msg .. "• 4-ship formation sees " .. fourShipMult .. "x further than solo\n"
|
|
msg = msg .. "• Spot bandits before they spot you\n"
|
|
msg = msg .. "• Better ground reconnaissance coverage\n"
|
|
MESSAGE:New(msg, 25):ToGroup(group)
|
|
end
|
|
|
|
function PilotIntuition:ShowGuideEnvironment(group)
|
|
local nightMult = PILOT_INTUITION_CONFIG.nightDetectionMultiplier
|
|
local weatherMult = PILOT_INTUITION_CONFIG.badWeatherMultiplier
|
|
local mergeRange = PILOT_INTUITION_CONFIG.mergeRange
|
|
local tailRange = PILOT_INTUITION_CONFIG.tailWarningRange
|
|
local headOnRange = PILOT_INTUITION_CONFIG.headOnRange
|
|
local beamRange = PILOT_INTUITION_CONFIG.beamRange
|
|
local combinedMult = nightMult * weatherMult
|
|
|
|
local msg = "ENVIRONMENT EFFECTS\n\n"
|
|
msg = msg .. "Detection ranges are affected by conditions:\n\n"
|
|
msg = msg .. "NIGHT TIME:\n"
|
|
msg = msg .. "• " .. (nightMult * 100) .. "% detection range\n"
|
|
msg = msg .. "• Harder to spot targets in darkness\n"
|
|
msg = msg .. "• Formation flying still helps!\n\n"
|
|
msg = msg .. "BAD WEATHER:\n"
|
|
msg = msg .. "• " .. (weatherMult * 100) .. "% detection range\n"
|
|
msg = msg .. "• Rain, fog, clouds reduce visibility\n"
|
|
msg = msg .. "• Stacks with night penalty\n\n"
|
|
msg = msg .. "COMBINED EFFECTS:\n"
|
|
msg = msg .. "Night + Bad Weather = ~" .. math.floor(combinedMult * 100) .. "% of normal range.\n\n"
|
|
msg = msg .. "DOGFIGHT RANGES:\n"
|
|
msg = msg .. "• Merge: " .. mergeRange .. "m\n"
|
|
msg = msg .. "• Tail Warning: " .. tailRange .. "m\n"
|
|
msg = msg .. "• Head-On: " .. headOnRange .. "m\n"
|
|
msg = msg .. "• Beam: " .. beamRange .. "m\n\n"
|
|
msg = msg .. "These ranges are NOT affected by environment - they're visual merge distances."
|
|
MESSAGE:New(msg, 30):ToGroup(group)
|
|
end
|
|
|
|
-- WireEventHandlers: Set up Birth event handler (CTLD-style)
|
|
function PilotIntuition:WireEventHandlers()
|
|
PILog(LOG_INFO, "PilotIntuition: Wiring event handlers using EVENTHANDLER")
|
|
|
|
local handler = EVENTHANDLER:New()
|
|
handler:HandleEvent(EVENTS.Birth)
|
|
handler:HandleEvent(EVENTS.PlayerEnterUnit)
|
|
handler:HandleEvent(EVENTS.Shot)
|
|
handler:HandleEvent(EVENTS.Land)
|
|
|
|
local selfref = self
|
|
|
|
function handler:OnEventBirth(EventData)
|
|
if not EventData or not EventData.IniUnit then return end
|
|
local unit = EventData.IniUnit
|
|
if not unit or not unit:IsAlive() then return end
|
|
|
|
-- Only create menus for player-controlled units
|
|
local playerName = unit:GetPlayerName()
|
|
if not playerName then
|
|
PILog(LOG_TRACE, "PilotIntuition: Birth event for non-player unit, skipping menu creation")
|
|
return
|
|
end
|
|
|
|
local group = unit:GetGroup()
|
|
if not group then return end
|
|
local groupName = group:GetName()
|
|
|
|
-- Simple check: if menu exists for this group, skip
|
|
if selfref.playerMenus[groupName] then
|
|
PILog(LOG_DEBUG, "PilotIntuition: Menu already exists for group: " .. groupName)
|
|
return
|
|
end
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: Birth event - creating menu for player group: " .. groupName)
|
|
selfref.playerMenus[groupName] = selfref:BuildGroupMenus(group)
|
|
|
|
-- Send welcome message to group
|
|
MESSAGE:New("WWII Pilot Intuition active! Use F10 menu for settings.", 10):ToGroup(group)
|
|
end
|
|
|
|
function handler:OnEventPlayerEnterUnit(EventData)
|
|
if not EventData or not EventData.IniUnit then return end
|
|
local unit = EventData.IniUnit
|
|
if not unit or not unit:IsAlive() then return end
|
|
|
|
local group = unit:GetGroup()
|
|
if not group then return end
|
|
local groupName = group:GetName()
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: PlayerEnterUnit event for group: " .. groupName)
|
|
|
|
-- Add a small delay to ensure the unit is fully initialized in multiplayer
|
|
SCHEDULER:New(nil, function()
|
|
-- Check if menu already exists
|
|
if not selfref.playerMenus[groupName] then
|
|
PILog(LOG_INFO, "PilotIntuition: Creating menu for player group: " .. groupName)
|
|
selfref.playerMenus[groupName] = selfref:BuildGroupMenus(group)
|
|
MESSAGE:New("WWII Pilot Intuition active! Use F10 menu for settings.", 10):ToGroup(group)
|
|
else
|
|
PILog(LOG_DEBUG, "PilotIntuition: Menu already exists for group: " .. groupName)
|
|
end
|
|
end, {}, 1)
|
|
end
|
|
|
|
function handler:OnEventShot(EventData)
|
|
selfref:OnPlayerShot(EventData)
|
|
selfref:OnShotFired(EventData)
|
|
end
|
|
|
|
function handler:OnEventLand(EventData)
|
|
if not EventData or not EventData.IniUnit then return end
|
|
local unit = EventData.IniUnit
|
|
if not unit or not unit:IsAlive() then return end
|
|
|
|
-- Check if it's a player unit
|
|
local playerName = unit:GetPlayerName()
|
|
if not playerName then return end
|
|
|
|
local playerData = selfref.players[playerName]
|
|
if not playerData then return end
|
|
|
|
-- Check if landed at a friendly airbase
|
|
local place = EventData.Place
|
|
if place and place:getCoalition() == unit:GetCoalition() then
|
|
-- Rearm illumination flares
|
|
local prevCount = playerData.illuminationFlares or 0
|
|
playerData.illuminationFlares = PILOT_INTUITION_CONFIG.illuminationFlaresDefault
|
|
|
|
local client = unit:GetClient()
|
|
if client and prevCount < PILOT_INTUITION_CONFIG.illuminationFlaresDefault then
|
|
MESSAGE:New(string.format(self:GetText("illuminationRearmed", clientName), PILOT_INTUITION_CONFIG.illuminationFlaresDefault), 10):ToClient(client)
|
|
end
|
|
|
|
PILog(LOG_INFO, "PilotIntuition: Player " .. playerName .. " rearmed illumination flares at friendly airbase")
|
|
end
|
|
end
|
|
|
|
self.EventHandler = handler
|
|
PILog(LOG_INFO, "PilotIntuition: Event handlers wired successfully")
|
|
end
|
|
|
|
function PilotIntuition:OnPlayerEnterUnit(EventData)
|
|
env.info("PilotIntuition: OnPlayerEnterUnit event triggered")
|
|
if EventData and EventData.IniUnit then
|
|
local unit = EventData.IniUnit
|
|
if unit and unit:IsAlive() then
|
|
env.info("PilotIntuition: Player entered unit: " .. tostring(unit:GetName()))
|
|
-- Small delay to ensure unit is fully initialized
|
|
SCHEDULER:New(nil, function()
|
|
self:SetupPlayerMenus()
|
|
end, {}, 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:SetupPlayerMenus()
|
|
env.info("PilotIntuition: SetupPlayerMenus called")
|
|
|
|
local activePlayers = self:GetActivePlayers()
|
|
local playersFound = 0
|
|
|
|
for clientName, unit in pairs(activePlayers) do
|
|
playersFound = playersFound + 1
|
|
env.info("PilotIntuition: Processing player: " .. tostring(clientName))
|
|
|
|
if unit and unit:IsAlive() then
|
|
env.info("PilotIntuition: Unit is alive: " .. tostring(unit:GetName()))
|
|
|
|
-- Get the actual player name for aliasing
|
|
local actualPlayerName = unit:GetPlayerName() or unit:GetName()
|
|
|
|
-- Ensure player data exists (may have been created by ScanTargets)
|
|
if not self.players[clientName] then
|
|
self.players[clientName] = {
|
|
trackedAirTargets = {},
|
|
lastMessageTime = timer.getTime(),
|
|
lastDogfightTime = 0, -- For multi-bandit tactical picture cooldown
|
|
wingmenList = {},
|
|
formationWarned = false,
|
|
lastFormationChangeTime = 0,
|
|
lastConclusionTime = 0,
|
|
trackedGroundTargets = {},
|
|
dogfightAssist = PILOT_INTUITION_CONFIG.dogfightAssistEnabled,
|
|
lastDogfightAssistTime = 0,
|
|
markerType = PILOT_INTUITION_CONFIG.markerType,
|
|
lastSpeed = 0,
|
|
primaryTarget = nil,
|
|
lastPrimaryTargetBearing = nil,
|
|
lastSummaryTime = 0,
|
|
hasBeenWelcomed = false,
|
|
lastComplimentTime = 0,
|
|
lastHeadOnWarningTime = 0,
|
|
enableAirScanning = PILOT_INTUITION_CONFIG.enableAirScanning,
|
|
enableGroundScanning = PILOT_INTUITION_CONFIG.enableGroundScanning,
|
|
scannedGroundTargets = {},
|
|
cachedWingmen = 0,
|
|
lastWingmenUpdate = 0,
|
|
previousWingmen = 0,
|
|
frequencyMultiplier = 1.0,
|
|
threateningBandits = {}, -- Reusable table for detected threats
|
|
distanceUnit = PILOT_INTUITION_CONFIG.distanceUnit,
|
|
language = PILOT_INTUITION_CONFIG.defaultLanguage,
|
|
}
|
|
end
|
|
|
|
-- Create alias for actual player name
|
|
if actualPlayerName ~= clientName then
|
|
env.info("PilotIntuition: Creating player alias in SetupPlayerMenus: '" .. actualPlayerName .. "' -> '" .. clientName .. "'")
|
|
self.players[actualPlayerName] = self.players[clientName]
|
|
end
|
|
|
|
local playerGroup = unit:GetGroup()
|
|
if not playerGroup then
|
|
env.info("PilotIntuition: No group found for unit " .. tostring(unit:GetName()))
|
|
else
|
|
-- Skip if menu already created for this player
|
|
if self.playerMenus[clientName] then
|
|
env.info("PilotIntuition: Menu already exists for " .. clientName)
|
|
else
|
|
env.info("PilotIntuition: Creating menu for player " .. clientName)
|
|
|
|
-- Create the main menu for this player's group
|
|
local playerSubMenu = MENU_GROUP:New(playerGroup, "WWII Pilot Intuition")
|
|
local currentGroupName = playerGroup:GetName()
|
|
self.playerMenus[clientName] = {
|
|
menu = playerSubMenu,
|
|
groupName = currentGroupName
|
|
}
|
|
|
|
-- Dogfight Assist submenu
|
|
local dogfightMenu = MENU_GROUP:New(playerGroup, "Dogfight Assist", playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Enable", dogfightMenu, function() self:MenuSetPlayerDogfightAssist(unit, true) end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Disable", dogfightMenu, function() self:MenuSetPlayerDogfightAssist(unit, false) end)
|
|
|
|
-- Marker Type submenu
|
|
local markerMenu = MENU_GROUP:New(playerGroup, "Marker Type", playerSubMenu)
|
|
local smokeMenu = MENU_GROUP:New(playerGroup, "Smoke", markerMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Red", smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_red") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Green", smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_green") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Blue", smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_blue") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "White", smokeMenu, function() self:MenuSetPlayerMarker(unit, "smoke_white") end)
|
|
local flareMenu = MENU_GROUP:New(playerGroup, "Flare", markerMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Red", flareMenu, function() self:MenuSetPlayerMarker(unit, "flare_red") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Green", flareMenu, function() self:MenuSetPlayerMarker(unit, "flare_green") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "White", flareMenu, function() self:MenuSetPlayerMarker(unit, "flare_white") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "None", markerMenu, function() self:MenuSetPlayerMarker(unit, "none") end)
|
|
|
|
-- Air scanning submenu
|
|
local airScanMenu = MENU_GROUP:New(playerGroup, "Air Scanning", playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Enable", airScanMenu, function() self:MenuSetPlayerAirScanning(unit, true) end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Disable", airScanMenu, function() self:MenuSetPlayerAirScanning(unit, false) end)
|
|
|
|
-- Ground scanning submenu
|
|
local groundScanMenu = MENU_GROUP:New(playerGroup, "Ground Scanning", playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Enable", groundScanMenu, function() self:MenuSetPlayerGroundScanning(unit, true) end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Disable", groundScanMenu, function() self:MenuSetPlayerGroundScanning(unit, false) end)
|
|
|
|
-- Ground targeting submenu
|
|
local groundMenu = MENU_GROUP:New(playerGroup, "Ground Targeting", playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Scan for Targets", groundMenu, function() self:MenuScanGroundTargets(unit) end)
|
|
local markMenu = MENU_GROUP:New(playerGroup, "Mark Target", groundMenu)
|
|
for i=1,5 do
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Target " .. i, markMenu, function() self:MenuMarkGroundTarget(unit, i) end)
|
|
end
|
|
|
|
-- Alert frequency submenu
|
|
local freqMenu = MENU_GROUP:New(playerGroup, "Alert Frequency", playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Normal", freqMenu, function() self:MenuSetPlayerAlertFrequency(unit, "normal") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Quiet", freqMenu, function() self:MenuSetPlayerAlertFrequency(unit, "quiet") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Verbose", freqMenu, function() self:MenuSetPlayerAlertFrequency(unit, "verbose") end)
|
|
|
|
-- Summary submenu
|
|
local summaryMenu = MENU_GROUP:New(playerGroup, "Summary", playerSubMenu)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Brief", summaryMenu, function() self:MenuSendPlayerSummary(unit, "brief") end)
|
|
MENU_GROUP_COMMAND:New(playerGroup, "Detailed", summaryMenu, function() self:MenuSendPlayerSummary(unit, "detailed") end)
|
|
|
|
env.info("PilotIntuition: Menu created successfully for " .. clientName)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
env.info("PilotIntuition: SetupPlayerMenus completed, processed " .. playersFound .. " players")
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerMarker(playerUnit, markerType)
|
|
env.info("====== PilotIntuition: MenuSetPlayerMarker CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
env.info("PilotIntuition: playerUnit:GetPlayerName() = " .. tostring(playerUnit:GetPlayerName()))
|
|
end
|
|
env.info("PilotIntuition: markerType = " .. tostring(markerType))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
-- store as player pref only - not global config
|
|
if playerKey and self.players[playerKey] then
|
|
self.players[playerKey].markerType = markerType
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
local niceType = markerType:gsub("_", " "):gsub("(%w)(%w*)", function(first, rest) return first:upper() .. rest:lower() end)
|
|
MESSAGE:New(self:GetRandomMessage("markerSet", {niceType}, playerName), 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
function PilotIntuition:MenuSetMarkerType(type)
|
|
env.info("PilotIntuition: MenuSetMarkerType called with " .. tostring(type))
|
|
PILOT_INTUITION_CONFIG.markerType = type
|
|
local niceType = type:gsub("_", " "):gsub("(%w)(%w*)", function(first, rest) return first:upper() .. rest:lower() end)
|
|
local msg = self:GetRandomMessage("markerSet", {niceType})
|
|
env.info("PilotIntuition: Marker message: " .. msg)
|
|
self:BroadcastMessageToAll(msg)
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerDogfightAssist(playerUnit, onoff)
|
|
env.info("====== PilotIntuition: MenuSetPlayerDogfightAssist CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: onoff = " .. tostring(onoff))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
env.info("PilotIntuition: Looking for player data with key: " .. tostring(playerKey))
|
|
env.info("PilotIntuition: Available players: " .. table.concat(self:GetPlayerKeys(), ", "))
|
|
|
|
if playerKey and self.players[playerKey] then
|
|
env.info("PilotIntuition: Found player data, setting dogfightAssist to " .. tostring(onoff))
|
|
self.players[playerKey].dogfightAssist = onoff
|
|
local status = onoff and "enabled" or "disabled"
|
|
local client = playerUnit:GetClient()
|
|
env.info("PilotIntuition: Getting client: " .. tostring(client))
|
|
if client then
|
|
env.info("PilotIntuition: Sending message to client")
|
|
MESSAGE:New(self:GetRandomMessage("dogfightAssistToggle", {status}, playerName), 10):ToClient(client)
|
|
else
|
|
env.info("PilotIntuition: ERROR - Could not get client")
|
|
end
|
|
else
|
|
env.info("PilotIntuition: ERROR - Player data not found for: " .. tostring(playerKey))
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerAirScanning(playerUnit, onoff)
|
|
env.info("====== PilotIntuition: MenuSetPlayerAirScanning CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: onoff = " .. tostring(onoff))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
if playerKey and self.players[playerKey] then
|
|
self.players[playerKey].enableAirScanning = onoff
|
|
local status = onoff and "enabled" or "disabled"
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
MESSAGE:New(self:GetRandomMessage("airScanningToggle", {status}, playerName), 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerGroundScanning(playerUnit, onoff)
|
|
env.info("====== PilotIntuition: MenuSetPlayerGroundScanning CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: onoff = " .. tostring(onoff))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
if playerKey and self.players[playerKey] then
|
|
self.players[playerKey].enableGroundScanning = onoff
|
|
local status = onoff and "enabled" or "disabled"
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
MESSAGE:New(self:GetRandomMessage("groundScanningToggle", {status}, playerName), 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerAlertFrequency(playerUnit, mode)
|
|
env.info("====== PilotIntuition: MenuSetPlayerAlertFrequency CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: mode = " .. tostring(mode))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
if playerKey and self.players[playerKey] then
|
|
local multiplier
|
|
if mode == "normal" then
|
|
multiplier = 1.0
|
|
elseif mode == "quiet" then
|
|
multiplier = 2.0
|
|
elseif mode == "verbose" then
|
|
multiplier = 0.5
|
|
else
|
|
multiplier = 1.0 -- Default
|
|
end
|
|
self.players[playerKey].frequencyMultiplier = multiplier
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
MESSAGE:New(self:GetRandomMessage("alertFrequencyToggle", {mode}, playerName), 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerDistanceUnit(playerUnit, unit)
|
|
env.info("====== PilotIntuition: MenuSetPlayerDistanceUnit CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: unit = " .. tostring(unit))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
if playerKey and self.players[playerKey] then
|
|
self.players[playerKey].distanceUnit = unit
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
local unitName = unit == "mi" and "Miles (Nautical)" or "Kilometers"
|
|
MESSAGE:New(string.format(self:GetText("distanceUnitsSet", playerKey), unitName), 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuSetPlayerLanguage(playerUnit, language)
|
|
env.info("====== PilotIntuition: MenuSetPlayerLanguage CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: language = " .. tostring(language))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
if playerKey and self.players[playerKey] then
|
|
-- Validate language
|
|
if not PILOT_INTUITION_LANGUAGES[language] then
|
|
env.info("PilotIntuition: Invalid language: " .. tostring(language))
|
|
return
|
|
end
|
|
|
|
self.players[playerKey].language = language
|
|
|
|
-- Rebuild the menu immediately to apply language change
|
|
self:RebuildPlayerMenu(playerUnit)
|
|
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
local langNames = {
|
|
EN = "English",
|
|
DE = "Deutsch",
|
|
FR = "Français",
|
|
ES = "Español",
|
|
RU = "Русский"
|
|
}
|
|
local msg = string.format("Language set to %s. Menu has been updated.", langNames[language] or language)
|
|
MESSAGE:New(msg, 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuScanGroundTargets(playerUnit)
|
|
env.info("====== PilotIntuition: MenuScanGroundTargets CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
if not playerUnit or not playerUnit:IsAlive() then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided or unit not alive")
|
|
return
|
|
end
|
|
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
if not playerPos then
|
|
env.info("PilotIntuition: ERROR - Cannot get player position")
|
|
return
|
|
end
|
|
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
local playerData = playerKey and self.players[playerKey]
|
|
if not playerData then
|
|
env.info("PilotIntuition: ERROR - No player data found for " .. tostring(playerKey))
|
|
return
|
|
end
|
|
|
|
local client = playerUnit:GetClient()
|
|
if not client then
|
|
env.info("PilotIntuition: ERROR - No client found")
|
|
return
|
|
end
|
|
|
|
-- Collect enemy ground groups within range
|
|
local playerCoalition = playerUnit:GetCoalition()
|
|
local enemyCoalition = (playerCoalition == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
|
|
local allGroups = coalition.getGroups(enemyCoalition)
|
|
local enemyGroundGroups = {}
|
|
for _, dcsGroup in ipairs(allGroups) do
|
|
-- Wrap DCS group object with Moose GROUP
|
|
local group = GROUP:Find(dcsGroup)
|
|
if group and group:IsAlive() and (dcsGroup:getCategory() == Group.Category.GROUND or dcsGroup:getCategory() == Group.Category.SHIP) then
|
|
local groupCoord = group:GetCoordinate()
|
|
if groupCoord then
|
|
local distance = playerPos:Get2DDistance(groupCoord)
|
|
if distance <= PILOT_INTUITION_CONFIG.groundDetectionRange then
|
|
table.insert(enemyGroundGroups, {group = group, distance = distance})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Sort by distance
|
|
table.sort(enemyGroundGroups, function(a,b) return a.distance < b.distance end)
|
|
|
|
-- Take top 5
|
|
local scanned = {}
|
|
for i=1, math.min(5, #enemyGroundGroups) do
|
|
scanned[i] = enemyGroundGroups[i].group
|
|
end
|
|
|
|
-- Store in playerData
|
|
playerData.scannedGroundTargets = scanned
|
|
|
|
-- Send messages listing targets
|
|
for i, group in ipairs(scanned) do
|
|
local targetPos = group:GetCoordinate()
|
|
local bearing = playerPos:HeadingTo(targetPos) -- Returns heading in degrees as number
|
|
|
|
-- Ensure bearing is a valid number
|
|
if not bearing or type(bearing) ~= "number" then
|
|
bearing = 0 -- Fallback to 0 if bearing is invalid
|
|
end
|
|
|
|
local distanceMeters = playerPos:Get2DDistance(targetPos)
|
|
local distance, unit = self:FormatDistance(distanceMeters, playerKey)
|
|
local unitType = group:GetUnits()[1]:GetTypeName()
|
|
local category = self:ClassifyGroundUnit(unitType)
|
|
local groupSize = #group:GetUnits()
|
|
local sizeDesc = groupSize == 1 and "single" or (groupSize <= 4 and "group" or "platoon")
|
|
MESSAGE:New(string.format("Target %d: %s %s %s, Bearing %.0f, Range %.1f %s", i, category, sizeDesc, unitType, bearing, distance, unit), 30):ToClient(client)
|
|
end
|
|
|
|
if #scanned == 0 then
|
|
MESSAGE:New(self:GetText("noGroundTargets", playerName), 10):ToClient(client)
|
|
else
|
|
MESSAGE:New(self:GetText("selectTargetFromMenu", playerName), 10):ToClient(client)
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuMarkGroundTarget(playerUnit, index)
|
|
env.info("====== PilotIntuition: MenuMarkGroundTarget CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
env.info("PilotIntuition: index = " .. tostring(index))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: ERROR - No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
local playerData = playerKey and self.players[playerKey]
|
|
if not playerData then
|
|
env.info("PilotIntuition: ERROR - No player data found for " .. tostring(playerKey))
|
|
return
|
|
end
|
|
local client = playerUnit:GetClient()
|
|
if not client then
|
|
env.info("PilotIntuition: ERROR - No client found")
|
|
return
|
|
end
|
|
|
|
local scanned = playerData.scannedGroundTargets or {}
|
|
local group = scanned[index]
|
|
if not group or not group:IsAlive() then
|
|
MESSAGE:New("Target " .. index .. " not available.", 10):ToClient(client)
|
|
return
|
|
end
|
|
|
|
-- Mark this target
|
|
self:ReportGroundTarget(group, playerUnit, client, true) -- placeMarker = true
|
|
MESSAGE:New("Marked Target " .. index .. ".", 10):ToClient(client)
|
|
end
|
|
|
|
function PilotIntuition:MenuDropIlluminationAtPlayer(playerUnit)
|
|
env.info("====== PilotIntuition: MenuDropIlluminationAtPlayer CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
if not playerUnit or not playerUnit:IsAlive() then
|
|
env.info("PilotIntuition: ERROR - No playerUnit or unit not alive")
|
|
return
|
|
end
|
|
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
env.info("PilotIntuition: playerKey = " .. tostring(playerKey))
|
|
|
|
if not playerKey or not self.players[playerKey] then
|
|
env.info("PilotIntuition: ERROR - Player data not found for: " .. tostring(playerKey))
|
|
return
|
|
end
|
|
|
|
local playerData = self.players[playerKey]
|
|
local client = playerUnit:GetClient()
|
|
if not client then
|
|
env.info("PilotIntuition: ERROR - Could not get client")
|
|
return
|
|
end
|
|
|
|
-- Check if player has flares remaining
|
|
if (playerData.illuminationFlares or 0) <= 0 then
|
|
MESSAGE:New("No illumination flares remaining. Land at a friendly airbase to rearm.", 10):ToClient(client)
|
|
return
|
|
end
|
|
|
|
-- Check cooldown
|
|
local now = timer.getTime()
|
|
local timeSinceLast = now - (playerData.lastIlluminationTime or 0)
|
|
if timeSinceLast < PILOT_INTUITION_CONFIG.illuminationCooldown then
|
|
local remaining = math.ceil(PILOT_INTUITION_CONFIG.illuminationCooldown - timeSinceLast)
|
|
MESSAGE:New("Illumination not ready. Wait " .. remaining .. " seconds.", 5):ToClient(client)
|
|
return
|
|
end
|
|
|
|
-- Drop illumination flare at player position with altitude offset
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
if not playerPos then
|
|
env.info("PilotIntuition: ERROR - Could not get player coordinate")
|
|
MESSAGE:New("Cannot determine position for illumination drop.", 5):ToClient(client)
|
|
return
|
|
end
|
|
PILog(LOG_INFO, "PilotIntuition: Player position: " .. playerPos:ToStringLLDMS())
|
|
|
|
-- Get altitude from unit directly (more reliable than coordinate)
|
|
local currentAlt = playerUnit:GetAltitude()
|
|
if not currentAlt or type(currentAlt) ~= "number" then
|
|
env.info("PilotIntuition: ERROR - Could not get valid altitude from unit")
|
|
MESSAGE:New("Cannot determine altitude for illumination drop.", 5):ToClient(client)
|
|
return
|
|
end
|
|
PILog(LOG_INFO, "PilotIntuition: Current altitude: " .. currentAlt .. "m")
|
|
|
|
-- Create new coordinate at higher altitude
|
|
local illuAlt = currentAlt + PILOT_INTUITION_CONFIG.illuminationAltitude
|
|
local illuPos = COORDINATE:NewFromVec3(playerPos:GetVec3())
|
|
illuPos = illuPos:SetAltitude(illuAlt)
|
|
PILog(LOG_INFO, "PilotIntuition: Illumination altitude: " .. illuAlt .. "m")
|
|
|
|
-- Try to drop illumination bomb
|
|
PILog(LOG_INFO, "PilotIntuition: Attempting to drop illumination bomb")
|
|
illuPos:IlluminationBomb()
|
|
PILog(LOG_INFO, "PilotIntuition: IlluminationBomb() called successfully")
|
|
|
|
-- Decrement flare count
|
|
playerData.illuminationFlares = playerData.illuminationFlares - 1
|
|
playerData.lastIlluminationTime = now
|
|
|
|
MESSAGE:New("Illumination flare dropped at your position. (" .. playerData.illuminationFlares .. " remaining)", 10):ToClient(client)
|
|
PILog(LOG_INFO, "PilotIntuition: Player " .. playerKey .. " dropped illumination at own position. " .. playerData.illuminationFlares .. " flares remaining")
|
|
|
|
-- Rebuild menu to update flare count display
|
|
self:RebuildPlayerMenu(playerUnit)
|
|
end
|
|
|
|
function PilotIntuition:MenuDropIlluminationOnTarget(playerUnit, index)
|
|
env.info("====== PilotIntuition: MenuDropIlluminationOnTarget CALLED ======")
|
|
env.info("PilotIntuition: playerUnit = " .. tostring(playerUnit))
|
|
env.info("PilotIntuition: index = " .. tostring(index))
|
|
if playerUnit then
|
|
env.info("PilotIntuition: playerUnit:GetName() = " .. tostring(playerUnit:GetName()))
|
|
end
|
|
if not playerUnit or not playerUnit:IsAlive() then
|
|
env.info("PilotIntuition: ERROR - No playerUnit or unit not alive")
|
|
return
|
|
end
|
|
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
env.info("PilotIntuition: playerKey = " .. tostring(playerKey))
|
|
|
|
if not playerKey or not self.players[playerKey] then
|
|
env.info("PilotIntuition: ERROR - Player data not found for: " .. tostring(playerKey))
|
|
return
|
|
end
|
|
|
|
local playerData = self.players[playerKey]
|
|
local client = playerUnit:GetClient()
|
|
if not client then
|
|
env.info("PilotIntuition: ERROR - Could not get client")
|
|
return
|
|
end
|
|
|
|
-- Check if player has flares remaining
|
|
if (playerData.illuminationFlares or 0) <= 0 then
|
|
MESSAGE:New("No illumination flares remaining. Land at a friendly airbase to rearm.", 10):ToClient(client)
|
|
return
|
|
end
|
|
|
|
-- Check cooldown
|
|
local now = timer.getTime()
|
|
local timeSinceLast = now - (playerData.lastIlluminationTime or 0)
|
|
if timeSinceLast < PILOT_INTUITION_CONFIG.illuminationCooldown then
|
|
local remaining = math.ceil(PILOT_INTUITION_CONFIG.illuminationCooldown - timeSinceLast)
|
|
MESSAGE:New("Illumination not ready. Wait " .. remaining .. " seconds.", 5):ToClient(client)
|
|
return
|
|
end
|
|
|
|
-- Get scanned target
|
|
local scanned = playerData.scannedGroundTargets or {}
|
|
local group = scanned[index]
|
|
if not group or not group:IsAlive() then
|
|
MESSAGE:New("Target " .. index .. " not available.", 10):ToClient(client)
|
|
return
|
|
end
|
|
|
|
-- Drop illumination flare over target with altitude offset
|
|
local targetPos = group:GetCoordinate()
|
|
if not targetPos then
|
|
env.info("PilotIntuition: ERROR - Could not get target coordinate")
|
|
MESSAGE:New("Cannot determine target position for illumination drop.", 5):ToClient(client)
|
|
return
|
|
end
|
|
PILog(LOG_INFO, "PilotIntuition: Target position: " .. targetPos:ToStringLLDMS())
|
|
|
|
-- Get altitude from ground level (ground units are at ground level)
|
|
-- Use a safe default altitude for ground targets
|
|
local targetAlt = 0
|
|
local firstUnit = group:GetUnit(1)
|
|
if firstUnit and firstUnit:IsAlive() then
|
|
local unitAlt = firstUnit:GetAltitude()
|
|
if unitAlt and type(unitAlt) == "number" then
|
|
targetAlt = unitAlt
|
|
end
|
|
end
|
|
PILog(LOG_INFO, "PilotIntuition: Target altitude: " .. targetAlt .. "m")
|
|
|
|
-- Create new coordinate at higher altitude
|
|
local illuAlt = targetAlt + PILOT_INTUITION_CONFIG.illuminationAltitude
|
|
local illuPos = COORDINATE:NewFromVec3(targetPos:GetVec3())
|
|
illuPos = illuPos:SetAltitude(illuAlt)
|
|
PILog(LOG_INFO, "PilotIntuition: Illumination altitude: " .. illuAlt .. "m")
|
|
|
|
-- Try to drop illumination bomb
|
|
PILog(LOG_INFO, "PilotIntuition: Attempting to drop illumination bomb on target")
|
|
illuPos:IlluminationBomb()
|
|
PILog(LOG_INFO, "PilotIntuition: IlluminationBomb() called successfully")
|
|
|
|
-- Decrement flare count
|
|
playerData.illuminationFlares = playerData.illuminationFlares - 1
|
|
playerData.lastIlluminationTime = now
|
|
|
|
local playerCoord = playerUnit:GetCoordinate()
|
|
if not playerCoord then
|
|
PILog(LOG_ERROR, "PilotIntuition: Could not get player coordinate for illumination drop")
|
|
MESSAGE:New("Error: Could not determine your position.", 10):ToClient(client)
|
|
return
|
|
end
|
|
|
|
local bearing = playerCoord:GetAngleDegrees(playerCoord:GetDirectionVec3(targetPos))
|
|
local rawDistance = playerCoord:Get2DDistance(targetPos)
|
|
local distance, unit = self:FormatDistance(rawDistance, playerKey)
|
|
MESSAGE:New(string.format("Illumination flare dropped on Target %d (%.0f°, %.1f%s). (%d remaining)", index, bearing, distance, unit, playerData.illuminationFlares), 10):ToClient(client)
|
|
PILog(LOG_INFO, "PilotIntuition: Player " .. playerKey .. " dropped illumination on target " .. index .. ". " .. playerData.illuminationFlares .. " flares remaining")
|
|
|
|
-- Rebuild menu to update flare count display
|
|
self:RebuildPlayerMenu(playerUnit)
|
|
end
|
|
|
|
function PilotIntuition:MenuSetActiveMessaging(onoff)
|
|
env.info("PilotIntuition: MenuSetActiveMessaging called with " .. tostring(onoff))
|
|
PILOT_INTUITION_CONFIG.activeMessaging = onoff
|
|
if onoff then
|
|
local msg = self:GetRandomMessage("activeMessagingToggle", {"enabled"})
|
|
env.info("PilotIntuition: Active messaging message: " .. msg)
|
|
self:BroadcastMessageToAll(msg)
|
|
if self.summaryScheduler then
|
|
self.summaryScheduler:Stop()
|
|
self.summaryScheduler = nil
|
|
end
|
|
else
|
|
local msg = self:GetRandomMessage("activeMessagingToggle", {"disabled. Use summaries for updates."})
|
|
env.info("PilotIntuition: Active messaging message: " .. msg)
|
|
self:BroadcastMessageToAll(msg)
|
|
if PILOT_INTUITION_CONFIG.summaryInterval > 0 then
|
|
self.summaryScheduler = SCHEDULER:New(nil, self.SendScheduledSummaries, {self}, 1, PILOT_INTUITION_CONFIG.summaryInterval)
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:MenuSetAirScanning(onoff)
|
|
env.info("PilotIntuition: MenuSetAirScanning called with " .. tostring(onoff))
|
|
PILOT_INTUITION_CONFIG.enableAirScanning = onoff
|
|
local status = onoff and "enabled" or "disabled"
|
|
local msg = self:GetRandomMessage("airScanningToggle", {status})
|
|
env.info("PilotIntuition: Air scanning message: " .. msg)
|
|
self:BroadcastMessageToAll(msg)
|
|
end
|
|
|
|
function PilotIntuition:MenuSetGroundScanning(onoff)
|
|
env.info("PilotIntuition: MenuSetGroundScanning called with " .. tostring(onoff))
|
|
PILOT_INTUITION_CONFIG.enableGroundScanning = onoff
|
|
local status = onoff and "enabled" or "disabled"
|
|
local msg = self:GetRandomMessage("groundScanningToggle", {status})
|
|
env.info("PilotIntuition: Ground scanning message: " .. msg)
|
|
self:BroadcastMessageToAll(msg)
|
|
end
|
|
|
|
function PilotIntuition:MenuSendPlayerSummary(playerUnit, detailLevel)
|
|
self:SendPlayerSummary(playerUnit, detailLevel)
|
|
end
|
|
|
|
function PilotIntuition:BroadcastMessageToAll(text)
|
|
env.info("PilotIntuition: Broadcasting message: " .. tostring(text))
|
|
MESSAGE:New(text, 10):ToAll() -- Added duration 10 seconds
|
|
env.info("PilotIntuition: Broadcasted to all with duration 10")
|
|
end
|
|
|
|
function PilotIntuition:GetPlayerSummary(playerName, detailLevel, playerUnit)
|
|
local data = self.players[playerName]
|
|
if not data then return nil end
|
|
|
|
-- Get player position for bearing calculations
|
|
local playerPos = nil
|
|
local playerHeading = 0
|
|
if playerUnit and playerUnit:IsAlive() then
|
|
playerPos = playerUnit:GetCoordinate()
|
|
playerHeading = math.deg(playerUnit:GetHeading())
|
|
end
|
|
|
|
-- Collect air threats with details
|
|
local airThreats = {}
|
|
for id, t in pairs(data.trackedAirTargets or {}) do
|
|
if t.unit and t.unit:IsAlive() then
|
|
local distance = t.lastRange or 0
|
|
local bearing = 0
|
|
local relativeBearing = 0
|
|
local threat = "UNKNOWN"
|
|
|
|
if playerPos then
|
|
bearing = playerPos:GetAngleDegrees(playerPos:GetDirectionVec3(t.unit:GetCoordinate()))
|
|
relativeBearing = (bearing - playerHeading + 360) % 360
|
|
|
|
-- Determine threat level
|
|
if distance < PILOT_INTUITION_CONFIG.threatHotRange then
|
|
threat = "HOT"
|
|
elseif distance < PILOT_INTUITION_CONFIG.threatColdRange then
|
|
threat = "COLD"
|
|
else
|
|
threat = "FAR"
|
|
end
|
|
end
|
|
|
|
table.insert(airThreats, {
|
|
name = t.unit:GetTypeName() or "Unknown",
|
|
bearing = bearing,
|
|
distance = distance,
|
|
altitude = t.unit:GetAltitude() or 0,
|
|
threat = threat
|
|
})
|
|
end
|
|
end
|
|
|
|
-- Sort air threats by distance (closest first)
|
|
table.sort(airThreats, function(a, b) return a.distance < b.distance end)
|
|
|
|
-- Collect ground threats with details
|
|
local groundThreats = {}
|
|
for id, _ in pairs(data.trackedGroundTargets or {}) do
|
|
-- Look up actual ground group from global table
|
|
local groundData = self.trackedGroundTargets[id]
|
|
if groundData and groundData.group and groundData.group:IsAlive() then
|
|
local g = groundData.group
|
|
local gPos = g:GetCoordinate()
|
|
local distance = 0
|
|
local bearing = 0
|
|
local category = "Ground"
|
|
|
|
if playerPos and gPos then
|
|
distance = playerPos:Get2DDistance(gPos)
|
|
bearing = playerPos:GetAngleDegrees(playerPos:GetDirectionVec3(gPos))
|
|
end
|
|
|
|
-- Classify ground unit
|
|
local firstUnit = g:GetUnit(1)
|
|
if firstUnit then
|
|
local unitType = firstUnit:GetTypeName() or ""
|
|
category = self:ClassifyGroundUnit(unitType)
|
|
end
|
|
|
|
table.insert(groundThreats, {
|
|
name = g:GetName() or "Unknown",
|
|
category = category,
|
|
bearing = bearing,
|
|
distance = distance
|
|
})
|
|
end
|
|
end
|
|
|
|
-- Sort ground threats by distance
|
|
table.sort(groundThreats, function(a, b) return a.distance < b.distance end)
|
|
|
|
-- Build summary based on detail level
|
|
if detailLevel == "brief" then
|
|
-- Brief: Quick tactical overview
|
|
local parts = {}
|
|
|
|
if #airThreats > 0 then
|
|
local closest = airThreats[1]
|
|
local distance, unit = self:FormatDistance(closest.distance, playerName)
|
|
table.insert(parts, string.format("%d bandit%s (closest: %s @ %.1f%s %s)",
|
|
#airThreats,
|
|
#airThreats > 1 and "s" or "",
|
|
closest.name,
|
|
distance,
|
|
unit,
|
|
closest.threat))
|
|
end
|
|
|
|
if #groundThreats > 0 then
|
|
table.insert(parts, string.format("%d ground group%s", #groundThreats, #groundThreats > 1 and "s" or ""))
|
|
end
|
|
|
|
-- Formation status
|
|
local wingmen = data.cachedWingmen or 0
|
|
if wingmen > 0 then
|
|
local mult = (wingmen > 0) and (2 * wingmen) or 1
|
|
if mult > PILOT_INTUITION_CONFIG.maxMultiplier then mult = PILOT_INTUITION_CONFIG.maxMultiplier end
|
|
local envMult = self:GetDetectionMultiplier()
|
|
local airRange, airUnit = self:FormatDistance(PILOT_INTUITION_CONFIG.airDetectionRange * mult * envMult, playerName)
|
|
local groundRange, groundUnit = self:FormatDistance(PILOT_INTUITION_CONFIG.groundDetectionRange * mult * envMult, playerName)
|
|
table.insert(parts, string.format("Formation: %d wingmen (detect: %.0f%s air/%.0f%s gnd)", wingmen, airRange, airUnit, groundRange, groundUnit))
|
|
else
|
|
table.insert(parts, "Solo (reduced detection)")
|
|
end
|
|
|
|
if #parts == 0 then
|
|
return nil
|
|
end
|
|
|
|
return table.concat(parts, " | ")
|
|
|
|
else
|
|
-- Detailed: Full tactical situation report
|
|
local lines = {}
|
|
table.insert(lines, "=== TACTICAL SITREP ===")
|
|
|
|
-- Air threats section
|
|
if #airThreats > 0 then
|
|
table.insert(lines, "\nAIR THREATS:")
|
|
for i, threat in ipairs(airThreats) do
|
|
local angels = math.floor(threat.altitude * 3.28084 / 1000) -- Convert to thousands of feet
|
|
local distance, unit = self:FormatDistance(threat.distance, playerName)
|
|
table.insert(lines, string.format(" %d. %s @ %03d°, %.1f%s, angels %d (%s)",
|
|
i, threat.name, math.floor(threat.bearing), distance, unit, angels, threat.threat))
|
|
end
|
|
else
|
|
table.insert(lines, "\nAIR THREATS: None")
|
|
end
|
|
|
|
-- Ground threats section
|
|
if #groundThreats > 0 then
|
|
table.insert(lines, "\nGROUND THREATS:")
|
|
for i, threat in ipairs(groundThreats) do
|
|
local distance, unit = self:FormatDistance(threat.distance, playerName)
|
|
table.insert(lines, string.format(" %d. %s @ %03d°, %.1f%s",
|
|
i, threat.category, math.floor(threat.bearing), distance, unit))
|
|
end
|
|
else
|
|
table.insert(lines, "\nGROUND THREATS: None")
|
|
end
|
|
|
|
-- Formation and detection status
|
|
local wingmen = data.cachedWingmen or 0
|
|
local mult = (wingmen > 0) and (2 * wingmen) or 1
|
|
if mult > PILOT_INTUITION_CONFIG.maxMultiplier then mult = PILOT_INTUITION_CONFIG.maxMultiplier end
|
|
local envMult = self:GetDetectionMultiplier()
|
|
local airRange, airUnit = self:FormatDistance(PILOT_INTUITION_CONFIG.airDetectionRange * mult * envMult, playerName)
|
|
local groundRange, groundUnit = self:FormatDistance(PILOT_INTUITION_CONFIG.groundDetectionRange * mult * envMult, playerName)
|
|
|
|
table.insert(lines, "\nFORMATION:")
|
|
if wingmen > 0 then
|
|
table.insert(lines, string.format(" Wingmen: %d (multiplier: x%d)", wingmen, mult))
|
|
else
|
|
table.insert(lines, " Status: Solo flight")
|
|
end
|
|
table.insert(lines, string.format(" Detection: %.0f%s air, %.0f%s ground", airRange, airUnit, groundRange, groundUnit))
|
|
|
|
return table.concat(lines, "\n")
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:SendPlayerSummary(playerUnit, detailLevel)
|
|
env.info("PilotIntuition: SendPlayerSummary called with detailLevel: " .. tostring(detailLevel))
|
|
if not playerUnit then
|
|
env.info("PilotIntuition: No playerUnit provided")
|
|
return
|
|
end
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
env.info("PilotIntuition: Player key from unit: " .. tostring(playerKey))
|
|
local client = playerUnit:GetClient()
|
|
if not client then
|
|
env.info("PilotIntuition: Could not get client for player")
|
|
return
|
|
end
|
|
if not playerKey or not self.players[playerKey] then
|
|
env.info("PilotIntuition: Player data not found for: " .. tostring(playerKey))
|
|
env.info("PilotIntuition: Available players: " .. table.concat(self:GetPlayerKeys(), ", "))
|
|
return
|
|
end
|
|
local now = timer.getTime()
|
|
local data = self.players[playerKey]
|
|
if data.lastSummaryTime and (now - data.lastSummaryTime) < (PILOT_INTUITION_CONFIG.summaryCooldown or 2) then
|
|
MESSAGE:New(self:GetRandomMessage("summaryCooldown"), 5):ToClient(client)
|
|
return
|
|
end
|
|
local summary = self:GetPlayerSummary(playerKey, detailLevel, playerUnit)
|
|
env.info("PilotIntuition: Summary generated: " .. tostring(summary))
|
|
if summary and summary ~= "" then
|
|
-- Use longer duration for detailed reports
|
|
local duration = (detailLevel == "detailed") and 30 or 15
|
|
MESSAGE:New(summary, duration):ToClient(client)
|
|
data.lastSummaryTime = now
|
|
else
|
|
MESSAGE:New(self:GetRandomMessage("noThreats"), 10):ToClient(client)
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:GetPlayerKeys()
|
|
local keys = {}
|
|
for k, _ in pairs(self.players) do
|
|
table.insert(keys, k)
|
|
end
|
|
return keys
|
|
end
|
|
|
|
function PilotIntuition:SendScheduledSummaries()
|
|
if not PILOT_INTUITION_CONFIG.activeMessaging then
|
|
local clients = SET_CLIENT:New():FilterActive():FilterOnce()
|
|
clients:ForEachClient(function(client)
|
|
if client and type(client.GetUnit) == "function" then
|
|
local unit = client:GetUnit()
|
|
if unit and unit:IsAlive() then
|
|
self:SendPlayerSummary(unit, "brief")
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:StartScheduler()
|
|
-- Schedule periodic scans
|
|
SCHEDULER:New(nil, self.ScanTargets, {self}, 1, PILOT_INTUITION_CONFIG.scanInterval)
|
|
end
|
|
|
|
-- (old ScanTargets removed; using the optimized ScanTargets above)
|
|
|
|
function PilotIntuition:ScanAirTargetsForPlayer(playerUnit, playerData, client, activeClients, enemyAirUnits)
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
local playerCoalition = playerUnit:GetCoalition()
|
|
local enemyCoalition = (playerCoalition == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
|
|
|
|
-- Use cached wingmen count
|
|
local wingmen = playerData.cachedWingmen
|
|
local multiplier = (wingmen > 0) and (2 * wingmen) or 1
|
|
-- Clamp multiplier to configured maximum
|
|
if multiplier > PILOT_INTUITION_CONFIG.maxMultiplier then
|
|
multiplier = PILOT_INTUITION_CONFIG.maxMultiplier
|
|
end
|
|
local envMult = self:GetDetectionMultiplier()
|
|
local detectionRange = PILOT_INTUITION_CONFIG.airDetectionRange * multiplier * envMult
|
|
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Scanning %d enemy air units within %.0fm for player %s",
|
|
#enemyAirUnits, detectionRange, playerKey))
|
|
|
|
-- Notify formation changes (suppress during active combat if configured)
|
|
local previousWingmen = playerData.previousWingmen or 0
|
|
local now = timer.getTime()
|
|
PILog(LOG_DEBUG, "PilotIntuition: Formation check - previous: " .. previousWingmen .. ", current: " .. wingmen .. ", cached: " .. playerData.cachedWingmen)
|
|
|
|
-- Check if player is in active combat (has engaged bandits within hot range recently)
|
|
local inCombat = false
|
|
if PILOT_INTUITION_CONFIG.suppressFormationInCombat then
|
|
for _, data in pairs(playerData.trackedAirTargets) do
|
|
if data.engaged and (now - (data.lastEngagedTime or 0)) < 30 then
|
|
inCombat = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not inCombat and (now - playerData.lastFormationMessageTime) >= PILOT_INTUITION_CONFIG.formationMessageCooldown then
|
|
local newWingmen = playerData.cachedWingmen
|
|
local newMultiplier = (newWingmen > 0) and (2 * newWingmen) or 1
|
|
-- Clamp multiplier to configured maximum
|
|
if newMultiplier > PILOT_INTUITION_CONFIG.maxMultiplier then
|
|
newMultiplier = PILOT_INTUITION_CONFIG.maxMultiplier
|
|
end
|
|
local newAirRangeMeters = PILOT_INTUITION_CONFIG.airDetectionRange * newMultiplier * envMult
|
|
local newGroundRangeMeters = PILOT_INTUITION_CONFIG.groundDetectionRange * newMultiplier * envMult
|
|
|
|
-- Convert to player's preferred units
|
|
local newAirRange, airUnit = self:FormatDistance(newAirRangeMeters, playerKey)
|
|
local newGroundRange, groundUnit = self:FormatDistance(newGroundRangeMeters, playerKey)
|
|
|
|
PILog(LOG_DEBUG, "PilotIntuition: Formation change check - new: " .. newWingmen .. ", prev: " .. previousWingmen .. ", activeMessaging: " .. tostring(PILOT_INTUITION_CONFIG.activeMessaging))
|
|
|
|
if newWingmen > previousWingmen then
|
|
PILog(LOG_INFO, "PilotIntuition: Formation joined - sending message")
|
|
if PILOT_INTUITION_CONFIG.activeMessaging then
|
|
MESSAGE:New(self:GetRandomMessage("formationJoin", {"wingman", newAirRange, airUnit, newGroundRange, groundUnit}), 10):ToClient(client)
|
|
end
|
|
playerData.lastFormationMessageTime = now
|
|
elseif newWingmen < previousWingmen then
|
|
PILog(LOG_INFO, "PilotIntuition: Formation left - sending message")
|
|
if PILOT_INTUITION_CONFIG.activeMessaging then
|
|
MESSAGE:New(self:GetRandomMessage("formationLeave", {"wingman", newAirRange, airUnit, newGroundRange, groundUnit}), 10):ToClient(client)
|
|
end
|
|
playerData.lastFormationMessageTime = now
|
|
end
|
|
else
|
|
if inCombat then
|
|
PILog(LOG_DEBUG, "PilotIntuition: Formation change suppressed - player in combat")
|
|
else
|
|
PILog(LOG_DEBUG, "PilotIntuition: Formation change message on cooldown")
|
|
end
|
|
end
|
|
playerData.previousWingmen = playerData.cachedWingmen
|
|
|
|
-- Formation integrity warning (only if previously had wingmen)
|
|
if previousWingmen >= PILOT_INTUITION_CONFIG.minFormationWingmen and wingmen < PILOT_INTUITION_CONFIG.minFormationWingmen then
|
|
if not playerData.formationWarned and (now - playerData.lastFormationMessageTime) >= PILOT_INTUITION_CONFIG.formationMessageCooldown then
|
|
if PILOT_INTUITION_CONFIG.activeMessaging then
|
|
MESSAGE:New(self:GetRandomMessage("formationIntegrityLow"), 10):ToClient(client)
|
|
end
|
|
playerData.formationWarned = true
|
|
playerData.lastFormationMessageTime = now
|
|
end
|
|
elseif wingmen >= PILOT_INTUITION_CONFIG.minFormationWingmen then
|
|
playerData.formationWarned = false
|
|
end
|
|
|
|
-- Prune dead or out-of-range air targets
|
|
for id, data in pairs(playerData.trackedAirTargets) do
|
|
if not data.unit:IsAlive() or playerPos:Get2DDistance(data.unit:GetCoordinate()) > detectionRange then
|
|
-- Explicitly nil out sub-fields before removing to help GC
|
|
data.unit = nil
|
|
data.banditName = nil
|
|
playerData.trackedAirTargets[id] = nil
|
|
end
|
|
end
|
|
|
|
local banditCount = 0
|
|
local closestUnit = nil
|
|
local minDistance = math.huge
|
|
-- Reuse existing threateningBandits table to reduce GC pressure
|
|
local threateningBandits = playerData.threateningBandits or {}
|
|
-- Clear the table (faster than creating new one)
|
|
for i = #threateningBandits, 1, -1 do
|
|
threateningBandits[i] = nil
|
|
end
|
|
|
|
for _, unit in ipairs(enemyAirUnits) do
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Checking enemy unit: %s, IsAlive: %s",
|
|
unit and unit:GetName() or "nil", tostring(unit and unit:IsAlive())))
|
|
if unit and unit:IsAlive() then
|
|
local unitCoord = unit:GetCoordinate()
|
|
if not unitCoord then
|
|
PILog(LOG_DEBUG, "PilotIntuition: Could not get coordinate for " .. unit:GetName())
|
|
else
|
|
local distance = playerPos:Get2DDistance(unitCoord)
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Unit %s distance: %.0fm (range: %.0fm)",
|
|
unit:GetName(), distance, detectionRange))
|
|
|
|
-- Check if within detection range AND has line of sight
|
|
if distance <= detectionRange then
|
|
-- Use coordinate-based LOS check from player to enemy
|
|
local hasLOS = playerPos:IsLOS(unitCoord)
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Unit %s in range, LOS check: %s",
|
|
unit:GetName(), tostring(hasLOS)))
|
|
|
|
if hasLOS then
|
|
banditCount = banditCount + 1
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Enemy detected with LOS: %s at %.0fm", unit:GetName(), distance))
|
|
if distance < minDistance then
|
|
minDistance = distance
|
|
closestUnit = unit
|
|
end
|
|
|
|
local targetID = unit:GetName()
|
|
local now = timer.getTime()
|
|
|
|
-- Calculate bearing from player to bandit
|
|
local bearing = playerPos:HeadingTo(unitCoord)
|
|
local playerHeading = playerUnit:GetHeading() -- Already in degrees
|
|
|
|
-- Calculate relative bearing (clock position)
|
|
local relativeBearing = (bearing - playerHeading + 360) % 360
|
|
|
|
-- Calculate bandit's aspect angle (is he nose-on or tail-on to us?)
|
|
local banditHeading = unit:GetHeading() -- Already in degrees
|
|
local banditToBearing = (bearing + 180) % 360 -- Reverse bearing (from bandit to player)
|
|
local aspectAngle = math.abs(banditToBearing - banditHeading)
|
|
if aspectAngle > 180 then aspectAngle = 360 - aspectAngle end -- Normalize to 0-180
|
|
|
|
-- Debug logging for first contact
|
|
if distance < 20000 and not playerData.trackedAirTargets[targetID] then
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Initial contact %s - playerHdg:%.0f° bearing:%.0f° relBrg:%.0f° banditHdg:%.0f° aspect:%.0f°",
|
|
targetID, playerHeading, bearing, relativeBearing, banditHeading, aspectAngle))
|
|
end
|
|
|
|
if not playerData.trackedAirTargets[targetID] then
|
|
local banditName = unit:GetPlayerName() or unit:GetName()
|
|
playerData.trackedAirTargets[targetID] = { unit = unit, engaged = false, lastRange = distance, lastTime = now, wasHot = false, lastRelativeBearing = relativeBearing, lastEngagedTime = 0, banditName = banditName }
|
|
|
|
-- Safety: Limit tracked targets to prevent memory bloat in long missions (keep 50 most recent)
|
|
local trackedCount = 0
|
|
for _ in pairs(playerData.trackedAirTargets) do trackedCount = trackedCount + 1 end
|
|
if trackedCount > 50 then
|
|
-- Remove oldest non-engaged target
|
|
local oldestID, oldestTime = nil, math.huge
|
|
for tid, tdata in pairs(playerData.trackedAirTargets) do
|
|
if not tdata.engaged and tdata.lastTime < oldestTime then
|
|
oldestTime = tdata.lastTime
|
|
oldestID = tid
|
|
end
|
|
end
|
|
if oldestID then
|
|
playerData.trackedAirTargets[oldestID].unit = nil
|
|
playerData.trackedAirTargets[oldestID].banditName = nil
|
|
playerData.trackedAirTargets[oldestID] = nil
|
|
end
|
|
end
|
|
else
|
|
playerData.trackedAirTargets[targetID].lastRange = playerData.trackedAirTargets[targetID].lastRange or distance
|
|
playerData.trackedAirTargets[targetID].lastTime = playerData.trackedAirTargets[targetID].lastTime or now
|
|
playerData.trackedAirTargets[targetID].lastRelativeBearing = playerData.trackedAirTargets[targetID].lastRelativeBearing or relativeBearing
|
|
end
|
|
local data = playerData.trackedAirTargets[targetID]
|
|
local closing = distance < data.lastRange
|
|
local wasHot = data.wasHot
|
|
if distance <= PILOT_INTUITION_CONFIG.threatHotRange then
|
|
data.wasHot = true
|
|
end
|
|
data.lastRange = distance
|
|
data.lastTime = now
|
|
local lastRelativeBearing = data.lastRelativeBearing
|
|
data.lastRelativeBearing = relativeBearing
|
|
|
|
-- Reset engaged status after 2 minutes of no engagement or if bandit is out of range
|
|
if data.engaged and ((now - data.lastEngagedTime) > 120 or distance > PILOT_INTUITION_CONFIG.airDetectionRange) then
|
|
data.engaged = false
|
|
-- Only report "dogfight concluded" if the bandit was actually hot at some point (actual engagement)
|
|
if data.wasHot and (now - playerData.lastConclusionTime) >= (PILOT_INTUITION_CONFIG.messageCooldown * playerData.frequencyMultiplier) then
|
|
if PILOT_INTUITION_CONFIG.activeMessaging then
|
|
local banditName = data.banditName or "bandit"
|
|
local msg = string.format("%s escaped!", banditName)
|
|
MESSAGE:New(msg, 10):ToClient(client)
|
|
end
|
|
playerData.lastConclusionTime = now
|
|
end
|
|
end
|
|
|
|
if not data.engaged then
|
|
-- Calculate altitude information
|
|
local playerAlt = playerUnit:GetAltitude()
|
|
local banditAlt = unit:GetAltitude()
|
|
local altDelta = banditAlt - playerAlt
|
|
|
|
-- Check for dogfight situations
|
|
local onTail = relativeBearing > 150 and relativeBearing < 210
|
|
local headOn = relativeBearing < 30 or relativeBearing > 330
|
|
local beam = (relativeBearing > 60 and relativeBearing < 120) or (relativeBearing > 240 and relativeBearing < 300)
|
|
local overshoot = distance < PILOT_INTUITION_CONFIG.mergeRange and lastRelativeBearing < 90 and relativeBearing > 270 and not closing
|
|
|
|
-- High/Low merge detection
|
|
local highMerge = distance < PILOT_INTUITION_CONFIG.mergeRange and closing and altDelta > PILOT_INTUITION_CONFIG.highMergeAltitude
|
|
local lowMerge = distance < PILOT_INTUITION_CONFIG.mergeRange and closing and altDelta < -PILOT_INTUITION_CONFIG.lowMergeAltitude
|
|
local coAltMerge = distance < PILOT_INTUITION_CONFIG.mergeRange and closing and math.abs(altDelta) < 200
|
|
|
|
-- Separation after being hot
|
|
local separating = wasHot and distance > PILOT_INTUITION_CONFIG.separatingRange and not closing
|
|
|
|
-- Build threat descriptor for ALL detected bandits
|
|
local threatType = nil
|
|
local threatDetail = nil
|
|
|
|
-- Determine threat level based on aspect angle AND distance
|
|
-- Hot = nose-on (0-45°), Cold = tail aspect (135-180°), Flanking/Beam = side aspect
|
|
local threatLevel = "distant"
|
|
if distance <= PILOT_INTUITION_CONFIG.threatHotRange then
|
|
threatLevel = "hot"
|
|
elseif distance <= PILOT_INTUITION_CONFIG.threatColdRange then
|
|
-- Use aspect angle to determine if truly hot or cold
|
|
if aspectAngle <= 45 then
|
|
threatLevel = "hot" -- Nose-on, coming at us
|
|
elseif aspectAngle >= 135 then
|
|
threatLevel = "cold" -- Tail aspect, running away
|
|
else
|
|
threatLevel = "flanking" -- Side aspect
|
|
end
|
|
else
|
|
threatLevel = "distant"
|
|
end
|
|
|
|
-- Check for specific tactical situations (high priority)
|
|
if highMerge then
|
|
threatType = "high merge"
|
|
threatDetail = string.format("%.0fm above", altDelta)
|
|
elseif lowMerge then
|
|
threatType = "low merge"
|
|
threatDetail = string.format("%.0fm below", math.abs(altDelta))
|
|
elseif coAltMerge then
|
|
threatType = "co-alt merge"
|
|
elseif onTail and distance < PILOT_INTUITION_CONFIG.tailWarningRange then
|
|
if altDelta > 200 then
|
|
threatType = "tail high"
|
|
elseif altDelta < -200 then
|
|
threatType = "tail low"
|
|
else
|
|
threatType = "tail"
|
|
end
|
|
elseif headOn and distance < PILOT_INTUITION_CONFIG.headOnRange then
|
|
threatType = "head-on"
|
|
if math.abs(altDelta) > 200 then
|
|
threatDetail = string.format("alt %.0f", altDelta)
|
|
end
|
|
elseif beam and distance < PILOT_INTUITION_CONFIG.beamRange then
|
|
threatType = "beam"
|
|
local side = (relativeBearing > 180) and "left" or "right"
|
|
threatDetail = side
|
|
elseif separating then
|
|
threatType = "separating"
|
|
elseif overshoot then
|
|
threatType = "overshot"
|
|
else
|
|
-- Generic aspect-based description for distant contacts
|
|
local clockPos = math.floor((relativeBearing + 15) / 30) + 1
|
|
if clockPos > 12 then clockPos = clockPos - 12 end
|
|
threatType = threatLevel
|
|
local distValue, distUnit = self:FormatDistance(distance, playerKey)
|
|
threatDetail = string.format("%d o'clock, %.1f%s", clockPos, distValue, distUnit)
|
|
if closing then
|
|
threatDetail = threatDetail .. ", closing"
|
|
end
|
|
end
|
|
|
|
-- Add ALL detected bandits to the list for multi-bandit reporting
|
|
local banditName = unit:GetPlayerName() or unit:GetName()
|
|
table.insert(threateningBandits, {
|
|
unit = unit,
|
|
distance = distance,
|
|
threatType = threatType,
|
|
threatDetail = threatDetail,
|
|
relativeBearing = relativeBearing,
|
|
threatLevel = threatLevel,
|
|
banditName = banditName
|
|
})
|
|
end
|
|
|
|
-- Dogfight assist features (if enabled)
|
|
if playerData.dogfightAssist and data.engaged then
|
|
-- Calculate effective cooldown based on combat intensity
|
|
local baseCooldown = PILOT_INTUITION_CONFIG.dogfightMessageCooldown
|
|
local effectiveCooldown = baseCooldown
|
|
|
|
-- If in high-intensity combat (many bandits), increase cooldown to reduce spam
|
|
local engagedBandits = 0
|
|
for _, targetData in pairs(playerData.trackedAirTargets) do
|
|
if targetData.engaged then
|
|
engagedBandits = engagedBandits + 1
|
|
end
|
|
end
|
|
|
|
if engagedBandits >= PILOT_INTUITION_CONFIG.combatIntensityThreshold then
|
|
effectiveCooldown = baseCooldown * PILOT_INTUITION_CONFIG.combatIntensityCooldownMultiplier
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: High-intensity combat (%d bandits) - dogfight cooldown increased to %.1fs", engagedBandits, effectiveCooldown))
|
|
end
|
|
|
|
self:ProvideDogfightAssist(playerUnit, unit, distance, relativeBearing, lastRelativeBearing, playerData, client, closing, effectiveCooldown)
|
|
end
|
|
end -- End if hasLOS
|
|
else
|
|
PILog(LOG_TRACE, string.format("PilotIntuition: Unit %s out of range: %.0fm > %.0fm",
|
|
unit:GetName(), distance, detectionRange))
|
|
end -- End if distance <= detectionRange
|
|
end -- End if unitCoord
|
|
end -- End if unit and unit:IsAlive()
|
|
end -- End for _, unit in ipairs(enemyAirUnits)
|
|
|
|
-- Report multiple bandits situation with tactical picture
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: Threatening bandits count: %d", #threateningBandits))
|
|
|
|
-- COMBAT FOCUS: Filter out distant threats when in close combat
|
|
local inCloseCombat = false
|
|
local combatFocusRange = PILOT_INTUITION_CONFIG.threatHotRange * 2 -- 2km threshold for "close combat"
|
|
for _, threat in ipairs(threateningBandits) do
|
|
if threat.distance <= combatFocusRange and threat.threatLevel == "hot" then
|
|
inCloseCombat = true
|
|
break
|
|
end
|
|
end
|
|
|
|
-- If in close combat, filter to only show immediate threats
|
|
local filteredThreats = threateningBandits
|
|
if inCloseCombat then
|
|
filteredThreats = {}
|
|
for _, threat in ipairs(threateningBandits) do
|
|
-- Only report threats within combat focus range OR threats that are hot/flanking
|
|
if threat.distance <= combatFocusRange or threat.threatLevel == "hot" or threat.threatLevel == "flanking" then
|
|
table.insert(filteredThreats, threat)
|
|
end
|
|
end
|
|
PILog(LOG_DEBUG, string.format("PilotIntuition: COMBAT FOCUS - Filtered %d distant threats, showing %d immediate threats",
|
|
#threateningBandits - #filteredThreats, #filteredThreats))
|
|
end
|
|
|
|
if #filteredThreats > 0 then
|
|
for i, threat in ipairs(filteredThreats) do
|
|
PILog(LOG_DEBUG, string.format(" Threat %d: %s at %.0fm, type: %s", i, threat.unit:GetName(), threat.distance, threat.threatType))
|
|
end
|
|
local now = timer.getTime()
|
|
if (now - playerData.lastDogfightTime) >= (PILOT_INTUITION_CONFIG.messageCooldown * playerData.frequencyMultiplier) then
|
|
local message = ""
|
|
if #filteredThreats == 1 then
|
|
-- Single bandit - detailed callout
|
|
local threat = filteredThreats[1]
|
|
local banditName = threat.banditName or "bandit"
|
|
if threat.threatDetail then
|
|
message = string.format("%s - %s! %s", banditName, threat.threatType:gsub("^%l", string.upper), threat.threatDetail)
|
|
else
|
|
message = string.format("%s - %s!", banditName, threat.threatType:gsub("^%l", string.upper))
|
|
end
|
|
else
|
|
-- Multiple bandits - build tactical picture, showing only most threatening
|
|
-- Sort by threat priority: hot > flanking > cold > distant, then by distance
|
|
table.sort(filteredThreats, function(a, b)
|
|
local priorityOrder = {hot = 1, flanking = 2, cold = 3, distant = 4}
|
|
local aPriority = priorityOrder[a.threatLevel] or 5
|
|
local bPriority = priorityOrder[b.threatLevel] or 5
|
|
if aPriority ~= bPriority then
|
|
return aPriority < bPriority
|
|
else
|
|
return a.distance < b.distance
|
|
end
|
|
end)
|
|
|
|
local maxDisplay = PILOT_INTUITION_CONFIG.maxThreatDisplay
|
|
local displayCount = math.min(maxDisplay, #filteredThreats)
|
|
local totalCount = #filteredThreats
|
|
|
|
message = string.format("Multiple bandits - %d contacts", totalCount)
|
|
if displayCount < totalCount then
|
|
message = message .. string.format(" (showing %d most threatening)", displayCount)
|
|
end
|
|
message = message .. ": "
|
|
|
|
local threats = {}
|
|
for i = 1, displayCount do
|
|
local threat = filteredThreats[i]
|
|
local banditName = threat.banditName or "unknown"
|
|
local desc = banditName .. " - " .. threat.threatType
|
|
if threat.threatDetail then
|
|
desc = desc .. " (" .. threat.threatDetail .. ")"
|
|
end
|
|
table.insert(threats, desc)
|
|
end
|
|
message = message .. table.concat(threats, ", ")
|
|
end
|
|
|
|
if PILOT_INTUITION_CONFIG.activeMessaging then
|
|
MESSAGE:New(message, 10):ToClient(client)
|
|
end
|
|
playerData.lastDogfightTime = now
|
|
|
|
-- Mark only HOT bandits as engaged (within hot range or actively threatening)
|
|
for _, threat in ipairs(threateningBandits) do
|
|
local data = playerData.trackedAirTargets[threat.unit:GetName()]
|
|
if data and threat.threatLevel == "hot" then
|
|
data.engaged = true
|
|
data.lastEngagedTime = now
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Report the closest unengaged bandit (if no threats)
|
|
if #threateningBandits == 0 and closestUnit then
|
|
local data = playerData.trackedAirTargets[closestUnit:GetName()]
|
|
if data and not data.engaged then
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
self:ReportAirTarget(closestUnit, playerPos, playerData, client, playerKey)
|
|
end
|
|
end
|
|
|
|
-- Check for multiple bandits (with 5-minute cooldown to reduce spam)
|
|
if banditCount > 1 then
|
|
local now = timer.getTime()
|
|
if (now - (playerData.lastMultipleBanditsWarningTime or 0)) >= PILOT_INTUITION_CONFIG.multipleBanditsWarningCooldown then
|
|
self:ReportDogfight(nil, playerPos, playerData, client, "Multiple bandits in vicinity!")
|
|
playerData.lastMultipleBanditsWarningTime = now
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:ReportAirTarget(unit, playerPos, playerData, client, playerKey)
|
|
local now = timer.getTime()
|
|
if now - playerData.lastMessageTime < (PILOT_INTUITION_CONFIG.messageCooldown * playerData.frequencyMultiplier) then return end
|
|
if not PILOT_INTUITION_CONFIG.activeMessaging then return end
|
|
|
|
local unitCoord = unit:GetCoordinate()
|
|
if not unitCoord then
|
|
PILog(LOG_DEBUG, "PilotIntuition: Could not get unit coordinate for air target report")
|
|
return
|
|
end
|
|
|
|
local bearing = playerPos:GetAngleDegrees(playerPos:GetDirectionVec3(unitCoord))
|
|
local rangeMeters = playerPos:Get2DDistance(unitCoord)
|
|
local range, distUnit = self:FormatDistance(rangeMeters, playerKey)
|
|
-- Convert altitude to angels (thousands of feet)
|
|
local altMeters = unit:GetAltitude()
|
|
local altFeet = altMeters * 3.28084
|
|
local angels = math.floor(altFeet / 1000)
|
|
local threat = "cold"
|
|
if rangeMeters <= PILOT_INTUITION_CONFIG.threatHotRange then
|
|
threat = "hot"
|
|
end
|
|
|
|
local group = unit:GetGroup()
|
|
if not group then
|
|
PILog(LOG_DEBUG, "PilotIntuition: Could not get group for air target")
|
|
return
|
|
end
|
|
local groupSize = #group:GetUnits()
|
|
local sizeDesc = groupSize == 1 and "single" or (groupSize == 2 and "pair" or "flight of " .. groupSize)
|
|
|
|
MESSAGE:New(self:GetRandomMessage("airTargetDetected", {threat, bearing, range, distUnit, angels, sizeDesc}), 10):ToClient(client)
|
|
playerData.lastMessageTime = now
|
|
end
|
|
|
|
function PilotIntuition:ReportDogfight(unit, playerPos, playerData, client, message)
|
|
local now = timer.getTime()
|
|
if now - playerData.lastMessageTime < (PILOT_INTUITION_CONFIG.messageCooldown * playerData.frequencyMultiplier) then return end
|
|
if not PILOT_INTUITION_CONFIG.activeMessaging then return end
|
|
|
|
MESSAGE:New(message, 10):ToClient(client)
|
|
playerData.lastMessageTime = now
|
|
end
|
|
|
|
function PilotIntuition:ScanGroundTargetsForPlayer(playerUnit, client, activeClients, enemyGroundGroups, placeMarker)
|
|
if placeMarker == nil then placeMarker = true end
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
local playerCoalition = playerUnit:GetCoalition()
|
|
local enemyCoalition = (playerCoalition == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
|
|
local unitName = playerUnit:GetName()
|
|
local playerData = self.players[unitName]
|
|
if not playerData then return end
|
|
|
|
-- Use cached wingmen count
|
|
local wingmen = playerData.cachedWingmen
|
|
local multiplier = (wingmen > 0) and (2 * wingmen) or 1
|
|
if multiplier > PILOT_INTUITION_CONFIG.maxMultiplier then
|
|
multiplier = PILOT_INTUITION_CONFIG.maxMultiplier
|
|
end
|
|
local envMult = self:GetDetectionMultiplier()
|
|
local detectionRange = PILOT_INTUITION_CONFIG.groundDetectionRange * multiplier * envMult
|
|
|
|
-- Prune out-of-range ground targets for this player
|
|
for id, _ in pairs(playerData.trackedGroundTargets) do
|
|
local group = self.trackedGroundTargets[id]
|
|
if not group or not group.group:IsAlive() or playerPos:Get2DDistance(group.group:GetCoordinate()) > detectionRange then
|
|
playerData.trackedGroundTargets[id] = nil
|
|
end
|
|
end
|
|
|
|
-- Find the closest unmarked target for this player (no sorting needed)
|
|
local closestGroup = nil
|
|
local minDistance = math.huge
|
|
for _, group in ipairs(enemyGroundGroups or {}) do
|
|
local distance = playerPos:Get2DDistance(group:GetCoordinate())
|
|
if distance <= detectionRange then
|
|
local targetID = group:GetName()
|
|
if not self.trackedGroundTargets[targetID] then
|
|
self.trackedGroundTargets[targetID] = { group = group, marked = false }
|
|
end
|
|
if not playerData.trackedGroundTargets[targetID] and distance < minDistance then
|
|
closestGroup = group
|
|
minDistance = distance
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Mark and report the closest unmarked target
|
|
if closestGroup then
|
|
local targetID = closestGroup:GetName()
|
|
self:ReportGroundTarget(closestGroup, playerUnit, client, placeMarker)
|
|
self.trackedGroundTargets[targetID].marked = true
|
|
playerData.trackedGroundTargets[targetID] = true
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:CheckCloseFlyingForPlayer(playerUnit, playerData, client, activeClients)
|
|
if not PILOT_INTUITION_CONFIG.enableCloseFlyingCompliments then return end
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
local playerCoalition = playerUnit:GetCoalition()
|
|
local now = timer.getTime()
|
|
local playerHeading = math.deg(playerUnit:GetHeading())
|
|
|
|
for _, info in ipairs(activeClients) do
|
|
local otherUnit = info.unit
|
|
if otherUnit and otherUnit:IsAlive() and otherUnit:GetCoalition() == playerCoalition and otherUnit:GetName() ~= playerUnit:GetName() then
|
|
local otherCoord = otherUnit:GetCoordinate()
|
|
if otherCoord then
|
|
local distance = playerPos:Get2DDistance(otherCoord)
|
|
if distance <= PILOT_INTUITION_CONFIG.complimentRange then
|
|
-- Calculate relative bearing
|
|
local bearing = playerPos:GetAngleDegrees(playerPos:GetDirectionVec3(otherCoord))
|
|
local relativeBearing = bearing - playerHeading
|
|
relativeBearing = (relativeBearing % 360 + 360) % 360 -- Normalize to 0-360
|
|
|
|
-- Check if head-on (within 30 degrees of front or back, but for pass, focus on front)
|
|
local isHeadOn = relativeBearing < 30 or relativeBearing > 330
|
|
|
|
if isHeadOn and distance <= PILOT_INTUITION_CONFIG.headOnWarningRange then
|
|
if now - playerData.lastHeadOnWarningTime >= (PILOT_INTUITION_CONFIG.closeFlyingMessageCooldown * playerData.frequencyMultiplier) then
|
|
MESSAGE:New(self:GetRandomMessage("headOnWarning"), 10):ToClient(client)
|
|
playerData.lastHeadOnWarningTime = now
|
|
end
|
|
elseif not isHeadOn then
|
|
if now - playerData.lastComplimentTime >= (PILOT_INTUITION_CONFIG.closeFlyingMessageCooldown * playerData.frequencyMultiplier) then
|
|
MESSAGE:New(self:GetRandomMessage("closeFlyingCompliment"), 10):ToClient(client)
|
|
playerData.lastComplimentTime = now
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:ReportGroundTarget(group, playerUnit, client, placeMarker)
|
|
if placeMarker == nil then placeMarker = true end
|
|
local now = timer.getTime()
|
|
if now - self.lastMessageTime < PILOT_INTUITION_CONFIG.messageCooldown then return end
|
|
if not PILOT_INTUITION_CONFIG.activeMessaging then return end
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
local targetPos = group:GetCoordinate()
|
|
local bearing = playerPos:HeadingTo(targetPos) -- Returns heading in degrees as number
|
|
|
|
-- Ensure bearing is a valid number
|
|
if not bearing or type(bearing) ~= "number" then
|
|
bearing = 0 -- Fallback to 0 if bearing is invalid
|
|
end
|
|
|
|
local distanceMeters = playerPos:Get2DDistance(targetPos)
|
|
local playerKey = self:GetPlayerDataKey(playerUnit)
|
|
local distance, unit = self:FormatDistance(distanceMeters, playerKey)
|
|
local unitType = group:GetUnits()[1]:GetTypeName()
|
|
local category = self:ClassifyGroundUnit(unitType)
|
|
local groupSize = #group:GetUnits()
|
|
local sizeDesc = groupSize == 1 and "single" or (groupSize <= 4 and "group" or "platoon")
|
|
|
|
MESSAGE:New(self:GetRandomMessage("groundTargetDetected", {category, sizeDesc, unitType, bearing, distance, unit}), 10):ToClient(client)
|
|
self.lastMessageTime = now
|
|
|
|
-- Place marker if requested
|
|
if placeMarker then
|
|
-- Determine marker type: prefer player preference if set, otherwise global config
|
|
local markerType = PILOT_INTUITION_CONFIG.markerType
|
|
local playerName = playerUnit:GetPlayerName() or playerUnit:GetName()
|
|
if self.players[playerName] and self.players[playerName].markerType then
|
|
markerType = self.players[playerName].markerType
|
|
end
|
|
if markerType ~= "none" then
|
|
local coord = group:GetCoordinate()
|
|
local markerTypePart, color = markerType:match("(%w+)_(%w+)")
|
|
if markerTypePart == "smoke" then
|
|
if color == "red" then coord:SmokeRed()
|
|
elseif color == "green" then coord:SmokeGreen()
|
|
elseif color == "blue" then coord:SmokeBlue()
|
|
elseif color == "white" then coord:SmokeWhite()
|
|
end
|
|
elseif markerTypePart == "flare" then
|
|
if color == "red" then coord:FlareRed()
|
|
elseif color == "green" then coord:FlareGreen()
|
|
elseif color == "white" then coord:FlareWhite()
|
|
end
|
|
end
|
|
-- Note: Markers are temporary; Moose doesn't have built-in timed markers, so this is basic
|
|
end
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:GetDetectionMultiplier()
|
|
-- Basic night detection: reduce range at night (22:00 to 06:00)
|
|
local time = timer.getAbsTime() % 86400
|
|
local hour = math.floor(time / 3600)
|
|
local isNight = hour >= 22 or hour < 6
|
|
local mult = 1
|
|
if isNight then
|
|
mult = mult * PILOT_INTUITION_CONFIG.nightDetectionMultiplier
|
|
end
|
|
-- TODO: Add weather check if available
|
|
return mult
|
|
end
|
|
|
|
function PilotIntuition:ClassifyGroundUnit(unitType)
|
|
unitType = string.lower(unitType)
|
|
if string.find(unitType, "tank") or string.find(unitType, "armor") or string.find(unitType, "panzer") then
|
|
return "Armor"
|
|
elseif string.find(unitType, "infantry") or string.find(unitType, "soldier") then
|
|
return "Infantry"
|
|
elseif string.find(unitType, "aaa") or string.find(unitType, "flak") or string.find(unitType, "aa") then
|
|
return "Anti-Air"
|
|
elseif string.find(unitType, "truck") or string.find(unitType, "vehicle") or string.find(unitType, "transport") then
|
|
return "Logistics"
|
|
else
|
|
return "Ground"
|
|
end
|
|
end
|
|
|
|
function PilotIntuition:ProvideDogfightAssist(playerUnit, banditUnit, distance, relativeBearing, lastRelativeBearing, playerData, client, closing, effectiveCooldown)
|
|
local now = timer.getTime()
|
|
-- Use provided effectiveCooldown (for combat intensity adjustment) or fall back to default
|
|
local cooldown = effectiveCooldown or PILOT_INTUITION_CONFIG.dogfightMessageCooldown
|
|
if (now - playerData.lastDogfightAssistTime) < (cooldown * playerData.frequencyMultiplier) then
|
|
return
|
|
end
|
|
if not PILOT_INTUITION_CONFIG.activeMessaging then return end
|
|
|
|
local playerPos = playerUnit:GetCoordinate()
|
|
local banditPos = banditUnit:GetCoordinate()
|
|
local playerAlt = playerUnit:GetAltitude()
|
|
local banditAlt = banditUnit:GetAltitude()
|
|
local altDelta = banditAlt - playerAlt
|
|
|
|
-- Check for speed warning
|
|
local velocity = playerUnit:GetVelocityKMH()
|
|
local speedKnots = velocity * 0.539957
|
|
if speedKnots < PILOT_INTUITION_CONFIG.criticalSpeedThreshold then
|
|
MESSAGE:New("Speed critical! Extend!", 10):ToClient(client)
|
|
playerData.lastDogfightAssistTime = now
|
|
return
|
|
end
|
|
|
|
-- Target loss detection (front to rear hemisphere)
|
|
if lastRelativeBearing and lastRelativeBearing < 90 and relativeBearing > 270 then
|
|
MESSAGE:New("Lost visual! Bandit reversing on you!", 10):ToClient(client)
|
|
playerData.lastDogfightAssistTime = now
|
|
return
|
|
end
|
|
|
|
-- Check for bandit moving behind (now behind, wasn't before)
|
|
if relativeBearing > 150 and relativeBearing < 210 and distance < PILOT_INTUITION_CONFIG.tailWarningRange then
|
|
local direction = relativeBearing < 180 and "left" or "right"
|
|
MESSAGE:New("Bandit at 6 o'clock! Break " .. direction .. "!", 10):ToClient(client)
|
|
playerData.lastDogfightAssistTime = now
|
|
return
|
|
end
|
|
|
|
-- Closure rate warning (check this BEFORE altitude to prioritize immediate threats)
|
|
if closing and distance < PILOT_INTUITION_CONFIG.threatHotRange then
|
|
MESSAGE:New("Bandit closing fast! Prepare to engage!", 10):ToClient(client)
|
|
playerData.lastDogfightAssistTime = now
|
|
return
|
|
end
|
|
|
|
-- Significant position change
|
|
if playerData.lastPrimaryTargetBearing then
|
|
local bearingChange = math.abs(relativeBearing - playerData.lastPrimaryTargetBearing)
|
|
if bearingChange > PILOT_INTUITION_CONFIG.positionChangeThreshold and bearingChange < (360 - PILOT_INTUITION_CONFIG.positionChangeThreshold) then
|
|
local clockPos = math.floor(relativeBearing / 30) + 1
|
|
if clockPos > 12 then clockPos = clockPos - 12 end
|
|
MESSAGE:New("Bandit moving to " .. clockPos .. " o'clock!", 10):ToClient(client)
|
|
playerData.lastDogfightAssistTime = now
|
|
playerData.lastPrimaryTargetBearing = relativeBearing
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Altitude advantage/disadvantage (lower priority - only if no other callouts triggered)
|
|
if math.abs(altDelta) > PILOT_INTUITION_CONFIG.altitudeDeltaThreshold then
|
|
local clockPos = math.floor(relativeBearing / 30) + 1
|
|
if clockPos > 12 then clockPos = clockPos - 12 end
|
|
local distKM = math.floor(distance / 1000 * 10) / 10
|
|
|
|
if altDelta > 0 then
|
|
-- Bandit is higher
|
|
local altKFeet = math.floor(altDelta * 3.28084 / 1000)
|
|
MESSAGE:New(string.format("Bandit %d o'clock, %.1fkm, %dk above!", clockPos, distKM, altKFeet), 10):ToClient(client)
|
|
else
|
|
-- You have altitude advantage
|
|
local altKFeet = math.floor(math.abs(altDelta) * 3.28084 / 1000)
|
|
MESSAGE:New(string.format("Bandit %d o'clock, %.1fkm, you're %dk high!", clockPos, distKM, altKFeet), 10):ToClient(client)
|
|
end
|
|
playerData.lastDogfightAssistTime = now
|
|
end
|
|
|
|
playerData.lastPrimaryTargetBearing = relativeBearing
|
|
end
|
|
|
|
-- Handle engagement (simplified: if player shoots, assume engagement for nearest target)
|
|
function PilotIntuition:OnPlayerShot(EventData)
|
|
if not self.enabled then return end
|
|
local playerUnit = EventData.IniUnit
|
|
if not playerUnit then return end
|
|
local client = playerUnit:GetClient()
|
|
if client then
|
|
local unitName = playerUnit:GetName()
|
|
if self.players[unitName] then
|
|
local playerData = self.players[unitName]
|
|
-- Calculate wingmen for extended range
|
|
local clients = SET_CLIENT:New():FilterActive():FilterOnce()
|
|
local wingmen = 0
|
|
clients:ForEachClient(function(c)
|
|
if c and type(c.GetUnit) == "function" then
|
|
local u = c:GetUnit()
|
|
-- Validate playerUnit still exists before using it
|
|
if u and u:IsAlive() and playerUnit and playerUnit:IsAlive() and u:GetCoalition() == playerUnit:GetCoalition() and u:GetName() ~= playerUnit:GetName() then
|
|
local playerCoord = playerUnit:GetCoordinate()
|
|
local uCoord = u:GetCoordinate()
|
|
if playerCoord and uCoord then
|
|
local dist = playerCoord:Get2DDistance(uCoord)
|
|
if dist <= PILOT_INTUITION_CONFIG.formationRange then
|
|
wingmen = wingmen + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
local multiplier = (wingmen > 0) and (2 * wingmen) or 1
|
|
if multiplier > PILOT_INTUITION_CONFIG.maxMultiplier then
|
|
multiplier = PILOT_INTUITION_CONFIG.maxMultiplier
|
|
end
|
|
local detectionRange = PILOT_INTUITION_CONFIG.airDetectionRange * multiplier
|
|
-- Find nearest air target and mark as engaged
|
|
local minDist = math.huge
|
|
local nearestID = nil
|
|
for id, data in pairs(playerData.trackedAirTargets) do
|
|
if not data.engaged and data.unit and data.unit:IsAlive() and playerUnit and playerUnit:IsAlive() then
|
|
local playerCoord = playerUnit:GetCoordinate()
|
|
local targetCoord = data.unit:GetCoordinate()
|
|
if playerCoord and targetCoord then
|
|
local distance = playerCoord:Get2DDistance(targetCoord)
|
|
if distance < minDist then
|
|
minDist = distance
|
|
nearestID = id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if nearestID and minDist <= detectionRange then
|
|
playerData.trackedAirTargets[nearestID].engaged = true
|
|
playerData.trackedAirTargets[nearestID].lastEngagedTime = timer.getTime()
|
|
if PILOT_INTUITION_CONFIG.activeMessaging then
|
|
MESSAGE:New(self:GetRandomMessage("dogfightEngaged"), 10):ToClient(client)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle being shot at
|
|
function PilotIntuition:OnShotFired(EventData)
|
|
if not self.enabled then return end
|
|
local shooter = EventData.IniUnit
|
|
if not shooter then return end
|
|
|
|
-- Check if any player is the target
|
|
local clients = SET_CLIENT:New():FilterActive():FilterOnce()
|
|
clients:ForEachClient(function(client)
|
|
if client and type(client.GetUnit) == "function" then
|
|
local playerUnit = client:GetUnit()
|
|
if playerUnit and playerUnit:IsAlive() and playerUnit:GetCoalition() ~= shooter:GetCoalition() then
|
|
local playerName = playerUnit:GetPlayerName() or playerUnit:GetName()
|
|
if self.players[playerName] and self.players[playerName].dogfightAssist then
|
|
local playerData = self.players[playerName]
|
|
local now = timer.getTime()
|
|
if not PILOT_INTUITION_CONFIG.activeMessaging then return end
|
|
|
|
-- Check if shot is directed at player (within range and aspect)
|
|
local playerCoord = playerUnit:GetCoordinate()
|
|
local shooterCoord = shooter:GetCoordinate()
|
|
if not playerCoord or not shooterCoord then
|
|
return
|
|
end
|
|
local distance = playerCoord:Get2DDistance(shooterCoord)
|
|
if distance < 1500 and (now - playerData.lastDogfightAssistTime) >= (PILOT_INTUITION_CONFIG.dogfightMessageCooldown * playerData.frequencyMultiplier) then
|
|
local bearing = playerCoord:GetAngleDegrees(playerCoord:GetDirectionVec3(shooterCoord))
|
|
local playerHeading = math.deg(playerUnit:GetHeading())
|
|
local relativeBearing = (bearing - playerHeading + 360) % 360
|
|
|
|
-- Determine evasion direction
|
|
local direction = "left"
|
|
if relativeBearing > 180 then
|
|
direction = "right"
|
|
end
|
|
|
|
-- Check if above or below
|
|
local altDelta = shooter:GetAltitude() - playerUnit:GetAltitude()
|
|
local vertical = ""
|
|
if altDelta > 100 then
|
|
vertical = " Push!"
|
|
elseif altDelta < -100 then
|
|
vertical = " Pull!"
|
|
end
|
|
|
|
MESSAGE:New(self:GetRandomMessage("underFire", {direction, vertical})):ToClient(client)
|
|
playerData.lastDogfightAssistTime = now
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Initialize the Pilot Intuition system
|
|
PILog(LOG_INFO, "====== PILOT INTUITION SYSTEM STARTING ======")
|
|
local pilotIntuitionSystem = PilotIntuition:New()
|
|
PILog(LOG_INFO, "====== PILOT INTUITION SYSTEM INITIALIZED ======") |