"use strict"; const GameView = (function(){ const GameView = function(xgravity){ const self = this; let view_element = null, canvas_element = null, ctx = null, animation_frame_id = null, last_timestamp = 0, is_paused = false, pause_modal = null, bg_manager = null, current_bg_mode = null; let cockpit_model = null; let drone_model = null; let hud_model = null; let enemies_manager = null; // let explosions_model = null; // Variables de balanceo e inercia de cámara let camera_drift_x = 0; let camera_drift_y = 0; let camera_sway_clock = 0; let bg_parallax_x = 0; let bg_parallax_y = 0; // Variables de Screen Shake let shake_trauma = 0.0; let shake_duration = 0.0; let shake_max_duration = 0.1; let shake_intensity = 0.0; let shake_offset_x = 0; let shake_offset_y = 0; let shake_angle = 0; // Estado del juego let current_score = 0; let screen_shake_intensity = 0; let screen_shake_decay = 4.0; let drone_lives = 3; let max_drone_lives = 3; let is_respawning_drone = false; let is_game_over = false; let session_stats = { total_spawned : 0, total_destroyed : 0, shield_kills : 0 }; // const trigger_mega_shake = () => { // screen_shake_intensity = 28.0; // Temblores violentos en píxeles // }; // Métodos de control de estado expuestos this.is_active = () => animation_frame_id !== null; this.is_paused = () => is_paused; this.pause = () => { is_paused = true; if(xgravity.views.fps){ xgravity.views.fps.close(); } }; this.resume = () => { is_paused = false; last_timestamp = 0; // Evita saltos de delta_time al volver }; const on_pointer_move = (e) => { if(!canvas_element || !drone_model) return; const rect = canvas_element.getBoundingClientRect(); const client_x = e.touches ? e.touches[0].clientX : e.clientX; const client_y = e.touches ? e.touches[0].clientY : e.clientY; const scale_x = canvas_element.width / rect.width; const scale_y = canvas_element.height / rect.height; const canvas_pixel_x = (client_x - rect.left) * scale_x; const canvas_pixel_y = (client_y - rect.top) * scale_y; drone_model.set_target_pixels(canvas_pixel_x, canvas_pixel_y, canvas_element.width, canvas_element.height); }; // Controladores de activación de escudo const on_mouse_down = (e) => { if(e.button === 0 && drone_model){ drone_model.set_shield_active(true); } }; const on_mouse_up = (e) => { if(e.button === 0 && drone_model){ drone_model.set_shield_active(false); } }; const on_key_down = (e) => { // if((e.code === "Space" || e.key === " ") && !e.repeat && drone_model){ // drone_model.set_shield_active(true); // } // Tecla ESC: abre/cierra menú o pausa la partida if(e.code === "Escape" || e.key === "Escape"){ e.preventDefault(); self.toggle_pause(); return; } const action_key = xgravity.settings.get("action_key", null, "Space"); const matches_action = (action_key === "Space" && (e.code === "Space" || e.key === " ")) || (e.code === action_key || e.key.toUpperCase() === action_key.toUpperCase()); if(matches_action && !e.repeat && drone_model){ drone_model.set_shield_active(true); } }; const on_key_up = (e) => { // if((e.code === "Space" || e.key === " ") && drone_model){ // drone_model.set_shield_active(false); // } const action_key = xgravity.settings.get("action_key", null, "Space"); const matches_action = (action_key === "Space" && (e.code === "Space" || e.key === " ")) || (e.code === action_key || e.key.toUpperCase() === action_key.toUpperCase()); if(matches_action && drone_model){ drone_model.set_shield_active(false); } }; const on_touch_start = (e) => { if(drone_model){ drone_model.set_shield_active(true); } }; const on_touch_end = (e) => { if(drone_model){ drone_model.set_shield_active(false); } }; // Callback cuando el escudo destruye a un enemigo const on_enemy_destroyed = (enemy, impact_damage = 0) => { const bounds = enemy.get_bounds(); // 1. Efecto visual de destrucción if(xgravity.explosions){ xgravity.explosions.add(bounds.x, bounds.y, "shield"); } trigger_screen_shake("shield"); // 2. Puntos base configurados en el enemigo const base_pts = (typeof enemy.get_points === "function") ? enemy.get_points() : 100; // 3. MULTIPLICADOR CINÉTICO: // Si el daño fue 20 -> x1.20 | Si embistes con inercia y fue 65 -> x1.65 | Si fue 120 -> x2.20 const kinetic_mult = impact_damage > 0 ? (1.0 + (impact_damage * 0.01)) : 1.0; const final_pts = Math.round(base_pts * kinetic_mult); // 4. Sumar puntos y actualizar el HUD inmediatamente current_score += final_pts; if(hud_model){ hud_model.set_score(current_score); } // 5. Estadísticas de la partida session_stats.total_destroyed++; session_stats.shield_kills++; // 6. Recarga del escudo según dificultad if(drone_model){ const base_reward = xgravity.settings.get("base_drone_shield_kill_reward", null, 10.0); const preset = xgravity.settings.get_difficulty_preset(); const reward = base_reward * (preset ? preset.shield_reward_mult : 1.0); drone_model.restore_shield(reward); } }; const on_mothership_impact = (base_damage = 25) => { console.log("¡BOOM! Impacto en cabina recibido con daño base:", base_damage); const preset = xgravity.settings.get_difficulty_preset(); const dmg_mult = preset ? preset.mothership_damage_mult : 1.0; const final_damage = base_damage * dmg_mult; if(xgravity.audio){ xgravity.audio.play("mothership_impact"); } if(cockpit_model && typeof cockpit_model.take_damage === "function"){ const is_destroyed = cockpit_model.take_damage(final_damage); if(is_destroyed){ on_game_over("mothership_destroyed"); } } // Disparo del temblor brutal de la nodriza trigger_screen_shake("mothership"); if(xgravity.explosions && typeof xgravity.explosions.add_mothership_impact === "function"){ xgravity.explosions.add_mothership_impact(); } }; // Callback cuando impacta al dron sin escudo const on_drone_hit = (enemy, damage_to_drone = 35) => { const bounds = enemy.get_bounds(); if(xgravity.explosions){ xgravity.explosions.add(bounds.x, bounds.y, "hit"); } trigger_screen_shake("hit"); if(cockpit_model) cockpit_model.set_alert(true); if(drone_model && damage_to_drone > 0){ drone_model.damage_shield(damage_to_drone); // Si el escudo/vida cae a 0, estalla y entra el siguiente if(drone_model.get_position().shield <= 0){ destroy_current_drone(); } } }; // Callback cuando el enemigo sobrevive al choque (chispas y deflexión) const on_enemy_deflected = (x, y, nx, ny, intensity, damage, shield_active) => { if(xgravity.explosions){ xgravity.explosions.add_sparks(x, y, nx, ny, intensity); } if(!shield_active && drone_model){ drone_model.damage_shield(damage * 0.5); trigger_screen_shake("shield"); } else if(shield_active && drone_model){ drone_model.damage_shield(damage * 0.2); } }; const on_game_over = (reason = "mothership_destroyed") => { if(is_game_over) return; is_game_over = true; console.log("=== GAME OVER ===", reason); // Ocultamos el botón de pausa para que no estorbe en el Game Over if(view_element){ const pause_btn = view_element.querySelector(".xg-hud-pause-btn"); if(pause_btn) pause_btn.style.display = "none"; } trigger_mega_shake(); // Si EnemiesManager lleva la cuenta de spawns, sincronizamos: if(enemies_manager && typeof enemies_manager.get_total_spawned === "function"){ session_stats.total_spawned = enemies_manager.get_total_spawned(); } const preset = (xgravity.settings && typeof xgravity.settings.get_difficulty_preset === "function") ? xgravity.settings.get_difficulty_preset() : null; const final_data = { score : current_score, reason : reason, difficulty : preset ? preset.id : "average", total_spawned : Math.max(session_stats.total_spawned, session_stats.total_destroyed), total_destroyed : session_stats.total_destroyed, shield_kills : session_stats.shield_kills }; // Pausar el juego y abrir la pantalla tras un breve retardo dramático setTimeout(() => { self.pause(); if(xgravity.views.game_over){ xgravity.views.game_over.go(final_data); } }, 600); }; const destroy_current_drone = () => { if(!drone_model || drone_model.is_deploying() || is_respawning_drone || is_game_over) return; is_respawning_drone = true; const pos = drone_model.get_position(); // 1. Explosión de destrucción en el plano de batalla espacial if(xgravity.explosions){ for(let k = 0; k < 3; k++){ xgravity.explosions.add(pos.cell_x + ((Math.random() - 0.5) * 1.5), pos.cell_y + ((Math.random() - 0.5) * 1.5), "hit"); } xgravity.explosions.add_sparks(pos.cell_x, pos.cell_y, 0, -1, 3.0); } trigger_screen_shake("mothership"); // 2. Restar vida drone_lives = Math.max(0, drone_lives - 1); if(hud_model){ hud_model.set_lives(drone_lives, max_drone_lives); } // 3. Evaluar derrota o siguiente dron if(drone_lives <= 0){ on_game_over("all_drones_lost"); } else { // Pequeña pausa dramática para que se aprecie la explosión (~0.4s) antes de que la nodriza lance el siguiente setTimeout(() => { if(drone_model && !is_game_over){ drone_model.restore_shield(drone_model.get_position().max_shield || 100); drone_model.start_deployment(); // Emerge a tamaño x10 desde abajo } is_respawning_drone = false; }, 400); } }; // Disparador de sacudida con perfiles de intensidad const trigger_screen_shake = (type = "shield") => { shake_trauma = 1.0; if(type === "shield"){ // Golpe amortiguado en el escudo del dron shake_intensity = xgravity.settings.get("screen_shake_shield_intensity", null, 0.25); shake_max_duration = xgravity.settings.get("screen_shake_shield_duration", null, 0.15); } else if(type === "mothership" || type === "critical"){ // IMPACTO CRÍTICO EN CABINA: Sacudida masiva y violenta shake_intensity = xgravity.settings.get("screen_shake_mothership_intensity", null, 4.2); // ~4.2 celdas de amplitud shake_max_duration = xgravity.settings.get("screen_shake_mothership_duration", null, 1.15); // >1 segundo de temblor } else { // Golpe en el dron sin escudo shake_intensity = xgravity.settings.get("screen_shake_hit_intensity", null, 0.85); shake_max_duration = xgravity.settings.get("screen_shake_hit_duration", null, 0.42); } shake_duration = shake_max_duration; }; const trigger_mega_shake = () => { trigger_screen_shake("mothership"); }; const sync_background = () => { const mode = xgravity.settings.get("background_mode", null, "stars"); if(current_bg_mode !== mode || !bg_manager){ current_bg_mode = mode; if(mode === "image") bg_manager = new ImageBackground(xgravity); else if(mode === "procedural") bg_manager = new ProceduralBackground(xgravity); else bg_manager = new StarsBackground(xgravity); if(canvas_element) bg_manager.resize(canvas_element.width, canvas_element.height); } }; const resize_canvas = () => { if(!canvas_element) return; const quality_pct = xgravity.settings.get("canvas_quality", null, 100) / 100; const rect = canvas_element.getBoundingClientRect(); const internal_w = Math.max(1, Math.floor(rect.width * quality_pct)); const internal_h = Math.max(1, Math.floor(rect.height * quality_pct)); if(canvas_element.width !== internal_w || canvas_element.height !== internal_h){ canvas_element.width = internal_w; canvas_element.height = internal_h; if(bg_manager) bg_manager.resize(internal_w, internal_h); } }; const update_camera = (delta_time, cell_px) => { camera_sway_clock += delta_time; const sway_speed = xgravity.settings.get("cockpit_sway_speed", null, 0.8); if(drone_model){ const drone_pos = drone_model.get_position(); // Inercia de cabina const drift_factor = xgravity.settings.get("cockpit_drift_amount", null, 0.6); const target_drift_x = (-drone_pos.cell_x / 20.0) * drift_factor; const target_drift_y = (-drone_pos.cell_y / 20.0) * drift_factor; camera_drift_x += (target_drift_x - camera_drift_x) * 4.0 * delta_time; camera_drift_y += (target_drift_y - camera_drift_y) * 4.0 * delta_time; // Parallax del fondo estelar const bg_factor = xgravity.settings.get("background_parallax_amount", null, 0.25); const target_bg_x = (drone_pos.cell_x / 20.0) * bg_factor * cell_px; const target_bg_y = (drone_pos.cell_y / 20.0) * bg_factor * cell_px; bg_parallax_x += (target_bg_x - bg_parallax_x) * 3.0 * delta_time; bg_parallax_y += (target_bg_y - bg_parallax_y) * 3.0 * delta_time; } // Screen Shake if(shake_duration > 0){ shake_duration -= delta_time; shake_trauma = shake_max_duration > 0 ? Math.max(0, shake_duration / shake_max_duration) : 0; // Decaimiento no lineal (trauma al cuadrado): arranca fuertísimo y decae suave const trauma_factor = shake_trauma * shake_trauma; const shake_offset_cells = trauma_factor * shake_intensity; shake_offset_x = (Math.random() * 2 - 1) * shake_offset_cells * cell_px; shake_offset_y = (Math.random() * 2 - 1) * shake_offset_cells * cell_px; // Si es un impacto de nodriza (shake_intensity > 2.0), el latigazo angular se triplica (~0.08 rad ≈ 4.6°) const max_roll = (shake_intensity > 2.0) ? 0.08 : 0.025; shake_angle = (Math.random() * 2 - 1) * (trauma_factor * max_roll); } else { shake_offset_x = 0; shake_offset_y = 0; shake_angle = 0; } }; const game_loop = (timestamp) => { if(!last_timestamp) last_timestamp = timestamp; const delta_time = Math.min((timestamp - last_timestamp) / 1000, 0.1); last_timestamp = timestamp; const cell_size = (canvas_element.width < canvas_element.height ? canvas_element.width : canvas_element.height) / xgravity.settings.get("cells", null, 40); if(!is_paused){ sync_background(); resize_canvas(); if(xgravity.views.fps){ xgravity.views.fps.tick(delta_time); } // 1. FÍSICAS Y ACTUALIZACIONES (Aquí solo van updates) if(enemies_manager){ enemies_manager.update(delta_time, drone_model, on_enemy_destroyed, on_drone_hit, on_enemy_deflected, on_mothership_impact); } if(drone_model){ drone_model.update(delta_time); } if(cockpit_model){ cockpit_model.update(delta_time); } if(xgravity.explosions){ xgravity.explosions.update(delta_time); // <-- ACTIVADO } if(hud_model){ hud_model.render(ctx, canvas_element.width, canvas_element.height, delta_time, drone_model, cockpit_model, current_score, drone_lives, max_drone_lives); } // 2. CÁMARA Y TRAUMA update_camera(delta_time, cell_size); // 3. LIMPIEZA ctx.clearRect(0, 0, canvas_element.width, canvas_element.height); // 4. FONDO ESTELAR CON PARALLAX if(bg_manager){ ctx.save(); ctx.translate(bg_parallax_x + (shake_offset_x * 0.4), bg_parallax_y + (shake_offset_y * 0.4)); bg_manager.render(ctx, canvas_element.width, canvas_element.height, delta_time); ctx.restore(); } // 5. SCREEN SHAKE ACTIVO PARA ENTIDADES Y CABINA ctx.save(); const cx = canvas_element.width / 2; const cy = canvas_element.height / 2; ctx.translate(cx + shake_offset_x, cy + shake_offset_y); if(shake_angle !== 0) ctx.rotate(shake_angle); ctx.translate(-cx, -cy); // A) Enemigos en el plano de batalla (debajo del dron) if(enemies_manager){ enemies_manager.render_under(ctx, canvas_element.width, canvas_element.height); } // B) Dron y explosiones de combate espacial if(drone_model){ drone_model.render(ctx, canvas_element.width, canvas_element.height); } if(xgravity.explosions){ xgravity.explosions.render(ctx, canvas_element.width, canvas_element.height); } // C) Enemigos en sobrevuelo + Retículas y Daño flotante if(enemies_manager){ enemies_manager.render_over(ctx, canvas_element.width, canvas_element.height); } // D) Cúpula / Cabina con balanceo inercial ctx.save(); const drift_px_x = (camera_drift_x + Math.sin(camera_sway_clock * 0.8) * 0.15) * cell_size; const drift_px_y = (camera_drift_y + Math.cos(camera_sway_clock * 0.56) * 0.15) * cell_size; ctx.translate(drift_px_x, drift_px_y); if(cockpit_model){ cockpit_model.render(ctx, canvas_element.width, canvas_element.height, delta_time); } ctx.restore(); // E) EFECTOS DE CABINA (Flash de pantalla y fragmentos de roca sobre el cristal) if(xgravity.explosions && typeof xgravity.explosions.render_cockpit === "function"){ xgravity.explosions.render_cockpit(ctx, canvas_element.width, canvas_element.height); } ctx.restore(); // Cierre de Screen Shake // 6. HUD HOLOGRÁFICO (fijo en cabina) if(hud_model){ hud_model.render(ctx, canvas_element.width, canvas_element.height, delta_time, drone_model, cockpit_model, current_score); } } animation_frame_id = requestAnimationFrame(game_loop); }; this.update_hud = () => {}; this.toggle_pause = () => { if(!is_paused){ self.pause(); if(xgravity.views.main_menu){ xgravity.views.main_menu.go(); } } else { // Si ya estaba pausado y pulsa ESC de nuevo, reanudamos if(xgravity.views.main_menu){ xgravity.views.main_menu.close(); } self.resume(); } }; this.stop = () => { if(animation_frame_id){ cancelAnimationFrame(animation_frame_id); animation_frame_id = null; } if(view_element && view_element.parentNode){ view_element.parentNode.removeChild(view_element); view_element = null; } if(xgravity.views.fps){ xgravity.views.fps.close(); } window.removeEventListener("mousedown", on_mouse_down); window.removeEventListener("mouseup", on_mouse_up); window.removeEventListener("keydown", on_key_down); window.removeEventListener("keyup", on_key_up); window.removeEventListener("touchstart", on_touch_start); window.removeEventListener("touchend", on_touch_end); window.removeEventListener("mousemove", on_pointer_move); window.removeEventListener("touchmove", on_pointer_move); cockpit_model = null; drone_model = null; hud_model = null; enemies_manager = null; // explosions_model = null; bg_manager = null; current_bg_mode = null; }; this.go = () => { self.stop(); is_paused = false; current_score = 0; last_timestamp = 0; cockpit_model = new CockpitModel(xgravity); drone_model = new DroneModel(xgravity); hud_model = new TelemetryHudModel(xgravity); enemies_manager = new EnemiesManager(xgravity); // explosions_model = new ExplosionsModel(xgravity); window.addEventListener("mousedown", on_mouse_down); window.addEventListener("mouseup", on_mouse_up); window.addEventListener("keydown", on_key_down); window.addEventListener("keyup", on_key_up); window.addEventListener("touchstart", on_touch_start, { passive : true }); window.addEventListener("touchend", on_touch_end, { passive : true }); window.addEventListener("mousemove", on_pointer_move); window.addEventListener("touchmove", on_pointer_move, { passive : true }); session_stats = { total_spawned : 0, total_destroyed : 0, shield_kills : 0 }; if(enemies_manager){ enemies_manager.set_on_mothership_impact((base_damage, impact_x, impact_y) => { // 1. Calcular daño mitigado o potenciado por la dificultad const preset = xgravity.settings.get_difficulty_preset(); const dmg_mult = preset ? preset.mothership_damage_mult : 1.0; const final_damage = base_damage * dmg_mult; // 2. Aplicar daño a la nave nodriza let is_destroyed = false; if(cockpit_model && typeof cockpit_model.take_damage === "function"){ is_destroyed = cockpit_model.take_damage(final_damage); } // 3. Efectos visuales brutales trigger_mega_shake(); const cx = canvas.width / 2; const cy = canvas.height / 2; if(xgravity.explosions && typeof xgravity.explosions.add_mothership_impact === "function"){ xgravity.explosions.add_mothership_impact(cx, cy); } // 4. Si el casco llega a cero: Game Over if(is_destroyed){ // Trigger de fin de partida console.log("¡NAVE NODRIZA DESTRUIDA!"); } }); } const nodes = xgravity.html(xgravity.item_self, [ "div", { class : "xg-game-container" }, [ ["canvas", { class : "xg-canvas" }], ["div", { class : "xg-hud" }, [ // Arriba (FPS y telemetría) ["div", { class : "xg-hud-top" }], // Abajo a la derecha: Botón de Pausa ["div", { class : "xg-hud-bottom" }, [ ["button", { class : "xg-btn xg-hud-pause-btn", "data-i18n" : "hud_pause_title", onclick : () => self.toggle_pause() }] ]] ]] ] ]); const preset = (xgravity.settings && typeof xgravity.settings.get_difficulty_preset === "function") ? xgravity.settings.get_difficulty_preset() : null; max_drone_lives = (preset && preset.drone_lives !== undefined) ? preset.drone_lives : xgravity.settings.get("drone_lives", null, 3); drone_lives = max_drone_lives; is_game_over = false; is_respawning_drone = false; if(hud_model){ hud_model.set_lives(drone_lives, max_drone_lives); } view_element = nodes[0]; canvas_element = view_element.querySelector("canvas"); ctx = canvas_element.getContext("2d"); sync_background(); resize_canvas(); self.update_hud(); xgravity.i18n.update(); animation_frame_id = requestAnimationFrame(game_loop); }; }; return GameView; })();