Private
Public Access
1
0

fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m3s

This commit is contained in:
Sebastian Unterschütz
2026-03-22 10:44:58 +01:00
parent 1dc5005cf3
commit aff505773a
12 changed files with 693 additions and 16 deletions

View File

@@ -497,6 +497,22 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
msg := fmt.Sprintf("GO IN: %d", g.gameState.TimeLeft)
text.Draw(screen, msg, basicfont.Face7x13, canvasW/2-40, canvasH/2, color.RGBA{255, 255, 0, 255})
} else if g.gameState.Status == "RUNNING" {
// Danger-Overlay: Ab DifficultyFactor > 0.5 rötlicher Bildschirmrand
g.stateMutex.Lock()
df := g.gameState.DifficultyFactor
g.stateMutex.Unlock()
if df > 0.5 {
// Alpha von 0 (bei df=0.5) bis 60 (bei df=1.0)
dangerAlpha := uint8((df - 0.5) * 2.0 * 60)
canvasWf, canvasHf := float32(canvasW), float32(canvasH)
borderW := float32(8)
col := color.RGBA{200, 0, 0, dangerAlpha}
vector.DrawFilledRect(screen, 0, 0, canvasWf, borderW, col, false)
vector.DrawFilledRect(screen, 0, canvasHf-borderW, canvasWf, borderW, col, false)
vector.DrawFilledRect(screen, 0, 0, borderW, canvasHf, col, false)
vector.DrawFilledRect(screen, canvasWf-borderW, 0, borderW, canvasHf, col, false)
}
// Score/Distance Anzeige mit grauem Hintergrund (oben rechts)
dist := fmt.Sprintf("Distance: %.0f m", g.camX/64.0)
scoreStr := fmt.Sprintf("Score: %d", myScore)

View File

@@ -45,10 +45,11 @@ func (g *Game) ApplyInput(input InputState) {
ActiveChunks: g.gameState.WorldChunks,
MovingPlatforms: g.gameState.MovingPlatforms,
}
difficultyFactor := g.gameState.DifficultyFactor
g.stateMutex.Unlock()
// Gemeinsame Physik anwenden (1:1 wie Server)
physics.ApplyPhysics(&state, physicsInput, g.currentSpeed, collisionChecker, physics.DefaultPlayerConstants())
// Gemeinsame Physik anwenden (1:1 wie Server, inkl. Schwierigkeits-Skalierung)
physics.ApplyPhysics(&state, physicsInput, g.currentSpeed, difficultyFactor, collisionChecker, physics.DefaultPlayerConstants())
// Ergebnis zurückschreiben
g.predictedX = state.X

435
cmd/client/web/admin.html Normal file
View File

@@ -0,0 +1,435 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin Panel Escape From Teacher</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
body { background: #0f1117; color: #e0e0e0; }
.navbar { background: #1a1d27 !important; border-bottom: 2px solid #ffc107; }
.navbar-brand { color: #ffc107 !important; font-weight: 700; letter-spacing: 1px; }
.card { background: #1a1d27; border: 1px solid #2a2d3a; }
.card-header { background: #22263a; border-bottom: 1px solid #2a2d3a; }
.table { color: #d0d0d0; }
.table thead th { background: #22263a; color: #ffc107; border-color: #2a2d3a; }
.table tbody td { border-color: #2a2d3a; vertical-align: middle; }
.table tbody tr:hover { background: rgba(255,193,7,0.05); }
.badge-valid { background: #198754; }
.badge-invalid { background: #dc3545; }
.badge-running { background: #0d6efd; }
.badge-lobby { background: #6c757d; }
.badge-countdown { background: #fd7e14; }
.badge-gameover { background: #dc3545; }
.tab-btn { background: #22263a; color: #aaa; border: 1px solid #2a2d3a; }
.tab-btn.active { background: #ffc107; color: #111; border-color: #ffc107; font-weight: 600; }
.tab-btn:hover:not(.active) { background: #2a2d3a; color: #fff; }
.player-row { font-size: 0.85rem; background: #181b28; }
.difficulty-bar { height: 6px; background: #2a2d3a; border-radius: 3px; overflow: hidden; }
.difficulty-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.stat-card { background: #22263a; border-radius: 8px; padding: 14px 18px; border: 1px solid #2a2d3a; }
.stat-card .stat-value { font-size: 2rem; font-weight: 700; color: #ffc107; }
.stat-card .stat-label { font-size: 0.78rem; color: #888; text-transform: uppercase; letter-spacing: 1px; }
.spinner-inline { width: 16px; height: 16px; border-width: 2px; }
.alert-toast { position: fixed; bottom: 24px; right: 24px; z-index: 9999; min-width: 260px; }
[x-cloak] { display: none !important; }
</style>
</head>
<body x-data="adminApp()" x-init="init()" x-cloak>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg">
<div class="container-fluid px-4">
<span class="navbar-brand">🎮 EFT Admin Panel</span>
<div class="d-flex align-items-center gap-3">
<span class="text-muted small" x-text="'Letztes Update: ' + lastUpdate"></span>
<div class="spinner-border spinner-inline text-warning" x-show="loading" role="status"></div>
</div>
</div>
</nav>
<div class="container-fluid px-4 py-4">
<!-- Stat-Karten -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-value" x-text="rooms.length"></div>
<div class="stat-label">Aktive Räume</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-value" x-text="totalPlayers"></div>
<div class="stat-label">Spieler Online</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-value" x-text="leaderboard.length"></div>
<div class="stat-label">Leaderboard-Einträge</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-value" x-text="invalidCount"></div>
<div class="stat-label">Ungültige Einträge</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="d-flex gap-2 mb-3">
<button class="btn tab-btn px-4" :class="{ active: tab === 'rooms' }" @click="tab = 'rooms'">
🏠 Laufende Räume
</button>
<button class="btn tab-btn px-4" :class="{ active: tab === 'leaderboard' }" @click="tab = 'leaderboard'">
🏆 Leaderboard
</button>
</div>
<!-- ===== TAB: RÄUME ===== -->
<div x-show="tab === 'rooms'">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center py-2 px-3">
<span class="fw-semibold">Laufende Räume</span>
<button class="btn btn-sm btn-outline-warning" @click="loadRooms()">⟳ Aktualisieren</button>
</div>
<div class="card-body p-0">
<template x-if="rooms.length === 0">
<p class="text-muted text-center py-4 mb-0">Keine aktiven Räume</p>
</template>
<div class="accordion" id="roomsAccordion">
<template x-for="(room, i) in rooms" :key="room.id">
<div class="accordion-item bg-transparent border-0 border-bottom" style="border-color: #2a2d3a !important">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2 px-3"
style="background:#1a1d27; color:#ddd; box-shadow:none;"
type="button"
@click="room._open = !room._open">
<div class="d-flex align-items-center gap-3 w-100">
<!-- Status Badge -->
<span class="badge"
:class="{
'badge-running': room.status === 'RUNNING',
'badge-lobby': room.status === 'LOBBY',
'badge-countdown': room.status === 'COUNTDOWN',
'badge-gameover': room.status === 'GAMEOVER'
}"
x-text="room.status"></span>
<!-- Raum ID -->
<span class="fw-semibold font-monospace small" x-text="room.id"></span>
<!-- Spieler -->
<span class="text-muted small">
👤 <span x-text="room.alive_count"></span>/<span x-text="room.player_count"></span> am Leben
</span>
<!-- Zeit -->
<span class="text-muted small ms-auto me-3" x-show="room.status === 'RUNNING'">
<span x-text="formatTime(room.elapsed_seconds)"></span>
</span>
</div>
</button>
</h2>
<div x-show="room._open" x-transition>
<div class="p-3">
<!-- Raum-Stats -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<div class="small text-muted">Scroll X</div>
<div class="fw-semibold" x-text="Math.round(room.scroll_x) + ' px'"></div>
</div>
<div class="col-6 col-md-3">
<div class="small text-muted">Geschwindigkeit</div>
<div class="fw-semibold" x-text="room.current_speed.toFixed(1) + ' px/tick'"></div>
</div>
<div class="col-6 col-md-3">
<div class="small text-muted">Schwierigkeit</div>
<div class="difficulty-bar mt-1">
<div class="difficulty-fill"
:style="'width:' + (room.difficulty_factor * 100) + '%; background:' + diffColor(room.difficulty_factor)"></div>
</div>
<div class="small mt-1" x-text="(room.difficulty_factor * 100).toFixed(0) + '%'"></div>
</div>
<div class="col-6 col-md-3">
<div class="small text-muted">Host</div>
<div class="fw-semibold font-monospace small" x-text="room.host_id || '—'"></div>
</div>
</div>
<!-- Spieler-Tabelle -->
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Name</th>
<th>Score</th>
<th>Status</th>
<th>Powerups</th>
</tr>
</thead>
<tbody>
<template x-for="p in room.players" :key="p.name">
<tr class="player-row">
<td x-text="p.name"></td>
<td class="text-warning fw-semibold" x-text="p.score"></td>
<td>
<span class="badge" :class="p.is_alive ? 'bg-success' : 'bg-secondary'"
x-text="p.is_spectator ? 'Zuschauer' : (p.is_alive ? 'Am Leben' : 'Tot')"></span>
</td>
<td>
<span x-show="p.has_double_jump" class="badge bg-info me-1">2x Sprung</span>
<span x-show="p.has_godmode" class="badge bg-warning text-dark">Godmode</span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- ===== TAB: LEADERBOARD ===== -->
<div x-show="tab === 'leaderboard'">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center py-2 px-3">
<div class="d-flex align-items-center gap-3">
<span class="fw-semibold">Leaderboard-Einträge</span>
<!-- Filter -->
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="filterInvalid" x-model="showOnlyInvalid">
<label class="form-check-label small" for="filterInvalid">Nur ungültige</label>
</div>
</div>
<div class="d-flex gap-2">
<input class="form-control form-control-sm bg-dark text-light border-secondary"
style="max-width:200px"
placeholder="🔍 Suchen..." x-model="search">
<button class="btn btn-sm btn-outline-warning" @click="loadLeaderboard()">⟳ Aktualisieren</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Score</th>
<th>Player Code</th>
<th>Proof Code</th>
<th>Zeitstempel</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<template x-for="(e, i) in filteredLeaderboard" :key="e.key">
<tr :class="!e.valid ? 'table-danger bg-opacity-25' : ''">
<td class="text-muted small" x-text="i + 1"></td>
<td class="fw-semibold" x-text="e.player_name"></td>
<td class="text-warning fw-bold" x-text="e.score"></td>
<td class="font-monospace small text-muted" x-text="e.player_code?.substring(0,12) + '...'"></td>
<td class="font-monospace small" x-text="e.proof_code"></td>
<td class="small text-muted" x-text="formatTs(e.timestamp)"></td>
<td>
<span class="badge"
:class="e.valid ? 'badge-valid' : 'badge-invalid'"
x-text="e.valid ? '✓ Gültig' : '✗ Ungültig'"></span>
</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="confirmDelete(e)"
title="Eintrag löschen">
🗑
</button>
</td>
</tr>
</template>
<template x-if="filteredLeaderboard.length === 0">
<tr>
<td colspan="8" class="text-center text-muted py-4">Keine Einträge gefunden</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-light border border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title text-warning">Eintrag löschen?</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" x-show="deleteTarget">
<p>Soll dieser Eintrag wirklich dauerhaft gelöscht werden?</p>
<table class="table table-sm">
<tr>
<td class="text-muted">Name</td>
<td class="fw-semibold" x-text="deleteTarget?.player_name"></td>
</tr>
<tr>
<td class="text-muted">Score</td>
<td class="text-warning fw-bold" x-text="deleteTarget?.score"></td>
</tr>
<tr>
<td class="text-muted">Status</td>
<td>
<span class="badge" :class="deleteTarget?.valid ? 'badge-valid' : 'badge-invalid'"
x-text="deleteTarget?.valid ? '✓ Gültig' : '✗ Ungültig'"></span>
</td>
</tr>
</table>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-danger" @click="deleteEntry()">Löschen</button>
</div>
</div>
</div>
</div>
<!-- Toast Notification -->
<div class="alert-toast" x-show="toast.show" x-transition>
<div class="alert mb-0 shadow"
:class="toast.type === 'success' ? 'alert-success' : 'alert-danger'"
x-text="toast.message">
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function adminApp() {
return {
tab: 'rooms',
rooms: [],
leaderboard: [],
loading: false,
lastUpdate: '—',
search: '',
showOnlyInvalid: false,
deleteTarget: null,
deleteModal: null,
toast: { show: false, message: '', type: 'success' },
autoRefreshInterval: null,
init() {
this.deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'))
this.loadAll()
// Auto-Refresh alle 5 Sekunden
this.autoRefreshInterval = setInterval(() => this.loadAll(), 5000)
},
async loadAll() {
await Promise.all([this.loadRooms(), this.loadLeaderboard()])
this.lastUpdate = new Date().toLocaleTimeString('de-DE')
},
async loadRooms() {
this.loading = true
try {
const r = await fetch('/admin/api/rooms')
if (!r.ok) throw new Error('Fehler ' + r.status)
const data = await r.json()
// _open-State erhalten
const openMap = {}
this.rooms.forEach(r => { if (r._open) openMap[r.id] = true })
this.rooms = (data || []).map(r => ({ ...r, _open: !!openMap[r.id] }))
} catch(e) {
this.showToast('Räume konnten nicht geladen werden: ' + e.message, 'error')
} finally {
this.loading = false
}
},
async loadLeaderboard() {
try {
const r = await fetch('/admin/api/leaderboard')
if (!r.ok) throw new Error('Fehler ' + r.status)
this.leaderboard = await r.json() || []
} catch(e) {
this.showToast('Leaderboard konnte nicht geladen werden: ' + e.message, 'error')
}
},
get filteredLeaderboard() {
return this.leaderboard.filter(e => {
if (this.showOnlyInvalid && e.valid) return false
if (this.search) {
const q = this.search.toLowerCase()
return e.player_name?.toLowerCase().includes(q) ||
e.player_code?.toLowerCase().includes(q) ||
String(e.score).includes(q)
}
return true
})
},
get totalPlayers() {
return this.rooms.reduce((s, r) => s + r.player_count, 0)
},
get invalidCount() {
return this.leaderboard.filter(e => !e.valid).length
},
confirmDelete(entry) {
this.deleteTarget = entry
this.deleteModal.show()
},
async deleteEntry() {
if (!this.deleteTarget) return
const key = this.deleteTarget.key
try {
const r = await fetch('/admin/api/leaderboard/' + encodeURIComponent(key), { method: 'DELETE' })
if (!r.ok) throw new Error('Fehler ' + r.status)
this.leaderboard = this.leaderboard.filter(e => e.key !== key)
this.showToast('Eintrag gelöscht: ' + this.deleteTarget.player_name, 'success')
} catch(e) {
this.showToast('Löschen fehlgeschlagen: ' + e.message, 'error')
} finally {
this.deleteModal.hide()
this.deleteTarget = null
}
},
showToast(message, type = 'success') {
this.toast = { show: true, message, type }
setTimeout(() => { this.toast.show = false }, 3500)
},
diffColor(f) {
if (f < 0.33) return '#198754'
if (f < 0.66) return '#fd7e14'
return '#dc3545'
},
formatTime(s) {
const m = Math.floor(s / 60)
const sec = s % 60
return (m > 0 ? m + 'm ' : '') + sec + 's'
},
formatTs(ts) {
if (!ts) return '—'
return new Date(ts * 1000).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
})
}
}
}
</script>
</body>
</html>