diff --git a/app/game-api/src/router/general/index.ts b/app/game-api/src/router/general/index.ts index c66911b..01256fb 100644 --- a/app/game-api/src/router/general/index.ts +++ b/app/game-api/src/router/general/index.ts @@ -230,6 +230,12 @@ export const generalRouter = router({ injury: true, experience: true, dedication: true, + age: true, + turnTime: true, + crewTypeId: true, + personalCode: true, + specialCode: true, + special2Code: true, weaponCode: true, horseCode: true, bookCode: true, @@ -309,6 +315,19 @@ export const generalRouter = router({ injury: general.injury, experience: general.experience, dedication: general.dedication, + age: general.age, + turnTime: general.turnTime.toISOString(), + crewTypeId: general.crewTypeId, + traits: { + personal: general.personalCode, + specialWar: general.specialCode, + specialDomestic: general.special2Code, + }, + progression: { + experienceLevel: readNumber(metaRecord.explevel, 0), + dedicationLevel: readNumber(metaRecord.dedlevel, 0), + dex: [1, 2, 3, 4, 5].map((index) => readNumber(metaRecord[`dex${index}`], 0)), + }, items: { horse: normalizeItemCode(general.horseCode), weapon: normalizeItemCode(general.weaponCode), @@ -468,12 +487,12 @@ export const generalRouter = router({ ctx.db.logEntry.findMany({ where: { scope: LogScope.SYSTEM, - category: LogCategory.SUMMARY, + category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] }, id: { gte: input.lastGeneralRecordId }, }, orderBy: { id: 'desc' }, take, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }), ctx.db.logEntry.findMany({ where: { @@ -484,7 +503,7 @@ export const generalRouter = router({ }, orderBy: { id: 'desc' }, take, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }), ctx.db.logEntry.findMany({ where: { @@ -494,7 +513,7 @@ export const generalRouter = router({ }, orderBy: { id: 'desc' }, take, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }), ]); diff --git a/app/game-api/src/router/vote/index.ts b/app/game-api/src/router/vote/index.ts index 1ea73de..2905b46 100644 --- a/app/game-api/src/router/vote/index.ts +++ b/app/game-api/src/router/vote/index.ts @@ -186,9 +186,14 @@ const zRevealMode = z.enum(['after_vote', 'after_end']); export const voteRouter = router({ getVoteList: authedProcedure.query(async ({ ctx }) => { const worldState = await ctx.db.worldState.findFirst(); + const worldMeta = asRecord(worldState?.meta ?? {}); const config = asRecord(worldState?.config ?? {}); const constValues = asRecord(config.const); - const develCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0); + const develCost = resolveNumber( + worldMeta, + ['develcost', 'develCost'], + resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0) + ); const voteReward = develCost * 5; const rows = await ctx.db.$queryRaw(GamePrisma.sql` @@ -404,12 +409,16 @@ export const voteRouter = router({ throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' }); } + const worldMeta = asRecord(worldState.meta); const config = asRecord(worldState.config); const constValues = asRecord(config.const); - const develCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0); + const develCost = resolveNumber( + worldMeta, + ['develcost', 'develCost'], + resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0) + ); const voteReward = develCost * 5; - const worldMeta = asRecord(worldState.meta); const scenarioMeta = asRecord(worldMeta.scenarioMeta); const startYear = readMetaNumber(scenarioMeta, 'startYear', worldState.currentYear); const initYear = readMetaNumber(worldMeta, 'initYear', startYear); diff --git a/app/game-api/test/inGameMenuPermissions.test.ts b/app/game-api/test/inGameMenuPermissions.test.ts index a1bb7e3..0479625 100644 --- a/app/game-api/test/inGameMenuPermissions.test.ts +++ b/app/game-api/test/inGameMenuPermissions.test.ts @@ -241,10 +241,10 @@ describe('in-game my information ownership', () => { expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - where: { scope: 'SYSTEM', category: 'SUMMARY', id: { gte: 0 } }, + where: { scope: 'SYSTEM', category: { in: ['SUMMARY', 'ACTION'] }, id: { gte: 0 } }, orderBy: { id: 'desc' }, take: 16, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }) ); expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith( @@ -253,7 +253,7 @@ describe('in-game my information ownership', () => { where: { scope: 'GENERAL', category: 'ACTION', generalId: 7, id: { gte: 0 } }, orderBy: { id: 'desc' }, take: 16, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }) ); expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith( @@ -262,7 +262,7 @@ describe('in-game my information ownership', () => { where: { scope: 'SYSTEM', category: 'HISTORY', id: { gte: 0 } }, orderBy: { id: 'desc' }, take: 16, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }) ); }); diff --git a/app/game-api/test/mainRecordsRouter.test.ts b/app/game-api/test/mainRecordsRouter.test.ts index 157353d..a53923d 100644 --- a/app/game-api/test/mainRecordsRouter.test.ts +++ b/app/game-api/test/mainRecordsRouter.test.ts @@ -24,13 +24,13 @@ const auth: GameSessionTokenPayload = { type LogQuery = { where: { scope: LogScope; - category: LogCategory; + category: LogCategory | { in: LogCategory[] }; generalId?: number; id: { gte: number }; }; orderBy: { id: 'desc' }; take: number; - select: { id: true; text: true }; + select: { id: true; text: true; createdAt: true }; }; const buildContext = (findMany: (query: LogQuery) => Promise>) => @@ -56,7 +56,7 @@ describe('general.getRecentRecords', () => { { id: 20, text: '개인 cursor' }, ]; } - if (query.where.category === LogCategory.SUMMARY) { + if (typeof query.where.category === 'object' && query.where.category.in.includes(LogCategory.SUMMARY)) { return [ { id: 32, text: '장수 최신' }, { id: 20, text: '장수 cursor' }, @@ -89,7 +89,7 @@ describe('general.getRecentRecords', () => { }, orderBy: { id: 'desc' }, take: 16, - select: { id: true, text: true }, + select: { id: true, text: true, createdAt: true }, }); }); diff --git a/app/game-api/test/voteRouter.test.ts b/app/game-api/test/voteRouter.test.ts index 69ba616..7568010 100644 --- a/app/game-api/test/voteRouter.test.ts +++ b/app/game-api/test/voteRouter.test.ts @@ -92,6 +92,7 @@ const buildContext = (options: { voteRows?: Array<{ selection: number[]; cnt: number }>; pollRow?: typeof poll; configConst?: Record; + metaDevelCost?: number; auctionTargets?: string[]; }) => { const auth = options.auth === undefined ? buildAuth() : options.auth; @@ -136,6 +137,7 @@ const buildContext = (options: { tickSeconds: 3600, config: { const: { develCost: 18, allItems: {}, ...(options.configConst ?? {}) } }, meta: { + ...(options.metaDevelCost === undefined ? {} : { develcost: options.metaDevelCost }), hiddenSeed: 'seed', scenarioId: 200, initYear: 180, @@ -216,6 +218,16 @@ describe('vote router actor and permission boundaries', () => { ); }); + it('uses the current world develcost for the legacy five-times survey reward', async () => { + const fixture = buildContext({ metaDevelCost: 30, configConst: { develCost: 0 } }); + + await expect(appRouter.createCaller(fixture.context).vote.getVoteList()).resolves.toMatchObject({ + voteReward: 150, + }); + await appRouter.createCaller(fixture.context).vote.submitVote({ voteId: 1, selection: [0] }); + expect(fixture.requestCommand).toHaveBeenCalledWith(expect.objectContaining({ goldReward: 150 })); + }); + it('includes active unique auctions in the API-side reward expectation', async () => { const fixture = buildContext({ configConst: { diff --git a/app/game-engine/src/turn/ai/generalAi/core.ts b/app/game-engine/src/turn/ai/generalAi/core.ts index d3a3310..e75a92c 100644 --- a/app/game-engine/src/turn/ai/generalAi/core.ts +++ b/app/game-engine/src/turn/ai/generalAi/core.ts @@ -50,6 +50,16 @@ const d징병 = 2; const d직전 = 3; const d전쟁 = 4; +export const selectNpcMessageForTurn = ( + message: unknown, + rng: Pick, + frequencyPerDay: number, + turnTermMinutes: number +): string | null => { + if (!message) return null; + return rng.nextBool((frequencyPerDay * turnTermMinutes) / (60 * 24)) ? String(message) : null; +}; + export const resolveLegacyAiStats = ( general: Pick, nation: Nation | null | undefined, @@ -97,6 +107,7 @@ export class GeneralAI { public readonly env: ConstraintEnv; public readonly startYear: number; public readonly turnTermMinutes: number; + private pendingNpcMessage: string | null = null; public readonly aiConst: { baseGold: number; @@ -213,9 +224,16 @@ export class GeneralAI { return (...args: unknown[]) => { const result = Reflect.apply(value, receiver, args); if ( - ['nextFloat1', 'nextRangeInt', 'nextInt', 'nextBit', 'nextBool', 'choice', 'choiceUsingWeight', 'choiceUsingWeightPair'].includes( - String(property) - ) + [ + 'nextFloat1', + 'nextRangeInt', + 'nextInt', + 'nextBit', + 'nextBool', + 'choice', + 'choiceUsingWeight', + 'choiceUsingWeightPair', + ].includes(String(property)) ) { process.stdout.write( `AI_RNG_TRACE ${JSON.stringify({ @@ -330,11 +348,7 @@ export class GeneralAI { // Ref refreshes the cached AI state after these selected nation // commands, before choosing the general command with the same // RNG. The refresh includes another mixed-general type draw. - if ( - ['유저장긴급포상', 'NPC긴급포상', '선전포고', '천도'].includes( - actionName - ) - ) { + if (['유저장긴급포상', 'NPC긴급포상', '선전포고', '천도'].includes(actionName)) { this.reqUpdateInstance = true; } return result; @@ -377,6 +391,12 @@ export class GeneralAI { return { set, unset }; } + consumeNpcMessage(): string | null { + const message = this.pendingNpcMessage; + this.pendingNpcMessage = null; + return message; + } + chooseGeneralTurn(reservedTurn: ReservedTurnEntry): AiCommandCandidate | null { this.updateInstance(); if (!this.worldRef) { @@ -384,10 +404,12 @@ export class GeneralAI { } const generalMeta = asRecord(this.general.meta); - const npcMessage = generalMeta.npcmsg ?? generalMeta.text; - if (npcMessage && this.rng.nextBool((this.aiConst.npcMessageFreqByDay * this.turnTermMinutes) / (60 * 24))) { - // 메시지 영속화는 turn handler가 담당한다. 여기서는 레거시와 같은 RNG 소비를 보존한다. - } + this.pendingNpcMessage = selectNpcMessageForTurn( + generalMeta.npcmsg ?? generalMeta.text, + this.rng, + this.aiConst.npcMessageFreqByDay, + this.turnTermMinutes + ); if (this.general.npcState >= 2) { this.general.meta = { ...this.general.meta, defence_train: 80 }; diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index ae75706..cf9cca0 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -1048,7 +1048,10 @@ export const createReservedTurnHandler = async (options: { } const actionContext = specificContext ?? baseContext; if ((process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(String(currentGeneral.id))) { - const tracedContext = actionContext as ActionContextBase & { destCity?: City; destGeneral?: TurnGeneral }; + const tracedContext = actionContext as ActionContextBase & { + destCity?: City; + destGeneral?: TurnGeneral; + }; process.stdout.write( `AI_ACTION_INPUT_TRACE ${JSON.stringify({ generalId: currentGeneral.id, kind, actionKey, actionArgs, destCityId: tracedContext.destCity?.id, destGeneralId: tracedContext.destGeneral?.id })}\n` ); @@ -1731,6 +1734,26 @@ export const createReservedTurnHandler = async (options: { nationFallback, }); const candidate = ai.chooseGeneralTurn(generalCommand); + const npcMessage = ai.consumeNpcMessage(); + if (npcMessage) { + const messageTarget = { + generalId: currentGeneral.id, + generalName: currentGeneral.name, + nationId: currentGeneral.nationId, + nationName: currentNation?.name ?? '재야', + color: currentNation?.color ?? '#000000', + icon: currentGeneral.picture ?? '', + }; + messages.push({ + msgType: 'public', + src: messageTarget, + dest: messageTarget, + text: npcMessage, + time: new Date(context.world.lastTurnTime), + validUntil: new Date('9999-12-31T00:00:00.000Z'), + option: {}, + }); + } if (candidate) { generalAutorunMode = candidate.action !== generalCommand.action || diff --git a/app/game-engine/test/npcGeneralDomesticTurn.test.ts b/app/game-engine/test/npcGeneralDomesticTurn.test.ts index cbdd1b1..769cd8e 100644 --- a/app/game-engine/test/npcGeneralDomesticTurn.test.ts +++ b/app/game-engine/test/npcGeneralDomesticTurn.test.ts @@ -117,7 +117,7 @@ describe('NPC 일반 내정 턴', () => { specialWar: null, }, triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, - meta: { killturn: 999 }, + meta: { killturn: 999, text: '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다' }, officerLevel: 4, experience: 0, dedication: 0, @@ -222,7 +222,7 @@ describe('NPC 일반 내정 턴', () => { stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 }, iconPath: '', map: {}, - const: {}, + const: { npcMessageFreqByDay: 144 }, environment: { mapName: 'npc_domestic_map', unitSet: 'default' }, }, scenarioMeta: { @@ -291,5 +291,12 @@ describe('NPC 일반 내정 턴', () => { security: 1063, }); expect(world.getGeneralById(1)!.turnTime.getTime()).toBe(addMinutes(mockDate, 10).getTime()); + expect(world.peekDirtyState().messages).toContainEqual( + expect.objectContaining({ + msgType: 'public', + text: '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다', + src: expect.objectContaining({ generalId: 1, generalName: 'NPC_무장', nationId: 1 }), + }) + ); }); }); diff --git a/app/game-engine/test/npcMessage.test.ts b/app/game-engine/test/npcMessage.test.ts new file mode 100644 index 0000000..75be676 --- /dev/null +++ b/app/game-engine/test/npcMessage.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { selectNpcMessageForTurn } from '../src/turn/ai/generalAi/core.js'; + +describe('legacy NPC public chatter', () => { + it('uses the per-turn legacy probability and returns the scenario text', () => { + const nextBool = vi.fn(() => true); + + expect(selectNpcMessageForTurn('기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다', { nextBool }, 2, 10)).toBe( + '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다' + ); + expect(nextBool).toHaveBeenCalledWith(2 / 144); + }); + + it('does not consume RNG when a scenario NPC has no message', () => { + const nextBool = vi.fn(() => true); + expect(selectNpcMessageForTurn(null, { nextBool }, 2, 10)).toBeNull(); + expect(nextBool).not.toHaveBeenCalled(); + }); +}); diff --git a/app/game-frontend/src/components/main/CityBasicCard.vue b/app/game-frontend/src/components/main/CityBasicCard.vue index e20a90c..92759f3 100644 --- a/app/game-frontend/src/components/main/CityBasicCard.vue +++ b/app/game-frontend/src/components/main/CityBasicCard.vue @@ -29,37 +29,52 @@ const props = defineProps<{
도시 정보를 불러오지 못했습니다.
-
{{ props.city.name }} (Lv {{ props.city.level }})
+
+ {{ props.city.name }} (Lv {{ props.city.level }}) · 국가 {{ props.city.nationId || '무주' }} +
-
인구 {{ props.city.population }}
-
농업 {{ props.city.agriculture }}
-
상업 {{ props.city.commerce }}
-
치안 {{ props.city.security }}
-
방어 {{ props.city.defence }}
-
성벽 {{ props.city.wall }}
-
보급 {{ props.city.supplyState }}
-
전방 {{ props.city.frontState }}
+ 인구{{ props.city.population.toLocaleString() }} 농업{{ props.city.agriculture.toLocaleString() }} 상업{{ props.city.commerce.toLocaleString() }} 치안{{ props.city.security.toLocaleString() }} 수비{{ props.city.defence.toLocaleString() }} 성벽{{ props.city.wall.toLocaleString() }} 보급{{ props.city.supplyState }} 전방{{ props.city.frontState }}
diff --git a/app/game-frontend/src/views/BattleCenterView.vue b/app/game-frontend/src/views/BattleCenterView.vue index c2d96c0..495a707 100644 --- a/app/game-frontend/src/views/BattleCenterView.vue +++ b/app/game-frontend/src/views/BattleCenterView.vue @@ -457,7 +457,7 @@ onMounted(() => { .log-block { border: 1px solid #666; padding: 0; - background: #111; + background: #000; min-height: 0; } @@ -469,7 +469,7 @@ onMounted(() => { justify-content: center; border-bottom: 1px solid #666; color: orange; - background: #252525; + background: #000; font-size: 1.3em; font-weight: 500; } @@ -479,6 +479,11 @@ onMounted(() => { border-bottom: 0; } +.log-line :deep(.hidden_but_copyable) { + color: transparent !important; + font-size: 0; +} + .empty { padding: 2px 8px; color: #999; diff --git a/app/game-frontend/src/views/GlobalInfoView.vue b/app/game-frontend/src/views/GlobalInfoView.vue index 5c03897..67dcbc9 100644 --- a/app/game-frontend/src/views/GlobalInfoView.vue +++ b/app/game-frontend/src/views/GlobalInfoView.vue @@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import MapViewer from '../components/main/MapViewer.vue'; import { trpc } from '../utils/trpc'; +import { legacyNationTextColor } from '../utils/legacyNationColor'; type Result = Awaited>; type Layout = Awaited>; @@ -14,17 +15,9 @@ const goBack = () => router.push('/'); const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ'; const stateClass = (value: number) => `state-${value}`; const nationMap = computed(() => new Map(data.value?.nations.map((nation) => [nation.id, nation]) ?? [])); -const isBrightColor = (color: string): boolean => { - const normalized = color.trim().replace(/^#/u, ''); - if (!/^[0-9a-f]{6}$/iu.test(normalized)) return false; - const red = Number.parseInt(normalized.slice(0, 2), 16); - const green = Number.parseInt(normalized.slice(2, 4), 16); - const blue = Number.parseInt(normalized.slice(4, 6), 16); - return red * 0.299 + green * 0.587 + blue * 0.114 > 170; -}; const nationNameStyle = (color: string) => ({ backgroundColor: color, - color: isBrightColor(color) ? '#000' : '#fff', + color: legacyNationTextColor(color), }); onMounted(async () => { try { @@ -55,7 +48,7 @@ onMounted(async () => { v-for="nation in data.nations" :key="nation.id" class="vertical" - :style="{ backgroundColor: nation.color }" + :style="nationNameStyle(nation.color)" > {{ nation.name }} @@ -63,7 +56,7 @@ onMounted(async () => { - {{ me.name }} + {{ me.name }} { {{ conflict.cityName }}
- {{ + {{ nationMap.get(Number(id))?.name }}{{ percent.toFixed(1) }}% { } return map; }); +const selectedSpecialWarInfo = computed( + () => status.value?.availableSpecialWar.find((entry) => entry.key === nextSpecialKey.value)?.info ?? '' +); const buffCost = (key: BuffKey, target: number): number => { const points = status.value?.inheritConst.inheritBuffPoints ?? [0, 0, 0, 0, 0, 0]; @@ -493,7 +496,11 @@ onMounted(() => {
{{ specialNameMap.get(nextSpecialKey) }} 특기를 다음에 얻도록 지정합니다.
{{ + selectedSpecialWarInfo + }}
{{ specialNameMap.get(nextSpecialKey) }} 특기를 다음에 + 얻도록 지정합니다.
필요 포인트: {{ status.inheritConst.inheritSpecificSpecialPoint }}
diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 2106881..f9bb136 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -65,6 +65,18 @@ const nationAccess = computed(() => ({ })); const nationColor = computed(() => nation.value?.color ?? '#000000'); const voteActive = computed(() => Boolean(frontStatus.value?.latestVote)); +const formatRecord = (entry: { text: string; createdAt?: string | Date }, appendTime = false): string => { + if (!appendTime || /\d{2}:\d{2}\s*$/u.test(entry.text)) return formatLog(entry.text); + const parsed = entry.createdAt ? new Date(entry.createdAt) : null; + if (!parsed || Number.isNaN(parsed.getTime())) return formatLog(entry.text); + const time = new Intl.DateTimeFormat('ko-KR', { + timeZone: 'Asia/Seoul', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(parsed); + return formatLog(`${entry.text} ${time}`); +}; let surveyNoticeTimer: ReturnType | null = null; watch(surveyNotice, (notice) => { @@ -151,7 +163,9 @@ watch( > 실시간 동기화: {{ realtimeLabel }} - +
@@ -241,7 +255,7 @@ watch( v-for="entry in globalRecords" :key="entry.id" class="record-line" - v-html="formatLog(entry.text)" + v-html="formatRecord(entry)" />
기록이 없습니다.
@@ -255,7 +269,7 @@ watch( v-for="entry in generalRecords" :key="entry.id" class="record-line" - v-html="formatLog(entry.text)" + v-html="formatRecord(entry, true)" />
기록이 없습니다.
@@ -269,7 +283,7 @@ watch( v-for="entry in worldHistory" :key="entry.id" class="record-line" - v-html="formatLog(entry.text)" + v-html="formatRecord(entry)" />
기록이 없습니다.
@@ -350,7 +364,7 @@ watch( v-for="entry in globalRecords" :key="entry.id" class="record-line" - v-html="formatLog(entry.text)" + v-html="formatRecord(entry)" />
기록이 없습니다.
@@ -364,7 +378,7 @@ watch( v-for="entry in generalRecords" :key="entry.id" class="record-line" - v-html="formatLog(entry.text)" + v-html="formatRecord(entry, true)" />
기록이 없습니다.
@@ -378,7 +392,7 @@ watch( v-for="entry in worldHistory" :key="entry.id" class="record-line" - v-html="formatLog(entry.text)" + v-html="formatRecord(entry)" />
기록이 없습니다.
@@ -624,7 +638,6 @@ button { .desktop-message-panel { grid-column: 1 / -1; - height: 1377.5px; } .common-menu-middle { @@ -654,6 +667,11 @@ button { white-space: nowrap; } +.record-line :deep(.hidden_but_copyable) { + color: transparent !important; + font-size: 0; +} + .record-empty { color: #aaa; } diff --git a/app/game-frontend/src/views/MyPageView.vue b/app/game-frontend/src/views/MyPageView.vue index 7ff5f29..838adc0 100644 --- a/app/game-frontend/src/views/MyPageView.vue +++ b/app/game-frontend/src/views/MyPageView.vue @@ -386,20 +386,49 @@ onMounted(() => {
경험/공헌
{{ data.general.experience }} / {{ data.general.dedication }}
+
+
성격/특기
+
+ {{ data.general.traits?.personal ?? '-' }} / + {{ data.general.traits?.specialWar ?? '-' }} +
+
+
+
나이/다음턴
+
{{ data.general.age ?? '-' }}세 / {{ data.general.turnTime?.slice(11, 16) ?? '-' }}
+
- 명망 약간 ({{ data.general.experience }}) · 계급 - 약간 ({{ data.general.dedication }}) + 명망 + Lv {{ data.general.progression?.experienceLevel ?? 0 }} ({{ + data.general.experience + }}) + · 계급 + Lv {{ data.general.progression?.dedicationLevel ?? 0 }} ({{ + data.general.dedication + }})
전투 0 · 계략 0 · 사관 7년
승률 0% · 승리 0 · 패배 0
살상률 0% · 사살 0 · 피살 0
숙련도
-
보병 0.0K · 궁병 0.0K · 기병 0.0K · 귀병 0.0K · 차병 0.0K
- 병종 {{ data.general.crew ? '보병' : '-' }} · 부상 {{ data.general.injury }} · 부대 - · 벌점 - + 보병 {{ data.general.progression?.dex?.[0] ?? 0 }} · 궁병 + {{ data.general.progression?.dex?.[1] ?? 0 }} · 기병 + {{ data.general.progression?.dex?.[2] ?? 0 }} · 귀병 + {{ data.general.progression?.dex?.[3] ?? 0 }} · 차병 + {{ data.general.progression?.dex?.[4] ?? 0 }} +
+
+ 병종 {{ data.general.crewTypeId || '-' }} · 내정특기 + {{ data.general.traits?.specialDomestic ?? '-' }} · 부상 {{ data.general.injury }} · 부대 + {{ data.general.troopId || '-' }} · 벌점 {{ penalties.length || '-' }}
diff --git a/app/game-frontend/src/views/NationCitiesView.vue b/app/game-frontend/src/views/NationCitiesView.vue index f548695..f36e392 100644 --- a/app/game-frontend/src/views/NationCitiesView.vue +++ b/app/game-frontend/src/views/NationCitiesView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import { getNpcColor } from '../utils/npcColor'; +import { legacyNationTextColor } from '../utils/legacyNationColor'; import { cityLevelMap, regionMap } from '../utils/nationFormat'; import { trpc } from '../utils/trpc'; @@ -149,7 +150,14 @@ onMounted(async () => { > - + 【 {{ regionMap[city.region] }} | {{ cityLevelMap[city.level] }} 】 {{ city.id === data?.nation.capitalCityId ? `[${city.name}]` : city.name diff --git a/app/game-frontend/src/views/NationInfoView.vue b/app/game-frontend/src/views/NationInfoView.vue index dd303c2..e344999 100644 --- a/app/game-frontend/src/views/NationInfoView.vue +++ b/app/game-frontend/src/views/NationInfoView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import { formatLog } from '../utils/formatLog'; +import { legacyNationTextColor } from '../utils/legacyNationColor'; import { trpc } from '../utils/trpc'; type Result = Awaited>; @@ -35,7 +36,11 @@ onMounted(async () => { - diff --git a/app/game-frontend/src/views/NationPersonnelView.vue b/app/game-frontend/src/views/NationPersonnelView.vue index 7243853..47671ba 100644 --- a/app/game-frontend/src/views/NationPersonnelView.vue +++ b/app/game-frontend/src/views/NationPersonnelView.vue @@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'; import { resolveGeneralIconBackgroundImage } from '../utils/generalIcon'; import { trpc } from '../utils/trpc'; import { cityLevelMap, formatOfficerLevelText, getNationChiefLevel, regionMap } from '../utils/nationFormat'; +import { legacyNationTextColor } from '../utils/legacyNationColor'; type PersonnelResponse = Awaited>; type GeneralEntry = PersonnelResponse['generals'][number]; @@ -213,7 +214,10 @@ onMounted(() => void loadPersonnel()); @@ -415,10 +419,22 @@ onMounted(() => void loadPersonnel()); - -
+ 【{{ data.nation.name }}】
【 {{ data.nation.name }} 】 【 {{ regionMap[city.region] ?? '-' }} 】
+ 【{{ cityLevelMap[city.level] ?? '-' }}】 + {{ city.name }} >; type NationEntry = StratFinanResponse['nationsList'][number]; @@ -194,7 +196,9 @@ onMounted(() => void loadStratFinan());
종료 시점
-
{{ nation.name }}
+
+ {{ nation.name }} +
{{ formatNumber(nation.power) }}
{{ formatNumber(nation.generalCount) }}
{{ formatNumber(nation.cityCount) }}
@@ -245,7 +249,7 @@ onMounted(() => void loadStratFinan());
-