feat: initialize TRXTD browser tower defense

This commit is contained in:
Tronax 2026-08-15 16:07:09 +02:00
commit c4347f8420
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
34 changed files with 8302 additions and 0 deletions

222
src/components/Hud.vue Normal file
View 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>