전투 시뮬레이터 구현

This commit is contained in:
2026-01-18 17:41:13 +00:00
parent 5b51aad6d0
commit 4647b67ba3
14 changed files with 2589 additions and 10 deletions
@@ -0,0 +1,433 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { BattleSimOptions, GeneralDraft } from '../../utils/battleSimulatorTypes';
interface Props {
general: GeneralDraft;
options: BattleSimOptions;
mode: 'attacker' | 'defender';
title: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(event: 'import'): void;
(event: 'save'): void;
(event: 'load', payload: { file: File }): void;
(event: 'copy'): void;
(event: 'remove'): void;
}>();
const isDefender = computed(() => props.mode === 'defender');
const fileInput = ref<HTMLInputElement | null>(null);
const triggerLoad = () => {
fileInput.value?.click();
};
const handleFileChange = (event: Event) => {
const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) {
return;
}
emit('load', { file });
if (target) {
target.value = '';
}
};
const officerLevelOptions = [
{ value: 1, label: '일반' },
{ value: 4, label: '태수' },
{ value: 3, label: '군사' },
{ value: 2, label: '종사' },
{ value: 10, label: '무장 수뇌' },
{ value: 9, label: '지장 수뇌' },
{ value: 11, label: '참모' },
{ value: 12, label: '군주' },
];
</script>
<template>
<section class="general-card">
<header class="general-header">
<div>
<div class="general-title">{{ title }}</div>
<div class="general-subtitle">No {{ general.no }}</div>
</div>
<div class="general-actions">
<button class="action" type="button" @click="emit('import')">서버에서 가져오기</button>
<button class="action" type="button" @click="emit('save')">저장</button>
<input ref="fileInput" type="file" accept=".json" hidden @change="handleFileChange" />
<button class="action" type="button" @click="triggerLoad">불러오기</button>
<button v-if="isDefender" class="action ghost" type="button" @click="emit('copy')">복제</button>
<button v-if="isDefender" class="action danger" type="button" @click="emit('remove')">제거</button>
</div>
</header>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>이름</span>
<input v-model="general.name" type="text" />
</label>
<label class="field">
<span>직위</span>
<select v-model.number="general.officerLevel">
<option v-for="option in officerLevelOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</label>
<label class="field">
<span>Level</span>
<input v-model.number="general.expLevel" type="number" min="0" max="300" />
</label>
</div>
</div>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>통솔</span>
<input v-model.number="general.leadership" type="number" min="1" max="300" />
</label>
<label class="field">
<span>무력</span>
<input v-model.number="general.strength" type="number" min="1" max="300" />
</label>
<label class="field">
<span>지력</span>
<input v-model.number="general.intel" type="number" min="1" max="300" />
</label>
</div>
</div>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>명마</span>
<select v-model="general.horse">
<option :value="null">-</option>
<option v-for="item in options.items.horse" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</select>
</label>
<label class="field">
<span>무기</span>
<select v-model="general.weapon">
<option :value="null">-</option>
<option v-for="item in options.items.weapon" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</select>
</label>
<label class="field">
<span>서적</span>
<select v-model="general.book">
<option :value="null">-</option>
<option v-for="item in options.items.book" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</select>
</label>
</div>
<div class="form-row">
<label class="field">
<span>부상</span>
<input v-model.number="general.injury" type="number" min="0" max="80" />
</label>
<label class="field">
<span>군량</span>
<input v-model.number="general.rice" type="number" min="50" max="40000" step="50" />
</label>
<label class="field">
<span>도구</span>
<select v-model="general.item">
<option :value="null">-</option>
<option v-for="item in options.items.item" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</select>
</label>
</div>
</div>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>병종</span>
<select v-model.number="general.crewtype">
<option v-for="crew in options.unitSet.crewTypes" :key="crew.id" :value="crew.id">
{{ crew.name }}
</option>
</select>
</label>
<label class="field">
<span>병사</span>
<input v-model.number="general.crew" type="number" min="100" step="100" />
</label>
<label class="field">
<span>성격</span>
<select v-model="general.personal">
<option :value="null">-</option>
<option v-for="trait in options.personalities" :key="trait.key" :value="trait.key">
{{ trait.name }}
</option>
</select>
</label>
</div>
</div>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>훈련</span>
<input
v-model.number="general.train"
type="number"
min="40"
:max="options.config.maxTrainByWar"
/>
</label>
<label class="field">
<span>사기</span>
<input
v-model.number="general.atmos"
type="number"
min="40"
:max="options.config.maxAtmosByWar"
/>
</label>
<label class="field">
<span>전특</span>
<select v-model="general.special2">
<option :value="null">-</option>
<option v-for="trait in options.warTraits" :key="trait.key" :value="trait.key">
{{ trait.name }}
</option>
</select>
</label>
</div>
</div>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>보병숙련</span>
<select v-model.number="general.dex1">
<option v-for="dex in options.dexLevels" :key="dex.value" :value="dex.value">
{{ dex.label }} ({{ dex.value.toLocaleString('ko-KR') }})
</option>
</select>
</label>
<label class="field">
<span>궁병숙련</span>
<select v-model.number="general.dex2">
<option v-for="dex in options.dexLevels" :key="dex.value" :value="dex.value">
{{ dex.label }} ({{ dex.value.toLocaleString('ko-KR') }})
</option>
</select>
</label>
<label class="field">
<span>기병숙련</span>
<select v-model.number="general.dex3">
<option v-for="dex in options.dexLevels" :key="dex.value" :value="dex.value">
{{ dex.label }} ({{ dex.value.toLocaleString('ko-KR') }})
</option>
</select>
</label>
</div>
<div class="form-row">
<label class="field">
<span>귀병숙련</span>
<select v-model.number="general.dex4">
<option v-for="dex in options.dexLevels" :key="dex.value" :value="dex.value">
{{ dex.label }} ({{ dex.value.toLocaleString('ko-KR') }})
</option>
</select>
</label>
<label class="field">
<span>차병숙련</span>
<select v-model.number="general.dex5">
<option v-for="dex in options.dexLevels" :key="dex.value" :value="dex.value">
{{ dex.label }} ({{ dex.value.toLocaleString('ko-KR') }})
</option>
</select>
</label>
<label v-if="isDefender" class="field">
<span>수비여부</span>
<select v-model.number="general.defenceTrain">
<option :value="90">훈사 90</option>
<option :value="80">훈사 80</option>
<option :value="60">훈사 60</option>
<option :value="40">훈사 40</option>
<option :value="999">안함</option>
</select>
</label>
</div>
</div>
<div class="form-block">
<div class="form-row">
<label class="field">
<span>전투 </span>
<input v-model.number="general.warnum" type="number" min="0" />
</label>
<label class="field">
<span>승리 </span>
<input v-model.number="general.killnum" type="number" min="0" />
</label>
<label class="field">
<span>사살 </span>
<input v-model.number="general.killcrew" type="number" min="0" />
</label>
</div>
</div>
<div class="form-block">
<div class="buff-title">유산 버프</div>
<div class="form-row buff-row">
<label class="field">
<span>회피 확률</span>
<input v-model.number="general.inheritBuff.warAvoidRatio" type="number" min="0" max="5" />
</label>
<label class="field">
<span>필살 확률</span>
<input v-model.number="general.inheritBuff.warCriticalRatio" type="number" min="0" max="5" />
</label>
<label class="field">
<span>계략 시도</span>
<input v-model.number="general.inheritBuff.warMagicTrialProb" type="number" min="0" max="5" />
</label>
</div>
<div class="form-row buff-row">
<label class="field">
<span>상대 회피</span>
<input
v-model.number="general.inheritBuff.warAvoidRatioOppose"
type="number"
min="0"
max="5"
/>
</label>
<label class="field">
<span>상대 필살</span>
<input
v-model.number="general.inheritBuff.warCriticalRatioOppose"
type="number"
min="0"
max="5"
/>
</label>
<label class="field">
<span>상대 계략</span>
<input
v-model.number="general.inheritBuff.warMagicTrialProbOppose"
type="number"
min="0"
max="5"
/>
</label>
</div>
</div>
</section>
</template>
<style scoped>
.general-card {
border: 1px solid rgba(201, 164, 90, 0.4);
background: rgba(10, 10, 10, 0.75);
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.general-header {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
border-bottom: 1px solid rgba(201, 164, 90, 0.3);
padding-bottom: 8px;
}
.general-title {
font-weight: 600;
font-size: 0.95rem;
}
.general-subtitle {
font-size: 0.7rem;
color: rgba(232, 221, 196, 0.6);
}
.general-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.action {
border: 1px solid rgba(201, 164, 90, 0.4);
background: rgba(16, 16, 16, 0.7);
color: inherit;
padding: 4px 10px;
font-size: 0.75rem;
cursor: pointer;
}
.action.ghost {
background: rgba(30, 30, 30, 0.7);
}
.action.danger {
border-color: rgba(233, 94, 94, 0.6);
color: #f0b6b6;
}
.form-block {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 8px;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.75rem;
color: rgba(232, 221, 196, 0.75);
}
.field input,
.field select {
background: rgba(6, 6, 6, 0.7);
border: 1px solid rgba(201, 164, 90, 0.35);
color: #e8ddc4;
padding: 4px 6px;
font-size: 0.8rem;
}
.buff-title {
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(201, 164, 90, 0.9);
}
.buff-row {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
</style>