mirror of
https://github.com/iTracerFacer/Moose_DualCoalitionZoneCapture.git
synced 2026-07-16 14:32:31 +00:00
1659 lines
67 KiB
Lua
1659 lines
67 KiB
Lua
---@diagnostic disable: undefined-global, lowercase-global
|
|
-- MOOSE framework globals are defined at runtime by DCS World
|
|
-- **Author**: F99th-TracerFacer
|
|
-- **Discord:** https://discord.gg/NdZ2JuSU (The Fighting 99th Discord Server where I spend most of my time.)
|
|
|
|
|
|
-- ==========================================
|
|
-- SYNOPSIS
|
|
-- ==========================================
|
|
-- This script implements a dual-coalition zone capture system for DCS World using the MOOSE framework.
|
|
-- It allows two coalitions (Blue and Red) to capture and control strategic zones on the map.
|
|
-- Zones change ownership based on unit presence, with visual markers, tactical information, and victory conditions.
|
|
--
|
|
-- SIMPLE INSTRUCTIONS:
|
|
-- 1. Configure the zones in the ZONE_CONFIG table below (add zone names under RED, BLUE, or NEUTRAL).
|
|
-- 2. Set coalition display names in COALITION_TITLES (e.g., "USA" for Blue, "Russia" for Red).
|
|
-- 3. Adjust zone colors in ZONE_COLORS if needed (RGB values 0.0 to 1.0).
|
|
-- 4. Ensure zone names match exactly with trigger zones defined in the DCS mission editor.
|
|
-- 5. Load the script in your DCS mission and ensure the MOOSE framework is installed.
|
|
-- 6. Use F10 radio menu for zone status reports and victory progress during the mission.
|
|
|
|
|
|
|
|
-- ==========================================
|
|
-- MESSAGE AND TIMING CONFIGURATION
|
|
-- ==========================================
|
|
local MESSAGE_CONFIG = {
|
|
STATUS_BROADCAST_FREQUENCY = 3602, -- Zone status broadcast cadence (seconds)
|
|
STATUS_BROADCAST_START_DELAY = 10, -- Delay before first broadcast (seconds)
|
|
COLOR_VERIFICATION_FREQUENCY = 240, -- Zone color verification cadence (seconds)
|
|
COLOR_VERIFICATION_START_DELAY = 60, -- Delay before first color check (seconds)
|
|
TACTICAL_UPDATE_FREQUENCY = 180, -- Tactical marker update cadence (seconds)
|
|
TACTICAL_UPDATE_START_DELAY = 30, -- Delay before first tactical update (seconds)
|
|
STATUS_MESSAGE_DURATION = 15, -- How long general status messages stay onscreen
|
|
VICTORY_MESSAGE_DURATION = 300, -- How long victory/defeat alerts stay onscreen
|
|
CAPTURE_MESSAGE_DURATION = 15, -- Duration for capture/guard/empty notices
|
|
ATTACK_MESSAGE_DURATION = 15, -- Duration for attack alerts
|
|
GARBAGE_COLLECTION_FREQUENCY = 600 -- Lua garbage collection cadence (seconds) - helps prevent memory buildup
|
|
}
|
|
|
|
-- ==========================================
|
|
-- ZONE CONFIGURATION
|
|
-- ==========================================
|
|
-- Mission makers: Edit this table to define zones and their initial ownership
|
|
-- Just list the zone names under RED, BLUE, or NEUTRAL coalition
|
|
-- The script will automatically create and configure all zones
|
|
-- Make sure the zone names match exactly with those defined in the mission editor
|
|
-- Zones must be defined in the mission editor as trigger zones named "Capture <ZoneName>"
|
|
-- Note: Red/Blue/Neutral zones defined below are only setting their initial ownership state.
|
|
-- If there are existing units in the zone at mission start, ownership may change based on unit presence.
|
|
|
|
local ZONE_CONFIG = {
|
|
-- Zones that start under RED coalition control
|
|
-- IMPORTANT: Use the EXACT zone names from the mission editor (including "Capture " prefix if present)
|
|
RED = {
|
|
"Capture Zone-1",
|
|
"Capture Zone-2",
|
|
"Capture Zone-3",
|
|
|
|
-- Add more zone names here for RED starting zones
|
|
},
|
|
|
|
-- Zones that start under BLUE coalition control
|
|
BLUE = {
|
|
"Capture Zone-4",
|
|
"Capture Zone-5",
|
|
"Capture Zone-6",
|
|
},
|
|
|
|
-- Zones that start neutral (empty/uncontrolled)
|
|
NEUTRAL = {
|
|
|
|
}
|
|
}
|
|
|
|
-- Advanced settings (usually don't need to change these)
|
|
local ZONE_SETTINGS = {
|
|
guardDelay = 1, -- Delay before entering Guard state after capture
|
|
scanInterval = 30, -- How often to scan for units in the zone (seconds)
|
|
captureScore = 200 -- Points awarded for capturing a zone
|
|
}
|
|
|
|
-- ==========================================
|
|
-- ZONE COLOR CONFIGURATION (Centralized)
|
|
-- ==========================================
|
|
-- Colors are in RGB format: {Red, Green, Blue} where each value is 0.0 to 1.0
|
|
local ZONE_COLORS = {
|
|
-- Blue coalition zones
|
|
BLUE_CAPTURED = {0, 0, 1}, -- Blue (owned by Blue)
|
|
BLUE_ATTACKED = {0, 1, 1}, -- Cyan (owned by Blue, under attack)
|
|
|
|
-- Red coalition zones
|
|
RED_CAPTURED = {1, 0, 0}, -- Red (owned by Red)
|
|
RED_ATTACKED = {1, 0.5, 0}, -- Orange (owned by Red, under attack)
|
|
|
|
-- Neutral/Empty zones
|
|
EMPTY = {0, 1, 0} -- Green (no owner)
|
|
}
|
|
|
|
-- ==========================================
|
|
-- COALITION TITLES CONFIGURATION
|
|
-- ==========================================
|
|
-- Mission makers: Customize the display names for each coalition
|
|
-- These will be used in messages, mission names, and UI elements
|
|
local COALITION_TITLES = {
|
|
BLUE = "USA", -- Display name for Blue coalition (e.g., "USA", "NATO", "Allied Forces")
|
|
RED = "Russia", -- Display name for Red coalition (e.g., "Russia", "Germany", "Axis Powers")
|
|
BLUE_OPERATION = "Operation Polar Shield", -- Name of Blue coalition's operation
|
|
RED_OPERATION = "Defend the Motherland" -- Name of Red coalition's operation
|
|
}
|
|
|
|
-- ==========================================
|
|
-- LANGUAGE CONFIGURATION
|
|
-- ==========================================
|
|
local LANGUAGE_CONFIG = {
|
|
defaultLanguage = "EN", -- Default language: "EN", "DE", "FR", "ES", "RU"
|
|
}
|
|
|
|
-- Multilingual message table
|
|
-- Each language contains all messages and menu text
|
|
local ZONE_CAPTURE_LANGUAGES = {
|
|
EN = {
|
|
-- Zone state messages
|
|
zoneGuarded = "%s is under protection of the %s",
|
|
zoneEmpty = "%s is unprotected, and can be captured!",
|
|
zoneUnderAttack = "%s is under attack by %s",
|
|
zoneWeAttacking = "We are attacking %s",
|
|
zoneCapturedByEnemy = "%s is captured by the %s, we lost it!",
|
|
zoneCapturedByUs = "We captured %s, Excellent job!",
|
|
|
|
-- Victory messages
|
|
victoryBlue = "VICTORY! All capture zones have been secured by %s forces!\n\n%s is complete. Outstanding work!\nMission will end in 60 seconds.",
|
|
defeatBlue = "DEFEAT! All strategic positions have been lost to %s forces.\n\n%s has failed. Mission ending in 60 seconds.",
|
|
victoryRed = "VICTORY! All strategic positions secured for %s!\n\n%s forces have been repelled. Outstanding work!\nMission will end in 60 seconds.",
|
|
defeatRed = "DEFEAT! All capture zones have been lost to %s forces.\n\n%s has failed. Mission ending in 60 seconds.",
|
|
missionComplete = "Mission Complete! Congratulations on your victory!\nFinal Status: All %d strategic zones secured.",
|
|
|
|
-- Status report messages
|
|
zoneControlReport = "ZONE CONTROL REPORT:\nBlue Coalition: %d/%d zones\nRed Coalition: %d/%d zones\nNeutral: %d/%d zones",
|
|
zoneDetails = "\nZONE DETAILS:\n",
|
|
zoneDetailLine = "• %s: %s\n",
|
|
yourProgress = "\n\nYour Progress to Victory: %d%%",
|
|
|
|
-- Progress warning messages
|
|
blueNearVictory = "TACTICAL ALERT: %s forces control %d%% of strategic zones!\nVictory is within reach!",
|
|
redNearVictory = "WARNING: %s forces have captured %d%% of zones!\nDefend our positions!",
|
|
|
|
-- Coalition names
|
|
coalitionBlue = "Blue Coalition",
|
|
coalitionRed = "Red Coalition",
|
|
coalitionNeutral = "Neutral",
|
|
|
|
-- Menu items
|
|
menuZoneControl = "Zone Control",
|
|
menuStatusReport = "Get Zone Status Report",
|
|
menuVictoryProgress = "Check Victory Progress",
|
|
menuRefreshColors = "Refresh Zone Colors",
|
|
menuLanguage = "Language / Sprache / Langue",
|
|
menuEnglish = "English",
|
|
menuGerman = "Deutsch (German)",
|
|
menuFrench = "Français (French)",
|
|
menuSpanish = "Español (Spanish)",
|
|
menuRussian = "Русский (Russian)",
|
|
|
|
-- Victory progress messages
|
|
victoryProgressTitle = "VICTORY PROGRESS: %d%%",
|
|
zonesCaptured = "Zones Captured: %d/%d",
|
|
zonesRemaining = "Remaining: %d zones",
|
|
progressComplete = "MISSION COMPLETE!",
|
|
progressAlmostThere = "ALMOST THERE!",
|
|
progressGood = "GOOD PROGRESS!",
|
|
progressKeepFighting = "KEEP FIGHTING!",
|
|
|
|
-- System messages
|
|
zoneMarkersRefreshed = "Zone visual markers have been refreshed!",
|
|
languageChanged = "Language changed to English",
|
|
},
|
|
|
|
DE = {
|
|
-- Zone state messages
|
|
zoneGuarded = "%s steht unter dem Schutz der %s",
|
|
zoneEmpty = "%s ist ungeschützt und kann erobert werden!",
|
|
zoneUnderAttack = "%s wird von %s angegriffen",
|
|
zoneWeAttacking = "Wir greifen %s an",
|
|
zoneCapturedByEnemy = "%s wurde von %s erobert, wir haben es verloren!",
|
|
zoneCapturedByUs = "Wir haben %s erobert, hervorragende Arbeit!",
|
|
|
|
-- Victory messages
|
|
victoryBlue = "SIEG! Alle Eroberungszonen wurden von %s Streitkräften gesichert!\n\n%s ist abgeschlossen. Hervorragende Arbeit!\nMission endet in 60 Sekunden.",
|
|
defeatBlue = "NIEDERLAGE! Alle strategischen Positionen wurden an %s Streitkräfte verloren.\n\n%s ist gescheitert. Mission endet in 60 Sekunden.",
|
|
victoryRed = "SIEG! Alle strategischen Positionen für %s gesichert!\n\n%s Streitkräfte wurden zurückgeschlagen. Hervorragende Arbeit!\nMission endet in 60 Sekunden.",
|
|
defeatRed = "NIEDERLAGE! Alle Eroberungszonen wurden an %s Streitkräfte verloren.\n\n%s ist gescheitert. Mission endet in 60 Sekunden.",
|
|
missionComplete = "Mission erfolgreich! Herzlichen Glückwunsch zu Ihrem Sieg!\nEndstatus: Alle %d strategischen Zonen gesichert.",
|
|
|
|
-- Status report messages
|
|
zoneControlReport = "ZONENKONTROLLBERICHT:\nBlaue Koalition: %d/%d Zonen\nRote Koalition: %d/%d Zonen\nNeutral: %d/%d Zonen",
|
|
zoneDetails = "\nZONENDETAILS:\n",
|
|
zoneDetailLine = "• %s: %s\n",
|
|
yourProgress = "\n\nIhr Fortschritt zum Sieg: %d%%",
|
|
|
|
-- Progress warning messages
|
|
blueNearVictory = "TAKTISCHER ALARM: %s Streitkräfte kontrollieren %d%% der strategischen Zonen!\nSieg ist in Reichweite!",
|
|
redNearVictory = "WARNUNG: %s Streitkräfte haben %d%% der Zonen erobert!\nVerteidigt unsere Positionen!",
|
|
|
|
-- Coalition names
|
|
coalitionBlue = "Blaue Koalition",
|
|
coalitionRed = "Rote Koalition",
|
|
coalitionNeutral = "Neutral",
|
|
|
|
-- Menu items
|
|
menuZoneControl = "Zonenkontrolle",
|
|
menuStatusReport = "Zonenstatusbericht abrufen",
|
|
menuVictoryProgress = "Siegfortschritt prüfen",
|
|
menuRefreshColors = "Zonenfarben aktualisieren",
|
|
menuLanguage = "Language / Sprache / Langue",
|
|
menuEnglish = "English",
|
|
menuGerman = "Deutsch (German)",
|
|
menuFrench = "Français (French)",
|
|
menuSpanish = "Español (Spanish)",
|
|
menuRussian = "Русский (Russian)",
|
|
|
|
-- Victory progress messages
|
|
victoryProgressTitle = "SIEGFORTSCHRITT: %d%%",
|
|
zonesCaptured = "Zonen erobert: %d/%d",
|
|
zonesRemaining = "Verbleibend: %d Zonen",
|
|
progressComplete = "MISSION ABGESCHLOSSEN!",
|
|
progressAlmostThere = "FAST GESCHAFFT!",
|
|
progressGood = "GUTER FORTSCHRITT!",
|
|
progressKeepFighting = "WEITER KÄMPFEN!",
|
|
|
|
-- System messages
|
|
zoneMarkersRefreshed = "Zonenmarkierungen wurden aktualisiert!",
|
|
languageChanged = "Sprache auf Deutsch geändert",
|
|
},
|
|
|
|
FR = {
|
|
-- Zone state messages
|
|
zoneGuarded = "%s est sous la protection de %s",
|
|
zoneEmpty = "%s est sans protection et peut être capturée !",
|
|
zoneUnderAttack = "%s est attaquée par %s",
|
|
zoneWeAttacking = "Nous attaquons %s",
|
|
zoneCapturedByEnemy = "%s est capturée par %s, nous l'avons perdue !",
|
|
zoneCapturedByUs = "Nous avons capturé %s, excellent travail !",
|
|
|
|
-- Victory messages
|
|
victoryBlue = "VICTOIRE ! Toutes les zones de capture ont été sécurisées par les forces %s !\n\n%s est terminée. Excellent travail !\nLa mission se terminera dans 60 secondes.",
|
|
defeatBlue = "DÉFAITE ! Toutes les positions stratégiques ont été perdues face aux forces %s.\n\n%s a échoué. Mission se terminant dans 60 secondes.",
|
|
victoryRed = "VICTOIRE ! Toutes les positions stratégiques sécurisées pour %s !\n\nLes forces %s ont été repoussées. Excellent travail !\nLa mission se terminera dans 60 secondes.",
|
|
defeatRed = "DÉFAITE ! Toutes les zones de capture ont été perdues face aux forces %s.\n\n%s a échoué. Mission se terminant dans 60 secondes.",
|
|
missionComplete = "Mission accomplie ! Félicitations pour votre victoire !\nStatut final : Toutes les %d zones stratégiques sécurisées.",
|
|
|
|
-- Status report messages
|
|
zoneControlReport = "RAPPORT DE CONTRÔLE DES ZONES :\nCoalition bleue : %d/%d zones\nCoalition rouge : %d/%d zones\nNeutre : %d/%d zones",
|
|
zoneDetails = "\nDÉTAILS DES ZONES :\n",
|
|
zoneDetailLine = "• %s : %s\n",
|
|
yourProgress = "\n\nVotre progression vers la victoire : %d%%",
|
|
|
|
-- Progress warning messages
|
|
blueNearVictory = "ALERTE TACTIQUE : Les forces %s contrôlent %d%% des zones stratégiques !\nLa victoire est à portée de main !",
|
|
redNearVictory = "AVERTISSEMENT : Les forces %s ont capturé %d%% des zones !\nDéfendez nos positions !",
|
|
|
|
-- Coalition names
|
|
coalitionBlue = "Coalition bleue",
|
|
coalitionRed = "Coalition rouge",
|
|
coalitionNeutral = "Neutre",
|
|
|
|
-- Menu items
|
|
menuZoneControl = "Contrôle des zones",
|
|
menuStatusReport = "Obtenir le rapport d'état des zones",
|
|
menuVictoryProgress = "Vérifier la progression vers la victoire",
|
|
menuRefreshColors = "Actualiser les couleurs des zones",
|
|
menuLanguage = "Language / Sprache / Langue",
|
|
menuEnglish = "English",
|
|
menuGerman = "Deutsch (German)",
|
|
menuFrench = "Français (French)",
|
|
menuSpanish = "Español (Spanish)",
|
|
menuRussian = "Русский (Russian)",
|
|
|
|
-- Victory progress messages
|
|
victoryProgressTitle = "PROGRÈS DE LA VICTOIRE : %d%%",
|
|
zonesCaptured = "Zones capturées : %d/%d",
|
|
zonesRemaining = "Restantes : %d zones",
|
|
progressComplete = "MISSION ACCOMPLIE !",
|
|
progressAlmostThere = "PRESQUE TERMINÉ !",
|
|
progressGood = "BON PROGRÈS !",
|
|
progressKeepFighting = "CONTINUEZ À COMBATTRE !",
|
|
|
|
-- System messages
|
|
zoneMarkersRefreshed = "Les marqueurs de zone ont été actualisés !",
|
|
languageChanged = "Langue changée en français",
|
|
},
|
|
|
|
ES = {
|
|
-- Zone state messages
|
|
zoneGuarded = "%s está bajo la protección de %s",
|
|
zoneEmpty = "¡%s está desprotegida y puede ser capturada!",
|
|
zoneUnderAttack = "%s está siendo atacada por %s",
|
|
zoneWeAttacking = "Estamos atacando %s",
|
|
zoneCapturedByEnemy = "¡%s ha sido capturada por %s, la hemos perdido!",
|
|
zoneCapturedByUs = "¡Hemos capturado %s, excelente trabajo!",
|
|
|
|
-- Victory messages
|
|
victoryBlue = "¡VICTORIA! ¡Todas las zonas de captura han sido aseguradas por las fuerzas %s!\n\n%s está completa. ¡Excelente trabajo!\nLa misión terminará en 60 segundos.",
|
|
defeatBlue = "¡DERROTA! Todas las posiciones estratégicas se han perdido ante las fuerzas %s.\n\n%s ha fallado. Misión terminando en 60 segundos.",
|
|
victoryRed = "¡VICTORIA! ¡Todas las posiciones estratégicas aseguradas para %s!\n\n¡Las fuerzas %s han sido repelidas. Excelente trabajo!\nLa misión terminará en 60 segundos.",
|
|
defeatRed = "¡DERROTA! Todas las zonas de captura se han perdido ante las fuerzas %s.\n\n%s ha fallado. Misión terminando en 60 segundos.",
|
|
missionComplete = "¡Misión completada! ¡Felicitaciones por tu victoria!\nEstado final: Todas las %d zonas estratégicas aseguradas.",
|
|
|
|
-- Status report messages
|
|
zoneControlReport = "INFORME DE CONTROL DE ZONAS:\nCoalición Azul: %d/%d zonas\nCoalición Roja: %d/%d zonas\nNeutral: %d/%d zonas",
|
|
zoneDetails = "\nDETALLES DE ZONAS:\n",
|
|
zoneDetailLine = "• %s: %s\n",
|
|
yourProgress = "\n\nTu progreso hacia la victoria: %d%%",
|
|
|
|
-- Progress warning messages
|
|
blueNearVictory = "ALERTA TÁCTICA: ¡Las fuerzas %s controlan el %d%% de las zonas estratégicas!\n¡La victoria está al alcance!",
|
|
redNearVictory = "ADVERTENCIA: ¡Las fuerzas %s han capturado el %d%% de las zonas!\n¡Defiende nuestras posiciones!",
|
|
|
|
-- Coalition names
|
|
coalitionBlue = "Coalición Azul",
|
|
coalitionRed = "Coalición Roja",
|
|
coalitionNeutral = "Neutral",
|
|
|
|
-- Menu items
|
|
menuZoneControl = "Control de zonas",
|
|
menuStatusReport = "Obtener informe de estado de zonas",
|
|
menuVictoryProgress = "Verificar progreso de victoria",
|
|
menuRefreshColors = "Actualizar colores de zonas",
|
|
menuLanguage = "Language / Sprache / Langue",
|
|
menuEnglish = "English",
|
|
menuGerman = "Deutsch (German)",
|
|
menuFrench = "Français (French)",
|
|
menuSpanish = "Español (Spanish)",
|
|
menuRussian = "Русский (Russian)",
|
|
|
|
-- Victory progress messages
|
|
victoryProgressTitle = "PROGRESO DE VICTORIA: %d%%",
|
|
zonesCaptured = "Zonas capturadas: %d/%d",
|
|
zonesRemaining = "Restantes: %d zonas",
|
|
progressComplete = "¡MISIÓN COMPLETADA!",
|
|
progressAlmostThere = "¡CASI TERMINADO!",
|
|
progressGood = "¡BUEN PROGRESO!",
|
|
progressKeepFighting = "¡SIGUE LUCHANDO!",
|
|
|
|
-- System messages
|
|
zoneMarkersRefreshed = "¡Los marcadores de zona se han actualizado!",
|
|
languageChanged = "Idioma cambiado a español",
|
|
},
|
|
|
|
RU = {
|
|
-- Zone state messages
|
|
zoneGuarded = "%s находится под защитой %s",
|
|
zoneEmpty = "%s не защищена и может быть захвачена!",
|
|
zoneUnderAttack = "%s атакована силами %s",
|
|
zoneWeAttacking = "Мы атакуем %s",
|
|
zoneCapturedByEnemy = "%s захвачена %s, мы её потеряли!",
|
|
zoneCapturedByUs = "Мы захватили %s, отличная работа!",
|
|
|
|
-- Victory messages
|
|
victoryBlue = "ПОБЕДА! Все зоны захвата обеспечены силами %s!\n\n%s завершена. Отличная работа!\nМиссия закончится через 60 секунд.",
|
|
defeatBlue = "ПОРАЖЕНИЕ! Все стратегические позиции потеряны силами %s.\n\n%s провалена. Миссия заканчивается через 60 секунд.",
|
|
victoryRed = "ПОБЕДА! Все стратегические позиции обеспечены для %s!\n\nСилы %s отброшены. Отличная работа!\nМиссия закончится через 60 секунд.",
|
|
defeatRed = "ПОРАЖЕНИЕ! Все зоны захвата потеряны силами %s.\n\n%s провалена. Миссия заканчивается через 60 секунд.",
|
|
missionComplete = "Миссия выполнена! Поздравляем с победой!\nИтоговый статус: Все %d стратегических зон обеспечены.",
|
|
|
|
-- Status report messages
|
|
zoneControlReport = "ОТЧЁТ О КОНТРОЛЕ ЗОН:\nСиняя коалиция: %d/%d зон\nКрасная коалиция: %d/%d зон\nНейтральные: %d/%d зон",
|
|
zoneDetails = "\nПОДРОБНОСТИ ЗОН:\n",
|
|
zoneDetailLine = "• %s: %s\n",
|
|
yourProgress = "\n\nВаш прогресс к победе: %d%%",
|
|
|
|
-- Progress warning messages
|
|
blueNearVictory = "ТАКТИЧЕСКАЯ ТРЕВОГА: Силы %s контролируют %d%% стратегических зон!\nПобеда близка!",
|
|
redNearVictory = "ВНИМАНИЕ: Силы %s захватили %d%% зон!\nЗащищайте наши позиции!",
|
|
|
|
-- Coalition names
|
|
coalitionBlue = "Синяя коалиция",
|
|
coalitionRed = "Красная коалиция",
|
|
coalitionNeutral = "Нейтральные",
|
|
|
|
-- Menu items
|
|
menuZoneControl = "Контроль зон",
|
|
menuStatusReport = "Получить отчёт о статусе зон",
|
|
menuVictoryProgress = "Проверить прогресс победы",
|
|
menuRefreshColors = "Обновить цвета зон",
|
|
menuLanguage = "Language / Sprache / Langue",
|
|
menuEnglish = "English",
|
|
menuGerman = "Deutsch (German)",
|
|
menuFrench = "Français (French)",
|
|
menuSpanish = "Español (Spanish)",
|
|
menuRussian = "Русский (Russian)",
|
|
|
|
-- Victory progress messages
|
|
victoryProgressTitle = "ПРОГРЕСС ПОБЕДЫ: %d%%",
|
|
zonesCaptured = "Зоны захвачены: %d/%d",
|
|
zonesRemaining = "Осталось: %d зон",
|
|
progressComplete = "МИССИЯ ВЫПОЛНЕНА!",
|
|
progressAlmostThere = "ПОЧТИ ГОТОВО!",
|
|
progressGood = "ХОРОШИЙ ПРОГРЕСС!",
|
|
progressKeepFighting = "ПРОДОЛЖАЙТЕ БОРОТЬСЯ!",
|
|
|
|
-- System messages
|
|
zoneMarkersRefreshed = "Маркеры зон обновлены!",
|
|
languageChanged = "Язык изменён на русский",
|
|
}
|
|
}
|
|
|
|
-- ==========================================
|
|
-- PLAYER LANGUAGE TRACKING
|
|
-- ==========================================
|
|
-- Store per-player language preferences
|
|
local playerLanguages = {}
|
|
|
|
-- Helper function to get player's language preference
|
|
local function GetPlayerLanguage(playerName)
|
|
if playerName and playerLanguages[playerName] then
|
|
return playerLanguages[playerName]
|
|
end
|
|
return LANGUAGE_CONFIG.defaultLanguage
|
|
end
|
|
|
|
-- Helper function to set player's language preference
|
|
local function SetPlayerLanguage(playerName, language)
|
|
if playerName and language then
|
|
playerLanguages[playerName] = language
|
|
env.info(string.format("[LANGUAGE] Player %s language set to %s", playerName, language))
|
|
end
|
|
end
|
|
|
|
-- Helper function to get translated text
|
|
local function GetText(textKey, playerName)
|
|
local lang = GetPlayerLanguage(playerName)
|
|
local langTable = ZONE_CAPTURE_LANGUAGES[lang] or ZONE_CAPTURE_LANGUAGES.EN
|
|
return langTable[textKey] or textKey
|
|
end
|
|
|
|
-- Helper function to get coalition-specific text
|
|
local function GetCoalitionText(coalitionSide, playerName)
|
|
local lang = GetPlayerLanguage(playerName)
|
|
local langTable = ZONE_CAPTURE_LANGUAGES[lang] or ZONE_CAPTURE_LANGUAGES.EN
|
|
|
|
if coalitionSide == coalition.side.BLUE then
|
|
return langTable.coalitionBlue
|
|
elseif coalitionSide == coalition.side.RED then
|
|
return langTable.coalitionRed
|
|
else
|
|
return langTable.coalitionNeutral
|
|
end
|
|
end
|
|
|
|
-- Helper function to send message to coalition with language support
|
|
local function MessageToCoalition(commandCenter, messageKey, params, coalitionSide, duration)
|
|
if not commandCenter then return end
|
|
|
|
-- Get all players in the coalition
|
|
local playerList = coalition.getPlayers(coalitionSide)
|
|
|
|
if not playerList or #playerList == 0 then
|
|
-- No players in coalition, send default language message
|
|
local defaultText = ZONE_CAPTURE_LANGUAGES[LANGUAGE_CONFIG.defaultLanguage][messageKey]
|
|
if params then
|
|
defaultText = string.format(defaultText, unpack(params))
|
|
end
|
|
commandCenter:MessageTypeToCoalition(defaultText, MESSAGE.Type.Information, duration)
|
|
return
|
|
end
|
|
|
|
-- Group players by language preference
|
|
local languageGroups = {}
|
|
for _, unit in ipairs(playerList) do
|
|
if unit then
|
|
local playerName = unit:getPlayerName()
|
|
if playerName then
|
|
local lang = GetPlayerLanguage(playerName)
|
|
if not languageGroups[lang] then
|
|
languageGroups[lang] = {}
|
|
end
|
|
table.insert(languageGroups[lang], playerName)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Send message in each language to respective players
|
|
for lang, players in pairs(languageGroups) do
|
|
local langTable = ZONE_CAPTURE_LANGUAGES[lang] or ZONE_CAPTURE_LANGUAGES.EN
|
|
local text = langTable[messageKey]
|
|
if params then
|
|
text = string.format(text, unpack(params))
|
|
end
|
|
|
|
-- For now, send to entire coalition (MOOSE doesn't support per-player filtering easily)
|
|
-- This is a limitation, but better than nothing
|
|
commandCenter:MessageTypeToCoalition(text, MESSAGE.Type.Information, duration)
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- ==========================================
|
|
-- END OF CONFIGURATION
|
|
-- ==========================================
|
|
|
|
-- Helper to get the appropriate color for a zone based on state/ownership
|
|
local function GetZoneColor(zoneCapture)
|
|
local zoneCoalition = zoneCapture:GetCoalition()
|
|
local state = zoneCapture:GetCurrentState()
|
|
|
|
-- Priority 1: Attacked overrides ownership color
|
|
if state == "Attacked" then
|
|
if zoneCoalition == coalition.side.BLUE then
|
|
return ZONE_COLORS.BLUE_ATTACKED
|
|
elseif zoneCoalition == coalition.side.RED then
|
|
return ZONE_COLORS.RED_ATTACKED
|
|
end
|
|
end
|
|
|
|
-- Priority 2: Empty/neutral
|
|
if state == "Empty" then
|
|
return ZONE_COLORS.EMPTY
|
|
end
|
|
|
|
-- Priority 3: Ownership color
|
|
if zoneCoalition == coalition.side.BLUE then
|
|
return ZONE_COLORS.BLUE_CAPTURED
|
|
elseif zoneCoalition == coalition.side.RED then
|
|
return ZONE_COLORS.RED_CAPTURED
|
|
end
|
|
|
|
-- Fallback
|
|
return ZONE_COLORS.EMPTY
|
|
end
|
|
|
|
-- Build Command Center and Mission for Blue Coalition
|
|
local blueHQ = GROUP:FindByName("BLUEHQ")
|
|
if blueHQ then
|
|
US_CC = COMMANDCENTER:New(blueHQ, COALITION_TITLES.BLUE .. " HQ")
|
|
US_Mission = MISSION:New(US_CC, "Zone Capture Example Mission", "Primary", "", coalition.side.BLUE)
|
|
US_Score = SCORING:New("Zone Capture Example Mission")
|
|
--US_Mission:AddScoring(US_Score)
|
|
--US_Mission:Start()
|
|
env.info("Blue Coalition Command Center and Mission started successfully")
|
|
else
|
|
env.info("ERROR: BLUEHQ group not found! Blue mission will not start.")
|
|
end
|
|
|
|
--Build Command Center and Mission Red
|
|
local redHQ = GROUP:FindByName("REDHQ")
|
|
if redHQ then
|
|
RU_CC = COMMANDCENTER:New(redHQ, COALITION_TITLES.RED .. " HQ")
|
|
RU_Mission = MISSION:New(RU_CC, "Zone Capture Example Mission", "Primary", "Hold what we have, take what we don't.", coalition.side.RED)
|
|
--RU_Score = SCORING:New("Zone Capture Example Mission")
|
|
--RU_Mission:AddScoring(RU_Score)
|
|
RU_Mission:Start()
|
|
env.info("Red Coalition Command Center and Mission started successfully")
|
|
else
|
|
env.info("ERROR: REDHQ group not found! Red mission will not start.")
|
|
end
|
|
|
|
|
|
-- Setup BLUE Missions
|
|
do -- BLUE Mission
|
|
|
|
US_Mission_Capture_Airfields = MISSION:New( US_CC, "Capture the Zones", "Primary",
|
|
"Capture the Zones marked on your F10 map.\n" ..
|
|
"Destroy " .. COALITION_TITLES.RED .. " ground forces in the surrounding area, " ..
|
|
"then occupy each capture zone with a platoon.\n " ..
|
|
"Your orders are to hold position until all capture zones are taken.\n" ..
|
|
"Use the map (F10) for a clear indication of the location of each capture zone.\n" ..
|
|
"Note that heavy resistance can be expected at the airbases!\n"
|
|
, coalition.side.BLUE)
|
|
|
|
--US_Score = SCORING:New( "Capture Airfields" )
|
|
|
|
--US_Mission_Capture_Airfields:AddScoring( US_Score )
|
|
|
|
US_Mission_Capture_Airfields:Start()
|
|
|
|
end
|
|
|
|
-- Setup RED Missions
|
|
do -- RED Mission
|
|
|
|
RU_Mission_Capture_Airfields = MISSION:New( RU_CC, "Defend the Territory", "Primary",
|
|
"Defend " .. COALITION_TITLES.RED .. " territory and recapture lost zones.\n" ..
|
|
"Eliminate " .. COALITION_TITLES.BLUE .. " forces in capture zones and " ..
|
|
"maintain control with ground units.\n" ..
|
|
"Your orders are to prevent the " .. COALITION_TITLES.BLUE .. " from capturing all strategic zones.\n" ..
|
|
"Use the map (F10) for a clear indication of the location of each capture zone.\n" ..
|
|
"Expect heavy " .. COALITION_TITLES.BLUE .. " resistance!\n"
|
|
, coalition.side.RED)
|
|
|
|
--RU_Score = SCORING:New( "Defend Territory" )
|
|
|
|
--RU_Mission_Capture_Airfields:AddScoring( RU_Score )
|
|
|
|
RU_Mission_Capture_Airfields:Start()
|
|
|
|
end
|
|
-- Logging configuration: toggle logging behavior for this module
|
|
-- Set `CAPTURE_ZONE_LOGGING.enabled = false` to silence module logs
|
|
if not CAPTURE_ZONE_LOGGING then
|
|
CAPTURE_ZONE_LOGGING = { enabled = false, prefix = "[CAPTURE Module]" }
|
|
end
|
|
|
|
local function log(message, detailed)
|
|
if CAPTURE_ZONE_LOGGING.enabled then
|
|
-- Preserve the previous prefixing used across the module
|
|
if CAPTURE_ZONE_LOGGING.prefix then
|
|
env.info(tostring(CAPTURE_ZONE_LOGGING.prefix) .. " " .. tostring(message))
|
|
else
|
|
env.info(tostring(message))
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- ==========================================
|
|
-- ZONE INITIALIZATION SYSTEM
|
|
-- ==========================================
|
|
|
|
-- Storage for all zone capture objects and metadata
|
|
-- NOTE: These are exported as globals for plugin compatibility (e.g., Moose_DynamicGroundBattle_Plugin.lua)
|
|
zoneCaptureObjects = {} -- Global: accessible by other scripts
|
|
zoneNames = {} -- Global: accessible by other scripts
|
|
local zoneMetadata = {} -- Stores coalition ownership info
|
|
local activeTacticalMarkers = {} -- Track tactical markers to prevent memory leaks
|
|
|
|
-- Function to initialize all zones from configuration
|
|
local function InitializeZones()
|
|
log("[INIT] Starting zone initialization from configuration...")
|
|
|
|
local totalZones = 0
|
|
|
|
-- Process each coalition's zones
|
|
for coalitionName, zones in pairs(ZONE_CONFIG) do
|
|
local coalitionSide = nil
|
|
|
|
-- Map coalition name to DCS coalition constant
|
|
if coalitionName == "RED" then
|
|
coalitionSide = coalition.side.RED
|
|
elseif coalitionName == "BLUE" then
|
|
coalitionSide = coalition.side.BLUE
|
|
elseif coalitionName == "NEUTRAL" then
|
|
coalitionSide = coalition.side.NEUTRAL
|
|
else
|
|
log(string.format("[INIT] WARNING: Unknown coalition '%s' in ZONE_CONFIG", coalitionName))
|
|
end
|
|
|
|
if coalitionSide then
|
|
for _, zoneName in ipairs(zones) do
|
|
log(string.format("[INIT] Creating zone: %s (Coalition: %s)", zoneName, coalitionName))
|
|
|
|
-- Create the MOOSE zone object (using exact name from config)
|
|
local zone = ZONE:New(zoneName)
|
|
|
|
if zone then
|
|
-- Create the zone capture coalition object
|
|
local zoneCapture = ZONE_CAPTURE_COALITION:New(zone, coalitionSide)
|
|
|
|
if zoneCapture then
|
|
-- Configure the zone
|
|
zoneCapture:__Guard(ZONE_SETTINGS.guardDelay)
|
|
zoneCapture:Start(ZONE_SETTINGS.scanInterval, ZONE_SETTINGS.scanInterval)
|
|
|
|
-- Store in our data structures
|
|
table.insert(zoneCaptureObjects, zoneCapture)
|
|
table.insert(zoneNames, zoneName)
|
|
zoneMetadata[zoneName] = {
|
|
coalition = coalitionSide,
|
|
index = #zoneCaptureObjects
|
|
}
|
|
|
|
totalZones = totalZones + 1
|
|
log(string.format("[INIT] ✓ Zone '%s' initialized successfully", zoneName))
|
|
else
|
|
log(string.format("[INIT] ✗ ERROR: Failed to create ZONE_CAPTURE_COALITION for '%s'", zoneName))
|
|
end
|
|
else
|
|
log(string.format("[INIT] ✗ ERROR: Zone '%s' not found in mission editor!", zoneName))
|
|
log(string.format("[INIT] Make sure you have a trigger zone named exactly: '%s'", zoneName))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
log(string.format("[INIT] Zone initialization complete. Total zones created: %d", totalZones))
|
|
return totalZones
|
|
end
|
|
|
|
-- Initialize all zones
|
|
local totalZones = InitializeZones()
|
|
|
|
|
|
-- Global cached unit set - created once and maintained automatically by MOOSE
|
|
local CachedUnitSet = nil
|
|
|
|
-- Utility guard to safely test whether a unit is inside a zone without throwing
|
|
local function IsUnitInZone(unit, zone)
|
|
if not unit or not zone then
|
|
return false
|
|
end
|
|
|
|
local ok, point = pcall(function()
|
|
return unit:GetPointVec3()
|
|
end)
|
|
|
|
if not ok or not point then
|
|
return false
|
|
end
|
|
|
|
local inZone = false
|
|
pcall(function()
|
|
inZone = zone:IsPointVec3InZone(point)
|
|
end)
|
|
|
|
return inZone
|
|
end
|
|
|
|
-- Initialize the cached unit set once
|
|
local function InitializeCachedUnitSet()
|
|
if not CachedUnitSet then
|
|
CachedUnitSet = SET_UNIT:New()
|
|
:FilterCategories({"ground", "plane", "helicopter"}) -- Only scan relevant unit types
|
|
:FilterStart() -- Keep the set updated by MOOSE without recreating it
|
|
log("[PERFORMANCE] Initialized cached unit set for zone scanning")
|
|
end
|
|
end
|
|
|
|
local function GetZoneForceStrengths(ZoneCapture)
|
|
if not ZoneCapture then
|
|
return { red = 0, blue = 0, neutral = 0 }
|
|
end
|
|
|
|
local success, zone = pcall(function()
|
|
return ZoneCapture:GetZone()
|
|
end)
|
|
|
|
if not success or not zone then
|
|
return { red = 0, blue = 0, neutral = 0 }
|
|
end
|
|
|
|
local redCount = 0
|
|
local blueCount = 0
|
|
local neutralCount = 0
|
|
|
|
InitializeCachedUnitSet()
|
|
|
|
if CachedUnitSet then
|
|
CachedUnitSet:ForEachUnit(function(unit)
|
|
if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then
|
|
local unitCoalition = unit:GetCoalition()
|
|
if unitCoalition == coalition.side.RED then
|
|
redCount = redCount + 1
|
|
elseif unitCoalition == coalition.side.BLUE then
|
|
blueCount = blueCount + 1
|
|
elseif unitCoalition == coalition.side.NEUTRAL then
|
|
neutralCount = neutralCount + 1
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d",
|
|
ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount))
|
|
|
|
return {
|
|
red = redCount,
|
|
blue = blueCount,
|
|
neutral = neutralCount
|
|
}
|
|
end
|
|
|
|
local function GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition)
|
|
if not ZoneCapture or not enemyCoalition then
|
|
return {}
|
|
end
|
|
|
|
local success, zone = pcall(function()
|
|
return ZoneCapture:GetZone()
|
|
end)
|
|
|
|
if not success or not zone then
|
|
return {}
|
|
end
|
|
|
|
InitializeCachedUnitSet()
|
|
|
|
local coords = {}
|
|
local totalUnits = 0
|
|
local enemyUnits = 0
|
|
local unitsWithCoords = 0
|
|
|
|
if CachedUnitSet then
|
|
CachedUnitSet:ForEachUnit(function(unit)
|
|
if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then
|
|
totalUnits = totalUnits + 1
|
|
local unitCoalition = unit:GetCoalition()
|
|
|
|
if unitCoalition == enemyCoalition then
|
|
enemyUnits = enemyUnits + 1
|
|
local coord = unit:GetCoordinate()
|
|
|
|
if coord then
|
|
local mgrs = nil
|
|
local success_mgrs = false
|
|
|
|
success_mgrs, mgrs = pcall(function()
|
|
return coord:ToStringMGRS(5)
|
|
end)
|
|
|
|
if not success_mgrs or not mgrs then
|
|
success_mgrs, mgrs = pcall(function()
|
|
return coord:ToStringMGRS()
|
|
end)
|
|
end
|
|
|
|
if not success_mgrs or not mgrs then
|
|
success_mgrs, mgrs = pcall(function()
|
|
return coord:ToMGRS()
|
|
end)
|
|
end
|
|
|
|
if not success_mgrs or not mgrs then
|
|
success_mgrs, mgrs = pcall(function()
|
|
local lat, lon = coord:GetLLDDM()
|
|
return string.format("N%s E%s", lat, lon)
|
|
end)
|
|
end
|
|
|
|
if success_mgrs and mgrs then
|
|
unitsWithCoords = unitsWithCoords + 1
|
|
local unitType = unit:GetTypeName() or "Unknown"
|
|
table.insert(coords, {
|
|
name = unit:GetName(),
|
|
type = unitType,
|
|
mgrs = mgrs
|
|
})
|
|
else
|
|
log(string.format("[TACTICAL DEBUG] All coordinate methods failed for unit %s", unit:GetName() or "unknown"))
|
|
end
|
|
else
|
|
log(string.format("[TACTICAL DEBUG] No coordinate for unit %s", unit:GetName() or "unknown"))
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
log(string.format("[TACTICAL DEBUG] %s - Total units scanned: %d, Enemy units: %d, units with MGRS: %d",
|
|
ZoneCapture:GetZoneName(), totalUnits, enemyUnits, unitsWithCoords))
|
|
|
|
log(string.format("[TACTICAL] Found %d enemy units with coordinates in %s",
|
|
#coords, ZoneCapture:GetZoneName()))
|
|
|
|
return coords
|
|
end
|
|
|
|
local function CreateTacticalInfoMarker(ZoneCapture)
|
|
-- Validate ZoneCapture
|
|
if not ZoneCapture then
|
|
log("[TACTICAL ERROR] ZoneCapture object is nil")
|
|
return
|
|
end
|
|
|
|
-- Safely get the zone with error handling
|
|
local ok, zone = pcall(function() return ZoneCapture:GetZone() end)
|
|
if not ok or not zone then
|
|
log("[TACTICAL ERROR] Failed to get zone from ZoneCapture object")
|
|
return
|
|
end
|
|
|
|
local forces = GetZoneForceStrengths(ZoneCapture)
|
|
local zoneName = ZoneCapture:GetZoneName()
|
|
|
|
-- Build coalition-specific tactical info text
|
|
local function buildTacticalText(viewerCoalition)
|
|
local text = string.format("TACTICAL: %s\nForces: R:%d B:%d", zoneName, forces.red, forces.blue)
|
|
if forces.neutral and forces.neutral > 0 then
|
|
text = text .. string.format(" C:%d", forces.neutral)
|
|
end
|
|
|
|
-- Append TGTS for the enemy of the viewer, capped at 5 units (reduced from 10 to lower memory usage)
|
|
local enemyCoalition = (viewerCoalition == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
|
|
local enemyCount = (enemyCoalition == coalition.side.RED) and (forces.red or 0) or (forces.blue or 0)
|
|
if enemyCount > 0 and enemyCount <= 8 then -- Only process if 8 or fewer enemies (reduced from 10)
|
|
local enemyCoords = GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition)
|
|
log(string.format("[TACTICAL DEBUG] Building marker text for %s viewer: %d enemy units", (viewerCoalition==coalition.side.BLUE and "BLUE" or "RED"), #enemyCoords))
|
|
if #enemyCoords > 0 then
|
|
text = text .. "\nTGTS:"
|
|
for i, unit in ipairs(enemyCoords) do
|
|
if i <= 5 then -- Reduced from 10 to 5 to save memory
|
|
local shortType = (unit.type or "Unknown"):gsub("^%w+%-", ""):gsub("%s.*", "")
|
|
local cleanMgrs = (unit.mgrs or ""):gsub("^MGRS%s+", ""):gsub("%s+", " ")
|
|
if i == 1 then
|
|
text = text .. string.format(" %s@%s", shortType, cleanMgrs)
|
|
else
|
|
text = text .. string.format(", %s@%s", shortType, cleanMgrs)
|
|
end
|
|
end
|
|
end
|
|
if #enemyCoords > 5 then -- Updated threshold
|
|
text = text .. string.format(" (+%d)", #enemyCoords - 5)
|
|
end
|
|
end
|
|
end
|
|
|
|
return text
|
|
end
|
|
|
|
local tacticalTextBLUE = buildTacticalText(coalition.side.BLUE)
|
|
local tacticalTextRED = buildTacticalText(coalition.side.RED)
|
|
|
|
-- Debug: Log what will be displayed
|
|
log(string.format("[TACTICAL DEBUG] Marker text (BLUE) for %s:\n%s", zoneName, tacticalTextBLUE))
|
|
log(string.format("[TACTICAL DEBUG] Marker text (RED) for %s:\n%s", zoneName, tacticalTextRED))
|
|
|
|
-- Create tactical marker offset from zone center
|
|
local coord = zone:GetCoordinate()
|
|
if coord then
|
|
local offsetCoord = coord:Translate(200, 45) -- 200m NE
|
|
|
|
local function removeMarker(markerID)
|
|
if not markerID then
|
|
return
|
|
end
|
|
|
|
local removed = pcall(function()
|
|
offsetCoord:RemoveMark(markerID)
|
|
end)
|
|
|
|
if not removed then
|
|
removed = pcall(function()
|
|
trigger.action.removeMark(markerID)
|
|
end)
|
|
end
|
|
|
|
if not removed then
|
|
pcall(function()
|
|
coord:RemoveMark(markerID)
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- Remove legacy single marker if present
|
|
if ZoneCapture.TacticalMarkerID then
|
|
log(string.format("[TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName))
|
|
removeMarker(ZoneCapture.TacticalMarkerID)
|
|
ZoneCapture.TacticalMarkerID = nil
|
|
end
|
|
|
|
-- BLUE Coalition Marker
|
|
if ZoneCapture.TacticalMarkerID_BLUE then
|
|
log(string.format("[TACTICAL] Removing old BLUE marker ID %d for %s", ZoneCapture.TacticalMarkerID_BLUE, zoneName))
|
|
removeMarker(ZoneCapture.TacticalMarkerID_BLUE)
|
|
ZoneCapture.TacticalMarkerID_BLUE = nil
|
|
end
|
|
local successBlue, markerIDBlue = pcall(function()
|
|
return offsetCoord:MarkToCoalition(tacticalTextBLUE, coalition.side.BLUE)
|
|
end)
|
|
if successBlue and markerIDBlue then
|
|
ZoneCapture.TacticalMarkerID_BLUE = markerIDBlue
|
|
pcall(function() offsetCoord:SetMarkReadOnly(markerIDBlue, true) end)
|
|
log(string.format("[TACTICAL] Created BLUE marker for %s", zoneName))
|
|
else
|
|
log(string.format("[TACTICAL] Failed to create BLUE marker for %s", zoneName))
|
|
end
|
|
|
|
-- RED Coalition Marker
|
|
if ZoneCapture.TacticalMarkerID_RED then
|
|
log(string.format("[TACTICAL] Removing old RED marker ID %d for %s", ZoneCapture.TacticalMarkerID_RED, zoneName))
|
|
removeMarker(ZoneCapture.TacticalMarkerID_RED)
|
|
ZoneCapture.TacticalMarkerID_RED = nil
|
|
end
|
|
local successRed, markerIDRed = pcall(function()
|
|
return offsetCoord:MarkToCoalition(tacticalTextRED, coalition.side.RED)
|
|
end)
|
|
if successRed and markerIDRed then
|
|
ZoneCapture.TacticalMarkerID_RED = markerIDRed
|
|
pcall(function() offsetCoord:SetMarkReadOnly(markerIDRed, true) end)
|
|
log(string.format("[TACTICAL] Created RED marker for %s", zoneName))
|
|
else
|
|
log(string.format("[TACTICAL] Failed to create RED marker for %s", zoneName))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Event handler functions - define them separately for each zone
|
|
local function OnEnterGuarded(ZoneCapture, From, Event, To)
|
|
if From ~= To then
|
|
local Coalition = ZoneCapture:GetCoalition()
|
|
if Coalition == coalition.side.BLUE then
|
|
ZoneCapture:Smoke( SMOKECOLOR.Blue )
|
|
-- Update zone visual markers to BLUE
|
|
ZoneCapture:UndrawZone()
|
|
local color = ZONE_COLORS.BLUE_CAPTURED
|
|
ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
|
|
MessageToCoalition(US_CC, "zoneGuarded", {ZoneCapture:GetZoneName(), COALITION_TITLES.BLUE}, coalition.side.BLUE, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "zoneGuarded", {ZoneCapture:GetZoneName(), COALITION_TITLES.BLUE}, coalition.side.RED, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
else
|
|
ZoneCapture:Smoke( SMOKECOLOR.Red )
|
|
-- Update zone visual markers to RED
|
|
ZoneCapture:UndrawZone()
|
|
local color = ZONE_COLORS.RED_CAPTURED
|
|
ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
|
|
MessageToCoalition(RU_CC, "zoneGuarded", {ZoneCapture:GetZoneName(), COALITION_TITLES.RED}, coalition.side.RED, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
MessageToCoalition(US_CC, "zoneGuarded", {ZoneCapture:GetZoneName(), COALITION_TITLES.RED}, coalition.side.BLUE, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
end
|
|
-- Create/update tactical information marker
|
|
CreateTacticalInfoMarker(ZoneCapture)
|
|
end
|
|
end
|
|
|
|
local function OnEnterEmpty(ZoneCapture)
|
|
ZoneCapture:Smoke( SMOKECOLOR.Green )
|
|
-- Update zone visual markers to GREEN (neutral)
|
|
ZoneCapture:UndrawZone()
|
|
local color = ZONE_COLORS.EMPTY
|
|
ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
|
|
MessageToCoalition(US_CC, "zoneEmpty", {ZoneCapture:GetZoneName()}, coalition.side.BLUE, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "zoneEmpty", {ZoneCapture:GetZoneName()}, coalition.side.RED, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
-- Create/update tactical information marker
|
|
CreateTacticalInfoMarker(ZoneCapture)
|
|
end
|
|
|
|
local function OnEnterAttacked(ZoneCapture)
|
|
ZoneCapture:Smoke( SMOKECOLOR.White )
|
|
-- Update zone visual markers based on owner (attacked state)
|
|
ZoneCapture:UndrawZone()
|
|
local Coalition = ZoneCapture:GetCoalition()
|
|
local color
|
|
if Coalition == coalition.side.BLUE then
|
|
color = ZONE_COLORS.BLUE_ATTACKED
|
|
MessageToCoalition(US_CC, "zoneUnderAttack", {ZoneCapture:GetZoneName(), COALITION_TITLES.RED}, coalition.side.BLUE, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "zoneWeAttacking", {ZoneCapture:GetZoneName()}, coalition.side.RED, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION)
|
|
else
|
|
color = ZONE_COLORS.RED_ATTACKED
|
|
MessageToCoalition(RU_CC, "zoneUnderAttack", {ZoneCapture:GetZoneName(), COALITION_TITLES.BLUE}, coalition.side.RED, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION)
|
|
MessageToCoalition(US_CC, "zoneWeAttacking", {ZoneCapture:GetZoneName()}, coalition.side.BLUE, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION)
|
|
end
|
|
ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
|
|
-- Create/update tactical information marker
|
|
CreateTacticalInfoMarker(ZoneCapture)
|
|
end
|
|
|
|
-- Victory condition monitoring for BOTH coalitions
|
|
local function CheckVictoryCondition()
|
|
local blueZonesCount = 0
|
|
local redZonesCount = 0
|
|
local totalZones = #zoneCaptureObjects
|
|
|
|
for i, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
local zoneCoalition = zoneCapture:GetCoalition()
|
|
if zoneCoalition == coalition.side.BLUE then
|
|
blueZonesCount = blueZonesCount + 1
|
|
elseif zoneCoalition == coalition.side.RED then
|
|
redZonesCount = redZonesCount + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
log(string.format("[VICTORY CHECK] Blue owns %d/%d zones, Red owns %d/%d zones",
|
|
blueZonesCount, totalZones, redZonesCount, totalZones))
|
|
|
|
-- Check for BLUE victory
|
|
if blueZonesCount >= totalZones then
|
|
log("[VICTORY] All zones captured by BLUE! Triggering victory sequence...")
|
|
|
|
MessageToCoalition(US_CC, "victoryBlue", {COALITION_TITLES.BLUE, COALITION_TITLES.BLUE_OPERATION}, coalition.side.BLUE, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "defeatBlue", {COALITION_TITLES.BLUE, COALITION_TITLES.BLUE_OPERATION}, coalition.side.RED, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
|
|
-- Add victory celebration effects
|
|
for _, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
zoneCapture:Smoke( SMOKECOLOR.Blue )
|
|
local zone = zoneCapture:GetZone()
|
|
if zone then
|
|
zone:FlareZone( FLARECOLOR.Blue, 90, 60 )
|
|
end
|
|
end
|
|
end
|
|
|
|
SCHEDULER:New( nil, function()
|
|
log("[VICTORY] Ending mission due to complete zone capture by BLUE")
|
|
trigger.action.setUserFlag("BLUE_VICTORY", 1)
|
|
|
|
MessageToCoalition(US_CC, "missionComplete", {totalZones}, coalition.side.BLUE, 10)
|
|
end, {}, 60 )
|
|
|
|
return true
|
|
end
|
|
|
|
-- Check for RED victory
|
|
if redZonesCount >= totalZones then
|
|
log("[VICTORY] All zones captured by RED! Triggering victory sequence...")
|
|
|
|
MessageToCoalition(RU_CC, "victoryRed", {COALITION_TITLES.RED_OPERATION, COALITION_TITLES.BLUE}, coalition.side.RED, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
MessageToCoalition(US_CC, "defeatRed", {COALITION_TITLES.RED, COALITION_TITLES.BLUE_OPERATION}, coalition.side.BLUE, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
|
|
-- Add victory celebration effects
|
|
for _, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
zoneCapture:Smoke( SMOKECOLOR.Red )
|
|
local zone = zoneCapture:GetZone()
|
|
if zone then
|
|
zone:FlareZone( FLARECOLOR.Red, 90, 60 )
|
|
end
|
|
end
|
|
end
|
|
|
|
SCHEDULER:New( nil, function()
|
|
log("[VICTORY] Ending mission due to complete zone capture by RED")
|
|
trigger.action.setUserFlag("RED_VICTORY", 1)
|
|
|
|
MessageToCoalition(RU_CC, "missionComplete", {totalZones}, coalition.side.RED, 10)
|
|
end, {}, 60 )
|
|
|
|
return true
|
|
end
|
|
|
|
return false -- Victory not yet achieved by either side
|
|
end
|
|
|
|
local function OnEnterCaptured(ZoneCapture)
|
|
local Coalition = ZoneCapture:GetCoalition()
|
|
if Coalition == coalition.side.BLUE then
|
|
-- Update zone visual markers to BLUE for captured
|
|
ZoneCapture:UndrawZone()
|
|
local color = ZONE_COLORS.BLUE_CAPTURED
|
|
ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
|
|
MessageToCoalition(RU_CC, "zoneCapturedByEnemy", {ZoneCapture:GetZoneName(), COALITION_TITLES.BLUE}, coalition.side.RED, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
MessageToCoalition(US_CC, "zoneCapturedByUs", {ZoneCapture:GetZoneName()}, coalition.side.BLUE, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
else
|
|
-- Update zone visual markers to RED for captured
|
|
ZoneCapture:UndrawZone()
|
|
local color = ZONE_COLORS.RED_CAPTURED
|
|
ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
|
|
MessageToCoalition(US_CC, "zoneCapturedByEnemy", {ZoneCapture:GetZoneName(), COALITION_TITLES.RED}, coalition.side.BLUE, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "zoneCapturedByUs", {ZoneCapture:GetZoneName()}, coalition.side.RED, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION)
|
|
end
|
|
|
|
ZoneCapture:AddScore( "Captured", "Zone captured: Extra points granted.", ZONE_SETTINGS.captureScore )
|
|
ZoneCapture:__Guard( 30 )
|
|
|
|
-- Create/update tactical information marker
|
|
CreateTacticalInfoMarker(ZoneCapture)
|
|
|
|
-- Check victory condition after any zone capture
|
|
CheckVictoryCondition()
|
|
end
|
|
|
|
-- Set up event handlers for each zone with proper MOOSE methods and debugging
|
|
for i, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
local zoneName = zoneNames[i] or ("Zone " .. i)
|
|
|
|
-- Proper MOOSE event handlers for ZONE_CAPTURE_COALITION
|
|
zoneCapture.OnEnterGuarded = OnEnterGuarded
|
|
zoneCapture.OnEnterEmpty = OnEnterEmpty
|
|
zoneCapture.OnEnterAttacked = OnEnterAttacked
|
|
zoneCapture.OnEnterCaptured = OnEnterCaptured
|
|
|
|
-- Debug: Check if the underlying zone exists
|
|
local success, zone = pcall(function() return zoneCapture:GetZone() end)
|
|
if success and zone then
|
|
log("✓ Zone '" .. zoneName .. "' successfully created and linked")
|
|
|
|
-- Get initial coalition color for this zone
|
|
local initialCoalition = zoneCapture:GetCoalition()
|
|
local colorRGB = ZONE_COLORS.EMPTY
|
|
|
|
if initialCoalition == coalition.side.RED then
|
|
colorRGB = ZONE_COLORS.RED_CAPTURED
|
|
elseif initialCoalition == coalition.side.BLUE then
|
|
colorRGB = ZONE_COLORS.BLUE_CAPTURED
|
|
end
|
|
|
|
-- Initialize zone borders with appropriate initial color
|
|
local drawSuccess, drawError = pcall(function()
|
|
zone:DrawZone(-1, {0, 0, 0}, 1, colorRGB, 0.2, 2, true)
|
|
end)
|
|
|
|
if not drawSuccess then
|
|
log("⚠ Zone 'Capture " .. zoneName .. "' border drawing failed: " .. tostring(drawError))
|
|
-- Alternative: Try simpler zone marking
|
|
pcall(function()
|
|
if initialCoalition == coalition.side.RED then
|
|
zone:SmokeZone(SMOKECOLOR.Red, 30)
|
|
elseif initialCoalition == coalition.side.BLUE then
|
|
zone:SmokeZone(SMOKECOLOR.Blue, 30)
|
|
else
|
|
zone:SmokeZone(SMOKECOLOR.Green, 30)
|
|
end
|
|
end)
|
|
else
|
|
local coalitionName = "NEUTRAL"
|
|
if initialCoalition == coalition.side.RED then
|
|
coalitionName = "RED"
|
|
elseif initialCoalition == coalition.side.BLUE then
|
|
coalitionName = "BLUE"
|
|
end
|
|
log("✓ Zone '" .. zoneName .. "' border drawn successfully with " .. coalitionName .. " initial color")
|
|
end
|
|
else
|
|
log("✗ ERROR: Zone '" .. zoneName .. "' not found in mission editor!")
|
|
log(" Make sure you have a trigger zone named exactly: '" .. zoneName .. "'")
|
|
end
|
|
else
|
|
log("✗ ERROR: Zone capture object " .. i .. " (" .. (zoneNames[i] or "Unknown") .. ") is nil!")
|
|
end
|
|
end
|
|
|
|
-- ==========================================
|
|
-- VICTORY MONITORING SYSTEM
|
|
-- ==========================================
|
|
|
|
-- Function to get current zone ownership status
|
|
local function GetZoneOwnershipStatus()
|
|
local status = {
|
|
blue = 0,
|
|
red = 0,
|
|
neutral = 0,
|
|
total = #zoneCaptureObjects,
|
|
zones = {}
|
|
}
|
|
|
|
-- Explicitly reference the global coalition table to avoid parameter shadowing
|
|
local coalitionTable = _G.coalition or coalition
|
|
|
|
for i, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
local zoneCoalition = zoneCapture:GetCoalition()
|
|
local zoneName = zoneNames[i] or ("Zone " .. i)
|
|
|
|
-- Get the current state of the zone
|
|
local currentState = zoneCapture:GetCurrentState()
|
|
local stateString = ""
|
|
|
|
-- Determine status based on coalition and state
|
|
if zoneCoalition == coalitionTable.side.BLUE then
|
|
status.blue = status.blue + 1
|
|
if currentState == "Attacked" then
|
|
status.zones[zoneName] = "BLUE (Under Attack)"
|
|
else
|
|
status.zones[zoneName] = "BLUE"
|
|
end
|
|
elseif zoneCoalition == coalitionTable.side.RED then
|
|
status.red = status.red + 1
|
|
if currentState == "Attacked" then
|
|
status.zones[zoneName] = "RED (Under Attack)"
|
|
else
|
|
status.zones[zoneName] = "RED"
|
|
end
|
|
else
|
|
status.neutral = status.neutral + 1
|
|
if currentState == "Attacked" then
|
|
status.zones[zoneName] = "NEUTRAL (Under Attack)"
|
|
else
|
|
status.zones[zoneName] = "NEUTRAL"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return status
|
|
end
|
|
|
|
-- Function to broadcast zone status report to BOTH coalitions
|
|
local function BroadcastZoneStatus()
|
|
local status = GetZoneOwnershipStatus()
|
|
|
|
-- Build report for BLUE coalition in their language
|
|
local bluePlayerList = coalition.getPlayers(coalition.side.BLUE)
|
|
local bluePlayerName = nil
|
|
if bluePlayerList and #bluePlayerList > 0 then
|
|
local unit = Unit.getByName(Unit.getName(bluePlayerList[1]))
|
|
if unit then
|
|
bluePlayerName = unit:getPlayerName()
|
|
end
|
|
end
|
|
|
|
local blueReportMessage = string.format(
|
|
GetText("zoneControlReport", bluePlayerName),
|
|
status.blue, status.total,
|
|
status.red, status.total,
|
|
status.neutral, status.total
|
|
)
|
|
|
|
local blueDetailMessage = GetText("zoneDetails", bluePlayerName)
|
|
for zoneName, owner in pairs(status.zones) do
|
|
blueDetailMessage = blueDetailMessage .. string.format(GetText("zoneDetailLine", bluePlayerName), zoneName, owner)
|
|
end
|
|
|
|
local totalZones = math.max(status.total, 1)
|
|
local blueProgressPercent = math.floor((status.blue / totalZones) * 100)
|
|
local blueFullMessage = blueReportMessage .. blueDetailMessage .. string.format(GetText("yourProgress", bluePlayerName), blueProgressPercent)
|
|
US_CC:MessageTypeToCoalition( blueFullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION )
|
|
|
|
-- Build report for RED coalition in their language
|
|
local redPlayerList = coalition.getPlayers(coalition.side.RED)
|
|
local redPlayerName = nil
|
|
if redPlayerList and #redPlayerList > 0 then
|
|
local unit = Unit.getByName(Unit.getName(redPlayerList[1]))
|
|
if unit then
|
|
redPlayerName = unit:getPlayerName()
|
|
end
|
|
end
|
|
|
|
local redReportMessage = string.format(
|
|
GetText("zoneControlReport", redPlayerName),
|
|
status.blue, status.total,
|
|
status.red, status.total,
|
|
status.neutral, status.total
|
|
)
|
|
|
|
local redDetailMessage = GetText("zoneDetails", redPlayerName)
|
|
for zoneName, owner in pairs(status.zones) do
|
|
redDetailMessage = redDetailMessage .. string.format(GetText("zoneDetailLine", redPlayerName), zoneName, owner)
|
|
end
|
|
|
|
local redProgressPercent = math.floor((status.red / totalZones) * 100)
|
|
local redFullMessage = redReportMessage .. redDetailMessage .. string.format(GetText("yourProgress", redPlayerName), redProgressPercent)
|
|
RU_CC:MessageTypeToCoalition( redFullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION )
|
|
|
|
log("[ZONE STATUS] Blue:" .. status.blue .. "/" .. status.total .. " Red:" .. status.red .. "/" .. status.total)
|
|
|
|
return status
|
|
end
|
|
|
|
-- Periodic zone monitoring (every 5 minutes) for BOTH coalitions
|
|
local ZoneMonitorScheduler = SCHEDULER:New( nil, function()
|
|
local status = BroadcastZoneStatus()
|
|
|
|
-- Check if BLUE is close to victory (80% or more zones captured)
|
|
if status.blue >= math.floor(status.total * 0.8) and status.blue < status.total then
|
|
local bluePercent = math.floor((status.blue / status.total) * 100)
|
|
MessageToCoalition(US_CC, "blueNearVictory", {COALITION_TITLES.BLUE, bluePercent}, coalition.side.BLUE, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "redNearVictory", {COALITION_TITLES.BLUE, bluePercent}, coalition.side.RED, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
end
|
|
|
|
-- Check if RED is close to victory (80% or more zones captured)
|
|
if status.red >= math.floor(status.total * 0.8) and status.red < status.total then
|
|
local redPercent = math.floor((status.red / status.total) * 100)
|
|
MessageToCoalition(RU_CC, "blueNearVictory", {COALITION_TITLES.RED, redPercent}, coalition.side.RED, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
MessageToCoalition(US_CC, "redNearVictory", {COALITION_TITLES.RED, redPercent}, coalition.side.BLUE, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION)
|
|
end
|
|
|
|
end, {}, MESSAGE_CONFIG.STATUS_BROADCAST_START_DELAY, MESSAGE_CONFIG.STATUS_BROADCAST_FREQUENCY )
|
|
|
|
-- Periodic garbage collection to prevent Lua memory buildup
|
|
SCHEDULER:New( nil, function()
|
|
collectgarbage("collect")
|
|
local memKB = collectgarbage("count")
|
|
log(string.format("[MEMORY] Lua garbage collection complete. Current usage: %.1f MB", memKB / 1024))
|
|
end, {}, 120, MESSAGE_CONFIG.GARBAGE_COLLECTION_FREQUENCY )
|
|
|
|
-- Periodic zone color verification system (every 2 minutes)
|
|
local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
|
|
log("[ZONE COLORS] Running periodic zone color verification...")
|
|
|
|
-- Verify each zone's visual marker matches its CURRENT STATE (not just coalition)
|
|
for i, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
local zoneCoalition = zoneCapture:GetCoalition()
|
|
local zoneName = zoneNames[i] or ("Zone " .. i)
|
|
local currentState = zoneCapture:GetCurrentState()
|
|
|
|
local zoneColor = GetZoneColor(zoneCapture)
|
|
|
|
-- Force redraw the zone with correct color based on CURRENT STATE
|
|
zoneCapture:UndrawZone()
|
|
zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true)
|
|
|
|
-- Log the color assignment for debugging
|
|
local colorName = "UNKNOWN"
|
|
if currentState == "Attacked" then
|
|
colorName = (zoneCoalition == coalition.side.BLUE) and "LIGHT BLUE (Blue Attacked)" or "ORANGE (Red Attacked)"
|
|
elseif currentState == "Empty" then
|
|
colorName = "GREEN (Empty)"
|
|
elseif zoneCoalition == coalition.side.BLUE then
|
|
colorName = "BLUE (Owned)"
|
|
elseif zoneCoalition == coalition.side.RED then
|
|
colorName = "RED (Owned)"
|
|
else
|
|
colorName = "GREEN (Fallback)"
|
|
end
|
|
log(string.format("[ZONE COLORS] %s: Set to %s", zoneName, colorName))
|
|
end
|
|
end
|
|
|
|
end, {}, MESSAGE_CONFIG.COLOR_VERIFICATION_START_DELAY, MESSAGE_CONFIG.COLOR_VERIFICATION_FREQUENCY )
|
|
|
|
-- Periodic tactical marker update system with change detection
|
|
local __lastForceCountsByZone = {}
|
|
local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function()
|
|
log("[TACTICAL] Running periodic tactical marker update (change-detected)...")
|
|
|
|
for i, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
local zoneName = zoneCapture.GetZoneName and zoneCapture:GetZoneName() or (zoneNames[i] or ("Zone " .. i))
|
|
local counts = GetZoneForceStrengths(zoneCapture)
|
|
local last = __lastForceCountsByZone[zoneName]
|
|
local changed = (not last)
|
|
or (last.red ~= counts.red)
|
|
or (last.blue ~= counts.blue)
|
|
or (last.neutral ~= counts.neutral)
|
|
|
|
if changed then
|
|
__lastForceCountsByZone[zoneName] = {
|
|
red = counts.red,
|
|
blue = counts.blue,
|
|
neutral = counts.neutral
|
|
}
|
|
CreateTacticalInfoMarker(zoneCapture)
|
|
end
|
|
end
|
|
end
|
|
|
|
end, {}, MESSAGE_CONFIG.TACTICAL_UPDATE_START_DELAY, MESSAGE_CONFIG.TACTICAL_UPDATE_FREQUENCY )
|
|
|
|
-- Function to refresh all zone colors based on current ownership
|
|
local function RefreshAllZoneColors()
|
|
log("[ZONE COLORS] Refreshing all zone visual markers...")
|
|
|
|
for i, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
if zoneCapture then
|
|
local zoneCoalition = zoneCapture:GetCoalition()
|
|
local zoneName = zoneNames[i] or ("Zone " .. i)
|
|
local currentState = zoneCapture:GetCurrentState()
|
|
|
|
-- Get color for current state/ownership
|
|
local zoneColor = GetZoneColor(zoneCapture)
|
|
|
|
-- Clear existing drawings
|
|
zoneCapture:UndrawZone()
|
|
|
|
-- Redraw with correct color
|
|
zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true)
|
|
|
|
-- Log the color assignment for debugging
|
|
local colorName = "UNKNOWN"
|
|
if currentState == "Attacked" then
|
|
colorName = (zoneCoalition == coalition.side.BLUE) and "LIGHT BLUE (Blue Attacked)" or "ORANGE (Red Attacked)"
|
|
elseif currentState == "Empty" then
|
|
colorName = "GREEN (Empty)"
|
|
elseif zoneCoalition == coalition.side.BLUE then
|
|
colorName = "BLUE (Owned)"
|
|
elseif zoneCoalition == coalition.side.RED then
|
|
colorName = "RED (Owned)"
|
|
else
|
|
colorName = "GREEN (Fallback)"
|
|
end
|
|
log(string.format("[ZONE COLORS] %s: Set to %s", zoneName, colorName))
|
|
end
|
|
end
|
|
|
|
-- Notify BOTH coalitions
|
|
MessageToCoalition(US_CC, "zoneMarkersRefreshed", nil, coalition.side.BLUE, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION)
|
|
MessageToCoalition(RU_CC, "zoneMarkersRefreshed", nil, coalition.side.RED, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION)
|
|
end
|
|
|
|
-- NOTE: All menus are now created per-player in CreatePlayerLanguageMenu() function below
|
|
-- No coalition-wide menus needed
|
|
|
|
-- ==========================================
|
|
-- PER-PLAYER MENU SETUP (ALL ZONE CONTROL FUNCTIONS)
|
|
-- ==========================================
|
|
-- Track player menus to avoid duplicates per group
|
|
-- Key format: "playerName|groupName" to handle aircraft switches
|
|
local playerMenus = {}
|
|
|
|
-- Function to create complete Zone Control menu for a specific player
|
|
local function CreatePlayerMenu(playerUnit)
|
|
env.info("[MENU DEBUG] CreatePlayerMenu called")
|
|
if not playerUnit then
|
|
env.info("[MENU DEBUG] No playerUnit")
|
|
return
|
|
end
|
|
|
|
local playerName = playerUnit:getPlayerName()
|
|
env.info(string.format("[MENU DEBUG] PlayerName: %s", tostring(playerName)))
|
|
if not playerName then return end
|
|
|
|
-- Get DCS group and convert to MOOSE GROUP wrapper
|
|
local dcsGroup = playerUnit:getGroup()
|
|
env.info(string.format("[MENU DEBUG] DCS Group: %s", tostring(dcsGroup and dcsGroup:getName() or "nil")))
|
|
if not dcsGroup then return end
|
|
|
|
local groupName = dcsGroup:getName()
|
|
local group = GROUP:FindByName(groupName)
|
|
env.info(string.format("[MENU DEBUG] MOOSE Group found: %s", tostring(group ~= nil)))
|
|
if not group then return end
|
|
|
|
local menuKey = playerName .. "|" .. groupName
|
|
|
|
-- Don't create duplicate menus for same player in same group
|
|
if playerMenus[menuKey] then
|
|
env.info(string.format("[MENU DEBUG] Menu already exists for %s", menuKey))
|
|
return
|
|
end
|
|
|
|
local playerCoalition = playerUnit:getCoalition()
|
|
|
|
-- Get appropriate command center
|
|
local commandCenter = playerCoalition == coalition.side.BLUE and US_CC or RU_CC
|
|
if not commandCenter then
|
|
env.info("[MENU DEBUG] No command center found")
|
|
return
|
|
end
|
|
|
|
env.info(string.format("[MENU] Creating Zone Control menu for player %s (group: %s)", playerName, groupName))
|
|
|
|
-- Create Zone Control root menu (use GROUP object, not group name string)
|
|
-- Use MenuManager if available to nest under "Mission Options"
|
|
local zoneControlMenu = MenuManager and MenuManager.CreateGroupMenu(group, GetText("menuZoneControl", playerName))
|
|
or MENU_GROUP:New(group, GetText("menuZoneControl", playerName))
|
|
|
|
-- Add Zone Status Report command
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuStatusReport", playerName), zoneControlMenu, function()
|
|
BroadcastZoneStatus()
|
|
end)
|
|
|
|
-- Add Victory Progress command
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuVictoryProgress", playerName), zoneControlMenu, function()
|
|
local status = GetZoneOwnershipStatus()
|
|
local totalZones = math.max(status.total, 1)
|
|
local progressPercent = 0
|
|
|
|
-- Calculate progress based on player's coalition
|
|
if playerCoalition == coalition.side.BLUE then
|
|
progressPercent = math.floor((status.blue / totalZones) * 100)
|
|
else
|
|
progressPercent = math.floor((status.red / totalZones) * 100)
|
|
end
|
|
|
|
-- Determine progress message based on percentage
|
|
local progressMsg
|
|
if progressPercent >= 100 then
|
|
progressMsg = GetText("progressComplete", playerName)
|
|
elseif progressPercent >= 80 then
|
|
progressMsg = GetText("progressAlmostThere", playerName)
|
|
elseif progressPercent >= 50 then
|
|
progressMsg = GetText("progressGood", playerName)
|
|
else
|
|
progressMsg = GetText("progressKeepFighting", playerName)
|
|
end
|
|
|
|
local zonesCaptured = playerCoalition == coalition.side.BLUE and status.blue or status.red
|
|
local zonesRemaining = status.total - zonesCaptured
|
|
|
|
MESSAGE:New(
|
|
string.format(
|
|
"%s\n%s\n%s\n\n%s",
|
|
string.format(GetText("victoryProgressTitle", playerName), progressPercent),
|
|
string.format(GetText("zonesCaptured", playerName), zonesCaptured, status.total),
|
|
string.format(GetText("zonesRemaining", playerName), zonesRemaining),
|
|
progressMsg
|
|
),
|
|
MESSAGE_CONFIG.STATUS_MESSAGE_DURATION
|
|
):ToGroup(group)
|
|
end)
|
|
|
|
-- Add Refresh Zone Colors command
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuRefreshColors", playerName), zoneControlMenu, function()
|
|
RefreshAllZoneColors()
|
|
end)
|
|
|
|
-- Create Language submenu
|
|
local langMenu = MENU_GROUP:New(group, GetText("menuLanguage", playerName), zoneControlMenu)
|
|
|
|
-- Add language options
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuEnglish", playerName), langMenu, function()
|
|
SetPlayerLanguage(playerName, "EN")
|
|
MESSAGE:New(GetText("languageChanged", playerName), 5):ToGroup(group)
|
|
end)
|
|
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuGerman", playerName), langMenu, function()
|
|
SetPlayerLanguage(playerName, "DE")
|
|
MESSAGE:New(GetText("languageChanged", playerName), 5):ToGroup(group)
|
|
end)
|
|
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuFrench", playerName), langMenu, function()
|
|
SetPlayerLanguage(playerName, "FR")
|
|
MESSAGE:New(GetText("languageChanged", playerName), 5):ToGroup(group)
|
|
end)
|
|
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuSpanish", playerName), langMenu, function()
|
|
SetPlayerLanguage(playerName, "ES")
|
|
MESSAGE:New(GetText("languageChanged", playerName), 5):ToGroup(group)
|
|
end)
|
|
|
|
MENU_GROUP_COMMAND:New(group, GetText("menuRussian", playerName), langMenu, function()
|
|
SetPlayerLanguage(playerName, "RU")
|
|
MESSAGE:New(GetText("languageChanged", playerName), 5):ToGroup(group)
|
|
end)
|
|
|
|
playerMenus[menuKey] = true
|
|
log(string.format("[MENU] Zone Control menu created for %s in group %s", playerName, groupName))
|
|
end
|
|
|
|
-- Event handler to create player menus when players spawn
|
|
local function OnPlayerBirth(event)
|
|
env.info(string.format("[MENU DEBUG] OnPlayerBirth called, event id: %s", tostring(event.id)))
|
|
if event.id == world.event.S_EVENT_BIRTH and event.initiator then
|
|
local unit = event.initiator
|
|
local playerName = unit and unit:getPlayerName()
|
|
env.info(string.format("[MENU DEBUG] Birth event for player: %s", tostring(playerName)))
|
|
if unit and playerName then
|
|
-- Small delay to ensure everything is initialized
|
|
timer.scheduleFunction(function()
|
|
CreatePlayerMenu(unit)
|
|
end, nil, timer.getTime() + 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Set up event handler
|
|
world.addEventHandler({
|
|
onEvent = OnPlayerBirth
|
|
})
|
|
|
|
-- Create menus for any players already in the mission
|
|
timer.scheduleFunction(function()
|
|
env.info("[MENU] Checking for existing players...")
|
|
for _, coalitionSide in pairs({coalition.side.BLUE, coalition.side.RED}) do
|
|
local players = coalition.getPlayers(coalitionSide)
|
|
env.info(string.format("[MENU DEBUG] Found %d players in coalition %d", players and #players or 0, coalitionSide))
|
|
if players then
|
|
for _, unit in ipairs(players) do
|
|
local playerName = unit and unit:getPlayerName()
|
|
env.info(string.format("[MENU DEBUG] Processing existing player: %s", tostring(playerName)))
|
|
if unit and playerName then
|
|
CreatePlayerMenu(unit)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end, nil, timer.getTime() + 5)
|
|
|
|
-- Initialize zone status monitoring
|
|
SCHEDULER:New( nil, function()
|
|
log("[VICTORY SYSTEM] Initializing zone monitoring system...")
|
|
|
|
-- Initialize performance optimization caches
|
|
InitializeCachedUnitSet()
|
|
|
|
-- Note: All menus are created per-player via event handlers
|
|
|
|
-- Initial status report
|
|
SCHEDULER:New( nil, function()
|
|
log("[VICTORY SYSTEM] Broadcasting initial zone status...")
|
|
BroadcastZoneStatus()
|
|
end, {}, 30 ) -- Initial report after 30 seconds
|
|
|
|
end, {}, 5 ) -- Initialize after 5 seconds
|
|
|
|
log("[VICTORY SYSTEM] Zone capture victory monitoring system loaded successfully!")
|
|
log(string.format("[CONFIG] Loaded %d zones from configuration", totalZones))
|