모바일 장수 정보 묶음과 숫자 가독성 및 의병 표기 개선
This commit is contained in:
@@ -234,6 +234,7 @@ const measure = async (page: Page, root: string, table: string, name: string) =>
|
|||||||
rect: rect(row),
|
rect: rect(row),
|
||||||
cells: Array.from(row.querySelectorAll('td'), (cell) => ({
|
cells: Array.from(row.querySelectorAll('td'), (cell) => ({
|
||||||
field: cell.dataset.field,
|
field: cell.dataset.field,
|
||||||
|
label: getComputedStyle(cell, '::before').content,
|
||||||
gridArea: getComputedStyle(cell).gridArea,
|
gridArea: getComputedStyle(cell).gridArea,
|
||||||
rect: rect(cell),
|
rect: rect(cell),
|
||||||
text: cell.innerText.replace(/\s+/g, ''),
|
text: cell.innerText.replace(/\s+/g, ''),
|
||||||
@@ -276,6 +277,12 @@ for (const [route, root, table] of [
|
|||||||
await page.setViewportSize({ width: 1000, height: 900 });
|
await page.setViewportSize({ width: 1000, height: 900 });
|
||||||
await page.goto(route);
|
await page.goto(route);
|
||||||
const desktop = await measure(page, root, table, `${route.replace('/', '-')}-1000`);
|
const desktop = await measure(page, root, table, `${route.replace('/', '-')}-1000`);
|
||||||
|
const firstGeneral = page.locator(`${table} tbody tr`).first();
|
||||||
|
await expect(firstGeneral.locator('[data-field="crew"]')).toHaveText('12,345');
|
||||||
|
if (route === 'nation/secret') {
|
||||||
|
await expect(firstGeneral.locator('[data-field="gold"]')).toHaveText('123,456,789');
|
||||||
|
await expect(firstGeneral.locator('[data-field="rice"]')).toHaveText('234,567');
|
||||||
|
}
|
||||||
for (const width of [500, 501, 800]) {
|
for (const width of [500, 501, 800]) {
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
const mobile = await measure(page, root, table, `${route.replace('/', '-')}-${width}`);
|
const mobile = await measure(page, root, table, `${route.replace('/', '-')}-${width}`);
|
||||||
@@ -293,6 +300,18 @@ for (const [route, root, table] of [
|
|||||||
for (const row of mobile.rows) {
|
for (const row of mobile.rows) {
|
||||||
const turns = row.cells.find((cell) => cell.field === 'turns')!;
|
const turns = row.cells.find((cell) => cell.field === 'turns')!;
|
||||||
expect(turns.rect.width).toBeCloseTo(166, 0);
|
expect(turns.rect.width).toBeCloseTo(166, 0);
|
||||||
|
const train = row.cells.find((cell) => cell.field === 'train')!;
|
||||||
|
const atmos = row.cells.find((cell) => cell.field === 'atmos')!;
|
||||||
|
expect(train.label).toBe('"훈/사"');
|
||||||
|
expect(atmos.label).toBe('"/"');
|
||||||
|
expect(train.rect.y).toBe(atmos.rect.y);
|
||||||
|
expect(train.rect.x + train.rect.width).toBeCloseTo(atmos.rect.x, 1);
|
||||||
|
if (route === 'nation/secret') {
|
||||||
|
const gold = row.cells.find((cell) => cell.field === 'gold')!;
|
||||||
|
const rice = row.cells.find((cell) => cell.field === 'rice')!;
|
||||||
|
expect(gold.rect.width).toBeCloseTo(rice.rect.width, 1);
|
||||||
|
expect(gold.rect.y).toBe(rice.rect.y);
|
||||||
|
}
|
||||||
for (const [index, cell] of row.cells.entries()) {
|
for (const [index, cell] of row.cells.entries()) {
|
||||||
const reference = mobile.rows[0]!.cells[index]!;
|
const reference = mobile.rows[0]!.cells[index]!;
|
||||||
expect(cell.gridArea).toBe(reference.gridArea);
|
expect(cell.gridArea).toBe(reference.gridArea);
|
||||||
|
|||||||
@@ -126,12 +126,12 @@ const npcColorStates = [0, 1, 2, 4, 5, 6] as const;
|
|||||||
const npcColorGenerals = npcColorStates.map((npcState) => ({
|
const npcColorGenerals = npcColorStates.map((npcState) => ({
|
||||||
...general,
|
...general,
|
||||||
id: 100 + npcState,
|
id: 100 + npcState,
|
||||||
name: `색상장수${npcState}`,
|
name: `${({ 4: 'ⓖ', 5: '㉥', 6: 'ⓤ' } as Record<number, string>)[npcState] ?? ''}색상장수${npcState}`,
|
||||||
npcState,
|
npcState,
|
||||||
}));
|
}));
|
||||||
const npcColorSecretGenerals = npcColorStates.map((npcState) => ({
|
const npcColorSecretGenerals = npcColorStates.map((npcState) => ({
|
||||||
id: 100 + npcState,
|
id: 100 + npcState,
|
||||||
name: `색상장수${npcState}`,
|
name: `${({ 4: 'ⓖ', 5: '㉥', 6: 'ⓤ' } as Record<number, string>)[npcState] ?? ''}색상장수${npcState}`,
|
||||||
npcState,
|
npcState,
|
||||||
injury: 0,
|
injury: 0,
|
||||||
baseStats: { leadership: 70, strength: 60, intelligence: 50 },
|
baseStats: { leadership: 70, strength: 60, intelligence: 50 },
|
||||||
@@ -292,6 +292,9 @@ test('NPC general names use the complete Ref palette in secret and nation lists
|
|||||||
for (const npcState of npcColorStates) {
|
for (const npcState of npcColorStates) {
|
||||||
const name = table.locator(`[data-npc-state="${npcState}"] [data-general-name]`);
|
const name = table.locator(`[data-npc-state="${npcState}"] [data-general-name]`);
|
||||||
await expect(name).toBeVisible();
|
await expect(name).toBeVisible();
|
||||||
|
if (npcState === 4) await expect(name).toHaveText('ⓖ색상장수4');
|
||||||
|
if (npcState === 5) await expect(name).toHaveText('㉥색상장수5');
|
||||||
|
if (npcState === 6) await expect(name).toHaveText('ⓤ색상장수6');
|
||||||
if (npcState === 0) {
|
if (npcState === 0) {
|
||||||
expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe('');
|
expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe('');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ const commandBrief = (command: ReservedCommand): string =>
|
|||||||
</td>
|
</td>
|
||||||
<td data-field="defence" data-label="守">{{ defenceTrainText(general.defenceTrain) }}</td>
|
<td data-field="defence" data-label="守">{{ defenceTrainText(general.defenceTrain) }}</td>
|
||||||
<td data-field="type" data-label="병종">{{ general.crewTypeName ?? '?' }}</td>
|
<td data-field="type" data-label="병종">{{ general.crewTypeName ?? '?' }}</td>
|
||||||
<td data-field="crew" data-label="병사">{{ general.crew ?? '?' }}</td>
|
<td data-field="crew" data-label="병사">{{ show(general.crew) }}</td>
|
||||||
<td data-field="train" data-label="훈">{{ general.train ?? '?' }}</td>
|
<td data-field="train" data-label="훈">{{ general.train ?? '?' }}</td>
|
||||||
<td data-field="atmos" data-label="사">{{ general.atmos ?? '?' }}</td>
|
<td data-field="atmos" data-label="사">{{ general.atmos ?? '?' }}</td>
|
||||||
<td data-field="turns" class="turns" :class="{ 'turns--reserved': general.turns.length > 0 }">
|
<td data-field="turns" class="turns" :class="{ 'turns--reserved': general.turns.length > 0 }">
|
||||||
@@ -590,6 +590,22 @@ const commandBrief = (command: ReservedCommand): string =>
|
|||||||
.generals [data-field='crew'] {
|
.generals [data-field='crew'] {
|
||||||
grid-area: crew;
|
grid-area: crew;
|
||||||
}
|
}
|
||||||
|
/* 같은 척도의 훈련·사기는 한 쌍으로 읽되 원래 데이터 셀과 값을 보존한다. */
|
||||||
|
.generals tbody td[data-field='train'] {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.generals tbody td[data-field='train']::before {
|
||||||
|
content: '훈/사';
|
||||||
|
}
|
||||||
|
.generals tbody td[data-field='atmos'] {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.generals tbody td[data-field='atmos']::before {
|
||||||
|
content: '/';
|
||||||
|
margin-inline: 3px;
|
||||||
|
}
|
||||||
.generals [data-field='train'] {
|
.generals [data-field='train'] {
|
||||||
grid-area: train;
|
grid-area: train;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const generals = computed(() =>
|
|||||||
);
|
);
|
||||||
const closeWindow = () => window.close();
|
const closeWindow = () => window.close();
|
||||||
const displayName = (general: { name: string; npcState: number }) =>
|
const displayName = (general: { name: string; npcState: number }) =>
|
||||||
general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
general.npcState > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
||||||
const injuryInfo = (injury: number) => generalInjuryPresentation(injury);
|
const injuryInfo = (injury: number) => generalInjuryPresentation(injury);
|
||||||
const injuryDescription = (general: Result['generals'][number]): string =>
|
const injuryDescription = (general: Result['generals'][number]): string =>
|
||||||
general.injury > 0 ? `부상 ${general.injury}% · ${injuryInfo(general.injury).text}` : '';
|
general.injury > 0 ? `부상 ${general.injury}% · ${injuryInfo(general.injury).text}` : '';
|
||||||
@@ -312,12 +312,12 @@ onMounted(load);
|
|||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td data-field="troop" data-label="부대">{{ general.troopName ?? '-' }}</td>
|
<td data-field="troop" data-label="부대">{{ general.troopName ?? '-' }}</td>
|
||||||
<td data-field="gold" data-label="금">{{ general.gold }}</td>
|
<td data-field="gold" data-label="금">{{ general.gold.toLocaleString('ko-KR') }}</td>
|
||||||
<td data-field="rice" data-label="쌀">{{ general.rice }}</td>
|
<td data-field="rice" data-label="쌀">{{ general.rice.toLocaleString('ko-KR') }}</td>
|
||||||
<td data-field="city" data-label="도시">{{ general.cityName ?? '-' }}</td>
|
<td data-field="city" data-label="도시">{{ general.cityName ?? '-' }}</td>
|
||||||
<td data-field="defence" data-label="守">{{ general.defenceTrainText }}</td>
|
<td data-field="defence" data-label="守">{{ general.defenceTrainText }}</td>
|
||||||
<td data-field="type" data-label="병종">{{ general.crewTypeName }}</td>
|
<td data-field="type" data-label="병종">{{ general.crewTypeName }}</td>
|
||||||
<td data-field="crew" data-label="병사">{{ general.crew }}</td>
|
<td data-field="crew" data-label="병사">{{ general.crew.toLocaleString('ko-KR') }}</td>
|
||||||
<td data-field="train" data-label="훈">{{ general.train }}</td>
|
<td data-field="train" data-label="훈">{{ general.train }}</td>
|
||||||
<td data-field="atmos" data-label="사">{{ general.atmos }}</td>
|
<td data-field="atmos" data-label="사">{{ general.atmos }}</td>
|
||||||
<td data-field="turns" class="turns">
|
<td data-field="turns" class="turns">
|
||||||
@@ -571,12 +571,12 @@ th,
|
|||||||
}
|
}
|
||||||
.list tbody tr {
|
.list tbody tr {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr)) 166px;
|
grid-template-columns: repeat(24, minmax(0, 1fr)) 166px;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'name name name name name name stats stats stats stats stats stats turns'
|
'name name name name name name name name name name name name stats stats stats stats stats stats stats stats stats stats stats stats turns'
|
||||||
'troop troop troop troop troop troop city city city city defence defence turns'
|
'troop troop troop troop troop troop troop troop troop troop troop troop city city city city city city city city defence defence defence defence turns'
|
||||||
'gold gold gold gold gold rice rice rice rice time time time turns'
|
'gold gold gold gold gold gold gold gold gold rice rice rice rice rice rice rice rice rice time time time time time time turns'
|
||||||
'type type type crew crew crew train train atmos atmos kill kill turns';
|
'type type type type type type crew crew crew crew crew crew train train train train train atmos atmos atmos kill kill kill kill turns';
|
||||||
height: auto;
|
height: auto;
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
@@ -601,7 +601,11 @@ th,
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.general-level {
|
.general-level {
|
||||||
margin-left: 4px;
|
margin-left: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #bbb;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.list tbody .turns {
|
.list tbody .turns {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -637,6 +641,22 @@ th,
|
|||||||
.list [data-field='crew'] {
|
.list [data-field='crew'] {
|
||||||
grid-area: crew;
|
grid-area: crew;
|
||||||
}
|
}
|
||||||
|
/* 같은 척도의 훈련·사기는 한 쌍으로 읽되 원래 데이터 셀과 값을 보존한다. */
|
||||||
|
.list tbody td[data-field='train'] {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.list tbody td[data-field='train']::before {
|
||||||
|
content: '훈/사';
|
||||||
|
}
|
||||||
|
.list tbody td[data-field='atmos'] {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.list tbody td[data-field='atmos']::before {
|
||||||
|
content: '/';
|
||||||
|
margin-inline: 3px;
|
||||||
|
}
|
||||||
.list [data-field='train'] {
|
.list [data-field='train'] {
|
||||||
grid-area: train;
|
grid-area: train;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,12 @@ cell, command and image from the desktop dataset. Statistics share vertical spac
|
|||||||
with reserved commands; current-city portraits remain 64px. Both lists reserve
|
with reserved commands; current-city portraits remain 64px. Both lists reserve
|
||||||
166px of the 500px table for commands and 332px for information (plus borders).
|
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
|
Secret rows use the same four-line field positions for players, NPCs and empty
|
||||||
reservations. Long content wraps without truncation.
|
reservations. Long content wraps without truncation. Gold and rice have equal
|
||||||
|
widths; training and morale form a compact `훈/사 value / value` pair in both
|
||||||
|
views. The secret level stays visible as secondary 11px text. Existing decorated
|
||||||
|
NPC names, including volunteer `ⓖ` and `ⓤ`, are not prefixed with an extra `ⓝ`.
|
||||||
|
Crew, gold and rice values use Korean-locale thousands separators in both modes;
|
||||||
|
redacted current-city crew values remain `?`.
|
||||||
|
|
||||||
The fixture measures 100 generals with five-command and NPC rows at CSS viewport
|
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
|
widths 1000, 500, 501 and 800. The mixed-list height must stay below 1.5 times the
|
||||||
|
|||||||
Reference in New Issue
Block a user