feat: JRPG UI redesign (환세취호전 style), localStorage history persist

This commit is contained in:
hyoseung 2026-05-29 12:03:09 +09:00
parent 9b7a7f432f
commit 4c98358b87
2 changed files with 557 additions and 193 deletions

View File

@ -13,12 +13,14 @@ html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
overflow: hidden;
background: #0a0a0f; background: #0a0a0f;
color: #e0d0ff; color: #e0d0ff;
} }
#app { #app {
height: 100vh; height: 100vh;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@ -1,52 +1,86 @@
<template> <template>
<div class="game-wrap"> <div class="jrpg-wrap">
<header class="game-header">
<span class="header-title"> 스티그마타</span> <div class="scene-bg" :class="sceneBgClass">
<div class="header-right"> <div class="scene-vignette"></div>
<span class="username">{{ username }}</span> <div class="scene-title-deco"> 스티그마타 </div>
<button class="btn-status" @click="showStatus = !showStatus" title="캐릭터 상태">📊</button> </div>
<button class="btn-reset" @click="confirmReset" title="초기화">🔄</button>
<button class="btn-logout" @click="logout" title="로그아웃">🚪</button> <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> </div>
</header> </header>
<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>
<main class="game-main"> <div class="log-panel" v-if="showLog">
<div class="story-area" ref="storyRef"> <div class="log-inner" ref="logRef">
<div v-for="(entry, i) in history" :key="i" class="story-entry" :class="entry.type"> <div v-for="(entry, i) in history" :key="i" class="log-entry" :class="entry.type">
<pre class="story-text">{{ entry.text }}</pre> <pre class="log-text">{{ entry.text }}</pre>
</div> </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> <span></span><span></span><span></span>
</div> </div>
</div> </div>
</div>
<div class="choices-area" v-if="!loading && choices.length > 0"> <div class="choices-frame" v-if="!loading && choices.length > 0">
<button <div class="choices-inner">
<div
v-for="(choice, i) in choices" v-for="(choice, i) in choices"
:key="i" :key="i"
class="choice-btn" class="choice-row"
:class="{ hovered: hoveredChoice === i }"
@mouseenter="hoveredChoice = i"
@mouseleave="hoveredChoice = -1"
@click="selectChoice(i + 1)" @click="selectChoice(i + 1)"
> >
<span class="choice-num">{{ numEmoji(i) }}</span> <span class="choice-cursor">{{ hoveredChoice === i ? '▶' : ' ' }}</span>
<span class="choice-text">{{ choice.text }}</span> <span class="choice-label">{{ choice.text }}</span>
</button> </div>
</div>
</div> </div>
<div class="restart-area" v-if="!loading && choices.length === 0 && history.length > 0"> <div class="restart-frame" v-if="!loading && choices.length === 0 && history.length > 0">
<button class="btn-restart" @click="startGame">다시 시작</button> <button class="jrpg-btn-restart" @click="startGame"> 다시 시작</button>
</div>
</div> </div>
</main>
<div class="modal-overlay" v-if="resetConfirm" @click.self="resetConfirm = false"> <div class="modal-overlay" v-if="resetConfirm" @click.self="resetConfirm = false">
<div class="modal"> <div class="jrpg-modal">
<p> 캐릭터를 초기화하면 모든 진행 상황이 삭제됩니다. 계속하시겠습니까?</p> <div class="jrpg-modal-title"> 경고</div>
<p>캐릭터를 초기화하면 모든 진행 상황이 삭제됩니다.<br>계속하시겠습니까?</p>
<div class="modal-btns"> <div class="modal-btns">
<button @click="doReset" class="btn-danger">초기화</button> <button class="jrpg-btn danger" @click="doReset">초기화</button>
<button @click="resetConfirm = false">취소</button> <button class="jrpg-btn" @click="resetConfirm = false">취소</button>
</div> </div>
</div> </div>
</div> </div>
@ -54,7 +88,7 @@
</template> </template>
<script setup lang="ts"> <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 { useRouter } from 'vue-router'
import { api, type RpgResult } from '../api' import { api, type RpgResult } from '../api'
@ -64,34 +98,130 @@ const history = ref<{ text: string; type: 'story' | 'choice' }[]>([])
const choices = ref<{ text: string }[]>([]) const choices = ref<{ text: string }[]>([])
const loading = ref(false) const loading = ref(false)
const showStatus = ref(false) const showStatus = ref(false)
const showLog = ref(false)
const statusText = ref('') const statusText = ref('')
const resetConfirm = ref(false) 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⃣'] const HISTORY_KEY = `rpg_history_${username.value}`
function numEmoji(i: number) { return NUM_EMOJIS[i] || `${i+1}.` }
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(() => { 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' }) history.value.push({ text: result.text, type: 'story' })
choices.value = result.choices || [] choices.value = result.choices || []
scrollDown() saveHistory()
scrollLog()
} }
async function startGame() { async function startGame() {
loading.value = true loading.value = true
history.value = [] clearHistory()
choices.value = [] choices.value = []
try { try {
const result = await api.rpgStart() const result = await api.rpgStart()
applyResult(result) applyResult(result)
} catch (e: any) { } 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 { } finally {
loading.value = false loading.value = false
} }
@ -103,13 +233,14 @@ async function selectChoice(choice: number) {
history.value.push({ text: `${selected.text}`, type: 'choice' }) history.value.push({ text: `${selected.text}`, type: 'choice' })
choices.value = [] choices.value = []
loading.value = true loading.value = true
scrollDown() saveHistory()
try { try {
const result = await api.rpgAction(choice) const result = await api.rpgAction(choice)
applyResult(result) applyResult(result)
} catch (e: any) { } catch (e: any) {
history.value.push({ text: ` ${e.message}`, type: 'story' }) history.value.push({ text: `오류: ${e.message}`, type: 'story' })
choices.value = [] choices.value = []
saveHistory()
} finally { } finally {
loading.value = false loading.value = false
} }
@ -125,6 +256,7 @@ async function loadStatus() {
} }
watch(showStatus, (v) => { if (v) loadStatus() }) watch(showStatus, (v) => { if (v) loadStatus() })
watch(showLog, (v) => { if (v) scrollLog() })
function confirmReset() { resetConfirm.value = true } function confirmReset() { resetConfirm.value = true }
@ -145,74 +277,120 @@ function logout() {
router.push('/login') router.push('/login')
} }
onMounted(startGame) onMounted(resumeGame)
</script> </script>
<style scoped> <style scoped>
.game-wrap { .jrpg-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100dvh;
overflow: hidden; overflow: hidden;
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Serif KR', serif;
background: #0a0a0f; background: #0a0a0f;
color: #e0d0ff; color: #f0e6c8;
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; 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; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 20px; padding: 8px 14px;
background: #13131f; background: linear-gradient(180deg, rgba(10,8,20,0.95) 0%, rgba(10,8,20,0.7) 100%);
border-bottom: 1px solid #2a1a4e; border-bottom: 1px solid rgba(180,140,60,0.3);
flex-shrink: 0; flex-shrink: 0;
} }
.header-title { .hud-left, .hud-right {
font-size: 16px;
font-weight: 700;
color: #c8a8ff;
letter-spacing: 1px;
}
.header-right {
display: flex; display: flex;
align-items: center; gap: 6px;
gap: 8px;
} }
.username { .hud-center {
font-size: 13px; font-size: 13px;
color: #8877bb; color: #c8a060;
margin-right: 4px; letter-spacing: 2px;
} }
.game-header button { .hud-btn {
background: transparent; background: rgba(180,140,60,0.08);
border: 1px solid #2a1a4e; border: 1px solid rgba(180,140,60,0.3);
border-radius: 6px; border-radius: 4px;
color: #8877bb; color: #c8a060;
font-size: 16px; font-size: 16px;
width: 34px; width: 32px;
height: 34px; height: 32px;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.15s;
} }
.game-header button:hover { .hud-btn:hover {
background: #2a1a4e; background: rgba(180,140,60,0.2);
color: #c8a8ff; border-color: rgba(180,140,60,0.7);
} }
/* ─── Status Panel ─── */
.status-panel { .status-panel {
background: #0d0d1a; position: relative;
border-bottom: 1px solid #2a1a4e; z-index: 10;
background: rgba(10,8,20,0.95);
border-bottom: 1px solid rgba(180,140,60,0.3);
padding: 12px 20px; padding: 12px 20px;
max-height: 200px; max-height: 40dvh;
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
} }
@ -220,68 +398,265 @@ onMounted(startGame)
.status-text { .status-text {
margin: 0; margin: 0;
font-size: 13px; font-size: 13px;
color: #c8a8ff; color: #d4b878;
white-space: pre-wrap; white-space: pre-wrap;
font-family: inherit; font-family: inherit;
line-height: 1.7;
} }
.game-main { /* ─── Log Panel ─── */
.log-panel {
position: relative;
z-index: 10;
flex: 1; flex: 1;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; min-height: 0;
max-width: 700px; background: rgba(5,4,12,0.97);
width: 100%; border-bottom: 1px solid rgba(180,140,60,0.3);
margin: 0 auto;
padding: 0 16px;
} }
.story-area { .log-inner {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; padding: 16px 20px;
padding: 20px 0 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 12px;
} }
.story-entry.story .story-text { .log-entry.story .log-text {
background: #13131f; font-size: 14px;
border: 1px solid #2a1a4e; color: #e8dfc0;
border-radius: 10px; white-space: pre-wrap;
padding: 16px 18px; font-family: inherit;
margin: 0; margin: 0;
font-size: 15px;
line-height: 1.8; line-height: 1.8;
color: #e0d0ff; background: rgba(180,140,60,0.05);
white-space: pre-wrap; border-left: 2px solid rgba(180,140,60,0.4);
font-family: inherit; padding: 8px 12px;
border-radius: 2px;
} }
.story-entry.choice .story-text { .log-entry.choice .log-text {
text-align: right; font-size: 12px;
color: #8877bb;
white-space: pre-wrap;
font-family: inherit;
margin: 0; margin: 0;
font-size: 13px;
color: #7744cc;
font-style: italic; font-style: italic;
white-space: pre-wrap; text-align: right;
font-family: inherit; padding: 2px 0;
padding: 4px 4px;
} }
.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 { .loading-dots {
display: flex; display: flex;
gap: 6px; gap: 6px;
padding: 8px 18px; padding: 8px 0;
align-items: center;
} }
.loading-dots span { .loading-dots span {
width: 8px; width: 8px;
height: 8px; height: 8px;
background: #5522aa; background: #c8a060;
border-radius: 50%; border-radius: 50%;
animation: bounce 1.2s infinite; animation: bounce 1.2s infinite;
} }
@ -294,125 +669,112 @@ onMounted(startGame)
40% { transform: scale(1); opacity: 1; } 40% { transform: scale(1); opacity: 1; }
} }
.choices-area { /* ─── Restart ─── */
flex-shrink: 0; .restart-frame {
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;
text-align: center; text-align: center;
border-top: 1px solid #2a1a4e; padding: 10px 0;
} }
.btn-restart { .jrpg-btn-restart {
background: linear-gradient(135deg, #5522aa, #7744cc); background: linear-gradient(135deg, rgba(180,140,60,0.2), rgba(140,100,30,0.2));
border: none; border: 1px solid rgba(180,140,60,0.6);
border-radius: 8px; border-radius: 3px;
color: #fff; color: #d4a830;
padding: 12px 32px; padding: 12px 36px;
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
cursor: pointer; 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 { .modal-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0,0,0,0.7); background: rgba(0,0,0,0.8);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 100; z-index: 200;
padding: 20px; padding: 20px;
} }
.modal { .jrpg-modal {
background: #13131f; background: linear-gradient(180deg, #0e0b1e 0%, #080614 100%);
border: 1px solid #2a1a4e; border: 1px solid rgba(180,140,60,0.6);
border-radius: 12px; border-radius: 4px;
padding: 28px 24px; padding: 28px 24px;
max-width: 360px; max-width: 340px;
width: 100%;
text-align: center; text-align: center;
box-shadow: 0 0 40px rgba(0,0,0,0.9);
position: relative;
} }
.modal p { .jrpg-modal::before,
color: #e0d0ff; .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; font-size: 14px;
line-height: 1.7; line-height: 1.8;
margin: 0 0 20px; margin: 0 0 20px;
} }
.modal-btns { .modal-btns {
display: flex; display: flex;
gap: 10px; gap: 12px;
justify-content: center; 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; padding: 10px 24px;
border-radius: 8px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
border: 1px solid #2a1a4e; font-family: inherit;
background: #0d0d1a; transition: all 0.15s;
color: #8877bb;
transition: all 0.2s;
} }
.modal-btns button:hover { background: #2a1a4e; color: #c8a8ff; } .jrpg-btn:hover {
background: rgba(180,140,60,0.25);
.btn-danger {
background: #3d0a1a !important;
border-color: #7a1a2e !important;
color: #ff6677 !important;
} }
.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> </style>