전투 시뮬레이터 구현
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user