add pics
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m11s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m11s
This commit is contained in:
40
config.go
40
config.go
@@ -9,13 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Gravity = 0.6
|
Gravity = 0.6
|
||||||
JumpPower = -12.0
|
JumpPower = -12.0
|
||||||
GroundY = 350.0
|
HighJumpPower = -16.0
|
||||||
PlayerHeight = 50.0
|
GroundY = 350.0
|
||||||
PlayerYBase = GroundY - PlayerHeight
|
PlayerHeight = 50.0
|
||||||
GameSpeed = 5.0
|
PlayerYBase = GroundY - PlayerHeight
|
||||||
GameWidth = 800.0
|
BaseSpeed = 5.0
|
||||||
|
GameWidth = 800.0
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globale Variablen
|
// Globale Variablen
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
201
simulation.go
201
simulation.go
@@ -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,62 +77,92 @@ 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
|
|
||||||
|
|
||||||
oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX
|
|
||||||
oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-paddingY_Bottom
|
|
||||||
|
|
||||||
if pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom {
|
|
||||||
playerDead = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if obs.X+obs.Width > -100 {
|
pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX
|
||||||
nextObstacles = append(nextObstacles, obs)
|
pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-5.0
|
||||||
if obs.X+obs.Width > rightmostX {
|
oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX
|
||||||
rightmostX = obs.X + obs.Width
|
oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-5.0
|
||||||
|
|
||||||
|
isCollision := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextObstacles = append(nextObstacles, obs)
|
||||||
|
if obs.X+obs.Width > rightmostX {
|
||||||
|
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()
|
||||||
@@ -178,14 +170,21 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
if def != nil {
|
if def != nil {
|
||||||
spawnY := GroundY - def.Height - def.YOffset
|
if def.Type == "powerup" && rng.NextFloat() > 0.1 {
|
||||||
obstacles = append(obstacles, ActiveObstacle{
|
def = nil
|
||||||
ID: def.ID,
|
}
|
||||||
X: spawnX,
|
|
||||||
Y: spawnY,
|
if def != nil {
|
||||||
Width: def.Width,
|
spawnY := GroundY - def.Height - def.YOffset
|
||||||
Height: def.Height,
|
obstacles = append(obstacles, ActiveObstacle{
|
||||||
})
|
ID: def.ID,
|
||||||
|
Type: def.Type,
|
||||||
|
X: spawnX,
|
||||||
|
Y: spawnY,
|
||||||
|
Width: def.Width,
|
||||||
|
Height: def.Height,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
BIN
static/assets/gym-background.jpg
Normal file
BIN
static/assets/gym-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
BIN
static/assets/school2-background.jpg
Normal file
BIN
static/assets/school2-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
@@ -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;
|
||||||
|
|||||||
@@ -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)) {
|
||||||
player.color = "darkred";
|
// TYPE CHECK
|
||||||
if (!isGameOver) { sendChunk(); gameOver("Kollision"); }
|
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";
|
||||||
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
3
types.go
3
types.go
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user