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