암행부 API가 앞 5턴의 action과 args를 함께 반환하도록 수정한다.\n개인턴·사령부 공용 formatter를 암행부와 세력도시 연동 표에 적용하고 Chromium 회귀를 추가한다.
715 lines
31 KiB
Vue
715 lines
31 KiB
Vue
<script setup lang="ts">
|
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
|
import type { CommandTable } from '../components/command/types';
|
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
|
import { getNpcColor } from '../utils/npcColor';
|
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
|
import { cityLevelMap, regionMap } from '../utils/nationFormat';
|
|
import { trpc } from '../utils/trpc';
|
|
|
|
type Result = Awaited<ReturnType<typeof trpc.nation.getCityOverview.query>>;
|
|
type SecretResult = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
|
type PersonnelResult = Awaited<ReturnType<typeof trpc.nation.getPersonnelInfo.query>>;
|
|
type City = Result['cities'][number];
|
|
type SecretGeneral = SecretResult['generals'][number];
|
|
type ReservedCommand = SecretGeneral['reservedCommands'][number];
|
|
type OfficerLevel = 2 | 3 | 4;
|
|
type Sort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
const data = ref<Result | null>(null);
|
|
const secretData = ref<SecretResult | null>(null);
|
|
const commandTable = ref<CommandTable | null>(null);
|
|
const personnelData = ref<PersonnelResult | null>(null);
|
|
const error = ref('');
|
|
const integrationError = ref('');
|
|
const secretLoading = ref(false);
|
|
const personnelLoading = ref(false);
|
|
const pendingAppointment = ref('');
|
|
const sort = ref<Sort>(10);
|
|
const extraSort = ref<
|
|
| 'name'
|
|
| 'populationRate'
|
|
| 'populationRemain'
|
|
| 'agricultureRemain'
|
|
| 'commerceRemain'
|
|
| 'securityRemain'
|
|
| 'defenceRemain'
|
|
| 'wallRemain'
|
|
| 'generalCount'
|
|
| null
|
|
>(null);
|
|
const router = useRouter();
|
|
const { error: showErrorToast, info: showInfoToast, success: showSuccessToast } = useGameFeedback();
|
|
const options = ['기본', '인구', '인구율', '민심', '농업', '상업', '치안', '수비', '성벽', '시세', '지역', '규모'];
|
|
const officerLabels: Record<OfficerLevel, string> = { 4: '태수', 3: '군사', 2: '종사' };
|
|
const generalsForCity = (cityId: number) => data.value?.generals.filter((general) => general.cityId === cityId) ?? [];
|
|
const secretGeneralsForCity = (cityId: number) =>
|
|
secretData.value?.generals.filter((general) => general.cityId === cityId) ?? [];
|
|
const displayGeneralName = (general: Result['generals'][number]) =>
|
|
general.npcState > 0 && !/^[ⓜⓝ]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
|
const displaySecretGeneralName = (general: SecretGeneral) =>
|
|
general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
|
const generalCount = (cityId: number) =>
|
|
data.value?.generals.filter((general) => general.cityId === cityId).length ?? 0;
|
|
const cities = computed(() => {
|
|
const values = [...(data.value?.cities ?? [])];
|
|
if (extraSort.value) {
|
|
const key = extraSort.value;
|
|
return values.sort((a, b) => {
|
|
if (key === 'name') return a.name.localeCompare(b.name);
|
|
if (key === 'populationRate') return a.population / a.populationMax - b.population / b.populationMax;
|
|
if (key === 'populationRemain') return a.population - a.populationMax - (b.population - b.populationMax);
|
|
if (key === 'agricultureRemain')
|
|
return a.agriculture - a.agricultureMax - (b.agriculture - b.agricultureMax);
|
|
if (key === 'commerceRemain') return a.commerce - a.commerceMax - (b.commerce - b.commerceMax);
|
|
if (key === 'securityRemain') return a.security - a.securityMax - (b.security - b.securityMax);
|
|
if (key === 'defenceRemain') return a.defence - a.defenceMax - (b.defence - b.defenceMax);
|
|
if (key === 'wallRemain') return a.wall - a.wallMax - (b.wall - b.wallMax);
|
|
return generalCount(b.id) - generalCount(a.id);
|
|
});
|
|
}
|
|
return values.sort((a, b) => {
|
|
const key = sort.value;
|
|
if (key === 1) return a.id - b.id;
|
|
if (key === 2) return b.population - a.population;
|
|
if (key === 3) return b.population / b.populationMax - a.population / a.populationMax;
|
|
if (key === 4) return b.trust - a.trust;
|
|
if (key === 5) return b.agriculture - a.agriculture;
|
|
if (key === 6) return b.commerce - a.commerce;
|
|
if (key === 7) return b.security - a.security;
|
|
if (key === 8) return b.defence - a.defence;
|
|
if (key === 9) return b.wall - a.wall;
|
|
if (key === 10) return (b.trade ?? -1) - (a.trade ?? -1);
|
|
if (key === 11) return a.region - b.region || b.level - a.level;
|
|
return b.level - a.level || a.region - b.region;
|
|
});
|
|
});
|
|
const setExtraSort = (value: NonNullable<typeof extraSort.value>) => {
|
|
extraSort.value = value;
|
|
};
|
|
const remain = (value: number, maximum: number) => value - maximum;
|
|
const warnRemain = (
|
|
kind: 'agriculture' | 'commerce' | 'security' | 'defence' | 'wall',
|
|
value: number,
|
|
maximum: number
|
|
) => {
|
|
const threshold = kind === 'defence' || kind === 'wall' ? -700 : -1000;
|
|
return remain(value, maximum) > threshold;
|
|
};
|
|
const developmentClass = (
|
|
kind: 'population' | 'agriculture' | 'commerce' | 'security' | 'defence' | 'wall',
|
|
value: number,
|
|
maximum: number
|
|
) => {
|
|
const ratio = value / maximum;
|
|
if (kind === 'population')
|
|
return ratio > 0.9 ? 'development-high' : ratio > 0.7 ? 'development-mid' : 'development-low';
|
|
if (kind === 'defence' || kind === 'wall')
|
|
return ratio > 0.6 ? 'development-high' : ratio > 0.3 ? 'development-mid' : 'development-low';
|
|
return ratio > 0.8 ? 'development-high' : ratio > 0.4 ? 'development-mid' : 'development-low';
|
|
};
|
|
const isRegionBreak = (city: City, index: number) =>
|
|
sort.value === 10 && extraSort.value === null && (index === 0 || cities.value[index - 1]?.region !== city.region);
|
|
const officer = (city: City, level: 2 | 3 | 4) => city.officers[level]?.name ?? '-';
|
|
const officerIsStationed = (city: City, level: OfficerLevel): boolean =>
|
|
secretData.value !== null && city.officers[level]?.cityId === city.id;
|
|
const personnelGeneralMap = computed(
|
|
() => new Map((personnelData.value?.generals ?? []).map((general) => [general.id, general]))
|
|
);
|
|
const personnelCityMap = computed(
|
|
() => new Map((personnelData.value?.cityAssignments ?? []).map((city) => [city.id, city]))
|
|
);
|
|
const officerLocked = (cityId: number, level: OfficerLevel): boolean => {
|
|
const officerSet = personnelCityMap.value.get(cityId)?.officerSet ?? 0;
|
|
return (officerSet & (1 << level)) !== 0;
|
|
};
|
|
const canAppoint = (cityId: number, generalId: number, level: OfficerLevel): boolean => {
|
|
if (!personnelData.value?.me.canManage || officerLocked(cityId, level)) return false;
|
|
const general = personnelGeneralMap.value.get(generalId);
|
|
if (!general || general.officerLevel === 12) return false;
|
|
if (level === 4) return general.stats.strength >= personnelData.value.chiefStatMin;
|
|
if (level === 3) return general.stats.intelligence >= personnelData.value.chiefStatMin;
|
|
return true;
|
|
};
|
|
const canShowAppointmentButtons = (generalId: number): boolean => {
|
|
const general = personnelGeneralMap.value.get(generalId);
|
|
return personnelData.value?.me.canManage === true && general !== undefined && general.officerLevel !== 12;
|
|
};
|
|
const isChief = (generalId: number): boolean => (personnelGeneralMap.value.get(generalId)?.officerLevel ?? 0) >= 5;
|
|
const appointmentKey = (cityId: number, generalId: number, level: OfficerLevel): string =>
|
|
`${cityId}:${generalId}:${level}`;
|
|
const commandNeedsAttention = (city: City, command: string): boolean => {
|
|
const normalized = command.replaceAll(/\s/gu, '');
|
|
if (normalized.includes('정착장려')) {
|
|
return city.population - city.populationMax > -20_000 || city.population > city.populationMax * 0.92;
|
|
}
|
|
if (normalized.includes('농지개간')) return city.agriculture - city.agricultureMax > -1_000;
|
|
if (normalized.includes('상업투자')) return city.commerce - city.commerceMax > -1_000;
|
|
if (normalized.includes('치안강화')) return city.security - city.securityMax > -1_000;
|
|
if (normalized.includes('수비강화')) return city.defence - city.defenceMax > -700;
|
|
if (normalized.includes('성벽보수')) return city.wall - city.wallMax > -700;
|
|
return false;
|
|
};
|
|
const commandBrief = (command: ReservedCommand): string =>
|
|
formatReservedCommandBrief('general', command.action, command.args, commandTable.value);
|
|
|
|
const loadSecretIntegration = async (): Promise<void> => {
|
|
if (secretLoading.value) {
|
|
showInfoToast('암행부 정보를 불러오는 중입니다.');
|
|
return;
|
|
}
|
|
if (secretData.value) {
|
|
showInfoToast('암행부 정보가 이미 연동되어 있습니다.');
|
|
return;
|
|
}
|
|
secretLoading.value = true;
|
|
integrationError.value = '';
|
|
try {
|
|
const secret = await trpc.nation.getSecretGeneralList.query();
|
|
const table = await trpc.turns.getCommandTable.query({ generalId: secret.viewer.generalId });
|
|
secretData.value = secret;
|
|
commandTable.value = table;
|
|
} catch (cause) {
|
|
integrationError.value = cause instanceof Error ? cause.message : '암행부 연동에 실패했습니다.';
|
|
showErrorToast(integrationError.value);
|
|
} finally {
|
|
secretLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadPersonnelIntegration = async (): Promise<void> => {
|
|
if (personnelLoading.value) {
|
|
showInfoToast('인사부 정보를 불러오는 중입니다.');
|
|
return;
|
|
}
|
|
if (personnelData.value?.me.canManage) {
|
|
showInfoToast('인사부 정보가 이미 연동되어 있습니다.');
|
|
return;
|
|
}
|
|
personnelLoading.value = true;
|
|
integrationError.value = '';
|
|
try {
|
|
const personnel = await trpc.nation.getPersonnelInfo.query();
|
|
if (!personnel.me.canManage) {
|
|
window.alert('수뇌가 아닙니다!');
|
|
return;
|
|
}
|
|
personnelData.value = personnel;
|
|
} catch (cause) {
|
|
integrationError.value = cause instanceof Error ? cause.message : '인사부 연동에 실패했습니다.';
|
|
showErrorToast(integrationError.value);
|
|
} finally {
|
|
personnelLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const refreshIntegratedData = async (): Promise<void> => {
|
|
const [overview, secret, personnel] = await Promise.all([
|
|
trpc.nation.getCityOverview.query(),
|
|
trpc.nation.getSecretGeneralList.query(),
|
|
trpc.nation.getPersonnelInfo.query(),
|
|
]);
|
|
data.value = overview;
|
|
secretData.value = secret;
|
|
personnelData.value = personnel;
|
|
};
|
|
|
|
const appointCityOfficer = async (city: City, general: SecretGeneral, level: OfficerLevel): Promise<void> => {
|
|
if (!canAppoint(city.id, general.id, level)) return;
|
|
const key = appointmentKey(city.id, general.id, level);
|
|
if (pendingAppointment.value) {
|
|
showInfoToast('다른 임명을 처리하는 중입니다.');
|
|
return;
|
|
}
|
|
if (isChief(general.id) && !window.confirm('수뇌입니다. 임명할까요?')) return;
|
|
|
|
pendingAppointment.value = key;
|
|
integrationError.value = '';
|
|
try {
|
|
await trpc.nation.appoint.mutate({
|
|
destGeneralId: general.id,
|
|
destCityId: city.id,
|
|
officerLevel: level,
|
|
});
|
|
showSuccessToast(`${general.name}을(를) ${city.name} ${officerLabels[level]}로 임명했습니다.`);
|
|
try {
|
|
await refreshIntegratedData();
|
|
} catch (cause) {
|
|
integrationError.value =
|
|
cause instanceof Error
|
|
? `임명은 완료됐지만 화면을 갱신하지 못했습니다: ${cause.message}`
|
|
: '임명은 완료됐지만 화면을 갱신하지 못했습니다.';
|
|
showErrorToast(integrationError.value);
|
|
}
|
|
} catch (cause) {
|
|
integrationError.value = cause instanceof Error ? cause.message : '임명에 실패했습니다.';
|
|
showErrorToast(integrationError.value);
|
|
} finally {
|
|
pendingAppointment.value = '';
|
|
}
|
|
};
|
|
onMounted(async () => {
|
|
try {
|
|
data.value = await trpc.nation.getCityOverview.query();
|
|
} catch (cause) {
|
|
error.value = cause instanceof Error ? cause.message : '세력 도시를 불러오지 못했습니다.';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<main class="nation-cities-page">
|
|
<table class="legacy-table legacy-bg0 title">
|
|
<tbody>
|
|
<tr>
|
|
<td>
|
|
세 력 도 시<br /><button class="back-button" type="button" @click="router.push('/')">
|
|
돌아가기
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<form @submit.prevent="extraSort = null">
|
|
정렬순서 :
|
|
<select v-model.number="sort">
|
|
<option v-for="(label, index) in options" :key="label" :value="index + 1">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
<input type="submit" value="정렬하기" />
|
|
<button type="button" :aria-busy="secretLoading" @click="loadSecretIntegration">
|
|
암행부 연동
|
|
</button>
|
|
<button
|
|
v-if="secretData"
|
|
id="load-duty-button"
|
|
type="button"
|
|
:aria-busy="personnelLoading"
|
|
@click="loadPersonnelIntegration"
|
|
>
|
|
인사부 연동
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="sort-more">
|
|
재 정렬 순서 :
|
|
<button type="button" @click="setExtraSort('name')">도시명</button>
|
|
<button type="button" @click="setExtraSort('populationRate')">인구율</button>
|
|
<button type="button" @click="setExtraSort('populationRemain')">남은 주민</button>
|
|
<button type="button" @click="setExtraSort('agricultureRemain')">남은 농업</button>
|
|
<button type="button" @click="setExtraSort('commerceRemain')">남은 상업</button>
|
|
<button type="button" @click="setExtraSort('securityRemain')">남은 치안</button>
|
|
<button type="button" @click="setExtraSort('defenceRemain')">남은 수비</button>
|
|
<button type="button" @click="setExtraSort('wallRemain')">남은 성벽</button>
|
|
<button type="button" @click="setExtraSort('generalCount')">배치 장수 수</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p v-if="error" class="error" role="alert">{{ error }}</p>
|
|
<p v-if="integrationError" class="error integration-error" role="alert">{{ integrationError }}</p>
|
|
<table
|
|
v-for="(city, index) in cities"
|
|
:key="city.id"
|
|
class="legacy-table city legacy-bg2"
|
|
:class="{ 'region-break': isRegionBreak(city, index) }"
|
|
:data-city-id="city.id"
|
|
>
|
|
<tbody>
|
|
<tr>
|
|
<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
|
|
}}</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>주민</th>
|
|
<td :class="developmentClass('population', city.population, city.populationMax)">
|
|
{{ city.population }}/{{ city.populationMax }}
|
|
</td>
|
|
<th>인구율</th>
|
|
<td :class="developmentClass('population', city.population, city.populationMax)">
|
|
{{ Number(((city.population / city.populationMax) * 100).toFixed(2)) }}%
|
|
</td>
|
|
<th>자금 수입</th>
|
|
<td>{{ city.incomes.gold.toLocaleString() }}</td>
|
|
<th>군량 수입</th>
|
|
<td>{{ city.incomes.rice.toLocaleString() }}</td>
|
|
<th>둔전 수입</th>
|
|
<td>{{ city.incomes.wall.toLocaleString() }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>농업</th>
|
|
<td :class="developmentClass('agriculture', city.agriculture, city.agricultureMax)">
|
|
{{ city.agriculture }}/{{ city.agricultureMax
|
|
}}<span v-if="warnRemain('agriculture', city.agriculture, city.agricultureMax)" class="remain"
|
|
>[{{ remain(city.agriculture, city.agricultureMax) }}]</span
|
|
>
|
|
</td>
|
|
<th>상업</th>
|
|
<td :class="developmentClass('commerce', city.commerce, city.commerceMax)">
|
|
{{ city.commerce }}/{{ city.commerceMax
|
|
}}<span v-if="warnRemain('commerce', city.commerce, city.commerceMax)" class="remain"
|
|
>[{{ remain(city.commerce, city.commerceMax) }}]</span
|
|
>
|
|
</td>
|
|
<th>치안</th>
|
|
<td :class="developmentClass('security', city.security, city.securityMax)">
|
|
{{ city.security }}/{{ city.securityMax
|
|
}}<span v-if="warnRemain('security', city.security, city.securityMax)" class="remain"
|
|
>[{{ remain(city.security, city.securityMax) }}]</span
|
|
>
|
|
</td>
|
|
<th>수비</th>
|
|
<td :class="developmentClass('defence', city.defence, city.defenceMax)">
|
|
{{ city.defence }}/{{ city.defenceMax
|
|
}}<span v-if="warnRemain('defence', city.defence, city.defenceMax)" class="remain"
|
|
>[{{ remain(city.defence, city.defenceMax) }}]</span
|
|
>
|
|
</td>
|
|
<th>성벽</th>
|
|
<td :class="developmentClass('wall', city.wall, city.wallMax)">
|
|
{{ city.wall }}/{{ city.wallMax
|
|
}}<span v-if="warnRemain('wall', city.wall, city.wallMax)" class="remain"
|
|
>[{{ remain(city.wall, city.wallMax) }}]</span
|
|
>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>민심</th>
|
|
<td>{{ city.trust.toFixed(1) }}</td>
|
|
<th>시세</th>
|
|
<td>{{ city.trade ?? '-' }}%</td>
|
|
<th>태수</th>
|
|
<td class="officer-4-value" :class="{ 'effective-officer': officerIsStationed(city, 4) }">
|
|
{{ officer(city, 4) }}
|
|
</td>
|
|
<th>군사</th>
|
|
<td class="officer-3-value" :class="{ 'effective-officer': officerIsStationed(city, 3) }">
|
|
{{ officer(city, 3) }}
|
|
</td>
|
|
<th>종사</th>
|
|
<td class="officer-2-value" :class="{ 'effective-officer': officerIsStationed(city, 2) }">
|
|
{{ officer(city, 2) }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>장수</th>
|
|
<td colspan="9" class="general-list">
|
|
<template v-if="generalsForCity(city.id).length">
|
|
<template v-for="(general, cityGeneralIndex) in generalsForCity(city.id)" :key="general.id">
|
|
<span v-if="cityGeneralIndex">, </span
|
|
><span :style="{ color: getNpcColor(general.npcState) }">{{
|
|
displayGeneralName(general)
|
|
}}</span>
|
|
</template>
|
|
</template>
|
|
<template v-else>-</template>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="secretData" class="secret-integration-row">
|
|
<td colspan="10">
|
|
<table class="city-user-table legacy-bg0">
|
|
<colgroup>
|
|
<col class="secret-name-column" />
|
|
<col class="secret-stat-column" />
|
|
<col class="secret-troop-column" />
|
|
<col class="secret-gold-column" />
|
|
<col class="secret-rice-column" />
|
|
<col class="secret-defence-column" />
|
|
<col class="secret-crew-type-column" />
|
|
<col class="secret-crew-column" />
|
|
<col class="secret-train-column" />
|
|
<col class="secret-atmos-column" />
|
|
<col class="secret-command-column" />
|
|
<col class="secret-kill-column" />
|
|
<col class="secret-turn-column" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>이 름</th>
|
|
<th>통무지</th>
|
|
<th>부 대</th>
|
|
<th>자 금</th>
|
|
<th>군 량</th>
|
|
<th>守</th>
|
|
<th>병 종</th>
|
|
<th>병 사</th>
|
|
<th>훈련</th>
|
|
<th>사기</th>
|
|
<th>명 령</th>
|
|
<th>삭턴</th>
|
|
<th>턴</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="general in secretGeneralsForCity(city.id)"
|
|
:key="general.id"
|
|
:data-general-id="general.id"
|
|
>
|
|
<td class="secret-name-cell">
|
|
<span :style="{ color: getNpcColor(general.npcState) }">{{
|
|
displaySecretGeneralName(general)
|
|
}}</span
|
|
><br />Lv {{ general.experienceLevel }}
|
|
<template v-if="canShowAppointmentButtons(general.id)">
|
|
<br class="for-duty" />
|
|
<button
|
|
v-for="level in [4, 3, 2] as const"
|
|
:key="level"
|
|
type="button"
|
|
class="appointment-button for-duty"
|
|
:class="[`mode-${level}`, { 'chief-target': isChief(general.id) }]"
|
|
:disabled="
|
|
!canAppoint(city.id, general.id, level) || pendingAppointment !== ''
|
|
"
|
|
:aria-label="`${general.name}을(를) ${city.name} ${officerLabels[level]}로 임명`"
|
|
@click="appointCityOfficer(city, general, level)"
|
|
>
|
|
{{ officerLabels[level].slice(0, 1) }}
|
|
</button>
|
|
</template>
|
|
</td>
|
|
<td :class="{ injured: general.injury > 0 }">
|
|
{{ general.stats.leadership
|
|
}}<span v-if="general.leadershipBonus" class="bonus"
|
|
>+{{ general.leadershipBonus }}</span
|
|
>∥{{ general.stats.strength }}∥{{ general.stats.intelligence }}
|
|
</td>
|
|
<td>{{ general.troopName ?? '-' }}</td>
|
|
<td>{{ general.gold }}</td>
|
|
<td>{{ general.rice }}</td>
|
|
<td>{{ general.defenceTrainText }}</td>
|
|
<td>{{ general.crewTypeName }}</td>
|
|
<td>{{ general.crew }}</td>
|
|
<td>{{ general.train }}</td>
|
|
<td>{{ general.atmos }}</td>
|
|
<td class="secret-commands">
|
|
<template v-if="general.npcState >= 2">NPC 장수</template>
|
|
<template v-else>
|
|
<div
|
|
v-for="(command, commandIndex) in general.reservedCommands"
|
|
:key="commandIndex"
|
|
:title="commandBrief(command)"
|
|
>
|
|
{{ commandIndex + 1 }} :
|
|
<span
|
|
:class="{
|
|
'command-attention': commandNeedsAttention(
|
|
city,
|
|
commandBrief(command)
|
|
),
|
|
}"
|
|
>{{ commandBrief(command) }}</span
|
|
>
|
|
</div>
|
|
</template>
|
|
</td>
|
|
<td>{{ general.killTurn }}</td>
|
|
<td>{{ formatServerDateTime(general.turnTime, { format: 'minuteSecond' }) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table class="legacy-table legacy-bg0 title footer">
|
|
<tbody>
|
|
<tr>
|
|
<td><button class="back-button" type="button" @click="router.push('/')">돌아가기</button></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="legacy-banner">
|
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
|
<a href="mailto:hided62@gmail.com">HideD(hided62@gmail.com)</a> /
|
|
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.nation-cities-page {
|
|
width: 1000px;
|
|
margin: 0 auto;
|
|
font-size: 14px;
|
|
line-height: 1.3;
|
|
}
|
|
.legacy-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background-color: transparent;
|
|
}
|
|
.legacy-table td,
|
|
.legacy-table th {
|
|
border: 1px solid #808080;
|
|
padding: 0;
|
|
font-weight: 400;
|
|
}
|
|
.title {
|
|
text-align: left;
|
|
}
|
|
.city {
|
|
margin-top: 0;
|
|
}
|
|
.city.region-break {
|
|
margin-top: 18px;
|
|
}
|
|
.city th {
|
|
width: 60px;
|
|
text-align: center;
|
|
background-image: var(--sammo-texture-green);
|
|
}
|
|
.city td {
|
|
width: 140px;
|
|
text-align: center;
|
|
}
|
|
.city-title {
|
|
text-align: left !important;
|
|
}
|
|
.general-list {
|
|
text-align: left !important;
|
|
}
|
|
.effective-officer {
|
|
color: lightgreen;
|
|
}
|
|
.secret-integration-row > td {
|
|
padding: 0;
|
|
}
|
|
.city-user-table {
|
|
width: 940px;
|
|
margin: 0 auto;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
.city-user-table td,
|
|
.city-user-table th {
|
|
width: auto;
|
|
border: 1px solid #808080;
|
|
padding: 0;
|
|
text-align: center;
|
|
word-break: break-all;
|
|
}
|
|
.city-user-table th {
|
|
background-image: var(--sammo-texture-green);
|
|
}
|
|
.secret-name-column,
|
|
.secret-stat-column,
|
|
.secret-troop-column {
|
|
width: 100px;
|
|
}
|
|
.secret-gold-column,
|
|
.secret-rice-column,
|
|
.secret-crew-type-column,
|
|
.secret-crew-column,
|
|
.secret-kill-column,
|
|
.secret-turn-column {
|
|
width: 60px;
|
|
}
|
|
.secret-defence-column {
|
|
width: 30px;
|
|
}
|
|
.secret-train-column,
|
|
.secret-atmos-column {
|
|
width: 50px;
|
|
}
|
|
.secret-command-column {
|
|
width: 150px;
|
|
}
|
|
.secret-name-cell {
|
|
line-height: normal;
|
|
}
|
|
.secret-commands {
|
|
text-align: left !important;
|
|
font-size: 12px;
|
|
}
|
|
.bonus {
|
|
color: cyan;
|
|
}
|
|
.injured {
|
|
color: red;
|
|
}
|
|
.command-attention {
|
|
color: yellow;
|
|
}
|
|
.nation-cities-page .appointment-button {
|
|
margin: 0;
|
|
padding: 1px 4px;
|
|
}
|
|
.nation-cities-page .appointment-button.chief-target:not(:disabled) {
|
|
color: red;
|
|
}
|
|
.nation-cities-page .appointment-button:disabled {
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
cursor: default;
|
|
}
|
|
.capital {
|
|
color: #0ff;
|
|
}
|
|
.footer {
|
|
margin-top: 0;
|
|
}
|
|
.nation-cities-page button,
|
|
.nation-cities-page input[type='submit'] {
|
|
border: 2px outset #fff;
|
|
background-color: buttonface;
|
|
color: buttontext;
|
|
cursor: pointer;
|
|
padding: 1px 6px;
|
|
}
|
|
.nation-cities-page .back-button {
|
|
border: 0;
|
|
padding: 5.25px 10.5px;
|
|
background-color: rgb(55 90 127);
|
|
color: #fff;
|
|
font-weight: 700;
|
|
line-height: 21px;
|
|
}
|
|
.sort-more button {
|
|
margin: 0;
|
|
}
|
|
.development-high {
|
|
color: lightgreen;
|
|
}
|
|
.development-mid,
|
|
.remain {
|
|
color: yellow;
|
|
}
|
|
.development-low {
|
|
color: orangered;
|
|
}
|
|
.legacy-banner a {
|
|
color: inherit;
|
|
}
|
|
.error {
|
|
text-align: center;
|
|
color: #ff7373;
|
|
}
|
|
@media (max-width: 700px) {
|
|
.nation-cities-page {
|
|
width: 1000px;
|
|
transform-origin: top left;
|
|
}
|
|
}
|
|
</style>
|