-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathblooddebt
More file actions
1100 lines (937 loc) · 50 KB
/
blooddebt
File metadata and controls
1100 lines (937 loc) · 50 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
--NU-FWQ
--[[
Integrated ESP LocalScript with Rayfield UI, Role Detection, Distance Locking,
and Enhanced Hint Matching including AssignedTask check.
Fixed issue where players were tagged as hint match when no hints were present.
Updated hint parsing to handle AssignedTask directly after "Is often seen ".
Corrected hint matching logic to handle hints with only traits or only tasks,
and only performs hint matching if the local player is the killer based on the hint text prefix.
UPDATED: Modified hint parsing and matching to check AssignedTask or Configuration.Value based on hint type ("Is often seen" presence).
Ensured Hint Match tagging is correctly prioritized and applied.
FIXED: Ensure "Hints : ", " + ", and "[1] ", "[2] " (including spaces) are removed during parsing.
ADDED: More detailed debug prints to specifically track tagging logic in detectRoles, including hint match conditions.
ADDED: Automatic hint text check if the local player is the solo killer under the New Highest Priority rule.
]]
-- Load Rayfield
local Rayfield = loadstring(game:HttpGet('https://sirius.menu/rayfield'))()
-- Create the window
local Window = Rayfield:CreateWindow({
Name = "Blood Debt Role Detector",
Icon = 0,
LoadingTitle = "Rayfield Role Detector",
LoadingSubtitle = "by Sirius",
Theme = "Default",
DisableRayfieldPrompts = false,
DisableBuildWarnings = false,
ConfigurationSaving = {
Enabled = true,
FolderName = nil,
FileName = "Big Hub"
}
})
-- Create the tab
local Tab = Window:CreateTab("Players", "rewind")
-- Weapon lists (UPDATED AS PROVIDED in the larger script)
local killerWeapons = {
["CharcoalSteel JS-22"] = true,
["Pretty Pink RR-LCP"] = true,
["JS2-BondsDerringy"] = true,
["GILDED"] = true,
["Kamatov"] = true,
["JS2-Derringy"] = true,
["JS-22"] = true,
["NGO"] = true,
["Throwing Dagger"] = true,
["SoundMaker"] = true,
["SoundMakerSlower"] = true,
["RR-LightCompactPistolS"] = true,
["J9-Mereta"] = true,
["RY's GG-17"] = true, -- Special Killer Weapon
["RR-LCP"] = true,
["JS1 Competitor"] = true,
["AT's KAR15"] = true, -- Special Killer Weapon
["VK's ANKM"] = true, -- Special Killer Weapon
["Clothed Sawn-off"] = true,
["Sawn-off"] = true,
["Clothed Rosen-Obrez"] = true,
["Rosen-Obrez"] = true,
["GraySteel K1911"] = true,
["DarkSteel K1911"] = true,
["SilverSteel K1911"] = true,
["K1911"] = true,
["ZZ-90"] = true,
["SKORPION"] = true,
["Mares Leg"] = true, -- Added
["RR-LightCompactPistol"] = true,
["RR-LightCompactPistolS"] = true,
["KamatovS"] = true,
["Throwing Tomahawk"] = true,
["Throwing Kunai"] = true,
["ChromeSlide Turqoise RR-LCP"] = true,
["JS-1 CYCLOPS"] = true,
["THUMPA"] = true,
["LUT-E 'KRUS'"] = true,
["Memories"] = true,
["Hammer n Bullet"] = true,
["Throwing Shuriken"] = true,
["Kamatov"] = true,
["JS-22GILDED"] = true,
["Sawn-offGILDED"] = true,
["Door'bler TIGERSTRIPES"] = true,
["Anatoly's JS-22"] = true,
["Plastic JS-22"] = true,
["JTS225-OBREZ"] = true,
["HEARBALLA"] = true,
["Whiteout Rosen-Obrez"] = true,
["Whiteout RR-LCP"] = true,
["JTS225-Obrez Monochrome"] = true,
["JTS225-Obrez"] = true,
["Sawn-off10"] = true,
["Nikolai's 'Dented'"] = true,
["JS-44"] = true,
["BandagePack"] = true,
["JTS225-Obrez Poly"] = true,
["Mares Leg10"] = true,
["RR-LCP10"] = true,
["JTS225-Obrez Partycannon"] = true,
["Bandages"] = true,
}
local vigilanteWeapons = {
["Beagle"] = true,
["IZVEKH-412"] = true,
["SilverSteel RR-Snubby"] = true,
["RR-Snubby"] = true,
["ZKZ-Obrez"] = true,
["GG-17"] = true,
["J9-M"] = true,
["J9-Meretta"] = true, -- Typo? Should this be J9-Mereta? Keeping as is from
["Pretty Pink GG-17"] = true, -- Added
["GG-17 TAN"] = true, -- Added
["GG-17 GILDED"] = true, -- Added
["RR-Snubby GILDED"] = true, -- Added
["HWISSH-226"] = true, -- Added
["ZKZ-Obrez10"] = true, -- Added
["Buxxberg-COMPACT"] = true,
["RR-SnubbyGILDED"] = true,
["Pretty Pink Buxxberg-COMPACT"] = true,
["JS-5A-Obrez"] = true,
["Dual Elites"] = true,
["HWISSH-KP9"] = true,
}
-- Define Special Killer weapons (for the global check)
local specialKillerWeapons = {
["RY's GG-17"] = true,
["AT's KAR15"] = true,
["VK's ANKM"] = true,
["RVK"] = true,
}
-- Define a combined list of all relevant weapons
local allRoleWeapons = {}
for name, _ in pairs(killerWeapons) do allRoleWeapons[name] = true end
for name, _ in pairs(vigilanteWeapons) do allRoleWeapons[name] = true end
-- Define Role Colors and Labels
local killerColor = Color3.fromRGB(255, 0, 0) -- Red
local killerLabel = "KILLER"
local vigilanteColor = Color3.fromRGB(0, 255, 255) -- Cyan
local vigilanteLabel = "VIGILANTE"
local innocentColor = Color3.fromRGB(0, 255, 0) -- Green
local innocentLabel = "INNOCENT"
local hintMatchColor = Color3.new(1, 1, 0) -- Yellow
local hintMatchLabel = "HINT MATCH"
local vigilanteHintColor = Color3.fromRGB(128, 0, 128) -- Purple -- THIS IS THE PURPLE COLOR
local vigilanteHintLabel = "VIGILANTE + HINT MATCH" -- THIS IS THE PURPLE LABEL
-- Define the distance threshold for the new rule
local distanceThreshold = 30
local Players = game:GetService("Players")
local lp = Players.LocalPlayer
local Workspace = game:GetService("Workspace")
local NPCSFolder = Workspace:WaitForChild("NPCSFolder") -- Ensure NPCSFolder is waited for
local BloodFolder = Workspace:WaitForChild("BloodFolder") -- Ensure BloodFolder is waited for
-- State variables for controlling ESP
local espEnabled = false
local stopEspLoop = false -- Signal to stop the detection loop
local espPlayerAddedConnection = nil -- Store the main PlayerAdded connection
local espCharacterAddedConnections = {} -- Store per-player CharacterAdded connections
-- State variables for the distance locking rule
local rolesLockedByDistance = false -- Flag indicating if distance roles are locked
local lockedDistanceRoles = {} -- Stores the determined distance role ("Killer" or "Innocent")
-- State variables for the Hint Matching rule
local playersMatchingHints = {} -- Stores players who currently match hints (populated by updateMatchingHintPlayers)
local hintTextConnection = nil -- Stores the signal connection for the hint text
local firstVigilanteTracker = {} -- Stores the first player detected as Vigilante (player -> true)
-- Add floating name tag (smaller and neater)
local function addNameTag(character, text, color)
local head = character:FindFirstChild("Head")
if not head then return end
local oldTag = head:FindFirstChild("RoleBillboard")
if oldTag then oldTag:Destroy() end
local bb = Instance.new("BillboardGui")
bb.Name = "RoleBillboard"
bb.Size = UDim2.new(0, 100, 0, 20)
bb.StudsOffset = Vector3.new(0, 2.5, 0)
bb.Adornee = head
bb.AlwaysOnTop = true
bb.Parent = head
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, 0, 1, 0)
label.BackgroundTransparency = 1
label.Text = text
label.TextColor3 = color
label.TextStrokeTransparency = 0.2
label.TextScaled = true
label.Font = Enum.Font.SourceSansBold
label.Parent = bb
end
-- Clear previous overlays
local function clearOldStuff(character)
if not character then return end
local oldHighlight = character:FindFirstChild("RoleHighlight")
if oldHighlight and oldHighlight:IsA("Highlight") then
oldHighlight:Destroy()
end
local head = character:FindFirstChild("Head")
if head then
local tag = head:FindFirstChild("RoleBillboard")
if tag then tag:Destroy() end
end
end
-- Tag player by role
local function tagPlayer(player, roleColor, labelText)
if not player.Character then return end
clearOldStuff(player.Character)
local highlight = Instance.new("Highlight", player.Character)
highlight.Name = "RoleHighlight"
highlight.Archivable = true
highlight.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
highlight.Enabled = true
highlight.FillColor = roleColor
highlight.OutlineColor = Color3.fromRGB(255, 255, 255) -- White outline
highlight.FillTransparency = 0.5
highlight.OutlineTransparency = 0
if labelText then
addNameTag(player.Character, labelText, roleColor)
end
end
-- Helper function to collect a player's tools
-- UPDATED: Now checks the player's model within NPCSFolder for tools as well.
local function collectPlayerTools(player)
local tools = {}
local backpack = player:FindFirstChildOfClass("Backpack")
if backpack then
for _, tool in ipairs(backpack:GetChildren()) do
if tool:IsA("Tool") then
tools[tool.Name] = tool
end
end
end
if player.Character then
for _, tool in ipairs(player.Character:GetChildren()) do
if tool:IsA("Tool") then
tools[tool.Name] = tool
end
end
end
-- NEW: Check the player's model within NPCSFolder for tools
if NPCSFolder then -- Ensure NPCSFolder exists
local playerNPCModel = NPCSFolder:FindFirstChild(player.Name)
if playerNPCModel then
for _, child in ipairs(playerNPCModel:GetChildren()) do
if child:IsA("Tool") then
tools[child.Name] = child -- Add tools found in the NPCSFolder model
end
end
end
end
return tools -- Return table keyed by tool name
end
-- Helper function to get standard role based on weapons (excluding special killer check)
local function getStandardRoleFromWeapons(toolsByName)
local role = nil
local color = nil
local label = nil
-- Check standard Killer weapons first (priority), *excluding* special ones here
for weaponName, _ in pairs(killerWeapons) do
if not specialKillerWeapons[weaponName] and toolsByName[weaponName] then
role = "Killer"
color = killerColor
label = killerLabel
return role, color, label -- Standard Killer overrides Vigilante
end
end
-- Check Vigilante weapons if no standard Killer weapon found
for weaponName, _ in pairs(vigilanteWeapons) do
if toolsByName[weaponName] then
role = "Vigilante"
color = vigilanteColor
label = vigilanteLabel
return role, color, label -- Found a Vigilante weapon
end
end
-- No standard role weapon found
return nil, nil, nil
end
-- Function to parse a single string of hint content (after removing prefixes like [1] or [2])
-- Returns the type ("task" or "trait") and the extracted value, or "invalid" and nil.
local function parseSingleHint(hintContent)
local hintType = "invalid"
local hintValue = nil
local cleanedContent = hintContent:match("^%s*(.-)%s*$") or "" -- Trim leading/trailing whitespace
print("DEBUG: parseSingleHint called with:", cleanedContent)
if string.len(cleanedContent) == 0 then
print("DEBUG: parseSingleHint: Empty content after trimming.")
return hintType, hintValue
end
-- Check for task hint format: "Is often seen " followed by the task
local taskMatch = cleanedContent:match("^Is often seen%s*(.*)$")
if taskMatch then
hintType = "task"
hintValue = taskMatch:match("^%s*(.-)%s*$") -- Trim extracted task
print("DEBUG: Parsed as Task. Value:", hintValue)
return hintType, hintValue
end
-- Check for trait hint format: text within square brackets []
local traitBracketMatch = cleanedContent:match("^%[.-%]$") -- Check if the whole part is just a bracketed trait
if traitBracketMatch then
-- Extract content within brackets
local cleanClue = traitBracketMatch:gsub("[%[%]]", ""):match("^%s*(.-)%s*$") or ""
if string.len(cleanClue) > 0 and cleanClue:lower() ~= "assigned task" and cleanClue:lower() ~= "seen" then
hintType = "trait"
hintValue = cleanClue
print("DEBUG: Parsed as Bracketed Trait. Value:", hintValue)
return hintType, hintValue
end
end
-- If neither format matched, treat the entire cleaned content as a single unbracketed trait.
-- This handles cases like "Has no clothes covering their knee" or "Has long sleeves".
if hintType == "invalid" then
hintType = "trait" -- Assume it's an unbracketed trait
hintValue = cleanedContent
print("DEBUG: Parsed as Unbracketed Trait. Value:", hintValue)
end
return hintType, hintValue
end
-- Function to update the playersMatchingHints table based on current GUI hints
-- MODIFIED: Now correctly handles hints for multiple targets ([1], [2], etc.)
-- A player matches if they meet ALL conditions for AT LEAST ONE target.
local function updateMatchingHintPlayers()
print("DEBUG: updateMatchingHintPlayers called")
playersMatchingHints = {} -- Clear the previous results
print("DEBUG: playersMatchingHints cleared")
if not espEnabled then print("DEBUG: updateMatchingHintPlayers: ESP not enabled"); return end -- Only update if ESP is on
local PlayerGui = Players.LocalPlayer:FindFirstChild("PlayerGui")
if not PlayerGui then print("DEBUG: updateMatchingHintPlayers: PlayerGui not found"); return end
-- Safely find the TARGETHINT label. Assuming it's under RESETONDEATHStatusGui based on the larger script.
local TargetHintLabel = PlayerGui:FindFirstChild("RESETONDEATHStatusGui") and PlayerGui.RESETONDEATHStatusGui:FindFirstChild("TARGETHINT") -- Corrected path based on typical GUI structure
if not TargetHintLabel or not TargetHintLabel:IsA("TextLabel") then
-- warn("Blood Debt Role Detector: TARGETHINT label not found or not a TextLabel.") -- Optional warning
print("DEBUG: updateMatchingHintPlayers: TARGETHINT label not found or invalid")
return
end
local hintText = TargetHintLabel.Text
print("DEBUG: Raw TARGETHINT text:", hintText) -- Print raw hint text
if string.len(string.gsub(hintText, "%s", "")) == 0 then -- Check if the hint text is empty or just whitespace
-- No hints means no one can match all hints
print("DEBUG: Hint text is empty.") -- Optional Debug
return
end
-- Check if the local player is the killer based on the hint text prefix
local hintPrefix = "Hints : " -- Note: Capitalized 'H' as per your example
local lowerHintText = string.lower(hintText) -- Store lowercased text in a variable
local lowerHintPrefix = string.lower(hintPrefix) -- Lowercase the prefix for comparison
if lowerHintText:sub(1, string.len(lowerHintPrefix)) ~= lowerHintPrefix then
print("DEBUG: Local player is not the killer ('" .. hintText:sub(1, string.len(hintPrefix)) .. "' != '" .. hintPrefix .. "'). Skipping hint matching.") -- Optional Debug
return -- Exit if the hint text doesn't start with "Hints : "
end
-- Remove the "Hints : " prefix and any trailing space
local actualHintContent = hintText:sub(string.len(hintPrefix) + 1):match("^%s*(.-)%s*$")
print("DEBUG: Raw Hint Content (after removing 'Hints : ' and trimming):", actualHintContent) -- Print raw hint content
-- Split the hint content by " + " into individual hint parts.
-- Each part might belong to a different target indicated by [1], [2], etc.
local individualHintParts = {}
local currentPos = 1
while currentPos <= string.len(actualHintContent) do
local nextPlus = string.find(actualHintContent, " + ", currentPos, true)
if nextPlus then
local hintPart = string.sub(actualHintContent, currentPos, nextPlus - 1)
table.insert(individualHintParts, hintPart)
currentPos = nextPlus + string.len(" + ")
else
local hintPart = string.sub(actualHintContent, currentPos)
table.insert(individualHintParts, hintPart)
currentPos = string.len(actualHintContent) + 1 -- Exit loop
end
end
-- Handle the case of a single hint without " + "
if #individualHintParts == 0 and string.len(actualHintContent) > 0 then
table.insert(individualHintParts, actualHintContent)
end
print("DEBUG: Split into Individual Hint Parts:", individualHintParts)
local targetConditions = {} -- Table to hold conditions, grouped by target number
for i, hintPartContent in ipairs(individualHintParts) do
-- Extract target number and cleaned content from the hint part
local targetNumberMatch = hintPartContent:match("^%[%s*(%d+)%s*%]") -- Capture the number inside brackets
local targetNumber = tonumber(targetNumberMatch) or 1 -- Default to target 1 if no number found
local cleanedHintPartContent = hintPartContent:gsub("^%[%s*%d+%s*%]%s*", ""):match("^%s*(.-)%s*$") or "" -- Remove [number] prefix and trim
print("DEBUG: Processing Hint Part:", hintPartContent, "Target Number:", targetNumber, "Cleaned Content:", cleanedHintPartContent)
local hintType, hintValue = parseSingleHint(cleanedHintPartContent)
if hintType ~= "invalid" and hintValue and string.len(hintValue) > 0 then
-- Add the parsed condition to the list for this target number
if not targetConditions[targetNumber] then
targetConditions[targetNumber] = {}
end
table.insert(targetConditions[targetNumber], { type = hintType, value = hintValue })
print("DEBUG: Parsed Condition for Target", targetNumber, ": Type =", hintType, "Value =", hintValue)
else
print("DEBUG: Hint Part", i, "contained no valid condition after parsing. Skipping.")
end
end
-- Now, check each player. A player matches if they meet ALL conditions for AT LEAST ONE target.
if next(targetConditions) == nil then -- Check if the table is empty
print("DEBUG: No valid target conditions parsed from hint text. No players will match.")
return -- No conditions means no matches
end
-- Ensure NPCSFolder is available (waited for at the top)
if not NPCSFolder then
warn("Blood Debt Role Detector: NPCSFolder not available for hint matching.")
return
end
for _, player in Players:GetPlayers() do
-- Only check other players
if player ~= lp then
local playerNPCModel = NPCSFolder:FindFirstChild(player.Name)
if playerNPCModel then
local configObject = playerNPCModel:FindFirstChild("Configuration")
-- Configuration object is needed for trait checks
local playerMatchesAnyTarget = false -- Flag to see if player matches ANY target's conditions
for targetNumber, conditionsForTarget in pairs(targetConditions) do -- Iterate through each target's conditions
local playerMatchesAllConditionsForTarget = true -- Flag to see if player matches ALL conditions for THIS target
print("DEBUG: Checking player", player.Name, "against conditions for Target", targetNumber)
for i, condition in ipairs(conditionsForTarget) do -- Iterate through each condition for the current target
print("DEBUG: Checking Condition", i, " (Type:", condition.type, ", Value:", condition.value, ") for Target", targetNumber)
local conditionMet = false
if condition.type == "task" then
-- Check AssignedTask directly under the player's NPC model
local assignedTaskObject = playerNPCModel:FindFirstChild("AssignedTask")
print("DEBUG: Checking Task for player", player.Name, ": Hint Value =", condition.value, ", AssignedTask Object Found =", assignedTaskObject ~= nil, ", AssignedTask Value =", assignedTaskObject and assignedTaskObject.Value or "N/A")
if assignedTaskObject and assignedTaskObject:IsA("StringValue") and assignedTaskObject.Value == condition.value then
conditionMet = true
print("DEBUG: Player", player.Name, "MATCHES Task:", condition.value, " for Target", targetNumber)
else
print("DEBUG: Player", player.Name, "does NOT match Task:", condition.value, " for Target", targetNumber)
end
elseif condition.type == "trait" then
-- Check for the trait under Configuration.Value
if configObject then -- Ensure Configuration exists for trait checks
-- MODIFIED: Iterate through all children of Configuration
print("DEBUG: Checking Trait for player", player.Name, ": Hint Value =", condition.value, ". Searching Configuration children.")
for _, configChild in ipairs(configObject:GetChildren()) do
if configChild:IsA("StringValue") then
print("DEBUG: Checking Configuration StringValue:", configChild.Name, ", Value:", configChild.Value)
if configChild.Value == condition.value then
conditionMet = true
print("DEBUG: Player", player.Name, "MATCHES Trait (Configuration child):", condition.value, " via StringValue:", configChild.Name, " for Target", targetNumber)
break -- Found a matching trait, no need to check other StringValues in Configuration for this condition
end
end
end
if not conditionMet then
print("DEBUG: Player", player.Name, "does NOT match Trait (Configuration children):", condition.value, " for Target", targetNumber)
end
else
print("DEBUG: Player", player.Name, "NPC model does not have 'Configuration' object. Cannot check trait for Target", targetNumber)
end
end
-- If the player does NOT meet this specific condition for THIS target, they do NOT match ALL conditions for THIS target
if not conditionMet then
playerMatchesAllConditionsForTarget = false
print("DEBUG: Player", player.Name, "does NOT match Condition", i, " for Target", targetNumber, ". Marking as NOT matching ALL conditions for THIS target.")
break -- Player doesn't match this condition for this target, no need to check other conditions for THIS target
end
end -- End of loop through conditions for the current target
-- If the player matched ALL conditions for THIS target, they match ANY target
if playerMatchesAllConditionsForTarget then
playerMatchesAnyTarget = true
print("DEBUG: Player", player.Name, "MATCHES ALL conditions for Target", targetNumber, ". Marking as matching ANY target.")
break -- Player matches this target, no need to check other targets for this player
end
end -- End of loop through targets
-- If the player matched ANY target's conditions, add them to playersMatchingHints
if playerMatchesAnyTarget then
playersMatchingHints[player] = true
print("DEBUG: Player", player.Name, "MARKED as playersMatchingHints (matched ANY target).") -- Optional Debug
-- else
-- print("DEBUG: Player", player.Name, "does NOT match ANY target's conditions.") -- Optional Debug
end
-- else
-- print("DEBUG: Player", player.Name, "model under NPCSFolder does not have 'Configuration' object.") -- Optional Debug
end
-- else
-- print("DEBUG: Player", player.Name, "model not found under NPCSFolder.") -- Optional Debug
end
end
end -- End of updateMatchingHintPlayers function
-- Function to connect the hint text changed signal (called when ESP is enabled, and now on character added)
local function connectHintTextSignal()
if not espEnabled then return end
-- Disconnect existing connection before trying to connect a new one
if hintTextConnection then
print("DEBUG: Disconnecting existing hintTextConnection.")
hintTextConnection:Disconnect()
hintTextConnection = nil
end
local PlayerGui = Players.LocalPlayer:FindFirstChild("PlayerGui")
if not PlayerGui then print("DEBUG: connectHintTextSignal: PlayerGui not found"); return end
-- Safely wait for the GUI elements needed for the hint
-- Increased wait times slightly for robustness
local statusGui = PlayerGui:WaitForChild("RESETONDEATHStatusGui", 20) -- Increased wait time
if not statusGui then
warn("Blood Debt Role Detector: Could not find RESETONDEATHStatusGui after waiting. Hint matching disabled.")
print("DEBUG: connectHintTextSignal: RESETONDEATHStatusGui not found.")
return
end
local TargetHintLabel = statusGui:WaitForChild("TARGETHINT", 10) -- Increased wait time
if not TargetHintLabel or not TargetHintLabel:IsA("TextLabel") then
warn("Blood Debt Role Detector: Could not find TARGETHINT TextLabel or it's not a TextLabel after waiting. Hint matching disabled.")
print("DEBUG: connectHintTextSignal: TARGETHINT TextLabel not found or invalid.")
return
end
-- Connect the signal from TargetHintLabel directly
hintTextConnection = TargetHintLabel:GetPropertyChangedSignal("Text"):Connect(updateMatchingHintPlayers)
print("Blood Debt Role Detector: TARGETHINT TextLabel signal connected.")
updateMatchingHintPlayers() -- Run initial check immediately after connecting
end
-- Detect and apply roles - Integrated updated logic and priorities
local function detectRoles()
if not espEnabled then print("DEBUG: detectRoles: ESP not enabled, skipping."); return end -- Safety check
print("DEBUG: detectRoles called.")
-- State for New Highest Priority Killer rule (1 Vigilante + 1 Killer Gun)
local newHighestPriorityKillerDetected = false
local theSingleKillerGunHolder = nil
-- State for existing Special Killer rule (Priority 2)
local specialKillerDetected = false
local playersWithSpecialWeapons = {}
-- State for Distance Locking rule (Priority 3)
local everyoneHasGunConditionMet = false
local noOneHasGunConditionMet = false
-- rolesLockedByDistance and lockedDistanceRoles are state variables outside this function
local playersWithValidCharacters = {} -- Track players who are not local and have characters/HRP
local playersWithoutAnyGun = {} -- Track players without *any* role weapon
local playersWithAnyGun = {} -- Track players with *any* role weapon
local playersWithVigilanteWeapons = {} -- Track players with Vigilante weapons
-- Counts for New Highest Priority Killer rule
local vigilanteCount = 0
local killerGunHoldersCount = 0
local singleKillerGunHolderCandidate = nil
-- **Pass 1: Scan for conditions and identify player states**
for _, player in ipairs(Players:GetPlayers()) do
-- Only process players who are not the local player and have a valid character model with HRP
-- NOTE: We still check player.Character here for tagging purposes, even though hint data is elsewhere
if player ~= lp and player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
playersWithValidCharacters[player] = true -- Mark as valid target for tagging
local toolsByName = collectPlayerTools(player)
local hasAnyRoleWeapon = false
local hasVigilanteWeapon = false
local hasKillerWeapon = false
-- Check for conditions and weapons on this player
for name, tool in pairs(toolsByName) do
-- Check for Special Killers (for Priority 2)
if specialKillerWeapons[name] then
specialKillerDetected = true
playersWithSpecialWeapons[player] = true
end
-- Check if it's *any* role weapon (for Everyone Has Gun/No One Has Gun)
if allRoleWeapons[name] then
hasAnyRoleWeapon = true
end
-- Check for Vigilante weapon (for New #1 rule and First Vigilante)
if vigilanteWeapons[name] then
hasVigilanteWeapon = true
playersWithVigilanteWeapons[player] = true -- Mark player as having a Vigilante weapon
end
-- Check for ANY Killer weapon (for New #1 rule) - Use updated killerWeapons list
if killerWeapons[name] then
hasKillerWeapon = true
end
end
if hasVigilanteWeapon then
vigilanteCount = vigilanteCount + 1
-- Track the first vigilante detected across all ESP runs
if firstVigilanteTracker[player] == nil then
firstVigilanteTracker[player] = true
end
end
if hasKillerWeapon then
killerGunHoldersCount = killerGunHoldersCount + 1
singleKillerGunHolderCandidate = player -- Candidate for the single killer gun holder
end
if not hasAnyRoleWeapon then
playersWithoutAnyGun[player] = true -- Mark if they have no role weapon
else
playersWithAnyGun[player] = true -- Mark if they DO have a role weapon
end
else
-- Player is local player or without a valid character - ensure tags are cleared
clearOldStuff(player.Character)
end
end
-- **Step 2: Determine if the New Highest Priority Killer rule applies**
if vigilanteCount == 1 and killerGunHoldersCount == 1 and singleKillerGunHolderCandidate then
newHighestPriorityKillerDetected = true
theSingleKillerGunHolder = singleKillerGunHolderCandidate
-- Note: This rule overrides lower priority conditions if it applies
specialKillerDetected = false
playersWithSpecialWeapons = {}
everyoneHasGunConditionMet = false -- Ensure distance lock rule is NOT checked if this one is ON
rolesLockedByDistance = false -- Ensure distance lock is OFF if this one is ON
lockedDistanceRoles = {}
-- ADDED: If local player is the New Highest Priority Killer, trigger hint check
if theSingleKillerGunHolder == lp then
print("DEBUG: Local player is New #1 Killer. Triggering updateMatchingHintPlayers.")
updateMatchingHintPlayers()
end
-- print("DEBUG: New Highest Priority Killer rule MET (1 Vigilante, 1 Killer Gun).") -- DEBUG
-- else
-- print("DEBUG: New Highest Priority Killer rule NOT MET. Vigilante Count:", vigilanteCount, "Killer Gun Holders:", killerGunHoldersCount) -- Optional DEBUG
end
-- **Step 3: Determine the state of "Everyone has a gun" and "No one has a gun" conditions (Only if no New Highest Killer)**
if not newHighestPriorityKillerDetected then -- Check these ONLY if New #1 rule is OFF
-- Check "Everyone has a gun" (only if no special killer)
local allValidTargetsHaveGun = true
local otherPlayersWithCharCount = 0
for player, _ in pairs(playersWithValidCharacters) do
if player ~= lp then otherPlayersWithCharCount = otherPlayersWithCharCount + 1 end
if playersWithoutAnyGun[player] then
allValidTargetsHaveGun = false -- Found someone without a gun
break
end
end
-- Condition is met if all valid targets had a gun AND there is at least one valid target besides local player
if allValidTargetsHaveGun and otherPlayersWithCharCount > 0 then
everyoneHasGunConditionMet = true
-- print("DEBUG: 'Everyone has a gun' condition MET.") -- DEBUG
-- else
-- print("DEBUG: 'Everyone has a gun' condition NOT MET.") -- DEBUG
end
-- Check "No one has a gun" (only if no special killer)
local anyValidTargetHasGun = false
for player, _ in pairs(playersWithValidCharacters) do
if playersWithAnyGun[player] then
anyValidTargetHasGun = true -- Found someone *with* a gun
break
end
end
-- Condition is met if NO valid target has a gun AND there is at least one valid target
if not anyValidTargetHasGun and otherPlayersWithCharCount > 0 then
noOneHasGunConditionMet = true
-- print("DEBUG: 'No one has a gun' condition MET.") -- DEBUG
-- else
-- print("DEBUG: 'No one has a gun' condition NOT MET.") -- Optional Debug
end
end
-- **Step 4: Manage the distance locking state (rolesLockedByDistance)**
-- Note: This lock is independent of the New Highest Priority rule, but *depends* on it being OFF to trigger
-- local prevRolesLockedByDistance = rolesLockedByDistance -- Store state before checking transitions (not strictly needed for logic, but useful for debugging)
-- Condition to ACTIVATE lock: No New Highest Killer AND No special killer AND Everyone has a gun AND Not already locked
if not newHighestPriorityKillerDetected and not specialKillerDetected and everyoneHasGunConditionMet and not rolesLockedByDistance then
rolesLockedByDistance = true -- Activate the lock
lockedDistanceRoles = {} -- Clear any old locked roles
print("Blood Debt Role Detector: Distance Lock ACTIVATED.") -- DEBUG
-- Determine and store the distance-based roles for locking
local localHRP = lp.Character and lp.Character:FindFirstChild("HumanoidRootPart") -- Need local HRP for distance
if localHRP then
for player, _ in pairs(playersWithValidCharacters) do
local playerHRP = player.Character:FindFirstChild("HumanoidRootPart")
if playerHRP then
local distance = (localHRP.Position - playerHRP.Position).Magnitude
if distance >= distanceThreshold then
lockedDistanceRoles[player] = "Killer"
else
lockedDistanceRoles[player] = "Innocent"
end
else
-- Should not happen if playersWithValidCharacters is correct, but safety
end
end
else
-- Local player HRP missing - cannot lock distance roles
rolesLockedByDistance = false -- Force lock off if cannot calculate distance
lockedDistanceRoles = {}
warn("Blood Debt Role Detector: Distance Lock failed to activate: Local HRP missing.")
end
end
-- Condition to DEACTIVATE lock: No New Highest Killer AND No special killer AND No one has a gun AND Currently locked
if not newHighestPriorityKillerDetected and not specialKillerDetected and noOneHasGunConditionMet and rolesLockedByDistance then
rolesLockedByDistance = false -- Deactivate the lock
lockedDistanceRoles = {} -- Clear stored roles
print("Blood Debt Role Detector: Distance Lock DEACTIVATED.") -- DEBUG
end
-- Note: If neither transition happens, rolesLockedByDistance keeps its state
-- **Step 5: Trigger Hint Matching Update**
-- This is called here to ensure playersMatchingHints is up-to-date before applying tags.
-- It's also connected to the TextChanged signal for real-time updates.
-- The New #1 Killer check above also triggers this.
-- RESTORED: Call updateMatchingHintPlayers regularly in the loop
updateMatchingHintPlayers()
-- **Pass 2: Apply Tags based on determined state (New #1 > Special Killer > Distance Locked > Vigilante+Hint > Hint Match > Standard)**
for _, player in ipairs(Players:GetPlayers()) do
-- Only process players who were marked as valid targets in Pass 1
if playersWithValidCharacters[player] then
if newHighestPriorityKillerDetected and player == theSingleKillerGunHolder then
-- NEW Highest Priority: New #1 Killer Rule
print("DEBUG: Applying New #1 Killer tag for", player.Name)
tagPlayer(player, killerColor, killerLabel)
elseif specialKillerDetected then
-- Priority 2: Special Killer rule (Existing)
-- Rule: Holder is Killer. Everyone else is Innocent when special is present.
if playersWithSpecialWeapons[player] then
print("DEBUG: Applying Special Killer tag for", player.Name)
tagPlayer(player, killerColor, killerLabel)
else
print("DEBUG: Applying Innocent tag (under Special Killer Rule) for", player.Name)
tagPlayer(player, innocentColor, innocentLabel)
end
elseif rolesLockedByDistance then
-- Priority 3: Roles locked by "Everyone has a gun" rule - Use LOCKED distance roles
local lockedRole = lockedDistanceRoles[player] -- Look up the stored distance role string
print("DEBUG: Applying Distance Locked Rule for", player.Name, "Locked Role:", lockedRole)
if lockedRole then
if lockedRole == "Killer" then
print("DEBUG: Tagging", player.Name, "as KILLER (by locked distance).")
tagPlayer(player, killerColor, killerLabel)
elseif lockedRole == "Innocent" then
print("DEBUG: Tagging", player.Name, "as INNOCENT (by locked distance).")
tagPlayer(player, innocentColor, innocentLabel)
end
else
-- Player somehow valid but not in lockedDistanceRoles - safety clear
clearOldStuff(player.Character)
print("DEBUG: Player", player.Name, "valid but not in lockedDistanceRoles. Clearing.")
end
-- NEW: Priority 4: Vigilante + Hint Match (Purple)
-- Apply this tag if the player matches hints AND has a vigilante weapon.
print("DEBUG: Checking Vigilante + Hint Match condition for", player.Name, ": playersMatchingHints[player] =", playersMatchingHints[player] ~= nil, ", playersWithVigilanteWeapons[player] =", playersWithVigilanteWeapons[player] ~= nil)
-- REMOVED: 'and not firstVigilanteTracker[player]' to tag ALL matching vigilantes
elseif playersMatchingHints[player] and playersWithVigilanteWeapons[player] then
print("DEBUG: Applying VIGILANTE + HINT MATCH tag for", player.Name, ". Condition met.")
tagPlayer(player, vigilanteHintColor, vigilanteHintLabel)
elseif playersMatchingHints[player] and not firstVigilanteTracker[player] then
-- Priority 5: Hint Matching Yellow Color (Original Priority 4)
-- Tag yellow if matches hints AND is *not* the first detected Vigilante (to avoid overriding Cyan tag)
-- This priority is now below the purple tag, so a matching vigilante gets purple.
print("DEBUG: Applying HINT MATCH tag (Yellow) for", player.Name, ". Condition met.")
tagPlayer(player, hintMatchColor, hintMatchLabel)
else
-- Priority 6: Standard detection (Original Priority 5)
local toolsByName = collectPlayerTools(player)
local standardRole, standardColor, standardLabel = getStandardRoleFromWeapons(toolsByName)
print("DEBUG: Applying Standard Detection for", player.Name, ". Standard Role:", standardLabel)
if standardRole then
tagPlayer(player, standardColor, standardLabel)
else
tagPlayer(player, innocentColor, innocentLabel)
end
end
else
-- Players not in playersWithValidCharacters were handled in Pass 1 (cleared)
print("DEBUG: Skipping tagging for", player.Name, " (not a valid target)")
end
end
-- No need to update previous state flags like prevEveryoneHasGunConditionMet
-- because the lock state `rolesLockedByDistance` directly controls the behavior.
end -- End of detectRoles function
-- Function to disable ESP - Shared logic (Includes clearing lock/hint state)
local function disableEsp()
if espEnabled then -- Only disable if it's currently enabled
espEnabled = false
stopEspLoop = true -- Signal to stop the loop
print("Blood Debt Role Detector: ESP Disabled")
-- Clear state variables
rolesLockedByDistance = false
lockedDistanceRoles = {}
playersMatchingHints = {} -- Clear hint matches
-- Do NOT clear firstVigilanteTracker, it should persist across disable/enable
-- Disconnect the main PlayerAdded connection
if espPlayerAddedConnection then
espPlayerAddedConnection:Disconnect()
espPlayerAddedConnection = nil
end
-- Disconnect hint text signal
if hintTextConnection then
print("DEBUG: Disconnecting hintTextConnection during disable.")
hintTextConnection:Disconnect()
hintTextConnection = nil
end
-- Disconnect all stored CharacterAdded connections
for player, connection in pairs(espCharacterAddedConnections) do
if connection and typeof(connection) == "RBXScriptConnection" then -- Safety check connection type
connection:Disconnect()
end
espCharacterAddedConnections[player] = nil
end
espCharacterAddedConnections = {} -- Clear the table itself
-- Clear ESP visuals for all players currently in the game
for _, player in ipairs(Players:GetPlayers()) do
if player.Character then
clearOldStuff(player.Character)
end
end
Rayfield:Notify({
Title = "ESP Disabled",
Content = "Role detection has been turned off.",
Duration = 3,
Image = 4483362458 -- Replace with a suitable asset ID
})
end
end
-- Function to teleport to dropped gun - Includes all relevant weapons
local function tpToDroppedGun()
-- Ensure BloodFolder is available (waited for at the top)
if not BloodFolder then
warn("Blood Debt Role Detector: BloodFolder not available for teleport.")
Rayfield:Notify({
Title = "Error",
Content = "BloodFolder not found in Workspace.",
Duration = 5,
Image = 4483362458 -- Replace with a suitable asset ID
})
return
end
local foundGun = false
for _, item in ipairs(BloodFolder:GetChildren()) do
-- Check if the dropped item is a Killer, Vigilante, or Special Killer weapon
if item:IsA("Tool") and (killerWeapons[item.Name] or vigilanteWeapons[item.Name] or specialKillerWeapons[item.Name]) then
local targetPosition = item.Position + Vector3.new(0, 5, 0) -- Teleport slightly above the item
if lp.Character and lp.Character:FindFirstChild("HumanoidRootPart") then
lp.Character:SetPrimaryPartCFrame(CFrame.new(targetPosition))
foundGun = true
break -- Teleported to the first found gun
else
warn("Blood Debt Role Detector: Local player character or HRP not found for teleport.")
Rayfield:Notify({
Title = "Error",
Content = "Cannot teleport: Your character is not ready.",
Duration = 5,
Image = 4483362458 -- Replace with a suitable asset ID
})
return -- Exit if character not ready
end
end
end
if not foundGun then
Rayfield:Notify({
Title = "No Gun Found",
Content = "There are no valid guns in the BloodFolder.",
Duration = 5,
Image = 4483362458 -- Replace with a suitable asset ID
})
end
end
-- Add a button for teleporting to the dropped gun
local ButtonTPGunCreateSuccess, ButtonTPGun = pcall(function()
return Tab:CreateButton({
Name = "Teleport to Dropped Gun",
Callback = function()
tpToDroppedGun()
end
})
end)
if not ButtonTPGunCreateSuccess or not ButtonTPGun then
warn("Blood Debt Role Detector: Failed to create Teleport to Dropped Gun button. Success:", ButtonTPGunCreateSuccess)
else
print("Blood Debt Role Detector: Teleport to Dropped Gun button created successfully.")
end
-- Create Enable ESP button - Only enables if not already enabled
local ButtonEnableESPCreateSuccess, ButtonEnableESP = pcall(function()
return Tab:CreateButton({
Name = "Enable ESP",
Callback = function()
if not espEnabled then -- Only enable if it's currently disabled
espEnabled = true
stopEspLoop = false -- Ensure loop will run
print("Blood Debt Role Detector: ESP Enabled")
-- Start the detection loop in a new thread/coroutine