Update Healer Man server
This commit is contained in:
1 parent
f8314e7de1
commit
1fedb9368d
76 files changed
+20556
-2523
No files matched your search
+164
-16
@@ -118,7 +118,7 @@ def build_aliases(build: dict[str, Any]) -> set[str]:
|
||||
class_name = build["className"]
|
||||
role = build["role"]
|
||||
specialization = build["specialization"]
|
||||
return {
|
||||
aliases = {
|
||||
normalized(build["id"]),
|
||||
normalized(f"{class_id}:{specialization}"),
|
||||
normalized(f"{class_name}:{specialization}"),
|
||||
@@ -126,6 +126,11 @@ def build_aliases(build: dict[str, Any]) -> set[str]:
|
||||
normalized(f"{class_name}:{role}:{specialization}"),
|
||||
normalized(build["label"]),
|
||||
}
|
||||
for value in (build.get("characterName"), build.get("talentBuildId"), build.get("talentBuildName")):
|
||||
if value:
|
||||
aliases.add(normalized(str(value)))
|
||||
aliases.add(normalized(f"{class_id}:{specialization}:{value}"))
|
||||
return aliases
|
||||
|
||||
|
||||
def resolve_build(selector: str, builds: Sequence[dict[str, Any]]) -> dict[str, Any]:
|
||||
@@ -135,6 +140,9 @@ def resolve_build(selector: str, builds: Sequence[dict[str, Any]]) -> dict[str,
|
||||
return matches[0]
|
||||
if not matches:
|
||||
raise SystemExit(f"No party member matches '{selector}'. Use --list-members to see valid choices.")
|
||||
defaults = [match for match in matches if match.get("defaultTalentBuild")]
|
||||
if len(defaults) == 1:
|
||||
return defaults[0]
|
||||
labels = ", ".join(match["label"] for match in matches)
|
||||
raise SystemExit(f"Party member selector '{selector}' is ambiguous: {labels}")
|
||||
|
||||
@@ -156,6 +164,11 @@ def resolve_boss(selector: str, bosses: Sequence[dict[str, Any]]) -> dict[str, A
|
||||
|
||||
def random_party(builds: Sequence[dict[str, Any]], seed: int) -> list[dict[str, Any]]:
|
||||
generator = random.Random(seed)
|
||||
# Named alternatives remain directly selectable and are covered by "all",
|
||||
# but ordinary random/filler seats should use the declared default profile.
|
||||
defaults = [build for build in builds if build.get("defaultTalentBuild")]
|
||||
if defaults:
|
||||
builds = defaults
|
||||
by_role: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for build in builds:
|
||||
by_role[build["role"]].append(build)
|
||||
@@ -165,6 +178,27 @@ def random_party(builds: Sequence[dict[str, Any]], seed: int) -> list[dict[str,
|
||||
return [generator.choice(by_role["tank"]), generator.choice(by_role["healer"]), *damage]
|
||||
|
||||
|
||||
def readiness_eligible_builds(
|
||||
builds: Sequence[dict[str, Any]],
|
||||
policy: dict[str, Any] | None,
|
||||
include_beta_specs: bool,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Filter only WoW NPC builds; CoA and RoM catalogs remain unchanged."""
|
||||
published = policy or {}
|
||||
config = published.get("config", published)
|
||||
statuses = config.get("statuses", {}) if isinstance(config, dict) else {}
|
||||
eligible: list[dict[str, Any]] = []
|
||||
for build in builds:
|
||||
if build.get("source") != "wow":
|
||||
eligible.append(build)
|
||||
continue
|
||||
specialization_id = build.get("specializationId")
|
||||
status = statuses.get(specialization_id, build.get("readinessStatus", "needs-testing"))
|
||||
if status == "ready" or (include_beta_specs and status == "beta"):
|
||||
eligible.append(build)
|
||||
return eligible
|
||||
|
||||
|
||||
def automatic_parties(
|
||||
builds: Sequence[dict[str, Any]],
|
||||
sources: Sequence[str],
|
||||
@@ -206,6 +240,7 @@ def coverage_parties(
|
||||
sources: Sequence[str],
|
||||
selected_build_ids: Sequence[str] | None,
|
||||
seed: int,
|
||||
filler_builds: Sequence[dict[str, Any]] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build the minimum deterministic party set that covers every requested build."""
|
||||
requested = set(selected_build_ids) if selected_build_ids is not None else None
|
||||
@@ -220,7 +255,9 @@ def coverage_parties(
|
||||
continue
|
||||
available_by_role: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
targets_by_role: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for build in source_builds:
|
||||
source_fillers = [build for build in (filler_builds or builds) if build["source"] == source]
|
||||
default_builds = [build for build in source_fillers if build.get("defaultTalentBuild")]
|
||||
for build in default_builds or source_fillers:
|
||||
available_by_role[build["role"]].append(build)
|
||||
for build in targets:
|
||||
targets_by_role[build["role"]].append(build)
|
||||
@@ -794,7 +831,8 @@ def create_reports(raw_path: Path, output_dir: Path) -> dict[str, Any]:
|
||||
"policyId", "rotationVersion", "bossKey", "bossName", "bossCategory", "dungeonId", "dungeonTitle", "seed", "outcome",
|
||||
"elapsedSeconds", "timeToDefeatSeconds", "bossLevel", "bossMaxHealth", "bossRemainingHealth",
|
||||
"partyDamage", "bossDamage", "partyDps", "bossDpsTaken", "incomingDamage", "rawIncomingDamage",
|
||||
"incomingDtps", "deaths", "deathEvents", "bossAttacks",
|
||||
"incomingDtps", "pressureWindows", "healerThroughputWindows", "healerResponseWindows",
|
||||
"healerResponseRate", "deaths", "deathEvents", "bossAttacks",
|
||||
))
|
||||
member_abilities = flattened_abilities(raw, "members")
|
||||
write_csv(output_dir / "member_abilities.csv", member_abilities, (
|
||||
@@ -822,6 +860,13 @@ def aggregate_class_specs(
|
||||
"className": class_name,
|
||||
"specialization": specialization,
|
||||
"role": role,
|
||||
"characterName": observations[0].get("characterName"),
|
||||
"talentBuildId": observations[0].get("talentBuildId"),
|
||||
"talentBuildName": observations[0].get("talentBuildName"),
|
||||
"talentBuildSource": observations[0].get("talentBuildSource"),
|
||||
"talentBuildSourcePointCount": observations[0].get("talentBuildSourcePointCount"),
|
||||
"talentBuildSourcePointsRetained": observations[0].get("talentBuildSourcePointsRetained"),
|
||||
"talentBuildFallbackPointCount": observations[0].get("talentBuildFallbackPointCount"),
|
||||
"partyConfigurations": len({row["partyConfigurationId"] for row in observations}),
|
||||
"runs": len(observations),
|
||||
"dps": weighted_rate(observations, "damage"),
|
||||
@@ -829,6 +874,10 @@ def aggregate_class_specs(
|
||||
"hps": weighted_rate(observations, "healing"),
|
||||
"hpsReceived": weighted_rate(observations, "healingReceived"),
|
||||
"dtps": weighted_rate(observations, "damageTaken"),
|
||||
"gcdUptimePercent": mean(
|
||||
float(row["gcdUptimePercent"]) for row in observations
|
||||
if row.get("gcdUptimePercent") is not None
|
||||
),
|
||||
"totalDamage": sum(row["damage"] for row in observations),
|
||||
"totalDamageTaken": sum(row["damageTaken"] for row in observations),
|
||||
"totalHealing": sum(row["healing"] for row in observations),
|
||||
@@ -878,9 +927,19 @@ def aggregate_class_abilities(
|
||||
available_runs = sum(bool(row["available"]) for row in observations)
|
||||
class_damage = sum(float(row["damage"]) for row in class_observations)
|
||||
metadata = next((row for row in observations if row["available"]), observations[0])
|
||||
rotation_decisions = sum(int(row.get("rotationDecisions", 0)) for row in observations)
|
||||
effective_healing = sum(float(row["effectiveHealing"]) for row in observations)
|
||||
absorbs = sum(float(row["absorbs"]) for row in observations)
|
||||
if active_runs == 0 and available_runs > 0:
|
||||
# A profile entry is often an alternative, execute, or lower-priority
|
||||
# fallback and is not required to appear in every short fight. Flag it
|
||||
# only when the AI repeatedly selected it without any committed cast or
|
||||
# effect; one terminal decision per run may simply be interrupted by
|
||||
# the boss dying or the duration limit.
|
||||
if (active_runs == 0 and available_runs > 0
|
||||
and rotation_decisions > available_runs
|
||||
and metadata.get("expectedUsage") == "profile"):
|
||||
status = "missing-profile-action"
|
||||
elif active_runs == 0 and available_runs > 0:
|
||||
status = "unused"
|
||||
elif effective_damage > 0:
|
||||
status = "damage"
|
||||
@@ -900,6 +959,10 @@ def aggregate_class_abilities(
|
||||
"icon": metadata.get("icon") or "",
|
||||
"categories": ", ".join(metadata.get("categories") or []),
|
||||
"cooldownSeconds": metadata.get("cooldownSeconds"),
|
||||
"rotationProfileId": metadata.get("rotationProfileId"),
|
||||
"rotationPriority": metadata.get("rotationPriority"),
|
||||
"expectedUsage": metadata.get("expectedUsage", "available"),
|
||||
"rotationDecisions": rotation_decisions,
|
||||
"partyConfigurations": len({row["partyConfigurationId"] for row in class_observations}),
|
||||
"bosses": len({row["bossKey"] for row in class_observations}),
|
||||
"runs": len(class_observations),
|
||||
@@ -920,7 +983,8 @@ def aggregate_class_abilities(
|
||||
"overheal": sum(float(row["overheal"]) for row in observations),
|
||||
"absorbs": absorbs,
|
||||
"status": status,
|
||||
"unusedAvailable": status == "unused" and available_runs > 0,
|
||||
"unusedAvailable": status in {"unused", "missing-profile-action"} and available_runs > 0,
|
||||
"missingProfileAction": status == "missing-profile-action",
|
||||
})
|
||||
rows.sort(key=lambda row: (
|
||||
row["source"], row["className"], row["specialization"],
|
||||
@@ -1107,6 +1171,8 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
class_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
|
||||
class_boss_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
|
||||
class_ability_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
|
||||
npc_class_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
|
||||
npc_class_ability_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
|
||||
policy_ids: set[str] = set()
|
||||
rotation_versions: set[str] = set()
|
||||
for entry in entries:
|
||||
@@ -1157,10 +1223,15 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
)
|
||||
class_groups[key].append(observation)
|
||||
class_boss_groups[(*key, run["bossKey"], run["bossName"], run["dungeonTitle"])].append(observation)
|
||||
if not member.get("activePlayer"):
|
||||
npc_class_groups[key].append(observation)
|
||||
available = {
|
||||
ability["abilityId"]: ability for ability in member.get("availableAbilities", [])
|
||||
}
|
||||
observed = {ability["abilityId"]: ability for ability in member.get("abilities", [])}
|
||||
decisions_by_ability: dict[str, int] = defaultdict(int)
|
||||
for decision in member.get("rotationDecisions", []):
|
||||
decisions_by_ability[decision["abilityId"]] += int(decision.get("decisions", 0))
|
||||
for ability_id in sorted((set(available) | set(observed)) - {"balance-lab-pull"}):
|
||||
expected = available.get(ability_id, {})
|
||||
metrics = observed.get(ability_id, {})
|
||||
@@ -1168,7 +1239,7 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
"casts", "hits", "rawDamage", "effectiveDamage", "rawHealing",
|
||||
"effectiveHealing", "overheal", "absorbs",
|
||||
))
|
||||
class_ability_groups[(*key, ability_id)].append({
|
||||
ability_observation = {
|
||||
"partyConfigurationId": entry["id"],
|
||||
"bossKey": run["bossKey"],
|
||||
"elapsedSeconds": run["elapsedSeconds"],
|
||||
@@ -1176,6 +1247,10 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
"icon": expected.get("icon"),
|
||||
"categories": expected.get("categories", []),
|
||||
"cooldownSeconds": expected.get("cooldownSeconds"),
|
||||
"rotationProfileId": expected.get("rotationProfileId"),
|
||||
"rotationPriority": expected.get("rotationPriority"),
|
||||
"expectedUsage": expected.get("expectedUsage", "available"),
|
||||
"rotationDecisions": decisions_by_ability[ability_id],
|
||||
"available": ability_id in available,
|
||||
"used": used,
|
||||
"casts": metrics.get("casts", 0),
|
||||
@@ -1186,10 +1261,15 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
"effectiveHealing": metrics.get("effectiveHealing", 0),
|
||||
"overheal": metrics.get("overheal", 0),
|
||||
"absorbs": metrics.get("absorbs", 0),
|
||||
})
|
||||
}
|
||||
class_ability_groups[(*key, ability_id)].append(ability_observation)
|
||||
if not member.get("activePlayer"):
|
||||
npc_class_ability_groups[(*key, ability_id)].append(ability_observation)
|
||||
bosses, pressure_median = aggregate_boss_runs(compact_runs)
|
||||
class_specs = aggregate_class_specs(class_groups)
|
||||
class_abilities = aggregate_class_abilities(class_ability_groups, class_groups)
|
||||
npc_class_specs = aggregate_class_specs(npc_class_groups)
|
||||
npc_class_abilities = aggregate_class_abilities(npc_class_ability_groups, npc_class_groups)
|
||||
class_spec_by_boss = []
|
||||
for key, observations in class_boss_groups.items():
|
||||
source, build_id, class_id, class_name, specialization, role, boss_key, boss_name, dungeon_title = key
|
||||
@@ -1233,10 +1313,14 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
"runs": len(compact_runs),
|
||||
"classSpecs": len(class_specs),
|
||||
"classAbilities": len(class_abilities),
|
||||
"npcClassSpecs": len(npc_class_specs),
|
||||
"npcClassAbilities": len(npc_class_abilities),
|
||||
"bossPressureMedianDtps": pressure_median,
|
||||
},
|
||||
"classSpecs": class_specs,
|
||||
"classAbilities": class_abilities,
|
||||
"npcClassSpecs": npc_class_specs,
|
||||
"npcClassAbilities": npc_class_abilities,
|
||||
"partyConfigurations": party_rows,
|
||||
"bosses": bosses,
|
||||
"bossByParty": boss_by_party,
|
||||
@@ -1250,16 +1334,34 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
"defeatRate", "wipeRate", "averageDeaths", "reportPath",
|
||||
))
|
||||
write_csv(output_dir / "class_specs_across_parties.csv", class_specs, (
|
||||
"source", "buildId", "classId", "className", "specialization", "role", "partyConfigurations",
|
||||
"runs", "dps", "bossDps", "hps", "hpsReceived", "dtps", "damageVsSourceRoleMedian", "damageFlag",
|
||||
"source", "buildId", "characterName", "talentBuildId", "talentBuildName", "talentBuildSource",
|
||||
"talentBuildSourcePointCount", "talentBuildSourcePointsRetained", "talentBuildFallbackPointCount",
|
||||
"classId", "className", "specialization", "role", "partyConfigurations",
|
||||
"runs", "dps", "bossDps", "hps", "hpsReceived", "dtps", "gcdUptimePercent", "damageVsSourceRoleMedian", "damageFlag",
|
||||
"totalDamage", "totalDamageTaken", "totalHealing", "totalHealingReceived", "totalAbsorbs", "deathEvents",
|
||||
))
|
||||
write_csv(output_dir / "class_abilities_across_parties.csv", class_abilities, (
|
||||
"source", "buildId", "classId", "className", "specialization", "role", "abilityId",
|
||||
"abilityName", "categories", "cooldownSeconds", "partyConfigurations", "bosses", "runs",
|
||||
"abilityName", "categories", "cooldownSeconds", "rotationProfileId", "rotationPriority",
|
||||
"expectedUsage", "rotationDecisions", "partyConfigurations", "bosses", "runs",
|
||||
"availableRuns", "activeRuns", "usageRate", "casts", "castsPerMinute", "hits", "hitsPerRun",
|
||||
"rawDamage", "effectiveDamage", "dps", "damageShare", "averageDamagePerHit", "rawHealing",
|
||||
"effectiveHealing", "overheal", "absorbs", "status", "unusedAvailable",
|
||||
"effectiveHealing", "overheal", "absorbs", "status", "unusedAvailable", "missingProfileAction",
|
||||
))
|
||||
write_csv(output_dir / "npc_class_specs_across_parties.csv", npc_class_specs, (
|
||||
"source", "buildId", "characterName", "talentBuildId", "talentBuildName", "talentBuildSource",
|
||||
"talentBuildSourcePointCount", "talentBuildSourcePointsRetained", "talentBuildFallbackPointCount",
|
||||
"classId", "className", "specialization", "role", "partyConfigurations",
|
||||
"runs", "dps", "bossDps", "hps", "hpsReceived", "dtps", "gcdUptimePercent", "damageVsSourceRoleMedian", "damageFlag",
|
||||
"totalDamage", "totalDamageTaken", "totalHealing", "totalHealingReceived", "totalAbsorbs", "deathEvents",
|
||||
))
|
||||
write_csv(output_dir / "npc_class_abilities_across_parties.csv", npc_class_abilities, (
|
||||
"source", "buildId", "classId", "className", "specialization", "role", "abilityId",
|
||||
"abilityName", "categories", "cooldownSeconds", "rotationProfileId", "rotationPriority",
|
||||
"expectedUsage", "rotationDecisions", "partyConfigurations", "bosses", "runs",
|
||||
"availableRuns", "activeRuns", "usageRate", "casts", "castsPerMinute", "hits", "hitsPerRun",
|
||||
"rawDamage", "effectiveDamage", "dps", "damageShare", "averageDamagePerHit", "rawHealing",
|
||||
"effectiveHealing", "overheal", "absorbs", "status", "unusedAvailable", "missingProfileAction",
|
||||
))
|
||||
write_csv(output_dir / "bosses_across_parties.csv", bosses, (
|
||||
"bossKey", "bossName", "category", "dungeonId", "dungeonTitle", "runs", "incomingDtps",
|
||||
@@ -1282,7 +1384,8 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
|
||||
"dungeonTitle", "seed", "outcome",
|
||||
"elapsedSeconds", "timeToDefeatSeconds", "bossLevel", "bossMaxHealth", "bossRemainingHealth",
|
||||
"partyDamage", "bossDamage", "partyDps", "bossDpsTaken", "incomingDamage", "rawIncomingDamage",
|
||||
"incomingDtps", "deaths", "deathEvents", "bossAttacks",
|
||||
"incomingDtps", "pressureWindows", "healerThroughputWindows", "healerResponseWindows",
|
||||
"healerResponseRate", "deaths", "deathEvents", "bossAttacks",
|
||||
))
|
||||
return report
|
||||
|
||||
@@ -1300,12 +1403,20 @@ def parser() -> argparse.ArgumentParser:
|
||||
result.add_argument("--party-configurations", type=int, default=10, help="automatic parties per selected source")
|
||||
result.add_argument("--member", action="append", default=[], help="manual build ID or class:spec selector; repeat five times")
|
||||
result.add_argument("--active-slot", type=int, help="1-based member controlled by the benchmark policy; defaults to healer")
|
||||
result.add_argument("--seat-sweep", action="store_true",
|
||||
help="run every party five times so each member is active once and an NPC four times")
|
||||
result.add_argument("--boss-category", choices=("all", "wow", "rom"), default="all")
|
||||
result.add_argument("--boss", action="append", default=[], help="boss key/name selector; repeat as needed; default is every boss")
|
||||
result.add_argument("--simulations-per-party", type=int, default=3,
|
||||
help="deterministic runs for each party/boss pairing")
|
||||
result.add_argument("--seeds", type=parse_seeds, help="explicit comma-separated seeds; overrides --simulations-per-party")
|
||||
result.add_argument("--duration", type=int, default=60, help="maximum fight duration in seconds")
|
||||
result.add_argument("--cooldown-policy", type=Path,
|
||||
help="JSON companion cooldown policy; defaults to the built-in generic rules")
|
||||
result.add_argument("--companion-readiness-policy", type=Path,
|
||||
help="published companion readiness policy JSON; defaults to shipped catalog statuses")
|
||||
result.add_argument("--include-beta-specs", action="store_true",
|
||||
help="include Beta WoW specs in automatic, random, and filler NPC seats")
|
||||
result.add_argument("--output-dir", type=Path, help="report directory")
|
||||
result.add_argument("--list-members", action="store_true")
|
||||
result.add_argument("--list-bosses", action="store_true")
|
||||
@@ -1349,14 +1460,32 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
if args.simulations_per_party < 1:
|
||||
session.close()
|
||||
raise SystemExit("--simulations-per-party must be at least 1")
|
||||
if args.seat_sweep and args.active_slot is not None:
|
||||
session.close()
|
||||
raise SystemExit("--seat-sweep and --active-slot cannot be used together")
|
||||
seeds = args.seeds or list(range(1, args.simulations_per_party + 1))
|
||||
cooldown_policy = None
|
||||
if args.cooldown_policy:
|
||||
try:
|
||||
cooldown_policy = json.loads(args.cooldown_policy.resolve().read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
session.close()
|
||||
raise SystemExit(f"Could not read --cooldown-policy: {error}") from error
|
||||
readiness_policy = None
|
||||
if args.companion_readiness_policy:
|
||||
try:
|
||||
readiness_policy = json.loads(args.companion_readiness_policy.resolve().read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
session.close()
|
||||
raise SystemExit(f"Could not read --companion-readiness-policy: {error}") from error
|
||||
eligible_builds = readiness_eligible_builds(builds, readiness_policy, args.include_beta_specs)
|
||||
if args.party_mode == "automatic":
|
||||
sources = ("wow", "coa", "rom") if args.source == "all" else (args.source,)
|
||||
if args.spec_selection != "specific" and args.spec:
|
||||
session.close()
|
||||
raise SystemExit("--spec values require --spec-selection specific")
|
||||
if args.spec_selection == "random":
|
||||
configurations = automatic_parties(builds, sources, args.party_configurations, args.party_seed)
|
||||
configurations = automatic_parties(eligible_builds, sources, args.party_configurations, args.party_seed)
|
||||
selected_build_ids: list[str] = []
|
||||
else:
|
||||
if args.spec_selection == "specific" and not args.spec:
|
||||
@@ -1366,10 +1495,11 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
resolve_build(selector, builds)["id"] for selector in args.spec
|
||||
))
|
||||
configurations = coverage_parties(
|
||||
builds,
|
||||
eligible_builds if args.spec_selection == "all" else builds,
|
||||
sources,
|
||||
None if args.spec_selection == "all" else selected_build_ids,
|
||||
args.party_seed,
|
||||
eligible_builds,
|
||||
)
|
||||
if args.active_slot is not None:
|
||||
if not 1 <= args.active_slot <= 5:
|
||||
@@ -1387,7 +1517,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
configurations = [{"id": f"{args.source}-party-001", "source": args.source,
|
||||
"party": party, "activeSlot": active_slot - 1}]
|
||||
else:
|
||||
party = random_party(builds, args.party_seed)
|
||||
party = random_party(eligible_builds, args.party_seed)
|
||||
active_slot = args.active_slot or next(
|
||||
(index + 1 for index, build in enumerate(party) if build["role"] == "healer"), 1
|
||||
)
|
||||
@@ -1396,6 +1526,17 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
if any(not 0 <= configuration["activeSlot"] < 5 for configuration in configurations):
|
||||
session.close()
|
||||
raise SystemExit("--active-slot must be from 1 through 5")
|
||||
if args.seat_sweep:
|
||||
configurations = [
|
||||
{
|
||||
**configuration,
|
||||
"id": f"{configuration['id']}-seat-{slot + 1}",
|
||||
"seatSweepGroupId": configuration["id"],
|
||||
"activeSlot": slot,
|
||||
}
|
||||
for configuration in configurations
|
||||
for slot in range(5)
|
||||
]
|
||||
bosses = [resolve_boss(selector, catalog["bosses"]) for selector in args.boss]
|
||||
boss_count = len(bosses) if bosses else sum(
|
||||
args.boss_category == "all" or boss["category"] == args.boss_category for boss in catalog["bosses"]
|
||||
@@ -1406,13 +1547,16 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
f"x {len(seeds)} runs per party/boss)..."
|
||||
)
|
||||
entries = []
|
||||
automatic = args.party_mode == "automatic"
|
||||
automatic = args.party_mode == "automatic" or args.seat_sweep
|
||||
if automatic:
|
||||
write_json(output_dir / "suite-config.json", {
|
||||
"schemaVersion": 1,
|
||||
"policyId": catalog["policyId"],
|
||||
"rotationVersion": catalog["rotationVersion"],
|
||||
"mode": "automatic",
|
||||
"seatSweep": args.seat_sweep,
|
||||
"includeBetaSpecs": args.include_beta_specs,
|
||||
"companionReadinessPolicy": readiness_policy,
|
||||
"specSelection": args.spec_selection,
|
||||
"selectedBuildIds": selected_build_ids,
|
||||
"source": args.source,
|
||||
@@ -1428,6 +1572,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
"parties": [{
|
||||
"id": configuration["id"],
|
||||
"source": configuration["source"],
|
||||
"seatSweepGroupId": configuration.get("seatSweepGroupId"),
|
||||
"activeSlot": configuration["activeSlot"],
|
||||
"partyBuildIds": [build["id"] for build in configuration["party"]],
|
||||
} for configuration in configurations],
|
||||
@@ -1445,6 +1590,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
"partyBuildIds": [build["id"] for build in party],
|
||||
"activeSlot": configuration["activeSlot"],
|
||||
"bossCategory": args.boss_category,
|
||||
**({"cooldownPolicy": cooldown_policy} if cooldown_policy else {}),
|
||||
**({"seatSweepGroupId": configuration["seatSweepGroupId"]}
|
||||
if configuration.get("seatSweepGroupId") else {}),
|
||||
**({"bossKeys": [boss["key"] for boss in bosses]} if bosses else {}),
|
||||
}
|
||||
config_path = party_dir / "config.json"
|
||||
|
||||
Reference in new issue
Block a user