Profile system: per-user char profiles + shared pickit profiles
Per-user character profiles (gitignored, survive git pulls): - config/profiles/<name>/profile.ini overrides any params.ini key (runs, difficulty, char type, keybinds...). Priority: custom.ini > profile.ini > params.ini. Injected into _select_val and all 22 build-section merges. - Active profile selected via config/active_profile.txt (gitignored); Config.get/set_active_profile + list_profiles/list_pickit_profiles. Shared pickit profiles (git-tracked team content): - config/pickit_profiles/<set>/*.bnip - one folder per season phase, built once, shared via git. Selected per user with [general] pickit_profile=<set> in their profile. - Pickit dir priority: config/profiles/<me>/pickit/ (personal) > config/pickit_profiles/<set>/ > config/bnip/ > default.bnip. Menu integration (main.py): - startup banner shows active profile + pickit set + available lists - "end" hotkey cycles character profiles (applies on restart) Verified: fistman profile active end-to-end (general/char/build-section overrides + shared pickit set resolution + 474 expressions loaded). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0834831220
commit
b06c77d2e4
@@ -77,3 +77,7 @@ test_config_load.py
|
||||
|
||||
# Debug directory
|
||||
botty_debug/
|
||||
|
||||
# per-user character profiles (survive git pulls)
|
||||
config/profiles/
|
||||
config/active_profile.txt
|
||||
|
||||
+13
-701
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"generated_at": "2026-06-12T06:22:04.139244+00:00",
|
||||
"generated_at": "2026-06-12T07:41:10.635476+00:00",
|
||||
"mode": "offline",
|
||||
"offline_dir": "data\\d2jsp_pages",
|
||||
"ladder_start_date": "2026-06-05",
|
||||
"days": 14,
|
||||
"ladder_start_date": "2026-05-12",
|
||||
"days": 21,
|
||||
"topics_scanned": 840,
|
||||
"estimates": {
|
||||
"day_1": {},
|
||||
@@ -12,708 +12,20 @@
|
||||
"day_4": {},
|
||||
"day_5": {},
|
||||
"day_6": {},
|
||||
"day_7": {
|
||||
"CTA": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 358.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1199.0,
|
||||
"samples": 5
|
||||
},
|
||||
"Unid Anni": {
|
||||
"median_fg": 550.0,
|
||||
"avg_fg": 416.7,
|
||||
"min_fg": 100.0,
|
||||
"max_fg": 600.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Aldur's Advance": {
|
||||
"median_fg": 27.5,
|
||||
"avg_fg": 43.8,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 4
|
||||
},
|
||||
"Ist Rune": {
|
||||
"median_fg": 95.0,
|
||||
"avg_fg": 112.2,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Um Rune": {
|
||||
"median_fg": 75.0,
|
||||
"avg_fg": 99.6,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 10
|
||||
},
|
||||
"Gul Rune": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 63.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 200.0,
|
||||
"samples": 8
|
||||
},
|
||||
"Mal Rune": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 112.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 13
|
||||
},
|
||||
"Stealth RW": {
|
||||
"median_fg": 55.0,
|
||||
"avg_fg": 55.0,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 2
|
||||
},
|
||||
"Dual Leech Ring": {
|
||||
"median_fg": 85.0,
|
||||
"avg_fg": 85.0,
|
||||
"min_fg": 70.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 2
|
||||
},
|
||||
"Lo Rune": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 38.8,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 4
|
||||
},
|
||||
"Sorc Torch": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 100.0,
|
||||
"min_fg": 100.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 1
|
||||
},
|
||||
"Cham Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 121.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 450.0,
|
||||
"samples": 5
|
||||
},
|
||||
"Lem Rune": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 72.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 190.0,
|
||||
"samples": 7
|
||||
},
|
||||
"Pul Rune": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 72.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 190.0,
|
||||
"samples": 7
|
||||
},
|
||||
"Bone": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Java SK": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Moser": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Black": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Key": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Shako": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 91.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 6
|
||||
},
|
||||
"Vex Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Ohm Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Insight": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 91.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 6
|
||||
},
|
||||
"White": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Ber Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Skin": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Vampire Gaze": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Jah Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Monarch": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Oculus": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"BK Ring": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Spirit": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"5/5 Facet": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 163.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Arach": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 163.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Mara": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 163.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 3
|
||||
}
|
||||
},
|
||||
"day_8": {
|
||||
"Pul Rune": {
|
||||
"median_fg": 15.0,
|
||||
"avg_fg": 59.5,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1425.0,
|
||||
"samples": 75
|
||||
},
|
||||
"Highlord": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 82.8,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1000.0,
|
||||
"samples": 61
|
||||
},
|
||||
"Sacred": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 138.4,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 149
|
||||
},
|
||||
"Griffon": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 115.4,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 77
|
||||
},
|
||||
"Giant": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 134.6,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 84
|
||||
},
|
||||
"Ist Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 89.6,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 135
|
||||
},
|
||||
"Key": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 58.6,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 470.0,
|
||||
"samples": 44
|
||||
},
|
||||
"Shako": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 83.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 122
|
||||
},
|
||||
"Arach": {
|
||||
"median_fg": 42.5,
|
||||
"avg_fg": 113.7,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 96
|
||||
},
|
||||
"Vex Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 185.2,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 106
|
||||
},
|
||||
"Ohm Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 61.8,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 480.0,
|
||||
"samples": 76
|
||||
},
|
||||
"Um Rune": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 122.3,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 160
|
||||
},
|
||||
"Pcomb SK": {
|
||||
"median_fg": 35.0,
|
||||
"avg_fg": 110.9,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 93
|
||||
},
|
||||
"White": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 48.6,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 94
|
||||
},
|
||||
"Lem Rune": {
|
||||
"median_fg": 12.0,
|
||||
"avg_fg": 25.8,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 160.0,
|
||||
"samples": 92
|
||||
},
|
||||
"Skin": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 123.4,
|
||||
"min_fg": 2.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 103
|
||||
},
|
||||
"Vampire Gaze": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 423.9,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 36
|
||||
},
|
||||
"Gul Rune": {
|
||||
"median_fg": 24.0,
|
||||
"avg_fg": 93.3,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 122
|
||||
},
|
||||
"Monarch": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 109.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 212
|
||||
},
|
||||
"Oculus": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 155.7,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 600.0,
|
||||
"samples": 27
|
||||
},
|
||||
"Lo Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 149.5,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 96
|
||||
},
|
||||
"Unid Torch": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 134.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 550.0,
|
||||
"samples": 39
|
||||
},
|
||||
"BK Ring": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 97.2,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 117
|
||||
},
|
||||
"Viper Gorge": {
|
||||
"median_fg": 49.0,
|
||||
"avg_fg": 134.9,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 145
|
||||
},
|
||||
"Mal Rune": {
|
||||
"median_fg": 24.0,
|
||||
"avg_fg": 64.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 500.0,
|
||||
"samples": 110
|
||||
},
|
||||
"Mara": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 145.2,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 67
|
||||
},
|
||||
"Crescent": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 44.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 180.0,
|
||||
"samples": 60
|
||||
},
|
||||
"Thresher": {
|
||||
"median_fg": 27.5,
|
||||
"avg_fg": 130.0,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 134
|
||||
},
|
||||
"Jah Rune": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 311.2,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 106
|
||||
},
|
||||
"Bone": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 167.4,
|
||||
"min_fg": 2.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 239
|
||||
},
|
||||
"Treachery": {
|
||||
"median_fg": 62.5,
|
||||
"avg_fg": 112.9,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 42
|
||||
},
|
||||
"Insight": {
|
||||
"median_fg": 35.0,
|
||||
"avg_fg": 112.8,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 112
|
||||
},
|
||||
"Ber Rune": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 278.3,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 87
|
||||
},
|
||||
"Guardian's Light": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 145.8,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 245
|
||||
},
|
||||
"Enigma": {
|
||||
"median_fg": 95.0,
|
||||
"avg_fg": 324.4,
|
||||
"min_fg": 30.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 36
|
||||
},
|
||||
"Fortitude": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 102.2,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 23
|
||||
},
|
||||
"Black": {
|
||||
"median_fg": 39.5,
|
||||
"avg_fg": 131.8,
|
||||
"min_fg": 2.0,
|
||||
"max_fg": 1750.0,
|
||||
"samples": 116
|
||||
},
|
||||
"Cham Rune": {
|
||||
"median_fg": 15.0,
|
||||
"avg_fg": 65.1,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 700.0,
|
||||
"samples": 44
|
||||
},
|
||||
"Spirit": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 105.2,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 156
|
||||
},
|
||||
"5/5 Facet": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 263.5,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 39
|
||||
},
|
||||
"War Traveler": {
|
||||
"median_fg": 14.0,
|
||||
"avg_fg": 239.7,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 7
|
||||
},
|
||||
"CTA": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 275.5,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 72
|
||||
},
|
||||
"Grief": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 139.6,
|
||||
"min_fg": 15.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 26
|
||||
},
|
||||
"Aldur's Advance": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 65.0,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 150.0,
|
||||
"samples": 24
|
||||
},
|
||||
"Doom": {
|
||||
"median_fg": 137.5,
|
||||
"avg_fg": 245.8,
|
||||
"min_fg": 50.0,
|
||||
"max_fg": 550.0,
|
||||
"samples": 24
|
||||
},
|
||||
"Wind": {
|
||||
"median_fg": 40.0,
|
||||
"avg_fg": 155.7,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 58
|
||||
},
|
||||
"Cold SK": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 100.0,
|
||||
"min_fg": 50.0,
|
||||
"max_fg": 150.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Mithril": {
|
||||
"median_fg": 4200.0,
|
||||
"avg_fg": 3900.0,
|
||||
"min_fg": 2100.0,
|
||||
"max_fg": 5400.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Java SK": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 1325.3,
|
||||
"min_fg": 2.0,
|
||||
"max_fg": 5400.0,
|
||||
"samples": 37
|
||||
},
|
||||
"Unid Anni": {
|
||||
"median_fg": 39.0,
|
||||
"avg_fg": 152.6,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 40
|
||||
},
|
||||
"Fury": {
|
||||
"median_fg": 75.0,
|
||||
"avg_fg": 93.1,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 250.0,
|
||||
"samples": 21
|
||||
},
|
||||
"Light SK": {
|
||||
"median_fg": 120.0,
|
||||
"avg_fg": 767.9,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1800.0,
|
||||
"samples": 7
|
||||
},
|
||||
"Stealth RW": {
|
||||
"median_fg": 1600.0,
|
||||
"avg_fg": 1600.0,
|
||||
"min_fg": 1600.0,
|
||||
"max_fg": 1600.0,
|
||||
"samples": 1
|
||||
},
|
||||
"Ward": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 45.4,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 35
|
||||
},
|
||||
"Moser": {
|
||||
"median_fg": 10.0,
|
||||
"avg_fg": 13.3,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 20.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Blessed": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 23.3,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 50.0,
|
||||
"samples": 24
|
||||
},
|
||||
"Fist": {
|
||||
"median_fg": 10.0,
|
||||
"avg_fg": 16.7,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Bulwark": {
|
||||
"median_fg": 75.0,
|
||||
"avg_fg": 64.0,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 75.0,
|
||||
"samples": 5
|
||||
},
|
||||
"Sanctuary": {
|
||||
"median_fg": 17.5,
|
||||
"avg_fg": 30.0,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 75.0,
|
||||
"samples": 16
|
||||
},
|
||||
"Sorc Torch": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 160.7,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 750.0,
|
||||
"samples": 15
|
||||
},
|
||||
"Hellfire": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 216.7,
|
||||
"min_fg": 50.0,
|
||||
"max_fg": 750.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Light Jewel": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 65.5,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 42
|
||||
},
|
||||
"Troll": {
|
||||
"median_fg": 39.0,
|
||||
"avg_fg": 37.3,
|
||||
"min_fg": 24.0,
|
||||
"max_fg": 49.0,
|
||||
"samples": 21
|
||||
},
|
||||
"Dual Leech Ring": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 80.0,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 180.0,
|
||||
"samples": 12
|
||||
}
|
||||
},
|
||||
"day_7": {},
|
||||
"day_8": {},
|
||||
"day_9": {},
|
||||
"day_10": {},
|
||||
"day_11": {},
|
||||
"day_12": {},
|
||||
"day_13": {},
|
||||
"day_14": {}
|
||||
"day_14": {},
|
||||
"day_15": {},
|
||||
"day_16": {},
|
||||
"day_17": {},
|
||||
"day_18": {},
|
||||
"day_19": {},
|
||||
"day_20": {},
|
||||
"day_21": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Shared pickit profiles (git-tracked)
|
||||
|
||||
One folder per pickit set, e.g. for season phases:
|
||||
|
||||
```
|
||||
config/pickit_profiles/
|
||||
season_start/ *.bnip (leveling: keep bases, gems, chipped...)
|
||||
mid_season/ *.bnip
|
||||
endgame/ *.bnip (GG-only filter)
|
||||
```
|
||||
|
||||
Drop `.bnip` (or `.nip`) files in a folder; a `.nipignore` works like in
|
||||
`config/bnip`. These folders ARE committed — build them once, everyone
|
||||
gets them via git pull.
|
||||
|
||||
## Selecting a set
|
||||
Per user, in your gitignored `config/profiles/<you>/profile.ini`:
|
||||
|
||||
```ini
|
||||
[general]
|
||||
pickit_profile=season_start
|
||||
```
|
||||
|
||||
Priority: `config/profiles/<you>/pickit/` (personal set, gitignored)
|
||||
> `config/pickit_profiles/<pickit_profile>/` (shared, from this folder)
|
||||
> `config/bnip/` > `config/default.bnip`.
|
||||
+28
-1
@@ -212,7 +212,34 @@ def _load_bnip_expressions(filepath):
|
||||
|
||||
|
||||
default_bnip_file_path = f"{os.getcwd()}/config/default.bnip"
|
||||
bnip_path = f"{os.getcwd()}/config/bnip"
|
||||
|
||||
|
||||
def _resolve_bnip_dir() -> str:
|
||||
"""Pickit set priority: profile-local pickit/ > shared pickit profile
|
||||
(config/pickit_profiles/<name>, git-tracked, [general] pickit_profile=<name>)
|
||||
> config/bnip > default.bnip fallback."""
|
||||
try:
|
||||
from config import Config
|
||||
cfg = Config()
|
||||
prof = getattr(cfg, "active_profile", "")
|
||||
if prof:
|
||||
local = os.path.join(os.getcwd(), "config", "profiles", prof, "pickit")
|
||||
if os.path.isdir(local):
|
||||
Logger.info(f"Pickit: using profile-local set ({prof}/pickit)")
|
||||
return local
|
||||
shared = cfg.general.get("pickit_profile", "")
|
||||
if shared:
|
||||
shared_dir = os.path.join(os.getcwd(), "config", "pickit_profiles", shared)
|
||||
if os.path.isdir(shared_dir):
|
||||
Logger.info(f"Pickit: using shared pickit profile '{shared}'")
|
||||
return shared_dir
|
||||
Logger.warning(f"Pickit profile '{shared}' not found in config/pickit_profiles — falling back to config/bnip")
|
||||
except Exception as e:
|
||||
Logger.warning(f"Pickit profile resolution failed ({e}) — falling back to config/bnip")
|
||||
return os.path.join(os.getcwd(), "config", "bnip")
|
||||
|
||||
|
||||
bnip_path = _resolve_bnip_dir()
|
||||
|
||||
glob_bnip_path = os.path.join(bnip_path, '**', '*.bnip')
|
||||
glob_nip_path = os.path.join(bnip_path, '**', '*.nip')
|
||||
|
||||
@@ -73,6 +73,9 @@ class Config:
|
||||
if section in self.configs["custom"]["parser"] and key in self.configs["custom"]["parser"][section]:
|
||||
found_val = self.configs["custom"]["parser"][section][key]
|
||||
found_in = "custom"
|
||||
elif section in self.configs["profile"]["parser"] and key in self.configs["profile"]["parser"][section]:
|
||||
found_val = self.configs["profile"]["parser"][section][key]
|
||||
found_in = "profile"
|
||||
elif section in self.configs["config"]["parser"]:
|
||||
found_val = self.configs["config"]["parser"][section][key]
|
||||
found_in = "config"
|
||||
@@ -165,6 +168,34 @@ class Config:
|
||||
Logger.info("All stash tabs and character are no longer full of gold. Turn gold stashing back on.")
|
||||
self.char["stash_gold"] = True
|
||||
|
||||
@staticmethod
|
||||
def get_active_profile() -> str:
|
||||
try:
|
||||
with open(os.path.join("config", "active_profile.txt"), encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def set_active_profile(name: str):
|
||||
with open(os.path.join("config", "active_profile.txt"), "w", encoding="utf-8") as f:
|
||||
f.write(name.strip())
|
||||
|
||||
@staticmethod
|
||||
def list_profiles() -> list[str]:
|
||||
base = os.path.join("config", "profiles")
|
||||
if not os.path.isdir(base):
|
||||
return []
|
||||
return sorted(d for d in os.listdir(base)
|
||||
if os.path.isfile(os.path.join(base, d, "profile.ini")))
|
||||
|
||||
@staticmethod
|
||||
def list_pickit_profiles() -> list[str]:
|
||||
base = os.path.join("config", "pickit_profiles")
|
||||
if not os.path.isdir(base):
|
||||
return []
|
||||
return sorted(d for d in os.listdir(base) if os.path.isdir(os.path.join(base, d)))
|
||||
|
||||
def load_data(self):
|
||||
Logger.debug("Loading config")
|
||||
self._env_overrides = self._load_env_overrides()
|
||||
@@ -173,6 +204,7 @@ class Config:
|
||||
"game": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"shop": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"transmute": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"profile": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"custom": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
}
|
||||
self.configs["config"]["parser"].read('config/params.ini')
|
||||
@@ -180,6 +212,21 @@ class Config:
|
||||
self.configs["shop"]["parser"].read('config/shop.ini')
|
||||
self.configs["transmute"]["parser"].read('config/transmute.ini')
|
||||
|
||||
# Profile layer: per-user/per-character overrides that survive git
|
||||
# (config/profiles/ is gitignored). Selected via config/active_profile.txt.
|
||||
# Priority: custom.ini > profile.ini > params.ini.
|
||||
self.active_profile = Config.get_active_profile()
|
||||
if self.active_profile:
|
||||
profile_ini = os.path.join("config", "profiles", self.active_profile, "profile.ini")
|
||||
if os.path.exists(profile_ini):
|
||||
try:
|
||||
self.configs["profile"]["parser"].read(profile_ini)
|
||||
Logger.info(f"Profile active: {self.active_profile} ({profile_ini})")
|
||||
except configparser.MissingSectionHeaderError:
|
||||
Logger.error(f"{profile_ini} missing section header — profile ignored")
|
||||
else:
|
||||
Logger.warning(f"Active profile '{self.active_profile}' has no profile.ini — ignored")
|
||||
|
||||
if os.environ.get('RUN_ENV') != "test" and os.path.exists('config/custom.ini'):
|
||||
try:
|
||||
self.configs["custom"]["parser"].read('config/custom.ini')
|
||||
@@ -199,6 +246,7 @@ class Config:
|
||||
self.general = {
|
||||
"saved_games_folder": self._select_val("general", "saved_games_folder"),
|
||||
"name": _default_iff(self._select_val("general", "name"), "", "botty"),
|
||||
"pickit_profile": self._select_optional("general", "pickit_profile", ""),
|
||||
"max_game_length_s": float(self._select_val("general", "max_game_length_s")),
|
||||
"max_consecutive_fails": int(self._select_val("general", "max_consecutive_fails")),
|
||||
"max_maintenance_time_s": float(self._select_optional("general", "max_maintenance_time_s", "120")),
|
||||
@@ -333,85 +381,119 @@ class Config:
|
||||
}
|
||||
# Sorc base config
|
||||
sorc_base_cfg = dict(self.configs["config"]["parser"]["sorceress"])
|
||||
if "sorceress" in self.configs["profile"]["parser"]:
|
||||
sorc_base_cfg.update(dict(self.configs["profile"]["parser"]["sorceress"]))
|
||||
if "sorceress" in self.configs["custom"]["parser"]:
|
||||
sorc_base_cfg.update(dict(self.configs["custom"]["parser"]["sorceress"]))
|
||||
# blizz sorc
|
||||
self.blizz_sorc = dict(self.configs["config"]["parser"]["blizz_sorc"])
|
||||
if "blizz_sorc" in self.configs["profile"]["parser"]:
|
||||
self.blizz_sorc.update(dict(self.configs["profile"]["parser"]["blizz_sorc"]))
|
||||
if "blizz_sorc" in self.configs["custom"]["parser"]:
|
||||
self.blizz_sorc.update(dict(self.configs["custom"]["parser"]["blizz_sorc"]))
|
||||
self.blizz_sorc.update(sorc_base_cfg)
|
||||
# blizzorb sorc
|
||||
self.blizzorb_sorc = dict(self.configs["config"]["parser"]["blizzorb_sorc"])
|
||||
if "blizzorb_sorc" in self.configs["profile"]["parser"]:
|
||||
self.blizzorb_sorc.update(dict(self.configs["profile"]["parser"]["blizzorb_sorc"]))
|
||||
if "blizzorb_sorc" in self.configs["custom"]["parser"]:
|
||||
self.blizzorb_sorc.update(dict(self.configs["custom"]["parser"]["blizzorb_sorc"]))
|
||||
self.blizzorb_sorc.update(sorc_base_cfg)
|
||||
# light sorc
|
||||
self.light_sorc = dict(self.configs["config"]["parser"]["light_sorc"])
|
||||
if "light_sorc" in self.configs["profile"]["parser"]:
|
||||
self.light_sorc.update(dict(self.configs["profile"]["parser"]["light_sorc"]))
|
||||
if "light_sorc" in self.configs["custom"]["parser"]:
|
||||
self.light_sorc.update(dict(self.configs["custom"]["parser"]["light_sorc"]))
|
||||
self.light_sorc.update(sorc_base_cfg)
|
||||
# nova sorc
|
||||
self.nova_sorc = dict(self.configs["config"]["parser"]["nova_sorc"])
|
||||
if "nova_sorc" in self.configs["profile"]["parser"]:
|
||||
self.nova_sorc.update(dict(self.configs["profile"]["parser"]["nova_sorc"]))
|
||||
if "nova_sorc" in self.configs["custom"]["parser"]:
|
||||
self.nova_sorc.update(dict(self.configs["custom"]["parser"]["nova_sorc"]))
|
||||
self.nova_sorc.update(sorc_base_cfg)
|
||||
# hydra sorc
|
||||
self.hydra_sorc = dict(self.configs["config"]["parser"]["hydra_sorc"])
|
||||
if "hydra_sorc" in self.configs["profile"]["parser"]:
|
||||
self.hydra_sorc.update(dict(self.configs["profile"]["parser"]["hydra_sorc"]))
|
||||
if "hydra_sorc" in self.configs["custom"]["parser"]:
|
||||
self.hydra_sorc.update(dict(self.configs["custom"]["parser"]["hydra_sorc"]))
|
||||
self.hydra_sorc.update(sorc_base_cfg)
|
||||
|
||||
# Warlock base config
|
||||
warlock_base_cfg = dict(self.configs["config"]["parser"]["warlock"])
|
||||
if "warlock" in self.configs["profile"]["parser"]:
|
||||
warlock_base_cfg.update(dict(self.configs["profile"]["parser"]["warlock"]))
|
||||
if "warlock" in self.configs["custom"]["parser"]:
|
||||
warlock_base_cfg.update(dict(self.configs["custom"]["parser"]["warlock"]))
|
||||
# fire warlock
|
||||
self.fire_lock = dict(self.configs["config"]["parser"]["fire_lock"])
|
||||
if "fire_lock" in self.configs["profile"]["parser"]:
|
||||
self.fire_lock.update(dict(self.configs["profile"]["parser"]["fire_lock"]))
|
||||
if "fire_lock" in self.configs["custom"]["parser"]:
|
||||
self.fire_lock.update(dict(self.configs["custom"]["parser"]["fire_lock"]))
|
||||
self.fire_lock.update(warlock_base_cfg)
|
||||
# echo warlock
|
||||
self.echo_lock = dict(self.configs["config"]["parser"]["echo_lock"])
|
||||
if "echo_lock" in self.configs["profile"]["parser"]:
|
||||
self.echo_lock.update(dict(self.configs["profile"]["parser"]["echo_lock"]))
|
||||
if "echo_lock" in self.configs["custom"]["parser"]:
|
||||
self.echo_lock.update(dict(self.configs["custom"]["parser"]["echo_lock"]))
|
||||
self.echo_lock.update(warlock_base_cfg)
|
||||
# abyss warlock
|
||||
self.abyss_lock = dict(self.configs["config"]["parser"]["abyss_lock"])
|
||||
if "abyss_lock" in self.configs["profile"]["parser"]:
|
||||
self.abyss_lock.update(dict(self.configs["profile"]["parser"]["abyss_lock"]))
|
||||
if "abyss_lock" in self.configs["custom"]["parser"]:
|
||||
self.abyss_lock.update(dict(self.configs["custom"]["parser"]["abyss_lock"]))
|
||||
self.abyss_lock.update(warlock_base_cfg)
|
||||
# Paladin base config
|
||||
paladin_base_cfg = dict(self.configs["config"]["parser"]["paladin"])
|
||||
if "paladin" in self.configs["profile"]["parser"]:
|
||||
paladin_base_cfg.update(dict(self.configs["profile"]["parser"]["paladin"]))
|
||||
if "paladin" in self.configs["custom"]["parser"]:
|
||||
paladin_base_cfg.update(dict(self.configs["custom"]["parser"]["paladin"]))
|
||||
# Hammerdin config
|
||||
self.hammerdin = self.configs["config"]["parser"]["hammerdin"]
|
||||
if "hammerdin" in self.configs["profile"]["parser"]:
|
||||
self.hammerdin.update(dict(self.configs["profile"]["parser"]["hammerdin"]))
|
||||
if "hammerdin" in self.configs["custom"]["parser"]:
|
||||
self.hammerdin.update(self.configs["custom"]["parser"]["hammerdin"])
|
||||
self.hammerdin.update(paladin_base_cfg)
|
||||
# FoHdin config
|
||||
self.fohdin = dict(self.configs["config"]["parser"]["fohdin"])
|
||||
if "fohdin" in self.configs["profile"]["parser"]:
|
||||
self.fohdin.update(dict(self.configs["profile"]["parser"]["fohdin"]))
|
||||
if "fohdin" in self.configs["custom"]["parser"]:
|
||||
self.fohdin.update(self.configs["custom"]["parser"]["fohdin"])
|
||||
self.fohdin.update(paladin_base_cfg)
|
||||
|
||||
# Amazon base config
|
||||
amazon_base_cfg = dict(self.configs["config"]["parser"]["amazon"])
|
||||
if "amazon" in self.configs["profile"]["parser"]:
|
||||
amazon_base_cfg.update(dict(self.configs["profile"]["parser"]["amazon"]))
|
||||
if "amazon" in self.configs["custom"]["parser"]:
|
||||
amazon_base_cfg.update(dict(self.configs["custom"]["parser"]["amazon"]))
|
||||
# javazon
|
||||
self.javazon = dict(self.configs["config"]["parser"]["javazon"])
|
||||
if "javazon" in self.configs["profile"]["parser"]:
|
||||
self.javazon.update(dict(self.configs["profile"]["parser"]["javazon"]))
|
||||
if "javazon" in self.configs["custom"]["parser"]:
|
||||
self.javazon.update(dict(self.configs["custom"]["parser"]["javazon"]))
|
||||
self.javazon.update(amazon_base_cfg)
|
||||
|
||||
# Assassin config
|
||||
self.trapsin = self.configs["config"]["parser"]["trapsin"]
|
||||
if "trapsin" in self.configs["profile"]["parser"]:
|
||||
self.trapsin.update(dict(self.configs["profile"]["parser"]["trapsin"]))
|
||||
if "trapsin" in self.configs["custom"]["parser"]:
|
||||
self.trapsin.update(self.configs["custom"]["parser"]["trapsin"])
|
||||
|
||||
# Barbarian config
|
||||
self.barbarian = self.configs["config"]["parser"]["barbarian"]
|
||||
if "barbarian" in self.configs["profile"]["parser"]:
|
||||
self.barbarian.update(dict(self.configs["profile"]["parser"]["barbarian"]))
|
||||
if "barbarian" in self.configs["custom"]["parser"]:
|
||||
self.barbarian.update(self.configs["custom"]["parser"]["barbarian"])
|
||||
self.barbarian = dict(self.barbarian)
|
||||
@@ -419,24 +501,34 @@ class Config:
|
||||
|
||||
# Basic config
|
||||
self.basic = self.configs["config"]["parser"]["basic"]
|
||||
if "basic" in self.configs["profile"]["parser"]:
|
||||
self.basic.update(dict(self.configs["profile"]["parser"]["basic"]))
|
||||
if "basic" in self.configs["custom"]["parser"]:
|
||||
self.basic.update(self.configs["custom"]["parser"]["basic"])
|
||||
|
||||
# Basic Ranged config
|
||||
self.basic_ranged = self.configs["config"]["parser"]["basic_ranged"]
|
||||
if "basic_ranged" in self.configs["profile"]["parser"]:
|
||||
self.basic_ranged.update(dict(self.configs["profile"]["parser"]["basic_ranged"]))
|
||||
if "basic_ranged" in self.configs["custom"]["parser"]:
|
||||
self.basic_ranged.update(self.configs["custom"]["parser"]["basic_ranged"])
|
||||
|
||||
# Necro config
|
||||
self.necro = self.configs["config"]["parser"]["necro"]
|
||||
if "necro" in self.configs["profile"]["parser"]:
|
||||
self.necro.update(dict(self.configs["profile"]["parser"]["necro"]))
|
||||
if "necro" in self.configs["custom"]["parser"]:
|
||||
self.necro.update(self.configs["custom"]["parser"]["necro"])
|
||||
|
||||
self.poison_necro = self.configs["config"]["parser"]["poison_necro"]
|
||||
if "poison_necro" in self.configs["profile"]["parser"]:
|
||||
self.poison_necro.update(dict(self.configs["profile"]["parser"]["poison_necro"]))
|
||||
if "poison_necro" in self.configs["custom"]["parser"]:
|
||||
self.poison_necro.update(self.configs["custom"]["parser"]["poison_necro"])
|
||||
|
||||
self.bone_necro = self.configs["config"]["parser"]["bone_necro"]
|
||||
if "bone_necro" in self.configs["profile"]["parser"]:
|
||||
self.bone_necro.update(dict(self.configs["profile"]["parser"]["bone_necro"]))
|
||||
if "bone_necro" in self.configs["custom"]["parser"]:
|
||||
self.bone_necro.update(self.configs["custom"]["parser"]["bone_necro"])
|
||||
|
||||
|
||||
+18
@@ -154,6 +154,12 @@ def main():
|
||||
Logger.info("D2R is already running")
|
||||
|
||||
print(f"============ Botty {__version__} [name: {Config().general['name']}] ============")
|
||||
_profiles = Config.list_profiles()
|
||||
_pickit_profiles = Config.list_pickit_profiles()
|
||||
print(f"profile: {Config().active_profile or '(none)'}"
|
||||
+ (f" | available: {', '.join(_profiles)}" if _profiles else " | none in config/profiles/"))
|
||||
print(f"pickit: {Config().general.get('pickit_profile') or '(default config/bnip)'}"
|
||||
+ (f" | available: {', '.join(_pickit_profiles)}" if _pickit_profiles else ""))
|
||||
print("\nFor gettings started and documentation\nplease read https://github.com/aeon0/botty\n")
|
||||
table = BeautifulTable()
|
||||
table.set_style(BeautifulTable.STYLE_DEFAULT)
|
||||
@@ -164,6 +170,7 @@ def main():
|
||||
table.rows.append([Config().advanced_options['graphic_debugger_key'], "Start / Stop Graphic debugger"])
|
||||
table.rows.append([Config().advanced_options['resume_key'], "Start / Pause Botty"])
|
||||
table.rows.append([Config().advanced_options['exit_key'], "Stop bot"])
|
||||
table.rows.append(["end", "Cycle character profile"])
|
||||
table.columns.header = ["hotkey", "action"]
|
||||
try:
|
||||
print(table)
|
||||
@@ -192,6 +199,17 @@ def main():
|
||||
keyboard.add_hotkey(Config().advanced_options['settings_backup_key'], lambda: backup_settings())
|
||||
keyboard.add_hotkey(Config().advanced_options['resume_key'], lambda: start_or_pause_bot(controllers))
|
||||
keyboard.add_hotkey(Config().advanced_options["exit_key"], lambda: on_exit(controllers))
|
||||
|
||||
def _cycle_profile():
|
||||
profiles = Config.list_profiles()
|
||||
if not profiles:
|
||||
Logger.warning("No profiles found — create config/profiles/<name>/profile.ini first")
|
||||
return
|
||||
current = Config.get_active_profile()
|
||||
new = profiles[(profiles.index(current) + 1) % len(profiles)] if current in profiles else profiles[0]
|
||||
Config.set_active_profile(new)
|
||||
Logger.info(f"Active profile -> '{new}' — restart Botty to apply (config reloads at startup)")
|
||||
keyboard.add_hotkey("end", _cycle_profile)
|
||||
keyboard.wait()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user