XGravity/Public/ecma/Models/TelemetryHudModel.ecma.js

442 lines
18 KiB
JavaScript

"use strict";
const TelemetryHudModel = (function(){
const TelemetryHudModel = function(xgravity){
const self = this;
let blink_timer = 0;
let current_score = 0;
// Vidas dinámicas según dificultad o configuración
const get_initial_lives = () => {
const preset = (xgravity.settings && typeof xgravity.settings.get_difficulty_preset === "function")
? xgravity.settings.get_difficulty_preset()
: null;
return (preset && preset.drone_lives !== undefined)
? preset.drone_lives
: xgravity.settings.get("drone_lives", null, 3);
};
let current_lives = get_initial_lives();
let max_lives = current_lives;
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.set_score = (val) => {
current_score = Math.max(0, Math.floor(val));
};
this.add_score = (val) => {
current_score += Math.max(0, Math.floor(val));
};
this.get_score = () => current_score;
this.set_lives = (lives, max = null) => {
current_lives = Math.max(0, Math.floor(lives));
if(max !== null){
max_lives = Math.max(1, Math.floor(max));
}
};
this.get_lives = () => current_lives;
// Dibuja texto con contorno perimetral oscuro de alto contraste
const draw_hud_text = (ctx, text, x, y, fill_color, cell_px) => {
ctx.save();
ctx.lineWidth = cell_px * 0.28;
ctx.strokeStyle = "rgba(2, 6, 23, 0.95)";
ctx.lineJoin = "round";
ctx.strokeText(text, x, y);
ctx.fillStyle = fill_color;
ctx.fillText(text, x, y);
ctx.restore();
};
// Dibuja silueta táctica de caza/dron en la barra de reservas
const draw_drone_life_pip = (ctx, x, y, size, is_active, color, cell_px) => {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
ctx.moveTo(0, -size * 0.45);
ctx.lineTo(size * 0.42, size * 0.45);
ctx.lineTo(0, size * 0.22);
ctx.lineTo(-size * 0.42, size * 0.45);
ctx.closePath();
if(is_active){
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = cell_px * 0.35;
ctx.fill();
ctx.lineWidth = cell_px * 0.08;
ctx.strokeStyle = "rgba(255, 255, 255, 0.85)";
ctx.stroke();
} else {
ctx.fillStyle = "rgba(2, 6, 23, 0.75)";
ctx.fill();
ctx.lineWidth = cell_px * 0.07;
ctx.strokeStyle = "rgba(56, 189, 248, 0.25)";
ctx.stroke();
}
ctx.restore();
};
// Dibuja una barra táctica con centro en 0 (bipolar: -1 a +1)
const draw_bipolar_bar = (ctx, x, y, width, height, value, fill_color, cell_px) => {
ctx.save();
ctx.fillStyle = "rgba(2, 6, 23, 0.88)";
ctx.fillRect(x - (cell_px * 0.1), y - (cell_px * 0.1), width + (cell_px * 0.2), height + (cell_px * 0.2));
ctx.strokeStyle = "rgba(56, 189, 248, 0.65)";
ctx.lineWidth = cell_px * 0.09;
ctx.strokeRect(x, y, width, height);
const mid_x = x + (width / 2);
ctx.beginPath();
ctx.moveTo(mid_x, y);
ctx.lineTo(mid_x, y + height);
ctx.strokeStyle = "rgba(255, 255, 255, 0.85)";
ctx.lineWidth = cell_px * 0.08;
ctx.stroke();
const clamped = Math.max(-1.0, Math.min(1.0, value));
const bar_length = (width / 2) * Math.abs(clamped);
ctx.fillStyle = fill_color;
if(clamped > 0){
ctx.fillRect(mid_x, y, bar_length, height);
} else if(clamped < 0){
ctx.fillRect(mid_x - bar_length, y, bar_length, height);
}
ctx.restore();
};
// Dibuja una barra segmentada genérica (Escudo y Nodriza)
const draw_segmented_bar = (ctx, x, y, width, height, pct, fill_color, cell_px, segments = 10) => {
ctx.save();
const gap = cell_px * 0.12;
const seg_width = (width - (gap * (segments - 1))) / segments;
ctx.fillStyle = "rgba(2, 6, 23, 0.88)";
ctx.fillRect(x - (cell_px * 0.1), y - (cell_px * 0.1), width + (cell_px * 0.2), height + (cell_px * 0.2));
for(let s = 0; s < segments; s++){
const seg_x = x + (s * (seg_width + gap));
const seg_threshold = (s + 1) / segments;
ctx.strokeStyle = "rgba(56, 189, 248, 0.4)";
ctx.lineWidth = cell_px * 0.07;
ctx.strokeRect(seg_x, y, seg_width, height);
if(pct >= seg_threshold - 0.05){
ctx.fillStyle = fill_color;
ctx.fillRect(seg_x, y, seg_width, height);
}
}
ctx.restore();
};
// Dibuja las marcas perimétricas que siguen a la nave en X e Y
const draw_axis_tracking_ticks = (ctx, cx, cy, drone_px_x, drone_px_y, radius_px, color, cell_px) => {
ctx.save();
const tick_len = cell_px * 1.0;
const gap = cell_px * 0.4;
ctx.strokeStyle = color;
ctx.lineWidth = cell_px * 0.12;
ctx.lineCap = "round";
const x_pos = drone_px_x;
const top_base_y = cy - radius_px - gap;
ctx.beginPath();
ctx.moveTo(x_pos, top_base_y);
ctx.lineTo(x_pos, top_base_y - tick_len);
ctx.stroke();
const btm_base_y = cy + radius_px + gap;
ctx.beginPath();
ctx.moveTo(x_pos, btm_base_y);
ctx.lineTo(x_pos, btm_base_y + tick_len);
ctx.stroke();
const y_pos = drone_px_y;
const left_base_x = cx - radius_px - gap;
ctx.beginPath();
ctx.moveTo(left_base_x, y_pos);
ctx.lineTo(left_base_x - tick_len, y_pos);
ctx.stroke();
const right_base_x = cx + radius_px + gap;
ctx.beginPath();
ctx.moveTo(right_base_x, y_pos);
ctx.lineTo(right_base_x + tick_len, y_pos);
ctx.stroke();
ctx.restore();
};
this.render = (ctx, width, height, delta_time, drone_model, cockpit_model, score_val = null, lives_val = null, max_lives_val = null) => {
if(!drone_model) return;
if(score_val !== null){
current_score = score_val;
}
if(lives_val !== null){
current_lives = Math.max(0, Math.floor(lives_val));
}
if(max_lives_val !== null){
max_lives = Math.max(1, Math.floor(max_lives_val));
}
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
blink_timer += delta_time;
const safe_radius_cells = xgravity.settings.get("safe_zone_radius", null, 18.5);
const warn_radius_cells = xgravity.settings.get("safe_zone_warning", null, 13.5);
const safe_radius_px = safe_radius_cells * cell_px;
const drone_info = drone_model.get_position();
// 1. Coordenadas y distancias en celdas
const dcx = drone_info.cell_x || 0;
const dcy = drone_info.cell_y || 0;
const dist_cells = Math.sqrt(dcx * dcx + dcy * dcy);
const ptr_cx = drone_info.target_cell_x || 0;
const ptr_cy = drone_info.target_cell_y || 0;
const ptr_dist_cells = Math.sqrt(ptr_cx * ptr_cx + ptr_cy * ptr_cy);
const v_cell_x = drone_info.vel_cell_x || 0;
const v_cell_y = drone_info.vel_cell_y || 0;
const drone_px_x = cx + (dcx * cell_px);
const drone_px_y = cy + (dcy * cell_px);
// 2. Alertas
let drone_danger_alpha = 0;
const is_drone_lost = dist_cells > safe_radius_cells;
if(dist_cells > warn_radius_cells){
drone_danger_alpha = Math.min((dist_cells - warn_radius_cells) / (safe_radius_cells - warn_radius_cells), 1.0);
}
let ptr_danger_alpha = 0;
const is_ptr_lost = ptr_dist_cells > safe_radius_cells;
if(ptr_dist_cells > warn_radius_cells){
ptr_danger_alpha = Math.min((ptr_dist_cells - warn_radius_cells) / (safe_radius_cells - warn_radius_cells), 1.0);
}
if(cockpit_model && typeof cockpit_model.set_alert === "function"){
cockpit_model.set_alert(drone_danger_alpha > 0.4 || is_drone_lost || ptr_danger_alpha > 0.4 || is_ptr_lost || current_lives <= 1);
}
// Colores dinámicos
const drone_color = is_drone_lost
? ((Math.sin(blink_timer * 14) > 0) ? "#ef4444" : "#7f1d1d")
: (drone_danger_alpha > 0 ? "#f59e0b" : "#38bdf8");
const ptr_color = is_ptr_lost
? ((Math.sin(blink_timer * 14) > 0) ? "#ef4444" : "#7f1d1d")
: (ptr_danger_alpha > 0 ? "#f59e0b" : "#c084fc");
ctx.save();
// 3. Perímetro táctico centrado
ctx.save();
ctx.lineWidth = cell_px * 0.16;
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)";
ctx.beginPath();
ctx.arc(cx, cy, safe_radius_px, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.1;
ctx.setLineDash([cell_px * 0.5, cell_px * 0.35]);
ctx.strokeStyle = (drone_danger_alpha > 0 || ptr_danger_alpha > 0)
? `rgba(239, 68, 68, ${0.35 + Math.max(drone_danger_alpha, ptr_danger_alpha) * 0.65})`
: "rgba(56, 189, 248, 0.45)";
ctx.stroke();
ctx.restore();
// 4. LÍNEAS DE POSICIÓN PERIMÉTRICAS
draw_axis_tracking_ticks(ctx, cx, cy, drone_px_x, drone_px_y, safe_radius_px, drone_color, cell_px);
// 5. Degradado de escape
if(drone_danger_alpha > 0){
const drone_angle = Math.atan2(dcy, dcx);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(drone_angle);
const grad_x = safe_radius_px;
const grad_radius = cell_px * 18.5;
const grad = ctx.createRadialGradient(grad_x, 0, 0, grad_x, 0, grad_radius);
const peak_color = is_drone_lost ? "rgba(220, 38, 38, " : "rgba(239, 68, 68, ";
grad.addColorStop(0, `${peak_color}${drone_danger_alpha * 0.55})`);
grad.addColorStop(0.5, `${peak_color}${drone_danger_alpha * 0.22})`);
grad.addColorStop(1, "rgba(239, 68, 68, 0)");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(grad_x, 0, grad_radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 6. Bloque de telemetría en esquina inferior izquierda (8 líneas)
const margin = cell_px * 1.0;
const hud_x = margin;
const line_spacing = cell_px * 1.35;
const hud_y = height - margin - (line_spacing * 7);
ctx.font = `bold ${cell_px * 0.95}px monospace`;
// Línea 0: Puntuación (SCR)
const score_str = current_score.toString().padStart(8, "0");
draw_hud_text(ctx, `SCR : [${score_str}]`, hud_x, hud_y, "#38bdf8", cell_px);
// Línea 1: Vidas de Drones (LVS)
const bar_w = cell_px * 8.5;
const bar_h = cell_px * 0.5;
const bar_start_x = hud_x + (cell_px * 16.5);
const lvs_y = hud_y + line_spacing;
let lvs_color = "#38bdf8";
if(current_lives <= 1){
lvs_color = (Math.sin(blink_timer * 12) > 0) ? "#ef4444" : "#7f1d1d";
}
draw_hud_text(ctx, `LVS : [ ${current_lives} ]`, hud_x, lvs_y, lvs_color, cell_px);
// Iconos de cazas/drones en la barra táctica de reservas
const slot_w = Math.min(cell_px * 1.6, bar_w / Math.max(1, max_lives));
const icon_size = cell_px * 0.9;
for(let i = 0; i < max_lives; i++){
const pip_x = bar_start_x + (i * slot_w) + (slot_w * 0.5);
const pip_y = lvs_y - (cell_px * 0.32);
const is_active = i < current_lives;
draw_drone_life_pip(ctx, pip_x, pip_y, icon_size, is_active, lvs_color, cell_px);
}
// Línea 2: Dron (DRN)
const pos_cells_x = dcx.toFixed(2);
const pos_cells_y = dcy.toFixed(2);
draw_hud_text(ctx, `DRN : X[${pos_cells_x >= 0 ? "+" : ""}${pos_cells_x}] Y[${pos_cells_y >= 0 ? "+" : ""}${pos_cells_y}]`, hud_x, hud_y + (line_spacing * 2), drone_color, cell_px);
draw_bipolar_bar(ctx, bar_start_x, (hud_y + (line_spacing * 2)) - (cell_px * 0.75), bar_w, bar_h, dcx / safe_radius_cells, drone_color, cell_px);
// Línea 3: Puntero (PTR)
const ptr_x = ptr_cx.toFixed(2);
const ptr_y = ptr_cy.toFixed(2);
draw_hud_text(ctx, `PTR : X[${ptr_x >= 0 ? "+" : ""}${ptr_x}] Y[${ptr_y >= 0 ? "+" : ""}${ptr_y}]`, hud_x, hud_y + (line_spacing * 3), ptr_color, cell_px);
draw_bipolar_bar(ctx, bar_start_x, (hud_y + (line_spacing * 3)) - (cell_px * 0.75), bar_w, bar_h, ptr_cx / safe_radius_cells, ptr_color, cell_px);
// Línea 4: Velocidad (SPD)
const spd_x = v_cell_x.toFixed(2);
const spd_y = v_cell_y.toFixed(2);
draw_hud_text(ctx, `SPD : X[${spd_x >= 0 ? "+" : ""}${spd_x}] Y[${spd_y >= 0 ? "+" : ""}${spd_y}]`, hud_x, hud_y + (line_spacing * 4), "#10b981", cell_px);
const current_speed_cells = Math.sqrt(v_cell_x * v_cell_x + v_cell_y * v_cell_y);
const spd_pct = Math.min(current_speed_cells / 15.0, 1.0);
ctx.fillStyle = "rgba(2, 6, 23, 0.88)";
ctx.fillRect(bar_start_x - (cell_px * 0.1), (hud_y + (line_spacing * 4)) - (cell_px * 0.75) - (cell_px * 0.1), bar_w + (cell_px * 0.2), bar_h + (cell_px * 0.2));
ctx.strokeStyle = "rgba(56, 189, 248, 0.65)";
ctx.lineWidth = cell_px * 0.09;
ctx.strokeRect(bar_start_x, (hud_y + (line_spacing * 4)) - (cell_px * 0.75), bar_w, bar_h);
ctx.fillStyle = "#10b981";
ctx.fillRect(bar_start_x, (hud_y + (line_spacing * 4)) - (cell_px * 0.75), bar_w * spd_pct, bar_h);
// Línea 5: Bloque de Escudo del Dron (SHD)
const shield_info = drone_info.shield !== undefined
? { current : drone_info.shield, max : drone_info.max_shield || 100 }
: { current : 100, max : 100 };
const shield_pct = Math.max(0, Math.min(1.0, shield_info.current / shield_info.max));
const shield_y = hud_y + (line_spacing * 5);
let shield_color = "#38bdf8";
let is_critical_shield = false;
if(shield_pct <= 0.25){
shield_color = (Math.sin(blink_timer * 12) > 0) ? "#ef4444" : "#7f1d1d";
is_critical_shield = true;
} else if(shield_pct <= 0.55){
shield_color = "#f59e0b";
}
draw_hud_text(ctx, `SHD : [${(shield_pct * 100).toFixed(0).padStart(3, " ")}%]`, hud_x, shield_y, shield_color, cell_px);
draw_segmented_bar(ctx, bar_start_x, shield_y - (cell_px * 0.75), bar_w, bar_h, shield_pct, shield_color, cell_px, 10);
// Línea 6: Integridad de la Nave Nodriza / Cabina (MTH)
let mothership_pct = 1.0;
if(cockpit_model && typeof cockpit_model.get_health_percentage === "function"){
mothership_pct = cockpit_model.get_health_percentage();
} else if(cockpit_model && typeof cockpit_model.get_hull === "function"){
mothership_pct = cockpit_model.get_hull() / 100.0;
} else if(typeof drone_info.mothership_pct === "number"){
mothership_pct = drone_info.mothership_pct;
}
mothership_pct = Math.max(0, Math.min(1.0, mothership_pct));
const mth_y = hud_y + (line_spacing * 6);
let mth_color = "#10b981";
let is_critical_mth = false;
if(mothership_pct <= 0.25){
mth_color = (Math.sin(blink_timer * 12) > 0) ? "#ef4444" : "#7f1d1d";
is_critical_mth = true;
} else if(mothership_pct <= 0.55){
mth_color = "#f59e0b";
}
draw_hud_text(ctx, `MTH : [${(mothership_pct * 100).toFixed(0).padStart(3, " ")}%]`, hud_x, mth_y, mth_color, cell_px);
draw_segmented_bar(ctx, bar_start_x, mth_y - (cell_px * 0.75), bar_w, bar_h, mothership_pct, mth_color, cell_px, 10);
// Línea 7: Estado del Enlace (LINK)
const link_y = hud_y + (line_spacing * 7);
let status_text = "LINK: OPTIMAL";
let status_color = "#38bdf8";
if(current_lives <= 0){
status_text = "DRONES DEPLETED - MISSION FAILED";
status_color = "#ef4444";
} else if(current_lives === 1){
status_text = (Math.sin(blink_timer * 12) > 0) ? "WARN: LAST DRONE STANDING!" : "WARN: CRITICAL RESERVES";
status_color = "#ef4444";
} else if(is_drone_lost || is_ptr_lost){
status_text = (Math.sin(blink_timer * 15) > 0) ? "LINK: SIGNAL LOST!" : "";
status_color = "#ef4444";
} else if(drone_danger_alpha > 0 || ptr_danger_alpha > 0){
status_text = (Math.sin(blink_timer * 8) > 0) ? "LINK: BOUNDARY WARNING" : "LINK: UNSTABLE";
status_color = "#f59e0b";
} else if(is_critical_mth){
status_text = "WARN: MOTHERSHIP CRITICAL";
status_color = "#ef4444";
} else if(is_critical_shield){
status_text = "WARN: SHIELD CRITICAL";
status_color = "#ef4444";
}
draw_hud_text(ctx, status_text, hud_x, link_y, status_color, cell_px);
ctx.restore();
};
};
return TelemetryHudModel;
})();