feat(game): NAVIGATION AI unlocks two-hop jumps in one action (#35)
CI / build + unit + e2e (pull_request) Failing after 3m11s
CI / build + unit + e2e (pull_request) Failing after 3m11s
This commit is contained in:
+69
-3
@@ -117,16 +117,21 @@ test('tab bar stays pinned while panel content scrolls (#31)', async ({ page })
|
||||
|
||||
test('world map shows the generated starting system and its neighbours', async ({ page }) => {
|
||||
const world = (await (await page.request.get('/api/world')).json()) as WorldState;
|
||||
// #35 — once NAVIGATION AI is researched the chart also offers two-hop systems, so the expected
|
||||
// button count depends on the save's tech state (the CI DB persists across retries).
|
||||
const techs = (await (await page.request.get('/api/tech')).json()) as { slug: string; state: string }[];
|
||||
const maxHops = techs.some((t) => t.slug === 'NAV_AI' && t.state === 'RESEARCHED') ? 2 : 1;
|
||||
|
||||
await page.goto('/');
|
||||
// Live chart title carries the turn counter.
|
||||
await expect(page.getByText(/GALACTIC CHART \/\/ JUMP ROUTES — TURN \d+/)).toBeVisible();
|
||||
// The current system is marked, and every adjacent system is a clickable chart field (role=button).
|
||||
// The current system is marked, and every in-range system is a clickable chart field (role=button).
|
||||
const current = world.systems.find((s) => s.current)!;
|
||||
await expect(page.getByText('CURRENT', { exact: true })).toBeVisible();
|
||||
const panel = page.getByRole('tabpanel');
|
||||
const adjacent = world.edges.filter(([a, b]) => a === current.id || b === current.id); // six on a fresh save
|
||||
await expect(panel.getByRole('button')).toHaveCount(adjacent.length);
|
||||
// Six on a fresh save without NAV AI; the two-hop ring joins once it is researched.
|
||||
const jumpable = world.systems.filter((s) => s.id !== current.id && shortestJumpPath(current.id, s.id, world.edges).length <= maxHops);
|
||||
await expect(panel.getByRole('button')).toHaveCount(jumpable.length);
|
||||
});
|
||||
|
||||
test('jumping to an adjacent system updates the world state and turn counter', async ({ page }) => {
|
||||
@@ -364,6 +369,67 @@ test('SKILL TREE tab researches WEAPONS SYSTEMS and unlocks the gated WEAPONS bu
|
||||
await expect(weaponsCard.getByText('LV 1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('NAVIGATION AI research unlocks two-hop jumps on the world map (#35)', async ({ page }) => {
|
||||
// Research costs two resources (70% of scans drop one), so a short scan loop makes it affordable;
|
||||
// jumping keeps it going if a system exhausts. Idempotent: a retry finds it already RESEARCHED.
|
||||
let techs = (await (await page.request.get('/api/tech')).json()) as { id: number; slug: string; state: string }[];
|
||||
const navAiState = () => techs.find((t) => t.slug === 'NAV_AI')!.state;
|
||||
if (navAiState() !== 'RESEARCHED') {
|
||||
let ship = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
let iterations = 0;
|
||||
while (totalQty(ship.inventory, 'resource') < 2 && iterations < 40) {
|
||||
iterations++;
|
||||
const sys = (await (await page.request.get('/api/system')).json()) as SystemState;
|
||||
const unexplored = sys.planets.filter((p) => !p.explored);
|
||||
if (unexplored.length > 0) {
|
||||
expect((await page.request.post('/api/explore', { data: { planetId: unexplored[0].id } })).status()).toBe(200);
|
||||
} else {
|
||||
await jumpToFreshSystem(page); // prefer undiscovered territory — first-edge ping-pongs between exhausted systems
|
||||
}
|
||||
ship = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
}
|
||||
expect(totalQty(ship.inventory, 'resource')).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await page.goto('/');
|
||||
// SKILL TREE tab: research NAVIGATION AI (two resources) — the card flips to RESEARCHED.
|
||||
await page.getByRole('tab', { name: 'SKILL TREE' }).click();
|
||||
const techCard = page.getByRole('heading', { name: 'NAVIGATION AI' }).locator('xpath=ancestor::div[3]');
|
||||
await expect(techCard.getByText('AVAILABLE')).toBeVisible(); // root tech — no prerequisites
|
||||
await expect(techCard.getByRole('button', { name: 'RESEARCH' })).toBeEnabled(); // resources ≥ 2
|
||||
await techCard.getByRole('button', { name: 'RESEARCH' }).click();
|
||||
await expect(techCard.getByText('RESEARCHED')).toBeVisible();
|
||||
}
|
||||
|
||||
// Pick a system exactly two hops from the current one. A completely fresh save has none (the start
|
||||
// region is only one hop deep), so jump once first — that expansion creates the two-hop territory.
|
||||
let world = (await (await page.request.get('/api/world')).json()) as WorldState;
|
||||
const findTwoHop = () => {
|
||||
const currentId = world.systems.find((s) => s.current)!.id;
|
||||
return world.systems.find((s) => shortestJumpPath(currentId, s.id, world.edges).length === 2);
|
||||
};
|
||||
let guard = 0;
|
||||
while (!findTwoHop() && guard < 5) {
|
||||
await jumpToFreshSystem(page); // +1 turn — jumping into undiscovered territory always expands the chart
|
||||
world = (await (await page.request.get('/api/world')).json()) as WorldState;
|
||||
guard++;
|
||||
}
|
||||
const target = findTwoHop()!;
|
||||
expect(target).toBeDefined();
|
||||
|
||||
await page.goto('/'); // fresh chart with NAV AI researched — two-hop fields are clickable now
|
||||
await expect(page.getByText(/GALACTIC CHART \/\/ JUMP ROUTES — TURN \d+/)).toBeVisible();
|
||||
const panel = page.getByRole('tabpanel');
|
||||
const turnBefore = world.turn;
|
||||
|
||||
// The two-hop systems render as distinct chart fields (// JUMP ×2 sublabel), and the target is one of them.
|
||||
await expect(panel.getByText(/(DISCOVERED|UNKNOWN) \/\/ JUMP ×2/)).toBeVisible();
|
||||
await panel.getByRole('button', { name: target.name.toUpperCase() }).click();
|
||||
|
||||
const after = (await (await page.request.get('/api/world')).json()) as WorldState;
|
||||
expect(after.systems.find((s) => s.id === target.id)!.current).toBe(true); // world state updated in one action
|
||||
expect(after.turn).toBe(turnBefore + 2); // the turn cost scales with hop count (#35)
|
||||
});
|
||||
|
||||
test('MISSIONS tab accepts a contract and completing it grants the reward', async ({ page }) => {
|
||||
const board = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
|
||||
expect(board.filter((m) => m.state === 'AVAILABLE').length).toBeGreaterThanOrEqual(3); // the board is always topped up
|
||||
|
||||
Reference in New Issue
Block a user