avrae-ls 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
avrae_ls/api.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import math
4
4
  from dataclasses import dataclass, field
5
- from typing import Any, ClassVar, Iterable, Mapping, MutableMapping, Sequence
5
+ from typing import Any, ClassVar, Iterable, Mapping, MutableMapping, Optional, Sequence
6
6
 
7
7
  import d20
8
8
 
@@ -203,31 +203,37 @@ class AliasContextAPI(_DirMixin):
203
203
 
204
204
  @property
205
205
  def guild(self) -> GuildAPI | None:
206
+ """Guild info for the alias invocation (server context)."""
206
207
  guild_data = self.data.get("guild")
207
208
  return GuildAPI(guild_data) if guild_data is not None else None
208
209
 
209
210
  @property
210
211
  def channel(self) -> ChannelAPI | None:
212
+ """Channel where the alias was invoked."""
211
213
  channel_data = self.data.get("channel")
212
214
  return ChannelAPI(channel_data) if channel_data is not None else None
213
215
 
214
216
  @property
215
217
  def author(self) -> AuthorAPI | None:
218
+ """User who invoked the alias."""
216
219
  author_data = self.data.get("author")
217
220
  return AuthorAPI(author_data) if author_data is not None else None
218
221
 
219
222
  @property
220
223
  def prefix(self) -> str | None:
224
+ """Command prefix that triggered the alias (e.g., `!`)."""
221
225
  val = self.data.get("prefix")
222
226
  return str(val) if val is not None else None
223
227
 
224
228
  @property
225
229
  def alias(self) -> str | None:
230
+ """Alias name that was run."""
226
231
  val = self.data.get("alias")
227
232
  return str(val) if val is not None else None
228
233
 
229
234
  @property
230
235
  def message_id(self) -> int | None:
236
+ """Discord message id for the invocation."""
231
237
  raw = self.data.get("message_id")
232
238
  return int(raw) if raw is not None else None
233
239
 
@@ -928,24 +934,29 @@ class AliasAction(_DirMixin):
928
934
 
929
935
  @property
930
936
  def name(self) -> str:
937
+ """Action name."""
931
938
  return str(self.data.get("name", "Action"))
932
939
 
933
940
  @property
934
941
  def activation_type(self) -> int | None:
942
+ """Numeric activation type (matches Avrae constants)."""
935
943
  raw = self.data.get("activation_type")
936
944
  return int(raw) if raw is not None else None
937
945
 
938
946
  @property
939
947
  def activation_type_name(self) -> str | None:
948
+ """Human-readable activation type (e.g., ACTION, BONUS_ACTION)."""
940
949
  val = self.data.get("activation_type_name")
941
950
  return str(val) if val is not None else None
942
951
 
943
952
  @property
944
953
  def description(self) -> str:
954
+ """Long description of the action."""
945
955
  return str(self.data.get("description", ""))
946
956
 
947
957
  @property
948
958
  def snippet(self) -> str:
959
+ """Short snippet shown in the sheet for the action."""
949
960
  return str(self.data.get("snippet", self.description))
950
961
 
951
962
  def __str__(self) -> str:
@@ -1102,22 +1113,27 @@ class AliasStatBlock(_DirMixin):
1102
1113
 
1103
1114
  @property
1104
1115
  def name(self) -> str:
1116
+ """Character or statblock name."""
1105
1117
  return str(self.data.get("name", "Statblock"))
1106
1118
 
1107
1119
  @property
1108
1120
  def stats(self) -> AliasBaseStats:
1121
+ """Ability scores and proficiency bonus helper."""
1109
1122
  return AliasBaseStats(self.data.get("stats") or {}, prof_bonus_override=self._prof_bonus())
1110
1123
 
1111
1124
  @property
1112
1125
  def levels(self) -> AliasLevels:
1126
+ """Class levels keyed by class name."""
1113
1127
  return AliasLevels(self.data.get("levels") or self.data.get("class_levels") or {})
1114
1128
 
1115
1129
  @property
1116
1130
  def attacks(self) -> AliasAttackList:
1131
+ """Attacks available on the statblock."""
1117
1132
  return AliasAttackList(self.data.get("attacks") or [], self.data)
1118
1133
 
1119
1134
  @property
1120
1135
  def skills(self) -> AliasSkills:
1136
+ """Skill bonuses computed from abilities and prof bonus."""
1121
1137
  abilities = {
1122
1138
  "strength": self.stats.strength,
1123
1139
  "dexterity": self.stats.dexterity,
@@ -1130,6 +1146,7 @@ class AliasStatBlock(_DirMixin):
1130
1146
 
1131
1147
  @property
1132
1148
  def saves(self) -> AliasSaves:
1149
+ """Saving throw bonuses computed from abilities and prof bonus."""
1133
1150
  abilities = {
1134
1151
  "strength": self.stats.strength,
1135
1152
  "dexterity": self.stats.dexterity,
@@ -1148,41 +1165,50 @@ class AliasStatBlock(_DirMixin):
1148
1165
 
1149
1166
  @property
1150
1167
  def resistances(self) -> AliasResistances:
1168
+ """Damage resistances, immunities, and vulnerabilities."""
1151
1169
  return AliasResistances(self.data.get("resistances") or {})
1152
1170
 
1153
1171
  @property
1154
1172
  def ac(self) -> int | None:
1173
+ """Armor class."""
1155
1174
  raw = self.data.get("ac")
1156
1175
  return int(raw) if raw is not None else None
1157
1176
 
1158
1177
  @property
1159
1178
  def max_hp(self) -> int | None:
1179
+ """Maximum hit points."""
1160
1180
  raw = self.data.get("max_hp")
1161
1181
  return int(raw) if raw is not None else None
1162
1182
 
1163
1183
  @property
1164
1184
  def hp(self) -> int | None:
1185
+ """Current hit points."""
1165
1186
  raw = self.data.get("hp")
1166
1187
  return int(raw) if raw is not None else None
1167
1188
 
1168
1189
  @property
1169
1190
  def temp_hp(self) -> int:
1191
+ """Temporary hit points."""
1170
1192
  return _safe_int(self.data.get("temp_hp"), 0)
1171
1193
 
1172
1194
  @property
1173
1195
  def spellbook(self) -> AliasSpellbook:
1196
+ """Known/prepared spells grouped by level."""
1174
1197
  return AliasSpellbook(self.data.get("spellbook") or {})
1175
1198
 
1176
1199
  @property
1177
1200
  def creature_type(self) -> str | None:
1201
+ """Creature type (e.g., humanoid, undead)."""
1178
1202
  val = self.data.get("creature_type")
1179
1203
  return str(val) if val is not None else None
1180
1204
 
1181
1205
  def set_hp(self, new_hp: int) -> int:
1206
+ """Set current hit points."""
1182
1207
  self.data["hp"] = int(new_hp)
1183
1208
  return self.data["hp"]
1184
1209
 
1185
1210
  def modify_hp(self, amount: int, ignore_temp: bool = False, overflow: bool = True) -> int:
1211
+ """Adjust hit points by `amount`, respecting overflow limits when requested."""
1186
1212
  hp = self.hp or 0
1187
1213
  new_hp = hp + int(amount)
1188
1214
  if not overflow and self.max_hp is not None:
@@ -1191,15 +1217,18 @@ class AliasStatBlock(_DirMixin):
1191
1217
  return new_hp
1192
1218
 
1193
1219
  def hp_str(self) -> str:
1220
+ """String summary of HP and temp HP."""
1194
1221
  return f"{self.hp}/{self.max_hp} (+{self.temp_hp} temp)"
1195
1222
 
1196
1223
  def reset_hp(self) -> int:
1224
+ """Restore to max HP and clear temp HP."""
1197
1225
  if self.max_hp is not None:
1198
1226
  self.data["hp"] = self.max_hp
1199
1227
  self.data["temp_hp"] = 0
1200
1228
  return self.hp or 0
1201
1229
 
1202
1230
  def set_temp_hp(self, new_temp: int) -> int:
1231
+ """Set temporary hit points."""
1203
1232
  self.data["temp_hp"] = int(new_temp)
1204
1233
  return self.temp_hp
1205
1234
 
@@ -1268,77 +1297,99 @@ class CharacterAPI(AliasStatBlock):
1268
1297
 
1269
1298
  @property
1270
1299
  def actions(self) -> list[AliasAction]:
1300
+ """Actions on the character sheet (mapped from Beyond/custom actions)."""
1271
1301
  acts = self.data.get("actions") or []
1272
1302
  return [AliasAction(a, self.data) for a in acts]
1273
1303
 
1274
1304
  @property
1275
1305
  def coinpurse(self) -> AliasCoinpurse:
1306
+ """Coin totals by denomination."""
1276
1307
  return AliasCoinpurse(self.data.get("coinpurse") or {})
1277
1308
 
1278
1309
  @property
1279
1310
  def csettings(self) -> Mapping[str, Any]:
1311
+ """Character settings blob."""
1280
1312
  return self.data.get("csettings", {})
1281
1313
 
1282
1314
  @property
1283
1315
  def race(self) -> str | None:
1316
+ """Race label."""
1284
1317
  val = self.data.get("race")
1285
1318
  return str(val) if val is not None else None
1286
1319
 
1287
1320
  @property
1288
1321
  def background(self) -> str | None:
1322
+ """Background name."""
1289
1323
  val = self.data.get("background")
1290
1324
  return str(val) if val is not None else None
1291
1325
 
1292
1326
  @property
1293
1327
  def owner(self) -> int | None:
1328
+ """Discord user id of the owning account."""
1294
1329
  raw = self.data.get("owner")
1295
1330
  return int(raw) if raw is not None else None
1296
1331
 
1297
1332
  @property
1298
1333
  def upstream(self) -> str | None:
1334
+ """Upstream character id (e.g., Beyond character slug)."""
1299
1335
  val = self.data.get("upstream")
1300
1336
  return str(val) if val is not None else None
1301
1337
 
1302
1338
  @property
1303
1339
  def sheet_type(self) -> str | None:
1340
+ """Source sheet provider (beyond, custom, etc.)."""
1304
1341
  val = self.data.get("sheet_type")
1305
1342
  return str(val) if val is not None else None
1306
1343
 
1307
1344
  @property
1308
1345
  def cvars(self) -> Mapping[str, Any]:
1346
+ """Character variables (string values)."""
1309
1347
  return dict(self.data.get("cvars") or {})
1310
1348
 
1311
- def get_cvar(self, name: str, default: Any = None) -> Any:
1312
- return self.data.setdefault("cvars", {}).get(str(name), default)
1349
+ def get_cvar(self, name: str, default: Any = None) -> Optional[str]:
1350
+ """Fetch a character variable, returning `default` if missing."""
1351
+ val = self.data.setdefault("cvars", {}).get(str(name), default)
1352
+ return str(val) if val is not None else default
1313
1353
 
1314
- def set_cvar(self, name: str, val: Any) -> Any:
1315
- self.data.setdefault("cvars", {})[str(name)] = val
1316
- return val
1354
+ def set_cvar(self, name: str, val: str) -> Optional[str]:
1355
+ """Sets a character variable. Avrae stores cvars as strings."""
1356
+ str_val = str(val) if val is not None else None
1357
+ self.data.setdefault("cvars", {})[str(name)] = str_val
1358
+ return str_val
1317
1359
 
1318
- def set_cvar_nx(self, name: str, val: Any) -> Any:
1360
+ def set_cvar_nx(self, name: str, val: str) -> str:
1361
+ """Set a character variable only if it does not already exist."""
1319
1362
  cvars = self.data.setdefault("cvars", {})
1320
- return cvars.setdefault(str(name), val)
1363
+ str_val = str(val) if val is not None else None
1364
+ return cvars.setdefault(str(name), str_val)
1321
1365
 
1322
- def delete_cvar(self, name: str) -> Any:
1366
+ def delete_cvar(self, name: str) -> Optional[str]:
1367
+ """Delete a character variable and return its old value if present."""
1323
1368
  return self.data.setdefault("cvars", {}).pop(str(name), None)
1324
1369
 
1325
1370
  @property
1326
1371
  def consumables(self) -> list[AliasCustomCounter]:
1372
+ """Custom counters/consumables on the character."""
1327
1373
  return [AliasCustomCounter(v) for v in self._consumable_map().values()]
1328
1374
 
1329
1375
  def cc(self, name: str) -> AliasCustomCounter:
1376
+ """Get (or create placeholder for) a custom counter by name."""
1330
1377
  return AliasCustomCounter(self._consumable_map()[str(name)])
1331
1378
 
1332
1379
  def get_cc(self, name: str) -> int:
1380
+ """Current value of a custom counter."""
1333
1381
  return self.cc(name).value
1334
1382
 
1335
1383
  def get_cc_max(self, name: str) -> int:
1384
+ """Maximum value for a custom counter."""
1336
1385
  return self.cc(name).max
1337
1386
 
1338
1387
  def get_cc_min(self, name: str) -> int:
1388
+ """Minimum value for a custom counter."""
1339
1389
  return self.cc(name).min
1340
1390
 
1341
1391
  def set_cc(self, name: str, value: int | None = None, maximum: int | None = None, minimum: int | None = None) -> int:
1392
+ """Set value/max/min for a custom counter."""
1342
1393
  con = self._consumable_map().setdefault(str(name), {"name": str(name)})
1343
1394
  if value is not None:
1344
1395
  con["value"] = int(value)
@@ -1349,10 +1400,12 @@ class CharacterAPI(AliasStatBlock):
1349
1400
  return _safe_int(con.get("value"), 0)
1350
1401
 
1351
1402
  def mod_cc(self, name: str, val: int, strict: bool = False) -> int:
1403
+ """Modify a custom counter by `val` (optionally enforcing bounds)."""
1352
1404
  counter = self.cc(name)
1353
1405
  return counter.mod(val, strict)
1354
1406
 
1355
1407
  def delete_cc(self, name: str) -> Any:
1408
+ """Remove a custom counter and return its payload."""
1356
1409
  return self._consumable_map().pop(str(name), None)
1357
1410
 
1358
1411
  def create_cc_nx(
@@ -1368,6 +1421,7 @@ class CharacterAPI(AliasStatBlock):
1368
1421
  desc: str | None = None,
1369
1422
  initial_value: str | None = None,
1370
1423
  ) -> AliasCustomCounter:
1424
+ """Create a custom counter if missing, preserving existing ones."""
1371
1425
  if not self.cc_exists(name):
1372
1426
  self.create_cc(
1373
1427
  name,
@@ -1396,6 +1450,7 @@ class CharacterAPI(AliasStatBlock):
1396
1450
  desc: str | None = None,
1397
1451
  initial_value: str | None = None,
1398
1452
  ) -> AliasCustomCounter:
1453
+ """Create or overwrite a custom counter."""
1399
1454
  payload = {
1400
1455
  "name": str(name),
1401
1456
  "min": _safe_int(minVal, -(2**31)) if minVal is not None else -(2**31),
@@ -1424,6 +1479,7 @@ class CharacterAPI(AliasStatBlock):
1424
1479
  desc: Any = UNSET,
1425
1480
  new_name: str | None = None,
1426
1481
  ) -> AliasCustomCounter:
1482
+ """Edit fields on an existing custom counter."""
1427
1483
  counter = dict(self._consumable_map().get(str(name)) or {"name": str(name)})
1428
1484
  for key, val in (
1429
1485
  ("min", minVal),
@@ -1443,22 +1499,27 @@ class CharacterAPI(AliasStatBlock):
1443
1499
  return AliasCustomCounter(counter)
1444
1500
 
1445
1501
  def cc_exists(self, name: str) -> bool:
1502
+ """Return True if a custom counter with the name exists."""
1446
1503
  return str(name) in self._consumable_map()
1447
1504
 
1448
1505
  def cc_str(self, name: str) -> str:
1506
+ """String form of a custom counter."""
1449
1507
  return str(self.cc(name))
1450
1508
 
1451
1509
  @property
1452
1510
  def death_saves(self) -> AliasDeathSaves:
1511
+ """Death save successes/failures."""
1453
1512
  return AliasDeathSaves(self.data.get("death_saves") or {})
1454
1513
 
1455
1514
  @property
1456
1515
  def description(self) -> str | None:
1516
+ """Character description/biography."""
1457
1517
  val = self.data.get("description")
1458
1518
  return str(val) if val is not None else None
1459
1519
 
1460
1520
  @property
1461
1521
  def image(self) -> str | None:
1522
+ """Avatar or sheet image URL."""
1462
1523
  val = self.data.get("image")
1463
1524
  return str(val) if val is not None else None
1464
1525
 
@@ -1583,55 +1644,67 @@ class SimpleCombatant(AliasStatBlock):
1583
1644
 
1584
1645
  @property
1585
1646
  def id(self) -> str | None:
1647
+ """Unique combatant id."""
1586
1648
  val = self.data.get("id")
1587
1649
  return str(val) if val is not None else None
1588
1650
 
1589
1651
  @property
1590
1652
  def effects(self) -> list[SimpleEffect]:
1653
+ """Active effects on the combatant."""
1591
1654
  return [SimpleEffect(e) for e in self.data.get("effects", [])]
1592
1655
 
1593
1656
  @property
1594
1657
  def init(self) -> int:
1658
+ """Initiative score."""
1595
1659
  return _safe_int(self.data.get("init"), 0)
1596
1660
 
1597
1661
  @property
1598
1662
  def initmod(self) -> int:
1663
+ """Initiative modifier."""
1599
1664
  return _safe_int(self.data.get("initmod"), 0)
1600
1665
 
1601
1666
  @property
1602
1667
  def type(self) -> str:
1668
+ """Combatant type (combatant/group)."""
1603
1669
  return str(self.data.get("type", "combatant"))
1604
1670
 
1605
1671
  @property
1606
1672
  def note(self) -> str | None:
1673
+ """DM note attached to the combatant."""
1607
1674
  val = self.data.get("note")
1608
1675
  return str(val) if val is not None else None
1609
1676
 
1610
1677
  @property
1611
1678
  def controller(self) -> int | None:
1679
+ """Discord id of the controller (if any)."""
1612
1680
  raw = self.data.get("controller")
1613
1681
  return int(raw) if raw is not None else None
1614
1682
 
1615
1683
  @property
1616
1684
  def group(self) -> str | None:
1685
+ """Group name the combatant belongs to."""
1617
1686
  val = self.data.get("group")
1618
1687
  return str(val) if val is not None else None
1619
1688
 
1620
1689
  @property
1621
1690
  def race(self) -> str | None:
1691
+ """Race/creature type label."""
1622
1692
  val = self.data.get("race")
1623
1693
  return str(val) if val is not None else None
1624
1694
 
1625
1695
  @property
1626
1696
  def monster_name(self) -> str | None:
1697
+ """Monster name if this combatant represents a monster."""
1627
1698
  val = self.data.get("monster_name")
1628
1699
  return str(val) if val is not None else None
1629
1700
 
1630
1701
  @property
1631
1702
  def is_hidden(self) -> bool:
1703
+ """Whether the combatant is hidden in the tracker."""
1632
1704
  return bool(self.data.get("is_hidden", False))
1633
1705
 
1634
1706
  def save(self, ability: str, adv: bool | None = None) -> SimpleRollResult:
1707
+ """Roll a saving throw using the combatant's stats."""
1635
1708
  roll_expr = self.saves.get(ability).d20(base_adv=adv)
1636
1709
  try:
1637
1710
  roll_result = d20.roll(roll_expr)
@@ -1648,6 +1721,7 @@ class SimpleCombatant(AliasStatBlock):
1648
1721
  critdice: int = 0,
1649
1722
  overheal: bool = False,
1650
1723
  ) -> dict[str, Any]:
1724
+ """Apply damage expression to the combatant and return the roll breakdown."""
1651
1725
  expr = str(dice_str)
1652
1726
  if crit:
1653
1727
  expr = f"({expr})*2"
@@ -1665,25 +1739,32 @@ class SimpleCombatant(AliasStatBlock):
1665
1739
  return {"damage": f"**{label}**: {roll_result}", "total": roll_result.total, "roll": SimpleRollResult(roll_result)}
1666
1740
 
1667
1741
  def set_ac(self, ac: int) -> None:
1742
+ """Set armor class."""
1668
1743
  self.data["ac"] = int(ac)
1669
1744
 
1670
1745
  def set_maxhp(self, maxhp: int) -> None:
1746
+ """Set maximum HP."""
1671
1747
  self.data["max_hp"] = int(maxhp)
1672
1748
 
1673
1749
  def set_init(self, init: int) -> None:
1750
+ """Set initiative score."""
1674
1751
  self.data["init"] = int(init)
1675
1752
 
1676
1753
  def set_name(self, name: str) -> None:
1754
+ """Rename the combatant."""
1677
1755
  self.data["name"] = str(name)
1678
1756
 
1679
1757
  def set_group(self, group: str | None) -> str | None:
1758
+ """Assign the combatant to a group."""
1680
1759
  self.data["group"] = str(group) if group is not None else None
1681
1760
  return self.group
1682
1761
 
1683
1762
  def set_note(self, note: str) -> None:
1763
+ """Attach/update a DM note."""
1684
1764
  self.data["note"] = str(note) if note is not None else None
1685
1765
 
1686
1766
  def get_effect(self, name: str, strict: bool = False) -> SimpleEffect | None:
1767
+ """Find an effect by name (optionally requiring exact match)."""
1687
1768
  name_lower = str(name).lower()
1688
1769
  for effect in self.effects:
1689
1770
  if strict and effect.name.lower() == name_lower:
@@ -1706,6 +1787,7 @@ class SimpleCombatant(AliasStatBlock):
1706
1787
  buttons: list[dict] | None = None,
1707
1788
  tick_on_combatant_id: str | None = None,
1708
1789
  ) -> SimpleEffect:
1790
+ """Add a new effect to the combatant."""
1709
1791
  duration_val = int(duration) if duration is not None else None
1710
1792
  desc_val = str(desc) if desc is not None else None
1711
1793
  payload: dict[str, Any] = {
@@ -1739,6 +1821,7 @@ class SimpleCombatant(AliasStatBlock):
1739
1821
  return SimpleEffect(payload)
1740
1822
 
1741
1823
  def remove_effect(self, name: str, strict: bool = False) -> None:
1824
+ """Remove an effect by name."""
1742
1825
  effect = self.get_effect(name, strict)
1743
1826
  if effect:
1744
1827
  try:
@@ -1759,26 +1842,32 @@ class SimpleGroup(_DirMixin):
1759
1842
 
1760
1843
  @property
1761
1844
  def combatants(self) -> list[SimpleCombatant]:
1845
+ """Members of the group."""
1762
1846
  return [SimpleCombatant(c) for c in self.data.get("combatants", [])]
1763
1847
 
1764
1848
  @property
1765
1849
  def type(self) -> str:
1850
+ """Group type identifier (always 'group')."""
1766
1851
  return str(self.data.get("type", "group"))
1767
1852
 
1768
1853
  @property
1769
1854
  def init(self) -> int:
1855
+ """Initiative score for the group."""
1770
1856
  return _safe_int(self.data.get("init"), 0)
1771
1857
 
1772
1858
  @property
1773
1859
  def name(self) -> str:
1860
+ """Group name."""
1774
1861
  return str(self.data.get("name", "Group"))
1775
1862
 
1776
1863
  @property
1777
1864
  def id(self) -> str | None:
1865
+ """Group id."""
1778
1866
  val = self.data.get("id")
1779
1867
  return str(val) if val is not None else None
1780
1868
 
1781
1869
  def get_combatant(self, name: str, strict: bool | None = None) -> SimpleCombatant | None:
1870
+ """Find a combatant within the group."""
1782
1871
  name_lower = str(name).lower()
1783
1872
  for combatant in self.combatants:
1784
1873
  if strict is True and combatant.name.lower() == name_lower:
@@ -1808,44 +1897,53 @@ class SimpleCombat(_DirMixin):
1808
1897
 
1809
1898
  @property
1810
1899
  def combatants(self) -> list[SimpleCombatant]:
1900
+ """All combatants in the encounter."""
1811
1901
  return [SimpleCombatant(c) for c in self.data.get("combatants", [])]
1812
1902
 
1813
1903
  @property
1814
1904
  def groups(self) -> list[SimpleGroup]:
1905
+ """Combatant groups in the encounter."""
1815
1906
  return [SimpleGroup(g) for g in self.data.get("groups", [])]
1816
1907
 
1817
1908
  @property
1818
1909
  def me(self) -> SimpleCombatant | None:
1910
+ """The player's combatant if present."""
1819
1911
  me_data = self.data.get("me")
1820
1912
  return SimpleCombatant(me_data) if me_data is not None else None
1821
1913
 
1822
1914
  @property
1823
1915
  def current(self) -> SimpleCombatant | SimpleGroup | None:
1916
+ """Current turn holder (combatant or group)."""
1824
1917
  cur = self.data.get("current")
1825
1918
  if cur is None:
1826
1919
  return None
1827
1920
  if cur.get("type") == "group":
1828
1921
  return SimpleGroup(cur)
1829
- return SimpleCombatant(cur)
1922
+ return SimpleCombatant(cur)
1830
1923
 
1831
1924
  @property
1832
1925
  def name(self) -> str | None:
1926
+ """Name of the combat encounter."""
1833
1927
  val = self.data.get("name")
1834
1928
  return str(val) if val is not None else None
1835
1929
 
1836
1930
  @property
1837
1931
  def round_num(self) -> int:
1932
+ """Current round number."""
1838
1933
  return _safe_int(self.data.get("round_num"), 1)
1839
1934
 
1840
1935
  @property
1841
1936
  def turn_num(self) -> int:
1937
+ """Current turn number within the round."""
1842
1938
  return _safe_int(self.data.get("turn_num"), 1)
1843
1939
 
1844
1940
  @property
1845
1941
  def metadata(self) -> MutableMapping[str, Any]:
1942
+ """Free-form metadata key/value store for the combat."""
1846
1943
  return self.data.setdefault("metadata", {})
1847
1944
 
1848
1945
  def get_combatant(self, name: str, strict: bool | None = None) -> SimpleCombatant | None:
1946
+ """Find a combatant by name (strict, substring, or fuzzy)."""
1849
1947
  name_lower = str(name).lower()
1850
1948
  for combatant in self.combatants:
1851
1949
  if strict is True and combatant.name.lower() == name_lower:
@@ -1861,6 +1959,7 @@ class SimpleCombat(_DirMixin):
1861
1959
  return None
1862
1960
 
1863
1961
  def get_group(self, name: str, strict: bool | None = None) -> SimpleGroup | None:
1962
+ """Find a combatant group by name."""
1864
1963
  name_lower = str(name).lower()
1865
1964
  for group in self.groups:
1866
1965
  if strict is True and group.name.lower() == name_lower:
@@ -1876,6 +1975,7 @@ class SimpleCombat(_DirMixin):
1876
1975
  return None
1877
1976
 
1878
1977
  def set_metadata(self, k: str, v: str) -> None:
1978
+ """Set a metadata key/value pair, enforcing Avrae size limits."""
1879
1979
  key = str(k)
1880
1980
  value = str(v)
1881
1981
  existing = {str(ke): str(va) for ke, va in self.metadata.items() if str(ke) != key}
@@ -1888,12 +1988,15 @@ class SimpleCombat(_DirMixin):
1888
1988
  return self.metadata.get(str(k), default)
1889
1989
 
1890
1990
  def delete_metadata(self, k: str) -> Any:
1991
+ """Delete a metadata key."""
1891
1992
  return self.metadata.pop(str(k), None)
1892
1993
 
1893
1994
  def set_round(self, round_num: int) -> None:
1995
+ """Advance combat to the specified round number."""
1894
1996
  self.data["round_num"] = int(round_num)
1895
1997
 
1896
1998
  def end_round(self) -> None:
1999
+ """Increment round number and reset turn counter."""
1897
2000
  self.data["turn_num"] = 0
1898
2001
  self.data["round_num"] = self.round_num + 1
1899
2002