add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user