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

View file

@ -0,0 +1,332 @@
<script setup lang="ts">
import { ref } from 'vue'
import { DIFFICULTIES } from '@/game/config'
import { engine } from '@/game/engine'
import { mpgame } from '@/game/mpgame'
import { loadBest, store } from '@/game/store'
import type { DifficultyId, MPMode } from '@/game/types'
const selected = ref<DifficultyId>(
(typeof localStorage !== 'undefined' && (localStorage.getItem('trxtd-diff') as DifficultyId)) || 'normal',
)
const mpName = ref((typeof localStorage !== 'undefined' && localStorage.getItem('trxtd-name')) || '')
const mpCode = ref('')
function start(): void {
if (typeof localStorage !== 'undefined') localStorage.setItem('trxtd-diff', selected.value)
engine.toggleMute()
engine.toggleMute()
engine.startGame(selected.value)
}
async function createRoom(mode: MPMode): Promise<void> {
const name = mpName.value.trim() || 'Spieler'
if (typeof localStorage !== 'undefined') localStorage.setItem('trxtd-name', name)
await mpgame.create(mode, name)
}
async function joinRoom(): Promise<void> {
const code = mpCode.value.trim().toUpperCase()
if (code.length !== 4) {
store.mp.status = 'Bitte einen 4-stelligen Raum-Code eingeben.'
return
}
const name = mpName.value.trim() || 'Spieler'
if (typeof localStorage !== 'undefined') localStorage.setItem('trxtd-name', name)
await mpgame.join(code, name)
}
</script>
<template>
<div class="start">
<div class="hero">
<div class="towers-float">
<span>🏹</span><span>🧨</span><span></span><span></span><span>💫</span>
</div>
<h1>TRXTD</h1>
<p class="subtitle">Tower Defense im Browser verteidige deine Basis gegen 20 Wellen!</p>
</div>
<div class="diffs">
<button
v-for="d in Object.values(DIFFICULTIES)"
:key="d.id"
class="diff-card"
:class="{ active: selected === d.id }"
@click="selected = d.id"
>
<div class="diff-name">{{ d.name }}</div>
<div class="diff-desc">{{ d.desc }}</div>
<div class="diff-best">🏆 Bestleistung: {{ loadBest(d.id) || '—' }}</div>
</button>
</div>
<button class="start-btn" @click="start"> Solo spielen</button>
<div class="mp">
<h3>Mit Freunden spielen</h3>
<div class="mp-row">
<input v-model="mpName" class="mp-input" placeholder="Dein Name" maxlength="12" />
</div>
<div class="mp-row">
<button class="mp-btn coop" @click="createRoom('coop')">🤝 Coop-Raum erstellen</button>
<button class="mp-btn duel" @click="createRoom('duel')"> 1v1-Raum erstellen</button>
</div>
<div class="mp-row">
<input
v-model="mpCode"
class="mp-input code"
placeholder="CODE"
maxlength="4"
@keyup.enter="joinRoom"
/>
<button class="mp-btn join" @click="joinRoom">Beitreten</button>
</div>
<p v-if="store.mp.status" class="mp-status">{{ store.mp.status }}</p>
<p class="mp-hint">
Benötigt den lokalen Server: <code>npm run server</code> funktioniert im Heimnetzwerk.
</p>
</div>
<div class="help">
<div class="help-col">
<h3>Spielziel</h3>
<p>
Gegner laufen vom <b>Portal</b> (links) zur <b>Basis</b> (rechts). Baue Türme neben dem Weg,
um sie aufzuhalten. Jeder Gegner, der durchkommt, kostet Leben bei 0 ist das Spiel vorbei.
</p>
<h3>Steuerung</h3>
<ul>
<li><b>15</b> Turm auswählen, Klick platziert ihn</li>
<li><b>Klick auf Turm</b> Infos, Upgrade, Verkauf, Zielmodus</li>
<li><b>Leertaste</b> nächste Welle früh starten (Bonus-Gold!)</li>
<li><b>P</b> Pause · <b>M</b> Ton · <b>U</b> Upgrade · <b>V</b> Verkaufen · <b>Esc</b> Abbrechen</li>
</ul>
</div>
<div class="help-col">
<h3>Tipps</h3>
<ul>
<li>Die <b>Kanone</b> trifft keine Flieger die fliegen auf halber Höhe ganz geradeaus!</li>
<li><b>Eisturm</b> verlangsamt alles in Reichweite perfekt an Engstellen.</li>
<li>Wellen früh starten bringt Gold, kostet aber Vorbereitungszeit.</li>
<li><b>Bäume und Felsen</b> blockieren Bauplätze anklicken und gegen Gold entfernen.</li>
<li>Verkauf bringt 70% der Investition zurück umbauen lohnt sich.</li>
</ul>
</div>
</div>
</div>
</template>
<style scoped>
.start {
max-width: 760px;
margin: 0 auto;
padding: 32px 16px 48px;
display: flex;
flex-direction: column;
gap: 22px;
align-items: center;
}
.hero {
text-align: center;
}
.towers-float {
font-size: 34px;
display: flex;
gap: 18px;
justify-content: center;
margin-bottom: 6px;
}
.towers-float span {
animation: float 3s ease-in-out infinite;
display: inline-block;
}
.towers-float span:nth-child(2) { animation-delay: 0.3s; }
.towers-float span:nth-child(3) { animation-delay: 0.6s; }
.towers-float span:nth-child(4) { animation-delay: 0.9s; }
.towers-float span:nth-child(5) { animation-delay: 1.2s; }
@keyframes float {
50% { transform: translateY(-8px); }
}
h1 {
font-size: 56px;
margin: 0;
letter-spacing: 6px;
background: linear-gradient(180deg, #ffd23e, #e08a2e);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: none;
}
.subtitle {
color: var(--text-dim);
margin: 6px 0 0;
font-size: 15px;
}
.diffs {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.diff-card {
background: var(--panel);
border: 2px solid var(--panel-border);
border-radius: 14px;
padding: 14px 16px;
width: 200px;
text-align: left;
cursor: pointer;
color: var(--text);
font-family: inherit;
transition: border-color 0.15s ease, transform 0.1s ease;
}
.diff-card:hover {
transform: translateY(-2px);
}
.diff-card.active {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(245, 197, 66, 0.25);
}
.diff-name {
font-weight: 800;
font-size: 17px;
}
.diff-desc {
color: var(--text-dim);
font-size: 12.5px;
margin-top: 4px;
min-height: 34px;
}
.diff-best {
margin-top: 8px;
font-size: 12px;
color: #ffd23e;
}
.start-btn {
background: linear-gradient(180deg, #59b34d, #3f8f37);
border: none;
color: #fff;
font-size: 20px;
font-weight: 800;
padding: 14px 44px;
border-radius: 14px;
cursor: pointer;
font-family: inherit;
box-shadow: 0 6px 20px rgba(89, 179, 77, 0.35);
}
.start-btn:hover {
filter: brightness(1.1);
}
.mp {
width: 100%;
max-width: 560px;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 14px;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.mp h3 {
margin: 0;
font-size: 15px;
}
.mp-row {
display: flex;
gap: 8px;
}
.mp-input {
flex: 1;
background: var(--panel-inset);
border: 1px solid var(--panel-border);
border-radius: 9px;
color: var(--text);
font-family: inherit;
font-size: 14px;
padding: 9px 12px;
outline: none;
}
.mp-input:focus {
border-color: var(--accent);
}
.mp-input.code {
text-transform: uppercase;
letter-spacing: 4px;
font-weight: 800;
max-width: 140px;
}
.mp-btn {
background: var(--panel-inset);
border: 1px solid var(--panel-border);
color: var(--text);
font-family: inherit;
font-weight: 700;
font-size: 13px;
border-radius: 9px;
padding: 9px 14px;
cursor: pointer;
white-space: nowrap;
}
.mp-btn.coop {
flex: 1;
border-color: #3d6f9e;
}
.mp-btn.duel {
flex: 1;
border-color: #9e503d;
}
.mp-btn.coop:hover,
.mp-btn.duel:hover,
.mp-btn.join:hover {
border-color: var(--accent);
}
.mp-status {
margin: 0;
color: #ff8a7a;
font-size: 12.5px;
}
.mp-hint {
margin: 0;
color: var(--text-dim);
font-size: 11.5px;
}
.mp-hint code {
background: var(--panel-inset);
border-radius: 4px;
padding: 1px 5px;
}
.help {
display: flex;
gap: 24px;
flex-wrap: wrap;
justify-content: center;
color: var(--text-dim);
font-size: 13px;
line-height: 1.55;
}
.help-col {
max-width: 330px;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 12px;
padding: 14px 18px;
}
.help-col h3 {
margin: 4px 0 6px;
color: var(--text);
font-size: 14px;
}
.help-col ul {
margin: 0;
padding-left: 18px;
}
.help-col li {
margin-bottom: 4px;
}
.help-col b {
color: var(--text);
}
</style>