fix: render neutral main nation and trait names
This commit is contained in:
@@ -18,7 +18,7 @@ import { ConflictingTurnDaemonCommandError } from '../../daemon/databaseTranspor
|
|||||||
import { resolveAccessWindows } from '../../services/generalAccess.js';
|
import { resolveAccessWindows } from '../../services/generalAccess.js';
|
||||||
import { adjustAccountIconForUser } from '../../services/accountIconSync.js';
|
import { adjustAccountIconForUser } from '../../services/accountIconSync.js';
|
||||||
import { getMyGeneral } from '../shared/general.js';
|
import { getMyGeneral } from '../shared/general.js';
|
||||||
import { resolveNationNotice } from '../nation/shared.js';
|
import { loadTraitNames, resolveNationNotice, type TraitNameMap } from '../nation/shared.js';
|
||||||
|
|
||||||
const zGeneralSettings = z.object({
|
const zGeneralSettings = z.object({
|
||||||
tnmt: z.number().int().optional(),
|
tnmt: z.number().int().optional(),
|
||||||
@@ -34,6 +34,17 @@ const zImmediateActionInput = z
|
|||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
const MAIN_RECORD_LIMIT = 15;
|
const MAIN_RECORD_LIMIT = 15;
|
||||||
|
const NEUTRAL_NATION_CONTEXT = {
|
||||||
|
id: 0,
|
||||||
|
name: '재야',
|
||||||
|
color: '#000000',
|
||||||
|
level: 0,
|
||||||
|
gold: 0,
|
||||||
|
rice: 0,
|
||||||
|
tech: 0,
|
||||||
|
typeCode: 'None',
|
||||||
|
capitalCityId: null,
|
||||||
|
} as const;
|
||||||
|
|
||||||
const resolveImmediateActionRequestId = (
|
const resolveImmediateActionRequestId = (
|
||||||
contextRequestId: string | undefined,
|
contextRequestId: string | undefined,
|
||||||
@@ -135,6 +146,18 @@ const normalizeItemCode = (value: string | null): string | null => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveTraitDisplayName = (code: string, names: TraitNameMap): string => {
|
||||||
|
if (!code || code === 'None') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const loadedName = names.get(code)?.name;
|
||||||
|
if (loadedName) {
|
||||||
|
return loadedName;
|
||||||
|
}
|
||||||
|
// Ref는 class getName()을 표시하므로 로더가 모르는 선택적 특기도 raw namespace는 노출하지 않는다.
|
||||||
|
return code.replace(/^che_(?:event_)?/u, '');
|
||||||
|
};
|
||||||
|
|
||||||
const resolveUserSettings = (meta: Record<string, unknown>) => {
|
const resolveUserSettings = (meta: Record<string, unknown>) => {
|
||||||
// The legacy general columns are persisted at the top level of General.meta.
|
// The legacy general columns are persisted at the top level of General.meta.
|
||||||
// Keep reading the short-lived nested shape for installations that ran the
|
// Keep reading the short-lived nested shape for installations that ran the
|
||||||
@@ -265,10 +288,16 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
capitalCityId: true,
|
capitalCityId: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: null,
|
: Promise.resolve(NEUTRAL_NATION_CONTEXT),
|
||||||
ctx.db.worldState.findFirst({ select: { config: true } }),
|
ctx.db.worldState.findFirst({ select: { config: true } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const [personalityNames, domesticNames, warNames] = await Promise.all([
|
||||||
|
loadTraitNames([general.personalCode], 'personality'),
|
||||||
|
loadTraitNames([general.specialCode], 'domestic'),
|
||||||
|
loadTraitNames([general.special2Code], 'war'),
|
||||||
|
]);
|
||||||
|
|
||||||
const metaRecord = asRecord(general.meta);
|
const metaRecord = asRecord(general.meta);
|
||||||
const worldConfig = asRecord(worldState?.config);
|
const worldConfig = asRecord(worldState?.config);
|
||||||
const constValues = asRecord(worldConfig.const ?? worldConfig.consts);
|
const constValues = asRecord(worldConfig.const ?? worldConfig.consts);
|
||||||
@@ -303,9 +332,9 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
turnTime: general.turnTime.toISOString(),
|
turnTime: general.turnTime.toISOString(),
|
||||||
crewTypeId: general.crewTypeId,
|
crewTypeId: general.crewTypeId,
|
||||||
traits: {
|
traits: {
|
||||||
personal: general.personalCode,
|
personal: resolveTraitDisplayName(general.personalCode, personalityNames),
|
||||||
specialWar: general.specialCode,
|
specialDomestic: resolveTraitDisplayName(general.specialCode, domesticNames),
|
||||||
specialDomestic: general.special2Code,
|
specialWar: resolveTraitDisplayName(general.special2Code, warNames),
|
||||||
},
|
},
|
||||||
progression: {
|
progression: {
|
||||||
experienceLevel: readNumber(metaRecord.explevel, 0),
|
experienceLevel: readNumber(metaRecord.explevel, 0),
|
||||||
|
|||||||
@@ -248,6 +248,40 @@ describe('in-game my information ownership', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns the Ref-style neutral nation frame and trait display names on the main read model', async () => {
|
||||||
|
const fixture = createContext({
|
||||||
|
me: buildGeneral({
|
||||||
|
nationId: 0,
|
||||||
|
officerLevel: 0,
|
||||||
|
personalCode: 'che_안전',
|
||||||
|
specialCode: 'che_상재',
|
||||||
|
special2Code: 'che_신산',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||||
|
general: {
|
||||||
|
traits: {
|
||||||
|
personal: '안전',
|
||||||
|
specialDomestic: '상재',
|
||||||
|
specialWar: '신산',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nation: {
|
||||||
|
id: 0,
|
||||||
|
name: '재야',
|
||||||
|
color: '#000000',
|
||||||
|
level: 0,
|
||||||
|
gold: 0,
|
||||||
|
rice: 0,
|
||||||
|
tech: 0,
|
||||||
|
typeCode: 'None',
|
||||||
|
capitalCityId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fixture.db.nation.findUnique).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('reads legacy top-level settings and dispatches only the session-owned general', async () => {
|
it('reads legacy top-level settings and dispatches only the session-owned general', async () => {
|
||||||
const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 }));
|
const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 }));
|
||||||
const fixture = createContext({ requestCommand });
|
const fixture = createContext({ requestCommand });
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type FixtureState = {
|
|||||||
adjustIconInputs?: Array<Record<string, unknown>>;
|
adjustIconInputs?: Array<Record<string, unknown>>;
|
||||||
joinConfig?: Record<string, unknown>;
|
joinConfig?: Record<string, unknown>;
|
||||||
createGeneralInputs?: Array<Record<string, unknown>>;
|
createGeneralInputs?: Array<Record<string, unknown>>;
|
||||||
|
mainTraits?: { personal: string; specialDomestic: string; specialWar: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
type TrpcRequestPayload = {
|
type TrpcRequestPayload = {
|
||||||
@@ -75,7 +76,7 @@ const myGeneral = (state: FixtureState) => ({
|
|||||||
age: 30,
|
age: 30,
|
||||||
turnTime: '2026-01-01 00:10:00',
|
turnTime: '2026-01-01 00:10:00',
|
||||||
crewTypeId: 1,
|
crewTypeId: 1,
|
||||||
traits: { personal: 'None', specialDomestic: 'None', specialWar: 'None' },
|
traits: state.mainTraits ?? { personal: '-', specialDomestic: '-', specialWar: '-' },
|
||||||
progression: {
|
progression: {
|
||||||
experienceLevel: 1,
|
experienceLevel: 1,
|
||||||
dedicationLevel: 2,
|
dedicationLevel: 2,
|
||||||
@@ -86,7 +87,19 @@ const myGeneral = (state: FixtureState) => ({
|
|||||||
items: { horse: 'che_명마', weapon: null, book: null, item: null },
|
items: { horse: 'che_명마', weapon: null, book: null, item: null },
|
||||||
},
|
},
|
||||||
city: { id: 1, name: '업', level: 8, nationId: 1 },
|
city: { id: 1, name: '업', level: 8, nationId: 1 },
|
||||||
nation: { id: 1, name: '위', color: '#777777', level: 3 },
|
nation: state.buildNationCandidateEnabled
|
||||||
|
? {
|
||||||
|
id: 0,
|
||||||
|
name: '재야',
|
||||||
|
color: '#000000',
|
||||||
|
level: 0,
|
||||||
|
gold: 0,
|
||||||
|
rice: 0,
|
||||||
|
tech: 0,
|
||||||
|
typeCode: 'None',
|
||||||
|
capitalCityId: null,
|
||||||
|
}
|
||||||
|
: { id: 1, name: '위', color: '#777777', level: 3 },
|
||||||
settings: {
|
settings: {
|
||||||
tnmt: 0,
|
tnmt: 0,
|
||||||
defence_train: 80,
|
defence_train: 80,
|
||||||
@@ -221,6 +234,25 @@ const install = async (page: Page, state: FixtureState) => {
|
|||||||
state.createGeneralInputs?.push(jsonInput);
|
state.createGeneralInputs?.push(jsonInput);
|
||||||
return response({ generalId: 9 });
|
return response({ generalId: 9 });
|
||||||
}
|
}
|
||||||
|
if (operation === 'dashboard.getContextBundleDelta') {
|
||||||
|
return response({
|
||||||
|
context: {
|
||||||
|
kind: 'snapshot',
|
||||||
|
revision: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
|
data: myGeneral(state),
|
||||||
|
},
|
||||||
|
commandTable: {
|
||||||
|
kind: 'snapshot',
|
||||||
|
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
|
||||||
|
data: { general: [], nation: [] },
|
||||||
|
},
|
||||||
|
boardAccess: {
|
||||||
|
kind: 'snapshot',
|
||||||
|
revision: 'CCCCCCCCCCCCCCCCCCCCCC',
|
||||||
|
data: { permission: 4, canMeeting: true, canSecret: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (operation === 'general.me') {
|
if (operation === 'general.me') {
|
||||||
state.generalMeQueries = (state.generalMeQueries ?? 0) + 1;
|
state.generalMeQueries = (state.generalMeQueries ?? 0) + 1;
|
||||||
return response(myGeneral(state));
|
return response(myGeneral(state));
|
||||||
@@ -437,6 +469,49 @@ test('정화된 국가 방침은 실행 가능한 속성 없이 Chromium에 표
|
|||||||
.toBeUndefined();
|
.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('재야 메인은 국가 틀과 성격·특기 표기명을 Chromium에 표시한다', async ({ page }) => {
|
||||||
|
const state: FixtureState = {
|
||||||
|
permission: 'member',
|
||||||
|
myset: 0,
|
||||||
|
buildNationCandidateEnabled: true,
|
||||||
|
mainTraits: { personal: '안전', specialDomestic: '상재', specialWar: '신산' },
|
||||||
|
settingMutations: [],
|
||||||
|
accessPages: [],
|
||||||
|
};
|
||||||
|
await install(page, state);
|
||||||
|
await page.setViewportSize({ width: 1000, height: 900 });
|
||||||
|
await page.goto('');
|
||||||
|
|
||||||
|
const nationCard = page.locator('.nation-card');
|
||||||
|
await expect(nationCard.locator('.title')).toHaveText('재야');
|
||||||
|
await expect(nationCard.locator('.empty')).toHaveCount(0);
|
||||||
|
await expect(nationCard.locator('.grid strong')).toHaveText(Array.from({ length: 6 }, () => '해당 없음'));
|
||||||
|
|
||||||
|
const generalCard = page.locator('.general-card');
|
||||||
|
await expect(generalCard).toContainText('성격안전');
|
||||||
|
await expect(generalCard).toContainText('전투특기신산');
|
||||||
|
await expect(generalCard).toContainText('내정특기상재');
|
||||||
|
await expect(generalCard).not.toContainText('che_');
|
||||||
|
|
||||||
|
const geometry = await nationCard.evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const title = element.querySelector<HTMLElement>('.title')!;
|
||||||
|
return {
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
titleBackground: getComputedStyle(title).backgroundColor,
|
||||||
|
placeholderCount: [...element.querySelectorAll('.grid strong')].filter(
|
||||||
|
(cell) => cell.textContent?.trim() === '해당 없음'
|
||||||
|
).length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(geometry.width).toBeGreaterThan(0);
|
||||||
|
expect(geometry.height).toBeGreaterThan(0);
|
||||||
|
expect(geometry.titleBackground).toBe('rgb(0, 0, 0)');
|
||||||
|
expect(geometry.placeholderCount).toBe(6);
|
||||||
|
await persistParityArtifact(page, 'main-neutral-trait-display', geometry);
|
||||||
|
});
|
||||||
|
|
||||||
test('접속량정보 keeps the legacy public 1016px chart geometry', async ({ page }) => {
|
test('접속량정보 keeps the legacy public 1016px chart geometry', async ({ page }) => {
|
||||||
const state: FixtureState = { permission: 'member', myset: 0, settingMutations: [], accessPages: [] };
|
const state: FixtureState = { permission: 'member', myset: 0, settingMutations: [], accessPages: [] };
|
||||||
await install(page, state);
|
await install(page, state);
|
||||||
|
|||||||
@@ -31,15 +31,19 @@ const props = defineProps<{
|
|||||||
class="title"
|
class="title"
|
||||||
:style="{ backgroundColor: props.nation.color, color: legacyNationTextColor(props.nation.color) }"
|
:style="{ backgroundColor: props.nation.color, color: legacyNationTextColor(props.nation.color) }"
|
||||||
>
|
>
|
||||||
{{ props.nation.name }} (Lv {{ props.nation.level }})
|
{{ props.nation.name }}<template v-if="props.nation.id > 0"> (Lv {{ props.nation.level }})</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<span>국고</span><strong>{{ props.nation.gold.toLocaleString() }}</strong> <span>국량</span
|
<span>국고</span
|
||||||
><strong>{{ props.nation.rice.toLocaleString() }}</strong> <span>기술</span
|
><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.gold.toLocaleString() }}</strong>
|
||||||
><strong>{{ props.nation.tech.toLocaleString() }}</strong> <span>체제</span
|
<span>국량</span
|
||||||
><strong>{{ props.nation.typeCode }}</strong> <span>수도</span
|
><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.rice.toLocaleString() }}</strong>
|
||||||
><strong>{{ props.nation.capitalCityId ?? '-' }}</strong> <span>국가 등급</span
|
<span>기술</span
|
||||||
><strong>{{ props.nation.level }}</strong>
|
><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.tech.toLocaleString() }}</strong>
|
||||||
|
<span>체제</span><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.typeCode }}</strong>
|
||||||
|
<span>수도</span
|
||||||
|
><strong>{{ props.nation.id === 0 ? '해당 없음' : (props.nation.capitalCityId ?? '-') }}</strong>
|
||||||
|
<span>국가 등급</span><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.level }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user