XGravity/Public/ecma/Views/GameView.ecma.js

683 lines
27 KiB
JavaScript

"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;
})();