332 lines
8.2 KiB
Vue
332 lines
8.2 KiB
Vue
<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>1–5</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>
|