mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b1e45b62c | ||
|
|
3fc5f274be | ||
|
|
3f164ac06a | ||
|
|
ece613e260 | ||
|
|
5b52010b12 | ||
|
|
7b9daf7bbf | ||
|
|
afc6c3c43a | ||
|
|
69874aef48 | ||
|
|
9f46032653 |
No files matched your search
@@ -452,6 +452,7 @@ impl Extras {
|
||||
pub fn set_key(&mut self, name: &str, value: String) -> bool {
|
||||
match name {
|
||||
SORT_KEY => self.sort_key = value,
|
||||
crate::pvp::HUD_EDITOR_KEY => self.pvp.editor_key = value,
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
@@ -979,6 +980,7 @@ impl Item {
|
||||
pub fn key_name(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Item::SortKey => Some(SORT_KEY),
|
||||
Item::Pvp(crate::pvp::Opt::HudEditorKey) => Some(crate::pvp::HUD_EDITOR_KEY),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1309,6 +1309,8 @@ enum Act {
|
||||
ToggleMod(usize),
|
||||
/// PvP & HUD's PvP preset (`pvp::apply_preset`).
|
||||
PvpPreset,
|
||||
/// PvP & HUD's Edit HUD button.
|
||||
HudEditor,
|
||||
/// Pick `s.shader_packs[i]`, or turn it off when it is the pick.
|
||||
PickShaderPack(usize),
|
||||
OpenShaderPackFolder,
|
||||
@@ -1491,6 +1493,8 @@ pub enum Event {
|
||||
NativeButton(usize, usize),
|
||||
/// Roll the credits (`CreditsAndAttributionScreen.openCreditsScreen`).
|
||||
Credits,
|
||||
/// PvP & HUD's Edit HUD: open the HUD editor over the game.
|
||||
HudEditor,
|
||||
/// Copy how an agent connects (the AI Agent page).
|
||||
CopyAgentDetails,
|
||||
/// The AI Agent page's recording button: start or stop.
|
||||
@@ -1955,6 +1959,7 @@ fn tooltip_text(s: &Settings, wd: &Widget) -> Option<Text> {
|
||||
Act::Push(Page::Language) => Some("options.language.tooltip".into()),
|
||||
Act::Push(Page::Accessibility) => Some("options.accessibility.tooltip".into()),
|
||||
Act::PvpPreset => Some("horizon.pvp.preset.tooltip".into()),
|
||||
Act::HudEditor => Some("horizon.pvp.editor.tooltip".into()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(key) = key.filter(|key| lang::has(key)) {
|
||||
@@ -2523,7 +2528,10 @@ impl OptionsUi {
|
||||
let preset = button(0.0, 0.0, 150.0, tr("horizon.pvp.preset"), Act::PvpPreset);
|
||||
let mut rows = Vec::new();
|
||||
if self.page() == Page::Pvp {
|
||||
rows.push(Row::Buttons(preset, None));
|
||||
// The editor moves what the game shows: only from a world.
|
||||
let edit = button(0.0, 0.0, 150.0, tr("horizon.pvp.editor"), Act::HudEditor);
|
||||
let edit = if self.in_world { edit } else { inactive(edit) };
|
||||
rows.push(Row::Buttons(preset, Some(edit)));
|
||||
}
|
||||
let mut section = Vec::new();
|
||||
let flush = |rows: &mut Vec<Row>, section: &mut Vec<crate::extras::Item>| {
|
||||
@@ -4260,6 +4268,7 @@ impl OptionsUi {
|
||||
crate::pvp::apply_preset(s);
|
||||
events.push(Event::Changed);
|
||||
}
|
||||
Act::HudEditor => events.push(Event::HudEditor),
|
||||
Act::AccessibilityHelp => events.push(Event::AccessibilityHelp),
|
||||
Act::PickShaderPack(i) => {
|
||||
if let Some(pack) = s.shader_packs.get(i) {
|
||||
@@ -5222,6 +5231,7 @@ impl OptionsUi {
|
||||
crate::pvp::apply_preset(s);
|
||||
events.push(Event::Changed);
|
||||
}
|
||||
Act::HudEditor => events.push(Event::HudEditor),
|
||||
Act::AccessibilityHelp => events.push(Event::AccessibilityHelp),
|
||||
Act::PickShaderPack(i) => {
|
||||
if let Some(pack) = s.shader_packs.get(i) {
|
||||
|
||||
@@ -575,8 +575,53 @@ pub struct Hud {
|
||||
/// Hits in a row the server confirmed, until this player is hurt or 3 seconds pass.
|
||||
pub combo: bool,
|
||||
pub combo_corner: Corner,
|
||||
/// Frames drawn in the last second.
|
||||
pub fps: bool,
|
||||
pub fps_corner: Corner,
|
||||
/// Left and right clicks in the last second.
|
||||
pub cps: bool,
|
||||
pub cps_corner: Corner,
|
||||
/// The movement keys, mouse buttons and jump, lit while held, with clicks per second.
|
||||
pub keys: bool,
|
||||
pub keys_corner: Corner,
|
||||
/// The computer's local time.
|
||||
pub clock: bool,
|
||||
pub clock_corner: Corner,
|
||||
/// How far the last hit landed from the eyes, for 3 seconds; it only measures.
|
||||
pub reach: bool,
|
||||
pub reach_corner: Corner,
|
||||
/// Horizontal speed in blocks per second.
|
||||
pub speed: bool,
|
||||
pub speed_corner: Corner,
|
||||
/// A dark box behind each widget.
|
||||
pub background: bool,
|
||||
/// The boxes' opacity, in percent.
|
||||
pub opacity: u8,
|
||||
/// The widgets' text colour (durability keeps its own).
|
||||
pub text: Colour,
|
||||
/// A shadow under the widgets' text, as vanilla's HUD text has.
|
||||
pub shadow: bool,
|
||||
}
|
||||
impl Hud {
|
||||
/// Whether any widget is on: with none, the HUD draws nothing and reads nothing.
|
||||
pub fn any(&self) -> bool {
|
||||
[
|
||||
self.armour,
|
||||
self.effects,
|
||||
self.held,
|
||||
self.ping,
|
||||
self.coords,
|
||||
self.server,
|
||||
self.combo,
|
||||
self.fps,
|
||||
self.cps,
|
||||
self.keys,
|
||||
self.clock,
|
||||
self.reach,
|
||||
self.speed,
|
||||
]
|
||||
.contains(&true)
|
||||
}
|
||||
}
|
||||
impl Default for Hud {
|
||||
fn default() -> Self {
|
||||
@@ -596,11 +641,35 @@ impl Default for Hud {
|
||||
server_corner: Corner::TopLeft,
|
||||
combo: false,
|
||||
combo_corner: Corner::TopLeft,
|
||||
fps: false,
|
||||
fps_corner: Corner::TopLeft,
|
||||
cps: false,
|
||||
cps_corner: Corner::TopLeft,
|
||||
keys: false,
|
||||
keys_corner: Corner::BottomLeft,
|
||||
clock: false,
|
||||
clock_corner: Corner::TopRight,
|
||||
reach: false,
|
||||
reach_corner: Corner::TopLeft,
|
||||
speed: false,
|
||||
speed_corner: Corner::TopLeft,
|
||||
background: true,
|
||||
opacity: HUD_OPACITY,
|
||||
text: Colour::White,
|
||||
shadow: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use crate::extras::Corner;
|
||||
/// The widgets' boxes at first: the extras' 0x78 alpha, 47%.
|
||||
pub const HUD_OPACITY: u8 = 47;
|
||||
pub const HUD_OPACITIES: (u8, u8) = (0, 100);
|
||||
impl Hud {
|
||||
/// The boxes' colour, ARGB.
|
||||
pub fn background_argb(&self) -> u32 {
|
||||
alpha(self.opacity) << 24
|
||||
}
|
||||
}
|
||||
/// The value of the view options while they follow the world.
|
||||
fn world_label() -> String {
|
||||
crate::lang::get_or("horizon.pvp.view.world", "World's")
|
||||
@@ -682,7 +751,7 @@ impl ChatOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pvp {
|
||||
pub crosshair: Crosshair,
|
||||
pub outline: Outline,
|
||||
@@ -690,6 +759,26 @@ pub struct Pvp {
|
||||
pub hud: Hud,
|
||||
pub tweaks: Tweaks,
|
||||
pub chat: ChatOptions,
|
||||
/// Where each HUD widget sits and its size.
|
||||
pub layout: crate::pvp_hud::Layout,
|
||||
/// The key that opens the HUD editor from the game (unbound at first).
|
||||
pub editor_key: String,
|
||||
}
|
||||
/// The HUD editor key's name for the controls (`Extras::set_key`).
|
||||
pub const HUD_EDITOR_KEY: &str = "horizon.hud_editor";
|
||||
impl Default for Pvp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
crosshair: Crosshair::default(),
|
||||
outline: Outline::default(),
|
||||
hit: Hit::default(),
|
||||
hud: Hud::default(),
|
||||
tweaks: Tweaks::default(),
|
||||
chat: ChatOptions::default(),
|
||||
layout: crate::pvp_hud::Layout::default(),
|
||||
editor_key: "key.keyboard.unknown".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn flag(value: &str, d: bool) -> bool {
|
||||
match value {
|
||||
@@ -743,6 +832,15 @@ impl Pvp {
|
||||
"hudHeld" => u.held = flag(value, u.held),
|
||||
"hudHeldCorner" => u.held_corner = Corner::parse(value, u.held_corner),
|
||||
"hudBackground" => u.background = flag(value, u.background),
|
||||
"hudOpacity" => u.opacity = num(value, HUD_OPACITIES, u.opacity),
|
||||
"hudTextColour" => {
|
||||
u.text = Colour::parse(value)
|
||||
.filter(|c| *c != Colour::Inverted)
|
||||
.unwrap_or(u.text)
|
||||
}
|
||||
"hudShadow" => u.shadow = flag(value, u.shadow),
|
||||
"hudPlace" => self.layout.parse(value),
|
||||
"hudEditorKey" if value.starts_with("key.") => self.editor_key = value.to_owned(),
|
||||
"hudPing" => u.ping = flag(value, u.ping),
|
||||
"hudPingCorner" => u.ping_corner = Corner::parse(value, u.ping_corner),
|
||||
"hudCoords" => u.coords = flag(value, u.coords),
|
||||
@@ -751,6 +849,18 @@ impl Pvp {
|
||||
"hudServerCorner" => u.server_corner = Corner::parse(value, u.server_corner),
|
||||
"hudCombo" => u.combo = flag(value, u.combo),
|
||||
"hudComboCorner" => u.combo_corner = Corner::parse(value, u.combo_corner),
|
||||
"hudFps" => u.fps = flag(value, u.fps),
|
||||
"hudFpsCorner" => u.fps_corner = Corner::parse(value, u.fps_corner),
|
||||
"hudCps" => u.cps = flag(value, u.cps),
|
||||
"hudCpsCorner" => u.cps_corner = Corner::parse(value, u.cps_corner),
|
||||
"hudKeys" => u.keys = flag(value, u.keys),
|
||||
"hudKeysCorner" => u.keys_corner = Corner::parse(value, u.keys_corner),
|
||||
"hudClock" => u.clock = flag(value, u.clock),
|
||||
"hudClockCorner" => u.clock_corner = Corner::parse(value, u.clock_corner),
|
||||
"hudReach" => u.reach = flag(value, u.reach),
|
||||
"hudReachCorner" => u.reach_corner = Corner::parse(value, u.reach_corner),
|
||||
"hudSpeed" => u.speed = flag(value, u.speed),
|
||||
"hudSpeedCorner" => u.speed_corner = Corner::parse(value, u.speed_corner),
|
||||
"numericEnchants" => t.numeric_enchants = flag(value, t.numeric_enchants),
|
||||
"lowFire" => t.fire_lower = num(value, FIRE_LOWER, t.fire_lower),
|
||||
"shieldSize" => t.shield_size = num(value, SHIELD_SIZES, t.shield_size),
|
||||
@@ -838,6 +948,35 @@ impl Pvp {
|
||||
u.combo,
|
||||
u.combo_corner.name(),
|
||||
);
|
||||
let _ = write!(
|
||||
out,
|
||||
"hudFps:{}\nhudFpsCorner:{}\nhudCps:{}\nhudCpsCorner:{}\nhudKeys:{}\nhudKeysCorner:{}\n",
|
||||
u.fps,
|
||||
u.fps_corner.name(),
|
||||
u.cps,
|
||||
u.cps_corner.name(),
|
||||
u.keys,
|
||||
u.keys_corner.name(),
|
||||
);
|
||||
let _ = write!(
|
||||
out,
|
||||
"hudClock:{}\nhudClockCorner:{}\nhudReach:{}\nhudReachCorner:{}\nhudSpeed:{}\nhudSpeedCorner:{}\n",
|
||||
u.clock,
|
||||
u.clock_corner.name(),
|
||||
u.reach,
|
||||
u.reach_corner.name(),
|
||||
u.speed,
|
||||
u.speed_corner.name(),
|
||||
);
|
||||
let _ = write!(
|
||||
out,
|
||||
"hudOpacity:{}\nhudTextColour:{}\nhudShadow:{}\n",
|
||||
u.opacity,
|
||||
u.text.name(),
|
||||
u.shadow,
|
||||
);
|
||||
self.layout.serialize(out);
|
||||
let _ = writeln!(out, "hudEditorKey:{}", self.editor_key);
|
||||
let t = &self.tweaks;
|
||||
let _ = write!(
|
||||
out,
|
||||
@@ -955,7 +1094,27 @@ opts! {
|
||||
HudCombo, "hud_combo", "Combo counter",
|
||||
"Hits in a row the server confirmed, until you are hurt or 3 seconds pass without one.";
|
||||
HudComboCorner, "hud_combo_corner", "Combo corner", "Where the combo shows.";
|
||||
HudFps, "hud_fps", "FPS", "Frames drawn in the last second.";
|
||||
HudFpsCorner, "hud_fps_corner", "FPS corner", "Where the FPS shows.";
|
||||
HudCps, "hud_cps", "Clicks per second", "Your left and right clicks in the last second.";
|
||||
HudCpsCorner, "hud_cps_corner", "CPS corner", "Where the clicks per second show.";
|
||||
HudKeys, "hud_keys", "Keystrokes",
|
||||
"Your movement keys, mouse buttons and jump, lit while you hold them, with clicks per second.";
|
||||
HudKeysCorner, "hud_keys_corner", "Keystrokes corner", "Where the keystrokes show.";
|
||||
HudClock, "hud_clock", "Clock", "Your computer's time.";
|
||||
HudClockCorner, "hud_clock_corner", "Clock corner", "Where the clock shows.";
|
||||
HudReach, "hud_reach", "Reach",
|
||||
"How far from your eyes your last hit landed, for 3 seconds. It only measures: your reach stays vanilla's.";
|
||||
HudReachCorner, "hud_reach_corner", "Reach corner", "Where the reach shows.";
|
||||
HudSpeed, "hud_speed", "Speed", "How fast you move across the ground, in blocks per second.";
|
||||
HudSpeedCorner, "hud_speed_corner", "Speed corner", "Where the speed shows.";
|
||||
HudBackground, "hud_background", "Widget backgrounds", "A dark box behind each widget.";
|
||||
HudOpacity, "hud_opacity", "Background opacity", "How dark the boxes behind the widgets are.";
|
||||
HudTextColour, "hud_text_colour", "Text colour",
|
||||
"The widgets' text colour. Durability keeps its green to red.";
|
||||
HudShadow, "hud_shadow", "Text shadow", "A shadow under the widgets' text, as vanilla's HUD text has.";
|
||||
HudEditorKey, "hud_editor_key", "HUD editor key",
|
||||
"Opens the HUD editor from the game. Click, then press a key; Escape unbinds it.";
|
||||
NumericEnchants, "numeric_enchants", "Enchantment levels as numbers",
|
||||
"Tooltips and the enchanting table show Sharpness 5 instead of Sharpness V.";
|
||||
FireLower, "fire_lower", "Lower fire",
|
||||
@@ -1030,7 +1189,23 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
|
||||
HudServerCorner,
|
||||
HudCombo,
|
||||
HudComboCorner,
|
||||
HudFps,
|
||||
HudFpsCorner,
|
||||
HudCps,
|
||||
HudCpsCorner,
|
||||
HudKeys,
|
||||
HudKeysCorner,
|
||||
HudClock,
|
||||
HudClockCorner,
|
||||
HudReach,
|
||||
HudReachCorner,
|
||||
HudSpeed,
|
||||
HudSpeedCorner,
|
||||
HudBackground,
|
||||
HudOpacity,
|
||||
HudTextColour,
|
||||
HudShadow,
|
||||
HudEditorKey,
|
||||
]
|
||||
.map(Ok),
|
||||
);
|
||||
@@ -1054,7 +1229,15 @@ impl Opt {
|
||||
OutlineColour | OutlineOpacity | OutlineThickness | OutlineFill => Some(Outline),
|
||||
HitColour | HitOpacity => Some(Hit),
|
||||
HudArmour | HudEffects | HudHeld | HudPing | HudCoords | HudServer | HudCombo
|
||||
| HudBackground | NumericEnchants => None,
|
||||
| HudFps | HudCps | HudKeys | HudClock | HudReach | HudSpeed | HudBackground
|
||||
| HudTextColour | HudShadow | HudEditorKey | NumericEnchants => None,
|
||||
HudOpacity => Some(HudBackground),
|
||||
HudFpsCorner => Some(HudFps),
|
||||
HudCpsCorner => Some(HudCps),
|
||||
HudKeysCorner => Some(HudKeys),
|
||||
HudClockCorner => Some(HudClock),
|
||||
HudReachCorner => Some(HudReach),
|
||||
HudSpeedCorner => Some(HudSpeed),
|
||||
FireLower | ShieldSize | TotemSize | GlintColour | ClearWater | ClearLava => None,
|
||||
ViewTime | ViewWeather => None,
|
||||
ChatTimestamps | ChatCompact | ChatCopy | ChatSearch => None,
|
||||
@@ -1081,10 +1264,17 @@ impl Opt {
|
||||
Opt::HudEffects => p.hud.effects,
|
||||
Opt::HudHeld => p.hud.held,
|
||||
Opt::HudBackground => p.hud.background,
|
||||
Opt::HudShadow => p.hud.shadow,
|
||||
Opt::HudPing => p.hud.ping,
|
||||
Opt::HudCoords => p.hud.coords,
|
||||
Opt::HudServer => p.hud.server,
|
||||
Opt::HudCombo => p.hud.combo,
|
||||
Opt::HudFps => p.hud.fps,
|
||||
Opt::HudCps => p.hud.cps,
|
||||
Opt::HudKeys => p.hud.keys,
|
||||
Opt::HudClock => p.hud.clock,
|
||||
Opt::HudReach => p.hud.reach,
|
||||
Opt::HudSpeed => p.hud.speed,
|
||||
Opt::NumericEnchants => p.tweaks.numeric_enchants,
|
||||
Opt::ClearWater => p.tweaks.clear_water,
|
||||
Opt::ClearLava => p.tweaks.clear_lava,
|
||||
@@ -1124,6 +1314,15 @@ impl Opt {
|
||||
Opt::HudCoordsCorner => corner_label(p.hud.coords_corner),
|
||||
Opt::HudServerCorner => corner_label(p.hud.server_corner),
|
||||
Opt::HudComboCorner => corner_label(p.hud.combo_corner),
|
||||
Opt::HudFpsCorner => corner_label(p.hud.fps_corner),
|
||||
Opt::HudCpsCorner => corner_label(p.hud.cps_corner),
|
||||
Opt::HudKeysCorner => corner_label(p.hud.keys_corner),
|
||||
Opt::HudClockCorner => corner_label(p.hud.clock_corner),
|
||||
Opt::HudReachCorner => corner_label(p.hud.reach_corner),
|
||||
Opt::HudSpeedCorner => corner_label(p.hud.speed_corner),
|
||||
Opt::HudOpacity => format!("{}%", p.hud.opacity),
|
||||
Opt::HudTextColour => p.hud.text.label(),
|
||||
Opt::HudEditorKey => crate::keybinds::display_name(&p.editor_key).plain(),
|
||||
Opt::FireLower => match p.tweaks.fire_lower {
|
||||
0 => crate::lang::get_or("Off", "Off"),
|
||||
v => format!("{v}%"),
|
||||
@@ -1191,6 +1390,11 @@ impl Opt {
|
||||
Opt::HudHeld => u.held = !u.held,
|
||||
Opt::HudHeldCorner => u.held_corner = corner(u.held_corner),
|
||||
Opt::HudBackground => u.background = !u.background,
|
||||
Opt::HudOpacity => u.opacity = step_by(u.opacity, HUD_OPACITIES, 10, back),
|
||||
Opt::HudTextColour => u.text = u.text.step_solid(back),
|
||||
Opt::HudShadow => u.shadow = !u.shadow,
|
||||
// Bound through the controls' key capture (`Item::key_name`).
|
||||
Opt::HudEditorKey => {}
|
||||
Opt::HudPing => u.ping = !u.ping,
|
||||
Opt::HudPingCorner => u.ping_corner = corner(u.ping_corner),
|
||||
Opt::HudCoords => u.coords = !u.coords,
|
||||
@@ -1199,6 +1403,18 @@ impl Opt {
|
||||
Opt::HudServerCorner => u.server_corner = corner(u.server_corner),
|
||||
Opt::HudCombo => u.combo = !u.combo,
|
||||
Opt::HudComboCorner => u.combo_corner = corner(u.combo_corner),
|
||||
Opt::HudFps => u.fps = !u.fps,
|
||||
Opt::HudFpsCorner => u.fps_corner = corner(u.fps_corner),
|
||||
Opt::HudCps => u.cps = !u.cps,
|
||||
Opt::HudCpsCorner => u.cps_corner = corner(u.cps_corner),
|
||||
Opt::HudKeys => u.keys = !u.keys,
|
||||
Opt::HudKeysCorner => u.keys_corner = corner(u.keys_corner),
|
||||
Opt::HudClock => u.clock = !u.clock,
|
||||
Opt::HudClockCorner => u.clock_corner = corner(u.clock_corner),
|
||||
Opt::HudReach => u.reach = !u.reach,
|
||||
Opt::HudReachCorner => u.reach_corner = corner(u.reach_corner),
|
||||
Opt::HudSpeed => u.speed = !u.speed,
|
||||
Opt::HudSpeedCorner => u.speed_corner = corner(u.speed_corner),
|
||||
Opt::NumericEnchants => p.tweaks.numeric_enchants = !p.tweaks.numeric_enchants,
|
||||
Opt::FireLower => t.fire_lower = step_by(t.fire_lower, FIRE_LOWER, 10, back),
|
||||
Opt::ShieldSize => t.shield_size = step_by(t.shield_size, SHIELD_SIZES, 10, back),
|
||||
|
||||
@@ -214,6 +214,320 @@ impl Combo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clicks per second: the presses of the last second, kept for the widgets that are on.
|
||||
#[derive(Default)]
|
||||
pub struct Clicks {
|
||||
/// Press times in seconds, oldest first; at most [`Clicks::KEEP`].
|
||||
times: std::collections::VecDeque<f64>,
|
||||
}
|
||||
impl Clicks {
|
||||
/// More than any hand clicks in a second: the ring never grows past it.
|
||||
const KEEP: usize = 64;
|
||||
pub fn press(&mut self, now: f64) {
|
||||
if self.times.len() == Self::KEEP {
|
||||
self.times.pop_front();
|
||||
}
|
||||
self.times.push_back(now);
|
||||
}
|
||||
/// The presses in the second up to `now`.
|
||||
pub fn per_second(&mut self, now: f64) -> u32 {
|
||||
while self.times.front().is_some_and(|&t| now - t >= 1.0) {
|
||||
self.times.pop_front();
|
||||
}
|
||||
self.times.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal speed in blocks per second, from the player's own position sampled at most
|
||||
/// every [`Speed::SAMPLE`] seconds (nothing is asked of the server).
|
||||
#[derive(Default)]
|
||||
pub struct Speed {
|
||||
last: Option<([f64; 2], f64)>,
|
||||
/// Tenths of a block per second.
|
||||
tenths: u32,
|
||||
}
|
||||
impl Speed {
|
||||
pub const SAMPLE: f64 = 0.25;
|
||||
pub fn sample(&mut self, x: f64, z: f64, now: f64) -> u32 {
|
||||
match self.last {
|
||||
Some((_, at)) if now - at < Self::SAMPLE => {}
|
||||
Some(([lx, lz], at)) => {
|
||||
let d = ((x - lx).powi(2) + (z - lz).powi(2)).sqrt();
|
||||
// A teleport or respawn isn't speed.
|
||||
let v = d / (now - at);
|
||||
self.tenths = if v > 200.0 {
|
||||
0
|
||||
} else {
|
||||
(v * 10.0).round() as u32
|
||||
};
|
||||
self.last = Some(([x, z], now));
|
||||
}
|
||||
None => self.last = Some(([x, z], now)),
|
||||
}
|
||||
self.tenths
|
||||
}
|
||||
}
|
||||
|
||||
/// The reach display: how far the last hit this player made was, measured from the eyes to
|
||||
/// the point the crosshair's ray met the target's box. It measures what the client already
|
||||
/// computed for the attack and changes nothing about it.
|
||||
#[derive(Default)]
|
||||
pub struct Reach {
|
||||
last: Option<(f64, f64)>,
|
||||
}
|
||||
impl Reach {
|
||||
/// How long the last hit's distance stays shown.
|
||||
pub const SHOWN: f64 = 3.0;
|
||||
pub fn hit(&mut self, distance: f64, now: f64) {
|
||||
self.last = Some((distance, now));
|
||||
}
|
||||
/// Hundredths of a block, while the last hit is recent.
|
||||
pub fn hundredths(&self, now: f64) -> Option<u32> {
|
||||
let (d, at) = self.last?;
|
||||
(now - at <= Self::SHOWN).then(|| (d * 100.0).round() as u32)
|
||||
}
|
||||
}
|
||||
/// `n` hundredths as "3.05".
|
||||
pub fn hundredths(n: u32) -> String {
|
||||
format!("{}.{:02}", n / 100, n % 100)
|
||||
}
|
||||
/// `n` tenths as "4.3".
|
||||
pub fn tenths(n: u32) -> String {
|
||||
format!("{}.{}", n / 10, n % 10)
|
||||
}
|
||||
/// The local time as "14:05".
|
||||
pub fn clock(hour: u8, minute: u8) -> String {
|
||||
format!("{hour:02}:{minute:02}")
|
||||
}
|
||||
|
||||
/// The keystrokes widget's keys, in drawing order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Key {
|
||||
Forward,
|
||||
Left,
|
||||
Back,
|
||||
Right,
|
||||
Attack,
|
||||
Use,
|
||||
Jump,
|
||||
}
|
||||
impl Key {
|
||||
pub const ALL: [Key; 7] = [
|
||||
Key::Forward,
|
||||
Key::Left,
|
||||
Key::Back,
|
||||
Key::Right,
|
||||
Key::Attack,
|
||||
Key::Use,
|
||||
Key::Jump,
|
||||
];
|
||||
/// The key mapping it shows.
|
||||
pub fn mapping(self) -> &'static str {
|
||||
match self {
|
||||
Key::Forward => "key.forward",
|
||||
Key::Left => "key.left",
|
||||
Key::Back => "key.back",
|
||||
Key::Right => "key.right",
|
||||
Key::Attack => "key.attack",
|
||||
Key::Use => "key.use",
|
||||
Key::Jump => "key.jump",
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The keystrokes' key size and the gap between keys, in GUI pixels.
|
||||
pub const KEY: f32 = 22.0;
|
||||
pub const KEY_GAP: f32 = 2.0;
|
||||
/// The space bar's height.
|
||||
pub const SPACE: f32 = 12.0;
|
||||
/// Where each key of the keystrokes widget sits in its box: W above A S D, the two mouse
|
||||
/// buttons under them, the space bar at the bottom, as `(x, y, w, h)`.
|
||||
pub fn key_rect(key: Key) -> (f32, f32, f32, f32) {
|
||||
let step = KEY + KEY_GAP;
|
||||
let full = 3.0 * KEY + 2.0 * KEY_GAP;
|
||||
let half = (full - KEY_GAP) / 2.0;
|
||||
match key {
|
||||
Key::Forward => (step, 0.0, KEY, KEY),
|
||||
Key::Left => (0.0, step, KEY, KEY),
|
||||
Key::Back => (step, step, KEY, KEY),
|
||||
Key::Right => (2.0 * step, step, KEY, KEY),
|
||||
Key::Attack => (0.0, 2.0 * step, half, KEY),
|
||||
Key::Use => (half + KEY_GAP, 2.0 * step, half, KEY),
|
||||
Key::Jump => (0.0, 3.0 * step, full, SPACE),
|
||||
}
|
||||
}
|
||||
/// The keystrokes widget's size.
|
||||
pub fn keys_size() -> (f32, f32) {
|
||||
let (_, y, w, h) = key_rect(Key::Jump);
|
||||
(w, y + h)
|
||||
}
|
||||
|
||||
/// The HUD widgets, for their places on screen ([`Layout`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum WidgetId {
|
||||
Armour,
|
||||
Effects,
|
||||
Held,
|
||||
Ping,
|
||||
Coords,
|
||||
Server,
|
||||
Combo,
|
||||
Fps,
|
||||
Cps,
|
||||
Keys,
|
||||
Clock,
|
||||
Reach,
|
||||
Speed,
|
||||
}
|
||||
impl WidgetId {
|
||||
pub const ALL: [WidgetId; 13] = [
|
||||
WidgetId::Armour,
|
||||
WidgetId::Effects,
|
||||
WidgetId::Held,
|
||||
WidgetId::Ping,
|
||||
WidgetId::Coords,
|
||||
WidgetId::Server,
|
||||
WidgetId::Combo,
|
||||
WidgetId::Fps,
|
||||
WidgetId::Cps,
|
||||
WidgetId::Keys,
|
||||
WidgetId::Clock,
|
||||
WidgetId::Reach,
|
||||
WidgetId::Speed,
|
||||
];
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
WidgetId::Armour => "armour",
|
||||
WidgetId::Effects => "effects",
|
||||
WidgetId::Held => "held",
|
||||
WidgetId::Ping => "ping",
|
||||
WidgetId::Coords => "coords",
|
||||
WidgetId::Server => "server",
|
||||
WidgetId::Combo => "combo",
|
||||
WidgetId::Fps => "fps",
|
||||
WidgetId::Cps => "cps",
|
||||
WidgetId::Keys => "keys",
|
||||
WidgetId::Clock => "clock",
|
||||
WidgetId::Reach => "reach",
|
||||
WidgetId::Speed => "speed",
|
||||
}
|
||||
}
|
||||
pub fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a widget sits: in its corner, stacked with the others there (`at` is `None`), or at a
|
||||
/// place the player dragged it to, as fractions of the room the screen leaves around the box
|
||||
/// (0 at the left or top edge, 1 at the right or bottom), so it stays on screen at any size.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Place {
|
||||
pub at: Option<(f32, f32)>,
|
||||
/// Its size in percent.
|
||||
pub scale: u8,
|
||||
}
|
||||
impl Default for Place {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
at: None,
|
||||
scale: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub const SCALES: (u8, u8) = (50, 200);
|
||||
/// Every widget's place.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Layout(pub [Place; 13]);
|
||||
impl Layout {
|
||||
pub fn get(&self, id: WidgetId) -> Place {
|
||||
self.0[id.index()]
|
||||
}
|
||||
pub fn get_mut(&mut self, id: WidgetId) -> &mut Place {
|
||||
&mut self.0[id.index()]
|
||||
}
|
||||
/// One `hudPlace` value: `armour,0.25,0.5,150`, or `armour,corner,,150`.
|
||||
pub fn parse(&mut self, value: &str) {
|
||||
let mut parts = value.split(',');
|
||||
let Some(id) = parts
|
||||
.next()
|
||||
.and_then(|n| WidgetId::ALL.into_iter().find(|w| w.name() == n))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (x, y, scale) = (parts.next(), parts.next(), parts.next());
|
||||
let frac = |v: Option<&str>| {
|
||||
v.and_then(|v| v.parse::<f32>().ok())
|
||||
.filter(|v| v.is_finite())
|
||||
.map(|v| v.clamp(0.0, 1.0))
|
||||
};
|
||||
let place = self.get_mut(id);
|
||||
place.at = frac(x).zip(frac(y));
|
||||
if let Some(s) = scale.and_then(|s| s.parse::<u8>().ok()) {
|
||||
place.scale = s.clamp(SCALES.0, SCALES.1);
|
||||
}
|
||||
}
|
||||
/// The `hudPlace` lines of the widgets that left their corner or their size.
|
||||
pub fn serialize(&self, out: &mut String) {
|
||||
use std::fmt::Write;
|
||||
for id in WidgetId::ALL {
|
||||
let p = self.get(id);
|
||||
if p == Place::default() {
|
||||
continue;
|
||||
}
|
||||
let _ = match p.at {
|
||||
Some((x, y)) => writeln!(out, "hudPlace:{},{x:.4},{y:.4},{}", id.name(), p.scale),
|
||||
None => writeln!(out, "hudPlace:{},corner,,{}", id.name(), p.scale),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A box `w` x `h` placed at fractions `(fx, fy)` of the room left on a `screen`.
|
||||
pub fn from_fractions((fx, fy): (f32, f32), (w, h): (f32, f32), screen: (f32, f32)) -> (f32, f32) {
|
||||
(
|
||||
(fx * (screen.0 - w).max(0.0)).round(),
|
||||
(fy * (screen.1 - h).max(0.0)).round(),
|
||||
)
|
||||
}
|
||||
/// The fractions that put a `w` x `h` box at `(x, y)` on a `screen`, kept on screen.
|
||||
pub fn to_fractions((x, y): (f32, f32), (w, h): (f32, f32), screen: (f32, f32)) -> (f32, f32) {
|
||||
let frac = |v: f32, room: f32| {
|
||||
if room > 0.0 {
|
||||
(v / room).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
(frac(x, screen.0 - w), frac(y, screen.1 - h))
|
||||
}
|
||||
/// How near, in GUI pixels, an edge must come to another to snap to it.
|
||||
pub const SNAP: f32 = 4.0;
|
||||
/// Snaps a box's start `x` of length `w` along one axis of length `screen`: to the screen's
|
||||
/// edges and middle, and to the edges of the `others` boxes (`(start, length)`), where one
|
||||
/// is within [`SNAP`]. Returns the new start and the line it snapped to, if any.
|
||||
pub fn snap(x: f32, w: f32, screen: f32, others: &[(f32, f32)]) -> (f32, Option<f32>) {
|
||||
let mut best: Option<(f32, f32, f32)> = None;
|
||||
let mut consider = |start: f32, line: f32| {
|
||||
let d = (start - x).abs();
|
||||
if d <= SNAP && best.is_none_or(|(b, _, _)| d < b) {
|
||||
best = Some((d, start, line));
|
||||
}
|
||||
};
|
||||
// The screen's edges, with the corners' margin, and its middle.
|
||||
consider(2.0, 0.0);
|
||||
consider(screen - 2.0 - w, screen);
|
||||
consider((screen - w) / 2.0, screen / 2.0);
|
||||
for &(o, ow) in others {
|
||||
// Side by side, or lined up on the same edge.
|
||||
consider(o + ow, o + ow);
|
||||
consider(o - w, o);
|
||||
consider(o, o);
|
||||
consider(o + ow - w, o + ow);
|
||||
}
|
||||
match best {
|
||||
Some((_, start, line)) => (start.clamp(0.0, (screen - w).max(0.0)), Some(line)),
|
||||
None => (x.clamp(0.0, (screen - w).max(0.0)), None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -338,4 +652,141 @@ mod tests {
|
||||
assert_eq!(h, 18.0 + 10.0 + 20.0 + 2.0 * PAD - 2.0);
|
||||
assert!(size(&[]) == (0.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicks_count_the_presses_of_the_last_second() {
|
||||
let mut c = Clicks::default();
|
||||
for t in [0.0, 0.1, 0.5, 0.9] {
|
||||
c.press(t);
|
||||
}
|
||||
assert_eq!(c.per_second(0.95), 4);
|
||||
// 1.0 s after the first press it has left the window.
|
||||
assert_eq!(c.per_second(1.0), 3);
|
||||
assert_eq!(c.per_second(1.6), 1);
|
||||
assert_eq!(c.per_second(5.0), 0);
|
||||
// The ring never keeps more than 64 presses.
|
||||
for i in 0..200 {
|
||||
c.press(10.0 + f64::from(i) * 0.001);
|
||||
}
|
||||
assert_eq!(c.per_second(10.2), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_is_horizontal_blocks_per_second_between_samples() {
|
||||
let mut s = Speed::default();
|
||||
assert_eq!(s.sample(0.0, 0.0, 0.0), 0);
|
||||
// Sprinting: 5.612 blocks per second along x, with a jump (y is ignored).
|
||||
assert_eq!(s.sample(0.5, 0.0, 0.1), 0, "too soon for a new sample");
|
||||
assert_eq!(s.sample(1.403, 0.0, 0.25), 56);
|
||||
// 3 east and 4 south in a second: 5 blocks per second.
|
||||
assert_eq!(s.sample(1.403 + 3.0, 4.0, 1.25), 50);
|
||||
// A teleport reads as standing still.
|
||||
assert_eq!(s.sample(1000.0, 4.0, 1.5), 0);
|
||||
assert_eq!(tenths(56), "5.6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reach_shows_the_last_hit_for_three_seconds() {
|
||||
let mut r = Reach::default();
|
||||
assert_eq!(r.hundredths(0.0), None);
|
||||
r.hit(2.954, 1.0);
|
||||
assert_eq!(r.hundredths(1.5), Some(295));
|
||||
assert_eq!(hundredths(295), "2.95");
|
||||
assert_eq!(hundredths(300), "3.00");
|
||||
assert_eq!(r.hundredths(4.1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_clock_reads_hours_and_minutes() {
|
||||
assert_eq!(clock(14, 5), "14:05");
|
||||
assert_eq!(clock(0, 0), "00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keystrokes_keys_tile_their_box_without_overlap() {
|
||||
let (w, h) = keys_size();
|
||||
assert_eq!((w, h), (70.0, 3.0 * 24.0 + 12.0));
|
||||
for (i, a) in Key::ALL.iter().enumerate() {
|
||||
let (x, y, kw, kh) = key_rect(*a);
|
||||
assert!(
|
||||
x >= 0.0 && y >= 0.0 && x + kw <= w && y + kh <= h,
|
||||
"{a:?} inside"
|
||||
);
|
||||
for b in &Key::ALL[i + 1..] {
|
||||
let (bx, by, bw, bh) = key_rect(*b);
|
||||
let apart = x + kw <= bx || bx + bw <= x || y + kh <= by || by + bh <= y;
|
||||
assert!(apart, "{a:?} and {b:?} overlap");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn places_round_trip_and_only_moved_widgets_are_written() {
|
||||
let mut l = Layout::default();
|
||||
let mut text = String::new();
|
||||
l.serialize(&mut text);
|
||||
assert_eq!(text, "", "a vanilla layout writes nothing");
|
||||
*l.get_mut(WidgetId::Keys) = Place {
|
||||
at: Some((0.25, 1.0)),
|
||||
scale: 150,
|
||||
};
|
||||
l.get_mut(WidgetId::Fps).scale = 75;
|
||||
l.serialize(&mut text);
|
||||
assert_eq!(
|
||||
text,
|
||||
"hudPlace:fps,corner,,75\nhudPlace:keys,0.2500,1.0000,150\n"
|
||||
);
|
||||
let mut back = Layout::default();
|
||||
for line in text.lines() {
|
||||
back.parse(line.split_once(':').unwrap().1);
|
||||
}
|
||||
assert_eq!(back, l);
|
||||
// Out of range values are clamped, unknown widgets and bad numbers ignored.
|
||||
back.parse("speed,-3,7,250");
|
||||
assert_eq!(back.get(WidgetId::Speed).at, Some((0.0, 1.0)));
|
||||
assert_eq!(back.get(WidgetId::Speed).scale, 200);
|
||||
back.parse("radar,0.5,0.5,100");
|
||||
back.parse("clock,NaN,0.5,100");
|
||||
assert_eq!(back.get(WidgetId::Clock).at, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractions_keep_a_box_on_screen_at_any_size() {
|
||||
let screen = (480.0, 270.0);
|
||||
let at = to_fractions((100.0, 50.0), (80.0, 20.0), screen);
|
||||
assert_eq!(from_fractions(at, (80.0, 20.0), screen), (100.0, 50.0));
|
||||
// A box at the right edge stays at the right edge on a wider screen.
|
||||
let right = to_fractions((400.0, 0.0), (80.0, 20.0), screen);
|
||||
assert_eq!(right, (1.0, 0.0));
|
||||
assert_eq!(
|
||||
from_fractions(right, (80.0, 20.0), (960.0, 540.0)),
|
||||
(880.0, 0.0)
|
||||
);
|
||||
// Dragged past the edge: kept inside.
|
||||
assert_eq!(
|
||||
to_fractions((-30.0, 900.0), (80.0, 20.0), screen),
|
||||
(0.0, 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boxes_snap_to_screen_edges_middle_and_each_other() {
|
||||
// Near the left margin.
|
||||
assert_eq!(snap(4.5, 50.0, 480.0, &[]), (2.0, Some(0.0)));
|
||||
// Centred: the middle of 480 for a 50-wide box starts at 215.
|
||||
assert_eq!(snap(213.0, 50.0, 480.0, &[]), (215.0, Some(240.0)));
|
||||
// Beside another box (100..160): its start goes to 160.
|
||||
assert_eq!(
|
||||
snap(163.0, 50.0, 480.0, &[(100.0, 60.0)]),
|
||||
(160.0, Some(160.0))
|
||||
);
|
||||
// Lined up with its left edge.
|
||||
assert_eq!(
|
||||
snap(98.0, 50.0, 480.0, &[(100.0, 60.0)]),
|
||||
(100.0, Some(100.0))
|
||||
);
|
||||
// Too far from anything: stays, but never off screen.
|
||||
assert_eq!(snap(300.0, 50.0, 480.0, &[(100.0, 60.0)]), (300.0, None));
|
||||
assert_eq!(snap(470.0, 50.0, 480.0, &[]), (430.0, None));
|
||||
}
|
||||
}
|
||||
@@ -676,6 +676,12 @@
|
||||
"horizon.pvp.hud_held_corner.tooltip": "Where the count shows.",
|
||||
"horizon.pvp.hud_background": "Widget backgrounds",
|
||||
"horizon.pvp.hud_background.tooltip": "A dark box behind each widget.",
|
||||
"horizon.pvp.hud_opacity": "Background opacity",
|
||||
"horizon.pvp.hud_opacity.tooltip": "How dark the boxes behind the widgets are.",
|
||||
"horizon.pvp.hud_text_colour": "Text colour",
|
||||
"horizon.pvp.hud_text_colour.tooltip": "The widgets' text colour. Durability keeps its green to red.",
|
||||
"horizon.pvp.hud_shadow": "Text shadow",
|
||||
"horizon.pvp.hud_shadow.tooltip": "A shadow under the widgets' text, as vanilla's HUD text has.",
|
||||
"horizon.pvp.header.hud": "HUD widgets",
|
||||
"horizon.pvp.corner.top_left": "Top left",
|
||||
"horizon.pvp.corner.top_right": "Top right",
|
||||
@@ -705,6 +711,37 @@
|
||||
"horizon.pvp.hud.direction.south": "South",
|
||||
"horizon.pvp.hud.direction.east": "East",
|
||||
"horizon.pvp.hud.direction.west": "West",
|
||||
"horizon.pvp.hud_fps": "FPS",
|
||||
"horizon.pvp.hud_fps.tooltip": "Frames drawn in the last second.",
|
||||
"horizon.pvp.hud_fps_corner": "FPS corner",
|
||||
"horizon.pvp.hud_fps_corner.tooltip": "Where the FPS shows.",
|
||||
"horizon.pvp.hud_cps": "Clicks per second",
|
||||
"horizon.pvp.hud_cps.tooltip": "Your left and right clicks in the last second.",
|
||||
"horizon.pvp.hud_cps_corner": "CPS corner",
|
||||
"horizon.pvp.hud_cps_corner.tooltip": "Where the clicks per second show.",
|
||||
"horizon.pvp.hud_keys": "Keystrokes",
|
||||
"horizon.pvp.hud_keys.tooltip": "Your movement keys, mouse buttons and jump, lit while you hold them, with clicks per second.",
|
||||
"horizon.pvp.hud_keys_corner": "Keystrokes corner",
|
||||
"horizon.pvp.hud_keys_corner.tooltip": "Where the keystrokes show.",
|
||||
"horizon.pvp.hud_clock": "Clock",
|
||||
"horizon.pvp.hud_clock.tooltip": "Your computer's time.",
|
||||
"horizon.pvp.hud_clock_corner": "Clock corner",
|
||||
"horizon.pvp.hud_clock_corner.tooltip": "Where the clock shows.",
|
||||
"horizon.pvp.hud_reach": "Reach",
|
||||
"horizon.pvp.hud_reach.tooltip": "How far from your eyes your last hit landed, for 3 seconds. It only measures: your reach stays vanilla's.",
|
||||
"horizon.pvp.hud_reach_corner": "Reach corner",
|
||||
"horizon.pvp.hud_reach_corner.tooltip": "Where the reach shows.",
|
||||
"horizon.pvp.hud_speed": "Speed",
|
||||
"horizon.pvp.hud_speed.tooltip": "How fast you move across the ground, in blocks per second.",
|
||||
"horizon.pvp.hud_speed_corner": "Speed corner",
|
||||
"horizon.pvp.hud_speed_corner.tooltip": "Where the speed shows.",
|
||||
"horizon.pvp.hud.fps": "%s FPS",
|
||||
"horizon.pvp.hud.cps": "CPS: %s | %s",
|
||||
"horizon.pvp.hud.reach": "Reach: %s",
|
||||
"horizon.pvp.hud.speed": "Speed: %s b/s",
|
||||
"horizon.pvp.hud.keys.attack": "LMB",
|
||||
"horizon.pvp.hud.keys.use": "RMB",
|
||||
"horizon.pvp.hud.keys.cps": "%s CPS",
|
||||
"horizon.pvp.numeric_enchants": "Enchantment levels as numbers",
|
||||
"horizon.pvp.numeric_enchants.tooltip": "Tooltips and the enchanting table show Sharpness 5 instead of Sharpness V.",
|
||||
"horizon.pvp.header.items": "Items",
|
||||
@@ -749,6 +786,11 @@
|
||||
"horizon.pvp.chat_search.tooltip": "Ctrl+F in the open chat: what you type shows only the messages holding it. Enter or Ctrl+F again stops.",
|
||||
"horizon.pvp.preset": "PvP preset",
|
||||
"horizon.pvp.preset.tooltip": "Turns on the custom crosshair, the armour and effects widgets, toggle sprint and sneak, lower fire, and the FPS counter and keystrokes mods if installed. Your settings for each stay.",
|
||||
"horizon.pvp.hud_editor_key": "HUD editor key",
|
||||
"horizon.pvp.hud_editor_key.tooltip": "Opens the HUD editor from the game. Click, then press a key; Escape unbinds it.",
|
||||
"horizon.pvp.editor": "Edit HUD",
|
||||
"horizon.pvp.editor.tooltip": "Move the widgets on the game: drag them anywhere, resize them with the wheel, right click one to put it back in its corner.",
|
||||
"horizon.pvp.editor.hint": "Drag widgets · Wheel: size · Right click: back to its corner",
|
||||
"Bindings": "Bindings",
|
||||
"Device": "Device",
|
||||
"Look": "Look",
|
||||
|
||||
@@ -145,6 +145,7 @@ mod golems;
|
||||
mod growth;
|
||||
mod hand;
|
||||
mod headless;
|
||||
mod hud_editor;
|
||||
mod ime;
|
||||
mod initial_spawn;
|
||||
mod input_trace;
|
||||
@@ -863,6 +864,8 @@ enum Screen {
|
||||
/// A screen of a native mod's own (`native_mod_screen_open`): it draws and
|
||||
/// takes its input through the screen hooks.
|
||||
Mod,
|
||||
/// PvP & HUD's widget editor (game/hud_editor.rs).
|
||||
HudEditor,
|
||||
}
|
||||
impl Screen {
|
||||
/// The id native mods see for this screen (`NativeScreenView.id`, the
|
||||
@@ -891,6 +894,7 @@ impl Screen {
|
||||
Screen::Win => "win",
|
||||
Screen::DebugOptions => "debug_options",
|
||||
Screen::Mod => "mod",
|
||||
Screen::HudEditor => "hud_editor",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8292,6 +8296,7 @@ impl Game<'_> {
|
||||
| Screen::DialogWarning
|
||||
| Screen::CommandBlock
|
||||
| Screen::DebugOptions
|
||||
| Screen::HudEditor
|
||||
) || self.book.is_some()
|
||||
|| self.win_screen_pauses())
|
||||
}
|
||||
@@ -9102,6 +9107,7 @@ impl Game<'_> {
|
||||
Screen::CommandBlock => self.draw_command_block(&mut batch),
|
||||
Screen::BlockEditor => self.draw_block_editor(&mut batch),
|
||||
Screen::DebugOptions => self.draw_debug_options(&mut batch),
|
||||
Screen::HudEditor => self.draw_hud_editor(&mut batch),
|
||||
Screen::Inventory => {
|
||||
self.draw_inventory(&mut batch);
|
||||
self.draw_mod_browser(&mut batch);
|
||||
@@ -16799,7 +16805,11 @@ impl Game<'_> {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if self.sort_key_press(key) || self.hdr_key_press(key) {
|
||||
if self.sort_key_press(key)
|
||||
|| self.hdr_key_press(key)
|
||||
|| horizon_client::keybinds::key_name(key)
|
||||
.is_some_and(|name| self.hud_editor_key_press(name))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if self.remote.is_some() && self.remote_key(key)? {
|
||||
@@ -16848,6 +16858,7 @@ impl Game<'_> {
|
||||
Screen::Win => self.close_win_screen()?,
|
||||
Screen::DebugOptions => self.close_debug_options(),
|
||||
Screen::Mod => self.close_mod_screen(),
|
||||
Screen::HudEditor => self.close_hud_editor(),
|
||||
},
|
||||
// `checkHotbarKeyPressed`: the keys the controls bind to the off
|
||||
// hand and the hotbar slots, so a rebound key swaps instead of
|
||||
@@ -17023,6 +17034,9 @@ impl Game<'_> {
|
||||
}
|
||||
/// A mouse button while playing (window or control socket).
|
||||
fn play_mouse(&mut self, button: MouseButton, pressed: bool) -> Result<()> {
|
||||
if pressed {
|
||||
self.pvp_click(button);
|
||||
}
|
||||
if self.sleeping.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -18131,6 +18145,9 @@ impl Game<'_> {
|
||||
if self.screen == Screen::DebugOptions {
|
||||
self.debug_options_drag();
|
||||
}
|
||||
if self.screen == Screen::HudEditor {
|
||||
self.hud_editor_drag();
|
||||
}
|
||||
if self.screen == Screen::WorldOptions {
|
||||
self.world_options_drag();
|
||||
}
|
||||
@@ -18303,6 +18320,9 @@ impl Game<'_> {
|
||||
if self.screen == Screen::DebugOptions && d != 0.0 {
|
||||
self.debug_options_scroll(d);
|
||||
}
|
||||
if self.screen == Screen::HudEditor && d != 0.0 {
|
||||
self.hud_editor_scroll(f64::from(d));
|
||||
}
|
||||
if self.screen == Screen::Advancements {
|
||||
self.advancements_scroll(scaled(raw_x) as f32, d);
|
||||
}
|
||||
@@ -18445,7 +18465,13 @@ impl Game<'_> {
|
||||
// Neither edge of a bound button is an ordinary slot click.
|
||||
return Ok(());
|
||||
}
|
||||
if pressed && (self.sort_mouse_press(button) || self.hdr_mouse_press(button)) {
|
||||
if pressed
|
||||
&& (self.sort_mouse_press(button)
|
||||
|| self.hdr_mouse_press(button)
|
||||
|| self.hud_editor_key_press(&horizon_client::keybinds::mouse_name(
|
||||
button,
|
||||
)))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
match self.screen {
|
||||
@@ -18531,6 +18557,15 @@ impl Game<'_> {
|
||||
Screen::DebugOptions if button == MouseButton::Left => {
|
||||
self.debug_options_release()
|
||||
}
|
||||
Screen::HudEditor
|
||||
if pressed
|
||||
&& matches!(button, MouseButton::Left | MouseButton::Right) =>
|
||||
{
|
||||
self.hud_editor_press(button == MouseButton::Right)
|
||||
}
|
||||
Screen::HudEditor if button == MouseButton::Left => {
|
||||
self.hud_editor_release()
|
||||
}
|
||||
Screen::ConfirmLink if pressed && button == MouseButton::Left => {
|
||||
self.confirm_link_click()
|
||||
}
|
||||
|
||||
@@ -434,6 +434,7 @@ impl Game<'_> {
|
||||
Screen::Win => "win",
|
||||
Screen::DebugOptions => "debug-options",
|
||||
Screen::Mod => "mod",
|
||||
Screen::HudEditor => "hud-editor",
|
||||
}
|
||||
}
|
||||
pub(super) fn inventory_counts(&self) -> BTreeMap<String, i64> {
|
||||
|
||||
@@ -1257,6 +1257,7 @@ impl Game<'_> {
|
||||
Screen::Win => "win",
|
||||
Screen::DebugOptions => "debug-options",
|
||||
Screen::Mod => "mod",
|
||||
Screen::HudEditor => "hud-editor",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -526,7 +526,8 @@ fn debug_options_interrupts(screen: Screen, playing_overlay: bool) -> bool {
|
||||
| Screen::BlockEditor
|
||||
| Screen::Dead
|
||||
| Screen::Win
|
||||
| Screen::DebugOptions => false,
|
||||
| Screen::DebugOptions
|
||||
| Screen::HudEditor => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
//! The HUD editor (`docs/pvp.md`): the PvP & HUD widgets on the game, each in a frame the
|
||||
//! player drags anywhere (snapping to the screen's edges and middle and to the other
|
||||
//! widgets), resizes with the wheel and sends back to its corner with a right click; buttons
|
||||
//! for the text colour, shadow, boxes and their opacity, and Reset. Opened from the PvP & HUD
|
||||
//! page or its key. Nothing here reaches the server.
|
||||
use super::{Game, Screen};
|
||||
use horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
pvp::Opt,
|
||||
pvp_hud::{self, Layout, Place, WidgetId},
|
||||
text::Text,
|
||||
ui,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HudEditor {
|
||||
/// The widget held, and where the cursor took it from its top left corner.
|
||||
drag: Option<(WidgetId, f32, f32)>,
|
||||
/// The lines the dragged box snapped to: a vertical one at x, a horizontal one at y.
|
||||
guides: (Option<f32>, Option<f32>),
|
||||
/// Opened from the PvP & HUD page: Done goes back there.
|
||||
from_options: bool,
|
||||
}
|
||||
|
||||
/// The style buttons, two to a row, then Reset and Done.
|
||||
const STYLE: [Opt; 4] = [
|
||||
Opt::HudTextColour,
|
||||
Opt::HudShadow,
|
||||
Opt::HudBackground,
|
||||
Opt::HudOpacity,
|
||||
];
|
||||
const BUTTON_W: f32 = 150.0;
|
||||
const BUTTON_H: f32 = 20.0;
|
||||
const GAP: f32 = 4.0;
|
||||
/// A frame's colour, and the held or hovered one's.
|
||||
const FRAME: u32 = 0xB0FF_FFFF;
|
||||
const FRAME_HOT: u32 = 0xFFFF_FF55;
|
||||
const GUIDE: u32 = 0xFF55_FFFF;
|
||||
|
||||
/// The buttons' places, in the middle of the screen: style buttons, then Reset and Done.
|
||||
fn buttons((w, h): (f32, f32)) -> [(f32, f32); 6] {
|
||||
let left = (w / 2.0 - BUTTON_W - GAP / 2.0).floor();
|
||||
let right = left + BUTTON_W + GAP;
|
||||
let top = (h / 2.0 - (3.0 * BUTTON_H + 2.0 * GAP) / 2.0).floor();
|
||||
let row = |i: f32| top + i * (BUTTON_H + GAP);
|
||||
[
|
||||
(left, row(0.0)),
|
||||
(right, row(0.0)),
|
||||
(left, row(1.0)),
|
||||
(right, row(1.0)),
|
||||
(left, row(2.0)),
|
||||
(right, row(2.0)),
|
||||
]
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Opens the editor over the game, from the page (`from_options`) or the key.
|
||||
pub(super) fn open_hud_editor(&mut self, from_options: bool) {
|
||||
if self.player.is_none() || self.title.is_some() {
|
||||
return;
|
||||
}
|
||||
self.keys.clear();
|
||||
self.pvp.editor = Some(HudEditor {
|
||||
from_options,
|
||||
..HudEditor::default()
|
||||
});
|
||||
self.screen = Screen::HudEditor;
|
||||
self.capture(false);
|
||||
}
|
||||
pub(super) fn close_hud_editor(&mut self) {
|
||||
let from_options = self.pvp.editor.take().is_some_and(|e| e.from_options);
|
||||
self.settings.save(&Self::options_path());
|
||||
if from_options {
|
||||
self.open_options_at(false, horizon_client::options_ui::Page::Pvp);
|
||||
} else {
|
||||
self.screen = Screen::Playing;
|
||||
self.capture(true);
|
||||
}
|
||||
}
|
||||
/// The editor key, from the game.
|
||||
pub(super) fn hud_editor_key_press(&mut self, name: &str) -> bool {
|
||||
if self.screen != Screen::Playing
|
||||
|| self.settings.extras.pvp.editor_key != name
|
||||
|| name == "key.keyboard.unknown"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.open_hud_editor(false);
|
||||
self.screen == Screen::HudEditor
|
||||
}
|
||||
/// Whether the HUD draws the editor's stand-ins for widgets that have nothing to show.
|
||||
pub(super) fn hud_editing(&self) -> bool {
|
||||
self.screen == Screen::HudEditor
|
||||
}
|
||||
fn style_label(&self, opt: Opt) -> Text {
|
||||
let p = &self.settings.extras.pvp;
|
||||
let value = match opt.switch(p) {
|
||||
Some(true) => Text::translate("options.on", &[]),
|
||||
Some(false) => Text::translate("options.off", &[]),
|
||||
None => Text::literal(opt.value(p).unwrap_or_default()),
|
||||
};
|
||||
Text::option_name_value(Text::translate(opt.caption_key(), &[]), value)
|
||||
}
|
||||
/// The topmost widget under the cursor.
|
||||
fn widget_under_cursor(&self) -> Option<(WidgetId, [f32; 4])> {
|
||||
let (cx, cy) = self.cursor;
|
||||
self.pvp
|
||||
.boxes
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.find(|(_, [x, y, w, h])| cx >= *x && cx < x + w && cy >= *y && cy < y + h)
|
||||
}
|
||||
pub(super) fn draw_hud_editor(&mut self, batch: &mut HudBatch) {
|
||||
let atlas = &self.gui.atlas;
|
||||
let (w, h) = (batch.width, batch.height);
|
||||
let colour = |c: u32| {
|
||||
let [a, r, g, b] = c.to_be_bytes();
|
||||
rgba(r, g, b, a)
|
||||
};
|
||||
let held = self.pvp.editor.as_ref().and_then(|e| e.drag).map(|d| d.0);
|
||||
let hot = held.or(self.widget_under_cursor().map(|(id, _)| id));
|
||||
for &(id, [x, y, bw, bh]) in &self.pvp.boxes {
|
||||
let c = colour(if Some(id) == hot { FRAME_HOT } else { FRAME });
|
||||
batch.fill(atlas, x - 1.0, y - 1.0, bw + 2.0, 1.0, c);
|
||||
batch.fill(atlas, x - 1.0, y + bh, bw + 2.0, 1.0, c);
|
||||
batch.fill(atlas, x - 1.0, y, 1.0, bh, c);
|
||||
batch.fill(atlas, x + bw, y, 1.0, bh, c);
|
||||
}
|
||||
if let Some(e) = &self.pvp.editor {
|
||||
if let Some(x) = e.guides.0 {
|
||||
batch.fill(atlas, x, 0.0, 1.0, h, colour(GUIDE));
|
||||
}
|
||||
if let Some(y) = e.guides.1 {
|
||||
batch.fill(atlas, 0.0, y, w, 1.0, colour(GUIDE));
|
||||
}
|
||||
}
|
||||
let places = buttons((w, h));
|
||||
let hint = Text::translate("horizon.pvp.editor.hint", &[]);
|
||||
let hw = atlas.font.width_text(&hint);
|
||||
let top = places[0].1;
|
||||
let hx = ((w - hw) / 2.0).floor();
|
||||
batch.text_styled(atlas, &hint, hx, top - 14.0, u32::MAX, true);
|
||||
let labels = STYLE.map(|o| self.style_label(o));
|
||||
let rest = [
|
||||
Text::translate("controls.reset", &[]),
|
||||
Text::translate("gui.done", &[]),
|
||||
];
|
||||
// Opacity does nothing without the boxes.
|
||||
let boxes = self.settings.extras.pvp.hud.background;
|
||||
for (i, (label, &(x, y))) in labels.iter().chain(&rest).zip(&places).enumerate() {
|
||||
let active = i != 3 || boxes;
|
||||
let hover = active && ui::hovered(self.cursor, x, y, BUTTON_W, BUTTON_H);
|
||||
ui::button(batch, atlas, x, y, BUTTON_W, BUTTON_H, label, active, hover);
|
||||
}
|
||||
}
|
||||
/// A press: a button, else the widget under the cursor (right button: back to its corner).
|
||||
pub(super) fn hud_editor_press(&mut self, right: bool) {
|
||||
let size = self.gui_size();
|
||||
for (i, &(x, y)) in buttons(size).iter().enumerate() {
|
||||
if right || !ui::hovered(self.cursor, x, y, BUTTON_W, BUTTON_H) {
|
||||
continue;
|
||||
}
|
||||
let back = self.has_shift_down();
|
||||
let pvp = &mut self.settings.extras.pvp;
|
||||
match i {
|
||||
3 if !pvp.hud.background => return,
|
||||
0..4 => STYLE[i].cycle(pvp, back),
|
||||
4 => pvp.layout = Layout::default(),
|
||||
_ => {
|
||||
self.audio.play_button_click();
|
||||
self.close_hud_editor();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.audio.play_button_click();
|
||||
self.settings.save(&Self::options_path());
|
||||
return;
|
||||
}
|
||||
let Some((id, [x, y, _, _])) = self.widget_under_cursor() else {
|
||||
return;
|
||||
};
|
||||
if right {
|
||||
*self.settings.extras.pvp.layout.get_mut(id) = Place::default();
|
||||
self.settings.save(&Self::options_path());
|
||||
} else if let Some(e) = &mut self.pvp.editor {
|
||||
e.drag = Some((id, self.cursor.0 - x, self.cursor.1 - y));
|
||||
}
|
||||
}
|
||||
/// The cursor moved: the held widget follows, snapping to edges near it.
|
||||
pub(super) fn hud_editor_drag(&mut self) {
|
||||
let Some((id, dx, dy)) = self.pvp.editor.as_ref().and_then(|e| e.drag) else {
|
||||
return;
|
||||
};
|
||||
let Some(&(_, [_, _, w, h])) = self.pvp.boxes.iter().find(|(b, _)| *b == id) else {
|
||||
return;
|
||||
};
|
||||
let screen = self.gui_size();
|
||||
let others = || self.pvp.boxes.iter().filter(|(b, _)| *b != id);
|
||||
let xs: Vec<(f32, f32)> = others().map(|(_, r)| (r[0], r[2])).collect();
|
||||
let ys: Vec<(f32, f32)> = others().map(|(_, r)| (r[1], r[3])).collect();
|
||||
let (x, gx) = pvp_hud::snap(self.cursor.0 - dx, w, screen.0, &xs);
|
||||
let (y, gy) = pvp_hud::snap(self.cursor.1 - dy, h, screen.1, &ys);
|
||||
let at = pvp_hud::to_fractions((x, y), (w, h), screen);
|
||||
self.settings.extras.pvp.layout.get_mut(id).at = Some(at);
|
||||
if let Some(e) = &mut self.pvp.editor {
|
||||
e.guides = (gx, gy);
|
||||
}
|
||||
}
|
||||
pub(super) fn hud_editor_release(&mut self) {
|
||||
if let Some(e) = &mut self.pvp.editor
|
||||
&& e.drag.take().is_some()
|
||||
{
|
||||
e.guides = (None, None);
|
||||
self.settings.save(&Self::options_path());
|
||||
}
|
||||
}
|
||||
/// The wheel over a widget resizes it by 10%.
|
||||
pub(super) fn hud_editor_scroll(&mut self, amount: f64) {
|
||||
let Some((id, _)) = self.widget_under_cursor() else {
|
||||
return;
|
||||
};
|
||||
let place = self.settings.extras.pvp.layout.get_mut(id);
|
||||
let (lo, hi) = pvp_hud::SCALES;
|
||||
place.scale = if amount > 0.0 {
|
||||
place.scale.saturating_add(10).min(hi)
|
||||
} else {
|
||||
place.scale.saturating_sub(10).max(lo)
|
||||
};
|
||||
self.settings.save(&Self::options_path());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_buttons_sit_in_the_middle_without_overlapping() {
|
||||
let screen = (480.0, 270.0);
|
||||
let places = buttons(screen);
|
||||
for (i, &(x, y)) in places.iter().enumerate() {
|
||||
assert!(x >= 0.0 && x + BUTTON_W <= screen.0 && y >= 0.0 && y + BUTTON_H <= screen.1);
|
||||
for &(ox, oy) in &places[i + 1..] {
|
||||
let apart = x + BUTTON_W <= ox
|
||||
|| ox + BUTTON_W <= x
|
||||
|| y + BUTTON_H <= oy
|
||||
|| oy + BUTTON_H <= y;
|
||||
assert!(apart);
|
||||
}
|
||||
}
|
||||
let (left, right) = (places[0].0, places[1].0 + BUTTON_W);
|
||||
assert!((screen.0 - right - left).abs() <= 1.0, "centred");
|
||||
}
|
||||
}
|
||||
@@ -446,6 +446,7 @@ impl Game<'_> {
|
||||
Event::NativeSettings(i) => self.native_settings_refresh(i),
|
||||
Event::NativeButton(i, b) => self.native_settings_event(i, b),
|
||||
Event::Credits => self.open_win_screen(false, super::win::Finish::Options),
|
||||
Event::HudEditor => self.open_hud_editor(true),
|
||||
// AccessibilityOptionsScreen's link button: ConfirmLinkScreen
|
||||
// back to the options screen.
|
||||
Event::AccessibilityHelp => {
|
||||
|
||||
@@ -7,9 +7,11 @@ use horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
mesher::Vertex,
|
||||
pvp::{Colour, CrosshairCache},
|
||||
pvp_hud::{self, Combo, Durability, Icon, Line, Widget},
|
||||
pvp_hud::{self, Combo, Durability, Icon, Key, Line, Widget},
|
||||
text::Text,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use winit::event::MouseButton;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct PvpState {
|
||||
@@ -26,6 +28,22 @@ pub(super) struct PvpState {
|
||||
server: Widget<Option<String>>,
|
||||
combo: Combo,
|
||||
combo_count: Widget<u32>,
|
||||
fps: Widget<u32>,
|
||||
/// Left and right presses, counted while the CPS or keystrokes widget is on.
|
||||
clicks: [pvp_hud::Clicks; 2],
|
||||
cps: Widget<(u32, u32)>,
|
||||
keys: Keystrokes,
|
||||
/// The local hour and minute, and when they were read.
|
||||
clock: Widget<(u8, u8)>,
|
||||
clock_read: f64,
|
||||
reach: pvp_hud::Reach,
|
||||
reach_shown: Widget<Option<u32>>,
|
||||
speed: pvp_hud::Speed,
|
||||
speed_shown: Widget<u32>,
|
||||
/// Each widget drawn last frame and its box on screen, for the HUD editor.
|
||||
pub(super) boxes: Vec<(pvp_hud::WidgetId, [f32; 4])>,
|
||||
/// The HUD editor, while it is open.
|
||||
pub(super) editor: Option<super::hud_editor::HudEditor>,
|
||||
/// The open chat's box holds a chat search's query (Ctrl+F).
|
||||
pub(super) chat_search: bool,
|
||||
/// What the chat box held when the search borrowed it.
|
||||
@@ -33,8 +51,25 @@ pub(super) struct PvpState {
|
||||
}
|
||||
/// `GuiGraphicsExtractor`'s grey for a detail line (an effect's time left).
|
||||
const DETAIL: u32 = 0xFF7F_7F7F;
|
||||
/// The extras' box colour.
|
||||
const BACKGROUND: u32 = 0x7800_0000;
|
||||
/// How the widgets look: their boxes' colour (none without boxes), text colour and shadow.
|
||||
struct Style {
|
||||
background: Option<u32>,
|
||||
text: u32,
|
||||
shadow: bool,
|
||||
}
|
||||
/// A held key's box in the keystrokes widget, and its text.
|
||||
const KEY_HELD: u32 = 0xB4FF_FFFF;
|
||||
const KEY_HELD_TEXT: u32 = 0xFF00_0000;
|
||||
|
||||
/// The keystrokes widget: each key's label, made again only when its binding changes, which
|
||||
/// keys are held, and the clicks per second under the mouse buttons.
|
||||
#[derive(Default)]
|
||||
struct Keystrokes {
|
||||
bound: [String; 7],
|
||||
labels: Vec<(Text, f32)>,
|
||||
held: [bool; 7],
|
||||
cps: Widget<(u32, u32)>,
|
||||
}
|
||||
const ARROWS: [&str; 3] = [
|
||||
"minecraft:arrow",
|
||||
"minecraft:spectral_arrow",
|
||||
@@ -165,10 +200,35 @@ impl Game<'_> {
|
||||
_ => self.weather_levels(partial),
|
||||
}
|
||||
}
|
||||
/// This player attacked entity `id` (the combo counter waits for the server's report).
|
||||
pub(super) fn pvp_attacked(&mut self, id: i32) {
|
||||
/// This player attacked entity `id`, `distance` from the eyes to where the crosshair's
|
||||
/// ray met its box (the combo counter waits for the server's report; the reach widget
|
||||
/// shows the distance).
|
||||
pub(super) fn pvp_attacked(&mut self, id: i32, distance: f64) {
|
||||
let now = self.started.elapsed().as_secs_f64();
|
||||
self.pvp.combo.attacked(id, now);
|
||||
self.pvp.reach.hit(distance, now);
|
||||
}
|
||||
/// A mouse press, counted for clicks per second while playing with a widget that shows them.
|
||||
pub(super) fn pvp_click(&mut self, button: MouseButton) {
|
||||
let hud = &self.settings.extras.pvp.hud;
|
||||
if !(hud.cps || hud.keys) || self.screen != super::Screen::Playing {
|
||||
return;
|
||||
}
|
||||
let i = match button {
|
||||
MouseButton::Left => 0,
|
||||
MouseButton::Right => 1,
|
||||
_ => return,
|
||||
};
|
||||
let now = self.started.elapsed().as_secs_f64();
|
||||
self.pvp.clicks[i].press(now);
|
||||
}
|
||||
/// Whether the key `mapping` is bound to is held: a keyboard key or a mouse button.
|
||||
fn binding_held(&self, mapping: &str) -> bool {
|
||||
let bound = self.settings.keys.get(mapping);
|
||||
if let Some(button) = horizon_client::keybinds::mouse_button(bound) {
|
||||
return self.physical_buttons.contains(&button);
|
||||
}
|
||||
self.mapped_key_down(mapping)
|
||||
}
|
||||
/// The server reports entity `id` hurt; `own` when it is this player,
|
||||
/// `by_me` whether this player was the damage's direct entity (if named).
|
||||
@@ -188,7 +248,9 @@ impl Game<'_> {
|
||||
/// lines only when what it shows changed.
|
||||
pub(super) fn draw_pvp_hud(&mut self, batch: &mut HudBatch, corners: &mut Corners) {
|
||||
let hud = self.settings.extras.pvp.hud;
|
||||
if self.player.is_none() {
|
||||
if self.player.is_none() || !hud.any() {
|
||||
// The editor frames only what was drawn.
|
||||
self.pvp.boxes.clear();
|
||||
return;
|
||||
}
|
||||
// What each widget shows, read before its cache is borrowed.
|
||||
@@ -210,6 +272,13 @@ impl Game<'_> {
|
||||
let server = (hud.server && self.pvp.server.next.as_deref() != server)
|
||||
.then(|| server.map(str::to_owned));
|
||||
let now = self.started.elapsed().as_secs_f64();
|
||||
let fps = self.fps.round() as u32;
|
||||
let position = self.player.as_ref().map(|p| (p.position.x, p.position.z));
|
||||
if hud.keys {
|
||||
let held = Key::ALL.map(|k| self.binding_held(k.mapping()));
|
||||
self.pvp.keys.held = held;
|
||||
self.keystroke_labels();
|
||||
}
|
||||
let atlas = &self.gui.atlas;
|
||||
let width = |s: &str| HudBatch::text_width(atlas, s);
|
||||
let state = &mut self.pvp;
|
||||
@@ -338,46 +407,278 @@ impl Game<'_> {
|
||||
} else {
|
||||
state.combo_count.clear();
|
||||
}
|
||||
let background = hud.background;
|
||||
let cps = if hud.cps || hud.keys {
|
||||
[0, 1].map(|i| state.clicks[i].per_second(now))
|
||||
} else {
|
||||
[0; 2]
|
||||
};
|
||||
if hud.fps {
|
||||
state.fps.next = fps;
|
||||
state
|
||||
.fps
|
||||
.refresh(|fps, lines| lines.push(text("horizon.pvp.hud.fps", &[fps])));
|
||||
} else {
|
||||
state.fps.clear();
|
||||
}
|
||||
if hud.cps {
|
||||
state.cps.next = (cps[0], cps[1]);
|
||||
state.cps.refresh(|(l, r), lines| {
|
||||
lines.push(text("horizon.pvp.hud.cps", &[l, r]));
|
||||
});
|
||||
} else {
|
||||
state.cps.clear();
|
||||
}
|
||||
if hud.keys {
|
||||
state.keys.cps.next = (cps[0], cps[1]);
|
||||
state.keys.cps.refresh(|(l, r), lines| {
|
||||
for n in [l, r] {
|
||||
lines.push(text("horizon.pvp.hud.keys.cps", &[n]));
|
||||
}
|
||||
});
|
||||
}
|
||||
if hud.clock {
|
||||
// The time is read once a second, not every frame.
|
||||
if now - state.clock_read >= 1.0 || state.clock.lines.is_empty() {
|
||||
use chrono::Timelike;
|
||||
let t = chrono::Local::now();
|
||||
state.clock.next = (t.hour() as u8, t.minute() as u8);
|
||||
state.clock_read = now;
|
||||
}
|
||||
state.clock.refresh(|&(h, m), lines| {
|
||||
lines.push(Line::new(Icon::None, pvp_hud::clock(h, m), &width));
|
||||
});
|
||||
} else {
|
||||
state.clock.clear();
|
||||
}
|
||||
if hud.reach {
|
||||
state.reach_shown.next = state.reach.hundredths(now);
|
||||
state.reach_shown.refresh(|reach, lines| {
|
||||
if let Some(n) = reach {
|
||||
lines.push(text("horizon.pvp.hud.reach", &[&pvp_hud::hundredths(*n)]));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
state.reach_shown.clear();
|
||||
}
|
||||
if hud.speed
|
||||
&& let Some((x, z)) = position
|
||||
{
|
||||
state.speed_shown.next = state.speed.sample(x, z, now);
|
||||
state.speed_shown.refresh(|n, lines| {
|
||||
lines.push(text("horizon.pvp.hud.speed", &[&pvp_hud::tenths(*n)]));
|
||||
});
|
||||
} else if !hud.speed {
|
||||
state.speed_shown.clear();
|
||||
}
|
||||
let style = Style {
|
||||
background: hud.background.then(|| hud.background_argb()),
|
||||
text: hud.text.argb(),
|
||||
shadow: hud.shadow,
|
||||
};
|
||||
let layout = self.settings.extras.pvp.layout;
|
||||
let screen = (batch.width, batch.height);
|
||||
let mut boxes = std::mem::take(&mut self.pvp.boxes);
|
||||
boxes.clear();
|
||||
let p = &self.pvp;
|
||||
use pvp_hud::WidgetId as W;
|
||||
let widgets = [
|
||||
(
|
||||
W::Armour,
|
||||
hud.armour,
|
||||
hud.armour_corner,
|
||||
&p.armour.lines,
|
||||
p.armour.size,
|
||||
),
|
||||
(
|
||||
W::Effects,
|
||||
hud.effects,
|
||||
hud.effects_corner,
|
||||
&p.effects.lines,
|
||||
p.effects.size,
|
||||
),
|
||||
(hud.held, hud.held_corner, &p.held.lines, p.held.size),
|
||||
(hud.ping, hud.ping_corner, &p.ping.lines, p.ping.size),
|
||||
(
|
||||
W::Held,
|
||||
hud.held,
|
||||
hud.held_corner,
|
||||
&p.held.lines,
|
||||
p.held.size,
|
||||
),
|
||||
(
|
||||
W::Ping,
|
||||
hud.ping,
|
||||
hud.ping_corner,
|
||||
&p.ping.lines,
|
||||
p.ping.size,
|
||||
),
|
||||
(
|
||||
W::Coords,
|
||||
hud.coords,
|
||||
hud.coords_corner,
|
||||
&p.coords.lines,
|
||||
p.coords.size,
|
||||
),
|
||||
(
|
||||
W::Server,
|
||||
hud.server,
|
||||
hud.server_corner,
|
||||
&p.server.lines,
|
||||
p.server.size,
|
||||
),
|
||||
(
|
||||
W::Combo,
|
||||
hud.combo,
|
||||
hud.combo_corner,
|
||||
&p.combo_count.lines,
|
||||
p.combo_count.size,
|
||||
),
|
||||
(W::Fps, hud.fps, hud.fps_corner, &p.fps.lines, p.fps.size),
|
||||
(W::Cps, hud.cps, hud.cps_corner, &p.cps.lines, p.cps.size),
|
||||
(
|
||||
W::Clock,
|
||||
hud.clock,
|
||||
hud.clock_corner,
|
||||
&p.clock.lines,
|
||||
p.clock.size,
|
||||
),
|
||||
(
|
||||
W::Reach,
|
||||
hud.reach,
|
||||
hud.reach_corner,
|
||||
&p.reach_shown.lines,
|
||||
p.reach_shown.size,
|
||||
),
|
||||
(
|
||||
W::Speed,
|
||||
hud.speed,
|
||||
hud.speed_corner,
|
||||
&p.speed_shown.lines,
|
||||
p.speed_shown.size,
|
||||
),
|
||||
];
|
||||
for (on, corner, lines, size) in widgets {
|
||||
if on && !lines.is_empty() && self.native_corner_shown(corner) {
|
||||
self.paint_widget(batch, corners, corner, lines, size, background);
|
||||
// The editor shows a widget with nothing to show yet by its name.
|
||||
let editing = self.hud_editing();
|
||||
for (id, on, corner, lines, size) in widgets {
|
||||
if !on {
|
||||
continue;
|
||||
}
|
||||
let stand_in;
|
||||
let (lines, size) = match lines.is_empty() {
|
||||
false => (lines, size),
|
||||
true if editing => {
|
||||
let key = format!("horizon.pvp.hud_{}", id.name());
|
||||
let name = horizon_client::lang::get_or(&key, id.name());
|
||||
let width = |s: &str| HudBatch::text_width(&self.gui.atlas, s);
|
||||
stand_in = vec![Line::new(Icon::None, name, &width)];
|
||||
(&stand_in, pvp_hud::size(&stand_in))
|
||||
}
|
||||
true => continue,
|
||||
};
|
||||
let place = layout.get(id);
|
||||
if let Some(rect) = self.widget_rect(corners, place, corner, size, screen) {
|
||||
self.paint_widget(batch, rect, lines, size, &style);
|
||||
boxes.push((id, rect));
|
||||
}
|
||||
}
|
||||
if hud.keys {
|
||||
let place = layout.get(W::Keys);
|
||||
let size = pvp_hud::keys_size();
|
||||
if let Some(rect) = self.widget_rect(corners, place, hud.keys_corner, size, screen) {
|
||||
self.paint_keys(batch, rect, &style);
|
||||
boxes.push((W::Keys, rect));
|
||||
}
|
||||
}
|
||||
self.pvp.boxes = boxes;
|
||||
}
|
||||
/// Where a widget of `size` goes at its `place`'s scale: where the player put it, else in
|
||||
/// its corner (unless a mod hides that corner). `[x, y, w, h]`, scaled.
|
||||
fn widget_rect(
|
||||
&self,
|
||||
corners: &mut Corners,
|
||||
place: pvp_hud::Place,
|
||||
corner: Corner,
|
||||
(w, h): (f32, f32),
|
||||
screen: (f32, f32),
|
||||
) -> Option<[f32; 4]> {
|
||||
let scale = f32::from(place.scale) / 100.0;
|
||||
let size = (w * scale, h * scale);
|
||||
let (x, y) = match place.at {
|
||||
Some(at) => pvp_hud::from_fractions(at, size, screen),
|
||||
None if self.native_corner_shown(corner) => corners.place(corner, size.0, size.1),
|
||||
None => return None,
|
||||
};
|
||||
Some([x, y, size.0, size.1])
|
||||
}
|
||||
/// The keystrokes' labels: each key's name, made again only when a binding changed.
|
||||
fn keystroke_labels(&mut self) {
|
||||
let keys = &self.settings.keys;
|
||||
let k = &mut self.pvp.keys;
|
||||
let same = Key::ALL
|
||||
.iter()
|
||||
.zip(&k.bound)
|
||||
.all(|(key, b)| keys.get(key.mapping()) == b);
|
||||
if same && k.labels.len() == Key::ALL.len() {
|
||||
return;
|
||||
}
|
||||
let atlas = &self.gui.atlas;
|
||||
k.labels.clear();
|
||||
for (i, key) in Key::ALL.into_iter().enumerate() {
|
||||
let bound = keys.get(key.mapping());
|
||||
k.bound[i] = bound.to_owned();
|
||||
let label = match horizon_client::keybinds::mouse_button(bound) {
|
||||
Some(MouseButton::Left) => Text::tr("horizon.pvp.hud.keys.attack", &[]),
|
||||
Some(MouseButton::Right) => Text::tr("horizon.pvp.hud.keys.use", &[]),
|
||||
_ => horizon_client::keybinds::display_name(bound),
|
||||
};
|
||||
let w = HudBatch::text_width(atlas, &label.plain());
|
||||
k.labels.push((label, w));
|
||||
}
|
||||
}
|
||||
/// The keystrokes widget at `(x, y)`: a box per key, light while held, its name centred,
|
||||
/// and under each mouse button its clicks per second.
|
||||
fn paint_keys(&self, batch: &mut HudBatch, [x, y, sw, _]: [f32; 4], style: &Style) {
|
||||
let start = batch.vertices.len();
|
||||
let atlas = &self.gui.atlas;
|
||||
let k = &self.pvp.keys;
|
||||
let colour = |c: u32| {
|
||||
let [a, r, g, b] = c.to_be_bytes();
|
||||
rgba(r, g, b, a)
|
||||
};
|
||||
for (i, key) in Key::ALL.into_iter().enumerate() {
|
||||
let (kx, ky, w, h) = pvp_hud::key_rect(key);
|
||||
let (kx, ky) = (x + kx, y + ky);
|
||||
let held = k.held[i];
|
||||
if held {
|
||||
batch.fill(atlas, kx, ky, w, h, colour(KEY_HELD));
|
||||
} else if let Some(background) = style.background {
|
||||
batch.fill(atlas, kx, ky, w, h, colour(background));
|
||||
}
|
||||
let text_colour = colour(if held { KEY_HELD_TEXT } else { style.text });
|
||||
let Some((label, lw)) = k.labels.get(i) else {
|
||||
continue;
|
||||
};
|
||||
let cps = match key {
|
||||
Key::Attack => k.cps.lines.first(),
|
||||
Key::Use => k.cps.lines.get(1),
|
||||
_ => None,
|
||||
};
|
||||
let top = match cps {
|
||||
Some(_) => ky + 2.0,
|
||||
None => ky + (h - 8.0) / 2.0,
|
||||
};
|
||||
let shadow = style.shadow && !held;
|
||||
batch.text_styled(atlas, label, kx + (w - lw) / 2.0, top, text_colour, shadow);
|
||||
if let Some(line) = cps {
|
||||
// The count at half size under the name.
|
||||
let start = batch.vertices.len();
|
||||
let (cx, cy) = (kx + w / 2.0, ky + h - 6.0);
|
||||
let tx = cx - line.text_width / 2.0;
|
||||
batch.text_styled(atlas, &line.text, tx, cy, text_colour, false);
|
||||
batch.scale_since(start, cx, cy, 0.5, 0.5);
|
||||
}
|
||||
}
|
||||
let scale = sw / pvp_hud::keys_size().0;
|
||||
if scale != 1.0 {
|
||||
batch.scale_since(start, x, y, scale, scale);
|
||||
}
|
||||
}
|
||||
/// The held item count: the first slot holding what is counted, its item, and the total in
|
||||
@@ -403,16 +704,15 @@ impl Game<'_> {
|
||||
fn paint_widget(
|
||||
&self,
|
||||
batch: &mut HudBatch,
|
||||
corners: &mut Corners,
|
||||
corner: Corner,
|
||||
[x, y, sw, _]: [f32; 4],
|
||||
lines: &[Line],
|
||||
(w, h): (f32, f32),
|
||||
background: bool,
|
||||
style: &Style,
|
||||
) {
|
||||
let start = batch.vertices.len();
|
||||
let atlas = &self.gui.atlas;
|
||||
let (x, y) = corners.place(corner, w, h);
|
||||
if background {
|
||||
let [a, r, g, b] = BACKGROUND.to_be_bytes();
|
||||
if let Some(background) = style.background {
|
||||
let [a, r, g, b] = background.to_be_bytes();
|
||||
batch.fill(atlas, x, y, w, h, rgba(r, g, b, a));
|
||||
}
|
||||
let icons = pvp_hud::icon_column(lines);
|
||||
@@ -446,11 +746,19 @@ impl Game<'_> {
|
||||
let [a, r, g, b] = c.to_be_bytes();
|
||||
rgba(r, g, b, a)
|
||||
};
|
||||
batch.text_styled(atlas, &line.text, tx, ty, argb(line.colour), true);
|
||||
// White lines take the widgets' text colour; durability keeps its own.
|
||||
let colour = match line.colour {
|
||||
0xFFFF_FFFF => style.text,
|
||||
c => c,
|
||||
};
|
||||
batch.text_styled(atlas, &line.text, tx, ty, argb(colour), style.shadow);
|
||||
if let Some(detail) = &line.detail {
|
||||
batch.text_styled(atlas, detail, tx, ty + 10.0, argb(DETAIL), true);
|
||||
batch.text_styled(atlas, detail, tx, ty + 10.0, argb(DETAIL), style.shadow);
|
||||
}
|
||||
top += line.height();
|
||||
}
|
||||
if sw != w {
|
||||
batch.scale_since(start, x, y, sw / w, sw / w);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9042,7 +9042,7 @@ impl Game<'_> {
|
||||
self.send(S::Attack(play::Attack {
|
||||
id: self.mobs[i].id as i32,
|
||||
}));
|
||||
self.pvp_attacked(self.mobs[i].id as i32);
|
||||
self.pvp_attacked(self.mobs[i].id as i32, distance);
|
||||
self.remote_player_attack(i);
|
||||
self.reset_attack_strength();
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ impl Screen {
|
||||
| Self::Dead
|
||||
| Self::CommandBlock
|
||||
| Self::BlockEditor
|
||||
| Self::Mod => false,
|
||||
| Self::Mod
|
||||
// The HUD editor shows the HUD it moves.
|
||||
| Self::HudEditor => false,
|
||||
// WinScreen.extractBackground: the poem fills the screen with the End portal, the
|
||||
// credits alone blur. Game::menu_blur knows which one rolls.
|
||||
Self::Win => false,
|
||||
|
||||
@@ -611,7 +611,8 @@ impl Loading {
|
||||
super::packs::open_uri(super::options::ACCESSIBILITY_HELP_LINK)
|
||||
}
|
||||
// Mods' settings pages open only in a running game.
|
||||
Event::NativeSettings(_) | Event::NativeButton(..) => {}
|
||||
// The HUD editor needs a world: its button is inactive here.
|
||||
Event::NativeSettings(_) | Event::NativeButton(..) | Event::HudEditor => {}
|
||||
// The startup menu runs before the game opens a control socket,
|
||||
// and before its sound engine (no clicks, no volume previews,
|
||||
// and no music tracks to list or play).
|
||||
|
||||
+31
-2
@@ -6,8 +6,10 @@ off by default: a fresh install looks exactly like vanilla 26.3, and turning one
|
||||
## Fair play
|
||||
|
||||
Nothing here tells the player more than vanilla does or plays for them, so the game stays fine on servers
|
||||
that ban unfair clients. That rules out, on purpose: a reach display, entities on the minimap, freelook,
|
||||
auto-clicking or any other automatic action, seeing through walls, and hitboxes beyond vanilla's F3+B.
|
||||
that ban unfair clients. That rules out, on purpose: entities on the minimap, freelook, auto-clicking or any
|
||||
other automatic action, seeing through walls, and hitboxes beyond vanilla's F3+B. The HUD widgets read only
|
||||
what this client already has: they send no packet and ask the server nothing, so an anticheat sees nothing new.
|
||||
The reach widget measures your own last hit and never changes reach.
|
||||
Three options do see further than vanilla would at that moment, and some strict servers ban fog or light
|
||||
changes: clear water and clear lava (below), and a fixed time of day, whose sky light brightens a night.
|
||||
They are off by default; check a server's rules before turning them on there.
|
||||
@@ -74,6 +76,33 @@ box's quads. With nothing changing, a frame allocates nothing.
|
||||
as the damage's direct entity (`ClientboundDamageEventPacket`: melee and spear hits, not arrows or fire). A
|
||||
report that names no attacker counts once, within a second of a swing at that entity. Back to 0 when you are hurt or
|
||||
3 seconds pass without a hit. It counts what the game already shows (the target's hurt flash).
|
||||
- **FPS** (`hudFps`, top left): the frames drawn in the last second, as F3's first line counts them.
|
||||
- **Clicks per second** (`hudCps`, top left): your left and right presses in the last second, "CPS: 8 | 3".
|
||||
Counted from the mouse buttons while playing (no screen open); nothing is sent for them.
|
||||
- **Keystrokes** (`hudKeys`, bottom left): W A S D, both mouse buttons with their clicks per second, and jump,
|
||||
lit while held. The letters follow your key bindings.
|
||||
- **Clock** (`hudClock`, top right): your computer's local time, HH:MM.
|
||||
- **Reach** (`hudReach`, top left): for 3 seconds after you hit an entity, how far from your eyes the hit
|
||||
landed (the pick's distance to its box), "Reach: 2.87". It reads the pick the attack already used.
|
||||
- **Speed** (`hudSpeed`, top left): your speed across the ground in blocks per second, over the last quarter
|
||||
second; a teleport reads 0.
|
||||
|
||||
Each `hud…` option has a `hud…Corner` beside it. With every widget off, a frame costs one test of the options.
|
||||
|
||||
Style, for every widget: **Background opacity** (`hudOpacity`, 0 to 100, default 47, under the background
|
||||
switch), **Text colour** (`hudTextColour`, default white; coloured lines such as durability keep their colour)
|
||||
and **Text shadow** (`hudShadow`, default on).
|
||||
|
||||
Layout: a widget moved by hand stays where it was put, as `hudPlace:<widget>,<x>,<y>,<scale>` lines (`x` and `y`
|
||||
are fractions of the room the screen leaves beside the widget, so it stays on screen after a resize; `corner` instead of them keeps the corner; scale in
|
||||
percent, 50 to 200). Widgets without such a line keep their corner. `pvp_hud::Layout`, `pvp_hud::snap`.
|
||||
|
||||
**HUD editor**: the **Edit HUD** button on the PvP & HUD page beside the preset (in a world), or the **HUD editor key** (unbound by
|
||||
default). The game pauses where it can; each widget that is on gets a frame. Drag one anywhere: it snaps to the
|
||||
screen's edges and middle and to the other widgets' edges (the guides show where). The mouse wheel over a widget
|
||||
scales it by 10%, a right click puts it back in its corner at 100%, and the buttons set the text colour, shadow,
|
||||
background and its opacity, put every widget back in its corner at 100% (Reset), or close the editor (Esc too). The layout is saved when it closes.
|
||||
Code: `game/hud_editor.rs`.
|
||||
|
||||
## Enchantment levels as numbers (`numericEnchants`, default off)
|
||||
|
||||
|
||||
Reference in new issue
Block a user