avrae-ls 0.3.0__py3-none-any.whl → 0.4.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/argparser.py CHANGED
@@ -2,7 +2,7 @@ import collections
2
2
  import itertools
3
3
  import re
4
4
  import string
5
- from typing import Iterator
5
+ from typing import ClassVar, Iterator
6
6
 
7
7
 
8
8
  class BadArgument(Exception):
@@ -169,6 +169,19 @@ def argparse(args, character=None, splitter=argsplit, parse_ephem=True) -> "Pars
169
169
 
170
170
 
171
171
  class ParsedArguments:
172
+ ATTRS: ClassVar[list[str]] = []
173
+ METHODS: ClassVar[list[str]] = [
174
+ "get",
175
+ "last",
176
+ "adv",
177
+ "join",
178
+ "ignore",
179
+ "update",
180
+ "update_nx",
181
+ "set_context",
182
+ "add_context",
183
+ ]
184
+
172
185
  def __init__(self, args: list[Argument]):
173
186
  self._parsed = collections.defaultdict(lambda: [])
174
187
  for arg in args:
@@ -299,7 +312,7 @@ class ParsedArguments:
299
312
  def __contains__(self, item):
300
313
  return item in self._parsed and self._parsed[item]
301
314
 
302
- def __len__(self):
315
+ def __len__(self) -> int:
303
316
  return len(self._parsed)
304
317
 
305
318
  def __setitem__(self, key, value):
@@ -317,7 +330,7 @@ class ParsedArguments:
317
330
  if arg in self._parsed:
318
331
  del self._parsed[arg]
319
332
 
320
- def __iter__(self):
333
+ def __iter__(self) -> Iterator[str]:
321
334
  return iter(self._parsed.keys())
322
335
 
323
336
  def __repr__(self):
avrae_ls/completions.py CHANGED
@@ -6,11 +6,12 @@ import re
6
6
  import typing
7
7
  from dataclasses import dataclass
8
8
  from functools import lru_cache
9
- from typing import Any, Dict, Iterable, List, Optional
9
+ from typing import Any, ClassVar, Dict, Iterable, List, Optional
10
10
 
11
11
  from lsprotocol import types
12
12
 
13
13
  from .context import ContextData, GVarResolver
14
+ from .argparser import ParsedArguments
14
15
  from .runtime import _default_builtins
15
16
  from .api import (
16
17
  AliasAction,
@@ -42,6 +43,76 @@ from .api import (
42
43
  )
43
44
  from .signature_help import FunctionSig
44
45
 
46
+
47
+ class _BuiltinList:
48
+ ATTRS: ClassVar[list[str]] = []
49
+ METHODS: ClassVar[list[str]] = ["append", "extend", "insert", "remove", "pop", "clear", "index", "count", "sort", "reverse", "copy"]
50
+
51
+ def __iter__(self) -> Iterable[Any]:
52
+ return iter([])
53
+
54
+ def append(self, value: Any) -> None: ...
55
+ def extend(self, iterable: Iterable[Any]) -> None: ...
56
+ def insert(self, index: int, value: Any) -> None: ...
57
+ def remove(self, value: Any) -> None: ...
58
+ def pop(self, index: int = -1) -> Any: ...
59
+ def clear(self) -> None: ...
60
+ def index(self, value: Any, start: int = 0, stop: int | None = None) -> int: ...
61
+ def count(self, value: Any) -> int: ...
62
+ def sort(self, *, key=None, reverse: bool = False) -> None: ...
63
+ def reverse(self) -> None: ...
64
+ def copy(self) -> list[Any]: ...
65
+
66
+
67
+ class _BuiltinDict:
68
+ ATTRS: ClassVar[list[str]] = []
69
+ METHODS: ClassVar[list[str]] = ["get", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "clear", "copy"]
70
+
71
+ def __iter__(self) -> Iterable[Any]:
72
+ return iter({})
73
+
74
+ def get(self, key: Any, default: Any = None) -> Any: ...
75
+ def keys(self) -> Any: ...
76
+ def values(self) -> Any: ...
77
+ def items(self) -> Any: ...
78
+ def pop(self, key: Any, default: Any = None) -> Any: ...
79
+ def popitem(self) -> tuple[Any, Any]: ...
80
+ def update(self, *args, **kwargs) -> None: ...
81
+ def setdefault(self, key: Any, default: Any = None) -> Any: ...
82
+ def clear(self) -> None: ...
83
+ def copy(self) -> dict[Any, Any]: ...
84
+
85
+
86
+ class _BuiltinStr:
87
+ ATTRS: ClassVar[list[str]] = []
88
+ METHODS: ClassVar[list[str]] = [
89
+ "lower",
90
+ "upper",
91
+ "title",
92
+ "split",
93
+ "join",
94
+ "replace",
95
+ "strip",
96
+ "startswith",
97
+ "endswith",
98
+ "format",
99
+ ]
100
+
101
+ def __iter__(self) -> Iterable[str]:
102
+ return iter("")
103
+
104
+ def lower(self) -> str: ...
105
+ def upper(self) -> str: ...
106
+ def title(self) -> str: ...
107
+ def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...
108
+ def join(self, iterable: Iterable[str]) -> str: ...
109
+ def replace(self, old: str, new: str, count: int = -1) -> str: ...
110
+ def strip(self, chars: str | None = None) -> str: ...
111
+ def startswith(self, prefix, start: int = 0, end: int | None = None) -> bool: ...
112
+ def endswith(self, suffix, start: int = 0, end: int | None = None) -> bool: ...
113
+ def format(self, *args, **kwargs) -> str: ...
114
+
115
+
45
116
  TYPE_MAP: Dict[str, object] = {
46
117
  "character": CharacterAPI,
47
118
  "combat": SimpleCombat,
@@ -74,6 +145,10 @@ TYPE_MAP: Dict[str, object] = {
74
145
  "SimpleGroup": SimpleGroup,
75
146
  "effect": SimpleEffect,
76
147
  "SimpleEffect": SimpleEffect,
148
+ "list": _BuiltinList,
149
+ "dict": _BuiltinDict,
150
+ "str": _BuiltinStr,
151
+ "ParsedArguments": ParsedArguments,
77
152
  }
78
153
 
79
154
 
@@ -108,6 +183,7 @@ class MethodMeta:
108
183
  class TypeMeta:
109
184
  attrs: Dict[str, AttrMeta]
110
185
  methods: Dict[str, MethodMeta]
186
+ element_type: str = ""
111
187
 
112
188
 
113
189
  def gather_suggestions(
@@ -304,9 +380,16 @@ def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture
304
380
  dot = line_text.rfind(".")
305
381
  if dot == -1:
306
382
  return None
307
- prefix = line_text[dot + 1 :].strip()
383
+ tail = line_text[dot + 1 :]
384
+ prefix_match = re.match(r"\s*([A-Za-z_]\w*)?", tail)
385
+ prefix = prefix_match.group(1) or "" if prefix_match else ""
386
+ suffix = tail[prefix_match.end() if prefix_match else 0 :]
308
387
  placeholder = "__COMPLETE__"
309
- new_line = f"{line_text[:dot]}.{placeholder}"
388
+ new_line = f"{line_text[:dot]}.{placeholder}{suffix}"
389
+ # Close unmatched parentheses so the temporary code parses.
390
+ paren_balance = new_line.count("(") - new_line.count(")")
391
+ if paren_balance > 0:
392
+ new_line = new_line + (")" * paren_balance)
310
393
  mod_lines = list(lines)
311
394
  mod_lines[line] = new_line
312
395
  mod_code = "\n".join(mod_lines)
@@ -330,17 +413,29 @@ def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture
330
413
  Finder().visit(tree)
331
414
  if receiver_src is None:
332
415
  return None
333
- cleaned = re.sub(r"\[[^\]]*\]", "", receiver_src)
334
- return cleaned, prefix
416
+ return receiver_src, prefix
335
417
 
336
418
 
337
419
  def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
338
420
  lines = code.splitlines()
339
421
  if 0 <= line < len(lines):
340
- prefix = lines[line][:character].rstrip()
341
- if prefix.endswith("."):
342
- prefix = prefix[:-1]
422
+ prefix = lines[line][:character]
423
+ trimmed = prefix.rstrip()
424
+ if trimmed.endswith("."):
425
+ prefix = trimmed[:-1]
426
+ else:
427
+ dot = prefix.rfind(".")
428
+ if dot != -1:
429
+ after = prefix[dot + 1 :]
430
+ if not re.match(r"\s*[A-Za-z_]", after):
431
+ prefix = prefix[:dot] + after
343
432
  lines[line] = prefix
433
+ candidate = "\n".join(lines)
434
+ try:
435
+ ast.parse(candidate)
436
+ except SyntaxError:
437
+ indent = re.match(r"[ \t]*", lines[line]).group(0)
438
+ lines[line] = indent + "pass"
344
439
  return "\n".join(lines)
345
440
 
346
441
 
@@ -370,54 +465,91 @@ def _infer_type_map(code: str) -> Dict[str, str]:
370
465
 
371
466
  class Visitor(ast.NodeVisitor):
372
467
  def visit_Assign(self, node: ast.Assign):
373
- val_type = self._value_type(node.value)
468
+ val_type, elem_type = self._value_type(node.value)
374
469
  for target in node.targets:
375
470
  if not isinstance(target, ast.Name):
376
471
  continue
377
472
  if val_type:
378
473
  type_map[target.id] = val_type
474
+ if elem_type:
475
+ type_map[f"{target.id}.__element__"] = elem_type
379
476
  self._record_dict_key_types(target.id, node.value)
380
477
  self.generic_visit(node)
381
478
 
479
+ def visit_For(self, node: ast.For):
480
+ iter_type, elem_type = self._value_type(node.iter)
481
+ if not elem_type and isinstance(node.iter, ast.Name):
482
+ elem_type = type_map.get(f"{node.iter.id}.__element__")
483
+ if elem_type and isinstance(node.target, ast.Name):
484
+ type_map[node.target.id] = elem_type
485
+ self.generic_visit(node)
486
+
382
487
  def visit_AnnAssign(self, node: ast.AnnAssign):
383
- val_type = self._value_type(node.value) if node.value else None
488
+ val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
384
489
  if isinstance(node.target, ast.Name):
385
490
  if val_type:
386
491
  type_map[node.target.id] = val_type
492
+ if elem_type:
493
+ type_map[f"{node.target.id}.__element__"] = elem_type
387
494
  self._record_dict_key_types(node.target.id, node.value)
388
495
  self.generic_visit(node)
389
496
 
390
- def _value_type(self, value: ast.AST | None) -> Optional[str]:
497
+ def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
391
498
  if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
392
499
  if value.func.id in {"character", "combat"}:
393
- return value.func.id
500
+ return value.func.id, None
394
501
  if value.func.id == "vroll":
395
- return "SimpleRollResult"
502
+ return "SimpleRollResult", None
503
+ if value.func.id == "argparse":
504
+ return "ParsedArguments", None
505
+ if value.func.id in {"list", "dict", "str"}:
506
+ return value.func.id, None
507
+ if isinstance(value, ast.List):
508
+ return "list", None
509
+ if isinstance(value, ast.Dict):
510
+ return "dict", None
511
+ if isinstance(value, ast.Constant):
512
+ if isinstance(value.value, str):
513
+ return "str", None
396
514
  if isinstance(value, ast.Name):
397
515
  if value.id in type_map:
398
- return type_map[value.id]
516
+ return type_map[value.id], type_map.get(f"{value.id}.__element__")
399
517
  if value.id in {"character", "combat", "ctx"}:
400
- return value.id
518
+ return value.id, None
401
519
  if isinstance(value, ast.Attribute):
402
520
  attr_name = value.attr
403
521
  base_type = None
522
+ base_elem = None
404
523
  if isinstance(value.value, ast.Name):
405
524
  base_type = type_map.get(value.value.id)
525
+ base_elem = type_map.get(f"{value.value.id}.__element__")
406
526
  if base_type is None:
407
- base_type = self._value_type(value.value)
408
- if base_type and attr_name in TYPE_MAP:
409
- return attr_name
410
- return None
411
- return None
527
+ base_type, base_elem = self._value_type(value.value)
528
+ if base_type:
529
+ meta = _type_meta(base_type)
530
+ attr_meta = meta.attrs.get(attr_name)
531
+ if attr_meta:
532
+ if attr_meta.type_name:
533
+ return attr_meta.type_name, attr_meta.element_type or None
534
+ if attr_meta.element_type:
535
+ return base_type, attr_meta.element_type
536
+ if base_elem:
537
+ return base_elem, None
538
+ if attr_name in TYPE_MAP:
539
+ return attr_name, None
540
+ return None, None
541
+ return None, None
412
542
 
413
543
  def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
414
544
  if not isinstance(value, ast.Dict):
415
545
  return
416
546
  for key_node, val_node in zip(value.keys or [], value.values or []):
417
547
  if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
418
- val_type = self._value_type(val_node)
548
+ val_type, elem_type = self._value_type(val_node)
419
549
  if val_type:
420
550
  type_map[f"{var_name}.{key_node.value}"] = val_type
551
+ if elem_type:
552
+ type_map[f"{var_name}.{key_node.value}.__element__"] = elem_type
421
553
 
422
554
  Visitor().visit(tree)
423
555
  return type_map
@@ -431,13 +563,26 @@ def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None
431
563
  dict_key = f"{base}.{key}"
432
564
  if dict_key in mapping:
433
565
  return mapping[dict_key]
566
+ bracket = receiver.rfind("[")
567
+ if bracket != -1 and receiver.endswith("]"):
568
+ base_expr = receiver[:bracket]
569
+ elem_hint = mapping.get(f"{base_expr}.__element__")
570
+ if elem_hint:
571
+ return elem_hint
572
+ base_type = _resolve_type_name(base_expr, code, mapping)
573
+ if base_type:
574
+ base_meta = _type_meta(base_type)
575
+ if base_meta.element_type:
576
+ return base_meta.element_type
577
+ return base_type
434
578
  receiver = receiver.rstrip("()")
435
579
  if "." in receiver:
436
580
  base_expr, attr_name = receiver.rsplit(".", 1)
437
581
  base_type = _resolve_type_name(base_expr, code, mapping)
438
582
  if base_type:
439
583
  meta = _type_meta(base_type)
440
- attr_meta = meta.attrs.get(attr_name)
584
+ attr_key = attr_name.split("[", 1)[0]
585
+ attr_meta = meta.attrs.get(attr_key)
441
586
  if attr_meta:
442
587
  if attr_meta.element_type:
443
588
  return attr_meta.element_type
@@ -446,16 +591,19 @@ def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None
446
591
 
447
592
  if receiver in mapping:
448
593
  return mapping[receiver]
594
+ elem_key = f"{receiver}.__element__"
595
+ if elem_key in mapping:
596
+ return mapping[elem_key]
449
597
  if receiver in TYPE_MAP:
450
598
  return receiver
451
- tail = receiver.split(".")[-1]
599
+ tail = receiver.split(".")[-1].split("[", 1)[0]
452
600
  if tail in TYPE_MAP:
453
601
  return tail
454
602
  return receiver
455
603
 
456
604
 
457
605
  def _type_meta(type_name: str) -> TypeMeta:
458
- return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}))
606
+ return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
459
607
 
460
608
 
461
609
  @lru_cache()
@@ -474,6 +622,7 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
474
622
  for type_name, cls in TYPE_MAP.items():
475
623
  attrs: dict[str, AttrMeta] = {}
476
624
  methods: dict[str, MethodMeta] = {}
625
+ iterable_element = _iter_element_for_type_name(type_name)
477
626
 
478
627
  for attr in getattr(cls, "ATTRS", []):
479
628
  doc = ""
@@ -508,7 +657,7 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
508
657
  doc = (meth_obj.__doc__ or "").strip()
509
658
  methods[meth] = MethodMeta(signature=sig_label, doc=doc)
510
659
 
511
- meta[type_name] = TypeMeta(attrs=attrs, methods=methods)
660
+ meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=iterable_element)
512
661
  return meta
513
662
 
514
663
 
@@ -571,8 +720,9 @@ def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) ->
571
720
  if args:
572
721
  elem = args[0]
573
722
  elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
574
- return "", elem_name
575
- return "", ""
723
+ container_name = reverse_type_map.get(origin) or "list"
724
+ return container_name, elem_name
725
+ return reverse_type_map.get(origin) or "list", ""
576
726
 
577
727
  if isinstance(ann, type) and ann in reverse_type_map:
578
728
  return reverse_type_map[ann], ""
avrae_ls/config.py CHANGED
@@ -3,12 +3,15 @@ from __future__ import annotations
3
3
  import json
4
4
  import logging
5
5
  import math
6
+ import os
7
+ import re
6
8
  from dataclasses import dataclass, field
7
9
  from pathlib import Path
8
- from typing import Any, Dict, Iterable, Tuple
10
+ from typing import Any, Dict, Iterable, Mapping, Tuple
9
11
 
10
12
  CONFIG_FILENAME = ".avraels.json"
11
13
  log = logging.getLogger(__name__)
14
+ _ENV_VAR_PATTERN = re.compile(r"\$(\w+)|\$\{([^}]+)\}")
12
15
 
13
16
 
14
17
  class ConfigError(Exception):
@@ -25,6 +28,8 @@ class DiagnosticSettings:
25
28
  class AvraeServiceConfig:
26
29
  base_url: str = "https://api.avrae.io"
27
30
  token: str | None = None
31
+ verify_timeout: float = 5.0
32
+ verify_retries: int = 0
28
33
 
29
34
 
30
35
  @dataclass
@@ -360,6 +365,30 @@ class AvraeLSConfig:
360
365
  )
361
366
 
362
367
 
368
+ def _expand_env_vars(data: Any, env: Mapping[str, str], missing_vars: set[str]) -> Any:
369
+ if isinstance(data, dict):
370
+ return {key: _expand_env_vars(value, env, missing_vars) for key, value in data.items()}
371
+ if isinstance(data, list):
372
+ return [_expand_env_vars(value, env, missing_vars) for value in data]
373
+ if isinstance(data, str):
374
+ def _replace(match: re.Match[str]) -> str:
375
+ var = match.group(1) or match.group(2) or ""
376
+ if var in env:
377
+ return env[var]
378
+ missing_vars.add(var)
379
+ return ""
380
+
381
+ return _ENV_VAR_PATTERN.sub(_replace, data)
382
+ return data
383
+
384
+
385
+ def _coerce_optional_str(value: Any) -> str | None:
386
+ if value is None:
387
+ return None
388
+ value_str = str(value)
389
+ return value_str if value_str.strip() else None
390
+
391
+
363
392
  def load_config(workspace_root: Path) -> Tuple[AvraeLSConfig, Iterable[str]]:
364
393
  """Load `.avraels.json` from the workspace root, returning config and warnings."""
365
394
  path = workspace_root / CONFIG_FILENAME
@@ -374,12 +403,42 @@ def load_config(workspace_root: Path) -> Tuple[AvraeLSConfig, Iterable[str]]:
374
403
  return AvraeLSConfig.default(workspace_root), [warning]
375
404
 
376
405
  warnings: list[str] = []
406
+ env_missing: set[str] = set()
407
+ env = dict(os.environ)
408
+ env.setdefault("workspaceRoot", str(workspace_root))
409
+ env.setdefault("workspaceFolder", str(workspace_root))
410
+ raw = _expand_env_vars(raw, env, env_missing)
411
+ for var in sorted(env_missing):
412
+ warning = f"{CONFIG_FILENAME}: environment variable '{var}' is not set; substituting an empty string."
413
+ warnings.append(warning)
414
+ log.warning(warning)
415
+
377
416
  enable_gvar_fetch = bool(raw.get("enableGvarFetch", False))
378
417
 
379
418
  service_cfg = raw.get("avraeService") or {}
419
+ def _get_service_timeout(raw_timeout) -> float:
420
+ try:
421
+ timeout = float(raw_timeout)
422
+ if timeout > 0:
423
+ return timeout
424
+ except Exception:
425
+ pass
426
+ return AvraeServiceConfig.verify_timeout
427
+
428
+ def _get_service_retries(raw_retries) -> int:
429
+ try:
430
+ retries = int(raw_retries)
431
+ if retries >= 0:
432
+ return retries
433
+ except Exception:
434
+ pass
435
+ return AvraeServiceConfig.verify_retries
436
+
380
437
  service = AvraeServiceConfig(
381
438
  base_url=str(service_cfg.get("baseUrl") or AvraeServiceConfig.base_url),
382
- token=service_cfg.get("token"),
439
+ token=_coerce_optional_str(service_cfg.get("token")),
440
+ verify_timeout=_get_service_timeout(service_cfg.get("verifySignatureTimeout")),
441
+ verify_retries=_get_service_retries(service_cfg.get("verifySignatureRetries")),
383
442
  )
384
443
 
385
444
  diag_cfg = raw.get("diagnostics") or {}
avrae_ls/context.py CHANGED
@@ -33,11 +33,12 @@ class ContextBuilder:
33
33
 
34
34
  def build(self, profile_name: str | None = None) -> ContextData:
35
35
  profile = self._select_profile(profile_name)
36
+ combat = self._ensure_me_combatant(profile)
36
37
  merged_vars = self._merge_character_cvars(profile.character, self._load_var_files().merge(profile.vars))
37
38
  self._gvar_resolver.seed(merged_vars.gvars)
38
39
  return ContextData(
39
40
  ctx=dict(profile.ctx),
40
- combat=dict(profile.combat),
41
+ combat=combat,
41
42
  character=dict(profile.character),
42
43
  vars=merged_vars,
43
44
  )
@@ -69,6 +70,66 @@ class ContextBuilder:
69
70
  merged = merged.merge(VarSources(cvars=builtin_cvars))
70
71
  return merged
71
72
 
73
+ def _ensure_me_combatant(self, profile: ContextProfile) -> Dict[str, Any]:
74
+ combat = dict(profile.combat or {})
75
+ combatants = list(combat.get("combatants") or [])
76
+ me = combat.get("me")
77
+ author_id = (profile.ctx.get("author") or {}).get("id")
78
+
79
+ def _matches_author(combatant: Dict[str, Any]) -> bool:
80
+ try:
81
+ return author_id is not None and str(combatant.get("controller")) == str(author_id)
82
+ except Exception:
83
+ return False
84
+
85
+ # Use an existing combatant controlled by the author if me is missing.
86
+ if me is None:
87
+ for existing in combatants:
88
+ if _matches_author(existing):
89
+ me = existing
90
+ break
91
+
92
+ # If still missing, synthesize a combatant from the character sheet.
93
+ if me is None and profile.character:
94
+ me = {
95
+ "name": profile.character.get("name", "Player"),
96
+ "id": "cmb_player",
97
+ "controller": author_id,
98
+ "group": None,
99
+ "race": profile.character.get("race"),
100
+ "monster_name": None,
101
+ "is_hidden": False,
102
+ "init": profile.character.get("stats", {}).get("dexterity", 10),
103
+ "initmod": 0,
104
+ "type": "combatant",
105
+ "note": "Mock combatant for preview",
106
+ "effects": [],
107
+ "stats": profile.character.get("stats") or {},
108
+ "levels": profile.character.get("levels") or profile.character.get("class_levels") or {},
109
+ "skills": profile.character.get("skills") or {},
110
+ "saves": profile.character.get("saves") or {},
111
+ "resistances": profile.character.get("resistances") or {},
112
+ "spellbook": profile.character.get("spellbook") or {},
113
+ "attacks": profile.character.get("attacks") or [],
114
+ "max_hp": profile.character.get("max_hp"),
115
+ "hp": profile.character.get("hp"),
116
+ "temp_hp": profile.character.get("temp_hp"),
117
+ "ac": profile.character.get("ac"),
118
+ "creature_type": profile.character.get("creature_type"),
119
+ }
120
+
121
+ if me is not None:
122
+ combat["me"] = me
123
+ if not any(c is me for c in combatants) and not any(_matches_author(c) for c in combatants):
124
+ combatants.insert(0, me)
125
+ combat["combatants"] = combatants
126
+ if "current" not in combat or combat.get("current") is None:
127
+ combat["current"] = me
128
+ else:
129
+ combat["combatants"] = combatants
130
+
131
+ return combat
132
+
72
133
 
73
134
  class GVarResolver:
74
135
  def __init__(self, config: AvraeLSConfig):