feat: toggleable inventory overlay with mouse navigation
- Press Tab to open/close inventory overlay - 4x2 grid of item slots (8 slots) centered on screen - Each slot shows 2-char glyph + item name label - Mouse hover highlights slot with golden border - Hover shows action hint (Use/Equip/Equipped) - Left-click slot: use/equip item at that index - Right-click slot: drop item at that index - Equipped items shown at bottom of panel - Character panel hidden when inventory is open (saves space) - Shooting disabled while inventory is open (prevents accidental fire) - Mouse position tracked in UI coordinates for slot detection All 171 tests + 14 scenarios pass.
This commit is contained in:
+19
-2
@@ -40,6 +40,9 @@ pub struct Game {
|
||||
pub score: u32,
|
||||
pub depth: u32,
|
||||
pub fps: f32,
|
||||
pub inventory_open: bool,
|
||||
pub inventory_mouse_x: i32,
|
||||
pub inventory_mouse_y: i32,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@@ -73,6 +76,9 @@ impl Game {
|
||||
score: 0,
|
||||
depth: 1,
|
||||
fps: 0.0,
|
||||
inventory_open: false,
|
||||
inventory_mouse_x: 0,
|
||||
inventory_mouse_y: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,8 +431,10 @@ impl Game {
|
||||
let player_alive = player.as_ref().map(|e| e.alive).unwrap_or(false);
|
||||
let ui_w = (vw as i32) * crate::ui::UI_SCALE;
|
||||
let ui_h = (vh as i32) * crate::ui::UI_SCALE;
|
||||
self.ui
|
||||
.draw_character_panel(1, 1, player.as_ref(), &self.player);
|
||||
if !self.inventory_open {
|
||||
self.ui
|
||||
.draw_character_panel(1, 1, player.as_ref(), &self.player);
|
||||
}
|
||||
self.ui.draw_hud(
|
||||
ui_w as usize,
|
||||
ui_h as usize,
|
||||
@@ -461,6 +469,15 @@ impl Game {
|
||||
self.cam_x,
|
||||
self.cam_y,
|
||||
);
|
||||
if self.inventory_open {
|
||||
self.ui.draw_inventory_overlay(
|
||||
ui_w as usize,
|
||||
ui_h as usize,
|
||||
&self.player,
|
||||
self.inventory_mouse_x,
|
||||
self.inventory_mouse_y,
|
||||
);
|
||||
}
|
||||
if !player_alive {
|
||||
self.ui
|
||||
.draw_death_screen(ui_w as usize, ui_h as usize, self.kills, self.score);
|
||||
|
||||
+84
-1
@@ -304,7 +304,7 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
|
||||
game.player.stop_horizontal(&mut game.entities);
|
||||
}
|
||||
|
||||
if input.shoot_mouse
|
||||
if (input.shoot_mouse && !game.inventory_open)
|
||||
|| input.shoot_left
|
||||
|| input.shoot_right
|
||||
|| input.shoot_up
|
||||
@@ -348,6 +348,89 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
|
||||
game.drop_item(0);
|
||||
}
|
||||
|
||||
if input.inventory_toggle {
|
||||
game.inventory_open = !game.inventory_open;
|
||||
}
|
||||
|
||||
let ui_scale = verbatim::ui::UI_SCALE as f64;
|
||||
let cell_px = 8.0_f64;
|
||||
let ui_cell_px = cell_px / ui_scale;
|
||||
game.inventory_mouse_x = (input.mouse_x / ui_cell_px) as i32;
|
||||
game.inventory_mouse_y = (input.mouse_y / ui_cell_px) as i32;
|
||||
|
||||
if game.inventory_open {
|
||||
if let Some((mx, my)) = &input.inventory_click {
|
||||
let mouse_ui_x = (*mx / ui_cell_px) as i32;
|
||||
let mouse_ui_y = (*my / ui_cell_px) as i32;
|
||||
let vw = renderer.grid_w();
|
||||
let vh = renderer.grid_h();
|
||||
let ui_w = (vw as i32) * verbatim::ui::UI_SCALE;
|
||||
let ui_h = (vh as i32) * verbatim::ui::UI_SCALE;
|
||||
|
||||
let cols = 4i32;
|
||||
let rows = 2i32;
|
||||
let slot_w = 8i32;
|
||||
let slot_h = 6i32;
|
||||
let gap = 2i32;
|
||||
let panel_w = cols * slot_w + (cols + 1) * gap + 4;
|
||||
let panel_h = rows * slot_h + (rows + 1) * gap + 20;
|
||||
let px_start = (ui_w - panel_w) / 2;
|
||||
let py_start = (ui_h - panel_h) / 2;
|
||||
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let idx = (row * cols + col) as usize;
|
||||
let sx = px_start + gap + col * (slot_w + gap) + 2;
|
||||
let sy = py_start + 10 + row * (slot_h + gap) + gap;
|
||||
if mouse_ui_x >= sx
|
||||
&& mouse_ui_x < sx + slot_w
|
||||
&& mouse_ui_y >= sy
|
||||
&& mouse_ui_y < sy + slot_h
|
||||
{
|
||||
if idx < game.player.inventory.len() {
|
||||
game.use_item(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((mx, my)) = &input.inventory_right_click {
|
||||
let mouse_ui_x = (*mx / ui_cell_px) as i32;
|
||||
let mouse_ui_y = (*my / ui_cell_px) as i32;
|
||||
let vw = renderer.grid_w();
|
||||
let vh = renderer.grid_h();
|
||||
let ui_w = (vw as i32) * verbatim::ui::UI_SCALE;
|
||||
let ui_h = (vh as i32) * verbatim::ui::UI_SCALE;
|
||||
|
||||
let cols = 4i32;
|
||||
let rows = 2i32;
|
||||
let slot_w = 8i32;
|
||||
let slot_h = 6i32;
|
||||
let gap = 2i32;
|
||||
let panel_w = cols * slot_w + (cols + 1) * gap + 4;
|
||||
let panel_h = rows * slot_h + (rows + 1) * gap + 20;
|
||||
let px_start = (ui_w - panel_w) / 2;
|
||||
let py_start = (ui_h - panel_h) / 2;
|
||||
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let idx = (row * cols + col) as usize;
|
||||
let sx = px_start + gap + col * (slot_w + gap) + 2;
|
||||
let sy = py_start + 10 + row * (slot_h + gap) + gap;
|
||||
if mouse_ui_x >= sx
|
||||
&& mouse_ui_x < sx + slot_w
|
||||
&& mouse_ui_y >= sy
|
||||
&& mouse_ui_y < sy + slot_h
|
||||
{
|
||||
if idx < game.player.inventory.len() {
|
||||
game.drop_item(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let (px, _py) = game.player.center(&game.entities);
|
||||
let player_screen_x = ((px as i32 - game.cam_x) as f64) * 8.0 + 4.0;
|
||||
|
||||
@@ -23,8 +23,14 @@ pub struct WindowInput {
|
||||
pub mouse_y: f64,
|
||||
pub mouse_left: bool,
|
||||
pub mouse_left_was_down: bool,
|
||||
pub mouse_right: bool,
|
||||
pub mouse_right_was_down: bool,
|
||||
pub shoot_mouse: bool,
|
||||
pub inventory_toggle: bool,
|
||||
pub inventory_click: Option<(f64, f64)>,
|
||||
pub inventory_right_click: Option<(f64, f64)>,
|
||||
jump_was_down: bool,
|
||||
inventory_tab_was_down: bool,
|
||||
shoot_left_was_down: bool,
|
||||
shoot_right_was_down: bool,
|
||||
shoot_up_was_down: bool,
|
||||
@@ -60,8 +66,14 @@ impl WindowInput {
|
||||
mouse_y: 0.0,
|
||||
mouse_left: false,
|
||||
mouse_left_was_down: false,
|
||||
mouse_right: false,
|
||||
mouse_right_was_down: false,
|
||||
shoot_mouse: false,
|
||||
inventory_toggle: false,
|
||||
inventory_click: None,
|
||||
inventory_right_click: None,
|
||||
jump_was_down: false,
|
||||
inventory_tab_was_down: false,
|
||||
shoot_left_was_down: false,
|
||||
shoot_right_was_down: false,
|
||||
shoot_up_was_down: false,
|
||||
@@ -93,6 +105,7 @@ impl WindowInput {
|
||||
pub fn clear_keys(&mut self) {
|
||||
self.down_keys.clear();
|
||||
self.mouse_left = false;
|
||||
self.mouse_right = false;
|
||||
}
|
||||
|
||||
pub fn on_mouse_move(&mut self, x: f64, y: f64) {
|
||||
@@ -112,6 +125,12 @@ impl WindowInput {
|
||||
(winit::event::MouseButton::Left, winit::event::ElementState::Released) => {
|
||||
self.mouse_left = false;
|
||||
}
|
||||
(winit::event::MouseButton::Right, winit::event::ElementState::Pressed) => {
|
||||
self.mouse_right = true;
|
||||
}
|
||||
(winit::event::MouseButton::Right, winit::event::ElementState::Released) => {
|
||||
self.mouse_right = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +183,20 @@ impl WindowInput {
|
||||
self.shoot_mouse = self.mouse_left && !self.mouse_left_was_down;
|
||||
self.mouse_left_was_down = self.mouse_left;
|
||||
|
||||
let now_tab = keys.contains(&KeyCode::Tab);
|
||||
self.inventory_toggle = now_tab && !self.inventory_tab_was_down;
|
||||
self.inventory_tab_was_down = now_tab;
|
||||
|
||||
self.inventory_click = None;
|
||||
self.inventory_right_click = None;
|
||||
if self.mouse_left && !self.mouse_left_was_down {
|
||||
self.inventory_click = Some((self.mouse_x, self.mouse_y));
|
||||
}
|
||||
if self.mouse_right && !self.mouse_right_was_down {
|
||||
self.inventory_right_click = Some((self.mouse_x, self.mouse_y));
|
||||
}
|
||||
self.mouse_right_was_down = self.mouse_right;
|
||||
|
||||
self.cam_left = keys.contains(&KeyCode::KeyY);
|
||||
self.cam_right = keys.contains(&KeyCode::KeyU);
|
||||
self.cam_up = keys.contains(&KeyCode::KeyI);
|
||||
|
||||
+191
@@ -464,6 +464,197 @@ impl UiLayer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_inventory_overlay(
|
||||
&mut self,
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
player_state: &crate::entity::player::Player,
|
||||
mouse_ui_x: i32,
|
||||
mouse_ui_y: i32,
|
||||
) -> Vec<(usize, i32, i32)> {
|
||||
let bg = [15u8, 18, 28];
|
||||
let border = [160u8, 180, 255];
|
||||
let title_col = [220u8, 230, 255];
|
||||
let dim = [120u8, 130, 160];
|
||||
let slot_bg = [30u8, 36, 50];
|
||||
let slot_border = [80u8, 100, 140];
|
||||
let hover_border = [255u8, 220, 100];
|
||||
|
||||
let cols = 4i32;
|
||||
let rows = 2i32;
|
||||
let slot_w = 8i32;
|
||||
let slot_h = 6i32;
|
||||
let gap = 2i32;
|
||||
let panel_w = cols * slot_w + (cols + 1) * gap + 4;
|
||||
let panel_h = rows * slot_h + (rows + 1) * gap + 20;
|
||||
|
||||
let px_start = (screen_w as i32 - panel_w) / 2;
|
||||
let py_start = (screen_h as i32 - panel_h) / 2;
|
||||
|
||||
for y in 0..panel_h {
|
||||
for x in 0..panel_w {
|
||||
self.set_alpha(px_start + x, py_start + y, ' ', [0, 0, 0], bg, 200);
|
||||
}
|
||||
}
|
||||
for x in 0..panel_w {
|
||||
if x % 2 == 0 {
|
||||
self.set_alpha(px_start + x, py_start, '.', border, bg, 240);
|
||||
self.set_alpha(px_start + x, py_start + panel_h - 1, '.', border, bg, 240);
|
||||
}
|
||||
}
|
||||
for y in 0..panel_h {
|
||||
if y % 2 == 0 {
|
||||
self.set_alpha(px_start, py_start + y, '.', border, bg, 240);
|
||||
self.set_alpha(px_start + panel_w - 1, py_start + y, '.', border, bg, 240);
|
||||
}
|
||||
}
|
||||
self.set_alpha(px_start, py_start, '*', border, bg, 240);
|
||||
self.set_alpha(px_start + panel_w - 1, py_start, '*', border, bg, 240);
|
||||
self.set_alpha(px_start, py_start + panel_h - 1, '*', border, bg, 240);
|
||||
self.set_alpha(
|
||||
px_start + panel_w - 1,
|
||||
py_start + panel_h - 1,
|
||||
'*',
|
||||
border,
|
||||
bg,
|
||||
240,
|
||||
);
|
||||
|
||||
let title = "INVENTORY";
|
||||
self.draw_text(px_start + 4, py_start + 3, title, title_col, 255);
|
||||
let count_text = format!("({}/{} items)", player_state.inventory.len(), cols * rows);
|
||||
self.draw_text(
|
||||
px_start + 4 + Self::text_width(title) + 2,
|
||||
py_start + 3,
|
||||
&count_text,
|
||||
dim,
|
||||
200,
|
||||
);
|
||||
|
||||
let mut hover_slot: Option<usize> = None;
|
||||
let mut slots: Vec<(usize, i32, i32)> = Vec::new();
|
||||
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let idx = (row * cols + col) as usize;
|
||||
let sx = px_start + gap + col * (slot_w + gap) + 2;
|
||||
let sy = py_start + 10 + row * (slot_h + gap) + gap;
|
||||
|
||||
for y in 0..slot_h {
|
||||
for x in 0..slot_w {
|
||||
self.set_alpha(sx + x, sy + y, ' ', [0, 0, 0], slot_bg, 200);
|
||||
}
|
||||
}
|
||||
let sb = if mouse_ui_x >= sx
|
||||
&& mouse_ui_x < sx + slot_w
|
||||
&& mouse_ui_y >= sy
|
||||
&& mouse_ui_y < sy + slot_h
|
||||
{
|
||||
hover_slot = Some(idx);
|
||||
hover_border
|
||||
} else {
|
||||
slot_border
|
||||
};
|
||||
for x in 0..slot_w {
|
||||
self.set_alpha(sx + x, sy, '.', sb, slot_bg, 240);
|
||||
self.set_alpha(sx + x, sy + slot_h - 1, '.', sb, slot_bg, 240);
|
||||
}
|
||||
for y in 0..slot_h {
|
||||
self.set_alpha(sx, sy + y, '.', sb, slot_bg, 240);
|
||||
self.set_alpha(sx + slot_w - 1, sy + y, '.', sb, slot_bg, 240);
|
||||
}
|
||||
|
||||
if idx < player_state.inventory.len() {
|
||||
let item = &player_state.inventory[idx];
|
||||
let [ch1, ch2] = item.display_glyph();
|
||||
let col = item.color();
|
||||
let cx = sx + slot_w / 2 - 1;
|
||||
let cy = sy + slot_h / 2 - 1;
|
||||
self.set(cx, cy, ch1, col, slot_bg);
|
||||
self.set(
|
||||
cx + 1,
|
||||
cy,
|
||||
ch2,
|
||||
[col[0] / 2, col[1] / 2, col[2] / 2],
|
||||
slot_bg,
|
||||
);
|
||||
|
||||
let label = item.name();
|
||||
let label_w = Self::text_width(label) as i32;
|
||||
let lx = sx + (slot_w - label_w) / 2;
|
||||
let ly = sy + slot_h - 1;
|
||||
if lx >= sx && lx + label_w <= sx + slot_w {
|
||||
self.draw_text(lx, ly, label, dim, 200);
|
||||
}
|
||||
|
||||
if hover_slot == Some(idx) {
|
||||
let action = if item.is_consumable() {
|
||||
"L-click: Use"
|
||||
} else if item.is_weapon() {
|
||||
if player_state
|
||||
.weapon
|
||||
.as_ref()
|
||||
.map(|w| w.typ == item.typ)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"Equipped"
|
||||
} else {
|
||||
"L-click: Equip"
|
||||
}
|
||||
} else if item.is_armor() {
|
||||
if player_state
|
||||
.armor
|
||||
.as_ref()
|
||||
.map(|a| a.typ == item.typ)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"Equipped"
|
||||
} else {
|
||||
"L-click: Equip"
|
||||
}
|
||||
} else {
|
||||
"L-click: Use"
|
||||
};
|
||||
self.draw_text(sx, sy + slot_h, action, hover_border, 240);
|
||||
}
|
||||
} else if hover_slot == Some(idx) {
|
||||
self.draw_text(sx + 1, sy + slot_h / 2, "empty", dim, 200);
|
||||
}
|
||||
|
||||
slots.push((idx, sx, sy));
|
||||
}
|
||||
}
|
||||
|
||||
if player_state.weapon.is_some() || player_state.armor.is_some() {
|
||||
let eq_y = py_start + panel_h - 6;
|
||||
self.draw_text(px_start + 4, eq_y, "EQ:", dim, 200);
|
||||
let mut eq_x = px_start + 8;
|
||||
if let Some(ref w) = player_state.weapon {
|
||||
let [c1, c2] = w.display_glyph();
|
||||
let col = w.color();
|
||||
self.set(eq_x, eq_y, c1, col, bg);
|
||||
self.set(eq_x + 1, eq_y, c2, [col[0] / 2, col[1] / 2, col[2] / 2], bg);
|
||||
eq_x += 4;
|
||||
}
|
||||
if let Some(ref a) = player_state.armor {
|
||||
let [c1, c2] = a.display_glyph();
|
||||
let col = a.color();
|
||||
self.set(eq_x, eq_y, c1, col, bg);
|
||||
self.set(eq_x + 1, eq_y, c2, [col[0] / 2, col[1] / 2, col[2] / 2], bg);
|
||||
}
|
||||
}
|
||||
|
||||
self.draw_text(
|
||||
px_start + 4,
|
||||
py_start + panel_h - 3,
|
||||
"Tab: close L-click: use R-click: drop",
|
||||
dim,
|
||||
200,
|
||||
);
|
||||
|
||||
slots
|
||||
}
|
||||
|
||||
pub fn draw_hud(
|
||||
&mut self,
|
||||
screen_w: usize,
|
||||
|
||||
Reference in New Issue
Block a user