442 lines
18 KiB
JavaScript
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;
|
|
})(); |