Tooltips
This commit is contained in:
+27
-15
@@ -106,8 +106,10 @@ export class Client {
|
||||
if (this.#stopped) {
|
||||
return;
|
||||
}
|
||||
const screen = this.screen;
|
||||
this.screen?.on_tick(delta);
|
||||
this.#handle_keybinds();
|
||||
// a screen that closed itself just now, like chat on enter, had this frame's keys
|
||||
this.#handle_keybinds(screen !== undefined && this.screen === undefined);
|
||||
|
||||
this.#pending_time += delta;
|
||||
let ticks = 0;
|
||||
@@ -143,7 +145,30 @@ export class Client {
|
||||
this.level.tick();
|
||||
}
|
||||
|
||||
#handle_keybinds() {
|
||||
// closed_screen: a screen closed this frame and took its keys, so they don't also open the inventory or pick a
|
||||
// hotbar slot. typing "/give pickaxe" and enter in the same slow frame would otherwise open the inventory
|
||||
#handle_keybinds(closed_screen: boolean) {
|
||||
const player = this.player;
|
||||
|
||||
if (!closed_screen) {
|
||||
this.#handle_keys();
|
||||
}
|
||||
|
||||
if (InputManager.is_mouse_grabbed()) {
|
||||
const mouse = InputManager.get_mouse_delta();
|
||||
player.turn(mouse.x, mouse.y);
|
||||
}
|
||||
InputManager.set_mouse_grabbed(!this.screen);
|
||||
|
||||
this.hit_result = this.level.pick(player.x, player.y + player.eye_height, player.z, player.yaw, player.pitch);
|
||||
this.#handle_block_interaction();
|
||||
|
||||
if (!this.screen && !closed_screen) {
|
||||
this.#handle_hotbar();
|
||||
}
|
||||
}
|
||||
|
||||
#handle_keys() {
|
||||
const options = this.options;
|
||||
const player = this.player;
|
||||
|
||||
@@ -177,19 +202,6 @@ export class Client {
|
||||
if (InputManager.is_key_pressed(options.key_fullscreen)) {
|
||||
InputManager.toggle_fullscreen();
|
||||
}
|
||||
|
||||
if (InputManager.is_mouse_grabbed()) {
|
||||
const mouse = InputManager.get_mouse_delta();
|
||||
player.turn(mouse.x, mouse.y);
|
||||
}
|
||||
InputManager.set_mouse_grabbed(!this.screen);
|
||||
|
||||
this.hit_result = this.level.pick(player.x, player.y + player.eye_height, player.z, player.yaw, player.pitch);
|
||||
this.#handle_block_interaction();
|
||||
|
||||
if (!this.screen) {
|
||||
this.#handle_hotbar();
|
||||
}
|
||||
}
|
||||
|
||||
// the held item while playing, the slot under the mouse in an inventory (only with nothing on the cursor)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { InputManager } from "../input_manager.ts";
|
||||
import { ClientInventories } from "../inventory.ts";
|
||||
import { canvas, Texture } from "../renderer/mod.ts";
|
||||
import { draw_item, draw_nine_slice } from "../rendering/render_utils.ts";
|
||||
import { draw_tooltip, item_tooltip } from "./tooltip.ts";
|
||||
|
||||
export class Slot {
|
||||
container: ContainerKey;
|
||||
@@ -99,9 +100,15 @@ export class GuiInventoryScreen extends GuiScreen {
|
||||
}
|
||||
}
|
||||
|
||||
const mouse = InputManager.get_mouse_position();
|
||||
if (this.inventories.cursor.item) {
|
||||
const mouse = InputManager.get_mouse_position();
|
||||
draw_item(this.inventories.cursor.item, mouse.x, mouse.y);
|
||||
} else if (this.hovering) {
|
||||
// what's in the slot under the mouse, while not carrying anything
|
||||
const item = this.get_container(this.hovering.container)?.get_item(this.hovering.index);
|
||||
if (item) {
|
||||
draw_tooltip(item_tooltip(item), mouse.x, mouse.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-1
@@ -3,15 +3,26 @@ import { AssetManager } from "$/client/assets.ts";
|
||||
import type { Client } from "$/client/client.ts";
|
||||
import type { ClientInventories } from "$/client/inventory.ts";
|
||||
import { draw_item, draw_nine_slice } from "$/client/rendering/render_utils.ts";
|
||||
import { canvas, draw_rect_stroke, Texture } from "$/client/renderer/mod.ts";
|
||||
import { canvas, draw_rect_stroke, draw_text, measure_text, Texture } from "$/client/renderer/mod.ts";
|
||||
import { display_name } from "$/common/utils.ts";
|
||||
import { TEXT_HEIGHT, TEXT_SCALE } from "./widgets.ts";
|
||||
|
||||
const PADDING = 10;
|
||||
const CROSSHAIR_SIZE = 8;
|
||||
// how long the held item's name shows above the hotbar after it changes, and the last part of that it fades out in
|
||||
const HELD_NAME_MS = 2000;
|
||||
const HELD_NAME_FADE_MS = 500;
|
||||
const HELD_NAME_GAP = 12;
|
||||
|
||||
// what's drawn over the world while playing, like minecraft's Gui: hotbar, crosshair and chat
|
||||
export class Hud {
|
||||
// what was held last frame, as slot and item id, and when the held item last changed
|
||||
#held = "";
|
||||
#held_since = -Infinity;
|
||||
|
||||
render(client: Client) {
|
||||
this.#render_hotbar(client.player.inventories);
|
||||
this.#render_held_name(client.player.inventories);
|
||||
this.#render_crosshair();
|
||||
client.chat.render(false);
|
||||
}
|
||||
@@ -54,6 +65,30 @@ export class Hud {
|
||||
}
|
||||
}
|
||||
|
||||
// like minecraft: switching slots, or a different item ending up in the selected one, shows its name for a bit
|
||||
#render_held_name(inventories: ClientInventories) {
|
||||
const item = inventories.inventory.get_item(inventories.hotbar_selected);
|
||||
const held = item ? `${inventories.hotbar_selected} ${item.type_id}` : "";
|
||||
const now = performance.now();
|
||||
if (held !== this.#held) {
|
||||
this.#held = held;
|
||||
this.#held_since = item ? now : -Infinity;
|
||||
}
|
||||
|
||||
const shown_for = now - this.#held_since;
|
||||
if (!item || shown_for > HELD_NAME_MS) {
|
||||
return;
|
||||
}
|
||||
const alpha = Math.min(1, (HELD_NAME_MS - shown_for) / HELD_NAME_FADE_MS);
|
||||
const name = display_name(item.type_id);
|
||||
const hotbar_top = canvas.height - (PADDING * 2 + SLOT_SIZE);
|
||||
const x = (canvas.width - measure_text(name, TEXT_SCALE)) / 2;
|
||||
const y = hotbar_top - HELD_NAME_GAP - TEXT_HEIGHT * TEXT_SCALE;
|
||||
// a shadow so it reads over bright sky and snow
|
||||
draw_text(name, x + TEXT_SCALE, y + TEXT_SCALE, TEXT_SCALE, [0.15, 0.15, 0.15, alpha]);
|
||||
draw_text(name, x, y, TEXT_SCALE, [1, 1, 1, alpha]);
|
||||
}
|
||||
|
||||
#render_crosshair() {
|
||||
draw_rect_stroke(
|
||||
(canvas.width - CROSSHAIR_SIZE) / 2,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// the box next to the mouse saying what an item is, like minecraft's: its name, then its lore
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import type { ItemStack } from "$/common/inventory.ts";
|
||||
import { display_name } from "$/common/utils.ts";
|
||||
import { canvas, draw_rect, draw_text, measure_text } from "$/client/renderer/mod.ts";
|
||||
import { TEXT_HEIGHT, TEXT_SCALE } from "./widgets.ts";
|
||||
|
||||
type Color = [number, number, number, number];
|
||||
|
||||
const NAME_COLOR: Color = [1, 1, 1, 1];
|
||||
const LORE_COLOR: Color = [0.66, 0.66, 0.66, 1];
|
||||
const BACKGROUND: Color = [0.06, 0.02, 0.1, 0.94];
|
||||
const BORDER: Color = [0.31, 0.1, 0.6, 1];
|
||||
const BORDER_WIDTH = 2;
|
||||
const PADDING = 8;
|
||||
const LINE_GAP = 4;
|
||||
// from the mouse, so the cursor doesn't cover it
|
||||
const OFFSET = 16;
|
||||
|
||||
export interface TooltipLine {
|
||||
text: string;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
// its name, the item's lore from its json, then what the server's scripts said about this stack
|
||||
export function item_tooltip(item: ItemStack): TooltipLine[] {
|
||||
const lines: TooltipLine[] = [{ text: display_name(item.type_id), color: NAME_COLOR }];
|
||||
const static_lore = EverythingRegistry.get<ItemRegistry>("items", item.type_id)?.lore;
|
||||
for (const lore of [static_lore, item.lore]) {
|
||||
for (const text of lore?.split("\n") ?? []) {
|
||||
lines.push({ text, color: LORE_COLOR });
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
// below and right of the mouse, moved back inside the window when it would go past its edge
|
||||
export function draw_tooltip(lines: TooltipLine[], mouse_x: number, mouse_y: number) {
|
||||
const line_height = TEXT_HEIGHT * TEXT_SCALE;
|
||||
const width = Math.max(...lines.map((line) => measure_text(line.text, TEXT_SCALE))) + PADDING * 2;
|
||||
const height = lines.length * line_height + (lines.length - 1) * LINE_GAP + PADDING * 2;
|
||||
|
||||
let x = mouse_x + OFFSET;
|
||||
let y = mouse_y + OFFSET;
|
||||
if (x + width > canvas.width) x = mouse_x - OFFSET - width;
|
||||
if (y + height > canvas.height) y = canvas.height - height;
|
||||
x = Math.max(0, x);
|
||||
y = Math.max(0, y);
|
||||
|
||||
draw_rect(x, y, width, height, BORDER);
|
||||
draw_rect(x + BORDER_WIDTH, y + BORDER_WIDTH, width - BORDER_WIDTH * 2, height - BORDER_WIDTH * 2, BACKGROUND);
|
||||
lines.forEach((line, i) => {
|
||||
draw_text(line.text, x + PADDING, y + PADDING + i * (line_height + LINE_GAP), TEXT_SCALE, line.color);
|
||||
});
|
||||
}
|
||||
@@ -89,7 +89,8 @@ export class InputManager {
|
||||
static mouse_delta_y = 0;
|
||||
static wheel_delta = 0;
|
||||
|
||||
static typed_characters = new Set<string>();
|
||||
// in the order they were typed. a list, since the same letter can be typed twice in one frame
|
||||
static typed_characters: string[] = [];
|
||||
|
||||
static pointer_lock_flag = false;
|
||||
static mouse_grab_timer = 0;
|
||||
@@ -108,7 +109,7 @@ export class InputManager {
|
||||
}
|
||||
this.keys_down.add(e.code);
|
||||
if (e.key.length === 1) {
|
||||
this.typed_characters.add(e.key);
|
||||
this.typed_characters.push(e.key);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -221,8 +222,8 @@ export class InputManager {
|
||||
}
|
||||
|
||||
static get_typed_characters(): string[] {
|
||||
const chars = [...this.typed_characters];
|
||||
this.typed_characters.clear();
|
||||
const chars = this.typed_characters;
|
||||
this.typed_characters = [];
|
||||
return chars;
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ export class InputManager {
|
||||
this.mouse_delta_x = 0;
|
||||
this.mouse_delta_y = 0;
|
||||
this.wheel_delta = 0;
|
||||
this.typed_characters.clear();
|
||||
this.typed_characters = [];
|
||||
this.mouse_buttons_consumed.clear();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user