2525from pathlib import Path
2626from typing import AsyncIterator , Any , Dict , Type , Optional , TYPE_CHECKING
2727
28- from .enums import Resource
28+ from .enums import PETS_ORDER , Resource
2929from .miscmodels import try_enum , Badge , TimeDelta
3030from .iterators import PlayerIterator
3131from .utils import CaseInsensitiveDict , UnitStat , _get_maybe_first
3232
3333if TYPE_CHECKING :
3434 from .players import Player
3535
36- BUILDING_FILE_PATH = Path (__file__ ).parent .joinpath (Path ("static/buildings.json" ))
36+ BUILDING_FILE_PATH = Path (__file__ ).parent .joinpath (
37+ Path ("static/buildings.json" ))
3738
3839
3940class BaseClan :
40- """An ABC that implements some common operations on clans, regardless of type.
41+ """
42+ Abstract data class that represents base Clan objects
4143
4244 Attributes
4345 ----------
@@ -50,48 +52,51 @@ class BaseClan:
5052 level: :class:`int`
5153 The clan's level.
5254 """
55+ __slots__ = ("tag" , "name" , "_client" , "badge" , "level" , "_response_retry" , "_raw_data" )
5356
54- __slots__ = ("tag" , "name" , "_client" , "badge" , "level" , "_response_retry" )
57+ def __init__ (self , * , data , client , ** kwargs ):
58+ self ._client = client
59+
60+ self ._response_retry = data .get ("_response_retry" )
61+ self .tag = data .get ("tag" )
62+ self .name = data .get ("name" )
63+ self .badge = try_enum (Badge , data = data .get ("badgeUrls" ),
64+ client = self ._client )
65+ self .level = data .get ("clanLevel" )
66+ self ._raw_data = data if client and client .raw_attribute else None
5567
5668 def __str__ (self ):
5769 return self .name
5870
5971 def __repr__ (self ):
60- return "<%s tag=%s name=%s>" % ( self .__class__ .__name__ , self .tag , self .name )
72+ return f"< { self .__class__ .__name__ } tag= { self .tag } name= { self .name } >"
6173
6274 def __eq__ (self , other ):
6375 return isinstance (other , BaseClan ) and self .tag == other .tag
6476
65- def __init__ (self , * , data , client , ** _ ):
66- self ._client = client
67-
68- self ._response_retry = data .get ("_response_retry" )
69- self .tag = data .get ("tag" )
70- self .name = data .get ("name" )
71- self .badge = try_enum (Badge , data = data .get ("badgeUrls" ), client = self ._client )
72- self .level = data .get ("clanLevel" )
73-
7477 @property
7578 def share_link (self ) -> str :
76- """:class:` str` - A formatted link to open the clan in-game"""
77- return "https://link.clashofclans.com/en?action=OpenClanProfile&tag=%23{}" . format ( self .tag .strip ("#" ))
79+ """str: A formatted link to open the clan in-game"""
80+ return f "https://link.clashofclans.com/en?action=OpenClanProfile&tag=%23{ self .tag .strip ('#' ) } "
7881
7982 @property
8083 def members (self ):
8184 # pylint: disable=missing-function-docstring
8285 return NotImplemented
8386
84- def get_detailed_members (self , cls : Type ["Player" ] = None , load_game_data : bool = None ) -> AsyncIterator ["Player" ]:
87+ def get_detailed_members (self , cls : Type ["Player" ] = None ,
88+ load_game_data : bool = None ) -> AsyncIterator ["Player" ]:
8589 """Get detailed player information for every player in the clan.
8690
87- This returns a :class:`PlayerIterator` which fetches all player tags in the clan in parallel.
91+ This returns a :class:`PlayerIterator` which fetches all player
92+ tags in the clan in parallel.
8893
8994 Example
9095 ---------
9196
9297 .. code-block:: python3
9398
94- clan = await client.get_clan('tag' )
99+ clan = await client.get_clan(clan_tag )
95100
96101 async for player in clan.get_detailed_members():
97102 print(player.name)
@@ -125,41 +130,37 @@ class BasePlayer:
125130 The player's name
126131 """
127132
128- __slots__ = ("tag" , "name" , "_client" , "_response_retry" )
133+ __slots__ = ("tag" , "name" , "_client" , "_response_retry" , "_raw_data" )
129134
130135 def __str__ (self ):
131136 return self .name
132137
133138 def __repr__ (self ):
134- return "<%s tag=%s name=%s>" % ( self .__class__ .__name__ , self .tag , self .name ,)
139+ return f"< { self .__class__ .__name__ } tag= { self .tag } name= { self .name } >"
135140
136141 def __eq__ (self , other ):
137142 return isinstance (other , BasePlayer ) and self .tag == other .tag
138143
139144 def __init__ (self , * , data , client , ** _ ):
140145 self ._client = client
141146 self ._response_retry = data .get ("_response_retry" )
142-
147+ self . _raw_data = data if client and client . raw_attribute else None
143148 self .tag = data .get ("tag" )
144149 self .name = data .get ("name" )
145150
146151 @property
147152 def share_link (self ) -> str :
148153 """:class:`str` - A formatted link to open the player in-game"""
149- return "https://link.clashofclans.com/en?action=OpenPlayerProfile&tag=%23{}" . format ( self .tag .strip ("#" ))
154+ return f "https://link.clashofclans.com/en?action=OpenPlayerProfile&tag=%23{ self .tag .strip ('#' ) } "
150155
151156
152157class DataContainerMetaClass (type ):
153- def __repr__ (cls ):
154- attrs = [
155- ("name" , cls .name ),
156- ("id" , cls .id ),
157- ]
158- return "<%s %s>" % (cls .__name__ , " " .join ("%s=%r" % t for t in attrs ),)
158+ pass
159159
160160
161161class DataContainer (metaclass = DataContainerMetaClass ):
162162 lab_to_townhall : Dict [int , int ]
163+ name : str
163164
164165 def __init__ (self , data , townhall ):
165166 self .name : str = data ["name" ]
@@ -176,7 +177,8 @@ def __repr__(self):
176177 ("level" , self .level ),
177178 ("is_active" , self .is_active ),
178179 ]
179- return "<%s %s>" % (self .__class__ .__name__ , " " .join ("%s=%r" % t for t in attrs ),)
180+ return "<%s %s>" % (
181+ self .__class__ .__name__ , " " .join ("%s=%r" % t for t in attrs ),)
180182
181183 @classmethod
182184 def _load_json_meta (cls , troop_meta , id , name , lab_to_townhall ):
@@ -186,7 +188,8 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
186188
187189 cls .range = try_enum (UnitStat , troop_meta .get ("AttackRange" ))
188190 cls .dps = try_enum (UnitStat , troop_meta .get ("DPS" ))
189- cls .ground_target = _get_maybe_first (troop_meta , "GroundTargets" , default = True )
191+ cls .ground_target = _get_maybe_first (troop_meta , "GroundTargets" ,
192+ default = True )
190193 cls .hitpoints = try_enum (UnitStat , troop_meta .get ("Hitpoints" ))
191194
192195 # get production building
@@ -201,32 +204,58 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
201204 cls .is_elixir_spell = True
202205 elif production_building == "Mini Spell Factory" :
203206 cls .is_dark_spell = True
207+ elif name in PETS_ORDER :
208+ production_building = "Pet Shop"
204209
205210 # load buildings
206211 with open (BUILDING_FILE_PATH ) as fp :
207212 buildings = ujson .load (fp )
208213
209- # without production_building, it is a hero or pet
214+ # without production_building, it is a hero
210215 if not production_building :
211216 laboratory_levels = troop_meta .get ("LaboratoryLevel" )
212217 else :
213- # it is a troop or spell
218+ # it is a troop or spell or siege
214219 prod_unit = buildings .get (production_building )
215- min_prod_unit_level = troop_meta .get ("BarrackLevel" , [None , ])[0 ]
216- # there are some special troops, which have no BarrackLevel attribute
217- if not min_prod_unit_level :
218- laboratory_levels = troop_meta .get ("LaboratoryLevel" )
219- else :
220+ if production_building in ("SiegeWorkshop" , "Spell Forge" , "Mini Spell Factory" ,
221+ "Dark Elixir Barrack" , "Barrack" , "Barrack2" ):
222+ min_prod_unit_level = troop_meta .get ("BarrackLevel" , [None , ])[0 ]
223+ # there are some special troops, which have no BarrackLevel attribute
224+ if not min_prod_unit_level :
225+ laboratory_levels = troop_meta .get ("LaboratoryLevel" )
226+ else :
227+ # get the min th level were we can unlock by the required level of the production building
228+ min_th_level = [th for i , th in
229+ enumerate (prod_unit ["TownHallLevel" ], start = 1 )
230+ if i == min_prod_unit_level ]
231+ # map the min th level to a lab level
232+ [first_lab_level ] = [lab_level for lab_level , th_level in
233+ lab_to_townhall .items ()
234+ if th_level in min_th_level ]
235+ # the first_lab_level is the lowest possible (there are some inconsistencies with siege machines)
236+ # To handle them properly, replacing all lab_level lower than first_lab_level with first_lab_level
237+ laboratory_levels = []
238+ for lab_level in troop_meta .get ("LaboratoryLevel" ):
239+ laboratory_levels .append (max (lab_level , first_lab_level ))
240+ elif production_building == "Pet Shop" :
241+ min_prod_unit_level = troop_meta .get ("LaboratoryLevel" , [None , ])[0 ]
242+ # there are some special troops, which have no BarrackLevel attribute
243+
220244 # get the min th level were we can unlock by the required level of the production building
221- min_th_level = [th for i , th in enumerate (prod_unit ["TownHallLevel" ], start = 1 )
245+ min_th_level = [th for i , th in
246+ enumerate (prod_unit ["TownHallLevel" ], start = 1 )
222247 if i == min_prod_unit_level ]
223248 # map the min th level to a lab level
224- [first_lab_level ] = [lab_level for lab_level , th_level in lab_to_townhall .items ()
249+ [first_lab_level ] = [lab_level for lab_level , th_level in
250+ lab_to_townhall .items ()
225251 if th_level in min_th_level ]
226252 # the first_lab_level is the lowest possible (there are some inconsistencies with siege machines)
227253 # To handle them properly, replacing all lab_level lower than first_lab_level with first_lab_level
228- laboratory_levels = [x if x > first_lab_level else first_lab_level
229- for x in troop_meta .get ("LaboratoryLevel" )]
254+ laboratory_levels = []
255+ for lab_level in troop_meta .get ("LaboratoryLevel" ):
256+ laboratory_levels .append (max (lab_level , first_lab_level ))
257+ else :
258+ return
230259
231260 cls .lab_level = try_enum (UnitStat , laboratory_levels )
232261 cls .housing_space = _get_maybe_first (troop_meta , "HousingSpace" , default = 0 )
@@ -236,7 +265,9 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
236265 # all 3
237266 cls .upgrade_cost = try_enum (UnitStat , troop_meta .get ("UpgradeCost" ))
238267 cls .upgrade_resource = Resource (value = troop_meta ["UpgradeResource" ][0 ])
239- cls .upgrade_time = try_enum (UnitStat , [TimeDelta (hours = hours ) for hours in troop_meta .get ("UpgradeTimeH" , [])])
268+ cls .upgrade_time = try_enum (UnitStat ,
269+ [TimeDelta (hours = hours ) for hours in
270+ troop_meta .get ("UpgradeTimeH" , [])])
240271 cls ._is_home_village = False if troop_meta .get ("VillageType" ) else True
241272 cls .village = "home" if cls ._is_home_village else "builderBase"
242273
@@ -247,8 +278,10 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
247278 # only heroes
248279 cls .ability_time = try_enum (UnitStat , troop_meta .get ("AbilityTime" ))
249280 cls .ability_troop_count = try_enum (UnitStat , troop_meta .get ("AbilitySummonTroopCount" ))
250- cls .required_th_level = try_enum (UnitStat , troop_meta .get ("RequiredTownHallLevel" ))
251- cls .regeneration_time = try_enum (UnitStat , [TimeDelta (minutes = value ) for value in troop_meta .get ("RegenerationTimeMinutes" , [])])
281+ cls .required_th_level = try_enum (UnitStat , troop_meta .get ("RequiredTownHallLevel" ) or laboratory_levels )
282+ cls .regeneration_time = try_enum (
283+ UnitStat , [TimeDelta (minutes = value ) for value in troop_meta .get ("RegenerationTimeMinutes" , [])]
284+ )
252285
253286 cls .is_loaded = True
254287 return cls
@@ -297,7 +330,7 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
297330 with open (self .FILE_PATH ) as fp :
298331 data = ujson .load (fp )
299332
300- for supercell_name , meta in data .items ():
333+ for c , [ supercell_name , meta ] in enumerate ( data .items () ):
301334 # Not interested if it doesn't have a TID, since it likely isn't a real troop.
302335 if not meta .get ("TID" ):
303336 continue
@@ -307,14 +340,17 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
307340 continue
308341
309342 # SC game files have "DisableProduction" true for all pet objects, which we want
310- if "DisableProduction" in meta and "pets" not in str (self .FILE_PATH ):
343+ if "DisableProduction" in meta and "pets" not in str (
344+ self .FILE_PATH ):
311345 continue
312346
313- # Little bit of a hacky way to create a "copy" of a new Troop object that hasn't been initiated yet.
314- new_item = type (self .data_object .__name__ , self .data_object .__bases__ , dict (self .data_object .__dict__ ))
347+ # A bit of a hacky way to create a "copy" of a new Troop object that hasn't been initiated yet.
348+ new_item = type (self .data_object .__name__ ,
349+ self .data_object .__bases__ ,
350+ dict (self .data_object .__dict__ ))
315351 new_item ._load_json_meta (
316352 meta ,
317- id = object_ids .get (supercell_name , 0 ),
353+ id = object_ids .get (supercell_name , c ),
318354 name = english_aliases [meta ["TID" ][0 ]][0 ],
319355 lab_to_townhall = lab_to_townhall ,
320356 )
@@ -325,8 +361,8 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
325361 self .loaded = True
326362
327363 def load (
328- self , data , townhall : int , default : Type [DataContainer ] = None , load_game_data : bool = True
329- ) -> DataContainer :
364+ self , data : dict , townhall : int , default : Type [DataContainer ] = None ,
365+ load_game_data : bool = True ) -> DataContainer :
330366 if load_game_data is True :
331367 try :
332368 item = self .item_lookup [data ["name" ]]
0 commit comments