alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates client src gui progression.rs
use super::{Geometry, cell, corners, gray};
use crate::menu::{Menu, contains};
use common::item::ItemStack;
use common::stats::{self, ACHIEVEMENTS};
use common::world::Block;
const ACHIEVEMENT: i32 = crate::assets::entity_texture("achievement/bg.png");
const SLOT: i32 = crate::assets::entity_texture("gui/slot.png");
const ROW_COLORS: [u32; 2] = [0xffffffff, 0xff909090];
impl Geometry {
fn translation(&self, key: &str) -> String {
self.translations
.get(key)
.cloned()
.unwrap_or_else(|| key.to_owned())
}
fn achievement_name(&self, index: usize) -> String {
self.translation(&format!("achievement.{}", ACHIEVEMENTS[index].name))
}
/// `Achievement.getDescription`, with the inventory key filled in.
fn achievement_description(&self, index: usize, options: &crate::settings::Options) -> String {
let key = options.keys[crate::settings::Binding::Inventory as usize];
self.translation(&format!("achievement.{}.desc", ACHIEVEMENTS[index].name))
.replace("%1$s", &crate::settings::key_name(key))
}
/// `FontRenderer.drawSplitString`: words wrapped into lines narrower than `width`.
fn split_lines(&self, text: &str, width: f32) -> Vec<String> {
let mut lines = Vec::new();
let mut words = text.split(' ').peekable();
while let Some(word) = words.next() {
let mut line = format!("{word} ");
while let Some(next) =
words.next_if(|next| self.text_width(&(line.clone() + next)) < width)
{
line.push_str(next);
line.push(' ');
}
if !line.trim().is_empty() {
lines.push(line);
}
}
lines
}
pub fn achievement_toast(
&mut self,
stats: &crate::stats::Stats,
options: &crate::settings::Options,
) {
let Some((index, since, hint)) = stats.toast else {
return;
};
let elapsed = since.elapsed().as_secs_f64() / 3.0;
if elapsed > 1.0 && !hint {
return;
}
let progress = (elapsed * 2.0).min(2.0 - elapsed * 2.0);
let offset = (1.0 - progress * 4.0).max(0.0).powi(4);
let x = self.size[0] - 160.0;
let y = -(offset * 36.0) as i32 as f32;
self.sprite([x, y, 160.0, 32.0], [96.0, 202.0, 160.0, 32.0], ACHIEVEMENT);
if hint {
let description = self.achievement_description(index, options);
for (row, line) in self.split_lines(&description, 120.0).iter().enumerate() {
let y = y + 7.0 + row as f32 * 8.0;
self.text(line, x + 30.0, y, 0xffffffff, false);
}
} else {
let get = self.translation("achievement.get");
self.text(&get, x + 30.0, y + 7.0, 0xff00ffff, false);
let name = self.achievement_name(index);
self.text(&name, x + 30.0, y + 18.0, 0xffffffff, false);
}
self.item(ItemStack::one(ACHIEVEMENTS[index].icon), x + 8.0, y + 8.0);
}
pub(super) fn achievements(
&mut self,
menu: &Menu,
options: &crate::settings::Options,
cursor: [f32; 2],
) {
let stats = menu.statistics.lock().unwrap();
let x = ((self.size[0] - 256.0) / 2.0).floor();
let y = ((self.size[1] - 202.0) / 2.0).floor();
let [sx, sy] = menu.achievement_scroll.map(f32::floor);
let origin = [x + 16.0, y + 17.0];
let clip = [origin[0], origin[1], 224.0, 155.0];
let start = self.vertices.len();
let cx = (sx as i32 + 288) >> 4;
let cy = (sy as i32 + 288) >> 4;
let ox = (sx as i32 + 288) % 16;
let oy = (sy as i32 + 288) % 16;
for row in 0..11 {
let color = gray((0.6 - (cy + row) as f32 / 25.0 * 0.3).clamp(0.0, 1.0));
for column in 0..15 {
let mut random = common::JavaRandom::new(i64::from(1234 + cx + column));
random.next_i32();
let layer = random.next_int(1 + cy + row) + (cy + row) / 2;
let block = match layer {
_ if layer > 37 || cy + row == 35 => Block::BEDROCK,
22 if random.next_int(2) == 0 => Block::DIAMOND_ORE,
22 => Block::REDSTONE_ORE,
10 => Block::IRON_ORE,
8 => Block::COAL_ORE,
5.. => Block::STONE,
1.. => Block::DIRT,
_ => Block::SAND,
};
let tile = block.properties().unwrap().texture;
let [x, y] = [column * 16 - ox, row * 16 - oy].map(|v| v as f32);
let rect = [origin[0] + x, origin[1] + y, 16.0, 16.0];
self.tinted(rect, cell(tile, 16.0, 16.0), 0, color);
}
}
let pulse =
(menu.opened.elapsed().as_millis() % 600) as f64 / 600.0 * std::f64::consts::TAU;
let at = |position: [i32; 2]| {
[
origin[0] + position[0] as f32 * 24.0 - sx,
origin[1] + position[1] as f32 * 24.0 - sy,
]
};
for (index, achievement) in ACHIEVEMENTS.iter().enumerate() {
if let Some(parent) = achievement.parent {
let a = at(achievement.position).map(|v| v + 11.0);
let b = at(ACHIEVEMENTS[parent].position).map(|v| v + 11.0);
let color = if stats.unlocked(index) {
0xff707070
} else if stats.available(index) {
if pulse.sin() > 0.6 {
0xff00ff00
} else {
0x8200ff00
}
} else {
0xff000000
};
self.fill(
[a[0].min(b[0]), a[1], (a[0] - b[0]).abs() + 1.0, 1.0],
color,
);
self.fill(
[b[0], a[1].min(b[1]), 1.0, (a[1] - b[1]).abs() + 1.0],
color,
);
}
}
let mut hovered = None;
for (index, achievement) in ACHIEVEMENTS.iter().enumerate() {
let [ax, ay] = at(achievement.position);
if ax < origin[0] - 24.0
|| ay < origin[1] - 24.0
|| ax > origin[0] + 224.0
|| ay > origin[1] + 155.0
{
continue;
}
let shade = if stats.unlocked(index) {
1.0
} else if stats.available(index) {
if pulse.sin() < 0.6 { 0.6 } else { 0.8 }
} else {
0.3
};
let u = if achievement.special { 26.0 } else { 0.0 };
let rect = [ax - 2.0, ay - 2.0, 26.0, 26.0];
self.tinted(rect, [u, 202.0, 26.0, 26.0], ACHIEVEMENT, gray(shade));
let item_start = self.vertices.len();
self.item(ItemStack::one(achievement.icon), ax + 3.0, ay + 3.0);
let icon_shade = if stats.available(index) { shade } else { 0.1 };
for vertex in &mut self.vertices[item_start..] {
let channel =
|shift: u32| ((vertex.color >> shift & 255) as f32 * icon_shade) as u32;
vertex.color =
(vertex.color & 0xff000000) | channel(0) | channel(8) << 8 | channel(16) << 16;
}
if contains(clip, cursor) && contains([ax, ay, 22.0, 22.0], cursor) {
hovered = Some(index);
}
}
self.clip_quads(start, clip);
self.sprite([x, y, 256.0, 202.0], [0.0, 0.0, 256.0, 202.0], ACHIEVEMENT);
self.text("Achievements", x + 15.0, y + 5.0, 0xff404040, false);
if let Some(index) = hovered {
let title = self.achievement_name(index);
let description = if stats.available(index) {
self.achievement_description(index, options)
} else {
self.translation("achievement.requires").replace(
"%1$s",
&self.achievement_name(ACHIEVEMENTS[index].parent.unwrap()),
)
};
let width = 120.0_f32.max(self.text_width(&title));
let color = if ACHIEVEMENTS[index].special {
0xff80ffff
} else {
0xffffffff
};
let mut lines = vec![(title, color)];
let description = self.wrap_words(&description, width);
lines.extend(description.into_iter().map(|line| (line, 0xffa0a0a0)));
if stats.unlocked(index) {
lines.push((self.translation("achievement.taken"), 0xffff9190));
}
self.progression_tooltip(&lines, cursor);
}
}
fn progression_tooltip(&mut self, lines: &[(String, u32)], cursor: [f32; 2]) {
let width = lines
.iter()
.map(|(text, _)| self.text_width(text))
.fold(0.0_f32, f32::max);
let x = (cursor[0] + 12.0).min(self.size[0] - width - 6.0);
let height = lines.len() as f32 * 10.0;
let y = (cursor[1] - 4.0).min(self.size[1] - height - 6.0);
self.fill([x - 3.0, y - 3.0, width + 6.0, height + 6.0], 0xc0000000);
for (index, (text, color)) in lines.iter().enumerate() {
self.text(text, x, y + index as f32 * 10.0, *color, true);
}
}
/// An 18 by 18 sprite of `gui/slot.png`.
fn slot_sprite(&mut self, [x, y]: [f32; 2], [u, v]: [f32; 2]) {
let uv = [u / 128.0, v / 128.0, 18.0 / 128.0, 18.0 / 128.0];
self.quad(corners([x, y, 18.0, 18.0]), uv, SLOT, 0xffffffff);
}
pub(super) fn statistics(&mut self, menu: &Menu, cursor: [f32; 2]) {
let items = menu.stat_items();
let stats = menu.statistics.lock().unwrap();
let [w, h] = self.size;
let left = (w / 2.0).floor() - 110.0;
let bottom = h - 64.0;
self.background([0.0, 32.0, w, bottom - 32.0], 0xff202020, menu.scroll);
let start = self.vertices.len();
let top = 36.0 - menu.scroll;
let mut tooltip = None;
if menu.stats_tab == 0 {
for (index, &(id, key)) in stats::GENERAL.iter().enumerate() {
let y = top + index as f32 * 10.0;
if y < 22.0 || y > bottom {
continue;
}
let color = ROW_COLORS[index % 2];
let name = self.translation(&format!("stat.{key}"));
self.text(&name, left + 2.0, y + 1.0, color, true);
let value = stats::format(id, stats.get(id));
let x = left + 215.0 - self.text_width(&value);
self.text(&value, x, y + 1.0, color, true);
}
} else {
let bases = Menu::stat_columns(menu.stats_tab);
let headers = if menu.stats_tab == 1 {
["crafted", "used", "mined"]
} else {
["depleted", "crafted", "used"]
};
for (column, header) in headers.iter().enumerate() {
let x = left + 97.0 + column as f32 * 50.0;
self.slot_sprite([x, top + 1.0], [0.0, 18.0]);
let u = if menu.stats_tab == 2 && column == 0 {
72.0
} else {
((column + usize::from(menu.stats_tab == 1)) * 18) as f32
};
self.slot_sprite([x, top + 1.0], [u, 18.0]);
if let Some((sorted, descending)) = menu.stats_sort
&& sorted == column
{
let u = if descending { 18.0 } else { 36.0 };
self.slot_sprite([x - 18.0, top + 1.0], [u, 0.0]);
}
if contains([x - 18.0, top, 36.0, 20.0], cursor) {
tooltip = Some(self.translation(&format!("stat.{}", header)));
}
}
for (index, &item) in items.iter().enumerate() {
let y = top + 20.0 + index as f32 * 20.0;
if y < 12.0 || y > bottom {
continue;
}
self.slot_sprite([left + 41.0, y + 1.0], [0.0, 0.0]);
self.item(ItemStack::one(item), left + 42.0, y + 2.0);
for (column, base) in bases.into_iter().enumerate() {
let id = base + stats::item_id(item);
let value = if stats.guids.contains_key(&id) {
stats::format(id, stats.get(id))
} else {
"-".into()
};
let x = left + 115.0 + column as f32 * 50.0 - self.text_width(&value);
self.text(&value, x, y + 5.0, ROW_COLORS[index % 2], true);
}
if contains([left + 40.0, y, 20.0, 20.0], cursor) {
let kind = if item.0 < 256 { "tile" } else { "item" };
let name = item.properties().unwrap().name;
tooltip = Some(self.translation(&format!("{kind}.{name}.name")));
}
}
}
self.clip_quads(start, [0.0, 32.0, w, bottom - 32.0]);
self.background([0.0, 0.0, w, 32.0], 0xff404040, 0.0);
self.background([0.0, bottom, w, h - bottom], 0xff404040, 0.0);
self.centered("Statistics", w / 2.0, 20.0);
if let Some(text) = tooltip {
self.progression_tooltip(&[(text, 0xffffffff)], cursor);
}
}
}