feat: complete legacy game screen parity

This commit is contained in:
2026-08-04 13:31:05 +00:00
parent c4c4ee6e0f
commit a333ef9681
19 changed files with 436 additions and 207 deletions
@@ -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;
+5 -12
View File
@@ -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<ReturnType<typeof trpc.world.getGlobalInfo.query>>;
type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
@@ -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 }}
</th>
@@ -63,7 +56,7 @@ onMounted(async () => {
</thead>
<tbody>
<tr v-for="me in data.nations" :key="me.id">
<th :style="{ backgroundColor: me.color }">{{ me.name }}</th>
<th :style="nationNameStyle(me.color)">{{ me.name }}</th>
<td
v-for="you in data.nations"
:key="you.id"
@@ -93,7 +86,7 @@ onMounted(async () => {
<strong>{{ conflict.cityName }}</strong>
<div>
<div v-for="(percent, id) in conflict.nations" :key="id" class="conflict-row">
<span :style="{ backgroundColor: nationMap.get(Number(id))?.color }">{{
<span :style="nationNameStyle(nationMap.get(Number(id))?.color ?? '#000000')">{{
nationMap.get(Number(id))?.name
}}</span
><em>{{ percent.toFixed(1) }}%</em
+8 -1
View File
@@ -199,6 +199,9 @@ const specialNameMap = computed(() => {
}
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(() => {
</select>
</div>
<small
>{{ specialNameMap.get(nextSpecialKey) }} 특기를 다음에 얻도록 지정합니다.<br /><b
><span v-if="selectedSpecialWarInfo" class="special-description">{{
selectedSpecialWarInfo
}}</span
><br v-if="selectedSpecialWarInfo" />{{ specialNameMap.get(nextSpecialKey) }} 특기를 다음에
얻도록 지정합니다.<br /><b
>필요 포인트: {{ status.inheritConst.inheritSpecificSpecialPoint }}</b
></small
>
+26 -8
View File
@@ -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<typeof setTimeout> | null = null;
watch(surveyNotice, (notice) => {
@@ -151,7 +163,9 @@ watch(
>
실시간 동기화: {{ realtimeLabel }}
</button>
<button class="game-shell__action game-shell__action--navigation" type="button" @click="loadMainData"> </button>
<button class="game-shell__action game-shell__action--navigation" type="button" @click="loadMainData">
</button>
<button class="game-shell__action" type="button" @click="moveLobby">로비로</button>
</div>
</header>
@@ -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)"
/>
<div v-if="globalRecords.length === 0" class="record-empty">기록이 없습니다.</div>
</div>
@@ -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)"
/>
<div v-if="generalRecords.length === 0" class="record-empty">기록이 없습니다.</div>
</div>
@@ -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)"
/>
<div v-if="worldHistory.length === 0" class="record-empty">기록이 없습니다.</div>
</div>
@@ -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)"
/>
<div v-if="globalRecords.length === 0" class="record-empty">기록이 없습니다.</div>
</div>
@@ -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)"
/>
<div v-if="generalRecords.length === 0" class="record-empty">기록이 없습니다.</div>
</div>
@@ -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)"
/>
<div v-if="worldHistory.length === 0" class="record-empty">기록이 없습니다.</div>
</div>
@@ -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;
}
+33 -4
View File
@@ -386,20 +386,49 @@ onMounted(() => {
<dt>경험/공헌</dt>
<dd>{{ data.general.experience }} / {{ data.general.dedication }}</dd>
</div>
<div>
<dt>성격/특기</dt>
<dd>
{{ data.general.traits?.personal ?? '-' }} /
{{ data.general.traits?.specialWar ?? '-' }}
</dd>
</div>
<div>
<dt>나이/다음턴</dt>
<dd>{{ data.general.age ?? '-' }} / {{ data.general.turnTime?.slice(11, 16) ?? '-' }}</dd>
</div>
</dl>
</div>
<div v-if="data" class="legacy-general-details">
<div>
명망 <strong>약간 ({{ data.general.experience }})</strong> · 계급
<strong>약간 ({{ data.general.dedication }})</strong>
명망
<strong
>Lv {{ data.general.progression?.experienceLevel ?? 0 }} ({{
data.general.experience
}})</strong
>
· 계급
<strong
>Lv {{ data.general.progression?.dedicationLevel ?? 0 }} ({{
data.general.dedication
}})</strong
>
</div>
<div>전투 0 · 계략 0 · 사관 7</div>
<div>승률 0% · 승리 0 · 패배 0</div>
<div>살상률 0% · 사살 0 · 피살 0</div>
<div class="dexterity-title">숙련도</div>
<div>보병 0.0K · 궁병 0.0K · 기병 0.0K · 귀병 0.0K · 차병 0.0K</div>
<div>
{{ 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 }}
</div>
<div>
병종 {{ data.general.crewTypeId || '-' }} · 내정특기
{{ data.general.traits?.specialDomestic ?? '-' }} · 부상 {{ data.general.injury }} · 부대
{{ data.general.troopId || '-' }} · 벌점 {{ penalties.length || '-' }}
</div>
</div>
</div>
@@ -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 () => {
>
<tbody>
<tr>
<td colspan="10" class="city-title" :style="{ backgroundColor: data?.nation.color }">
<td
colspan="10"
class="city-title"
:style="{
backgroundColor: data?.nation.color,
color: legacyNationTextColor(data?.nation.color ?? '#000000'),
}"
>
{{ regionMap[city.region] }} | {{ cityLevelMap[city.level] }}
<span :class="{ capital: city.id === data?.nation.capitalCityId }">{{
city.id === data?.nation.capitalCityId ? `[${city.name}]` : city.name
@@ -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<ReturnType<typeof trpc.nation.getNationInfo.query>>;
@@ -35,7 +36,11 @@ onMounted(async () => {
<table v-if="data" class="legacy-table info-table legacy-bg2">
<tbody>
<tr>
<td colspan="8" class="nation-title" :style="{ backgroundColor: data.nation.color }">
<td
colspan="8"
class="nation-title"
:style="{ backgroundColor: data.nation.color, color: legacyNationTextColor(data.nation.color) }"
>
{{ data.nation.name }}
</td>
</tr>
@@ -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<ReturnType<typeof trpc.nation.getPersonnelInfo.query>>;
type GeneralEntry = PersonnelResponse['generals'][number];
@@ -213,7 +214,10 @@ onMounted(() => void loadPersonnel());
<td
class="nation-heading"
colspan="6"
:style="{ color: '#fff', backgroundColor: data.nation.color }"
:style="{
color: legacyNationTextColor(data.nation.color),
backgroundColor: data.nation.color,
}"
>
{{ data.nation.name }}
</td>
@@ -415,10 +419,22 @@ onMounted(() => void loadPersonnel());
<td colspan="5" class="region-heading"> {{ regionMap[city.region] ?? '-' }} </td>
</tr>
<tr>
<td class="nation-city" :style="{ backgroundColor: data.nation.color }">
<td
class="nation-city"
:style="{
backgroundColor: data.nation.color,
color: legacyNationTextColor(data.nation.color),
}"
>
{{ cityLevelMap[city.level] ?? '-' }}
</td>
<td class="nation-city city-name" :style="{ backgroundColor: data.nation.color }">
<td
class="nation-city city-name"
:style="{
backgroundColor: data.nation.color,
color: legacyNationTextColor(data.nation.color),
}"
>
{{ city.name }}
</td>
<td
@@ -3,6 +3,8 @@ import { computed, onMounted, reactive, ref } from 'vue';
import { trpc } from '../utils/trpc';
import { resolveDiplomacyInfo } from '../utils/diplomacy';
import { legacyNationTextColor } from '../utils/legacyNationColor';
import LegacyHtmlEditor from '../components/ui/LegacyHtmlEditor.vue';
type StratFinanResponse = Awaited<ReturnType<typeof trpc.nation.getStratFinan.query>>;
type NationEntry = StratFinanResponse['nationsList'][number];
@@ -194,7 +196,9 @@ onMounted(() => void loadStratFinan());
<div>종료 시점</div>
</div>
<div v-for="nation in nationsList" :key="nation.id" class="diplomacy-row">
<div :style="{ backgroundColor: nation.color }">{{ nation.name }}</div>
<div :style="{ backgroundColor: nation.color, color: legacyNationTextColor(nation.color) }">
{{ nation.name }}
</div>
<div>{{ formatNumber(nation.power) }}</div>
<div>{{ formatNumber(nation.generalCount) }}</div>
<div>{{ formatNumber(nation.cityCount) }}</div>
@@ -245,7 +249,7 @@ onMounted(() => void loadStratFinan());
</span>
</header>
<div v-if="!editingNationMsg" class="message-preview" v-html="nationMsg || '내용 없음'" />
<textarea v-else v-model="nationMsgDraft" aria-label="국가 방침" maxlength="16384" />
<LegacyHtmlEditor v-else v-model="nationMsgDraft" :max-length="16384" />
</section>
<section id="scout-message-form" class="message-form">
<header class="green-header">
@@ -279,7 +283,7 @@ onMounted(() => void loadStratFinan());
</header>
<div class="scout-limit">870px x 200px를 넘어서는 내용은 표시되지 않습니다.</div>
<div v-if="!editingScoutMsg" class="message-preview scout-preview" v-html="scoutMsg || '내용 없음'" />
<textarea v-else v-model="scoutMsgDraft" class="scout-editor" aria-label="임관 권유" maxlength="1000" />
<LegacyHtmlEditor v-else v-model="scoutMsgDraft" :max-length="1000" />
</section>
<div class="finance-title">예산&amp;정책</div>