feat: initialize TRXTD browser tower defense
This commit is contained in:
commit
c4347f8420
34 changed files with 8302 additions and 0 deletions
222
src/components/Hud.vue
Normal file
222
src/components/Hud.vue
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { store } from '@/game/store'
|
||||
import { engine } from '@/game/engine'
|
||||
import { mpgame, uiSetSpeed, uiStartWaveEarly, uiTogglePause } from '@/game/mpgame'
|
||||
import { DIFFICULTIES, ENEMIES, earlyCallBonus } from '@/game/config'
|
||||
|
||||
const confirmExit = ref(false)
|
||||
|
||||
const waveLabel = computed(() => {
|
||||
if (store.endless || store.mp.active) return `Welle ${store.waveNo}`
|
||||
return `Welle ${store.waveNo}/${store.totalWaves}`
|
||||
})
|
||||
|
||||
const nextBonus = computed(() => (store.phase === 'intermission' ? earlyCallBonus(store.nextWaveIn) : 0))
|
||||
|
||||
function previewTooltip(kind: string): string {
|
||||
return ENEMIES[kind as keyof typeof ENEMIES].name
|
||||
}
|
||||
|
||||
function exitToMenu(): void {
|
||||
confirmExit.value = false
|
||||
if (store.mp.active) {
|
||||
mpgame.leave()
|
||||
} else {
|
||||
store.screen = 'menu'
|
||||
store.result = null
|
||||
engine.setPaused(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hud">
|
||||
<div class="hud-stats">
|
||||
<div class="stat gold" title="Gold">🪙 {{ store.money }}</div>
|
||||
<div class="stat lives" title="Leben">❤ {{ store.lives }}</div>
|
||||
<div class="stat wave" title="Welle">🌊 {{ waveLabel }}</div>
|
||||
<div v-if="!store.mp.active" class="stat score" title="Punkte">🏆 {{ store.score }}</div>
|
||||
<div class="stat diff">
|
||||
{{ store.mp.active ? (store.mp.mode === 'coop' ? '🤝 Coop' : '⚔ 1v1') : DIFFICULTIES[store.difficulty].name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hud-wave">
|
||||
<button
|
||||
v-if="store.phase === 'intermission'"
|
||||
class="btn wave-btn pulse"
|
||||
title="Nächste Welle sofort starten (Leertaste)"
|
||||
@click="uiStartWaveEarly()"
|
||||
>
|
||||
⚔ Nächste Welle <span v-if="nextBonus > 0" class="bonus">+{{ nextBonus }}🪙</span>
|
||||
</button>
|
||||
<div v-else-if="store.phase === 'wave'" class="wave-running">
|
||||
<span class="dot" /> Welle läuft…
|
||||
</div>
|
||||
<div v-if="store.phase === 'intermission'" class="countdown">
|
||||
automatisch in {{ store.nextWaveIn }}s
|
||||
</div>
|
||||
<div v-if="store.nextPreview.length && store.phase === 'intermission'" class="preview">
|
||||
<span class="preview-label">Danach:</span>
|
||||
<span
|
||||
v-for="p in store.nextPreview"
|
||||
:key="p.kind"
|
||||
class="preview-chip"
|
||||
:style="{ background: ENEMIES[p.kind].color }"
|
||||
:title="previewTooltip(p.kind)"
|
||||
>
|
||||
{{ ENEMIES[p.kind].name }} ×{{ p.count }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hud-controls">
|
||||
<button
|
||||
v-if="!store.mp.active"
|
||||
class="btn icon"
|
||||
:title="store.paused ? 'Weiter (P)' : 'Pause (P)'"
|
||||
@click="uiTogglePause()"
|
||||
>
|
||||
{{ store.paused ? '▶' : '⏸' }}
|
||||
</button>
|
||||
<button
|
||||
v-for="s in [1, 2, 3]"
|
||||
:key="s"
|
||||
class="btn icon"
|
||||
:class="{ active: store.speed === s }"
|
||||
:title="`Geschwindigkeit ${s}x`"
|
||||
@click="uiSetSpeed(s)"
|
||||
>
|
||||
{{ s }}×
|
||||
</button>
|
||||
<button class="btn icon" :title="store.muted ? 'Ton an (M)' : 'Ton aus (M)'" @click="engine.toggleMute()">
|
||||
{{ store.muted ? '🔇' : '🔊' }}
|
||||
</button>
|
||||
<template v-if="!confirmExit">
|
||||
<button class="btn icon danger" title="Zurück zum Hauptmenü" @click="confirmExit = true">🚪</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn icon danger confirm" title="Wirklich beenden?" @click="exitToMenu">Sicher?</button>
|
||||
<button class="btn icon" @click="confirmExit = false">✖</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hud {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.hud-stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat {
|
||||
background: var(--panel-inset);
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stat.gold { color: #ffd23e; }
|
||||
.stat.lives { color: #ff7a6e; }
|
||||
.stat.wave { color: #7fd8ff; }
|
||||
.stat.score { color: #b8f5c0; }
|
||||
.stat.diff { color: #c9d4e0; font-size: 12px; align-self: center; }
|
||||
|
||||
.hud-wave {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
/* feste Höhe, damit das Spielfeld nicht springt, wenn der Wellen-Button kommt/geht */
|
||||
min-height: 78px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
}
|
||||
.wave-btn {
|
||||
background: linear-gradient(180deg, #e0563f, #b93a28);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
}
|
||||
.wave-btn .bonus {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 6px;
|
||||
padding: 1px 6px;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.wave-running {
|
||||
color: var(--text-dim);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #e0563f;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
.countdown {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.preview {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.preview-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.preview-chip {
|
||||
color: #10151c;
|
||||
font-size: 10.5px;
|
||||
font-weight: 800;
|
||||
padding: 2px 6px;
|
||||
border-radius: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hud-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.icon {
|
||||
min-width: 40px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.icon.active {
|
||||
background: var(--accent);
|
||||
color: #10151c;
|
||||
}
|
||||
.icon.danger:hover {
|
||||
background: #a33;
|
||||
color: #fff;
|
||||
}
|
||||
.icon.confirm {
|
||||
background: #a33;
|
||||
color: #fff;
|
||||
min-width: 60px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue