Update Healer Man server
This commit is contained in:
1 parent
fdb6739f8d
commit
f90ba7d989
41 files changed
+1671
-118
No files matched your search
+38
-8
@@ -196,15 +196,12 @@ def readiness_eligible_builds(
|
||||
policy: dict[str, Any] | None,
|
||||
include_beta_specs: bool,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Filter only WoW NPC builds; CoA and RoM catalogs remain unchanged."""
|
||||
"""Return the Ready pool across every source; Beta requires explicit opt-in."""
|
||||
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"):
|
||||
@@ -282,7 +279,12 @@ 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)
|
||||
source_fillers = [build for build in (filler_builds or builds) if build["source"] == source]
|
||||
filler_catalog = list(filler_builds or builds)
|
||||
# Prefer same-source companions, then fall back to the globally Ready
|
||||
# pool so an intentional Needs Testing target can still be diagnosed
|
||||
# without admitting other uncertified seats.
|
||||
source_fillers = [build for build in filler_catalog if build["source"] == source]
|
||||
source_fillers.extend(build for build in filler_catalog 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)
|
||||
@@ -1443,7 +1445,9 @@ def parser() -> argparse.ArgumentParser:
|
||||
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")
|
||||
help="include Beta specs in automatic, random, and filler NPC seats")
|
||||
result.add_argument("--ready-only", action="store_true",
|
||||
help="reject explicit Needs Testing targets; used by certification and regression runs")
|
||||
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")
|
||||
@@ -1507,7 +1511,14 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
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,)
|
||||
requested_sources = ("wow", "coa", "rom") if args.source == "all" else (args.source,)
|
||||
sources = requested_sources if args.spec_selection == "specific" else tuple(
|
||||
source for source in requested_sources
|
||||
if any(build["source"] == source for build in eligible_builds)
|
||||
)
|
||||
if not sources:
|
||||
session.close()
|
||||
raise SystemExit("No Ready companion builds exist for the selected source. Promote specs before running automatic coverage.")
|
||||
if args.spec_selection != "specific" and args.spec:
|
||||
session.close()
|
||||
raise SystemExit("--spec values require --spec-selection specific")
|
||||
@@ -1521,6 +1532,14 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
selected_build_ids = list(dict.fromkeys(
|
||||
resolve_build(selector, builds)["id"] for selector in args.spec
|
||||
))
|
||||
if args.ready_only:
|
||||
eligible_ids = {build["id"] for build in eligible_builds}
|
||||
rejected = [build_id for build_id in selected_build_ids if build_id not in eligible_ids]
|
||||
if rejected:
|
||||
session.close()
|
||||
raise SystemExit(
|
||||
"--ready-only rejected Needs Testing targets: " + ", ".join(rejected)
|
||||
)
|
||||
configurations = coverage_parties(
|
||||
eligible_builds if args.spec_selection == "all" else builds,
|
||||
sources,
|
||||
@@ -1538,13 +1557,23 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
session.close()
|
||||
raise SystemExit("Manual mode requires exactly five --member values.")
|
||||
party = [resolve_build(selector, builds) for selector in args.member]
|
||||
if args.ready_only:
|
||||
eligible_ids = {build["id"] for build in eligible_builds}
|
||||
rejected = [build["id"] for build in party if build["id"] not in eligible_ids]
|
||||
if rejected:
|
||||
session.close()
|
||||
raise SystemExit("--ready-only rejected Needs Testing members: " + ", ".join(rejected))
|
||||
active_slot = args.active_slot or next(
|
||||
(index + 1 for index, build in enumerate(party) if build["role"] == "healer"), 1
|
||||
)
|
||||
configurations = [{"id": f"{args.source}-party-001", "source": args.source,
|
||||
"party": party, "activeSlot": active_slot - 1}]
|
||||
else:
|
||||
party = random_party(eligible_builds, args.party_seed)
|
||||
random_pool = [
|
||||
build for build in eligible_builds
|
||||
if args.source == "all" or build["source"] == args.source
|
||||
]
|
||||
party = random_party(random_pool, args.party_seed)
|
||||
active_slot = args.active_slot or next(
|
||||
(index + 1 for index, build in enumerate(party) if build["role"] == "healer"), 1
|
||||
)
|
||||
@@ -1583,6 +1612,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
"mode": "automatic",
|
||||
"seatSweep": args.seat_sweep,
|
||||
"includeBetaSpecs": args.include_beta_specs,
|
||||
"readyOnly": args.ready_only,
|
||||
"companionReadinessPolicy": readiness_policy,
|
||||
"specSelection": args.spec_selection,
|
||||
"selectedBuildIds": selected_build_ids,
|
||||
|
||||
@@ -82,6 +82,8 @@ def build_balance_lab_command(args: argparse.Namespace, level: int, output_dir:
|
||||
command.extend(("--companion-readiness-policy", str(args.companion_readiness_policy.resolve())))
|
||||
if getattr(args, "include_beta_specs", False):
|
||||
command.append("--include-beta-specs")
|
||||
if getattr(args, "ready_only", False):
|
||||
command.append("--ready-only")
|
||||
for selector in args.spec:
|
||||
command.extend(("--spec", selector))
|
||||
for selector in args.boss:
|
||||
@@ -444,6 +446,8 @@ def parser() -> argparse.ArgumentParser:
|
||||
result.add_argument("--cooldown-policy", type=Path)
|
||||
result.add_argument("--companion-readiness-policy", type=Path)
|
||||
result.add_argument("--include-beta-specs", action="store_true")
|
||||
result.add_argument("--ready-only", action="store_true",
|
||||
help="reject Needs Testing targets and retain only Ready/Beta automatic pools")
|
||||
result.add_argument("--boss-category", choices=("all", "wow", "rom"), default="all")
|
||||
result.add_argument("--boss", action="append", default=[])
|
||||
result.add_argument("--jobs", type=int, default=min(4, os.cpu_count() or 1),
|
||||
|
||||
@@ -90,7 +90,7 @@ function help(): string {
|
||||
"Usage: npm run balance:benchmark -- [options]",
|
||||
"",
|
||||
"Options:",
|
||||
" --class mage,templar Limit primary classes.",
|
||||
" --class mage,templar Explicit targeted class diagnostic.",
|
||||
" --role damage,healer Limit roles.",
|
||||
" --scenario id,id Limit scenario IDs.",
|
||||
" --level 60 Comparison level (default 60).",
|
||||
@@ -99,6 +99,9 @@ function help(): string {
|
||||
" --output path Output directory under the current path or absolute.",
|
||||
" --help Show this help.",
|
||||
"",
|
||||
"Default scope: the five Ready WoW specializations only.",
|
||||
"CoA, RoM, and other WoW specializations require an explicit --class filter.",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const directory = path.resolve('playtest-artifacts', 'party-heal-popups');
|
||||
const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-party-heals-'));
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
|
||||
(async () => {
|
||||
const { createGameServer } = await import('../server/server.mjs');
|
||||
const runtime = createGameServer({ dataDir: runtimeData, enableShelvedModes: false });
|
||||
await new Promise((resolve) => runtime.server.listen(4194, '127.0.0.1', resolve));
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
page.setDefaultTimeout(180_000);
|
||||
const pageErrors = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
const moduleUrls = fs.readdirSync('dist/assets')
|
||||
.filter((file) => /^(?:index|GameRuntime|combatStore)-.*\.js$/.test(file))
|
||||
.map((file) => `/assets/${file}`);
|
||||
await page.addInitScript((urls) => {
|
||||
const stores = async () => {
|
||||
const values = (await Promise.all(urls.map((url) => import(url)))).flatMap(Object.values);
|
||||
const candidates = values.filter((value) => value && typeof value.getState === 'function');
|
||||
const party = candidates.find((store) => Array.isArray(store.getState()?.members)
|
||||
&& typeof store.getState()?.initializeParty === 'function');
|
||||
const combat = candidates.find((store) => Array.isArray(store.getState()?.combatEvents)
|
||||
&& typeof store.getState()?.healActor === 'function');
|
||||
const shell = candidates.find((store) => Array.isArray(store.getState()?.characters)
|
||||
&& typeof store.getState()?.openMainMenu === 'function');
|
||||
if (!party || !combat || !shell) throw new Error('Party/combat/shell stores were not found.');
|
||||
return { party, combat, shell };
|
||||
};
|
||||
window.openMainMenuForPartyHealPlaytest = async () => {
|
||||
const { shell } = await stores();
|
||||
shell.getState().openMainMenu();
|
||||
};
|
||||
window.enterDungeonForPartyHealPlaytest = async () => {
|
||||
const { shell } = await stores();
|
||||
const ok = shell.getState().enterDungeon('wailing-caverns');
|
||||
return { ok, notice: shell.getState().notice, phase: shell.getState().phase };
|
||||
};
|
||||
window.openDungeonsForPartyHealPlaytest = async () => {
|
||||
const { shell } = await stores();
|
||||
shell.getState().openDungeons();
|
||||
};
|
||||
window.triggerPartyHealPopup = async () => {
|
||||
const { party, combat } = await stores();
|
||||
const member = party.getState().members[0];
|
||||
if (!member) throw new Error('No NPC party member is available.');
|
||||
const injuredHealth = Math.max(1, member.maxHealth - 300);
|
||||
party.setState({
|
||||
members: party.getState().members.map((candidate) => candidate.id === member.id
|
||||
? { ...candidate, health: injuredHealth }
|
||||
: candidate),
|
||||
});
|
||||
const actor = combat.getState().actors[member.id];
|
||||
if (!actor) throw new Error('The NPC party actor is unavailable.');
|
||||
combat.setState({ actors: {
|
||||
...combat.getState().actors,
|
||||
[member.id]: { ...actor, health: injuredHealth, alive: true },
|
||||
} });
|
||||
const mob = Object.values(combat.getState().mobs).find((candidate) => !candidate.dead && !candidate.despawned);
|
||||
if (!mob) throw new Error('No living dungeon enemy is available.');
|
||||
combat.setState({ mobs: { ...combat.getState().mobs, [mob.id]: { ...mob, engaged: true } } });
|
||||
const result = combat.getState().healActor(member.id, 137, {
|
||||
sourceActorId: '__player__',
|
||||
abilityId: 'party-frame-playtest-heal',
|
||||
now: Date.now(),
|
||||
amountAlreadyModified: true,
|
||||
});
|
||||
return { memberId: member.id, amount: result.effectiveAmount };
|
||||
};
|
||||
}, moduleUrls);
|
||||
|
||||
try {
|
||||
await page.goto('http://127.0.0.1:4194', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: /Play offline/ }).click();
|
||||
await page.getByRole('button', { name: /Create character/ }).click();
|
||||
await page.getByRole('button', { name: /Customize & name/ }).click();
|
||||
await page.getByLabel('Character name').fill('HealpopupQA');
|
||||
await page.getByRole('button', { name: /Create character/ }).click();
|
||||
await page.evaluate(() => window.openMainMenuForPartyHealPlaytest());
|
||||
await page.locator('.main-menu-screen').waitFor();
|
||||
await page.evaluate(() => window.openDungeonsForPartyHealPlaytest());
|
||||
await page.locator('.dungeon-select-screen').waitFor();
|
||||
const entry = await page.evaluate(() => window.enterDungeonForPartyHealPlaytest());
|
||||
assert.equal(entry.ok, true, JSON.stringify(entry));
|
||||
await page.locator('.party-frames').waitFor();
|
||||
await page.locator('.party-frame').nth(4).waitFor();
|
||||
await page.locator('.loading-screen').waitFor({ state: 'detached' });
|
||||
|
||||
const result = await page.evaluate(() => window.triggerPartyHealPopup());
|
||||
assert.equal(result.amount, 137);
|
||||
const popup = page.locator('.party-frame__heal-popup', { hasText: '+137' });
|
||||
await popup.waitFor({ state: 'visible' });
|
||||
const targetFrame = page.locator('.party-frame').filter({ has: popup });
|
||||
assert.equal(await targetFrame.count(), 1, 'heal number belongs to exactly one receiving frame');
|
||||
assert.equal(await page.locator('.desktop-action-feedback').count(), 0, 'center action feedback is absent');
|
||||
assert.equal((await page.locator('.action-bar__feedback').allInnerTexts()).join('').trim(), '', 'controller bar has no combat log text');
|
||||
await page.screenshot({ path: path.join(directory, 'party-heal-popup.png') });
|
||||
assert.deepEqual(pageErrors, []);
|
||||
console.log(`Party-frame heal popup playtest passed for ${result.memberId}.`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await new Promise((resolve) => runtime.server.close(resolve));
|
||||
fs.rmSync(runtimeData, { recursive: true, force: true });
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -141,7 +141,7 @@ class BalanceLabControllerTests(unittest.TestCase):
|
||||
self.assertTrue(set(selected).issubset(covered))
|
||||
self.assertTrue(all(observed.count(build_id) == 1 for build_id in selected))
|
||||
|
||||
def test_readiness_filters_only_wow_and_beta_requires_opt_in(self) -> None:
|
||||
def test_readiness_filters_every_source_and_beta_requires_opt_in(self) -> None:
|
||||
builds = [
|
||||
{"id": "ready-tank", "source": "wow", "specializationId": "warrior:protection"},
|
||||
{"id": "beta-damage", "source": "wow", "specializationId": "paladin:retribution"},
|
||||
@@ -155,11 +155,11 @@ class BalanceLabControllerTests(unittest.TestCase):
|
||||
}}}
|
||||
self.assertEqual(
|
||||
[build["id"] for build in readiness_eligible_builds(builds, policy, False)],
|
||||
["ready-tank", "rom-damage"],
|
||||
["ready-tank"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[build["id"] for build in readiness_eligible_builds(builds, policy, True)],
|
||||
["ready-tank", "beta-damage", "rom-damage"],
|
||||
["ready-tank", "beta-damage"],
|
||||
)
|
||||
|
||||
def test_targeted_needs_testing_build_uses_ready_fillers(self) -> None:
|
||||
@@ -178,6 +178,21 @@ class BalanceLabControllerTests(unittest.TestCase):
|
||||
self.assertIn("testing-shadow", ids)
|
||||
self.assertTrue(all(build_id == "testing-shadow" or build_id.startswith("ready-") for build_id in ids))
|
||||
|
||||
def test_targeted_non_wow_build_uses_ready_wow_fillers(self) -> None:
|
||||
ready = [
|
||||
{"id": "ready-tank", "source": "wow", "role": "tank", "readinessStatus": "ready", "defaultTalentBuild": True},
|
||||
{"id": "ready-healer", "source": "wow", "role": "healer", "readinessStatus": "ready", "defaultTalentBuild": True},
|
||||
*(
|
||||
{"id": f"ready-damage-{index}", "source": "wow", "role": "damage", "readinessStatus": "ready", "defaultTalentBuild": True}
|
||||
for index in range(3)
|
||||
),
|
||||
]
|
||||
target = {"id": "coa-target", "source": "coa", "role": "damage", "readinessStatus": "needs-testing", "defaultTalentBuild": True}
|
||||
parties = coverage_parties([*ready, target], ("coa",), ["coa-target"], 21, ready)
|
||||
ids = [member["id"] for member in parties[0]["party"]]
|
||||
self.assertIn("coa-target", ids)
|
||||
self.assertEqual(sum(build_id.startswith("ready-") for build_id in ids), 4)
|
||||
|
||||
def test_boss_search_matches_name_dungeon_key_and_source(self) -> None:
|
||||
bosses = [
|
||||
{"key": "gundrak/eck", "name": "Eck the Ferocious", "dungeonTitle": "Gundrak", "category": "wow"},
|
||||
|
||||
@@ -27,6 +27,7 @@ class BalanceLabBracketTests(unittest.TestCase):
|
||||
active_slot=None, spec=[], boss=["stormwind-stockade/entry-1717"],
|
||||
seat_sweep=True, cooldown_policy=None,
|
||||
companion_readiness_policy=Path("policy.json"), include_beta_specs=True,
|
||||
ready_only=True,
|
||||
)
|
||||
command = build_balance_lab_command(args, 40, Path("output/level-40"))
|
||||
self.assertEqual(command[0], sys.executable)
|
||||
@@ -36,6 +37,7 @@ class BalanceLabBracketTests(unittest.TestCase):
|
||||
self.assertEqual(command[command.index("--seeds") + 1], "1,2,3")
|
||||
self.assertIn("--seat-sweep", command)
|
||||
self.assertIn("--include-beta-specs", command)
|
||||
self.assertIn("--ready-only", command)
|
||||
self.assertEqual(
|
||||
command[command.index("--companion-readiness-policy") + 1],
|
||||
str(Path("policy.json").resolve()),
|
||||
|
||||
Reference in new issue
Block a user