bug fixes
This commit is contained in:
86
handlers.go
86
handlers.go
@@ -3,9 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html"
|
"html"
|
||||||
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -63,7 +65,7 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---> HIER RUFEN WIR JETZT DIE SIMULATION AUF <---
|
// ---> HIER RUFEN WIR JETZT DIE SIMULATION AUF <---
|
||||||
isDead, score, obstacles := simulateChunk(req.SessionID, req.Inputs, req.TotalTicks, vals)
|
isDead, score, obstacles, powerUpState, serverTick := simulateChunk(req.SessionID, req.Inputs, req.TotalTicks, vals)
|
||||||
|
|
||||||
status := "alive"
|
status := "alive"
|
||||||
if isDead {
|
if isDead {
|
||||||
@@ -76,6 +78,8 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
|
|||||||
Status: status,
|
Status: status,
|
||||||
VerifiedScore: score,
|
VerifiedScore: score,
|
||||||
ServerObs: obstacles,
|
ServerObs: obstacles,
|
||||||
|
PowerUps: powerUpState,
|
||||||
|
ServerTick: serverTick,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +90,19 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validierung
|
||||||
|
if len(req.Name) > 4 {
|
||||||
|
http.Error(w, "Zu lang", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if containsBadWord(req.Name) {
|
||||||
|
http.Error(w, "Name verboten", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
safeName := html.EscapeString(req.Name)
|
safeName := html.EscapeString(req.Name)
|
||||||
sessionKey := "session:" + req.SessionID
|
sessionKey := "session:" + req.SessionID
|
||||||
|
|
||||||
scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result()
|
scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Session expired", 404)
|
http.Error(w, "Session expired", 404)
|
||||||
@@ -104,10 +119,13 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
|
|||||||
"created_at": timestamp,
|
"created_at": timestamp,
|
||||||
})
|
})
|
||||||
|
|
||||||
rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{
|
// Leaderboard Eintrag
|
||||||
Score: float64(scoreInt),
|
rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
|
||||||
Member: req.SessionID,
|
rdb.ZAdd(ctx, "leaderboard:public", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
|
||||||
})
|
|
||||||
|
rdb.Persist(ctx, sessionKey)
|
||||||
|
|
||||||
|
rdb.HDel(ctx, sessionKey, "obstacles", "rng_state", "pos_y", "vel_y", "p_god_lives", "p_has_bat", "p_boot_ticks")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(SubmitResponse{ClaimCode: claimCode})
|
json.NewEncoder(w).Encode(SubmitResponse{ClaimCode: claimCode})
|
||||||
@@ -191,6 +209,9 @@ func handleAdminAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Bad Request", 400)
|
http.Error(w, "Bad Request", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("👮 ADMIN ACTION: %s on %s", req.Action, req.SessionID)
|
||||||
|
|
||||||
if req.Action == "approve" {
|
if req.Action == "approve" {
|
||||||
score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result()
|
score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -200,7 +221,10 @@ func handleAdminAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else if req.Action == "delete" {
|
} else if req.Action == "delete" {
|
||||||
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
|
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
|
||||||
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
|
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
|
||||||
|
|
||||||
|
rdb.Del(ctx, "session:"+req.SessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,16 +234,28 @@ func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Bad Request", 400)
|
http.Error(w, "Bad Request", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionKey := "session:" + req.SessionID
|
sessionKey := "session:" + req.SessionID
|
||||||
realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()
|
realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()
|
||||||
|
|
||||||
if err != nil || realCode != req.ClaimCode {
|
if err != nil || realCode == "" {
|
||||||
http.Error(w, "Error", 403)
|
http.Error(w, "Not found", 404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if realCode != req.ClaimCode {
|
||||||
|
http.Error(w, "Wrong Code", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)
|
||||||
|
|
||||||
|
// Aus Listen entfernen
|
||||||
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
|
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
|
||||||
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
|
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
|
||||||
rdb.HDel(ctx, sessionKey, "name")
|
|
||||||
|
rdb.Del(ctx, sessionKey)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,3 +267,37 @@ func generateClaimCode() string {
|
|||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
|
||||||
|
key := "config:badwords"
|
||||||
|
|
||||||
|
// GET: Liste abrufen
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
words, _ := rdb.SMembers(ctx, key).Result()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(words)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Hinzufügen oder Löschen
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
// Wir nutzen ein einfaches Struct für den Request
|
||||||
|
type WordReq struct {
|
||||||
|
Word string `json:"word"`
|
||||||
|
Action string `json:"action"` // "add" oder "remove"
|
||||||
|
}
|
||||||
|
var req WordReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Bad Request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Action == "add" && req.Word != "" {
|
||||||
|
rdb.SAdd(ctx, key, strings.ToLower(req.Word))
|
||||||
|
} else if req.Action == "remove" && req.Word != "" {
|
||||||
|
rdb.SRem(ctx, key, strings.ToLower(req.Word))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -30,6 +30,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initGameConfig()
|
initGameConfig()
|
||||||
|
initBadWords()
|
||||||
|
|
||||||
fs := http.FileServer(http.Dir("./static"))
|
fs := http.FileServer(http.Dir("./static"))
|
||||||
http.Handle("/", fs)
|
http.Handle("/", fs)
|
||||||
@@ -44,6 +45,7 @@ func main() {
|
|||||||
|
|
||||||
// Admin Routes (Logger + BasicAuth kombinieren)
|
// Admin Routes (Logger + BasicAuth kombinieren)
|
||||||
http.HandleFunc("/admin", Logger(BasicAuth(handleAdminPage)))
|
http.HandleFunc("/admin", Logger(BasicAuth(handleAdminPage)))
|
||||||
|
http.HandleFunc("/api/admin/badwords", Logger(BasicAuth(handleAdminBadwords)))
|
||||||
http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList)))
|
http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList)))
|
||||||
http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction)))
|
http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction)))
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
padding: 10px; font-size: 16px; width: 250px;
|
padding: 10px; font-size: 16px; width: 250px;
|
||||||
background: #333; border: 1px solid #666; color: white;
|
background: #333; border: 1px solid #666; color: white; outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
@@ -22,14 +22,15 @@
|
|||||||
}
|
}
|
||||||
.tab-btn.active { background: #4caf50; color: white; }
|
.tab-btn.active { background: #4caf50; color: white; }
|
||||||
.tab-btn#tab-public.active { background: #2196F3; } /* Blau für Public */
|
.tab-btn#tab-public.active { background: #2196F3; } /* Blau für Public */
|
||||||
|
.tab-btn#tab-badwords.active { background: #f44336; } /* Rot für Badwords */
|
||||||
|
|
||||||
/* Liste */
|
/* Normale Liste */
|
||||||
.entry {
|
.entry {
|
||||||
background: #333; padding: 15px; margin-bottom: 8px;
|
background: #333; padding: 15px; margin-bottom: 8px;
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
border-left: 5px solid #555;
|
border-left: 5px solid #555;
|
||||||
}
|
}
|
||||||
.entry.highlight { border-left-color: #ffeb3b; background: #444; } /* Suchtreffer */
|
.entry.highlight { border-left-color: #ffeb3b; background: #444; }
|
||||||
|
|
||||||
.info { font-size: 1.1em; }
|
.info { font-size: 1.1em; }
|
||||||
.meta { font-size: 0.85em; color: #aaa; margin-top: 4px; font-family: monospace; }
|
.meta { font-size: 0.85em; color: #aaa; margin-top: 4px; font-family: monospace; }
|
||||||
@@ -37,10 +38,42 @@
|
|||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
button { cursor: pointer; padding: 8px 15px; border: none; font-weight: bold; color: white; border-radius: 4px; }
|
button { cursor: pointer; padding: 8px 15px; border: none; font-weight: bold; color: white; border-radius: 4px; }
|
||||||
.btn-approve { background: #4caf50; margin-right: 5px; }
|
|
||||||
.btn-delete { background: #f44336; }
|
.btn-delete { background: #f44336; }
|
||||||
.btn-delete:hover { background: #d32f2f; }
|
.btn-delete:hover { background: #d32f2f; }
|
||||||
|
|
||||||
|
/* BADWORD STYLES (Tags) */
|
||||||
|
#badword-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badword-tag {
|
||||||
|
background: #222;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid #f44336;
|
||||||
|
color: #ff8a80;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badword-remove {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #f44336;
|
||||||
|
font-weight: bold;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
width: 20px; height: 20px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.badword-remove:hover { background: #f44336; color: white; }
|
||||||
|
|
||||||
.empty { text-align: center; padding: 40px; color: #666; font-style: italic; }
|
.empty { text-align: center; padding: 40px; color: #666; font-style: italic; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -52,28 +85,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button id="tab-unverified" class="tab-btn active" onclick="switchTab('unverified')">⏳ Warteschlange (Prüfen)</button>
|
<button id="tab-public" class="tab-btn active" onclick="switchTab('public')">🏆 Bestenliste</button>
|
||||||
<button id="tab-public" class="tab-btn" onclick="switchTab('public')">🏆 Bestenliste (Fertig)</button>
|
<button id="tab-badwords" class="tab-btn" onclick="switchTab('badwords')">🤬 Badwords</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="list">Lade...</div>
|
<div id="list-section">
|
||||||
|
<div id="list">Lade...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="badword-section" style="display:none;">
|
||||||
|
<div style="margin-bottom: 20px; display:flex; gap:10px;">
|
||||||
|
<input type="text" id="newBadword" placeholder="Neues Wort hinzufügen...">
|
||||||
|
<button onclick="addBadword()" style="background:#f44336; color:white; border:none; padding:10px 20px; cursor:pointer;">+ Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Aktuelle Liste:</h3>
|
||||||
|
<div id="badword-container">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentTab = 'unverified';
|
let currentTab = 'public';
|
||||||
let allEntries = []; // Speichert die geladenen Daten für die Suche
|
let allEntries = [];
|
||||||
|
|
||||||
|
// Initial laden
|
||||||
|
loadList();
|
||||||
|
|
||||||
|
// --- FUNKTIONEN FÜR BESTENLISTE ---
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
const listEl = document.getElementById('list');
|
const listEl = document.getElementById('list');
|
||||||
listEl.innerHTML = '<div style="text-align:center">Lade Daten...</div>';
|
listEl.innerHTML = '<div style="text-align:center">Lade Daten...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// API Aufruf mit Typ (unverified oder public)
|
const res = await fetch('/api/admin/list?type=public');
|
||||||
const res = await fetch('/api/admin/list?type=' + currentTab);
|
|
||||||
allEntries = await res.json();
|
allEntries = await res.json();
|
||||||
|
|
||||||
renderList(allEntries);
|
renderList(allEntries);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
listEl.innerHTML = '<div class="empty">Fehler beim Laden der Daten.</div>';
|
listEl.innerHTML = '<div class="empty">Fehler beim Laden.</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +135,6 @@
|
|||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
data.forEach(entry => {
|
data.forEach(entry => {
|
||||||
// Buttons: In der "Public" Liste brauchen wir keinen "Freigeben" Button mehr
|
|
||||||
let actions = '';
|
|
||||||
if (currentTab === 'unverified') {
|
|
||||||
actions += `<button class="btn-approve" onclick="decide('${entry.sessionId}', 'approve')">✔ OK</button>`;
|
|
||||||
}
|
|
||||||
// Löschen darf man immer (falls man sich verklickt hat oder Schüler nervt)
|
|
||||||
actions += `<button class="btn-delete" onclick="decide('${entry.sessionId}', 'delete')">🗑 Weg</button>`;
|
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@@ -105,65 +145,104 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
${actions}
|
<button class="btn-delete" onclick="decide('${entry.sessionId}', 'delete')">🗑 Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
listEl.innerHTML = html;
|
listEl.innerHTML = html;
|
||||||
|
filterList(); // Suche anwenden falls Text drin steht
|
||||||
// Filter direkt anwenden, falls noch Text in der Suche steht
|
|
||||||
filterList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Such-Logik (Client-Side, rasend schnell)
|
|
||||||
function filterList() {
|
|
||||||
const term = document.getElementById('searchInput').value.toUpperCase();
|
|
||||||
const entries = document.querySelectorAll('.entry');
|
|
||||||
|
|
||||||
entries.forEach(div => {
|
|
||||||
// Wir suchen im gesamten Text des Eintrags (Name, Code, Score)
|
|
||||||
const text = div.innerText.toUpperCase();
|
|
||||||
if (text.includes(term)) {
|
|
||||||
div.style.display = "flex";
|
|
||||||
// Kleines Highlight wenn gesucht wird
|
|
||||||
if(term.length > 0) div.classList.add("highlight");
|
|
||||||
else div.classList.remove("highlight");
|
|
||||||
} else {
|
|
||||||
div.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decide(sid, action) {
|
async function decide(sid, action) {
|
||||||
if(!confirm(action === 'approve' ? "Freigeben?" : "Endgültig löschen?")) return;
|
if(!confirm("Eintrag endgültig löschen?")) return;
|
||||||
|
|
||||||
await fetch('/api/admin/action', {
|
await fetch('/api/admin/action', {
|
||||||
method: 'POST',
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({ sessionId: sid, action: action })
|
body: JSON.stringify({ sessionId: sid, action: action })
|
||||||
});
|
});
|
||||||
loadList(); // Neu laden
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchTab(tab) {
|
|
||||||
currentTab = tab;
|
|
||||||
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
document.getElementById('tab-' + tab).classList.add('active');
|
|
||||||
|
|
||||||
document.getElementById('searchInput').value = "";
|
|
||||||
|
|
||||||
loadList();
|
loadList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
function filterList() {
|
||||||
loadList();
|
const term = document.getElementById('searchInput').value.toUpperCase();
|
||||||
|
const entries = document.querySelectorAll('.entry');
|
||||||
|
entries.forEach(div => {
|
||||||
|
if (div.innerText.toUpperCase().includes(term)) {
|
||||||
|
div.style.display = "flex";
|
||||||
|
if(term.length > 0) div.classList.add("highlight"); else div.classList.remove("highlight");
|
||||||
|
} else { div.style.display = "none"; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
// --- FUNKTIONEN FÜR BADWORDS ---
|
||||||
if(currentTab === 'unverified' && document.getElementById('searchInput').value === "") {
|
async function loadBadwords() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/badwords');
|
||||||
|
const words = await res.json(); // Array von Strings
|
||||||
|
|
||||||
|
const container = document.getElementById('badword-container');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (!words || words.length === 0) {
|
||||||
|
container.innerHTML = "<div class='empty'>Keine Badwords definiert.</div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags generieren
|
||||||
|
words.forEach(w => {
|
||||||
|
container.innerHTML += `
|
||||||
|
<div class="badword-tag">
|
||||||
|
${w}
|
||||||
|
<span class="badword-remove" onclick="removeBadword('${w}')" title="Löschen">✖</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('badword-container').innerHTML = "Fehler beim Laden.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addBadword() {
|
||||||
|
const input = document.getElementById('newBadword');
|
||||||
|
const word = input.value.trim();
|
||||||
|
if(!word) return;
|
||||||
|
|
||||||
|
await fetch('/api/admin/badwords', {
|
||||||
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ action: 'add', word: word })
|
||||||
|
});
|
||||||
|
input.value = "";
|
||||||
|
loadBadwords();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeBadword(word) {
|
||||||
|
if(!confirm(`Das Wort "${word}" von der Liste entfernen?`)) return;
|
||||||
|
await fetch('/api/admin/badwords', {
|
||||||
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ action: 'remove', word: word })
|
||||||
|
});
|
||||||
|
loadBadwords();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TAB UMSCHALTEN ---
|
||||||
|
function switchTab(tab) {
|
||||||
|
currentTab = tab;
|
||||||
|
|
||||||
|
// Buttons stylen
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.getElementById('tab-' + tab).classList.add('active');
|
||||||
|
|
||||||
|
// Ansicht wechseln
|
||||||
|
if (tab === 'badwords') {
|
||||||
|
document.getElementById('list-section').style.display = 'none';
|
||||||
|
document.getElementById('badword-section').style.display = 'block';
|
||||||
|
document.getElementById('searchInput').style.display = 'none';
|
||||||
|
loadBadwords();
|
||||||
|
} else {
|
||||||
|
document.getElementById('list-section').style.display = 'block';
|
||||||
|
document.getElementById('badword-section').style.display = 'none';
|
||||||
|
document.getElementById('searchInput').style.display = 'block';
|
||||||
loadList();
|
loadList();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -8,21 +8,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) {
|
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, PowerUpState, int) {
|
||||||
// --- 1. STATE LADEN ---
|
// 1. State laden
|
||||||
posY := parseOr(vals["pos_y"], PlayerYBase)
|
posY := parseOr(vals["pos_y"], PlayerYBase)
|
||||||
velY := parseOr(vals["vel_y"], 0.0)
|
velY := parseOr(vals["vel_y"], 0.0)
|
||||||
score := int(parseOr(vals["score"], 0))
|
score := int(parseOr(vals["score"], 0))
|
||||||
ticksAlive := int(parseOr(vals["total_ticks"], 0)) // Zeit-Basis
|
ticksAlive := int(parseOr(vals["total_ticks"], 0))
|
||||||
|
|
||||||
rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
|
rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
|
||||||
|
|
||||||
// Powerups
|
// Powerups laden
|
||||||
godLives := int(parseOr(vals["p_god_lives"], 0))
|
godLives := int(parseOr(vals["p_god_lives"], 0))
|
||||||
hasBat := vals["p_has_bat"] == "1"
|
hasBat := vals["p_has_bat"] == "1"
|
||||||
bootTicks := int(parseOr(vals["p_boot_ticks"], 0))
|
bootTicks := int(parseOr(vals["p_boot_ticks"], 0))
|
||||||
|
|
||||||
// Anti-Cheat State laden (Wichtig für Heuristik über Chunks hinweg)
|
// Anti-Cheat State
|
||||||
lastJumpDist := parseOr(vals["ac_last_dist"], 0.0)
|
lastJumpDist := parseOr(vals["ac_last_dist"], 0.0)
|
||||||
suspicionScore := int(parseOr(vals["ac_suspicion"], 0))
|
suspicionScore := int(parseOr(vals["ac_suspicion"], 0))
|
||||||
|
|
||||||
@@ -35,11 +34,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
obstacles = []ActiveObstacle{}
|
obstacles = []ActiveObstacle{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG: Start des Chunks
|
// --- DEBUG: Chunk Info ---
|
||||||
// log.Printf("[%s] Simulating Chunk: %d Ticks, Score: %d", sessionID, totalTicks, score)
|
if len(inputs) > 0 {
|
||||||
|
log.Printf("📦 [%s] Processing Chunk: %d Ticks, %d Inputs", sessionID, totalTicks, len(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
// --- ANTI-CHEAT 1: SPAM SCHUTZ ---
|
// --- ANTI-CHEAT: Spam Check ---
|
||||||
// Wer mehr als 10x pro Sekunde springt, ist ein Bot oder nutzt ein Makro
|
|
||||||
jumpCount := 0
|
jumpCount := 0
|
||||||
for _, inp := range inputs {
|
for _, inp := range inputs {
|
||||||
if inp.Act == "JUMP" {
|
if inp.Act == "JUMP" {
|
||||||
@@ -47,8 +47,8 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if jumpCount > 10 {
|
if jumpCount > 10 {
|
||||||
log.Printf("🤖BOT-ALARM [%s]: Spammt Sprünge (%d Inputs)", sessionID, jumpCount)
|
log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge (%d)", sessionID, jumpCount)
|
||||||
return true, score, obstacles // Sofort tot
|
return true, score, obstacles, PowerUpState{}, ticksAlive
|
||||||
}
|
}
|
||||||
|
|
||||||
playerDead := false
|
playerDead := false
|
||||||
@@ -57,24 +57,26 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
for i := 0; i < totalTicks; i++ {
|
for i := 0; i < totalTicks; i++ {
|
||||||
ticksAlive++
|
ticksAlive++
|
||||||
|
|
||||||
// Speed Scaling (Zeitbasiert)
|
// 1. Speed (Zeitbasiert)
|
||||||
currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5
|
currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5
|
||||||
if currentSpeed > 20.0 {
|
if currentSpeed > 12.0 {
|
||||||
currentSpeed = 20.0
|
currentSpeed = 12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jump Power (Boots Powerup)
|
// 2. Powerups Timer
|
||||||
currentJumpPower := JumpPower
|
currentJumpPower := JumpPower
|
||||||
if bootTicks > 0 {
|
if bootTicks > 0 {
|
||||||
currentJumpPower = HighJumpPower
|
currentJumpPower = HighJumpPower
|
||||||
bootTicks--
|
bootTicks--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input
|
// 3. Input Verarbeitung (MIT DEBUG LOG)
|
||||||
didJump := false
|
didJump := false
|
||||||
isCrouching := false
|
isCrouching := false
|
||||||
for _, inp := range inputs {
|
for _, inp := range inputs {
|
||||||
if inp.Tick == i {
|
if inp.Tick == i {
|
||||||
|
log.Printf("🕹️ [%s] ACTION at Tick %d: %s", sessionID, ticksAlive, inp.Act)
|
||||||
|
|
||||||
if inp.Act == "JUMP" {
|
if inp.Act == "JUMP" {
|
||||||
didJump = true
|
didJump = true
|
||||||
}
|
}
|
||||||
@@ -84,47 +86,38 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Physik Status
|
// 4. Physik
|
||||||
isGrounded := posY >= PlayerYBase-1.0
|
isGrounded := posY >= PlayerYBase-1.0
|
||||||
|
|
||||||
currentHeight := PlayerHeight
|
currentHeight := PlayerHeight
|
||||||
if isCrouching {
|
if isCrouching {
|
||||||
currentHeight = PlayerHeight / 2
|
currentHeight = PlayerHeight / 2
|
||||||
if !isGrounded {
|
if !isGrounded {
|
||||||
velY += 2.0
|
velY += 2.0
|
||||||
} // Fast fall
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Springen & ANTI-CHEAT 2 (Heuristik)
|
|
||||||
if didJump && isGrounded && !isCrouching {
|
if didJump && isGrounded && !isCrouching {
|
||||||
velY = currentJumpPower
|
velY = currentJumpPower
|
||||||
|
|
||||||
// Wir messen den Abstand zum nächsten Hindernis beim Absprung
|
// Heuristik Anti-Cheat (Abstand messen)
|
||||||
var distToObs float64 = -1.0
|
var distToObs float64 = -1.0
|
||||||
for _, o := range obstacles {
|
for _, o := range obstacles {
|
||||||
if o.X > 50.0 { // Das nächste Hindernis vor uns
|
if o.X > 50.0 {
|
||||||
distToObs = o.X - 50.0
|
distToObs = o.X - 50.0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bot Check: Wenn der Abstand IMMER gleich ist (z.B. exakt 75.5px)
|
|
||||||
if distToObs > 0 {
|
if distToObs > 0 {
|
||||||
diff := math.Abs(distToObs - lastJumpDist)
|
diff := math.Abs(distToObs - lastJumpDist)
|
||||||
if diff < 1.0 {
|
if diff < 1.0 {
|
||||||
// Verdächtig perfekt wiederholt
|
|
||||||
suspicionScore++
|
suspicionScore++
|
||||||
} else {
|
} else if suspicionScore > 0 {
|
||||||
// Menschliche Varianz -> Verdacht senken
|
suspicionScore--
|
||||||
if suspicionScore > 0 {
|
|
||||||
suspicionScore--
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
lastJumpDist = distToObs
|
lastJumpDist = distToObs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Physik Anwendung
|
|
||||||
velY += Gravity
|
velY += Gravity
|
||||||
posY += velY
|
posY += velY
|
||||||
if posY > PlayerYBase {
|
if posY > PlayerYBase {
|
||||||
@@ -137,7 +130,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
hitboxY = posY + (PlayerHeight - currentHeight)
|
hitboxY = posY + (PlayerHeight - currentHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hindernisse bewegen & Kollision
|
// 5. Hindernisse & Kollision
|
||||||
nextObstacles := []ActiveObstacle{}
|
nextObstacles := []ActiveObstacle{}
|
||||||
rightmostX := 0.0
|
rightmostX := 0.0
|
||||||
|
|
||||||
@@ -148,7 +141,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passed Check (Verhindert "Geister-Kollision" von hinten)
|
// Passed Check
|
||||||
paddingX := 10.0
|
paddingX := 10.0
|
||||||
if obs.X+obs.Width-paddingX < 55.0 {
|
if obs.X+obs.Width-paddingX < 55.0 {
|
||||||
nextObstacles = append(nextObstacles, obs)
|
nextObstacles = append(nextObstacles, obs)
|
||||||
@@ -186,7 +179,6 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
// Kollision mit Gegner
|
|
||||||
if hasBat && obs.Type == "teacher" {
|
if hasBat && obs.Type == "teacher" {
|
||||||
hasBat = false
|
hasBat = false
|
||||||
log.Printf("[%s] ⚾ Bat used on %s", sessionID, obs.ID)
|
log.Printf("[%s] ⚾ Bat used on %s", sessionID, obs.ID)
|
||||||
@@ -194,10 +186,9 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
if godLives > 0 {
|
if godLives > 0 {
|
||||||
godLives--
|
godLives--
|
||||||
log.Printf("[%s] 🛡️ Godmode saved life (%d left)", sessionID, godLives)
|
log.Printf("[%s] 🛡️ Godmode saved life", sessionID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive)
|
log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive)
|
||||||
playerDead = true
|
playerDead = true
|
||||||
}
|
}
|
||||||
@@ -210,7 +201,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
obstacles = nextObstacles
|
obstacles = nextObstacles
|
||||||
|
|
||||||
// Spawning
|
// 6. Spawning
|
||||||
if rightmostX < GameWidth-10.0 {
|
if rightmostX < GameWidth-10.0 {
|
||||||
gap := float64(int(400.0 + rng.NextRange(0, 500)))
|
gap := float64(int(400.0 + rng.NextRange(0, 500)))
|
||||||
spawnX := rightmostX + gap
|
spawnX := rightmostX + gap
|
||||||
@@ -252,7 +243,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
if def != nil {
|
if def != nil {
|
||||||
spawnY := GroundY - def.Height - def.YOffset
|
spawnY := GroundY - def.Height - def.YOffset
|
||||||
obstacles = append(obstacles, ActiveObstacle{
|
obstacles = append(obstacles, ActiveObstacle{
|
||||||
ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height,
|
ID: def.ID,
|
||||||
|
Type: def.Type,
|
||||||
|
X: spawnX,
|
||||||
|
Y: spawnY,
|
||||||
|
Width: def.Width,
|
||||||
|
Height: def.Height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,13 +261,11 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ANTI-CHEAT CHECK (Ergebnis) ---
|
|
||||||
if suspicionScore > 10 {
|
if suspicionScore > 10 {
|
||||||
log.Printf("🤖BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik Fail)", sessionID)
|
log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
|
||||||
playerDead = true
|
playerDead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SPEICHERN ---
|
|
||||||
obsJson, _ := json.Marshal(obstacles)
|
obsJson, _ := json.Marshal(obstacles)
|
||||||
batStr := "0"
|
batStr := "0"
|
||||||
if hasBat {
|
if hasBat {
|
||||||
@@ -288,12 +282,18 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
"p_god_lives": godLives,
|
"p_god_lives": godLives,
|
||||||
"p_has_bat": batStr,
|
"p_has_bat": batStr,
|
||||||
"p_boot_ticks": bootTicks,
|
"p_boot_ticks": bootTicks,
|
||||||
// AC Daten speichern für nächsten Chunk
|
|
||||||
"ac_last_dist": fmt.Sprintf("%f", lastJumpDist),
|
"ac_last_dist": fmt.Sprintf("%f", lastJumpDist),
|
||||||
"ac_suspicion": suspicionScore,
|
"ac_suspicion": suspicionScore,
|
||||||
})
|
})
|
||||||
|
|
||||||
return playerDead, score, obstacles
|
// Return PowerUp State
|
||||||
|
pState := PowerUpState{
|
||||||
|
GodLives: godLives,
|
||||||
|
HasBat: hasBat,
|
||||||
|
BootTicks: bootTicks,
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerDead, score, obstacles, pState, ticksAlive
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOr(s string, def float64) float64 {
|
func parseOr(s string, def float64) float64 {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<p>Dein Score: <span id="finalScore" style="color:yellow; font-size: 24px;">0</span></p>
|
<p>Dein Score: <span id="finalScore" style="color:yellow; font-size: 24px;">0</span></p>
|
||||||
|
|
||||||
<div id="inputSection">
|
<div id="inputSection">
|
||||||
<input type="text" id="playerNameInput" placeholder="Dein Name" maxlength="10">
|
<input type="text" id="playerNameInput" placeholder="NAME" maxlength="4" style="text-transform:uppercase;">
|
||||||
<button id="submitBtn" onclick="submitScore()">EINTRAGEN</button>
|
<button id="submitBtn" onclick="submitScore()">EINTRAGEN</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const CHUNK_SIZE = 60;
|
|||||||
const TARGET_FPS = 60;
|
const TARGET_FPS = 60;
|
||||||
const MS_PER_TICK = 1000 / TARGET_FPS;
|
const MS_PER_TICK = 1000 / TARGET_FPS;
|
||||||
|
|
||||||
const DEBUG_SYNC = true;
|
const DEBUG_SYNC = false;
|
||||||
const SYNC_TOLERANCE = 5.0;
|
const SYNC_TOLERANCE = 5.0;
|
||||||
|
|
||||||
// RNG Klasse
|
// RNG Klasse
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ function updateGameLogic() {
|
|||||||
else if (obs.def.type === "powerup") {
|
else if (obs.def.type === "powerup") {
|
||||||
if (obs.def.id === "p_god") godModeLives = 3;
|
if (obs.def.id === "p_god") godModeLives = 3;
|
||||||
if (obs.def.id === "p_bat") hasBat = true;
|
if (obs.def.id === "p_bat") hasBat = true;
|
||||||
if (obs.def.id === "p_boot") bootTicks = 600; // ca. 10 Sekunden
|
if (obs.def.id === "p_boot") bootTicks = 600;
|
||||||
|
|
||||||
|
lastPowerupTick = currentTick;
|
||||||
continue; // Entfernen (Eingesammelt)
|
continue; // Entfernen (Eingesammelt)
|
||||||
}
|
}
|
||||||
// C. GEGNER / HINDERNIS
|
// C. GEGNER / HINDERNIS
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ window.startGameClick = async function() {
|
|||||||
sessionID = sData.sessionId;
|
sessionID = sData.sessionId;
|
||||||
rng = new PseudoRNG(sData.seed);
|
rng = new PseudoRNG(sData.seed);
|
||||||
isGameRunning = true;
|
isGameRunning = true;
|
||||||
// Wir resetten die Zeit, damit es keinen Sprung gibt
|
maxRawBgIndex = 0;
|
||||||
lastTime = performance.now();
|
lastTime = performance.now();
|
||||||
resize();
|
resize();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -100,32 +100,48 @@ window.submitScore = async function() {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
window.showMyCodes = function() {
|
window.showMyCodes = function() {
|
||||||
if(window.openModal) window.openModal('codes');
|
if(window.openModal) window.openModal('codes');
|
||||||
|
|
||||||
const listEl = document.getElementById('codesList');
|
const listEl = document.getElementById('codesList');
|
||||||
if(!listEl) return;
|
if(!listEl) return;
|
||||||
const claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
|
||||||
|
|
||||||
if (claims.length === 0) {
|
|
||||||
|
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
|
|
||||||
|
if (rawClaims.length === 0) {
|
||||||
listEl.innerHTML = "<div style='padding:10px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>";
|
listEl.innerHTML = "<div style='padding:10px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedClaims = rawClaims
|
||||||
|
.map((item, index) => ({ ...item, originalIndex: index }))
|
||||||
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
for (let i = claims.length - 1; i >= 0; i--) {
|
|
||||||
const c = claims[i];
|
|
||||||
|
sortedClaims.forEach(c => {
|
||||||
const canDelete = c.sessionId ? true : false;
|
const canDelete = c.sessionId ? true : false;
|
||||||
const btnStyle = canDelete ? "cursor:pointer; color:#ff4444; border-color:#ff4444;" : "cursor:not-allowed; color:gray; border-color:gray;";
|
const btnStyle = canDelete ? "cursor:pointer; color:#ff4444; border-color:#ff4444;" : "cursor:not-allowed; color:gray; border-color:gray;";
|
||||||
const btnAttr = canDelete ? `onclick="deleteClaim(${i}, '${c.sessionId}', '${c.code}')"` : "disabled";
|
const btnAttr = canDelete ? `onclick="deleteClaim(${c.originalIndex}, '${c.sessionId}', '${c.code}')"` : "disabled";
|
||||||
|
|
||||||
|
let rankIcon = "📄";
|
||||||
|
if (c.score >= 10000) rankIcon = "🔥";
|
||||||
|
if (c.score >= 5000) rankIcon = "⭐";
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
|
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
|
||||||
<div style="text-align:left;">
|
<div style="text-align:left;">
|
||||||
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${c.code}</span>
|
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${rankIcon} ${c.code}</span>
|
||||||
<span style="color:#ffcc00;">(${c.score} Pkt)</span><br>
|
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
|
||||||
<span style="color:#aaa; font-size:9px;">${c.name} • ${c.date}</span>
|
<span style="color:#aaa; font-size:9px;">${c.name} • ${c.date}</span>
|
||||||
</div>
|
</div>
|
||||||
<button ${btnAttr} style="background:transparent; border:1px solid; padding:5px; font-size:9px; margin:0; ${btnStyle}">LÖSCHEN</button>
|
<button ${btnAttr}
|
||||||
|
style="background:transparent; border:1px solid; padding:5px; font-size:9px; margin:0; ${btnStyle}">
|
||||||
|
LÖSCHEN
|
||||||
|
</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
});
|
||||||
|
|
||||||
listEl.innerHTML = html;
|
listEl.innerHTML = html;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,13 +166,38 @@ async function loadLeaderboard() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
||||||
const entries = await res.json();
|
const entries = await res.json();
|
||||||
let html = "<h3>BESTENLISTE</h3>";
|
|
||||||
|
let html = "<h3 style='margin-bottom:5px'>BESTENLISTE</h3>";
|
||||||
|
|
||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
const color = e.isMe ? "yellow" : "white";
|
const color = e.isMe ? "yellow" : "white";
|
||||||
html += `<div style="display:flex; justify-content:space-between; color:${color}; margin-bottom:5px;"><span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
|
const bgStyle = e.isMe ? "background:rgba(255,255,0,0.1);" : "";
|
||||||
|
|
||||||
|
const betterThanMe = e.rank - 1;
|
||||||
|
let infoText = "";
|
||||||
|
|
||||||
|
if (e.isMe && betterThanMe > 0) {
|
||||||
|
infoText = `<div style='font-size:8px; color:#aaa;'>(${betterThanMe} waren besser)</div>`;
|
||||||
|
} else if (e.isMe && betterThanMe === 0) {
|
||||||
|
infoText = `<div style='font-size:8px; color:#ffcc00;'>👑 NIEMAND ist besser!</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div style="border-bottom:1px dotted #444; padding:5px; ${bgStyle} margin-bottom:2px;">
|
||||||
|
<div style="display:flex; justify-content:space-between; color:${color};">
|
||||||
|
<span>#${e.rank} ${e.name.toUpperCase()}</span>
|
||||||
|
<span>${Math.floor(e.score/10)}</span>
|
||||||
|
</div>
|
||||||
|
${infoText}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if(e.rank === 3 && entries.length > 3 && !entries[3].isMe) {
|
||||||
|
html += "<div style='text-align:center; color:gray; font-size:8px;'>...</div>";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('leaderboard').innerHTML = html;
|
document.getElementById('leaderboard').innerHTML = html;
|
||||||
} catch(e) {}
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStartScreenLeaderboard() {
|
async function loadStartScreenLeaderboard() {
|
||||||
|
|||||||
@@ -27,11 +27,21 @@ async function sendChunk() {
|
|||||||
if (data.serverObs) {
|
if (data.serverObs) {
|
||||||
serverObstacles = data.serverObs;
|
serverObstacles = data.serverObs;
|
||||||
|
|
||||||
// --- NEU: DEBUG MODUS VERGLEICH ---
|
|
||||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||||
compareState(snapshotobstacles, data.serverObs);
|
compareState(snapshotobstacles, data.serverObs);
|
||||||
}
|
}
|
||||||
// ----------------------------------
|
|
||||||
|
if (data.powerups) {
|
||||||
|
const sTick = data.serverTick;
|
||||||
|
|
||||||
|
if (lastPowerupTick > sTick) {
|
||||||
|
} else {
|
||||||
|
godModeLives = data.powerups.godLives;
|
||||||
|
hasBat = data.powerups.hasBat;
|
||||||
|
bootTicks = data.powerups.bootTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.status === "dead") {
|
if (data.status === "dead") {
|
||||||
|
|||||||
@@ -44,29 +44,29 @@ resize();
|
|||||||
// --- DRAWING ---
|
// --- DRAWING ---
|
||||||
|
|
||||||
function drawGame() {
|
function drawGame() {
|
||||||
// 1. Alles löschen
|
|
||||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
|
|
||||||
// --- HINTERGRUND (Level-Wechsel) ---
|
|
||||||
let currentBg = null;
|
let currentBg = null;
|
||||||
|
|
||||||
// Haben wir Hintergründe geladen?
|
|
||||||
if (bgSprites.length > 0) {
|
if (bgSprites.length > 0) {
|
||||||
// Wechsel alle 2000 Punkte (Server-Score) = 200 Punkte (Anzeige)
|
|
||||||
const changeInterval = 10000;
|
const changeInterval = 10000;
|
||||||
|
|
||||||
// Berechne Index: 0-1999 -> 0, 2000-3999 -> 1, etc.
|
const currentRawIndex = Math.floor(score / changeInterval);
|
||||||
// Das % (Modulo) sorgt dafür, dass es wieder von vorne anfängt, wenn die Bilder ausgehen
|
|
||||||
const bgIndex = Math.floor(score / changeInterval) % bgSprites.length;
|
if (currentRawIndex > maxRawBgIndex) {
|
||||||
|
maxRawBgIndex = currentRawIndex;
|
||||||
|
}
|
||||||
|
const bgIndex = maxRawBgIndex % bgSprites.length;
|
||||||
|
|
||||||
currentBg = bgSprites[bgIndex];
|
currentBg = bgSprites[bgIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
|
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
|
||||||
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Hellgrau, falls Bild fehlt
|
// Fallback
|
||||||
ctx.fillStyle = "#f0f0f0";
|
ctx.fillStyle = "#f0f0f0";
|
||||||
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ function drawGame() {
|
|||||||
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
|
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
|
||||||
|
|
||||||
// Drift Info (nur wenn Objekte da sind)
|
// Drift Info (nur wenn Objekte da sind)
|
||||||
if (obstacles.length > 0 && serverObstacles.length > 0) {
|
if (DEBUG_SYNC == true && length > 0 && serverObstacles.length > 0) {
|
||||||
const drift = Math.abs(obstacles[0].x - serverObstacles[0].x).toFixed(1);
|
const drift = Math.abs(obstacles[0].x - serverObstacles[0].x).toFixed(1);
|
||||||
statusText += ` | Drift: ${drift}px`; // Einkommentieren für Debugging
|
statusText += ` | Drift: ${drift}px`; // Einkommentieren für Debugging
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ let bootTicks = 0;
|
|||||||
|
|
||||||
// Hintergrund
|
// Hintergrund
|
||||||
let currentBgIndex = 0;
|
let currentBgIndex = 0;
|
||||||
|
let maxRawBgIndex = 0;
|
||||||
|
|
||||||
// Tick Time
|
// Tick Time
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
let accumulator = 0;
|
let accumulator = 0;
|
||||||
|
let lastPowerupTick = -9999;
|
||||||
|
|
||||||
// Grafiken
|
// Grafiken
|
||||||
let sprites = {};
|
let sprites = {};
|
||||||
|
|||||||
9
types.go
9
types.go
@@ -39,11 +39,18 @@ type ValidateRequest struct {
|
|||||||
TotalTicks int `json:"totalTicks"`
|
TotalTicks int `json:"totalTicks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PowerUpState struct {
|
||||||
|
GodLives int `json:"godLives"`
|
||||||
|
HasBat bool `json:"hasBat"`
|
||||||
|
BootTicks int `json:"bootTicks"`
|
||||||
|
}
|
||||||
|
|
||||||
type ValidateResponse struct {
|
type ValidateResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
VerifiedScore int `json:"verifiedScore"`
|
VerifiedScore int `json:"verifiedScore"`
|
||||||
ServerObs []ActiveObstacle `json:"serverObs"`
|
ServerObs []ActiveObstacle `json:"serverObs"`
|
||||||
ActivePowerup string `json:"activePowerup"`
|
PowerUps PowerUpState `json:"powerups"`
|
||||||
|
ServerTick int `json:"serverTick"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StartResponse struct {
|
type StartResponse struct {
|
||||||
|
|||||||
35
utils.go
Normal file
35
utils.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initBadWords() {
|
||||||
|
key := "config:badwords"
|
||||||
|
count, _ := rdb.SCard(ctx, key).Result()
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
log.Println("Lade Default Badwords...")
|
||||||
|
defaults := []string{"admin", "root", "hitler", "nazi", "sex", "fuck", "shit", "ass"}
|
||||||
|
for _, w := range defaults {
|
||||||
|
rdb.SAdd(ctx, key, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsBadWord(name string) bool {
|
||||||
|
badwords, _ := rdb.SMembers(ctx, "config:badwords").Result()
|
||||||
|
|
||||||
|
lowerName := strings.ToLower(name)
|
||||||
|
|
||||||
|
for _, word := range badwords {
|
||||||
|
if word == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(lowerName, strings.ToLower(word)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user