fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m3s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m3s
This commit is contained in:
7
.github/workflows/deploy.yaml
vendored
7
.github/workflows/deploy.yaml
vendored
@@ -165,6 +165,13 @@ jobs:
|
|||||||
sed -i "s|\${TARGET_NS}|${{ env.TARGET_NS }}|g" k8s/ingress.yaml
|
sed -i "s|\${TARGET_NS}|${{ env.TARGET_NS }}|g" k8s/ingress.yaml
|
||||||
sed -i "s|\${IMAGE_NAME}|${{ env.DEPLOY_IMAGE }}|g" k8s/app.yaml
|
sed -i "s|\${IMAGE_NAME}|${{ env.DEPLOY_IMAGE }}|g" k8s/app.yaml
|
||||||
|
|
||||||
|
# Admin-Credentials Secret anlegen/aktualisieren (aus Gitea Secret)
|
||||||
|
kubectl create secret generic admin-credentials \
|
||||||
|
--from-literal=username="${{ secrets.ADMIN_USER }}" \
|
||||||
|
--from-literal=password="${{ secrets.ADMIN_PASSWORD }}" \
|
||||||
|
--namespace=${{ env.TARGET_NS }} \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
# Anwenden
|
# Anwenden
|
||||||
echo "Deploying to Namespace: ${{ env.TARGET_NS }} (Image: ${{ env.DEPLOY_IMAGE }})"
|
echo "Deploying to Namespace: ${{ env.TARGET_NS }} (Image: ${{ env.DEPLOY_IMAGE }})"
|
||||||
kubectl apply -f k8s/compress-middleware.yaml -n ${{ env.TARGET_NS }}
|
kubectl apply -f k8s/compress-middleware.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
|||||||
@@ -497,6 +497,22 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
msg := fmt.Sprintf("GO IN: %d", g.gameState.TimeLeft)
|
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})
|
text.Draw(screen, msg, basicfont.Face7x13, canvasW/2-40, canvasH/2, color.RGBA{255, 255, 0, 255})
|
||||||
} else if g.gameState.Status == "RUNNING" {
|
} 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)
|
// Score/Distance Anzeige mit grauem Hintergrund (oben rechts)
|
||||||
dist := fmt.Sprintf("Distance: %.0f m", g.camX/64.0)
|
dist := fmt.Sprintf("Distance: %.0f m", g.camX/64.0)
|
||||||
scoreStr := fmt.Sprintf("Score: %d", myScore)
|
scoreStr := fmt.Sprintf("Score: %d", myScore)
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ func (g *Game) ApplyInput(input InputState) {
|
|||||||
ActiveChunks: g.gameState.WorldChunks,
|
ActiveChunks: g.gameState.WorldChunks,
|
||||||
MovingPlatforms: g.gameState.MovingPlatforms,
|
MovingPlatforms: g.gameState.MovingPlatforms,
|
||||||
}
|
}
|
||||||
|
difficultyFactor := g.gameState.DifficultyFactor
|
||||||
g.stateMutex.Unlock()
|
g.stateMutex.Unlock()
|
||||||
|
|
||||||
// Gemeinsame Physik anwenden (1:1 wie Server)
|
// Gemeinsame Physik anwenden (1:1 wie Server, inkl. Schwierigkeits-Skalierung)
|
||||||
physics.ApplyPhysics(&state, physicsInput, g.currentSpeed, collisionChecker, physics.DefaultPlayerConstants())
|
physics.ApplyPhysics(&state, physicsInput, g.currentSpeed, difficultyFactor, collisionChecker, physics.DefaultPlayerConstants())
|
||||||
|
|
||||||
// Ergebnis zurückschreiben
|
// Ergebnis zurückschreiben
|
||||||
g.predictedX = state.X
|
g.predictedX = state.X
|
||||||
|
|||||||
435
cmd/client/web/admin.html
Normal file
435
cmd/client/web/admin.html
Normal 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>
|
||||||
135
cmd/server/admin.go
Normal file
135
cmd/server/admin.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// adminRoomPlayer ist eine vereinfachte Spieler-Ansicht für das Admin-Panel
|
||||||
|
type adminRoomPlayer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
IsAlive bool `json:"is_alive"`
|
||||||
|
IsSpectator bool `json:"is_spectator"`
|
||||||
|
X float64 `json:"x"`
|
||||||
|
HasDoubleJump bool `json:"has_double_jump"`
|
||||||
|
HasGodMode bool `json:"has_godmode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// adminRoom ist eine vereinfachte Raum-Ansicht für das Admin-Panel
|
||||||
|
type adminRoom struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
PlayerCount int `json:"player_count"`
|
||||||
|
AliveCount int `json:"alive_count"`
|
||||||
|
ScrollX float64 `json:"scroll_x"`
|
||||||
|
CurrentSpeed float64 `json:"current_speed"`
|
||||||
|
DifficultyFactor float64 `json:"difficulty_factor"`
|
||||||
|
ElapsedSeconds int `json:"elapsed_seconds"`
|
||||||
|
HostID string `json:"host_id"`
|
||||||
|
Players []adminRoomPlayer `json:"players"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAdminRoutes registriert alle Admin-Routen mit BasicAuth
|
||||||
|
func RegisterAdminRoutes(r *gin.Engine) {
|
||||||
|
user := os.Getenv("ADMIN_USER")
|
||||||
|
if user == "" {
|
||||||
|
user = "admin"
|
||||||
|
}
|
||||||
|
password := os.Getenv("ADMIN_PASSWORD")
|
||||||
|
if password == "" {
|
||||||
|
password = "changeme"
|
||||||
|
}
|
||||||
|
|
||||||
|
admin := r.Group("/admin", gin.BasicAuth(gin.Accounts{user: password}))
|
||||||
|
|
||||||
|
// Admin Panel HTML
|
||||||
|
admin.GET("", func(c *gin.Context) {
|
||||||
|
c.Header("Cache-Control", "no-store")
|
||||||
|
http.ServeFile(c.Writer, c.Request, "./cmd/client/web/admin.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- API: Leaderboard ---
|
||||||
|
|
||||||
|
admin.GET("/api/leaderboard", func(c *gin.Context) {
|
||||||
|
if server.GlobalLeaderboard == nil {
|
||||||
|
c.JSON(503, gin.H{"error": "Leaderboard nicht verfügbar"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entries := server.GlobalLeaderboard.GetAll()
|
||||||
|
c.JSON(200, entries)
|
||||||
|
})
|
||||||
|
|
||||||
|
admin.DELETE("/api/leaderboard/:key", func(c *gin.Context) {
|
||||||
|
key := c.Param("key")
|
||||||
|
if key == "" {
|
||||||
|
c.JSON(400, gin.H{"error": "Kein Key angegeben"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if server.GlobalLeaderboard == nil {
|
||||||
|
c.JSON(503, gin.H{"error": "Leaderboard nicht verfügbar"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := server.GlobalLeaderboard.DeleteEntry(key); err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{"ok": true, "deleted": key})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- API: Rooms ---
|
||||||
|
|
||||||
|
admin.GET("/api/rooms", func(c *gin.Context) {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]adminRoom, 0, len(rooms))
|
||||||
|
for _, room := range rooms {
|
||||||
|
room.Mutex.RLock()
|
||||||
|
|
||||||
|
elapsed := 0
|
||||||
|
if room.Status == "RUNNING" || room.Status == "GAMEOVER" {
|
||||||
|
elapsed = int(time.Since(room.GameStartTime).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
players := make([]adminRoomPlayer, 0, len(room.Players))
|
||||||
|
aliveCount := 0
|
||||||
|
for _, p := range room.Players {
|
||||||
|
if p.IsAlive && !p.IsSpectator {
|
||||||
|
aliveCount++
|
||||||
|
}
|
||||||
|
players = append(players, adminRoomPlayer{
|
||||||
|
Name: p.Name,
|
||||||
|
Score: p.Score,
|
||||||
|
IsAlive: p.IsAlive,
|
||||||
|
IsSpectator: p.IsSpectator,
|
||||||
|
X: p.X,
|
||||||
|
HasDoubleJump: p.HasDoubleJump,
|
||||||
|
HasGodMode: p.HasGodMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, adminRoom{
|
||||||
|
ID: room.ID,
|
||||||
|
Status: room.Status,
|
||||||
|
PlayerCount: len(room.Players),
|
||||||
|
AliveCount: aliveCount,
|
||||||
|
ScrollX: room.GlobalScrollX,
|
||||||
|
CurrentSpeed: room.CurrentSpeed,
|
||||||
|
DifficultyFactor: room.DifficultyFactor,
|
||||||
|
ElapsedSeconds: elapsed,
|
||||||
|
HostID: room.HostID,
|
||||||
|
Players: players,
|
||||||
|
})
|
||||||
|
|
||||||
|
room.Mutex.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -88,6 +88,9 @@ func SetupGinServer(ec *nats.EncodedConn, port string) *gin.Engine {
|
|||||||
r.StaticFile("/main.wasm", "./cmd/client/web/main.wasm")
|
r.StaticFile("/main.wasm", "./cmd/client/web/main.wasm")
|
||||||
r.StaticFile("/background.jpg", "./cmd/client/web/background.jpg")
|
r.StaticFile("/background.jpg", "./cmd/client/web/background.jpg")
|
||||||
|
|
||||||
|
// Admin Panel
|
||||||
|
RegisterAdminRoutes(r)
|
||||||
|
|
||||||
// 404 Handler
|
// 404 Handler
|
||||||
r.NoRoute(func(c *gin.Context) {
|
r.NoRoute(func(c *gin.Context) {
|
||||||
c.JSON(404, gin.H{
|
c.JSON(404, gin.H{
|
||||||
|
|||||||
10
k8s/app.yaml
10
k8s/app.yaml
@@ -27,6 +27,16 @@ spec:
|
|||||||
value: "redis:6379"
|
value: "redis:6379"
|
||||||
- name: NATS_URL
|
- name: NATS_URL
|
||||||
value: "nats://nats:4222"
|
value: "nats://nats:4222"
|
||||||
|
- name: ADMIN_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: admin-credentials
|
||||||
|
key: username
|
||||||
|
- name: ADMIN_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: admin-credentials
|
||||||
|
key: password
|
||||||
- name: TOTAL_REPLICAS
|
- name: TOTAL_REPLICAS
|
||||||
value: "2"
|
value: "2"
|
||||||
- name: POD_NAME
|
- name: POD_NAME
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ const (
|
|||||||
TileSize = 64
|
TileSize = 64
|
||||||
|
|
||||||
// Player Movement (bei 20 TPS)
|
// Player Movement (bei 20 TPS)
|
||||||
RunSpeed = 21.0 // Basis-Scroll-Geschwindigkeit
|
RunSpeed = 21.0 // Basis-Scroll-Geschwindigkeit
|
||||||
PlayerSpeed = 33.0 // Links/Rechts Bewegung relativ zu Scroll (war 11.0 * 3)
|
PlayerSpeed = 33.0 // Links/Rechts Bewegung relativ zu Scroll (war 11.0 * 3)
|
||||||
JumpVelocity = 24.0 // Sprunghöhe (reduziert für besseres Gefühl)
|
AirControlFactor = 0.4 // In der Luft: nur 40% der normalen Horizontalkontrolle (Basis)
|
||||||
FastFall = 45.0 // Schnell-Fall nach unten
|
AirControlMin = 0.15 // Minimale Air-Control bei maximaler Schwierigkeit
|
||||||
WallSlideMax = 9.0 // Maximale Rutsch-Geschwindigkeit an Wand
|
JumpVelocity = 24.0 // Sprunghöhe (reduziert für besseres Gefühl)
|
||||||
WallClimbSpeed = 15.0 // Kletter-Geschwindigkeit
|
FastFall = 45.0 // Schnell-Fall nach unten
|
||||||
|
WallSlideMax = 9.0 // Maximale Rutsch-Geschwindigkeit an Wand
|
||||||
|
WallClimbSpeed = 15.0 // Kletter-Geschwindigkeit
|
||||||
|
GravityMax = 2.8 // Maximale Gravitation (bei DifficultyFactor=1.0)
|
||||||
|
MaxDifficultySeconds = 180.0 // Sekunden bis maximale Schwierigkeit erreicht ist
|
||||||
|
|
||||||
// Gameplay
|
// Gameplay
|
||||||
StartTime = 5 // Sekunden Countdown
|
StartTime = 5 // Sekunden Countdown
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ type GameState struct {
|
|||||||
MovingPlatforms []MovingPlatformSync `json:"moving_platforms"` // Bewegende Plattformen
|
MovingPlatforms []MovingPlatformSync `json:"moving_platforms"` // Bewegende Plattformen
|
||||||
Sequence uint32 `json:"sequence"` // Sequenznummer für Out-of-Order-Erkennung
|
Sequence uint32 `json:"sequence"` // Sequenznummer für Out-of-Order-Erkennung
|
||||||
CurrentSpeed float64 `json:"current_speed"` // Aktuelle Scroll-Geschwindigkeit (für Client-Prediction)
|
CurrentSpeed float64 `json:"current_speed"` // Aktuelle Scroll-Geschwindigkeit (für Client-Prediction)
|
||||||
|
DifficultyFactor float64 `json:"difficulty_factor"` // 0.0 (Anfang) bis 1.0 (Maximum) – skaliert Gravitation & Air-Control
|
||||||
}
|
}
|
||||||
|
|
||||||
// MovingPlatformSync: Synchronisiert die Position einer bewegenden Plattform
|
// MovingPlatformSync: Synchronisiert die Position einer bewegenden Plattform
|
||||||
|
|||||||
@@ -56,11 +56,23 @@ func ApplyPhysics(
|
|||||||
state *PlayerPhysicsState,
|
state *PlayerPhysicsState,
|
||||||
input PhysicsInput,
|
input PhysicsInput,
|
||||||
currentSpeed float64,
|
currentSpeed float64,
|
||||||
|
difficultyFactor float64,
|
||||||
collisionChecker CollisionChecker,
|
collisionChecker CollisionChecker,
|
||||||
playerConst PlayerConstants,
|
playerConst PlayerConstants,
|
||||||
) {
|
) {
|
||||||
|
// Schwierigkeits-skalierte Parameter
|
||||||
|
// Air Control sinkt von AirControlFactor (0.4) auf AirControlMin (0.15)
|
||||||
|
effectiveAirControl := config.AirControlFactor - (config.AirControlFactor-config.AirControlMin)*difficultyFactor
|
||||||
|
// Gravitation steigt von Gravity (1.5) auf GravityMax (2.8)
|
||||||
|
effectiveGravity := config.Gravity + (config.GravityMax-config.Gravity)*difficultyFactor
|
||||||
|
|
||||||
// --- HORIZONTALE BEWEGUNG MIT KOLLISION ---
|
// --- HORIZONTALE BEWEGUNG MIT KOLLISION ---
|
||||||
playerMovement := input.InputX * config.PlayerSpeed
|
// In der Luft: reduzierte Horizontalkontrolle (skaliert mit Schwierigkeit)
|
||||||
|
airControl := 1.0
|
||||||
|
if !state.OnGround && !state.OnWall {
|
||||||
|
airControl = effectiveAirControl
|
||||||
|
}
|
||||||
|
playerMovement := input.InputX * config.PlayerSpeed * airControl
|
||||||
speed := currentSpeed + playerMovement
|
speed := currentSpeed + playerMovement
|
||||||
nextX := state.X + speed
|
nextX := state.X + speed
|
||||||
|
|
||||||
@@ -96,8 +108,8 @@ func ApplyPhysics(
|
|||||||
state.VY = -config.WallClimbSpeed
|
state.VY = -config.WallClimbSpeed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal: Volle Gravität
|
// Normal: Schwierigkeit-skalierte Gravität
|
||||||
state.VY += config.Gravity
|
state.VY += effectiveGravity
|
||||||
if state.VY > config.MaxFall {
|
if state.VY > config.MaxFall {
|
||||||
state.VY = config.MaxFall
|
state.VY = config.MaxFall
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,52 @@ func (lb *Leaderboard) AddScore(name, code string, score int) (bool, string) {
|
|||||||
return true, proofCode
|
return true, proofCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminLeaderboardEntry ist ein LeaderboardEntry mit zusätzlichem Key und Validierungsstatus
|
||||||
|
type AdminLeaderboardEntry struct {
|
||||||
|
game.LeaderboardEntry
|
||||||
|
Key string `json:"key"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll gibt alle Leaderboard-Einträge zurück (für Admin-Panel)
|
||||||
|
func (lb *Leaderboard) GetAll() []AdminLeaderboardEntry {
|
||||||
|
raw, err := lb.rdb.HGetAll(lb.ctx, "leaderboard:entries").Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Fehler beim Abrufen aller Einträge: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]AdminLeaderboardEntry, 0, len(raw))
|
||||||
|
for key, dataStr := range raw {
|
||||||
|
var e game.LeaderboardEntry
|
||||||
|
if err := json.Unmarshal([]byte(dataStr), &e); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expected := GenerateProofCode(e.PlayerCode, e.Score, e.Timestamp)
|
||||||
|
entries = append(entries, AdminLeaderboardEntry{
|
||||||
|
LeaderboardEntry: e,
|
||||||
|
Key: key,
|
||||||
|
Valid: e.ProofCode == expected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absteigend nach Score sortieren
|
||||||
|
for i := 0; i < len(entries); i++ {
|
||||||
|
for j := i + 1; j < len(entries); j++ {
|
||||||
|
if entries[j].Score > entries[i].Score {
|
||||||
|
entries[i], entries[j] = entries[j], entries[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEntry löscht einen Eintrag aus Leaderboard (Hash + Sorted Set)
|
||||||
|
func (lb *Leaderboard) DeleteEntry(key string) error {
|
||||||
|
lb.rdb.ZRem(lb.ctx, leaderboardKey, key)
|
||||||
|
return lb.rdb.HDel(lb.ctx, "leaderboard:entries", key).Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (lb *Leaderboard) GetTop10() []game.LeaderboardEntry {
|
func (lb *Leaderboard) GetTop10() []game.LeaderboardEntry {
|
||||||
// Hole Top 10 (höchste Scores zuerst)
|
// Hole Top 10 (höchste Scores zuerst)
|
||||||
uniqueKeys, err := lb.rdb.ZRevRange(lb.ctx, leaderboardKey, 0, 9).Result()
|
uniqueKeys, err := lb.rdb.ZRevRange(lb.ctx, leaderboardKey, 0, 9).Result()
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ type Room struct {
|
|||||||
CollectedPowerups map[string]bool // Key: "chunkID_objectIndex"
|
CollectedPowerups map[string]bool // Key: "chunkID_objectIndex"
|
||||||
ScoreAccum float64 // Akkumulator für Distanz-Score
|
ScoreAccum float64 // Akkumulator für Distanz-Score
|
||||||
CurrentSpeed float64 // Aktuelle Geschwindigkeit (steigt mit der Zeit)
|
CurrentSpeed float64 // Aktuelle Geschwindigkeit (steigt mit der Zeit)
|
||||||
|
DifficultyFactor float64 // 0.0 (Start) bis 1.0 (Maximum) – skaliert Schwierigkeit
|
||||||
GameStartTime time.Time // Wann das Spiel gestartet wurde
|
GameStartTime time.Time // Wann das Spiel gestartet wurde
|
||||||
|
|
||||||
// Chunk-Pool für fairen Random-Spawn
|
// Chunk-Pool für fairen Random-Spawn
|
||||||
@@ -368,12 +369,17 @@ func (r *Room) Update() {
|
|||||||
r.CurrentSpeed = config.RunSpeed
|
r.CurrentSpeed = config.RunSpeed
|
||||||
}
|
}
|
||||||
} else if r.Status == "RUNNING" {
|
} else if r.Status == "RUNNING" {
|
||||||
// Geschwindigkeit erhöhen: +0.5 pro 10 Sekunden (max +5.0 nach 100 Sekunden)
|
|
||||||
elapsed := time.Since(r.GameStartTime).Seconds()
|
elapsed := time.Since(r.GameStartTime).Seconds()
|
||||||
speedIncrease := (elapsed / 10.0) * 0.5
|
|
||||||
if speedIncrease > 5.0 {
|
// DifficultyFactor: 0.0 am Start, 1.0 nach MaxDifficultySeconds (180s)
|
||||||
speedIncrease = 5.0
|
r.DifficultyFactor = elapsed / config.MaxDifficultySeconds
|
||||||
|
if r.DifficultyFactor > 1.0 {
|
||||||
|
r.DifficultyFactor = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Geschwindigkeit: quadratische Kurve → am Anfang langsam, dann immer schneller
|
||||||
|
// Bei MaxDifficultySeconds: +18 auf RunSpeed (39 total)
|
||||||
|
speedIncrease := r.DifficultyFactor * r.DifficultyFactor * 18.0
|
||||||
r.CurrentSpeed = config.RunSpeed + speedIncrease
|
r.CurrentSpeed = config.RunSpeed + speedIncrease
|
||||||
|
|
||||||
r.GlobalScrollX += r.CurrentSpeed
|
r.GlobalScrollX += r.CurrentSpeed
|
||||||
@@ -429,7 +435,7 @@ func (r *Room) Update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gemeinsame Physik anwenden (1:1 wie Client!)
|
// Gemeinsame Physik anwenden (1:1 wie Client!)
|
||||||
physics.ApplyPhysics(&state, physicsInput, r.CurrentSpeed, collisionChecker, physics.DefaultPlayerConstants())
|
physics.ApplyPhysics(&state, physicsInput, r.CurrentSpeed, r.DifficultyFactor, collisionChecker, physics.DefaultPlayerConstants())
|
||||||
|
|
||||||
// Ergebnis zurückschreiben
|
// Ergebnis zurückschreiben
|
||||||
p.X = state.X
|
p.X = state.X
|
||||||
@@ -829,6 +835,7 @@ func (r *Room) Broadcast() {
|
|||||||
MovingPlatforms: make([]game.MovingPlatformSync, 0, len(r.MovingPlatforms)),
|
MovingPlatforms: make([]game.MovingPlatformSync, 0, len(r.MovingPlatforms)),
|
||||||
Sequence: r.sequence,
|
Sequence: r.sequence,
|
||||||
CurrentSpeed: r.CurrentSpeed,
|
CurrentSpeed: r.CurrentSpeed,
|
||||||
|
DifficultyFactor: r.DifficultyFactor,
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, p := range r.Players {
|
for id, p := range r.Players {
|
||||||
|
|||||||
Reference in New Issue
Block a user