mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #9244 from horizon-rust/pvp/outline-hit
🤖 Agent: block outline and hit colour stay render-only and off by default; verify and parity pass at 4adfac92b. The composter fix #9508 is merged in. pack_screen's failures came from the playhorizon runner missing 142 vanilla lang files: the files were copied there and it passes now.
This commit is contained in:
13 files changed
+371
-12
No files matched your search
@@ -355,6 +355,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -87,7 +87,11 @@ fn entity_light(normal: vec3<f32>, nether: bool) -> f32 {
|
||||
// primed TNT flash (5) and at column overlay - 16 for 16..=31 (a creeper's
|
||||
// swelling). Alpha 1 changes nothing.
|
||||
fn overlay_texel(overlay: u32) -> vec4<f32> {
|
||||
if overlay == 1u || overlay == 8u { return HURT_OVERLAY; }
|
||||
if overlay == 1u || overlay == 8u {
|
||||
// PvP & HUD's hit colour (pvp.rs `Hit::texel`) rides in colored_count.y; 0 is vanilla's.
|
||||
let hit = globals.colored_count.y;
|
||||
return select(HURT_OVERLAY, unpack4x8unorm(hit), hit != 0u);
|
||||
}
|
||||
if overlay == 5u { return vec4(1.0, 1.0, 1.0, WHITE_OVERLAY_ALPHA[15]); }
|
||||
if overlay >= 16u && overlay < 32u { return vec4(1.0, 1.0, 1.0, WHITE_OVERLAY_ALPHA[overlay - 16u]); }
|
||||
return vec4(1.0);
|
||||
|
||||
@@ -883,6 +883,8 @@ pub struct Frame<'a> {
|
||||
pub outline_after_terrain: bool,
|
||||
/// `BlockOutlineRenderState.highContrast`: a thick black outline under a bright one.
|
||||
pub outline_high_contrast: bool,
|
||||
/// The PvP & HUD page's outline and hit colour (`pvp::Frame`; default: vanilla's).
|
||||
pub pvp: crate::pvp::Frame,
|
||||
/// `lines` with a colour and pixel width (`gizmos::camera_relative`):
|
||||
/// the fishing line and debug gizmos, depth tested, then gizmos drawn
|
||||
/// over everything.
|
||||
@@ -7252,7 +7254,8 @@ impl Renderer {
|
||||
},
|
||||
colored_count: [
|
||||
frame.colored_lights.len().min(crate::dynamic_light::MAX) as u32,
|
||||
0,
|
||||
// PvP & HUD's hit colour: entity.wgsl's hurt texel, 0 for vanilla's.
|
||||
frame.pvp.hurt_texel,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
@@ -8133,12 +8136,14 @@ impl Renderer {
|
||||
line
|
||||
})
|
||||
};
|
||||
// A custom outline (PvP & HUD) takes the bright stroke's colour, scales both widths.
|
||||
let (custom, scale) = frame.pvp.outline.map_or((None, 1.0), |(c, s)| (Some(c), s));
|
||||
let lines: Vec<[f32; 8]> = if frame.outline_high_contrast {
|
||||
stroke(0xff00_0000, 7.0)
|
||||
.chain(stroke(0xff57_ffe1, width))
|
||||
stroke(0xff00_0000, 7.0 * scale)
|
||||
.chain(stroke(custom.unwrap_or(0xff57_ffe1), width * scale))
|
||||
.collect()
|
||||
} else {
|
||||
stroke(0x6600_0000, width).collect()
|
||||
stroke(custom.unwrap_or(0x6600_0000), width * scale).collect()
|
||||
};
|
||||
self.uploads
|
||||
.buffer(&mut encoder, &self.outline, 0, bytemuck::cast_slice(&lines));
|
||||
|
||||
@@ -517,6 +517,7 @@ impl Scene {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -595,6 +595,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -438,6 +438,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -328,6 +328,7 @@ fn frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -1447,6 +1447,7 @@ mod initial_history_tests {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! PvP and HUD options (`docs/pvp.md`): a custom crosshair. Kept in
|
||||
//! PvP and HUD options (`docs/pvp.md`): a custom crosshair, block outline and hit colour. Kept in
|
||||
//! `horizon-extras.txt` with the other extras ([`crate::extras::Extras::pvp`]) and shown on
|
||||
//! Horizon Settings > PvP & HUD. Every option is off by default, so the game looks like
|
||||
//! vanilla until the player changes one, and none of them shows anything vanilla doesn't
|
||||
@@ -71,7 +71,7 @@ impl Colour {
|
||||
pub fn argb(self) -> u32 {
|
||||
match self {
|
||||
Colour::Inverted | Colour::White => 0xFFFF_FFFF,
|
||||
Colour::Red => 0xFFFF_3030,
|
||||
Colour::Red => 0xFFFF_0000,
|
||||
Colour::Orange => 0xFFFF_A020,
|
||||
Colour::Yellow => 0xFFFF_FF40,
|
||||
Colour::Green => 0xFF40_FF40,
|
||||
@@ -87,6 +87,18 @@ impl Colour {
|
||||
fn step(self, back: bool) -> Colour {
|
||||
step(&Self::ALL, self, back)
|
||||
}
|
||||
/// The next solid colour (`Inverted` left out).
|
||||
fn step_solid(self, back: bool) -> Colour {
|
||||
step(&Self::ALL[1..], self, back)
|
||||
}
|
||||
/// The colour at `percent` opacity, as ARGB.
|
||||
pub fn with_opacity(self, percent: u8) -> u32 {
|
||||
self.argb() & 0x00FF_FFFF | alpha(percent) << 24
|
||||
}
|
||||
}
|
||||
/// `percent` of 255, rounded.
|
||||
fn alpha(percent: u8) -> u32 {
|
||||
(u32::from(percent.min(100)) * 255 + 50) / 100
|
||||
}
|
||||
|
||||
/// The next (or previous) value of `all` after `v`, wrapping.
|
||||
@@ -97,11 +109,15 @@ fn step<T: Copy + PartialEq>(all: &[T], v: T, back: bool) -> T {
|
||||
}
|
||||
/// `v` one step up or down within `lo..=hi`, wrapping.
|
||||
fn step_num(v: u8, lo: u8, hi: u8, back: bool) -> u8 {
|
||||
step_by(v, (lo, hi), 1, back)
|
||||
}
|
||||
/// `v` `by` up or down within `lo..=hi`, wrapping.
|
||||
fn step_by(v: u8, (lo, hi): (u8, u8), by: u8, back: bool) -> u8 {
|
||||
match (back, v) {
|
||||
(false, v) if v >= hi => lo,
|
||||
(false, v) => v + 1,
|
||||
(false, v) => (v + by).min(hi),
|
||||
(true, v) if v <= lo => hi,
|
||||
(true, v) => v - 1,
|
||||
(true, v) => v.saturating_sub(by).max(lo),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,9 +355,104 @@ impl CrosshairCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// The block outline (`LevelRenderer.submitBlockOutline`). Off, vanilla's: black at 40%, or the
|
||||
/// accessibility option's high contrast one.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Outline {
|
||||
pub custom: bool,
|
||||
/// A solid colour, at `opacity` percent.
|
||||
pub colour: Colour,
|
||||
pub opacity: u8,
|
||||
/// Times vanilla's width (`Window.getAppropriateLineWidth`).
|
||||
pub thickness: u8,
|
||||
/// The block's faces tinted with this colour, lightly.
|
||||
pub fill: Option<Colour>,
|
||||
}
|
||||
pub const OPACITIES: (u8, u8) = (10, 100);
|
||||
pub const OUTLINE_THICKNESSES: (u8, u8) = (1, 4);
|
||||
/// How much the fill covers the block's faces, in percent.
|
||||
pub const FILL_OPACITY: u8 = 25;
|
||||
impl Default for Outline {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
custom: false,
|
||||
colour: Colour::Black,
|
||||
opacity: 40,
|
||||
thickness: 1,
|
||||
fill: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Outline {
|
||||
/// The stroke's ARGB and its width as a multiple of vanilla's; none for vanilla's.
|
||||
pub fn stroke(&self) -> Option<(u32, f32)> {
|
||||
self.custom.then(|| {
|
||||
(
|
||||
self.colour.with_opacity(self.opacity),
|
||||
f32::from(self.thickness),
|
||||
)
|
||||
})
|
||||
}
|
||||
/// The fill's ARGB, when there is one.
|
||||
pub fn fill(&self) -> Option<u32> {
|
||||
self.fill
|
||||
.filter(|_| self.custom)
|
||||
.map(|c| c.with_opacity(FILL_OPACITY))
|
||||
}
|
||||
}
|
||||
/// The tint a hurt entity flashes (`OverlayTexture`'s red rows). Off, vanilla's: red over 30%.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Hit {
|
||||
pub custom: bool,
|
||||
pub colour: Colour,
|
||||
/// How much of the colour covers the entity, in percent.
|
||||
pub opacity: u8,
|
||||
}
|
||||
pub const HIT_OPACITIES: (u8, u8) = (0, 100);
|
||||
impl Default for Hit {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
custom: false,
|
||||
colour: Colour::Red,
|
||||
opacity: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Hit {
|
||||
/// The overlay texel as RGBA bytes in a little-endian u32 (WGSL's `unpack4x8unorm`), its alpha
|
||||
/// what of the entity's own colour stays (`entity.fsh` mixes by it); 0 for vanilla's texel.
|
||||
pub fn texel(&self) -> u32 {
|
||||
if !self.custom {
|
||||
return 0;
|
||||
}
|
||||
let [_, r, g, b] = self.colour.argb().to_be_bytes();
|
||||
u32::from_le_bytes([r, g, b, (255 - alpha(self.opacity)) as u8])
|
||||
}
|
||||
}
|
||||
|
||||
/// What the renderer reads of these options each frame (`FrameInputs::pvp`); the default is
|
||||
/// vanilla's look.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Frame {
|
||||
/// The block outline's stroke ([`Outline::stroke`]).
|
||||
pub outline: Option<(u32, f32)>,
|
||||
/// The hurt overlay texel ([`Hit::texel`]), 0 for vanilla's.
|
||||
pub hurt_texel: u32,
|
||||
}
|
||||
impl Pvp {
|
||||
pub fn frame(&self) -> Frame {
|
||||
Frame {
|
||||
outline: self.outline.stroke(),
|
||||
hurt_texel: self.hit.texel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Pvp {
|
||||
pub crosshair: Crosshair,
|
||||
pub outline: Outline,
|
||||
pub hit: Hit,
|
||||
}
|
||||
fn flag(value: &str, d: bool) -> bool {
|
||||
match value {
|
||||
@@ -357,6 +468,7 @@ impl Pvp {
|
||||
/// One `key:value` line of `horizon-extras.txt`; false when the key isn't a PvP one.
|
||||
pub fn parse_line(&mut self, key: &str, value: &str) -> bool {
|
||||
let c = &mut self.crosshair;
|
||||
let (o, h) = (&mut self.outline, &mut self.hit);
|
||||
match key {
|
||||
"crosshair" => c.custom = flag(value, c.custom),
|
||||
"crosshairShape" => c.shape = Shape::parse(value).unwrap_or(c.shape),
|
||||
@@ -372,6 +484,19 @@ impl Pvp {
|
||||
v => Colour::parse(v).or(c.hover),
|
||||
}
|
||||
}
|
||||
"outline" => o.custom = flag(value, o.custom),
|
||||
"outlineColour" => o.colour = Colour::parse(value).unwrap_or(o.colour),
|
||||
"outlineOpacity" => o.opacity = num(value, OPACITIES, o.opacity),
|
||||
"outlineThickness" => o.thickness = num(value, OUTLINE_THICKNESSES, o.thickness),
|
||||
"outlineFill" => {
|
||||
o.fill = match value {
|
||||
"off" => None,
|
||||
v => Colour::parse(v).or(o.fill),
|
||||
}
|
||||
}
|
||||
"hit" => h.custom = flag(value, h.custom),
|
||||
"hitColour" => h.colour = Colour::parse(value).unwrap_or(h.colour),
|
||||
"hitOpacity" => h.opacity = num(value, HIT_OPACITIES, h.opacity),
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
@@ -393,6 +518,19 @@ impl Pvp {
|
||||
c.dot,
|
||||
c.hover.map_or("off", Colour::name),
|
||||
);
|
||||
let (o, h) = (&self.outline, &self.hit);
|
||||
let _ = write!(
|
||||
out,
|
||||
"outline:{}\noutlineColour:{}\noutlineOpacity:{}\noutlineThickness:{}\noutlineFill:{}\nhit:{}\nhitColour:{}\nhitOpacity:{}\n",
|
||||
o.custom,
|
||||
o.colour.name(),
|
||||
o.opacity,
|
||||
o.thickness,
|
||||
o.fill.map_or("off", Colour::name),
|
||||
h.custom,
|
||||
h.colour.name(),
|
||||
h.opacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +571,15 @@ opts! {
|
||||
CrosshairDot, "crosshair_dot", "Centre dot", "A dot in the middle of the crosshair.";
|
||||
CrosshairHover, "crosshair_hover", "Colour on entities",
|
||||
"Another colour while the crosshair is over a mob or a player you could hit, as vanilla's attack indicator knows it.";
|
||||
Outline, "outline", "Custom block outline", "Changes the outline around the block you look at.";
|
||||
OutlineColour, "outline_colour", "Colour", "The outline's colour. Black at 40% is vanilla's.";
|
||||
OutlineOpacity, "outline_opacity", "Opacity", "How solid the outline is.";
|
||||
OutlineThickness, "outline_thickness", "Thickness", "The outline's width, in times vanilla's.";
|
||||
OutlineFill, "outline_fill", "Fill", "Tints the faces of the block you look at, lightly.";
|
||||
Hit, "hit", "Custom hit colour",
|
||||
"Changes the colour mobs and players flash when they are hurt. Red at 30% is vanilla's.";
|
||||
HitColour, "hit_colour", "Colour", "The colour hurt mobs and players flash.";
|
||||
HitOpacity, "hit_opacity", "Opacity", "How much of the colour covers them.";
|
||||
}
|
||||
/// The PvP & HUD page: headers (language keys) and options.
|
||||
pub fn rows() -> Vec<Result<Opt, &'static str>> {
|
||||
@@ -452,14 +599,30 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
|
||||
]
|
||||
.map(Ok),
|
||||
);
|
||||
rows.push(Err("horizon.pvp.header.outline"));
|
||||
rows.extend(
|
||||
[
|
||||
Outline,
|
||||
OutlineColour,
|
||||
OutlineOpacity,
|
||||
OutlineThickness,
|
||||
OutlineFill,
|
||||
]
|
||||
.map(Ok),
|
||||
);
|
||||
rows.push(Err("horizon.pvp.header.hit"));
|
||||
rows.extend([Hit, HitColour, HitOpacity].map(Ok));
|
||||
rows
|
||||
}
|
||||
impl Opt {
|
||||
/// The switch this option belongs to.
|
||||
pub fn parent(self) -> Option<Opt> {
|
||||
use Opt::*;
|
||||
match self {
|
||||
Opt::Crosshair => None,
|
||||
_ => Some(Opt::Crosshair),
|
||||
Crosshair | Outline | Hit => None,
|
||||
OutlineColour | OutlineOpacity | OutlineThickness | OutlineFill => Some(Outline),
|
||||
HitColour | HitOpacity => Some(Hit),
|
||||
_ => Some(Crosshair),
|
||||
}
|
||||
}
|
||||
pub fn switch(self, p: &Pvp) -> Option<bool> {
|
||||
@@ -468,6 +631,8 @@ impl Opt {
|
||||
Opt::Crosshair => c.custom,
|
||||
Opt::CrosshairOutline => c.outline,
|
||||
Opt::CrosshairDot => c.dot,
|
||||
Opt::Outline => p.outline.custom,
|
||||
Opt::Hit => p.hit.custom,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -484,12 +649,29 @@ impl Opt {
|
||||
Some(colour) => colour.label(),
|
||||
None => crate::lang::get_or("Off", "Off"),
|
||||
},
|
||||
Opt::OutlineColour => p.outline.colour.label(),
|
||||
Opt::OutlineOpacity => format!("{}%", p.outline.opacity),
|
||||
Opt::OutlineThickness => format!("{}x", p.outline.thickness),
|
||||
Opt::OutlineFill => match p.outline.fill {
|
||||
Some(colour) => colour.label(),
|
||||
None => crate::lang::get_or("Off", "Off"),
|
||||
},
|
||||
Opt::HitColour => p.hit.colour.label(),
|
||||
Opt::HitOpacity => format!("{}%", p.hit.opacity),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// A click; `back` (right button, wheel up) goes the other way.
|
||||
pub fn cycle(self, p: &mut Pvp, back: bool) {
|
||||
let c = &mut p.crosshair;
|
||||
let (o, h) = (&mut p.outline, &mut p.hit);
|
||||
// Off, then every solid colour.
|
||||
let solid_or_off = |v: Option<Colour>| {
|
||||
let all = [None]
|
||||
.into_iter()
|
||||
.chain(Colour::ALL[1..].iter().copied().map(Some));
|
||||
step(&all.collect::<Vec<_>>(), v, back)
|
||||
};
|
||||
match self {
|
||||
Opt::Crosshair => c.custom = !c.custom,
|
||||
Opt::CrosshairShape => c.shape = step(&Shape::ALL, c.shape, back),
|
||||
@@ -508,6 +690,16 @@ impl Opt {
|
||||
.collect();
|
||||
c.hover = step(&all, c.hover, back);
|
||||
}
|
||||
Opt::Outline => o.custom = !o.custom,
|
||||
Opt::OutlineColour => o.colour = o.colour.step_solid(back),
|
||||
Opt::OutlineOpacity => o.opacity = step_by(o.opacity, OPACITIES, 10, back),
|
||||
Opt::OutlineThickness => {
|
||||
o.thickness = step_by(o.thickness, OUTLINE_THICKNESSES, 1, back)
|
||||
}
|
||||
Opt::OutlineFill => o.fill = solid_or_off(o.fill),
|
||||
Opt::Hit => h.custom = !h.custom,
|
||||
Opt::HitColour => h.colour = h.colour.step_solid(back),
|
||||
Opt::HitOpacity => h.opacity = step_by(h.opacity, HIT_OPACITIES, 10, back),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,6 +856,54 @@ mod tests {
|
||||
assert!(!p.parse_line("fullbright", "true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_and_hit_colours_default_to_vanillas() {
|
||||
// Turned on with their first settings, they give vanilla's colours: the outline's
|
||||
// 0x66000000 (LevelRenderer) and OverlayTexture's hurt texel 0xB2FF0000.
|
||||
let o = Outline {
|
||||
custom: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(o.stroke(), Some((0x6600_0000, 1.0)));
|
||||
assert_eq!(o.fill(), None);
|
||||
let [r, g, b, a] = crate::overlay::HURT_TEXEL;
|
||||
let hit = Hit {
|
||||
custom: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(hit.texel(), u32::from_le_bytes([r, g, b, a]));
|
||||
assert_eq!(
|
||||
(Outline::default().stroke(), Hit::default().texel()),
|
||||
(None, 0)
|
||||
);
|
||||
// Fully opaque cyan covers the entity entirely: none of its own colour stays.
|
||||
let cyan = Hit {
|
||||
custom: true,
|
||||
colour: Colour::Cyan,
|
||||
opacity: 100,
|
||||
};
|
||||
assert_eq!(cyan.texel().to_le_bytes(), [0x40, 0xFF, 0xFF, 0]);
|
||||
let mut p = Pvp::default();
|
||||
Opt::Outline.cycle(&mut p, false);
|
||||
Opt::OutlineFill.cycle(&mut p, false);
|
||||
assert_eq!(p.outline.fill(), Some(0x40FF_FFFF), "white at 25%");
|
||||
Opt::OutlineOpacity.cycle(&mut p, true);
|
||||
Opt::OutlineOpacity.cycle(&mut p, true);
|
||||
Opt::OutlineOpacity.cycle(&mut p, true);
|
||||
Opt::OutlineOpacity.cycle(&mut p, true);
|
||||
assert_eq!(p.outline.opacity, 100, "down from 10% wraps to 100%");
|
||||
Opt::OutlineColour.cycle(&mut p, true);
|
||||
assert_eq!(p.outline.colour, Colour::Magenta, "never Inverted");
|
||||
let mut text = String::new();
|
||||
p.serialize(&mut text);
|
||||
let mut read = Pvp::default();
|
||||
for line in text.lines() {
|
||||
let (k, v) = line.split_once(':').unwrap();
|
||||
assert!(read.parse_line(k, v), "{k}");
|
||||
}
|
||||
assert_eq!(read, p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_option_and_value_has_its_english_text() {
|
||||
let en = crate::lang::Language::load(&[], "en_us");
|
||||
|
||||
@@ -594,5 +594,23 @@
|
||||
"horizon.pvp.shape.x": "X",
|
||||
"horizon.pvp.shape.circle": "Circle",
|
||||
"horizon.pvp.shape.square": "Square",
|
||||
"horizon.pvp.shape.dot": "Dot"
|
||||
"horizon.pvp.shape.dot": "Dot",
|
||||
"horizon.pvp.outline": "Custom block outline",
|
||||
"horizon.pvp.outline.tooltip": "Changes the outline around the block you look at.",
|
||||
"horizon.pvp.outline_colour": "Colour",
|
||||
"horizon.pvp.outline_colour.tooltip": "The outline's colour. Black at 40% is vanilla's.",
|
||||
"horizon.pvp.outline_opacity": "Opacity",
|
||||
"horizon.pvp.outline_opacity.tooltip": "How solid the outline is.",
|
||||
"horizon.pvp.outline_thickness": "Thickness",
|
||||
"horizon.pvp.outline_thickness.tooltip": "The outline's width, in times vanilla's.",
|
||||
"horizon.pvp.outline_fill": "Fill",
|
||||
"horizon.pvp.outline_fill.tooltip": "Tints the faces of the block you look at, lightly.",
|
||||
"horizon.pvp.hit": "Custom hit colour",
|
||||
"horizon.pvp.hit.tooltip": "Changes the colour mobs and players flash when they are hurt. Red at 30% is vanilla's.",
|
||||
"horizon.pvp.hit_colour": "Colour",
|
||||
"horizon.pvp.hit_colour.tooltip": "The colour hurt mobs and players flash.",
|
||||
"horizon.pvp.hit_opacity": "Opacity",
|
||||
"horizon.pvp.hit_opacity.tooltip": "How much of the colour covers them.",
|
||||
"horizon.pvp.header.outline": "Block outline",
|
||||
"horizon.pvp.header.hit": "Hit colour"
|
||||
}
|
||||
@@ -16222,6 +16222,7 @@ impl Game<'_> {
|
||||
if !title {
|
||||
self.end_geometry(camera, &mut entities);
|
||||
}
|
||||
self.outline_fill(&outline, camera, &mut entities);
|
||||
let weather = if !title && self.can_have_weather() && self.settings.horizon.weather {
|
||||
horizon_client::spans::time("weather", || self.weather_geometry(camera, alpha))
|
||||
} else {
|
||||
@@ -16309,6 +16310,7 @@ impl Game<'_> {
|
||||
outline,
|
||||
outline_after_terrain,
|
||||
outline_high_contrast: self.settings.file.bool("highContrastBlockOutline"),
|
||||
pvp: self.settings.extras.pvp.frame(),
|
||||
debug_lines: &debug_lines,
|
||||
debug_lines_on_top: &debug_lines_on_top,
|
||||
debug_crosshair: &debug_crosshair,
|
||||
@@ -21033,6 +21035,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use super::Game;
|
||||
use horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
mesher::Vertex,
|
||||
pvp::{Colour, CrosshairCache},
|
||||
};
|
||||
|
||||
@@ -45,4 +46,70 @@ impl Game<'_> {
|
||||
batch.invert_since(start);
|
||||
}
|
||||
}
|
||||
/// The block outline's fill: each box of the targeted block's shape (`getShape`, the boxes
|
||||
/// the outline's edges come from) as translucent faces drawn after translucent terrain,
|
||||
/// pushed out a little so they don't fight the block's own faces. Only while the outline
|
||||
/// shows (`outline` is its edges).
|
||||
pub(super) fn outline_fill(
|
||||
&self,
|
||||
outline: &[[f64; 6]],
|
||||
camera: glam::DVec3,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let Some(argb) = self.settings.extras.pvp.outline.fill() else {
|
||||
return;
|
||||
};
|
||||
let Some(hit) = self.pick().filter(|_| !outline.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
let [x, y, z] = hit.pos;
|
||||
let state = self.world.block(x, y, z);
|
||||
let facts = self.res.facts().get(state);
|
||||
let offset = match facts.outline_offset {
|
||||
true => self.pack.context.tables.offset(state, x, z),
|
||||
false => None,
|
||||
}
|
||||
.unwrap_or([0.0; 3]);
|
||||
let base = [f64::from(x), f64::from(y), f64::from(z)];
|
||||
let context = self.context_outline(hit.pos, state);
|
||||
let boxes = context.as_slice().iter().chain(match context {
|
||||
Some(_) => &[][..],
|
||||
None => &facts.outline[..],
|
||||
});
|
||||
let uv = horizon_client::gfx::lightning::white_uv(&self.gui.atlas.font.white());
|
||||
let [a, r, g, b] = argb.to_be_bytes();
|
||||
let color = u32::from_le_bytes([r, g, b, a]);
|
||||
const OUT: f64 = 0.002;
|
||||
for shape in boxes {
|
||||
let corner = |i: usize, high: bool| {
|
||||
let v = if high {
|
||||
shape[i + 3] + OUT
|
||||
} else {
|
||||
shape[i] - OUT
|
||||
};
|
||||
(base[i] + offset[i] + v - camera[i]) as f32
|
||||
};
|
||||
// Each face: the axis it faces along, its side, and its corners' other two axes.
|
||||
for axis in 0..3 {
|
||||
let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
|
||||
for high in [false, true] {
|
||||
for (hu, hv) in [(false, false), (true, false), (true, true), (false, true)] {
|
||||
let mut pos = [0.0; 3];
|
||||
pos[axis] = corner(axis, high);
|
||||
pos[u] = corner(u, hu);
|
||||
pos[v] = corner(v, hv);
|
||||
out.push(Vertex {
|
||||
pos,
|
||||
uv,
|
||||
layer: horizon_client::gfx::TRANSLUCENT_PARTICLE_OVERLAY,
|
||||
color,
|
||||
light: 240 | 240 << 8,
|
||||
normal: [0; 4],
|
||||
glint_uv: [0.0; 2],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -27,3 +27,19 @@ Each feature is built from what it visibly does, never from another client's or
|
||||
attack indicator reads (`crosshairPickEntity`).
|
||||
- The attack indicator, the F3 axes and the spectator rules stay vanilla's; only the sprite is replaced.
|
||||
- Cost: the pixels are worked out once per settings change (`CrosshairCache`); a frame adds a few quads.
|
||||
|
||||
## Block outline (`outline`, default off)
|
||||
|
||||
- Colour (solid), opacity 10-100%, thickness 1-4 times vanilla's width, and an optional fill that tints the
|
||||
targeted block's faces at 25%. Turned on, its first settings are vanilla's: black at 40%, vanilla's width.
|
||||
- With Accessibility's high contrast outline, the colour replaces the bright stroke and the black one stays.
|
||||
- The fill draws each box of the block's shape (`getShape`, the boxes the outline's edges come from) as
|
||||
translucent faces after translucent terrain, 0.002 out from the block. Only while the outline shows.
|
||||
- Cost: the stroke is vanilla's draw with another colour and width; the fill adds at most a few dozen quads.
|
||||
|
||||
## Hit colour (`hit`, default off)
|
||||
|
||||
- The colour hurt mobs and players flash, and how much of it covers them (0-100%). Its first settings are
|
||||
vanilla's: `OverlayTexture`'s red over 30%.
|
||||
- The texel rides in the frame's uniforms (`colored_count.y`, 0 for vanilla's), so changing it costs nothing.
|
||||
- Shader packs keep vanilla's red (`pack_entities::entity_color`).
|
||||
Reference in new issue
Block a user