feat(sasch40): v4.0 full-screen mobile conversion, Hinter den Bergen room with Liars Dice, beat-only concert audio, and updated art

This commit is contained in:
Moritz Graf
2026-09-24 21:11:12 +02:00
parent 8a0b65c3fa
commit fbc4158016
17 changed files with 1272 additions and 629 deletions
@@ -7,7 +7,7 @@ import { sound } from '../audio/synthAudio'
const store = useGameStore()
type TavernRoom = 'schankraum' | 'nebenraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
type TavernRoom = 'schankraum' | 'nebenraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer' | 'hinter_den_bergen'
const currentRoom = ref<TavernRoom>('schankraum')
interface ActiveDialog {
@@ -309,9 +309,242 @@ function openCharacterDialog(charId: string) {
activeDialog.value = null
}
}
} else if (charId === 'markus') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Servus mein Sohn! Schau di an, 40 Jahre und herrschst über ganz Haumdaucher! Meine taiwanischen Freunde und ich trinken 58% Kinmen-Gaoliang und spielen Liar\'s Dice (Chui Niu / 吹牛)! Traust du dich gegen deinen alten Herrn anzutreten? Wer verliert, kippt einen Kurzen!',
actionLabel: '🎲 Auf geht\'s Papa, Liar\'s Dice spielen!',
actionFn: () => {
activeDialog.value = null
openLiarsDiceIntro()
}
}
}
}
// Münchanien Hint Modal
const munchanienHintActive = ref(false)
function openMunchanienHint() {
sound.playDialogueBlip()
munchanienHintActive.value = true
}
// Liar's Dice (Chui Niu / 吹牛) State & Logic
const liarsDiceIntroOpen = ref(false)
const liarsDiceActive = ref(false)
const liarsDiceGameOver = ref(false)
const liarsDicePlayerWon = ref(false)
const liarsDiceRound = ref(1)
const liarsDicePlayerWins = ref(0)
const liarsDiceMarkusWins = ref(0)
const playerDice = ref<number[]>([1, 2, 3, 4, 5])
const markusDice = ref<number[]>([1, 2, 3, 4, 5])
const markusDiceRevealed = ref(false)
const currentBid = ref<{ count: number; value: number } | null>(null)
const lastBidder = ref<'player' | 'markus' | null>(null)
const liarsDiceMsg = ref('')
const selectedBidCount = ref(2)
const selectedBidValue = ref(2)
const isShaking = ref(false)
const shotAnimation = ref<'sascha' | 'markus' | null>(null)
const isRoundEvaluating = ref(false)
function dieIcon(val: number): string {
const icons = ['', '⚀', '⚁', '⚂', '⚃', '⚄', '⚅']
return icons[val] || '🎲'
}
function openLiarsDiceIntro() {
sound.playDialogueBlip()
liarsDiceIntroOpen.value = true
}
function startLiarsDiceMatch() {
sound.playDialogueBlip()
liarsDiceIntroOpen.value = false
liarsDiceActive.value = true
liarsDiceGameOver.value = false
liarsDicePlayerWon.value = false
liarsDicePlayerWins.value = 0
liarsDiceMarkusWins.value = 0
liarsDiceRound.value = 1
startNewDiceRound()
}
function startNewDiceRound() {
sound.playDiceShake()
isShaking.value = true
markusDiceRevealed.value = false
currentBid.value = null
lastBidder.value = null
shotAnimation.value = null
isRoundEvaluating.value = false
selectedBidCount.value = 2
selectedBidValue.value = 2
// Generate 5 random dice each (1 to 6)
playerDice.value = Array.from({ length: 5 }, () => Math.floor(Math.random() * 6) + 1).sort((a, b) => a - b)
markusDice.value = Array.from({ length: 5 }, () => Math.floor(Math.random() * 6) + 1).sort((a, b) => a - b)
setTimeout(() => {
isShaking.value = false
liarsDiceMsg.value = `Runde ${liarsDiceRound.value}: Schüttle die Becher! Gib dein Gebot ab (z.B. mindestens zwei Dreier).`
}, 400)
}
function canMakeBid(count: number, value: number): boolean {
if (!currentBid.value) return count >= 1 && count <= 10
if (count > currentBid.value.count) return true
if (count === currentBid.value.count && value > currentBid.value.value) return true
return false
}
function makePlayerBid() {
if (isRoundEvaluating.value || isShaking.value) return
if (!canMakeBid(selectedBidCount.value, selectedBidValue.value)) {
alert('Dein Gebot muss höher sein als das vorherige (höhere Anzahl oder bei gleicher Anzahl höhere Augenzahl)!')
return
}
sound.playDialogueBlip()
currentBid.value = { count: selectedBidCount.value, value: selectedBidValue.value }
lastBidder.value = 'player'
liarsDiceMsg.value = `Du sagst an: Mindestens ${currentBid.value.count}x die ${dieIcon(currentBid.value.value)}!`
// Next minimum suggestion for controls
if (selectedBidValue.value < 6) {
selectedBidValue.value++
} else {
selectedBidCount.value++
selectedBidValue.value = 2
}
// Trigger Markus AI after a pause
setTimeout(() => {
markusTurn()
}, 1000)
}
function markusTurn() {
if (isRoundEvaluating.value || !currentBid.value) return
const bid = currentBid.value
// Count how many matching dice Markus has (1s are wild unless bid is on 1)
const markusMatches = markusDice.value.filter(d => d === bid.value || (bid.value !== 1 && d === 1)).length
const expectedPlayerMatches = bid.value === 1 ? 0.83 : 1.67
const totalEstimated = markusMatches + expectedPlayerMatches
// Call bluff if the bid is too high or improbable
const bluffThreshold = bid.count >= 6 ? 0.8 : 1.3
if (bid.count > totalEstimated + bluffThreshold) {
callBluff('markus')
return
}
// Otherwise, Markus raises!
const counts: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }
markusDice.value.forEach(d => counts[d]++)
let bestVal = 2
let maxC = 0
for (let v = 2; v <= 6; v++) {
const effective = counts[v] + counts[1]
if (effective > maxC) {
maxC = effective
bestVal = v
}
}
let nextCount = bid.count
let nextVal = bid.value + 1
if (nextVal > 6) {
nextCount = bid.count + 1
nextVal = Math.min(6, Math.max(2, bestVal))
}
if (nextCount === bid.count && bestVal > bid.value) {
nextVal = bestVal
}
sound.playDialogueBlip()
currentBid.value = { count: nextCount, value: nextVal }
lastBidder.value = 'markus'
liarsDiceMsg.value = `Papa Markus erhöht: Mindestens ${nextCount}x die ${dieIcon(nextVal)}! Was sagst du, mein Sohn?`
// Update player selectors to be just above Markus' bid
if (nextVal < 6) {
selectedBidCount.value = nextCount
selectedBidValue.value = nextVal + 1
} else {
selectedBidCount.value = nextCount + 1
selectedBidValue.value = 2
}
}
function callBluff(caller: 'player' | 'markus') {
if (isRoundEvaluating.value || !currentBid.value) return
isRoundEvaluating.value = true
sound.playCupSlam()
markusDiceRevealed.value = true
const bid = currentBid.value
const allDice = [...playerDice.value, ...markusDice.value]
const totalMatches = allDice.filter(d => d === bid.value || (bid.value !== 1 && d === 1)).length
const bidWasTrue = totalMatches >= bid.count
setTimeout(() => {
sound.playGaoliangShot()
if (caller === 'player') {
if (bidWasTrue) {
// Player called bluff wrongly -> Player loses round!
liarsDiceMarkusWins.value++
shotAnimation.value = 'sascha'
liarsDiceMsg.value = `Aufgedeckt: Es liegen tatsächlich ${totalMatches}x ${dieIcon(bid.value)}! Das Gebot stimmte. Sascha kippt einen Kurzen Gaoliang! 🍶`
} else {
// Player caught Markus bluffing -> Markus loses round!
liarsDicePlayerWins.value++
shotAnimation.value = 'markus'
liarsDiceMsg.value = `Aufgedeckt: Nur ${totalMatches}x ${dieIcon(bid.value)}! Markus hat geblufft und muss einen Gaoliang kippen! 乾杯!`
}
} else {
// Markus called bluff
if (bidWasTrue) {
// Markus called bluff wrongly -> Markus loses round!
liarsDicePlayerWins.value++
shotAnimation.value = 'markus'
liarsDiceMsg.value = `Aufgedeckt: Es liegen ${totalMatches}x ${dieIcon(bid.value)}! Saschas Gebot stimmte! Markus kippt einen Gaoliang!`
} else {
// Markus caught player bluffing -> Player loses round!
liarsDiceMarkusWins.value++
shotAnimation.value = 'sascha'
liarsDiceMsg.value = `Aufgedeckt: Nur ${totalMatches}x ${dieIcon(bid.value)}! Du hast geblufft, Sascha! Trink einen Gaoliang!`
}
}
// Check Match End (Best of 3)
if (liarsDicePlayerWins.value >= 2) {
liarsDiceGameOver.value = true
liarsDicePlayerWon.value = true
sound.playFanfare()
store.addKronkorken(10)
store.addItem('58% Kinmen Gaoliang')
store.unlockEasterEgg('taiwan_dice_champion')
} else if (liarsDiceMarkusWins.value >= 2) {
liarsDiceGameOver.value = true
liarsDicePlayerWon.value = false
}
}, 600)
}
function proceedNextDiceRound() {
liarsDiceRound.value++
startNewDiceRound()
}
function inspectMirror() {
sound.playFanfare()
zulumanMirrorActive.value = true
@@ -356,7 +589,7 @@ function proceedToExpress() {
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
<template v-if="currentRoom === 'schankraum'">
<img src="/assets/scenes/schankraum_v2.jpg?v=3" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
<img src="/assets/scenes/schankraum.jpg" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
<!-- Verena on the wooden gallery -->
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
@@ -463,7 +696,7 @@ function proceedToExpress() {
</button>
</template>
<!-- ================= 3. HINTERLAND ================= -->
<!-- ================= 4. HINTERLAND ================= -->
<template v-else-if="currentRoom === 'hinterland'">
<img src="/assets/scenes/hinterland.jpg" alt="Hinterland Haumdaucher Alm" class="tavern-bg" />
@@ -474,10 +707,22 @@ function proceedToExpress() {
<span class="hotspot-badge">Mane (Wanderin) 🥾</span>
</button>
<!-- Hansi enjoying the view on the bench -->
<!-- Hansi walking towards Münchanien -->
<button class="hotspot hotspot-hansi" @click="openCharacterDialog('hansi')">
<img :src="CHARACTERS.hansi.portrait" class="hotspot-thumb" alt="Hansi" />
<span class="hotspot-badge">Hansi 🧔</span>
<span class="hotspot-badge">Hansi (Richtung Münchanien) 🧔</span>
</button>
<!-- Signpost to Münchanien -->
<button class="hotspot hotspot-sign-munchanien" @click="openMunchanienHint">
<span class="sign-icon">🪧</span>
<span class="hotspot-badge">Wegweiser: Münchanien 🏰</span>
</button>
<!-- Trail to Hinter den Bergen (Taiwan & Papa Markus) -->
<button class="hotspot door-hotspot hotspot-door-hinter-den-bergen" @click="switchRoom('hinter_den_bergen')">
<span class="door-icon">🏔️</span>
<span class="hotspot-badge glow-door">Hinter den Bergen ▶</span>
</button>
<!-- Trail back to Marktplatz -->
@@ -487,7 +732,24 @@ function proceedToExpress() {
</button>
</template>
<!-- ================= 4. HINTERZIMMER (SECRET VIP RAVE) ================= -->
<!-- ================= 5. HINTER DEN BERGEN (TAIWAN & PAPA MARKUS) ================= -->
<template v-else-if="currentRoom === 'hinter_den_bergen'">
<img src="/assets/scenes/hinter_den_bergen.jpg" alt="Hinter den Bergen mit Papa Markus" class="tavern-bg" />
<!-- Markus and Taiwanese companions table -->
<button class="hotspot hotspot-markus" @click="openCharacterDialog('markus')">
<img :src="CHARACTERS.markus.portrait" class="hotspot-thumb" alt="Papa Markus" />
<span class="hotspot-badge">Papa Markus 🎲🍶</span>
</button>
<!-- Trail back to Hinterland -->
<button class="hotspot door-hotspot hotspot-door-back-hinterland" @click="switchRoom('hinterland')">
<span class="door-icon">🌲</span>
<span class="hotspot-badge glow-door">◀ Zum Hinterland</span>
</button>
</template>
<!-- ================= 6. HINTERZIMMER (SECRET VIP RAVE) ================= -->
<template v-else-if="currentRoom === 'hinterzimmer'">
<img src="/assets/scenes/hinterzimmer.jpg" alt="Geheimes VIP Hinterzimmer" class="tavern-bg" />
<div class="strobe-overlay"></div>
@@ -527,6 +789,7 @@ function proceedToExpress() {
currentRoom === 'nebenraum' ? '🎨 AKT III: KÖNIGLICHER NEBENRAUM & ATELIER 🎨' :
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
currentRoom === 'hinter_den_bergen' ? '🏮 AKT III: HINTER DEN BERGEN (TAIWAN) 🏮' :
'🪩 AKT III: DER GEHEIME PARTY BUNKER 🪩'
}}
</div>
@@ -561,6 +824,13 @@ function proceedToExpress() {
>
🌲 Hinterland
</button>
<button
class="room-tab"
:class="{ active: currentRoom === 'hinter_den_bergen' }"
@click="switchRoom('hinter_den_bergen')"
>
🏮 Hinter den Bergen
</button>
<button
v-if="store.unlockedEasterEggs.includes('hinterzimmer') || currentRoom === 'hinterzimmer'"
class="room-tab vip-tab"
@@ -775,6 +1045,178 @@ function proceedToExpress() {
</div>
</div>
</div>
<!-- Münchanien Hint Modal -->
<div v-if="munchanienHintActive" class="dialog-overlay" @click="munchanienHintActive = false">
<div class="dialog-box intro-modal-box" @click.stop>
<div class="intro-modal-header">
<span class="intro-tag">WEGWEISER NACH MÜNCHANIE</span>
<button class="duel-close-btn" @click="munchanienHintActive = false" aria-label="Schließen">✕</button>
</div>
<h2 class="intro-modal-title">🏰 WEGWEISER: NACH MÜNCHANIE</h2>
<p class="intro-modal-story">
Auf dem geschnitzten Wegweiser steht mit Kreide geschrieben:<br><br>
<em>„Hansi ist schon vorausgegangen, um die mächtige Sound-Anlage für das Abschiedskonzert in der Residenzstadt Münchanie aufzubauen! Bereite dich gut vor, sammle Kronkorken und Helles in allen Räumen und besuche alle Freunde, bevor du nach Akt IV aufbrichst!“</em>
</p>
<button class="action-btn" @click="munchanienHintActive = false">
Verstanden! Weiter erkunden
</button>
</div>
</div>
<!-- Liar's Dice Intro Modal -->
<div v-if="liarsDiceIntroOpen" class="dialog-overlay" @click="liarsDiceIntroOpen = false">
<div class="dialog-box intro-modal-box" @click.stop>
<div class="intro-modal-header">
<span class="intro-tag">MINISPIEL INTRO</span>
<button class="duel-close-btn" @click="liarsDiceIntroOpen = false" aria-label="Schließen">✕</button>
</div>
<h2 class="intro-modal-title">🎲 LIAR'S DICE (CHUI NIU / 吹牛)</h2>
<p class="intro-modal-story">
Papa Markus fordert dich zu einer Partie Liar's Dice heraus, wie man es in den Gassen von Taipeh und Kinmen spielt! Wer blufft besser und wer muss den 58% Gaoliang-Schnaps kippen?
</p>
<div class="intro-modal-rules">
<div class="rule-line">🎲 <strong>5 Würfel pro Spieler:</strong> Jeder sieht nur seine eigenen Würfel!</div>
<div class="rule-line">✨ <strong>1er sind Joker:</strong> Zählen für alle Augenzahlen (außer wenn auf 1er geboten wird).</div>
<div class="rule-line">🗣️ <strong>Gebote steigern:</strong> Schätze die Gesamtzahl aller Würfel (deine + Markus). Nächstes Gebot muss höhere Anzahl oder bei gleicher Anzahl höhere Augenzahl sein.</div>
<div class="rule-line">🍶 <strong>Bluff anzweifeln:</strong> Glaubst du Markus lügt? Sag „Bluff!“ – wer unrecht hat, kippt einen Schnaps!</div>
<div class="rule-line">🏆 <strong>Modus:</strong> Best of 3! Belohnung: +10 Kronkorken & 58% Kinmen Gaoliang!</div>
</div>
<button class="action-btn start-duel-action-btn" @click="startLiarsDiceMatch">
🎲 Würfelbecher schütteln!
</button>
</div>
</div>
<!-- Liar's Dice Game Modal -->
<div v-if="liarsDiceActive" class="dialog-overlay">
<div class="dialog-box liars-dice-box">
<div class="liars-dice-header">
<div class="dice-title-group">
<h3>🎲 LIAR'S DICE GEGEN PAPA MARKUS</h3>
<span class="dice-score">Stand: Sascha {{ liarsDicePlayerWins }} : {{ liarsDiceMarkusWins }} Markus (Best of 3)</span>
</div>
<button class="duel-close-btn" @click="liarsDiceActive = false" aria-label="Spiel verlassen">✕</button>
</div>
<!-- Status Message -->
<div class="dice-status-msg">
{{ liarsDiceMsg }}
</div>
<!-- Cups and Dice Arena -->
<div class="dice-arena">
<!-- Markus Side -->
<div class="dice-player-row markus-row">
<div class="dice-player-label">
<img :src="CHARACTERS.markus.portrait" class="dice-avatar" alt="Papa Markus" />
<span>Papa Markus</span>
</div>
<div class="dice-cup-area">
<div v-if="!markusDiceRevealed" class="cup-covered">
<span class="cup-icon">🥤</span>
<span class="cup-text">[ 5 verdeckte Würfel ]</span>
</div>
<div v-else class="dice-revealed-row">
<span v-for="(die, idx) in markusDice" :key="idx" class="dice-die markus-die">
{{ dieIcon(die) }}
</span>
</div>
</div>
</div>
<!-- Current Bid Banner -->
<div class="current-bid-banner" v-if="currentBid">
<span class="bid-label">Aktuelles Gebot:</span>
<span class="bid-value">Mindestens {{ currentBid.count }}x {{ dieIcon(currentBid.value) }}</span>
<span class="bidder-tag">({{ lastBidder === 'player' ? 'von Sascha' : 'von Papa Markus' }})</span>
</div>
<!-- Player Side -->
<div class="dice-player-row player-row">
<div class="dice-player-label">
<span class="player-icon">👑</span>
<span>König Sascha (Du)</span>
</div>
<div class="dice-revealed-row">
<span v-for="(die, idx) in playerDice" :key="idx" class="dice-die player-die">
{{ dieIcon(die) }}
</span>
</div>
</div>
<!-- Shot Drink Penalty Animation Banner -->
<div v-if="shotAnimation" class="shot-animation-banner">
🍶 {{ shotAnimation === 'sascha' ? 'Sascha trinkt einen 58% Gaoliang Shot! 🥴' : 'Markus kippt den Gaoliang Shot! 乾杯! 🎉' }}
</div>
</div>
<!-- Controls / Actions -->
<div v-if="!liarsDiceGameOver && !isRoundEvaluating" class="dice-controls">
<div class="bid-selectors">
<div class="selector-col">
<label>Anzahl:</label>
<div class="counter-btns">
<button class="mini-btn" @click="selectedBidCount = Math.max(1, selectedBidCount - 1)">-</button>
<span class="counter-val">{{ selectedBidCount }}</span>
<button class="mini-btn" @click="selectedBidCount = Math.min(10, selectedBidCount + 1)">+</button>
</div>
</div>
<div class="selector-col">
<label>Augenzahl:</label>
<div class="die-val-btns">
<button
v-for="val in [2, 3, 4, 5, 6, 1]"
:key="val"
class="die-select-btn"
:class="{ active: selectedBidValue === val }"
@click="selectedBidValue = val"
>
{{ dieIcon(val) }} ({{ val }})
</button>
</div>
</div>
</div>
<div class="dice-action-buttons">
<button class="action-btn bid-action-btn" @click="makePlayerBid">
🗣️ Gebot abgeben ({{ selectedBidCount }}x {{ dieIcon(selectedBidValue) }})
</button>
<button
v-if="currentBid && lastBidder === 'markus'"
class="action-btn call-bluff-btn"
@click="callBluff('player')"
>
🤥 Lüge! (Bluff aufdecken)
</button>
</div>
</div>
<!-- Next Round or Game Over -->
<div v-else-if="isRoundEvaluating && !liarsDiceGameOver" class="dice-round-eval">
<button class="action-btn next-round-btn" @click="proceedNextDiceRound">
🎲 Nächste Runde starten ▶
</button>
</div>
<div v-else-if="liarsDiceGameOver" class="dice-game-over">
<template v-if="liarsDicePlayerWon">
<h2 class="victory-title">👑 SASCHA GEWINNT DAS LIAR'S DICE MATCH!</h2>
<p class="dice-over-desc">Papa Markus klopft dir auf die Schulter: „Respekt, Sascha! Du hast das Zeug zum König von Taiwan!“</p>
<p class="reward-text">+10 Kronkorken, 58% Kinmen Gaoliang Flasche & Titel freigeschaltet!</p>
<button class="action-btn" @click="liarsDiceActive = false">乾杯 Papa! Zurücklehnen</button>
</template>
<template v-else>
<h2 class="defeat-title">🍶 PAPA MARKUS TRIUMPHIERT!</h2>
<p class="dice-over-desc">Papa Markus lacht schallend: „Gegen jahrzehntelange Gaoliang-Erfahrung kommst du noch nicht an, Sascha!“</p>
<div class="boar-retry-actions">
<button class="action-btn retry-btn" @click="startLiarsDiceMatch">Revanche spielen!</button>
<button class="close-btn" @click="liarsDiceActive = false">Vielleicht später</button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
@@ -901,11 +1343,11 @@ function proceedToExpress() {
}
/* 1. Schankraum Hotspots */
.hotspot-verena { top: 17%; left: 28%; }
.hotspot-door-nebenraum { top: 42%; left: 34%; }
.hotspot-armend { top: 62%; left: 18%; }
.hotspot-lutz { top: 54%; left: 56%; }
.hotspot-door-marktplatz { top: 50%; left: 84%; }
.hotspot-verena { top: 21%; left: 28%; }
.hotspot-door-nebenraum { top: 45%; left: 34%; }
.hotspot-armend { top: 58%; left: 74%; }
.hotspot-lutz { top: 56%; left: 52%; }
.hotspot-door-marktplatz { top: 48%; left: 86%; }
/* 2. Nebenraum Hotspots */
.hotspot-mirror-nebenraum { top: 36%; left: 24%; }
@@ -974,8 +1416,8 @@ function proceedToExpress() {
.hotspot-door-back-gaststube { top: 74%; left: 20%; }
.hotspot-door-hinterland { top: 34%; left: 76%; }
/* 3. Hinterland Hotspots */
.hotspot-mane { top: 54%; left: 36%; }
/* 4. Hinterland Hotspots */
.hotspot-mane { top: 52%; left: 82%; }
.hiking-anim {
animation: hikeBob 1.2s infinite ease-in-out;
}
@@ -990,10 +1432,16 @@ function proceedToExpress() {
transform: rotate(20deg);
}
.hotspot-hansi { top: 56%; left: 74%; }
.hotspot-door-back-marktplatz { top: 72%; left: 22%; }
.hotspot-hansi { top: 44%; left: 63%; }
.hotspot-sign-munchanien { top: 53%; left: 24%; }
.hotspot-door-hinter-den-bergen { top: 42%; left: 24%; }
.hotspot-door-back-marktplatz { top: 74%; left: 16%; }
/* 4. Hinterzimmer Hotspots & Strobe */
/* 5. Hinter den Bergen Hotspots */
.hotspot-markus { top: 56%; left: 50%; }
.hotspot-door-back-hinterland { top: 74%; left: 20%; }
/* 6. Hinterzimmer Hotspots & Strobe */
.strobe-overlay {
position: absolute;
inset: 0;
@@ -1596,4 +2044,291 @@ function proceedToExpress() {
.start-duel-action-btn:hover {
background: #16a34a !important;
}
/* Liar's Dice (Chui Niu) Modal & UI */
.liars-dice-box {
max-width: 360px;
width: 95%;
}
.liars-dice-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid #78350f;
margin-bottom: 10px;
padding-bottom: 6px;
}
.dice-title-group {
flex: 1;
}
.dice-title-group h3 {
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
color: #ffd700;
margin: 0 0 4px 0;
line-height: 1.3;
}
.dice-score {
font-family: 'Press Start 2P', monospace;
font-size: 6.5px;
color: #fef08a;
}
.dice-status-msg {
font-family: 'MedievalSharp', serif;
font-size: 13.5px;
color: #fef3c7;
min-height: 38px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.3;
margin-bottom: 8px;
}
.dice-arena {
background: #1a0f0a;
border: 1.5px solid #d97706;
border-radius: 6px;
padding: 10px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.dice-player-row {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(0, 0, 0, 0.4);
padding: 6px 8px;
border-radius: 6px;
}
.dice-player-label {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Press Start 2P', monospace;
font-size: 6.5px;
color: #fef08a;
}
.dice-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid #ffd700;
object-fit: cover;
}
.player-icon {
font-size: 14px;
}
.dice-cup-area {
display: flex;
align-items: center;
}
.cup-covered {
display: flex;
align-items: center;
gap: 4px;
color: #94a3b8;
font-family: 'MedievalSharp', serif;
font-size: 12px;
}
.cup-icon {
font-size: 18px;
}
.dice-revealed-row {
display: flex;
gap: 4px;
}
.dice-die {
font-size: 20px;
line-height: 1;
}
.markus-die {
color: #f87171;
}
.player-die {
color: #60a5fa;
}
.current-bid-banner {
background: #451a03;
border: 1px dashed #ffd700;
border-radius: 4px;
padding: 5px 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 11px;
color: #fef08a;
text-align: center;
}
.bid-label {
font-family: 'Press Start 2P', monospace;
font-size: 6px;
color: #f59e0b;
}
.bid-value {
font-weight: bold;
font-size: 13px;
color: #ffd700;
}
.bidder-tag {
font-size: 9.5px;
color: #cbd5e1;
}
.shot-animation-banner {
background: #7f1d1d;
border: 1.5px solid #ef4444;
border-radius: 4px;
padding: 6px;
text-align: center;
font-family: 'MedievalSharp', serif;
font-size: 13px;
color: #fef08a;
animation: pulseAnim 0.8s infinite alternate;
}
.dice-controls {
display: flex;
flex-direction: column;
gap: 10px;
}
.bid-selectors {
display: flex;
gap: 8px;
}
.selector-col {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.selector-col label {
font-family: 'Press Start 2P', monospace;
font-size: 6px;
color: #fed7aa;
}
.counter-btns {
display: flex;
align-items: center;
background: #1a0f0a;
border: 1px solid #78350f;
border-radius: 4px;
justify-content: space-between;
padding: 2px 4px;
}
.mini-btn {
background: #b45309;
border: 1px solid #f59e0b;
color: #fff;
border-radius: 3px;
width: 24px;
height: 24px;
font-weight: bold;
cursor: pointer;
}
.counter-val {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
color: #ffd700;
}
.die-val-btns {
display: flex;
gap: 2px;
}
.die-select-btn {
background: #231209;
border: 1px solid #78350f;
border-radius: 3px;
color: #fef3c7;
font-size: 10px;
padding: 4px 3px;
cursor: pointer;
flex: 1;
}
.die-select-btn.active {
background: #d97706;
border-color: #ffd700;
color: #fff;
font-weight: bold;
}
.dice-action-buttons {
display: flex;
gap: 8px;
}
.bid-action-btn {
flex: 1.5;
background: #15803d !important;
border-color: #86efac !important;
font-family: 'Press Start 2P', monospace;
font-size: 6.5px;
padding: 8px !important;
}
.call-bluff-btn {
flex: 1;
background: #dc2626 !important;
border-color: #fca5a5 !important;
font-family: 'Press Start 2P', monospace;
font-size: 6.5px;
padding: 8px !important;
}
.dice-round-eval {
text-align: center;
}
.next-round-btn {
width: 100%;
background: #2563eb !important;
border-color: #93c5fd !important;
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
padding: 10px !important;
}
.dice-game-over {
text-align: center;
}
.dice-over-desc {
font-family: 'MedievalSharp', serif;
font-size: 13.5px;
margin: 6px 0;
color: #fef3c7;
}
</style>