Private
Public Access
1
0

add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s

This commit is contained in:
Sebastian Unterschütz
2025-11-29 23:37:57 +01:00
parent 5ce097bbb7
commit 669c783a06
43 changed files with 3001 additions and 878 deletions

View File

@@ -1,172 +1,210 @@
function updateGameLogic() {
// 1. Input Logging (Ducken)
if (isCrouching) {
inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" });
}
// ===============================================
// 1. GESCHWINDIGKEIT
// ===============================================
// Wir nutzen den lokalen Score für die Geschwindigkeit
let currentSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
if (currentSpeed > 36.0) currentSpeed = 36.0;
// 2. Geschwindigkeit (Basiert auf ZEIT/Ticks, nicht Score!)
let currentSpeed = 5 + (currentTick / 3000.0) * 0.5;
if (currentSpeed > 12.0) currentSpeed = 12.0;
updateParticles();
// 3. Spieler Physik & Größe
player.prevY = player.y;
obstacleBuffer.forEach(o => o.prevX = o.x);
platformBuffer.forEach(p => p.prevX = p.x);
// ===============================================
// 2. SPIELER PHYSIK (CLIENT PREDICTION)
// ===============================================
const originalHeight = 50;
const crouchHeight = 25;
// Hitbox & Y-Pos anpassen
player.h = isCrouching ? crouchHeight : originalHeight;
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
// Alte Position (für One-Way Check)
const oldY = player.y;
// Physik
player.vy += GRAVITY;
if (isCrouching && !player.grounded) player.vy += 2.0; // Fast Fall
player.y += player.vy;
if (isCrouching && !player.grounded) player.vy += 2.0;
if (player.y + originalHeight >= GROUND_Y) {
player.y = GROUND_Y - originalHeight;
let newY = player.y + player.vy;
let landed = false;
// --- PLATTFORMEN ---
if (player.vy > 0) {
for (let plat of platformBuffer) {
// Nur relevante Plattformen prüfen
if (plat.x < GAME_WIDTH + 100 && plat.x > -100) {
if (player.x + 30 > plat.x && player.x < plat.x + plat.w) {
// "Passed Check": Vorher drüber, jetzt drauf/drunter
const feetOld = oldY + originalHeight;
const feetNew = newY + originalHeight;
if (feetOld <= plat.y && feetNew >= plat.y) {
newY = plat.y - originalHeight;
player.vy = 0;
landed = true;
sendPhysicsSync(newY, 0);
break;
}
}
}
}
}
// --- BODEN ---
if (!landed && newY + originalHeight >= GROUND_Y) {
newY = GROUND_Y - originalHeight;
player.vy = 0;
player.grounded = true;
} else {
player.grounded = false;
landed = true;
}
// 4. Hindernisse Bewegen & Kollision
let nextObstacles = [];
if (currentTick % 10 === 0) {
sendPhysicsSync(player.y, player.vy);
}
for (let obs of obstacles) {
obs.x -= currentSpeed;
player.y = newY;
player.grounded = landed;
// Aufräumen, wenn links raus
if (obs.x + obs.def.width < -50.0) continue;
// ===============================================
// 3. PUFFER BEWEGEN (STREAMING)
// ===============================================
// --- PASSED CHECK (Wichtig!) ---
// Wenn das Hindernis den Spieler schon passiert hat, ignorieren wir Kollisionen.
// Das verhindert "Geister-Treffer" von hinten durch CCD.
const paddingX = 10;
const realRightEdge = obs.x + obs.def.width - paddingX;
obstacleBuffer.forEach(o => o.x -= currentSpeed);
platformBuffer.forEach(p => p.x -= currentSpeed);
// Spieler ist bei 50. Wir geben 5px Puffer.
if (realRightEdge < 55) {
nextObstacles.push(obs); // Behalten, aber keine Kollisionsprüfung mehr
continue;
}
// -------------------------------
// Aufräumen (Links raus)
obstacleBuffer = obstacleBuffer.filter(o => o.x + (o.w||30) > -200); // Muss -200 sein
platformBuffer = platformBuffer.filter(p => p.x + (p.w||100) > -200); // Muss -200 sein
// Kollisionsprüfung
const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
// ===============================================
// 4. KOLLISION & TRANSFER (LOGIK + RENDER LISTE)
// ===============================================
if (checkCollision(playerHitbox, obs)) {
// A. COIN
if (obs.def.type === "coin") {
score += 2000;
continue; // Entfernen
}
// B. POWERUP
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;
lastPowerupTick = currentTick; // Für Sync merken
continue; // Entfernen
}
// C. GEGNER
else {
if (hasBat && obs.def.type === "teacher") {
hasBat = false;
continue; // Zerstört
}
if (godModeLives > 0) {
godModeLives--;
continue; // Geschützt
obstacles = [];
platforms = [];
const RENDER_LIMIT = 900;
// Hitbox definieren (für lokale Prüfung)
const pHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
// --- HINDERNISSE ---
obstacleBuffer.forEach(obs => {
// Nur verarbeiten, wenn im Sichtbereich
if (obs.x < RENDER_LIMIT) {
// A. Metadaten laden (falls noch nicht da)
if (!obs.def) {
let baseDef = null;
if(gameConfig && gameConfig.obstacles) {
baseDef = gameConfig.obstacles.find(x => x.id === obs.id);
}
obs.def = {
id: obs.id,
type: obs.type || (baseDef ? baseDef.type : "obstacle"),
width: obs.w || (baseDef ? baseDef.width : 30),
height: obs.h || (baseDef ? baseDef.height : 30),
color: obs.color || (baseDef ? baseDef.color : "red"),
image: baseDef ? baseDef.image : null,
imgScale: baseDef ? baseDef.imgScale : 1.0,
imgOffsetX: baseDef ? baseDef.imgOffsetX : 0,
imgOffsetY: baseDef ? baseDef.imgOffsetY : 0
};
}
player.color = "darkred";
if (!isGameOver) {
sendChunk();
gameOver("Kollision");
// B. Kollision prüfen (Nur wenn noch nicht eingesammelt)
// Wir nutzen 'obs.collected' als Flag, damit wir Coins nicht doppelt zählen
if (!obs.collected && !isGameOver) {
if (checkCollision(pHitbox, obs)) {
const type = obs.def.type;
const id = obs.def.id;
// 1. COIN
if (type === "coin") {
score += 2000; // Sofort addieren!
obs.collected = true; // Markieren als "weg"
playSound('coin');
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 10);
}
// 2. POWERUP
else if (type === "powerup") {
if (id === "p_god") godModeLives = 3;
if (id === "p_bat") hasBat = true;
if (id === "p_boot") bootTicks = 600; // ca. 10 Sekunden
playSound('powerup');
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 20); // Mehr Partikel
obs.collected = true; // Markieren als "weg"
}
// 3. GEGNER (Teacher/Obstacle)
else {
// Baseballschläger vs Lehrer
if (hasBat && type === "teacher") {
hasBat = false;
obs.collected = true; // Wegschlagen
playSound('hit');
spawnParticles(obs.x, obs.y, 'explosion', 5);
// Effekt?
}
// Godmode (Schild)
else if (godModeLives > 0) {
godModeLives--;
// Optional: Gegner entfernen oder durchlaufen lassen?
// Hier entfernen wir ihn, damit man nicht 2 Leben im selben Objekt verliert
obs.collected = true;
}
// TOT
else {
console.log("💥 Kollision!");
player.color = "darkred";
gameOver("Kollision");
playSound('hit');
spawnParticles(player.x + 15, player.y + 25, 'explosion', 50); // Riesige Explosion
if (typeof sendInput === "function") sendInput("input", "DEATH");
}
}
}
}
}
nextObstacles.push(obs);
}
obstacles = nextObstacles;
// 5. Spawning (Zeitbasiert & Synchron)
// Fallback für Init
if (typeof nextSpawnTick === 'undefined' || nextSpawnTick === 0) {
nextSpawnTick = currentTick + 50;
}
if (currentTick >= nextSpawnTick && gameConfig) {
// A. Nächsten Termin berechnen
const gapPixel = Math.floor(400 + rng.nextRange(0, 500));
const ticksToWait = Math.floor(gapPixel / currentSpeed);
nextSpawnTick = currentTick + ticksToWait;
// B. Position setzen (Fix rechts außen)
let spawnX = GAME_WIDTH + 50;
// C. Objekt auswählen
const isBossPhase = (currentTick % 1500) > 1200;
let possibleObs = [];
gameConfig.obstacles.forEach(def => {
if (isBossPhase) {
if (def.id === "principal" || def.id === "trashcan") possibleObs.push(def);
} else {
if (def.id === "principal") return;
// Eraser erst ab Tick 3000
if (def.id === "eraser" && currentTick < 3000) return;
possibleObs.push(def);
// C. Zur Render-Liste hinzufügen (Nur wenn NICHT eingesammelt)
if (!obs.collected) {
obstacles.push(obs);
}
});
let def = rng.pick(possibleObs);
// RNG Sync: Speech
let speech = null;
if (def && def.canTalk) {
if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines);
}
});
// RNG Sync: Powerup Rarity
if (def && def.type === "powerup") {
if (rng.nextFloat() > 0.1) def = null;
// --- PLATTFORMEN ---
platformBuffer.forEach(plat => {
if (plat.x < RENDER_LIMIT) {
platforms.push(plat);
}
if (def) {
const yOffset = def.yOffset || 0;
obstacles.push({
x: spawnX,
y: GROUND_Y - def.height - yOffset,
def: def,
speech: speech
});
}
}
});
}
// Helper: Robuste Kollisionsprüfung
function checkCollision(p, obs) {
const paddingX = 10;
const paddingY_Top = (obs.def.type === "teacher") ? 25 : 10;
const paddingY_Bottom = 5;
const def = obs.def || {};
const w = def.width || obs.w || 30;
const h = def.height || obs.h || 30;
// Speed-basierte Hitbox-Erweiterung (CCD)
// Wir schätzen den Speed hier, damit er ungefähr dem Server entspricht
let currentSpeed = 5 + (currentTick / 3000.0) * 0.5;
if (currentSpeed > 12.0) currentSpeed = 12.0;
// Kleines Padding, damit es fair ist
const padX = 8;
const padY = (def.type === "teacher" || def.type === "principal") ? 20 : 5;
const pLeft = p.x + paddingX;
const pRight = p.x + p.w - paddingX;
const pTop = p.y + paddingY_Top;
const pBottom = p.y + p.h - paddingY_Bottom;
// Koordinaten
const pL = p.x + padX;
const pR = p.x + p.w - padX;
const pT = p.y + padY;
const pB = p.y + p.h - 5;
const oLeft = obs.x + paddingX;
// Wir erweitern die Hitbox nach rechts um die Geschwindigkeit,
// um schnelle Durchschüsse zu verhindern.
const oRight = obs.x + obs.def.width - paddingX + currentSpeed;
const oL = obs.x + padX;
const oR = obs.x + w - padX;
const oT = obs.y + padY;
const oB = obs.y + h - 5;
const oTop = obs.y + paddingY_Top;
const oBottom = obs.y + obs.def.height - paddingY_Bottom;
return (pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom);
return (pR > oL && pL < oR && pB > oT && pT < oB);
}