feat: minimap overlay, fix dialog scroll, remove name plate
This commit is contained in:
parent
3dc9cdd7b2
commit
e74f24d943
@ -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%
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user