avrae-ls 0.2.1__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/__main__.py +84 -0
- avrae_ls/api.py +113 -10
- avrae_ls/completions.py +298 -36
- avrae_ls/context.py +25 -7
- avrae_ls/diagnostics.py +32 -8
- avrae_ls/runtime.py +161 -36
- avrae_ls/server.py +1 -1
- avrae_ls/signature_help.py +73 -19
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/METADATA +4 -3
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/RECORD +14 -14
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/WHEEL +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/top_level.txt +0 -0
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) ->
|
|
1312
|
-
|
|
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:
|
|
1315
|
-
|
|
1316
|
-
|
|
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:
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
|