Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions controller/src/enhancements/player/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,45 @@ impl PlayerESP {

None
}

fn draw_offscreen_arrow(
&self,
draw: &imgui::DrawListMut,
position: mint::Vector2<f32>,
angle: f32,
size: f32,
color: [f32; 4],
) {
// Create arrow pointing to the right (0 radians)
// Then rotate it based on the angle
let arrow_points = [
[size, 0.0], // Tip
[-size * 0.5, size * 0.6], // Bottom
[-size * 0.5, -size * 0.6], // Top
];

let cos_angle = angle.cos();
let sin_angle = angle.sin();

let rotated_points: Vec<[f32; 2]> = arrow_points
.iter()
.map(|[x, y]| {
[
position.x + x * cos_angle - y * sin_angle,
position.y + x * sin_angle + y * cos_angle,
]
})
.collect();

draw.add_triangle(
rotated_points[0],
rotated_points[1],
rotated_points[2],
color,
)
.filled(true)
.build();
}
}

impl Enhancement for PlayerESP {
Expand Down Expand Up @@ -590,6 +629,41 @@ impl Enhancement for PlayerESP {
.build();
}
}

// Draw offscreen indicators for players not visible on screen
if esp_settings.offscreen_arrows {
// Use head position for more accurate direction, fallback to body position
let target_position = if let Some(head_bone_index) = entry_model
.bones
.iter()
.position(|bone| bone.name == "head_0")
{
pawn_model
.bone_states
.get(head_bone_index)
.map(|head_state| head_state.position)
.unwrap_or(pawn_info.position)
} else {
// If no head bone, use top of the player hull
entry_model.vhull_max + pawn_info.position
};

if let Some((indicator_pos, angle)) = view.calculate_offscreen_indicator(
&target_position,
esp_settings.offscreen_arrows_distance_from_edge_x,
esp_settings.offscreen_arrows_distance_from_edge_y,
) {
self.draw_offscreen_arrow(
&draw,
indicator_pos,
angle,
esp_settings.offscreen_arrows_size,
esp_settings
.offscreen_arrows_color
.calculate_color(player_rel_health, distance),
);
}
}
}

Ok(())
Expand Down
18 changes: 18 additions & 0 deletions controller/src/settings/esp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ pub struct EspPlayerSettings {
pub head_dot_thickness: f32,
pub head_dot_base_radius: f32,
pub head_dot_z: f32,

pub offscreen_arrows: bool,
pub offscreen_arrows_color: EspColor,
pub offscreen_arrows_size: f32,
pub offscreen_arrows_distance_from_edge_x: f32,
pub offscreen_arrows_distance_from_edge_y: f32,
}

const ESP_COLOR_FRIENDLY: EspColor = EspColor::from_rgba(0.0, 1.0, 0.0, 0.75);
Expand Down Expand Up @@ -325,6 +331,12 @@ impl EspPlayerSettings {
head_dot_thickness: 2.0,
head_dot_base_radius: 3.0,
head_dot_z: 1.0,

offscreen_arrows: false,
offscreen_arrows_color: color.clone(),
offscreen_arrows_size: 15.0,
offscreen_arrows_distance_from_edge_x: 20.0,
offscreen_arrows_distance_from_edge_y: 20.0,
}
}
}
Expand Down Expand Up @@ -380,6 +392,12 @@ impl Default for EspPlayerSettings {
head_dot_thickness: 2.0,
head_dot_base_radius: 3.0,
head_dot_z: 1.0,

offscreen_arrows: false,
offscreen_arrows_color: neutral_color,
offscreen_arrows_size: 15.0,
offscreen_arrows_distance_from_edge_x: 20.0,
offscreen_arrows_distance_from_edge_y: 20.0,
}
}
}
Expand Down
38 changes: 38 additions & 0 deletions controller/src/settings/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@ impl SettingsUI {
ui.slider_config("Max distance", 0.0, 50.0)
.build(&mut config.near_players_distance);
}

ui.dummy([0.0, 10.0]);
ui.text("Offscreen Indicators");
ui.checkbox(obfstr!("Offscreen arrows"), &mut config.offscreen_arrows);
}
}

Expand Down Expand Up @@ -793,6 +797,40 @@ impl SettingsUI {
obfstr!("Color info grenades"),
&mut config.info_grenades_color,
);

ui.table_next_row();
Self::render_esp_settings_player_style_color(
ui,
obfstr!("Offscreen arrow color"),
&mut config.offscreen_arrows_color,
);

ui.table_next_row();
Self::render_esp_settings_player_style_width(
ui,
obfstr!("Offscreen arrow size"),
5.0,
40.0,
&mut config.offscreen_arrows_size,
);

ui.table_next_row();
Self::render_esp_settings_player_style_width(
ui,
obfstr!("Offscreen arrow distance from edge x"),
20.0,
1250.0,
&mut config.offscreen_arrows_distance_from_edge_x,
);

ui.table_next_row();
Self::render_esp_settings_player_style_width(
ui,
obfstr!("Offscreen arrow distance from edge y"),
20.0,
500.0,
&mut config.offscreen_arrows_distance_from_edge_y,
);
}
}
}
Expand Down
150 changes: 150 additions & 0 deletions controller/src/view/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,154 @@ impl ViewController {
}
}
}

/// Calculate offscreen indicator position and rotation for a world position
/// Returns (screen_position, rotation_angle_radians) if the target is offscreen
pub fn calculate_offscreen_indicator(
&self,
world_pos: &nalgebra::Vector3<f32>,
distance_from_edge_x: f32,
distance_from_edge_y: f32,
) -> Option<(mint::Vector2<f32>, f32)> {
let screen_center_x = self.screen_bounds.x / 2.0;
let screen_center_y = self.screen_bounds.y / 2.0;

// Try to project to screen - if it succeeds and is within bounds, don't show indicator
if let Some(_) = self.world_to_screen(world_pos, false) {
return None;
}

let camera_pos = match self.get_camera_world_position() {
Some(pos) => pos,
None => {
return None;
}
};

// Calculate direction vector from camera to target in world space
let to_target = world_pos - camera_pos;

// Calculate distance to ensure we're not too close
let distance = to_target.norm();
if distance < 1.0 {
return None;
}

// Extract camera view vectors from view matrix
let forward_x = -self.view_matrix.m13;
let forward_y = -self.view_matrix.m23;
let forward_z = -self.view_matrix.m33;

let right_x = self.view_matrix.m11;
let right_y = self.view_matrix.m21;
let right_z = self.view_matrix.m31;

// Project target direction onto camera's forward and right vectors
let forward_dot =
to_target.x * forward_x + to_target.y * forward_y + to_target.z * forward_z;
let right_dot = to_target.x * right_x + to_target.y * right_y + to_target.z * right_z;

// Calculate angle in screen space
// right_dot positive = right side, negative = left side
// forward_dot positive = in front, negative = behind
let angle = (-right_dot).atan2(forward_dot);

let sin_angle = angle.sin();
let cos_angle = angle.cos();

// Calculate the safe zone bounds (screen bounds minus edge distances)
let safe_bounds_x = screen_center_x - distance_from_edge_x;
let safe_bounds_y = screen_center_y - distance_from_edge_y;

// Ensure safe bounds are positive
if safe_bounds_x <= 0.0 || safe_bounds_y <= 0.0 {
return None;
}

// Calculate which edge the ray intersects first
// Ray equation: point = center + t * direction
// direction = (-sin_angle, cos_angle) in screen space

let indicator_x: f32;
let indicator_y: f32;

// Calculate t values for each edge intersection
// Using a small epsilon to avoid division by very small numbers
const EPSILON: f32 = 0.001;

let t_right = if sin_angle < -EPSILON {
-safe_bounds_x / sin_angle // right edge (positive x direction)
} else {
f32::INFINITY
};

let t_left = if sin_angle > EPSILON {
safe_bounds_x / sin_angle // left edge (negative x direction)
} else {
f32::INFINITY
};

let t_top = if cos_angle > EPSILON {
safe_bounds_y / cos_angle // top edge (positive y direction from center)
} else {
f32::INFINITY
};

let t_bottom = if cos_angle < -EPSILON {
-safe_bounds_y / cos_angle // bottom edge (negative y direction from center)
} else {
f32::INFINITY
};

// Find the minimum positive t (first intersection)
let t = t_right.min(t_left).min(t_top).min(t_bottom);

if t.is_finite() && t > 0.0 {
indicator_x = screen_center_x - t * sin_angle;
indicator_y = screen_center_y + t * cos_angle;
} else {
// Fallback to old method if something goes wrong
let t_fallback = if sin_angle.abs() > cos_angle.abs() {
if sin_angle.abs() > EPSILON {
safe_bounds_x / sin_angle.abs()
} else {
safe_bounds_y / EPSILON
}
} else {
if cos_angle.abs() > EPSILON {
safe_bounds_y / cos_angle.abs()
} else {
safe_bounds_x / EPSILON
}
};
indicator_x = screen_center_x - t_fallback * sin_angle;
indicator_y = screen_center_y + t_fallback * cos_angle;
}

// Clamp to screen bounds with edge distances to prevent any out of bounds values
let final_x = indicator_x.clamp(
distance_from_edge_x,
self.screen_bounds.x - distance_from_edge_x,
);
let final_y = indicator_y.clamp(
distance_from_edge_y,
self.screen_bounds.y - distance_from_edge_y,
);

// Verify final values are valid
if !final_x.is_finite() || !final_y.is_finite() {
return None;
}

// Rotate arrow angle by 90 degrees because we want it to point towards the target
let arrow_angle = angle + std::f32::consts::PI / 2.0;

Some((
mint::Vector2 {
x: final_x,
y: final_y,
},
arrow_angle,
))
}
}
Loading