암행부와 현재 도시 모바일 명령 폭을 줄이고 장수 배치 통일

This commit is contained in:
2026-09-12 16:15:10 +00:00
parent 0cd2b7770c
commit 35f587768e
4 changed files with 39 additions and 83 deletions
+21 -1
View File
@@ -233,6 +233,8 @@ const measure = async (page: Page, root: string, table: string, name: string) =>
rows: rows.map((row) => ({
rect: rect(row),
cells: Array.from(row.querySelectorAll('td'), (cell) => ({
field: cell.dataset.field,
gridArea: getComputedStyle(cell).gridArea,
rect: rect(cell),
text: cell.innerText.replace(/\s+/g, ''),
scrollWidth: cell.scrollWidth,
@@ -287,6 +289,17 @@ for (const [route, root, table] of [
expect(mobile.rows.map((row) => row.cells.map((cell) => cell.text))).toEqual(
desktop.rows.map((row) => row.cells.map((cell) => cell.text))
);
// Every general shares the same columns, including NPCs and empty reservations.
for (const row of mobile.rows) {
const turns = row.cells.find((cell) => cell.field === 'turns')!;
expect(turns.rect.width).toBeCloseTo(166, 0);
for (const [index, cell] of row.cells.entries()) {
const reference = mobile.rows[0]!.cells[index]!;
expect(cell.gridArea).toBe(reference.gridArea);
expect(cell.rect.x).toBeCloseTo(reference.rect.x, 1);
expect(cell.rect.width).toBeCloseTo(reference.rect.width, 1);
}
}
for (const row of mobile.rows)
for (const cell of row.cells) {
expect(cell.rect.width).toBeGreaterThan(0);
@@ -378,7 +391,14 @@ test('idle and empty information lists stay compact; doubled text wraps without
const desktop = await measure(page, root, table, `idle-${route.replace('/', '-')}-1000`);
await page.setViewportSize({ width: 500, height: 900 });
const mobile = await measure(page, root, table, `idle-${route.replace('/', '-')}-500`);
expect(mobile.table.height / desktop.table.height).toBeLessThan(1.8);
if (route === 'nation/secret') {
// The uniform four-line layout intentionally replaces the former NPC-only two-line layout.
// Bound the ordinary row directly; desktop idle rows have no command height to share.
for (const row of mobile.rows.slice(1)) expect(row.rect.height).toBeLessThan(76);
expect(mobile.table.height).toBeLessThan(7600);
} else {
expect(mobile.table.height / desktop.table.height).toBeLessThan(1.8);
}
expect(mobile.document.width).toBe(500);
await page.unroute(gameTrpcRoute);
await install(page);
@@ -514,7 +514,7 @@ const commandBrief = (command: ReservedCommand): string =>
}
.generals tbody tr {
display: grid;
grid-template-columns: 64px 62px 68px 40px 40px minmax(0, 1fr);
grid-template-columns: 64px 74px 90px 52px 52px 166px;
grid-template-areas:
'icon name name name name turns'
'icon lead str intel intel turns'
@@ -539,6 +539,7 @@ const commandBrief = (command: ReservedCommand): string =>
content: attr(data-label);
margin-right: 3px;
font-size: 11px;
line-height: 1;
color: #bbb;
}
.generals [data-field='train'],
@@ -241,7 +241,6 @@ onMounted(load);
v-for="general in generals"
:key="general.id"
:data-general-id="general.id"
:class="{ 'has-commands': general.npcState < 2 && general.reservedCommands.length > 0 }"
:data-npc-state="general.npcState"
>
<td data-field="name">
@@ -525,8 +524,7 @@ th,
}
}
/* 500px에서는 명령을 숨기지 않고 그 옆 높이를 통계에 사용한다.
* 예약이 없는 장수는 전체 폭 2행으로 배치해 빈 명령 칸 때문에 길어지지 않는다. */
/* 장수 종류와 예약 유무에 관계없이 정보 2/3, 명령 1/3의 같은 배치를 사용한다. */
@media (max-width: 939.98px) {
.secret-page {
width: 500px;
@@ -573,20 +571,16 @@ th,
}
.list tbody tr {
display: grid;
grid-template-columns: repeat(100, minmax(0, 1fr));
grid-template-columns: repeat(12, minmax(0, 1fr)) 166px;
grid-template-areas:
'name name name name name name stats stats stats stats stats stats turns'
'troop troop troop troop troop troop city city city city defence defence turns'
'gold gold gold gold gold rice rice rice rice time time time turns'
'type type type crew crew crew train train atmos atmos kill kill turns';
height: auto;
border: 1px solid gray;
border-top: 0;
}
.list tbody tr.has-commands {
grid-template-columns: 64px 70px 70px 70px minmax(0, 1fr);
grid-template-areas:
'name name stats stats turns'
'troop troop city city turns'
'gold gold rice rice turns'
'type crew train atmos turns'
'defence kill time time turns';
}
.list tbody td {
min-width: 0;
padding: 0 2px;
@@ -600,6 +594,7 @@ th,
content: attr(data-label);
margin-right: 3px;
font-size: 11px;
line-height: 1;
color: #bbb;
}
.list tbody td[data-field='name'] br {
@@ -657,70 +652,5 @@ th,
.list [data-field='time'] {
grid-area: time;
}
/* 예약이 없으면 서로 다른 열폭을 가진 2행을 100등분 grid 위에 놓는다.
* 첫 행: 이름/능력/부대/도시/NPC, 둘째 행: 자원/병력/훈사/턴. */
.list tbody tr:not(.has-commands) td {
grid-area: auto;
padding-inline: 1px;
}
.list tbody tr:not(.has-commands) td::before {
margin-right: 2px;
}
.list tbody tr:not(.has-commands) [data-field='name'] {
grid-row: 1;
grid-column: 1 / span 27;
}
.list tbody tr:not(.has-commands) [data-field='stats'] {
grid-row: 1;
grid-column: 28 / span 28;
}
.list tbody tr:not(.has-commands) [data-field='troop'] {
grid-row: 1;
grid-column: 56 / span 18;
}
.list tbody tr:not(.has-commands) [data-field='city'] {
grid-row: 1;
grid-column: 74 / span 12;
}
.list tbody tr:not(.has-commands) [data-field='turns'] {
grid-row: 1;
grid-column: 86 / span 15;
}
.list tbody tr:not(.has-commands) [data-field='gold'] {
grid-row: 2;
grid-column: 1 / span 19;
}
.list tbody tr:not(.has-commands) [data-field='rice'] {
grid-row: 2;
grid-column: 20 / span 16;
}
.list tbody tr:not(.has-commands) [data-field='defence'] {
grid-row: 2;
grid-column: 36 / span 7;
}
.list tbody tr:not(.has-commands) [data-field='type'] {
grid-row: 2;
grid-column: 43 / span 11;
}
.list tbody tr:not(.has-commands) [data-field='crew'] {
grid-row: 2;
grid-column: 54 / span 14;
}
.list tbody tr:not(.has-commands) [data-field='train'] {
grid-row: 2;
grid-column: 68 / span 8;
}
.list tbody tr:not(.has-commands) [data-field='atmos'] {
grid-row: 2;
grid-column: 76 / span 8;
}
.list tbody tr:not(.has-commands) [data-field='kill'] {
grid-row: 2;
grid-column: 84 / span 6;
}
.list tbody tr:not(.has-commands) [data-field='time'] {
grid-row: 2;
grid-column: 90 / span 11;
}
}
</style>
+8 -3
View File
@@ -125,12 +125,17 @@ the selected radio option so the user can retry.
`app/game-frontend/e2e/infoMobileLayout.spec.ts` verifies the intentional Core
500px layouts for `/nation/secret` and `/current-city`. Both keep every visible
cell, command and image from the desktop dataset. Statistics share vertical space
with reserved commands; current-city portraits remain 64px. Empty reservation
cells in the secret list use a shorter full-width two-row layout.
with reserved commands; current-city portraits remain 64px. Both lists reserve
166px of the 500px table for commands and 332px for information (plus borders).
Secret rows use the same four-line field positions for players, NPCs and empty
reservations. Long content wraps without truncation.
The fixture measures 100 generals with five-command and NPC rows at CSS viewport
widths 1000, 500, 501 and 800. The mixed-list height must stay below 1.5 times the
same desktop list, and page height below 1.55 times; idle lists stay below 1.8 times.
same desktop list, and page height below 1.55 times. Idle current-city lists stay
below 1.8 times; ordinary idle secret rows stay below 76px and the 100-row fixture
below 7600px. The secret idle limit replaces the old NPC-specific two-row limit
to preserve consistent field positions, as requested in the follow-up review.
These limits apply to the deterministic fixture, not arbitrary-length user text.
It also verifies exact cell text preservation, no horizontal overflow, sorting,
injury tooltip interaction, city selection, empty lists, doubled text, and actual