TRXTD/src/components/StartScreen.vue

332 lines
8.2 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>