Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

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);
        }
    }
}