1212from .utils import parse_tibia_datetime
1313
1414deleted_regexp = re .compile (r'([^,]+), will be deleted at (.*)' )
15- death_regexp = re .compile (r'Level (\d+) by ([^.]+)' )
15+ # Extracts the death's level and killers.
16+ death_regexp = re .compile (r'Level (?P<level>\d+) by (?P<killers>.*)\.</td>' )
17+ # From the killers list, filters out the assists.
18+ death_assisted = re .compile (r'(?P<killers>.+)\.<br/>Assisted by (?P<assists>.+)' )
19+ # From a killer entry, extracts the summoned creature
20+ death_summon = re .compile (r'(?P<summon>.+) of <a[^>]+>(?P<name>[^<]+)</a>' )
21+ # Extracts the contents of a tag
22+ link_content = re .compile (r'>([^<]+)<' )
23+
1624house_regexp = re .compile (r'paid until (.*)' )
1725guild_regexp = re .compile (r'([\s\w]+)\sof the\s(.+)' )
1826
@@ -271,26 +279,41 @@ def _parse_deaths(char, rows):
271279 """
272280 deaths = []
273281 for row in rows :
274- cols_raw = row .find_all ('td' )
275- cols = [ele .text .strip () for ele in cols_raw ]
276- if len (cols ) != 2 :
277- continue
278- death_time , death = cols
282+ cols = row .find_all ('td' )
283+ death_time = cols [0 ].text .strip ()
284+ death = str (cols [1 ]).replace ("\xa0 " , " " )
279285 death_time = death_time .replace ("\xa0 " , " " )
280286 death_info = death_regexp .search (death )
281287 if death_info :
282- level = death_info .group (1 )
283- killer = death_info .group (2 )
288+ level = int ( death_info .group ("level" ) )
289+ killers_str = death_info .group ("killers" )
284290 else :
285291 continue
286- death_link = cols_raw [1 ].find ('a' )
287- death_player = False
288- if death_link :
289- death_player = True
290- killer = death_link .text .strip ().replace ("\xa0 " , " " )
292+ assists = []
293+ # Check if the killers list contains assists
294+ assist_match = death_assisted .search (killers_str )
295+ if assist_match :
296+ # Filter out assists
297+ killers_str = assist_match .group ("killers" )
298+ # Split assists into a list.
299+ assists = Character ._split_list (assist_match .group ("assists" ))
300+ killers = Character ._split_list (killers_str )
301+ for (i , killer ) in enumerate (killers ):
302+ # If the killer contains a link, it is a player.
303+ if "href" in killer :
304+ killer_dict = {"name" : link_content .search (killer ).group (1 ), "player" : True }
305+ else :
306+ killer_dict = {"name" : killer , "player" : False }
307+ # Check if it contains a summon.
308+ m = death_summon .search (killer )
309+ if m :
310+ killer_dict ["summon" ] = m .group ("summon" )
311+ killers [i ] = killer_dict
312+ for (i , assist ) in enumerate (assists ):
313+ # Extract names from character links in assists list.
314+ assists [i ] = {"name" : link_content .search (assist ).group (1 ), "player" : True }
291315 try :
292- deaths .append ({'time' : death_time , 'level' : int (level ), 'killer' : killer ,
293- 'is_player' : death_player })
316+ deaths .append ({'time' : death_time , 'level' : level , 'killers' : killers , 'assists' : assists })
294317 except ValueError :
295318 # Some pvp deaths have no level, so they are raising a ValueError, they will be ignored for now.
296319 continue
@@ -353,11 +376,11 @@ def _split_list(items, separator=",", last_separator=" and "):
353376 separator: :class:`str`
354377 The separator between each item. A comma by default.
355378 last_separator: :class:`str`
356- The separator used for the last imte . ' and ' by default.
379+ The separator used for the last item . ' and ' by default.
357380
358381 Returns
359382 -------
360- List[str]
383+ List[:class:` str` ]
361384 A list containing each one of the items.
362385 """
363386 if items is None :
@@ -376,9 +399,9 @@ def parse_to_json(content, indent=None):
376399
377400 Parameters
378401 -------------
379- content: str
402+ content: :class:` str`
380403 The HTML content of the page.
381- indent: int
404+ indent: :class:` int`
382405 The number of spaces to indent the output with.
383406
384407 Returns
@@ -463,39 +486,40 @@ class Death:
463486 The name of the character this death belongs to.
464487
465488 level: :class:`int`
466- The level at which the level occurred.
489+ The level at which the death occurred.
467490
468- killer: :class:`str`
469- The main killer.
491+ killers: List[:class:`Killer`]
492+ A list of all the killers involved.
493+
494+ assists: List[:class:`Killer`]
495+ A list of characters that were involved, without dealing damage.
470496
471497 time: :class:`datetime.datetime`
472498 The time at which the death occurred.
473-
474- is_player: :class:`bool`
475- True if the killer is a player, False otherwise.
476-
477- participants: :class:`list`
478- List of all participants in the death.
479499 """
480- __slots__ = ("level" , "killer " , "time" , "is_player " , "name" , "participants " )
500+ __slots__ = ("level" , "killers " , "time" , "assists " , "name" )
481501
482- def __init__ (self , level = 0 , killer = None , time = None , is_player = False , name = None , participants = None ):
502+ def __init__ (self , name = None , level = 0 , ** kwargs ):
483503 self .name = name
484504 self .level = level
485- self .killer = killer
505+ self .killers = kwargs .get ("killers" , [])
506+ if self .killers and isinstance (self .killers [0 ], dict ):
507+ self .killers = [Killer (** k ) for k in self .killers ]
508+ self .assists = kwargs .get ("assists" , [])
509+ if self .assists and isinstance (self .assists [0 ], dict ):
510+ self .assists = [Killer (** k ) for k in self .assists ]
511+ time = kwargs .get ("time" )
486512 if isinstance (time , datetime .datetime ):
487513 self .time = time
488514 elif isinstance (time , str ):
489515 self .time = parse_tibia_datetime (time )
490516 else :
491517 self .time = None
492- self .is_player = is_player
493- self .participants = participants or []
494518
495- def __repr__ (self ) -> str :
519+ def __repr__ (self ):
496520 attributes = ""
497521 for attr in self .__slots__ :
498- if attr in ["level " , "killer " ]:
522+ if attr in ["name " , "level " ]:
499523 continue
500524 v = getattr (self , attr )
501525 if isinstance (v , int ) and v == 0 and not isinstance (v , bool ):
@@ -505,8 +529,59 @@ def __repr__(self) -> str:
505529 if v is None :
506530 continue
507531 attributes += ",%s=%r" % (attr , v )
508- return "{0.__class__.__name__}({0.level!r},{0.killer!r}{1})" .format (self , attributes )
532+ return "{0.__class__.__name__}({0.name!r},{0.level!r}{1})" .format (self , attributes )
533+
534+ @property
535+ def killer (self ):
536+ """Optional[:class:`Killer`]: The first killer in the list.
537+
538+ This is usually the killer that gave the killing blow."""
539+ return self .killers [0 ] if self .killers else None
540+
541+ @property
542+ def by_player (self ):
543+ """:class:`bool`: Whether the kill involves other characters."""
544+ return any ([k .player and self .name != k .name for k in self .killers ])
545+
546+ class Killer :
547+ """
548+ Represents a killer.
549+
550+ A killer can be:
509551
552+ a) Another character.
553+ b) A creature.
554+ c) A creature summoned by a character.
555+
556+ Attributes
557+ -----------
558+ name: :class:`str`
559+ The name of the killer.
560+ player: :class:`bool`
561+ Whether the killer is a player or not.
562+ summon: Optional[:class:`str`]
563+ The name of the summoned creature, if applicable.
564+ """
565+ __slots__ = ("name" , "player" , "summon" )
566+ def __init__ (self , name = None , player = False , summon = None ):
567+ self .name = name
568+ self .player = player
569+ self .summon = summon
570+
571+ def __repr__ (self ):
572+ attributes = ""
573+ for attr in self .__slots__ :
574+ if attr in ["name" ]:
575+ continue
576+ v = getattr (self , attr )
577+ if isinstance (v , int ) and v == 0 and not isinstance (v , bool ):
578+ continue
579+ if isinstance (v , list ) and len (v ) == 0 :
580+ continue
581+ if v is None :
582+ continue
583+ attributes += ",%s=%r" % (attr , v )
584+ return "{0.__class__.__name__}({0.name!r}{1})" .format (self , attributes )
510585
511586class OtherCharacter (abc .Character ):
512587 """
0 commit comments