From f5fa1cd7037a9006f1a8277dbbbdbd02d1837712 Mon Sep 17 00:00:00 2001 From: jerick Date: Tue, 25 Nov 2025 14:34:32 -0500 Subject: [PATCH] file and folder organization, removal of monoliths --- __pycache__/config.cpython-313.pyc | Bin 0 -> 456 bytes __pycache__/main.cpython-313.pyc | Bin 0 -> 2021 bytes cogs/__pycache__/assignments.cpython-313.pyc | Bin 0 -> 4043 bytes cogs/__pycache__/commands.cpython-313.pyc | Bin 0 -> 3966 bytes cogs/assignments.py | 64 ++++ cogs/commands.py | 54 +++ config.py | 13 + main.py | 324 ++---------------- .../__pycache__/ffscouter.cpython-313.pyc | Bin 0 -> 1897 bytes services/__pycache__/torn_api.cpython-313.pyc | Bin 0 -> 1565 bytes services/ffscouter.py | 22 ++ services/torn_api.py | 12 + 12 files changed, 188 insertions(+), 301 deletions(-) create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 cogs/__pycache__/assignments.cpython-313.pyc create mode 100644 cogs/__pycache__/commands.cpython-313.pyc create mode 100644 cogs/assignments.py create mode 100644 cogs/commands.py create mode 100644 config.py create mode 100644 services/__pycache__/ffscouter.cpython-313.pyc create mode 100644 services/__pycache__/torn_api.cpython-313.pyc create mode 100644 services/ffscouter.py create mode 100644 services/torn_api.py diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6db65d1a25867fa2fcea2da440ca9d9a4f33116 GIT binary patch literal 456 zcmYjNO-sW-6x00IOchA?P= z3?P=UXpjt|6iJ~pNuwb$bdo6!$Nc)9T3K!e_R99vzJ6+NI}eWlI3Hibpq-*MpL{6X zGAlC-i1~e^)2!<22gbR2;r3R0Z__mJwwcg1C9Rl=vvs|uVX>;<3N&IKYH-)UB~hv? zx`q`w&KY{6_TLahRn?nN#!^|-G^i#au2e#j-l)UcPYYMoRqAV0?}=(Gl$AP7L#guT zjn<%ukg}s;8LFaj5Bl^T5SOJ5v*FvsH_vS1Sm&nfS(hE#4SZ9!dv@FFCc->!S%KrZ z_^L(lk>ht;fpv-xy}-0Q_t-gE>fXee&5n0;*|xWcK#P+{`)icmS%zU=aXP5AvXDP40w|^5tc@q_1M|O-Ze98 zngj`jR%$_2MSDOkQYCumgx6sf-Z+)}2<3M@RzWI6H+nIg8_u-xP z_Aml@rhH_(3POL1A594~z)p{Z&|PF8Lz+TVtn!pJDN|Xm?_di@mve(nuhx1Rq7p*8^WESX%Eyehvobx9Kad-$TlQ)r+=q5=?g5|VUTYm2 zk<-A35FM4IMD(SA*@VPHmRqTqPJty<9v{hME`E_t;nDGtY&M<26Di()>0$;zb|#(s zd?dr;;}bJrPLEEt_F}p8h+#~OWpOH<8JSPWJYq6tmz)Z5JVr$=N3fZCIK`<=uBm=!{a79!S|lfNLgObu8QE z5sMPjBiJ+9G7Z6Dyr<$iw&zm3hYwtK-6{_)lzcx1DNq?H7nz7h=NCrL+hF7w_u z=^JX9I^j4o0|C|t7^O~)Z+F53kvu1?4_4hvT- zZ-t3#b)D`L8*P?b!qy_OmU+)xvF=*-gCpw&B|_cmh~K$>`}&<5w{JY|Pd@BOZiJGJ z@DAxxLZV&Zm`!Lrgt22*2*x~&am6jv$^u6*zFITO4NC{cMVm6OY&*npF{a`w+6Rhv zW6V6$vn_0T9<>*09$^?iKs#WBF(8zfWWEwb&7z>bL4V6iApB2A3Y?+?O*5DJp|N6( zZpBqfiuMZEY{wH#_C|XNuKbM50cdo?ZnqfHmEG*+gy&oGu5O5-q}=QS7$1I?5^{E} zJ>!;2q@df5ZZaJPNT5pLgSY~f$kiH@F;@jiv>z_1hvMfe1eFV|Qdp(Oz#x*s-UsCu zq1tw9`j;d32k#BuKYs7{&xbd{i6`pOMsfy^N^9fOGnY?HFO`znrP*Y5W_E2l1$b>_ zW%}~aShhBPi4M(DQkzemJbz|>eyPY7ia9I2uy|p0Y4z&0#mgyD&5dSHEiWhfxCXsr zxwOC|4KPGjJbz%(7<>>9TCP*HOMdlH@y|lVte~2(sZd(1-sVw=7dPi9*Cy&YfzwIR ztd%_~I*fNz2xDf6fTM5capF+7TqXrk9HzAlP0u65A(d6AE>a^rE}Hxr**iRV7j0rB-YDF{FCqij0wl{ndJ(tIsYU7(|R3$Mn3^}Q5WnxP}_)KY|p>^#sI{2dd;0B6s#X8p`TXnd< zdjoa7R)f;1ALf3_|CoP`0KV!(TKBJ`zaM-UJiD&EkkoH`Z}z^DCF$6Xg5=1yK-)nC Q``S%yBb0lB48KzT19Di`Qvd(} literal 0 HcmV?d00001 diff --git a/cogs/__pycache__/assignments.cpython-313.pyc b/cogs/__pycache__/assignments.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa03944bf036e15778c7cd6b1f570eedb89bdfb5 GIT binary patch literal 4043 zcmbVPT~Hg>6~6l?Ek+Vp5)ywBi%>xlHa0dSHO18Y7;qgl8Ob=fFx_sn3#b;mba%0f zUz$m0S~BTO?9O%QxPJzU$zbba~;hg zmB=EM%n~YyI zQRbk-SD*dcZ2$HSIJAJ0L^@exQEw``RV&HT(klP+6=_AX{6bF8Uy^vC`kj_vl2xTp zkVIb4bs=|AQZ>szdFC8HI(d5ZEPp0_?sVq-@N1SYb9y+NJu{K!$43BIk8Q-Kn3)(~@qB)}+F`%ED$<2oa>>h*4{SST&1zozE>wxr>$$ zl-XP3MX4aHYS{c-xmlUffw^f7b~pDkNzLaj&W$O#728#FW6~w5pe*B;xq1AFip*aY zR9?($%YvR;?Z$lc&?B~G(}ik0@3HT&CfjPTt?!>RBLhZcpconals&lxT2QVMB=CZj z+u?K@nCl3f3(wsxvE3np8k-PYwb$j*?_r z#ZjK5%06@n%gd51TGTQ)n^bX_T5Khr@JxZ@<0Miyh{t|JWN;4kN^4-ns z=y2RUL5LDUvlT4N;B&L9#gHSF!N+J}2i;*f#WFMVdd6SXSSsSjn{$0eXE6`dTK`U66>I1Wg#-2hmM+Q<5lZ_^{YL8@(!8Se7;?Eei@gXQ&i#1Z$h9>~4q z{yP5u7#86yOb}Bd2CfCf0-t`DMd`n^gGJg<`mZOcHZ)H_&w2R2@+vh?B-ym()$>bE zev-te*i|Yv^Oa~uTFT3!qz-Twlojr>q)J>F%_Wgbr7XtIa~3TNOVX7{D#bx%!0C(9 zDks29Svk%$XuLznetAu|lbFTib!kacaT2#!Jb4AO6wY_DsN!>{Heu3?hwCc&O&3&{ zYB5?tl9rQf88InYxomkWbMO)tTh8Bk(V|5`7c4g<#$~)guLG~!>*eKnMZ-8z&X?PG zt77=H;yV#`za))MjrJCS&{GVxKL2}*?<|_WF2mPV^d(I;xxpq&{?LuYwS?)98~(WI z?|kU*eBy61{fUSEMA4rz{YMP{k)pqMZM4)HU7LCm+kwMlM%P%eYy5F+;+pR&TWXH1 z^9I*%H1}V1mjcaZwBLyK7X$qtc9~S!xZE^qwTg+&$5$*jn&|3<&o56$;Ox$XG96a`{33W^oTdgRPf-Qzy%y5qp?lHs1 zjPSAB-!Fy-uhOMZvl&Vmq13Iu+wKoe7DFe@`V$Z9PdsBl=t~v_nr|Hb-Qjhj=#QD6 z*oG&z*^L6>7p(T!-}#(EHK}Jo*wZ(gE~2UZvsUgu&phbhz!#p_zcmRw@pg3l2gEut z-c8>e83Ohm7ac!J-WwvudtCQ^NCW>d<%Rx_gTo$pe8Lgquev^IZwLN9NsOPM?^7L^ zPg0on5Fl{>C^_E8+&|{RJtqjDIh>iDhR>Rv1qUGpDp?Tt-K~;a$Y~PiQ7BA^X>zur zD?{&=Kv3a3Qkk7K_4Y((EKBrYjNBvJO%quXZ?FsgFvGrYytqoHlH;oV(-mX`?4>Gs zB4+0aYOfkQOTkJsUI{V;^KBWTjB06D8P66_8P9ASbETm!Y6l%Qbg3A;Eix}&X+3A3 zdd@lFfLyx5ttu-6fM6=@3SFU6sdj*aRxJEIOgb^aUtO-4v1!y4Cfz`iv<)0P);bQo z8n!=10lVT-@kPjL*@*zo`cSoF6grA!ZUV6nKCY{hMj%-XbVF5D8!&2HN*+HHx1p9& zBljc{HzUU%MvfOF{gwLmnVUX(V2v)WBwos{dWTd<{QY7ChBgOfu5wx z#^y2>+t&`;i1sZ6S5Ek<1Q!9EIN&01fajMKaixHH4-fYyp|Cwt$Mf@fRnrT3S&|i= zS8;h#_7n!$vYi`vUeg6VpTp&?ntyXemo%RL0NF)n*&jH#R7|j&v=ESAqvvD|GxR*f zFpbZBVW#7&WA#i!Sp-i^GIlIjwQ!-yDXQ25<+BRu6AISGl#C=H*5)D@qHx6Ap^c4cu7XBa7wlOdO literal 0 HcmV?d00001 diff --git a/cogs/__pycache__/commands.cpython-313.pyc b/cogs/__pycache__/commands.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..006efa9ba7bbda53c4edebff930da4c9e23a7f43 GIT binary patch literal 3966 zcmds4U2Gd!6~6O7jvfD{NgP+uX_lsO5+_ZwNoZNxZPK*czm(0?-7E>rI`;U7+B4~$ zaoR>8(5{41cBLjNRlR^jK|Hiil(&FDVk^4OHeRrIXwmZX(zm1mq`n}|9se{9sj9y4 zz?J6AnRD+w_ndRTd+wQy=H?~@?G)RUekdUHH}X+$T#eb70_FyikxZRJCn-!xgr`r@ zCmGDxJadXY$ze`H0n~|PHi%?ymYQQVRn|= z&YVs(T$?B}!3K|o7mfxO>oc+V!qMEO(GHo%O-UgnTFy7p=6KEVM89mZHLPbcYFvq$W;AvI!e}|GQN5T` zb83jSSVPSuFxh|XB!ujjL2QtqS!c%&&PEKFHv0_~r(+jpkLs~pR@F>n_Ncm`X7qXT zF`J0S%(Sj4m!epSr;T}ggTR2siel~9OYn{c5zz!^VQZ7A;ab>F0RB_3ISbScW|DKk>PZwq+i9F~Y}EqV>AD{2IV(NK z6Z@n-tIrMRaHyegJG(?Q`6fKVD3hQ<%;aLn+j>rlVpWP}uo{hDk?J_6#9MqcXQp&)(doF29fBn-4&wQF@)ig*VCWqNH-o_z z6EiPcLJrWRq~ivGH6RSTNf%#T-|}z9L{&yWX

K2r%)Pzx8I%O3%&kN_a`AIQ`|e zaIr01a)v9=(Y?}L*f~=2kCt7d1=ncB<*$gYvbd`#?kbC2MX~GFrBB3xiraTBbv2cL z<;OiA_LPHt#b95dBU}idE3`*S?(=2wd{I2VE}-TwuJg$87WHJK6Bd8>;FzEKH8tjD ze{;kM>^+eh^Ro9GJmI|*(EkB20eJuKz-+ApQ`5}`{`wcc@D!9-&FTwkTuNX)YooG` z$o>$IN#?*i2(fcnD{vNHoq^klH~{2X=&^CE&s*Njz#|f({cq{XXQjhwtK?FCtlZb9c6gC0NzaBuV@qz_Hi>w-<1WO^SOu4US7`BgCq2Ap~<8nYOO8 zK2sAS+;%oagcsP%%R=2JO^h(x>3Au>$ss0we--@Zr^udeX)>%LJ4ieruQx>mpiYpu zYLe!N=te_g-7g89`F1sCvRiXRc! zQ`?~9sfMlbhq@0|o&JIBC3?cJrsg;^R<$SMsN=4dv|dtk67EwG9j7K^Ha%w065vl` z5+I%YH$1Py(`w&nzRAVFq%M&nWI>f`^`ex}b6ULL;z^;HGc1n0)evp5XD>vrSnP>- zMzwQ=<(o4U)iBdp=vPuXC_*jj@FLyQW3gH^t1b#XJyQ3HqX!p-kn~OI^nzq@8B+W# z`rvb-HZd-Z$ppg#d?KM_qw}y@T2l>+nNPu>c?>*XiE_y`@07?1#(r&z`nMG8-=WN6M~|f@=hl* z)!g!Q9+y0wWlvYp1NV2yGf)^kUh*9O)ElU{eAiB2J)Pg5kKW=xOy_5A3wN2@xx2?# zkE|wtd2;o&!tv?C+p~q~uN6AJ{zsRx?nM5MH8=9LfHgZhmX1E~_?L&@U;NSdE%t{e zik>~U*^;NP($;Y^wUWx`uIpv*o`QEzB@kTZV6FVom6@VT`rgF41GNVK)r4HWTEOzd z*9Xe3-9^{#H3p=Q!>FbIudb0Nk9p*YQlI+5>p|q}S{G4s=My6d@qgESjHQ;Ti6M4n z7jSo}i4pb}4&Z*(J#m2jsLMgr}VfgsnVd@y-@1s88@Apy1X!d?s zAp8hL=m8$+$B6JB(}aeYYFSSzdiy?{3^_^tO)w4hcax%I^>{8rc$cDF%tbS`9*?3V z(%3Kwyfj@=u!m?M$JY{SwLFGofv`l#0U~cz?EkTJjQnWe^jywV4Mn+wu#XHOH6AAE z+$Q847;`|riyqSqw|A`x@$NF$`g^Xm#I>)}Jogs0!5}`cX0vPFt=;FIcAulRb_cjl zd-4ViTy=S}@+A4!f?t!Ej^q96WwYvz4~o%+bWAn+O&x1WbUtk#TFc*%ClW9Pyur2O zJFX|Io*W_WG`zFK*Q&uhgpWZ7oG)a29>^L)QPe{uJw$^K(Y}AMgH-2o_szbQzJDTM H?fL%!maHoj literal 0 HcmV?d00001 diff --git a/cogs/assignments.py b/cogs/assignments.py new file mode 100644 index 0000000..4bfe2c2 --- /dev/null +++ b/cogs/assignments.py @@ -0,0 +1,64 @@ +import asyncio +from discord.ext import commands + +class Assignments(commands.Cog): + def __init__(self, bot, enemy_queue, active_assignments, enrolled_attackers, hit_check, reassign_delay): + self.bot = bot + self.enemy_queue = enemy_queue + self.active_assignments = active_assignments + self.enrolled_attackers = enrolled_attackers + self.HIT_CHECK_INTERVAL = hit_check + self.REASSIGN_DELAY = reassign_delay + + # Start background task + bot.loop.create_task(self.monitor_assignments_loop()) + + def get_next_attacker(self): + if not self.enrolled_attackers: + return None + attacker = self.enrolled_attackers[0] + self.enrolled_attackers.append(self.enrolled_attackers.pop(0)) # round-robin + return attacker + + async def monitor_assignments_loop(self): + await self.bot.wait_until_ready() + while not self.bot.is_closed(): + now = asyncio.get_event_loop().time() + reassign_list = [] + for enemy_id, data in list(self.active_assignments.items()): + elapsed = now - data["time_assigned"] + if elapsed >= self.HIT_CHECK_INTERVAL and elapsed < self.HIT_CHECK_INTERVAL + 5: + attacker_user = self.bot.get_user(data["attacker"]) + if attacker_user: + try: + await attacker_user.send( + f"Reminder: You were assigned **{data['enemy']['name']}** and they are not down yet!" + ) + except: + pass + if elapsed >= self.REASSIGN_DELAY: + reassign_list.append(enemy_id) + + for enemy_id in reassign_list: + info = self.active_assignments.pop(enemy_id) + await self.reassign_target(info["enemy"]) + + await asyncio.sleep(5) + + async def reassign_target(self, enemy): + attacker = self.get_next_attacker() + if attacker is None: + return + + self.active_assignments[enemy["id"]] = { + "enemy": enemy, + "attacker": attacker, + "time_assigned": asyncio.get_event_loop().time() + } + + attacker_user = self.bot.get_user(attacker) + if attacker_user: + try: + await attacker_user.send(f"Target reassigned to you: **{enemy['name']}**!") + except: + pass diff --git a/cogs/commands.py b/cogs/commands.py new file mode 100644 index 0000000..1da96a5 --- /dev/null +++ b/cogs/commands.py @@ -0,0 +1,54 @@ +from discord.ext import commands +from services.torn_api import fetch_enemy_members +from services.ffscouter import fetch_batch_stats + +class HitCommands(commands.Cog): + def __init__(self, bot, enrolled_attackers, enemy_queue): + self.bot = bot + self.enrolled_attackers = enrolled_attackers + self.enemy_queue = enemy_queue + + @commands.command() + async def enroll(self, ctx): + user_id = ctx.author.id + if user_id in self.enrolled_attackers: + await ctx.send("You are already enrolled.") + return + self.enrolled_attackers.append(user_id) + await ctx.send(f"{ctx.author.mention} has been enrolled in the hit rotation!") + + @commands.command() + async def drop(self, ctx): + user_id = ctx.author.id + if user_id not in self.enrolled_attackers: + await ctx.send("You are not enrolled.") + return + self.enrolled_attackers.remove(user_id) + await ctx.send(f"{ctx.author.mention} has been removed from the rotation.") + + @commands.command() + async def stats(self, ctx): + members = await fetch_enemy_members() + if not members: + await ctx.send("No active members found.") + return + + ids = [m["id"] for m in members if m.get("status", {}).get("state") in ("Okay", "Idle")] + ff_map = await fetch_batch_stats(ids) + + lines = [] + for m in members: + pid = str(m["id"]) + est = ff_map.get(pid, {}).get("bs_estimate_human", "?") + if m.get("status", {}).get("state") not in ("Okay", "Idle"): + continue + lines.append(f"**{m['name']}** (ID:{pid}) | Lv {m['level']} | Estimated BS: {est}") + + chunk = "" + for line in lines: + if len(chunk) + len(line) > 1900: + await ctx.send(chunk) + chunk = "" + chunk += line + "\n" + if chunk: + await ctx.send(chunk) diff --git a/config.py b/config.py new file mode 100644 index 0000000..67a5122 --- /dev/null +++ b/config.py @@ -0,0 +1,13 @@ +# Torn API +TORN_API_KEY = "9VLK0Wte1BwXOheB" +ENEMY_FACTION_ID = 52935 +YOUR_FACTION_ID = 654321 +ALLOWED_CHANNEL_ID = 1442876328536707316 + +# FFScouter API +FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v" + +# Intervals +POLL_INTERVAL = 30 +HIT_CHECK_INTERVAL = 60 +REASSIGN_DELAY = 120 diff --git a/main.py b/main.py index 6089558..6b504f4 100644 --- a/main.py +++ b/main.py @@ -1,324 +1,46 @@ import discord -import asyncio -import aiohttp -from discord.ext import commands -import re - -# ============================== -# CONFIGURATION -# ============================== - -TORN_API_KEY = "9VLK0Wte1BwXOheB" -ENEMY_FACTION_ID = 52935 -YOUR_FACTION_ID = 654321 -ALLOWED_CHANNEL_ID = 1442876328536707316 -FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v" - -POLL_INTERVAL = 30 -HIT_CHECK_INTERVAL = 60 -REASSIGN_DELAY = 120 +from discord.ext import commands +from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY +from cogs.assignments import Assignments +from cogs.commands import HitCommands intents = discord.Intents.default() intents.message_content = True -# ============================== -# STATE STORAGE -# ============================== - +# Global state enrolled_attackers = [] enemy_queue = [] active_assignments = {} round_robin_index = 0 -# ============================== -# BOT SUBCLASS -# ============================== - class HitDispatchBot(commands.Bot): async def setup_hook(self): - # Start background loops - self.bg_tasks = [] - self.bg_tasks.append(asyncio.create_task(update_enemy_queue_loop())) - self.bg_tasks.append(asyncio.create_task(monitor_assignments_loop())) + # Load cogs with injected state + await self.add_cog( + Assignments( + self, + enemy_queue=enemy_queue, + active_assignments=active_assignments, + enrolled_attackers=enrolled_attackers, + hit_check=HIT_CHECK_INTERVAL, + reassign_delay=REASSIGN_DELAY + ) + ) + await self.add_cog( + HitCommands( + self, + enrolled_attackers=enrolled_attackers, + enemy_queue=enemy_queue + ) + ) async def cog_check(self, ctx): return ctx.channel.id == ALLOWED_CHANNEL_ID - -# Create bot now (ONE bot only) bot = HitDispatchBot(command_prefix="!", intents=intents) -async def fetch_ffscouter_stats(session: aiohttp.ClientSession, torn_id: int): - """ - Calls FFScouter and returns predicted battle stats. - Uses an existing aiohttp session (caller must provide). - """ - url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={torn_id}" - - #print(url) - - async with session.get(url) as resp: - if resp.status != 200: - print(f"FFScouter Error for {torn_id}:", resp.status) - return None - - data = await resp.json() - # FFScouter v1 returns: {"code":200,"message":"OK","data": {"": {...}}} - if "data" not in data: - return None - inner = data["data"] - return inner.get(str(torn_id)) - - -async def fetch_enemy_faction(): - """ - Pulls faction members from Torn (selections=members), - then fetches FFScouter stats in a single batch request. - Returns a list of enemies with name, id, level, and estimated BS. - """ - url = ( - f"https://api.torn.com/v2/faction/{ENEMY_FACTION_ID}" - f"?selections=members&key={TORN_API_KEY}" - ) - - enemies = [] - - async with aiohttp.ClientSession() as session: - # --- Fetch faction members --- - async with session.get(url) as resp: - if resp.status != 200: - print("Torn faction fetch error:", resp.status) - return enemies - data = await resp.json() - - members_list = data.get("members", []) - if not members_list: - return enemies - - # --- Build comma-separated list of IDs --- - member_ids = [ - str(info.get("player_id", info.get("id", 0))) - for info in members_list - if info.get("status", {}).get("state", "Unknown") in ("Okay", "Idle") - ] - - if not member_ids: - return enemies - - ids_str = ",".join(member_ids) - - # --- Single FFScouter batch request --- - ff_url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={ids_str}" - async with session.get(ff_url) as resp: - if resp.status != 200: - print("FFScouter batch error:", resp.status) - ff_data_list = [] - else: - ff_data_list = await resp.json() - - # --- Map FFScouter data by player_id for quick lookup --- - ff_map = {str(d["player_id"]): d for d in ff_data_list} - - # --- Build final enemies list --- - for info in members_list: - pid = str(info.get("player_id", info.get("id", 0))) - state = info.get("status", {}).get("state", "Unknown") - if state not in ("Okay", "Idle"): - continue - - name = info.get("name", "Unknown") - level = int(info.get("level", 0)) - est = ff_map.get(pid, {}).get("bs_estimate_human", "?") - - enemies.append({ - "id": int(pid), - "name": name, - "level": level, - "estimate": est - }) - - return enemies - -async def update_enemy_queue_loop(): - global enemy_queue - - await bot.wait_until_ready() - - while not bot.is_closed(): - print("Refreshing enemy list...") - enemy_queue = await fetch_enemy_faction() - print(f"Enemy queue updated: {len(enemy_queue)} valid targets.") - await asyncio.sleep(POLL_INTERVAL) - - -# ============================== -# ASSIGNMENT SYSTEM -# ============================== - -def get_next_attacker(): - global round_robin_index - - if not enrolled_attackers: - return None - - attacker = enrolled_attackers[round_robin_index] - round_robin_index = (round_robin_index + 1) % len(enrolled_attackers) - return attacker - - -async def assign_next_target(): - if not enemy_queue: - return None, None - - # Pop the weakest enemy (already sorted by score_value) - enemy = enemy_queue.pop(0) - attacker = get_next_attacker() - - if attacker is None: - return None, None - - active_assignments[enemy["id"]] = { - "enemy": enemy, - "attacker": attacker, - "time_assigned": asyncio.get_event_loop().time() - } - - return enemy, attacker - -async def monitor_assignments_loop(): - await bot.wait_until_ready() - - while not bot.is_closed(): - now = asyncio.get_event_loop().time() - reassign_list = [] - - for enemy_id, data in list(active_assignments.items()): - elapsed = now - data["time_assigned"] - - if elapsed >= HIT_CHECK_INTERVAL and elapsed < HIT_CHECK_INTERVAL + 5: - attacker_user = bot.get_user(data["attacker"]) - if attacker_user: - try: - await attacker_user.send( - f"Reminder: You were assigned **{data['enemy']['name']}** and they are not down yet!" - ) - except: - pass - - if elapsed >= REASSIGN_DELAY: - reassign_list.append(enemy_id) - - for enemy_id in reassign_list: - info = active_assignments.pop(enemy_id) - await reassign_target(info["enemy"]) - - await asyncio.sleep(5) - - -async def reassign_target(enemy): - attacker = get_next_attacker() - - if attacker is None: - return - - active_assignments[enemy["id"]] = { - "enemy": enemy, - "attacker": attacker, - "time_assigned": asyncio.get_event_loop().time() - } - - attacker_user = bot.get_user(attacker) - if attacker_user: - try: - await attacker_user.send( - f"Target reassigned to you: **{enemy['name']}**!" - ) - except: - pass - - -# ============================== -# COMMANDS -# ============================== - -@bot.command() -async def enroll(ctx): - user_id = ctx.author.id - - if user_id in enrolled_attackers: - await ctx.send("You are already enrolled.") - return - - enrolled_attackers.append(user_id) - await ctx.send(f"{ctx.author.mention} has been enrolled in the hit rotation!") - - -@bot.command() -async def drop(ctx): - user_id = ctx.author.id - - if user_id not in enrolled_attackers: - await ctx.send("You are not enrolled.") - return - - enrolled_attackers.remove(user_id) - await ctx.send(f"{ctx.author.mention} has been removed from the rotation.") - - -@bot.command() -async def next(ctx): - enemy, attacker = await assign_next_target() - - if enemy is None: - await ctx.send("No targets available or no attackers enrolled.") - return - - attacker_user = bot.get_user(attacker) - await ctx.send(f"Assigned **{enemy['name']}** → <@{attacker}>") - - if attacker_user: - try: - await attacker_user.send( - f"Hit assignment: **{enemy['name']}**" - ) - except: - pass - -@bot.command() -async def stats(ctx): - enemies = await fetch_enemy_faction() - - if not enemies: - await ctx.send("No active members found.") - return - - lines = [ - f"**{e['name']}** (ID:{e['id']}) | Lv {e['level']} | Estimated BS: {e['estimate']}" - for e in enemies - ] - - # Discord chunking - chunk = "" - for line in lines: - if len(chunk) + len(line) > 1900: - await ctx.send(chunk) - chunk = "" - chunk += line + "\n" - if chunk: - await ctx.send(chunk) - - -# ============================== -# BOT READY -# ============================== - @bot.event async def on_ready(): print(f"Logged in as {bot.user.name}") - -# ============================== -# RUN BOT -# ============================== - bot.run("MTQ0Mjg3NjU3NTUzMDg3NzAxMQ.GNuHPr.UreuYD1B7YYjfsbfRcEbhFyjyqvhQDepRCN4kk") diff --git a/services/__pycache__/ffscouter.cpython-313.pyc b/services/__pycache__/ffscouter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27ff874f7bb1a82645871bd1283ea5e6542bc50b GIT binary patch literal 1897 zcmb7FO>7fK6n^7fuN`}B$7~3g6tZArNC^o6qKNRPDjV8Bn^rL{La}5Sdz~yeyJlt` zl%T4py?|5+P>V{{N=~~QHxsj&^P{RKt-Y*Yj)qf z`QG=vdHY6Q2?TtA<0m0*+;ai^!WWGp)Qy!+G_FAc666?A{&pQBM_tsVLPAIhDbbDl zdtC`}1DqDAdyI6rAM?=R4&nrk97znkojg7`l0G(g(h-at%M!j>A6|Tah;!vzRF8s+ zI!V}G-^`(J7sOC^jBCP}zPrJ4^Qh#4nI5cY_sYm{qO z$4IpXjaBWv1$yfa0z)Jrj-%Qnxh4#|WiW-LFcNqk&sRxTL-hqQVp1Fl{yQe&?mq{R z=?54MH+B=!i?NNfsaO?;_2$2ZZwS}V=BWM$L<~?zX{fHg8z&!v=Q&OH%QRoMf5NT6 zyApHp{1td(4S*?)^7g5Yq?GXRMWc#IDeZcz1;CGe!Ca;k11}8U#(4uL#hJzVCeA~f zp%bbVz)q-rB`?UUeT2P`$8K#$9u_RhdD7vXF8Yq> zc6MB6YLV(WBWvq9mDw4asdXx+!jFDEipJ;m3Eez|*a(vZpebyQ))a(SE1tAC(TDBc8Sl z(-9^zMMq#Zr2FTjDAN))-f}%V} z=Eg>O3>V;cAIPDlQ21ig&%yBJ@k`^MurKz^2p_+<5Zrp<^(DFQ`@pUDZyWRPoVYtR z?>l*6;8Cb`F%-KWiY%5bwnZg&QG41{ zLp31FLD<=|;P3fbn>qW@z!h?3$JOA1+&&Y&FK@Z2-ShX%i#^LBqAHpozNJ{14!*C5Ed|6q@>93Q+E99&Okn~4^dX2SxvTUxb!!tAR=Q@pb~1a5b2 z*EYK6T4l87wh(QtcP=V&dz*~-xjjT%C(Z43b97!R6n?`lKgPhKlGwE?#f!n%G)}0~Saos0iWWyAR!M72qR2>yz+#gCi)Po3 z>|CPu(wx#BqPp#&J+@LKrM>iQ@S#WAAlceP%AqIUY;mv@_0Ts9Yv*7o?Vx${y~p={ zXS5x++X=W{aiQ{S0`NCe%w_3;&M1N>kcKo_0Lo6w0=aIXmg^klL`ZYPK=~CC=J%OH zDs0hROE(r%xvQTha|`pgb?Ymto2@7rhUVj{+>ak8S+v-05BiB1Ay7-~{n!+LEZhS)=$L7(u{1f|>Q? zG&@gX{5NCPxVPne+yA$?)8>}8MvnUkf&)^&;rBvZ`Vpom-JL-KJ`JZB5m!by}`zI)7JHYGGdI z)@Vhyt34)9WwoyJ#k`hRSzHm9o|dB7jEbdY@5;1XxR*^Rg|#ZiSF;IuU9Ko~reu3{ z=I-Zdu2@#L#c#H66nQR^hqef9XBj8kl-^`S;mhXWQPny}_X)JDdq3wdD&Oao`=tFnAg~x9Ic!(H1{?=tl1szQ7@ia_EFJW2RF# z`fJk{Gqi-zKSpDpzn}1vUx+kfd&(htHjuD$+cqNk9ou$}VdsQ&-nxCs#c-TR0mt?n zkGR3Pklql8G-hjftqh;_A+s?-q;W?h%rktUpD|~M6t*=!HoRi}$ZR@D!pb-M1{wAf zZPmD`W21ZHaUC9XCmMT{|8Z!^3v8 zhpm)-MRi+2sg=qr#(V6(Om?R~LkA_$cNoQSrK%LyDsr3-qRPH`su#rpM+kWVo>wr{ L;T(!- literal 0 HcmV?d00001 diff --git a/services/ffscouter.py b/services/ffscouter.py new file mode 100644 index 0000000..866949d --- /dev/null +++ b/services/ffscouter.py @@ -0,0 +1,22 @@ +import aiohttp +from config import FFSCOUTER_KEY + +async def fetch_batch_stats(ids: list[int]): + """ + Fetches predicted stats for a list of Torn IDs in a single FFScouter request. + Returns dict keyed by player_id. + """ + if not ids: + return {} + + ids_str = ",".join(map(str, ids)) + url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={ids_str}" + + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status != 200: + print("FFScouter batch error:", resp.status) + return {} + data = await resp.json() + + return {str(d["player_id"]): d for d in data} diff --git a/services/torn_api.py b/services/torn_api.py new file mode 100644 index 0000000..7ce297c --- /dev/null +++ b/services/torn_api.py @@ -0,0 +1,12 @@ +import aiohttp +from config import TORN_API_KEY, ENEMY_FACTION_ID + +async def fetch_enemy_members(): + url = f"https://api.torn.com/v2/faction/{ENEMY_FACTION_ID}?selections=members&key={TORN_API_KEY}" + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status != 200: + print("Torn faction fetch error:", resp.status) + return [] + data = await resp.json() + return data.get("members", [])