Dungeon loot: patterns, masterworks, relics, uniques

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-24 13:48:16 +01:00
parent 60b446acfd
commit 8afcba79fc
8 changed files with 358 additions and 18 deletions

View file

@ -145,6 +145,45 @@ def known_abilities(style: str, cls: str, level: int, shield: bool) -> list[str]
return sorted(kit, key=PRIORITY.index)
def modded(ab: dict, mod: dict | None) -> dict:
"""An ability as something you wear changes it: `cd` and `cast` multiply, `c` multiplies every
effect's size, `dur` adds seconds to every effect that lasts, `cost` replaces the arrows it takes,
and `extra` adds effects."""
if not mod:
return ab
ab = {**ab, "effects": [dict(eff) for eff in ab["effects"]]}
for key in ("cd", "cast"):
if key in mod and ab.get(key):
ab[key] = ab[key] * mod[key]
if "cost" in mod:
ab["cost"] = mod["cost"]
for eff in ab["effects"]:
if "c" in eff and "c" in mod:
eff["c"] *= mod["c"]
if "dur" in eff and "dur" in mod:
eff["dur"] += mod["dur"]
ab["effects"] += [dict(eff) for eff in mod.get("extra", [])]
return ab
def combine_mods(mods: list[dict]) -> dict:
"""Several things changing the same abilities: multipliers multiply, durations add, extras pile up."""
out = {}
for mod in mods:
for ab_id, change in mod.items():
have = out.setdefault(ab_id, {})
for key, value in change.items():
if key in ("cd", "cast", "c"):
have[key] = have.get(key, 1) * value
elif key == "dur":
have[key] = have.get(key, 0) + value
elif key == "extra":
have[key] = have.get(key, []) + value
else:
have[key] = value
return out
def role(style: str, shield: bool) -> str:
return "tank" if style == "sword" and shield else "healer" if style == "staff" else "damage"
@ -225,6 +264,7 @@ def _member_unit(member: dict) -> dict:
"damage": me["damage"], "interval": me["interval"], "accuracy": me["accuracy"], "evasion": me["evasion"],
"crit": me["crit"], "block": me["block"], "taken": me["taken"], "base": me["base"],
"abilities": list(me["abilities"]), "cds": {}, "busy": 0.0, "cast": None, "auras": [],
"kit": {a: modded(ABILITIES[a], me.get("mods", {}).get(a)) for a in me["abilities"]},
"next": member["at"] + (0.5 if me["first"] else me["interval"]), "stun": 0.0, "target": None,
"mend": member["mend"], "wind": member["wind"], "wind_used": False,
"potion": member.get("potion") or 0, "drank": False, "arrows": member.get("arrows", 0), "shot": 0,
@ -428,7 +468,7 @@ def _think(state: dict, unit: dict, t: float, rng: random.Random) -> None:
watching = t < unit["watch"]
queued = unit["queued"]
if queued:
ab = ABILITIES[queued["ab"]]
ab = unit["kit"][queued["ab"]]
why = _ready(unit, ab, t, manual=True)
if why is None:
unit["queued"] = None
@ -440,7 +480,7 @@ def _think(state: dict, unit: dict, t: float, rng: random.Random) -> None:
elif why == "arrows" or t - queued["at"] > QUEUE:
unit["queued"] = None
for ab_id in unit["abilities"]:
ab = ABILITIES[ab_id]
ab = unit["kit"][ab_id]
if (watching and ab_id in unit["manual"]) or _ready(unit, ab, t, manual=False):
continue
target = _rule(state, unit, ab, t)
@ -491,7 +531,7 @@ def _use(state: dict, unit: dict, ab: dict, target, t: float, rng: random.Random
def _finish_cast(state: dict, unit: dict, t: float, rng: random.Random) -> None:
cast, unit["cast"] = unit["cast"], None
if unit["side"] == "party":
_apply(state, unit, ABILITIES[cast["ab"]], cast["target"], t, rng)
_apply(state, unit, unit["kit"][cast["ab"]], cast["target"], t, rng)
else:
_enemy_effect(state, unit, cast["ab"], cast["target"], t, rng)
@ -536,13 +576,17 @@ def _apply(state: dict, unit: dict, ab: dict, target_id, t: float, rng: random.R
"amount": eff["c"] * _avg(unit, target), "every": eff["every"],
"next": t + eff["every"], "until": t + eff["dur"] + 1e-6})
elif kind == "heal":
_heal(state, unit, target, eff["c"] * _avg(unit) * spread(), t, ab["id"])
_heal(state, unit, _second(state, target) if eff.get("second") else target,
eff["c"] * _avg(unit) * spread(), t, ab["id"])
elif kind == "hot":
for ally in _party(state):
_add_aura(ally, {"id": ab["id"], "kind": "hot", "by": unit["id"], "amount": eff["c"] * _avg(unit),
"every": eff["every"], "next": t + eff["every"], "until": t + eff["dur"] + 1e-6})
elif kind == "shield":
_add_aura(target, {"id": ab["id"], "kind": "shield", "by": unit["id"], "amount": eff["c"] * _avg(unit),
holder = _second(state, target) if eff.get("second") else target
if not holder:
continue
_add_aura(holder, {"id": ab["id"], "kind": "shield", "by": unit["id"], "amount": eff["c"] * _avg(unit),
"until": t + eff["dur"]})
_heal_threat(state, unit, eff["c"] * _avg(unit))
elif kind == "aura":
@ -580,6 +624,12 @@ def _apply(state: dict, unit: dict, ab: dict, target_id, t: float, rng: random.R
enemy["next"] += eff["by"]
def _second(state: dict, first: dict | None) -> dict | None:
"""The most hurt of your side after `first`: for heals and wards that reach a second ally."""
others = [u for u in _party(state) if u is not first]
return min(others, key=_share) if others else None
def _stop_cast(state: dict, enemy: dict, by: dict, t: float, force: bool = False, quiet: bool = False) -> None:
cast = enemy.get("cast")
if not cast or (not force and not ENEMY_ABILITIES[cast["ab"]]["interrupt"]):