Private
Public Access
1
0

Merge pull request 'add-new-player-skin' (#10) from add-new-player-skin into main
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m10s

Reviewed-on: #10
This commit is contained in:
2025-11-26 18:01:38 +00:00
13 changed files with 393 additions and 145 deletions

View File

@@ -3,9 +3,11 @@ package main
import (
"encoding/json"
"html"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
@@ -63,19 +65,21 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
}
// ---> 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"
if isDead {
status = "dead"
rdb.HSet(ctx, key, "is_dead", 1)
}
rdb.Expire(ctx, key, 4000*time.Hour)
rdb.Expire(ctx, key, 1*time.Hour)
json.NewEncoder(w).Encode(ValidateResponse{
Status: status,
VerifiedScore: score,
ServerObs: obstacles,
PowerUps: powerUpState,
ServerTick: serverTick,
})
}
@@ -86,8 +90,19 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
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)
sessionKey := "session:" + req.SessionID
scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result()
if err != nil {
http.Error(w, "Session expired", 404)
@@ -104,10 +119,13 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
"created_at": timestamp,
})
rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{
Score: float64(scoreInt),
Member: req.SessionID,
})
// Leaderboard Eintrag
rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{Score: float64(scoreInt), 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")
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)
return
}
log.Printf("👮 ADMIN ACTION: %s on %s", req.Action, req.SessionID)
if req.Action == "approve" {
score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result()
if err == nil {
@@ -200,7 +221,10 @@ func handleAdminAction(w http.ResponseWriter, r *http.Request) {
} else if req.Action == "delete" {
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
rdb.Del(ctx, "session:"+req.SessionID)
}
w.WriteHeader(http.StatusOK)
}
@@ -210,16 +234,28 @@ func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400)
return
}
sessionKey := "session:" + req.SessionID
realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()
if err != nil || realCode != req.ClaimCode {
http.Error(w, "Error", 403)
if err != nil || realCode == "" {
http.Error(w, "Not found", 404)
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:public", req.SessionID)
rdb.HDel(ctx, sessionKey, "name")
rdb.Del(ctx, sessionKey)
w.WriteHeader(http.StatusOK)
}
@@ -231,3 +267,37 @@ func generateClaimCode() string {
}
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)
}
}

View File

@@ -30,6 +30,7 @@ func main() {
}
initGameConfig()
initBadWords()
fs := http.FileServer(http.Dir("./static"))
http.Handle("/", fs)
@@ -44,6 +45,7 @@ func main() {
// Admin Routes (Logger + BasicAuth kombinieren)
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/action", Logger(BasicAuth(handleAdminAction)))

View File

@@ -12,7 +12,7 @@
input[type="text"] {
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 */
@@ -22,14 +22,15 @@
}
.tab-btn.active { background: #4caf50; color: white; }
.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 {
background: #333; padding: 15px; margin-bottom: 8px;
display: flex; justify-content: space-between; align-items: center;
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; }
.meta { font-size: 0.85em; color: #aaa; margin-top: 4px; font-family: monospace; }
@@ -37,10 +38,42 @@
/* Buttons */
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: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; }
</style>
</head>
@@ -52,28 +85,43 @@
</div>
<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" onclick="switchTab('public')">🏆 Bestenliste (Fertig)</button>
<button id="tab-public" class="tab-btn active" onclick="switchTab('public')">🏆 Bestenliste</button>
<button id="tab-badwords" class="tab-btn" onclick="switchTab('badwords')">🤬 Badwords</button>
</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>
let currentTab = 'unverified';
let allEntries = []; // Speichert die geladenen Daten für die Suche
let currentTab = 'public';
let allEntries = [];
// Initial laden
loadList();
// --- FUNKTIONEN FÜR BESTENLISTE ---
async function loadList() {
const listEl = document.getElementById('list');
listEl.innerHTML = '<div style="text-align:center">Lade Daten...</div>';
try {
// API Aufruf mit Typ (unverified oder public)
const res = await fetch('/api/admin/list?type=' + currentTab);
const res = await fetch('/api/admin/list?type=public');
allEntries = await res.json();
renderList(allEntries);
} 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 = "";
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 += `
<div class="entry">
<div class="info">
@@ -105,65 +145,104 @@
</div>
</div>
<div class="actions">
${actions}
<button class="btn-delete" onclick="decide('${entry.sessionId}', 'delete')">🗑 Löschen</button>
</div>
</div>`;
});
listEl.innerHTML = html;
// 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";
}
});
filterList(); // Suche anwenden falls Text drin steht
}
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', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sid, action: action })
});
loadList(); // Neu laden
loadList();
}
function filterList() {
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"; }
});
}
// --- FUNKTIONEN FÜR BADWORDS ---
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');
document.getElementById('searchInput').value = "";
// 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();
}
// Start
loadList();
setInterval(() => {
if(currentTab === 'unverified' && document.getElementById('searchInput').value === "") {
loadList();
}
}, 5000);
</script>
</body>
</html>

View File

@@ -8,21 +8,20 @@ import (
"strconv"
)
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) {
// --- 1. STATE LADEN ---
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, PowerUpState, int) {
// 1. State laden
posY := parseOr(vals["pos_y"], PlayerYBase)
velY := parseOr(vals["vel_y"], 0.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)
// Powerups
// Powerups laden
godLives := int(parseOr(vals["p_god_lives"], 0))
hasBat := vals["p_has_bat"] == "1"
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)
suspicionScore := int(parseOr(vals["ac_suspicion"], 0))
@@ -35,11 +34,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
obstacles = []ActiveObstacle{}
}
// DEBUG: Start des Chunks
// log.Printf("[%s] Simulating Chunk: %d Ticks, Score: %d", sessionID, totalTicks, score)
// --- DEBUG: Chunk Info ---
if len(inputs) > 0 {
log.Printf("📦 [%s] Processing Chunk: %d Ticks, %d Inputs", sessionID, totalTicks, len(inputs))
}
// --- ANTI-CHEAT 1: SPAM SCHUTZ ---
// Wer mehr als 10x pro Sekunde springt, ist ein Bot oder nutzt ein Makro
// --- ANTI-CHEAT: Spam Check ---
jumpCount := 0
for _, inp := range inputs {
if inp.Act == "JUMP" {
@@ -47,8 +47,8 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
}
}
if jumpCount > 10 {
log.Printf("🤖BOT-ALARM [%s]: Spammt Sprünge (%d Inputs)", sessionID, jumpCount)
return true, score, obstacles // Sofort tot
log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge (%d)", sessionID, jumpCount)
return true, score, obstacles, PowerUpState{}, ticksAlive
}
playerDead := false
@@ -57,24 +57,26 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
for i := 0; i < totalTicks; i++ {
ticksAlive++
// Speed Scaling (Zeitbasiert)
// 1. Speed (Zeitbasiert)
currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5
if currentSpeed > 20.0 {
currentSpeed = 20.0
if currentSpeed > 12.0 {
currentSpeed = 12.0
}
// Jump Power (Boots Powerup)
// 2. Powerups Timer
currentJumpPower := JumpPower
if bootTicks > 0 {
currentJumpPower = HighJumpPower
bootTicks--
}
// Input
// 3. Input Verarbeitung (MIT DEBUG LOG)
didJump := false
isCrouching := false
for _, inp := range inputs {
if inp.Tick == i {
log.Printf("🕹️ [%s] ACTION at Tick %d: %s", sessionID, ticksAlive, inp.Act)
if inp.Act == "JUMP" {
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
currentHeight := PlayerHeight
if isCrouching {
currentHeight = PlayerHeight / 2
if !isGrounded {
velY += 2.0
} // Fast fall
}
}
// Springen & ANTI-CHEAT 2 (Heuristik)
if didJump && isGrounded && !isCrouching {
velY = currentJumpPower
// Wir messen den Abstand zum nächsten Hindernis beim Absprung
// Heuristik Anti-Cheat (Abstand messen)
var distToObs float64 = -1.0
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
break
}
}
// Bot Check: Wenn der Abstand IMMER gleich ist (z.B. exakt 75.5px)
if distToObs > 0 {
diff := math.Abs(distToObs - lastJumpDist)
if diff < 1.0 {
// Verdächtig perfekt wiederholt
suspicionScore++
} else {
// Menschliche Varianz -> Verdacht senken
if suspicionScore > 0 {
} else if suspicionScore > 0 {
suspicionScore--
}
}
lastJumpDist = distToObs
}
}
// Physik Anwendung
velY += Gravity
posY += velY
if posY > PlayerYBase {
@@ -137,7 +130,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
hitboxY = posY + (PlayerHeight - currentHeight)
}
// Hindernisse bewegen & Kollision
// 5. Hindernisse & Kollision
nextObstacles := []ActiveObstacle{}
rightmostX := 0.0
@@ -148,7 +141,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
continue
}
// Passed Check (Verhindert "Geister-Kollision" von hinten)
// Passed Check
paddingX := 10.0
if obs.X+obs.Width-paddingX < 55.0 {
nextObstacles = append(nextObstacles, obs)
@@ -186,7 +179,6 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
}
continue
} else {
// Kollision mit Gegner
if hasBat && obs.Type == "teacher" {
hasBat = false
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 {
godLives--
log.Printf("[%s] 🛡️ Godmode saved life (%d left)", sessionID, godLives)
log.Printf("[%s] 🛡️ Godmode saved life", sessionID)
continue
}
log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive)
playerDead = true
}
@@ -210,7 +201,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
}
obstacles = nextObstacles
// Spawning
// 6. Spawning
if rightmostX < GameWidth-10.0 {
gap := float64(int(400.0 + rng.NextRange(0, 500)))
spawnX := rightmostX + gap
@@ -252,7 +243,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
if def != nil {
spawnY := GroundY - def.Height - def.YOffset
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 {
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
}
// --- SPEICHERN ---
obsJson, _ := json.Marshal(obstacles)
batStr := "0"
if hasBat {
@@ -288,12 +282,18 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
"p_god_lives": godLives,
"p_has_bat": batStr,
"p_boot_ticks": bootTicks,
// AC Daten speichern für nächsten Chunk
"ac_last_dist": fmt.Sprintf("%f", lastJumpDist),
"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 {

View File

@@ -69,7 +69,7 @@
<p>Dein Score: <span id="finalScore" style="color:yellow; font-size: 24px;">0</span></p>
<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>
</div>

View File

@@ -10,7 +10,7 @@ const CHUNK_SIZE = 60;
const TARGET_FPS = 60;
const MS_PER_TICK = 1000 / TARGET_FPS;
const DEBUG_SYNC = true;
const DEBUG_SYNC = false;
const SYNC_TOLERANCE = 5.0;
// RNG Klasse

View File

@@ -53,7 +53,9 @@ function updateGameLogic() {
else if (obs.def.type === "powerup") {
if (obs.def.id === "p_god") godModeLives = 3;
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)
}
// C. GEGNER / HINDERNIS

View File

@@ -50,7 +50,7 @@ window.startGameClick = async function() {
sessionID = sData.sessionId;
rng = new PseudoRNG(sData.seed);
isGameRunning = true;
// Wir resetten die Zeit, damit es keinen Sprung gibt
maxRawBgIndex = 0;
lastTime = performance.now();
resize();
} catch(e) {
@@ -100,32 +100,48 @@ window.submitScore = async function() {
// ==========================================
window.showMyCodes = function() {
if(window.openModal) window.openModal('codes');
const listEl = document.getElementById('codesList');
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>";
return;
}
const sortedClaims = rawClaims
.map((item, index) => ({ ...item, originalIndex: index }))
.sort((a, b) => b.score - a.score);
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 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 += `
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
<div style="text-align:left;">
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${c.code}</span>
<span style="color:#ffcc00;">(${c.score} Pkt)</span><br>
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${rankIcon} ${c.code}</span>
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
<span style="color:#aaa; font-size:9px;">${c.name}${c.date}</span>
</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>`;
}
});
listEl.innerHTML = html;
};
@@ -150,13 +166,38 @@ async function loadLeaderboard() {
try {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json();
let html = "<h3>BESTENLISTE</h3>";
let html = "<h3 style='margin-bottom:5px'>BESTENLISTE</h3>";
entries.forEach(e => {
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;
} catch(e) {}
} catch(e) { console.error(e); }
}
async function loadStartScreenLeaderboard() {

View File

@@ -27,11 +27,21 @@ async function sendChunk() {
if (data.serverObs) {
serverObstacles = data.serverObs;
// --- NEU: DEBUG MODUS VERGLEICH ---
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
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") {

View File

@@ -44,29 +44,29 @@ resize();
// --- DRAWING ---
function drawGame() {
// 1. Alles löschen
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// --- HINTERGRUND (Level-Wechsel) ---
let currentBg = null;
// Haben wir Hintergründe geladen?
if (bgSprites.length > 0) {
// Wechsel alle 2000 Punkte (Server-Score) = 200 Punkte (Anzeige)
const changeInterval = 10000;
// Berechne Index: 0-1999 -> 0, 2000-3999 -> 1, etc.
// Das % (Modulo) sorgt dafür, dass es wieder von vorne anfängt, wenn die Bilder ausgehen
const bgIndex = Math.floor(score / changeInterval) % bgSprites.length;
const currentRawIndex = Math.floor(score / changeInterval);
if (currentRawIndex > maxRawBgIndex) {
maxRawBgIndex = currentRawIndex;
}
const bgIndex = maxRawBgIndex % bgSprites.length;
currentBg = bgSprites[bgIndex];
}
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
} else {
// Fallback: Hellgrau, falls Bild fehlt
// Fallback
ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
}
@@ -127,7 +127,7 @@ function drawGame() {
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
// 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);
statusText += ` | Drift: ${drift}px`; // Einkommentieren für Debugging
}

View File

@@ -19,10 +19,12 @@ let bootTicks = 0;
// Hintergrund
let currentBgIndex = 0;
let maxRawBgIndex = 0;
// Tick Time
let lastTime = 0;
let accumulator = 0;
let lastPowerupTick = -9999;
// Grafiken
let sprites = {};

View File

@@ -39,11 +39,18 @@ type ValidateRequest struct {
TotalTicks int `json:"totalTicks"`
}
type PowerUpState struct {
GodLives int `json:"godLives"`
HasBat bool `json:"hasBat"`
BootTicks int `json:"bootTicks"`
}
type ValidateResponse struct {
Status string `json:"status"`
VerifiedScore int `json:"verifiedScore"`
ServerObs []ActiveObstacle `json:"serverObs"`
ActivePowerup string `json:"activePowerup"`
PowerUps PowerUpState `json:"powerups"`
ServerTick int `json:"serverTick"`
}
type StartResponse struct {

35
utils.go Normal file
View 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
}