feat: minimap overlay, fix dialog scroll, remove name plate

This commit is contained in:
hyoseung 2026-05-29 15:07:02 +09:00
parent 3dc9cdd7b2
commit e74f24d943

View File

@ -10,6 +10,7 @@
<div class="hud-left"> <div class="hud-left">
<button class="hud-btn" @click="showLog = !showLog" title="대화 로그">📜</button> <button class="hud-btn" @click="showLog = !showLog" title="대화 로그">📜</button>
<button class="hud-btn" @click="showStatus = !showStatus" title="캐릭터 상태">📊</button> <button class="hud-btn" @click="showStatus = !showStatus" title="캐릭터 상태">📊</button>
<button class="hud-btn" @click="showMinimap = !showMinimap" title="미니맵" v-if="savedMapData && !mapData" :class="{ 'hud-btn--active': showMinimap }">🗺</button>
</div> </div>
<div class="hud-center">{{ username }}</div> <div class="hud-center">{{ username }}</div>
<div class="hud-right"> <div class="hud-right">
@ -18,6 +19,36 @@
</div> </div>
</header> </header>
<div class="minimap-overlay" v-if="showMinimap && savedMapData && !mapData && !showLog && !showStatus">
<div class="minimap-header">
<span class="minimap-area-name">{{ savedMapData.areaName }}</span>
<button class="minimap-close" @click="showMinimap = false"></button>
</div>
<div class="minimap-canvas">
<svg class="minimap-svg" xmlns="http://www.w3.org/2000/svg">
<line
v-for="conn in minimapConnections" :key="conn.id"
:x1="`${conn.x1}%`" :y1="`${conn.y1}%`"
:x2="`${conn.x2}%`" :y2="`${conn.y2}%`"
class="minimap-line"
/>
</svg>
<div
v-for="loc in savedMapData.locations" :key="loc.id"
class="minimap-node"
:class="{
'minimap-node--current': loc.isCurrent,
'minimap-node--other': !loc.isCurrent
}"
:style="{ left: loc.x + '%', top: loc.y + '%' }"
:title="loc.name"
>
<span class="minimap-node-icon">{{ loc.emoji }}</span>
<span class="minimap-node-label">{{ loc.name }}</span>
</div>
</div>
</div>
<div class="status-panel" v-if="showStatus"> <div class="status-panel" v-if="showStatus">
<pre class="status-text">{{ statusText || '불러오는 중...' }}</pre> <pre class="status-text">{{ statusText || '불러오는 중...' }}</pre>
</div> </div>
@ -91,9 +122,6 @@
<div class="dialog-section" v-if="!mapData && !showLog && !showStatus"> <div class="dialog-section" v-if="!mapData && !showLog && !showStatus">
<div class="dialog-box" v-if="currentText || loading"> <div class="dialog-box" v-if="currentText || loading">
<div class="dialog-name-plate" v-if="speakerName && !loading">
<span class="dialog-name">{{ speakerName }}</span>
</div>
<div class="dialog-text-area"> <div class="dialog-text-area">
<pre class="dialog-text" v-if="!loading">{{ displayText }}</pre> <pre class="dialog-text" v-if="!loading">{{ displayText }}</pre>
<div class="loading-dots" v-else> <div class="loading-dots" v-else>
@ -150,8 +178,23 @@ const statusText = ref('')
const resetConfirm = ref(false) const resetConfirm = ref(false)
const logRef = ref<HTMLElement>() const logRef = ref<HTMLElement>()
const mapData = ref<RpgResult['mapData'] | null>(null) const mapData = ref<RpgResult['mapData'] | null>(null)
const savedMapData = ref<RpgResult['mapData'] | null>(null)
const showMinimap = ref(false)
const HISTORY_KEY = `rpg_history_${username.value}` const HISTORY_KEY = `rpg_history_${username.value}`
const MINIMAP_KEY = `rpg_minimap_${username.value}`
function saveMinimapData(data: RpgResult['mapData']) {
localStorage.setItem(MINIMAP_KEY, JSON.stringify(data))
savedMapData.value = data
}
function loadMinimapData() {
try {
const saved = localStorage.getItem(MINIMAP_KEY)
if (saved) savedMapData.value = JSON.parse(saved)
} catch { savedMapData.value = null }
}
function saveHistory() { function saveHistory() {
localStorage.setItem(HISTORY_KEY, JSON.stringify(history.value)) localStorage.setItem(HISTORY_KEY, JSON.stringify(history.value))
@ -242,6 +285,25 @@ const mapConnections = computed(() => {
.map(l => ({ id: `${current.id}-${l.id}`, x1: current.x, y1: current.y, x2: l.x, y2: l.y })) .map(l => ({ id: `${current.id}-${l.id}`, x1: current.x, y1: current.y, x2: l.x, y2: l.y }))
}) })
const minimapConnections = computed(() => {
if (!savedMapData.value) return []
const locs = savedMapData.value.locations
const seen = new Set<string>()
const lines: { id: string; x1: number; y1: number; x2: number; y2: number }[] = []
for (const loc of locs) {
for (const other of locs) {
if (loc.id === other.id) continue
const key = [loc.id, other.id].sort().join('-')
if (seen.has(key)) continue
if (loc.isConnected && other.isCurrent || other.isConnected && loc.isCurrent) {
seen.add(key)
lines.push({ id: key, x1: loc.x, y1: loc.y, x2: other.x, y2: other.y })
}
}
}
return lines
})
function onMapNodeClick(loc: MapLocation) { function onMapNodeClick(loc: MapLocation) {
if (loc.isCurrent || loading.value) return if (loc.isCurrent || loading.value) return
if (!loc.isConnected) return if (!loc.isConnected) return
@ -257,6 +319,7 @@ function scrollLog() {
function applyResult(result: RpgResult, skipDuplicate = false) { function applyResult(result: RpgResult, skipDuplicate = false) {
mapData.value = result.mapData || null mapData.value = result.mapData || null
if (result.mapData) saveMinimapData(result.mapData)
if (skipDuplicate && history.value.length > 0) { if (skipDuplicate && history.value.length > 0) {
const last = history.value[history.value.length - 1]! const last = history.value[history.value.length - 1]!
if (last.type === 'story' && last.text === result.text) { if (last.type === 'story' && last.text === result.text) {
@ -348,11 +411,15 @@ function logout() {
router.push('/login') router.push('/login')
} }
onMounted(resumeGame) onMounted(() => {
loadMinimapData()
resumeGame()
})
</script> </script>
<style scoped> <style scoped>
.jrpg-wrap { .jrpg-wrap {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100dvh; height: 100dvh;
@ -454,6 +521,109 @@ onMounted(resumeGame)
border-color: rgba(180,140,60,0.7); border-color: rgba(180,140,60,0.7);
} }
.hud-btn--active {
background: rgba(180,140,60,0.28);
border-color: rgba(220,180,80,0.9);
}
/* ─── Minimap Overlay ─── */
.minimap-overlay {
position: absolute;
top: 56px;
left: 12px;
z-index: 20;
width: 210px;
background: rgba(6,4,15,0.94);
border: 1px solid rgba(180,140,60,0.5);
border-radius: 4px;
box-shadow: 0 4px 24px rgba(0,0,0,0.7), 0 0 0 1px rgba(0,0,0,0.6);
overflow: hidden;
}
.minimap-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: linear-gradient(90deg, rgba(180,140,60,0.18), rgba(100,70,20,0.12));
border-bottom: 1px solid rgba(180,140,60,0.3);
}
.minimap-area-name {
font-size: 11px;
color: #d4a830;
letter-spacing: 1px;
font-weight: 600;
}
.minimap-close {
background: none;
border: none;
color: rgba(180,140,60,0.6);
font-size: 12px;
cursor: pointer;
padding: 0 2px;
line-height: 1;
}
.minimap-close:hover { color: #d4a830; }
.minimap-canvas {
position: relative;
width: 100%;
height: 160px;
}
.minimap-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.minimap-line {
stroke: rgba(180,140,60,0.35);
stroke-width: 1.5;
stroke-dasharray: 4 3;
}
.minimap-node {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.minimap-node--current .minimap-node-icon {
font-size: 18px;
filter: drop-shadow(0 0 6px rgba(255,200,60,0.9));
animation: minimap-pulse 2s ease-in-out infinite;
}
.minimap-node--other .minimap-node-icon {
font-size: 14px;
opacity: 0.6;
}
@keyframes minimap-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
.minimap-node-label {
font-size: 9px;
color: rgba(220,200,150,0.75);
white-space: nowrap;
text-shadow: 0 1px 3px rgba(0,0,0,0.9);
}
.minimap-node--current .minimap-node-label {
color: #ffd870;
font-weight: 700;
}
/* ─── Status Panel ─── */ /* ─── Status Panel ─── */
.status-panel { .status-panel {
position: relative; position: relative;
@ -583,9 +753,8 @@ onMounted(resumeGame)
.dialog-section { .dialog-section {
position: relative; position: relative;
z-index: 10; z-index: 10;
flex: 0 1 auto; flex-shrink: 0;
min-height: 0; min-height: 0;
overflow: hidden;
padding: 0 12px 12px; padding: 0 12px 12px;
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
display: flex; display: flex;
@ -599,6 +768,8 @@ onMounted(resumeGame)
/* ─── Dialog Box ─── */ /* ─── Dialog Box ─── */
.dialog-box { .dialog-box {
position: relative; position: relative;
max-height: 48dvh;
overflow-y: auto;
background: linear-gradient(180deg, background: linear-gradient(180deg,
rgba(12,10,25,0.96) 0%, rgba(12,10,25,0.96) 0%,
rgba(8,6,18,0.98) 100% rgba(8,6,18,0.98) 100%
@ -665,10 +836,10 @@ onMounted(resumeGame)
/* ─── Choices ─── */ /* ─── Choices ─── */
.choices-frame { .choices-frame {
flex: 1 1 auto; flex-shrink: 0;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 40dvh;
background: linear-gradient(180deg, background: linear-gradient(180deg,
rgba(10,8,22,0.97) 0%, rgba(10,8,22,0.97) 0%,
rgba(6,4,15,0.99) 100% rgba(6,4,15,0.99) 100%