Compare commits

...
Author SHA1 Message Date
Ludovic Marie 8b1e45b62c Merge pull request #10079 from horizon-rust/pvp/hud-editor
🤖 Agent: merging. The editor opens only from a world (the Edit button is inactive in the title screen's options), and it doesn't blur or pause anything else. Verify is green; stacked on #10078.
2026-11-05 18:58:46 +01:00
Horizon agentandClaude Opus 5.5 3fc5f274be PvP & HUD: one stand-in line per widget in the editor
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:22:42 +01:00
Horizon agentandClaude Opus 5.5 3f164ac06a PvP & HUD: the HUD editor in main's new screen and menu matches
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:18:51 +01:00
Horizon agentandClaude Opus 5.5 ece613e260 PvP & HUD: document the HUD editor; it frames only drawn widgets
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:41 +01:00
Horizon agentandClaude Opus 5.5 5b52010b12 PvP & HUD: a HUD editor to drag, snap and resize the widgets
Edit HUD on the PvP & HUD page (in a world) or the HUD editor key
(unbound at first) opens a screen over the game: each widget in a frame,
dragged anywhere with snapping to the screen's edges and middle and to
the other widgets, the wheel resizing it, a right click sending it back
to its corner; buttons for text colour, shadow, boxes and opacity, Reset
and Done. Widgets with nothing to show yet stand in by their name. The
layout is saved to horizon-extras.txt as it changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:40 +01:00
Horizon agentandClaude Opus 5.5 7b9daf7bbf PvP & HUD: document the widgets' style and layout
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:22 +01:00
Horizon agentandClaude Opus 5.5 afc6c3c43a PvP & HUD: free widget places, sizes, text colour, opacity and shadow
Each HUD widget can leave its corner for a place kept as fractions of the
screen's free room (so it stays on screen at any size) and a size from 50
to 200%, saved as hudPlace lines in horizon-extras.txt only once moved.
The page gains the boxes' opacity, the text colour and the text shadow.
Snapping (screen edges, middle, other boxes) is here for the editor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:22 +01:00
Horizon agentandClaude Opus 5.5 69874aef48 PvP & HUD: no widget on, no HUD work; document the new widgets
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:17 +01:00
Horizon agentandClaude Opus 5.5 9f46032653 PvP & HUD: FPS, CPS, keystrokes, clock, reach and speed widgets
Six more widgets on the PvP & HUD page, off by default, each from the
client's own state: the frame counter, presses of the last second, the
movement and mouse keys while held, the local time, the distance of the
last hit the crosshair's ray measured, and horizontal speed sampled from
the player's position. Each caches its lines and rebuilds them only when
the shown value changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:17 +01:00
16 changed files with 1386 additions and 31 deletions

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,
}
}
+11 -1
View File
@@ -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) {
+218 -2
View File
@@ -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),
+451
View File
@@ -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",
+37 -2
View File
@@ -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()
}
+1
View File
@@ -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 => {
+328 -20
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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();
}
+3 -1
View File
@@ -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
View File
@@ -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)