From e52006b91c98e8adab85b418e9b6495f847968ac Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 01:06:06 -0300 Subject: [PATCH 01/10] v0.9 --- cover.png | Bin 21982 -> 7646 bytes game.js | 3224 ++++++++++++++++++++++++++++++++++++++++++++----- metadata.json | 4 +- 3 files changed, 2913 insertions(+), 315 deletions(-) diff --git a/cover.png b/cover.png index 717f038488d5aa39fd130c80ce87192223ab7145..3fbd2c0da136362fa5db508620826761bde28de4 100644 GIT binary patch literal 7646 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 Cw;clj diff --git a/game.js b/game.js index 59872012..55c3f467 100644 --- a/game.js +++ b/game.js @@ -1,436 +1,3034 @@ -// Platanus Hack 25: Snake Game -// Navigate the snake around the "PLATANUS HACK ARCADE" title made of blocks! - -// ============================================================================= -// ARCADE BUTTON MAPPING - COMPLETE TEMPLATE -// ============================================================================= -// Reference: See button-layout.webp at hack.platan.us/assets/images/arcade/ -// -// Maps arcade button codes to keyboard keys for local testing. -// Each arcade code can map to multiple keyboard keys (array values). -// The arcade cabinet sends codes like 'P1U', 'P1A', etc. when buttons are pressed. -// -// To use in your game: -// if (key === 'P1U') { ... } // Works on both arcade and local (via keyboard) -// -// CURRENT GAME USAGE (Snake): -// - P1U/P1D/P1L/P1R (Joystick) → Snake Direction -// - P1A (Button A) or START1 (Start Button) → Restart Game -// ============================================================================= +// POTASIUMABYSS - Platanus Hack 25 +// A gambling roguelike mining arcade game +// ARCADE CONTROLS (Player 1 only: WASD + U/I/O/J/K/L) const ARCADE_CONTROLS = { - // ===== PLAYER 1 CONTROLS ===== - // Joystick - Left hand on WASD 'P1U': ['w'], 'P1D': ['s'], 'P1L': ['a'], 'P1R': ['d'], - 'P1DL': null, // Diagonal down-left (no keyboard default) - 'P1DR': null, // Diagonal down-right (no keyboard default) - - // Action Buttons - Right hand on home row area (ergonomic!) - // Top row (ABC): U, I, O | Bottom row (XYZ): J, K, L 'P1A': ['u'], 'P1B': ['i'], 'P1C': ['o'], 'P1X': ['j'], 'P1Y': ['k'], 'P1Z': ['l'], - - // Start Button - 'START1': ['1', 'Enter'], - - // ===== PLAYER 2 CONTROLS ===== - // Joystick - Right hand on Arrow Keys - 'P2U': ['ArrowUp'], - 'P2D': ['ArrowDown'], - 'P2L': ['ArrowLeft'], - 'P2R': ['ArrowRight'], - 'P2DL': null, // Diagonal down-left (no keyboard default) - 'P2DR': null, // Diagonal down-right (no keyboard default) - - // Action Buttons - Left hand (avoiding P1's WASD keys) - // Top row (ABC): R, T, Y | Bottom row (XYZ): F, G, H - 'P2A': ['r'], - 'P2B': ['t'], - 'P2C': ['y'], - 'P2X': ['f'], - 'P2Y': ['g'], - 'P2Z': ['h'], - - // Start Button - 'START2': ['2'] + 'START1': ['1', 'Enter'] }; -// Build reverse lookup: keyboard key → arcade button code const KEYBOARD_TO_ARCADE = {}; for (const [arcadeCode, keyboardKeys] of Object.entries(ARCADE_CONTROLS)) { if (keyboardKeys) { - // Handle both array and single value const keys = Array.isArray(keyboardKeys) ? keyboardKeys : [keyboardKeys]; - keys.forEach(key => { - KEYBOARD_TO_ARCADE[key] = arcadeCode; - }); + keys.forEach(key => { KEYBOARD_TO_ARCADE[key] = arcadeCode; }); } } +// AI Behavior Definitions (Data-Driven) +const AI = { + RAT: { smallType: 'rat', bigType: 'troll', sprite: 'rat', bigSprite: 'troll', smallScale: 1.5 }, + GOLEM: { smallType: 'rat', bigType: 'golem', sprite: 'rat', bigSprite: 'golem', smallScale: 1.5, bigScale: 6 }, + ALMA: { smallType: 'alma', bigType: 'troll_abyss', sprite: 'rat', bigSprite: 'troll', smallScale: 3.5, smallTint: 0xf5f5f5, bigScale: 6, bigTint: 0x6633aa }, + DEMON: { smallType: 'rat', bigType: 'demon', sprite: 'rat', bigSprite: 'troll', smallScale: 2.25, bigScale: 6, bigTint: 0xaa0000 }, + DRAGON: { smallType: 'rat', bigType: 'dragon', sprite: 'rat', bigSprite: 'dragon', smallScale: 2.25, smallTint: 0xff0000 }, + BOSS: { smallType: 'alma', bigType: 'dragon_boss', sprite: 'rat', bigSprite: 'hero', smallScale: 3.5, smallTint: 0xf5f5f5, bigScale: 7 } +}; + +// GAME DATA - Zone data: [name, common, commonVal, rare, rareVal, smallE, smallHP[3], smallDmg[3], bigE, bigHP, bigDmg[3], chest, color, aiType] +const ZONES = { + 0: ['BOSQUE', 'HIERRO', 10, 'PLATA', 50, 'RATONCITO', [2,10,25], [1,10,3], 'TROLL', 50, [2,10,5], 30, 0x2D5016, 'RAT'], + 1: ['MINAS OLVIDADAS', 'ORO', 25, 'ESMERALDA', 150, 'RATATA', [2,10,40], [2,10,5], 'GOLEM', 80, [3,10,8], 75, 0x4A4A4A, 'GOLEM'], + 2: ['LAS PROFUNDIDADES', 'DIAMANTE', 50, 'RUBI', 300, 'WAREN', [2,10,60], [2,10,8], 'DEMON', 120, [4,10,5], 150, 0x2C1810, 'DEMON'], + 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'DIABLILLO', [3,10,90], [3,10,5], 'DRAGON', 180, [4,10,15], 300, 0x8B0000, 'DRAGON'], + 4: ['ABISMO', 'PIEDRA ABISMAL', 1000, 'CRISTAL-SOMBRA', 8000, 'ALMA EN PENA', [5,10,500], [5,10,20], 'TROLL ABYSS', 800, [10,15,40], 4000, 0x1A0033, 'ALMA'], + 5: ['???', null, 0, 'CORAZON-ABYSS', 30000, 'ALMA EN PENA', [8,10,60], [9,10,45], 'HEROE CORRUPTO', 2000, [15,20,80], 15000, 0x000000, 'BOSS'] +}; + +// Zone array indices (optimized access) +const Z = { NAME:0, COMMON:1, COMMON_VAL:2, RARE:3, RARE_VAL:4, SMALL_E:5, SMALL_HP:6, SMALL_DMG:7, BIG_E:8, BIG_HP:9, BIG_DMG:10, CHEST:11, COLOR:12, AI:13 }; + +// Get max events per zone +const getMaxEvents = z => { + if (z === 5) return 1; // Jefe final: solo 1 evento + if (z === 4) return 3; // El Abismo: 3 eventos de combate + return 5; // Zonas normales: 5 eventos +}; + +// Projectile speed multiplier per zone (increases difficulty) +const getProjSpeed = z => 1.0 + (z * 0.1); // Zone 0: 1.0x, Zone 1: 1.1x, Zone 2: 1.2x, Zone 3: 1.3x, Zone 4: 1.4x, Zone 5: 1.5x + +// MINERAL COLORS - simple color mapping for optimized drawing +const MINERAL_COLORS = { + 'HIERRO': 0x888888, + 'ORO': 0xFFD700, + 'PIEDRA INFERNAL': 0xFF4500, + 'DIAMANTE': 0x87CEEB, + 'PIEDRA ABISMAL': 0x4B0082 +}; + +// Simple unified mineral drawing function +function drawMineral(ox, oy, color, g = graphics) { + // Main rock shape (rectangle) + g.fillStyle(color, 1); + g.fillRect(ox - 35, oy - 42, 70, 84); + + // Add some darker shading + g.fillStyle(0x000000, 0.3); + g.fillRect(ox - 35, oy + 20, 70, 22); + + // Add 2-3 mineral deposits with proper colors + let gemColor; + if (color === 0x87CEEB) { // DIAMANTE + gemColor = 0xADD8E6; // Light blue + } else if (color === 0xFF4500) { // PIEDRA INFERNAL + gemColor = 0xFF6347; // Bright red-orange + } else if (color === 0x4B0082) { // PIEDRA ABISMAL + gemColor = 0x9370DB; // Medium purple + } else if (color === 0xFFD700) { // ORO + gemColor = 0xFFFF00; // Bright yellow + } else { // HIERRO + gemColor = 0xC0C0C0; // Silver + } + + g.fillStyle(gemColor, 0.9); + g.fillCircle(ox - 15, oy - 20, 8); + g.fillRect(ox + 10, oy - 10, 12, 12); + g.fillCircle(ox - 5, oy + 15, 6); +} + +// GAME STATE +let state = 'MENU'; // MENU, GAME, SHOP, GAMEOVER +let zone = 0; +let mineralsInZone = 0; // Track minerals spawned in current zone (max 3) +let eventNum = 0; +let currentEvent = null; +// DMG SET TO 333 FOR TESTING every 333 config on normal should be 1 +let player = { hp: 1, maxHp: 1, dmg: 333, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0 }; // Y: 200-400, X: 50-350 +let enemy = null; +let ore = null; +let chest = null; +let escapeCount = 0; +let attackTimer = 0; + +// Input state for smooth movement +let inputUp = false; +let inputDown = false; +let inputLeft = false; +let inputRight = false; + +// Input debounce to prevent spam +let lastInputTime = 0; +const INPUT_DEBOUNCE_MS = 150; // Minimum time between inputs +let canAttack = true; +let graphics, scene, texts = {}; +let runMoney = 0; +let upgradePrices = { hp: 50, dmg: 100, speed: 75, timing: 200 }; +let upgradeLevel = { hp: 0, dmg: 0, speed: 0, timing: 0 }; +const heartPrices = [50, 200, 500, 2000, 10000]; +let shakeAmt = 0; +let particles = []; +let projectiles = []; +let damageNumbers = []; +let floatingTexts = []; +let shopSelection = 0; +let bgStars = []; +let particleEmitters = []; +let playerSprite = null; +let directionChoice = null; // null, 'forward', 'back' +let lastEventWasChest = false; // Track if last event was chest (don't increment counter) +let directionArrows = []; // Array to hold arrow sprites +let enemySprites = []; // Array to hold enemy sprite references +let timingSlider = { position: 0, direction: 1, speed: 2 }; // Timing slider for pickaxe +let timingZone = { min: 0.45, max: 0.55 }; // Sweet spot zone (45%-55%) +let bgMusicInterval = null; // Background music interval + +// Get speed multiplier based on zone (0-2: Caves, 3-5: Desert, 6-8: Abyss) +function getZoneSpeedMultiplier(z) { + if (z <= 2) return 0.75; // Caves - slower + if (z <= 3) return 1.0; // Desert - normal (base speed) + return 1.25; // Abyss - faster (reduced from 1.5) +} + +// Update timing zone based on upgrade level +function updateTimingZone(randomize = true) { + const baseZone = 0.1; // 10% base zone + const upgradeBonus = upgradeLevel.timing * 0.05; // 5% extra per upgrade level + const totalZone = baseZone + upgradeBonus; + + // If zone is too large (>0.8), center it + if (totalZone >= 0.8) { + timingZone.min = 0.5 - totalZone / 2; + timingZone.max = 0.5 + totalZone / 2; + } else if (randomize) { + // Randomize position within valid range (only for new events) + const maxStartPos = 1.0 - totalZone; // Maximum starting position to fit zone + const randomStart = Math.random() * maxStartPos; + timingZone.min = randomStart; + timingZone.max = randomStart + totalZone; + } else { + // Keep current center position, just resize (for upgrades) + const currentCenter = (timingZone.min + timingZone.max) / 2; + timingZone.min = Math.max(0, currentCenter - totalZone / 2); + timingZone.max = Math.min(1, currentCenter + totalZone / 2); + } +} + +// PHASER CONFIG const config = { type: Phaser.AUTO, width: 800, height: 600, - backgroundColor: '#000000', - scene: { - create: create, - update: update - } + backgroundColor: '#111111', + scene: { create: create, update: update } }; const game = new Phaser.Game(config); -// Game variables -let snake = []; -let snakeSize = 15; -let direction = { x: 1, y: 0 }; -let nextDirection = { x: 1, y: 0 }; -let food; -let score = 0; -let scoreText; -let titleBlocks = []; -let gameOver = false; -let moveTimer = 0; -let moveDelay = 100; // Faster initial speed (was 150ms) -let graphics; - -// Pixel font patterns (5x5 grid for each letter) -const letters = { - P: [[1,1,1,1],[1,0,0,1],[1,1,1,1],[1,0,0,0],[1,0,0,0]], - L: [[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,1,1,1]], - A: [[0,1,1,0],[1,0,0,1],[1,1,1,1],[1,0,0,1],[1,0,0,1]], - T: [[1,1,1,1],[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]], - N: [[1,0,0,1],[1,1,0,1],[1,0,1,1],[1,0,0,1],[1,0,0,1]], - U: [[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,1,1,1]], - S: [[0,1,1,1],[1,0,0,0],[0,1,1,0],[0,0,0,1],[1,1,1,0]], - H: [[1,0,0,1],[1,0,0,1],[1,1,1,1],[1,0,0,1],[1,0,0,1]], - C: [[0,1,1,1],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,1]], - K: [[1,0,0,1],[1,0,1,0],[1,1,0,0],[1,0,1,0],[1,0,0,1]], - '2': [[1,1,1,0],[0,0,0,1],[0,1,1,0],[1,0,0,0],[1,1,1,1]], - '5': [[1,1,1,1],[1,0,0,0],[1,1,1,0],[0,0,0,1],[1,1,1,0]], - ':': [[0,0,0,0],[0,1,0,0],[0,0,0,0],[0,1,0,0],[0,0,0,0]], - R: [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,1,0],[1,0,0,1]], - D: [[1,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,1,1,0]], - E: [[1,1,1,1],[1,0,0,0],[1,1,1,0],[1,0,0,0],[1,1,1,1]] -}; +// UTILITY FUNCTIONS +function roll(count, sides, bonus = 0) { + let sum = 0; + for (let i = 0; i < count; i++) { + sum += Math.floor(Math.random() * sides) + 1; + } + return sum + bonus; +} -// Bold font for ARCADE (filled/solid style) -const boldLetters = { - A: [[1,1,1,1,1],[1,1,0,1,1],[1,1,1,1,1],[1,1,0,1,1],[1,1,0,1,1]], - R: [[1,1,1,1,0],[1,1,0,1,1],[1,1,1,1,0],[1,1,0,1,1],[1,1,0,1,1]], - C: [[1,1,1,1,1],[1,1,0,0,0],[1,1,0,0,0],[1,1,0,0,0],[1,1,1,1,1]], - D: [[1,1,1,1,0],[1,1,0,1,1],[1,1,0,1,1],[1,1,0,1,1],[1,1,1,1,0]], - E: [[1,1,1,1,1],[1,1,0,0,0],[1,1,1,1,0],[1,1,0,0,0],[1,1,1,1,1]] -}; +function takeDmg() { + player.hp -= 1; + player.combo = 0; + showDamage(player.x, player.pos, 1); + spawnBloodParticles(player.x, player.pos, 1); + updateGameUI(); + if (player.hp <= 0) { + state = 'GAMEOVER'; + showGameOver(); + } +} -function create() { - const scene = this; - graphics = this.add.graphics(); +function play(freq, dur = 0.1, type = 'square') { + const ctx = scene.sound.context; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + const reverb = ctx.createConvolver(); + const wetGain = ctx.createGain(); + const dryGain = ctx.createGain(); - // Build "PLATANUS HACK ARCADE" in cyan - centered and grid-aligned - // PLATANUS: 8 letters × (4 cols + 1 spacing) = 40 blocks, but last letter no spacing = 39 blocks × 15px = 585px - let x = Math.floor((800 - 585) / 2 / snakeSize) * snakeSize; - let y = Math.floor(180 / snakeSize) * snakeSize; - 'PLATANUS'.split('').forEach(char => { - x = drawLetter(char, x, y, 0x00ffff); - }); + // Create simple reverb impulse + const impulseLength = ctx.sampleRate * 2; // 2 seconds + const impulse = ctx.createBuffer(2, impulseLength, ctx.sampleRate); + for (let channel = 0; channel < 2; channel++) { + const channelData = impulse.getChannelData(channel); + for (let i = 0; i < impulseLength; i++) { + channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / impulseLength, 2) * 0.1; + } + } + reverb.buffer = impulse; - // HACK: 4 letters × (4 cols + 1 spacing) = 20 blocks, but last letter no spacing = 19 blocks × 15px = 285px - x = Math.floor((800 - 285) / 2 / snakeSize) * snakeSize; - y = Math.floor(280 / snakeSize) * snakeSize; - 'HACK'.split('').forEach(char => { - x = drawLetter(char, x, y, 0x00ffff); - }); + // Connect: osc -> dryGain -> destination + // osc -> reverb -> wetGain -> destination + osc.connect(dryGain); + osc.connect(reverb); + reverb.connect(wetGain); + dryGain.connect(ctx.destination); + wetGain.connect(ctx.destination); - // ARCADE: 6 letters × (5 cols + 1 spacing) = 36 blocks, but last letter no spacing = 35 blocks × 15px = 525px - x = Math.floor((800 - 525) / 2 / snakeSize) * snakeSize; - y = Math.floor(380 / snakeSize) * snakeSize; - 'ARCADE'.split('').forEach(char => { - x = drawLetter(char, x, y, 0xff00ff, true); - }); + osc.frequency.value = freq; + osc.type = type; - // Score display - scoreText = this.add.text(16, 16, 'Score: 0', { - fontSize: '24px', - fontFamily: 'Arial, sans-serif', - color: '#00ff00' - }); + // Dry signal (direct) + dryGain.gain.setValueAtTime(0.04, ctx.currentTime); + dryGain.gain.exponentialRampToValueAtTime(0.005, ctx.currentTime + dur); - // Instructions - this.add.text(400, 560, 'Use Joystick to Move | Avoid Walls, Yourself & The Title!', { - fontSize: '16px', - fontFamily: 'Arial, sans-serif', - color: '#888888', - align: 'center' - }).setOrigin(0.5); + // Wet signal (reverb) + wetGain.gain.setValueAtTime(0.02, ctx.currentTime); + wetGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur); + + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + dur); +} + +function playDrum(freq, dur = 0.12, decay = 0.05) { + const ctx = scene.sound.context; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + const filter = ctx.createBiquadFilter(); + + osc.connect(filter); + filter.connect(gain); + gain.connect(ctx.destination); + + // Harmonic bass-like percussion + osc.frequency.value = freq; // Use frequency directly for musical harmony + osc.type = 'triangle'; // Softer, more musical than square + + // Low-pass filter for warm bass sound + filter.type = 'lowpass'; + filter.frequency.value = 800; // Warm cutoff + filter.Q.value = 1; // Gentle resonance + + gain.gain.setValueAtTime(0.08, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur); + + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + dur); +} + +function shake(amount) { + shakeAmt = amount; +} + +// Music synchronized with timing slider BPM (60 BPM = 1 second per cycle) +function startGameMusic() { + if (bgMusicInterval) clearInterval(bgMusicInterval); - // Initialize snake (start top left) - snake = [ - { x: 75, y: 60 }, - { x: 60, y: 60 }, - { x: 45, y: 60 } + // Two main melodies in simple alternating loop + const melody1 = [ // A Major scale: A4-B4-C#5-D5-E5-F#5-G#5-A5 (8 notes = 1 second) + { freq: 440, dur: 0.1 }, // A4 + { freq: 494, dur: 0.1 }, // B4 + { freq: 523, dur: 0.1 }, // C#5 + { freq: 587, dur: 0.1 }, // D5 + { freq: 659, dur: 0.1 }, // E5 + { freq: 698, dur: 0.1 }, // F#5 + { freq: 784, dur: 0.1 }, // G#5 + { freq: 880, dur: 0.1 } // A5 ]; - // Spawn initial food - spawnFood(); + const melody2 = [ // F Lydian scale: F4-G4-A4-B4-C5-D5-E5-F5 (mysterious minas theme) + { freq: 349, dur: 0.1 }, // F4 + { freq: 392, dur: 0.1 }, // G4 + { freq: 440, dur: 0.1 }, // A4 + { freq: 494, dur: 0.1 }, // B4 + { freq: 523, dur: 0.1 }, // C5 + { freq: 587, dur: 0.1 }, // D5 + { freq: 659, dur: 0.1 }, // E5 + { freq: 698, dur: 0.1 } // F5 + ]; - // Keyboard and Arcade Button input - this.input.keyboard.on('keydown', (event) => { - // Normalize keyboard input to arcade codes for easier testing - const key = KEYBOARD_TO_ARCADE[event.key] || event.key; + // Harmonic percussion pattern (3/4 time) - musical with melody + const percussionPattern = [ + { beat: 0, type: 'bass', freq: 55 }, // F2 - root note harmony + { beat: 3, type: 'mid', freq: 110 }, // F3 - octave harmony + { beat: 6, type: 'bass', freq: 55 } // F2 - root note harmony + ]; - // Restart game (arcade buttons only) - if (gameOver && (key === 'P1A' || key === 'START1')) { - restartGame(scene); - return; + let noteIndex = 0; + + // Calculate interval based on zone speed multiplier + // Base interval is 125ms (Desert speed) + // Caves (0.75x): 125 / 0.75 = 166.67ms + // Desert (1.0x): 125ms + // Abyss (1.25x): 125 / 1.25 = 100ms + const baseInterval = 125; + const currentInterval = Math.round(baseInterval / getZoneSpeedMultiplier(zone)); + + bgMusicInterval = setInterval(() => { + if (state === 'GAME') { + // Simple alternating melody loop: 4 cycles melody1, 4 cycles melody2, repeat + const currentCycle = Math.floor(noteIndex / 8); // Each cycle = 8 notes = 1 second + const melodyBlock = Math.floor(currentCycle / 4); // Change every 4 cycles + const useMelody2 = melodyBlock % 2 === 1; // Alternate: 4 cycles melody1, 4 cycles melody2 + + const mainMelody = useMelody2 ? melody2 : melody1; + const mainNote = mainMelody[noteIndex % 8]; + play(mainNote.freq, mainNote.dur, 'sine'); + + // Add percussion on specific beats (waltz rhythm) + const beatInMeasure = noteIndex % 8; + percussionPattern.forEach(perc => { + if (beatInMeasure === perc.beat) { + playDrum(perc.freq, 0.12); + } + }); + + noteIndex++; } + }, currentInterval); +} + +function stopGameMusic() { + if (bgMusicInterval) { + clearInterval(bgMusicInterval); + bgMusicInterval = null; + } +} + +// Epic Shop Music - 90 BPM (667ms per beat) +function startShopMusic() { + if (bgMusicInterval) clearInterval(bgMusicInterval); + + // Simple 8-beat loop with bass + pad - i-V harmonic minor progression + const chordI = [261.63, 311.13, 392.00]; // C minor chord (C4-Eb4-G4) + const chordV = [392.00, 493.88, 587.33]; // G Major chord (G4-B4-D5) + let beat = 0; - // Direction controls (keyboard keys get mapped to arcade codes) - if (key === 'P1U' && direction.y === 0) { - nextDirection = { x: 0, y: -1 }; - } else if (key === 'P1D' && direction.y === 0) { - nextDirection = { x: 0, y: 1 }; - } else if (key === 'P1L' && direction.x === 0) { - nextDirection = { x: -1, y: 0 }; - } else if (key === 'P1R' && direction.x === 0) { - nextDirection = { x: 1, y: 0 }; + bgMusicInterval = setInterval(() => { + if (state === 'SHOP') { + const currentChord = beat < 4 ? chordI : chordV; // i for beats 0-3, V for beats 4-7 + const bassNote = beat < 4 ? 130.81 : 196.00; // C3 for i, G3 for V + + // Bass on beat 1, 5 + if (beat === 0 || beat === 4) { + play(bassNote, 0.4, 'sine'); + } + + // Pad (sustained chord) - soft continuous + if (beat % 2 === 0) { + currentChord.forEach(freq => play(freq, 1.2, 'sine')); + } + + // Epic timpani hit on beat 1 + if (beat === 0) { + playDrum(80, 0.3); // Deep timpani + } + + beat = (beat + 1) % 8; } - }); + }, 667); // 90 BPM = 667ms per beat +} + +function stopShopMusic() { + if (bgMusicInterval) { + clearInterval(bgMusicInterval); + bgMusicInterval = null; + } +} + +function spawnParticles(x, y, color, count = 10) { + for (let i = 0; i < count; i++) { + // Create particle as a sprite instead of graphics object for proper depth layering + const particle = scene.add.graphics(); + particle.fillStyle(color, 1); + particle.fillRect(-2, -2, 4, 4); + particle.setPosition(x, y); + particle.setDepth(150); // High depth to appear above everything + + // Add velocity and life properties + particle.vx = (Math.random() - 0.5) * 200; + particle.vy = (Math.random() - 0.5) * 200 - 50; + particle.life = 1.0; + particle.color = color; + + particles.push(particle); + } +} + +function spawnBloodParticles(x, y, damage = 1) { + // Spawn MANY more particles for dramatic blood effect + const particleCount = Math.min(40, 15 + damage * 4); + + for (let i = 0; i < particleCount; i++) { + // Create blood particle as a sprite + const particle = scene.add.graphics(); + + // Varied blood colors for realism (dark red to bright red) + const bloodColors = [0x8B0000, 0xDC143C, 0xB22222, 0xFF0000]; + const bloodColor = bloodColors[Math.floor(Math.random() * bloodColors.length)]; + + // Varied sizes - some small droplets, some big splatters + const isBigSplatter = Math.random() < 0.3; // 30% chance of big splatter + const size = isBigSplatter ? (4 + Math.random() * 4) : (1 + Math.random() * 2); - playTone(this, 440, 0.1); + particle.fillStyle(bloodColor, 0.95); + particle.fillRect(-size/2, -size/2, size, size); + particle.setPosition(x + (Math.random() - 0.5) * 30, y + (Math.random() - 0.5) * 30); + particle.setDepth(160); // Even higher depth than regular particles + + // Dramatic blood physics - fast and chaotic movement + const angle = Math.random() * Math.PI * 2; + const speed = 150 + Math.random() * 200; // Much faster + particle.vx = Math.cos(angle) * speed; + particle.vy = Math.sin(angle) * speed - 80; // More upward momentum for splatter effect + + particle.life = 1.5 + Math.random() * 0.5; // Live longer + particle.color = bloodColor; + + // Add gravity effect for more realistic blood physics + particle.gravity = 300 + Math.random() * 200; + + particles.push(particle); + } +} + +function showDamage(x, y, dmg, color = 0xff0000) { + damageNumbers.push({ x: x, y: y, dmg: dmg, life: 1.0, color: color }); } -function drawLetter(char, startX, startY, color, useBold = false) { - const pattern = useBold ? boldLetters[char] : letters[char]; - if (!pattern) return startX + 30; +function showBigText(text, x, y, color = '#ffff00', size = 32, duration = 1500) { + const txt = scene.add.text(x, y, text, { + fontSize: size + 'px', + fontFamily: 'Arial', + color: color, + stroke: '#000', + strokeThickness: 6 + }).setOrigin(0.5).setDepth(1000); + + floatingTexts.push(txt); + + scene.tweens.add({ + targets: txt, + y: y - 100, + alpha: { from: 1, to: 0 }, + scale: { from: 1, to: 1.5 }, + duration: duration, + ease: 'Power2', + onComplete: () => { + if (txt && txt.scene) { + txt.destroy(); + } + const idx = floatingTexts.indexOf(txt); + if (idx > -1) floatingTexts.splice(idx, 1); + } + }); +} - for (let row = 0; row < pattern.length; row++) { - for (let col = 0; col < pattern[row].length; col++) { - if (pattern[row][col]) { - const blockX = startX + col * snakeSize; - const blockY = startY + row * snakeSize; - titleBlocks.push({ x: blockX, y: blockY, color: color }); +function createTimingSuccessParticles(x, y) { + // Create 8 small particles that burst out from the timing hit position + const particleCount = 8; + const colors = [0x00ff00, 0x88ff88, 0xffff00, 0xffffff]; // Green and yellow variations + + for (let i = 0; i < particleCount; i++) { + const angle = (i / particleCount) * Math.PI * 2; + const speed = 100 + Math.random() * 50; + const size = 3 + Math.random() * 3; + const color = colors[Math.floor(Math.random() * colors.length)]; + + const particle = scene.add.rectangle(x, y, size, size, color); + particle.setDepth(999); + + // Burst animation + scene.tweens.add({ + targets: particle, + x: x + Math.cos(angle) * (30 + Math.random() * 20), + y: y + Math.sin(angle) * (30 + Math.random() * 20), + alpha: { from: 1, to: 0 }, + scale: { from: 1, to: 0 }, + duration: 400 + Math.random() * 200, + ease: 'Power2', + onComplete: () => { + if (particle && particle.scene) { + particle.destroy(); + } } + }); + } +} + +function createBgStars() { + // Create 3 layers of parallax stars + for (let layer = 0; layer < 3; layer++) { + const count = 30 - layer * 8; + const speed = (layer + 1) * 0.3; + const size = 1 + layer; + const brightness = 0.3 + layer * 0.2; + + for (let i = 0; i < count; i++) { + bgStars.push({ + x: Math.random() * 800, + y: Math.random() * 600, + layer: layer, + speed: speed, + size: size, + brightness: brightness, + twinkle: Math.random() * Math.PI * 2 + }); } } - return startX + (pattern[0].length + 1) * snakeSize; } -function update(_time, delta) { - if (gameOver) return; +function createExplosion(x, y, color, count = 20) { + // Create particle emitter for explosion + const emitter = scene.add.particles(x, y, null, { + speed: { min: 100, max: 300 }, + angle: { min: 0, max: 360 }, + scale: { start: 1, end: 0 }, + alpha: { start: 1, end: 0 }, + lifespan: 800, + gravityY: 200, + quantity: count, + blendMode: 'ADD', + emitting: false + }); + + // Custom renderer for colored particles + emitter.addEmitZone({ + type: 'random', + source: new Phaser.Geom.Circle(0, 0, 10) + }); + + emitter.explode(count); + + particleEmitters.push(emitter); + + // Clean up after particles die + scene.time.delayedCall(1000, () => { + emitter.destroy(); + const idx = particleEmitters.indexOf(emitter); + if (idx > -1) particleEmitters.splice(idx, 1); + }); +} + +// MAIN GAME LOOP +function create() { + scene = this; + graphics = this.add.graphics(); + graphics.setDepth(300); // UI elements like timing bar appear above game elements + + // Create procedural player sprite (no external URLs) + const tempGraphics = this.add.graphics(); + // Create a simple miner shape: head + body + pickaxe + tempGraphics.fillStyle(0x8B4513); // Brown color + tempGraphics.fillRect(8, 8, 16, 16); // Body + tempGraphics.fillStyle(0xFFD700); // Gold helmet + tempGraphics.fillRect(10, 4, 12, 8); // Head/helmet + tempGraphics.fillStyle(0x444444); // Dark gray pickaxe + tempGraphics.fillRect(24, 10, 6, 2); // Pickaxe handle + tempGraphics.fillRect(28, 6, 2, 8); // Pickaxe head + tempGraphics.generateTexture('miner', 32, 32); + tempGraphics.destroy(); + + // Create procedural pickaxe sprite + const pickaxeGraphics = this.add.graphics(); + pickaxeGraphics.fillStyle(0x8B4513); // Brown handle + pickaxeGraphics.fillRect(2, 6, 12, 4); // Handle + pickaxeGraphics.fillStyle(0xC0C0C0); // Silver head + pickaxeGraphics.fillRect(12, 2, 4, 12); // Pick head + pickaxeGraphics.fillStyle(0xFFD700); // Gold tip + pickaxeGraphics.fillRect(14, 4, 2, 8); // Gold edge + pickaxeGraphics.generateTexture('pickaxe', 20, 16); + pickaxeGraphics.destroy(); + + // Create procedural enemy sprites + + // Rat sprite + const ratGraphics = this.add.graphics(); + ratGraphics.fillStyle(0x8B4513); // Brown body + ratGraphics.fillRect(8, 12, 16, 12); // Body + ratGraphics.fillStyle(0x654321); // Darker brown head + ratGraphics.fillRect(10, 8, 12, 8); // Head + ratGraphics.fillStyle(0xFF0000); // Red eyes + ratGraphics.fillRect(12, 10, 2, 2); // Left eye + ratGraphics.fillRect(18, 10, 2, 2); // Right eye + ratGraphics.fillStyle(0xFFFF00); // Yellow teeth + ratGraphics.fillRect(14, 14, 4, 2); // Teeth + ratGraphics.generateTexture('rat', 32, 32); + ratGraphics.destroy(); + + // Troll sprite (big) + const trollGraphics = this.add.graphics(); + trollGraphics.fillStyle(0x228B22); // Green skin + trollGraphics.fillRect(4, 4, 24, 20); // Body + trollGraphics.fillStyle(0x32CD32); // Lighter green head + trollGraphics.fillRect(8, 0, 16, 12); // Head + trollGraphics.fillStyle(0x8B4513); // Brown club + trollGraphics.fillRect(20, 16, 8, 4); // Club + trollGraphics.generateTexture('troll', 32, 32); + trollGraphics.destroy(); + + // Golem sprite (big) + const golemGraphics = this.add.graphics(); + golemGraphics.fillStyle(0x696969); // Gray stone body + golemGraphics.fillRect(6, 8, 20, 16); // Body + golemGraphics.fillStyle(0x8B8B8B); // Lighter gray head + golemGraphics.fillRect(10, 2, 12, 10); // Head + golemGraphics.fillStyle(0xFFD700); // Gold eyes + golemGraphics.fillRect(12, 6, 2, 2); // Left eye + golemGraphics.fillRect(18, 6, 2, 2); // Right eye + // Arms + golemGraphics.fillStyle(0x696969); + golemGraphics.fillRect(2, 10, 6, 4); // Left arm + golemGraphics.fillRect(24, 10, 6, 4); // Right arm + golemGraphics.generateTexture('golem', 32, 32); + golemGraphics.destroy(); + +// Dragon sprite (intento 4 - Simplificado con triángulos y cuadrado) +const dragonGraphics = this.add.graphics(); + +// Colores +const darkRed = 0x8B0000; // Cuerpo y Alas +const crimson = 0xDC143C; // Cabeza +const gold = 0xFFD700; // Ojo y Espinas/Detalles + +// -- Cabeza (Triángulo) -- +dragonGraphics.fillStyle(crimson); +// Ajusta las coordenadas para que la punta del triángulo mire hacia arriba o hacia la derecha si prefieres +// Aquí la punta mira hacia la derecha +dragonGraphics.fillTriangle(14, 12, 20, 8, 20, 16); // Punta (x,y), Base inferior (x,y), Base superior (x,y) +// (x,y) = (14,12) -> Izquierda (pico) +// (x,y) = (20,8) -> Arriba derecha +// (x,y) = (20,16) -> Abajo derecha + +// -- Cuerpo (Cuadrado/Rectángulo) - Torso más delgado -- +dragonGraphics.fillStyle(darkRed); +dragonGraphics.fillRect(20, 10, 12, 8); // Más delgado verticalmente (altura reducida de 12 a 8) + +// -- Alas (Dos Triángulos Grandes Superpuestos con Offset) - Ajustadas al nuevo torso -- +dragonGraphics.fillStyle(darkRed); +// Ala 1 (detrás, ajustada verticalmente) +dragonGraphics.fillTriangle(20, 8, 30, 4, 30, 16); // Alineada con el torso más delgado +// (x,y) = (20,8) -> Punto más cercano al cuerpo (subido) +// (x,y) = (30,4) -> Punta superior del ala (subida) +// (x,y) = (30,16) -> Punta inferior del ala (ajustada) + +// Ala 2 (delante, ligeramente desplazada) +dragonGraphics.fillTriangle(22, 9, 32, 5, 32, 17); // Ajustada al nuevo centro vertical +// (x,y) = (22,9) -> Punto más cercano al cuerpo (ajustado) +// (x,y) = (32,5) -> Punta superior del ala (ajustada) +// (x,y) = (32,17) -> Punta inferior del ala (ajustada) + +// -- Cola (Triángulo al final del torso) - Centrada con el nuevo torso -- +dragonGraphics.fillStyle(darkRed); +dragonGraphics.fillTriangle(32, 14, 38, 11, 38, 17); // Centrada verticalmente +// (x,y) = (32,14) -> Punto base en el cuerpo (centrado) +// (x,y) = (38,11) -> Punta superior de la cola (ajustada) +// (x,y) = (38,17) -> Punta inferior de la cola (ajustada) + +// -- Ojo (Pequeño cuadrado en la cabeza) -- +dragonGraphics.fillStyle(gold); +dragonGraphics.fillRect(16, 11, 2, 2); // Pequeño cuadrado para el ojo + +dragonGraphics.generateTexture('dragon', 64, 32); // Ajustado a un tamaño más ancho para el dragón +dragonGraphics.destroy(); + + // Hero sprite (corrupted hero boss) + const heroGraphics = this.add.graphics(); + heroGraphics.fillStyle(0x6633aa); // Purple body + heroGraphics.fillRect(8, 10, 16, 14); // Body + heroGraphics.fillStyle(0x8844cc); // Lighter purple head + heroGraphics.fillRect(10, 4, 12, 10); // Head + heroGraphics.fillStyle(0xff00ff); // Magenta eyes (corrupted) + heroGraphics.fillRect(12, 8, 2, 2); // Left eye + heroGraphics.fillRect(18, 8, 2, 2); // Right eye + heroGraphics.fillStyle(0x4422aa); // Dark purple armor + heroGraphics.fillRect(10, 12, 4, 6); // Left shoulder + heroGraphics.fillRect(18, 12, 4, 6); // Right shoulder + heroGraphics.generateTexture('hero', 32, 32); + heroGraphics.destroy(); + + // Create background stars for parallax effect + createBgStars(); + + // Initialize timing zone + updateTimingZone(); + + // Text objects + texts.title = this.add.text(400, 130, 'PLATANUS ABYSS', { + fontSize: '72px', + fontFamily: 'Impact, Arial Black, Arial', + color: '#4b2457ff', + stroke: '#000', + strokeThickness: 12, + shadow: { offsetX: 4, offsetY: 4, color: '#330000', blur: 10, fill: true } + }).setOrigin(0.5); + + texts.subtitle = this.add.text(400, 230, 'PRESIONA START', { + fontSize: '32px', + fontFamily: 'Impact, Arial Black, Arial', + color: '#993535ff', + stroke: '#000', + strokeThickness: 6, + shadow: { offsetX: 2, offsetY: 2, color: '#4b1818ff', blur: 6, fill: true } + }).setOrigin(0.5); + + texts.info = this.add.text(400, 520, 'DESCIENDE A LA OSCURIDAD • ENFRENTA TU DESTINO • RECLAMA TU FORTUNA', { + fontSize: '18px', + fontFamily: 'Arial', + color: '#492020ff', + stroke: '#000', + strokeThickness: 3 + }).setOrigin(0.5); + + texts.zone = this.add.text(400, 100, '', { + fontSize: '28px', fontFamily: 'Arial', color: '#00ffff', + stroke: '#000', strokeThickness: 4 + }).setOrigin(0.5).setVisible(false).setDepth(500); + + texts.hp = this.add.text(50, 80, '', { + fontSize: '24px', fontFamily: 'Arial', color: '#ff0000', + stroke: '#000', strokeThickness: 3 + }).setVisible(false).setDepth(500); + + texts.money = this.add.text(750, 80, '', { + fontSize: '24px', fontFamily: 'Arial', color: '#ffff00', + stroke: '#000', strokeThickness: 3 + }).setOrigin(1, 0).setVisible(false).setDepth(500); + + texts.treasures = this.add.text(750, 110, '', { + fontSize: '20px', fontFamily: 'Arial', color: '#00ffff', + stroke: '#000', strokeThickness: 3 + }).setOrigin(1, 0).setVisible(false).setDepth(500); + + texts.combo = this.add.text(400, 530, '', { + fontSize: '24px', fontFamily: 'Courier New', color: '#FFD700', + stroke: '#000', strokeThickness: 3 + }).setOrigin(0.5, 0).setVisible(false).setDepth(500); + + texts.event = this.add.text(750, 145, '', { + fontSize: '24px', fontFamily: 'Arial', color: '#ffffff', + stroke: '#000', strokeThickness: 3 + }).setOrigin(1, 0).setVisible(false).setDepth(500); + + // Blink animation for menu + this.tweens.add({ + targets: texts.subtitle, + alpha: { from: 1, to: 0.3 }, + duration: 800, + yoyo: true, + repeat: -1 + }); + + // Input + this.input.keyboard.on('keydown', handleInput); + this.input.keyboard.on('keyup', handleKeyUp); +} + +function update(time, delta) { + graphics.clear(); - moveTimer += delta; - if (moveTimer >= moveDelay) { - moveTimer = 0; - direction = nextDirection; - moveSnake(this); + // Menu animations + if (state === 'MENU') { + // Pulse animation for subtitle "PRESIONA START" + const pulseScale = 1 + Math.sin(time / 300) * 0.15; // Pulse between 0.85 and 1.15 + texts.subtitle.setScale(pulseScale); + + // Subtle color shift for title (dark red to blood red) + const colorShift = Math.sin(time / 500) * 0.5 + 0.5; // 0 to 1 + const r = Math.floor(100 + colorShift * 39); // 100 to 139 (dark red range) + const g = Math.floor(0); + const b = Math.floor(0); + const hexColor = '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0'); + texts.title.setColor(hexColor); + + // Subtle pulse for subtitle color (dark red to brighter red/crimson) + const redShift = Math.sin(time / 400) * 0.5 + 0.5; + const sr = Math.floor(153 + redShift * 51); // 153 to 204 (darker to lighter red) + const sg = Math.floor(53 + redShift * 30); // 53 to 83 (keep low for red tones) + const sb = Math.floor(53 + redShift * 30); // 53 to 83 (keep low for red tones) + const redHex = '#' + sr.toString(16).padStart(2, '0') + sg.toString(16).padStart(2, '0') + sb.toString(16).padStart(2, '0'); + texts.subtitle.setColor(redHex); } + // Continuous player movement + if (state === 'GAME') { + const moveSpeed = player.moveSpeed * delta / 1000; // Use player's move speed stat + const topBarHeight = 180; + const bottomBarHeight = 180; + const minY = topBarHeight + 20; // 200 + const maxY = 600 - bottomBarHeight - 20; // 400 + + // Check if shop is available (unlock left boundary) + const canShop = directionArrows.length > 0 && eventNum >= getMaxEvents(zone) - 1 && zone !== 4 && zone !== 5; + const minX = canShop ? -50 : 50; // Open left boundary when shop available + const maxX = directionArrows.length > 0 ? 600 : 450; // Extended right limit when arrows visible + + if (inputUp) { + player.pos = Math.max(minY, player.pos - moveSpeed); + } + if (inputDown) { + player.pos = Math.min(maxY, player.pos + moveSpeed); + } + if (inputLeft) { + player.x = Math.max(minX, player.x - moveSpeed); + } + if (inputRight) { + player.x = Math.min(maxX, player.x + moveSpeed); + } + } + + // Auto-advance when player reaches right side OR go to shop when leaving screen + // Only works after completing events (when directionArrows are visible) + if (directionArrows.length > 0 && state === 'GAME') { + if (player.x > 550) { + // Teleport player back to starting position before advancing + player.x = 150; + player.pos = 300; + selectDirection('forward'); + } else if (player.x < -20) { + // Player walked off screen left - go to shop + if (zone !== 4 && zone !== 5) { + player.x = 150; + player.pos = 300; + escapeToShop(); + } + } + } else if (state === 'GAME') { + // Show blocked area indicators when trying to access restricted zones + const canShop = eventNum >= getMaxEvents(zone) - 1 && zone !== 4 && zone !== 5; + + if (player.x > 500) { + // Show "Complete event first" near right side + if (!window.centerBlockText) { + window.centerBlockText = scene.add.text(550, 350, 'Completa el evento primero!', { + fontSize: '18px', + fontFamily: 'Arial', + color: '#ff6666', + stroke: '#000', + strokeThickness: 2 + }).setOrigin(0.5).setDepth(1000); + } + } else if (player.x <= 40 && !canShop) { + // Show "Finish zone first" near left (only if shop not available) + if (!window.leftBlockText) { + window.leftBlockText = scene.add.text(100, 350, 'Termina la zona primero!', { + fontSize: '18px', + fontFamily: 'Arial', + color: '#ff6666', + stroke: '#000', + strokeThickness: 2 + }).setOrigin(0.5).setDepth(1000); + } + } else { + // Clear block texts when not in restricted areas + if (window.centerBlockText) { + window.centerBlockText.destroy(); + window.centerBlockText = null; + } + if (window.leftBlockText) { + window.leftBlockText.destroy(); + window.leftBlockText = null; + } + } + } + + // Draw animated background stars (parallax) + bgStars.forEach(star => { + star.x -= star.speed; + if (star.x < -10) star.x = 810; + + // Twinkling effect + star.twinkle += delta / 1000; + const alpha = star.brightness + Math.sin(star.twinkle) * 0.2; + + graphics.fillStyle(0xffffff, alpha); + graphics.fillRect(star.x, star.y, star.size, star.size); + }); + + // Camera shake + if (shakeAmt > 0) { + scene.cameras.main.setPosition( + (Math.random() - 0.5) * shakeAmt, + (Math.random() - 0.5) * shakeAmt + ); + shakeAmt *= 0.9; + if (shakeAmt < 0.1) { + shakeAmt = 0; + scene.cameras.main.setPosition(0, 0); + } + } + + // Update timing slider with zone-based speed multiplier + const speedMultiplier = getZoneSpeedMultiplier(zone); + timingSlider.position += timingSlider.direction * timingSlider.speed * speedMultiplier * delta / 1000; + if (timingSlider.position >= 1) { + timingSlider.position = 1; + timingSlider.direction = -1; + } else if (timingSlider.position <= 0) { + timingSlider.position = 0; + timingSlider.direction = 1; + } + + // Attack cooldown + if (!canAttack) { + attackTimer += delta / 1000; + if (attackTimer >= player.cooldown) { + canAttack = true; + attackTimer = 0; + } + } + + // Update particles (physics only, drawing moved to end) + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i]; + p.x += p.vx * delta / 1000; + p.y += p.vy * delta / 1000; + p.vy += 300 * delta / 1000; // gravity + p.life -= delta / 1000; + + if (p.life <= 0) { + particles.splice(i, 1); + } + } + + // Update projectiles + for (let i = projectiles.length - 1; i >= 0; i--) { + const proj = projectiles[i]; + + // Skip if projectile is undefined (safety check) + if (!proj) { + projectiles.splice(i, 1); + continue; + } + + // Update position based on velocity (vx, vy) or default left movement + if (proj.vx !== undefined) { + proj.x += proj.vx * delta / 1000; + proj.y += proj.vy * delta / 1000; + } else { + proj.x -= 400 * delta / 1000; + } + + proj.life -= delta / 1000; + + if (proj.x < -50 || proj.x > 850 || proj.y < -50 || proj.y > 650 || proj.life <= 0) { + projectiles.splice(i, 1); + } else { + // Only check collision with player during GAME state + if (state === 'GAME' && Math.abs(proj.x - player.x) < 11.25 && Math.abs(proj.y - player.pos) < 11.25) { + takeDmg(); + play(150, 0.2); + shake(10); + projectiles.splice(i, 1); + } else { + // Only draw projectiles during GAME state + if (state === 'GAME') { + // Different projectile visuals based on type + if (proj.type === 'fire') { + // Fire projectiles - orange/red with glow + graphics.fillStyle(0xff4400, 1); + graphics.fillCircle(proj.x, proj.y, 6); + graphics.fillStyle(0xffaa00, 0.6); + graphics.fillCircle(proj.x, proj.y, 12); + } else if (proj.type === 'wave') { + // Wave projectiles - blue with trail + graphics.fillStyle(0x0088ff, 1); + graphics.fillRect(proj.x - 4, proj.y - 2, 8, 4); + graphics.fillStyle(0x00aaff, 0.7); + graphics.fillCircle(proj.x, proj.y, 8); + } else if (proj.type === 'burst') { + // Burst projectiles - purple fast + graphics.fillStyle(0x8800ff, 1); + graphics.fillRect(proj.x - 3, proj.y - 3, 6, 6); + graphics.fillStyle(0xaa00ff, 0.8); + graphics.fillCircle(proj.x, proj.y, 5); + } else { + // Normal projectiles - red + graphics.fillStyle(0xff0000, 1); + graphics.fillCircle(proj.x, proj.y, 8); + } + } + } + } + } + + // Update damage numbers + for (let i = damageNumbers.length - 1; i >= 0; i--) { + const dn = damageNumbers[i]; + dn.y -= 50 * delta / 1000; + dn.life -= delta / 1000 * 2; + + if (dn.life <= 0) { + damageNumbers.splice(i, 1); + } else { + graphics.fillStyle(dn.color, dn.life); + graphics.fillRect(dn.x - 2, dn.y - 2, 4, 4); + // Simple number rendering + const dmgStr = '-' + dn.dmg; + for (let j = 0; j < dmgStr.length; j++) { + graphics.fillRect(dn.x + j * 10, dn.y, 2, 10); + } + } + } + + if (state === 'GAME') { drawGame(); + + // Enemy projectile shooting (solo si está vivo) + if (enemy && enemy.type === 'small' && enemy.hp > 0) { + enemy.shootTimer -= delta / 1000; + if (enemy.shootTimer <= 0) { + const aiType = AI[ZONES[zone][Z.AI]].smallType; + // Use the current playable vertical range (minY..maxY) so projectiles can reach edges + const targetY = (typeof minY !== 'undefined' && typeof maxY !== 'undefined') ? (minY + Math.random() * (maxY - minY)) : (200 + Math.random() * 200); + const speedMult = getProjSpeed(zone); + + if (aiType === 'rat') { + // Rata dispara MUCHO más rápido: 1.2s (zona 0) -> 0.2s (zona 4 ABISMO) + enemy.shootTimer = 1.2 - (zone * 0.25); + projectiles.push({ x: 600, y: targetY, dmg: 1, life: 3.0, vx: -200 * speedMult, vy: 0, type: 'normal' }); + play(800, 0.1); + } else if (aiType === 'alma') { + // Alma dispara MUCHO más rápido: 1.0s (zona 0) -> 0.15s (zona 4 ABISMO) + enemy.shootTimer = 1.0 - (zone * 0.2125); + projectiles.push({ x: 600, y: targetY, dmg: 1, life: 3.0, vx: -250 * speedMult, vy: 0, type: 'normal' }); + play(700, 0.1, 'square'); + } + } + } + + // Big enemy attack (solo si está vivo) + if (enemy && enemy.type === 'big' && enemy.hp > 0) { + enemy.attackTimer -= delta / 1000; + if (enemy.attackTimer <= 0) { + const aiType = AI[ZONES[zone][Z.AI]].bigType; + + if (aiType === 'troll') { + enemy.attackTimer = 2.8; + enemy.attacking = Math.random() < 0.5 ? 'upper' : 'lower'; + enemy.attackWarn = 1.2; + play(150, 0.4, 'sawtooth'); + } + else if (aiType === 'demon') { + // DEMON: Zone attack + 7 projectiles aimed at player with 15° spread + enemy.attackTimer = 2.8; + enemy.attacking = Math.random() < 0.5 ? 'upper' : 'lower'; + enemy.attackWarn = 1.2; + play(150, 0.4, 'sawtooth'); + // 7 projectiles aimed at player with 15° spread between each + const speedMult = getProjSpeed(zone); + const baseAngle = Math.atan2(player.pos - 300, player.x - 600); + const spreadDegrees = 15; // 15 degrees between each projectile + const spreadRad = (spreadDegrees * Math.PI) / 180; + for (let i = 0; i < 7; i++) { + setTimeout(() => { + if (enemy && enemy.hp > 0) { + const offset = (i - 3) * spreadRad; // Center the spread: -3, -2, -1, 0, 1, 2, 3 + const angle = baseAngle + offset; + projectiles.push({ x: 600, y: 300, dmg: 1, life: 3.0, vx: Math.cos(angle) * 140 * speedMult, vy: Math.sin(angle) * 140 * speedMult, type: 'fire' }); + } + }, i * 60); + } + } + else if (aiType === 'troll_abyss') { + enemy.attackTimer = 1.0; + enemy.attacking = 'zone'; + // Target zone on player position (forces constant movement) + const zoneW = 80; // Zone width + const zoneH = 80; // Zone height + enemy.attackZoneX = Math.max(50, Math.min(450 - zoneW, player.x - zoneW / 2)); // Center on player X + enemy.attackZoneY = Math.max(200, Math.min(400 - zoneH, player.pos - zoneH / 2)); // Center on player Y + enemy.attackZoneW = zoneW; + enemy.attackZoneH = zoneH; + enemy.attackWarn = 0.4; + const speedMult = getProjSpeed(zone); + for (let i = 0; i < 3; i++) { + setTimeout(() => { + if (enemy && enemy.hp > 0) { + const targetY = (typeof minY !== 'undefined' && typeof maxY !== 'undefined') ? (minY + Math.random() * (maxY - minY)) : (200 + Math.random() * 200); + const targetX = 70 + Math.random() * 360; + const angle = Math.atan2(targetY - 300, targetX - 600); + projectiles.push({ x: 600, y: 300, dmg: 1, life: 3.0, vx: Math.cos(angle) * 150 * speedMult, vy: Math.sin(angle) * 150 * speedMult, type: 'normal' }); + } + }, i * 200); + } + play(150, 0.4, 'sawtooth'); + } + else if (aiType === 'golem') { + enemy.attackTimer = 3.5; + enemy.attacking = 'line'; + // TRIPLE LINE ATTACK - 3 horizontal lines cascading down + enemy.attackLineY = 200 + Math.random() * 100; // First line: 200-300 + enemy.attackLineY2 = enemy.attackLineY + 50; // Second line: 50px below + enemy.attackLineY3 = enemy.attackLineY + 100; // Third line: 100px below + enemy.attackWarn = 1.0; + enemy.golemShake = true; + play(100, 0.5, 'sawtooth'); + } + else if (aiType === 'dragon') { + enemy.attackTimer = 3.2; + // Ataque combinado: Zona trasera (más ancha) + Zona centro + Proyectiles con spread + enemy.attacking = 'zone'; + enemy.attackZoneX = 50; // Borde izquierdo + enemy.attackZoneY = 200; // Desde arriba + enemy.attackZoneW = 120; // MÁS ANCHO: 120px (casi 1/3 del área) + enemy.attackZoneH = 200; // Toda la altura disponible + // Segunda zona en el centro + enemy.attackZone2X = 200; // Centro del área + enemy.attackZone2Y = 200; + enemy.attackZone2W = 100; // Zona centro + enemy.attackZone2H = 200; + enemy.attackWarn = 0.8; + const speedMult = getProjSpeed(zone); + const baseAngle = Math.atan2(player.pos - 300, player.x - 550); + // Lanzar 11 proyectiles con más dispersión (±35 grados) + for (let i = 0; i < 11; i++) { + setTimeout(() => { + if (enemy && enemy.hp > 0) { + const spread = (Math.random() - 0.5) * 1.22; // ±35 grados en radianes (1.22 rad ≈ 35°) + const angle = baseAngle + spread; + projectiles.push({ x: 550, y: 300, dmg: 1, life: 3.5, vx: Math.cos(angle) * 180 * speedMult, vy: Math.sin(angle) * 180 * speedMult, type: 'fire' }); + } + }, i * 50); // Delay más corto: 50ms entre proyectiles + } + play(300, 0.5, 'sawtooth'); + } + else if (aiType === 'dragon_boss') { + enemy.attackTimer = 1.2; // Reducido de 1.5 a 1.2 (ataca más frecuente) + enemy.attacking = 'zone'; + // Player area: X(50-450), Y(200-400) = 400x200px + // Cubrir el área izquierda/derecha que NO cubren los proyectiles (que van al centro) + const side = Math.random() < 0.5 ? 'left' : 'right'; + if (side === 'left') { + enemy.attackZoneX = 50; // Borde izquierdo + enemy.attackZoneW = 100 + Math.random() * 50; // 100-150px de ancho + } else { + enemy.attackZoneW = 100 + Math.random() * 50; // 100-150px de ancho + enemy.attackZoneX = 450 - enemy.attackZoneW; // Borde derecho + } + enemy.attackZoneY = 200 + Math.random() * 100; // Y: 200-300 + enemy.attackZoneH = 100 + Math.random() * 100; // Alto: 100-200px + enemy.attackWarn = 0.6; // Reducido de 0.8 a 0.6 (menos tiempo de advertencia) + const speedMult = getProjSpeed(zone); + const targetX = 150 + Math.random() * 200; // Random center dentro del área del jugador + const targetY = 260 + Math.random() * 80; // Centro del área vertical + const playerCenterAngle = Math.atan2(targetY - 300, targetX - 550); + for (let i = 0; i < 12; i++) { // Aumentado de 8 a 12 proyectiles + setTimeout(() => { + if (enemy && enemy.hp > 0) { + const spread = (i - 5.5) * 0.18; // Ajustado el spread para 12 proyectiles + const angle = playerCenterAngle + spread; + projectiles.push({ x: 550, y: 300, dmg: 1, life: 4.0, vx: Math.cos(angle) * 200 * speedMult, vy: Math.sin(angle) * 200 * speedMult, type: 'fire' }); + } + }, i * 50); // Reducido de 80ms a 50ms (dispara más rápido) + } + play(300, 0.5, 'sawtooth'); + } + } + + if (enemy && enemy.attackWarn > 0) { + enemy.attackWarn -= delta / 1000; + if (enemy.attackWarn <= 0) { + // Iniciar ataque activo + if (enemy) enemy.attackActive = 0.15; // Zona activa por 150ms + } + } + + if (enemy && enemy.attackActive > 0) { + enemy.attackActive -= delta / 1000; + + // Execute attack based on type + if (enemy.attacking === 'upper' || enemy.attacking === 'lower') { + // TROLL: Attack in targeted section + const attackMinY = enemy.attacking === 'upper' ? 200 : 300; + const attackMaxY = enemy.attacking === 'upper' ? 300 : 400; + + if (player.pos >= attackMinY && player.pos <= attackMaxY) { + takeDmg(); + if (state === 'GAMEOVER') return; // Salir inmediatamente si murió + play(150, 0.4); + shake(18); + if (enemy) enemy.attackActive = 0; // Detener tras golpear + } + } + else if (enemy.attacking === 'line') { + // GOLEM: Triple line cascade attack + let hit = false; + // Check first line + if (player.pos >= enemy.attackLineY && player.pos <= enemy.attackLineY + 50) { + hit = true; + } + // Check second line + if (enemy.attackLineY2 && player.pos >= enemy.attackLineY2 && player.pos <= enemy.attackLineY2 + 50) { + hit = true; + } + // Check third line + if (enemy.attackLineY3 && player.pos >= enemy.attackLineY3 && player.pos <= enemy.attackLineY3 + 50) { + hit = true; + } + if (hit) { + takeDmg(); + if (state === 'GAMEOVER') return; + play(150, 0.3); + shake(30); // Big shake for triple line + if (enemy) enemy.attackActive = 0; + } + } + else if (enemy.attacking === 'zone') { + // ABYSS ENEMIES: Attack in random zone + const zoneLeft = enemy.attackZoneX; + const zoneRight = enemy.attackZoneX + enemy.attackZoneW; + const zoneTop = enemy.attackZoneY; + const zoneBottom = enemy.attackZoneY + enemy.attackZoneH; + + let inZone = player.x >= zoneLeft && player.x <= zoneRight && + player.pos >= zoneTop && player.pos <= zoneBottom; + + // Verificar segunda zona si existe (dragon) + if (!inZone && enemy.attackZone2X !== undefined) { + const zone2Left = enemy.attackZone2X; + const zone2Right = enemy.attackZone2X + enemy.attackZone2W; + const zone2Top = enemy.attackZone2Y; + const zone2Bottom = enemy.attackZone2Y + enemy.attackZone2H; + inZone = player.x >= zone2Left && player.x <= zone2Right && + player.pos >= zone2Top && player.pos <= zone2Bottom; + } + + if (inZone) { + takeDmg(); + if (state === 'GAMEOVER') return; // Salir inmediatamente si murió + play(150, 0.4); + shake(20); // Strong shake for zone attacks + if (enemy) enemy.attackActive = 0; // Detener tras golpear + } + } + + if (enemy && enemy.attackActive <= 0) { + // Limpiar ataque cuando termine + if (state === 'GAMEOVER') return; // Salir si takeDmg() causó game over + + // Solo intentar limpiar si enemy todavía existe + if (enemy) { + enemy.attacking = null; + enemy.golemShake = false; + enemy.dragonBreath = false; + // Clear zone attack variables + enemy.attackZoneX = undefined; + enemy.attackZoneY = undefined; + enemy.attackZoneW = undefined; + enemy.attackZoneH = undefined; + enemy.attackZone2X = undefined; + enemy.attackZone2Y = undefined; + enemy.attackZone2W = undefined; + enemy.attackZone2H = undefined; + enemy.attackLineY = undefined; + } + } + } + } + } } -function moveSnake(scene) { - const head = snake[0]; - const newHead = { - x: head.x + direction.x * snakeSize, - y: head.y + direction.y * snakeSize - }; +function drawGame() { + const zoneColor = ZONES[zone][Z.COLOR]; + + // Background gradient with zone color + graphics.fillGradientStyle(0x000000, 0x000000, zoneColor, zoneColor, 0.3); + graphics.fillRect(0, 0, 800, 600); - // Check wall collision - if (newHead.x < 0 || newHead.x >= 800 || newHead.y < 0 || newHead.y >= 600) { - endGame(scene); - return; + // Cinematic black bars (only during gameplay) + if (state === 'GAME') { + const barHeight = 180; // Height of each cinematic bar (3x larger for more cinematic effect) + graphics.fillStyle(0x000000, 1); + graphics.fillRect(0, 0, 800, barHeight); // Top bar + graphics.fillRect(0, 600 - barHeight, 800, barHeight); // Bottom bar + } + + // Player (sprite-based) + const px = player.x; + const py = player.pos; + + // Create or update player sprite + if (!playerSprite || !playerSprite.scene) { + // Crear sprite solo si no existe o fue destruido + if (playerSprite && !playerSprite.scene) { + playerSprite = null; + } + playerSprite = scene.add.image(px, py, 'miner'); + playerSprite.setScale(1.5); // Scale up for better visibility + playerSprite.setDepth(10); // Asegurar que esté sobre otros elementos + } + + // Actualizar posición y visibilidad + if (playerSprite && playerSprite.scene) { + playerSprite.setPosition(px, py); + playerSprite.setVisible(true); + + // Pickaxe swing animation + const pickSwing = !canAttack ? Math.sin(attackTimer / player.cooldown * Math.PI) * 15 : 0; + playerSprite.setRotation(pickSwing * 0.01); // Subtle rotation during attack + } + + // Attack cooldown bar with glow + if (!canAttack) { + const pct = attackTimer / player.cooldown; + // Glow + graphics.fillStyle(0x00ff00, 0.3); + graphics.fillRect(px - 32, py + 38, 64, 10); + // Background + graphics.fillStyle(0x003300, 1); + graphics.fillRect(px - 30, py + 40, 60, 6); + // Fill + graphics.fillStyle(0x00ff00, 1); + graphics.fillRect(px - 30, py + 40, 60 * pct, 6); + // Border + graphics.lineStyle(2, 0x00ff00); + graphics.strokeRect(px - 30, py + 40, 60, 6); + // Segments + for (let i = 1; i < 4; i++) { + graphics.lineStyle(1, 0x004400); + graphics.lineBetween(px - 30 + (60 / 4) * i, py + 40, px - 30 + (60 / 4) * i, py + 46); + } + } + + // Draw timing slider (always visible during gameplay) + drawTimingSlider(); + + // Draw event + if (currentEvent === 'ORE' && ore) { + drawOre(); + } else if (currentEvent === 'ENEMY' && enemy) { + drawEnemy(); + } else if (currentEvent === 'CHEST' && chest) { + drawChest(); } - // Check self collision - for (let segment of snake) { - if (segment.x === newHead.x && segment.y === newHead.y) { - endGame(scene); - return; + // Update and draw particles (now as sprites with proper depth) + const deltaTime = 0.016; // ~60 FPS + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i]; + + // Update particle physics + p.x += p.vx * deltaTime; + p.y += p.vy * deltaTime; + p.vy += (p.gravity || 300) * deltaTime; // Use specific gravity or default + p.life -= 2 * deltaTime; + + // Update position and alpha + p.setPosition(p.x, p.y); + p.setAlpha(p.life); + + // Remove dead particles + if (p.life <= 0) { + p.destroy(); + particles.splice(i, 1); } } + + // Draw player hitbox indicator (always on top) + graphics.fillStyle(0xc32454, 0.3); + graphics.fillCircle(player.x, player.pos, 7.5); + graphics.lineStyle(2, 0xc32454, 0.6); + graphics.strokeCircle(player.x, player.pos, 7.5); +} - // Check title block collision - for (let block of titleBlocks) { - if (newHead.x === block.x && newHead.y === block.y) { - endGame(scene); - return; +function drawOre() { + if (ore.hp <= 0) return; + + const ox = 600; + const oy = 300; + const dmgPct = ore.hp / ore.maxHp; + + const color = MINERAL_COLORS[ZONES[zone][Z.COMMON]] || 0x888888; + const alpha = 0.3 + (dmgPct * 0.7); + + const tempGraphics = scene.add.graphics(); + tempGraphics.setAlpha(alpha); + + // Draw the simplified mineral + drawMineral(ox, oy, color, tempGraphics); + + scene.time.delayedCall(100, () => tempGraphics.destroy()); + + // HP bar + graphics.fillStyle(0x330000, 0.5); + graphics.fillRect(ox - 52, oy - 82, 104, 14); + graphics.fillStyle(0x330000, 1); + graphics.fillRect(ox - 50, oy - 80, 100, 10); + graphics.fillStyle(0xff0000, 1); + graphics.fillRect(ox - 50, oy - 80, 100 * dmgPct, 10); + graphics.lineStyle(1, 0xffffff); + graphics.strokeRect(ox - 50, oy - 80, 100, 10); + + texts.event.setText(`${ZONES[zone][Z.COMMON]}`).setVisible(true); + texts.event.setPosition(ox, oy - 150).setOrigin(0.5, 0); +} + +function drawEnemy() { + if (enemy.hp <= 0) return; // Don't draw if dead + + const ex = 600; + const ey = 300; + + // Clear previous enemy sprites + enemySprites.forEach(sprite => { + if (sprite && sprite.scene) { + sprite.destroy(); + } + }); + enemySprites = []; + + let spriteKey = 'rat'; // default + let scale = 4; // default scale + let tint = 0xffffff; // default no tint + + const ai = AI[ZONES[zone][Z.AI]]; + if (enemy.type === 'small') { + spriteKey = ai.sprite; + scale = ai.smallScale || 1.5; // Use custom scale if defined + tint = ai.smallTint || 0xffffff; + } else { + spriteKey = ai.bigSprite; + scale = ai.bigScale || 6; // Use custom scale if defined + tint = ai.bigTint || 0xffffff; + } + + const enemySprite = scene.add.image(ex, ey, spriteKey); + enemySprite.setScale(scale); + enemySprite.setTint(tint); // Apply color tint + enemySprite.setDepth(5); // Above background but below effects + + // Ghostly pulse effect for Alma enemies + if (ZONES[zone][Z.AI] === 'ALMA') { + const pulse = 0.5 + Math.sin(Date.now() / 300) * 0.3; // Pulse between 0.2 and 0.8 + enemySprite.setAlpha(pulse); + } + + // Final boss aura (Corrupted Hero) + if (ZONES[zone][Z.AI] === 'BOSS' && enemy.type === 'big') { + const auraPulse = 0.3 + Math.sin(Date.now() / 200) * 0.2; + graphics.fillStyle(0x9900ff, auraPulse); + graphics.fillCircle(ex, ey, 120); // Purple aura around boss + } + + enemySprites.push(enemySprite); // Store reference for cleanup + + if (enemy.type === 'small') { + texts.event.setText(`${enemy.name}`).setVisible(true); + texts.event.setPosition(ex, ey - 150).setOrigin(0.5, 0); + } else { + if (enemy.attackWarn > 0) { + const pulse = 0.3 + Math.sin(Date.now() / 100) * 0.2; + + if (enemy.attacking === 'upper') { + // TROLL: Warning for upper section + graphics.fillStyle(0xff0000, pulse); + graphics.fillRect(0, 200, 400, 100); // Upper playable area + graphics.lineStyle(3, 0xff0000, 0.8); + graphics.strokeRect(0, 200, 400, 100); + } + else if (enemy.attacking === 'lower') { + // TROLL: Warning for lower section + graphics.fillStyle(0xff0000, pulse); + graphics.fillRect(0, 300, 400, 100); // Lower playable area + graphics.lineStyle(3, 0xff0000, 0.8); + graphics.strokeRect(0, 300, 400, 100); + } + else if (enemy.attacking === 'line') { + // GOLEM: Triple horizontal line cascade attack + graphics.fillStyle(0xff0000, pulse); + graphics.lineStyle(3, 0xff0000, 0.8); + // First line + graphics.fillRect(0, enemy.attackLineY, 400, 50); + graphics.strokeRect(0, enemy.attackLineY, 400, 50); + // Second line + if (enemy.attackLineY2 && enemy.attackLineY2 <= 400) { + graphics.fillRect(0, enemy.attackLineY2, 400, 50); + graphics.strokeRect(0, enemy.attackLineY2, 400, 50); + } + // Third line + if (enemy.attackLineY3 && enemy.attackLineY3 <= 400) { + graphics.fillRect(0, enemy.attackLineY3, 400, 50); + graphics.strokeRect(0, enemy.attackLineY3, 400, 50); + } + } + else if (enemy.attacking === 'zone') { + // ABYSS ENEMIES: Warning for random zone attack + graphics.fillStyle(0xff0000, pulse); + graphics.fillRect(enemy.attackZoneX, enemy.attackZoneY, enemy.attackZoneW, enemy.attackZoneH); + graphics.lineStyle(3, 0xff0000, 0.8); + graphics.strokeRect(enemy.attackZoneX, enemy.attackZoneY, enemy.attackZoneW, enemy.attackZoneH); + // Segunda zona si existe (dragon) + if (enemy.attackZone2X !== undefined) { + graphics.fillStyle(0xff0000, pulse); + graphics.fillRect(enemy.attackZone2X, enemy.attackZone2Y, enemy.attackZone2W, enemy.attackZone2H); + graphics.lineStyle(3, 0xff0000, 0.8); + graphics.strokeRect(enemy.attackZone2X, enemy.attackZone2Y, enemy.attackZone2W, enemy.attackZone2H); + } + } + else if (enemy.dragonBreath) { + // DRAGON: Cone warning for focused fire breath + graphics.fillStyle(0xff4400, pulse * 0.8); + graphics.fillRect(200, 100, 400, 400); // Large cone area + graphics.lineStyle(4, 0xff4400, 0.9); + graphics.strokeRect(200, 100, 400, 400); + } } + + texts.event.setText(`${enemy.name}`).setVisible(true); + texts.event.setPosition(ex, ey - 150).setOrigin(0.5, 0); } + + // HP bar with glow + const dmgPct = enemy.hp / enemy.maxHp; + graphics.fillStyle(0x330000, 0.5); + graphics.fillRect(ex - 62, ey - 102, 124, 14); + graphics.fillStyle(0x330000, 1); + graphics.fillRect(ex - 60, ey - 100, 120, 10); + graphics.fillStyle(0xff0000, 1); + graphics.fillRect(ex - 60, ey - 100, 120 * dmgPct, 10); + graphics.lineStyle(1, 0xffffff); + graphics.strokeRect(ex - 60, ey - 100, 120, 10); +} - snake.unshift(newHead); +function drawChest() { + if (chest.opened) return; // Don't draw if opened + + const cx = 600; + const cy = 300; + + // Chest body + graphics.fillStyle(0x8B4513, 1); + graphics.fillRect(cx - 40, cy - 30, 80, 60); + graphics.lineStyle(2, 0x000000); + graphics.strokeRect(cx - 40, cy - 30, 80, 60); + + // Metal bands + graphics.fillStyle(0x666666, 1); + graphics.fillRect(cx - 40, cy - 20, 80, 4); + graphics.fillRect(cx - 40, cy + 10, 80, 4); + + // Lock + graphics.fillStyle(0xFFD700, 1); + graphics.fillRect(cx - 5, cy - 10, 10, 20); + graphics.lineStyle(2, 0x000000); + graphics.strokeRect(cx - 5, cy - 10, 10, 20); + + // Sparkles + const sparkles = [ + { x: cx - 45, y: cy - 35, phase: 0 }, + { x: cx + 45, y: cy - 25, phase: Math.PI }, + { x: cx - 35, y: cy + 35, phase: Math.PI * 0.5 } + ]; + sparkles.forEach(s => { + const alpha = 0.5 + Math.sin(Date.now() / 200 + s.phase) * 0.5; + graphics.fillStyle(0xFFFFFF, alpha); + // Dibujar estrella manualmente + const size = 4; + graphics.beginPath(); + for (let i = 0; i < 10; i++) { + const radius = i % 2 === 0 ? size : size / 2; + const angle = (i * Math.PI) / 5; + const px = s.x + Math.cos(angle) * radius; + const py = s.y + Math.sin(angle) * radius; + if (i === 0) graphics.moveTo(px, py); + else graphics.lineTo(px, py); + } + graphics.closePath(); + graphics.fillPath(); + }); + + texts.event.setText('COFRE DEL TESORO').setVisible(true); + texts.event.setPosition(600, 150).setOrigin(0.5, 0); +} - // Check food collision - if (newHead.x === food.x && newHead.y === food.y) { - score += 10; - scoreText.setText('Score: ' + score); - spawnFood(); - playTone(scene, 880, 0.1); +// INPUT HANDLING +function handleInput(event) { + const key = KEYBOARD_TO_ARCADE[event.key] || event.key; + const currentTime = Date.now(); - if (moveDelay > 50) { // Faster max speed (was 80ms) - moveDelay -= 2; + // Debounce discrete actions (not movement) + const needsDebounce = ['P1A', 'P1B', 'START1'].includes(key); + if (needsDebounce && currentTime - lastInputTime < INPUT_DEBOUNCE_MS) { + return; // Ignore input if too soon after last one + } + + if (state === 'MENU') { + if (key === 'START1' || key === 'P1A') { + lastInputTime = currentTime; + startGame(); } + } else if (state === 'GAME') { + if (key === 'P1U') { + inputUp = true; + } else if (key === 'P1D') { + inputDown = true; + } else if (key === 'P1L') { + inputLeft = true; + } else if (key === 'P1R') { + inputRight = true; + } else if (key === 'P1A') { + // Actions: mine/attack/open chest (NOT advance level - must walk) + lastInputTime = currentTime; + // Removed auto-advance with button - player must walk to center/shop + if (canAttack) { + doAction(); // Mine, attack, or open chest + } + } else if (key === 'P1B') { + // Escape to shop (single press) + lastInputTime = currentTime; + if (eventNum >= getMaxEvents(zone) - 1 && zone !== 4 && zone !== 5) { + escapeToShop(); + } else { + // Can't escape during events - show feedback + play(150, 0.2); // Error sound + showBigText('NO PUEDES VOLVER!', 400, 200, '#ff4444', 32, 2200); + } + } + } else if (state === 'SHOP') { + if (key === 'P1U') { + shopSelection = Math.max(0, shopSelection - 1); + play(330, 0.05); + updateShopText(); + } else if (key === 'P1D') { + shopSelection = Math.min(6, shopSelection + 1); + play(330, 0.05); + updateShopText(); + } else if (key === 'P1A') { + // Buy selected item + if (shopSelection === 0) { + // Sell treasures + if (player.treasures > 0) { + player.money += player.treasures; + showBigText(`TESOROS VENDIDOS! +💰${player.treasures}`, 400, 200, '#00ffff', 44, 2200); + play(1100, 0.5, 'sine'); + player.treasures = 0; + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 1) { + // Eat Banana - restore to max HP + if (player.money >= 25) { + player.money -= 25; + const healAmount = player.maxHp - player.hp; + player.hp = player.maxHp; + showBigText(`VIDA RESTAURADA! (+${healAmount})`, 400, 200, '#ffff00', 44, 2200); + play(660, 0.4, 'sawtooth'); + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 1) { + // Eat Banana - restore to max HP + if (player.money >= 25) { + player.money -= 25; + const healAmount = player.maxHp - player.hp; + player.hp = player.maxHp; + showBigText(`VIDA RESTAURADA! (+${healAmount})`, 400, 200, '#ffff00', 44, 2200); + play(660, 0.4, 'sawtooth'); + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 2) { + // Buy HP - upgrade max HP (+5) and heal that amount + if (player.money >= upgradePrices.hp) { + player.money -= upgradePrices.hp; + const gain = 1; // Fixed +1 HP + player.maxHp += gain; + player.hp += gain; // Heal the amount gained + upgradeLevel.hp++; + upgradePrices.hp = Math.floor(upgradePrices.hp * 2.5); + showBigText(`VIDA MAXIMA +${gain}!`, 400, 200, '#ff0000', 44, 2000); + play(880, 0.3); + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 3) { + // Buy DMG + if (player.money >= upgradePrices.dmg) { + player.money -= upgradePrices.dmg; + const gain = roll(3, 6); // Aumentado de 1-3 a 3-6 + player.dmg += gain; + upgradeLevel.dmg++; + upgradePrices.dmg = Math.floor(upgradePrices.dmg * 2.5); + showBigText(`DANO +${gain}!`, 400, 200, '#ff8800', 44, 2000); + play(1000, 0.3); + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 4) { + // Buy SPEED (Movement + Attack Speed) + if (player.money >= upgradePrices.speed) { + player.money -= upgradePrices.speed; + player.moveSpeed = Math.floor(player.moveSpeed * 1.10); // +10% movement speed + player.cooldown = Math.max(0.1, player.cooldown - 0.1); // -0.1s cooldown (min 0.1s) + upgradeLevel.speed++; + upgradePrices.speed = Math.floor(upgradePrices.speed * 2.5); + showBigText('MAS VELOCIDAD!', 400, 200, '#00ffff', 44, 2000); + play(1200, 0.3); + updateShopText(); } else { - snake.pop(); + play(150, 0.2); + } + } else if (shopSelection === 5) { + // Buy TIMING + if (player.money >= upgradePrices.timing) { + player.money -= upgradePrices.timing; + upgradeLevel.timing++; + upgradePrices.timing = Math.floor(upgradePrices.timing * 2.5); + updateTimingZone(false); // Apply the timing upgrade without randomizing position + showBigText('PRECISION MEJORADA!', 400, 200, '#ffff00', 44, 2000); + play(1300, 0.3); + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 6) { + // Return to mines - advance to next room (don't repeat cleared room) + state = 'GAME'; + currentEvent = null; + shopSelection = 0; + + // Advance to next event (reward for clearing the room that unlocked shop) + if (!lastEventWasChest) { + eventNum++; + } + + // Check if zone is complete + if (eventNum >= getMaxEvents(zone)) { + const oldPhase = Math.floor(zone / 3); + runMoney += ZONES[zone][Z.CHEST] * 5; + zone++; + mineralsInZone = 0; // Reset mineral counter for new zone + const newPhase = Math.floor(zone / 3); + eventNum = 0; + + if (zone >= 6) { + state = 'GAMEOVER'; + hideShop(); + showVictory(); + return; + } + } + + hideShop(); + showGameUI(); + startGameMusic(); // Continue background music + nextEvent(); + } + } + } else if (state === 'GAMEOVER') { + if (key === 'START1' || key === 'P1A') { + resetGame(); + } } } -function spawnFood() { - let valid = false; - let attempts = 0; +function handleKeyUp(event) { + const key = event.key; - while (!valid && attempts < 100) { - attempts++; - const gridX = Math.floor(Math.random() * 53) * snakeSize; - const gridY = Math.floor(Math.random() * 40) * snakeSize; + if (state === 'GAME') { + if (key === 'w' || key === 'ArrowUp') { + inputUp = false; + } else if (key === 's' || key === 'ArrowDown') { + inputDown = false; + } else if (key === 'a' || key === 'ArrowLeft') { + inputLeft = false; + } else if (key === 'd' || key === 'ArrowRight') { + inputRight = false; + } + } +} - // Check not on snake - let onSnake = false; - for (let segment of snake) { - if (segment.x === gridX && segment.y === gridY) { - onSnake = true; - break; +// GAME ACTIONS +function doAction() { + // No permitir atacar si el evento ya está completo + if (currentEvent === 'ORE' && ore && ore.hp <= 0) return; + if (currentEvent === 'ENEMY' && enemy && enemy.hp <= 0) return; + if (currentEvent === 'CHEST' && chest && chest.opened) return; + + // Check timing - must be in sweet spot zone + const inTimingZone = timingSlider.position >= timingZone.min && timingSlider.position <= timingZone.max; + + if (!inTimingZone) { + // Timing failed - reset combo and lose 5% of treasures (unless event already complete) + const eventComplete = (currentEvent === 'ORE' && ore && ore.hp <= 0) || + (currentEvent === 'ENEMY' && enemy && enemy.hp <= 0); + + if (!eventComplete) { + // Reset combo on fail + player.combo = 0; + updateGameUI(); + } + + if (currentEvent === 'ORE') { + // Lose 5% + 10 minimum (or all if < 10) + const treasureLoss = player.treasures < 10 ? player.treasures : Math.floor(player.treasures * 0.05) + 10; + player.treasures -= treasureLoss; + if (treasureLoss > 0) { + showBigText(`Te has tropezado y tus tesoros caen al vacío! -${treasureLoss}💎`, 400, 200, '#ff4444', 28, 2500); + } else { + showBigText('Te has tropezado! (pero no llevas tesoros)', 400, 200, '#ffaa00', 28, 2500); } + play(150, 0.2); // Error sound + shake(8); + updateGameUI(); + } else if (currentEvent === 'ENEMY') { + // Lose 5% + 10 minimum (or all if < 10) + const treasureLoss = player.treasures < 10 ? player.treasures : Math.floor(player.treasures * 0.05) + 10; + player.treasures -= treasureLoss; + if (treasureLoss > 0) { + showBigText(`Te has tropezado y tus tesoros caen al vacío! -${treasureLoss}💎`, 400, 200, '#ffaa00', 28, 2500); + } else { + showBigText('Te tropiezas y haces el ridiculo!', 400, 200, '#ffaa00', 28, 2500); + } + play(200, 0.15); // Different sound for embarrassment + shake(5); + updateGameUI(); + } else { + // Just show feedback for chests (no damage or treasure loss) + showBigText('Chocas las manos con el cofre.....', 400, 200, '#ffaa00', 32); + play(200, 0.15); + } + + // Check for game over + if (player.hp <= 0) { + state = 'GAMEOVER'; + showGameOver(); + return; } - // Check not on title blocks - let onTitle = false; - for (let block of titleBlocks) { - if (gridX === block.x && gridY === block.y) { - onTitle = true; - break; + updateGameUI(); + return; + } + + // Timing successful - execute action + canAttack = false; + attackTimer = 0; + + // Create success particles at the timing indicator position + const barHeight = 180; + const sliderX = 100; + const sliderY = 600 - barHeight + 70; + const sliderWidth = 600; + const indicatorX = sliderX + timingSlider.position * sliderWidth; + const indicatorY = sliderY + 10; // Center of the timing bar + createTimingSuccessParticles(indicatorX, indicatorY); + + // Show pickaxe swing effect near the event + const eventX = 600; + const eventY = 300; + showPickaxeSwing(eventX, eventY); + + if (currentEvent === 'ORE' && ore && ore.hp > 0) { + mineOre(); + } else if (currentEvent === 'ENEMY' && enemy && enemy.hp > 0) { + attackEnemy(); + } else if (currentEvent === 'CHEST' && chest && !chest.opened) { + openChest(); + } +} + +function mineOre() { + // Calculate base damage first + const maxDmg = 10 + player.dmg; + let dmg = roll(1, maxDmg, player.dmg); + + // Apply combo bonus only if damage is less than max + if (dmg < maxDmg && player.combo > 0) { + dmg = Math.min(dmg + player.combo, maxDmg); + } + + // Increase combo after hit + player.combo++; + + ore.hp -= dmg; + ore.hits++; + + play(220, 0.15); + shake(5); + spawnParticles(600, 300, ZONES[zone][Z.COLOR], 8); + showBigText(`-${dmg} !`, 600, 200, '#ffaa00', 36); + + // Always drop gold when mining (20% of zone common value per hit) + const miningReward = Math.floor(ZONES[zone][Z.COMMON_VAL] * 0.2); + player.treasures += miningReward; + runMoney += miningReward; + showBigText(`Minando ${ZONES[zone][Z.COMMON]} +${miningReward}`, 600, 250, '#00ffff', 28); + play(800, 0.2, 'sine'); // Mining sound + spawnParticles(600, 300, 0x00ffff, 12); // Cyan particles for treasures + updateGameUI(); // Update display immediately + + if (ore.hp <= 0) { + ore.hp = 0; + // Roll for loot + let totalMoney = 0; + let commonCount = 0; + let rareCount = 0; + let foundRare = false; + const rolls = 3 - ore.hits; + + for (let i = 0; i <= rolls; i++) { + const lootRoll = roll(1, 10); + if (lootRoll <= 4) { + // Mineral común + const commonVal = ZONES[zone][Z.COMMON_VAL]; + player.treasures += commonVal; + runMoney += commonVal; + totalMoney += commonVal; + commonCount++; + } else if (lootRoll === 10) { + // Mineral raro + const rareVal = ZONES[zone][Z.RARE_VAL]; + player.treasures += rareVal; + runMoney += rareVal; + totalMoney += rareVal; + rareCount++; + foundRare = true; + play(1200, 0.3, 'sine'); } + // 5-9 = Piedra (nada) + } + + // Mostrar recompensas + let yOffset = 280; + if (commonCount > 0) { + showBigText(`${ZONES[zone][Z.COMMON]} x${commonCount}`, 600, yOffset, '#cccccc', 32); + yOffset += 40; + } + if (rareCount > 0) { + showBigText(`${ZONES[zone][Z.RARE]} x${rareCount}!`, 600, yOffset, '#ff00ff', 48); + yOffset += 50; } + if (totalMoney > 0) { + showBigText(`+💎${totalMoney}`, 600, yOffset, '#00ffff', 44); + } else { + showBigText('Piedra...', 600, 300, '#666666', 36); + } + + play(440, 0.4); + spawnParticles(600, 300, ZONES[zone][Z.COLOR], 40); + shake(15); + updateGameUI(); + + // Auto-complete event when ore is destroyed + scene.time.delayedCall(200, () => { + if (currentEvent === 'ORE' && ore && ore.hp <= 0) { + completeEvent(); + } + }); + } +} - if (!onSnake && !onTitle) { - food = { x: gridX, y: gridY }; - valid = true; +function attackEnemy() { + // Calculate base damage first + const maxDmg = 10 + player.dmg; + let dmg = roll(1, maxDmg, player.dmg); + + // Apply combo bonus only if damage is less than max + if (dmg < maxDmg && player.combo > 0) { + dmg = Math.min(dmg + player.combo, maxDmg); + } + + // Increase combo after hit + player.combo++; + + enemy.hp -= dmg; + + play(440, 0.1); + shake(3); + spawnParticles(600, 300, 0xff0000, 6); + showBigText(`-${dmg} HP`, 600, 250, '#ff8800', 36); + updateGameUI(); // Update combo display immediately + + if (enemy.hp <= 0) { + enemy.hp = 0; + showBigText('DERROTADO!', 600, 300, '#00ff00', 40); + + if (enemy.type === 'big') { + const rareValue = ZONES[zone][Z.RARE_VAL]; + player.treasures += rareValue; + runMoney += rareValue; + showBigText(`+💎${rareValue}`, 600, 360, '#00ffff', 48); + play(1500, 0.5, 'sine'); + updateGameUI(); + } else if (enemy.type === 'small') { + // Small enemies give 1/5 of the rare value (rounded) + const rareValue = ZONES[zone][Z.RARE_VAL]; + const smallReward = Math.max(1, Math.floor(rareValue / 5)); + player.treasures += smallReward; + runMoney += smallReward; + showBigText(`+💎${smallReward}`, 600, 360, '#88ffff', 36); + play(1200, 0.3, 'sine'); + updateGameUI(); } + spawnParticles(600, 300, 0xff0000, 50); + shake(20); + + // Victory flash + const victory = scene.add.graphics(); + victory.fillStyle(0x00ff00, 0.3); + victory.fillRect(0, 0, 800, 600); + scene.tweens.add({ + targets: victory, + alpha: 0, + duration: 500, + onComplete: () => victory.destroy() + }); + + // Auto-complete event when enemy is defeated + scene.time.delayedCall(200, () => { + if (currentEvent === 'ENEMY' && enemy && enemy.hp <= 0) { + completeEvent(); + } + }); } } -function drawGame() { - graphics.clear(); +function openChest() { + const lootRoll = roll(1, 10); + + chest.opened = true; + play(660, 0.3); + spawnParticles(600, 300, 0xFFD700, 30); + shake(8); + + // Treasure burst flash + const burst = scene.add.graphics(); + burst.fillStyle(0xFFD700, 0.7); + // Dibujar estrella grande manualmente + burst.beginPath(); + for (let i = 0; i < 10; i++) { + const radius = i % 2 === 0 ? 60 : 30; + const angle = (i * Math.PI) / 5 - Math.PI / 2; + const px = 600 + Math.cos(angle) * radius; + const py = 300 + Math.sin(angle) * radius; + if (i === 0) burst.moveTo(px, py); + else burst.lineTo(px, py); + } + burst.closePath(); + burst.fillPath(); + scene.tweens.add({ + targets: burst, + alpha: 0, + scale: 3, + rotation: Math.PI * 2, + duration: 800, + ease: 'Power2', + onComplete: () => burst.destroy() + }); + + // Abyss chests (zone 4+): Only heal or damage (no gold - no shop available) + if (zone >= 4) { + const rand = Math.random(); + if (rand < 0.5) { + // 50% - Heal (Full restore) + const healAmount = player.maxHp - player.hp; + player.hp = player.maxHp; + showBigText('🍌 PLATANO MAGICO! 🍌', 600, 280, '#ffff00', 48, 2800); + showBigText(`VIDA TOTAL +${healAmount} HP!`, 600, 340, '#00ff00', 38, 2500); + play(880, 0.4, 'sine'); + spawnParticles(600, 300, 0xffff00, 30); + for (let i = 0; i < 15; i++) { + setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); + } + shake(10); + } else { + // 50% - +1 Damage + player.dmg += 1; + showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 600, 280, '#ff4400', 48); + play(1200, 0.6, 'sawtooth'); + for (let i = 0; i < 30; i++) { + setTimeout(() => spawnParticles(600, 300, 0xff4400, 5), i * 30); + } + shake(18); + } + } else { + // Normal zones (0-3): Full loot table with treasures + const rand = Math.random(); + if (rand < 0.12) { + // 12% - Empty + showBigText('COFRE VACIO!', 600, 300, '#666666', 40); + play(200, 0.3); + spawnParticles(600, 300, 0x666666, 8); + } else if (rand < 0.27) { + // 15% - Banana (heals 50% HP) + const healAmount = Math.floor(player.maxHp * 0.5); + const actualHeal = Math.min(healAmount, player.maxHp - player.hp); + player.hp = Math.min(player.maxHp, player.hp + healAmount); + showBigText('🍌 PLATANO ENCONTRADO! 🍌', 600, 280, '#ffff00', 48, 2800); + showBigText(`VIDA RESTAURADA +${actualHeal} HP!`, 600, 340, '#00ff00', 38, 2500); + play(880, 0.4, 'sine'); + spawnParticles(600, 300, 0xffff00, 30); + for (let i = 0; i < 15; i++) { + setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); + } + shake(10); + } else if (rand < 0.42) { + // 15% - Rare Loot + const money = ZONES[zone][Z.RARE_VAL]; + player.treasures += money; + runMoney += money; + showBigText(`+💎${money} ARTEFACTOS RUNICOS!`, 600, 340, '#ff66ff', 38); + play(1000, 0.5, 'sawtooth'); + spawnParticles(600, 300, 0xff00ff, 40); + for (let i = 0; i < 15; i++) { + setTimeout(() => spawnParticles(600, 300, 0xffffff, 2), i * 100); + } + shake(12); + } else if (rand < 0.54) { + // 12% - +1 Damage + player.dmg += 1; + showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 600, 280, '#ff4400', 48); + play(1200, 0.6, 'sawtooth'); + for (let i = 0; i < 30; i++) { + setTimeout(() => spawnParticles(600, 300, 0xff4400, 5), i * 30); + } + shake(18); + } else if (rand < 0.77) { + // 23% - Normal treasure + const money = ZONES[zone][Z.CHEST]; + player.treasures += money; + runMoney += money; + showBigText(`+💎${money} GEMAS Y JOYAS!`, 600, 340, '#ffff88', 36); + play(800, 0.4, 'sine'); + spawnParticles(600, 300, 0xffff00, 25); + for (let i = 0; i < 12; i++) { + setTimeout(() => spawnParticles(600, 300, 0xffdd00, 3), i * 80); + } + } else { + // 23% - Small treasure + const money = Math.floor(ZONES[zone][Z.CHEST] * 0.4); + player.treasures += money; + runMoney += money; + showBigText(`+💎${money} ROPA VIEJA!`, 600, 340, '#aaaaaa', 32); + play(600, 0.3); + spawnParticles(600, 300, 0xcccccc, 15); + } + } + + updateGameUI(); - // Draw title blocks - titleBlocks.forEach(block => { - graphics.fillStyle(block.color, 1); - graphics.fillRect(block.x, block.y, snakeSize - 2, snakeSize - 2); + // Auto-complete event when chest is opened + scene.time.delayedCall(200, () => { + if (currentEvent === 'CHEST' && chest && chest.opened) { + completeEvent(); + } }); +} - // Draw snake - snake.forEach((segment, index) => { - if (index === 0) { - graphics.fillStyle(0x00ff00, 1); - } else { - graphics.fillStyle(0x00aa00, 1); +function drawTimingSlider() { + const barHeight = 180; // Match cinematic bar height + const sliderX = 100; // Start from left side of screen + const sliderY = 600 - barHeight + 70; // Positioned in bottom cinematic bar (centered vertically) + const sliderWidth = 600; // Much wider for horizontal layout + const sliderHeight = 20; // Much shorter for horizontal layout + + // Draw slider background (track) - reduced alpha + graphics.fillStyle(0x333333, 0.4); + graphics.fillRect(sliderX - 2, sliderY - 2, sliderWidth + 4, sliderHeight + 4); + + // Draw slider track - reduced alpha + graphics.fillStyle(0x666666, 0.5); + graphics.fillRect(sliderX, sliderY, sliderWidth, sliderHeight); + + // Draw sweet spot zone (middle area) - reduced alpha + const zoneStartX = sliderX + sliderWidth * timingZone.min; + const zoneWidth = sliderWidth * (timingZone.max - timingZone.min); + graphics.fillStyle(0x00ff00, 0.3); + graphics.fillRect(zoneStartX, sliderY, zoneWidth, sliderHeight); + + // Draw slider indicator - reduced alpha (horizontal movement) + const indicatorX = sliderX + timingSlider.position * sliderWidth; + graphics.fillStyle(0xffff00, 0.8); + graphics.fillRect(indicatorX - 5, sliderY - 5, 10, sliderHeight + 10); + + // Draw border - reduced alpha + graphics.lineStyle(2, 0xffffff, 0.6); + graphics.strokeRect(sliderX, sliderY, sliderWidth, sliderHeight); +} + +function completeEvent() { + const wasChest = currentEvent === 'CHEST'; // Guardar antes de limpiar + currentEvent = null; + + // Limpiar animaciones y referencias + if (ore && ore.spawnAnim) { + clearInterval(ore.spawnAnim); + } + ore = null; + enemy = null; + chest = null; + + // Clean up enemy sprites + enemySprites.forEach(sprite => { + if (sprite && sprite.scene) { + sprite.destroy(); } - graphics.fillRect(segment.x, segment.y, snakeSize - 2, snakeSize - 2); }); + enemySprites = []; - // Draw food - graphics.fillStyle(0xff0000, 1); - graphics.fillRect(food.x, food.y, snakeSize - 2, snakeSize - 2); + // Reset timing slider to center for next event (horizontal movement) + timingSlider.position = 0.5; + timingSlider.direction = 1; + + texts.event.setVisible(false); + escapeCount = 0; + + // Show direction choice arrows instead of auto-advancing + showDirectionChoice(wasChest); +} + +function showPickaxeSwing(x, y) { + // Add random variation around the event position (radius of ~25 pixels) + const angle = Math.random() * Math.PI * 2; + const distance = Math.random() * 25; + const offsetX = Math.cos(angle) * distance; + const offsetY = Math.sin(angle) * distance - 30; // -30 to position above + + // Create pickaxe sprite near the event with random variation + const pickaxe = scene.add.image(x + offsetX, y + offsetY, 'pickaxe'); + pickaxe.setScale(8); + pickaxe.setAlpha(0.8); + pickaxe.setDepth(200); // High depth to appear above everything + + // Quick swing animation with particles + scene.tweens.add({ + targets: pickaxe, + angle: 45, + duration: 50, + yoyo: true, + ease: 'Power2', + onComplete: () => { + // Spawn explosion of particles when pickaxe hits + spawnParticles(x + offsetX, y + offsetY, 0xFFD700, 20); + + // Fade out quickly + scene.tweens.add({ + targets: pickaxe, + alpha: 0, + scale: 12, + duration: 100, + ease: 'Power2', + onComplete: () => pickaxe.destroy() + }); + } + }); } -function endGame(scene) { - gameOver = true; - playTone(scene, 220, 0.5); +function showDirectionChoice(wasChest = false) { + directionChoice = null; + lastEventWasChest = wasChest; - // Semi-transparent overlay - const overlay = scene.add.graphics(); - overlay.fillStyle(0x000000, 0.7); - overlay.fillRect(0, 0, 800, 600); + // Clear any existing arrows and block texts + directionArrows.forEach(arrow => arrow.destroy()); + directionArrows = []; - // Game Over title with glow effect - const gameOverText = scene.add.text(400, 300, 'GAME OVER', { + // Clear block texts when navigation becomes available + if (window.centerBlockText) { + window.centerBlockText.destroy(); + window.centerBlockText = null; + } + if (window.leftBlockText) { + window.leftBlockText.destroy(); + window.leftBlockText = null; + } + + // Create forward arrow (right) - always available + const forwardArrow = scene.add.text(700, 300, '→', { fontSize: '64px', - fontFamily: 'Arial, sans-serif', - color: '#ff0000', - align: 'center', - stroke: '#ff6666', - strokeThickness: 8 - }).setOrigin(0.5); + fontFamily: 'Arial', + color: '#00ff00', + stroke: '#000', + strokeThickness: 4 + }).setOrigin(0.5).setInteractive().setDepth(800); - // Pulsing animation for game over text + // Add pulsing animation to the arrow scene.tweens.add({ - targets: gameOverText, - scale: { from: 1, to: 1.1 }, - alpha: { from: 1, to: 0.8 }, + targets: forwardArrow, + scaleX: 1.2, + scaleY: 1.2, duration: 800, yoyo: true, repeat: -1, - ease: 'Sine.easeInOut' + ease: 'Power2' }); - // Score display - scene.add.text(400, 400, 'SCORE: ' + score, { - fontSize: '36px', - fontFamily: 'Arial, sans-serif', - color: '#00ffff', - align: 'center', - stroke: '#000000', - strokeThickness: 4 - }).setOrigin(0.5); + directionArrows.push(forwardArrow); - // Restart instruction with subtle animation - const restartText = scene.add.text(400, 480, 'Press Button A or START to Restart', { - fontSize: '24px', - fontFamily: 'Arial, sans-serif', + // Create back arrow (left) - only at end of zone and if shop available + let backArrow = null; + if (eventNum >= getMaxEvents(zone) - 1 && zone !== 4 && zone !== 5) { + backArrow = scene.add.text(150, 300, '←', { + fontSize: '64px', + fontFamily: 'Arial', color: '#ffff00', - align: 'center', - stroke: '#000000', - strokeThickness: 3 - }).setOrigin(0.5); + stroke: '#000', + strokeThickness: 4 + }).setOrigin(0.5).setInteractive().setDepth(800); + + // Add pulsing animation to back arrow too + scene.tweens.add({ + targets: backArrow, + scaleX: 1.2, + scaleY: 1.2, + duration: 800, + yoyo: true, + repeat: -1, + ease: 'Power2' + }); + + directionArrows.push(backArrow); + + // Add "TIENDA DISPONIBLE!" text above the back arrow + const shopAvailableText = scene.add.text(150, 240, 'TIENDA DISPONIBLE!', { + fontSize: '20px', + fontFamily: 'Arial', + color: '#ffff00', + stroke: '#000', + strokeThickness: 3 + }).setOrigin(0.5).setDepth(800); + + // Add pulsing animation to shop text too + scene.tweens.add({ + targets: shopAvailableText, + alpha: { from: 1, to: 0.5 }, + duration: 600, + yoyo: true, + repeat: -1, + ease: 'Power2' + }); + + directionArrows.push(shopAvailableText); + } - // Blinking animation for restart text + // Show hint text + const canShop = eventNum >= getMaxEvents(zone) - 1 && zone !== 4 && zone !== 5; + const hintMessage = canShop ? ' ← Tienda (Button B) | Continuar (Avanzar) → ' : ' '; + const centerHintText = scene.add.text(400, 450, hintMessage, { + fontSize: '20px', + fontFamily: 'Arial', + color: '#ffffff', + stroke: '#000', + strokeThickness: 2 + }).setOrigin(0.5).setDepth(800); + directionArrows.push(centerHintText); + + // Add click handlers + forwardArrow.on('pointerdown', () => selectDirection('forward')); + if (backArrow) { + backArrow.on('pointerdown', () => selectDirection('back')); + } + + // Add hover effects + forwardArrow.on('pointerover', () => forwardArrow.setScale(1.2)); + forwardArrow.on('pointerout', () => forwardArrow.setScale(1.0)); + if (backArrow) { + backArrow.on('pointerover', () => backArrow.setScale(1.2)); + backArrow.on('pointerout', () => backArrow.setScale(1.0)); + } + + // Animate arrows (pulse effect) + const targets = backArrow ? [forwardArrow, backArrow] : [forwardArrow]; scene.tweens.add({ - targets: restartText, - alpha: { from: 1, to: 0.3 }, + targets: targets, + scale: { from: 1, to: 1.1 }, duration: 600, yoyo: true, repeat: -1, - ease: 'Sine.easeInOut' + ease: 'Power2' }); } -function restartGame(scene) { - snake = [ - { x: 75, y: 60 }, - { x: 60, y: 60 }, - { x: 45, y: 60 } +function selectDirection(choice) { + directionChoice = choice; + + // Hide arrows and hint + directionArrows.forEach(arrow => arrow.destroy()); + directionArrows = []; + + if (choice === 'forward') { + // Continue to next event (cofres no suman al contador) + if (!lastEventWasChest) { + eventNum++; + } + if (eventNum >= getMaxEvents(zone)) { + // Zone complete + const oldPhase = Math.floor(zone / 3); + runMoney += ZONES[zone][Z.CHEST] * 5; + zone++; + mineralsInZone = 0; // Reset mineral counter for new zone + const newPhase = Math.floor(zone / 3); + eventNum = 0; + + if (oldPhase !== newPhase && state === 'GAME') { + stopGameMusic(); + startGameMusic(); + } + + if (zone >= 6) { + state = 'GAMEOVER'; + showVictory(); + return; + } + } + nextEvent(); + } else if (choice === 'back') { + // Go to shop + escapeToShop(); + } +} + +function nextEvent() { + // Randomize timing zone position for new event + updateTimingZone(); + + // Zona 8 (JEFE FINAL): Solo el jefe, un evento + if (zone === 5) { + spawnEnemy(); // Solo el HEROE CORRUPTO + updateGameUI(); + return; + } + + // Zona 4 (EL ABISMO): Solo enemigos y cofres, sin minerales (3 eventos) + if (zone === 4) { + const roll = Math.random(); + if (roll < 0.867) { // 86.7% enemigos (aumentado desde 60%) + spawnEnemy(); + } else { + spawnChest(); // 13.3% cofres (1/3 de 40% = 13.3%) + } + updateGameUI(); + return; + } + + // Zonas normales (0-3): Minerales, enemigos y cofres + const eventRoll = roll(1, 15); // Cambiar de 1-10 a 1-15 para ajustar probabilidades + if (eventRoll <= 4) { + spawnEnemy(); // 4/15 ≈ 26.7% + } else if (eventRoll <= 5) { + spawnChest(); // 1/15 ≈ 6.7% (1/3 de 20% original) + } else { + // Max 3 minerals per zone + if (mineralsInZone < 3) { + spawnOre(); // 10/15 ≈ 66.7% + mineralsInZone++; + } else { + spawnEnemy(); // Spawn enemy instead if mineral limit reached + } + } + + updateGameUI(); +} + +function spawnOre() { + currentEvent = 'ORE'; + ore = { hp: 30, maxHp: 30, hits: 0, scale: 0 }; + play(330, 0.2); + + // Show mineral discovery text + showBigText('MINERAL ENCONTRADO!', 400, 380, '#ffff00', 48); + + // Spawn animation - bounce in + const startTime = Date.now(); + ore.spawnAnim = setInterval(() => { + const elapsed = (Date.now() - startTime) / 300; + ore.scale = Math.min(1, elapsed); + if (elapsed >= 1) { + clearInterval(ore.spawnAnim); + ore.scale = 1; + } + }, 16); +} + +function spawnEnemy() { + currentEvent = 'ENEMY'; + + // Zona ??? (5) solo tiene enemigos grandes + if (zone === 5 || Math.random() >= 0.65) { + // Big enemy + enemy = { + type: 'big', + name: ZONES[zone][Z.BIG_E], + hp: ZONES[zone][Z.BIG_HP], + maxHp: ZONES[zone][Z.BIG_HP], + dmgDice: ZONES[zone][Z.BIG_DMG], + attackTimer: 1.5, // Empezar atacando pronto + attacking: null, + attackWarn: 0 + }; + } else { + // Small enemy + const hp = roll(ZONES[zone][Z.SMALL_HP][0], ZONES[zone][Z.SMALL_HP][1], ZONES[zone][Z.SMALL_HP][2]); + enemy = { + type: 'small', + name: ZONES[zone][Z.SMALL_E], + hp: hp, + maxHp: hp, + dmgDice: ZONES[zone][Z.SMALL_DMG], + shootTimer: 0.6 - (zone * 0.1) // Empezar disparando pronto, MUY rápido en zonas avanzadas + }; + } + + // Show enemy discovery text based on type + if (enemy.type === 'small') { + showBigText('ENEMIGO AVISTADO!', 400, 380, '#ff6600', 48); + } else { + // Cambia 180 por 300 + showBigText('PELIGRO ENEMIGO ELITE!', 400, 300, '#ff0000', 42); + } + + play(200, 0.3); +} + +function spawnChest() { + currentEvent = 'CHEST'; + chest = { opened: false }; + play(550, 0.2); + + // Show secret room discovery text + showBigText('SALA SECRETA ENCONTRADA!', 400, 380, '#ff00ff', 48); +} + +// STATE TRANSITIONS +function startGame() { + state = 'GAME'; + zone = 0; + eventNum = 0; + runMoney = 0; + currentEvent = null; + escapeCount = 0; + + hideMenu(); + showGameUI(); + startGameMusic(); // Start background music synchronized with timing + nextEvent(); +} + +function escapeToShop() { + // Clear navigation UI before going to shop + directionArrows.forEach(arrow => arrow.destroy()); + directionArrows = []; + + // Clear block texts + if (window.centerBlockText) { + window.centerBlockText.destroy(); + window.centerBlockText = null; + } + if (window.leftBlockText) { + window.leftBlockText.destroy(); + window.leftBlockText = null; + } + + state = 'SHOP'; + shopSelection = 0; + projectiles = []; + particles = []; + inputUp = false; // Reset input state + inputDown = false; + inputLeft = false; + inputRight = false; + lastInputTime = 0; // Reset debounce timer + stopGameMusic(); // Stop music when going to shop + hideGame(); + showShop(); +} + +function resetGame() { + // Stop background music + stopGameMusic(); + + // Complete deep reset - reinitialize ALL game state + + // Player and stats + player = { hp: 1, maxHp: 1, dmg: 1, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0 }; // Reset to center position + + // Reset input state + inputUp = false; + inputDown = false; + inputLeft = false; + inputRight = false; + lastInputTime = 0; // Reset debounce timer + + // Game state + state = 'MENU'; + zone = 0; + mineralsInZone = 0; // Reset mineral counter + eventNum = 0; + currentEvent = null; + lastEventWasChest = false; + + // Combat state + enemy = null; + ore = null; + chest = null; + escapeCount = 0; + attackTimer = 0; + canAttack = true; + + // Money and upgrades + runMoney = 0; + upgradePrices = { hp: 50, dmg: 100, speed: 75, timing: 200 }; + upgradeLevel = { hp: 0, dmg: 0, speed: 0, timing: 0 }; + updateTimingZone(); // Reset timing zone to base values + shopSelection = 0; + + // Visual effects + shakeAmt = 0; + // Destroy particle sprites before clearing array + particles.forEach(p => p.destroy()); + particles = []; + projectiles = []; + damageNumbers = []; + bgStars = []; + particleEmitters = []; + directionChoice = null; + directionArrows = []; + timingSlider = { position: 0, direction: 1, speed: 2 }; + + // Stop all active tweens FIRST (before destroying objects) + if (scene && scene.tweens) { + scene.tweens.killAll(); + } + + // Clear all timers/intervals + if (ore && ore.spawnAnim) { + clearInterval(ore.spawnAnim); + } + + // Destroy all sprites and graphics + if (playerSprite && playerSprite.scene) { + playerSprite.destroy(); + playerSprite = null; + } + + // Clean up floating texts (after killing tweens) + floatingTexts.forEach(t => { + if (t && t.scene) { + t.destroy(); + } + }); + floatingTexts = []; + + // Clean up direction arrows + directionArrows.forEach(arrow => arrow.destroy()); + directionArrows = []; + if (texts.directionHint) { + texts.directionHint.destroy(); + texts.directionHint = null; + } + + // Clean up enemy sprites + enemySprites.forEach(sprite => { + if (sprite && sprite.scene) { + sprite.destroy(); + } + }); + enemySprites = []; + + // Clean up particle emitters + particleEmitters.forEach(emitter => { + if (emitter && emitter.scene) { + emitter.destroy(); + } + }); + particleEmitters = []; + + // Clear graphics + if (graphics) { + graphics.clear(); + } + + // Reset camera position + if (scene && scene.cameras && scene.cameras.main) { + scene.cameras.main.setPosition(0, 0); + } + + // Reinitialize background stars + createBgStars(); + + // Show menu + showMenu(); +} + +// UI FUNCTIONS +function hideMenu() { + texts.title.setVisible(false); + texts.subtitle.setVisible(false); + texts.info.setVisible(false); +} + +function showMenu() { + // Restaurar textos originales del menú + texts.title.setText('EL ABYSS').setColor('#ff4400'); + texts.subtitle.setText('PRESIONA START').setColor('#ffaa00'); + texts.subtitle.setAlign('center'); + texts.subtitle.setWordWrapWidth(0); // Desactivar word wrap + texts.info.setText('DESCIENDE A LA OSCURIDAD • ENFRENTA TU DESTINO • RECLAMA TU FORTUNA').setColor('#806060ff'); + +  texts.combo.setVisible(false);     // <-- Faltaba este + texts.title.setVisible(true); + texts.subtitle.setVisible(true); + texts.info.setVisible(true); + texts.zone.setVisible(false); + texts.hp.setVisible(false); + texts.money.setVisible(false); + texts.event.setVisible(false); + if (texts.treasures) texts.treasures.setVisible(false); +} + +function showGameUI() { + // Hide menu texts + texts.title.setVisible(false); + texts.subtitle.setVisible(false); + texts.info.setVisible(false); + + // Show game texts + texts.zone.setVisible(true); + texts.hp.setVisible(true); + texts.money.setVisible(true); + texts.treasures.setVisible(true); + if (playerSprite && playerSprite.scene) playerSprite.setVisible(true); + updateGameUI(); +} + +function updateGameUI() { + const maxEvents = getMaxEvents(zone); + texts.zone.setText(`${ZONES[zone][Z.NAME]} - SALA ${eventNum + 1}/${maxEvents}`); + + // Display hearts + let hearts = ''; + for (let i = 0; i < player.maxHp; i++) { + hearts += i < player.hp ? '❤️' : '🖤'; + } + texts.hp.setText(hearts); + + texts.money.setText(`💰${player.money}`); + texts.treasures.setText(`💎 Tesoros: ${player.treasures}`); + + // Show combo counter if > 0 + if (player.combo > 0) { + texts.combo.setText(`COMBO x${player.combo}`); + texts.combo.setVisible(true); + } else { + texts.combo.setVisible(false); + } +} + +function hideGame() { + texts.zone.setVisible(false); + texts.hp.setVisible(false); + texts.money.setVisible(false); + texts.treasures.setVisible(false); + texts.combo.setVisible(false); + texts.event.setVisible(false); + if (playerSprite && playerSprite.scene) playerSprite.setVisible(false); +} + +function hideShop() { + // Stop shop music + stopShopMusic(); + + // Clear shop texts + if (texts.shop && Array.isArray(texts.shop)) { + texts.shop.forEach(t => t.destroy()); + texts.shop = []; + } + + // Hide menu texts (don't reset them to menu state since we're going to game) + texts.title.setVisible(false); + texts.subtitle.setVisible(false); + texts.info.setVisible(false); +} + +function showShop() { + const bg = scene.add.graphics(); + // Tavern wood background gradient + bg.fillGradientStyle(0x2a1810, 0x2a1810, 0x1a0f08, 0x1a0f08, 1); + bg.fillRect(0, 0, 800, 600); + // Wood planks texture + for(let i = 0; i < 12; i++) { + bg.fillStyle(0x1a0f08, 0.3); + bg.fillRect(0, i * 50, 800, 2); + } + // Stone counter + bg.fillStyle(0x4a4a4a, 0.9); + bg.fillRect(50, 90, 700, 480); + bg.lineStyle(4, 0x6a5a4a); + bg.strokeRect(50, 90, 700, 480); + // Counter edge highlight + bg.lineStyle(2, 0x8a7a6a, 0.5); + bg.strokeRect(52, 92, 696, 476); + bg.setDepth(400); + texts.shop = [bg]; + + // Tavern sign + texts.title.setText('La Taberna Del Platano Dorado') + .setStyle({ fontSize: '42px', fontFamily: 'Georgia, serif', color: '#ffcc66', stroke: '#331a00', strokeThickness: 6, fontStyle: 'bold italic' }) + .setVisible(true).setPosition(400, 35).setDepth(450); + + // Gold pouch display + texts.subtitle.setText(`💰 ${player.money} Monedas`) + .setStyle({ fontSize: '26px', fontFamily: 'Georgia, serif', color: '#ffd700', stroke: '#331a00', strokeThickness: 4, fontStyle: 'italic' }) + .setVisible(true).setPosition(400, 82).setOrigin(0.5).setDepth(450); + + // Tavern keeper message + texts.info.setText('↑↓ Elegir | ⚔ Pedir | ← Salir de la Taberna') + .setStyle({ fontSize: '15px', fontFamily: 'Georgia, serif', color: '#d4a574', stroke: '#1a0f08', strokeThickness: 2, fontStyle: 'italic' }) + .setVisible(true).setPosition(400, 565).setOrigin(0.5).setDepth(450); + + updateTimingZone(); + updateShopText(); + startShopMusic(); +} + +function updateShopText() { + // Update gold display + texts.subtitle.setText(`💰 ${player.money} Monedas`); + + if (texts.shop && Array.isArray(texts.shop)) { + texts.shop.slice(1).forEach(t => t.destroy()); + texts.shop = [texts.shop[0]]; + } + + const options = [ + { name: '💎 Vender Tesoros', price: -1, info: `Convierte tus tesoros en monedas (${player.treasures} tesoros disponibles)`, color: '#00ffff', cat: '💰' }, + { name: '🍌 Estofado de Potasio', price: 25, info: 'Rellena tus corazones', color: '#ffdd44', cat: '🍽️' }, + { name: '❤️ Entrenamiento Fisico', price: upgradePrices.hp, info: `FULL TANK +1 Corazon [${player.hp}/${player.maxHp}]`, color: '#ff5555', cat: '💪' }, + { name: '⚔️ Afilar Herramientas', price: upgradePrices.dmg, info: `Pico mas letal (+DMG)(+3d6) [${(1 + player.dmg)}-${(10 + player.dmg * 2)}]`, color: '#ff8833', cat: '🔨' }, + { name: '⚡ Cerveza Energizante', price: upgradePrices.speed, info: `Movimiento y ataque mas rapidos [${player.cooldown.toFixed(1)}s]`, color: '#44ddff', cat: '🍺' }, + { name: '🎯 Leccion de Precision', price: upgradePrices.timing, info: `Mejora tu punteria (ZONA VERDE MAS GRANDE) (Nivel ${upgradeLevel.timing})`, color: '#ffdd44', cat: '📜' }, + { name: '⛏️ Volver a las Minas', price: 0, info: 'Regresar al abismo oscuro...', color: '#88ff88', cat: '🚪' } ]; - direction = { x: 1, y: 0 }; - nextDirection = { x: 1, y: 0 }; - score = 0; - gameOver = false; - moveDelay = 100; // Match new faster initial speed - scoreText.setText('Score: 0'); - spawnFood(); - scene.scene.restart(); + + let y = 118; + options.forEach((opt, idx) => { + const selected = idx === shopSelection; + const canAfford = player.money >= opt.price || opt.price <= 0; + + const cardBg = scene.add.graphics(); + // Parchment-style card + if (selected) { + cardBg.fillStyle(canAfford ? 0x3a2f1f : 0x2f1a1a, 0.95); + cardBg.fillRect(80, y, 640, 60); + cardBg.lineStyle(3, canAfford ? 0xffd700 : 0xff4444, 1); + cardBg.strokeRect(80, y, 640, 60); + // Inner glow + cardBg.lineStyle(1, canAfford ? 0xffee99 : 0xff8888, 0.6); + cardBg.strokeRect(83, y + 3, 634, 54); + } else { + cardBg.fillStyle(0x2a1f12, 0.7); + cardBg.fillRect(80, y, 640, 60); + cardBg.lineStyle(1, 0x4a3a2a, 0.6); + cardBg.strokeRect(80, y, 640, 60); + } + cardBg.setDepth(410); + texts.shop.push(cardBg); + + // Category icon + const catText = scene.add.text(95, y + 10, opt.cat, { + fontSize: '26px', + fontFamily: 'Arial' + }).setDepth(420); + texts.shop.push(catText); + + // Item name with tavern style + const nameText = scene.add.text(135, y + 6, opt.name, { + fontSize: selected ? '22px' : '20px', + fontFamily: 'Georgia, serif', + color: selected ? (canAfford ? opt.color : '#665555') : '#998877', + stroke: '#0a0502', + strokeThickness: selected ? 3 : 2, + fontStyle: 'bold' + }).setDepth(420); + texts.shop.push(nameText); + + // Description + const descText = scene.add.text(135, y + 34, opt.info, { + fontSize: '13px', + fontFamily: 'Georgia, serif', + color: selected ? (canAfford ? '#ccbb99' : '#ff7777') : '#776655', + fontStyle: 'italic' + }).setDepth(420); + texts.shop.push(descText); + + // Price tag with tavern coin style + if (opt.price > 0) { + const priceText = scene.add.text(700, y + 30, `${opt.price}`, { + fontSize: selected ? '26px' : '22px', + fontFamily: 'Georgia, serif', + color: canAfford ? '#ffd700' : '#ff5555', + stroke: '#0a0502', + strokeThickness: 3, + fontStyle: 'bold' + }).setOrigin(1, 0.5).setDepth(420); + texts.shop.push(priceText); + + const coinIcon = scene.add.text(705, y + 30, '◉', { + fontSize: selected ? '20px' : '17px', + color: canAfford ? '#ffee99' : '#aa4444' + }).setOrigin(0, 0.5).setDepth(420); + texts.shop.push(coinIcon); + } else if (opt.price === -1) { + const freeText = scene.add.text(700, y + 30, player.treasures > 0 ? 'Vender!' : 'Sin tesoros', { + fontSize: '18px', + fontFamily: 'Georgia, serif', + color: player.treasures > 0 ? '#00ffff' : '#666666', + stroke: '#0a0502', + strokeThickness: 2, + fontStyle: 'italic bold' + }).setOrigin(1, 0.5).setDepth(420); + texts.shop.push(freeText); + } else { + const freeText = scene.add.text(700, y + 30, 'Vamos!', { + fontSize: '20px', + fontFamily: 'Georgia, serif', + color: '#88dd88', + stroke: '#0a0502', + strokeThickness: 2, + fontStyle: 'italic bold' + }).setOrigin(1, 0.5).setDepth(420); + texts.shop.push(freeText); + } + + y += 62; + }); +} + +function showGameOver() { + // Stop background music + stopGameMusic(); + + // Limpiar todos los estados de juego + currentEvent = null; + projectiles = []; + // Destroy particle sprites before clearing array + particles.forEach(p => p.destroy()); + particles = []; + + // Limpiar enemigos y sus ataques + if (enemy) { + enemy.hp = 0; + enemy.attacking = null; + enemy.attackWarn = 0; + } + enemy = null; + ore = null; + chest = null; + + hideGame(); + + texts.title.setText('Otro Deshecho en el ABYSS').setVisible(true).setPosition(400, 200); + texts.title.setColor('#880000'); + + texts.subtitle.setText( + `Profundidad Alcanzada: ${ZONES[zone][0]}\n` + + `Score Total: $${runMoney}\n` + + `Oro Final: $${player.money}\n\n` + + `PRESIONA START PARA DESCENDER DE NUEVO` + ).setVisible(true).setPosition(400, 320); + + texts.info.setVisible(false); + + play(100, 1.0); } -function playTone(scene, frequency, duration) { - const audioContext = scene.sound.context; - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); +function showVictory() { + hideGame(); + + texts.title.setText('ABYSS CONQUISTADO!').setVisible(true).setPosition(400, 200); + texts.title.setColor('#ffaa00'); + + // Use the last valid zone (5) if zone exceeds the array + const finalZone = Math.min(zone, 5); + + texts.subtitle.setText( + `Profundidad Alcanzada: ${ZONES[finalZone][0]}\n` + + `Score Total: $${runMoney}\n` + + `Oro Final: $${player.money}\n\n\n\n` + // <-- Prueba con 4 \n (o 3) + `PRESIONA START PARA DESCENDER DE NUEVO` + ).setVisible(true).setPosition(400, 320); - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); + // --- ⬇️ AQUÍ ESTÁ EL ARREGLO ⬇️ --- - oscillator.frequency.value = frequency; - oscillator.type = 'square'; + // 1. Define un ancho máximo. Tu juego es de 800px, + // así que 600px o 700px debería funcionar bien. + texts.subtitle.setWordWrapWidth(600); - gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + // 2. Asegúrate de que las líneas "partidas" también se centren. + texts.subtitle.setAlign('center'); - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + duration); + // --- ⬆️ FIN DEL ARREGLO ⬆️ --- + + texts.subtitle.setVisible(true).setPosition(400, 320); + + texts.info.setVisible(false); + + play(880, 0.5, 'sine'); + setTimeout(() => play(1100, 0.5, 'sine'), 200); + setTimeout(() => play(1320, 0.8, 'sine'), 400); } + +// AUDIO + diff --git a/metadata.json b/metadata.json index 45028c26..f2d01598 100644 --- a/metadata.json +++ b/metadata.json @@ -1,4 +1,4 @@ { - "game_name": "", - "description": "" + "game_name": "Platanus Abyss", + "description": "¡Desciende a las profundidades del Abyss! Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, domina el sistema de timing para atacar, y enfrenta a lo que espera fondo del abismo. ¿Sobrevivirás y reclamarás tu fortuna?" } From d2da3bdf745bb4b8f075d3be632ac63911f8dad8 Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 01:14:23 -0300 Subject: [PATCH 02/10] v0.91 --- game.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game.js b/game.js index 55c3f467..a3209cc5 100644 --- a/game.js +++ b/game.js @@ -103,7 +103,7 @@ let mineralsInZone = 0; // Track minerals spawned in current zone (max 3) let eventNum = 0; let currentEvent = null; // DMG SET TO 333 FOR TESTING every 333 config on normal should be 1 -let player = { hp: 1, maxHp: 1, dmg: 333, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0 }; // Y: 200-400, X: 50-350 +let player = { hp: 1, maxHp: 1, dmg: 1, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0 }; // Y: 200-400, X: 50-350 let enemy = null; let ore = null; let chest = null; From 15f557fde38d8b42d94d255ab80a5619d5948a3b Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 01:53:03 -0300 Subject: [PATCH 03/10] v0.92 Balancing --- game.js | 159 ++++++++++++++++++++++++++------------------------------ 1 file changed, 73 insertions(+), 86 deletions(-) diff --git a/game.js b/game.js index a3209cc5..630d8030 100644 --- a/game.js +++ b/game.js @@ -128,7 +128,6 @@ const heartPrices = [50, 200, 500, 2000, 10000]; let shakeAmt = 0; let particles = []; let projectiles = []; -let damageNumbers = []; let floatingTexts = []; let shopSelection = 0; let bgStars = []; @@ -196,7 +195,6 @@ function roll(count, sides, bonus = 0) { function takeDmg() { player.hp -= 1; player.combo = 0; - showDamage(player.x, player.pos, 1); spawnBloodParticles(player.x, player.pos, 1); updateGameUI(); if (player.hp <= 0) { @@ -209,39 +207,15 @@ function play(freq, dur = 0.1, type = 'square') { const ctx = scene.sound.context; const osc = ctx.createOscillator(); const gain = ctx.createGain(); - const reverb = ctx.createConvolver(); - const wetGain = ctx.createGain(); - const dryGain = ctx.createGain(); - - // Create simple reverb impulse - const impulseLength = ctx.sampleRate * 2; // 2 seconds - const impulse = ctx.createBuffer(2, impulseLength, ctx.sampleRate); - for (let channel = 0; channel < 2; channel++) { - const channelData = impulse.getChannelData(channel); - for (let i = 0; i < impulseLength; i++) { - channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / impulseLength, 2) * 0.1; - } - } - reverb.buffer = impulse; - // Connect: osc -> dryGain -> destination - // osc -> reverb -> wetGain -> destination - osc.connect(dryGain); - osc.connect(reverb); - reverb.connect(wetGain); - dryGain.connect(ctx.destination); - wetGain.connect(ctx.destination); + osc.connect(gain); + gain.connect(ctx.destination); osc.frequency.value = freq; osc.type = type; - // Dry signal (direct) - dryGain.gain.setValueAtTime(0.04, ctx.currentTime); - dryGain.gain.exponentialRampToValueAtTime(0.005, ctx.currentTime + dur); - - // Wet signal (reverb) - wetGain.gain.setValueAtTime(0.02, ctx.currentTime); - wetGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur); + gain.gain.setValueAtTime(0.04, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.005, ctx.currentTime + dur); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur); @@ -449,10 +423,6 @@ function spawnBloodParticles(x, y, damage = 1) { } } -function showDamage(x, y, dmg, color = 0xff0000) { - damageNumbers.push({ x: x, y: y, dmg: dmg, life: 1.0, color: color }); -} - function showBigText(text, x, y, color = '#ffff00', size = 32, duration = 1500) { const txt = scene.add.text(x, y, text, { fontSize: size + 'px', @@ -737,7 +707,7 @@ dragonGraphics.destroy(); strokeThickness: 3 }).setOrigin(0.5); - texts.zone = this.add.text(400, 100, '', { + texts.zone = this.add.text(400, 30, '', { fontSize: '28px', fontFamily: 'Arial', color: '#00ffff', stroke: '#000', strokeThickness: 4 }).setOrigin(0.5).setVisible(false).setDepth(500); @@ -1009,25 +979,6 @@ function update(time, delta) { } } - // Update damage numbers - for (let i = damageNumbers.length - 1; i >= 0; i--) { - const dn = damageNumbers[i]; - dn.y -= 50 * delta / 1000; - dn.life -= delta / 1000 * 2; - - if (dn.life <= 0) { - damageNumbers.splice(i, 1); - } else { - graphics.fillStyle(dn.color, dn.life); - graphics.fillRect(dn.x - 2, dn.y - 2, 4, 4); - // Simple number rendering - const dmgStr = '-' + dn.dmg; - for (let j = 0; j < dmgStr.length; j++) { - graphics.fillRect(dn.x + j * 10, dn.y, 2, 10); - } - } - } - if (state === 'GAME') { drawGame(); @@ -1689,17 +1640,24 @@ function handleInput(event) { play(150, 0.2); } } else if (shopSelection === 2) { - // Buy HP - upgrade max HP (+5) and heal that amount - if (player.money >= upgradePrices.hp) { + // Buy HP - upgrade max HP (max 10) and heal that amount + if (player.money >= upgradePrices.hp && player.maxHp < 10) { player.money -= upgradePrices.hp; const gain = 1; // Fixed +1 HP player.maxHp += gain; player.hp += gain; // Heal the amount gained upgradeLevel.hp++; - upgradePrices.hp = Math.floor(upgradePrices.hp * 2.5); - showBigText(`VIDA MAXIMA +${gain}!`, 400, 200, '#ff0000', 44, 2000); + // Dynamic pricing: cheaper progression, 10th heart costs ~1500 + // Hearts: 1->2(50), 2->3(87), 3->4(138), 4->5(211), 5->6(322), 6->7(492), 7->8(750), 8->9(1137), 9->10(1718) + if (player.maxHp < 10) { + upgradePrices.hp = Math.floor(upgradePrices.hp * 1.51 + 12); + } + showBigText(`VIDA MAXIMA +${gain}! [${player.maxHp}/10]`, 400, 200, '#ff0000', 44, 2000); play(880, 0.3); updateShopText(); + } else if (player.maxHp >= 10) { + showBigText('¡YA TIENES MAXIMO HP!', 400, 200, '#ffaa00', 36, 2000); + play(150, 0.2); } else { play(150, 0.2); } @@ -2083,17 +2041,32 @@ function openChest() { if (zone >= 4) { const rand = Math.random(); if (rand < 0.5) { - // 50% - Heal (Full restore) - const healAmount = player.maxHp - player.hp; - player.hp = player.maxHp; - showBigText('🍌 PLATANO MAGICO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`VIDA TOTAL +${healAmount} HP!`, 600, 340, '#00ff00', 38, 2500); - play(880, 0.4, 'sine'); - spawnParticles(600, 300, 0xffff00, 30); - for (let i = 0; i < 15; i++) { - setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); + // 50% - Heal (Full restore) - OR +1 maxHP if already at full + if (player.hp === player.maxHp) { + // Already at max HP - grant +1 maxHP! + player.maxHp += 1; + player.hp = player.maxHp; + showBigText('🍌 PLATANO DIVINO! 🍌', 600, 280, '#ffff00', 48, 2800); + showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp}/10]`, 600, 340, '#00ff00', 38, 2500); + play(1320, 0.5, 'sine'); + spawnParticles(600, 300, 0xffff00, 50); + for (let i = 0; i < 20; i++) { + setTimeout(() => spawnParticles(600, 300, 0x00ff00, 5), i * 50); + } + shake(15); + } else { + // Not at max HP - normal heal + const healAmount = player.maxHp - player.hp; + player.hp = player.maxHp; + showBigText('🍌 PLATANO MAGICO! 🍌', 600, 280, '#ffff00', 48, 2800); + showBigText(`VIDA TOTAL +${healAmount} HP!`, 600, 340, '#00ff00', 38, 2500); + play(880, 0.4, 'sine'); + spawnParticles(600, 300, 0xffff00, 30); + for (let i = 0; i < 15; i++) { + setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); + } + shake(10); } - shake(10); } else { // 50% - +1 Damage player.dmg += 1; @@ -2113,18 +2086,33 @@ function openChest() { play(200, 0.3); spawnParticles(600, 300, 0x666666, 8); } else if (rand < 0.27) { - // 15% - Banana (heals 50% HP) - const healAmount = Math.floor(player.maxHp * 0.5); - const actualHeal = Math.min(healAmount, player.maxHp - player.hp); - player.hp = Math.min(player.maxHp, player.hp + healAmount); - showBigText('🍌 PLATANO ENCONTRADO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`VIDA RESTAURADA +${actualHeal} HP!`, 600, 340, '#00ff00', 38, 2500); - play(880, 0.4, 'sine'); - spawnParticles(600, 300, 0xffff00, 30); - for (let i = 0; i < 15; i++) { - setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); + // 15% - Banana (heals 50% HP) - OR +1 maxHP if already at full + if (player.hp === player.maxHp) { + // Already at max HP - grant +1 maxHP! + player.maxHp += 1; + player.hp = player.maxHp; + showBigText('🍌 PLATANO DIVINO! 🍌', 600, 280, '#ffff00', 48, 2800); + showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp}/10]`, 600, 340, '#00ff00', 38, 2500); + play(1320, 0.5, 'sine'); + spawnParticles(600, 300, 0xffff00, 50); + for (let i = 0; i < 20; i++) { + setTimeout(() => spawnParticles(600, 300, 0x00ff00, 5), i * 50); + } + shake(15); + } else { + // Not at max HP - normal heal + const healAmount = Math.floor(player.maxHp * 0.5); + const actualHeal = Math.min(healAmount, player.maxHp - player.hp); + player.hp = Math.min(player.maxHp, player.hp + healAmount); + showBigText('🍌 PLATANO ENCONTRADO! 🍌', 600, 280, '#ffff00', 48, 2800); + showBigText(`VIDA RESTAURADA +${actualHeal} HP!`, 600, 340, '#00ff00', 38, 2500); + play(880, 0.4, 'sine'); + spawnParticles(600, 300, 0xffff00, 30); + for (let i = 0; i < 15; i++) { + setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); + } + shake(10); } - shake(10); } else if (rand < 0.42) { // 15% - Rare Loot const money = ZONES[zone][Z.RARE_VAL]; @@ -2454,10 +2442,10 @@ function nextEvent() { // Zona 4 (EL ABISMO): Solo enemigos y cofres, sin minerales (3 eventos) if (zone === 4) { const roll = Math.random(); - if (roll < 0.867) { // 86.7% enemigos (aumentado desde 60%) + if (roll < 0.70) { // 70% enemigos spawnEnemy(); } else { - spawnChest(); // 13.3% cofres (1/3 de 40% = 13.3%) + spawnChest(); // 30% cofres (¡AUMENTADO!) } updateGameUI(); return; @@ -2465,10 +2453,10 @@ function nextEvent() { // Zonas normales (0-3): Minerales, enemigos y cofres const eventRoll = roll(1, 15); // Cambiar de 1-10 a 1-15 para ajustar probabilidades - if (eventRoll <= 4) { - spawnEnemy(); // 4/15 ≈ 26.7% - } else if (eventRoll <= 5) { - spawnChest(); // 1/15 ≈ 6.7% (1/3 de 20% original) + if (eventRoll <= 3) { + spawnEnemy(); // 3/15 = 20% + } else if (eventRoll <= 6) { + spawnChest(); // 3/15 = 20% (¡AUMENTADO!) } else { // Max 3 minerals per zone if (mineralsInZone < 3) { @@ -2640,7 +2628,6 @@ function resetGame() { particles.forEach(p => p.destroy()); particles = []; projectiles = []; - damageNumbers = []; bgStars = []; particleEmitters = []; directionChoice = null; From 041cea705c7424f19397a7d6fed30a270f2afa57 Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 07:52:58 -0300 Subject: [PATCH 04/10] balanceo v0.98 --- game.js | 544 +++++++++++++++++++++++--------------------------------- 1 file changed, 223 insertions(+), 321 deletions(-) diff --git a/game.js b/game.js index 630d8030..c29d33d7 100644 --- a/game.js +++ b/game.js @@ -1,6 +1,4 @@ // POTASIUMABYSS - Platanus Hack 25 -// A gambling roguelike mining arcade game - // ARCADE CONTROLS (Player 1 only: WASD + U/I/O/J/K/L) const ARCADE_CONTROLS = { 'P1U': ['w'], @@ -38,10 +36,10 @@ const AI = { const ZONES = { 0: ['BOSQUE', 'HIERRO', 10, 'PLATA', 50, 'RATONCITO', [2,10,25], [1,10,3], 'TROLL', 50, [2,10,5], 30, 0x2D5016, 'RAT'], 1: ['MINAS OLVIDADAS', 'ORO', 25, 'ESMERALDA', 150, 'RATATA', [2,10,40], [2,10,5], 'GOLEM', 80, [3,10,8], 75, 0x4A4A4A, 'GOLEM'], - 2: ['LAS PROFUNDIDADES', 'DIAMANTE', 50, 'RUBI', 300, 'WAREN', [2,10,60], [2,10,8], 'DEMON', 120, [4,10,5], 150, 0x2C1810, 'DEMON'], - 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'DIABLILLO', [3,10,90], [3,10,5], 'DRAGON', 180, [4,10,15], 300, 0x8B0000, 'DRAGON'], - 4: ['ABISMO', 'PIEDRA ABISMAL', 1000, 'CRISTAL-SOMBRA', 8000, 'ALMA EN PENA', [5,10,500], [5,10,20], 'TROLL ABYSS', 800, [10,15,40], 4000, 0x1A0033, 'ALMA'], - 5: ['???', null, 0, 'CORAZON-ABYSS', 30000, 'ALMA EN PENA', [8,10,60], [9,10,45], 'HEROE CORRUPTO', 2000, [15,20,80], 15000, 0x000000, 'BOSS'] + 2: ['LAS PROFUNDIDADES', 'DIAMANTE', 50, 'RUBI', 300, 'WAREN', [3,10,100], [2,10,8], 'DEMON', 200, [4,10,5], 150, 0x2C1810, 'DEMON'], + 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'DIABLILLO', [4,10,150], [3,10,5], 'DRAGON', 300, [4,10,15], 300, 0x8B0000, 'DRAGON'], + 4: ['ABISMO', 'PIEDRA ABISMAL', 1000, 'CRISTAL-SOMBRA', 8000, 'ALMA EN PENA', [5,10,550], [5,10,20], 'TROLL ABYSS', 880, [10,15,40], 4000, 0x1A0033, 'ALMA'], + 5: ['???', null, 0, 'CORAZON-ABYSS', 30000, 'ALMA EN PENA', [8,10,63], [9,10,45], 'HEROE CORRUPTO', 2100, [15,20,80], 15000, 0x000000, 'BOSS'] }; // Zone array indices (optimized access) @@ -57,39 +55,27 @@ const getMaxEvents = z => { // Projectile speed multiplier per zone (increases difficulty) const getProjSpeed = z => 1.0 + (z * 0.1); // Zone 0: 1.0x, Zone 1: 1.1x, Zone 2: 1.2x, Zone 3: 1.3x, Zone 4: 1.4x, Zone 5: 1.5x -// MINERAL COLORS - simple color mapping for optimized drawing -const MINERAL_COLORS = { - 'HIERRO': 0x888888, - 'ORO': 0xFFD700, - 'PIEDRA INFERNAL': 0xFF4500, - 'DIAMANTE': 0x87CEEB, - 'PIEDRA ABISMAL': 0x4B0082 +// MINERAL DATA - [mainColor, gemColor] +const MINERAL_DATA = { + 'HIERRO': [0x888888, 0xC0C0C0], + 'ORO': [0xFFD700, 0xFFFF00], + 'PIEDRA INFERNAL': [0xFF4500, 0xFF6347], + 'DIAMANTE': [0x87CEEB, 0xADD8E6], + 'PIEDRA ABISMAL': [0x4B0082, 0x9370DB] }; // Simple unified mineral drawing function -function drawMineral(ox, oy, color, g = graphics) { - // Main rock shape (rectangle) - g.fillStyle(color, 1); +function drawMineral(ox, oy, mineralName, g = graphics) { + const colors = MINERAL_DATA[mineralName] || MINERAL_DATA['HIERRO']; + const mainColor = colors[0]; + const gemColor = colors[1]; + + g.fillStyle(mainColor, 1); g.fillRect(ox - 35, oy - 42, 70, 84); - // Add some darker shading g.fillStyle(0x000000, 0.3); g.fillRect(ox - 35, oy + 20, 70, 22); - // Add 2-3 mineral deposits with proper colors - let gemColor; - if (color === 0x87CEEB) { // DIAMANTE - gemColor = 0xADD8E6; // Light blue - } else if (color === 0xFF4500) { // PIEDRA INFERNAL - gemColor = 0xFF6347; // Bright red-orange - } else if (color === 0x4B0082) { // PIEDRA ABISMAL - gemColor = 0x9370DB; // Medium purple - } else if (color === 0xFFD700) { // ORO - gemColor = 0xFFFF00; // Bright yellow - } else { // HIERRO - gemColor = 0xC0C0C0; // Silver - } - g.fillStyle(gemColor, 0.9); g.fillCircle(ox - 15, oy - 20, 8); g.fillRect(ox + 10, oy - 10, 12, 12); @@ -102,12 +88,10 @@ let zone = 0; let mineralsInZone = 0; // Track minerals spawned in current zone (max 3) let eventNum = 0; let currentEvent = null; -// DMG SET TO 333 FOR TESTING every 333 config on normal should be 1 -let player = { hp: 1, maxHp: 1, dmg: 1, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0 }; // Y: 200-400, X: 50-350 +let player = { hp: 1, maxHp: 1, dmg: 1, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0, timingBonus: 0, miningBonus: 0 }; let enemy = null; let ore = null; let chest = null; -let escapeCount = 0; let attackTimer = 0; // Input state for smooth movement @@ -122,9 +106,8 @@ const INPUT_DEBOUNCE_MS = 150; // Minimum time between inputs let canAttack = true; let graphics, scene, texts = {}; let runMoney = 0; -let upgradePrices = { hp: 50, dmg: 100, speed: 75, timing: 200 }; -let upgradeLevel = { hp: 0, dmg: 0, speed: 0, timing: 0 }; -const heartPrices = [50, 200, 500, 2000, 10000]; +let upgradePrices = { hp: 50, dmg: 80, speed: 60, timing: 150, mining: 120 }; +let upgradeLevel = { timing: 0 }; let shakeAmt = 0; let particles = []; let projectiles = []; @@ -133,8 +116,8 @@ let shopSelection = 0; let bgStars = []; let particleEmitters = []; let playerSprite = null; -let directionChoice = null; // null, 'forward', 'back' let lastEventWasChest = false; // Track if last event was chest (don't increment counter) +let lastEventType = null; // Track last event type to reduce repetition let directionArrows = []; // Array to hold arrow sprites let enemySprites = []; // Array to hold enemy sprite references let timingSlider = { position: 0, direction: 1, speed: 2 }; // Timing slider for pickaxe @@ -152,7 +135,8 @@ function getZoneSpeedMultiplier(z) { function updateTimingZone(randomize = true) { const baseZone = 0.1; // 10% base zone const upgradeBonus = upgradeLevel.timing * 0.05; // 5% extra per upgrade level - const totalZone = baseZone + upgradeBonus; + const hawkEyeBonus = player.timingBonus; // Ojo de Halcón bonus + const totalZone = baseZone + upgradeBonus + hawkEyeBonus; // If zone is too large (>0.8), center it if (totalZone >= 0.8) { @@ -505,37 +489,6 @@ function createBgStars() { } } -function createExplosion(x, y, color, count = 20) { - // Create particle emitter for explosion - const emitter = scene.add.particles(x, y, null, { - speed: { min: 100, max: 300 }, - angle: { min: 0, max: 360 }, - scale: { start: 1, end: 0 }, - alpha: { start: 1, end: 0 }, - lifespan: 800, - gravityY: 200, - quantity: count, - blendMode: 'ADD', - emitting: false - }); - - // Custom renderer for colored particles - emitter.addEmitZone({ - type: 'random', - source: new Phaser.Geom.Circle(0, 0, 10) - }); - - emitter.explode(count); - - particleEmitters.push(emitter); - - // Clean up after particles die - scene.time.delayedCall(1000, () => { - emitter.destroy(); - const idx = particleEmitters.indexOf(emitter); - if (idx > -1) particleEmitters.splice(idx, 1); - }); -} // MAIN GAME LOOP function create() { @@ -618,43 +571,16 @@ const darkRed = 0x8B0000; // Cuerpo y Alas const crimson = 0xDC143C; // Cabeza const gold = 0xFFD700; // Ojo y Espinas/Detalles -// -- Cabeza (Triángulo) -- -dragonGraphics.fillStyle(crimson); -// Ajusta las coordenadas para que la punta del triángulo mire hacia arriba o hacia la derecha si prefieres -// Aquí la punta mira hacia la derecha -dragonGraphics.fillTriangle(14, 12, 20, 8, 20, 16); // Punta (x,y), Base inferior (x,y), Base superior (x,y) -// (x,y) = (14,12) -> Izquierda (pico) -// (x,y) = (20,8) -> Arriba derecha -// (x,y) = (20,16) -> Abajo derecha - -// -- Cuerpo (Cuadrado/Rectángulo) - Torso más delgado -- + dragonGraphics.fillStyle(crimson); + dragonGraphics.fillTriangle(14, 12, 20, 8, 20, 16); + dragonGraphics.fillStyle(darkRed); + dragonGraphics.fillRect(20, 10, 12, 8); dragonGraphics.fillStyle(darkRed); -dragonGraphics.fillRect(20, 10, 12, 8); // Más delgado verticalmente (altura reducida de 12 a 8) - -// -- Alas (Dos Triángulos Grandes Superpuestos con Offset) - Ajustadas al nuevo torso -- -dragonGraphics.fillStyle(darkRed); -// Ala 1 (detrás, ajustada verticalmente) -dragonGraphics.fillTriangle(20, 8, 30, 4, 30, 16); // Alineada con el torso más delgado -// (x,y) = (20,8) -> Punto más cercano al cuerpo (subido) -// (x,y) = (30,4) -> Punta superior del ala (subida) -// (x,y) = (30,16) -> Punta inferior del ala (ajustada) - -// Ala 2 (delante, ligeramente desplazada) -dragonGraphics.fillTriangle(22, 9, 32, 5, 32, 17); // Ajustada al nuevo centro vertical -// (x,y) = (22,9) -> Punto más cercano al cuerpo (ajustado) -// (x,y) = (32,5) -> Punta superior del ala (ajustada) -// (x,y) = (32,17) -> Punta inferior del ala (ajustada) - -// -- Cola (Triángulo al final del torso) - Centrada con el nuevo torso -- -dragonGraphics.fillStyle(darkRed); -dragonGraphics.fillTriangle(32, 14, 38, 11, 38, 17); // Centrada verticalmente -// (x,y) = (32,14) -> Punto base en el cuerpo (centrado) -// (x,y) = (38,11) -> Punta superior de la cola (ajustada) -// (x,y) = (38,17) -> Punta inferior de la cola (ajustada) - -// -- Ojo (Pequeño cuadrado en la cabeza) -- +dragonGraphics.fillTriangle(20, 8, 30, 4, 30, 16); +dragonGraphics.fillTriangle(22, 9, 32, 5, 32, 17); +dragonGraphics.fillTriangle(32, 14, 38, 11, 38, 17); dragonGraphics.fillStyle(gold); -dragonGraphics.fillRect(16, 11, 2, 2); // Pequeño cuadrado para el ojo +dragonGraphics.fillRect(16, 11, 2, 2); dragonGraphics.generateTexture('dragon', 64, 32); // Ajustado a un tamaño más ancho para el dragón dragonGraphics.destroy(); @@ -684,16 +610,16 @@ dragonGraphics.destroy(); texts.title = this.add.text(400, 130, 'PLATANUS ABYSS', { fontSize: '72px', fontFamily: 'Impact, Arial Black, Arial', - color: '#4b2457ff', + color: '#8b4789', stroke: '#000', strokeThickness: 12, shadow: { offsetX: 4, offsetY: 4, color: '#330000', blur: 10, fill: true } }).setOrigin(0.5); - texts.subtitle = this.add.text(400, 230, 'PRESIONA START', { + texts.subtitle = this.add.text(400, 450, 'PRESIONA START', { fontSize: '32px', fontFamily: 'Impact, Arial Black, Arial', - color: '#993535ff', + color: '#d4af37', stroke: '#000', strokeThickness: 6, shadow: { offsetX: 2, offsetY: 2, color: '#4b1818ff', blur: 6, fill: true } @@ -702,38 +628,38 @@ dragonGraphics.destroy(); texts.info = this.add.text(400, 520, 'DESCIENDE A LA OSCURIDAD • ENFRENTA TU DESTINO • RECLAMA TU FORTUNA', { fontSize: '18px', fontFamily: 'Arial', - color: '#492020ff', + color: '#9370db', stroke: '#000', strokeThickness: 3 }).setOrigin(0.5); texts.zone = this.add.text(400, 30, '', { - fontSize: '28px', fontFamily: 'Arial', color: '#00ffff', + fontSize: '28px', fontFamily: 'Arial', color: '#9370db', stroke: '#000', strokeThickness: 4 }).setOrigin(0.5).setVisible(false).setDepth(500); texts.hp = this.add.text(50, 80, '', { - fontSize: '24px', fontFamily: 'Arial', color: '#ff0000', + fontSize: '24px', fontFamily: 'Arial', color: '#ff4444', stroke: '#000', strokeThickness: 3 }).setVisible(false).setDepth(500); texts.money = this.add.text(750, 80, '', { - fontSize: '24px', fontFamily: 'Arial', color: '#ffff00', + fontSize: '24px', fontFamily: 'Arial', color: '#ffd700', stroke: '#000', strokeThickness: 3 }).setOrigin(1, 0).setVisible(false).setDepth(500); texts.treasures = this.add.text(750, 110, '', { - fontSize: '20px', fontFamily: 'Arial', color: '#00ffff', + fontSize: '20px', fontFamily: 'Arial', color: '#00ddff', stroke: '#000', strokeThickness: 3 }).setOrigin(1, 0).setVisible(false).setDepth(500); texts.combo = this.add.text(400, 530, '', { - fontSize: '24px', fontFamily: 'Courier New', color: '#FFD700', + fontSize: '24px', fontFamily: 'Courier New', color: '#ffaa00', stroke: '#000', strokeThickness: 3 }).setOrigin(0.5, 0).setVisible(false).setDepth(500); texts.event = this.add.text(750, 145, '', { - fontSize: '24px', fontFamily: 'Arial', color: '#ffffff', + fontSize: '24px', fontFamily: 'Arial', color: '#cccccc', stroke: '#000', strokeThickness: 3 }).setOrigin(1, 0).setVisible(false).setDepth(500); @@ -1151,7 +1077,8 @@ function update(time, delta) { const attackMinY = enemy.attacking === 'upper' ? 200 : 300; const attackMaxY = enemy.attacking === 'upper' ? 300 : 400; - if (player.pos >= attackMinY && player.pos <= attackMaxY) { + if (player.x >= 0 && player.x <= 400 && + player.pos >= attackMinY && player.pos <= attackMaxY) { takeDmg(); if (state === 'GAMEOVER') return; // Salir inmediatamente si murió play(150, 0.4); @@ -1163,15 +1090,18 @@ function update(time, delta) { // GOLEM: Triple line cascade attack let hit = false; // Check first line - if (player.pos >= enemy.attackLineY && player.pos <= enemy.attackLineY + 50) { + if (player.x >= 0 && player.x <= 400 && + player.pos >= enemy.attackLineY && player.pos <= enemy.attackLineY + 50) { hit = true; } // Check second line - if (enemy.attackLineY2 && player.pos >= enemy.attackLineY2 && player.pos <= enemy.attackLineY2 + 50) { + if (player.x >= 0 && player.x <= 400 && enemy.attackLineY2 && + player.pos >= enemy.attackLineY2 && player.pos <= enemy.attackLineY2 + 50) { hit = true; } // Check third line - if (enemy.attackLineY3 && player.pos >= enemy.attackLineY3 && player.pos <= enemy.attackLineY3 + 50) { + if (player.x >= 0 && player.x <= 400 && enemy.attackLineY3 && + player.pos >= enemy.attackLineY3 && player.pos <= enemy.attackLineY3 + 50) { hit = true; } if (hit) { @@ -1346,15 +1276,12 @@ function drawOre() { const ox = 600; const oy = 300; const dmgPct = ore.hp / ore.maxHp; - - const color = MINERAL_COLORS[ZONES[zone][Z.COMMON]] || 0x888888; const alpha = 0.3 + (dmgPct * 0.7); const tempGraphics = scene.add.graphics(); tempGraphics.setAlpha(alpha); - // Draw the simplified mineral - drawMineral(ox, oy, color, tempGraphics); + drawMineral(ox, oy, ZONES[zone][Z.COMMON], tempGraphics); scene.time.delayedCall(100, () => tempGraphics.destroy()); @@ -1599,7 +1526,7 @@ function handleInput(event) { play(330, 0.05); updateShopText(); } else if (key === 'P1D') { - shopSelection = Math.min(6, shopSelection + 1); + shopSelection = Math.min(7, shopSelection + 1); play(330, 0.05); updateShopText(); } else if (key === 'P1A') { @@ -1640,24 +1567,17 @@ function handleInput(event) { play(150, 0.2); } } else if (shopSelection === 2) { - // Buy HP - upgrade max HP (max 10) and heal that amount - if (player.money >= upgradePrices.hp && player.maxHp < 10) { + // Buy HP - upgrade max HP (no limit) and heal that amount + if (player.money >= upgradePrices.hp) { player.money -= upgradePrices.hp; const gain = 1; // Fixed +1 HP player.maxHp += gain; player.hp += gain; // Heal the amount gained - upgradeLevel.hp++; - // Dynamic pricing: cheaper progression, 10th heart costs ~1500 - // Hearts: 1->2(50), 2->3(87), 3->4(138), 4->5(211), 5->6(322), 6->7(492), 7->8(750), 8->9(1137), 9->10(1718) - if (player.maxHp < 10) { - upgradePrices.hp = Math.floor(upgradePrices.hp * 1.51 + 12); - } - showBigText(`VIDA MAXIMA +${gain}! [${player.maxHp}/10]`, 400, 200, '#ff0000', 44, 2000); + // Price based on shop purchases only (not total HP) + upgradePrices.hp = Math.floor(upgradePrices.hp * 1.51 + 12); + showBigText(`VIDA MAXIMA +${gain}! [${player.maxHp} HP]`, 400, 200, '#ff0000', 44, 2000); play(880, 0.3); updateShopText(); - } else if (player.maxHp >= 10) { - showBigText('¡YA TIENES MAXIMO HP!', 400, 200, '#ffaa00', 36, 2000); - play(150, 0.2); } else { play(150, 0.2); } @@ -1665,10 +1585,9 @@ function handleInput(event) { // Buy DMG if (player.money >= upgradePrices.dmg) { player.money -= upgradePrices.dmg; - const gain = roll(3, 6); // Aumentado de 1-3 a 3-6 + const gain = roll(4, 8); // Aumentado de 3-6 a 4-8 player.dmg += gain; - upgradeLevel.dmg++; - upgradePrices.dmg = Math.floor(upgradePrices.dmg * 2.5); + upgradePrices.dmg = Math.floor(upgradePrices.dmg * 2.0); showBigText(`DANO +${gain}!`, 400, 200, '#ff8800', 44, 2000); play(1000, 0.3); updateShopText(); @@ -1679,10 +1598,9 @@ function handleInput(event) { // Buy SPEED (Movement + Attack Speed) if (player.money >= upgradePrices.speed) { player.money -= upgradePrices.speed; - player.moveSpeed = Math.floor(player.moveSpeed * 1.10); // +10% movement speed - player.cooldown = Math.max(0.1, player.cooldown - 0.1); // -0.1s cooldown (min 0.1s) - upgradeLevel.speed++; - upgradePrices.speed = Math.floor(upgradePrices.speed * 2.5); + player.moveSpeed = Math.floor(player.moveSpeed * 1.12); // +12% movement speed (aumentado de 10%) + player.cooldown = Math.max(0.1, player.cooldown - 0.12); // -0.12s cooldown (aumentado de 0.1s) + upgradePrices.speed = Math.floor(upgradePrices.speed * 2.0); showBigText('MAS VELOCIDAD!', 400, 200, '#00ffff', 44, 2000); play(1200, 0.3); updateShopText(); @@ -1694,7 +1612,7 @@ function handleInput(event) { if (player.money >= upgradePrices.timing) { player.money -= upgradePrices.timing; upgradeLevel.timing++; - upgradePrices.timing = Math.floor(upgradePrices.timing * 2.5); + upgradePrices.timing = Math.floor(upgradePrices.timing * 2.0); updateTimingZone(false); // Apply the timing upgrade without randomizing position showBigText('PRECISION MEJORADA!', 400, 200, '#ffff00', 44, 2000); play(1300, 0.3); @@ -1703,6 +1621,18 @@ function handleInput(event) { play(150, 0.2); } } else if (shopSelection === 6) { + // Buy PICO SUERTUDO (Mining Bonus) + if (player.money >= upgradePrices.mining) { + player.money -= upgradePrices.mining; + player.miningBonus += 0.6; // +60% more minerals (aumentado de 50%) + upgradePrices.mining = Math.floor(upgradePrices.mining * 1.9); + showBigText('PICO SUERTUDO! +60% MINERALES!', 400, 200, '#00ffaa', 44, 2000); + play(1100, 0.4, 'triangle'); + updateShopText(); + } else { + play(150, 0.2); + } + } else if (shopSelection === 7) { // Return to mines - advance to next room (don't repeat cleared room) state = 'GAME'; currentEvent = null; @@ -1712,6 +1642,7 @@ function handleInput(event) { if (!lastEventWasChest) { eventNum++; } + lastEventWasChest = false; // Reset flag after using it // Check if zone is complete if (eventNum >= getMaxEvents(zone)) { @@ -1719,6 +1650,7 @@ function handleInput(event) { runMoney += ZONES[zone][Z.CHEST] * 5; zone++; mineralsInZone = 0; // Reset mineral counter for new zone + lastEventType = null; // Reset event tracking for new zone const newPhase = Math.floor(zone / 3); eventNum = 0; @@ -1869,62 +1801,26 @@ function mineOre() { spawnParticles(600, 300, ZONES[zone][Z.COLOR], 8); showBigText(`-${dmg} !`, 600, 200, '#ffaa00', 36); - // Always drop gold when mining (20% of zone common value per hit) - const miningReward = Math.floor(ZONES[zone][Z.COMMON_VAL] * 0.2); + // Always drop gold when mining (20% of zone common value per hit) + mining bonus + const baseMiningReward = Math.floor(ZONES[zone][Z.COMMON_VAL] * 0.2); + const miningReward = Math.floor(baseMiningReward * (1 + player.miningBonus)); player.treasures += miningReward; runMoney += miningReward; showBigText(`Minando ${ZONES[zone][Z.COMMON]} +${miningReward}`, 600, 250, '#00ffff', 28); - play(800, 0.2, 'sine'); // Mining sound - spawnParticles(600, 300, 0x00ffff, 12); // Cyan particles for treasures - updateGameUI(); // Update display immediately + play(800, 0.2, 'sine'); + spawnParticles(600, 300, 0x00ffff, 12); + updateGameUI(); if (ore.hp <= 0) { ore.hp = 0; - // Roll for loot - let totalMoney = 0; - let commonCount = 0; - let rareCount = 0; - let foundRare = false; - const rolls = 3 - ore.hits; + // Recompensa final fija al romper el mineral (aplica mining bonus) + const baseValue = ZONES[zone][Z.COMMON_VAL] * 3; + const finalReward = Math.floor(baseValue * (1 + player.miningBonus)); - for (let i = 0; i <= rolls; i++) { - const lootRoll = roll(1, 10); - if (lootRoll <= 4) { - // Mineral común - const commonVal = ZONES[zone][Z.COMMON_VAL]; - player.treasures += commonVal; - runMoney += commonVal; - totalMoney += commonVal; - commonCount++; - } else if (lootRoll === 10) { - // Mineral raro - const rareVal = ZONES[zone][Z.RARE_VAL]; - player.treasures += rareVal; - runMoney += rareVal; - totalMoney += rareVal; - rareCount++; - foundRare = true; - play(1200, 0.3, 'sine'); - } - // 5-9 = Piedra (nada) - } - - // Mostrar recompensas - let yOffset = 280; - if (commonCount > 0) { - showBigText(`${ZONES[zone][Z.COMMON]} x${commonCount}`, 600, yOffset, '#cccccc', 32); - yOffset += 40; - } - if (rareCount > 0) { - showBigText(`${ZONES[zone][Z.RARE]} x${rareCount}!`, 600, yOffset, '#ff00ff', 48); - yOffset += 50; - } - if (totalMoney > 0) { - showBigText(`+💎${totalMoney}`, 600, yOffset, '#00ffff', 44); - } else { - showBigText('Piedra...', 600, 300, '#666666', 36); - } + player.treasures += finalReward; + runMoney += finalReward; + showBigText(`${ZONES[zone][Z.COMMON]} +💎${finalReward}`, 600, 280, '#00ffff', 44); play(440, 0.4); spawnParticles(600, 300, ZONES[zone][Z.COLOR], 40); shake(15); @@ -2040,19 +1936,17 @@ function openChest() { // Abyss chests (zone 4+): Only heal or damage (no gold - no shop available) if (zone >= 4) { const rand = Math.random(); - if (rand < 0.5) { - // 50% - Heal (Full restore) - OR +1 maxHP if already at full + if (rand < 0.4) { + // 40% - Heal (Full restore) - OR +1 maxHP if already at full if (player.hp === player.maxHp) { // Already at max HP - grant +1 maxHP! player.maxHp += 1; player.hp = player.maxHp; showBigText('🍌 PLATANO DIVINO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp}/10]`, 600, 340, '#00ff00', 38, 2500); + showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp} HP]`, 600, 340, '#00ff00', 38, 2500); play(1320, 0.5, 'sine'); - spawnParticles(600, 300, 0xffff00, 50); - for (let i = 0; i < 20; i++) { - setTimeout(() => spawnParticles(600, 300, 0x00ff00, 5), i * 50); - } + spawnParticles(600, 300, 0xffff00, 60); + spawnParticles(600, 300, 0x00ff00, 25); shake(15); } else { // Not at max HP - normal heal @@ -2061,43 +1955,46 @@ function openChest() { showBigText('🍌 PLATANO MAGICO! 🍌', 600, 280, '#ffff00', 48, 2800); showBigText(`VIDA TOTAL +${healAmount} HP!`, 600, 340, '#00ff00', 38, 2500); play(880, 0.4, 'sine'); - spawnParticles(600, 300, 0xffff00, 30); - for (let i = 0; i < 15; i++) { - setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); - } + spawnParticles(600, 300, 0xffff00, 40); + spawnParticles(600, 300, 0x00ff00, 15); shake(10); } - } else { - // 50% - +1 Damage + } else if (rand < 0.8) { + // 40% - +1 Damage player.dmg += 1; showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 600, 280, '#ff4400', 48); play(1200, 0.6, 'sawtooth'); - for (let i = 0; i < 30; i++) { - setTimeout(() => spawnParticles(600, 300, 0xff4400, 5), i * 30); - } + spawnParticles(600, 300, 0xff4400, 40); shake(18); + } else { + // 20% - Ojo de Halcón (Timing Zone +10%) + player.timingBonus += 0.1; + updateTimingZone(false); + showBigText('🦅 OJO DE HALCON! 🦅', 600, 280, '#ffaa00', 48); + showBigText('ZONA VERDE +10% MAS GRANDE!', 600, 340, '#00ff00', 36); + play(1400, 0.5, 'sine'); + spawnParticles(600, 300, 0xffaa00, 35); + shake(12); } } else { // Normal zones (0-3): Full loot table with treasures const rand = Math.random(); - if (rand < 0.12) { - // 12% - Empty + if (rand < 0.05) { + // 5% - Empty (reduced from 12%) showBigText('COFRE VACIO!', 600, 300, '#666666', 40); play(200, 0.3); spawnParticles(600, 300, 0x666666, 8); - } else if (rand < 0.27) { + } else if (rand < 0.20) { // 15% - Banana (heals 50% HP) - OR +1 maxHP if already at full if (player.hp === player.maxHp) { // Already at max HP - grant +1 maxHP! player.maxHp += 1; player.hp = player.maxHp; showBigText('🍌 PLATANO DIVINO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp}/10]`, 600, 340, '#00ff00', 38, 2500); + showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp} HP]`, 600, 340, '#00ff00', 38, 2500); play(1320, 0.5, 'sine'); - spawnParticles(600, 300, 0xffff00, 50); - for (let i = 0; i < 20; i++) { - setTimeout(() => spawnParticles(600, 300, 0x00ff00, 5), i * 50); - } + spawnParticles(600, 300, 0xffff00, 60); + spawnParticles(600, 300, 0x00ff00, 25); shake(15); } else { // Not at max HP - normal heal @@ -2107,52 +2004,43 @@ function openChest() { showBigText('🍌 PLATANO ENCONTRADO! 🍌', 600, 280, '#ffff00', 48, 2800); showBigText(`VIDA RESTAURADA +${actualHeal} HP!`, 600, 340, '#00ff00', 38, 2500); play(880, 0.4, 'sine'); - spawnParticles(600, 300, 0xffff00, 30); - for (let i = 0; i < 15; i++) { - setTimeout(() => spawnParticles(600, 300, 0x00ff00, 3), i * 60); - } + spawnParticles(600, 300, 0xffff00, 40); + spawnParticles(600, 300, 0x00ff00, 15); shake(10); } - } else if (rand < 0.42) { - // 15% - Rare Loot - const money = ZONES[zone][Z.RARE_VAL]; - player.treasures += money; - runMoney += money; - showBigText(`+💎${money} ARTEFACTOS RUNICOS!`, 600, 340, '#ff66ff', 38); - play(1000, 0.5, 'sawtooth'); - spawnParticles(600, 300, 0xff00ff, 40); - for (let i = 0; i < 15; i++) { - setTimeout(() => spawnParticles(600, 300, 0xffffff, 2), i * 100); - } - shake(12); - } else if (rand < 0.54) { - // 12% - +1 Damage + } else if (rand < 0.35) { + // 15% - +1 Damage (increased from 10%) player.dmg += 1; showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 600, 280, '#ff4400', 48); play(1200, 0.6, 'sawtooth'); - for (let i = 0; i < 30; i++) { - setTimeout(() => spawnParticles(600, 300, 0xff4400, 5), i * 30); - } + spawnParticles(600, 300, 0xff4400, 40); shake(18); - } else if (rand < 0.77) { - // 23% - Normal treasure + } else if (rand < 0.50) { + // 15% - Ojo de Halcón (Timing Zone +10%) - increased from 10% + player.timingBonus += 0.1; + updateTimingZone(false); + showBigText('🦅 OJO DE HALCON! 🦅', 600, 280, '#ffaa00', 48); + showBigText('ZONA VERDE +10% MAS GRANDE!', 600, 340, '#00ff00', 36); + play(1400, 0.5, 'sine'); + spawnParticles(600, 300, 0xffaa00, 35); + shake(12); + } else if (zone === 0 && rand < 0.60) { + // 10% - Pico Bendito (ONLY in BOSQUE - zone 0) - reduced from 15% + player.miningBonus += 1.0; + showBigText('⛏️ PICO BENDITO! ⛏️', 600, 280, '#00ffff', 48); + showBigText('MINERALES DAN X2 RECURSOS!', 600, 340, '#ffff00', 36); + play(1100, 0.5, 'triangle'); + spawnParticles(600, 300, 0x00ffff, 35); + shake(15); + } else { + // 50% (zones 1-3) OR 40% (zone 0) - Treasure (consolidated from rare+normal+small) const money = ZONES[zone][Z.CHEST]; player.treasures += money; runMoney += money; - showBigText(`+💎${money} GEMAS Y JOYAS!`, 600, 340, '#ffff88', 36); + showBigText(`+💎${money} TESORO!`, 600, 340, '#ffff88', 36); play(800, 0.4, 'sine'); - spawnParticles(600, 300, 0xffff00, 25); - for (let i = 0; i < 12; i++) { - setTimeout(() => spawnParticles(600, 300, 0xffdd00, 3), i * 80); - } - } else { - // 23% - Small treasure - const money = Math.floor(ZONES[zone][Z.CHEST] * 0.4); - player.treasures += money; - runMoney += money; - showBigText(`+💎${money} ROPA VIEJA!`, 600, 340, '#aaaaaa', 32); - play(600, 0.3); - spawnParticles(600, 300, 0xcccccc, 15); + spawnParticles(600, 300, 0xffff00, 35); + spawnParticles(600, 300, 0xffdd00, 15); } } @@ -2222,7 +2110,6 @@ function completeEvent() { timingSlider.direction = 1; texts.event.setVisible(false); - escapeCount = 0; // Show direction choice arrows instead of auto-advancing showDirectionChoice(wasChest); @@ -2266,7 +2153,6 @@ function showPickaxeSwing(x, y) { } function showDirectionChoice(wasChest = false) { - directionChoice = null; lastEventWasChest = wasChest; // Clear any existing arrows and block texts @@ -2390,8 +2276,6 @@ function showDirectionChoice(wasChest = false) { } function selectDirection(choice) { - directionChoice = choice; - // Hide arrows and hint directionArrows.forEach(arrow => arrow.destroy()); directionArrows = []; @@ -2407,6 +2291,7 @@ function selectDirection(choice) { runMoney += ZONES[zone][Z.CHEST] * 5; zone++; mineralsInZone = 0; // Reset mineral counter for new zone + lastEventType = null; // Reset event tracking for new zone const newPhase = Math.floor(zone / 3); eventNum = 0; @@ -2432,7 +2317,7 @@ function nextEvent() { // Randomize timing zone position for new event updateTimingZone(); - // Zona 8 (JEFE FINAL): Solo el jefe, un evento + // Zona 5 (JEFE FINAL): Solo el jefe, un evento if (zone === 5) { spawnEnemy(); // Solo el HEROE CORRUPTO updateGameUI(); @@ -2441,26 +2326,38 @@ function nextEvent() { // Zona 4 (EL ABISMO): Solo enemigos y cofres, sin minerales (3 eventos) if (zone === 4) { - const roll = Math.random(); - if (roll < 0.70) { // 70% enemigos + const eventRoll = Math.random(); + if (eventRoll < 0.70) { // 70% enemigos spawnEnemy(); } else { - spawnChest(); // 30% cofres (¡AUMENTADO!) + spawnChest(); // 30% cofres } updateGameUI(); return; } // Zonas normales (0-3): Minerales, enemigos y cofres - const eventRoll = roll(1, 15); // Cambiar de 1-10 a 1-15 para ajustar probabilidades - if (eventRoll <= 3) { - spawnEnemy(); // 3/15 = 20% - } else if (eventRoll <= 6) { - spawnChest(); // 3/15 = 20% (¡AUMENTADO!) + let eventRoll = roll(1, 20); // 1-20 para probabilidades más finas + + // BOSQUE evento 2: Boost chest chance to 80% + if (zone === 0 && eventNum === 1 && eventRoll > 5) { + eventRoll = roll(5, 9); // Force chest range + } + + // Anti-repetición: -30% chance si es mismo tipo que anterior + const lastType = lastEventType; + if (lastType === 'ENEMY' && eventRoll <= 4) eventRoll = 5; // Evita enemigo + if (lastType === 'CHEST' && eventRoll >= 5 && eventRoll <= 9) eventRoll = 10; // Evita cofre + if (lastType === 'ORE' && eventRoll >= 10) eventRoll = roll(1, 9); // Evita mineral + + if (eventRoll <= 4) { + spawnEnemy(); // 4/20 = 20% + } else if (eventRoll <= 9) { + spawnChest(); // 5/20 = 25% (+5%) } else { // Max 3 minerals per zone if (mineralsInZone < 3) { - spawnOre(); // 10/15 ≈ 66.7% + spawnOre(); // 11/20 = 55% mineralsInZone++; } else { spawnEnemy(); // Spawn enemy instead if mineral limit reached @@ -2472,6 +2369,7 @@ function nextEvent() { function spawnOre() { currentEvent = 'ORE'; + lastEventType = 'ORE'; ore = { hp: 30, maxHp: 30, hits: 0, scale: 0 }; play(330, 0.2); @@ -2492,6 +2390,7 @@ function spawnOre() { function spawnEnemy() { currentEvent = 'ENEMY'; + lastEventType = 'ENEMY'; // Zona ??? (5) solo tiene enemigos grandes if (zone === 5 || Math.random() >= 0.65) { @@ -2532,6 +2431,7 @@ function spawnEnemy() { function spawnChest() { currentEvent = 'CHEST'; + lastEventType = 'CHEST'; chest = { opened: false }; play(550, 0.2); @@ -2546,7 +2446,7 @@ function startGame() { eventNum = 0; runMoney = 0; currentEvent = null; - escapeCount = 0; + lastEventType = null; hideMenu(); showGameUI(); @@ -2590,7 +2490,7 @@ function resetGame() { // Complete deep reset - reinitialize ALL game state // Player and stats - player = { hp: 1, maxHp: 1, dmg: 1, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0 }; // Reset to center position + player = { hp: 1, maxHp: 1, dmg: 1, cooldown: 0.6, moveSpeed: 250, money: 0, treasures: 0, pos: 300, x: 150, combo: 0, timingBonus: 0, miningBonus: 0 }; // Reset to center position // Reset input state inputUp = false; @@ -2606,19 +2506,19 @@ function resetGame() { eventNum = 0; currentEvent = null; lastEventWasChest = false; + lastEventType = null; // Combat state enemy = null; ore = null; chest = null; - escapeCount = 0; attackTimer = 0; canAttack = true; // Money and upgrades runMoney = 0; - upgradePrices = { hp: 50, dmg: 100, speed: 75, timing: 200 }; - upgradeLevel = { hp: 0, dmg: 0, speed: 0, timing: 0 }; + upgradePrices = { hp: 50, dmg: 100, speed: 75, timing: 200, mining: 150 }; + upgradeLevel = { timing: 0 }; updateTimingZone(); // Reset timing zone to base values shopSelection = 0; @@ -2630,7 +2530,6 @@ function resetGame() { projectiles = []; bgStars = []; particleEmitters = []; - directionChoice = null; directionArrows = []; timingSlider = { position: 0, direction: 1, speed: 2 }; @@ -2708,11 +2607,12 @@ function hideMenu() { function showMenu() { // Restaurar textos originales del menú - texts.title.setText('EL ABYSS').setColor('#ff4400'); - texts.subtitle.setText('PRESIONA START').setColor('#ffaa00'); + texts.title.setText('EL ABYSS').setColor('#8b4789'); + texts.title.setFontSize('72px'); + texts.subtitle.setText('PRESIONA START').setColor('#d4af37'); texts.subtitle.setAlign('center'); texts.subtitle.setWordWrapWidth(0); // Desactivar word wrap - texts.info.setText('DESCIENDE A LA OSCURIDAD • ENFRENTA TU DESTINO • RECLAMA TU FORTUNA').setColor('#806060ff'); + texts.info.setText('DESCIENDE A LA OSCURIDAD • ENFRENTA TU DESTINO • RECLAMA TU FORTUNA').setColor('#9370db');   texts.combo.setVisible(false);     // <-- Faltaba este texts.title.setVisible(true); @@ -2842,47 +2742,48 @@ function updateShopText() { const options = [ { name: '💎 Vender Tesoros', price: -1, info: `Convierte tus tesoros en monedas (${player.treasures} tesoros disponibles)`, color: '#00ffff', cat: '💰' }, { name: '🍌 Estofado de Potasio', price: 25, info: 'Rellena tus corazones', color: '#ffdd44', cat: '🍽️' }, - { name: '❤️ Entrenamiento Fisico', price: upgradePrices.hp, info: `FULL TANK +1 Corazon [${player.hp}/${player.maxHp}]`, color: '#ff5555', cat: '💪' }, + { name: '❤️ Entrenamiento Fisico', price: upgradePrices.hp, info: `FULL TANK +1 Corazon [${player.hp}/${player.maxHp} HP]`, color: '#ff5555', cat: '💪' }, { name: '⚔️ Afilar Herramientas', price: upgradePrices.dmg, info: `Pico mas letal (+DMG)(+3d6) [${(1 + player.dmg)}-${(10 + player.dmg * 2)}]`, color: '#ff8833', cat: '🔨' }, { name: '⚡ Cerveza Energizante', price: upgradePrices.speed, info: `Movimiento y ataque mas rapidos [${player.cooldown.toFixed(1)}s]`, color: '#44ddff', cat: '🍺' }, { name: '🎯 Leccion de Precision', price: upgradePrices.timing, info: `Mejora tu punteria (ZONA VERDE MAS GRANDE) (Nivel ${upgradeLevel.timing})`, color: '#ffdd44', cat: '📜' }, - { name: '⛏️ Volver a las Minas', price: 0, info: 'Regresar al abismo oscuro...', color: '#88ff88', cat: '🚪' } + { name: '⛏️ Pico Suertudo', price: upgradePrices.mining, info: `Minerales dan +50% mas recursos permanente [+${Math.floor(player.miningBonus * 100)}%]`, color: '#00ffaa', cat: '💎' }, + { name: '🚪 Volver a las Minas', price: 0, info: 'Regresar al abismo oscuro...', color: '#88ff88', cat: '�' } ]; - let y = 118; + let y = 100; options.forEach((opt, idx) => { const selected = idx === shopSelection; const canAfford = player.money >= opt.price || opt.price <= 0; const cardBg = scene.add.graphics(); - // Parchment-style card + // Parchment-style card (reduced height: 60 -> 52) if (selected) { cardBg.fillStyle(canAfford ? 0x3a2f1f : 0x2f1a1a, 0.95); - cardBg.fillRect(80, y, 640, 60); + cardBg.fillRect(80, y, 640, 52); cardBg.lineStyle(3, canAfford ? 0xffd700 : 0xff4444, 1); - cardBg.strokeRect(80, y, 640, 60); + cardBg.strokeRect(80, y, 640, 52); // Inner glow cardBg.lineStyle(1, canAfford ? 0xffee99 : 0xff8888, 0.6); - cardBg.strokeRect(83, y + 3, 634, 54); + cardBg.strokeRect(83, y + 3, 634, 46); } else { cardBg.fillStyle(0x2a1f12, 0.7); - cardBg.fillRect(80, y, 640, 60); + cardBg.fillRect(80, y, 640, 52); cardBg.lineStyle(1, 0x4a3a2a, 0.6); - cardBg.strokeRect(80, y, 640, 60); + cardBg.strokeRect(80, y, 640, 52); } cardBg.setDepth(410); texts.shop.push(cardBg); // Category icon - const catText = scene.add.text(95, y + 10, opt.cat, { - fontSize: '26px', + const catText = scene.add.text(95, y + 8, opt.cat, { + fontSize: '24px', fontFamily: 'Arial' }).setDepth(420); texts.shop.push(catText); // Item name with tavern style - const nameText = scene.add.text(135, y + 6, opt.name, { - fontSize: selected ? '22px' : '20px', + const nameText = scene.add.text(135, y + 4, opt.name, { + fontSize: selected ? '20px' : '18px', fontFamily: 'Georgia, serif', color: selected ? (canAfford ? opt.color : '#665555') : '#998877', stroke: '#0a0502', @@ -2892,8 +2793,8 @@ function updateShopText() { texts.shop.push(nameText); // Description - const descText = scene.add.text(135, y + 34, opt.info, { - fontSize: '13px', + const descText = scene.add.text(135, y + 29, opt.info, { + fontSize: '12px', fontFamily: 'Georgia, serif', color: selected ? (canAfford ? '#ccbb99' : '#ff7777') : '#776655', fontStyle: 'italic' @@ -2902,7 +2803,7 @@ function updateShopText() { // Price tag with tavern coin style if (opt.price > 0) { - const priceText = scene.add.text(700, y + 30, `${opt.price}`, { + const priceText = scene.add.text(700, y + 26, `${opt.price}`, { fontSize: selected ? '26px' : '22px', fontFamily: 'Georgia, serif', color: canAfford ? '#ffd700' : '#ff5555', @@ -2912,13 +2813,13 @@ function updateShopText() { }).setOrigin(1, 0.5).setDepth(420); texts.shop.push(priceText); - const coinIcon = scene.add.text(705, y + 30, '◉', { - fontSize: selected ? '20px' : '17px', + const coinIcon = scene.add.text(705, y + 26, '◉', { + fontSize: selected ? '18px' : '16px', color: canAfford ? '#ffee99' : '#aa4444' }).setOrigin(0, 0.5).setDepth(420); texts.shop.push(coinIcon); } else if (opt.price === -1) { - const freeText = scene.add.text(700, y + 30, player.treasures > 0 ? 'Vender!' : 'Sin tesoros', { + const freeText = scene.add.text(700, y + 26, player.treasures > 0 ? 'Vender!' : 'Sin tesoros', { fontSize: '18px', fontFamily: 'Georgia, serif', color: player.treasures > 0 ? '#00ffff' : '#666666', @@ -2939,7 +2840,7 @@ function updateShopText() { texts.shop.push(freeText); } - y += 62; + y += 56; // Reduced spacing (62 -> 56) to fit 7 options }); } @@ -2954,6 +2855,12 @@ function showGameOver() { particles.forEach(p => p.destroy()); particles = []; + // Limpiar y ocultar sprites de enemigos + enemySprites.forEach(sprite => { + if (sprite && sprite.destroy) sprite.destroy(); + }); + enemySprites = []; + // Limpiar enemigos y sus ataques if (enemy) { enemy.hp = 0; @@ -2965,16 +2872,17 @@ function showGameOver() { chest = null; hideGame(); - - texts.title.setText('Otro Deshecho en el ABYSS').setVisible(true).setPosition(400, 200); - texts.title.setColor('#880000'); +//el texto es muy grande asi que hay que subirlo mas de lo que parece + texts.title.setText('Otro Deshecho en el ABYSS').setVisible(true).setPosition(400, 70); + texts.title.setFontSize('48px'); + texts.title.setColor('#cc3333'); texts.subtitle.setText( - `Profundidad Alcanzada: ${ZONES[zone][0]}\n` + - `Score Total: $${runMoney}\n` + - `Oro Final: $${player.money}\n\n` + + `Profundidad Alcanzada: ${ZONES[zone][0]}\n\n` + + `Score Total: $${runMoney}\n\n` + + `Oro Final: $${player.money}\n\n\n` + `PRESIONA START PARA DESCENDER DE NUEVO` - ).setVisible(true).setPosition(400, 320); + ).setVisible(true).setPosition(400, 340).setColor('#ff6666'); texts.info.setVisible(false); @@ -2984,31 +2892,25 @@ function showGameOver() { function showVictory() { hideGame(); - texts.title.setText('ABYSS CONQUISTADO!').setVisible(true).setPosition(400, 200); - texts.title.setColor('#ffaa00'); + // Limpiar y ocultar sprites de enemigos + enemySprites.forEach(sprite => { + if (sprite && sprite.destroy) sprite.destroy(); + }); + enemySprites = []; + + texts.title.setText('ABYSS CONQUISTADO!').setVisible(true).setPosition(400, 65); + texts.title.setFontSize('56px'); + texts.title.setColor('#ffd700'); // Use the last valid zone (5) if zone exceeds the array const finalZone = Math.min(zone, 5); texts.subtitle.setText( - `Profundidad Alcanzada: ${ZONES[finalZone][0]}\n` + - `Score Total: $${runMoney}\n` + - `Oro Final: $${player.money}\n\n\n\n` + // <-- Prueba con 4 \n (o 3) + `Profundidad Alcanzada: ${ZONES[finalZone][0]}\n\n` + + `Score Total: $${runMoney}\n\n` + + `Oro Final: $${player.money}\n\n\n` + `PRESIONA START PARA DESCENDER DE NUEVO` - ).setVisible(true).setPosition(400, 320); - - // --- ⬇️ AQUÍ ESTÁ EL ARREGLO ⬇️ --- - - // 1. Define un ancho máximo. Tu juego es de 800px, - // así que 600px o 700px debería funcionar bien. - texts.subtitle.setWordWrapWidth(600); - - // 2. Asegúrate de que las líneas "partidas" también se centren. - texts.subtitle.setAlign('center'); - - // --- ⬆️ FIN DEL ARREGLO ⬆️ --- - - texts.subtitle.setVisible(true).setPosition(400, 320); + ).setVisible(true).setPosition(400, 280).setColor('#ffed4e'); texts.info.setVisible(false); From e0f0ea9a7f722314186b1c1d01b234bc61dd4121 Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 19:54:55 -0300 Subject: [PATCH 05/10] 1.0 fixeado y balanceado hasta el final --- game.js | 171 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 93 insertions(+), 78 deletions(-) diff --git a/game.js b/game.js index c29d33d7..beac2d3b 100644 --- a/game.js +++ b/game.js @@ -1,3 +1,4 @@ +let gameStartTime = 0; // POTASIUMABYSS - Platanus Hack 25 // ARCADE CONTROLS (Player 1 only: WASD + U/I/O/J/K/L) const ARCADE_CONTROLS = { @@ -34,12 +35,12 @@ const AI = { // GAME DATA - Zone data: [name, common, commonVal, rare, rareVal, smallE, smallHP[3], smallDmg[3], bigE, bigHP, bigDmg[3], chest, color, aiType] const ZONES = { - 0: ['BOSQUE', 'HIERRO', 10, 'PLATA', 50, 'RATONCITO', [2,10,25], [1,10,3], 'TROLL', 50, [2,10,5], 30, 0x2D5016, 'RAT'], - 1: ['MINAS OLVIDADAS', 'ORO', 25, 'ESMERALDA', 150, 'RATATA', [2,10,40], [2,10,5], 'GOLEM', 80, [3,10,8], 75, 0x4A4A4A, 'GOLEM'], - 2: ['LAS PROFUNDIDADES', 'DIAMANTE', 50, 'RUBI', 300, 'WAREN', [3,10,100], [2,10,8], 'DEMON', 200, [4,10,5], 150, 0x2C1810, 'DEMON'], - 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'DIABLILLO', [4,10,150], [3,10,5], 'DRAGON', 300, [4,10,15], 300, 0x8B0000, 'DRAGON'], - 4: ['ABISMO', 'PIEDRA ABISMAL', 1000, 'CRISTAL-SOMBRA', 8000, 'ALMA EN PENA', [5,10,550], [5,10,20], 'TROLL ABYSS', 880, [10,15,40], 4000, 0x1A0033, 'ALMA'], - 5: ['???', null, 0, 'CORAZON-ABYSS', 30000, 'ALMA EN PENA', [8,10,63], [9,10,45], 'HEROE CORRUPTO', 2100, [15,20,80], 15000, 0x000000, 'BOSS'] + 0: ['BOSQUE', 'HIERRO', 10, 'PLATA', 50, 'RATONCITO', 38, [1,10,3], 'TROLL', 75, [2,10,5], 30, 0x2D5016, 'RAT'], + 1: ['MINAS OLVIDADAS', 'ORO', 25, 'ESMERALDA', 150, 'RATATA', 78, [2,10,5], 'GOLEM', 120, [3,10,8], 75, 0x4A4A4A, 'GOLEM'], + 2: ['LAS PROFUNDIDADES', 'DIAMANTE', 50, 'RUBI', 300, 'WAREN', 400, [2,10,8], 'DEMON', 400, [4,10,5], 150, 0x2C1810, 'DEMON'], + 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'DIABLILLO', 600, [3,10,5], 'DRAGON', 700, [4,10,15], 300, 0x8B0000, 'DRAGON'], + 4: ['ABISMO', 'PIEDRA ABISMAL', 1000, 'CRISTAL-SOMBRA', 8000, 'ALMA EN PENA', 700, [5,10,20], 'TROLL ABYSS', 1000, [10,15,40], 4000, 0x1A0033, 'ALMA'], + 5: ['???', null, 0, 'CORAZON-ABYSS', 30000, 'ALMA EN PENA', 79, [9,10,45], 'HEROE CORRUPTO', 3000, [15,20,80], 15000, 0x000000, 'BOSS'] }; // Zone array indices (optimized access) @@ -106,7 +107,7 @@ const INPUT_DEBOUNCE_MS = 150; // Minimum time between inputs let canAttack = true; let graphics, scene, texts = {}; let runMoney = 0; -let upgradePrices = { hp: 50, dmg: 80, speed: 60, timing: 150, mining: 120 }; +let upgradePrices = { hp: 50, dmg: 100, speed: 33, timing: 200, mining: 99 } let upgradeLevel = { timing: 0 }; let shakeAmt = 0; let particles = []; @@ -500,12 +501,17 @@ function create() { const tempGraphics = this.add.graphics(); // Create a simple miner shape: head + body + pickaxe tempGraphics.fillStyle(0x8B4513); // Brown color - tempGraphics.fillRect(8, 8, 16, 16); // Body + tempGraphics.fillRect(12, 8, 8, 16); // Body (50% más flaco) tempGraphics.fillStyle(0xFFD700); // Gold helmet - tempGraphics.fillRect(10, 4, 12, 8); // Head/helmet + tempGraphics.beginPath(); + tempGraphics.arc(16, 8, 6, Math.PI, 2 * Math.PI, false); // Semicircle helmet + tempGraphics.closePath(); + tempGraphics.fillPath(); + tempGraphics.fillStyle(0xFFD7A0); // Skin color for face + tempGraphics.fillRect(10, 8, 12, 4); // Head/face tempGraphics.fillStyle(0x444444); // Dark gray pickaxe - tempGraphics.fillRect(24, 10, 6, 2); // Pickaxe handle - tempGraphics.fillRect(28, 6, 2, 8); // Pickaxe head + tempGraphics.fillRect(24, 17, 6, 2); // Pickaxe handle (7px más abajo) + tempGraphics.fillRect(28, 13, 2, 8); // Pickaxe head (7px más abajo) tempGraphics.generateTexture('miner', 32, 32); tempGraphics.destroy(); @@ -905,6 +911,7 @@ function update(time, delta) { } } + if (state === 'GAME') { drawGame(); @@ -921,6 +928,9 @@ function update(time, delta) { // Rata dispara MUCHO más rápido: 1.2s (zona 0) -> 0.2s (zona 4 ABISMO) enemy.shootTimer = 1.2 - (zone * 0.25); projectiles.push({ x: 600, y: targetY, dmg: 1, life: 3.0, vx: -200 * speedMult, vy: 0, type: 'normal' }); + // Segundo proyectil homing al jugador + const angle = Math.atan2(player.pos - 300, player.x - 600); + projectiles.push({ x: 600, y: 300, dmg: 1, life: 3.0, vx: Math.cos(angle) * 200 * speedMult, vy: Math.sin(angle) * 200 * speedMult, type: 'normal' }); play(800, 0.1); } else if (aiType === 'alma') { // Alma dispara MUCHO más rápido: 1.0s (zona 0) -> 0.15s (zona 4 ABISMO) @@ -938,14 +948,14 @@ function update(time, delta) { const aiType = AI[ZONES[zone][Z.AI]].bigType; if (aiType === 'troll') { - enemy.attackTimer = 2.8; + enemy.attackTimer = 1.6; // Reducido para atacar más seguido enemy.attacking = Math.random() < 0.5 ? 'upper' : 'lower'; enemy.attackWarn = 1.2; play(150, 0.4, 'sawtooth'); } else if (aiType === 'demon') { // DEMON: Zone attack + 7 projectiles aimed at player with 15° spread - enemy.attackTimer = 2.8; + enemy.attackTimer = 1.8; enemy.attacking = Math.random() < 0.5 ? 'upper' : 'lower'; enemy.attackWarn = 1.2; play(150, 0.4, 'sawtooth'); @@ -965,7 +975,7 @@ function update(time, delta) { } } else if (aiType === 'troll_abyss') { - enemy.attackTimer = 1.0; + enemy.attackTimer = 0.8; enemy.attacking = 'zone'; // Target zone on player position (forces constant movement) const zoneW = 80; // Zone width @@ -989,7 +999,8 @@ function update(time, delta) { play(150, 0.4, 'sawtooth'); } else if (aiType === 'golem') { - enemy.attackTimer = 3.5; + enemy._lineYs = null; + enemy.attackTimer = 1.8; enemy.attacking = 'line'; // TRIPLE LINE ATTACK - 3 horizontal lines cascading down enemy.attackLineY = 200 + Math.random() * 100; // First line: 200-300 @@ -1000,7 +1011,7 @@ function update(time, delta) { play(100, 0.5, 'sawtooth'); } else if (aiType === 'dragon') { - enemy.attackTimer = 3.2; + enemy.attackTimer = 3; // Ataque combinado: Zona trasera (más ancha) + Zona centro + Proyectiles con spread enemy.attacking = 'zone'; enemy.attackZoneX = 50; // Borde izquierdo @@ -1026,9 +1037,9 @@ function update(time, delta) { }, i * 50); // Delay más corto: 50ms entre proyectiles } play(300, 0.5, 'sawtooth'); - } + }// realmente este es el jefe final boss heroe no es un dragon pero quedo el nombre por legacy else if (aiType === 'dragon_boss') { - enemy.attackTimer = 1.2; // Reducido de 1.5 a 1.2 (ataca más frecuente) + enemy.attackTimer = 1.1; // Reducido de 1.5 a 1.2 (ataca más frecuente) enemy.attacking = 'zone'; // Player area: X(50-450), Y(200-400) = 400x200px // Cubrir el área izquierda/derecha que NO cubren los proyectiles (que van al centro) @@ -1077,7 +1088,7 @@ function update(time, delta) { const attackMinY = enemy.attacking === 'upper' ? 200 : 300; const attackMaxY = enemy.attacking === 'upper' ? 300 : 400; - if (player.x >= 0 && player.x <= 400 && + if (player.x >= 0 && player.x <= 520 && player.pos >= attackMinY && player.pos <= attackMaxY) { takeDmg(); if (state === 'GAMEOVER') return; // Salir inmediatamente si murió @@ -1087,22 +1098,15 @@ function update(time, delta) { } } else if (enemy.attacking === 'line') { - // GOLEM: Triple line cascade attack + // GOLEM: Triple line cascade attack (sincronizado con visual) let hit = false; - // Check first line - if (player.x >= 0 && player.x <= 400 && - player.pos >= enemy.attackLineY && player.pos <= enemy.attackLineY + 50) { - hit = true; - } - // Check second line - if (player.x >= 0 && player.x <= 400 && enemy.attackLineY2 && - player.pos >= enemy.attackLineY2 && player.pos <= enemy.attackLineY2 + 50) { - hit = true; - } - // Check third line - if (player.x >= 0 && player.x <= 400 && enemy.attackLineY3 && - player.pos >= enemy.attackLineY3 && player.pos <= enemy.attackLineY3 + 50) { - hit = true; + if (enemy._lineYs) { + for (let i = 0; i < enemy._lineYs.length; i++) { + if (player.x >= 0 && player.x <= 520 && + player.pos >= enemy._lineYs[i] && player.pos <= enemy._lineYs[i] + 50) { + hit = true; + } + } } if (hit) { takeDmg(); @@ -1149,7 +1153,6 @@ function update(time, delta) { if (enemy) { enemy.attacking = null; enemy.golemShake = false; - enemy.dragonBreath = false; // Clear zone attack variables enemy.attackZoneX = undefined; enemy.attackZoneY = undefined; @@ -1203,8 +1206,8 @@ function drawGame() { playerSprite.setVisible(true); // Pickaxe swing animation - const pickSwing = !canAttack ? Math.sin(attackTimer / player.cooldown * Math.PI) * 15 : 0; - playerSprite.setRotation(pickSwing * 0.01); // Subtle rotation during attack + const pickSwing = !canAttack ? Math.sin(attackTimer / player.cooldown * Math.PI * 4) * -15 * 0.5 : 0; // Giro invertido y más rápido + playerSprite.setRotation(pickSwing * 0.25); // Giro brutalmente exagerado } // Attack cooldown bar with glow @@ -1275,6 +1278,14 @@ function drawOre() { const ox = 600; const oy = 300; + // Escala la vida del mineral una vez según la zona (+25% por zona) + if (!ore._scaled) { + const factor = 1 + zone * 0.25; // ajusta aquí el incremento por zona + ore.maxHp = Math.max(1, Math.round(ore.maxHp * factor)); + ore.hp = Math.min(ore.maxHp, Math.max(1, Math.round(ore.hp * factor))); + ore._scaled = true; + } + const dmgPct = ore.hp / ore.maxHp; const alpha = 0.3 + (dmgPct * 0.7); @@ -1358,34 +1369,35 @@ function drawEnemy() { if (enemy.attacking === 'upper') { // TROLL: Warning for upper section graphics.fillStyle(0xff0000, pulse); - graphics.fillRect(0, 200, 400, 100); // Upper playable area - graphics.lineStyle(3, 0xff0000, 0.8); - graphics.strokeRect(0, 200, 400, 100); + graphics.fillRect(0, 200, 520, 100); // Upper playable area (ancho igual al golem) + graphics.lineStyle(3, 0xff0000, 0.8); + graphics.strokeRect(0, 200, 520, 100); } else if (enemy.attacking === 'lower') { // TROLL: Warning for lower section graphics.fillStyle(0xff0000, pulse); - graphics.fillRect(0, 300, 400, 100); // Lower playable area - graphics.lineStyle(3, 0xff0000, 0.8); - graphics.strokeRect(0, 300, 400, 100); + graphics.fillRect(0, 300, 520, 100); // Lower playable area (ancho igual al golem) + graphics.lineStyle(3, 0xff0000, 0.8); + graphics.strokeRect(0, 300, 520, 100); } else if (enemy.attacking === 'line') { // GOLEM: Triple horizontal line cascade attack graphics.fillStyle(0xff0000, pulse); graphics.lineStyle(3, 0xff0000, 0.8); - // First line - graphics.fillRect(0, enemy.attackLineY, 400, 50); - graphics.strokeRect(0, enemy.attackLineY, 400, 50); - // Second line - if (enemy.attackLineY2 && enemy.attackLineY2 <= 400) { - graphics.fillRect(0, enemy.attackLineY2, 400, 50); - graphics.strokeRect(0, enemy.attackLineY2, 400, 50); - } - // Third line - if (enemy.attackLineY3 && enemy.attackLineY3 <= 400) { - graphics.fillRect(0, enemy.attackLineY3, 400, 50); - graphics.strokeRect(0, enemy.attackLineY3, 400, 50); - } + // Draw 3 lines at random Y positions in range 200-400, with spacing + if (!enemy._lineYs) { + // Generate 3 random Y positions, sorted for visual order + const ys = []; + for (let i = 0; i < 3; i++) { + ys.push(200 + Math.random() * 200); + } + ys.sort((a, b) => a - b); + enemy._lineYs = ys; + } + enemy._lineYs.forEach(y => { + graphics.fillRect(0, y, 520, 50); + graphics.strokeRect(0, y, 520, 50); + }); } else if (enemy.attacking === 'zone') { // ABYSS ENEMIES: Warning for random zone attack @@ -1401,13 +1413,7 @@ function drawEnemy() { graphics.strokeRect(enemy.attackZone2X, enemy.attackZone2Y, enemy.attackZone2W, enemy.attackZone2H); } } - else if (enemy.dragonBreath) { - // DRAGON: Cone warning for focused fire breath - graphics.fillStyle(0xff4400, pulse * 0.8); - graphics.fillRect(200, 100, 400, 400); // Large cone area - graphics.lineStyle(4, 0xff4400, 0.9); - graphics.strokeRect(200, 100, 400, 400); - } + } texts.event.setText(`${enemy.name}`).setVisible(true); @@ -1596,15 +1602,18 @@ function handleInput(event) { } } else if (shopSelection === 4) { // Buy SPEED (Movement + Attack Speed) + if (player.cooldown <= 0.1) { + play(150, 0.2); + return; + } if (player.money >= upgradePrices.speed) { player.money -= upgradePrices.speed; - player.moveSpeed = Math.floor(player.moveSpeed * 1.12); // +12% movement speed (aumentado de 10%) - player.cooldown = Math.max(0.1, player.cooldown - 0.12); // -0.12s cooldown (aumentado de 0.1s) + player.cooldown = Math.max(0.1, player.cooldown - 0.2); // Solo velocidad de ataque, -0.2s cooldown upgradePrices.speed = Math.floor(upgradePrices.speed * 2.0); - showBigText('MAS VELOCIDAD!', 400, 200, '#00ffff', 44, 2000); + showBigText('MAS VELOCIDAD DE ATAQUE!', 400, 200, '#00ffff', 44, 2000); play(1200, 0.3); updateShopText(); - } else { + } else { play(150, 0.2); } } else if (shopSelection === 5) { @@ -1637,12 +1646,12 @@ function handleInput(event) { state = 'GAME'; currentEvent = null; shopSelection = 0; - + lastEventWasChest = false; // Reset flag after using it // Advance to next event (reward for clearing the room that unlocked shop) if (!lastEventWasChest) { eventNum++; } - lastEventWasChest = false; // Reset flag after using it + // Check if zone is complete if (eventNum >= getMaxEvents(zone)) { @@ -1738,7 +1747,7 @@ function doAction() { updateGameUI(); } else { // Just show feedback for chests (no damage or treasure loss) - showBigText('Chocas las manos con el cofre.....', 400, 200, '#ffaa00', 32); + showBigText('Nadie te ve hacer el ridiculo.....', 400, 200, '#ffaa00', 32); play(200, 0.15); } @@ -1962,7 +1971,7 @@ function openChest() { } else if (rand < 0.8) { // 40% - +1 Damage player.dmg += 1; - showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 600, 280, '#ff4400', 48); + showBigText('⚔️ PICO AFILADO +1! ⚔️', 600, 280, '#ff4400', 48); play(1200, 0.6, 'sawtooth'); spawnParticles(600, 300, 0xff4400, 40); shake(18); @@ -2314,6 +2323,7 @@ function selectDirection(choice) { } function nextEvent() { + projectiles = []; // Randomize timing zone position for new event updateTimingZone(); @@ -2407,7 +2417,7 @@ function spawnEnemy() { }; } else { // Small enemy - const hp = roll(ZONES[zone][Z.SMALL_HP][0], ZONES[zone][Z.SMALL_HP][1], ZONES[zone][Z.SMALL_HP][2]); + const hp = ZONES[zone][Z.SMALL_HP]; enemy = { type: 'small', name: ZONES[zone][Z.SMALL_E], @@ -2451,6 +2461,7 @@ function startGame() { hideMenu(); showGameUI(); startGameMusic(); // Start background music synchronized with timing + gameStartTime = Date.now(); nextEvent(); } @@ -2517,7 +2528,7 @@ function resetGame() { // Money and upgrades runMoney = 0; - upgradePrices = { hp: 50, dmg: 100, speed: 75, timing: 200, mining: 150 }; + upgradePrices = { hp: 50, dmg: 100, speed: 33, timing: 200, mining: 99 }; upgradeLevel = { timing: 0 }; updateTimingZone(); // Reset timing zone to base values shopSelection = 0; @@ -2744,7 +2755,7 @@ function updateShopText() { { name: '🍌 Estofado de Potasio', price: 25, info: 'Rellena tus corazones', color: '#ffdd44', cat: '🍽️' }, { name: '❤️ Entrenamiento Fisico', price: upgradePrices.hp, info: `FULL TANK +1 Corazon [${player.hp}/${player.maxHp} HP]`, color: '#ff5555', cat: '💪' }, { name: '⚔️ Afilar Herramientas', price: upgradePrices.dmg, info: `Pico mas letal (+DMG)(+3d6) [${(1 + player.dmg)}-${(10 + player.dmg * 2)}]`, color: '#ff8833', cat: '🔨' }, - { name: '⚡ Cerveza Energizante', price: upgradePrices.speed, info: `Movimiento y ataque mas rapidos [${player.cooldown.toFixed(1)}s]`, color: '#44ddff', cat: '🍺' }, + { name: '⚡ Cerveza Energizante', price: upgradePrices.speed, info: `Attack Speed [${player.cooldown.toFixed(1)}s]`, color: '#44ddff', cat: '🍺' }, { name: '🎯 Leccion de Precision', price: upgradePrices.timing, info: `Mejora tu punteria (ZONA VERDE MAS GRANDE) (Nivel ${upgradeLevel.timing})`, color: '#ffdd44', cat: '📜' }, { name: '⛏️ Pico Suertudo', price: upgradePrices.mining, info: `Minerales dan +50% mas recursos permanente [+${Math.floor(player.miningBonus * 100)}%]`, color: '#00ffaa', cat: '💎' }, { name: '🚪 Volver a las Minas', price: 0, info: 'Regresar al abismo oscuro...', color: '#88ff88', cat: '�' } @@ -2851,9 +2862,9 @@ function showGameOver() { // Limpiar todos los estados de juego currentEvent = null; projectiles = []; - // Destroy particle sprites before clearing array particles.forEach(p => p.destroy()); particles = []; + if(graphics) graphics.clear(); // Limpia el canvas para evitar restos // Limpiar y ocultar sprites de enemigos enemySprites.forEach(sprite => { @@ -2872,15 +2883,17 @@ function showGameOver() { chest = null; hideGame(); -//el texto es muy grande asi que hay que subirlo mas de lo que parece +// el texto es muy grande asi que hay que subirlo mas de lo que parece texts.title.setText('Otro Deshecho en el ABYSS').setVisible(true).setPosition(400, 70); texts.title.setFontSize('48px'); texts.title.setColor('#cc3333'); + const elapsed = Math.floor((Date.now() - gameStartTime) / 1000); texts.subtitle.setText( `Profundidad Alcanzada: ${ZONES[zone][0]}\n\n` + `Score Total: $${runMoney}\n\n` + - `Oro Final: $${player.money}\n\n\n` + + `Oro Final: $${player.money}\n\n` + + `Tiempo total: ${elapsed} segundos\n\n` + `PRESIONA START PARA DESCENDER DE NUEVO` ).setVisible(true).setPosition(400, 340).setColor('#ff6666'); @@ -2905,10 +2918,12 @@ function showVictory() { // Use the last valid zone (5) if zone exceeds the array const finalZone = Math.min(zone, 5); + const elapsed = Math.floor((Date.now() - gameStartTime) / 1000); texts.subtitle.setText( `Profundidad Alcanzada: ${ZONES[finalZone][0]}\n\n` + `Score Total: $${runMoney}\n\n` + - `Oro Final: $${player.money}\n\n\n` + + `Oro Final: $${player.money}\n\n` + + `Tiempo total: ${elapsed} segundos\n\n` + `PRESIONA START PARA DESCENDER DE NUEVO` ).setVisible(true).setPosition(400, 280).setColor('#ffed4e'); From 8ed604197566cce47662d01fbae2a4e91402426a Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 19:59:43 -0300 Subject: [PATCH 06/10] descripcion actualizada y mecanicas balanceado hasta el final boss + Time stamp de tiempo final para speedrun --- metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata.json b/metadata.json index f2d01598..d2c5327b 100644 --- a/metadata.json +++ b/metadata.json @@ -1,4 +1,4 @@ { "game_name": "Platanus Abyss", - "description": "¡Desciende a las profundidades del Abyss! Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, domina el sistema de timing para atacar, y enfrenta a lo que espera fondo del abismo. ¿Sobrevivirás y reclamarás tu fortuna?" + "description": "¡Desciende a las profundidades del Abyss! ATravisa este ROGUELIKE (a la antigua) Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, domina el sistema de timing para atacar y los patrones de los enemigos, y enfrenta a lo que espera fondo del abismo. ¿tienes lo que hace falta?" } From 0e6d5ef9aa26c6b0fe2fd0af11d194c2fd34a513 Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Sun, 9 Nov 2025 21:36:38 -0300 Subject: [PATCH 07/10] parche final, balance, bugfix y descripcion nueva --- game.js | 44 ++++++++++++++++++++++---------------------- metadata.json | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/game.js b/game.js index beac2d3b..d52ffa77 100644 --- a/game.js +++ b/game.js @@ -1951,8 +1951,8 @@ function openChest() { // Already at max HP - grant +1 maxHP! player.maxHp += 1; player.hp = player.maxHp; - showBigText('🍌 PLATANO DIVINO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp} HP]`, 600, 340, '#00ff00', 38, 2500); + showBigText('🍌 PLATANO DIVINO! 🍌', 400, 330, '#ffff00', 48, 2800); + showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp} HP]`, 400, 370, '#00ff00', 38, 2500); play(1320, 0.5, 'sine'); spawnParticles(600, 300, 0xffff00, 60); spawnParticles(600, 300, 0x00ff00, 25); @@ -1961,8 +1961,8 @@ function openChest() { // Not at max HP - normal heal const healAmount = player.maxHp - player.hp; player.hp = player.maxHp; - showBigText('🍌 PLATANO MAGICO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`VIDA TOTAL +${healAmount} HP!`, 600, 340, '#00ff00', 38, 2500); + showBigText('🍌 PLATANO MAGICO! 🍌', 400, 330, '#ffff00', 48, 2800); + showBigText(`VIDA TOTAL +${healAmount} HP!`, 400, 370, '#00ff00', 38, 2500); play(880, 0.4, 'sine'); spawnParticles(600, 300, 0xffff00, 40); spawnParticles(600, 300, 0x00ff00, 15); @@ -1971,7 +1971,7 @@ function openChest() { } else if (rand < 0.8) { // 40% - +1 Damage player.dmg += 1; - showBigText('⚔️ PICO AFILADO +1! ⚔️', 600, 280, '#ff4400', 48); + showBigText('⚔️ PICO AFILADO +1! ⚔️', 400, 330, '#ff4400', 48); play(1200, 0.6, 'sawtooth'); spawnParticles(600, 300, 0xff4400, 40); shake(18); @@ -1979,8 +1979,8 @@ function openChest() { // 20% - Ojo de Halcón (Timing Zone +10%) player.timingBonus += 0.1; updateTimingZone(false); - showBigText('🦅 OJO DE HALCON! 🦅', 600, 280, '#ffaa00', 48); - showBigText('ZONA VERDE +10% MAS GRANDE!', 600, 340, '#00ff00', 36); + showBigText('🦅 OJO DE HALCON! 🦅', 400, 330, '#ffaa00', 48); + showBigText('ZONA VERDE +10% MAS GRANDE!', 400, 370, '#00ff00', 36); play(1400, 0.5, 'sine'); spawnParticles(600, 300, 0xffaa00, 35); shake(12); @@ -1990,7 +1990,7 @@ function openChest() { const rand = Math.random(); if (rand < 0.05) { // 5% - Empty (reduced from 12%) - showBigText('COFRE VACIO!', 600, 300, '#666666', 40); + showBigText('COFRE VACIO!', 400, 370, '#666666', 40); play(200, 0.3); spawnParticles(600, 300, 0x666666, 8); } else if (rand < 0.20) { @@ -1999,8 +1999,8 @@ function openChest() { // Already at max HP - grant +1 maxHP! player.maxHp += 1; player.hp = player.maxHp; - showBigText('🍌 PLATANO DIVINO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp} HP]`, 600, 340, '#00ff00', 38, 2500); + showBigText('🍌 PLATANO DIVINO! 🍌', 400, 330, '#ffff00', 48, 2800); + showBigText(`¡+1 VIDA MAXIMA! [${player.maxHp} HP]`, 400, 370, '#00ff00', 38, 2500); play(1320, 0.5, 'sine'); spawnParticles(600, 300, 0xffff00, 60); spawnParticles(600, 300, 0x00ff00, 25); @@ -2010,8 +2010,8 @@ function openChest() { const healAmount = Math.floor(player.maxHp * 0.5); const actualHeal = Math.min(healAmount, player.maxHp - player.hp); player.hp = Math.min(player.maxHp, player.hp + healAmount); - showBigText('🍌 PLATANO ENCONTRADO! 🍌', 600, 280, '#ffff00', 48, 2800); - showBigText(`VIDA RESTAURADA +${actualHeal} HP!`, 600, 340, '#00ff00', 38, 2500); + showBigText('🍌 PLATANO ENCONTRADO! 🍌', 400, 330, '#ffff00', 48, 2800); + showBigText(`VIDA RESTAURADA +${actualHeal} HP!`, 400, 370, '#00ff00', 38, 2500); play(880, 0.4, 'sine'); spawnParticles(600, 300, 0xffff00, 40); spawnParticles(600, 300, 0x00ff00, 15); @@ -2020,7 +2020,7 @@ function openChest() { } else if (rand < 0.35) { // 15% - +1 Damage (increased from 10%) player.dmg += 1; - showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 600, 280, '#ff4400', 48); + showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 400, 330, '#ff4400', 48); play(1200, 0.6, 'sawtooth'); spawnParticles(600, 300, 0xff4400, 40); shake(18); @@ -2028,16 +2028,16 @@ function openChest() { // 15% - Ojo de Halcón (Timing Zone +10%) - increased from 10% player.timingBonus += 0.1; updateTimingZone(false); - showBigText('🦅 OJO DE HALCON! 🦅', 600, 280, '#ffaa00', 48); - showBigText('ZONA VERDE +10% MAS GRANDE!', 600, 340, '#00ff00', 36); + showBigText('🦅 OJO DE HALCON! 🦅', 400, 330, '#ffaa00', 48); + showBigText('ZONA VERDE +10% MAS GRANDE!', 400, 370, '#00ff00', 36); play(1400, 0.5, 'sine'); spawnParticles(600, 300, 0xffaa00, 35); shake(12); } else if (zone === 0 && rand < 0.60) { // 10% - Pico Bendito (ONLY in BOSQUE - zone 0) - reduced from 15% player.miningBonus += 1.0; - showBigText('⛏️ PICO BENDITO! ⛏️', 600, 280, '#00ffff', 48); - showBigText('MINERALES DAN X2 RECURSOS!', 600, 340, '#ffff00', 36); + showBigText('⛏️ PICO BENDITO! ⛏️', 400, 330, '#00ffff', 48); + showBigText('MINERALES DAN X2 RECURSOS!', 400, 370, '#ffff00', 36); play(1100, 0.5, 'triangle'); spawnParticles(600, 300, 0x00ffff, 35); shake(15); @@ -2046,7 +2046,7 @@ function openChest() { const money = ZONES[zone][Z.CHEST]; player.treasures += money; runMoney += money; - showBigText(`+💎${money} TESORO!`, 600, 340, '#ffff88', 36); + showBigText(`+💎${money} TESORO!`, 400, 370, '#ffff88', 36); play(800, 0.4, 'sine'); spawnParticles(600, 300, 0xffff00, 35); spawnParticles(600, 300, 0xffdd00, 15); @@ -2165,7 +2165,7 @@ function showDirectionChoice(wasChest = false) { lastEventWasChest = wasChest; // Clear any existing arrows and block texts - directionArrows.forEach(arrow => arrow.destroy()); + directionArrows.forEach(a => { if(a && a.destroy) a.destroy(); }); directionArrows = []; // Clear block texts when navigation becomes available @@ -2286,7 +2286,7 @@ function showDirectionChoice(wasChest = false) { function selectDirection(choice) { // Hide arrows and hint - directionArrows.forEach(arrow => arrow.destroy()); + directionArrows.forEach(a => { if(a && a.destroy) a.destroy(); }); directionArrows = []; if (choice === 'forward') { @@ -2922,10 +2922,10 @@ function showVictory() { texts.subtitle.setText( `Profundidad Alcanzada: ${ZONES[finalZone][0]}\n\n` + `Score Total: $${runMoney}\n\n` + - `Oro Final: $${player.money}\n\n` + + `Eres un maestro Del pico... para cuando la pala?\n\n` + `Tiempo total: ${elapsed} segundos\n\n` + `PRESIONA START PARA DESCENDER DE NUEVO` - ).setVisible(true).setPosition(400, 280).setColor('#ffed4e'); + ).setVisible(true).setPosition(400, 340).setColor('#ffed4e'); texts.info.setVisible(false); diff --git a/metadata.json b/metadata.json index d2c5327b..c9edfb23 100644 --- a/metadata.json +++ b/metadata.json @@ -1,4 +1,4 @@ { "game_name": "Platanus Abyss", - "description": "¡Desciende a las profundidades del Abyss! ATravisa este ROGUELIKE (a la antigua) Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, domina el sistema de timing para atacar y los patrones de los enemigos, y enfrenta a lo que espera fondo del abismo. ¿tienes lo que hace falta?" + "description": "¡Desciende a las profundidades del Abyss! ATravisa este ROGUELIKE (a la antigua) Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, pon a prueba tus reflejos y ritmo Dominando el Pico para atacar y esquivar los patrones de los enemigos, y enfrenta a la bestia al fondo del abismo. ¿tienes lo que hace falta?" } From ebd328dedd6da65dce8c6be74169f383748a6fa6 Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Mon, 10 Nov 2025 20:14:47 -0300 Subject: [PATCH 08/10] =?UTF-8?q?git=20finalisimo=20final,=20detalle=20vis?= =?UTF-8?q?ual=20+=20actualizaci=C3=B3n=20final=20descripcion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game.js | 18 +++++++++++++++--- metadata.json | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/game.js b/game.js index d52ffa77..15618a0a 100644 --- a/game.js +++ b/game.js @@ -107,6 +107,8 @@ const INPUT_DEBOUNCE_MS = 150; // Minimum time between inputs let canAttack = true; let graphics, scene, texts = {}; let runMoney = 0; +// Player trail (super-optimized) +let trail=[]; let upgradePrices = { hp: 50, dmg: 100, speed: 33, timing: 200, mining: 99 } let upgradeLevel = { timing: 0 }; let shakeAmt = 0; @@ -685,6 +687,11 @@ dragonGraphics.destroy(); function update(time, delta) { graphics.clear(); + // Draw player trail (tiny faded circles, longer) + for(let i=0;i20)trail.shift(); + } if (state === 'GAME') { const moveSpeed = player.moveSpeed * delta / 1000; // Use player's move speed stat const topBarHeight = 180; @@ -2020,7 +2032,7 @@ function openChest() { } else if (rand < 0.35) { // 15% - +1 Damage (increased from 10%) player.dmg += 1; - showBigText('⚔️ PICO MAS GRANDE +1 DMG! ⚔️', 400, 330, '#ff4400', 48); + showBigText('⚔️ PICO MAS GRANDE +1 DMG! ', 400, 330, '#ff4400', 48); play(1200, 0.6, 'sawtooth'); spawnParticles(600, 300, 0xff4400, 40); shake(18); @@ -2028,7 +2040,7 @@ function openChest() { // 15% - Ojo de Halcón (Timing Zone +10%) - increased from 10% player.timingBonus += 0.1; updateTimingZone(false); - showBigText('🦅 OJO DE HALCON! 🦅', 400, 330, '#ffaa00', 48); + showBigText('🦅 OJO DE HALCON! ', 400, 330, '#ffaa00', 48); showBigText('ZONA VERDE +10% MAS GRANDE!', 400, 370, '#00ff00', 36); play(1400, 0.5, 'sine'); spawnParticles(600, 300, 0xffaa00, 35); @@ -2036,7 +2048,7 @@ function openChest() { } else if (zone === 0 && rand < 0.60) { // 10% - Pico Bendito (ONLY in BOSQUE - zone 0) - reduced from 15% player.miningBonus += 1.0; - showBigText('⛏️ PICO BENDITO! ⛏️', 400, 330, '#00ffff', 48); + showBigText('⛏️ PICO BENDITO! ', 400, 330, '#00ffff', 48); showBigText('MINERALES DAN X2 RECURSOS!', 400, 370, '#ffff00', 36); play(1100, 0.5, 'triangle'); spawnParticles(600, 300, 0x00ffff, 35); diff --git a/metadata.json b/metadata.json index c9edfb23..d75a7bb6 100644 --- a/metadata.json +++ b/metadata.json @@ -1,4 +1,4 @@ { "game_name": "Platanus Abyss", - "description": "¡Desciende a las profundidades del Abyss! ATravisa este ROGUELIKE (a la antigua) Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, pon a prueba tus reflejos y ritmo Dominando el Pico para atacar y esquivar los patrones de los enemigos, y enfrenta a la bestia al fondo del abismo. ¿tienes lo que hace falta?" + "description": "¡Desciende a las profundidades del Abyss! ATravisa este Mini-Roguelike DIFICIL. Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, pon a prueba tus reflejos y ritmo Dominando el Pico para atacar y esquivar los patrones de los enemigos, y enfrenta a la bestia al fondo del abismo. ¿tienes lo que hace falta?" } From ed646bf8cb2e3f040b42592c3c6598464fa21597 Mon Sep 17 00:00:00 2001 From: tomske3312 Date: Mon, 10 Nov 2025 20:27:05 -0300 Subject: [PATCH 09/10] ultimo bugfix --- game.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/game.js b/game.js index 15618a0a..3805398e 100644 --- a/game.js +++ b/game.js @@ -688,7 +688,7 @@ dragonGraphics.destroy(); function update(time, delta) { graphics.clear(); // Draw player trail (tiny faded circles, longer) - for(let i=0;i Date: Mon, 10 Nov 2025 20:41:34 -0300 Subject: [PATCH 10/10] v1.2 --- game.js | 2 +- metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/game.js b/game.js index 3805398e..38e51ff8 100644 --- a/game.js +++ b/game.js @@ -38,7 +38,7 @@ const ZONES = { 0: ['BOSQUE', 'HIERRO', 10, 'PLATA', 50, 'RATONCITO', 38, [1,10,3], 'TROLL', 75, [2,10,5], 30, 0x2D5016, 'RAT'], 1: ['MINAS OLVIDADAS', 'ORO', 25, 'ESMERALDA', 150, 'RATATA', 78, [2,10,5], 'GOLEM', 120, [3,10,8], 75, 0x4A4A4A, 'GOLEM'], 2: ['LAS PROFUNDIDADES', 'DIAMANTE', 50, 'RUBI', 300, 'WAREN', 400, [2,10,8], 'DEMON', 400, [4,10,5], 150, 0x2C1810, 'DEMON'], - 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'DIABLILLO', 600, [3,10,5], 'DRAGON', 700, [4,10,15], 300, 0x8B0000, 'DRAGON'], + 3: ['INFIERNO', 'PIEDRA INFERNAL', 100, 'ZAFIRO', 600, 'IMP', 600, [3,10,5], 'DRAGON TUERTO', 700, [4,10,15], 300, 0x8B0000, 'DRAGON'], 4: ['ABISMO', 'PIEDRA ABISMAL', 1000, 'CRISTAL-SOMBRA', 8000, 'ALMA EN PENA', 700, [5,10,20], 'TROLL ABYSS', 1000, [10,15,40], 4000, 0x1A0033, 'ALMA'], 5: ['???', null, 0, 'CORAZON-ABYSS', 30000, 'ALMA EN PENA', 79, [9,10,45], 'HEROE CORRUPTO', 3000, [15,20,80], 15000, 0x000000, 'BOSS'] }; diff --git a/metadata.json b/metadata.json index d75a7bb6..a27ebb9e 100644 --- a/metadata.json +++ b/metadata.json @@ -1,4 +1,4 @@ { "game_name": "Platanus Abyss", - "description": "¡Desciende a las profundidades del Abyss! ATravisa este Mini-Roguelike DIFICIL. Mina tesoros, combate enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, pon a prueba tus reflejos y ritmo Dominando el Pico para atacar y esquivar los patrones de los enemigos, y enfrenta a la bestia al fondo del abismo. ¿tienes lo que hace falta?" + "description": "¡Desciende a las profundidades del Abyss! Atravisa este Mini-Roguelike DIFICIL. Enfrenta Encuentros ALEATORIOS.Mina tesoros, combate Enemigos oscuros y abre cofres en 6 zonas cada vez más peligrosas. Mejora tu equipo en la Taberna, pon a prueba tus reflejos y ritmo Dominando el Pico para atacar y esquivar los patrones de los enemigos, y enfrenta a la bestia al fondo del abismo. ¿tienes lo que hace falta?" }