Private
Public Access
1
0

add pics
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m11s

This commit is contained in:
Sebastian Unterschütz
2025-11-25 19:28:08 +01:00
parent 553f4c2944
commit 36a4847381
10 changed files with 248 additions and 140 deletions

View File

@@ -11,10 +11,11 @@ import (
const ( const (
Gravity = 0.6 Gravity = 0.6
JumpPower = -12.0 JumpPower = -12.0
HighJumpPower = -16.0
GroundY = 350.0 GroundY = 350.0
PlayerHeight = 50.0 PlayerHeight = 50.0
PlayerYBase = GroundY - PlayerHeight PlayerYBase = GroundY - PlayerHeight
GameSpeed = 5.0 BaseSpeed = 5.0
GameWidth = 800.0 GameWidth = 800.0
) )
@@ -37,12 +38,25 @@ func getEnv(key, fallback string) string {
func initGameConfig() { func initGameConfig() {
defaultConfig = GameConfig{ defaultConfig = GameConfig{
Obstacles: []ObstacleDef{ Obstacles: []ObstacleDef{
{ID: "desk", Width: 40, Height: 30, Color: "#8B4513", Image: "desk.png"}, // --- HINDERNISSE ---
{ID: "teacher", Width: 30, Height: 60, Color: "#000080", Image: "teacher.png", CanTalk: true, SpeechLines: []string{"Halt!", "Handy weg!", "Nachsitzen!"}}, {ID: "desk", Type: "obstacle", Width: 40, Height: 30, Color: "#8B4513", Image: "desk.png"},
{ID: "trashcan", Width: 25, Height: 35, Color: "#555", Image: "trash.png"}, {ID: "teacher", Type: "teacher", Width: 30, Height: 60, Color: "#000080", Image: "teacher.png", CanTalk: true, SpeechLines: []string{"Halt!", "Handy weg!"}},
{ID: "eraser", Width: 30, Height: 20, Color: "#fff", Image: "eraser.png", YOffset: 45.0}, {ID: "trashcan", Type: "obstacle", Width: 25, Height: 35, Color: "#555", Image: "trash.png"},
{ID: "eraser", Type: "obstacle", Width: 30, Height: 20, Color: "#fff", Image: "eraser.png", YOffset: 45.0},
// --- BOSS OBJEKTE (Kommen häufiger im Bosskampf) ---
{ID: "principal", Type: "teacher", Width: 40, Height: 70, Color: "#000", Image: "principal.png", CanTalk: true, SpeechLines: []string{"EXMATRIKULATION!"}},
// --- COINS ---
{ID: "coin", Type: "coin", Width: 20, Height: 20, Color: "gold", Image: "coin.png", YOffset: 20.0},
// --- POWERUPS ---
{ID: "p_god", Type: "powerup", Width: 30, Height: 30, Color: "cyan", Image: "powerup_god.png", YOffset: 20.0}, // Godmode
{ID: "p_bat", Type: "powerup", Width: 30, Height: 30, Color: "red", Image: "powerup_bat.png", YOffset: 20.0}, // Schläger
{ID: "p_boot", Type: "powerup", Width: 30, Height: 30, Color: "lime", Image: "powerup_boot.png", YOffset: 20.0}, // Boots
}, },
Backgrounds: []string{"background.jpg"}, // Mehrere Hintergründe für Level-Wechsel
Backgrounds: []string{"gym-background.jpg", "school-background.jpg", "school2-background.jpg"},
} }
log.Println("✅ Config geladen") log.Println("✅ Config mit Powerups geladen")
} }

View File

@@ -3,23 +3,18 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"math"
"strconv" "strconv"
) )
// Führt die Physik-Simulation durch und prüft auf Cheats
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) {
// State parsen
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))
rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64) rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
// Anti-Cheat State laden godLives := int(parseOr(vals["p_god_lives"], 0))
lastJumpDist := parseOr(vals["ac_last_dist"], 0.0) hasBat := vals["p_has_bat"] == "1"
suspicionScore := int(parseOr(vals["ac_suspicion"], 0)) bootTicks := int(parseOr(vals["p_boot_ticks"], 0))
rng := NewRNG(rngStateVal) rng := NewRNG(rngStateVal)
@@ -30,24 +25,20 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
obstacles = []ActiveObstacle{} obstacles = []ActiveObstacle{}
} }
// --- ANTI-CHEAT STUFE 2: SPAM SCHUTZ ---
jumpCount := 0
for _, inp := range inputs {
if inp.Act == "JUMP" {
jumpCount++
}
}
if jumpCount > 8 { // Wer mehr als 8x pro Sekunde springt, ist ein Bot
log.Printf("🤖 BOT ALARM (Spam): %s sprang %d mal!", sessionID, jumpCount)
return true, score, obstacles // Player Dead
}
playerDead := false playerDead := false
// --- SIMULATION LOOP ---
for i := 0; i < totalTicks; i++ { for i := 0; i < totalTicks; i++ {
currentSpeed := BaseSpeed + (float64(score)/500.0)*0.5
if currentSpeed > 12.0 {
currentSpeed = 12.0
}
currentJumpPower := JumpPower
if bootTicks > 0 {
currentJumpPower = HighJumpPower
bootTicks--
}
// A. INPUT
didJump := false didJump := false
isCrouching := false isCrouching := false
for _, inp := range inputs { for _, inp := range inputs {
@@ -61,39 +52,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
} }
// Physik Check
isGrounded := posY >= PlayerYBase-1.0 isGrounded := posY >= PlayerYBase-1.0
if didJump && isGrounded && !isCrouching {
velY = JumpPower
// --- ANTI-CHEAT STUFE 3: HEURISTIK (Perfektes Springen) ---
// Wir messen den Abstand zum nächsten Hindernis beim Absprung
nextObsDist := -1.0
for _, o := range obstacles {
if o.X > 50.0 { // Erstes Hindernis vor uns
nextObsDist = o.X - 50.0
break
}
}
if nextObsDist > 0 {
// Bot-Check: Springt er immer exakt bei "75.5" Pixel Abstand?
diff := math.Abs(nextObsDist - lastJumpDist)
if diff < 1.0 {
// Abstand ist fast identisch zum letzten Sprung -> Verdächtig
suspicionScore++
} else {
// Menschliche Varianz -> Reset (oder verringern)
if suspicionScore > 0 {
suspicionScore--
}
}
lastJumpDist = nextObsDist
}
}
// ... (Restliche Physik wie gehabt) ...
currentHeight := PlayerHeight currentHeight := PlayerHeight
if isCrouching { if isCrouching {
currentHeight = PlayerHeight / 2 currentHeight = PlayerHeight / 2
@@ -102,9 +61,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
} }
if didJump && isGrounded && !isCrouching {
velY = currentJumpPower
}
velY += Gravity velY += Gravity
posY += velY posY += velY
if posY > PlayerYBase { if posY > PlayerYBase {
posY = PlayerYBase posY = PlayerYBase
velY = 0 velY = 0
@@ -115,72 +77,108 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
hitboxY = posY + (PlayerHeight - currentHeight) hitboxY = posY + (PlayerHeight - currentHeight)
} }
// B. OBSTACLES
nextObstacles := []ActiveObstacle{} nextObstacles := []ActiveObstacle{}
rightmostX := 0.0 rightmostX := 0.0
for _, obs := range obstacles { for _, obs := range obstacles {
obs.X -= GameSpeed obs.X -= currentSpeed
if obs.X+obs.Width < 50.0 { if obs.X+obs.Width < -50.0 {
continue continue
} }
// Hitbox paddingX := 10.0
paddingX := 5.0 paddingY_Top := 10.0
paddingY_Top := 5.0 if obs.Type == "teacher" {
paddingY_Bottom := 5.0 paddingY_Top = 25.0
}
pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX
pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-paddingY_Bottom pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX
pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-5.0
oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX
oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-paddingY_Bottom oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-5.0
if pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom { isCollision := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom
playerDead = true
if isCollision {
if obs.Type == "coin" {
score += 2000
continue
} else if obs.Type == "powerup" {
if obs.ID == "p_god" {
godLives = 3
}
if obs.ID == "p_bat" {
hasBat = true
}
if obs.ID == "p_boot" {
bootTicks = 600
}
continue
} else {
if hasBat && obs.Type == "teacher" {
hasBat = false
continue
}
if godLives > 0 {
godLives--
continue
}
playerDead = true
}
} }
if obs.X+obs.Width > -100 {
nextObstacles = append(nextObstacles, obs) nextObstacles = append(nextObstacles, obs)
if obs.X+obs.Width > rightmostX { if obs.X+obs.Width > rightmostX {
rightmostX = obs.X + obs.Width rightmostX = obs.X + obs.Width
} }
} }
}
obstacles = nextObstacles obstacles = nextObstacles
// C. SPAWNING
if rightmostX < GameWidth-10.0 { if rightmostX < GameWidth-10.0 {
rawGap := 400.0 + rng.NextRange(0, 500) gap := float64(int(400.0 + rng.NextRange(0, 500)))
gap := float64(int(rawGap))
spawnX := rightmostX + gap spawnX := rightmostX + gap
if spawnX < GameWidth { if spawnX < GameWidth {
spawnX = GameWidth spawnX = GameWidth
} }
isBossPhase := (score % 1500) > 1200
var possibleDefs []ObstacleDef var possibleDefs []ObstacleDef
for _, d := range defaultConfig.Obstacles { for _, d := range defaultConfig.Obstacles {
if d.ID == "eraser" { if isBossPhase {
if score >= 500 { if d.ID == "principal" || d.ID == "trashcan" {
possibleDefs = append(possibleDefs, d) possibleDefs = append(possibleDefs, d)
} }
} else { } else {
if d.ID == "principal" {
continue
}
if d.ID == "eraser" && score < 500 {
continue
}
possibleDefs = append(possibleDefs, d) possibleDefs = append(possibleDefs, d)
} }
} }
def := rng.PickDef(possibleDefs) def := rng.PickDef(possibleDefs)
if def != nil && def.CanTalk { if def != nil && def.CanTalk {
if rng.NextFloat() > 0.7 { if rng.NextFloat() > 0.7 {
rng.NextFloat() rng.NextFloat()
} }
} }
if def != nil {
if def.Type == "powerup" && rng.NextFloat() > 0.1 {
def = nil
}
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, ID: def.ID,
Type: def.Type,
X: spawnX, X: spawnX,
Y: spawnY, Y: spawnY,
Width: def.Width, Width: def.Width,
@@ -188,6 +186,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
}) })
} }
} }
}
if !playerDead { if !playerDead {
score++ score++
@@ -196,23 +195,21 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
} }
// Ban Hammer für Bots obsJson, _ := json.Marshal(obstacles)
if suspicionScore > 8 { batStr := "0"
log.Printf("🤖 BOT ALARM (Heuristik): %s springt zu perfekt!", sessionID) if hasBat {
playerDead = true batStr = "1"
} }
// State speichern
obsJson, _ := json.Marshal(obstacles)
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{ rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"score": score, "score": score,
"pos_y": fmt.Sprintf("%f", posY), "pos_y": fmt.Sprintf("%f", posY),
"vel_y": fmt.Sprintf("%f", velY), "vel_y": fmt.Sprintf("%f", velY),
"rng_state": rng.State, "rng_state": rng.State,
"obstacles": string(obsJson), "obstacles": string(obsJson),
// Anti-Cheat Daten mitspeichern "p_god_lives": godLives,
"ac_last_dist": fmt.Sprintf("%f", lastJumpDist), "p_has_bat": batStr,
"ac_suspicion": suspicionScore, "p_boot_ticks": bootTicks,
}) })
return playerDead, score, obstacles return playerDead, score, obstacles

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -3,8 +3,9 @@ const GAME_WIDTH = 800;
const GAME_HEIGHT = 400; const GAME_HEIGHT = 400;
const GRAVITY = 0.6; const GRAVITY = 0.6;
const JUMP_POWER = -12; const JUMP_POWER = -12;
const HIGH_JUMP_POWER = -16;
const GROUND_Y = 350; const GROUND_Y = 350;
const GAME_SPEED = 5; const BASE_SPEED = 5;
const CHUNK_SIZE = 60; 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;

View File

@@ -1,12 +1,23 @@
function updateGameLogic() { function updateGameLogic() {
if (isCrouching) { // 1. Speed Berechnung (Sync mit Server!)
inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" }); let currentSpeed = BASE_SPEED + (score / 500.0) * 0.5;
} if (currentSpeed > 12.0) currentSpeed = 12.0;
// 2. Input & Sprung
if (isCrouching) inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" });
const originalHeight = 50; const crouchHeight = 25; const originalHeight = 50; const crouchHeight = 25;
player.h = isCrouching ? crouchHeight : originalHeight; player.h = isCrouching ? crouchHeight : originalHeight;
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y; let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
// Jump Power Check
let jumpP = JUMP_POWER;
if (bootTicks > 0) {
jumpP = HIGH_JUMP_POWER;
bootTicks--;
}
// Physik
player.vy += GRAVITY; player.vy += GRAVITY;
if (isCrouching && !player.grounded) player.vy += 2.0; if (isCrouching && !player.grounded) player.vy += 2.0;
player.y += player.vy; player.y += player.vy;
@@ -15,15 +26,44 @@ function updateGameLogic() {
player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true; player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true;
} else { player.grounded = false; } } else { player.grounded = false; }
let nextObstacles = []; let rightmostX = 0;
// 3. Obstacles
let nextObstacles = [];
let rightmostX = 0;
for (let obs of obstacles) { for (let obs of obstacles) {
obs.x -= GAME_SPEED; obs.x -= currentSpeed;
const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h }; const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
if (checkCollision(playerHitbox, obs)) { if (checkCollision(playerHitbox, obs)) {
// TYPE CHECK
if (obs.def.type === "coin") {
score += 2000;
continue;
}
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;
continue;
}
else {
// HINDERNIS
if (hasBat && obs.def.type === "teacher") {
hasBat = false;
continue; // Zerstört!
}
if (godModeLives > 0) {
godModeLives--;
continue; // Überlebt!
}
player.color = "darkred"; player.color = "darkred";
if (!isGameOver) { sendChunk(); gameOver("Kollision"); } if (!isGameOver) { sendChunk(); gameOver("Kollision"); }
} }
}
if (obs.x + obs.def.width > -100) { if (obs.x + obs.def.width > -100) {
nextObstacles.push(obs); nextObstacles.push(obs);
if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width; if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width;
@@ -31,23 +71,43 @@ function updateGameLogic() {
} }
obstacles = nextObstacles; obstacles = nextObstacles;
// Spawning // 4. Spawning (Sync mit Go!)
if (rightmostX < GAME_WIDTH - 10 && gameConfig) { if (rightmostX < GAME_WIDTH - 10 && gameConfig) {
const gap = Math.floor(400 + rng.nextRange(0, 500)); const gap = Math.floor(400 + rng.nextRange(0, 500));
let spawnX = rightmostX + gap; if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH; let spawnX = rightmostX + gap; if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH;
const isBossPhase = (score % 1500) > 1200;
let possibleObs = []; let possibleObs = [];
gameConfig.obstacles.forEach(def => { gameConfig.obstacles.forEach(def => {
if (def.id === "eraser") { if (score >= 500) possibleObs.push(def); } else possibleObs.push(def); if (isBossPhase) {
if (def.id === "principal" || def.id === "trashcan") possibleObs.push(def);
} else {
if (def.id === "principal") return;
if (def.id === "eraser" && score < 500) return;
possibleObs.push(def);
}
}); });
const def = rng.pick(possibleObs);
let def = rng.pick(possibleObs);
// Speech Sync
let speech = null; let speech = null;
if (def && def.canTalk) { if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines); } if (def && def.canTalk) {
if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines);
}
// Powerup Rarity Sync (Muss exakt wie Go sein: 10% Chance)
if (def && def.type === "powerup") {
if (rng.nextFloat() > 0.1) def = null;
}
if (def) { if (def) {
const yOffset = def.yOffset || 0; const yOffset = def.yOffset || 0;
obstacles.push({ x: spawnX, y: GROUND_Y - def.height - yOffset, def: def, speech: speech }); obstacles.push({
x: spawnX, y: GROUND_Y - def.height - yOffset,
def: def, speech: speech
});
} }
} }
} }

View File

@@ -44,46 +44,53 @@ resize();
// --- DRAWING --- // --- DRAWING ---
function drawGame() { function drawGame() {
// Alles löschen
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// Background // --- BACKGROUND ---
// Hier war der Check schon drin, das ist gut
if (bgSprite.complete && bgSprite.naturalHeight !== 0) { if (bgSprite.complete && bgSprite.naturalHeight !== 0) {
// Hintergrundbild exakt auf 800x400 skalieren
ctx.drawImage(bgSprite, 0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.drawImage(bgSprite, 0, 0, GAME_WIDTH, GAME_HEIGHT);
} else { } else {
// Fallback Farbe
ctx.fillStyle = "#f0f0f0"; ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
} }
// Boden
ctx.fillStyle = "rgba(60, 60, 60, 0.8)"; ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50); ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
// Hindernisse // --- HINDERNISSE (HIER WAR DER FEHLER) ---
obstacles.forEach(obs => { obstacles.forEach(obs => {
const img = sprites[obs.def.id]; const img = sprites[obs.def.id];
if (img) {
// FIX: Wir prüfen jetzt strikt, ob das Bild wirklich bereit ist
if (img && img.complete && img.naturalHeight !== 0) {
ctx.drawImage(img, obs.x, obs.y, obs.def.width, obs.def.height); ctx.drawImage(img, obs.x, obs.y, obs.def.width, obs.def.height);
} else { } else {
ctx.fillStyle = obs.def.color; // Fallback: Wenn Bild fehlt/kaputt -> Farbiges Rechteck
// Wir prüfen auf Typ Coin, damit Coins gold sind, auch wenn Bild fehlt
if (obs.def.type === "coin") ctx.fillStyle = "gold";
else ctx.fillStyle = obs.def.color;
ctx.fillRect(obs.x, obs.y, obs.def.width, obs.def.height); ctx.fillRect(obs.x, obs.y, obs.def.width, obs.def.height);
} }
if(obs.speech) drawSpeechBubble(obs.x, obs.y, obs.speech); if(obs.speech) drawSpeechBubble(obs.x, obs.y, obs.speech);
}); });
// Debug Rahmen (Server Hitboxen) /*
// --- DEBUG ---
ctx.strokeStyle = isGameOver ? "red" : "lime"; ctx.strokeStyle = isGameOver ? "red" : "lime";
ctx.lineWidth = 2; ctx.lineWidth = 2;
serverObstacles.forEach(srvObs => { serverObstacles.forEach(srvObs => {
ctx.strokeRect(srvObs.x, srvObs.y, srvObs.w, srvObs.h); ctx.strokeRect(srvObs.x, srvObs.y, srvObs.w, srvObs.h);
}); });
*/
// Spieler // --- PLAYER ---
const drawY = isCrouching ? player.y + 25 : player.y; const drawY = isCrouching ? player.y + 25 : player.y;
const drawH = isCrouching ? 25 : 50; const drawH = isCrouching ? 25 : 50;
// Hier war der Check auch schon korrekt
if (playerSprite.complete && playerSprite.naturalHeight !== 0) { if (playerSprite.complete && playerSprite.naturalHeight !== 0) {
ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH); ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
} else { } else {
@@ -91,13 +98,31 @@ function drawGame() {
ctx.fillRect(player.x, drawY, player.w, drawH); ctx.fillRect(player.x, drawY, player.w, drawH);
} }
// Game Over Overlay (Dunkelheit) // --- POWERUP UI (Oben Links) ---
if (isGameRunning && !isGameOver) {
ctx.fillStyle = "black";
ctx.font = "bold 10px monospace";
ctx.textAlign = "left";
let statusText = "";
if(godModeLives > 0) statusText += `🛡️ x${godModeLives} `;
if(hasBat) statusText += `⚾ BAT `;
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
// Drift Anzeige
if (obstacles.length > 0 && serverObstacles.length > 0) {
const drift = Math.abs(obstacles[0].x - serverObstacles[0].x).toFixed(1);
statusText += ` | Drift: ${drift}px`;
}
ctx.fillText(statusText, 10, 40);
}
if (isGameOver) { if (isGameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)"; ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT); ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
} }
} }
// Sprechblasen Helper
function drawSpeechBubble(x, y, text) { function drawSpeechBubble(x, y, text) {
const bX = x-20; const bY = y-40; const bW = 120; const bH = 30; const bX = x-20; const bY = y-40; const bW = 120; const bH = 30;
ctx.fillStyle="white"; ctx.fillRect(bX,bY,bW,bH); ctx.fillStyle="white"; ctx.fillRect(bX,bY,bW,bH);

View File

@@ -12,6 +12,14 @@ let lastSentTick = 0;
let inputLog = []; let inputLog = [];
let isCrouching = false; let isCrouching = false;
// Powerups Client State
let godModeLives = 0;
let hasBat = false;
let bootTicks = 0;
// Hintergrund
let currentBgIndex = 0;
// Tick Time // Tick Time
let lastTime = 0; let lastTime = 0;
let accumulator = 0; let accumulator = 0;

View File

@@ -2,6 +2,7 @@ package main
type ObstacleDef struct { type ObstacleDef struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"`
Width float64 `json:"width"` Width float64 `json:"width"`
Height float64 `json:"height"` Height float64 `json:"height"`
Color string `json:"color"` Color string `json:"color"`
@@ -19,6 +20,7 @@ type GameConfig struct {
// Dynamischer State // Dynamischer State
type ActiveObstacle struct { type ActiveObstacle struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"`
X float64 `json:"x"` X float64 `json:"x"`
Y float64 `json:"y"` Y float64 `json:"y"`
Width float64 `json:"w"` Width float64 `json:"w"`
@@ -41,6 +43,7 @@ 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"`
} }
type StartResponse struct { type StartResponse struct {