Tonblader

cPlantillaV21q

Dec 10th, 2025 (edited)
52
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 241.51 KB | None | 0 0
  1. #include <sourcemod>
  2. #include <sdktools>
  3. #include <sdkhooks>
  4. #include <left4dhooks>
  5. #define MA_FILE_SYSTEM_IMPL
  6. #include "ma_file_system.inc"
  7.  
  8. #define MA_DEBUG 1
  9.  
  10. #pragma newdecls required
  11. #pragma semicolon 1
  12.  
  13. // ========================================================================
  14. // CONFIGURACIÓN AUTOMATIZADA
  15. // ========================================================================
  16.  
  17. ConVar g_hCvarForceFile;
  18. ConVar g_hCvarPlay;
  19. ConVar g_hCvarStopPlayer;
  20. ConVar g_hCvarAutoSwitch;
  21.  
  22. // CVARs para grupos de movimiento
  23. ConVar g_hCvarGroupPM;
  24. ConVar g_hCvarGroupBile;
  25. ConVar g_hCvarGroupGL;
  26. ConVar g_hCvarGroupRun;
  27. ConVar g_hCvarGroupDual;
  28. ConVar g_hCvarGroupsEnable;
  29.  
  30. // CVAR de depuración
  31. ConVar g_hCvarDebug;
  32. ConVar g_hCvarMA_Debug;
  33.  
  34. // ========================================================================
  35. // SISTEMA DE CONTADOR REGRESIVO INDEPENDIENTE
  36. // ========================================================================
  37.  
  38. // Timers individuales por jugador
  39. Handle g_hClientCountdownTimer[MAXPLAYERS + 1];
  40. Handle g_hClientMovementTimer[MAXPLAYERS + 1];
  41.  
  42. // Información de movimiento por jugador
  43. float g_fClientMovementEndTime[MAXPLAYERS + 1];
  44. float g_fClientMovementDuration[MAXPLAYERS + 1];
  45. int g_iClientMovementGroup[MAXPLAYERS + 1];  // Grupo del movimiento actual
  46. char g_sClientMovementFileName[MAXPLAYERS + 1][128];
  47.  
  48. // Control de mensajes del contador (para evitar spam)
  49. float g_fLastCountdownUpdate[MAXPLAYERS + 1];
  50. int g_iLastCountdownSeconds[MAXPLAYERS + 1];
  51.  
  52. // Estados del plugin
  53. bool g_bClientInMovement[MAXPLAYERS + 1];
  54. char g_sClientMovementFile[MAXPLAYERS + 1][128];
  55. Handle g_hClientTimer[MAXPLAYERS + 1];
  56.  
  57. // Estado de detención manual
  58. bool g_bManualStop[MAXPLAYERS + 1];
  59.  
  60. // Sistema automático de detección
  61. ArrayList g_hBoostPositions;
  62. ArrayList g_hMovementFiles;
  63. StringMap g_hMovementTriggers;
  64. StringMap g_hFileExistence;
  65. StringMap g_hMovementDurations;
  66. ArrayList g_hMovementFileInfos; // <-- Ahora es ArrayList de MovementFileInfo
  67.  
  68. // Para eventos de mapa
  69. char g_sCurrentMap[64];
  70. char g_sMapFolder[64];
  71.  
  72. // Variables globales para triggers
  73. int g_iTriggers[2048] = {INVALID_ENT_REFERENCE, ...};
  74. int g_iVisualModels[2048] = {INVALID_ENT_REFERENCE, ...};
  75.  
  76. // Control de mensajes de espera
  77. float g_fLastWaitMessage[MAXPLAYERS + 1][100]; // [client][group] - tiempo del último mensaje
  78. bool g_bWaitMessageShown[MAXPLAYERS + 1][100]; // [client][group] - si ya se mostró mensaje en esta activación
  79.  
  80. // Mapa de mapas oficiales
  81. StringMap g_hOfficialMaps;
  82.  
  83. // Timer para verificación de grupos
  84. Handle g_hGroupCheckTimer;
  85.  
  86. // Para almacenar posiciones de grupos
  87. StringMap g_hGroupPositions;
  88.  
  89. // ========================================================================
  90. // SISTEMA DE TRACKING POR TRIGGER - MEJORADO CON LÓGICA V15
  91. // ========================================================================
  92.  
  93. #define MAX_TRIGGERS 2048
  94. #define REFIRE_TIME 3.0
  95. #define MAX_RETRY_ATTEMPTS 8
  96.  
  97. // Tracking de jugadores por trigger
  98. bool g_bPlayerInTrigger[MAXPLAYERS + 1][MAX_TRIGGERS];      // true = dentro del trigger
  99. bool g_bPlayerReady[MAXPLAYERS + 1][MAX_TRIGGERS];          // true = cumple condiciones para ese trigger
  100. float g_fPlayerLastCheck[MAXPLAYERS + 1][MAX_TRIGGERS];     // timestamp de última verificación
  101. int g_iPlayerRetryCount[MAXPLAYERS + 1][MAX_TRIGGERS];      // contador de reintentos
  102.  
  103. // Información de cada trigger
  104. int g_iTriggerGroup[MAX_TRIGGERS] = {-1, ...};              // Grupo al que pertenece
  105. char g_sTriggerType[MAX_TRIGGERS][16];                      // Tipo de movimiento
  106. char g_sTriggerFile[MAX_TRIGGERS][128];                     // Archivo de movimiento
  107. int g_iTriggerFileIndex[MAX_TRIGGERS];                      // Índice en g_hMovementFiles
  108. float g_fLastActivation[MAX_TRIGGERS];                      // Última vez que se activó este trigger
  109.  
  110. // Mapeo grupo → lista de triggers del grupo
  111. StringMap g_hGroupTriggers;                                 // "1" → ArrayList de índices de triggers
  112. StringMap g_hGroupTypes;                                    // "1" → ArrayList de tipos únicos en el grupo
  113.  
  114. // Capacidad de activación por tipo
  115. int g_iTriggerCapacity[MAX_TRIGGERS];                       // Capacidad de runners para este trigger
  116.  
  117. // Sistema de verificación continua
  118. bool g_bTriggerEnabled[MAX_TRIGGERS];                       // Trigger está habilitado
  119. float g_fNextActivationTime[MAX_TRIGGERS];                  // Próximo tiempo de activación permitido
  120.  
  121. // Timers de verificación por jugador
  122. Handle g_hRetryTimer[MAXPLAYERS + 1][MAX_TRIGGERS];         // Timer de reintento por jugador por trigger
  123. bool g_bWaitingForWeapon[MAXPLAYERS + 1][MAX_TRIGGERS];     // Si el jugador está esperando por arma
  124.  
  125. // ========================================================================
  126. // ENUMS Y ESTRUCTURAS (LO QUE NO ESTÁ EN EL INCLUDE)
  127. // ========================================================================
  128.  
  129. enum MovementMessageType {
  130.     MSG_INDIVIDUAL = 0,
  131.     MSG_DUAL,
  132.     MSG_GROUP,
  133.     MSG_GROUP_DUAL,
  134.     MSG_GROUP_MULTI
  135. };
  136.  
  137. // ========================================================================
  138. // NUEVAS ESTRUCTURAS PARA GRUPOS MÚLTIPLES
  139. // ========================================================================
  140.  
  141. StringMap g_hGroupsByNumber;          // Grupos por número: "1" -> ArrayList de MovementFileInfo
  142. StringMap g_hFilesByGroupAndType;     // Archivos por grupo y tipo: "1_gl" -> MovementFileInfo
  143. StringMap g_hIndividualFiles;         // Archivos individuales (sin grupo)
  144.  
  145. // Sistema de activación de grupos
  146. StringMap g_hActiveGroups;            // Grupos que están activos actualmente
  147. StringMap g_hWaitingGroups;           // Jugadores esperando por grupo
  148.  
  149. // NUEVO: Control de jugadores por grupo - PARA EVITAR DOBLE ACTIVACIÓN
  150. StringMap g_hPlayersInGroupActivation; // "grupo_jugador" -> true/false
  151.  
  152. // Límites de runners por tipo
  153. #define MAX_RUNNERS_PM 32
  154. #define MAX_RUNNERS_BILE 5
  155. #define MAX_RUNNERS_GL 1
  156.  
  157. // ========================================================================
  158. // NUEVA ESTRUCTURA PARA ANÁLISIS DE ARCHIVOS ACTIVADOS
  159. // ========================================================================
  160.  
  161. enum struct GroupAnalysis {
  162.     bool hasPureRunner;
  163.     int pureRunnerCount;
  164.     int hybridPMCount;
  165.     int hybridBileCount;
  166.     int hybridGLCount;
  167.     int boosterPMCount;
  168.     int boosterBileCount;
  169.     int boosterGLCount;
  170.     ArrayList uniqueFileNames;
  171.     ArrayList uniqueFileDurations;
  172. }
  173.  
  174. // ========================================================================
  175. // PLUGIN INFO
  176. // ========================================================================
  177.  
  178. public Plugin myinfo =
  179. {
  180.     name = "Sistema de Movimientos Automático V21q (Nuevo Sistema de Nombres)",
  181.     author = "Tu nombre",
  182.     description = "Detección automática con nuevo sistema de nombres para grupos",
  183.     version = "21.0.2",
  184.     url = ""
  185. };
  186.  
  187. // ========================================================================
  188. // FORWARDS Y DECLARACIONES DE FUNCIONES PÚBLICAS
  189. // ========================================================================
  190.  
  191. // Funciones públicas (callbacks de SourceMod)
  192. public void OnGroupCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue);
  193. public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast);
  194. public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast);
  195. public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast);
  196. public void Event_WeaponFire(Event event, const char[] name, bool dontBroadcast);
  197. public Action Command_ForceMovement(int client, int args);
  198. public Action Command_StopMovement(int client, int args);
  199. public Action Command_ReloadMovements(int client, int args);
  200. public Action Command_ForceDual(int client, int args);
  201. public Action Command_ShowModels(int client, int args);
  202. public Action Command_HideModels(int client, int args);
  203. public Action Command_Groups(int client, int args);
  204. public Action Command_ToggleGroup(int client, int args);
  205. public Action Command_GlobalStop(int client, const char[] command, int argc);
  206. public void OnStartTouch(const char[] output, int caller, int activator, float delay);
  207. public void OnEndTouch(const char[] output, int caller, int activator, float delay);
  208. public Action Timer_RoundStartLoad(Handle timer);
  209. public Action Timer_MapStartLoad(Handle timer);
  210. public Action Timer_StopMovement(Handle timer, DataPack pack);
  211. public Action Timer_EnableTrigger(Handle timer, any index);
  212. public Action Timer_EnableGroupTriggers(Handle timer, DataPack pack);
  213. public Action Timer_StartCountdown(Handle timer, DataPack pack);
  214. public Action Timer_UpdateCountdown(Handle timer, DataPack pack);
  215. public Action Timer_ResetGroup(Handle timer, any groupNumber);
  216. public Action Timer_RecheckWeapon(Handle timer, DataPack pack);
  217. public Action Timer_RemoveVisualModel(Handle timer, any ref);
  218. public Action Timer_InitialLoad(Handle timer);
  219. public Action Timer_DelayedLoad(Handle timer);
  220. public Action Timer_CheckTriggerPresence(Handle timer, any client);
  221. public Action Timer_RetryCheck(Handle timer, DataPack pack);
  222. public Action Command_StopMe(int client, int args);
  223.  
  224. // ========================================================================
  225. // NUEVAS FUNCIONES PARA ANÁLISIS DE ARCHIVOS
  226. // ========================================================================
  227.  
  228. /**
  229.  * Analiza los archivos que se activarán en un grupo
  230.  */
  231. bool AnalyzeActivatedFiles(int groupNumber, ArrayList files, GroupAnalysis analysis);
  232.  
  233. /**
  234.  * Valida si la combinación de archivos es válida
  235.  */
  236. bool ValidateGroupComposition(const GroupAnalysis analysis, char[] errorMsg, int errorLen);
  237.  
  238. /**
  239.  * Genera el nombre para mostrar del grupo
  240.  */
  241. void GenerateGroupDisplayName(const GroupAnalysis analysis, int playerCount, char[] buffer, int bufferLen);
  242.  
  243. /**
  244.  * Ordena archivos para mostrar en mensaje público
  245.  */
  246. void SortFilesForDisplay(ArrayList files, ArrayList durations, ArrayList sortedFiles, ArrayList sortedDurations);
  247.  
  248. /**
  249.  * Formatea el mensaje público con archivos y duraciones
  250.  */
  251. void FormatPublicMessage(const char[] groupName, int groupNumber, ArrayList files,
  252.                         ArrayList durations, char[] buffer, int bufferLen);
  253.  
  254. /**
  255.  * Obtiene el nombre base del archivo (sin path ni extensión)
  256.  */
  257. void GetBaseFileName(const char[] fullPath, char[] baseName, int maxlen);
  258.  
  259. /**
  260.  * Obtiene la capacidad de un booster
  261.  */
  262. int GetBoosterCapacityNew(const char[] boosterType);
  263.  
  264. // ========================================================================
  265. // FUNCIONES MODIFICADAS PARA EL NUEVO SISTEMA
  266. // ========================================================================
  267.  
  268. /**
  269.  * Muestra resumen de activación grupal con nuevo sistema de nombres
  270.  */
  271. void ShowGroupActivationSummary(int groupNumber, ArrayList players, ArrayList files);
  272.  
  273. /**
  274.  * Activa grupo en orden y recopila archivos activados
  275.  */
  276. void ActivateGroupInOrder(int groupNumber, int runnerLimit, ArrayList &activatedFiles);
  277.  
  278. // ========================================================================
  279. // MACROS PARA DEPURACIÓN
  280. // ========================================================================
  281.  
  282. #define DEBUG_PREFIX "[MOVEMENTS] "
  283.  
  284. stock void DebugPrint(const char[] format, any ...)
  285. {
  286.     if (g_hCvarMA_Debug != null && g_hCvarMA_Debug.BoolValue)
  287.     {
  288.         char buffer[256];
  289.         VFormat(buffer, sizeof(buffer), format, 2);
  290.         PrintToServer("%s%s", DEBUG_PREFIX, buffer);
  291.     }
  292. }
  293.  
  294. // ========================================================================
  295. // INICIALIZACIÓN DEL PLUGIN
  296. // ========================================================================
  297.  
  298. public void OnPluginStart()
  299. {
  300.     g_hCvarForceFile = FindConVar("st_mr_force_file");
  301.     g_hCvarPlay = FindConVar("st_mr_play");
  302.     g_hCvarStopPlayer = FindConVar("st_mr_stop_player");
  303.    
  304.     g_hCvarAutoSwitch = CreateConVar("st_mr_auto_switch", "1",
  305.         "Cambia automáticamente al arma requerida cuando activas un trigger: 1=Activado, 0=Desactivado",
  306.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  307.    
  308.     // CVAR de depuración
  309.     g_hCvarDebug = CreateConVar("st_mr_debug", "0",
  310.         "Activa/desactiva los mensajes de depuración: 1=Activado, 0=Desactivado",
  311.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  312.        
  313.     g_hCvarMA_Debug = CreateConVar("ma_debug", "0",
  314.     "Activa/desactiva la depuración del plugin de Movimientos Automáticos: 1=Activado, 0=Desactivado",
  315.     FCVAR_NOTIFY, true, 0.0, true, 1.0);
  316.    
  317.     // CVARs para grupos de movimiento
  318.     g_hCvarGroupsEnable = CreateConVar("st_mr_groups_enable", "1",
  319.         "Activa/desactiva el sistema de grupos de movimiento: 1=Activado, 0=Desactivado",
  320.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  321.    
  322.     g_hCvarGroupPM = CreateConVar("st_mr_group_pm", "1",
  323.         "Activa/desactiva el grupo de movimientos P/M: 1=Activado, 0=Desactivado",
  324.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  325.    
  326.     g_hCvarGroupBile = CreateConVar("st_mr_group_bile", "1",
  327.         "Activa/desactiva el grupo de movimientos Bile: 1=Activado, 0=Desactivado",
  328.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  329.    
  330.     g_hCvarGroupGL = CreateConVar("st_mr_group_gl", "1",
  331.         "Activa/desactiva el grupo de movimientos GL: 1=Activado, 0=Desactivado",
  332.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  333.    
  334.     g_hCvarGroupRun = CreateConVar("st_mr_group_run", "1",
  335.         "Activa/desactiva el grupo de movimientos Run: 1=Activado, 0=Desactivado",
  336.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  337.    
  338.     g_hCvarGroupDual = CreateConVar("st_mr_group_dual", "1",
  339.         "Activa/desactiva el grupo de movimientos Dual: 1=Activado, 0=Desactivado",
  340.         FCVAR_NOTIFY, true, 0.0, true, 1.0);
  341.    
  342.     // Hook para detectar cambios en CVARs de grupos
  343.     HookConVarChange(g_hCvarGroupsEnable, OnGroupCvarChanged);
  344.     HookConVarChange(g_hCvarGroupPM, OnGroupCvarChanged);
  345.     HookConVarChange(g_hCvarGroupBile, OnGroupCvarChanged);
  346.     HookConVarChange(g_hCvarGroupGL, OnGroupCvarChanged);
  347.     HookConVarChange(g_hCvarGroupRun, OnGroupCvarChanged);
  348.     HookConVarChange(g_hCvarGroupDual, OnGroupCvarChanged);
  349.    
  350.     // Comandos básicos necesarios
  351.     RegConsoleCmd("sm_move", Command_ForceMovement);
  352.     RegConsoleCmd("sm_stopmove", Command_StopMovement);
  353.     RegConsoleCmd("sm_reloadmovements", Command_ReloadMovements);
  354.     RegConsoleCmd("sm_forcedual", Command_ForceDual);
  355.     RegConsoleCmd("ma_stopme", Command_StopMe, "Detiene tu movimiento actual");
  356.    
  357.     // Comandos de visualización (solo admin)
  358.     RegAdminCmd("sm_show_models", Command_ShowModels, ADMFLAG_GENERIC, "Muestra los modelos de cono en los triggers");
  359.     RegAdminCmd("sm_hide_models", Command_HideModels, ADMFLAG_GENERIC, "Esconde los modelos de cono");
  360.    
  361.     // Comandos para grupos (solo admin)
  362.     RegAdminCmd("sm_groups", Command_Groups, ADMFLAG_GENERIC, "Muestra el estado de los grupos de movimiento");
  363.     RegAdminCmd("sm_togglegroup", Command_ToggleGroup, ADMFLAG_GENERIC, "Activa/desactiva un grupo: sm_togglegroup <pm|bile|gl|run|dual|all>");
  364.    
  365.     // Hookear comandos del plugin movement reader
  366.     AddCommandListener(Command_GlobalStop, "st_mr_stop");
  367.     AddCommandListener(Command_GlobalStop, "sm_stop_all_movements");
  368.    
  369.     // Eventos
  370.     HookEvent("round_start", Event_RoundStart);
  371.     HookEvent("round_end", Event_RoundEnd);
  372.     HookEvent("player_spawn", Event_PlayerSpawn);
  373.     HookEvent("weapon_fire", Event_WeaponFire);
  374.    
  375.     // Inicializar arrays
  376.     g_hBoostPositions = new ArrayList(8);
  377.     g_hMovementFiles = new ArrayList(ByteCountToCells(128));
  378.     g_hMovementTriggers = new StringMap();
  379.     g_hFileExistence = new StringMap();
  380.     g_hMovementDurations = new StringMap();
  381.     g_hMovementFileInfos = new ArrayList(sizeof(MovementFileInfo));
  382.    
  383.     // Inicializar nuevo sistema de grupos
  384.     InitializeGroupSystem();
  385.    
  386.     // Inicializar mapa de mapas oficiales USANDO EL INCLUDE
  387.     g_hOfficialMaps = new StringMap();
  388.     MA_InitializeOfficialMaps(g_hOfficialMaps);
  389.    
  390.     // Inicializar sistema de tracking mejorado
  391.     InitializeTrackingSystem();
  392.    
  393.     // Inicializar sistema de refire_time mejorado
  394.     for (int i = 0; i < MAX_TRIGGERS; i++)
  395.     {
  396.         g_iTriggerGroup[i] = -1;
  397.         g_sTriggerType[i][0] = '\0';
  398.         g_sTriggerFile[i][0] = '\0';
  399.         g_iTriggerFileIndex[i] = -1;
  400.         g_iTriggerCapacity[i] = 0;
  401.         g_fLastActivation[i] = 0.0;
  402.         g_bTriggerEnabled[i] = true;
  403.         g_fNextActivationTime[i] = 0.0;
  404.     }
  405.    
  406.     // Inicializar sistema de contador regresivo
  407.     for (int i = 0; i <= MaxClients; i++)
  408.     {
  409.         g_hClientCountdownTimer[i] = null;
  410.         g_fClientMovementEndTime[i] = 0.0;
  411.         ClearPlayerTriggerStates(i);
  412.        
  413.         for (int j = 0; j < MAX_TRIGGERS; j++)
  414.         {
  415.             g_hRetryTimer[i][j] = null;
  416.             g_bWaitingForWeapon[i][j] = false;
  417.         }
  418.     }
  419.    
  420.     // Inicializar timer de grupos
  421.     g_hGroupCheckTimer = null;
  422.    
  423.     DebugPrint("Sistema Automático V21.0.2 cargado - Nuevo Sistema de Nombres");
  424.     DebugPrint("CVAR st_mr_auto_switch: %d", g_hCvarAutoSwitch.IntValue);
  425.     DebugPrint("Sistema de grupos: %s", g_hCvarGroupsEnable.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  426.     DebugPrint("Activación cíclica: cada %.1f segundos", REFIRE_TIME);
  427.     DebugPrint("Sistema de reintentos: %d intentos máximos", MAX_RETRY_ATTEMPTS);
  428.    
  429.     // Carga inicial con timer para asegurar que el plugin esté completamente inicializado
  430.     CreateTimer(2.0, Timer_UpdateTriggers, _, TIMER_REPEAT);
  431.     CreateTimer(1.0, Timer_InitialLoad, _, TIMER_FLAG_NO_MAPCHANGE);
  432. }
  433.  
  434. // ========================================================================
  435. // FUNCIONES DE INICIALIZACIÓN Y LIMPIEZA
  436. // ========================================================================
  437.  
  438. void InitializeGroupSystem()
  439. {
  440.     g_hGroupsByNumber = new StringMap();
  441.     g_hFilesByGroupAndType = new StringMap();
  442.     g_hIndividualFiles = new StringMap();
  443.     g_hActiveGroups = new StringMap();
  444.     g_hWaitingGroups = new StringMap();
  445.     g_hGroupPositions = new StringMap();
  446. }
  447.  
  448. void ClearGroupSystem()
  449. {
  450.     // Limpiar grupos por número
  451.     if (g_hGroupsByNumber != null)
  452.     {
  453.         StringMapSnapshot snapshot = g_hGroupsByNumber.Snapshot();
  454.         char key[16];
  455.         for (int i = 0; i < snapshot.Length; i++)
  456.         {
  457.             snapshot.GetKey(i, key, sizeof(key));
  458.             ArrayList list;
  459.             if (g_hGroupsByNumber.GetValue(key, list))
  460.             {
  461.                 delete list;
  462.             }
  463.         }
  464.         delete snapshot;
  465.         g_hGroupsByNumber.Clear();
  466.     }
  467.    
  468.     if (g_hFilesByGroupAndType != null) g_hFilesByGroupAndType.Clear();
  469.     if (g_hIndividualFiles != null) g_hIndividualFiles.Clear();
  470.     if (g_hActiveGroups != null) g_hActiveGroups.Clear();
  471.     if (g_hWaitingGroups != null) g_hWaitingGroups.Clear();
  472.     if (g_hGroupPositions != null) g_hGroupPositions.Clear();
  473. }
  474.  
  475. void InitializeTrackingSystem()
  476. {
  477.     g_hGroupTriggers = new StringMap();
  478.     g_hGroupTypes = new StringMap();
  479.     g_hPlayersInGroupActivation = new StringMap();
  480.    
  481.     // Inicializar estados de tracking (más simple que antes)
  482.     for (int client = 0; client <= MaxClients; client++)
  483.     {
  484.         ClearPlayerTriggerStates(client);
  485.     }
  486. }
  487.  
  488. void ClearTrackingSystem()
  489. {
  490.     // Limpiar arrays de tracking
  491.     for (int i = 0; i <= MaxClients; i++)
  492.     {
  493.         ClearPlayerTriggerStates(i);
  494.     }
  495.    
  496.     // Limpiar información de triggers
  497.     for (int i = 0; i < MAX_TRIGGERS; i++)
  498.     {
  499.         g_iTriggerGroup[i] = -1;
  500.         g_sTriggerType[i][0] = '\0';
  501.         g_sTriggerFile[i][0] = '\0';
  502.         g_iTriggerFileIndex[i] = -1;
  503.         g_iTriggerCapacity[i] = 0;
  504.         g_fLastActivation[i] = 0.0;
  505.         g_bTriggerEnabled[i] = true;
  506.         g_fNextActivationTime[i] = 0.0;
  507.     }
  508.    
  509.     // Limpiar mapa de control de jugadores por grupo
  510.     if (g_hPlayersInGroupActivation != null)
  511.     {
  512.         g_hPlayersInGroupActivation.Clear();
  513.     }
  514.    
  515.     // Limpiar mapas de grupos
  516.     if (g_hGroupTriggers != null)
  517.     {
  518.         StringMapSnapshot snapshot = g_hGroupTriggers.Snapshot();
  519.         char key[16];
  520.         for (int i = 0; i < snapshot.Length; i++)
  521.         {
  522.             snapshot.GetKey(i, key, sizeof(key));
  523.             ArrayList list;
  524.             if (g_hGroupTriggers.GetValue(key, list))
  525.             {
  526.                 delete list;
  527.             }
  528.         }
  529.         delete snapshot;
  530.         g_hGroupTriggers.Clear();
  531.     }
  532.    
  533.     if (g_hGroupTypes != null)
  534.     {
  535.         StringMapSnapshot snapshot = g_hGroupTypes.Snapshot();
  536.         char key[16];
  537.         for (int i = 0; i < snapshot.Length; i++)
  538.         {
  539.             snapshot.GetKey(i, key, sizeof(key));
  540.             ArrayList list;
  541.             if (g_hGroupTypes.GetValue(key, list))
  542.             {
  543.                 delete list;
  544.             }
  545.         }
  546.         delete snapshot;
  547.         g_hGroupTypes.Clear();
  548.     }
  549. }
  550.  
  551. void ClearPlayerTriggerStates(int client)
  552. {
  553.     for (int i = 0; i < MAX_TRIGGERS; i++)
  554.     {
  555.         g_bPlayerInTrigger[client][i] = false;
  556.         g_bPlayerReady[client][i] = false;
  557.         g_fPlayerLastCheck[client][i] = 0.0;
  558.         g_iPlayerRetryCount[client][i] = 0;
  559.         g_bWaitingForWeapon[client][i] = false;
  560.        
  561.         // Cancelar timers de reintento
  562.         if (g_hRetryTimer[client][i] != null)
  563.         {
  564.             KillTimer(g_hRetryTimer[client][i]);
  565.             g_hRetryTimer[client][i] = null;
  566.         }
  567.     }
  568.    
  569.     // Limpiar control de mensajes de espera
  570.     for (int group = 0; group < 100; group++)
  571.     {
  572.         g_fLastWaitMessage[client][group] = 0.0;
  573.         g_bWaitMessageShown[client][group] = false;
  574.     }
  575. }
  576.  
  577. // ========================================================================
  578. // FUNCIONES PARA DETENER MOVIMIENTO DE CLIENTE
  579. // ========================================================================
  580.  
  581. void ClientStopMovement(int client)
  582. {
  583.     // Redirigir a la nueva función mejorada
  584.     ForceStopClientMovement(client, false);
  585. }
  586.  
  587. // ========================================================================
  588. // DETERMINAR CARPETA DEL MAPA - USANDO EL INCLUDE
  589. // ========================================================================
  590.  
  591. void DetermineMapFolder()
  592. {
  593.     char mapBuffer[64];
  594.     strcopy(mapBuffer, sizeof(mapBuffer), g_sCurrentMap);
  595.    
  596.     DebugPrint("DetermineMapFolder - Mapa original: %s", g_sCurrentMap);
  597.    
  598.     // Usar la función del include
  599.     MA_DetermineMapFolder(g_sCurrentMap, g_sMapFolder, sizeof(g_sMapFolder), g_hOfficialMaps);
  600.    
  601.     DebugPrint("DetermineMapFolder - Carpeta final: %s", g_sMapFolder);
  602. }
  603.  
  604. // ========================================================================
  605. // TIMER DE CARGA INICIAL
  606. // ========================================================================
  607.  
  608. public Action Timer_InitialLoad(Handle timer)
  609. {
  610.     DebugPrint("Timer_InitialLoad - Verificando estado del plugin...");
  611.    
  612.     // Si ya estamos en un mapa, cargar los movimientos
  613.     if (strlen(g_sCurrentMap) > 0 && strlen(g_sMapFolder) > 0)
  614.     {
  615.         DebugPrint("Mapa ya cargado: %s - Forzando recarga...", g_sCurrentMap);
  616.         LoadMovementFilesAutomatically();
  617.     }
  618.    
  619.     return Plugin_Stop;
  620. }
  621.  
  622. // ========================================================================
  623. // ON MAP START - CORREGIDA CON TIMERS
  624. // ========================================================================
  625.  
  626. public void OnMapStart()
  627. {
  628.     GetCurrentMap(g_sCurrentMap, sizeof(g_sCurrentMap));
  629.     DetermineMapFolder();
  630.    
  631.     DebugPrint("===== INICIO DE MAPA =====");
  632.     DebugPrint("Mapa: %s", g_sCurrentMap);
  633.     DebugPrint("Carpeta: %s", g_sMapFolder);
  634.    
  635.     // Limpiar estados
  636.     for (int i = 1; i <= MaxClients; i++)
  637.     {
  638.         ClientStopMovement(i);
  639.     }
  640.    
  641.     // Precache de modelos - CON FIRMEZA
  642.     char models[][PLATFORM_MAX_PATH] = {
  643.         "models/props_fortifications/orange_cone001_reference.mdl",
  644.         "models/props/cs_assault/money.mdl",
  645.         "models/props_junk/glassbottle01a.mdl",
  646.         "models/props_junk/cardboard_box001a.mdl",
  647.         "models/props/cs_militia/silo_01.mdl"
  648.     };
  649.    
  650.     for (int i = 0; i < sizeof(models); i++)
  651.     {
  652.         if (!IsModelPrecached(models[i]))
  653.         {
  654.             PrecacheModel(models[i]);
  655.             DebugPrint("Modelo precargado: %s", models[i]);
  656.         }
  657.     }
  658.    
  659.     if (g_hGroupCheckTimer != null)
  660.     {
  661.         KillTimer(g_hGroupCheckTimer);
  662.         g_hGroupCheckTimer = null;
  663.     }
  664.    
  665.     // CAMBIO CRÍTICO: Usar Timer como en V18 original
  666.     CreateTimer(2.0, Timer_MapStartLoad, _, TIMER_FLAG_NO_MAPCHANGE);
  667.     CreateTimer(3.0, Timer_CheckTriggers);
  668. }
  669.  
  670. public Action Timer_MapStartLoad(Handle timer)
  671. {
  672.     DebugPrint("Timer_MapStartLoad - Cargando movimientos...");
  673.     LoadMovementFilesAutomatically();
  674.     return Plugin_Stop;
  675. }
  676.  
  677. // ========================================================================
  678. // EVENTOS DE RONDA - CORREGIDOS
  679. // ========================================================================
  680.  
  681. public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
  682. {
  683.     DebugPrint("Ronda iniciada - Recargando triggers...");
  684.    
  685.     for (int i = 1; i <= MaxClients; i++)
  686.     {
  687.         ClientStopMovement(i);
  688.     }
  689.    
  690.     if (g_hActiveGroups != null)
  691.         g_hActiveGroups.Clear();
  692.    
  693.     if (g_hWaitingGroups != null)
  694.         g_hWaitingGroups.Clear();
  695.    
  696.     if (g_hGroupPositions != null)
  697.         g_hGroupPositions.Clear();
  698.    
  699.     // Limpiar control de jugadores por grupo
  700.     if (g_hPlayersInGroupActivation != null)
  701.     {
  702.         g_hPlayersInGroupActivation.Clear();
  703.     }
  704.    
  705.     // Resetear tiempos de activación de todos los triggers
  706.     for (int i = 0; i < MAX_TRIGGERS; i++)
  707.     {
  708.         g_fLastActivation[i] = 0.0;
  709.         g_bTriggerEnabled[i] = true;
  710.         g_fNextActivationTime[i] = 0.0;
  711.     }
  712.    
  713.     // CAMBIO CRÍTICO: Usar Timer igual que en V18
  714.     CreateTimer(2.0, Timer_RoundStartLoad, _, TIMER_FLAG_NO_MAPCHANGE);
  715.     CreateTimer(3.0, Timer_CheckTriggers);
  716. }
  717.  
  718. public Action Timer_RoundStartLoad(Handle timer)
  719. {
  720.     DebugPrint("Timer_RoundStartLoad - Recargando triggers...");
  721.     LoadMovementFilesAutomatically();
  722.     return Plugin_Stop;
  723. }
  724.  
  725. public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
  726. {
  727.     DebugPrint("Ronda terminada - Deteniendo movimientos");
  728.     for (int i = 1; i <= MaxClients; i++)
  729.     {
  730.         ClientStopMovement(i);
  731.     }
  732.    
  733.     if (g_hActiveGroups != null)
  734.         g_hActiveGroups.Clear();
  735.    
  736.     if (g_hWaitingGroups != null)
  737.         g_hWaitingGroups.Clear();
  738.    
  739.     // Limpiar control de jugadores por grupo
  740.     if (g_hPlayersInGroupActivation != null)
  741.     {
  742.         g_hPlayersInGroupActivation.Clear();
  743.     }
  744. }
  745.  
  746. // ========================================================================
  747. // EVENTOS Y TIMERS
  748. // ========================================================================
  749.  
  750. public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast)
  751. {
  752.     int client = GetClientOfUserId(event.GetInt("userid"));
  753.     if (client > 0 && client <= MaxClients && IsClientInGame(client))
  754.     {
  755.         // **IMPORTANTE: NUNCA detener movimiento por spawn**
  756.         // Esto incluye spawns por idle
  757.         // El movimiento solo debe detenerse cuando termine su duración
  758.         // o por comando explícito
  759.        
  760.         DebugPrint("Event_PlayerSpawn - %N ha reaparecido (posible idle), manteniendo movimiento si está activo", client);
  761.        
  762.         // Verificar si el jugador tenía un movimiento activo antes del idle
  763.         // y restaurarlo si es necesario
  764.         if (g_bClientInMovement[client] && strlen(g_sClientMovementFile[client]) > 0)
  765.         {
  766.             DebugPrint("Event_PlayerSpawn - %N mantiene movimiento activo: %s",
  767.                 client, g_sClientMovementFile[client]);
  768.         }
  769.     }
  770. }
  771.  
  772. public Action Timer_StopMovement(Handle timer, DataPack pack)
  773. {
  774.     pack.Reset();
  775.     int userid = pack.ReadCell();
  776.     char movementFile[128];
  777.     pack.ReadString(movementFile, sizeof(movementFile));
  778.     int groupNumber = pack.ReadCell();
  779.     delete pack;
  780.    
  781.     int client = GetClientOfUserId(userid);
  782.     if (client > 0 && IsClientInGame(client))
  783.     {
  784.         if (g_bClientInMovement[client] && StrEqual(g_sClientMovementFile[client], movementFile))
  785.         {
  786.             // NUEVO: Mostrar mensaje de completado
  787.             MovementFileInfo info;
  788.             bool foundInfo = MA_GetMovementFileInfo(movementFile, g_hMovementFileInfos, info);
  789.            
  790.             // IMPORTANTE: Liberar al jugador del grupo INMEDIATAMENTE
  791.             if (groupNumber > 0)
  792.             {
  793.                 DebugPrint("Timer_StopMovement - %N completó movimiento del grupo %d - liberando del grupo", client, groupNumber);
  794.                 MarkPlayerInGroupActivation(groupNumber, client, false);
  795.                
  796.                 // Mostrar mensaje al jugador
  797.                 PrintToChat(client, "[\x04MA Grupo (%d)\x01] ¡Completado! (%.1fs)", groupNumber, g_fClientMovementDuration[client]);
  798.                
  799.                 // Verificar si el grupo está vacío
  800.                 if (IsGroupEmpty(groupNumber))
  801.                 {
  802.                     char groupKey[16];
  803.                     IntToString(groupNumber, groupKey, sizeof(groupKey));
  804.                    
  805.                     if (g_hActiveGroups != null)
  806.                     {
  807.                         g_hActiveGroups.Remove(groupKey);
  808.                         DebugPrint("Timer_StopMovement - Grupo %d completamente liberado (todos terminaron)", groupNumber);
  809.                     }
  810.                 }
  811.             }
  812.             else
  813.             {
  814.                 PrintToChat(client, "[\x04MA Movimiento\x01] ¡Completado! (%.1fs)", g_fClientMovementDuration[client]);
  815.                 DebugPrint("Timer_StopMovement - %N completó movimiento individual", client);
  816.             }
  817.            
  818.             // Detener movimiento
  819.             ForceStopClientMovement(client, false);
  820.         }
  821.     }
  822.    
  823.     // Limpiar timer
  824.     g_hClientMovementTimer[client] = null;
  825.     return Plugin_Stop;
  826. }
  827.  
  828. bool IsGroupEmpty(int groupNumber)
  829. {
  830.     // Verificar si algún jugador todavía está marcado en este grupo
  831.     for (int client = 1; client <= MaxClients; client++)
  832.     {
  833.         if (IsValidSurvivor(client) && IsPlayerAlreadyInGroupActivation(groupNumber, client))
  834.         {
  835.             return false;
  836.         }
  837.     }
  838.     return true;
  839. }
  840.  
  841. public void Event_WeaponFire(Event event, const char[] name, bool dontBroadcast)
  842. {
  843.     // Esta función puede usarse para detectar cuándo un jugador dispara
  844.     // y actualizar su estado de "arma lista"
  845. }
  846.  
  847. // ========================================================================
  848. // EVENTO DE CLIENTE DESCONECTADO
  849. // ========================================================================
  850.  
  851. public void OnClientDisconnect(int client)
  852. {
  853.     // Solo procesar si es un cliente real (no bot)
  854.     if (IsClientInGame(client) && !IsFakeClient(client))
  855.     {
  856.         DebugPrint("OnClientDisconnect - %N se desconectó", client);
  857.        
  858.         // Detener movimiento solo para desconexiones reales
  859.         ClientStopMovement(client);
  860.         RemoveFromGroupWaitingLists(client);
  861.         ClearPlayerTriggerStates(client);
  862.        
  863.         // Remover del control de grupos
  864.         for (int groupNumber = 1; groupNumber <= 100; groupNumber++)
  865.         {
  866.             MarkPlayerInGroupActivation(groupNumber, client, false);
  867.         }
  868.     }
  869. }
  870.  
  871. void RemoveFromGroupWaitingLists(int client)
  872. {
  873.     if (g_hWaitingGroups == null)
  874.         return;
  875.    
  876.     int userid = GetClientUserId(client);
  877.    
  878.     StringMapSnapshot snapshot = g_hWaitingGroups.Snapshot();
  879.     for (int i = 0; i < snapshot.Length; i++)
  880.     {
  881.         char groupKey[16];
  882.         snapshot.GetKey(i, groupKey, sizeof(groupKey));
  883.        
  884.         ArrayList waitingList;
  885.         if (g_hWaitingGroups.GetValue(groupKey, waitingList))
  886.         {
  887.             int index = waitingList.FindValue(userid);
  888.             if (index != -1)
  889.             {
  890.                 waitingList.Erase(index);
  891.                
  892.                 if (waitingList.Length == 0)
  893.                 {
  894.                     delete waitingList;
  895.                     g_hWaitingGroups.Remove(groupKey);
  896.                 }
  897.             }
  898.         }
  899.     }
  900.     delete snapshot;
  901. }
  902.  
  903. // ========================================================================
  904. // FUNCIONES AUXILIARES ADICIONALES
  905. // ========================================================================
  906.  
  907. void GetFileNameFromPath(const char[] path, char[] filename, int maxlen)
  908. {
  909.     int pos = FindCharInString(path, '/', true);
  910.     if (pos == -1)
  911.         pos = FindCharInString(path, '\\', true);
  912.    
  913.     if (pos != -1)
  914.         strcopy(filename, maxlen, path[pos + 1]);
  915.     else
  916.         strcopy(filename, maxlen, path);
  917. }
  918.  
  919. bool IsValidSurvivor(int client)
  920. {
  921.     return (client > 0 && client <= MaxClients && IsClientInGame(client) && GetClientTeam(client) == 2);
  922. }
  923.  
  924. bool IsValidEntRef(int ref)
  925. {
  926.     return (ref != INVALID_ENT_REFERENCE && EntRefToEntIndex(ref) != INVALID_ENT_REFERENCE);
  927. }
  928.  
  929. // ========================================================================
  930. // NUEVAS FUNCIONES PARA CONTROL DE JUGADORES ÚNICOS POR GRUPO
  931. // ========================================================================
  932.  
  933. bool IsPlayerAlreadyInGroupActivation(int groupNumber, int client)
  934. {
  935.     char key[32];
  936.     Format(key, sizeof(key), "%d_%d", groupNumber, GetClientUserId(client));
  937.    
  938.     bool value;
  939.     if (g_hPlayersInGroupActivation.GetValue(key, value))
  940.     {
  941.         return value;
  942.     }
  943.     return false;
  944. }
  945.  
  946. void MarkPlayerInGroupActivation(int groupNumber, int client, bool inGroup)
  947. {
  948.     char key[32];
  949.     Format(key, sizeof(key), "%d_%d", groupNumber, GetClientUserId(client));
  950.    
  951.     g_hPlayersInGroupActivation.SetValue(key, inGroup);
  952.    
  953.     DebugPrint("MarkPlayerInGroupActivation - Jugador %N %s en activación del grupo %d",
  954.         client, inGroup ? "agregado" : "removido", groupNumber);
  955. }
  956.  
  957. void ClearGroupActivationPlayers(int groupNumber)
  958. {
  959.     if (g_hPlayersInGroupActivation == null) return;
  960.    
  961.     StringMapSnapshot snapshot = g_hPlayersInGroupActivation.Snapshot();
  962.     char key[32];
  963.    
  964.     for (int i = 0; i < snapshot.Length; i++)
  965.     {
  966.         snapshot.GetKey(i, key, sizeof(key));
  967.        
  968.         // Verificar si la clave pertenece a este grupo
  969.         char parts[2][16];
  970.         if (ExplodeString(key, "_", parts, 2, 16) == 2)
  971.         {
  972.             int keyGroup = StringToInt(parts[0]);
  973.             if (keyGroup == groupNumber)
  974.             {
  975.                 g_hPlayersInGroupActivation.Remove(key);
  976.             }
  977.         }
  978.     }
  979.    
  980.     delete snapshot;
  981. }
  982.  
  983. // ========================================================================
  984. // SISTEMA DE GRUPOS DE MOVIMIENTO
  985. // ========================================================================
  986.  
  987. public void OnGroupCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
  988. {
  989.     // Solo recargar si hay datos cargados
  990.     if (g_hMovementFiles.Length > 0)
  991.     {
  992.         DebugPrint("OnGroupCvarChanged - CVAR cambiada - Recargando triggers...");
  993.         RecreateTriggers();
  994.        
  995.         // Mostrar estado actualizado
  996.         PrintGroupStatus();
  997.     }
  998. }
  999.  
  1000. void PrintGroupStatus()
  1001. {
  1002.     DebugPrint("=== Estado de Grupos ===");
  1003.     DebugPrint("Sistema: %s", g_hCvarGroupsEnable.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  1004.     DebugPrint("P/M: %s", g_hCvarGroupPM.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  1005.     DebugPrint("Bile: %s", g_hCvarGroupBile.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  1006.     DebugPrint("GL: %s", g_hCvarGroupGL.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  1007.     DebugPrint("Run: %s", g_hCvarGroupRun.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  1008.     DebugPrint("Dual: %s", g_hCvarGroupDual.BoolValue ? "ACTIVADO" : "DESACTIVADO");
  1009. }
  1010.  
  1011. bool IsGroupEnabledByType(MovementType movType)
  1012. {
  1013.     if (!g_hCvarGroupsEnable.BoolValue)
  1014.         return false;
  1015.    
  1016.     if (movType == MOVEMENT_PM) return g_hCvarGroupPM.BoolValue;
  1017.     else if (movType == MOVEMENT_BILE) return g_hCvarGroupBile.BoolValue;
  1018.     else if (movType == MOVEMENT_GL) return g_hCvarGroupGL.BoolValue;
  1019.     else if (movType == MOVEMENT_RUN) return g_hCvarGroupRun.BoolValue;
  1020.     else if (movType == MOVEMENT_DUAL) return g_hCvarGroupDual.BoolValue;
  1021.     else if (movType == MOVEMENT_RUNPM) return g_hCvarGroupPM.BoolValue && g_hCvarGroupRun.BoolValue;
  1022.     else if (movType == MOVEMENT_RUNBILE) return g_hCvarGroupBile.BoolValue && g_hCvarGroupRun.BoolValue;
  1023.     else if (movType == MOVEMENT_RUNGL) return g_hCvarGroupGL.BoolValue && g_hCvarGroupRun.BoolValue;
  1024.    
  1025.     return false;
  1026. }
  1027.  
  1028. // ========================================================================
  1029. // SISTEMA DE MODELOS VISUALES
  1030. // ========================================================================
  1031.  
  1032. void CreateVisualModels()
  1033. {
  1034.     RemoveVisualModels();
  1035.    
  1036.     for (int i = 0; i < g_hBoostPositions.Length; i++)
  1037.     {
  1038.         CreateVisualModelForTrigger(i, 0.0);
  1039.     }
  1040.    
  1041.     DebugPrint("CreateVisualModels - Modelos visuales creados para %d triggers", g_hBoostPositions.Length);
  1042. }
  1043.  
  1044. void CreateVisualModelForTrigger(int index, float duration)
  1045. {
  1046.     if (index < 0 || index >= g_hBoostPositions.Length)
  1047.         return;
  1048.    
  1049.     int data[8];
  1050.     g_hBoostPositions.GetArray(index, data, sizeof(data));
  1051.    
  1052.     float pos[3];
  1053.     pos[0] = view_as<float>(data[0]);
  1054.     pos[1] = view_as<float>(data[1]);
  1055.     pos[2] = view_as<float>(data[2]);
  1056.    
  1057.     char file[128];
  1058.     g_hMovementFiles.GetString(data[6], file, sizeof(file));
  1059.    
  1060.     MovementType movType = MA_GetMovementType(file, g_hMovementFileInfos);
  1061.    
  1062.     int visualModel = CreateEntityByName("prop_dynamic");
  1063.     if (visualModel == -1)
  1064.         return;
  1065.    
  1066.     // Verificar modelo
  1067.     char model[] = "models/props_fortifications/orange_cone001_reference.mdl";
  1068.     if (!IsModelPrecached(model))
  1069.     {
  1070.         PrecacheModel(model);
  1071.     }
  1072.    
  1073.     SetEntityModel(visualModel, model);
  1074.    
  1075.     float adjustedPos[3];
  1076.     adjustedPos[0] = pos[0];
  1077.     adjustedPos[1] = pos[1];
  1078.     adjustedPos[2] = pos[2] + 16.0;
  1079.    
  1080.     DispatchKeyValue(visualModel, "targetname", "movement_visual_model");
  1081.     DispatchKeyValue(visualModel, "disableshadows", "1");
  1082.     DispatchKeyValue(visualModel, "solid", "0");
  1083.    
  1084.     char color[32];
  1085.     if (movType == MOVEMENT_PM)
  1086.         Format(color, sizeof(color), "255 0 0");
  1087.     else if (movType == MOVEMENT_BILE)
  1088.         Format(color, sizeof(color), "0 255 0");
  1089.     else if (movType == MOVEMENT_GL)
  1090.         Format(color, sizeof(color), "0 0 255");
  1091.     else if (movType == MOVEMENT_RUN)
  1092.         Format(color, sizeof(color), "255 255 0");
  1093.     else if (movType == MOVEMENT_RUNPM)
  1094.         Format(color, sizeof(color), "255 128 0");
  1095.     else if (movType == MOVEMENT_RUNBILE)
  1096.         Format(color, sizeof(color), "128 255 0");
  1097.     else if (movType == MOVEMENT_RUNGL)
  1098.         Format(color, sizeof(color), "0 128 255");
  1099.     else
  1100.         Format(color, sizeof(color), "255 255 255");
  1101.    
  1102.     DispatchKeyValue(visualModel, "rendercolor", color);
  1103.     DispatchKeyValue(visualModel, "renderamt", "200");
  1104.    
  1105.     DispatchSpawn(visualModel);
  1106.     TeleportEntity(visualModel, adjustedPos, NULL_VECTOR, NULL_VECTOR);
  1107.     SetEntityRenderMode(visualModel, RENDER_NORMAL);
  1108.    
  1109.     g_iVisualModels[index] = EntIndexToEntRef(visualModel);
  1110.    
  1111.     if (duration > 0)
  1112.         CreateTimer(duration, Timer_RemoveVisualModel, EntIndexToEntRef(visualModel));
  1113. }
  1114.  
  1115. public Action Timer_RemoveVisualModel(Handle timer, any ref)
  1116. {
  1117.     int model = EntRefToEntIndex(ref);
  1118.     if (model != INVALID_ENT_REFERENCE)
  1119.         RemoveEntity(model);
  1120.     return Plugin_Stop;
  1121. }
  1122.  
  1123. void RemoveVisualModels()
  1124. {
  1125.     for (int i = 0; i < MAX_TRIGGERS; i++)
  1126.     {
  1127.         if (IsValidEntRef(g_iVisualModels[i]))
  1128.         {
  1129.             int model = EntRefToEntIndex(g_iVisualModels[i]);
  1130.             if (model != INVALID_ENT_REFERENCE)
  1131.                 RemoveEntity(model);
  1132.             g_iVisualModels[i] = INVALID_ENT_REFERENCE;
  1133.         }
  1134.     }
  1135.     DebugPrint("RemoveVisualModels - Todos los modelos visuales eliminados");
  1136. }
  1137.  
  1138. // ========================================================================
  1139. // SISTEMA DE CONTADOR REGRESIVO
  1140. // ========================================================================
  1141.  
  1142. void StartMovementCountdown(int client, float duration, int groupNumber = 0, const char[] movementFile = "")
  1143. {
  1144.     // Cancelar timer anterior si existe
  1145.     if (g_hClientCountdownTimer[client] != null)
  1146.     {
  1147.         KillTimer(g_hClientCountdownTimer[client]);
  1148.         g_hClientCountdownTimer[client] = null;
  1149.     }
  1150.    
  1151.     if (duration <= 0) return;
  1152.    
  1153.     // Guardar información del movimiento
  1154.     g_fClientMovementDuration[client] = duration;
  1155.     g_fClientMovementEndTime[client] = GetGameTime() + duration;
  1156.     g_iClientMovementGroup[client] = groupNumber;
  1157.    
  1158.     if (strlen(movementFile) > 0)
  1159.     {
  1160.         strcopy(g_sClientMovementFileName[client], sizeof(g_sClientMovementFileName[]), movementFile);
  1161.     }
  1162.    
  1163.     DebugPrint("StartMovementCountdown - %N - Iniciando contador: %.1fs, Grupo: %d, Archivo: %s",
  1164.         client, duration, groupNumber, movementFile);
  1165.    
  1166.     // Crear DataPack con el userid del cliente
  1167.     DataPack countdownPack = new DataPack();
  1168.     countdownPack.WriteCell(GetClientUserId(client));
  1169.    
  1170.     // Iniciar contador regresivo
  1171.     g_hClientCountdownTimer[client] = CreateTimer(0.1, Timer_UpdateCountdown, countdownPack, TIMER_REPEAT);
  1172.    
  1173.     // Mostrar primer mensaje inmediatamente
  1174.     DisplayCountdownMessage(client, duration, true);
  1175. }
  1176.  
  1177. public Action Timer_StartCountdown(Handle timer, DataPack pack)
  1178. {
  1179.     pack.Reset();
  1180.     int userid = pack.ReadCell();
  1181.     float countdownDuration = pack.ReadFloat();
  1182.     float interval = pack.ReadFloat();
  1183.     delete pack;
  1184.    
  1185.     int client = GetClientOfUserId(userid);
  1186.     if (!IsValidSurvivor(client) || !g_bClientInMovement[client])
  1187.         return Plugin_Stop;
  1188.    
  1189.     // Iniciar timer recurrente para el contador
  1190.     DataPack countdownPack = new DataPack();
  1191.     countdownPack.WriteCell(userid);
  1192.     countdownPack.WriteFloat(countdownDuration);
  1193.     countdownPack.WriteFloat(interval);
  1194.    
  1195.     g_hClientCountdownTimer[client] = CreateTimer(interval, Timer_UpdateCountdown, countdownPack, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
  1196.    
  1197.     // Mostrar primer mensaje inmediatamente
  1198.     DisplayCountdownMessage(client, countdownDuration);
  1199.    
  1200.     return Plugin_Stop;
  1201. }
  1202.  
  1203. public Action Timer_UpdateCountdown(Handle timer, DataPack pack)
  1204. {
  1205.     pack.Reset();
  1206.     int userid = pack.ReadCell();
  1207.    
  1208.     int client = GetClientOfUserId(userid);
  1209.    
  1210.     // Verificar si el cliente es válido
  1211.     if (!IsValidSurvivor(client) || !g_bClientInMovement[client])
  1212.     {
  1213.         // Limpiar timer
  1214.         if (g_hClientCountdownTimer[client] != null)
  1215.         {
  1216.             KillTimer(g_hClientCountdownTimer[client]);
  1217.             g_hClientCountdownTimer[client] = null;
  1218.         }
  1219.        
  1220.         // Limpiar el DataPack
  1221.         delete pack;
  1222.         return Plugin_Stop;
  1223.     }
  1224.    
  1225.     // Calcular tiempo restante
  1226.     float currentTime = GetGameTime();
  1227.     float remainingTime = g_fClientMovementEndTime[client] - currentTime;
  1228.    
  1229.     // Si el tiempo se acabó
  1230.     if (remainingTime <= 0)
  1231.     {
  1232.         // Completado - no mostrar nada, el Timer_StopMovement mostrará el mensaje
  1233.         PrintHintText(client, " ");
  1234.        
  1235.         // Detener este timer
  1236.         if (g_hClientCountdownTimer[client] != null)
  1237.         {
  1238.             KillTimer(g_hClientCountdownTimer[client]);
  1239.             g_hClientCountdownTimer[client] = null;
  1240.         }
  1241.        
  1242.         // Limpiar el DataPack
  1243.         delete pack;
  1244.         return Plugin_Stop;
  1245.     }
  1246.    
  1247.     // Actualizar mensaje del contador
  1248.     DisplayCountdownMessage(client, remainingTime, false);
  1249.    
  1250.     // IMPORTANTE: NO borrar el DataPack aquí porque el timer se repite
  1251.     return Plugin_Continue;
  1252. }
  1253.  
  1254. void DisplayCountdownMessage(int client, float remaining, bool forceShow = false)
  1255. {
  1256.     // Solo mostrar si es un número entero o si es muy poco tiempo
  1257.     int seconds = RoundToFloor(remaining);
  1258.    
  1259.     // Control de spam: solo mostrar si cambió el segundo o si es forzado
  1260.     float currentTime = GetGameTime();
  1261.     bool shouldShow = forceShow ||
  1262.                      (seconds != g_iLastCountdownSeconds[client] && seconds <= 10) ||
  1263.                      (currentTime - g_fLastCountdownUpdate[client] > 1.0);
  1264.    
  1265.     if (!shouldShow) return;
  1266.    
  1267.     // Actualizar control
  1268.     g_iLastCountdownSeconds[client] = seconds;
  1269.     g_fLastCountdownUpdate[client] = currentTime;
  1270.    
  1271.     // Solo mostrar mensajes para 10 segundos o menos
  1272.     if (seconds > 10 && !forceShow) return;
  1273.    
  1274.     char message[64];
  1275.    
  1276.     if (remaining < 0.5)
  1277.     {
  1278.         Format(message, sizeof(message), "¡Completado!");
  1279.     }
  1280.     else if (seconds == 1 && remaining >= 0.5)
  1281.     {
  1282.         Format(message, sizeof(message), "Finaliza en: \x041\x01 segundo");
  1283.     }
  1284.     else
  1285.     {
  1286.         switch(seconds)
  1287.         {
  1288.             case 10: Format(message, sizeof(message), "Finaliza en: \x0410\x01 segundos");
  1289.             case 9: Format(message, sizeof(message), "Finaliza en: \x049\x01 segundos");
  1290.             case 8: Format(message, sizeof(message), "Finaliza en: \x048\x01 segundos");
  1291.             case 7: Format(message, sizeof(message), "Finaliza en: \x047\x01 segundos");
  1292.             case 6: Format(message, sizeof(message), "Finaliza en: \x046\x01 segundos");
  1293.             case 5: Format(message, sizeof(message), "Finaliza en: \x045\x01 segundos");
  1294.             case 4: Format(message, sizeof(message), "Finaliza en: \x044\x01 segundos");
  1295.             case 3: Format(message, sizeof(message), "Finaliza en: \x043\x01 segundos");
  1296.             case 2: Format(message, sizeof(message), "Finaliza en: \x042\x01 segundos");
  1297.             default:
  1298.             {
  1299.                 if (remaining < 1.0) {
  1300.                     Format(message, sizeof(message), "¡Completado!");
  1301.                 } else {
  1302.                     Format(message, sizeof(message), "Finaliza en: \x04%.1f\x01 segundos", remaining);
  1303.                 }
  1304.             }
  1305.         }
  1306.     }
  1307.    
  1308.     PrintHintText(client, message);
  1309. }
  1310.  
  1311. // ========================================================================
  1312. // CARGA AUTOMÁTICA DE ARCHIVOS DE MOVIMIENTO - USANDO EL INCLUDE
  1313. // ========================================================================
  1314.  
  1315. void LoadMovementFilesAutomatically()
  1316. {
  1317.     DebugPrint("===== INICIANDO CARGA AUTOMÁTICA DE MOVIMIENTOS =====");
  1318.    
  1319.     // Limpiar datos existentes
  1320.     g_hBoostPositions.Clear();
  1321.     g_hMovementFiles.Clear();
  1322.     g_hMovementTriggers.Clear();
  1323.     g_hFileExistence.Clear();
  1324.     g_hMovementDurations.Clear();
  1325.     RemoveVisualModels();
  1326.    
  1327.     // Limpiar estructuras de parsing
  1328.     g_hMovementFileInfos.Clear();
  1329.    
  1330.     // Limpiar mapas de archivos
  1331.     ClearGroupSystem();
  1332.    
  1333.     // Limpiar sistema de tracking
  1334.     ClearTrackingSystem();
  1335.    
  1336.     // Imprimir información de depuración
  1337.     DebugPrint("Mapa actual: %s", g_sCurrentMap);
  1338.     DebugPrint("Carpeta de mapa: %s", g_sMapFolder);
  1339.    
  1340.     char movementsDir[PLATFORM_MAX_PATH];
  1341.     BuildPath(Path_SM, movementsDir, sizeof(movementsDir), "plugins/disabled/movements/%s", g_sMapFolder);
  1342.    
  1343.     DebugPrint("Buscando directorio: %s", movementsDir);
  1344.    
  1345.     if (!DirExists(movementsDir))
  1346.     {
  1347.         DebugPrint("ERROR: No existe directorio: %s", movementsDir);
  1348.        
  1349.         // Intentar crear directorio alternativo
  1350.         char altDir[PLATFORM_MAX_PATH];
  1351.         BuildPath(Path_SM, altDir, sizeof(altDir), "plugins/disabled/movements");
  1352.        
  1353.         DebugPrint("Buscando directorio raíz: %s", altDir);
  1354.        
  1355.         if (DirExists(altDir))
  1356.         {
  1357.             // Listar subdirectorios disponibles
  1358.             DirectoryListing rootDir = OpenDirectory(altDir);
  1359.             if (rootDir != null)
  1360.             {
  1361.                 char subDir[64];
  1362.                 FileType type;
  1363.                 DebugPrint("Subdirectorios disponibles en movements:");
  1364.                
  1365.                 while (rootDir.GetNext(subDir, sizeof(subDir), type))
  1366.                 {
  1367.                     if (type == FileType_Directory)
  1368.                     {
  1369.                         DebugPrint("  - %s", subDir);
  1370.                     }
  1371.                 }
  1372.                 delete rootDir;
  1373.             }
  1374.         }
  1375.        
  1376.         // Crear el directorio si no existe
  1377.         if (CreateDirectory(movementsDir, 511))
  1378.         {
  1379.             DebugPrint("Directorio creado: %s", movementsDir);
  1380.         }
  1381.         else
  1382.         {
  1383.             DebugPrint("ERROR: No se pudo crear directorio");
  1384.         }
  1385.         return;
  1386.     }
  1387.    
  1388.     // USAR LA FUNCIÓN DEL INCLUDE PARA CARGAR ARCHIVOS
  1389.     int loadedCount = MA_LoadMovementFilesAutomatically(
  1390.         g_sMapFolder,
  1391.         g_hBoostPositions,
  1392.         g_hMovementFiles,
  1393.         g_hMovementTriggers,
  1394.         g_hFileExistence,
  1395.         g_hMovementDurations,
  1396.         g_hMovementFileInfos,
  1397.         g_hGroupsByNumber,
  1398.         g_hFilesByGroupAndType,
  1399.         g_hIndividualFiles
  1400.     );
  1401.    
  1402.     // Mostrar resumen detallado
  1403.     DebugPrint("===== RESUMEN DE CARGA =====");
  1404.     DebugPrint("Archivos cargados exitosamente: %d", loadedCount);
  1405.     DebugPrint("Posiciones totales: %d", g_hBoostPositions.Length);
  1406.     DebugPrint("Archivos en lista: %d", g_hMovementFiles.Length);
  1407.    
  1408.     if (loadedCount > 0)
  1409.     {
  1410.         // Mostrar estructura de archivos
  1411.         DebugMovementFilesStructure();
  1412.        
  1413.         // Mostrar estado de grupos
  1414.         PrintGroupStatus();
  1415.        
  1416.         // Crear triggers con filtro de grupos
  1417.         RecreateTriggers();
  1418.        
  1419.         CreateTimer(0.5, Timer_CheckTriggers);
  1420.        
  1421.         // Verificar que los triggers se crearon
  1422.         int triggerCount = 0;
  1423.         for (int i = 0; i < MAX_TRIGGERS; i++)
  1424.         {
  1425.             if (IsValidEntRef(g_iTriggers[i]))
  1426.                 triggerCount++;
  1427.         }
  1428.        
  1429.         DebugPrint("Triggers creados: %d", triggerCount);
  1430.        
  1431.         if (triggerCount == 0)
  1432.         {
  1433.             DebugPrint("ADVERTENCIA: No se crearon triggers!");
  1434.             DebugPrint("Verificar: 1) Modelos precargados 2) Posiciones válidas 3) Grupos activos");
  1435.         }
  1436.     }
  1437.     else
  1438.     {
  1439.         DebugPrint("ERROR: No se cargaron archivos de movimiento");
  1440.         DebugPrint("Verificar que los archivos .txt estén en: %s", movementsDir);
  1441.     }
  1442. }
  1443.  
  1444. // ========================================================================
  1445. // RECREAR TRIGGERS - FUNCIÓN COMPLETA Y CORREGIDA
  1446. // ========================================================================
  1447.  
  1448. void RecreateTriggers()
  1449. {
  1450.     DebugPrint("===== RECREANDO TRIGGERS =====");
  1451.     DebugPrint("Total de posiciones: %d", g_hBoostPositions.Length);
  1452.    
  1453.     if (g_hBoostPositions.Length == 0)
  1454.     {
  1455.         DebugPrint("ADVERTENCIA: No hay posiciones para crear triggers!");
  1456.         return;
  1457.     }
  1458.    
  1459.     // Limpiar sistema de tracking
  1460.     ClearTrackingSystem();
  1461.    
  1462.     // Destruir triggers existentes
  1463.     int destroyed = 0;
  1464.     for (int i = 0; i < MAX_TRIGGERS; i++)
  1465.     {
  1466.         if (IsValidEntRef(g_iTriggers[i]))
  1467.         {
  1468.             int entity = EntRefToEntIndex(g_iTriggers[i]);
  1469.             if (entity != INVALID_ENT_REFERENCE)
  1470.             {
  1471.                 AcceptEntityInput(entity, "Kill");
  1472.                 destroyed++;
  1473.             }
  1474.             g_iTriggers[i] = INVALID_ENT_REFERENCE;
  1475.         }
  1476.     }
  1477.    
  1478.     if (destroyed > 0)
  1479.     {
  1480.         DebugPrint("Triggers anteriores eliminados: %d", destroyed);
  1481.     }
  1482.    
  1483.     // Limpiar posiciones de grupos
  1484.     g_hGroupPositions.Clear();
  1485.    
  1486.     // Crear nuevos triggers respetando los grupos activos
  1487.     int created = 0;
  1488.     int skipped = 0;
  1489.    
  1490.     for (int i = 0; i < g_hBoostPositions.Length; i++)
  1491.     {
  1492.         int data[8];
  1493.         g_hBoostPositions.GetArray(i, data, sizeof(data));
  1494.        
  1495.         char file[128];
  1496.         g_hMovementFiles.GetString(data[6], file, sizeof(file));
  1497.        
  1498.         // Verificar que el archivo no esté vacío
  1499.         if (strlen(file) == 0)
  1500.         {
  1501.             DebugPrint("ADVERTENCIA: Archivo vacío en posición %d", i);
  1502.             skipped++;
  1503.             continue;
  1504.         }
  1505.        
  1506.         // Obtener tipo de movimiento USANDO EL INCLUDE
  1507.         MovementType movType = MA_GetMovementType(file, g_hMovementFileInfos);
  1508.        
  1509.         // Verificar si el grupo está activo
  1510.         if (!IsGroupEnabledByType(movType))
  1511.         {
  1512.             skipped++;
  1513.             continue;
  1514.         }
  1515.        
  1516.         float boostPos[3];
  1517.         boostPos[0] = view_as<float>(data[0]);
  1518.         boostPos[1] = view_as<float>(data[1]);
  1519.         boostPos[2] = view_as<float>(data[2]);
  1520.        
  1521.         DebugPrint("Creando trigger %d en (%.1f, %.1f, %.1f) para archivo: %s",
  1522.             i, boostPos[0], boostPos[1], boostPos[2], file);
  1523.        
  1524.         // Registrar información del trigger
  1525.         g_iTriggerGroup[i] = -1;
  1526.         g_iTriggerFileIndex[i] = data[6];
  1527.         g_fLastActivation[i] = 0.0;
  1528.         g_bTriggerEnabled[i] = true;
  1529.         g_fNextActivationTime[i] = 0.0;
  1530.        
  1531.         // Obtener información del archivo para determinar grupo USANDO EL INCLUDE
  1532.         MovementFileInfo fileInfo;
  1533.         if (MA_GetMovementFileInfo(file, g_hMovementFileInfos, fileInfo))
  1534.         {
  1535.             g_iTriggerGroup[i] = fileInfo.iNumber;
  1536.             strcopy(g_sTriggerType[i], sizeof(g_sTriggerType[]), fileInfo.sTypeString);
  1537.             strcopy(g_sTriggerFile[i], sizeof(g_sTriggerFile[]), file);
  1538.             g_iTriggerCapacity[i] = MA_GetBoosterCapacity(fileInfo.sTypeString);
  1539.            
  1540.             DebugPrint("Trigger %d -> Grupo: %d, Tipo: %s, Capacidad: %d",
  1541.                 i, fileInfo.iNumber, fileInfo.sTypeString, g_iTriggerCapacity[i]);
  1542.            
  1543.             // Si es parte de un grupo, agregar al sistema de tracking
  1544.             if (fileInfo.iNumber > 0)
  1545.             {
  1546.                 char groupKey[16];
  1547.                 IntToString(fileInfo.iNumber, groupKey, sizeof(groupKey));
  1548.                
  1549.                 // Agregar trigger al grupo
  1550.                 ArrayList groupTriggers = GetGroupTriggers(fileInfo.iNumber);
  1551.                 groupTriggers.Push(i);
  1552.                
  1553.                 // Agregar tipo al grupo si no existe
  1554.                 ArrayList groupTypes = GetGroupTypes(fileInfo.iNumber);
  1555.                 bool typeExists = false;
  1556.                 for (int j = 0; j < groupTypes.Length; j++)
  1557.                 {
  1558.                     char existingType[16];
  1559.                     groupTypes.GetString(j, existingType, sizeof(existingType));
  1560.                     if (StrEqual(existingType, fileInfo.sTypeString))
  1561.                     {
  1562.                         typeExists = true;
  1563.                         break;
  1564.                     }
  1565.                 }
  1566.                
  1567.                 if (!typeExists)
  1568.                 {
  1569.                     groupTypes.PushString(fileInfo.sTypeString);
  1570.                 }
  1571.             }
  1572.         }
  1573.         else
  1574.         {
  1575.             DebugPrint("ADVERTENCIA: No se pudo obtener info del archivo %s", file);
  1576.         }
  1577.        
  1578.         float vMaxs[3] = {2.5, 2.5, 5.0};
  1579.         float vMins[3] = {-2.5, -2.5, 0.0};
  1580.        
  1581.         if (CreateMovementTrigger(i, boostPos, vMaxs, vMins, file, data[7], true))
  1582.         {
  1583.             created++;
  1584.         }
  1585.         else
  1586.         {
  1587.             DebugPrint("ERROR: No se pudo crear trigger para posición %d", i);
  1588.             skipped++;
  1589.         }
  1590.     }
  1591.    
  1592.     DebugPrint("===== RESUMEN DE TRIGGERS =====");
  1593.     DebugPrint("Creados: %d, Omitidos: %d, Total: %d", created, skipped, g_hBoostPositions.Length);
  1594.    
  1595.     if (created == 0)
  1596.     {
  1597.         DebugPrint("ERROR CRÍTICO: No se creó ningún trigger!");
  1598.         DebugPrint("Posibles causas:");
  1599.         DebugPrint("1. Todos los grupos están desactivados");
  1600.         DebugPrint("2. Las posiciones son inválidas");
  1601.         DebugPrint("3. Error al crear entidades");
  1602.     }
  1603. }
  1604.  
  1605. // ========================================================================
  1606. // CREAR TRIGGER DE MOVIMIENTO - FUNCIÓN CRÍTICA CORREGIDA
  1607. // ========================================================================
  1608.  
  1609. bool CreateMovementTrigger(int index, float vPos[3], float vMaxs[3], float vMins[3], const char[] movementFile, int triggerType, bool autoload)
  1610. {
  1611.     // Verificar posición válida
  1612.     if (vPos[0] == 0.0 && vPos[1] == 0.0 && vPos[2] == 0.0)
  1613.     {
  1614.         DebugPrint("ERROR: Posición inválida (0,0,0) para trigger %d", index);
  1615.         return false;
  1616.     }
  1617.    
  1618.     // Verificar si el sistema de grupos está activo
  1619.     if (g_hCvarGroupsEnable.BoolValue)
  1620.     {
  1621.         // Obtener tipo de movimiento USANDO EL INCLUDE
  1622.         MovementType movType = MA_GetMovementType(movementFile, g_hMovementFileInfos);
  1623.        
  1624.         // Verificar si el grupo específico está activo
  1625.         if (!IsGroupEnabledByType(movType))
  1626.         {
  1627.             DebugPrint("Grupo no activo para archivo: %s", movementFile);
  1628.             return false;
  1629.         }
  1630.     }
  1631.    
  1632.     int trigger = CreateEntityByName("trigger_multiple");
  1633.     if (trigger == -1 || !IsValidEntity(trigger))
  1634.     {
  1635.         DebugPrint("ERROR: No se pudo crear entidad trigger_multiple");
  1636.         return false;
  1637.     }
  1638.  
  1639.     DebugPrint("Entidad creada con índice: %d", trigger);
  1640.    
  1641.     DispatchKeyValue(trigger, "StartDisabled", "0");  // Siempre habilitado al inicio
  1642.     DispatchKeyValue(trigger, "spawnflags", "1");
  1643.    
  1644.     // MODELO CORRECTO: Usar el modelo de cono naranja
  1645.     char model[] = "models/props_fortifications/orange_cone001_reference.mdl";
  1646.    
  1647.     // Verificar que el modelo esté precargado
  1648.     if (!IsModelPrecached(model))
  1649.     {
  1650.         PrecacheModel(model);
  1651.         DebugPrint("Modelo precargado: %s", model);
  1652.     }
  1653.    
  1654.     SetEntityModel(trigger, model);
  1655.    
  1656.     float adjustedPos[3];
  1657.     adjustedPos[0] = vPos[0];
  1658.     adjustedPos[1] = vPos[1];
  1659.     adjustedPos[2] = vPos[2] + 16.0;
  1660.    
  1661.     TeleportEntity(trigger, adjustedPos, NULL_VECTOR, NULL_VECTOR);
  1662.     DispatchSpawn(trigger);
  1663.    
  1664.     SetEntPropVector(trigger, Prop_Send, "m_vecMaxs", vMaxs);
  1665.     SetEntPropVector(trigger, Prop_Send, "m_vecMins", vMins);
  1666.     SetEntProp(trigger, Prop_Send, "m_nSolidType", 2);
  1667.  
  1668.     SetEntityRenderMode(trigger, RENDER_NORMAL);
  1669.     SetEntityRenderColor(trigger, 255, 255, 255, 50);
  1670.  
  1671.     char targetname[128];
  1672.     Format(targetname, sizeof(targetname), "movement_trigger_%s_%d", movementFile, triggerType);
  1673.     DispatchKeyValue(trigger, "targetname", targetname);
  1674.    
  1675.     DebugPrint("Trigger configurado: %s en (%.1f, %.1f, %.1f)",
  1676.         targetname, adjustedPos[0], adjustedPos[1], adjustedPos[2]);
  1677.  
  1678.     if (autoload)
  1679.     {
  1680.         AcceptEntityInput(trigger, "Enable");
  1681.         DebugPrint("Trigger habilitado: %d", trigger);
  1682.     }
  1683.  
  1684.     HookSingleEntityOutput(trigger, "OnStartTouch", OnStartTouch);
  1685.     HookSingleEntityOutput(trigger, "OnEndTouch", OnEndTouch);
  1686.    
  1687.     g_iTriggers[index] = EntIndexToEntRef(trigger);
  1688.    
  1689.     return true;
  1690. }
  1691.  
  1692. // ========================================================================
  1693. // FUNCIONES DE ACTIVACIÓN RÁPIDA
  1694. // ========================================================================
  1695.  
  1696. void TriggerMovement(int client, int triggerIndex)
  1697. {
  1698.     DebugPrint("TriggerMovement - Activando trigger %d para %N", triggerIndex, client);
  1699.    
  1700.     // PASO 1: Verificar condiciones individuales INMEDIATAMENTE
  1701.     bool conditionsMet = CheckPlayerConditions(client, triggerIndex);
  1702.    
  1703.     DebugPrint("TriggerMovement - Condiciones cumplidas: %d", conditionsMet);
  1704.    
  1705.     if (!conditionsMet)
  1706.     {
  1707.         char type[16];
  1708.         strcopy(type, sizeof(type), g_sTriggerType[triggerIndex]);
  1709.         DebugPrint("TriggerMovement - Tipo de trigger: %s", type);
  1710.        
  1711.         // Solo mostrar mensaje si NO es runner puro
  1712.         if (!StrEqual(type, "run"))
  1713.         {
  1714.             MovementType movType = GetMovementTypeFromString(type);
  1715.            
  1716.             if (movType == MOVEMENT_UNKNOWN)
  1717.             {
  1718.                 DebugPrint("TriggerMovement - Tipo de movimiento desconocido");
  1719.                 PrintToChat(client, "[\x04Trigger\x01] Error: Tipo de movimiento desconocido: %s", type);
  1720.             }
  1721.             else
  1722.             {
  1723.                 char weaponName[32];
  1724.                 GetWeaponNameForType(movType, weaponName, sizeof(weaponName));
  1725.                
  1726.                 // Verificar si tiene el arma pero no está lista
  1727.                 if (HasRequiredWeapon(client, type))
  1728.                 {
  1729.                     DebugPrint("TriggerMovement - %N tiene el arma %s pero NO está lista", client, weaponName);
  1730.                     PrintToChat(client, "[\x04Trigger\x01] Tu %s no está lista. Espera a que termine la animación.", weaponName);
  1731.                 }
  1732.                 else
  1733.                 {
  1734.                     DebugPrint("TriggerMovement - %N NO tiene el arma %s", client, weaponName);
  1735.                     PrintToChat(client, "[\x04Trigger\x01] Necesitas %s para este movimiento", weaponName);
  1736.                 }
  1737.             }
  1738.         }
  1739.        
  1740.         // Marcar como NO listo - el timer cíclico seguirá verificando
  1741.         MarkPlayerReady(client, triggerIndex, false);
  1742.         return;
  1743.     }
  1744.    
  1745.     // PASO 2: Marcar jugador como listo (CONDICIONES CUMPLIDAS)
  1746.     MarkPlayerReady(client, triggerIndex, true);
  1747.     DebugPrint("TriggerMovement - %N marcado como LISTO para trigger %d", client, triggerIndex);
  1748.    
  1749.     // PASO 3: Verificar si es parte de un grupo
  1750.     int groupNumber = g_iTriggerGroup[triggerIndex];
  1751.    
  1752.     if (groupNumber > 0)
  1753.     {
  1754.         DebugPrint("TriggerMovement - Trigger pertenece al grupo %d", groupNumber);
  1755.        
  1756.         // NUEVA VALIDACIÓN: Verificar si el jugador ya está en activación de este grupo
  1757.         if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  1758.         {
  1759.             DebugPrint("TriggerMovement - %N ya está en activación del grupo %d - ignorando", client, groupNumber);
  1760.             PrintToChat(client, "[\x04MA Error\x01] Ya estás participando en este grupo. No puedes activar múltiples roles.");
  1761.             MarkPlayerReady(client, triggerIndex, false);
  1762.             return;
  1763.         }
  1764.        
  1765.         // Es parte de un grupo - determinar si es Dual o Grupo
  1766.         ArrayList groupTypes = GetGroupTypes(groupNumber);
  1767.         bool isDual = (groupTypes != null && groupTypes.Length == 2);
  1768.         char boosterType[16] = "";
  1769.        
  1770.         if (isDual)
  1771.         {
  1772.             // Obtener tipo de booster para Dual
  1773.             for (int i = 0; i < groupTypes.Length; i++)
  1774.             {
  1775.                 char typeGroup[16];
  1776.                 groupTypes.GetString(i, typeGroup, sizeof(typeGroup));
  1777.                 if (StrEqual(typeGroup, "pm") || StrEqual(typeGroup, "bile") || StrEqual(typeGroup, "gl"))
  1778.                 {
  1779.                     if (StrEqual(typeGroup, "pm")) boosterType = "PM";
  1780.                     else if (StrEqual(typeGroup, "bile")) boosterType = "Bile";
  1781.                     else if (StrEqual(typeGroup, "gl")) boosterType = "GL";
  1782.                     break;
  1783.                 }
  1784.             }
  1785.         }
  1786.        
  1787.         // Mostrar mensaje SOLO UNA VEZ
  1788.         if (!g_bWaitMessageShown[client][groupNumber])
  1789.         {
  1790.             if (isDual && strlen(boosterType) > 0)
  1791.             {
  1792.                 PrintToChat(client, "[\x04MA Dual-%s (%d)\x01] Listo. Esperando otros participantes...",
  1793.                     boosterType, groupNumber);
  1794.             }
  1795.             else
  1796.             {
  1797.                 PrintToChat(client, "[\x04MA Grupo (%d)\x01] Listo. Esperando otros participantes...", groupNumber);
  1798.             }
  1799.            
  1800.             g_bWaitMessageShown[client][groupNumber] = true;
  1801.         }
  1802.        
  1803.         // Verificar si el grupo está completo INMEDIATAMENTE
  1804.         AttemptGroupActivation(groupNumber, triggerIndex, client);
  1805.     }
  1806.     else
  1807.     {
  1808.         DebugPrint("TriggerMovement - Trigger NO es parte de grupo - ejecutando SOLO");
  1809.        
  1810.         // NO es parte de un grupo - ejecutar individualmente INMEDIATAMENTE
  1811.         ExecuteSoloMovementFromTrigger(client, triggerIndex);
  1812.     }
  1813. }
  1814.  
  1815. void ExecuteSoloMovementFromTrigger(int client, int triggerIndex)
  1816. {
  1817.     DebugPrint("ExecuteSoloMovementFromTrigger - Ejecutando movimiento SOLO para %N desde trigger %d",
  1818.         client, triggerIndex);
  1819.    
  1820.     // Obtener archivo de movimiento del trigger
  1821.     char movementFile[128];
  1822.     GetMovementFileForTrigger(triggerIndex, movementFile, sizeof(movementFile));
  1823.    
  1824.     DebugPrint("ExecuteSoloMovementFromTrigger - Archivo: %s", movementFile);
  1825.    
  1826.     // Ejecutar movimiento individual
  1827.     ExecutePlayerMovementImmediate(client, movementFile, MSG_INDIVIDUAL);
  1828.    
  1829.     // Aplicar refire_time después de la activación exitosa
  1830.     ApplyRefireTime(triggerIndex);
  1831. }
  1832.  
  1833. // ========================================================================
  1834. // NUEVA FUNCIÓN: EJECUCIÓN INMEDIATA DE MOVIMIENTO
  1835. // ========================================================================
  1836.  
  1837. void ExecutePlayerMovementImmediate(int client, const char[] movementFile, MovementMessageType msgType = MSG_INDIVIDUAL, int groupNumber = 0)
  1838. {
  1839.     g_bManualStop[client] = false;
  1840.     DebugPrint("ExecutePlayerMovementImmediate - Intentando ejecutar movimiento %s para %N (msgType: %d, group: %d)",
  1841.         movementFile, client, msgType, groupNumber);
  1842.    
  1843.     if (!IsValidSurvivor(client) || g_bClientInMovement[client])
  1844.     {
  1845.         DebugPrint("ExecutePlayerMovementImmediate - Cliente no válido o ya en movimiento");
  1846.         return;
  1847.     }
  1848.    
  1849.     char filePath[PLATFORM_MAX_PATH];
  1850.     BuildPath(Path_SM, filePath, sizeof(filePath), "plugins/disabled/movements/%s.txt", movementFile);
  1851.    
  1852.     if (!FileExists(filePath))
  1853.     {
  1854.         DebugPrint("ExecutePlayerMovementImmediate - Archivo NO existe");
  1855.         PrintToChat(client, "[\x04Error\x01] Archivo no encontrado: %s", movementFile);
  1856.         return;
  1857.     }
  1858.    
  1859.     DebugPrint("ExecutePlayerMovementImmediate - Archivo EXISTE");
  1860.    
  1861.     // Buscar información del archivo
  1862.     MovementFileInfo info;
  1863.     bool found = MA_GetMovementFileInfo(movementFile, g_hMovementFileInfos, info);
  1864.    
  1865.     if (!found)
  1866.     {
  1867.         DebugPrint("ExecutePlayerMovementImmediate - No se pudo obtener info del archivo");
  1868.         PrintToChat(client, "[\x04Error\x01] No se pudo obtener información del archivo");
  1869.         return;
  1870.     }
  1871.    
  1872.     g_bClientInMovement[client] = true;
  1873.    
  1874.     // Guardar información del movimiento
  1875.     strcopy(g_sClientMovementFile[client], sizeof(g_sClientMovementFile[]), movementFile);
  1876.     strcopy(g_sClientMovementFileName[client], sizeof(g_sClientMovementFileName[]), movementFile);
  1877.     g_iClientMovementGroup[client] = groupNumber;
  1878.    
  1879.     if (groupNumber > 0)
  1880.     {
  1881.         DebugPrint("ExecutePlayerMovementImmediate - %N iniciando movimiento del grupo %d", client, groupNumber);
  1882.         MarkPlayerInGroupActivation(groupNumber, client, true);
  1883.     }
  1884.    
  1885.     // PASO 1: Forzar el archivo de movimiento
  1886.     if (g_hCvarForceFile != null)
  1887.     {
  1888.         g_hCvarForceFile.SetString(movementFile);
  1889.         DebugPrint("ExecutePlayerMovementImmediate - st_mr_force_file: %s", movementFile);
  1890.     }
  1891.    
  1892.     // PASO 2: Inmediatamente después, reproducir para el cliente específico
  1893.     if (g_hCvarPlay != null)
  1894.     {
  1895.         char clientIndex[12];
  1896.         IntToString(client, clientIndex, sizeof(clientIndex));
  1897.         g_hCvarPlay.SetString(clientIndex);
  1898.         DebugPrint("ExecutePlayerMovementImmediate - st_mr_play: %s", clientIndex);
  1899.     }
  1900.    
  1901.     // Obtener duración REAL del archivo específico
  1902.     float duration = 10.0;
  1903.     if (g_hMovementDurations != null)
  1904.     {
  1905.         g_hMovementDurations.GetValue(movementFile, duration);
  1906.     }
  1907.    
  1908.     // Guardar duración
  1909.     g_fClientMovementDuration[client] = duration;
  1910.    
  1911.     // Mostrar mensajes según el tipo
  1912.     if (msgType == MSG_INDIVIDUAL)
  1913.     {
  1914.         char author[32];
  1915.         strcopy(author, sizeof(author), info.sAuthor);
  1916.         if (strlen(author) == 0) strcopy(author, sizeof(author), "Desconocido");
  1917.        
  1918.         // Preparar nombre de visualización
  1919.         char displayName[64];
  1920.         MA_GetDisplayFileName(movementFile, g_hMovementFileInfos, displayName, sizeof(displayName));
  1921.        
  1922.         // Obtener nombre base sin autor
  1923.         char baseName[64];
  1924.         GetBaseFileNameWithoutAuthor(displayName, baseName, sizeof(baseName));
  1925.        
  1926.         char typeName[16];
  1927.         if (info.movType == MOVEMENT_RUN) typeName = "Solo";
  1928.         else if (info.movType == MOVEMENT_PM) typeName = "PM";
  1929.         else if (info.movType == MOVEMENT_BILE) typeName = "Bile";
  1930.         else if (info.movType == MOVEMENT_GL) typeName = "GL";
  1931.         else if (info.movType == MOVEMENT_RUNPM) typeName = "RunPM";
  1932.         else if (info.movType == MOVEMENT_RUNBILE) typeName = "RunBile";
  1933.         else if (info.movType == MOVEMENT_RUNGL) typeName = "RunGL";
  1934.         else typeName = "Solo";
  1935.        
  1936.         // Mostrar autor si existe
  1937.         if (!StrEqual(author, "Desconocido"))
  1938.         {
  1939.             PrintToChatAll("[\x04Movimiento\x01] Creado por '\x04%s\x01'", author);
  1940.         }
  1941.        
  1942.         PrintToChatAll("[\x04M. %s\x01] %s(%.1fs)", typeName, baseName, duration);
  1943.         PrintToChat(client, "[\x04M. %s\x01] %N", typeName, client);
  1944.     }
  1945.    
  1946.     if (msgType == MSG_GROUP_MULTI)
  1947.     {
  1948.         // Para grupos múltiples, NO mostrar mensajes individuales aquí
  1949.         DebugPrint("ExecutePlayerMovementImmediate - Movimiento múltiple para %N en grupo %d", client, groupNumber);
  1950.     }
  1951.    
  1952.     DebugPrint("ExecutePlayerMovementImmediate - %N ejecutando movimiento: %s (%.1fs)", client, movementFile, duration);
  1953.    
  1954.     // Iniciar contador regresivo INDEPENDIENTE
  1955.     StartMovementCountdown(client, duration, groupNumber, movementFile);
  1956.    
  1957.     // Timer para detener automáticamente
  1958.     DataPack pack = new DataPack();
  1959.     pack.WriteCell(GetClientUserId(client));
  1960.     pack.WriteString(movementFile);
  1961.     pack.WriteCell(groupNumber);
  1962.    
  1963.     // Guardar el timer para poder cancelarlo si es necesario
  1964.     g_hClientMovementTimer[client] = CreateTimer(duration + 0.5, Timer_StopMovement, pack, TIMER_FLAG_NO_MAPCHANGE);
  1965.    
  1966.     DebugPrint("ExecutePlayerMovementImmediate - MOVIMIENTO INICIADO EXITOSAMENTE para %N", client);
  1967. }
  1968.  
  1969. public void GetMovementFileForTrigger(int triggerIndex, char[] buffer, int size)
  1970. {
  1971.     if (triggerIndex >= 0 && triggerIndex < g_hBoostPositions.Length)
  1972.     {
  1973.         int data[8];
  1974.         g_hBoostPositions.GetArray(triggerIndex, data, sizeof(data));
  1975.         g_hMovementFiles.GetString(data[6], buffer, size);
  1976.     }
  1977.     else
  1978.     {
  1979.         buffer[0] = '\0';
  1980.     }
  1981. }
  1982.  
  1983. bool CheckPlayerHasRequiredWeapon(int client, MovementType movType)
  1984. {
  1985.     if (!IsValidSurvivor(client))
  1986.         return false;
  1987.    
  1988.     MovementType baseType = GetBaseMovementType(movType);
  1989.    
  1990.     // Búsqueda rápida en todas las ranuras
  1991.     for (int slot = 0; slot <= 4; slot++)
  1992.     {
  1993.         int weapon = GetPlayerWeaponSlot(client, slot);
  1994.         if (weapon > 0 && IsValidEntity(weapon))
  1995.         {
  1996.             char classname[32];
  1997.             GetEntityClassname(weapon, classname, sizeof(classname));
  1998.            
  1999.             if (baseType == MOVEMENT_PM && (StrEqual(classname, "weapon_pipe_bomb") || StrEqual(classname, "weapon_molotov")))
  2000.                 return true;
  2001.             else if (baseType == MOVEMENT_BILE && StrEqual(classname, "weapon_vomitjar"))
  2002.                 return true;
  2003.             else if (baseType == MOVEMENT_GL && StrEqual(classname, "weapon_grenade_launcher"))
  2004.                 return true;
  2005.         }
  2006.     }
  2007.    
  2008.     // Para RUN y DUAL, siempre retorna true
  2009.     if (baseType == MOVEMENT_RUN || baseType == MOVEMENT_DUAL)
  2010.         return true;
  2011.    
  2012.     return false;
  2013. }
  2014.  
  2015. // ========================================================================
  2016. // FUNCIONES DEL SISTEMA DE GRUPOS
  2017. // ========================================================================
  2018.  
  2019. ArrayList GetGroupTriggers(int groupNumber)
  2020. {
  2021.     char groupKey[16];
  2022.     IntToString(groupNumber, groupKey, sizeof(groupKey));
  2023.    
  2024.     ArrayList groupTriggers;
  2025.     if (!g_hGroupTriggers.GetValue(groupKey, groupTriggers))
  2026.     {
  2027.         groupTriggers = new ArrayList();
  2028.         g_hGroupTriggers.SetValue(groupKey, groupTriggers);
  2029.     }
  2030.    
  2031.     return groupTriggers;
  2032. }
  2033.  
  2034. ArrayList GetGroupTypes(int groupNumber)
  2035. {
  2036.     char groupKey[16];
  2037.     IntToString(groupNumber, groupKey, sizeof(groupKey));
  2038.    
  2039.     ArrayList groupTypes;
  2040.     if (!g_hGroupTypes.GetValue(groupKey, groupTypes))
  2041.     {
  2042.         groupTypes = new ArrayList(ByteCountToCells(16));
  2043.         g_hGroupTypes.SetValue(groupKey, groupTypes);
  2044.     }
  2045.    
  2046.     return groupTypes;
  2047. }
  2048.  
  2049. // ========================================================================
  2050. // FUNCIONES DE VERIFICACIÓN DE CONDICIONES (MEJORADAS)
  2051. // ========================================================================
  2052.  
  2053. bool CheckPlayerConditions(int client, int triggerIndex)
  2054. {
  2055.     DebugPrint("CheckPlayerConditions - Verificando condiciones para el jugador %N en trigger %d", client, triggerIndex);
  2056.    
  2057.     // 1. Verificar si es superviviente válido
  2058.     if (!IsValidSurvivor(client))
  2059.     {
  2060.         DebugPrint("CheckPlayerConditions - %N no es survivor válido", client);
  2061.         return false;
  2062.     }
  2063.    
  2064.     // 2. Verificar si está vivo, no incapacitado, no colgado
  2065.     if (!IsPlayerAlive(client) || IsPlayerIncapacitated(client) || IsPlayerHanging(client))
  2066.     {
  2067.         DebugPrint("CheckPlayerConditions - %N no está en condiciones válidas (muerto/incapacitado/colgado)", client);
  2068.         PrintToChat(client, "[\x04Error\x01] Debes estar vivo, no incapacitado y no colgado");
  2069.         return false;
  2070.     }
  2071.    
  2072.     // 3. LÓGICA MEJORADA: Verificar salud con mensajes anti-spam
  2073.     int permanentHealth = GetClientHealth(client);
  2074.     int tempHealth = GetPlayerTempHealth(client);
  2075.     int totalHealth = permanentHealth + tempHealth;
  2076.    
  2077.     // DEBUG EXTRA: Mostrar valores exactos
  2078.     DebugPrint("CheckPlayerConditions - %N - Base: %d, Temporal: %d, Total: %d",
  2079.         client, permanentHealth, tempHealth, totalHealth);
  2080.    
  2081.     bool rule1 = (permanentHealth >= 40);
  2082.     bool rule2 = (!rule1 && totalHealth >= 45);
  2083.    
  2084.     DebugPrint("CheckPlayerConditions - %N - Regla 1 (Base>=40): %s, Regla 2 (Total>=45): %s",
  2085.         client, rule1 ? "SÍ" : "NO", rule2 ? "SÍ" : "NO");
  2086.    
  2087.     if (rule1)
  2088.     {
  2089.         DebugPrint("CheckPlayerConditions - %N tiene 40+ de salud base (%d) - VÁLIDO por Regla 1",
  2090.             client, permanentHealth);
  2091.     }
  2092.     else if (rule2)
  2093.     {
  2094.         DebugPrint("CheckPlayerConditions - %N tiene <40 base pero 45+ total (%d) - VÁLIDO por Regla 2",
  2095.             client, totalHealth);
  2096.     }
  2097.     else
  2098.     {
  2099.         DebugPrint("CheckPlayerConditions - %N NO cumple con las reglas de salud", client);
  2100.         DebugPrint("CheckPlayerConditions - Base: %d (necesita >=40) | Total: %d (necesita >=45 si base<40)",
  2101.             permanentHealth, totalHealth);
  2102.        
  2103.         // SISTEMA ANTI-SPAM: Solo mostrar mensaje cada 2 segundos
  2104.         static float fLastHealthMessage[MAXPLAYERS + 1];
  2105.         float fCurrentTime = GetGameTime();
  2106.        
  2107.         if (fCurrentTime - fLastHealthMessage[client] > 2.0) // Mensaje cada 2 segundos máximo
  2108.         {
  2109.             // Mostrar mensaje específico MEJORADO
  2110.             if (permanentHealth >= 30 && permanentHealth < 40)
  2111.             {
  2112.                 int neededTemp = 45 - permanentHealth;
  2113.                 PrintToChat(client, "[\x04Error\x01] Salud base: \x04%d\x01 (necesita 40+). \x04O añade %d de salud temporal\x01 para llegar a 45 total",
  2114.                     permanentHealth, neededTemp);
  2115.             }
  2116.             else
  2117.             {
  2118.                 // Mensaje MÁS CLARO con colores
  2119.                 if (tempHealth > 0)
  2120.                 {
  2121.                     PrintToChat(client, "[\x04Error\x01] Necesitas \x0440+ de salud base\x01 O \x0445+ de salud total\x01. Tienes: \x04%d base\x01 + \x04%d temporal\x01 = \x04%d total",
  2122.                         permanentHealth, tempHealth, totalHealth);
  2123.                 }
  2124.                 else
  2125.                 {
  2126.                     PrintToChat(client, "[\x04Error\x01] Necesitas \x0440+ de salud base\x01 O \x0445+ de salud total\x01. Tienes: \x04%d base\x01 (sin salud temporal)",
  2127.                         permanentHealth);
  2128.                 }
  2129.             }
  2130.            
  2131.             fLastHealthMessage[client] = fCurrentTime;
  2132.         }
  2133.        
  2134.         return false;
  2135.     }
  2136.    
  2137.     // 4. Verificar si el jugador ya está en movimiento
  2138.     if (g_bClientInMovement[client])
  2139.     {
  2140.         DebugPrint("CheckPlayerConditions - %N ya está en movimiento", client);
  2141.         return false;
  2142.     }
  2143.    
  2144.     // 5. Verificar si el trigger tiene tipo válido
  2145.     if (strlen(g_sTriggerType[triggerIndex]) == 0)
  2146.     {
  2147.         DebugPrint("CheckPlayerConditions - Trigger %d no tiene tipo válido", triggerIndex);
  2148.         return false;
  2149.     }
  2150.    
  2151.     char type[16];
  2152.     strcopy(type, sizeof(type), g_sTriggerType[triggerIndex]);
  2153.     DebugPrint("CheckPlayerConditions - Tipo de trigger: %s", type);
  2154.    
  2155.     // 6. Verificar condiciones específicas por tipo
  2156.     // RUN (puro): siempre válido
  2157.     if (StrEqual(type, "run"))
  2158.     {
  2159.         DebugPrint("CheckPlayerConditions - Trigger es de tipo RUN, condiciones cumplidas");
  2160.         return true;
  2161.     }
  2162.    
  2163.     // HÍBRIDOS (runpm, runbile, rungl): necesitan arma correspondiente
  2164.     if (StrContains(type, "runpm") != -1)
  2165.     {
  2166.         DebugPrint("CheckPlayerConditions - Trigger es de tipo RUNPM, verificando arma");
  2167.         return CheckWeaponReady(client, "pm");
  2168.     }
  2169.     if (StrContains(type, "runbile") != -1)
  2170.     {
  2171.         DebugPrint("CheckPlayerConditions - Trigger es de tipo RUNBILE, verificando arma");
  2172.         return CheckWeaponReady(client, "bile");
  2173.     }
  2174.     if (StrContains(type, "rungl") != -1)
  2175.     {
  2176.         DebugPrint("CheckPlayerConditions - Trigger es de tipo RUNGL, verificando arma");
  2177.         return CheckWeaponReady(client, "gl");
  2178.     }
  2179.    
  2180.     // BOOSTERS PUROS (pm, bile, gl): necesitan arma correspondiente
  2181.     if (StrEqual(type, "pm") || StrEqual(type, "bile") || StrEqual(type, "gl"))
  2182.     {
  2183.         DebugPrint("CheckPlayerConditions - Trigger es de tipo %s, verificando arma", type);
  2184.         return CheckWeaponReady(client, type);
  2185.     }
  2186.    
  2187.     DebugPrint("CheckPlayerConditions - Tipo de trigger desconocido: %s", type);
  2188.     return false;
  2189. }
  2190.  
  2191. // ========================================================================
  2192. // FUNCIÓN AUXILIAR PARA VERIFICAR SI EL JUGADOR CUMPLE LAS REGLAS DE SALUD
  2193. // ========================================================================
  2194.  
  2195. bool CheckPlayerHealthRules(int client, char[] errorMsg = "", int errorLen = 0)
  2196. {
  2197.     int permanentHealth = GetClientHealth(client);
  2198.     int tempHealth = GetPlayerTempHealth(client);
  2199.     int totalHealth = permanentHealth + tempHealth;
  2200.    
  2201.     // REGLAS MEJORADAS:
  2202.     // 1. Salud base ≥ 40 -> VÁLIDO
  2203.     // 2. Si salud base < 40, entonces salud total ≥ 45 -> VÁLIDO
  2204.     // 3. Si no cumple ninguna -> NO VÁLIDO
  2205.    
  2206.     bool isValid = (permanentHealth >= 40) || (totalHealth >= 45);
  2207.    
  2208.     if (!isValid && errorLen > 0)
  2209.     {
  2210.         // Mensajes MÁS CLAROS y ÚTILES
  2211.         if (permanentHealth >= 30 && permanentHealth < 40)
  2212.         {
  2213.             int neededTemp = 45 - permanentHealth;
  2214.             Format(errorMsg, errorLen, "Salud base: \x04%d\x01 (necesitas 40+). \x04O añade %d de salud temporal\x01 para llegar a 45 total",
  2215.                 permanentHealth, neededTemp);
  2216.         }
  2217.         else
  2218.         {
  2219.             // Mostrar información específica
  2220.             if (tempHealth > 0)
  2221.             {
  2222.                 Format(errorMsg, errorLen, "Necesitas \x0440+ de salud base\x01 O \x0445+ de salud total\x01. Tienes: \x04%d base\x01 + \x04%d temporal\x01 = \x04%d total",
  2223.                     permanentHealth, tempHealth, totalHealth);
  2224.             }
  2225.             else
  2226.             {
  2227.                 Format(errorMsg, errorLen, "Necesitas \x0440+ de salud base\x01 O \x0445+ de salud total\x01. Tienes: \x04%d base\x01 (sin salud temporal)",
  2228.                     permanentHealth);
  2229.             }
  2230.         }
  2231.     }
  2232.    
  2233.     // DEBUG opcional (solo si hay problemas)
  2234.     if (g_hCvarDebug.BoolValue && !isValid)
  2235.     {
  2236.         DebugPrint("CheckPlayerHealthRules - %N - Base: %d, Temporal: %d, Total: %d | Regla1(Base>=40): %s, Regla2(Total>=45): %s",
  2237.             client, permanentHealth, tempHealth, totalHealth,
  2238.             (permanentHealth >= 40) ? "SÍ" : "NO",
  2239.             (totalHealth >= 45) ? "SÍ" : "NO");
  2240.     }
  2241.    
  2242.     return isValid;
  2243. }
  2244.  
  2245. // ========================================================================
  2246. // FUNCIÓN MEJORADA DE GetPlayerTempHealth (PARA L4D2)
  2247. // ========================================================================
  2248.  
  2249. int GetPlayerTempHealth(int client)
  2250. {
  2251.     if (!IsValidSurvivor(client))
  2252.         return 0;
  2253.    
  2254.     float fGameTime = GetGameTime();
  2255.     float fHealthBuffer = GetEntPropFloat(client, Prop_Send, "m_healthBuffer");
  2256.     float fHealthBufferTime = GetEntPropFloat(client, Prop_Send, "m_healthBufferTime");
  2257.    
  2258.     // Si no hay buffer, no hay salud temporal
  2259.     if (fHealthBuffer <= 0.0)
  2260.         return 0;
  2261.    
  2262.     // Si el tiempo del buffer es inválido (0 o futuro), usar el buffer actual
  2263.     if (fHealthBufferTime <= 0.0 || fHealthBufferTime > fGameTime)
  2264.     {
  2265.         int tempHealth = RoundToFloor(fHealthBuffer);
  2266.         return (tempHealth > 100) ? 100 : tempHealth;
  2267.     }
  2268.    
  2269.     // Cálculo CORRECTO según Valve
  2270.     float fTempHealth = fHealthBuffer - (fGameTime - fHealthBufferTime);
  2271.    
  2272.     // La salud temporal nunca puede ser negativa
  2273.     if (fTempHealth < 0.0)
  2274.         fTempHealth = 0.0;
  2275.    
  2276.     // Redondear hacia abajo para ser conservador (como hace el juego)
  2277.     int tempHealth = RoundToFloor(fTempHealth);
  2278.    
  2279.     // Límite máximo de 100 (máximo en L4D2)
  2280.     if (tempHealth > 100)
  2281.         tempHealth = 100;
  2282.    
  2283.     return tempHealth;
  2284. }
  2285.  
  2286. bool IsPlayerIncapacitated(int client)
  2287. {
  2288.     return view_as<bool>(GetEntProp(client, Prop_Send, "m_isIncapacitated"));
  2289. }
  2290.  
  2291. bool IsPlayerHanging(int client)
  2292. {
  2293.     return view_as<bool>(GetEntProp(client, Prop_Send, "m_isHangingFromLedge") ||
  2294.                          GetEntProp(client, Prop_Send, "m_isFallingFromLedge"));
  2295. }
  2296.  
  2297. bool CheckWeaponReady(int client, const char[] weaponType)
  2298. {
  2299.     DebugPrint("CheckWeaponReady - Verificando arma %s para %N", weaponType, client);
  2300.    
  2301.     // Si el arma está en la mano y lista, retornar true
  2302.     if (HasRequiredWeaponInHand(client, weaponType) && IsWeaponReadyForUse(client, GetMovementTypeFromString(weaponType)))
  2303.     {
  2304.         DebugPrint("CheckWeaponReady - %N tiene el arma lista", client);
  2305.         return true;
  2306.     }
  2307.    
  2308.     // Si tiene el arma en inventario, cambiarla
  2309.     if (HasRequiredWeapon(client, weaponType))
  2310.     {
  2311.         if (g_hCvarAutoSwitch.BoolValue)
  2312.         {
  2313.             DebugPrint("CheckWeaponReady - %N tiene arma en inventario, cambiando...", client);
  2314.             MovementType movType = GetMovementTypeFromString(weaponType);
  2315.             SwitchToRequiredWeapon(client, movType);
  2316.            
  2317.             int triggerIndex = -1;
  2318.             for (int i = 0; i < MAX_TRIGGERS; i++)
  2319.             {
  2320.                 if (g_bPlayerInTrigger[client][i])
  2321.                 {
  2322.                     triggerIndex = i;
  2323.                     break;
  2324.                 }
  2325.             }
  2326.            
  2327.             if (triggerIndex != -1)
  2328.             {
  2329.                 g_bWaitingForWeapon[client][triggerIndex] = true;
  2330.                 g_iPlayerRetryCount[client][triggerIndex] = 0;
  2331.                
  2332.                 DataPack pack = new DataPack();
  2333.                 pack.WriteCell(GetClientUserId(client));
  2334.                 pack.WriteString(weaponType);
  2335.                 pack.WriteCell(triggerIndex);
  2336.                 g_hRetryTimer[client][triggerIndex] = CreateTimer(2.0, Timer_RecheckWeapon, pack);
  2337.                
  2338.                 DebugPrint("CheckWeaponReady - Timer de recheck programado para %N", client);
  2339.                 return false;
  2340.             }
  2341.         }
  2342.         else
  2343.         {
  2344.             DebugPrint("CheckWeaponReady - Auto-switch deshabilitado");
  2345.             PrintToChat(client, "[\x04Error\x01] Auto-cambio desactivado. Activa 'st_mr_auto_switch 1'");
  2346.         }
  2347.         return false;
  2348.     }
  2349.    
  2350.     // Si no tiene el arma, esperar a que la recoja (timer)
  2351.     if (g_hCvarAutoSwitch.BoolValue)
  2352.     {
  2353.         DebugPrint("CheckWeaponReady - %N no tiene %s, esperando a que la recoja...", client, weaponType);
  2354.         int triggerIndex = -1;
  2355.         for (int i = 0; i < MAX_TRIGGERS; i++)
  2356.         {
  2357.             if (g_bPlayerInTrigger[client][i])
  2358.             {
  2359.                 triggerIndex = i;
  2360.                 break;
  2361.             }
  2362.         }
  2363.        
  2364.         if (triggerIndex != -1)
  2365.         {
  2366.             g_bWaitingForWeapon[client][triggerIndex] = true;
  2367.             g_iPlayerRetryCount[client][triggerIndex] = 0;
  2368.            
  2369.             DataPack pack = new DataPack();
  2370.             pack.WriteCell(GetClientUserId(client));
  2371.             pack.WriteString(weaponType);
  2372.             pack.WriteCell(triggerIndex);
  2373.             g_hRetryTimer[client][triggerIndex] = CreateTimer(2.0, Timer_RecheckWeapon, pack);
  2374.             return false;
  2375.         }
  2376.     }
  2377.    
  2378.     DebugPrint("CheckWeaponReady - %N NO tiene el arma %s", client, weaponType);
  2379.     return false;
  2380. }
  2381.  
  2382. int FindTriggerForPlayer(int client)
  2383. {
  2384.     // Buscar el primer trigger donde el jugador esté presente
  2385.     for (int i = 0; i < MAX_TRIGGERS; i++)
  2386.     {
  2387.         if (g_bPlayerInTrigger[client][i])
  2388.         {
  2389.             return i;
  2390.         }
  2391.     }
  2392.     return -1;
  2393. }
  2394.  
  2395. // ========================================================================
  2396. // ACTUALIZACIÓN EN Timer_RecheckWeapon PARA USAR LA LÓGICA CORREGIDA
  2397. // ========================================================================
  2398.  
  2399. public Action Timer_RecheckWeapon(Handle timer, DataPack pack)
  2400. {
  2401.     pack.Reset();
  2402.     int userid = pack.ReadCell();
  2403.     char weaponType[16];
  2404.     pack.ReadString(weaponType, sizeof(weaponType));
  2405.     int triggerIndex = pack.ReadCell();
  2406.     delete pack;
  2407.    
  2408.     int client = GetClientOfUserId(userid);
  2409.     if (!IsValidSurvivor(client))
  2410.         return Plugin_Stop;
  2411.    
  2412.     g_hRetryTimer[client][triggerIndex] = null;
  2413.    
  2414.     // Si el jugador ya no está en el trigger, no hacer nada (no limpiar)
  2415.     if (!g_bPlayerInTrigger[client][triggerIndex])
  2416.     {
  2417.         DebugPrint("Timer_RecheckWeapon - %N ya no está en el trigger, no se limpia", client);
  2418.         g_bWaitingForWeapon[client][triggerIndex] = false;
  2419.         return Plugin_Stop;
  2420.     }
  2421.    
  2422.     g_iPlayerRetryCount[client][triggerIndex]++;
  2423.     DebugPrint("Timer_RecheckWeapon - %N - Intento %d/%d - Arma: %s",
  2424.         client, g_iPlayerRetryCount[client][triggerIndex], MAX_RETRY_ATTEMPTS, weaponType);
  2425.    
  2426.     MovementType movType = GetMovementTypeFromString(weaponType);
  2427.     bool hasInHand = HasRequiredWeaponInHand(client, weaponType);
  2428.     bool isReady = IsWeaponReadyForUse(client, movType);
  2429.    
  2430.     DebugPrint("Timer_RecheckWeapon - %N - En mano: %s, Lista: %s",
  2431.         client, hasInHand ? "SÍ" : "NO", isReady ? "SÍ" : "NO");
  2432.    
  2433.     // CASO 1: Tiene el arma en mano y lista -> ACTIVAR
  2434.     if (hasInHand && isReady)
  2435.     {
  2436.         DebugPrint("Timer_RecheckWeapon - %N ahora tiene el arma lista", client);
  2437.         g_bWaitingForWeapon[client][triggerIndex] = false;
  2438.         g_iPlayerRetryCount[client][triggerIndex] = 0;
  2439.         g_bPlayerReady[client][triggerIndex] = true;
  2440.        
  2441.         int groupNumber = g_iTriggerGroup[triggerIndex];
  2442.         if (groupNumber > 0)
  2443.         {
  2444.             PrintToChat(client, "[\x04MA\x01] Arma lista, activando grupo %d...", groupNumber);
  2445.             AttemptGroupActivation(groupNumber, triggerIndex, client);
  2446.         }
  2447.         else
  2448.         {
  2449.             ExecuteSoloMovementFromTrigger(client, triggerIndex);
  2450.         }
  2451.         return Plugin_Stop;
  2452.     }
  2453.    
  2454.     // CASO 2: Tiene el arma en mano pero NO lista -> esperar
  2455.     if (hasInHand && !isReady)
  2456.     {
  2457.         DebugPrint("Timer_RecheckWeapon - %N tiene arma en mano pero no lista, esperando...", client);
  2458.         if (g_iPlayerRetryCount[client][triggerIndex] < MAX_RETRY_ATTEMPTS)
  2459.         {
  2460.             DataPack retryPack = new DataPack();
  2461.             retryPack.WriteCell(userid);
  2462.             retryPack.WriteString(weaponType);
  2463.             retryPack.WriteCell(triggerIndex);
  2464.             g_hRetryTimer[client][triggerIndex] = CreateTimer(2.0, Timer_RecheckWeapon, retryPack);
  2465.             return Plugin_Stop;
  2466.         }
  2467.         else
  2468.         {
  2469.             PrintToChat(client, "[\x04Error\x01] Tiempo de espera agotado. Vuelve a intentarlo.");
  2470.             g_bWaitingForWeapon[client][triggerIndex] = false;
  2471.             g_iPlayerRetryCount[client][triggerIndex] = 0;
  2472.             // NO eliminar del trigger
  2473.             return Plugin_Stop;
  2474.         }
  2475.     }
  2476.    
  2477.     // CASO 3: Tiene arma en inventario pero no en mano -> cambiar de nuevo
  2478.     if (HasRequiredWeapon(client, weaponType))
  2479.     {
  2480.         DebugPrint("Timer_RecheckWeapon - %N tiene arma en inventario, cambiando...", client);
  2481.         SwitchToRequiredWeapon(client, movType);
  2482.        
  2483.         if (g_iPlayerRetryCount[client][triggerIndex] < MAX_RETRY_ATTEMPTS)
  2484.         {
  2485.             DataPack retryPack = new DataPack();
  2486.             retryPack.WriteCell(userid);
  2487.             retryPack.WriteString(weaponType);
  2488.             retryPack.WriteCell(triggerIndex);
  2489.             g_hRetryTimer[client][triggerIndex] = CreateTimer(2.0, Timer_RecheckWeapon, retryPack);
  2490.             return Plugin_Stop;
  2491.         }
  2492.         else
  2493.         {
  2494.             PrintToChat(client, "[\x04Error\x01] No se pudo cambiar al arma requerida.");
  2495.             g_bWaitingForWeapon[client][triggerIndex] = false;
  2496.             g_iPlayerRetryCount[client][triggerIndex] = 0;
  2497.             // NO eliminar del trigger
  2498.             return Plugin_Stop;
  2499.         }
  2500.     }
  2501.    
  2502.     // CASO 4: No tiene el arma en absoluto -> NO ELIMINAR, solo esperar o finalizar sin limpiar
  2503.     DebugPrint("Timer_RecheckWeapon - %N no tiene arma %s, se mantiene en el trigger", client, weaponType);
  2504.     if (g_iPlayerRetryCount[client][triggerIndex] < MAX_RETRY_ATTEMPTS)
  2505.     {
  2506.         // Si aún hay intentos, seguir esperando (quizás la recoja después)
  2507.         DataPack retryPack = new DataPack();
  2508.         retryPack.WriteCell(userid);
  2509.         retryPack.WriteString(weaponType);
  2510.         retryPack.WriteCell(triggerIndex);
  2511.         g_hRetryTimer[client][triggerIndex] = CreateTimer(2.0, Timer_RecheckWeapon, retryPack);
  2512.         return Plugin_Stop;
  2513.     }
  2514.     else
  2515.     {
  2516.         // Agotados los intentos, ya no esperamos, pero no eliminamos
  2517.         PrintToChat(client, "[\x04Error\x01] No tienes %s para este movimiento.", weaponType);
  2518.         g_bWaitingForWeapon[client][triggerIndex] = false;
  2519.         g_iPlayerRetryCount[client][triggerIndex] = 0;
  2520.         // NO eliminar del trigger
  2521.         return Plugin_Stop;
  2522.     }
  2523.    
  2524.     return Plugin_Stop;
  2525. }
  2526.  
  2527. bool IsRequiredWeaponActive(int client, const char[] weaponType)
  2528. {
  2529.     int activeWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
  2530.     if (activeWeapon <= 0 || !IsValidEntity(activeWeapon))
  2531.         return false;
  2532.    
  2533.     char classname[32];
  2534.     GetEntityClassname(activeWeapon, classname, sizeof(classname));
  2535.    
  2536.     if (StrEqual(weaponType, "pm", false))
  2537.     {
  2538.         return StrEqual(classname, "weapon_pipe_bomb", false) || StrEqual(classname, "weapon_molotov", false);
  2539.     }
  2540.     else if (StrEqual(weaponType, "bile", false))
  2541.     {
  2542.         return StrEqual(classname, "weapon_vomitjar", false);
  2543.     }
  2544.     else if (StrEqual(weaponType, "gl", false))
  2545.     {
  2546.         return StrEqual(classname, "weapon_grenade_launcher", false);
  2547.     }
  2548.    
  2549.     return false;
  2550. }
  2551.  
  2552. public bool SwitchToRequiredWeapon(int client, MovementType movType)
  2553. {
  2554.     DebugPrint("SwitchToRequiredWeapon - Cambiando arma para %N (movType: %d)", client, movType);
  2555.    
  2556.     if (!IsValidSurvivor(client))
  2557.     {
  2558.         DebugPrint("SwitchToRequiredWeapon - Cliente no válido");
  2559.         return false;
  2560.     }
  2561.    
  2562.     char weaponClassname[32];
  2563.     bool foundWeapon = false;
  2564.    
  2565.     if (movType == MOVEMENT_PM || movType == MOVEMENT_RUNPM)
  2566.     {
  2567.         DebugPrint("SwitchToRequiredWeapon - Buscando arma PM para %N", client);
  2568.        
  2569.         // Buscar pipebomb primero
  2570.         if (FindWeaponByClassname(client, "weapon_pipe_bomb") != -1)
  2571.         {
  2572.             weaponClassname = "weapon_pipe_bomb";
  2573.             foundWeapon = true;
  2574.             DebugPrint("SwitchToRequiredWeapon - Encontrado pipe_bomb");
  2575.         }
  2576.         else if (FindWeaponByClassname(client, "weapon_molotov") != -1)
  2577.         {
  2578.             weaponClassname = "weapon_molotov";
  2579.             foundWeapon = true;
  2580.             DebugPrint("SwitchToRequiredWeapon - Encontrado molotov");
  2581.         }
  2582.     }
  2583.     else if (movType == MOVEMENT_BILE || movType == MOVEMENT_RUNBILE)
  2584.     {
  2585.         DebugPrint("SwitchToRequiredWeapon - Buscando arma BILE para %N", client);
  2586.        
  2587.         if (FindWeaponByClassname(client, "weapon_vomitjar") != -1)
  2588.         {
  2589.             weaponClassname = "weapon_vomitjar";
  2590.             foundWeapon = true;
  2591.             DebugPrint("SwitchToRequiredWeapon - Encontrado vomitjar");
  2592.         }
  2593.     }
  2594.     else if (movType == MOVEMENT_GL || movType == MOVEMENT_RUNGL)
  2595.     {
  2596.         DebugPrint("SwitchToRequiredWeapon - Buscando arma GL para %N", client);
  2597.        
  2598.         if (FindWeaponByClassname(client, "weapon_grenade_launcher") != -1)
  2599.         {
  2600.             weaponClassname = "weapon_grenade_launcher";
  2601.             foundWeapon = true;
  2602.             DebugPrint("SwitchToRequiredWeapon - Encontrado grenade_launcher");
  2603.         }
  2604.     }
  2605.    
  2606.     if (!foundWeapon)
  2607.     {
  2608.         DebugPrint("SwitchToRequiredWeapon - No se encontró arma requerida para %N", client);
  2609.         return false;
  2610.     }
  2611.    
  2612.     int currentWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
  2613.     int targetWeapon = FindWeaponByClassname(client, weaponClassname);
  2614.    
  2615.     DebugPrint("SwitchToRequiredWeapon - Arma actual: %d, Arma objetivo: %d", currentWeapon, targetWeapon);
  2616.    
  2617.     if (targetWeapon == -1)
  2618.     {
  2619.         DebugPrint("SwitchToRequiredWeapon - Error: arma objetivo no encontrada");
  2620.         return false;
  2621.     }
  2622.    
  2623.     if (currentWeapon != targetWeapon)
  2624.     {
  2625.         DebugPrint("SwitchToRequiredWeapon - Cambiando arma de %d a %d para %N",
  2626.             currentWeapon, targetWeapon, client);
  2627.        
  2628.         // Método natural: usar el comando de cambio de arma del juego
  2629.         FakeClientCommandEx(client, "use %s", weaponClassname);
  2630.        
  2631.         // Obtener nombre de arma para mostrar
  2632.         char displayWeaponName[32];
  2633.         GetWeaponNameForType(movType, displayWeaponName, sizeof(displayWeaponName));
  2634.        
  2635.         // NUEVO: Mostrar mensaje CORREGIDO solo al jugador
  2636.         PrintToChat(client, "[\x04MA Auto-Cambio\x01] Cambiando a %s...", displayWeaponName);
  2637.        
  2638.         DebugPrint("SwitchToRequiredWeapon - Cambio de arma EXITOSO para %N", client);
  2639.         return true;
  2640.     }
  2641.    
  2642.     DebugPrint("SwitchToRequiredWeapon - Ya tenía el arma en la mano");
  2643.     return false;
  2644. }
  2645.  
  2646. public int FindWeaponByClassname(int client, const char[] classname)
  2647. {
  2648.     for (int slot = 0; slot <= 4; slot++)
  2649.     {
  2650.         int weapon = GetPlayerWeaponSlot(client, slot);
  2651.         if (weapon > 0 && IsValidEntity(weapon))
  2652.         {
  2653.             char weaponClass[32];
  2654.             GetEntityClassname(weapon, weaponClass, sizeof(weaponClass));
  2655.             if (StrEqual(weaponClass, classname, false))
  2656.             {
  2657.                 return weapon;
  2658.             }
  2659.         }
  2660.     }
  2661.     return -1;
  2662. }
  2663.  
  2664. bool HasRequiredWeapon(int client, const char[] weaponType)
  2665. {
  2666.     if (!IsValidSurvivor(client))
  2667.         return false;
  2668.    
  2669.     // Búsqueda rápida en todas las ranuras
  2670.     for (int slot = 0; slot <= 4; slot++)
  2671.     {
  2672.         int weapon = GetPlayerWeaponSlot(client, slot);
  2673.         if (weapon > 0 && IsValidEntity(weapon))
  2674.         {
  2675.             char classname[32];
  2676.             GetEntityClassname(weapon, classname, sizeof(classname));
  2677.            
  2678.             if (StrEqual(weaponType, "pm") &&
  2679.                 (StrEqual(classname, "weapon_pipe_bomb") || StrEqual(classname, "weapon_molotov")))
  2680.                 return true;
  2681.             else if (StrEqual(weaponType, "bile") && StrEqual(classname, "weapon_vomitjar"))
  2682.                 return true;
  2683.             else if (StrEqual(weaponType, "gl") && StrEqual(classname, "weapon_grenade_launcher"))
  2684.                 return true;
  2685.         }
  2686.     }
  2687.    
  2688.     return false;
  2689. }
  2690.  
  2691. bool HasRequiredWeaponInHand(int client, const char[] weaponType)
  2692. {
  2693.     if (!IsValidSurvivor(client))
  2694.         return false;
  2695.    
  2696.     int activeWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
  2697.     if (activeWeapon <= 0 || !IsValidEntity(activeWeapon))
  2698.         return false;
  2699.    
  2700.     char classname[32];
  2701.     GetEntityClassname(activeWeapon, classname, sizeof(classname));
  2702.    
  2703.     if (StrEqual(weaponType, "pm", false) &&
  2704.         (StrEqual(classname, "weapon_pipe_bomb", false) || StrEqual(classname, "weapon_molotov", false)))
  2705.         return true;
  2706.     else if (StrEqual(weaponType, "bile", false) && StrEqual(classname, "weapon_vomitjar", false))
  2707.         return true;
  2708.     else if (StrEqual(weaponType, "gl", false) && StrEqual(classname, "weapon_grenade_launcher", false))
  2709.         return true;
  2710.    
  2711.     return false;
  2712. }
  2713.  
  2714. bool IsWeaponReadyForUse(int client, MovementType movType)
  2715. {
  2716.     if (!IsValidSurvivor(client))
  2717.         return false;
  2718.    
  2719.     int activeWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
  2720.     if (activeWeapon <= 0 || !IsValidEntity(activeWeapon))
  2721.         return false;
  2722.    
  2723.     float nextAttack = GetEntPropFloat(client, Prop_Send, "m_flNextAttack");
  2724.     if (nextAttack > GetGameTime() + 0.1)
  2725.     {
  2726.         DebugPrint("IsWeaponReadyForUse - %N no listo, nextAttack=%.2f", client, nextAttack - GetGameTime());
  2727.         return false;
  2728.     }
  2729.    
  2730.     DebugPrint("IsWeaponReadyForUse - %N listo", client);
  2731.     return true;
  2732. }
  2733.  
  2734. MovementType GetMovementTypeFromString(const char[] typeStr)
  2735. {
  2736.     DebugPrint("GetMovementTypeFromString - Convirtiendo: %s", typeStr);
  2737.    
  2738.     if (StrEqual(typeStr, "pm", false) || StrEqual(typeStr, "runpm", false))
  2739.     {
  2740.         DebugPrint("GetMovementTypeFromString - %s -> MOVEMENT_PM (1)", typeStr);
  2741.         return MOVEMENT_PM;
  2742.     }
  2743.     if (StrEqual(typeStr, "bile", false) || StrEqual(typeStr, "runbile", false))
  2744.     {
  2745.         DebugPrint("GetMovementTypeFromString - %s -> MOVEMENT_BILE (2)", typeStr);
  2746.         return MOVEMENT_BILE;
  2747.     }
  2748.     if (StrEqual(typeStr, "gl", false) || StrEqual(typeStr, "rungl", false))
  2749.     {
  2750.         DebugPrint("GetMovementTypeFromString - %s -> MOVEMENT_GL (3)", typeStr);
  2751.         return MOVEMENT_GL;
  2752.     }
  2753.     if (StrEqual(typeStr, "run", false))
  2754.     {
  2755.         DebugPrint("GetMovementTypeFromString - %s -> MOVEMENT_RUN (4)", typeStr);
  2756.         return MOVEMENT_RUN;
  2757.     }
  2758.     if (StrEqual(typeStr, "dual", false))
  2759.     {
  2760.         DebugPrint("GetMovementTypeFromString - %s -> MOVEMENT_DUAL (5)", typeStr);
  2761.         return MOVEMENT_DUAL;
  2762.     }
  2763.    
  2764.     DebugPrint("GetMovementTypeFromString - %s -> MOVEMENT_UNKNOWN (0)", typeStr);
  2765.     return MOVEMENT_UNKNOWN;
  2766. }
  2767.  
  2768. // ========================================================================
  2769. // FUNCIONES AUXILIARES
  2770. // ========================================================================
  2771.  
  2772. MovementType GetBaseMovementType(MovementType movType)
  2773. {
  2774.     DebugPrint("GetBaseMovementType - Convirtiendo %d", movType);
  2775.    
  2776.     if (movType == MOVEMENT_RUNPM)
  2777.     {
  2778.         DebugPrint("GetBaseMovementType - %d -> MOVEMENT_PM (1)", movType);
  2779.         return MOVEMENT_PM;
  2780.     }
  2781.     else if (movType == MOVEMENT_RUNBILE)
  2782.     {
  2783.         DebugPrint("GetBaseMovementType - %d -> MOVEMENT_BILE (2)", movType);
  2784.         return MOVEMENT_BILE;
  2785.     }
  2786.     else if (movType == MOVEMENT_RUNGL)
  2787.     {
  2788.         DebugPrint("GetBaseMovementType - %d -> MOVEMENT_GL (3)", movType);
  2789.         return MOVEMENT_GL;
  2790.     }
  2791.    
  2792.     DebugPrint("GetBaseMovementType - %d -> %d (mismo)", movType, movType);
  2793.     return movType;
  2794. }
  2795.  
  2796. void GetWeaponNameForType(MovementType movType, char[] buffer, int size)
  2797. {
  2798.     if (movType == MOVEMENT_PM || movType == MOVEMENT_RUNPM)
  2799.         strcopy(buffer, size, "pipebomb o molotov");
  2800.     else if (movType == MOVEMENT_BILE || movType == MOVEMENT_RUNBILE)
  2801.         strcopy(buffer, size, "bilejar");
  2802.     else if (movType == MOVEMENT_GL || movType == MOVEMENT_RUNGL)
  2803.         strcopy(buffer, size, "lanzagranadas");
  2804.     else if (movType == MOVEMENT_RUN)
  2805.         strcopy(buffer, size, "ninguna (solo correr)");
  2806.     else
  2807.         strcopy(buffer, size, "arma desconocida");
  2808. }
  2809.  
  2810. void GetIndividualTypeString(MovementType movType, char[] buffer, int size)
  2811. {
  2812.     if (movType == MOVEMENT_PM)
  2813.         strcopy(buffer, size, "PM");
  2814.     else if (movType == MOVEMENT_BILE)
  2815.         strcopy(buffer, size, "Bile");
  2816.     else if (movType == MOVEMENT_GL)
  2817.         strcopy(buffer, size, "GL");
  2818.     else if (movType == MOVEMENT_RUN)
  2819.         strcopy(buffer, size, "Run");
  2820.     else if (movType == MOVEMENT_DUAL)
  2821.         strcopy(buffer, size, "Dual");
  2822.     else if (movType == MOVEMENT_RUNPM)
  2823.         strcopy(buffer, size, "RunPM");
  2824.     else if (movType == MOVEMENT_RUNBILE)
  2825.         strcopy(buffer, size, "RunBile");
  2826.     else if (movType == MOVEMENT_RUNGL)
  2827.         strcopy(buffer, size, "RunGL");
  2828.     else
  2829.         strcopy(buffer, size, "Unknown");
  2830. }
  2831.  
  2832. // ========================================================================
  2833. // CALLBACKS DE TRIGGERS CON LÓGICA V15 MEJORADA
  2834. // ========================================================================
  2835.  
  2836. public void OnStartTouch(const char[] output, int caller, int activator, float delay)
  2837. {
  2838.     DebugPrint("OnStartTouch - Trigger tocado - caller: %d, activator: %N", caller, activator);
  2839.    
  2840.     if (activator < 1 || activator > MaxClients || !IsClientInGame(activator) || !IsValidSurvivor(activator))
  2841.     {
  2842.         DebugPrint("OnStartTouch - Activator no válido");
  2843.         return;
  2844.     }
  2845.    
  2846.     // CRÍTICO: Verificar si el jugador ya está en movimiento
  2847.     if (g_bClientInMovement[activator])
  2848.     {
  2849.         DebugPrint("OnStartTouch - %N ya está en movimiento - ignorando trigger", activator);
  2850.         return;
  2851.     }
  2852.    
  2853.     caller = EntIndexToEntRef(caller);
  2854.    
  2855.     // PRIMERO: Limpiar al jugador de TODOS los triggers antes de agregarlo a uno nuevo
  2856.     // Esto evita que un jugador esté en múltiples triggers simultáneamente
  2857.     for (int i = 0; i < MAX_TRIGGERS; i++)
  2858.     {
  2859.         if (g_bPlayerInTrigger[activator][i])
  2860.         {
  2861.             DebugPrint("OnStartTouch - %N ya estaba en trigger %d - limpiando primero", activator, i);
  2862.             CompletelyCleanPlayerFromTrigger(activator, i);
  2863.         }
  2864.     }
  2865.    
  2866.     // SEGUNDO: Encontrar el trigger y agregar al jugador
  2867.     for (int i = 0; i < MAX_TRIGGERS; i++)
  2868.     {
  2869.         if (caller == g_iTriggers[i])
  2870.         {
  2871.             DebugPrint("OnStartTouch - Trigger encontrado en índice %d", i);
  2872.            
  2873.             // Agregar jugador al tracking
  2874.             g_bPlayerInTrigger[activator][i] = true;
  2875.             g_bPlayerReady[activator][i] = false;
  2876.             g_iPlayerRetryCount[activator][i] = 0;
  2877.             g_bWaitingForWeapon[activator][i] = false;
  2878.            
  2879.             // Iniciar verificación inmediata pero con validación extra
  2880.             // Esperar 0.2 segundos para asegurar que el jugador está estable en el trigger
  2881.             CreateTimer(0.2, Timer_DelayedCheck, activator);
  2882.            
  2883.             break;
  2884.         }
  2885.     }
  2886. }
  2887.  
  2888. public void OnEndTouch(const char[] output, int caller, int activator, float delay)
  2889. {
  2890.     if (activator < 1 || activator > MaxClients || !IsClientInGame(activator))
  2891.         return;
  2892.    
  2893.     DebugPrint("OnEndTouch - %N salió de un trigger", activator);
  2894.    
  2895.     caller = EntIndexToEntRef(caller);
  2896.    
  2897.     for (int i = 0; i < MAX_TRIGGERS; i++)
  2898.     {
  2899.         if (caller == g_iTriggers[i])
  2900.         {
  2901.             DebugPrint("OnEndTouch - %N salió del trigger %d", activator, i);
  2902.            
  2903.             // Limpieza COMPLETA del jugador de este trigger
  2904.             CompletelyCleanPlayerFromTrigger(activator, i);
  2905.            
  2906.             // También remover de todos los grupos de espera
  2907.             RemovePlayerFromAllWaitingLists(activator);
  2908.            
  2909.             break;
  2910.         }
  2911.     }
  2912. }
  2913.  
  2914. bool VerifyPlayersStillInTriggers(int groupNumber, ArrayList players)
  2915. {
  2916.     if (players == null) return false;
  2917.    
  2918.     bool allValid = true;
  2919.    
  2920.     for (int i = 0; i < players.Length; i++)
  2921.     {
  2922.         int client = players.Get(i);
  2923.        
  2924.         if (!IsValidSurvivor(client))
  2925.         {
  2926.             DebugPrint("VerifyPlayersStillInTriggers - %N ya no es válido", client);
  2927.             allValid = false;
  2928.             continue;
  2929.         }
  2930.        
  2931.         // Verificar que el jugador esté en ALGÚN trigger del grupo
  2932.         bool foundInGroupTrigger = false;
  2933.         ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  2934.        
  2935.         if (groupTriggers != null)
  2936.         {
  2937.             for (int j = 0; j < groupTriggers.Length; j++)
  2938.             {
  2939.                 int triggerIndex = groupTriggers.Get(j);
  2940.                 if (g_bPlayerInTrigger[client][triggerIndex] && g_bPlayerReady[client][triggerIndex])
  2941.                 {
  2942.                     foundInGroupTrigger = true;
  2943.                     break;
  2944.                 }
  2945.             }
  2946.         }
  2947.        
  2948.         if (!foundInGroupTrigger)
  2949.         {
  2950.             DebugPrint("VerifyPlayersStillInTriggers - %N ya no está en un trigger del grupo %d", client, groupNumber);
  2951.             allValid = false;
  2952.         }
  2953.     }
  2954.    
  2955.     return allValid;
  2956. }
  2957.  
  2958. // ========================================================================
  2959. // TIMER DE VERIFICACIÓN CONTINUA (LÓGICA V15 MEJORADA)
  2960. // ========================================================================
  2961.  
  2962. public Action Timer_CheckTriggerPresence(Handle timer, any client)
  2963. {
  2964.     // Verificar si el cliente es válido
  2965.     if (!IsValidSurvivor(client) || !IsPlayerAlive(client) || IsPlayerIncapacitated(client))
  2966.         return Plugin_Stop;
  2967.    
  2968.     // Verificar en qué triggers está el jugador
  2969.     for (int triggerIndex = 0; triggerIndex < MAX_TRIGGERS; triggerIndex++)
  2970.     {
  2971.         if (g_bPlayerInTrigger[client][triggerIndex])
  2972.         {
  2973.             // Verificar si el trigger puede activarse (pasado el refire_time)
  2974.             if (CanTriggerActivate(triggerIndex))
  2975.             {
  2976.                 // Verificar condiciones del jugador
  2977.                 if (CheckPlayerConditions(client, triggerIndex))
  2978.                 {
  2979.                     DebugPrint("Timer_CheckTriggerPresence - %N cumple condiciones en trigger %d", client, triggerIndex);
  2980.                    
  2981.                     // Marcar como listo
  2982.                     g_bPlayerReady[client][triggerIndex] = true;
  2983.                    
  2984.                     // Intentar activar según si es grupo o individual
  2985.                     int groupNumber = g_iTriggerGroup[triggerIndex];
  2986.                    
  2987.                     if (groupNumber > 0)
  2988.                     {
  2989.                         // Es parte de un grupo
  2990.                         if (!IsPlayerAlreadyInGroupActivation(groupNumber, client))
  2991.                         {
  2992.                             PrintToChat(client, "[\x04Grupo %d\x01] Listo. Esperando otros participantes...", groupNumber);
  2993.                             AttemptGroupActivation(groupNumber, triggerIndex, client);
  2994.                         }
  2995.                     }
  2996.                     else
  2997.                     {
  2998.                         // Es individual - activar inmediatamente
  2999.                         ExecuteSoloMovementFromTrigger(client, triggerIndex);
  3000.                     }
  3001.                 }
  3002.                 else
  3003.                 {
  3004.                     // Si no cumple condiciones, programar otro chequeo (como en V15)
  3005.                     CreateTimer(0.5, Timer_CheckTriggerPresence, client);
  3006.                 }
  3007.             }
  3008.             else
  3009.             {
  3010.                 // Trigger en cooldown, esperar y volver a verificar
  3011.                 float timeLeft = g_fNextActivationTime[triggerIndex] - GetGameTime();
  3012.                 if (timeLeft > 0)
  3013.                 {
  3014.                     PrintHintText(client, "Trigger en cooldown: %.1fs", timeLeft);
  3015.                     CreateTimer(timeLeft + 0.1, Timer_CheckTriggerPresence, client);
  3016.                 }
  3017.             }
  3018.            
  3019.             // Solo procesar el primer trigger donde está el jugador
  3020.             break;
  3021.         }
  3022.     }
  3023.    
  3024.     return Plugin_Stop;
  3025. }
  3026.  
  3027. // ========================================================================
  3028. // TIMER DE VERIFICACIÓN CONTINUA MEJORADO (PARA SALUD)
  3029. // ========================================================================
  3030.  
  3031. public Action Timer_DelayedCheck(Handle timer, any client)
  3032. {
  3033.     if (!IsValidSurvivor(client) || g_bClientInMovement[client])
  3034.     {
  3035.         DebugPrint("Timer_DelayedCheck - %N ya no es válido o está en movimiento", client);
  3036.         return Plugin_Stop;
  3037.     }
  3038.    
  3039.     // Verificar en qué triggers está el jugador (debería ser solo uno)
  3040.     bool foundTrigger = false;
  3041.     for (int triggerIndex = 0; triggerIndex < MAX_TRIGGERS; triggerIndex++)
  3042.     {
  3043.         if (g_bPlayerInTrigger[client][triggerIndex])
  3044.         {
  3045.             foundTrigger = true;
  3046.            
  3047.             // Verificar si el trigger puede activarse
  3048.             if (CanTriggerActivate(triggerIndex))
  3049.             {
  3050.                 // Verificar condiciones del jugador (incluyendo salud actualizada)
  3051.                 if (CheckPlayerConditions(client, triggerIndex))
  3052.                 {
  3053.                     DebugPrint("Timer_DelayedCheck - %N cumple condiciones en trigger %d", client, triggerIndex);
  3054.                    
  3055.                     // Marcar como listo
  3056.                     g_bPlayerReady[client][triggerIndex] = true;
  3057.                    
  3058.                     // Intentar activar según si es grupo o individual
  3059.                     int groupNumber = g_iTriggerGroup[triggerIndex];
  3060.                    
  3061.                     if (groupNumber > 0)
  3062.                     {
  3063.                         // Es parte de un grupo
  3064.                         if (!IsPlayerAlreadyInGroupActivation(groupNumber, client))
  3065.                         {
  3066.                             // Verificar que el jugador SIGA en el trigger
  3067.                             if (g_bPlayerInTrigger[client][triggerIndex])
  3068.                             {
  3069.                                 PrintToChat(client, "[\x04Grupo %d\x01] Listo. Esperando otros participantes...", groupNumber);
  3070.                                 AttemptGroupActivation(groupNumber, triggerIndex, client);
  3071.                             }
  3072.                             else
  3073.                             {
  3074.                                 DebugPrint("Timer_DelayedCheck - %N ya no está en el trigger %d", client, triggerIndex);
  3075.                             }
  3076.                         }
  3077.                     }
  3078.                     else
  3079.                     {
  3080.                         // Es individual - activar inmediatamente si sigue en el trigger
  3081.                         if (g_bPlayerInTrigger[client][triggerIndex])
  3082.                         {
  3083.                             ExecuteSoloMovementFromTrigger(client, triggerIndex);
  3084.                         }
  3085.                     }
  3086.                 }
  3087.                 else
  3088.                 {
  3089.                     DebugPrint("Timer_DelayedCheck - %N no cumple condiciones en trigger %d", client, triggerIndex);
  3090.                    
  3091.                     // Mostrar mensaje específico sobre salud si es el problema
  3092.                     int permanentHealth = GetClientHealth(client);
  3093.                     int tempHealth = GetPlayerTempHealth(client);
  3094.                     int totalHealth = permanentHealth + tempHealth;
  3095.                    
  3096.                     if (totalHealth < 45)
  3097.                     {
  3098.                         PrintToChat(client, "[\x04MA Error\x01] Necesitas 45 de salud. Tienes: %d", totalHealth);
  3099.                     }
  3100.                    
  3101.                     // Si no cumple condiciones, programar otro chequeo solo si sigue en el trigger
  3102.                     if (g_bPlayerInTrigger[client][triggerIndex])
  3103.                     {
  3104.                         CreateTimer(0.5, Timer_DelayedCheck, client);
  3105.                     }
  3106.                 }
  3107.             }
  3108.             else
  3109.             {
  3110.                 // Trigger en cooldown, esperar y volver a verificar solo si sigue en el trigger
  3111.                 float timeLeft = g_fNextActivationTime[triggerIndex] - GetGameTime();
  3112.                 if (timeLeft > 0 && g_bPlayerInTrigger[client][triggerIndex])
  3113.                 {
  3114.                     PrintHintText(client, "Trigger en cooldown: %.1fs", timeLeft);
  3115.                     CreateTimer(timeLeft + 0.1, Timer_DelayedCheck, client);
  3116.                 }
  3117.             }
  3118.            
  3119.             break; // Solo procesar el primer trigger donde está el jugador
  3120.         }
  3121.     }
  3122.    
  3123.     if (!foundTrigger)
  3124.     {
  3125.         DebugPrint("Timer_DelayedCheck - %N no está en ningún trigger", client);
  3126.     }
  3127.    
  3128.     return Plugin_Stop;
  3129. }
  3130.  
  3131. // ========================================================================
  3132. // FUNCIONES DE ACTIVACIÓN GRUPAL (MODIFICADAS PARA PERMITIR MÚLTIPLES RUNNERS)
  3133. // ========================================================================
  3134.  
  3135. public void AttemptGroupActivation(int groupNumber, int triggerIdx, int activator)
  3136. {
  3137.     DebugPrint("AttemptGroupActivation - Intentando activar el grupo %d, trigger %d, activador %N",
  3138.         groupNumber, triggerIdx, activator);
  3139.    
  3140.     // ================================================================
  3141.     // NUEVO: Verificar si el trigger está en cooldown
  3142.     // ================================================================
  3143.     if (triggerIdx >= 0 && triggerIdx < MAX_TRIGGERS)
  3144.     {
  3145.         float currentTime = GetGameTime();
  3146.         if (currentTime < g_fNextActivationTime[triggerIdx])
  3147.         {
  3148.             float timeLeft = g_fNextActivationTime[triggerIdx] - currentTime;
  3149.             DebugPrint("AttemptGroupActivation - Trigger %d en cooldown, reprogramando en %.2fs", triggerIdx, timeLeft);
  3150.            
  3151.             // Programar reintento después del cooldown
  3152.             DataPack pack = new DataPack();
  3153.             pack.WriteCell(groupNumber);
  3154.             pack.WriteCell(triggerIdx);
  3155.             pack.WriteCell(GetClientUserId(activator));
  3156.             CreateTimer(timeLeft + 0.1, Timer_RetryActivation, pack);
  3157.             return;
  3158.         }
  3159.     }
  3160.    
  3161.     // Obtener todos los triggers del grupo
  3162.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  3163.     DebugPrint("AttemptGroupActivation - El grupo %d tiene %d triggers", groupNumber, groupTriggers.Length);
  3164.    
  3165.     if (groupTriggers == null || groupTriggers.Length == 0)
  3166.         return;
  3167.    
  3168.     // Obtener información del grupo
  3169.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  3170.     DebugPrint("AttemptGroupActivation - El grupo %d tiene %d tipos:", groupNumber, groupTypes.Length);
  3171.    
  3172.     // Contar cuántos tipos únicos hay
  3173.     int uniqueTypes = 0;
  3174.     bool hasRun = false;
  3175.     bool hasBile = false;
  3176.     bool hasGL = false;
  3177.     bool hasPM = false;
  3178.    
  3179.     for (int i = 0; i < groupTypes.Length; i++)
  3180.     {
  3181.         char type[16];
  3182.         groupTypes.GetString(i, type, sizeof(type));
  3183.         DebugPrint("AttemptGroupActivation - Tipo %d: %s", i, type);
  3184.        
  3185.         if (StrEqual(type, "run")) hasRun = true;
  3186.         else if (StrEqual(type, "bile")) hasBile = true;
  3187.         else if (StrEqual(type, "gl")) hasGL = true;
  3188.         else if (StrEqual(type, "pm")) hasPM = true;
  3189.         else if (StrEqual(type, "runpm")) { hasRun = true; hasPM = true; }
  3190.         else if (StrEqual(type, "runbile")) { hasRun = true; hasBile = true; }
  3191.         else if (StrEqual(type, "rungl")) { hasRun = true; hasGL = true; }
  3192.     }
  3193.    
  3194.     // Determinar si es grupo múltiple
  3195.     int totalTypes = (hasRun ? 1 : 0) + (hasBile ? 1 : 0) + (hasGL ? 1 : 0) + (hasPM ? 1 : 0);
  3196.     DebugPrint("AttemptGroupActivation - Total tipos únicos en grupo %d: %d", groupNumber, totalTypes);
  3197.    
  3198.     // ================================================================
  3199.     // DECLARAR VARIABLES PARA ESTA FUNCIÓN (SOLO UNA VEZ)
  3200.     // ================================================================
  3201.     ArrayList activatedPlayers = new ArrayList();
  3202.     ArrayList activatedFiles = new ArrayList(ByteCountToCells(128));
  3203.    
  3204.     // Verificar si el grupo ya está activo (evitar múltiples activaciones)
  3205.     char groupKey[16];
  3206.     IntToString(groupNumber, groupKey, sizeof(groupKey));
  3207.    
  3208.     bool isGroupActive = false;
  3209.     if (g_hActiveGroups != null)
  3210.     {
  3211.         g_hActiveGroups.GetValue(groupKey, isGroupActive);
  3212.     }
  3213.    
  3214.     if (isGroupActive)
  3215.     {
  3216.         DebugPrint("AttemptGroupActivation - El grupo %d ya está activo - ignorando", groupNumber);
  3217.         delete activatedPlayers;
  3218.         delete activatedFiles;
  3219.         return;
  3220.     }
  3221.    
  3222.     // NUEVA VERIFICACIÓN: Comprobar que todos los jugadores siguen en sus triggers
  3223.     bool allPlayersStillValid = true;
  3224.     ArrayList playersToCheck = new ArrayList();
  3225.    
  3226.     // Determinar la capacidad del grupo basada en el booster de mayor capacidad
  3227.     int groupCapacity = 1; // Capacidad mínima
  3228.     int maxBoosterCapacity = 0;
  3229.     char maxBoosterType[16] = "";
  3230.    
  3231.     // Calcular capacidad basada en boosters
  3232.     if (hasPM)
  3233.     {
  3234.         int capacity = GetBoosterCapacityNew("PM"); // 32
  3235.         if (capacity > maxBoosterCapacity)
  3236.         {
  3237.             maxBoosterCapacity = capacity;
  3238.             strcopy(maxBoosterType, sizeof(maxBoosterType), "PM");
  3239.         }
  3240.     }
  3241.     if (hasBile)
  3242.     {
  3243.         int capacity = GetBoosterCapacityNew("Bile"); // 5
  3244.         if (capacity > maxBoosterCapacity)
  3245.         {
  3246.             maxBoosterCapacity = capacity;
  3247.             strcopy(maxBoosterType, sizeof(maxBoosterType), "Bile");
  3248.         }
  3249.     }
  3250.     if (hasGL)
  3251.     {
  3252.         int capacity = GetBoosterCapacityNew("GL"); // 1
  3253.         if (capacity > maxBoosterCapacity)
  3254.         {
  3255.             maxBoosterCapacity = capacity;
  3256.             strcopy(maxBoosterType, sizeof(maxBoosterType), "GL");
  3257.         }
  3258.     }
  3259.    
  3260.     // Si no hay boosters puros, verificar híbridos
  3261.     if (maxBoosterCapacity == 0)
  3262.     {
  3263.         // Buscar en todos los triggers del grupo para determinar la capacidad
  3264.         for (int i = 0; i < groupTriggers.Length; i++)
  3265.         {
  3266.             int currentTriggerIdx = groupTriggers.Get(i);
  3267.             char type[16];
  3268.             strcopy(type, sizeof(type), g_sTriggerType[currentTriggerIdx]);
  3269.            
  3270.             if (StrEqual(type, "runpm") || StrEqual(type, "runbile") || StrEqual(type, "rungl"))
  3271.             {
  3272.                 // Es un híbrido - obtener su capacidad
  3273.                 int capacity = MA_GetBoosterCapacity(type);
  3274.                 if (capacity > maxBoosterCapacity)
  3275.                 {
  3276.                     maxBoosterCapacity = capacity;
  3277.                     strcopy(maxBoosterType, sizeof(maxBoosterType), type);
  3278.                 }
  3279.             }
  3280.         }
  3281.     }
  3282.    
  3283.     // Establecer capacidad del grupo
  3284.     groupCapacity = maxBoosterCapacity > 0 ? maxBoosterCapacity : 32; // Default 32 si no hay booster
  3285.     DebugPrint("AttemptGroupActivation - Capacidad del grupo %d: %d runners (booster: %s)",
  3286.         groupNumber, groupCapacity, maxBoosterType);
  3287.    
  3288.     // ================================================================
  3289.     // RECOLECTAR JUGADORES PARA VERIFICACIÓN (para grupos de 1-2 tipos)
  3290.     // ================================================================
  3291.     if (totalTypes < 3)
  3292.     {
  3293.         for (int i = 0; i < groupTriggers.Length; i++)
  3294.         {
  3295.             int currentTriggerIdx = groupTriggers.Get(i);
  3296.            
  3297.             for (int client = 1; client <= MaxClients; client++)
  3298.             {
  3299.                 if (IsValidSurvivor(client) && g_bPlayerReady[client][currentTriggerIdx])
  3300.                 {
  3301.                     if (playersToCheck.FindValue(client) == -1)
  3302.                     {
  3303.                         playersToCheck.Push(client);
  3304.                         DebugPrint("AttemptGroupActivation - Jugador listo: %N en trigger %d", client, currentTriggerIdx);
  3305.                     }
  3306.                 }
  3307.             }
  3308.         }
  3309.     }
  3310.    
  3311.     // Verificar que todos los jugadores propuestos sigan válidos y en sus triggers
  3312.     if (playersToCheck.Length > 0)
  3313.     {
  3314.         for (int i = 0; i < playersToCheck.Length; i++)
  3315.         {
  3316.             int client = playersToCheck.Get(i);
  3317.            
  3318.             if (!IsValidSurvivor(client))
  3319.             {
  3320.                 DebugPrint("AttemptGroupActivation - %N ya no es válido", client);
  3321.                 allPlayersStillValid = false;
  3322.                 break;
  3323.             }
  3324.            
  3325.             if (g_bClientInMovement[client])
  3326.             {
  3327.                 DebugPrint("AttemptGroupActivation - %N ya está en movimiento", client);
  3328.                 allPlayersStillValid = false;
  3329.                 break;
  3330.             }
  3331.            
  3332.             bool foundInGroupTrigger = false;
  3333.             for (int j = 0; j < groupTriggers.Length; j++)
  3334.             {
  3335.                 int currentTriggerIdx = groupTriggers.Get(j);
  3336.                 if (g_bPlayerInTrigger[client][currentTriggerIdx] && g_bPlayerReady[client][currentTriggerIdx])
  3337.                 {
  3338.                     foundInGroupTrigger = true;
  3339.                     break;
  3340.                 }
  3341.             }
  3342.            
  3343.             if (!foundInGroupTrigger)
  3344.             {
  3345.                 DebugPrint("AttemptGroupActivation - %N ya no está en un trigger del grupo %d", client, groupNumber);
  3346.                 allPlayersStillValid = false;
  3347.                 break;
  3348.             }
  3349.         }
  3350.     }
  3351.    
  3352.     if (!allPlayersStillValid)
  3353.     {
  3354.         DebugPrint("AttemptGroupActivation - Validación falló - cancelando activación");
  3355.         delete playersToCheck;
  3356.         delete activatedPlayers;
  3357.         delete activatedFiles;
  3358.         return;
  3359.     }
  3360.    
  3361.     // ================================================================
  3362.     // VERIFICAR QUE TODOS LOS TIPOS TENGAN JUGADORES LISTOS
  3363.     // ================================================================
  3364.     if (totalTypes >= 3)
  3365.     {
  3366.         DebugPrint("AttemptGroupActivation - Verificando %d tipos para grupo %d", totalTypes, groupNumber);
  3367.        
  3368.         bool runReady = (hasRun && TypeHasReadyPlayers(groupNumber, "run"));
  3369.         bool bileReady = (hasBile && (TypeHasReadyPlayers(groupNumber, "bile") || TypeHasReadyPlayers(groupNumber, "runbile")));
  3370.         bool glReady = (hasGL && (TypeHasReadyPlayers(groupNumber, "gl") || TypeHasReadyPlayers(groupNumber, "rungl")));
  3371.         bool pmReady = (hasPM && (TypeHasReadyPlayers(groupNumber, "pm") || TypeHasReadyPlayers(groupNumber, "runpm")));
  3372.        
  3373.         DebugPrint("AttemptGroupActivation - Estados: Run=%d, Bile=%d, GL=%d, PM=%d",
  3374.             runReady, bileReady, glReady, pmReady);
  3375.        
  3376.         int readyTypes = 0;
  3377.         if (runReady) readyTypes++;
  3378.         if (bileReady) readyTypes++;
  3379.         if (glReady) readyTypes++;
  3380.         if (pmReady) readyTypes++;
  3381.        
  3382.         if (readyTypes < totalTypes)
  3383.         {
  3384.             DebugPrint("AttemptGroupActivation - No todos los tipos del grupo %d tienen jugadores listos (%d/%d)",
  3385.                 groupNumber, readyTypes, totalTypes);
  3386.            
  3387.             if (hasRun && !runReady)
  3388.             {
  3389.                 if (!g_bWaitMessageShown[activator][groupNumber])
  3390.                 {
  3391.                     PrintToChat(activator, "[\x04MA Grupo (%d)\x01] Esperando Runner...", groupNumber);
  3392.                     g_bWaitMessageShown[activator][groupNumber] = true;
  3393.                 }
  3394.             }
  3395.             if (hasBile && !bileReady)
  3396.             {
  3397.                 if (!g_bWaitMessageShown[activator][groupNumber])
  3398.                 {
  3399.                     PrintToChat(activator, "[\x04MA Grupo (%d)\x01] Esperando Bile...", groupNumber);
  3400.                     g_bWaitMessageShown[activator][groupNumber] = true;
  3401.                 }
  3402.             }
  3403.             if (hasGL && !glReady)
  3404.             {
  3405.                 if (!g_bWaitMessageShown[activator][groupNumber])
  3406.                 {
  3407.                     PrintToChat(activator, "[\x04MA Grupo (%d)\x01] Esperando GL...", groupNumber);
  3408.                     g_bWaitMessageShown[activator][groupNumber] = true;
  3409.                 }
  3410.             }
  3411.             if (hasPM && !pmReady)
  3412.             {
  3413.                 if (!g_bWaitMessageShown[activator][groupNumber])
  3414.                 {
  3415.                     PrintToChat(activator, "[\x04MA Grupo (%d)\x01] Esperando PM...", groupNumber);
  3416.                     g_bWaitMessageShown[activator][groupNumber] = true;
  3417.                 }
  3418.             }
  3419.            
  3420.             delete playersToCheck;
  3421.             delete activatedPlayers;
  3422.             delete activatedFiles;
  3423.             return;
  3424.         }
  3425.        
  3426.         DebugPrint("AttemptGroupActivation - TODOS los tipos (%d) tienen jugadores listos", totalTypes);
  3427.     }
  3428.     else
  3429.     {
  3430.         if (!AllGroupTypesHaveReadyPlayers(groupNumber))
  3431.         {
  3432.             DebugPrint("AttemptGroupActivation - No todos los tipos del grupo %d tienen jugadores listos", groupNumber);
  3433.             delete playersToCheck;
  3434.             delete activatedPlayers;
  3435.             delete activatedFiles;
  3436.             return;
  3437.         }
  3438.     }
  3439.    
  3440.     // Verificar runners disponibles
  3441.     ArrayList allRunners = GetAllRunnersInGroup(groupNumber);
  3442.     int availableRunners = allRunners.Length;
  3443.     delete allRunners;
  3444.    
  3445.     DebugPrint("AttemptGroupActivation - Runners disponibles en grupo %d: %d", groupNumber, availableRunners);
  3446.    
  3447.     if (availableRunners == 0 && hasRun)
  3448.     {
  3449.         DebugPrint("AttemptGroupActivation - No hay runners disponibles en el grupo %d", groupNumber);
  3450.         delete playersToCheck;
  3451.         delete activatedPlayers;
  3452.         delete activatedFiles;
  3453.         return;
  3454.     }
  3455.    
  3456.     DebugPrint("AttemptGroupActivation - TODAS las condiciones cumplidas - ACTIVANDO GRUPO %d", groupNumber);
  3457.    
  3458.     g_bWaitMessageShown[activator][groupNumber] = false;
  3459.    
  3460.     // Marcar grupo como activo
  3461.     if (g_hActiveGroups != null)
  3462.     {
  3463.         g_hActiveGroups.SetValue(groupKey, true);
  3464.     }
  3465.    
  3466.     // ================================================================
  3467.     // ACTIVAR SEGÚN EL TIPO DE GRUPO
  3468.     // ================================================================
  3469.    
  3470.     if (totalTypes >= 3)
  3471.     {
  3472.         DebugPrint("AttemptGroupActivation - Activando grupo MULTIPLE (%d tipos)", totalTypes);
  3473.        
  3474.         // 1. PRIMERO: Activar HÍBRIDOS (runpm, runbile, rungl)
  3475.         char hybridTypes[][] = {"runpm", "runbile", "rungl"};
  3476.         for (int h = 0; h < sizeof(hybridTypes); h++)
  3477.         {
  3478.             // Obtener TODOS los jugadores en esta posición híbrida
  3479.             ArrayList hybridPlayers = GetAllReadyPlayersInHybridPosition(groupNumber, hybridTypes[h]);
  3480.             if (hybridPlayers != null && hybridPlayers.Length > 0)
  3481.             {
  3482.                 // Determinar el arma requerida para este híbrido
  3483.                 char requiredWeapon[8];
  3484.                 if (StrEqual(hybridTypes[h], "runpm")) strcopy(requiredWeapon, sizeof(requiredWeapon), "pm");
  3485.                 else if (StrEqual(hybridTypes[h], "runbile")) strcopy(requiredWeapon, sizeof(requiredWeapon), "bile");
  3486.                 else if (StrEqual(hybridTypes[h], "rungl")) strcopy(requiredWeapon, sizeof(requiredWeapon), "gl");
  3487.                
  3488.                 // Verificar si al menos uno de los jugadores tiene el arma requerida
  3489.                 bool hasArmed = false;
  3490.                 for (int i = 0; i < hybridPlayers.Length; i++)
  3491.                 {
  3492.                     int client = hybridPlayers.Get(i);
  3493.                     if (HasRequiredWeapon(client, requiredWeapon))
  3494.                     {
  3495.                         hasArmed = true;
  3496.                         break;
  3497.                     }
  3498.                 }
  3499.                
  3500.                 // Si no hay ningún jugador con el arma, no activar este híbrido
  3501.                 if (!hasArmed)
  3502.                 {
  3503.                     DebugPrint("AttemptGroupActivation - No hay jugador con %s en posición híbrida %s, saltando",
  3504.                         requiredWeapon, hybridTypes[h]);
  3505.                    
  3506.                     // Mostrar mensaje de espera (solo una vez)
  3507.                     if (!g_bWaitMessageShown[activator][groupNumber])
  3508.                     {
  3509.                         PrintToChat(activator, "[\x04MA Grupo (%d)\x01] Esperando un jugador con %s en posición híbrida...",
  3510.                             groupNumber, requiredWeapon);
  3511.                         g_bWaitMessageShown[activator][groupNumber] = true;
  3512.                     }
  3513.                    
  3514.                     delete hybridPlayers;
  3515.                     continue;
  3516.                 }
  3517.                
  3518.                 // Activar TODOS los jugadores en la posición híbrida (incluyendo los que no tienen arma)
  3519.                 for (int i = 0; i < hybridPlayers.Length; i++)
  3520.                 {
  3521.                     int client = hybridPlayers.Get(i);
  3522.                    
  3523.                     if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  3524.                     {
  3525.                         if (g_bClientInMovement[client])
  3526.                         {
  3527.                             DebugPrint("AttemptGroupActivation - %N ya está en movimiento en el grupo %d - saltando", client, groupNumber);
  3528.                             continue;
  3529.                         }
  3530.                         else
  3531.                         {
  3532.                             DebugPrint("AttemptGroupActivation - %N completó movimiento previo, puede unirse al grupo %d de nuevo", client, groupNumber);
  3533.                             MarkPlayerInGroupActivation(groupNumber, client, false);
  3534.                         }
  3535.                     }
  3536.                    
  3537.                     int foundTriggerIdx = FindReadyTriggerForPlayer(client, hybridTypes[h]);
  3538.                     if (foundTriggerIdx != -1)
  3539.                     {
  3540.                         char movementFile[128];
  3541.                         GetMovementFileForTrigger(foundTriggerIdx, movementFile, sizeof(movementFile));
  3542.                        
  3543.                         if (g_bPlayerReady[client][foundTriggerIdx])
  3544.                         {
  3545.                             MarkPlayerInGroupActivation(groupNumber, client, true);
  3546.                             activatedPlayers.Push(client);
  3547.                             activatedFiles.PushString(movementFile);
  3548.                            
  3549.                             ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP_MULTI, groupNumber);
  3550.                             DebugPrint("AttemptGroupActivation - Híbrido %N (%s) activado para grupo %d",
  3551.                                 client, hybridTypes[h], groupNumber);
  3552.                         }
  3553.                     }
  3554.                 }
  3555.             }
  3556.             delete hybridPlayers;
  3557.         }
  3558.        
  3559.         // 2. SEGUNDO: Activar boosters puros
  3560.         if (hasBile)
  3561.         {
  3562.             ArrayList bilePlayers = GetReadyPlayersByType(groupNumber, "bile");
  3563.             if (bilePlayers != null && bilePlayers.Length > 0)
  3564.             {
  3565.                 for (int i = 0; i < bilePlayers.Length; i++)
  3566.                 {
  3567.                     int client = bilePlayers.Get(i);
  3568.                    
  3569.                     if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  3570.                     {
  3571.                         if (g_bClientInMovement[client])
  3572.                         {
  3573.                             DebugPrint("AttemptGroupActivation - %N ya está en movimiento en el grupo %d - saltando", client, groupNumber);
  3574.                             continue;
  3575.                         }
  3576.                         else
  3577.                         {
  3578.                             DebugPrint("AttemptGroupActivation - %N completó movimiento previo, puede unirse al grupo %d de nuevo", client, groupNumber);
  3579.                             MarkPlayerInGroupActivation(groupNumber, client, false);
  3580.                         }
  3581.                     }
  3582.                    
  3583.                     int foundTriggerIdx = FindReadyTriggerForPlayer(client, "bile");
  3584.                     if (foundTriggerIdx != -1)
  3585.                     {
  3586.                         char movementFile[128];
  3587.                         GetMovementFileForTrigger(foundTriggerIdx, movementFile, sizeof(movementFile));
  3588.                        
  3589.                         if (g_bPlayerReady[client][foundTriggerIdx])
  3590.                         {
  3591.                             MarkPlayerInGroupActivation(groupNumber, client, true);
  3592.                             activatedPlayers.Push(client);
  3593.                             activatedFiles.PushString(movementFile);
  3594.                            
  3595.                             ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP_MULTI, groupNumber);
  3596.                             DebugPrint("AttemptGroupActivation - Bile puro %N activado para grupo %d", client, groupNumber);
  3597.                             break;
  3598.                         }
  3599.                     }
  3600.                 }
  3601.             }
  3602.             delete bilePlayers;
  3603.         }
  3604.        
  3605.         if (hasGL)
  3606.         {
  3607.             ArrayList glPlayers = GetReadyPlayersByType(groupNumber, "gl");
  3608.             if (glPlayers != null && glPlayers.Length > 0)
  3609.             {
  3610.                 for (int i = 0; i < glPlayers.Length; i++)
  3611.                 {
  3612.                     int client = glPlayers.Get(i);
  3613.                    
  3614.                     if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  3615.                     {
  3616.                         if (g_bClientInMovement[client])
  3617.                         {
  3618.                             DebugPrint("AttemptGroupActivation - %N ya está en movimiento en el grupo %d - saltando", client, groupNumber);
  3619.                             continue;
  3620.                         }
  3621.                         else
  3622.                         {
  3623.                             DebugPrint("AttemptGroupActivation - %N completó movimiento previo, puede unirse al grupo %d de nuevo", client, groupNumber);
  3624.                             MarkPlayerInGroupActivation(groupNumber, client, false);
  3625.                         }
  3626.                     }
  3627.                    
  3628.                     int foundTriggerIdx = FindReadyTriggerForPlayer(client, "gl");
  3629.                     if (foundTriggerIdx != -1)
  3630.                     {
  3631.                         char movementFile[128];
  3632.                         GetMovementFileForTrigger(foundTriggerIdx, movementFile, sizeof(movementFile));
  3633.                        
  3634.                         if (g_bPlayerReady[client][foundTriggerIdx])
  3635.                         {
  3636.                             MarkPlayerInGroupActivation(groupNumber, client, true);
  3637.                             activatedPlayers.Push(client);
  3638.                             activatedFiles.PushString(movementFile);
  3639.                            
  3640.                             ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP_MULTI, groupNumber);
  3641.                             DebugPrint("AttemptGroupActivation - GL puro %N activado para grupo %d", client, groupNumber);
  3642.                             break;
  3643.                         }
  3644.                     }
  3645.                 }
  3646.             }
  3647.             delete glPlayers;
  3648.         }
  3649.        
  3650.         if (hasPM)
  3651.         {
  3652.             ArrayList pmPlayers = GetReadyPlayersByType(groupNumber, "pm");
  3653.             if (pmPlayers != null && pmPlayers.Length > 0)
  3654.             {
  3655.                 for (int i = 0; i < pmPlayers.Length; i++)
  3656.                 {
  3657.                     int client = pmPlayers.Get(i);
  3658.                    
  3659.                     if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  3660.                     {
  3661.                         if (g_bClientInMovement[client])
  3662.                         {
  3663.                             DebugPrint("AttemptGroupActivation - %N ya está en movimiento en el grupo %d - saltando", client, groupNumber);
  3664.                             continue;
  3665.                         }
  3666.                         else
  3667.                         {
  3668.                             DebugPrint("AttemptGroupActivation - %N completó movimiento previo, puede unirse al grupo %d de nuevo", client, groupNumber);
  3669.                             MarkPlayerInGroupActivation(groupNumber, client, false);
  3670.                         }
  3671.                     }
  3672.                    
  3673.                     int foundTriggerIdx = FindReadyTriggerForPlayer(client, "pm");
  3674.                     if (foundTriggerIdx != -1)
  3675.                     {
  3676.                         char movementFile[128];
  3677.                         GetMovementFileForTrigger(foundTriggerIdx, movementFile, sizeof(movementFile));
  3678.                        
  3679.                         if (g_bPlayerReady[client][foundTriggerIdx])
  3680.                         {
  3681.                             MarkPlayerInGroupActivation(groupNumber, client, true);
  3682.                             activatedPlayers.Push(client);
  3683.                             activatedFiles.PushString(movementFile);
  3684.                            
  3685.                             ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP_MULTI, groupNumber);
  3686.                             DebugPrint("AttemptGroupActivation - PM puro %N activado para grupo %d", client, groupNumber);
  3687.                             break;
  3688.                         }
  3689.                     }
  3690.                 }
  3691.             }
  3692.             delete pmPlayers;
  3693.         }
  3694.        
  3695.         // 3. TERCERO: Activar runners puros ADICIONALES
  3696.         int runnersActivated = 0;
  3697.         for (int i = 0; i < activatedPlayers.Length; i++)
  3698.         {
  3699.             int client = activatedPlayers.Get(i);
  3700.            
  3701.             for (int j = 0; j < groupTriggers.Length; j++)
  3702.             {
  3703.                 int currentTriggerIdx = groupTriggers.Get(j);
  3704.                 if (g_bPlayerReady[client][currentTriggerIdx])
  3705.                 {
  3706.                     char type[16];
  3707.                     strcopy(type, sizeof(type), g_sTriggerType[currentTriggerIdx]);
  3708.                    
  3709.                     if (StrEqual(type, "run") || StrContains(type, "run") != -1)
  3710.                     {
  3711.                         runnersActivated++;
  3712.                         break;
  3713.                     }
  3714.                 }
  3715.             }
  3716.         }
  3717.        
  3718.         DebugPrint("AttemptGroupActivation - Runners ya activados: %d, Capacidad del grupo: %d",
  3719.             runnersActivated, groupCapacity);
  3720.        
  3721.         if (runnersActivated < groupCapacity)
  3722.         {
  3723.             ArrayList additionalRunners = GetReadyPlayersByType(groupNumber, "run");
  3724.             if (additionalRunners != null && additionalRunners.Length > 0)
  3725.             {
  3726.                 for (int i = 0; i < additionalRunners.Length && runnersActivated < groupCapacity; i++)
  3727.                 {
  3728.                     int client = additionalRunners.Get(i);
  3729.                    
  3730.                     if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  3731.                     {
  3732.                         if (g_bClientInMovement[client])
  3733.                         {
  3734.                             DebugPrint("AttemptGroupActivation - %N ya está en movimiento en el grupo %d - saltando", client, groupNumber);
  3735.                             continue;
  3736.                         }
  3737.                         else
  3738.                         {
  3739.                             DebugPrint("AttemptGroupActivation - %N completó movimiento previo, puede unirse al grupo %d de nuevo", client, groupNumber);
  3740.                             MarkPlayerInGroupActivation(groupNumber, client, false);
  3741.                         }
  3742.                     }
  3743.                    
  3744.                     int foundTriggerIdx = FindReadyTriggerForPlayer(client, "run");
  3745.                     if (foundTriggerIdx != -1)
  3746.                     {
  3747.                         char movementFile[128];
  3748.                         GetMovementFileForTrigger(foundTriggerIdx, movementFile, sizeof(movementFile));
  3749.                        
  3750.                         if (g_bPlayerReady[client][foundTriggerIdx])
  3751.                         {
  3752.                             MarkPlayerInGroupActivation(groupNumber, client, true);
  3753.                             activatedPlayers.Push(client);
  3754.                             activatedFiles.PushString(movementFile);
  3755.                            
  3756.                             ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP_MULTI, groupNumber);
  3757.                             runnersActivated++;
  3758.                             DebugPrint("AttemptGroupActivation - Runner adicional %N activado para grupo %d (%d/%d)",
  3759.                                 client, groupNumber, runnersActivated, groupCapacity);
  3760.                         }
  3761.                     }
  3762.                 }
  3763.             }
  3764.             delete additionalRunners;
  3765.         }
  3766.        
  3767.         DebugPrint("AttemptGroupActivation - Grupo %d activado: %d jugadores (capacidad: %d)",
  3768.             groupNumber, activatedPlayers.Length, groupCapacity);
  3769.     }
  3770.     else
  3771.     {
  3772.         // Para grupos de 1-2 tipos, usar ActivateGroupInOrder
  3773.         ActivateGroupInOrder(groupNumber, groupCapacity, activatedFiles);
  3774.        
  3775.         // Obtener jugadores activados
  3776.         for (int i = 1; i <= MaxClients; i++)
  3777.         {
  3778.             if (IsValidSurvivor(i) && IsPlayerAlreadyInGroupActivation(groupNumber, i))
  3779.             {
  3780.                 activatedPlayers.Push(i);
  3781.             }
  3782.         }
  3783.     }
  3784.    
  3785.     // Mostrar resumen de activación
  3786.     if (activatedPlayers.Length > 0)
  3787.     {
  3788.         ShowGroupActivationSummary(groupNumber, activatedPlayers, activatedFiles);
  3789.     }
  3790.     else
  3791.     {
  3792.         DebugPrint("AttemptGroupActivation - ¡ADVERTENCIA! Grupo %d activado pero 0 jugadores registrados", groupNumber);
  3793.     }
  3794.    
  3795.     // Limpiar
  3796.     delete activatedPlayers;
  3797.     delete activatedFiles;
  3798.     delete playersToCheck;
  3799.    
  3800.     // Aplicar refire_time grupal
  3801.     ApplyGroupRefireTime(groupNumber);
  3802.    
  3803.     DebugPrint("AttemptGroupActivation - Grupo %d activado exitosamente", groupNumber);
  3804. }
  3805.  
  3806. /**
  3807.  * Obtiene TODOS los jugadores listos para un tipo específico (no solo el primero)
  3808.  */
  3809. ArrayList GetAllReadyPlayersForType(int groupNumber, const char[] type)
  3810. {
  3811.     ArrayList players = new ArrayList();
  3812.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  3813.    
  3814.     if (groupTriggers == null)
  3815.         return players;
  3816.    
  3817.     for (int i = 0; i < groupTriggers.Length; i++)
  3818.     {
  3819.         int triggerIndex = groupTriggers.Get(i);
  3820.        
  3821.         // Verificar si este trigger es del tipo buscado
  3822.         if (StrEqual(g_sTriggerType[triggerIndex], type))
  3823.         {
  3824.             // Agregar TODOS los jugadores listos en este trigger
  3825.             for (int client = 1; client <= MaxClients; client++)
  3826.             {
  3827.                 if (IsValidSurvivor(client) && g_bPlayerReady[client][triggerIndex])
  3828.                 {
  3829.                     // Verificar que el jugador no esté ya en la lista
  3830.                     if (players.FindValue(client) == -1)
  3831.                     {
  3832.                         players.Push(client);
  3833.                     }
  3834.                 }
  3835.             }
  3836.         }
  3837.     }
  3838.    
  3839.     return players;
  3840. }
  3841.  
  3842. // ========================================================================
  3843. // FUNCIÓN NUEVA: OBTENER CAPACIDAD DEL GRUPO (FIJA)
  3844. // ========================================================================
  3845.  
  3846. int GetGroupBoosterCapacityFixed(int groupNumber)
  3847. {
  3848.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  3849.     if (groupTypes == null)
  3850.     {
  3851.         DebugPrint("GetGroupBoosterCapacityFixed - Grupo %d no tiene tipos -> 1 runner", groupNumber);
  3852.         return 1;
  3853.     }
  3854.    
  3855.     DebugPrint("GetGroupBoosterCapacityFixed - Analizando grupo %d:", groupNumber);
  3856.    
  3857.     // 1. Verificar si el grupo tiene booster (PM, Bile, GL)
  3858.     bool hasBooster = false;
  3859.     int boosterCapacity = 1;
  3860.    
  3861.     for (int i = 0; i < groupTypes.Length; i++)
  3862.     {
  3863.         char type[16];
  3864.         groupTypes.GetString(i, type, sizeof(type));
  3865.        
  3866.         if (StrEqual(type, "pm") || StrEqual(type, "bile") || StrEqual(type, "gl") ||
  3867.             StrEqual(type, "runpm") || StrEqual(type, "runbile") || StrEqual(type, "rungl"))
  3868.         {
  3869.             hasBooster = true;
  3870.             boosterCapacity = MA_GetBoosterCapacity(type);
  3871.             DebugPrint("GetGroupBoosterCapacityFixed - Encontrado booster %s -> %d runners", type, boosterCapacity);
  3872.             break;
  3873.         }
  3874.     }
  3875.    
  3876.     if (hasBooster)
  3877.     {
  3878.         // 2. Grupo CON booster: capacidad limitada por el booster
  3879.         DebugPrint("GetGroupBoosterCapacityFixed - Grupo %d TIENE booster -> %d runners máx", groupNumber, boosterCapacity);
  3880.         return boosterCapacity;
  3881.     }
  3882.     else
  3883.     {
  3884.         // 3. Grupo SIN booster (solo runners): capacidad máxima 32
  3885.         DebugPrint("GetGroupBoosterCapacityFixed - Grupo %d SIN booster -> 32 runners máx", groupNumber);
  3886.         return 32;
  3887.     }
  3888. }
  3889.  
  3890. // NUEVA FUNCIÓN: Verificar que todos los tipos tengan jugadores DIFERENTES
  3891. bool AllTypesHaveDifferentPlayers(int groupNumber)
  3892. {
  3893.     ArrayList playersFound = new ArrayList();
  3894.    
  3895.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  3896.     if (groupTypes == null || groupTypes.Length == 0)
  3897.     {
  3898.         delete playersFound;
  3899.         return true;
  3900.     }
  3901.    
  3902.     for (int i = 0; i < groupTypes.Length; i++)
  3903.     {
  3904.         char type[16];
  3905.         groupTypes.GetString(i, type, sizeof(type));
  3906.        
  3907.         // Obtener el primer jugador listo de este tipo
  3908.         int playerForType = GetFirstReadyPlayerForType(groupNumber, type);
  3909.        
  3910.         if (playerForType != -1)
  3911.         {
  3912.             // Verificar si este jugador ya está en la lista (activando otro rol)
  3913.             if (playersFound.FindValue(playerForType) != -1)
  3914.             {
  3915.                 DebugPrint("AllTypesHaveDifferentPlayers - ¡Jugador duplicado! %N activando múltiples roles", playerForType);
  3916.                 delete playersFound;
  3917.                 return false;
  3918.             }
  3919.            
  3920.             playersFound.Push(playerForType);
  3921.         }
  3922.     }
  3923.    
  3924.     delete playersFound;
  3925.     return true;
  3926. }
  3927.  
  3928. // NUEVA FUNCIÓN: Obtener el primer jugador listo para un tipo específico
  3929. int GetFirstReadyPlayerForType(int groupNumber, const char[] type)
  3930. {
  3931.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  3932.    
  3933.     if (groupTriggers == null)
  3934.         return -1;
  3935.    
  3936.     for (int i = 0; i < groupTriggers.Length; i++)
  3937.     {
  3938.         int triggerIndex = groupTriggers.Get(i);
  3939.        
  3940.         // Verificar si este trigger es del tipo buscado
  3941.         if (StrEqual(g_sTriggerType[triggerIndex], type))
  3942.         {
  3943.             // Buscar primer jugador listo en este trigger
  3944.             for (int client = 1; client <= MaxClients; client++)
  3945.             {
  3946.                 if (IsValidSurvivor(client) && g_bPlayerReady[client][triggerIndex])
  3947.                 {
  3948.                     return client;
  3949.                 }
  3950.             }
  3951.         }
  3952.     }
  3953.    
  3954.     return -1;
  3955. }
  3956.  
  3957. char[] GetTypeDisplayName(const char[] type)
  3958. {
  3959.     char displayName[16];
  3960.    
  3961.     if (StrEqual(type, "pm")) strcopy(displayName, sizeof(displayName), "PM");
  3962.     else if (StrEqual(type, "bile")) strcopy(displayName, sizeof(displayName), "Bile");
  3963.     else if (StrEqual(type, "gl")) strcopy(displayName, sizeof(displayName), "GL");
  3964.     else if (StrEqual(type, "run")) strcopy(displayName, sizeof(displayName), "Runner");
  3965.     else if (StrEqual(type, "runpm")) strcopy(displayName, sizeof(displayName), "Runner+PM");
  3966.     else if (StrEqual(type, "runbile")) strcopy(displayName, sizeof(displayName), "Runner+Bile");
  3967.     else if (StrEqual(type, "rungl")) strcopy(displayName, sizeof(displayName), "Runner+GL");
  3968.     else if (StrEqual(type, "dual")) strcopy(displayName, sizeof(displayName), "Dual");
  3969.     else strcopy(displayName, sizeof(displayName), type);
  3970.    
  3971.     return displayName;
  3972. }
  3973.  
  3974. int CountTotalReadyPlayers(int groupNumber)
  3975. {
  3976.     int count = 0;
  3977.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  3978.    
  3979.     if (groupTriggers == null) return 0;
  3980.    
  3981.     for (int i = 0; i < groupTriggers.Length; i++)
  3982.     {
  3983.         int triggerIndex = groupTriggers.Get(i);
  3984.        
  3985.         // Contar jugadores listos en este trigger
  3986.         for (int client = 1; client <= MaxClients; client++)
  3987.         {
  3988.             if (IsValidSurvivor(client) && g_bPlayerReady[client][triggerIndex])
  3989.             {
  3990.                 count++;
  3991.                 break; // Solo contar una vez por jugador (en caso de múltiples triggers)
  3992.             }
  3993.         }
  3994.     }
  3995.    
  3996.     return count;
  3997. }
  3998.  
  3999. bool AllGroupTypesHaveReadyPlayers(int groupNumber)
  4000. {
  4001.     DebugPrint("AllGroupTypesHaveReadyPlayers - Verificando grupo %d", groupNumber);
  4002.    
  4003.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  4004.    
  4005.     if (groupTypes == null || groupTypes.Length == 0)
  4006.     {
  4007.         DebugPrint("AllGroupTypesHaveReadyPlayers - El grupo %d no tiene tipos", groupNumber);
  4008.         return false;
  4009.     }
  4010.    
  4011.     for (int i = 0; i < groupTypes.Length; i++)
  4012.     {
  4013.         char type[16];
  4014.         groupTypes.GetString(i, type, sizeof(type));
  4015.        
  4016.         DebugPrint("AllGroupTypesHaveReadyPlayers - Verificando tipo: %s", type);
  4017.        
  4018.         if (!TypeHasReadyPlayers(groupNumber, type))
  4019.         {
  4020.             DebugPrint("AllGroupTypesHaveReadyPlayers - El tipo %s NO tiene jugadores listos", type);
  4021.             return false;
  4022.         }
  4023.        
  4024.         DebugPrint("AllGroupTypesHaveReadyPlayers - El tipo %s SÍ tiene jugadores listos", type);
  4025.     }
  4026.    
  4027.     DebugPrint("AllGroupTypesHaveReadyPlayers - TODOS los tipos tienen jugadores listos");
  4028.     return true;
  4029. }
  4030.  
  4031. bool TypeHasReadyPlayers(int groupNumber, const char[] type)
  4032. {
  4033.     DebugPrint("TypeHasReadyPlayers - Verificando tipo %s en grupo %d", type, groupNumber);
  4034.    
  4035.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  4036.     if (groupTriggers == null)
  4037.         return false;
  4038.    
  4039.     // Detectar si es un tipo híbrido (runpm, runbile, rungl)
  4040.     bool isHybrid = StrEqual(type, "runpm") || StrEqual(type, "runbile") || StrEqual(type, "rungl");
  4041.    
  4042.     // Determinar el arma requerida para híbridos
  4043.     char requiredWeapon[8] = "";
  4044.     if (StrEqual(type, "runpm")) strcopy(requiredWeapon, sizeof(requiredWeapon), "pm");
  4045.     else if (StrEqual(type, "runbile")) strcopy(requiredWeapon, sizeof(requiredWeapon), "bile");
  4046.     else if (StrEqual(type, "rungl")) strcopy(requiredWeapon, sizeof(requiredWeapon), "gl");
  4047.    
  4048.     for (int i = 0; i < groupTriggers.Length; i++)
  4049.     {
  4050.         int triggerIndex = groupTriggers.Get(i);
  4051.         if (StrEqual(g_sTriggerType[triggerIndex], type))
  4052.         {
  4053.             for (int client = 1; client <= MaxClients; client++)
  4054.             {
  4055.                 if (!IsValidSurvivor(client))
  4056.                     continue;
  4057.                
  4058.                 // Si es híbrido, verificar si el jugador está en el trigger y tiene el arma
  4059.                 if (isHybrid)
  4060.                 {
  4061.                     if (g_bPlayerInTrigger[client][triggerIndex] && HasRequiredWeapon(client, requiredWeapon))
  4062.                     {
  4063.                         DebugPrint("TypeHasReadyPlayers - Jugador %N con arma %s en trigger %d para tipo %s",
  4064.                             client, requiredWeapon, triggerIndex, type);
  4065.                         return true;
  4066.                     }
  4067.                 }
  4068.                 else
  4069.                 {
  4070.                     // Para tipos normales (bile, pm, gl, run), usar g_bPlayerReady
  4071.                     if (g_bPlayerReady[client][triggerIndex])
  4072.                     {
  4073.                         DebugPrint("TypeHasReadyPlayers - Jugador %N listo en trigger %d para tipo %s",
  4074.                             client, triggerIndex, type);
  4075.                         return true;
  4076.                     }
  4077.                 }
  4078.             }
  4079.         }
  4080.     }
  4081.    
  4082.     DebugPrint("TypeHasReadyPlayers - Tipo %s NO tiene jugadores listos", type);
  4083.     return false;
  4084. }
  4085.  
  4086. int CalculateGroupCapacity(int groupNumber)
  4087. {
  4088.     int maxCapacity = 0;
  4089.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  4090.    
  4091.     for (int i = 0; i < groupTypes.Length; i++)
  4092.     {
  4093.         char type[16];
  4094.         groupTypes.GetString(i, type, sizeof(type));
  4095.        
  4096.         int capacity = MA_GetBoosterCapacity(type);
  4097.         if (capacity > maxCapacity)
  4098.         {
  4099.             maxCapacity = capacity;
  4100.         }
  4101.     }
  4102.    
  4103.     return maxCapacity;
  4104. }
  4105.  
  4106. int CountReadyRunners(int groupNumber)
  4107. {
  4108.     int count = 0;
  4109.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  4110.    
  4111.     for (int i = 0; i < groupTriggers.Length; i++)
  4112.     {
  4113.         int triggerIndex = groupTriggers.Get(i);
  4114.        
  4115.         // Solo contar triggers de tipo "run" o híbridos
  4116.         char type[16];
  4117.         strcopy(type, sizeof(type), g_sTriggerType[triggerIndex]);
  4118.        
  4119.         if (StrEqual(type, "run") || StrContains(type, "run") != -1)
  4120.         {
  4121.             // Contar jugadores listos en este trigger
  4122.             for (int client = 1; client <= MaxClients; client++)
  4123.             {
  4124.                 if (IsValidSurvivor(client) && g_bPlayerReady[client][triggerIndex])
  4125.                 {
  4126.                     count++;
  4127.                 }
  4128.             }
  4129.         }
  4130.     }
  4131.    
  4132.     return count;
  4133. }
  4134.  
  4135. // ========================================================================
  4136. // FUNCIÓN MODIFICADA: ACTIVAR GRUPO CON CAPACIDAD DE BOOSTER COMPLETA
  4137. // ========================================================================
  4138.  
  4139. // ========================================================================
  4140. // FUNCIÓN MODIFICADA: ACTIVAR GRUPO CON CAPACIDAD DE BOOSTER COMPLETA
  4141. // ========================================================================
  4142.  
  4143. public void ActivateGroupInOrder(int groupNumber, int runnerLimit, ArrayList &activatedFiles)
  4144. {
  4145.     DebugPrint("ActivateGroupInOrder - Activando grupo %d en orden (NUEVA VERSIÓN CON HÍBRIDOS)", groupNumber);
  4146.    
  4147.     if (activatedFiles == null)
  4148.     {
  4149.         activatedFiles = new ArrayList(ByteCountToCells(128));
  4150.     }
  4151.    
  4152.     // Obtener todos los tipos en este grupo
  4153.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  4154.     if (groupTypes == null)
  4155.     {
  4156.         DebugPrint("ActivateGroupInOrder - ERROR: El grupo %d no tiene tipos", groupNumber);
  4157.         return;
  4158.     }
  4159.    
  4160.     // ================================================================
  4161.     // DETECTAR SI EL GRUPO TIENE HÍBRIDOS
  4162.     // ================================================================
  4163.     bool hasHybrid = false;
  4164.     for (int i = 0; i < groupTypes.Length; i++)
  4165.     {
  4166.         char type[16];
  4167.         groupTypes.GetString(i, type, sizeof(type));
  4168.         if (StrEqual(type, "runpm") || StrEqual(type, "runbile") || StrEqual(type, "rungl"))
  4169.         {
  4170.             hasHybrid = true;
  4171.             break;
  4172.         }
  4173.     }
  4174.     DebugPrint("ActivateGroupInOrder - Grupo %d tiene híbridos: %s", groupNumber, hasHybrid ? "SÍ" : "NO");
  4175.    
  4176.     // Determinar el tipo de booster y su capacidad
  4177.     char boosterType[16] = "";
  4178.     int boosterCapacity = 0;
  4179.    
  4180.     for (int i = 0; i < groupTypes.Length; i++)
  4181.     {
  4182.         char type[16];
  4183.         groupTypes.GetString(i, type, sizeof(type));
  4184.        
  4185.         if (StrEqual(type, "pm") || StrEqual(type, "bile") || StrEqual(type, "gl"))
  4186.         {
  4187.             strcopy(boosterType, sizeof(boosterType), type);
  4188.             boosterCapacity = MA_GetBoosterCapacity(type);
  4189.             DebugPrint("ActivateGroupInOrder - Booster encontrado: %s, Capacidad: %d runners", boosterType, boosterCapacity);
  4190.             break;
  4191.         }
  4192.     }
  4193.    
  4194.     if (strlen(boosterType) == 0)
  4195.     {
  4196.         DebugPrint("ActivateGroupInOrder - Grupo %d no tiene booster puro", groupNumber);
  4197.         // Si no hay booster puro, verificar si hay híbridos
  4198.         for (int i = 0; i < groupTypes.Length; i++)
  4199.         {
  4200.             char type[16];
  4201.             groupTypes.GetString(i, type, sizeof(type));
  4202.            
  4203.             if (StrEqual(type, "runpm") || StrEqual(type, "runbile") || StrEqual(type, "rungl"))
  4204.             {
  4205.                 strcopy(boosterType, sizeof(boosterType), type);
  4206.                 boosterCapacity = MA_GetBoosterCapacity(type);
  4207.                 DebugPrint("ActivateGroupInOrder - Híbrido encontrado: %s, Capacidad: %d runners", boosterType, boosterCapacity);
  4208.                 break;
  4209.             }
  4210.         }
  4211.     }
  4212.    
  4213.     // Si aún no encontramos capacidad, usar capacidad por defecto
  4214.     if (boosterCapacity == 0)
  4215.     {
  4216.         boosterCapacity = 1; // Capacidad por defecto
  4217.         DebugPrint("ActivateGroupInOrder - Usando capacidad por defecto: %d runners", boosterCapacity);
  4218.     }
  4219.    
  4220.     // Array para almacenar jugadores activados
  4221.     ArrayList activatedPlayers = new ArrayList();
  4222.     int totalRunnersActivated = 0;
  4223.     bool boosterActivated = false;
  4224.     ArrayList groupTriggers = GetGroupTriggers(groupNumber); // Necesario para buscar triggers
  4225.    
  4226.     // ================================================================
  4227.     // PASO 1: ACTIVAR HÍBRIDOS PRIMERO (runpm, runbile, rungl)
  4228.     // ================================================================
  4229.     char hybridTypes[][] = {"runpm", "runbile", "rungl"};
  4230.     int hybridsActivated = 0;  // Contador de híbridos activados
  4231.    
  4232.     for (int h = 0; h < sizeof(hybridTypes) && totalRunnersActivated < boosterCapacity; h++)
  4233.     {
  4234.         // Obtener TODOS los jugadores en esta posición híbrida
  4235.         ArrayList allHybridPlayers = GetAllReadyPlayersInHybridPosition(groupNumber, hybridTypes[h]);
  4236.        
  4237.         if (allHybridPlayers != null && allHybridPlayers.Length > 0)
  4238.         {
  4239.             // Determinar qué arma requiere este híbrido
  4240.             char requiredWeapon[8];
  4241.             if (StrEqual(hybridTypes[h], "runpm")) strcopy(requiredWeapon, sizeof(requiredWeapon), "pm");
  4242.             else if (StrEqual(hybridTypes[h], "runbile")) strcopy(requiredWeapon, sizeof(requiredWeapon), "bile");
  4243.             else if (StrEqual(hybridTypes[h], "rungl")) strcopy(requiredWeapon, sizeof(requiredWeapon), "gl");
  4244.             else strcopy(requiredWeapon, sizeof(requiredWeapon), "pm");
  4245.            
  4246.             // ============================================================
  4247.             // VERIFICACIÓN CRÍTICA: ¿Al menos un jugador tiene el arma?
  4248.             // ============================================================
  4249.             bool hasArmedPlayer = false;
  4250.             int armedClient = -1;
  4251.            
  4252.             for (int i = 0; i < allHybridPlayers.Length; i++)
  4253.             {
  4254.                 int client = allHybridPlayers.Get(i);
  4255.                 if (HasRequiredWeapon(client, requiredWeapon))
  4256.                 {
  4257.                     hasArmedPlayer = true;
  4258.                     armedClient = client;
  4259.                     break;
  4260.                 }
  4261.             }
  4262.            
  4263.             if (!hasArmedPlayer)
  4264.             {
  4265.                 DebugPrint("ActivateGroupInOrder - Ningún jugador tiene %s en posición híbrida %s - saltando",
  4266.                     requiredWeapon, hybridTypes[h]);
  4267.                
  4268.                 // Mostrar mensaje de espera
  4269.                 PrintToChatAll("[\x04MA Grupo (%d)\x01] Esperando un jugador con %s en posición híbrida...",
  4270.                     groupNumber, requiredWeapon);
  4271.                
  4272.                 delete allHybridPlayers;
  4273.                 continue; // Saltar este híbrido
  4274.             }
  4275.            
  4276.             DebugPrint("ActivateGroupInOrder - Jugador %N tiene %s en posición híbrida %s - activando a TODOS",
  4277.                 armedClient, requiredWeapon, hybridTypes[h]);
  4278.            
  4279.             // ============================================================
  4280.             // ACTIVAR A TODOS los jugadores en la posición híbrida
  4281.             // ============================================================
  4282.             for (int i = 0; i < allHybridPlayers.Length && totalRunnersActivated < boosterCapacity; i++)
  4283.             {
  4284.                 int client = allHybridPlayers.Get(i);
  4285.                
  4286.                 // Verificar que el jugador no esté ya en activación
  4287.                 if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  4288.                 {
  4289.                     DebugPrint("ActivateGroupInOrder - %N ya está en activación del grupo %d - saltando", client, groupNumber);
  4290.                     continue;
  4291.                 }
  4292.                
  4293.                 // ================================================================
  4294.                 // NUEVO: Buscar un trigger de este tipo donde el jugador esté
  4295.                 // Usamos g_bPlayerInTrigger, NO g_bPlayerReady
  4296.                 // ================================================================
  4297.                 int triggerIndex = -1;
  4298.                 for (int j = 0; j < groupTriggers.Length; j++)
  4299.                 {
  4300.                     int idx = groupTriggers.Get(j);
  4301.                     if (StrEqual(g_sTriggerType[idx], hybridTypes[h]) && g_bPlayerInTrigger[client][idx])
  4302.                     {
  4303.                         triggerIndex = idx;
  4304.                         break;
  4305.                     }
  4306.                 }
  4307.                
  4308.                 if (triggerIndex == -1)
  4309.                 {
  4310.                     DebugPrint("ActivateGroupInOrder - %N no está en trigger de tipo %s, saltando", client, hybridTypes[h]);
  4311.                     continue;
  4312.                 }
  4313.                
  4314.                 char movementFile[128];
  4315.                 GetMovementFileForTrigger(triggerIndex, movementFile, sizeof(movementFile));
  4316.                
  4317.                 // Marcar jugador como en activación
  4318.                 MarkPlayerInGroupActivation(groupNumber, client, true);
  4319.                
  4320.                 // Almacenar para mensaje grupal
  4321.                 activatedPlayers.Push(client);
  4322.                 activatedFiles.PushString(movementFile);
  4323.                
  4324.                 // EJECUTAR MOVIMIENTO - TODOS los jugadores en posición híbrida son runners
  4325.                 ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP, groupNumber);
  4326.                 totalRunnersActivated++;
  4327.                 hybridsActivated++;
  4328.                
  4329.                 DebugPrint("ActivateGroupInOrder - Híbrido %N activado como runner (%d/%d)",
  4330.                     client, totalRunnersActivated, boosterCapacity);
  4331.             }
  4332.         }
  4333.         delete allHybridPlayers;
  4334.     }
  4335.    
  4336.     // ================================================================
  4337.     // SI EL GRUPO TIENE HÍBRIDOS Y NO SE ACTIVÓ NINGUNO, NO CONTINUAR
  4338.     // ================================================================
  4339.     if (hasHybrid && hybridsActivated == 0)
  4340.     {
  4341.         DebugPrint("ActivateGroupInOrder - Grupo %d tiene híbridos pero no se activó ninguno. No se activan boosters puros.", groupNumber);
  4342.         delete activatedPlayers;
  4343.         return;
  4344.     }
  4345.    
  4346.     // ================================================================
  4347.     // PASO 2: ACTIVAR RUNNERS PUROS ADICIONALES
  4348.     // ================================================================
  4349.     if (totalRunnersActivated < boosterCapacity)
  4350.     {
  4351.         ArrayList runners = GetReadyPlayersByType(groupNumber, "run");
  4352.         if (runners != null && runners.Length > 0)
  4353.         {
  4354.             DebugPrint("ActivateGroupInOrder - Encontrados %d runners puros adicionales", runners.Length);
  4355.            
  4356.             for (int i = 0; i < runners.Length && totalRunnersActivated < boosterCapacity; i++)
  4357.             {
  4358.                 int client = runners.Get(i);
  4359.                
  4360.                 // Verificar que el jugador no esté ya en activación
  4361.                 if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  4362.                 {
  4363.                     DebugPrint("ActivateGroupInOrder - %N ya está en activación del grupo %d - saltando", client, groupNumber);
  4364.                     continue;
  4365.                 }
  4366.                
  4367.                 int triggerIndex = FindReadyTriggerForPlayer(client, "run");
  4368.                 if (triggerIndex != -1)
  4369.                 {
  4370.                     char movementFile[128];
  4371.                     GetMovementFileForTrigger(triggerIndex, movementFile, sizeof(movementFile));
  4372.                    
  4373.                     // Marcar jugador como en activación
  4374.                     MarkPlayerInGroupActivation(groupNumber, client, true);
  4375.                    
  4376.                     // Almacenar para mensaje grupal
  4377.                     activatedPlayers.Push(client);
  4378.                     activatedFiles.PushString(movementFile);
  4379.                    
  4380.                     // Ejecutar movimiento del runner
  4381.                     ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP, groupNumber);
  4382.                     totalRunnersActivated++;
  4383.                    
  4384.                     DebugPrint("ActivateGroupInOrder - Runner puro adicional %N activado (%d/%d)",
  4385.                         client, totalRunnersActivated, boosterCapacity);
  4386.                 }
  4387.             }
  4388.         }
  4389.         delete runners;
  4390.     }
  4391.    
  4392.     // ================================================================
  4393.     // PASO 3: ACTIVAR BOOSTERS PUROS (pm, bile, gl) - ÚLTIMO
  4394.     // ================================================================
  4395.     char boosterTypes[][] = {"pm", "bile", "gl"};
  4396.    
  4397.     for (int b = 0; b < sizeof(boosterTypes) && !boosterActivated; b++)
  4398.     {
  4399.         ArrayList boosters = GetReadyPlayersByType(groupNumber, boosterTypes[b]);
  4400.         if (boosters != null && boosters.Length > 0)
  4401.         {
  4402.             DebugPrint("ActivateGroupInOrder - Encontrados %d boosters tipo %s",
  4403.                 boosters.Length, boosterTypes[b]);
  4404.            
  4405.             // Activar solo UN booster (el primero disponible)
  4406.             for (int i = 0; i < boosters.Length && !boosterActivated; i++)
  4407.             {
  4408.                 int client = boosters.Get(i);
  4409.                
  4410.                 // Verificar que el jugador no esté ya en activación
  4411.                 if (IsPlayerAlreadyInGroupActivation(groupNumber, client))
  4412.                 {
  4413.                     DebugPrint("ActivateGroupInOrder - %N ya está en activación del grupo %d - saltando", client, groupNumber);
  4414.                     continue;
  4415.                 }
  4416.                
  4417.                 int triggerIndex = FindReadyTriggerForPlayer(client, boosterTypes[b]);
  4418.                 if (triggerIndex != -1)
  4419.                 {
  4420.                     char movementFile[128];
  4421.                     GetMovementFileForTrigger(triggerIndex, movementFile, sizeof(movementFile));
  4422.                    
  4423.                     // Marcar jugador como en activación
  4424.                     MarkPlayerInGroupActivation(groupNumber, client, true);
  4425.                    
  4426.                     // Almacenar para mensaje grupal
  4427.                     activatedPlayers.Push(client);
  4428.                     activatedFiles.PushString(movementFile);
  4429.                    
  4430.                     // EJECUTAR MOVIMIENTO DEL BOOSTER - ÚLTIMO
  4431.                     ExecutePlayerMovementImmediate(client, movementFile, MSG_GROUP, groupNumber);
  4432.                     boosterActivated = true;
  4433.                    
  4434.                     DebugPrint("ActivateGroupInOrder - Booster %N activado para impulsar %d runners",
  4435.                         client, totalRunnersActivated);
  4436.                 }
  4437.             }
  4438.         }
  4439.         delete boosters;
  4440.     }
  4441.    
  4442.     // Si no se activó ningún booster pero hay runners, mostrar mensaje
  4443.     if (!boosterActivated && totalRunnersActivated > 0)
  4444.     {
  4445.         DebugPrint("ActivateGroupInOrder - ¡ADVERTENCIA! Se activaron %d runners pero NO se activó ningún booster",
  4446.             totalRunnersActivated);
  4447.     }
  4448.    
  4449.     DebugPrint("ActivateGroupInOrder - RESUMEN FINAL: %d runners activados (capacidad: %d), Booster: %s",
  4450.         totalRunnersActivated, boosterCapacity, boosterActivated ? "SÍ" : "NO");
  4451.    
  4452.     delete activatedPlayers;
  4453. }
  4454.  
  4455. // ========================================================================
  4456. // FUNCIÓN CORREGIDA: OBTENER JUGADORES LISTOS POR TIPO (MEJORADA)
  4457. // ========================================================================
  4458.  
  4459. ArrayList GetReadyPlayersByType(int groupNumber, const char[] type)
  4460. {
  4461.     ArrayList players = new ArrayList();
  4462.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  4463.    
  4464.     if (groupTriggers == null)
  4465.         return players;
  4466.    
  4467.     // Primera pasada: agregar todos los jugadores listos para este tipo
  4468.     for (int i = 0; i < groupTriggers.Length; i++)
  4469.     {
  4470.         int triggerIndex = groupTriggers.Get(i);
  4471.        
  4472.         // Verificar si este trigger es del tipo buscado
  4473.         if (StrEqual(g_sTriggerType[triggerIndex], type))
  4474.         {
  4475.             // Agregar jugadores listos en este trigger
  4476.             for (int client = 1; client <= MaxClients; client++)
  4477.             {
  4478.                 if (IsValidSurvivor(client) && g_bPlayerReady[client][triggerIndex])
  4479.                 {
  4480.                     // Verificar que el jugador no esté ya en la lista
  4481.                     if (players.FindValue(client) == -1)
  4482.                     {
  4483.                         players.Push(client);
  4484.                     }
  4485.                 }
  4486.             }
  4487.         }
  4488.     }
  4489.    
  4490.     return players;
  4491. }
  4492.  
  4493. // ========================================================================
  4494. // NUEVA FUNCIÓN: OBTENER TODOS LOS JUGADORES EN POSICIÓN HÍBRIDA
  4495. // ========================================================================
  4496.  
  4497. /**
  4498.  * Obtiene TODOS los jugadores en una posición híbrida (runpm, runbile, rungl)
  4499.  * Siempre y cuando al menos uno de ellos tenga el arma requerida.
  4500.  * Esto incluye tanto al que tiene el arma como a los que no la tienen (runners adicionales)
  4501.  */
  4502. ArrayList GetAllReadyPlayersInHybridPosition(int groupNumber, const char[] hybridType)
  4503. {
  4504.     ArrayList allPlayers = new ArrayList();
  4505.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  4506.    
  4507.     if (groupTriggers == null)
  4508.         return allPlayers;
  4509.    
  4510.     char requiredWeapon[8];
  4511.     if (StrEqual(hybridType, "runpm")) strcopy(requiredWeapon, sizeof(requiredWeapon), "pm");
  4512.     else if (StrEqual(hybridType, "runbile")) strcopy(requiredWeapon, sizeof(requiredWeapon), "bile");
  4513.     else if (StrEqual(hybridType, "rungl")) strcopy(requiredWeapon, sizeof(requiredWeapon), "gl");
  4514.     else
  4515.     {
  4516.         DebugPrint("GetAllReadyPlayersInHybridPosition - Tipo híbrido desconocido: %s", hybridType);
  4517.         return allPlayers;
  4518.     }
  4519.    
  4520.     bool hasArmed = false;
  4521.     ArrayList tempPlayers = new ArrayList();
  4522.    
  4523.     for (int i = 0; i < groupTriggers.Length; i++)
  4524.     {
  4525.         int triggerIndex = groupTriggers.Get(i);
  4526.         if (!StrEqual(g_sTriggerType[triggerIndex], hybridType))
  4527.             continue;
  4528.        
  4529.         int triggerEnt = EntRefToEntIndex(g_iTriggers[triggerIndex]);
  4530.         if (triggerEnt == INVALID_ENT_REFERENCE)
  4531.             continue;
  4532.        
  4533.         // Obtener la caja de colisión del trigger (con un margen de tolerancia)
  4534.         float origin[3], mins[3], maxs[3];
  4535.         GetEntPropVector(triggerEnt, Prop_Send, "m_vecOrigin", origin);
  4536.         GetEntPropVector(triggerEnt, Prop_Send, "m_vecMins", mins);
  4537.         GetEntPropVector(triggerEnt, Prop_Send, "m_vecMaxs", maxs);
  4538.        
  4539.         // Añadir un margen de 5 unidades para evitar errores de redondeo
  4540.         float tolerance = 5.0;
  4541.         float worldMin[3], worldMax[3];
  4542.         worldMin[0] = origin[0] + mins[0] - tolerance;
  4543.         worldMin[1] = origin[1] + mins[1] - tolerance;
  4544.         worldMin[2] = origin[2] + mins[2] - tolerance;
  4545.         worldMax[0] = origin[0] + maxs[0] + tolerance;
  4546.         worldMax[1] = origin[1] + maxs[1] + tolerance;
  4547.         worldMax[2] = origin[2] + maxs[2] + tolerance;
  4548.        
  4549.         for (int client = 1; client <= MaxClients; client++)
  4550.         {
  4551.             if (!IsValidSurvivor(client))
  4552.                 continue;
  4553.            
  4554.             // PRIMERO: confiar en g_bPlayerInTrigger (más fiable)
  4555.             bool inside = g_bPlayerInTrigger[client][triggerIndex];
  4556.            
  4557.             // SI no está registrado, verificar físicamente con tolerancia
  4558.             if (!inside)
  4559.             {
  4560.                 float clientPos[3];
  4561.                 GetClientAbsOrigin(client, clientPos);
  4562.                 inside = (clientPos[0] >= worldMin[0] && clientPos[0] <= worldMax[0] &&
  4563.                           clientPos[1] >= worldMin[1] && clientPos[1] <= worldMax[1] &&
  4564.                           clientPos[2] >= worldMin[2] && clientPos[2] <= worldMax[2]);
  4565.                 if (inside)
  4566.                 {
  4567.                     // Si está físicamente pero no registrado, lo registramos para futuras ocasiones
  4568.                     g_bPlayerInTrigger[client][triggerIndex] = true;
  4569.                     g_bPlayerReady[client][triggerIndex] = false;
  4570.                     g_iPlayerRetryCount[client][triggerIndex] = 0;
  4571.                     g_bWaitingForWeapon[client][triggerIndex] = false;
  4572.                     DebugPrint("GetAllReadyPlayersInHybridPosition - Jugador %N detectado físicamente en trigger %d", client, triggerIndex);
  4573.                 }
  4574.             }
  4575.            
  4576.             if (!inside)
  4577.                 continue;
  4578.            
  4579.             // Verificar salud y que no esté en movimiento
  4580.             if (!CheckPlayerHealthRules(client) || g_bClientInMovement[client])
  4581.                 continue;
  4582.            
  4583.             // Agregar a la lista temporal (evitar duplicados)
  4584.             if (tempPlayers.FindValue(client) == -1)
  4585.             {
  4586.                 tempPlayers.Push(client);
  4587.                 DebugPrint("GetAllReadyPlayersInHybridPosition - Jugador %N agregado (arma: %s)",
  4588.                     client, HasRequiredWeapon(client, requiredWeapon) ? "SÍ" : "NO");
  4589.             }
  4590.            
  4591.             // Marcar si este jugador tiene el arma requerida
  4592.             if (HasRequiredWeapon(client, requiredWeapon))
  4593.                 hasArmed = true;
  4594.         }
  4595.     }
  4596.    
  4597.     // Si no hay ningún jugador con el arma, devolver lista vacía
  4598.     if (!hasArmed)
  4599.     {
  4600.         DebugPrint("GetAllReadyPlayersInHybridPosition - No hay jugador con %s en posición híbrida %s", requiredWeapon, hybridType);
  4601.         delete tempPlayers;
  4602.         return allPlayers;
  4603.     }
  4604.    
  4605.     // Copiar todos los jugadores al resultado
  4606.     for (int i = 0; i < tempPlayers.Length; i++)
  4607.         allPlayers.Push(tempPlayers.Get(i));
  4608.    
  4609.     delete tempPlayers;
  4610.     DebugPrint("GetAllReadyPlayersInHybridPosition - Devolviendo %d jugadores en posición híbrida %s (al menos uno armado)", allPlayers.Length, hybridType);
  4611.     return allPlayers;
  4612. }
  4613.  
  4614. // ========================================================================
  4615. // NUEVA FUNCIÓN: OBTENER TODOS LOS RUNNERS DISPONIBLES EN EL GRUPO
  4616. // ========================================================================
  4617.  
  4618. ArrayList GetAllRunnersInGroup(int groupNumber)
  4619. {
  4620.     ArrayList allRunners = new ArrayList();
  4621.    
  4622.     // Buscar runners puros
  4623.     ArrayList pureRunners = GetReadyPlayersByType(groupNumber, "run");
  4624.     for (int i = 0; i < pureRunners.Length; i++)
  4625.     {
  4626.         int client = pureRunners.Get(i);
  4627.         if (allRunners.FindValue(client) == -1)
  4628.         {
  4629.             allRunners.Push(client);
  4630.         }
  4631.     }
  4632.     delete pureRunners;
  4633.    
  4634.     // Buscar híbridos (también son runners)
  4635.     char hybridTypes[][] = {"runpm", "runbile", "rungl"};
  4636.     for (int h = 0; h < sizeof(hybridTypes); h++)
  4637.     {
  4638.         ArrayList hybrids = GetReadyPlayersByType(groupNumber, hybridTypes[h]);
  4639.         for (int i = 0; i < hybrids.Length; i++)
  4640.         {
  4641.             int client = hybrids.Get(i);
  4642.             if (allRunners.FindValue(client) == -1)
  4643.             {
  4644.                 allRunners.Push(client);
  4645.             }
  4646.         }
  4647.         delete hybrids;
  4648.     }
  4649.    
  4650.     return allRunners;
  4651. }
  4652.  
  4653. // ========================================================================
  4654. // NUEVA FUNCIÓN: VERIFICAR CAPACIDAD DEL GRUPO
  4655. // ========================================================================
  4656.  
  4657. int GetGroupBoosterCapacity(int groupNumber)
  4658. {
  4659.     ArrayList groupTypes = GetGroupTypes(groupNumber);
  4660.     if (groupTypes == null)
  4661.         return 1; // Capacidad por defecto
  4662.    
  4663.     int maxCapacity = 1;
  4664.    
  4665.     for (int i = 0; i < groupTypes.Length; i++)
  4666.     {
  4667.         char type[16];
  4668.         groupTypes.GetString(i, type, sizeof(type));
  4669.        
  4670.         // Verificar si es un booster puro
  4671.         if (StrEqual(type, "pm") || StrEqual(type, "bile") || StrEqual(type, "gl"))
  4672.         {
  4673.             int capacity = MA_GetBoosterCapacity(type);
  4674.             if (capacity > maxCapacity)
  4675.             {
  4676.                 maxCapacity = capacity;
  4677.             }
  4678.         }
  4679.     }
  4680.    
  4681.     // Si no encontramos booster puro, verificar híbridos
  4682.     if (maxCapacity == 1)
  4683.     {
  4684.         for (int i = 0; i < groupTypes.Length; i++)
  4685.         {
  4686.             char type[16];
  4687.             groupTypes.GetString(i, type, sizeof(type));
  4688.            
  4689.             if (StrEqual(type, "runpm") || StrEqual(type, "runbile") || StrEqual(type, "rungl"))
  4690.             {
  4691.                 int capacity = MA_GetBoosterCapacity(type);
  4692.                 if (capacity > maxCapacity)
  4693.                 {
  4694.                     maxCapacity = capacity;
  4695.                 }
  4696.             }
  4697.         }
  4698.     }
  4699.    
  4700.     DebugPrint("GetGroupBoosterCapacity - Grupo %d - Capacidad máxima: %d runners", groupNumber, maxCapacity);
  4701.     return maxCapacity;
  4702. }
  4703.  
  4704. // ========================================================================
  4705. // NUEVAS FUNCIONES PARA ANÁLISIS DE ARCHIVOS (IMPLEMENTACIÓN COMPLETA)
  4706. // ========================================================================
  4707.  
  4708. /**
  4709.  * Analiza los archivos que se activarán en un grupo
  4710.  */
  4711. public bool AnalyzeActivatedFiles(int groupNumber, ArrayList files, GroupAnalysis analysis)
  4712. {
  4713.     // Inicializar estructura
  4714.     analysis.hasPureRunner = false;
  4715.     analysis.pureRunnerCount = 0;
  4716.     analysis.hybridPMCount = 0;
  4717.     analysis.hybridBileCount = 0;
  4718.     analysis.hybridGLCount = 0;
  4719.     analysis.boosterPMCount = 0;
  4720.     analysis.boosterBileCount = 0;
  4721.     analysis.boosterGLCount = 0;
  4722.     analysis.uniqueFileNames = new ArrayList(ByteCountToCells(128));
  4723.     analysis.uniqueFileDurations = new ArrayList();
  4724.    
  4725.     // Analizar cada archivo
  4726.     for (int i = 0; i < files.Length; i++)
  4727.     {
  4728.         char fileName[128];
  4729.         files.GetString(i, fileName, sizeof(fileName));
  4730.        
  4731.         // Obtener nombre base del archivo
  4732.         char baseName[64];
  4733.         GetBaseFileName(fileName, baseName, sizeof(baseName));
  4734.        
  4735.         // Clasificar por tipo usando la información del archivo
  4736.         MovementFileInfo info;
  4737.         if (MA_GetMovementFileInfo(fileName, g_hMovementFileInfos, info))
  4738.         {
  4739.             // Usar if-else if en lugar de switch para evitar problemas de sintaxis en SourcePawn
  4740.             if (info.movType == MOVEMENT_RUN)
  4741.             {
  4742.                 analysis.hasPureRunner = true;
  4743.                 analysis.pureRunnerCount++;
  4744.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Runner Puro", baseName);
  4745.             }
  4746.             else if (info.movType == MOVEMENT_RUNPM)
  4747.             {
  4748.                 analysis.hybridPMCount++;
  4749.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Híbrido PM", baseName);
  4750.             }
  4751.             else if (info.movType == MOVEMENT_RUNBILE)
  4752.             {
  4753.                 analysis.hybridBileCount++;
  4754.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Híbrido Bile", baseName);
  4755.             }
  4756.             else if (info.movType == MOVEMENT_RUNGL)
  4757.             {
  4758.                 analysis.hybridGLCount++;
  4759.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Híbrido GL", baseName);
  4760.             }
  4761.             else if (info.movType == MOVEMENT_PM)
  4762.             {
  4763.                 analysis.boosterPMCount++;
  4764.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Booster PM Puro", baseName);
  4765.             }
  4766.             else if (info.movType == MOVEMENT_BILE)
  4767.             {
  4768.                 analysis.boosterBileCount++;
  4769.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Booster Bile Puro", baseName);
  4770.             }
  4771.             else if (info.movType == MOVEMENT_GL)
  4772.             {
  4773.                 analysis.boosterGLCount++;
  4774.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Booster GL Puro", baseName);
  4775.             }
  4776.             else
  4777.             {
  4778.                 DebugPrint("AnalyzeActivatedFiles - Archivo %s -> Tipo desconocido: %d", baseName, info.movType);
  4779.             }
  4780.            
  4781.             // Agregar a lista única
  4782.             analysis.uniqueFileNames.PushString(baseName);
  4783.            
  4784.             // Obtener duración
  4785.             float duration = 10.0;
  4786.             if (g_hMovementDurations != null)
  4787.                 g_hMovementDurations.GetValue(fileName, duration);
  4788.             analysis.uniqueFileDurations.Push(duration);
  4789.         }
  4790.         else
  4791.         {
  4792.             // Fallback: clasificar por nombre
  4793.             if (StrContains(baseName, "runpm") != -1)
  4794.             {
  4795.                 analysis.hybridPMCount++;
  4796.                 DebugPrint("AnalyzeActivatedFiles - %s -> Híbrido PM (por nombre)", baseName);
  4797.             }
  4798.             else if (StrContains(baseName, "runbile") != -1)
  4799.             {
  4800.                 analysis.hybridBileCount++;
  4801.                 DebugPrint("AnalyzeActivatedFiles - %s -> Híbrido Bile (por nombre)", baseName);
  4802.             }
  4803.             else if (StrContains(baseName, "rungl") != -1)
  4804.             {
  4805.                 analysis.hybridGLCount++;
  4806.                 DebugPrint("AnalyzeActivatedFiles - %s -> Híbrido GL (por nombre)", baseName);
  4807.             }
  4808.             else if (StrContains(baseName, "pm") != -1 && StrContains(baseName, "runpm") == -1)
  4809.             {
  4810.                 analysis.boosterPMCount++;
  4811.                 DebugPrint("AnalyzeActivatedFiles - %s -> Booster PM Puro (por nombre)", baseName);
  4812.             }
  4813.             else if (StrContains(baseName, "bile") != -1 && StrContains(baseName, "runbile") == -1)
  4814.             {
  4815.                 analysis.boosterBileCount++;
  4816.                 DebugPrint("AnalyzeActivatedFiles - %s -> Booster Bile Puro (por nombre)", baseName);
  4817.             }
  4818.             else if (StrContains(baseName, "gl") != -1 && StrContains(baseName, "rungl") == -1)
  4819.             {
  4820.                 analysis.boosterGLCount++;
  4821.                 DebugPrint("AnalyzeActivatedFiles - %s -> Booster GL Puro (por nombre)", baseName);
  4822.             }
  4823.             else if (StrContains(baseName, "run") != -1)
  4824.             {
  4825.                 analysis.hasPureRunner = true;
  4826.                 analysis.pureRunnerCount++;
  4827.                 DebugPrint("AnalyzeActivatedFiles - %s -> Runner Puro (por nombre)", baseName);
  4828.             }
  4829.            
  4830.             analysis.uniqueFileNames.PushString(baseName);
  4831.            
  4832.             float duration = 10.0;
  4833.             if (g_hMovementDurations != null)
  4834.                 g_hMovementDurations.GetValue(fileName, duration);
  4835.             analysis.uniqueFileDurations.Push(duration);
  4836.         }
  4837.     }
  4838.    
  4839.     // Resumen del análisis
  4840.     DebugPrint("AnalyzeActivatedFiles RESUMEN - Grupo %d:", groupNumber);
  4841.     DebugPrint("  Runners Puros: %d", analysis.pureRunnerCount);
  4842.     DebugPrint("  Híbridos PM: %d", analysis.hybridPMCount);
  4843.     DebugPrint("  Híbridos Bile: %d", analysis.hybridBileCount);
  4844.     DebugPrint("  Híbridos GL: %d", analysis.hybridGLCount);
  4845.     DebugPrint("  Boosters PM: %d", analysis.boosterPMCount);
  4846.     DebugPrint("  Boosters Bile: %d", analysis.boosterBileCount);
  4847.     DebugPrint("  Boosters GL: %d", analysis.boosterGLCount);
  4848.    
  4849.     return true;
  4850. }
  4851.  
  4852. /**
  4853.  * Valida si la combinación de archivos es válida
  4854.  */
  4855. public bool ValidateGroupComposition(const GroupAnalysis analysis, char[] errorMsg, int errorLen)
  4856. {
  4857.     // Calcular totales por tipo
  4858.     int totalPM = analysis.hybridPMCount + analysis.boosterPMCount;
  4859.     int totalBile = analysis.hybridBileCount + analysis.boosterBileCount;
  4860.     int totalGL = analysis.hybridGLCount + analysis.boosterGLCount;
  4861.     int totalRunners = analysis.pureRunnerCount + analysis.hybridPMCount + analysis.hybridBileCount + analysis.hybridGLCount;
  4862.     int totalBoosters = analysis.boosterPMCount + analysis.boosterBileCount + analysis.boosterGLCount;
  4863.    
  4864.     DebugPrint("ValidateGroupComposition - Total PM: %d, Total Bile: %d, Total GL: %d", totalPM, totalBile, totalGL);
  4865.     DebugPrint("ValidateGroupComposition - Total Runners: %d, Total Boosters: %d", totalRunners, totalBoosters);
  4866.    
  4867.     // REGLA 1: Debe haber al menos 1 runner
  4868.     if (totalRunners == 0)
  4869.     {
  4870.         Format(errorMsg, errorLen, "No hay runners en la fórmula. Se necesita al menos 1 runner puro o híbrido.");
  4871.         return false;
  4872.     }
  4873.    
  4874.     // REGLA 2: Si hay boosters puros, debe haber runner puro o híbrido
  4875.     if (totalBoosters > 0 && totalRunners == 0)
  4876.     {
  4877.         Format(errorMsg, errorLen, "Los boosters puros requieren al menos 1 runner.");
  4878.         return false;
  4879.     }
  4880.    
  4881.     // REGLA 3: Runner puro es opcional si hay híbridos
  4882.     // (Ya está implícito en el cálculo de totalRunners)
  4883.    
  4884.     // REGLA 4: Validar capacidad según el booster de mayor capacidad
  4885.     int maxCapacity = 0;
  4886.     if (totalPM > 0)
  4887.     {
  4888.         int capPM = GetBoosterCapacityNew("PM");
  4889.         maxCapacity = (capPM > maxCapacity) ? capPM : maxCapacity;
  4890.     }
  4891.     if (totalBile > 0)
  4892.     {
  4893.         int capBile = GetBoosterCapacityNew("Bile");
  4894.         maxCapacity = (capBile > maxCapacity) ? capBile : maxCapacity;
  4895.     }
  4896.     if (totalGL > 0)
  4897.     {
  4898.         int capGL = GetBoosterCapacityNew("GL");
  4899.         maxCapacity = (capGL > maxCapacity) ? capGL : maxCapacity;
  4900.     }
  4901.    
  4902.     // Si no hay boosters ni híbridos (solo runners puros), capacidad máxima 32
  4903.     if (maxCapacity == 0) maxCapacity = 32;
  4904.    
  4905.     DebugPrint("ValidateGroupComposition - Capacidad máxima permitida: %d runners", maxCapacity);
  4906.    
  4907.     // La validación de capacidad se hace en otra parte del código
  4908.     // Aquí solo validamos la composición básica
  4909.    
  4910.     return true;
  4911. }
  4912.  
  4913. /**
  4914.  * Genera el nombre para mostrar del grupo
  4915.  */
  4916. public void GenerateGroupDisplayName(const GroupAnalysis analysis, int playerCount, char[] buffer, int bufferLen)
  4917. {
  4918.     // CASO 1: Solo 1 jugador
  4919.     if (playerCount == 1)
  4920.     {
  4921.         strcopy(buffer, bufferLen, "Solo");
  4922.         return;
  4923.     }
  4924.    
  4925.     // Calcular totales por tipo
  4926.     int totalPM = analysis.hybridPMCount + analysis.boosterPMCount;
  4927.     int totalBile = analysis.hybridBileCount + analysis.boosterBileCount;
  4928.     int totalGL = analysis.hybridGLCount + analysis.boosterGLCount;
  4929.    
  4930.     // Array para tipos con sus contadores
  4931.     char types[3][16];
  4932.     int counts[3];
  4933.     int typeCount = 0;
  4934.    
  4935.     // PM
  4936.     if (totalPM > 0)
  4937.     {
  4938.         strcopy(types[typeCount], 16, "PM");
  4939.         counts[typeCount] = totalPM;
  4940.         typeCount++;
  4941.     }
  4942.    
  4943.     // Bile
  4944.     if (totalBile > 0)
  4945.     {
  4946.         strcopy(types[typeCount], 16, "Bile");
  4947.         counts[typeCount] = totalBile;
  4948.         typeCount++;
  4949.     }
  4950.    
  4951.     // GL
  4952.     if (totalGL > 0)
  4953.     {
  4954.         strcopy(types[typeCount], 16, "GL");
  4955.         counts[typeCount] = totalGL;
  4956.         typeCount++;
  4957.     }
  4958.    
  4959.     // Ordenar por capacidad (PM > Bile > GL)
  4960.     for (int i = 0; i < typeCount - 1; i++)
  4961.     {
  4962.         for (int j = i + 1; j < typeCount; j++)
  4963.         {
  4964.             int capacityI = GetBoosterCapacityNew(types[i]);
  4965.             int capacityJ = GetBoosterCapacityNew(types[j]);
  4966.            
  4967.             if (capacityJ > capacityI ||
  4968.                (capacityJ == capacityI && StrCompare(types[j], types[i], false) < 0))
  4969.             {
  4970.                 // Intercambiar
  4971.                 char tempType[16];
  4972.                 strcopy(tempType, sizeof(tempType), types[i]);
  4973.                 strcopy(types[i], 16, types[j]);
  4974.                 strcopy(types[j], 16, tempType);
  4975.                
  4976.                 int tempCount = counts[i];
  4977.                 counts[i] = counts[j];
  4978.                 counts[j] = tempCount;
  4979.             }
  4980.         }
  4981.     }
  4982.    
  4983.     // Construir prefijo según cantidad de jugadores
  4984.     char prefix[16];
  4985.     if (playerCount == 2)
  4986.     {
  4987.         strcopy(prefix, sizeof(prefix), "Dual");
  4988.     }
  4989.     else if (playerCount == 3)
  4990.     {
  4991.         strcopy(prefix, sizeof(prefix), "Triple");
  4992.     }
  4993.     else if (playerCount == 4)
  4994.     {
  4995.         strcopy(prefix, sizeof(prefix), "Quad");
  4996.     }
  4997.     else
  4998.     {
  4999.         strcopy(prefix, sizeof(prefix), "Grupo");
  5000.     }
  5001.    
  5002.     // Construir nombre completo
  5003.     if (typeCount == 0)
  5004.     {
  5005.         // Solo runners puros
  5006.         Format(buffer, bufferLen, "%s", prefix);
  5007.     }
  5008.     else
  5009.     {
  5010.         Format(buffer, bufferLen, "%s-", prefix);
  5011.        
  5012.         for (int i = 0; i < typeCount; i++)
  5013.         {
  5014.             if (i > 0) StrCat(buffer, bufferLen, "/");
  5015.            
  5016.             // Mostrar contador solo si hay más de 1 y estamos en Triple o más
  5017.             if (counts[i] > 1 && playerCount >= 3)
  5018.             {
  5019.                 char typePart[32];
  5020.                 Format(typePart, sizeof(typePart), "(%d)%s", counts[i], types[i]);
  5021.                 StrCat(buffer, bufferLen, typePart);
  5022.             }
  5023.             else
  5024.             {
  5025.                 StrCat(buffer, bufferLen, types[i]);
  5026.             }
  5027.            
  5028.             // Limitar a 3 tipos para nombres largos
  5029.             if (playerCount == 3 && i >= 2) break;
  5030.         }
  5031.     }
  5032.    
  5033.     DebugPrint("GenerateGroupDisplayName - Generado: %s para %d jugadores", buffer, playerCount);
  5034. }
  5035.  
  5036. /**
  5037.  * Obtiene el nombre base del archivo (sin path ni extensión)
  5038.  */
  5039. public void GetBaseFileName(const char[] fullPath, char[] baseName, int maxlen)
  5040. {
  5041.     // Copiar la cadena completa primero
  5042.     strcopy(baseName, maxlen, fullPath);
  5043.    
  5044.     // Eliminar extensión .txt si existe
  5045.     int len = strlen(baseName);
  5046.     if (len > 4 && StrEqual(baseName[len-4], ".txt", false))
  5047.     {
  5048.         baseName[len-4] = '\0';
  5049.     }
  5050.    
  5051.     // Eliminar path si existe
  5052.     int slashPos = FindCharInString(baseName, '/', true);
  5053.     if (slashPos == -1) slashPos = FindCharInString(baseName, '\\', true);
  5054.    
  5055.     if (slashPos != -1)
  5056.     {
  5057.         char temp[128];
  5058.         strcopy(temp, sizeof(temp), baseName[slashPos+1]);
  5059.         strcopy(baseName, maxlen, temp);
  5060.     }
  5061. }
  5062.  
  5063. /**
  5064.  * Obtiene la capacidad de un booster (nueva función)
  5065.  */
  5066. public int GetBoosterCapacityNew(const char[] boosterType)
  5067. {
  5068.     if (StrEqual(boosterType, "PM", false))
  5069.         return 32;
  5070.     else if (StrEqual(boosterType, "Bile", false))
  5071.         return 5;
  5072.     else if (StrEqual(boosterType, "GL", false))
  5073.         return 1;
  5074.    
  5075.     return 1; // Default
  5076. }
  5077.  
  5078. /**
  5079.  * Ordena archivos para mostrar en mensaje público
  5080.  */
  5081. public void SortFilesForDisplay(ArrayList files, ArrayList durations, ArrayList sortedFiles, ArrayList sortedDurations)
  5082. {
  5083.     if (files == null || files.Length == 0)
  5084.         return;
  5085.    
  5086.     // Crear arrays temporales
  5087.     ArrayList tempFiles = new ArrayList(ByteCountToCells(128));
  5088.     ArrayList tempDurations = new ArrayList();
  5089.    
  5090.     // Copiar datos
  5091.     for (int i = 0; i < files.Length; i++)
  5092.     {
  5093.         char file[128];
  5094.         files.GetString(i, file, sizeof(file));
  5095.         tempFiles.PushString(file);
  5096.         tempDurations.Push(durations.Get(i));
  5097.     }
  5098.    
  5099.     // Ordenar: primero runners puros, luego híbridos, luego boosters puros
  5100.     for (int i = 0; i < tempFiles.Length; i++)
  5101.     {
  5102.         char file[128];
  5103.         tempFiles.GetString(i, file, sizeof(file));
  5104.        
  5105.         char baseName[64];
  5106.         GetBaseFileName(file, baseName, sizeof(baseName));
  5107.        
  5108.         // Determinar tipo
  5109.         bool isRunner = (StrContains(baseName, "run") != -1 &&
  5110.                         StrContains(baseName, "runpm") == -1 &&
  5111.                         StrContains(baseName, "runbile") == -1 &&
  5112.                         StrContains(baseName, "rungl") == -1);
  5113.        
  5114.         bool isHybrid = (StrContains(baseName, "runpm") != -1 ||
  5115.                         StrContains(baseName, "runbile") != -1 ||
  5116.                         StrContains(baseName, "rungl") != -1);
  5117.        
  5118.         bool isBooster = (!isRunner && !isHybrid &&
  5119.                          (StrContains(baseName, "pm") != -1 ||
  5120.                           StrContains(baseName, "bile") != -1 ||
  5121.                           StrContains(baseName, "gl") != -1));
  5122.        
  5123.         // Agregar en orden
  5124.         if (isRunner)
  5125.         {
  5126.             sortedFiles.PushString(file);
  5127.             sortedDurations.Push(tempDurations.Get(i));
  5128.         }
  5129.     }
  5130.    
  5131.     // Agregar híbridos
  5132.     for (int i = 0; i < tempFiles.Length; i++)
  5133.     {
  5134.         char file[128];
  5135.         tempFiles.GetString(i, file, sizeof(file));
  5136.        
  5137.         char baseName[64];
  5138.         GetBaseFileName(file, baseName, sizeof(baseName));
  5139.        
  5140.         bool isHybrid = (StrContains(baseName, "runpm") != -1 ||
  5141.                         StrContains(baseName, "runbile") != -1 ||
  5142.                         StrContains(baseName, "rungl") != -1);
  5143.        
  5144.         if (isHybrid)
  5145.         {
  5146.             sortedFiles.PushString(file);
  5147.             sortedDurations.Push(tempDurations.Get(i));
  5148.         }
  5149.     }
  5150.    
  5151.     // Agregar boosters puros
  5152.     for (int i = 0; i < tempFiles.Length; i++)
  5153.     {
  5154.         char file[128];
  5155.         tempFiles.GetString(i, file, sizeof(file));
  5156.        
  5157.         char baseName[64];
  5158.         GetBaseFileName(file, baseName, sizeof(baseName));
  5159.        
  5160.         bool isBooster = (StrContains(baseName, "pm") != -1 && StrContains(baseName, "runpm") == -1) ||
  5161.                         (StrContains(baseName, "bile") != -1 && StrContains(baseName, "runbile") == -1) ||
  5162.                         (StrContains(baseName, "gl") != -1 && StrContains(baseName, "rungl") == -1);
  5163.        
  5164.         if (isBooster)
  5165.         {
  5166.             sortedFiles.PushString(file);
  5167.             sortedDurations.Push(tempDurations.Get(i));
  5168.         }
  5169.     }
  5170.    
  5171.     delete tempFiles;
  5172.     delete tempDurations;
  5173. }
  5174.  
  5175. /**
  5176.  * Formatea el mensaje público con archivos y duraciones
  5177.  */
  5178. public void FormatPublicMessage(const char[] groupName, int groupNumber, ArrayList files,
  5179.                         ArrayList durations, char[] buffer, int bufferLen)
  5180. {
  5181.     // Base del mensaje
  5182.     char base[128];
  5183.    
  5184.     // Determinar prefijo basado en cantidad de archivos
  5185.     if (files.Length == 1)
  5186.     {
  5187.         Format(base, sizeof(base), "[\x04MA %s\x01] ", groupName);
  5188.     }
  5189.     else
  5190.     {
  5191.         Format(base, sizeof(base), "[\x04MA %s (%d)\x01] ", groupName, groupNumber);
  5192.     }
  5193.    
  5194.     // Concatenar archivos con duraciones
  5195.     char filePart[512];
  5196.     filePart[0] = '\0';
  5197.    
  5198.     for (int i = 0; i < files.Length; i++)
  5199.     {
  5200.         char fileName[64];
  5201.         files.GetString(i, fileName, sizeof(fileName));
  5202.         float duration = durations.Get(i);
  5203.        
  5204.         // Obtener nombre de visualización
  5205.         char displayName[64];
  5206.         GetBaseFileName(fileName, displayName, sizeof(displayName));
  5207.        
  5208.         char temp[128];
  5209.         Format(temp, sizeof(temp), "%s(%.1fs)", displayName, duration);
  5210.        
  5211.         if (i == 0)
  5212.             strcopy(filePart, sizeof(filePart), temp);
  5213.         else
  5214.             Format(filePart, sizeof(filePart), "%s + %s", filePart, temp);
  5215.     }
  5216.    
  5217.     // Combinar todo
  5218.     Format(buffer, bufferLen, "%s%s", base, filePart);
  5219.    
  5220.     DebugPrint("FormatPublicMessage - %s", buffer);
  5221. }
  5222.  
  5223. // ========================================================================
  5224. // FUNCIÓN MODIFICADA: ShowGroupActivationSummary
  5225. // ========================================================================
  5226.  
  5227. public void ShowGroupActivationSummary(int groupNumber, ArrayList players, ArrayList files)
  5228. {
  5229.     // 1. Validar entrada
  5230.     if (players == null || files == null || players.Length == 0 || files.Length == 0)
  5231.         return;
  5232.    
  5233.     DebugPrint("ShowGroupActivationSummary - Mostrando resumen para grupo %d con %d jugadores y %d archivos",
  5234.         groupNumber, players.Length, files.Length);
  5235.    
  5236.     // 2. Prevenir mensajes duplicados
  5237.     static int lastGroupActivation = 0;
  5238.     static float lastActivationTime = 0.0;
  5239.    
  5240.     float currentTime = GetGameTime();
  5241.     if (lastGroupActivation == groupNumber && (currentTime - lastActivationTime) < 0.5)
  5242.     {
  5243.         DebugPrint("ShowGroupActivationSummary - Evitando mensaje duplicado para grupo %d", groupNumber);
  5244.         return;
  5245.     }
  5246.    
  5247.     lastGroupActivation = groupNumber;
  5248.     lastActivationTime = currentTime;
  5249.    
  5250.     // 3. Analizar archivos
  5251.     GroupAnalysis analysis;
  5252.     if (!AnalyzeActivatedFiles(groupNumber, files, analysis))
  5253.     {
  5254.         DebugPrint("ShowGroupActivationSummary - No se pudo analizar archivos del grupo %d", groupNumber);
  5255.         return;
  5256.     }
  5257.    
  5258.     // 4. Validar composición
  5259.     char errorMsg[128];
  5260.     if (!ValidateGroupComposition(analysis, errorMsg, sizeof(errorMsg)))
  5261.     {
  5262.         // Mostrar error a los jugadores involucrados
  5263.         for (int i = 0; i < players.Length; i++)
  5264.         {
  5265.             int client = players.Get(i);
  5266.             if (IsValidSurvivor(client))
  5267.             {
  5268.                 PrintToChat(client, "[\x04Error\x01] %s", errorMsg);
  5269.             }
  5270.         }
  5271.        
  5272.         // Limpiar análisis
  5273.         delete analysis.uniqueFileNames;
  5274.         delete analysis.uniqueFileDurations;
  5275.         return;
  5276.     }
  5277.    
  5278.     // 5. Generar nombre del grupo
  5279.     char groupName[64];
  5280.     GenerateGroupDisplayName(analysis, players.Length, groupName, sizeof(groupName));
  5281.    
  5282.     DebugPrint("ShowGroupActivationSummary - Nombre generado: %s", groupName);
  5283.    
  5284.     // 6. Obtener duraciones de archivos y nombres base (sin "_autor")
  5285.     ArrayList durations = new ArrayList();
  5286.     ArrayList baseFileNames = new ArrayList(ByteCountToCells(64));
  5287.    
  5288.     for (int i = 0; i < files.Length; i++)
  5289.     {
  5290.         char file[128];
  5291.         files.GetString(i, file, sizeof(file));
  5292.        
  5293.         // Obtener nombre base sin autor (ej: "7run_Tonblader" -> "7run")
  5294.         char baseName[64];
  5295.         GetBaseFileNameWithoutAuthor(file, baseName, sizeof(baseName));
  5296.         baseFileNames.PushString(baseName);
  5297.        
  5298.         // Obtener duración
  5299.         float duration = 10.0;
  5300.         if (g_hMovementDurations != null)
  5301.             g_hMovementDurations.GetValue(file, duration);
  5302.         durations.Push(duration);
  5303.     }
  5304.    
  5305.     // 7. Obtener autores únicos (para mensaje opcional)
  5306.     ArrayList uniqueAuthors = new ArrayList(ByteCountToCells(32));
  5307.     for (int i = 0; i < files.Length; i++)
  5308.     {
  5309.         char file[128];
  5310.         files.GetString(i, file, sizeof(file));
  5311.        
  5312.         // Extraer autor del nombre del archivo
  5313.         char author[32];
  5314.         if (ExtractAuthorFromFileName(file, author, sizeof(author)))
  5315.         {
  5316.             if (uniqueAuthors.FindString(author) == -1)
  5317.             {
  5318.                 uniqueAuthors.PushString(author);
  5319.             }
  5320.         }
  5321.     }
  5322.    
  5323.     // 8. Mensaje de autores (OPCIONAL - solo si hay autores diferentes)
  5324.     if (uniqueAuthors.Length > 0)
  5325.     {
  5326.         // Construir lista de autores formateada
  5327.         char authorsFormatted[256];
  5328.         FormatAuthorsList(uniqueAuthors, authorsFormatted, sizeof(authorsFormatted));
  5329.        
  5330.         // Mostrar mensaje de autores solo si no está vacío
  5331.         if (strlen(authorsFormatted) > 0)
  5332.         {
  5333.             PrintToChatAll("[\x04MA %s (%d)\x01] Movimientos Creados por %s",
  5334.                           groupName, groupNumber, authorsFormatted);
  5335.         }
  5336.     }
  5337.    
  5338.     // 9. Mensaje público con archivos y duraciones (para TODOS)
  5339.     char publicMessage[512];
  5340.     FormatSimplePublicMessage(groupName, groupNumber, baseFileNames, durations, publicMessage, sizeof(publicMessage));
  5341.     PrintToChatAll("%s", publicMessage);
  5342.    
  5343.     // 10. Mensaje personal a los jugadores involucrados
  5344.     char privateMessage[128];
  5345.     Format(privateMessage, sizeof(privateMessage), "[\x04MA %s (%d)\x01] (%d) Jugadores en movimiento...",
  5346.            groupName, groupNumber, players.Length);
  5347.    
  5348.     for (int i = 0; i < players.Length; i++)
  5349.     {
  5350.         int client = players.Get(i);
  5351.         if (IsValidSurvivor(client))
  5352.         {
  5353.             PrintToChat(client, "%s", privateMessage);
  5354.         }
  5355.     }
  5356.    
  5357.     // 11. Limpiar memoria
  5358.     delete durations;
  5359.     delete baseFileNames;
  5360.     delete uniqueAuthors;
  5361.     delete analysis.uniqueFileNames;
  5362.     delete analysis.uniqueFileDurations;
  5363.    
  5364.     DebugPrint("ShowGroupActivationSummary - Resumen completado para grupo %d", groupNumber);
  5365. }
  5366.  
  5367. // ========================================================================
  5368. // SISTEMA DE REFIRE_TIME MEJORADO - LÓGICA V15
  5369. // ========================================================================
  5370.  
  5371. void ApplyRefireTime(int triggerIndex)
  5372. {
  5373.     if (triggerIndex < 0 || triggerIndex >= MAX_TRIGGERS)
  5374.         return;
  5375.    
  5376.     // Actualizar tiempos de activación
  5377.     g_fLastActivation[triggerIndex] = GetGameTime();
  5378.     g_fNextActivationTime[triggerIndex] = GetGameTime() + REFIRE_TIME;
  5379.    
  5380.     DebugPrint("ApplyRefireTime - Trigger %d - Cooldown activado por %.1f segundos", triggerIndex, REFIRE_TIME);
  5381.    
  5382.     // IMPORTANTE: NO resetear el estado de los jugadores que están dentro del trigger
  5383.     // Solo resetear los que NO están dentro
  5384.     for (int client = 1; client <= MaxClients; client++)
  5385.     {
  5386.         if (IsValidSurvivor(client))
  5387.         {
  5388.             // Si el jugador NO está en el trigger, resetearlo completamente
  5389.             if (!g_bPlayerInTrigger[client][triggerIndex])
  5390.             {
  5391.                 g_bPlayerReady[client][triggerIndex] = false;
  5392.                 g_iPlayerRetryCount[client][triggerIndex] = 0;
  5393.                 g_bWaitingForWeapon[client][triggerIndex] = false;
  5394.                
  5395.                 // Cancelar timer de reintento si existe
  5396.                 if (g_hRetryTimer[client][triggerIndex] != null)
  5397.                 {
  5398.                     KillTimer(g_hRetryTimer[client][triggerIndex]);
  5399.                     g_hRetryTimer[client][triggerIndex] = null;
  5400.                 }
  5401.             }
  5402.             else
  5403.             {
  5404.                 // El jugador SÍ está en el trigger - mantenerlo pero mostrar mensaje
  5405.                 PrintHintText(client, "Trigger en cooldown: %.1fs", REFIRE_TIME);
  5406.                
  5407.                 // Programar verificación para cuando termine el cooldown
  5408.                 CreateTimer(REFIRE_TIME + 0.1, Timer_CheckTriggerPresence, client);
  5409.             }
  5410.         }
  5411.     }
  5412. }
  5413.  
  5414. void ApplyGroupRefireTime(int groupNumber)
  5415. {
  5416.     // Deshabilitar TODOS los triggers del grupo temporalmente
  5417.     ArrayList groupTriggers = GetGroupTriggers(groupNumber);
  5418.    
  5419.     DebugPrint("ApplyGroupRefireTime - Aplicando refire_time a grupo %d (%d triggers)", groupNumber, groupTriggers.Length);
  5420.    
  5421.     for (int i = 0; i < groupTriggers.Length; i++)
  5422.     {
  5423.         int triggerIndex = groupTriggers.Get(i);
  5424.         ApplyRefireTime(triggerIndex);
  5425.     }
  5426.    
  5427.     // Remover grupo de la lista de activos después del refire_time
  5428.     char groupKey[16];
  5429.     IntToString(groupNumber, groupKey, sizeof(groupKey));
  5430.    
  5431.     CreateTimer(REFIRE_TIME, Timer_ResetGroup, groupNumber);
  5432. }
  5433.  
  5434. public Action Timer_ResetGroup(Handle timer, any groupNumber)
  5435. {
  5436.     char groupKey[16];
  5437.     IntToString(groupNumber, groupKey, sizeof(groupKey));
  5438.    
  5439.     // Remover grupo de la lista de activos
  5440.     if (g_hActiveGroups != null)
  5441.     {
  5442.         g_hActiveGroups.Remove(groupKey);
  5443.     }
  5444.    
  5445.     // Limpiar jugadores de este grupo
  5446.     ClearGroupActivationPlayers(groupNumber);
  5447.    
  5448.     DebugPrint("Timer_ResetGroup - Grupo %d re-habilitado después de refire_time", groupNumber);
  5449.    
  5450.     return Plugin_Stop;
  5451. }
  5452.  
  5453. // ========================================================================
  5454. // FUNCIÓN CAN TRIGGER ACTIVATE (NUEVA)
  5455. // ========================================================================
  5456.  
  5457. bool CanTriggerActivate(int triggerIndex)
  5458. {
  5459.     if (triggerIndex < 0 || triggerIndex >= MAX_TRIGGERS)
  5460.         return false;
  5461.    
  5462.     float currentTime = GetGameTime();
  5463.    
  5464.     // Verificar si ha pasado el refire_time
  5465.     if (currentTime < g_fNextActivationTime[triggerIndex])
  5466.     {
  5467.         return false;
  5468.     }
  5469.    
  5470.     // Verificar si el trigger está habilitado
  5471.     if (!g_bTriggerEnabled[triggerIndex])
  5472.     {
  5473.         return false;
  5474.     }
  5475.    
  5476.     return true;
  5477. }
  5478.  
  5479. // ========================================================================
  5480. // FUNCIONES DE TRACKING POR TRIGGER (COMPLEMENTO)
  5481. // ========================================================================
  5482.  
  5483. void AddPlayerToTrigger(int client, int triggerIndex)
  5484. {
  5485.     if (triggerIndex < 0 || triggerIndex >= MAX_TRIGGERS) return;
  5486.    
  5487.     g_bPlayerInTrigger[client][triggerIndex] = true;
  5488.     g_fPlayerLastCheck[client][triggerIndex] = GetGameTime();
  5489.     g_iPlayerRetryCount[client][triggerIndex] = 0;
  5490.     g_bWaitingForWeapon[client][triggerIndex] = false;
  5491.    
  5492.     // Iniciar verificación inmediata (como en V15)
  5493.     CreateTimer(0.1, Timer_CheckTriggerPresence, client);
  5494. }
  5495.  
  5496. void RemovePlayerFromTrigger(int client, int triggerIndex)
  5497. {
  5498.     if (triggerIndex < 0 || triggerIndex >= MAX_TRIGGERS) return;
  5499.    
  5500.     // Limpiar completamente el estado
  5501.     g_bPlayerInTrigger[client][triggerIndex] = false;
  5502.     g_bPlayerReady[client][triggerIndex] = false;
  5503.     g_bWaitingForWeapon[client][triggerIndex] = false;
  5504.    
  5505.     // Cancelar todos los timers relacionados
  5506.     if (g_hRetryTimer[client][triggerIndex] != null)
  5507.     {
  5508.         KillTimer(g_hRetryTimer[client][triggerIndex]);
  5509.         g_hRetryTimer[client][triggerIndex] = null;
  5510.     }
  5511.    
  5512.     // Remover de grupos si estaba en uno
  5513.     int groupNumber = g_iTriggerGroup[triggerIndex];
  5514.     if (groupNumber > 0)
  5515.     {
  5516.         MarkPlayerInGroupActivation(groupNumber, client, false);
  5517.     }
  5518. }
  5519.  
  5520. void RemovePlayerFromAllWaitingLists(int client)
  5521. {
  5522.     int userid = GetClientUserId(client);
  5523.    
  5524.     // Limpiar mensajes de espera para todos los grupos
  5525.     for (int group = 0; group < 100; group++)
  5526.     {
  5527.         g_fLastWaitMessage[client][group] = 0.0;
  5528.         g_bWaitMessageShown[client][group] = false;
  5529.     }
  5530.    
  5531.     // Remover de g_hWaitingGroups si existe
  5532.     if (g_hWaitingGroups != null)
  5533.     {
  5534.         StringMapSnapshot snapshot = g_hWaitingGroups.Snapshot();
  5535.         char groupKey[16];
  5536.        
  5537.         for (int i = 0; i < snapshot.Length; i++)
  5538.         {
  5539.             snapshot.GetKey(i, groupKey, sizeof(groupKey));
  5540.            
  5541.             ArrayList waitingList;
  5542.             if (g_hWaitingGroups.GetValue(groupKey, waitingList))
  5543.             {
  5544.                 int index = waitingList.FindValue(userid);
  5545.                 if (index != -1)
  5546.                 {
  5547.                     waitingList.Erase(index);
  5548.                     DebugPrint("RemovePlayerFromAllWaitingLists - %N removido de lista de espera del grupo %s", client, groupKey);
  5549.                    
  5550.                     if (waitingList.Length == 0)
  5551.                     {
  5552.                         delete waitingList;
  5553.                         g_hWaitingGroups.Remove(groupKey);
  5554.                     }
  5555.                 }
  5556.             }
  5557.         }
  5558.         delete snapshot;
  5559.     }
  5560. }
  5561.  
  5562. void CompletelyCleanPlayerFromTrigger(int client, int triggerIndex)
  5563. {
  5564.     if (triggerIndex < 0 || triggerIndex >= MAX_TRIGGERS) return;
  5565.    
  5566.     DebugPrint("CompletelyCleanPlayerFromTrigger - Limpiando completamente a %N del trigger %d", client, triggerIndex);
  5567.    
  5568.     // 1. Limpiar estados básicos
  5569.     g_bPlayerInTrigger[client][triggerIndex] = false;
  5570.     g_bPlayerReady[client][triggerIndex] = false;
  5571.     g_fPlayerLastCheck[client][triggerIndex] = 0.0;
  5572.     g_iPlayerRetryCount[client][triggerIndex] = 0;
  5573.     g_bWaitingForWeapon[client][triggerIndex] = false;
  5574.    
  5575.     // 2. Cancelar TODOS los timers relacionados
  5576.     if (g_hRetryTimer[client][triggerIndex] != null)
  5577.     {
  5578.         KillTimer(g_hRetryTimer[client][triggerIndex]);
  5579.         g_hRetryTimer[client][triggerIndex] = null;
  5580.     }
  5581.    
  5582.     // 3. Cancelar timer de verificación de presencia si existe
  5583.     // (no almacenamos su handle, pero podemos buscar alternativas)
  5584.    
  5585.     // 4. Remover de grupos de activación
  5586.     int groupNumber = g_iTriggerGroup[triggerIndex];
  5587.     if (groupNumber > 0)
  5588.     {
  5589.         MarkPlayerInGroupActivation(groupNumber, client, false);
  5590.         g_bWaitMessageShown[client][groupNumber] = false;
  5591.     }
  5592.    
  5593.     // 5. Remover de cualquier lista de espera de grupo
  5594.     RemovePlayerFromAllWaitingLists(client);
  5595. }
  5596.  
  5597. // ========================================================================
  5598. // FUNCIÓN PARA VERIFICAR JUGADORES DENTRO DE TRIGGERS AL INICIO
  5599. // ========================================================================
  5600.  
  5601. void CheckPlayersInTriggers()
  5602. {
  5603.     DebugPrint("CheckPlayersInTriggers - Iniciando verificación de jugadores en triggers...");
  5604.    
  5605.     for (int client = 1; client <= MaxClients; client++)
  5606.     {
  5607.         if (!IsValidSurvivor(client))
  5608.             continue;
  5609.        
  5610.         float clientPos[3];
  5611.         GetClientAbsOrigin(client, clientPos);
  5612.        
  5613.         for (int i = 0; i < MAX_TRIGGERS; i++)
  5614.         {
  5615.             if (!IsValidEntRef(g_iTriggers[i]))
  5616.                 continue;
  5617.            
  5618.             int trigger = EntRefToEntIndex(g_iTriggers[i]);
  5619.             if (trigger == INVALID_ENT_REFERENCE)
  5620.                 continue;
  5621.            
  5622.             float origin[3], mins[3], maxs[3];
  5623.             GetEntPropVector(trigger, Prop_Send, "m_vecOrigin", origin);
  5624.             GetEntPropVector(trigger, Prop_Send, "m_vecMins", mins);
  5625.             GetEntPropVector(trigger, Prop_Send, "m_vecMaxs", maxs);
  5626.            
  5627.             float worldMin[3], worldMax[3];
  5628.             worldMin[0] = origin[0] + mins[0];
  5629.             worldMin[1] = origin[1] + mins[1];
  5630.             worldMin[2] = origin[2] + mins[2];
  5631.             worldMax[0] = origin[0] + maxs[0];
  5632.             worldMax[1] = origin[1] + maxs[1];
  5633.             worldMax[2] = origin[2] + maxs[2];
  5634.            
  5635.             if (clientPos[0] >= worldMin[0] && clientPos[0] <= worldMax[0] &&
  5636.                 clientPos[1] >= worldMin[1] && clientPos[1] <= worldMax[1] &&
  5637.                 clientPos[2] >= worldMin[2] && clientPos[2] <= worldMax[2])
  5638.             {
  5639.                 if (!g_bPlayerInTrigger[client][i])
  5640.                 {
  5641.                     g_bPlayerInTrigger[client][i] = true;
  5642.                     g_bPlayerReady[client][i] = false;
  5643.                     g_iPlayerRetryCount[client][i] = 0;
  5644.                     g_bWaitingForWeapon[client][i] = false;
  5645.                    
  5646.                     DebugPrint("CheckPlayersInTriggers - Jugador %N agregado manualmente al trigger %d (tipo: %s)",
  5647.                         client, i, g_sTriggerType[i]);
  5648.                    
  5649.                     CreateTimer(0.2, Timer_DelayedCheck, client);
  5650.                 }
  5651.             }
  5652.             // ELIMINA la parte que limpia (else con CompletelyCleanPlayerFromTrigger)
  5653.         }
  5654.     }
  5655.    
  5656.     DebugPrint("CheckPlayersInTriggers - Verificación completada.");
  5657. }
  5658.  
  5659. // ========================================================================
  5660. // ACTUALIZACIÓN PERIÓDICA DE JUGADORES EN TRIGGERS (SOLO AGREGAR)
  5661. // ========================================================================
  5662.  
  5663. void UpdatePlayerTriggers()
  5664. {
  5665.     for (int client = 1; client <= MaxClients; client++)
  5666.     {
  5667.         if (!IsValidSurvivor(client))
  5668.             continue;
  5669.  
  5670.         float clientPos[3];
  5671.         GetClientAbsOrigin(client, clientPos);
  5672.  
  5673.         for (int i = 0; i < MAX_TRIGGERS; i++)
  5674.         {
  5675.             if (!IsValidEntRef(g_iTriggers[i]))
  5676.                 continue;
  5677.  
  5678.             int trigger = EntRefToEntIndex(g_iTriggers[i]);
  5679.             if (trigger == INVALID_ENT_REFERENCE)
  5680.                 continue;
  5681.  
  5682.             float origin[3], mins[3], maxs[3];
  5683.             GetEntPropVector(trigger, Prop_Send, "m_vecOrigin", origin);
  5684.             GetEntPropVector(trigger, Prop_Send, "m_vecMins", mins);
  5685.             GetEntPropVector(trigger, Prop_Send, "m_vecMaxs", maxs);
  5686.  
  5687.             float worldMin[3], worldMax[3];
  5688.             worldMin[0] = origin[0] + mins[0];
  5689.             worldMin[1] = origin[1] + mins[1];
  5690.             worldMin[2] = origin[2] + mins[2];
  5691.             worldMax[0] = origin[0] + maxs[0];
  5692.             worldMax[1] = origin[1] + maxs[1];
  5693.             worldMax[2] = origin[2] + maxs[2];
  5694.  
  5695.             bool inside = (clientPos[0] >= worldMin[0] && clientPos[0] <= worldMax[0] &&
  5696.                            clientPos[1] >= worldMin[1] && clientPos[1] <= worldMax[1] &&
  5697.                            clientPos[2] >= worldMin[2] && clientPos[2] <= worldMax[2]);
  5698.  
  5699.             // SOLO AGREGAR si está dentro y no está registrado
  5700.             if (inside && !g_bPlayerInTrigger[client][i])
  5701.             {
  5702.                 g_bPlayerInTrigger[client][i] = true;
  5703.                 g_bPlayerReady[client][i] = false;
  5704.                 g_iPlayerRetryCount[client][i] = 0;
  5705.                 g_bWaitingForWeapon[client][i] = false;
  5706.                 DebugPrint("UpdatePlayerTriggers - Jugador %N agregado manualmente al trigger %d (tipo: %s)",
  5707.                     client, i, g_sTriggerType[i]);
  5708.                 // Iniciar verificación
  5709.                 CreateTimer(0.2, Timer_DelayedCheck, client);
  5710.             }
  5711.             // NOTA: Ya no eliminamos a los que no están dentro, eso lo hace OnEndTouch.
  5712.         }
  5713.     }
  5714. }
  5715.  
  5716. void MarkPlayerReady(int client, int triggerIndex, bool ready)
  5717. {
  5718.     if (triggerIndex < 0 || triggerIndex >= MAX_TRIGGERS) return;
  5719.    
  5720.     g_bPlayerReady[client][triggerIndex] = ready;
  5721.     g_fPlayerLastCheck[client][triggerIndex] = GetGameTime();
  5722. }
  5723.  
  5724. int FindReadyTriggerForPlayer(int client, const char[] type)
  5725. {
  5726.     // Buscar en todos los triggers
  5727.     for (int i = 0; i < MAX_TRIGGERS; i++)
  5728.     {
  5729.         if (g_iTriggerGroup[i] != -1 && StrEqual(g_sTriggerType[i], type))
  5730.         {
  5731.             if (g_bPlayerReady[client][i])
  5732.             {
  5733.                 return i;
  5734.             }
  5735.         }
  5736.     }
  5737.    
  5738.     return -1;
  5739. }
  5740.  
  5741. // ========================================================================
  5742. // SISTEMA DE RETRY AUTOMÁTICO COMPLETO
  5743. // ========================================================================
  5744.  
  5745. public Action Timer_RetryCheck(Handle timer, DataPack pack)
  5746. {
  5747.     pack.Reset();
  5748.     int userid = pack.ReadCell();
  5749.     int triggerIndex = pack.ReadCell();
  5750.     delete pack;
  5751.    
  5752.     int client = GetClientOfUserId(userid);
  5753.     if (!IsValidSurvivor(client))
  5754.         return Plugin_Stop;
  5755.    
  5756.     // Cancelar referencia al timer
  5757.     g_hRetryTimer[client][triggerIndex] = null;
  5758.    
  5759.     // Verificar si el jugador sigue en el trigger
  5760.     if (!g_bPlayerInTrigger[client][triggerIndex])
  5761.     {
  5762.         DebugPrint("Timer_RetryCheck - %N ya no está en el trigger %d", client, triggerIndex);
  5763.         return Plugin_Stop;
  5764.     }
  5765.    
  5766.     // Verificar condiciones nuevamente
  5767.     if (CheckPlayerConditions(client, triggerIndex))
  5768.     {
  5769.         DebugPrint("Timer_RetryCheck - %N ahora cumple condiciones en trigger %d", client, triggerIndex);
  5770.         MarkPlayerReady(client, triggerIndex, true);
  5771.        
  5772.         // Intentar activar
  5773.         int groupNumber = g_iTriggerGroup[triggerIndex];
  5774.         if (groupNumber > 0)
  5775.         {
  5776.             AttemptGroupActivation(groupNumber, triggerIndex, client);
  5777.         }
  5778.         else
  5779.         {
  5780.             ExecuteSoloMovementFromTrigger(client, triggerIndex);
  5781.         }
  5782.     }
  5783.     else
  5784.     {
  5785.         // Incrementar contador de reintentos
  5786.         g_iPlayerRetryCount[client][triggerIndex]++;
  5787.        
  5788.         if (g_iPlayerRetryCount[client][triggerIndex] >= MAX_RETRY_ATTEMPTS)
  5789.         {
  5790.             DebugPrint("Timer_RetryCheck - %N excedió límite de reintentos en trigger %d", client, triggerIndex);
  5791.             PrintToChat(client, "[\x04Error\x01] Demasiados intentos fallidos. Verifica tu arma.");
  5792.             RemovePlayerFromTrigger(client, triggerIndex);
  5793.             return Plugin_Stop;
  5794.         }
  5795.        
  5796.         // Programar otro reintento
  5797.         DebugPrint("Timer_RetryCheck - %N aún no cumple condiciones, reintentando... (intento %d/%d)",
  5798.             client, g_iPlayerRetryCount[client][triggerIndex], MAX_RETRY_ATTEMPTS);
  5799.        
  5800.         DataPack retryPack = new DataPack();
  5801.         retryPack.WriteCell(userid);
  5802.         retryPack.WriteCell(triggerIndex);
  5803.         g_hRetryTimer[client][triggerIndex] = CreateTimer(0.5, Timer_RetryCheck, retryPack);
  5804.     }
  5805.    
  5806.     return Plugin_Stop;
  5807. }
  5808.  
  5809. // ========================================================================
  5810. // TIMER PARA REINTENTAR ACTIVACIÓN DESPUÉS DEL COOLDOWN
  5811. // ========================================================================
  5812.  
  5813. public Action Timer_RetryActivation(Handle timer, DataPack pack)
  5814. {
  5815.     pack.Reset();
  5816.     int groupNumber = pack.ReadCell();
  5817.     int triggerIdx = pack.ReadCell();
  5818.     int userid = pack.ReadCell();
  5819.     delete pack;
  5820.    
  5821.     int activator = GetClientOfUserId(userid);
  5822.     if (!IsValidSurvivor(activator))
  5823.         return Plugin_Stop;
  5824.    
  5825.     DebugPrint("Timer_RetryActivation - Reintentando activación del grupo %d (trigger %d)", groupNumber, triggerIdx);
  5826.     AttemptGroupActivation(groupNumber, triggerIdx, activator);
  5827.     return Plugin_Stop;
  5828. }
  5829.  
  5830. public Action Timer_UpdateTriggers(Handle timer)
  5831. {
  5832.     UpdatePlayerTriggers();
  5833.     return Plugin_Continue; // Para que siga ejecutándose
  5834. }
  5835.  
  5836. public Action Timer_CheckTriggers(Handle timer)
  5837. {
  5838.     CheckPlayersInTriggers();
  5839.     return Plugin_Stop;
  5840. }
  5841.  
  5842. // ========================================================================
  5843. // FUNCIÓN MEJORADA PARA DETENER MOVIMIENTO (CON LIMPIEZA DE GRUPO)
  5844. // ========================================================================
  5845.  
  5846. void ForceStopClientMovement(int client, bool showMessage = true)
  5847. {
  5848.     if (client < 1 || client > MaxClients) return;
  5849.    
  5850.     if (!g_bClientInMovement[client])
  5851.     {
  5852.         if (showMessage)
  5853.             PrintToChat(client, "[\x04Movimiento\x01] No estás en movimiento");
  5854.         return;
  5855.     }
  5856.    
  5857.     DebugPrint("ForceStopClientMovement - %N deteniendo movimiento manualmente", client);
  5858.    
  5859.     // Guardar información del grupo antes de limpiar
  5860.     int groupNumber = g_iClientMovementGroup[client];
  5861.     char movementFile[128];
  5862.     strcopy(movementFile, sizeof(movementFile), g_sClientMovementFile[client]);
  5863.    
  5864.     // 1. Limpiar contador visual
  5865.     PrintHintText(client, " ");
  5866.    
  5867.     // 2. Cancelar TODOS los timers
  5868.     if (g_hClientCountdownTimer[client] != null)
  5869.     {
  5870.         KillTimer(g_hClientCountdownTimer[client]);
  5871.         g_hClientCountdownTimer[client] = null;
  5872.     }
  5873.    
  5874.     if (g_hClientMovementTimer[client] != null)
  5875.     {
  5876.         KillTimer(g_hClientMovementTimer[client]);
  5877.         g_hClientMovementTimer[client] = null;
  5878.     }
  5879.    
  5880.     // 3. Llamar al ConVar del movement reader
  5881.     if (g_hCvarStopPlayer != null)
  5882.     {
  5883.         char clientStr[12];
  5884.         IntToString(client, clientStr, sizeof(clientStr));
  5885.         g_hCvarStopPlayer.SetString(clientStr);
  5886.     }
  5887.    
  5888.     // 4. Limpiar estado interno
  5889.     g_bClientInMovement[client] = false;
  5890.     g_sClientMovementFile[client][0] = '\0';
  5891.     g_sClientMovementFileName[client][0] = '\0';
  5892.     g_fClientMovementEndTime[client] = 0.0;
  5893.     g_fClientMovementDuration[client] = 0.0;
  5894.    
  5895.     // 5. Liberar del grupo si estaba en uno
  5896.     if (groupNumber > 0)
  5897.     {
  5898.         MarkPlayerInGroupActivation(groupNumber, client, false);
  5899.         DebugPrint("ForceStopClientMovement - %N liberado del grupo %d", client, groupNumber);
  5900.     }
  5901.    
  5902.     // 6. Mensaje al jugador
  5903.     if (showMessage)
  5904.     {
  5905.         PrintToChat(client, "[\x04Movimiento\x01] Movimiento detenido");
  5906.     }
  5907. }
  5908.  
  5909. // ========================================================================
  5910. // COMANDOS
  5911. // ========================================================================
  5912.  
  5913. public Action Command_ShowModels(int client, int args)
  5914. {
  5915.     CreateVisualModels();
  5916.     PrintToChat(client, "[\x04Sistema\x01] Modelos de cono mostrados en todos los triggers");
  5917.     return Plugin_Handled;
  5918. }
  5919.  
  5920. public Action Command_HideModels(int client, int args)
  5921. {
  5922.     RemoveVisualModels();
  5923.     PrintToChat(client, "[\x04Sistema\x01] Modelos de cono escondidos");
  5924.     return Plugin_Handled;
  5925. }
  5926.  
  5927. public Action Command_Groups(int client, int args)
  5928. {
  5929.     PrintToChat(client, "[\x04Grupos\x01] === Estado de Grupos ===");
  5930.     PrintToChat(client, "[\x04Grupos\x01] Sistema: %s", g_hCvarGroupsEnable.BoolValue ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5931.     PrintToChat(client, "[\x04Grupos\x01] P/M: %s", g_hCvarGroupPM.BoolValue ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5932.     PrintToChat(client, "[\x04Grupos\x01] Bile: %s", g_hCvarGroupBile.BoolValue ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5933.     PrintToChat(client, "[\x04Grupos\x01] GL: %s", g_hCvarGroupGL.BoolValue ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5934.     PrintToChat(client, "[\x04Grupos\x01] Run: %s", g_hCvarGroupRun.BoolValue ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5935.     PrintToChat(client, "[\x04Grupos\x01] Dual: %s", g_hCvarGroupDual.BoolValue ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5936.     PrintToChat(client, "[\x04Grupos\x01] Uso: !togglegroup <pm|bile|gl|run|dual|all>");
  5937.    
  5938.     return Plugin_Handled;
  5939. }
  5940.  
  5941. public Action Command_ToggleGroup(int client, int args)
  5942. {
  5943.     if (args < 1)
  5944.     {
  5945.         PrintToChat(client, "[\x04Uso\x01] !togglegroup <pm|bile|gl|run|dual|all>");
  5946.         return Plugin_Handled;
  5947.     }
  5948.    
  5949.     char group[16];
  5950.     GetCmdArg(1, group, sizeof(group));
  5951.    
  5952.     if (StrEqual(group, "all", false))
  5953.     {
  5954.         bool newState = !g_hCvarGroupsEnable.BoolValue;
  5955.         g_hCvarGroupsEnable.SetBool(newState);
  5956.        
  5957.         PrintToChat(client, "[\x04Grupos\x01] Todos los grupos: %s", newState ? "\x04ACTIVADOS" : "\x02DESACTIVADOS");
  5958.         DebugPrint("Command_ToggleGroup - %N cambió todos los grupos a: %s", client, newState ? "ACTIVADOS" : "DESACTIVADOS");
  5959.     }
  5960.     else if (StrEqual(group, "pm", false))
  5961.     {
  5962.         bool newState = !g_hCvarGroupPM.BoolValue;
  5963.         g_hCvarGroupPM.SetBool(newState);
  5964.        
  5965.         PrintToChat(client, "[\x04Grupos\x01] Grupo P/M: %s", newState ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5966.         DebugPrint("Command_ToggleGroup - %N cambió grupo P/M a: %s", client, newState ? "ACTIVADO" : "DESACTIVADO");
  5967.     }
  5968.     else if (StrEqual(group, "bile", false))
  5969.     {
  5970.         bool newState = !g_hCvarGroupBile.BoolValue;
  5971.         g_hCvarGroupBile.SetBool(newState);
  5972.        
  5973.         PrintToChat(client, "[\x04Grupos\x01] Grupo Bile: %s", newState ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5974.         DebugPrint("Command_ToggleGroup - %N cambió grupo Bile a: %s", client, newState ? "ACTIVADO" : "DESACTIVADO");
  5975.     }
  5976.     else if (StrEqual(group, "gl", false))
  5977.     {
  5978.         bool newState = !g_hCvarGroupGL.BoolValue;
  5979.         g_hCvarGroupGL.SetBool(newState);
  5980.        
  5981.         PrintToChat(client, "[\x04Grupos\x01] Grupo GL: %s", newState ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5982.         DebugPrint("Command_ToggleGroup - %N cambió grupo GL a: %s", client, newState ? "ACTIVADO" : "DESACTIVADO");
  5983.     }
  5984.     else if (StrEqual(group, "run", false))
  5985.     {
  5986.         bool newState = !g_hCvarGroupRun.BoolValue;
  5987.         g_hCvarGroupRun.SetBool(newState);
  5988.        
  5989.         PrintToChat(client, "[\x04Grupos\x01] Grupo Run: %s", newState ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5990.         DebugPrint("Command_ToggleGroup - %N cambió grupo Run a: %s", client, newState ? "ACTIVADO" : "DESACTIVADO");
  5991.     }
  5992.     else if (StrEqual(group, "dual", false))
  5993.     {
  5994.         bool newState = !g_hCvarGroupDual.BoolValue;
  5995.         g_hCvarGroupDual.SetBool(newState);
  5996.        
  5997.         PrintToChat(client, "[\x04Grupos\x01] Grupo Dual: %s", newState ? "\x04ACTIVADO" : "\x02DESACTIVADO");
  5998.         DebugPrint("Command_ToggleGroup - %N cambió grupo Dual a: %s", client, newState ? "ACTIVADO" : "DESACTIVADO");
  5999.     }
  6000.     else
  6001.     {
  6002.         PrintToChat(client, "[\x04Error\x01] Grupo no válido. Usa: pm, bile, gl, run, dual, all");
  6003.         return Plugin_Handled;
  6004.     }
  6005.    
  6006.     if (g_hMovementFiles.Length > 0)
  6007.     {
  6008.         RecreateTriggers();
  6009.     }
  6010.    
  6011.     return Plugin_Handled;
  6012. }
  6013.  
  6014. // ========================================================================
  6015. // COMANDOS BÁSICOS
  6016. // ========================================================================
  6017.  
  6018. public Action Command_ForceMovement(int client, int args)
  6019. {
  6020.     if (args < 1)
  6021.     {
  6022.         PrintToChat(client, "[\x04Uso\x01] !move <nombre_archivo>");
  6023.         return Plugin_Handled;
  6024.     }
  6025.    
  6026.     char movementFile[128];
  6027.     GetCmdArg(1, movementFile, sizeof(movementFile));
  6028.    
  6029.     bool exists;
  6030.     if (!g_hFileExistence.GetValue(movementFile, exists) || !exists)
  6031.     {
  6032.         PrintToChat(client, "[\x04Error\x01] Archivo no encontrado: %s", movementFile);
  6033.         return Plugin_Handled;
  6034.     }
  6035.    
  6036.     MovementFileInfo info;
  6037.     bool found = MA_GetMovementFileInfo(movementFile, g_hMovementFileInfos, info);
  6038.    
  6039.     if (!found)
  6040.     {
  6041.         PrintToChat(client, "[\x04Error\x01] No se pudo obtener información del archivo");
  6042.         return Plugin_Handled;
  6043.     }
  6044.    
  6045.     if (info.category == CATEGORY_BOOSTER_PURE || info.category == CATEGORY_RUNNER_GROUP)
  6046.     {
  6047.         PrintToChat(client, "[\x04Error\x01] Este movimiento requiere grupo. Actívalo desde el trigger.");
  6048.         return Plugin_Handled;
  6049.     }
  6050.    
  6051.     ExecutePlayerMovementImmediate(client, movementFile, MSG_INDIVIDUAL);
  6052.    
  6053.     return Plugin_Handled;
  6054. }
  6055.  
  6056. public Action Command_StopMovement(int client, int args)
  6057. {
  6058.     if (g_bClientInMovement[client])
  6059.     {
  6060.         ForceStopClientMovement(client, true);
  6061.         PrintToChat(client, "[\x04Movimiento\x01] Movimiento detenido");
  6062.     }
  6063.     else
  6064.     {
  6065.         PrintToChat(client, "[\x04Error\x01] No estás ejecutando ningún movimiento");
  6066.     }
  6067.    
  6068.     return Plugin_Handled;
  6069. }
  6070.  
  6071. public Action Command_StopMe(int client, int args)
  6072. {
  6073.     if (!client || !IsClientInGame(client))
  6074.     {
  6075.         ReplyToCommand(client, "[SM] Este comando solo puede ser usado en el juego.");
  6076.         return Plugin_Handled;
  6077.     }
  6078.    
  6079.     if (!g_bClientInMovement[client])
  6080.     {
  6081.         PrintToChat(client, "[\x04Movimiento\x01] No estás ejecutando ningún movimiento");
  6082.         return Plugin_Handled;
  6083.     }
  6084.    
  6085.     // Detener el movimiento
  6086.     ForceStopClientMovement(client, true);
  6087.    
  6088.     return Plugin_Handled;
  6089. }
  6090.  
  6091. public Action Command_ReloadMovements(int client, int args)
  6092. {
  6093.     LoadMovementFilesAutomatically();
  6094.     PrintToChat(client, "[\x04Sistema\x01] Movimientos recargados");
  6095.     return Plugin_Handled;
  6096. }
  6097.  
  6098. public Action Command_ForceDual(int client, int args)
  6099. {
  6100.     if (args < 2)
  6101.     {
  6102.         PrintToChat(client, "[\x04Uso\x01] !forcedual <archivo1> <archivo2> [jugador2]");
  6103.         return Plugin_Handled;
  6104.     }
  6105.    
  6106.     char file1[128], file2[128];
  6107.     GetCmdArg(1, file1, sizeof(file1));
  6108.     GetCmdArg(2, file2, sizeof(file2));
  6109.    
  6110.     int partner = client;
  6111.     if (args >= 3)
  6112.     {
  6113.         char arg[64];
  6114.         GetCmdArg(3, arg, sizeof(arg));
  6115.         partner = FindTarget(client, arg, true, false);
  6116.     }
  6117.    
  6118.     if (partner == -1)
  6119.     {
  6120.         PrintToChat(client, "[\x04Error\x01] Jugador no encontrado");
  6121.         return Plugin_Handled;
  6122.     }
  6123.    
  6124.     // Verificar archivos...
  6125.    
  6126.     // Obtener información de los archivos USANDO EL INCLUDE
  6127.     MovementFileInfo info1, info2;
  6128.     bool found1 = MA_GetMovementFileInfo(file1, g_hMovementFileInfos, info1);
  6129.     bool found2 = MA_GetMovementFileInfo(file2, g_hMovementFileInfos, info2);
  6130.    
  6131.     if (!found1 || !found2)
  6132.     {
  6133.         PrintToChat(client, "[\x04Error\x01] No se pudo obtener información de los archivos");
  6134.         return Plugin_Handled;
  6135.     }
  6136.    
  6137.     // Obtener nombres base para mostrar (sin autor)
  6138.     char baseName1[64], baseName2[64];
  6139.     GetBaseFileNameWithoutAuthor(file1, baseName1, sizeof(baseName1));
  6140.     GetBaseFileNameWithoutAuthor(file2, baseName2, sizeof(baseName2));
  6141.    
  6142.     // Obtener autores
  6143.     char author1[32], author2[32];
  6144.     strcopy(author1, sizeof(author1), info1.sAuthor);
  6145.     strcopy(author2, sizeof(author2), info2.sAuthor);
  6146.     if (strlen(author1) == 0) strcopy(author1, sizeof(author1), "Desconocido");
  6147.     if (strlen(author2) == 0) strcopy(author2, sizeof(author2), "Desconocido");
  6148.    
  6149.     // Obtener duraciones
  6150.     float duration1 = 10.0, duration2 = 10.0;
  6151.     if (g_hMovementDurations != null)
  6152.     {
  6153.         g_hMovementDurations.GetValue(file1, duration1);
  6154.         g_hMovementDurations.GetValue(file2, duration2);
  6155.     }
  6156.    
  6157.     // Mostrar autores (si son diferentes)
  6158.     if (!StrEqual(author1, author2))
  6159.     {
  6160.         PrintToChatAll("[\x04Movimiento\x01] Creado por '\x04%s\x01' y '\x04%s\x01'", author1, author2);
  6161.     }
  6162.     else if (!StrEqual(author1, "Desconocido"))
  6163.     {
  6164.         PrintToChatAll("[\x04Movimiento\x01] Creado por '\x04%s\x01'", author1);
  6165.     }
  6166.    
  6167.     // Determinar el tipo de dual basado en los tipos de movimiento
  6168.     char dualType[16] = "Run"; // Valor por defecto
  6169.    
  6170.     // Verificar si es un dual con booster
  6171.     if (info1.movType == MOVEMENT_PM || info2.movType == MOVEMENT_PM ||
  6172.         info1.movType == MOVEMENT_RUNPM || info2.movType == MOVEMENT_RUNPM)
  6173.     {
  6174.         dualType = "PM";
  6175.     }
  6176.     else if (info1.movType == MOVEMENT_BILE || info2.movType == MOVEMENT_BILE ||
  6177.              info1.movType == MOVEMENT_RUNBILE || info2.movType == MOVEMENT_RUNBILE)
  6178.     {
  6179.         dualType = "Bile";
  6180.     }
  6181.     else if (info1.movType == MOVEMENT_GL || info2.movType == MOVEMENT_GL ||
  6182.              info1.movType == MOVEMENT_RUNGL || info2.movType == MOVEMENT_RUNGL)
  6183.     {
  6184.         dualType = "GL";
  6185.     }
  6186.    
  6187.     // Mostrar mensaje público usando el sistema antiguo
  6188.     PrintToChatAll("[\x04M. Dual-%s\x01] %s(%.1fs) + %s(%.1fs)",
  6189.         dualType, baseName1, duration1, baseName2, duration2);
  6190.    
  6191.     // Mensajes para los jugadores
  6192.     PrintToChat(client, "[\x04M. Dual-%s\x01] %N + %N", dualType, client, partner);
  6193.     PrintToChat(partner, "[\x04M. Dual-%s\x01] %N + %N", dualType, client, partner);
  6194.    
  6195.     // También usar el nuevo sistema de nombres para consistencia
  6196.     ArrayList files = new ArrayList(ByteCountToCells(128));
  6197.     ArrayList players = new ArrayList();
  6198.    
  6199.     files.PushString(file1);
  6200.     files.PushString(file2);
  6201.     players.Push(client);
  6202.     players.Push(partner);
  6203.    
  6204.     // Analizar para obtener nombre del grupo
  6205.     GroupAnalysis analysis;
  6206.     if (AnalyzeActivatedFiles(0, files, analysis))
  6207.     {
  6208.         char groupName[64];
  6209.         GenerateGroupDisplayName(analysis, 2, groupName, sizeof(groupName));
  6210.        
  6211.         // Mostrar mensaje con nuevo formato también
  6212.         ArrayList durations = new ArrayList();
  6213.         durations.Push(duration1);
  6214.         durations.Push(duration2);
  6215.        
  6216.         ArrayList sortedFiles = new ArrayList(ByteCountToCells(128));
  6217.         ArrayList sortedDurations = new ArrayList();
  6218.         SortFilesForDisplay(files, durations, sortedFiles, sortedDurations);
  6219.        
  6220.         char publicMessage[512];
  6221.         FormatPublicMessage(groupName, 0, sortedFiles, sortedDurations, publicMessage, sizeof(publicMessage));
  6222.        
  6223.         PrintToChatAll("%s", publicMessage);
  6224.        
  6225.         delete durations;
  6226.         delete sortedFiles;
  6227.         delete sortedDurations;
  6228.     }
  6229.    
  6230.     // Ejecutar movimientos
  6231.     ExecutePlayerMovementImmediate(client, file1, MSG_DUAL);
  6232.     ExecutePlayerMovementImmediate(partner, file2, MSG_DUAL);
  6233.    
  6234.     delete files;
  6235.     delete players;
  6236.    
  6237.     return Plugin_Handled;
  6238. }
  6239.  
  6240. // =======================================================================
  6241. // COMMAND LISTENER PARA st_mr_stop (GLOBAL)
  6242. // =======================================================================
  6243.  
  6244. public Action Command_GlobalStop(int client, const char[] command, int argc)
  6245. {
  6246.     DebugPrint("Command_GlobalStop - Comando %s detectado - Deteniendo todos los movimientos", command);
  6247.    
  6248.     for (int i = 1; i <= MaxClients; i++)
  6249.     {
  6250.         if (g_bClientInMovement[i])
  6251.         {
  6252.             ForceStopClientMovement(i);
  6253.         }
  6254.     }
  6255.    
  6256.     if (g_hActiveGroups != null)
  6257.         g_hActiveGroups.Clear();
  6258.    
  6259.     if (g_hWaitingGroups != null)
  6260.         g_hWaitingGroups.Clear();
  6261.    
  6262.     // Limpiar control de jugadores por grupo
  6263.     if (g_hPlayersInGroupActivation != null)
  6264.     {
  6265.         g_hPlayersInGroupActivation.Clear();
  6266.     }
  6267.    
  6268.     return Plugin_Continue;
  6269. }
  6270.  
  6271. // ========================================================================
  6272. // FUNCIONES AUXILIARES ADICIONALES
  6273. // ========================================================================
  6274.  
  6275. void ShowGroupActivationMessages(int groupNumber, ArrayList players, ArrayList movementFiles)
  6276. {
  6277.     if (players == null || players.Length == 0) return;
  6278.    
  6279.     // Obtener autores únicos
  6280.     ArrayList uniqueAuthors = new ArrayList(ByteCountToCells(32));
  6281.     char authorsList[128] = "";
  6282.    
  6283.     for (int i = 0; i < movementFiles.Length; i++)
  6284.     {
  6285.         char file[128];
  6286.         movementFiles.GetString(i, file, sizeof(file));
  6287.        
  6288.         MovementFileInfo info;
  6289.         if (MA_GetMovementFileInfo(file, g_hMovementFileInfos, info))
  6290.         {
  6291.             if (strlen(info.sAuthor) > 0 && uniqueAuthors.FindString(info.sAuthor) == -1)
  6292.             {
  6293.                 uniqueAuthors.PushString(info.sAuthor);
  6294.             }
  6295.         }
  6296.     }
  6297.    
  6298.     // Construir lista de autores
  6299.     for (int i = 0; i < uniqueAuthors.Length; i++)
  6300.     {
  6301.         char author[32];
  6302.         uniqueAuthors.GetString(i, author, sizeof(author));
  6303.        
  6304.         if (i == 0)
  6305.             Format(authorsList, sizeof(authorsList), "'\x04%s\x01'", author);
  6306.         else if (i == uniqueAuthors.Length - 1)
  6307.             Format(authorsList, sizeof(authorsList), "%s y '\x04%s\x01'", authorsList, author);
  6308.         else
  6309.             Format(authorsList, sizeof(authorsList), "%s, '\x04%s\x01'", authorsList, author);
  6310.     }
  6311.    
  6312.     delete uniqueAuthors;
  6313.    
  6314.     // Mostrar mensaje según cantidad de jugadores
  6315.     if (players.Length == 2)
  6316.     {
  6317.         PrintToChatAll("[\x04Grupo %d\x01] Movimientos creados por %s", groupNumber, authorsList);
  6318.        
  6319.         // Mostrar nombres de jugadores a los involucrados
  6320.         int client1 = players.Get(0);
  6321.         int client2 = players.Get(1);
  6322.        
  6323.         if (IsValidSurvivor(client1) && IsValidSurvivor(client2))
  6324.         {
  6325.             PrintToChat(client1, "[\x04Grupo %d\x01] %N + %N", groupNumber, client1, client2);
  6326.             PrintToChat(client2, "[\x04Grupo %d\x01] %N + %N", groupNumber, client1, client2);
  6327.         }
  6328.     }
  6329.     else if (players.Length > 2)
  6330.     {
  6331.         PrintToChatAll("[\x04Grupo %d\x01] Movimientos creados por %s y otros", groupNumber, authorsList);
  6332.        
  6333.         // Mostrar lista de jugadores
  6334.         char playerList[256] = "";
  6335.         for (int i = 0; i < players.Length; i++)
  6336.         {
  6337.             int client = players.Get(i);
  6338.             if (IsValidSurvivor(client))
  6339.             {
  6340.                 if (i == 0)
  6341.                     Format(playerList, sizeof(playerList), "%N", client);
  6342.                 else if (i == players.Length - 1)
  6343.                     Format(playerList, sizeof(playerList), "%s y %N", playerList, client);
  6344.                 else
  6345.                     Format(playerList, sizeof(playerList), "%s, %N", playerList, client);
  6346.             }
  6347.         }
  6348.        
  6349.         // Mostrar a todos los jugadores del grupo
  6350.         for (int i = 0; i < players.Length; i++)
  6351.         {
  6352.             int client = players.Get(i);
  6353.             if (IsValidSurvivor(client))
  6354.             {
  6355.                 PrintToChat(client, "[\x04Grupo %d\x01] %s", groupNumber, playerList);
  6356.             }
  6357.         }
  6358.     }
  6359. }
  6360.  
  6361. // Preparación para sistema de aleatoriedad (futuro)
  6362. char[] GetRandomMovementForGroup(int groupNumber)
  6363. {
  6364.     char selectedFile[128] = "";
  6365.    
  6366.     // Buscar todos los archivos "run" de este grupo
  6367.     ArrayList groupRunFiles = new ArrayList(ByteCountToCells(128));
  6368.    
  6369.     for (int i = 0; i < g_hMovementFileInfos.Length; i++)
  6370.     {
  6371.         MovementFileInfo info;
  6372.         g_hMovementFileInfos.GetArray(i, info, sizeof(info));
  6373.        
  6374.         if (info.iNumber == groupNumber && info.movType == MOVEMENT_RUN)
  6375.         {
  6376.             groupRunFiles.PushString(info.sFullName);
  6377.         }
  6378.     }
  6379.    
  6380.     if (groupRunFiles.Length > 0)
  6381.     {
  6382.         // Por ahora: seleccionar el primero (aleatoriedad futura)
  6383.         // FUTURO: int randomIndex = GetRandomInt(0, groupRunFiles.Length - 1);
  6384.         int randomIndex = 0;
  6385.         groupRunFiles.GetString(randomIndex, selectedFile, sizeof(selectedFile));
  6386.     }
  6387.    
  6388.     delete groupRunFiles;
  6389.     return selectedFile;
  6390. }
  6391.  
  6392. // ========================================================================
  6393. // FUNCIÓN PARA DETERMINAR AUTORES Y MOSTRAR MENSAJES (COMPATIBILIDAD)
  6394. // ========================================================================
  6395.  
  6396. void ShowAuthorsAndNames(int groupNumber, ArrayList players, ArrayList files)
  6397. {
  6398.     if (players == null || players.Length == 0) return;
  6399.    
  6400.     // Usar el nuevo sistema para grupos de 1-3 jugadores
  6401.     if (players.Length <= 3)
  6402.     {
  6403.         ShowGroupActivationSummary(groupNumber, players, files);
  6404.         return;
  6405.     }
  6406.    
  6407.     // Para 4+ jugadores, usar sistema antiguo (compatibilidad)
  6408.     ShowGroupActivationMessages(groupNumber, players, files);
  6409. }
  6410.  
  6411. // Función para obtener nombre base sin autor
  6412. public void GetBaseFileNameWithoutAuthor(const char[] fullPath, char[] baseName, int maxlen)
  6413. {
  6414.     // Primero obtener el nombre base del archivo
  6415.     GetBaseFileName(fullPath, baseName, maxlen);
  6416.    
  6417.     // Remover el autor (todo después del último '_')
  6418.     int lastUnderscore = FindCharInString(baseName, '_', true);
  6419.     if (lastUnderscore != -1)
  6420.     {
  6421.         baseName[lastUnderscore] = '\0';
  6422.        
  6423.         // También remover cualquier guión bajo extra al final
  6424.         while (lastUnderscore > 0 && baseName[lastUnderscore-1] == '_')
  6425.         {
  6426.             baseName[lastUnderscore-1] = '\0';
  6427.             lastUnderscore--;
  6428.         }
  6429.     }
  6430. }
  6431.  
  6432. // Función para extraer autor del nombre del archivo
  6433. public bool ExtractAuthorFromFileName(const char[] fileName, char[] author, int maxlen)
  6434. {
  6435.     char temp[128];
  6436.     strcopy(temp, sizeof(temp), fileName);
  6437.    
  6438.     // Buscar el último "_" para separar nombre y autor
  6439.     int underscorePos = FindCharInString(temp, '_', true);
  6440.     if (underscorePos != -1)
  6441.     {
  6442.         strcopy(author, maxlen, temp[underscorePos + 1]);
  6443.         return true;
  6444.     }
  6445.    
  6446.     author[0] = '\0';
  6447.     return false;
  6448. }
  6449.  
  6450. // Función para formatear lista de autores
  6451. public void FormatAuthorsList(ArrayList authors, char[] buffer, int maxlen)
  6452. {
  6453.     buffer[0] = '\0';
  6454.    
  6455.     if (authors == null || authors.Length == 0)
  6456.         return;
  6457.    
  6458.     // Primero, eliminar duplicados (ya deberían estar eliminados, pero por seguridad)
  6459.     ArrayList unique = new ArrayList(ByteCountToCells(32));
  6460.     for (int i = 0; i < authors.Length; i++)
  6461.     {
  6462.         char author[32];
  6463.         authors.GetString(i, author, sizeof(author));
  6464.        
  6465.         if (unique.FindString(author) == -1)
  6466.         {
  6467.             unique.PushString(author);
  6468.         }
  6469.     }
  6470.    
  6471.     // Formatear lista
  6472.     for (int i = 0; i < unique.Length; i++)
  6473.     {
  6474.         char author[32];
  6475.         unique.GetString(i, author, sizeof(author));
  6476.        
  6477.         if (i == 0)
  6478.         {
  6479.             Format(buffer, maxlen, "'\x04%s\x01'", author);
  6480.         }
  6481.         else if (i == unique.Length - 1)
  6482.         {
  6483.             Format(buffer, maxlen, "%s y '\x04%s\x01'", buffer, author);
  6484.         }
  6485.         else
  6486.         {
  6487.             Format(buffer, maxlen, "%s, '\x04%s\x01'", buffer, author);
  6488.         }
  6489.     }
  6490.    
  6491.     delete unique;
  6492. }
  6493.  
  6494. // Función para mensaje público simple
  6495. public void FormatSimplePublicMessage(const char[] groupName, int groupNumber, ArrayList baseFileNames,
  6496.                                      ArrayList durations, char[] buffer, int bufferLen)
  6497. {
  6498.     // Construir parte de archivos
  6499.     char filesPart[512];
  6500.     filesPart[0] = '\0';
  6501.    
  6502.     for (int i = 0; i < baseFileNames.Length; i++)
  6503.     {
  6504.         char baseName[64];
  6505.         baseFileNames.GetString(i, baseName, sizeof(baseName));
  6506.         float duration = durations.Get(i);
  6507.        
  6508.         char temp[128];
  6509.         Format(temp, sizeof(temp), "%s(%.1fs)", baseName, duration);
  6510.        
  6511.         if (i == 0)
  6512.             strcopy(filesPart, sizeof(filesPart), temp);
  6513.         else
  6514.             Format(filesPart, sizeof(filesPart), "%s + %s", filesPart, temp);
  6515.     }
  6516.    
  6517.     // Combinar todo
  6518.     Format(buffer, bufferLen, "[\x04MA %s (%d)\x01] %s", groupName, groupNumber, filesPart);
  6519.    
  6520.     DebugPrint("FormatSimplePublicMessage - %s", buffer);
  6521. }
  6522.  
  6523. // ========================================================================
  6524. // FUNCIÓN DE DEPURACIÓN DE ARCHIVOS (OPCIONAL, SOLO SI HAY CVAR DE DEBUG)
  6525. // ========================================================================
  6526.  
  6527. void DebugMovementFilesStructure()
  6528. {
  6529.     if (!g_hCvarDebug.BoolValue) return;
  6530.    
  6531.     DebugPrint("\n=== ESTRUCTURA DE ARCHIVOS DE MOVIMIENTO ===");
  6532.     DebugPrint("Total de archivos cargados: %d", g_hMovementFileInfos.Length);
  6533.    
  6534.     for (int i = 0; i < g_hMovementFileInfos.Length; i++)
  6535.     {
  6536.         MovementFileInfo info;
  6537.         g_hMovementFileInfos.GetArray(i, info, sizeof(info));
  6538.        
  6539.         char typeName[32];
  6540.         switch(info.movType)
  6541.         {
  6542.             case MOVEMENT_PM: typeName = "PM";
  6543.             case MOVEMENT_BILE: typeName = "BILE";
  6544.             case MOVEMENT_GL: typeName = "GL";
  6545.             case MOVEMENT_RUN: typeName = "RUN";
  6546.             case MOVEMENT_DUAL: typeName = "DUAL";
  6547.             case MOVEMENT_RUNPM: typeName = "RUNPM";
  6548.             case MOVEMENT_RUNBILE: typeName = "RUNBILE";
  6549.             case MOVEMENT_RUNGL: typeName = "RUNGL";
  6550.             default: typeName = "UNKNOWN";
  6551.         }
  6552.        
  6553.         char catName[32];
  6554.         switch(info.category)
  6555.         {
  6556.             case CATEGORY_INDIVIDUAL: catName = "INDIVIDUAL";
  6557.             case CATEGORY_HYBRID: catName = "HYBRID";
  6558.             case CATEGORY_BOOSTER_PURE: catName = "BOOSTER";
  6559.             case CATEGORY_RUNNER_GROUP: catName = "RUNNER_GROUP";
  6560.             default: catName = "UNKNOWN";
  6561.         }
  6562.        
  6563.         if (info.bHasLetter)
  6564.         {
  6565.             DebugPrint("[%d] %s -> Grupo: %d, Letra: %c, Tipo: %s, Categoría: %s",
  6566.                 i, info.sFullName, info.iNumber, info.sLetter, typeName, catName);
  6567.         }
  6568.         else
  6569.         {
  6570.             DebugPrint("[%d] %s -> Grupo: %d, Tipo: %s, Categoría: %s",
  6571.                 i, info.sFullName, info.iNumber, typeName, catName);
  6572.         }
  6573.     }
  6574. }
  6575.  
  6576. // ========================================================================
  6577. // FIN DEL PLUGIN - SISTEMA COMPLETO V21R CON NUEVO SISTEMA DE NOMBRES
  6578. // ========================================================================
Advertisement
Add Comment
Please, Sign In to add comment