Repository navigation
Expand file tree
/
Copy pathWCS_BrainML.lua
More file actions
1194 lines (992 loc) · 44.3 KB
/
Copy pathWCS_BrainML.lua
File metadata and controls
1194 lines (992 loc) · 44.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--[[
WCS_BrainML.lua - Sistema de Machine Learning v6.4.2
Compatible con Lua 5.0 (WoW 1.12 / Turtle WoW)
SISTEMAS INCLUIDOS:
- Memoria Persistente (SavedVariables)
- Aprendizaje por Combate
- Pesos Dinamicos
- Decay Temporal
- Estadisticas de Rendimiento
]]--
if WCS_Brain and WCS_Brain.ENABLED == false then return end
WCS_BrainML = WCS_BrainML or {}
WCS_BrainML.VERSION = "6.4.2"
-- ============================================================================
-- UTILIDADES LUA 5.0
-- ============================================================================
local function getTime()
return GetTime and GetTime() or 0
end
local function debugPrint(msg)
if WCS_Brain and WCS_Brain.DEBUG then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r " .. tostring(msg))
end
end
-- ============================================================================
-- CONFIGURACION DEL SISTEMA DE APRENDIZAJE
-- ============================================================================
WCS_BrainML.Config = {
-- Tasa de aprendizaje (que tan rapido cambian los pesos)
learningRate = 0.1,
-- Decay temporal (half-life en minutos)
decayHalfLife = 30,
-- Minimo de combates para empezar a ajustar pesos
minCombatsForLearning = 3,
-- Peso minimo y maximo para evitar extremos
minWeight = 0.1,
maxWeight = 3.0,
-- Intervalo de auto-guardado (segundos)
autoSaveInterval = 300, -- 5 minutos
-- Maximo de registros de combate a guardar
maxCombatHistory = 100
}
-- ============================================================================
-- ESTRUCTURA DE DATOS PERSISTENTE
-- ============================================================================
-- Esta estructura se guarda en WCS_BrainSaved
WCS_BrainML.DefaultData = {
version = "1.0.0",
-- Pesos dinamicos por hechizo (modifican el scoring)
spellWeights = {
-- Formato: [spellName] = weight (1.0 = neutral)
["Shadow Bolt"] = 1.0,
["Corruption"] = 1.0,
["Curse of Agony"] = 1.0,
["Immolate"] = 1.0,
["Siphon Life"] = 1.0,
["Life Tap"] = 1.0,
["Dark Pact"] = 1.0,
["Drain Life"] = 1.0,
["Death Coil"] = 1.0,
["Fear"] = 1.0,
["Shadowburn"] = 1.0,
["Conflagrate"] = 1.0,
["Health Funnel"] = 1.0
},
-- Estadisticas por hechizo
spellStats = {
-- Formato: [spellName] = {casts, successes, totalDamage, avgDamage}
},
-- Historial de combates
combatHistory = {
-- Formato: {timestamp, duration, dps, survived, spellsUsed, context}
},
-- Estadisticas globales
globalStats = {
totalCombats = 0,
wins = 0,
losses = 0,
totalDamage = 0,
totalTime = 0,
avgDPS = 0,
lastUpdate = 0
},
-- Pesos por contexto (fase de combate)
contextWeights = {
["opener"] = {},
["sustain"] = {},
["execute"] = {},
["emergency"] = {},
["aoe"] = {}
}
}
-- Datos en memoria (cargados de SavedVariables)
WCS_BrainML.Data = nil
-- ============================================================================
-- TRACKING DE COMBATE ACTUAL
-- ============================================================================
WCS_BrainML.CurrentCombat = {
active = false,
startTime = 0,
startHealth = 100,
startMana = 100,
spellsCast = {}, -- {spellName = count}
damageDealt = 0,
damageTaken = 0,
context = "sustain",
targetType = "normal"
}
-- ============================================================================
-- INICIALIZACION Y PERSISTENCIA
-- ============================================================================
function WCS_BrainML:Initialize()
-- Cargar datos guardados o usar defaults
if WCS_BrainSaved and WCS_BrainSaved.ML then
self.Data = WCS_BrainSaved.ML
debugPrint("Datos ML cargados desde SavedVariables")
-- Migrar si es version antigua
self:MigrateData()
else
self.Data = self:DeepCopy(self.DefaultData)
debugPrint("Usando datos ML por defecto")
end
-- Asegurar que WCS_BrainSaved existe
if not WCS_BrainSaved then
WCS_BrainSaved = {}
end
-- Registrar eventos
self:RegisterEvents()
-- Integrar con WCS_BrainAI si existe
self:IntegrateWithBrainAI()
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[WCS_BrainML]|r v" .. self.VERSION .. " - Sistema de Aprendizaje cargado")
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[BrainML]|r Combates registrados: " .. (self.Data.globalStats.totalCombats or 0))
end
function WCS_BrainML:ToggleUI()
-- Por ahora, solo mostrar un mensaje
-- En el futuro se puede crear una UI dedicada para ML
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r UI de Machine Learning no implementada aún")
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Usa /brain status para ver estadísticas")
-- Mostrar estadísticas básicas
if self.Data and self.Data.globalStats then
local stats = self.Data.globalStats
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[ML Stats]|r Combates: " .. (stats.totalCombats or 0))
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[ML Stats]|r Victorias: " .. (stats.wins or 0))
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[ML Stats]|r DPS Promedio: " .. string.format("%.1f", stats.avgDPS or 0))
end
end
function WCS_BrainML:DeepCopy(orig)
local copy
if type(orig) == "table" then
copy = {}
for k, v in pairs(orig) do
copy[k] = self:DeepCopy(v)
end
else
copy = orig
end
return copy
end
function WCS_BrainML:MigrateData()
-- Migrar datos de versiones anteriores si es necesario
if not self.Data.version then
self.Data.version = "1.0.0"
end
-- Asegurar que todas las estructuras existen
if not self.Data.spellWeights then
self.Data.spellWeights = self:DeepCopy(self.DefaultData.spellWeights)
end
if not self.Data.spellStats then
self.Data.spellStats = {}
end
if not self.Data.combatHistory then
self.Data.combatHistory = {}
end
if not self.Data.globalStats then
self.Data.globalStats = self:DeepCopy(self.DefaultData.globalStats)
end
if not self.Data.contextWeights then
self.Data.contextWeights = self:DeepCopy(self.DefaultData.contextWeights)
end
end
function WCS_BrainML:Save()
if not self.Data then return end
-- Actualizar timestamp
self.Data.globalStats.lastUpdate = time()
-- Guardar en SavedVariables
WCS_BrainSaved.ML = self.Data
debugPrint("Datos ML guardados")
end
-- ============================================================================
-- REGISTRO DE EVENTOS
-- ============================================================================
function WCS_BrainML:RegisterEvents()
if self.EventFrame then return end
self.EventFrame = CreateFrame("Frame", "WCS_BrainML_EventFrame")
-- Eventos de combate
self.EventFrame:RegisterEvent("PLAYER_REGEN_DISABLED") -- Entramos en combate
self.EventFrame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Salimos de combate
self.EventFrame:RegisterEvent("PLAYER_DEAD") -- Morimos
self.EventFrame:RegisterEvent("PLAYER_LOGOUT") -- Guardar al salir
self.EventFrame:RegisterEvent("PLAYER_LEAVING_WORLD") -- Guardar al cambiar zona
-- Eventos de dano (para tracking)
self.EventFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
self.EventFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE")
-- Eventos de casteo manual del jugador (para aprender de decisiones)
self.EventFrame:RegisterEvent("SPELLCAST_START") -- Inicio de casteo
self.EventFrame:RegisterEvent("SPELLCAST_STOP") -- Casteo completado
self.EventFrame:RegisterEvent("SPELLCAST_FAILED") -- Casteo fallido
self.EventFrame:RegisterEvent("SPELLCAST_INTERRUPTED") -- Casteo interrumpido
-- Eventos adicionales para capturar hechizos instantaneos
self.EventFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") -- Buffs propios (Life Tap, etc)
self.EventFrame:SetScript("OnEvent", function()
WCS_BrainML:OnEvent(event, arg1, arg2, arg3)
end)
-- Timer para auto-guardado
self.LastAutoSave = getTime()
self.EventFrame:SetScript("OnUpdate", function()
WCS_BrainML:OnUpdate(arg1)
end)
end
function WCS_BrainML:OnEvent(event, arg1, arg2, arg3)
if event == "PLAYER_REGEN_DISABLED" then
self:StartCombat()
elseif event == "PLAYER_REGEN_ENABLED" then
self:EndCombat(true) -- Sobrevivimos
elseif event == "PLAYER_DEAD" then
self:EndCombat(false) -- Morimos
elseif event == "PLAYER_LOGOUT" or event == "PLAYER_LEAVING_WORLD" then
self:Save()
elseif event == "CHAT_MSG_SPELL_SELF_DAMAGE" or event == "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE" then
self:ParseDamageMessage(arg1)
-- Eventos de casteo manual
-- En WoW Vanilla, SPELLCAST_START no pasa el nombre del hechizo
-- Hay que leerlo de CastingBarFrame.spellName o de la barra de casteo
elseif event == "SPELLCAST_START" then
local spellName = self:GetCurrentCastingSpell()
if spellName then
self:OnManualCastStart(spellName)
-- Tambien registrar para el combate actual
self:RegisterSpellCast(spellName)
end
elseif event == "SPELLCAST_STOP" then
self:OnManualCastComplete()
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
self:OnManualCastFailed()
-- Capturar hechizos instantaneos via mensajes de chat
elseif event == "CHAT_MSG_SPELL_SELF_BUFF" then
self:ParseSelfBuffMessage(arg1)
end
end
-- Parsear mensajes de buffs propios para detectar hechizos instantaneos
function WCS_BrainML:ParseSelfBuffMessage(msg)
if not msg then return end
local spellName = nil
local _, _, s
-- Patron: "You gain X" o "Ganas X"
_, _, s = string.find(msg, "You cast ([%w%s]+)")
if s then spellName = s end
if not spellName then
_, _, s = string.find(msg, "You gain ([%w%s]+)")
if s then spellName = s end
end
-- Patron espanol
if not spellName then
_, _, s = string.find(msg, "Lanzas ([%w%s]+)")
if s then spellName = s end
end
if spellName then
-- Limpiar el nombre (quitar puntos, numeros de rango, etc)
spellName = string.gsub(spellName, "%.$", "")
spellName = string.gsub(spellName, "%s*%(%d+%)$", "")
-- Registrar si es un hechizo conocido de Warlock
local warlockSpells = {
["Life Tap"] = true,
["Dark Pact"] = true,
["Corruption"] = true,
["Curse of Agony"] = true,
["Curse of Doom"] = true,
["Curse of Tongues"] = true,
["Curse of Weakness"] = true,
["Curse of the Elements"] = true,
["Curse of Shadow"] = true,
["Siphon Life"] = true,
["Death Coil"] = true,
["Shadowburn"] = true,
["Conflagrate"] = true
}
if warlockSpells[spellName] then
self:RegisterSpellCast(spellName)
debugPrint("Instant detectado: " .. spellName)
end
end
end
-- Obtener el nombre del hechizo que se esta casteando actualmente
function WCS_BrainML:GetCurrentCastingSpell()
-- Metodo 1: CastingBarFrame (mas confiable)
if CastingBarFrame and CastingBarFrame:IsVisible() then
-- En algunos clientes el nombre esta en .spellName
if CastingBarFrame.spellName then
return CastingBarFrame.spellName
end
-- En otros esta en el texto de la barra
if CastingBarFrameText then
local text = CastingBarFrameText:GetText()
if text and text ~= "" then
return text
end
end
end
-- Metodo 2: Variable global (algunos addons la setean)
if CURRENT_SPELL_NAME then
return CURRENT_SPELL_NAME
end
return nil
end
function WCS_BrainML:OnUpdate(elapsed)
-- Auto-guardado periodico
local now = getTime()
-- Inicializar LastAutoSave si no existe (fix para llamadas antes de Initialize)
if not self.LastAutoSave then
self.LastAutoSave = now
return
end
if (now - self.LastAutoSave) > self.Config.autoSaveInterval then
self.LastAutoSave = now
self:Save()
end
end
-- ============================================================================
-- TRACKING DE COMBATE
-- ============================================================================
function WCS_BrainML:StartCombat()
local ctx = WCS_Brain and WCS_Brain.Context or {}
self.CurrentCombat = {
active = true,
startTime = getTime(),
startHealth = ctx.player and ctx.player.healthPct or 100,
startMana = ctx.player and ctx.player.manaPct or 100,
spellsCast = {},
damageDealt = 0,
damageTaken = 0,
context = WCS_BrainAI and WCS_BrainAI:GetOptimalRotation() or "sustain",
targetType = WCS_BrainAI and WCS_BrainAI:GetCombatType() or "normal"
}
debugPrint("Combate iniciado - Contexto: " .. self.CurrentCombat.context)
end
function WCS_BrainML:EndCombat(survived)
if not self.CurrentCombat.active then return end
local combat = self.CurrentCombat
combat.active = false
local duration = getTime() - combat.startTime
-- Ignorar combates muy cortos (menos de 3 segundos)
if duration < 3 then
debugPrint("Combate ignorado (muy corto)")
return
end
-- Calcular metricas
local dps = 0
if duration > 0 then
dps = combat.damageDealt / duration
end
-- Registrar resultado
local result = {
timestamp = time(),
duration = duration,
dps = dps,
survived = survived,
spellsUsed = combat.spellsCast,
context = combat.context,
targetType = combat.targetType,
damageDealt = combat.damageDealt,
healthLost = combat.startHealth - (WCS_Brain and WCS_Brain.Context.player.healthPct or 100)
}
-- Agregar al historial
table.insert(self.Data.combatHistory, result)
-- Limitar tamano del historial
while WCS_TableCount(self.Data.combatHistory) > self.Config.maxCombatHistory do
table.remove(self.Data.combatHistory, 1)
end
-- Actualizar estadisticas globales
self.Data.globalStats.totalCombats = self.Data.globalStats.totalCombats + 1
if survived then
self.Data.globalStats.wins = self.Data.globalStats.wins + 1
else
self.Data.globalStats.losses = self.Data.globalStats.losses + 1
end
self.Data.globalStats.totalDamage = self.Data.globalStats.totalDamage + combat.damageDealt
self.Data.globalStats.totalTime = self.Data.globalStats.totalTime + duration
-- Recalcular DPS promedio
if self.Data.globalStats.totalTime > 0 then
self.Data.globalStats.avgDPS = self.Data.globalStats.totalDamage / self.Data.globalStats.totalTime
end
-- Aprender de este combate
self:LearnFromCombat(result)
debugPrint("Combate terminado - " .. (survived and "Victoria" or "Derrota") .. " - DPS: " .. string.format("%.1f", dps))
-- Auto-guardar despues de cada combate
self:Save()
end
-- ============================================================================
-- REGISTRO DE HECHIZOS USADOS
-- ============================================================================
function WCS_BrainML:RegisterSpellCast(spellName)
if not self.CurrentCombat.active then return end
if not spellName then return end
-- Incrementar contador de uso
if not self.CurrentCombat.spellsCast[spellName] then
self.CurrentCombat.spellsCast[spellName] = 0
end
self.CurrentCombat.spellsCast[spellName] = self.CurrentCombat.spellsCast[spellName] + 1
-- Actualizar estadisticas globales del hechizo
if not self.Data.spellStats[spellName] then
self.Data.spellStats[spellName] = {
casts = 0,
successes = 0,
totalDamage = 0,
avgDamage = 0
}
end
self.Data.spellStats[spellName].casts = self.Data.spellStats[spellName].casts + 1
end
function WCS_BrainML:RegisterSpellDamage(spellName, damage)
if not spellName or not damage then return end
-- Actualizar dano del combate actual
if self.CurrentCombat.active then
self.CurrentCombat.damageDealt = self.CurrentCombat.damageDealt + damage
end
-- Actualizar estadisticas del hechizo
if self.Data.spellStats[spellName] then
local stats = self.Data.spellStats[spellName]
stats.totalDamage = stats.totalDamage + damage
if stats.casts > 0 then
stats.avgDamage = stats.totalDamage / stats.casts
end
end
end
-- ============================================================================
-- APRENDIZAJE DE DECISIONES MANUALES DEL JUGADOR
-- ============================================================================
-- Almacena el hechizo que el jugador esta casteando manualmente
WCS_BrainML.PendingManualCast = nil
WCS_BrainML.ManualCastContext = nil
function WCS_BrainML:OnManualCastStart(spellName)
if not spellName then return end
-- Guardar el hechizo y el contexto actual
self.PendingManualCast = spellName
-- Capturar contexto actual para aprender
local ctx = WCS_Brain and WCS_Brain.Context or {}
self.ManualCastContext = {
phase = ctx.phase or "idle",
playerHP = ctx.player and ctx.player.healthPct or 100,
playerMana = ctx.player and ctx.player.manaPct or 100,
targetHP = ctx.target and ctx.target.healthPct or 100,
targetExists = ctx.target and ctx.target.exists or false,
inCombat = UnitAffectingCombat("player"),
isMoving = ctx.player and ctx.player.isMoving or false,
timestamp = getTime()
}
debugPrint("Casteo manual detectado: " .. spellName)
end
function WCS_BrainML:OnManualCastComplete()
if not self.PendingManualCast then return end
local spellName = self.PendingManualCast
local context = self.ManualCastContext
-- Registrar el casteo exitoso
self:RegisterManualDecision(spellName, context, true)
-- Limpiar
self.PendingManualCast = nil
self.ManualCastContext = nil
end
function WCS_BrainML:OnManualCastFailed()
-- Limpiar sin registrar (el casteo fallo)
if self.PendingManualCast then
debugPrint("Casteo manual fallido: " .. self.PendingManualCast)
end
self.PendingManualCast = nil
self.ManualCastContext = nil
end
function WCS_BrainML:RegisterManualDecision(spellName, context, success)
if not spellName or not context then return end
-- Inicializar estructura de decisiones manuales si no existe
if not self.Data.manualDecisions then
self.Data.manualDecisions = {}
end
-- Inicializar para este hechizo
if not self.Data.manualDecisions[spellName] then
self.Data.manualDecisions[spellName] = {
totalCasts = 0,
contexts = {
emergency = 0, -- HP < 30%
execute = 0, -- Target HP < 25%
sustain = 0, -- Normal combat
opener = 0, -- Inicio de combate
moving = 0 -- Mientras se mueve
},
avgPlayerHP = 0,
avgTargetHP = 0,
successRate = 0
}
end
local data = self.Data.manualDecisions[spellName]
data.totalCasts = data.totalCasts + 1
-- Clasificar el contexto
if context.playerHP < 30 then
data.contexts.emergency = data.contexts.emergency + 1
elseif context.targetHP < 25 and context.targetExists then
data.contexts.execute = data.contexts.execute + 1
elseif context.isMoving then
data.contexts.moving = data.contexts.moving + 1
elseif not context.inCombat then
data.contexts.opener = data.contexts.opener + 1
else
data.contexts.sustain = data.contexts.sustain + 1
end
-- Actualizar promedios
local n = data.totalCasts
data.avgPlayerHP = ((data.avgPlayerHP * (n-1)) + context.playerHP) / n
data.avgTargetHP = ((data.avgTargetHP * (n-1)) + context.targetHP) / n
-- Aumentar peso del hechizo (el jugador lo prefiere)
self:AdjustWeightFromManual(spellName, context)
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[BrainML]|r Aprendido: |cFFFFCC00" .. spellName .. "|r en contexto " .. context.phase)
end
function WCS_BrainML:AdjustWeightFromManual(spellName, context)
if not self.Data.spellWeights[spellName] then
self.Data.spellWeights[spellName] = 1.0
end
local currentWeight = self.Data.spellWeights[spellName]
local adjustment = self.Config.learningRate * 0.5 -- Ajuste mas suave para decisiones manuales
-- Aumentar peso (el jugador eligio este hechizo)
local newWeight = currentWeight + adjustment
-- Bonus si fue en un contexto especifico
if context.phase == "emergency" and context.playerHP < 30 then
-- El jugador uso este hechizo en emergencia, darle mas peso en emergencias
newWeight = newWeight + adjustment
end
-- Limitar
if newWeight > self.Config.maxWeight then
newWeight = self.Config.maxWeight
end
self.Data.spellWeights[spellName] = newWeight
debugPrint("Peso ajustado: " .. spellName .. " = " .. string.format("%.2f", newWeight))
end
-- Funcion para ver estadisticas de decisiones manuales
function WCS_BrainML:GetManualStats(spellName)
if not self.Data.manualDecisions then return nil end
if spellName then
return self.Data.manualDecisions[spellName]
end
return self.Data.manualDecisions
end
-- ============================================================================
-- PARSING DE MENSAJES DE DANO
-- ============================================================================
function WCS_BrainML:ParseDamageMessage(msg)
if not msg then return end
local spell, damage
local _, _, s, d
-- Patrones en ingles
-- "Your Shadow Bolt hits Target for 500."
_, _, s, d = string.find(msg, "Your ([%w%s]+) hits .+ for (%d+)")
if s and d then
spell = s
damage = d
end
if not spell then
-- "Your Shadow Bolt crits Target for 1000."
_, _, s, d = string.find(msg, "Your ([%w%s]+) crits .+ for (%d+)")
if s and d then
spell = s
damage = d
end
end
-- Patrones en espanol
if not spell then
-- "Tu Descarga de las Sombras golpea a Objetivo por 500."
_, _, s, d = string.find(msg, "Tu ([%w%s]+) golpea .+ por (%d+)")
if s and d then
spell = s
damage = d
end
end
if not spell then
-- "Tu Descarga de las Sombras causa un golpe critico a Objetivo por 1000."
_, _, s, d = string.find(msg, "Tu ([%w%s]+) causa .+ por (%d+)")
if s and d then
spell = s
damage = d
end
end
if spell and damage then
damage = tonumber(damage) or 0
self:RegisterSpellDamage(spell, damage)
end
end
-- ============================================================================
-- SISTEMA DE APRENDIZAJE
-- ============================================================================
function WCS_BrainML:LearnFromCombat(result)
-- No aprender si no tenemos suficientes combates
if self.Data.globalStats.totalCombats < self.Config.minCombatsForLearning then
return
end
local learningRate = self.Config.learningRate
-- Calcular "exito" del combate (0 a 1)
local success = 0
-- Factor 1: Sobrevivir (40% del peso)
if result.survived then
success = success + 0.4
end
-- Factor 2: DPS relativo al promedio (30% del peso)
local avgDPS = self.Data.globalStats.avgDPS
if avgDPS > 0 and result.dps > 0 then
local dpsRatio = result.dps / avgDPS
-- Normalizar entre 0 y 0.3
success = success + math.min(0.3, dpsRatio * 0.15)
end
-- Factor 3: Eficiencia (poca vida perdida) (30% del peso)
local healthEfficiency = 1 - (result.healthLost / 100)
if healthEfficiency < 0 then healthEfficiency = 0 end
success = success + (healthEfficiency * 0.3)
debugPrint("Score de combate: " .. string.format("%.2f", success))
-- Ajustar pesos de los hechizos usados
for spellName, count in pairs(result.spellsUsed) do
self:AdjustSpellWeight(spellName, success, count, result.context)
end
end
function WCS_BrainML:AdjustSpellWeight(spellName, success, usageCount, context)
if not self.Data.spellWeights[spellName] then
self.Data.spellWeights[spellName] = 1.0
end
local currentWeight = self.Data.spellWeights[spellName]
local learningRate = self.Config.learningRate
-- Calcular ajuste basado en exito
-- success > 0.5 = aumentar peso, success < 0.5 = disminuir
local adjustment = (success - 0.5) * learningRate
-- Escalar por cantidad de usos (mas usos = mas confianza en el ajuste)
local usageFactor = math.min(1.0, usageCount / 5)
adjustment = adjustment * usageFactor
-- Aplicar ajuste
local newWeight = currentWeight + adjustment
-- Limitar a rango valido
newWeight = math.max(self.Config.minWeight, math.min(self.Config.maxWeight, newWeight))
self.Data.spellWeights[spellName] = newWeight
-- Tambien guardar peso por contexto
if context and self.Data.contextWeights[context] then
if not self.Data.contextWeights[context][spellName] then
self.Data.contextWeights[context][spellName] = 1.0
end
-- Ajuste mas agresivo para contexto especifico
local contextAdjustment = adjustment * 1.5
local contextWeight = self.Data.contextWeights[context][spellName] + contextAdjustment
contextWeight = math.max(self.Config.minWeight, math.min(self.Config.maxWeight, contextWeight))
self.Data.contextWeights[context][spellName] = contextWeight
end
if WCS_Brain and WCS_Brain.DEBUG then
debugPrint(spellName .. ": " .. string.format("%.2f", currentWeight) .. " -> " .. string.format("%.2f", newWeight))
end
end
-- ============================================================================
-- DECAY TEMPORAL
-- ============================================================================
function WCS_BrainML:ApplyDecay()
local now = time()
local halfLife = self.Config.decayHalfLife * 60 -- Convertir a segundos
-- Aplicar decay a los pesos (acercarlos a 1.0)
for spellName, weight in pairs(self.Data.spellWeights) do
if weight ~= 1.0 then
-- Calcular decay basado en tiempo desde ultima actualizacion
local timeSinceUpdate = now - (self.Data.globalStats.lastUpdate or now)
local decayFactor = math.pow(0.5, timeSinceUpdate / halfLife)
-- Mover peso hacia 1.0
local diff = weight - 1.0
local newWeight = 1.0 + (diff * decayFactor)
self.Data.spellWeights[spellName] = newWeight
end
end
debugPrint("Decay temporal aplicado")
end
-- ============================================================================
-- INTEGRACION CON WCS_BRAINAI
-- ============================================================================
function WCS_BrainML:IntegrateWithBrainAI()
if not WCS_BrainAI then
debugPrint("WCS_BrainAI no encontrado, integracion pendiente")
return
end
-- Guardar referencia a la funcion original de scoring
local originalScoreAction = WCS_BrainAI.ScoreAction
-- Reemplazar con nuestra version que aplica pesos aprendidos
WCS_BrainAI.ScoreAction = function(ai, action, context)
-- Obtener score base
local baseScore = originalScoreAction(ai, action, context)
if not action or not action.spell then
return baseScore
end
-- Aplicar peso aprendido
local spellWeight = WCS_BrainML:GetSpellWeight(action.spell, context.rotation)
local finalScore = baseScore * spellWeight
return finalScore
end
-- Hook para registrar hechizos casteados deshabilitado
-- (WCS_BrainAI ya hace el hook, evitamos duplicados)
debugPrint("Integrado con WCS_BrainAI")
end
function WCS_BrainML:GetSpellWeight(spellName, context)
if not self.Data then return 1.0 end
-- Primero buscar peso especifico del contexto
if context and self.Data.contextWeights[context] then
local contextWeight = self.Data.contextWeights[context][spellName]
if contextWeight then
return contextWeight
end
end
-- Fallback a peso global
return self.Data.spellWeights[spellName] or 1.0
end
-- ============================================================================
-- COMANDOS SLASH
-- ============================================================================
SLASH_WCSBRAINML1 = "/brainml"
SlashCmdList["WCSBRAINML"] = function(msg)
local cmd = string.lower(msg or "")
if cmd == "stats" then
local stats = WCS_BrainML.Data.globalStats
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Estadisticas Globales:")
DEFAULT_CHAT_FRAME:AddMessage(" Combates: " .. stats.totalCombats)
DEFAULT_CHAT_FRAME:AddMessage(" Victorias: " .. stats.wins .. " (" .. string.format("%.1f", (stats.wins / math.max(1, stats.totalCombats)) * 100) .. "%)")
DEFAULT_CHAT_FRAME:AddMessage(" Derrotas: " .. stats.losses)
DEFAULT_CHAT_FRAME:AddMessage(" DPS Promedio: " .. string.format("%.1f", stats.avgDPS))
DEFAULT_CHAT_FRAME:AddMessage(" Tiempo Total: " .. string.format("%.1f", stats.totalTime / 60) .. " min")
elseif cmd == "weights" then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Pesos de Hechizos:")
for spell, weight in pairs(WCS_BrainML.Data.spellWeights) do
local color = "|cFFFFFFFF"
if weight > 1.1 then
color = "|cFF00FF00" -- Verde = bueno
elseif weight < 0.9 then
color = "|cFFFF0000" -- Rojo = malo
end
DEFAULT_CHAT_FRAME:AddMessage(" " .. spell .. ": " .. color .. string.format("%.2f", weight) .. "|r")
end
elseif cmd == "spells" then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Estadisticas de Hechizos:")
for spell, stats in pairs(WCS_BrainML.Data.spellStats) do
DEFAULT_CHAT_FRAME:AddMessage(" " .. spell .. ": " .. stats.casts .. " casts, " .. string.format("%.0f", stats.avgDamage) .. " avg dmg")
end
elseif cmd == "reset" then
WCS_BrainML.Data = WCS_BrainML:DeepCopy(WCS_BrainML.DefaultData)
WCS_BrainML:Save()
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Datos reseteados")
elseif cmd == "save" then
WCS_BrainML:Save()
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Datos guardados manualmente")
elseif cmd == "decay" then
WCS_BrainML:ApplyDecay()
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Decay temporal aplicado")
elseif cmd == "history" then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Ultimos 5 combates:")
local history = WCS_BrainML.Data.combatHistory
local count = WCS_TableCount(history)
local start = math.max(1, count - 4)
for i = start, count do
local c = history[i]
if c then
local status = c.survived and "|cFF00FF00Win|r" or "|cFFFF0000Loss|r"
DEFAULT_CHAT_FRAME:AddMessage(" " .. status .. " - DPS: " .. string.format("%.1f", c.dps) .. " - " .. string.format("%.0f", c.duration) .. "s - " .. c.context)
end
end
elseif cmd == "manual" or cmd == "decisions" then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Decisiones Manuales Aprendidas:")
if WCS_BrainML.Data.manualDecisions then
for spell, data in pairs(WCS_BrainML.Data.manualDecisions) do
DEFAULT_CHAT_FRAME:AddMessage(" |cFFFFCC00" .. spell .. "|r: " .. data.totalCasts .. " usos")
DEFAULT_CHAT_FRAME:AddMessage(" Contextos: E:" .. data.contexts.emergency .. " X:" .. data.contexts.execute .. " S:" .. data.contexts.sustain .. " O:" .. data.contexts.opener .. " M:" .. data.contexts.moving)
DEFAULT_CHAT_FRAME:AddMessage(" HP Prom: " .. string.format("%.0f", data.avgPlayerHP) .. "% | Target: " .. string.format("%.0f", data.avgTargetHP) .. "%")
end
else
DEFAULT_CHAT_FRAME:AddMessage(" Sin datos aun. Castea hechizos manualmente para que aprenda.")
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFAA00[BrainML]|r Comandos:")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml stats - Ver estadisticas globales")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml weights - Ver pesos de hechizos")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml spells - Ver stats de hechizos")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml manual - Ver decisiones manuales aprendidas")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml history - Ver ultimos combates")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml decay - Aplicar decay temporal")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml save - Guardar manualmente")
DEFAULT_CHAT_FRAME:AddMessage(" /brainml reset - Resetear todos los datos")
end
end
-- ============================================================================
-- AUTO-INICIALIZACION
-- ============================================================================
local initFrame = CreateFrame("Frame", "WCS_BrainML_InitFrame")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:SetScript("OnEvent", function()
-- Esperar a que otros sistemas carguen
this.elapsed = 0
this:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed > 1.5 then
this:SetScript("OnUpdate", nil)
WCS_BrainML:Initialize()
end
end)
end)
-- Resetea los datos de aprendizaje y estadísticas globales
function WCS_BrainML:ResetStats()
self.Data = self:DeepCopy(self.DefaultData)
WCS_BrainSaved.ML = self.Data
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[BrainML]|r Estadísticas de aprendizaje reseteadas.")
end
-- ============================================================================
-- INTERFAZ DE USUARIO (Tab 4: Aprendizaje ML)
-- ============================================================================
function WCS_BrainML:CreateUI()
if _G["WCSBrainMLFrame"] then return end
local f = CreateFrame("Frame", "WCSBrainMLFrame", UIParent)
f:SetWidth(680)
f:SetHeight(540)
f:Hide()
-- Fondo principal
f:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",