fix(ui): Landeplatz nur auf ganzem Kollisionsboden, Trackpad-Zoom, Fokus und Ladefehler der Minimap
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e702517662
commit
dd5ced505b
9 files changed
+263
-54
No files matched your search
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"pairs": [["A1", 0, 0], ["L1", 11, 0], ["A12", 0, 11], ["L12", 11, 11], ["C10", 2, 9]],
|
||||
"lowercase": [["c10", 2, 9], ["l12", 11, 11]],
|
||||
"invalid": ["M1", "A0", "A13", "A", "10", "", "A01", "B-3", " ", "A1\n"],
|
||||
"invalid": ["M1", "A0", "A13", "A", "10", "", "A01", "B-3", " ", "A1\n", "A1١", "A11"],
|
||||
"invalidIndices": [[1.5, 3], [3, 2.5], [true, 0], [0, false]]
|
||||
}
|
||||
+57
-12
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CLICK_TOLERANCE_PX, chunkNameAtMap, clampView, fitView, headingFromDirection, hoverLabel, isClick,
|
||||
isMapToggleKey, landingFor, mapToWorld, nearestWalkable, screenToMap, sha256Hex, worldToMap, zoomAt,
|
||||
isMapToggleKey, isTextEntry, landingFor, mapFilesProblem, mapToWorld, maskMatches, nearestWalkable,
|
||||
screenToMap, sha256Hex, wheelZoomFactor, worldToMap, zoomAt,
|
||||
} from "../src/ui/mapMath.js";
|
||||
|
||||
const INFO = { scale: 1, walkable_scale: 0.25, slice: { x0: -1000, x1: 2000, y0: -500, y1: 2500 } };
|
||||
@@ -71,16 +72,17 @@ describe("Klick gegen Ziehen", () => {
|
||||
expect(CLICK_TOLERANCE_PX).toBe(4);
|
||||
expect(isClick({ x: 10, y: 10 }, { x: 13.9, y: 10 })).toBe(true);
|
||||
expect(isClick({ x: 10, y: 10 }, { x: 14, y: 10 })).toBe(false);
|
||||
expect(isClick({ x: 10, y: 10 }, { x: 10, y: 10 }, 4, 30)).toBe(false); // weggezogen und zurück
|
||||
});
|
||||
});
|
||||
|
||||
describe("Landeplatz", () => {
|
||||
const blank = (w, h) => new Uint8Array(w * h);
|
||||
|
||||
it("bleibt bei einem begehbaren Treffer genau am Klickpunkt", () => {
|
||||
it("landet bei einem begehbaren Treffer in der Mitte dieses Pixels", () => {
|
||||
const mask = blank(10, 10);
|
||||
mask[2 * 10 + 5] = 255;
|
||||
expect(nearestWalkable(mask, 10, 10, 5.4, 2.2, 5)).toEqual({ x: 5.4, y: 2.2 });
|
||||
expect(nearestWalkable(mask, 10, 10, 5.4, 2.2, 5)).toEqual({ x: 5.5, y: 2.5 });
|
||||
});
|
||||
|
||||
it("findet von einem Gebäude aus die Straße daneben", () => {
|
||||
@@ -102,10 +104,10 @@ describe("Landeplatz", () => {
|
||||
expect(nearestWalkable(beyond, 20, 20, 3, 3, 5)).toBeNull();
|
||||
});
|
||||
|
||||
it("rechnet einen Kartenklick über die Maske in eine Weltposition um", () => {
|
||||
it("rechnet einen Kartenklick über die Maske in eine Weltposition in der Pixelmitte um", () => {
|
||||
const walk = { mask: blank(750, 750), width: 750, height: 750 };
|
||||
walk.mask[200 * 750 + 100] = 255; // Maskenpixel (100, 200)
|
||||
expect(landingFor(INFO, walk, 400, 800)).toEqual({ x: -600, z: -1700 }); // Treffer: genau dorthin
|
||||
walk.mask[200 * 750 + 100] = 255; // Maskenpixel (100, 200) = Karte 400..404 / 800..804
|
||||
expect(landingFor(INFO, walk, 401, 801)).toEqual({ x: -598, z: -1698 }); // Treffer: Pixelmitte (402, 802)
|
||||
const nearby = landingFor(INFO, walk, 412, 800); // 3 Maskenpixel daneben
|
||||
expect(nearby.x).toBeCloseTo(-598, 9); // Mitte von (100, 200) = Karte (402, 802)
|
||||
expect(nearby.z).toBeCloseTo(-1698, 9);
|
||||
@@ -138,12 +140,24 @@ describe("Richtung und Tasten", () => {
|
||||
|
||||
it("schaltet mit M, aber nicht beim Tippen, nicht bei Wiederholung und nicht mit Strg", () => {
|
||||
const key = (extra) => ({ code: "KeyM", repeat: false, ctrlKey: false, metaKey: false, altKey: false, ...extra });
|
||||
expect(isMapToggleKey(key({}), "BODY")).toBe(true);
|
||||
expect(isMapToggleKey(key({}), "INPUT")).toBe(false);
|
||||
expect(isMapToggleKey(key({}), "SELECT")).toBe(false);
|
||||
expect(isMapToggleKey(key({ repeat: true }), "BODY")).toBe(false);
|
||||
expect(isMapToggleKey(key({ ctrlKey: true }), "BODY")).toBe(false);
|
||||
expect(isMapToggleKey(key({ code: "KeyN" }), "BODY")).toBe(false);
|
||||
expect(isMapToggleKey(key({}), false)).toBe(true);
|
||||
expect(isMapToggleKey(key({}), true)).toBe(false);
|
||||
expect(isMapToggleKey(key({ repeat: true }), false)).toBe(false);
|
||||
expect(isMapToggleKey(key({ ctrlKey: true }), false)).toBe(false);
|
||||
expect(isMapToggleKey(key({ code: "KeyN" }), false)).toBe(false);
|
||||
});
|
||||
|
||||
it("zählt Text- und Zahlenfelder als Tippen, Auswahllisten und Kontrollkästchen nicht", () => {
|
||||
expect(isTextEntry({ tagName: "INPUT", type: "text" })).toBe(true);
|
||||
expect(isTextEntry({ tagName: "INPUT", type: "number" })).toBe(true);
|
||||
expect(isTextEntry({ tagName: "INPUT" })).toBe(true);
|
||||
expect(isTextEntry({ tagName: "TEXTAREA" })).toBe(true);
|
||||
expect(isTextEntry({ tagName: "DIV", isContentEditable: true })).toBe(true);
|
||||
expect(isTextEntry({ tagName: "SELECT" })).toBe(false);
|
||||
expect(isTextEntry({ tagName: "INPUT", type: "checkbox" })).toBe(false);
|
||||
expect(isTextEntry({ tagName: "INPUT", type: "color" })).toBe(false);
|
||||
expect(isTextEntry({ tagName: "BODY" })).toBe(false);
|
||||
expect(isTextEntry(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,3 +171,34 @@ describe("Fingerabdruck", () => {
|
||||
expect(await sha256Hex(new TextEncoder().encode("abc"), null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mausrad und Trackpad", () => {
|
||||
it("zoomt proportional zur Scrollstrecke statt je Ereignis um einen festen Schritt", () => {
|
||||
expect(wheelZoomFactor(-100)).toBeGreaterThan(1.1);
|
||||
expect(wheelZoomFactor(-100)).toBeLessThan(1.25);
|
||||
expect(wheelZoomFactor(100)).toBeCloseTo(1 / wheelZoomFactor(-100), 9);
|
||||
expect(wheelZoomFactor(-4) ** 25).toBeCloseTo(wheelZoomFactor(-100), 9); // Trackpad: viele kleine Schritte
|
||||
});
|
||||
|
||||
it("rechnet Zeilen, Seiten und die Pinch-Geste in Pixel um und zoomt bei reiner Querbewegung nicht", () => {
|
||||
expect(wheelZoomFactor(-3, 1)).toBeCloseTo(wheelZoomFactor(-48), 9);
|
||||
expect(wheelZoomFactor(-1, 2)).toBeCloseTo(wheelZoomFactor(-100), 9);
|
||||
expect(wheelZoomFactor(-2, 0, true)).toBeCloseTo(wheelZoomFactor(-20), 9);
|
||||
expect(wheelZoomFactor(0)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kartendateien prüfen", () => {
|
||||
it("nimmt eine passende map.json mit passenden Bildern an", () => {
|
||||
expect(mapFilesProblem(INFO, 3000, 3000)).toBeNull();
|
||||
expect(maskMatches(INFO, 750, 750)).toBe(true);
|
||||
});
|
||||
|
||||
it("benennt eine unvollständige map.json oder ein unpassendes Bild, statt später zu werfen", () => {
|
||||
expect(mapFilesProblem({ ...INFO, scale: undefined }, 3000, 3000)).toMatch(/scale/);
|
||||
expect(mapFilesProblem({ ...INFO, slice: undefined }, 3000, 3000)).toMatch(/slice/);
|
||||
expect(mapFilesProblem(null, 3000, 3000)).toMatch(/scale/);
|
||||
expect(mapFilesProblem(INFO, 1500, 1500)).toMatch(/map\.png/);
|
||||
expect(maskMatches(INFO, 375, 375)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user