feat: JRPG UI redesign (환세취호전 style), localStorage history persist
This commit is contained in:
parent
9b7a7f432f
commit
4c98358b87
@ -13,12 +13,14 @@ html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #0a0a0f;
|
||||
color: #e0d0ff;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@ -1,52 +1,86 @@
|
||||
<template>
|
||||
<div class="game-wrap">
|
||||
<header class="game-header">
|
||||
<span class="header-title">⚔️ 스티그마타</span>
|
||||
<div class="header-right">
|
||||
<span class="username">{{ username }}</span>
|
||||
<button class="btn-status" @click="showStatus = !showStatus" title="캐릭터 상태">📊</button>
|
||||
<button class="btn-reset" @click="confirmReset" title="초기화">🔄</button>
|
||||
<button class="btn-logout" @click="logout" title="로그아웃">🚪</button>
|
||||
<div class="jrpg-wrap">
|
||||
|
||||
<div class="scene-bg" :class="sceneBgClass">
|
||||
<div class="scene-vignette"></div>
|
||||
<div class="scene-title-deco">⚔ 스티그마타 ⚔</div>
|
||||
</div>
|
||||
|
||||
<header class="jrpg-hud">
|
||||
<div class="hud-left">
|
||||
<button class="hud-btn" @click="showLog = !showLog" title="대화 로그">📜</button>
|
||||
<button class="hud-btn" @click="showStatus = !showStatus" title="캐릭터 상태">📊</button>
|
||||
</div>
|
||||
<div class="hud-center">{{ username }}</div>
|
||||
<div class="hud-right">
|
||||
<button class="hud-btn" @click="confirmReset" title="초기화">🔄</button>
|
||||
<button class="hud-btn" @click="logout" title="로그아웃">🚪</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="status-panel" v-if="showStatus">
|
||||
<pre class="status-text">{{ statusText || '상태를 불러오는 중...' }}</pre>
|
||||
<pre class="status-text">{{ statusText || '불러오는 중...' }}</pre>
|
||||
</div>
|
||||
|
||||
<main class="game-main">
|
||||
<div class="story-area" ref="storyRef">
|
||||
<div v-for="(entry, i) in history" :key="i" class="story-entry" :class="entry.type">
|
||||
<pre class="story-text">{{ entry.text }}</pre>
|
||||
<div class="log-panel" v-if="showLog">
|
||||
<div class="log-inner" ref="logRef">
|
||||
<div v-for="(entry, i) in history" :key="i" class="log-entry" :class="entry.type">
|
||||
<pre class="log-text">{{ entry.text }}</pre>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-dots">
|
||||
</div>
|
||||
<button class="log-close" @click="showLog = false">닫기</button>
|
||||
</div>
|
||||
|
||||
<div class="scene-area" v-if="!showLog && !showStatus">
|
||||
<div class="scene-portrait-area">
|
||||
<div class="speaker-portrait" v-if="speakerEmoji">
|
||||
<span class="portrait-icon">{{ speakerEmoji }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section" v-if="!showLog && !showStatus">
|
||||
<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">
|
||||
<pre class="dialog-text" v-if="!loading">{{ displayText }}</pre>
|
||||
<div class="loading-dots" v-else>
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="choices-area" v-if="!loading && choices.length > 0">
|
||||
<button
|
||||
<div class="choices-frame" v-if="!loading && choices.length > 0">
|
||||
<div class="choices-inner">
|
||||
<div
|
||||
v-for="(choice, i) in choices"
|
||||
:key="i"
|
||||
class="choice-btn"
|
||||
class="choice-row"
|
||||
:class="{ hovered: hoveredChoice === i }"
|
||||
@mouseenter="hoveredChoice = i"
|
||||
@mouseleave="hoveredChoice = -1"
|
||||
@click="selectChoice(i + 1)"
|
||||
>
|
||||
<span class="choice-num">{{ numEmoji(i) }}</span>
|
||||
<span class="choice-text">{{ choice.text }}</span>
|
||||
</button>
|
||||
<span class="choice-cursor">{{ hoveredChoice === i ? '▶' : ' ' }}</span>
|
||||
<span class="choice-label">{{ choice.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="restart-area" v-if="!loading && choices.length === 0 && history.length > 0">
|
||||
<button class="btn-restart" @click="startGame">다시 시작</button>
|
||||
<div class="restart-frame" v-if="!loading && choices.length === 0 && history.length > 0">
|
||||
<button class="jrpg-btn-restart" @click="startGame">▶ 다시 시작</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal-overlay" v-if="resetConfirm" @click.self="resetConfirm = false">
|
||||
<div class="modal">
|
||||
<p>⚠️ 캐릭터를 초기화하면 모든 진행 상황이 삭제됩니다. 계속하시겠습니까?</p>
|
||||
<div class="jrpg-modal">
|
||||
<div class="jrpg-modal-title">⚠ 경고</div>
|
||||
<p>캐릭터를 초기화하면 모든 진행 상황이 삭제됩니다.<br>계속하시겠습니까?</p>
|
||||
<div class="modal-btns">
|
||||
<button @click="doReset" class="btn-danger">초기화</button>
|
||||
<button @click="resetConfirm = false">취소</button>
|
||||
<button class="jrpg-btn danger" @click="doReset">초기화</button>
|
||||
<button class="jrpg-btn" @click="resetConfirm = false">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -54,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, watch } from 'vue'
|
||||
import { ref, onMounted, nextTick, watch, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api, type RpgResult } from '../api'
|
||||
|
||||
@ -64,34 +98,130 @@ const history = ref<{ text: string; type: 'story' | 'choice' }[]>([])
|
||||
const choices = ref<{ text: string }[]>([])
|
||||
const loading = ref(false)
|
||||
const showStatus = ref(false)
|
||||
const showLog = ref(false)
|
||||
const statusText = ref('')
|
||||
const resetConfirm = ref(false)
|
||||
const storyRef = ref<HTMLElement>()
|
||||
const logRef = ref<HTMLElement>()
|
||||
const hoveredChoice = ref(-1)
|
||||
|
||||
const NUM_EMOJIS = ['1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣']
|
||||
function numEmoji(i: number) { return NUM_EMOJIS[i] || `${i+1}.` }
|
||||
const HISTORY_KEY = `rpg_history_${username.value}`
|
||||
|
||||
function scrollDown() {
|
||||
function saveHistory() {
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(history.value))
|
||||
}
|
||||
|
||||
function loadHistory() {
|
||||
try {
|
||||
const saved = localStorage.getItem(HISTORY_KEY)
|
||||
if (saved) history.value = JSON.parse(saved)
|
||||
} catch { history.value = [] }
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
localStorage.removeItem(HISTORY_KEY)
|
||||
history.value = []
|
||||
}
|
||||
|
||||
const currentText = computed(() => {
|
||||
for (let i = history.value.length - 1; i >= 0; i--) {
|
||||
if (history.value[i]!.type === 'story') return history.value[i]!.text
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const SPEAKER_MAP: Record<string, string> = {
|
||||
'키리엘': '🧝',
|
||||
'루카스': '⚔️',
|
||||
'라샤': '🐺',
|
||||
'노인': '👴',
|
||||
'여인': '👩',
|
||||
'병사': '🛡️',
|
||||
'상인': '💰',
|
||||
'마왕': '👿',
|
||||
'왕': '👑',
|
||||
'여왕': '👸',
|
||||
'마법사': '🧙',
|
||||
'성기사': '✝️',
|
||||
'어둠의 기사': '🗡️',
|
||||
}
|
||||
|
||||
const speakerName = computed(() => {
|
||||
const text = currentText.value
|
||||
if (!text) return ''
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^([가-힣a-zA-Z\s·]+):\s/)
|
||||
if (m && m[1] && m[1].trim().length < 12) return m[1].trim()
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const speakerEmoji = computed(() => {
|
||||
const name = speakerName.value
|
||||
if (!name) return '📖'
|
||||
for (const key of Object.keys(SPEAKER_MAP)) {
|
||||
if (name.includes(key)) return SPEAKER_MAP[key]
|
||||
}
|
||||
return '💬'
|
||||
})
|
||||
|
||||
const displayText = computed(() => {
|
||||
return currentText.value
|
||||
})
|
||||
|
||||
const sceneBgClass = computed(() => {
|
||||
const text = currentText.value.toLowerCase()
|
||||
if (text.includes('전투') || text.includes('공격') || text.includes('몬스터')) return 'scene-battle'
|
||||
if (text.includes('마을') || text.includes('광장') || text.includes('성문')) return 'scene-town'
|
||||
if (text.includes('숲') || text.includes('나무') || text.includes('초원')) return 'scene-forest'
|
||||
if (text.includes('성') || text.includes('왕궁') || text.includes('왕')) return 'scene-castle'
|
||||
if (text.includes('동굴') || text.includes('지하') || text.includes('던전')) return 'scene-dungeon'
|
||||
return 'scene-default'
|
||||
})
|
||||
|
||||
function scrollLog() {
|
||||
nextTick(() => {
|
||||
if (storyRef.value) storyRef.value.scrollTop = storyRef.value.scrollHeight
|
||||
if (logRef.value) logRef.value.scrollTop = logRef.value.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
function applyResult(result: RpgResult) {
|
||||
function applyResult(result: RpgResult, skipDuplicate = false) {
|
||||
if (skipDuplicate && history.value.length > 0) {
|
||||
const last = history.value[history.value.length - 1]!
|
||||
if (last.type === 'story' && last.text === result.text) {
|
||||
choices.value = result.choices || []
|
||||
return
|
||||
}
|
||||
}
|
||||
history.value.push({ text: result.text, type: 'story' })
|
||||
choices.value = result.choices || []
|
||||
scrollDown()
|
||||
saveHistory()
|
||||
scrollLog()
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
loading.value = true
|
||||
history.value = []
|
||||
clearHistory()
|
||||
choices.value = []
|
||||
try {
|
||||
const result = await api.rpgStart()
|
||||
applyResult(result)
|
||||
} catch (e: any) {
|
||||
history.value.push({ text: `❌ ${e.message}`, type: 'story' })
|
||||
history.value.push({ text: `오류: ${e.message}`, type: 'story' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeGame() {
|
||||
loading.value = true
|
||||
loadHistory()
|
||||
choices.value = []
|
||||
try {
|
||||
const result = await api.rpgStart()
|
||||
applyResult(result, true)
|
||||
} catch (e: any) {
|
||||
history.value.push({ text: `오류: ${e.message}`, type: 'story' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -103,13 +233,14 @@ async function selectChoice(choice: number) {
|
||||
history.value.push({ text: `▶ ${selected.text}`, type: 'choice' })
|
||||
choices.value = []
|
||||
loading.value = true
|
||||
scrollDown()
|
||||
saveHistory()
|
||||
try {
|
||||
const result = await api.rpgAction(choice)
|
||||
applyResult(result)
|
||||
} catch (e: any) {
|
||||
history.value.push({ text: `❌ ${e.message}`, type: 'story' })
|
||||
history.value.push({ text: `오류: ${e.message}`, type: 'story' })
|
||||
choices.value = []
|
||||
saveHistory()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -125,6 +256,7 @@ async function loadStatus() {
|
||||
}
|
||||
|
||||
watch(showStatus, (v) => { if (v) loadStatus() })
|
||||
watch(showLog, (v) => { if (v) scrollLog() })
|
||||
|
||||
function confirmReset() { resetConfirm.value = true }
|
||||
|
||||
@ -145,74 +277,120 @@ function logout() {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(startGame)
|
||||
onMounted(resumeGame)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.game-wrap {
|
||||
.jrpg-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Serif KR', serif;
|
||||
background: #0a0a0f;
|
||||
color: #e0d0ff;
|
||||
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||
color: #f0e6c8;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-header {
|
||||
/* ─── Background Scene ─── */
|
||||
.scene-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
transition: background 1.5s ease;
|
||||
}
|
||||
|
||||
.scene-default {
|
||||
background: radial-gradient(ellipse at 50% 30%, #1a1040 0%, #0a0810 60%, #000005 100%);
|
||||
}
|
||||
.scene-battle {
|
||||
background: radial-gradient(ellipse at 50% 50%, #3d0a0a 0%, #1a0505 55%, #070003 100%);
|
||||
}
|
||||
.scene-town {
|
||||
background: radial-gradient(ellipse at 50% 20%, #0d1a2e 0%, #060e1a 60%, #020508 100%);
|
||||
}
|
||||
.scene-forest {
|
||||
background: radial-gradient(ellipse at 50% 30%, #061a0a 0%, #030e05 60%, #010403 100%);
|
||||
}
|
||||
.scene-castle {
|
||||
background: radial-gradient(ellipse at 50% 20%, #1a1408 0%, #0d0b03 60%, #040300 100%);
|
||||
}
|
||||
.scene-dungeon {
|
||||
background: radial-gradient(ellipse at 50% 80%, #0d0d1a 0%, #050508 60%, #020204 100%);
|
||||
}
|
||||
|
||||
.scene-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at 50% 50%, transparent 30%, rgba(0,0,0,0.7) 100%);
|
||||
}
|
||||
|
||||
.scene-title-deco {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: clamp(14px, 4vw, 22px);
|
||||
color: rgba(180, 140, 60, 0.12);
|
||||
letter-spacing: 6px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ─── HUD Header ─── */
|
||||
.jrpg-hud {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
background: #13131f;
|
||||
border-bottom: 1px solid #2a1a4e;
|
||||
padding: 8px 14px;
|
||||
background: linear-gradient(180deg, rgba(10,8,20,0.95) 0%, rgba(10,8,20,0.7) 100%);
|
||||
border-bottom: 1px solid rgba(180,140,60,0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #c8a8ff;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.hud-left, .hud-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.username {
|
||||
.hud-center {
|
||||
font-size: 13px;
|
||||
color: #8877bb;
|
||||
margin-right: 4px;
|
||||
color: #c8a060;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.game-header button {
|
||||
background: transparent;
|
||||
border: 1px solid #2a1a4e;
|
||||
border-radius: 6px;
|
||||
color: #8877bb;
|
||||
.hud-btn {
|
||||
background: rgba(180,140,60,0.08);
|
||||
border: 1px solid rgba(180,140,60,0.3);
|
||||
border-radius: 4px;
|
||||
color: #c8a060;
|
||||
font-size: 16px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.game-header button:hover {
|
||||
background: #2a1a4e;
|
||||
color: #c8a8ff;
|
||||
.hud-btn:hover {
|
||||
background: rgba(180,140,60,0.2);
|
||||
border-color: rgba(180,140,60,0.7);
|
||||
}
|
||||
|
||||
/* ─── Status Panel ─── */
|
||||
.status-panel {
|
||||
background: #0d0d1a;
|
||||
border-bottom: 1px solid #2a1a4e;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background: rgba(10,8,20,0.95);
|
||||
border-bottom: 1px solid rgba(180,140,60,0.3);
|
||||
padding: 12px 20px;
|
||||
max-height: 200px;
|
||||
max-height: 40dvh;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -220,68 +398,265 @@ onMounted(startGame)
|
||||
.status-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #c8a8ff;
|
||||
color: #d4b878;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.game-main {
|
||||
/* ─── Log Panel ─── */
|
||||
.log-panel {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
min-height: 0;
|
||||
background: rgba(5,4,12,0.97);
|
||||
border-bottom: 1px solid rgba(180,140,60,0.3);
|
||||
}
|
||||
|
||||
.story-area {
|
||||
.log-inner {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px 0 12px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.story-entry.story .story-text {
|
||||
background: #13131f;
|
||||
border: 1px solid #2a1a4e;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
.log-entry.story .log-text {
|
||||
font-size: 14px;
|
||||
color: #e8dfc0;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #e0d0ff;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
background: rgba(180,140,60,0.05);
|
||||
border-left: 2px solid rgba(180,140,60,0.4);
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.story-entry.choice .story-text {
|
||||
text-align: right;
|
||||
.log-entry.choice .log-text {
|
||||
font-size: 12px;
|
||||
color: #8877bb;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #7744cc;
|
||||
font-style: italic;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
padding: 4px 4px;
|
||||
text-align: right;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-close {
|
||||
flex-shrink: 0;
|
||||
margin: 10px 20px 14px;
|
||||
background: rgba(180,140,60,0.12);
|
||||
border: 1px solid rgba(180,140,60,0.4);
|
||||
border-radius: 4px;
|
||||
color: #c8a060;
|
||||
padding: 8px 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.log-close:hover { background: rgba(180,140,60,0.25); }
|
||||
|
||||
/* ─── Scene / Portrait Area ─── */
|
||||
.scene-area {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scene-portrait-area {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.speaker-portrait {
|
||||
width: clamp(70px, 18vw, 110px);
|
||||
height: clamp(70px, 18vw, 110px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(180,140,60,0.5);
|
||||
background: radial-gradient(circle, rgba(180,140,60,0.12) 0%, rgba(10,8,20,0.6) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 24px rgba(180,140,60,0.2), inset 0 0 20px rgba(0,0,0,0.4);
|
||||
animation: portrait-float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes portrait-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
.portrait-icon {
|
||||
font-size: clamp(28px, 8vw, 48px);
|
||||
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.8));
|
||||
}
|
||||
|
||||
/* ─── Dialog Section ─── */
|
||||
.dialog-section {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
padding: 0 12px 12px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 760px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ─── Dialog Box ─── */
|
||||
.dialog-box {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(12,10,25,0.96) 0%,
|
||||
rgba(8,6,18,0.98) 100%
|
||||
);
|
||||
border: 1px solid rgba(180,140,60,0.55);
|
||||
border-radius: 3px;
|
||||
padding: 14px 18px 14px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0,0,0,0.8),
|
||||
inset 0 0 30px rgba(0,0,0,0.3),
|
||||
0 -4px 20px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.dialog-box::before,
|
||||
.dialog-box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-color: rgba(180,140,60,0.8);
|
||||
border-style: solid;
|
||||
}
|
||||
.dialog-box::before {
|
||||
top: -2px; left: -2px;
|
||||
border-width: 2px 0 0 2px;
|
||||
}
|
||||
.dialog-box::after {
|
||||
bottom: -2px; right: -2px;
|
||||
border-width: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.dialog-name-plate {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
left: 16px;
|
||||
background: linear-gradient(135deg, rgba(180,140,60,0.9), rgba(140,100,30,0.9));
|
||||
border: 1px solid rgba(220,180,80,0.7);
|
||||
border-radius: 2px;
|
||||
padding: 2px 14px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.dialog-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1a1000;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.dialog-text-area {
|
||||
min-height: 60px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.dialog-text {
|
||||
margin: 0;
|
||||
font-size: clamp(13px, 3.5vw, 15px);
|
||||
line-height: 1.85;
|
||||
color: #f0e6c8;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
/* ─── Choices ─── */
|
||||
.choices-frame {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(10,8,22,0.97) 0%,
|
||||
rgba(6,4,15,0.99) 100%
|
||||
);
|
||||
border: 1px solid rgba(180,140,60,0.45);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0,0,0,0.8),
|
||||
0 -4px 16px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.choices-inner {
|
||||
max-height: 38dvh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.choice-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 18px;
|
||||
border-bottom: 1px solid rgba(180,140,60,0.12);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.choice-row:last-child { border-bottom: none; }
|
||||
|
||||
.choice-row.hovered,
|
||||
.choice-row:active {
|
||||
background: rgba(180,140,60,0.1);
|
||||
}
|
||||
|
||||
.choice-cursor {
|
||||
font-size: 14px;
|
||||
color: #d4a830;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.choice-label {
|
||||
font-size: clamp(13px, 3.4vw, 15px);
|
||||
color: #e8dfc0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.choice-row.hovered .choice-label {
|
||||
color: #ffd870;
|
||||
}
|
||||
|
||||
/* ─── Loading Dots ─── */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 18px;
|
||||
padding: 8px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #5522aa;
|
||||
background: #c8a060;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.2s infinite;
|
||||
}
|
||||
@ -294,125 +669,112 @@ onMounted(startGame)
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.choices-area {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 0 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
border-top: 1px solid #2a1a4e;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.choice-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
background: #13131f;
|
||||
border: 1px solid #2a1a4e;
|
||||
border-radius: 10px;
|
||||
padding: 13px 16px;
|
||||
color: #e0d0ff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.choice-btn:hover {
|
||||
background: #1e1030;
|
||||
border-color: #7744cc;
|
||||
color: #c8a8ff;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.choice-btn:active {
|
||||
transform: translateX(0);
|
||||
background: #2a1a4e;
|
||||
}
|
||||
|
||||
.choice-num {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.choice-text {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.restart-area {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 0;
|
||||
/* ─── Restart ─── */
|
||||
.restart-frame {
|
||||
text-align: center;
|
||||
border-top: 1px solid #2a1a4e;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.btn-restart {
|
||||
background: linear-gradient(135deg, #5522aa, #7744cc);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
padding: 12px 32px;
|
||||
.jrpg-btn-restart {
|
||||
background: linear-gradient(135deg, rgba(180,140,60,0.2), rgba(140,100,30,0.2));
|
||||
border: 1px solid rgba(180,140,60,0.6);
|
||||
border-radius: 3px;
|
||||
color: #d4a830;
|
||||
padding: 12px 36px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
letter-spacing: 2px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.jrpg-btn-restart:hover {
|
||||
background: linear-gradient(135deg, rgba(180,140,60,0.35), rgba(140,100,30,0.35));
|
||||
box-shadow: 0 0 16px rgba(180,140,60,0.3);
|
||||
}
|
||||
|
||||
/* ─── Modal ─── */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
z-index: 200;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #13131f;
|
||||
border: 1px solid #2a1a4e;
|
||||
border-radius: 12px;
|
||||
.jrpg-modal {
|
||||
background: linear-gradient(180deg, #0e0b1e 0%, #080614 100%);
|
||||
border: 1px solid rgba(180,140,60,0.6);
|
||||
border-radius: 4px;
|
||||
padding: 28px 24px;
|
||||
max-width: 360px;
|
||||
max-width: 340px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.9);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
color: #e0d0ff;
|
||||
.jrpg-modal::before,
|
||||
.jrpg-modal::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-color: rgba(180,140,60,0.8);
|
||||
border-style: solid;
|
||||
}
|
||||
.jrpg-modal::before { top: -2px; left: -2px; border-width: 2px 0 0 2px; }
|
||||
.jrpg-modal::after { bottom: -2px; right: -2px; border-width: 0 2px 2px 0; }
|
||||
|
||||
.jrpg-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #d4a830;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.jrpg-modal p {
|
||||
color: #e8dfc0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
line-height: 1.8;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.modal-btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btns button {
|
||||
.jrpg-btn {
|
||||
background: rgba(180,140,60,0.1);
|
||||
border: 1px solid rgba(180,140,60,0.5);
|
||||
border-radius: 3px;
|
||||
color: #d4a830;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid #2a1a4e;
|
||||
background: #0d0d1a;
|
||||
color: #8877bb;
|
||||
transition: all 0.2s;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.modal-btns button:hover { background: #2a1a4e; color: #c8a8ff; }
|
||||
|
||||
.btn-danger {
|
||||
background: #3d0a1a !important;
|
||||
border-color: #7a1a2e !important;
|
||||
color: #ff6677 !important;
|
||||
.jrpg-btn:hover {
|
||||
background: rgba(180,140,60,0.25);
|
||||
}
|
||||
|
||||
.btn-danger:hover { background: #5a1020 !important; }
|
||||
.jrpg-btn.danger {
|
||||
border-color: rgba(180,60,60,0.6);
|
||||
color: #ff7777;
|
||||
background: rgba(180,60,60,0.1);
|
||||
}
|
||||
|
||||
.jrpg-btn.danger:hover {
|
||||
background: rgba(180,60,60,0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user