// WaitingScreenAnimator - Handles waiting screen animations and visual effects var WaitingScreenAnimator = { _animationEnabled: true, _waitingAnimationInterval: null, _animating: false, // GFX3: FX overlay ambient state (additive; no-op unless window.FX exists) _fxAmbientOn: false, _fxTick: 0, _waitingAnimation: { frame: 0, playerX: 8, playerY: 12, monsterX: 5, monsterY: 12, treasureX: 35, treasureY: 12, phase: 'enter', frameCount: 0, direction: 1 }, // Separate animation state for "Awaiting payment" treasure hunt _paymentAnimation: { playerX: 8, playerY: 12, treasureX: 25, treasureY: 12, monsters: [], // pack of monsters {x, y} phase: 'approach', // approach, grab, escape, devoured frameCount: 0, outcome: 'escape', // pre-determined: 'escape' or 'devoured' treasureScale: 1.0, // for pulsing effect hasTreasure: false }, init: function() { this._animationEnabled = true; this._waitingAnimationInterval = null; this.resetAnimation(); this.resetPaymentAnimation(); }, drawStaticWaitingScreen: function(screenWidth, screenHeight, drawBorderFn, drawCenteredTextFn) { if (!DisplayManager.ensureDisplay()) return; const display = DisplayManager.getDisplay(); // Draw border drawBorderFn(); let y = Math.floor(screenHeight / 2) - 2; drawCenteredTextFn(y, "Waiting for next block..."); drawCenteredTextFn(y + 1, "Your game will start soon"); // Draw a simple but elegant static dungeon scene const centerX = Math.floor(screenWidth / 2); const roomY = y + 3; // Draw a simple room outline const roomWidth = 12; const startX = centerX - Math.floor(roomWidth / 2); // Room walls for (let x = startX; x < startX + roomWidth; x++) { display.draw(x, roomY - 1, "#", "transparent", "transparent"); // Top wall display.draw(x, roomY + 1, "#", "transparent", "transparent"); // Bottom wall } display.draw(startX - 1, roomY, "#", "transparent", "transparent"); // Left wall display.draw(startX + roomWidth, roomY, "#", "transparent", "transparent"); // Right wall // Floor tiles for (let x = startX; x < startX + roomWidth; x++) { display.draw(x, roomY, GameTiles.getFloorTile(), "white", "transparent"); } // Entrance and exit display.draw(startX - 2, roomY, "<", "transparent", "transparent"); // Entrance display.draw(startX + roomWidth + 1, roomY, ">", "transparent", "transparent"); // Exit // Characters in static positions display.draw(startX + 2, roomY, GameTiles.getPlayerTile(), "white", "transparent"); // Player display.draw(centerX, roomY, GameTiles.getTreasureTile(), "white", "transparent"); // Treasure display.draw(startX + roomWidth - 2, roomY, GameTiles.getMonsterTile(), "white", "transparent"); // Monster // Add some decorative floor elements display.draw(startX + 1, roomY, GameTiles.getFloorTile(true), "white", "transparent"); // Floor variation display.draw(startX + roomWidth - 1, roomY, GameTiles.getFloorTile(true), "white", "transparent"); // Floor variation }, // ===== GFX3: FX overlay helpers (additive, always guarded) ===== _fxBaseCanvas: function() { try { var d = (typeof DisplayManager !== 'undefined' && DisplayManager.getDisplay) ? DisplayManager.getDisplay() : null; return (d && d.getContainer) ? d.getContainer() : null; } catch (e) { return null; } }, // Turn on the torchlight gradient + drifting embers overlay behind the // scripted char-cell waiting animation. No-op unless window.FX is present. _startAmbientFX: function() { if (!window.FX) return; if (this._fxAmbientOn) return; var canvas = this._fxBaseCanvas(); if (!canvas) return; try { if (window.FX.attach) window.FX.attach(canvas); if (window.FX.syncTo) window.FX.syncTo(canvas); if (window.FX.setAmbient) window.FX.setAmbient(true); if (window.FX.start) window.FX.start(); this._fxAmbientOn = true; } catch (e) { /* additive: never break the waiting screen */ } }, _stopAmbientFX: function() { this._fxAmbientOn = false; if (!window.FX) return; try { if (window.FX.setAmbient) window.FX.setAmbient(false); if (window.FX.stop) window.FX.stop(); if (window.FX.clear) window.FX.clear(); } catch (e) { /* ignore */ } }, // Emit gentle amber embers rising from the scripted torch positions. // Throttled so we don't flood the FX particle system. _emitTorchEmbers: function(torchPositions, screenWidth, screenHeight) { if (!window.FX || !window.FX.sparkle) return; if (!torchPositions || !torchPositions.length) return; var canvas = this._fxBaseCanvas(); if (!canvas || !screenWidth || !screenHeight) return; this._fxTick = (this._fxTick + 1) % 1000000; // Only emit on every 3rd frame to keep the embers subtle. if (this._fxTick % 3 !== 0) return; var cellW = canvas.width / screenWidth; var cellH = canvas.height / screenHeight; // One torch per emit, cycling through the scripted positions. var t = torchPositions[this._fxTick % torchPositions.length]; if (!t) return; if (t.x < 0 || t.x >= screenWidth || t.y < 0 || t.y >= screenHeight) return; var px = (t.x + 0.5) * cellW; var py = (t.y + 0.5) * cellH; try { window.FX.sparkle(px, py, 'rgba(255, 170, 60, 0.9)'); } catch (e) { /* ignore */ } }, // JUICE: a one-shot particle burst at a grid cell (treasure grab / escape / death). Purely // additive over window.FX, no-op if FX is absent, and tile-agnostic, so it works identically // in every pack (it draws particles, never tiles). `spread` in grid cells, `count` particles. _burst: function(gridX, gridY, color, count, screenWidth, screenHeight, spread) { if (!window.FX || !window.FX.sparkle) return; var canvas = this._fxBaseCanvas(); if (!canvas || !screenWidth || !screenHeight) return; var cellW = canvas.width / screenWidth; var cellH = canvas.height / screenHeight; var s = spread || 1.6; for (var i = 0; i < count; i++) { var a = (i / count) * Math.PI * 2 + (this._fxTick || 0) * 0.3; var r = ((i * 2654435761 % 100) / 100) * s; // deterministic-ish scatter, no Math.random churn var px = (gridX + 0.5 + Math.cos(a) * r) * cellW; var py = (gridY + 0.5 + Math.sin(a) * r) * cellH; try { window.FX.sparkle(px, py, color); } catch (e) { /* ignore */ } } }, drawAnimatedWaitingScreen: function(screenWidth, screenHeight, drawBorderFn, drawCenteredTextFn) { if (!DisplayManager.ensureDisplay()) return; const display = DisplayManager.getDisplay(); // Draw border drawBorderFn(); // GFX3: layer the torchlight + ember FX overlay on top of the ROT scene this._startAmbientFX(); // Check if we're in "Awaiting payment" mode - use treasure hunt animation const isAwaitingPayment = typeof Game !== 'undefined' && Game._awaitingPayment; if (!isAwaitingPayment) { // Use the treasure hunt animation for queue waiting (the longer wait) this.drawPaymentAnimation(screenWidth, screenHeight, drawCenteredTextFn); return; } // Fixed-position animated text. The label reflects the ACTUAL wait state, not this function's // historical name: the two animations were wired to the opposite labels. This branch runs // while genuinely awaiting a payment. let y = Math.floor(screenHeight / 2) - 8; let baseText = (typeof Game !== 'undefined' && Game._awaitingPayment) ? 'Awaiting payment' : 'Awaiting next block'; const dots = ".".repeat((Math.floor(Date.now() / 500) % 4)); const paddedDots = dots.padEnd(3, " "); drawCenteredTextFn(y, `${baseText}${paddedDots}`); // ✨ EPIC DUNGEON SCENE WITH PROPER TRANSPARENCY ✨ const roomStartX = 6; const roomEndX = 44; const roomStartY = 8; const roomEndY = 16; const time = Date.now(); // 🏛️ BASE DUNGEON FLOOR - Dark stone with subtle variation for (let x = roomStartX; x <= roomEndX; x++) { for (let dy = roomStartY; dy <= roomEndY; dy++) { if (x >= 0 && x < screenWidth && dy >= 0 && dy < screenHeight) { const floorTile = GameTiles.getFloorTile(true); // Dark atmospheric base - no white foregrounds! const baseAlpha = 0.1 + Math.sin(x * 0.2 + dy * 0.3) * 0.05; display.draw(x, dy, floorTile, `rgba(100, 80, 60, ${baseAlpha})`, "transparent"); } } } // 🧱 DUNGEON WALLS - Ancient stone with moss for (let x = roomStartX; x <= roomEndX; x++) { if (x >= 0 && x < screenWidth) { const wallGlow = 0.2 + Math.sin(time * 0.001 + x * 0.1) * 0.1; display.draw(x, roomStartY - 1, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); display.draw(x, roomEndY + 1, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); } } for (let dy = roomStartY; dy <= roomEndY; dy++) { if (dy >= 0 && dy < screenHeight) { const wallGlow = 0.2 + Math.sin(time * 0.001 + dy * 0.1) * 0.1; display.draw(roomStartX - 1, dy, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); display.draw(roomEndX + 1, dy, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); } } // 🔥 MYSTICAL TORCHES - Dancing flames with heat distortion const torchPositions = [ {x: roomStartX - 1, y: roomStartY + 2}, {x: roomEndX + 1, y: roomStartY + 2}, {x: roomStartX - 1, y: roomEndY - 2}, {x: roomEndX + 1, y: roomEndY - 2} ]; for (let i = 0; i < torchPositions.length; i++) { const torch = torchPositions[i]; if (torch.x >= 0 && torch.x < screenWidth && torch.y >= 0 && torch.y < screenHeight) { // Main torch flame - animated intensity with tile stack const flameIntensity = 0.6 + Math.sin(time / 200 + i) * 0.4; const flameColor = `rgba(255, ${100 + flameIntensity * 100}, 20, ${flameIntensity})`; const wallGlow = 0.2 + Math.sin(time * 0.001 + torch.x * 0.1) * 0.1; const wallColor = `rgba(80, 70, 50, ${wallGlow})`; // Use tile stack for proper layering: wall + torch const ch = ["#", "torch"]; const fg = [wallColor, flameColor]; const bg = ["transparent", "transparent"]; display.draw(torch.x, torch.y, ch, fg, bg); // Light pool around torch - warm orange glow const lightRadius = 3 + Math.sin(time / 200 + i) * 0.5; for (let lx = torch.x - 3; lx <= torch.x + 3; lx++) { for (let ly = torch.y - 3; ly <= torch.y + 3; ly++) { if (lx >= roomStartX && lx <= roomEndX && ly >= roomStartY && ly <= roomEndY) { const dist = Math.sqrt((lx - torch.x) ** 2 + (ly - torch.y) ** 2); if (dist <= lightRadius) { const lightIntensity = Math.max(0, (lightRadius - dist) / lightRadius); const warmth = 0.05 + lightIntensity * 0.15; const floorTile = GameTiles.getFloorTile(); display.draw(lx, ly, floorTile, `rgba(255, 180, 80, ${warmth})`, "transparent"); } } } } } } // GFX3: drifting embers from the scripted torch positions this._emitTorchEmbers(torchPositions, screenWidth, screenHeight); // Update character positions this.updateAnimation(); const anim = this._waitingAnimation; // JUICE: a SUBTLE, tight sparkle when the hero breaks for the exit with the loot. No red // "caught" burst: keep it a small glint, not an explosion near the torches. if (this._epicPhase !== anim.phase) { if (anim.phase === 'escape') this._burst(anim.playerX, anim.playerY, 'rgba(255,220,90,0.9)', 6, screenWidth, screenHeight, 0.7); this._epicPhase = anim.phase; } // ===== PHASE 1: DRAW ALL GLOW EFFECTS FIRST ===== // This ensures glow appears behind entities // Monster red glow (draw first so monster appears on top) if (anim.monsterX >= 0 && anim.monsterX < screenWidth) { const floorTile = GameTiles.getFloorTile(); for (let sx = anim.monsterX - 2; sx <= anim.monsterX + 2; sx++) { for (let sy = anim.monsterY - 2; sy <= anim.monsterY + 2; sy++) { if (sx >= roomStartX && sx <= roomEndX && sy >= roomStartY && sy <= roomEndY) { const dist = Math.sqrt((sx - anim.monsterX) ** 2 + (sy - anim.monsterY) ** 2); if (dist <= 2 && !(sx === anim.monsterX && sy === anim.monsterY)) { const glowIntensity = Math.max(0, (2 - dist) / 2 * 0.25); const glowPulse = Math.sin(time / 300 + dist) * 0.1; display.draw(sx, sy, floorTile, `rgba(200, 50, 50, ${glowIntensity + glowPulse})`, "transparent"); } } } } } // Player aura glow (draw before player) if (anim.playerX >= 0 && anim.playerX < screenWidth) { if (anim.phase === 'enter' || anim.phase === 'escape' || anim.phase === 'chase') { const ringPulse = 0.35 + Math.sin(time / 250) * 0.25; const innerPulse = 0.15 + Math.sin(time / 180 + 1.2) * 0.15; const ringColor = (alpha) => `rgba(180, 230, 255, ${alpha})`; const floorTile = GameTiles.getFloorTile(); // 8-way surrounding ring const ringOffsets = [ {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: -1}, {dx: 1, dy: -1}, {dx: -1, dy: 1}, {dx: 1, dy: 1} ]; for (let i = 0; i < ringOffsets.length; i++) { const rx = anim.playerX + ringOffsets[i].dx; const ry = anim.playerY + ringOffsets[i].dy; if (rx >= roomStartX && rx <= roomEndX && ry >= roomStartY && ry <= roomEndY) { const phaseShift = (i / ringOffsets.length) * Math.PI * 2; const alpha = (ringPulse * 0.6) + Math.sin(time / 300 + phaseShift) * 0.15; display.draw(rx, ry, floorTile, ringColor(Math.max(0, alpha)), 'transparent'); } } // Trailing footprint path for (let trail = 1; trail <= 3; trail++) { const tx = anim.playerX - trail; const ty = anim.playerY; if (tx >= roomStartX && tx <= roomEndX && ty >= roomStartY && ty <= roomEndY) { const decay = Math.max(0, 0.5 - trail * 0.15); const alpha = innerPulse * decay; if (alpha > 0.03) { display.draw(tx, ty, floorTile, `rgba(120, 200, 255, ${alpha})`, 'transparent'); } } } } } // Treasure sparkles glow (draw before treasure) if (anim.treasureX >= 0 && anim.treasureX < screenWidth) { const sparklePositions = [ {x: anim.treasureX - 1, y: anim.treasureY - 1}, {x: anim.treasureX + 1, y: anim.treasureY + 1}, {x: anim.treasureX - 1, y: anim.treasureY + 1}, {x: anim.treasureX + 1, y: anim.treasureY - 1} ]; for (let sparkle of sparklePositions) { if (sparkle.x >= roomStartX && sparkle.x <= roomEndX && sparkle.y >= roomStartY && sparkle.y <= roomEndY) { const sparkleIntensity = Math.max(0, Math.sin(time / 150 + sparkle.x + sparkle.y) * 0.4); if (sparkleIntensity > 0.2) { const floorTile = GameTiles.getFloorTile(); display.draw(sparkle.x, sparkle.y, floorTile, `rgba(255, 255, 150, ${sparkleIntensity})`, "transparent"); } } } } // ===== PHASE 2: DRAW ENTITIES ON TOP OF GLOW ===== // 💎 LEGENDARY TREASURE if (anim.treasureX >= 0 && anim.treasureX < screenWidth) { const treasurePulse = 0.6 + Math.sin(time / 250) * 0.4; const treasureGlow = `rgba(255, 215, 0, ${treasurePulse})`; const floorTile = GameTiles.getFloorTile(); const baseAlpha = 0.1 + Math.sin(anim.treasureX * 0.2 + anim.treasureY * 0.3) * 0.05; display.draw(anim.treasureX, anim.treasureY, [floorTile, GameTiles.getTreasureTile()], [`rgba(100, 80, 60, ${baseAlpha})`, treasureGlow], ["transparent", "transparent"]); } // 🧙‍♂️ HEROIC PLAYER if (anim.playerX >= 0 && anim.playerX < screenWidth) { const heroGlow = 0.4 + Math.sin(time / 300) * 0.2; const heroColor = `rgba(100, 150, 255, ${heroGlow})`; const floorTile = GameTiles.getFloorTile(); const playerTile = GameTiles.getPlayerTile(); const baseAlpha = 0.1 + Math.sin(anim.playerX * 0.2 + anim.playerY * 0.3) * 0.05; display.draw(anim.playerX, anim.playerY, [floorTile, playerTile], [`rgba(100, 80, 60, ${baseAlpha})`, heroColor], ["transparent", "transparent"]); } // 👹 TERRIFYING MONSTER if (anim.monsterX >= 0 && anim.monsterX < screenWidth) { const menacePulse = 0.3 + Math.sin(time / 180) * 0.4; const fearColor = `rgba(255, 30, 30, ${menacePulse})`; const floorTile = GameTiles.getFloorTile(); const monsterTile = GameTiles.getMonsterTile(); const baseAlpha = 0.1 + Math.sin(anim.monsterX * 0.2 + anim.monsterY * 0.3) * 0.05; display.draw(anim.monsterX, anim.monsterY, [floorTile, monsterTile], [`rgba(100, 80, 60, ${baseAlpha})`, fearColor], ["transparent", "transparent"]); } // 🚪 MYSTICAL PORTALS - Entrance and exit with swirling energy const portalPulse = 0.3 + Math.sin(time / 400) * 0.2; const entranceGlow = `rgba(50, 255, 50, ${portalPulse})`; // Green entrance const exitGlow = `rgba(255, 100, 255, ${portalPulse})`; // Purple exit display.draw(roomStartX - 1, 12, '<', entranceGlow, "transparent"); display.draw(roomEndX + 1, 12, '>', exitGlow, "transparent"); }, // ========== PAYMENT WAITING ANIMATION ========== // Special "treasure hunt" animation shown while awaiting payment // Player runs for BIG pulsating treasure, always gets it, // then either escapes or gets overwhelmed by a monster pack drawPaymentAnimation: function(screenWidth, screenHeight, drawCenteredTextFn) { const display = DisplayManager.getDisplay(); const anim = this._paymentAnimation; const time = Date.now(); // Fixed-position animated text. This treasure-hunt animation runs for the block-QUEUE wait // (see drawAnimatedWaitingScreen routing), so show the block label unless we're genuinely // awaiting a payment. (Function name is historical: do not trust it for the label.) let y = Math.floor(screenHeight / 2) - 8; const dots = ".".repeat((Math.floor(time / 500) % 4)); const paddedDots = dots.padEnd(3, " "); const label = (typeof Game !== 'undefined' && Game._awaitingPayment) ? 'Awaiting payment' : 'Awaiting next block'; drawCenteredTextFn(y, `${label}${paddedDots}`); // Room dimensions - relative to actual screen size const roomStartX = 1; const roomEndX = screenWidth - 2; const roomStartY = Math.floor(screenHeight / 2) - 4; const roomEndY = Math.floor(screenHeight / 2) + 4; const centerY = Math.floor(screenHeight / 2); // Store room bounds for animation updates anim.roomStartX = roomStartX; anim.roomEndX = roomEndX; anim.roomStartY = roomStartY; anim.roomEndY = roomEndY; // Sync Y positions to actual screen center (handles first frame after reset) if (anim.playerY !== centerY) { anim.playerY = centerY; anim.treasureY = centerY; } // Ensure treasure is within visible bounds if (anim.treasureX > roomEndX - 3) { anim.treasureX = roomEndX - 3; } // 🏛️ BASE DUNGEON FLOOR for (let x = roomStartX; x <= roomEndX; x++) { for (let dy = roomStartY; dy <= roomEndY; dy++) { if (x >= 0 && x < screenWidth && dy >= 0 && dy < screenHeight) { const floorTile = GameTiles.getFloorTile(true); const baseAlpha = 0.1 + Math.sin(x * 0.2 + dy * 0.3) * 0.05; display.draw(x, dy, floorTile, `rgba(100, 80, 60, ${baseAlpha})`, "transparent"); } } } // 🧱 DUNGEON WALLS for (let x = roomStartX; x <= roomEndX; x++) { if (x >= 0 && x < screenWidth) { const wallGlow = 0.2 + Math.sin(time * 0.001 + x * 0.1) * 0.1; display.draw(x, roomStartY - 1, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); display.draw(x, roomEndY + 1, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); } } for (let dy = roomStartY; dy <= roomEndY; dy++) { if (dy >= 0 && dy < screenHeight) { const wallGlow = 0.2 + Math.sin(time * 0.001 + dy * 0.1) * 0.1; display.draw(roomStartX - 1, dy, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); display.draw(roomEndX + 1, dy, "#", `rgba(80, 70, 50, ${wallGlow})`, "transparent"); } } // 🔥 TORCHES for atmosphere (positioned relative to room) const torchPositions = [ {x: roomStartX - 1, y: roomStartY + 1}, {x: roomEndX + 1, y: roomStartY + 1}, {x: roomStartX - 1, y: roomEndY - 1}, {x: roomEndX + 1, y: roomEndY - 1} ]; for (let i = 0; i < torchPositions.length; i++) { const torch = torchPositions[i]; if (torch.x >= 0 && torch.x < screenWidth && torch.y >= 0 && torch.y < screenHeight) { const flameIntensity = 0.6 + Math.sin(time / 200 + i) * 0.4; const flameColor = `rgba(255, ${100 + flameIntensity * 100}, 20, ${flameIntensity})`; const wallGlow = 0.2 + Math.sin(time * 0.001 + torch.x * 0.1) * 0.1; const wallColor = `rgba(80, 70, 50, ${wallGlow})`; display.draw(torch.x, torch.y, ["#", "torch"], [wallColor, flameColor], ["transparent", "transparent"]); } } // GFX3: drifting embers from the scripted torch positions this._emitTorchEmbers(torchPositions, screenWidth, screenHeight); // Update animation state this.updatePaymentAnimation(); // JUICE: a SUBTLE, tight one-shot sparkle on the treasure grab / clean escape. Kept small and // low-spread so it reads as a glint on the hero, not an explosion, and never sprays onto the // wall torches. No red "death" burst (the animation's own red shockwave already covers that). if (this._payPhase !== anim.phase) { if (anim.phase === 'grab') this._burst(anim.treasureX, anim.treasureY, 'rgba(255,220,90,0.9)', 6, screenWidth, screenHeight, 0.6); else if (anim.phase === 'escaped') this._burst(anim.playerX, anim.playerY, 'rgba(130,255,150,0.9)', 7, screenWidth, screenHeight, 0.7); this._payPhase = anim.phase; } // 💎💎💎 MEGA TREASURE - BIG pulsating with intense golden glow if (!anim.hasTreasure && anim.treasureX >= roomStartX && anim.treasureX <= roomEndX) { // Intense pulsing effect - the treasure is CALLING to the player const megaPulse = 0.7 + Math.sin(time / 150) * 0.3; const shimmer = 0.5 + Math.sin(time / 80) * 0.5; // Massive golden aura around treasure (3-tile radius) for (let tx = anim.treasureX - 3; tx <= anim.treasureX + 3; tx++) { for (let ty = anim.treasureY - 2; ty <= anim.treasureY + 2; ty++) { if (tx >= roomStartX && tx <= roomEndX && ty >= roomStartY && ty <= roomEndY) { const dist = Math.sqrt((tx - anim.treasureX) ** 2 + (ty - anim.treasureY) ** 2); if (dist <= 3 && dist > 0) { const auraIntensity = Math.max(0, (3 - dist) / 3) * megaPulse * 0.4; const floorTile = GameTiles.getFloorTile(); display.draw(tx, ty, floorTile, `rgba(255, 200, 50, ${auraIntensity})`, "transparent"); } } } } // Sparkle ring around treasure - use floor tiles with bright golden color const sparkleRadius = 2; for (let angle = 0; angle < 8; angle++) { const sparkleAngle = (angle / 8) * Math.PI * 2 + time / 500; const sx = Math.round(anim.treasureX + Math.cos(sparkleAngle) * sparkleRadius); const sy = Math.round(anim.treasureY + Math.sin(sparkleAngle) * sparkleRadius * 0.6); if (sx >= roomStartX && sx <= roomEndX && sy >= roomStartY && sy <= roomEndY) { const sparkleIntensity = 0.5 + Math.sin(time / 100 + angle) * 0.4; if (sparkleIntensity > 0.3) { const floorTile = GameTiles.getFloorTile(); // Bright white-gold sparkle effect on floor tile display.draw(sx, sy, floorTile, `rgba(255, 255, 180, ${sparkleIntensity})`, "transparent"); } } } // The treasure itself - LARGE and glowing const treasureGlow = `rgba(255, 215, 0, ${megaPulse})`; const floorTile = GameTiles.getFloorTile(); display.draw(anim.treasureX, anim.treasureY, [floorTile, GameTiles.getTreasureTile()], [`rgba(100, 80, 60, 0.1)`, treasureGlow], ["transparent", "transparent"]); } // 🧙‍♂️ HEROIC PLAYER if (anim.playerX >= roomStartX && anim.playerX <= roomEndX + 2) { const heroGlow = anim.hasTreasure ? 0.6 + Math.sin(time / 200) * 0.3 // Brighter when has treasure : 0.4 + Math.sin(time / 300) * 0.2; // If player has treasure, show golden glow instead of blue const heroColor = anim.hasTreasure ? `rgba(255, 200, 50, ${heroGlow})` // Golden when carrying treasure : `rgba(100, 150, 255, ${heroGlow})`; const floorTile = GameTiles.getFloorTile(); const playerTile = GameTiles.getPlayerTile(); display.draw(anim.playerX, anim.playerY, [floorTile, playerTile], [`rgba(100, 80, 60, 0.1)`, heroColor], ["transparent", "transparent"]); // Determination aura when running for treasure if (anim.phase === 'approach' || anim.phase === 'escape') { const ringPulse = 0.3 + Math.sin(time / 200) * 0.2; const ringColor = anim.hasTreasure ? `rgba(255, 220, 100, ${ringPulse})` : `rgba(180, 230, 255, ${ringPulse})`; const ringOffsets = [{dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}]; for (const off of ringOffsets) { const rx = anim.playerX + off.dx; const ry = anim.playerY + off.dy; if (rx >= roomStartX && rx <= roomEndX && ry >= roomStartY && ry <= roomEndY) { display.draw(rx, ry, floorTile, ringColor, 'transparent'); } } } } // 👹👹👹 MONSTER PACK - Spread out formation, no overlapping // Only draw monsters that are on valid integer grid positions const drawnPositions = new Set(); // Track drawn positions to avoid overlap for (let i = 0; i < anim.monsters.length; i++) { const m = anim.monsters[i]; // Round to grid position const gridX = Math.round(m.x); const gridY = Math.round(m.y); const posKey = `${gridX},${gridY}`; // Skip if position already has a monster or out of bounds if (drawnPositions.has(posKey)) continue; if (gridX < roomStartX - 1 || gridX > roomEndX + 1) continue; if (gridY < roomStartY || gridY > roomEndY) continue; drawnPositions.add(posKey); // Draw red glow around this monster position const floorTile = GameTiles.getFloorTile(); for (let sx = gridX - 1; sx <= gridX + 1; sx++) { for (let sy = gridY - 1; sy <= gridY + 1; sy++) { if (sx >= roomStartX && sx <= roomEndX && sy >= roomStartY && sy <= roomEndY) { if (!(sx === gridX && sy === gridY)) { const glowIntensity = 0.2 + Math.sin(time / 200 + i) * 0.1; display.draw(sx, sy, floorTile, `rgba(180, 40, 40, ${glowIntensity})`, "transparent"); } } } } // Draw monster on top const menacePulse = 0.5 + Math.sin(time / 150 + i * 0.7) * 0.4; const fearColor = `rgba(255, 40, 40, ${menacePulse})`; const monsterTile = GameTiles.getMonsterTile(); display.draw(gridX, gridY, [floorTile, monsterTile], [`rgba(100, 80, 60, 0.1)`, fearColor], ["transparent", "transparent"]); } // DEATH EFFECT when devoured - expanding red flash if (anim.phase === 'devoured' && anim.playerX === -1) { const deathX = anim.deathX || 25; const deathY = anim.deathY || 12; const deathAge = anim.frameCount - (anim.deathFrame || 0); // Expanding red shockwave using floor tiles if (deathAge < 30) { const expandRadius = 1 + deathAge * 0.15; const fadeOut = Math.max(0, 1 - deathAge / 30); const floorTile = GameTiles.getFloorTile(); for (let angle = 0; angle < 8; angle++) { const particleAngle = (angle / 8) * Math.PI * 2; const px = Math.round(deathX + Math.cos(particleAngle) * expandRadius); const py = Math.round(deathY + Math.sin(particleAngle) * expandRadius * 0.5); if (px >= roomStartX && px <= roomEndX && py >= roomStartY && py <= roomEndY) { // Bright red flash expanding outward display.draw(px, py, floorTile, `rgba(255, 80, 80, ${fadeOut * 0.9})`, "transparent"); } } // Central bright flash if (deathAge < 15) { display.draw(deathX, deathY, floorTile, `rgba(255, 200, 200, ${fadeOut})`, "transparent"); } } } // 🚪 PORTALS (positioned at room center height) const portalPulse = 0.3 + Math.sin(time / 400) * 0.2; const entranceGlow = `rgba(50, 255, 50, ${portalPulse})`; const exitGlow = `rgba(255, 100, 255, ${portalPulse})`; display.draw(roomStartX - 1, centerY, '<', entranceGlow, "transparent"); display.draw(roomEndX + 1, centerY, '>', exitGlow, "transparent"); // ESCAPE SUCCESS indicator if (anim.phase === 'escaped') { // Draw "ESCAPED!" text using floor tiles as background const escapeY = Math.floor(screenHeight / 2) + 3; const escapeText = "ESCAPED!"; const startX = Math.floor((screenWidth - escapeText.length) / 2); const successPulse = 0.8 + Math.sin(time / 150) * 0.2; const floorTile = GameTiles.getFloorTile(); for (let i = 0; i < escapeText.length; i++) { const char = escapeText[i]; // Draw floor tile first, then letter on top display.draw(startX + i, escapeY, [floorTile, char], [`rgba(100, 80, 60, 0.1)`, `rgba(255, 220, 100, ${successPulse})`], ["transparent", "transparent"]); } } }, updatePaymentAnimation: function() { const anim = this._paymentAnimation; anim.frameCount++; // Animate every 5 frames for smooth but readable movement if (anim.frameCount % 5 !== 0) return; // Use stored room bounds (set by drawPaymentAnimation) const roomStartX = anim.roomStartX || 1; const roomEndX = anim.roomEndX || 23; const centerY = anim.playerY; // Use player's Y as center reference switch(anim.phase) { case 'approach': // Player moves toward treasure if (anim.playerX < anim.treasureX - 1) { anim.playerX++; } else { // Player grabs treasure! anim.hasTreasure = true; anim.phase = 'grab'; anim.grabFrame = anim.frameCount; } break; case 'grab': // Brief pause to show player got treasure if (anim.frameCount - anim.grabFrame > 8) { // Spawn monster pack in a spread-out formation anim.monsters = []; const monsterCount = 3; // Fewer monsters, better spaced // Spawn in a V-formation behind the player (left side of screen) const spawnX = roomStartX + 1; const formations = [ {x: spawnX, y: centerY}, // Center {x: spawnX, y: centerY - 2}, // Top {x: spawnX, y: centerY + 2}, // Bottom ]; for (let i = 0; i < monsterCount; i++) { const pos = formations[i]; anim.monsters.push({ x: pos.x, y: pos.y, speed: 1.2 + i * 0.1, // Slight speed variation targetY: centerY + (i - 1) * 2 // Spread vertically during chase }); } anim.phase = anim.outcome; // 'escape' or 'devoured' } break; case 'escape': // Player runs toward exit, monsters chase but player escapes if (anim.playerX < roomEndX) { anim.playerX += 2; // Player moves fast when escaping! } else { anim.phase = 'escaped'; anim.escapedFrame = anim.frameCount; } // Monsters chase in formation, staying spread out for (let i = 0; i < anim.monsters.length; i++) { const m = anim.monsters[i]; // Chase horizontally, stay behind player if (m.x < anim.playerX - 4) { m.x += m.speed; } // Maintain vertical spread const targetY = m.targetY || (centerY + (i - 1) * 2); if (m.y < targetY - 0.5) m.y += 0.4; else if (m.y > targetY + 0.5) m.y -= 0.4; } break; case 'devoured': // Monsters converge on player in stages let closeCount = 0; for (let i = 0; i < anim.monsters.length; i++) { const m = anim.monsters[i]; const dx = anim.playerX - m.x; const dy = anim.playerY - m.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 1.5) { // Move toward player but maintain some spacing m.x += (dx / dist) * m.speed * 0.8; m.y += (dy / dist) * m.speed * 0.5; } else { closeCount++; } } // Player is devoured when surrounded if (closeCount >= 2 && anim.playerX !== -1) { anim.deathX = anim.playerX; anim.deathY = anim.playerY; anim.deathFrame = anim.frameCount; anim.playerX = -1; // Player vanishes } // After death animation, reset if (anim.playerX === -1 && anim.frameCount - anim.deathFrame > 40) { this.resetPaymentAnimation(); } break; case 'escaped': // Brief celebration then reset if (anim.frameCount - anim.escapedFrame > 25) { this.resetPaymentAnimation(); } // Monsters wander off for (const m of anim.monsters) { m.x += (Math.random() - 0.5) * 2; } break; } }, resetPaymentAnimation: function() { // Determine outcome for this cycle (60% escape, 40% devoured) const outcome = Math.random() < 0.6 ? 'escape' : 'devoured'; // Use sensible defaults - will be updated with actual screen size on first draw // Player starts on left, treasure in middle-right area const playerStart = 3; const treasurePos = 15 + Math.floor(Math.random() * 4); // 15-18 (visible on 25-wide screen) const centerY = 9; // Will be recalculated based on actual screen height this._paymentAnimation = { playerX: playerStart, playerY: centerY, treasureX: treasurePos, treasureY: centerY, monsters: [], phase: 'approach', frameCount: 0, outcome: outcome, hasTreasure: false, // Room bounds - will be set properly on first draw roomStartX: 1, roomEndX: 23, roomStartY: 5, roomEndY: 13 }; }, updateAnimation: function() { const anim = this._waitingAnimation; anim.frameCount++; // Smooth animation timing - update every 4 frames for fluid movement if (anim.frameCount % 4 !== 0) return; switch(anim.phase) { case 'enter': // Player enters from left, smooth movement toward treasure if (anim.playerX < anim.treasureX - 2) { anim.playerX++; } else { anim.phase = 'monster_enter'; } break; case 'monster_enter': // Monster enters and starts chase if (anim.monsterX < anim.playerX - 3) { anim.monsterX++; } else { anim.phase = 'chase'; } break; case 'chase': // Dramatic chase sequence - both moving if (anim.playerX < anim.treasureX) { anim.playerX++; if (anim.monsterX < anim.playerX - 2) { anim.monsterX++; } } else { anim.treasureX = -1; // Player gets treasure anim.phase = Math.random() < 0.7 ? 'escape' : 'caught'; } break; case 'escape': // Player successfully escapes with treasure if (anim.playerX < 46) { anim.playerX++; if (anim.monsterX < anim.playerX - 1) { anim.monsterX++; } } else { anim.phase = 'exit'; } break; case 'caught': // Monster catches player - dramatic end if (anim.monsterX < anim.playerX) { anim.monsterX++; } else { anim.playerX = -1; // Player vanishes anim.phase = 'exit'; } break; case 'exit': if (anim.exitStartFrame === undefined) { anim.exitStartFrame = anim.frameCount; } if (anim.frameCount - anim.exitStartFrame > 40) { // ~4s at 10fps this.resetAnimation(); } else { if (anim.monsterX < 48) { anim.monsterX++; } } break; } }, resetAnimation: function() { // Track cycle count for variation if (typeof this._cycleCount === 'undefined') this._cycleCount = 0; this._cycleCount++; // Random variant selection after first loop const variant = this._cycleCount === 1 ? 'classic' : (Math.random() < 0.33 ? 'reverse' : (Math.random() < 0.5 ? 'race' : 'ambush')); const basePlayerX = 8; const baseMonsterX = 5; let playerStart = basePlayerX; let monsterStart = baseMonsterX; let treasureX = 35; let initialPhase = 'enter'; switch(variant) { case 'reverse': // Swap sides: player closer to treasure, monster further back playerStart = 25 + Math.floor(Math.random()*4); monsterStart = playerStart - 10; treasureX = playerStart + 5 + Math.floor(Math.random()*3); break; case 'race': // Both start near entrance racing for treasure playerStart = 6; monsterStart = 4; treasureX = 40 + Math.floor(Math.random()*3); break; case 'ambush': // Monster waits near treasure playerStart = 8; monsterStart = 30 + Math.floor(Math.random()*3); treasureX = monsterStart + 3; initialPhase = 'chase'; // jump into chase faster break; default: // classic break; } this._waitingAnimation = { frame: 0, playerX: playerStart, // variant start playerY: 12, monsterX: monsterStart, // variant start monsterY: 12, treasureX: treasureX, // variant treasure treasureY: 12, phase: initialPhase, frameCount: 0, direction: 1 }; }, toggleAnimation: function() { this._animationEnabled = !this._animationEnabled; console.log("WaitingScreenAnimator: Animation toggled:", this._animationEnabled ? "enabled" : "disabled"); if (!this._animationEnabled) { this.resetAnimation(); // GFX3: tear down the FX overlay when animation is switched off this._stopAmbientFX(); } return this._animationEnabled; }, startAnimation: function() { if (this._animating) return; // already running // Clear any existing interval defensively if (this._waitingAnimationInterval) { clearInterval(this._waitingAnimationInterval); } this._animating = true; this._waitingAnimationInterval = setInterval(() => { if (this._animationEnabled && !GameState.isGameActive()) { // Draw only the animated frame WITHOUT restarting the loop (avoid recursion) if (typeof ScreenManager !== 'undefined' && ScreenManager.drawWaitingScreen) { ScreenManager.drawWaitingScreen(true); // pass flag to skip auto-start } } else { this.stopAnimation(); } }, 100); // 10 FPS }, stopAnimation: function() { if (this._waitingAnimationInterval) { clearInterval(this._waitingAnimationInterval); this._waitingAnimationInterval = null; } this._animating = false; this.resetAnimation(); this.resetPaymentAnimation(); // GFX3: tear down the FX overlay when the waiting screen ends this._stopAmbientFX(); }, isAnimationEnabled: function() { return this._animationEnabled; } }; // Make it available globally if (typeof window !== 'undefined') { window.WaitingScreenAnimator = WaitingScreenAnimator; }