feat(player): add mobile directional pad controls
This commit is contained in:
@@ -63,6 +63,22 @@ test("latest movement intent coalesces while one command is in flight", () => {
|
||||
assert.equal(flow.latest, null);
|
||||
});
|
||||
|
||||
test("directional pad repeat remains slower than the authoritative cooldown", () => {
|
||||
assert.ok(player.DIRECTIONAL_PAD_REPEAT_MS > 150);
|
||||
const dispatched = [];
|
||||
const flow = new player.LatestIntentFlow((direction, element) => {
|
||||
dispatched.push({direction, element});
|
||||
return "move-1";
|
||||
});
|
||||
const button = {id: "north"};
|
||||
flow.request("north", button);
|
||||
flow.request("east", {id: "east"});
|
||||
assert.deepEqual(flow.latest, {direction: "east", element: {id: "east"}});
|
||||
flow.release();
|
||||
assert.equal(flow.latest, null);
|
||||
assert.deepEqual(dispatched, [{direction: "north", element: button}]);
|
||||
});
|
||||
|
||||
test("latency correlation is bounded and only completes matching authoritative views", () => {
|
||||
let clock = 0;
|
||||
const tracker = new player.LatencyTracker(() => clock);
|
||||
@@ -115,9 +131,9 @@ test("avatar control resets safely and retains accessible alternatives", () => {
|
||||
assert.match(script, /addEventListener\("pointerup", stopAvatarControl\)/);
|
||||
assert.match(script, /addEventListener\("pointercancel", stopAvatarControl\)/);
|
||||
assert.match(script, /addEventListener\("lostpointercapture", stopAvatarControl\)/);
|
||||
assert.match(script, /addEventListener\("blur", resetAvatarControl\)/);
|
||||
assert.match(script, /connectionState !== "online"\) resetAvatarControl\(\)/);
|
||||
assert.match(script, /snapshot\.phase === "paused"\) \{ resetAvatarControl\(\)/);
|
||||
assert.match(script, /addEventListener\("blur", resetPlayerControls\)/);
|
||||
assert.match(script, /connectionState !== "online"\) resetPlayerControls\(\)/);
|
||||
assert.match(script, /snapshot\.phase === "paused"\) \{ resetPlayerControls\(\)/);
|
||||
assert.match(script, /state\.avatar\.startX = event\.clientX/);
|
||||
assert.match(script, /event\.clientX - state\.avatar\.startX/);
|
||||
assert.match(script, /setMuted\(!state\.muted\)/);
|
||||
@@ -128,6 +144,29 @@ test("avatar control resets safely and retains accessible alternatives", () => {
|
||||
assert.match(html, /id="onboarding-dots"[\s\S]*?<span><\/span><span><\/span><span><\/span><span><\/span><span><\/span><span><\/span><span><\/span><span><\/span>/);
|
||||
});
|
||||
|
||||
test("mobile directional pad is explicit, bounded, and resettable", () => {
|
||||
const html = fs.readFileSync(path.join(project, "web/index.html"), "utf8");
|
||||
const css = fs.readFileSync(path.join(project, "web/player.css"), "utf8");
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
for (const label of ["Двигаться вверх", "Двигаться вниз", "Двигаться влево", "Двигаться вправо"]) {
|
||||
assert.match(html, new RegExp(`aria-label="${label}"`));
|
||||
}
|
||||
assert.match(html, /class="dpad-center" aria-hidden="true"/);
|
||||
assert.match(html, /data-direction="north"[^>]*disabled/);
|
||||
assert.match(css, /\.dpad \{[^}]*grid-template-rows: repeat\(3, 58px\)/);
|
||||
assert.match(css, /\.dpad button:disabled/);
|
||||
assert.match(css, /\.dpad button\.active/);
|
||||
assert.match(css, /\.dpad button\.pending/);
|
||||
assert.match(css, /\.dpad \{[^}]*touch-action: none/);
|
||||
assert.match(css, /\.dpad button \{[^}]*touch-action: none/);
|
||||
assert.match(css, /grid-template-columns: repeat\(3, 62px\)/);
|
||||
assert.match(script, /function resetDirectionalPad\(\)/);
|
||||
assert.match(script, /addEventListener\("pointercancel", stopDirectionalPad\)/);
|
||||
assert.match(script, /addEventListener\("lostpointercapture", stopDirectionalPad\)/);
|
||||
assert.match(script, /windowObject\.addEventListener\("blur", resetPlayerControls\)/);
|
||||
assert.match(script, /state\.moveFlow\.request\(direction, element\)/);
|
||||
});
|
||||
|
||||
test("frontend name suggestions randomize both words and remain bounded", () => {
|
||||
const picks = [3, 4];
|
||||
const suggestion = player.randomSuggestedName([], () => picks.shift());
|
||||
@@ -217,7 +256,7 @@ test("player assets are local, Russian, accessible, and responsive", () => {
|
||||
assert.match(css, /@media \(max-width: 720px\)/);
|
||||
assert.match(css, /@media \(min-width: 721px\) and \(max-width: 980px\)/);
|
||||
assert.match(css, /height: calc\(100dvh - 78px\)/);
|
||||
assert.match(css, /\.dpad \{ display: none; \}/);
|
||||
assert.match(css, /\.dpad \{[^}]*grid-template-columns: repeat\(3, 58px\)/);
|
||||
assert.match(html, /id="avatar-control"/);
|
||||
assert.match(html, /id="onboarding"/);
|
||||
assert.match(html, /id="mute-toggle"/);
|
||||
|
||||
Reference in New Issue
Block a user