- Add global settings & pause modal (gear icon) with mute and reset, removing obsolete bag button - Extend Prologue with Flo Maier (castle fire game over & Löschzug restart) and Viktor (vampire bite game over & Knoblauch restart) - Rebuild Akt II as 3-level vertical Doodle Jump climber to rescue sisters Saskia, Maja, and Miriam with custom pixel portraits - Update Akt III Schankraum with seamless oak floor, double-tap 'Luke zum Party Bunker', duel intro modals, and Hansi's departure in Hinterland - Rebuild Akt IV as 3-lane medieval Guitar Hero rock concert with live crowd meter, combo multipliers, and Hansi duo - Update AGENTS.md architectural documentation and playtest test suite
836 lines
20 KiB
Vue
836 lines
20 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { useGameStore } from '../stores/gameStore'
|
|
import { sound } from '../audio/synthAudio'
|
|
|
|
const store = useGameStore()
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
const showConcertIntro = ref(true)
|
|
const concertFinished = ref(false)
|
|
|
|
let animId: number | null = null
|
|
|
|
// Canvas dimensions
|
|
const W = 360
|
|
const H = 480
|
|
|
|
// Fretboard layout
|
|
const boardX = 60
|
|
const boardW = 240
|
|
const strikeY = 360
|
|
const laneCenters = [boardX + 40, boardX + 120, boardX + 200] // 100, 180, 260
|
|
const laneColors = ['#ef4444', '#f59e0b', '#38bdf8']
|
|
const laneGlow = ['rgba(239, 68, 68, 0.4)', 'rgba(245, 158, 11, 0.4)', 'rgba(56, 189, 248, 0.4)']
|
|
|
|
// Note structure
|
|
interface Note {
|
|
id: number
|
|
lane: 0 | 1 | 2
|
|
targetTime: number // in seconds from concert start
|
|
hit: boolean
|
|
missed: boolean
|
|
}
|
|
|
|
interface HitPopup {
|
|
x: number
|
|
y: number
|
|
text: string
|
|
color: string
|
|
life: number
|
|
}
|
|
|
|
interface Particle {
|
|
x: number
|
|
y: number
|
|
vx: number
|
|
vy: number
|
|
life: number
|
|
color: string
|
|
size: number
|
|
}
|
|
|
|
// Concert timeline
|
|
const SONG_DURATION = 42 // 42 seconds total concert
|
|
let startTime = 0
|
|
let elapsedTime = 0
|
|
let notes: Note[] = []
|
|
let hitPopups: HitPopup[] = []
|
|
let particles: Particle[] = []
|
|
|
|
// Score tracking
|
|
const score = ref(0)
|
|
const streak = ref(0)
|
|
const maxStreak = ref(0)
|
|
const totalHits = ref(0)
|
|
const totalPerfects = ref(0)
|
|
const totalMisses = ref(0)
|
|
|
|
// Active button pressed state for visual feedback
|
|
const lanePressed = ref([false, false, false])
|
|
|
|
// Stage background
|
|
let stageImg: HTMLImageElement | null = null
|
|
if (typeof Image !== 'undefined') {
|
|
stageImg = new Image()
|
|
stageImg.src = '/assets/scenes/lute_stage.jpg'
|
|
}
|
|
|
|
// Generate rhythmic 135-BPM note chart (~55 notes across 42 seconds)
|
|
function generateNoteChart() {
|
|
const chart: Note[] = []
|
|
let id = 1
|
|
const bpm = 135
|
|
const beatInterval = 60 / bpm // ~0.444s
|
|
|
|
// Intro riff (beats 4 to 16)
|
|
for (let b = 4; b < 16; b += 2) {
|
|
chart.push({ id: id++, lane: (b % 4 === 0 ? 0 : 1) as 0 | 1 | 2, targetTime: b * beatInterval, hit: false, missed: false })
|
|
}
|
|
|
|
// Main verse (beats 18 to 44) - driving rhythm across 3 lanes
|
|
const versePattern: (0 | 1 | 2)[] = [0, 1, 2, 1, 0, 0, 2, 1, 2, 0, 1, 2, 1]
|
|
for (let i = 0; i < versePattern.length; i++) {
|
|
chart.push({
|
|
id: id++,
|
|
lane: versePattern[i],
|
|
targetTime: (18 + i * 2) * beatInterval,
|
|
hit: false,
|
|
missed: false
|
|
})
|
|
}
|
|
|
|
// Solo riff / Fast rock rhythm (beats 46 to 76)
|
|
for (let b = 46; b < 76; b += 1.5) {
|
|
const lane = (Math.floor(b * 2) % 3) as 0 | 1 | 2
|
|
chart.push({
|
|
id: id++,
|
|
lane,
|
|
targetTime: b * beatInterval,
|
|
hit: false,
|
|
missed: false
|
|
})
|
|
}
|
|
|
|
// Grand finale chords / double hits (beats 78 to 92)
|
|
for (let b = 78; b <= 90; b += 2) {
|
|
chart.push({
|
|
id: id++,
|
|
lane: (b % 2 === 0 ? 1 : 2) as 0 | 1 | 2,
|
|
targetTime: b * beatInterval,
|
|
hit: false,
|
|
missed: false
|
|
})
|
|
if (b % 4 === 0) {
|
|
chart.push({
|
|
id: id++,
|
|
lane: 0,
|
|
targetTime: b * beatInterval,
|
|
hit: false,
|
|
missed: false
|
|
})
|
|
}
|
|
}
|
|
|
|
return chart
|
|
}
|
|
|
|
function startConcert() {
|
|
sound.playDialogueBlip()
|
|
showConcertIntro.value = false
|
|
notes = generateNoteChart()
|
|
startTime = performance.now()
|
|
score.value = 0
|
|
streak.value = 0
|
|
maxStreak.value = 0
|
|
totalHits.value = 0
|
|
totalPerfects.value = 0
|
|
totalMisses.value = 0
|
|
concertFinished.value = false
|
|
}
|
|
|
|
// Trigger hitting a lane
|
|
function hitLane(lane: 0 | 1 | 2) {
|
|
if (showConcertIntro.value || concertFinished.value) return
|
|
|
|
lanePressed.value[lane] = true
|
|
setTimeout(() => {
|
|
lanePressed.value[lane] = false
|
|
}, 120)
|
|
|
|
const currentT = (performance.now() - startTime) / 1000
|
|
|
|
// Find closest unhit note in this lane
|
|
let closestNote: Note | null = null
|
|
let minDiff = Infinity
|
|
|
|
for (const n of notes) {
|
|
if (!n.hit && !n.missed && n.lane === lane) {
|
|
const diff = Math.abs(n.targetTime - currentT)
|
|
if (diff < minDiff && diff < 0.35) {
|
|
minDiff = diff
|
|
closestNote = n
|
|
}
|
|
}
|
|
}
|
|
|
|
const laneX = laneCenters[lane]
|
|
|
|
if (closestNote) {
|
|
closestNote.hit = true
|
|
sound.playLuteNote(lane, true)
|
|
streak.value++
|
|
if (streak.value > maxStreak.value) maxStreak.value = streak.value
|
|
totalHits.value++
|
|
|
|
if (minDiff < 0.12) {
|
|
// Perfect!
|
|
totalPerfects.value++
|
|
score.value += 150 * Math.min(4, Math.floor(streak.value / 5) + 1)
|
|
addPopup(laneX, strikeY - 15, 'PERFEKT! ✨', '#ffd700')
|
|
spawnParticles(laneX, strikeY, '#ffd700', 12)
|
|
} else {
|
|
// Good!
|
|
score.value += 80 * Math.min(4, Math.floor(streak.value / 5) + 1)
|
|
addPopup(laneX, strikeY - 15, 'GUT! 🎵', '#4ade80')
|
|
spawnParticles(laneX, strikeY, '#4ade80', 8)
|
|
}
|
|
} else {
|
|
// Strummed without note -> minor mistwizzle
|
|
sound.playLuteNote(lane, false)
|
|
if (streak.value > 3) {
|
|
addPopup(laneX, strikeY - 10, 'UPPS!', '#f87171')
|
|
}
|
|
streak.value = 0
|
|
}
|
|
}
|
|
|
|
function addPopup(x: number, y: number, text: string, color: string) {
|
|
hitPopups.push({ x, y, text, color, life: 25 })
|
|
}
|
|
|
|
function spawnParticles(x: number, y: number, color: string, count = 8) {
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
x,
|
|
y,
|
|
vx: (Math.random() - 0.5) * 6,
|
|
vy: (Math.random() - 0.5) * 6,
|
|
life: 20 + Math.random() * 10,
|
|
color,
|
|
size: 2 + Math.random() * 3
|
|
})
|
|
}
|
|
}
|
|
|
|
function finishConcert() {
|
|
concertFinished.value = true
|
|
sound.playFanfare()
|
|
store.serveGuest()
|
|
store.serveGuest()
|
|
store.addKronkorken(10)
|
|
store.unlockEasterEgg('lute_hero')
|
|
}
|
|
|
|
function proceedToCertificate() {
|
|
store.setAct('finale')
|
|
}
|
|
|
|
// Controls
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.code === 'ArrowLeft' || e.code === 'KeyA') {
|
|
hitLane(0)
|
|
} else if (e.code === 'ArrowDown' || e.code === 'KeyS' || e.code === 'Space') {
|
|
hitLane(1)
|
|
} else if (e.code === 'ArrowRight' || e.code === 'KeyD') {
|
|
hitLane(2)
|
|
}
|
|
}
|
|
|
|
defineExpose({
|
|
hitLane
|
|
})
|
|
|
|
function updatePhysics() {
|
|
if (store.isPaused || showConcertIntro.value || concertFinished.value) return
|
|
|
|
elapsedTime = (performance.now() - startTime) / 1000
|
|
|
|
// Check missed notes
|
|
notes.forEach(n => {
|
|
if (!n.hit && !n.missed && elapsedTime - n.targetTime > 0.3) {
|
|
n.missed = true
|
|
totalMisses.value++
|
|
streak.value = 0
|
|
addPopup(laneCenters[n.lane], strikeY + 15, 'VORBEI! 💨', '#94a3b8')
|
|
}
|
|
})
|
|
|
|
// Popups update
|
|
hitPopups.forEach(p => {
|
|
p.y -= 1
|
|
p.life--
|
|
})
|
|
hitPopups = hitPopups.filter(p => p.life > 0)
|
|
|
|
// Particles update
|
|
particles.forEach(p => {
|
|
p.x += p.vx
|
|
p.y += p.vy
|
|
p.life--
|
|
})
|
|
particles = particles.filter(p => p.life > 0)
|
|
|
|
// Song completion
|
|
if (elapsedTime >= SONG_DURATION && !concertFinished.value) {
|
|
finishConcert()
|
|
}
|
|
}
|
|
|
|
function drawGame(ctx: CanvasRenderingContext2D) {
|
|
ctx.clearRect(0, 0, W, H)
|
|
|
|
// 1. Stage Background
|
|
if (stageImg && stageImg.complete && stageImg.naturalWidth > 0) {
|
|
ctx.drawImage(stageImg, 0, 0, W, H)
|
|
} else {
|
|
// Atmospheric dark medieval rock concert gradient
|
|
const grad = ctx.createLinearGradient(0, 0, 0, H)
|
|
grad.addColorStop(0, '#0f172a')
|
|
grad.addColorStop(0.5, '#451a03')
|
|
grad.addColorStop(1, '#170c06')
|
|
ctx.fillStyle = grad
|
|
ctx.fillRect(0, 0, W, H)
|
|
}
|
|
|
|
// Dark overlay over stage to keep fretboard high-contrast
|
|
ctx.fillStyle = 'rgba(10, 5, 3, 0.45)'
|
|
ctx.fillRect(0, 0, W, H)
|
|
|
|
// 2. Fretboard (Parchment & Eiche Neck)
|
|
ctx.fillStyle = 'rgba(25, 12, 6, 0.88)'
|
|
ctx.fillRect(boardX, 0, boardW, H)
|
|
ctx.strokeStyle = '#d4af37'
|
|
ctx.lineWidth = 2
|
|
ctx.strokeRect(boardX, 0, boardW, H)
|
|
|
|
// Frets (horizontal brass bars)
|
|
ctx.strokeStyle = 'rgba(212, 175, 55, 0.25)'
|
|
ctx.lineWidth = 1.5
|
|
for (let y = 60; y < H; y += 45) {
|
|
ctx.beginPath()
|
|
ctx.moveTo(boardX, y)
|
|
ctx.lineTo(boardX + boardW, y)
|
|
ctx.stroke()
|
|
}
|
|
|
|
// 3 Strings (vertical glowing lines)
|
|
for (let i = 0; i < 3; i++) {
|
|
const lx = laneCenters[i]
|
|
ctx.strokeStyle = lanePressed.value[i] ? '#ffffff' : laneColors[i]
|
|
ctx.lineWidth = lanePressed.value[i] ? 4 : 2
|
|
ctx.beginPath()
|
|
ctx.moveTo(lx, 0)
|
|
ctx.lineTo(lx, H)
|
|
ctx.stroke()
|
|
|
|
// Strike Target Circles at bottom
|
|
ctx.fillStyle = lanePressed.value[i] ? laneColors[i] : 'rgba(0, 0, 0, 0.6)'
|
|
ctx.beginPath()
|
|
ctx.arc(lx, strikeY, 18, 0, Math.PI * 2)
|
|
ctx.fill()
|
|
ctx.strokeStyle = laneColors[i]
|
|
ctx.lineWidth = 3
|
|
ctx.stroke()
|
|
|
|
// Target glyph
|
|
ctx.fillStyle = '#ffffff'
|
|
ctx.font = 'bold 9px monospace'
|
|
ctx.fillText(i === 0 ? 'D' : i === 1 ? 'G' : 'A', lx - 4, strikeY + 3)
|
|
}
|
|
|
|
// 3. Falling Notes
|
|
const speed = 260 // pixels per second travel speed
|
|
const noteRadius = 14
|
|
|
|
notes.forEach(n => {
|
|
if (n.hit || n.missed) return
|
|
const timeDiff = n.targetTime - elapsedTime
|
|
const ny = strikeY - timeDiff * speed
|
|
|
|
if (ny >= -20 && ny <= strikeY + 40) {
|
|
const nx = laneCenters[n.lane]
|
|
|
|
// Outer glow
|
|
ctx.fillStyle = laneGlow[n.lane]
|
|
ctx.beginPath()
|
|
ctx.arc(nx, ny, noteRadius + 5, 0, Math.PI * 2)
|
|
ctx.fill()
|
|
|
|
// Note body (flaming rune / beer cap pick)
|
|
ctx.fillStyle = laneColors[n.lane]
|
|
ctx.beginPath()
|
|
ctx.arc(nx, ny, noteRadius, 0, Math.PI * 2)
|
|
ctx.fill()
|
|
ctx.strokeStyle = '#ffd700'
|
|
ctx.lineWidth = 2
|
|
ctx.stroke()
|
|
|
|
// Center music note icon
|
|
ctx.fillStyle = '#ffffff'
|
|
ctx.font = 'bold 11px sans-serif'
|
|
ctx.fillText('♫', nx - 4, ny + 4)
|
|
}
|
|
})
|
|
|
|
// 4. Hit Burst Particles
|
|
particles.forEach(p => {
|
|
ctx.fillStyle = p.color
|
|
ctx.beginPath()
|
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
|
|
ctx.fill()
|
|
})
|
|
|
|
// 5. Floating Hit Popups
|
|
hitPopups.forEach(p => {
|
|
ctx.fillStyle = p.color
|
|
ctx.font = 'bold 9px monospace'
|
|
ctx.fillText(p.text, p.x - 24, p.y)
|
|
})
|
|
|
|
// 6. HUD (Top Banner)
|
|
drawHUD(ctx)
|
|
}
|
|
|
|
function drawHUD(ctx: CanvasRenderingContext2D) {
|
|
// Top Header Banner
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'
|
|
ctx.fillRect(0, 0, W, 32)
|
|
ctx.strokeStyle = '#d4af37'
|
|
ctx.lineWidth = 1
|
|
ctx.strokeRect(0, 0, W, 32)
|
|
|
|
// Score & Streak
|
|
ctx.fillStyle = '#ffd700'
|
|
ctx.font = 'bold 8px monospace'
|
|
ctx.fillText(`PUNKTE: ${score.value}`, 8, 14)
|
|
|
|
const multiplier = Math.min(4, Math.floor(streak.value / 5) + 1)
|
|
ctx.fillStyle = streak.value >= 5 ? '#f59e0b' : '#38bdf8'
|
|
ctx.fillText(`COMBO: x${multiplier} (${streak.value})`, 8, 26)
|
|
|
|
// Progress Bar for Song
|
|
const progress = Math.min(1, Math.max(0, elapsedTime / SONG_DURATION))
|
|
ctx.fillStyle = '#334155'
|
|
ctx.fillRect(W - 100, 10, 90, 12)
|
|
ctx.fillStyle = '#10b981'
|
|
ctx.fillRect(W - 98, 12, 86 * progress, 8)
|
|
ctx.strokeStyle = '#ffd700'
|
|
ctx.strokeRect(W - 100, 10, 90, 12)
|
|
}
|
|
|
|
function gameLoop() {
|
|
updatePhysics()
|
|
if (canvasRef.value) {
|
|
const ctx = canvasRef.value.getContext('2d')
|
|
if (ctx) drawGame(ctx)
|
|
}
|
|
animId = requestAnimationFrame(gameLoop)
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
animId = requestAnimationFrame(gameLoop)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', handleKeyDown)
|
|
if (animId) cancelAnimationFrame(animId)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="concert-viewport">
|
|
<canvas
|
|
ref="canvasRef"
|
|
:width="W"
|
|
:height="H"
|
|
class="game-canvas"
|
|
></canvas>
|
|
|
|
<!-- Bottom Thumb Controls (3 Huge Touch Buttons for the 3 Strings) -->
|
|
<div v-if="!showConcertIntro && !concertFinished" class="lute-hero-controls">
|
|
<button
|
|
class="string-btn btn-lane-0"
|
|
:class="{ active: lanePressed[0] }"
|
|
@touchstart.prevent="hitLane(0)"
|
|
@mousedown="hitLane(0)"
|
|
>
|
|
<span class="string-badge">D</span>
|
|
<span class="string-label">🎸 LINKS</span>
|
|
</button>
|
|
|
|
<button
|
|
class="string-btn btn-lane-1"
|
|
:class="{ active: lanePressed[1] }"
|
|
@touchstart.prevent="hitLane(1)"
|
|
@mousedown="hitLane(1)"
|
|
>
|
|
<span class="string-badge">G</span>
|
|
<span class="string-label">⚡ MITTE</span>
|
|
</button>
|
|
|
|
<button
|
|
class="string-btn btn-lane-2"
|
|
:class="{ active: lanePressed[2] }"
|
|
@touchstart.prevent="hitLane(2)"
|
|
@mousedown="hitLane(2)"
|
|
>
|
|
<span class="string-badge">A</span>
|
|
<span class="string-label">🤘 RECHTS</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Act IV Intro Modal -->
|
|
<div v-if="showConcertIntro" class="intro-overlay">
|
|
<div class="intro-card">
|
|
<span class="intro-act-tag">AKT IV: MITTELALTER GUITAR HERO</span>
|
|
<h2 class="intro-card-title">HANSIS ABSCHIEDSKONZERT</h2>
|
|
<p class="intro-card-story">
|
|
Ritter Hansi bricht auf nach Münchanien! Zum großen Abschied rocken König Sascha und Hansi als Barden-Duo die Burgbühne bei 135 BPM vor 12.000 begeisterten Fans!
|
|
</p>
|
|
|
|
<div class="mission-callout">
|
|
<span class="callout-icon">🎸</span>
|
|
<div class="callout-text">
|
|
<strong>Barden-Mission:</strong><br />
|
|
Triff die fallenden Noten im Takt auf den 3 Lauten-Saiten (Links, Mitte, Rechts). Halte die Combo oben für den maximalen Rock-Multiplikator!
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls-overview">
|
|
<div class="ctrl-item">
|
|
<span class="ctrl-badge">TOUCH</span>
|
|
<span class="ctrl-label">3 große Saiten-Tasten am unteren Bildschirmrand</span>
|
|
</div>
|
|
<div class="ctrl-item">
|
|
<span class="ctrl-badge">TASTATUR</span>
|
|
<span class="ctrl-label">Links: ◀ / A | Mitte: ▼ / S / Leertaste | Rechts: ▶ / D</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="start-act-btn" @click="startConcert">
|
|
🔥 BÜHNE FREI: KONZERT STARTEN!
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Concert Finished Score Modal -->
|
|
<div v-if="concertFinished" class="intro-overlay">
|
|
<div class="intro-card finish-card">
|
|
<span class="intro-act-tag">🎉 ZUGABE! DIE CROWD TOBT!</span>
|
|
<h2 class="intro-card-title">LEGENDÄRES ABSCHIEDSKONZERT!</h2>
|
|
<p class="intro-card-story">
|
|
Was für ein Solo! Hansi und Sascha haben die Burgmauern von Haumdaucher zum Beben gebracht! Hansi verabschiedet sich mit Freudentränen Richtung Münchanien.
|
|
</p>
|
|
|
|
<div class="concert-stats">
|
|
<div class="stat-row">
|
|
<span class="stat-name">Rock-Punkte:</span>
|
|
<span class="stat-val highlight">{{ score }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">Perfekte Treffer:</span>
|
|
<span class="stat-val">{{ totalPerfects }} ✨</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">Maximale Combo:</span>
|
|
<span class="stat-val">{{ maxStreak }}x 🔥</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="concert-rewards">
|
|
<span class="reward-pill">👑 +10 Kronkorken</span>
|
|
<span class="reward-pill">🍻 +2 Kaltes Helles</span>
|
|
</div>
|
|
|
|
<button class="start-act-btn victory-btn" @click="proceedToCertificate">
|
|
🏆 Zur königlichen Siegerurkunde!
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.concert-viewport {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: #000;
|
|
overflow: hidden;
|
|
user-select: none;
|
|
touch-action: none;
|
|
}
|
|
|
|
.game-canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
/* 3 Large Thumb Buttons at the Bottom */
|
|
.lute-hero-controls {
|
|
position: absolute;
|
|
bottom: 8px;
|
|
left: 12px;
|
|
right: 12px;
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 8px;
|
|
z-index: 30;
|
|
}
|
|
|
|
.string-btn {
|
|
height: 52px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.7);
|
|
transition: transform 0.08s ease, filter 0.08s ease;
|
|
-webkit-tap-highlight-color: transparent;
|
|
outline: none;
|
|
}
|
|
|
|
.string-btn:active, .string-btn.active {
|
|
transform: scale(0.92);
|
|
filter: brightness(1.3);
|
|
}
|
|
|
|
.btn-lane-0 {
|
|
background: linear-gradient(180deg, #b91c1c 0%, #7f1d1d 100%);
|
|
border: 2px solid #ef4444;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-lane-1 {
|
|
background: linear-gradient(180deg, #d97706 0%, #92400e 100%);
|
|
border: 2px solid #f59e0b;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-lane-2 {
|
|
background: linear-gradient(180deg, #0284c7 0%, #0369a1 100%);
|
|
border: 2px solid #38bdf8;
|
|
color: #fff;
|
|
}
|
|
|
|
.string-badge {
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 8px;
|
|
background: rgba(0, 0, 0, 0.4);
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.string-label {
|
|
font-family: 'MedievalSharp', serif;
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Modals */
|
|
.intro-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.88);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 12px;
|
|
z-index: 50;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.intro-card {
|
|
background: #231209;
|
|
border: 3px double #d4af37;
|
|
border-radius: 10px;
|
|
padding: 14px;
|
|
max-width: 320px;
|
|
width: 100%;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.95);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.finish-card {
|
|
border-color: #ffd700;
|
|
background: radial-gradient(circle at 50% 20%, #3b1d0c 0%, #1a0c06 100%);
|
|
}
|
|
|
|
.intro-act-tag {
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 6.5px;
|
|
color: #f59e0b;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.intro-card-title {
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 9px;
|
|
color: #ffd700;
|
|
margin: 0;
|
|
line-height: 1.35;
|
|
text-shadow: 1px 1px 0 #000;
|
|
}
|
|
|
|
.intro-card-story {
|
|
font-family: 'MedievalSharp', serif;
|
|
font-size: 12.5px;
|
|
color: #fef08a;
|
|
line-height: 1.35;
|
|
margin: 0;
|
|
}
|
|
|
|
.mission-callout {
|
|
background: #451a03;
|
|
border-left: 3px solid #f59e0b;
|
|
border-radius: 4px;
|
|
padding: 6px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
text-align: left;
|
|
}
|
|
|
|
.callout-icon {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.callout-text {
|
|
font-family: 'MedievalSharp', serif;
|
|
font-size: 12px;
|
|
color: #fef3c7;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.controls-overview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
width: 100%;
|
|
background: #170c06;
|
|
border: 1px solid #78350f;
|
|
border-radius: 6px;
|
|
padding: 6px 8px;
|
|
}
|
|
|
|
.ctrl-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 6px;
|
|
}
|
|
|
|
.ctrl-badge {
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 6px;
|
|
color: #ffd700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ctrl-label {
|
|
font-family: 'MedievalSharp', serif;
|
|
font-size: 11px;
|
|
color: #fed7aa;
|
|
text-align: right;
|
|
}
|
|
|
|
.start-act-btn {
|
|
background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
|
|
color: #fff;
|
|
border: 2px solid #ffd700;
|
|
border-radius: 8px;
|
|
padding: 9px 14px;
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 7.5px;
|
|
cursor: pointer;
|
|
margin-top: 4px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
|
|
transition: transform 0.1s;
|
|
}
|
|
|
|
.start-act-btn:hover {
|
|
background: linear-gradient(180deg, #ef4444 0%, #b91c1c 100%);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.start-act-btn:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.victory-btn {
|
|
background: linear-gradient(180deg, #16a34a 0%, #15803d 100%);
|
|
}
|
|
|
|
.victory-btn:hover {
|
|
background: linear-gradient(180deg, #22c55e 0%, #16a34a 100%);
|
|
}
|
|
|
|
.concert-stats {
|
|
width: 100%;
|
|
background: #1c0a04;
|
|
border: 1px solid #b45309;
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.stat-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat-name {
|
|
font-family: 'MedievalSharp', serif;
|
|
font-size: 13px;
|
|
color: #fed7aa;
|
|
}
|
|
|
|
.stat-val {
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 7.5px;
|
|
color: #fff;
|
|
}
|
|
|
|
.stat-val.highlight {
|
|
color: #ffd700;
|
|
font-size: 9px;
|
|
}
|
|
|
|
.concert-rewards {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.reward-pill {
|
|
background: #78350f;
|
|
color: #ffd700;
|
|
border: 1px solid #b45309;
|
|
border-radius: 12px;
|
|
padding: 4px 10px;
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 6.5px;
|
|
}
|
|
</style>
|