feat(player): add avatar-centered drag steering
This commit is contained in:
+52
-23
@@ -22,25 +22,45 @@ test("keyboard and swipe controls produce equivalent directions", () => {
|
||||
assert.equal(player.directionFromSwipe(0, 0, 12, 4), null);
|
||||
});
|
||||
|
||||
test("joystick produces only cardinal, bounded movement intentions", () => {
|
||||
assert.equal(player.joystickDirection(10, 10, null), null);
|
||||
assert.equal(player.joystickDirection(44, 8, null), "east");
|
||||
assert.equal(player.joystickDirection(-8, -44, null), "north");
|
||||
assert.equal(player.joystickDirection(42, 38, "east"), "east");
|
||||
assert.equal(player.joystickDirection(38, 42, "north"), "south");
|
||||
const vector = player.joystickVector(300, 0, 57, null);
|
||||
test("avatar drag produces only cardinal, bounded movement intentions", () => {
|
||||
assert.equal(player.avatarDirection(10, 10, null), null);
|
||||
assert.equal(player.avatarDirection(44, 8, null), "east");
|
||||
assert.equal(player.avatarDirection(-8, -44, null), "north");
|
||||
assert.equal(player.avatarDirection(42, 38, "east"), "east");
|
||||
assert.equal(player.avatarDirection(38, 42, "north"), "south");
|
||||
const vector = player.avatarVector(300, 0, 48, null);
|
||||
assert.equal(vector.direction, "east");
|
||||
assert.equal(vector.x, 57);
|
||||
assert.equal(vector.x, player.AVATAR_MAX_DEFLECTION);
|
||||
assert.equal(vector.y, 0);
|
||||
assert.ok(vector.distance <= player.AVATAR_MAX_DEFLECTION);
|
||||
});
|
||||
|
||||
test("joystick repeat interval is monotonic and remains slower than cooldown", () => {
|
||||
assert.equal(player.joystickRepeatMs(player.JOYSTICK_DEAD_ZONE, 60), null);
|
||||
const near = player.joystickRepeatMs(25, 60);
|
||||
const far = player.joystickRepeatMs(60, 60);
|
||||
test("avatar cadence is monotonic and remains slower than cooldown", () => {
|
||||
assert.equal(player.avatarRepeatMs(player.AVATAR_DEAD_ZONE, 48), null);
|
||||
const near = player.avatarRepeatMs(18, 48);
|
||||
const far = player.avatarRepeatMs(48, 48);
|
||||
assert.ok(near > far);
|
||||
assert.ok(far >= player.JOYSTICK_FAST_REPEAT_MS);
|
||||
assert.ok(player.JOYSTICK_FAST_REPEAT_MS > 150);
|
||||
assert.ok(far >= player.AVATAR_FAST_REPEAT_MS);
|
||||
assert.ok(player.AVATAR_FAST_REPEAT_MS > 150);
|
||||
});
|
||||
|
||||
test("latest movement intent coalesces while one command is in flight", () => {
|
||||
const dispatched = [];
|
||||
const flow = new player.LatestIntentFlow(direction => {
|
||||
const requestId = `move-${dispatched.length + 1}`;
|
||||
dispatched.push({direction, requestId});
|
||||
return requestId;
|
||||
});
|
||||
assert.equal(flow.request("east"), "move-1");
|
||||
assert.equal(flow.request("north"), null);
|
||||
assert.equal(flow.request("west"), null);
|
||||
assert.deepEqual(dispatched.map(item => item.direction), ["east"]);
|
||||
flow.complete("move-1");
|
||||
assert.deepEqual(dispatched.map(item => item.direction), ["east", "west"]);
|
||||
flow.release();
|
||||
flow.complete("move-2");
|
||||
assert.equal(flow.inFlight, null);
|
||||
assert.equal(flow.latest, null);
|
||||
});
|
||||
|
||||
test("latency correlation is bounded and only completes matching authoritative views", () => {
|
||||
@@ -60,6 +80,11 @@ test("latency correlation is bounded and only completes matching authoritative v
|
||||
tracker.sent(`move-${index + 2}`);
|
||||
}
|
||||
assert.ok(tracker.pending.size <= player.LATENCY_SAMPLE_LIMIT);
|
||||
for (let index = 0; index < player.LATENCY_SAMPLE_LIMIT + 4; index++) {
|
||||
clock++;
|
||||
tracker.pointerPainted(0);
|
||||
}
|
||||
assert.equal(tracker.pointerSnapshot().length, player.LATENCY_SAMPLE_LIMIT);
|
||||
});
|
||||
|
||||
test("player map cells enlarge small projections without exceeding a readable bound", () => {
|
||||
@@ -77,16 +102,19 @@ test("onboarding has bounded, replayable steps", () => {
|
||||
assert.equal(player.nextOnboardingStep(7), null);
|
||||
});
|
||||
|
||||
test("child controls reset safely and retain accessible alternatives", () => {
|
||||
test("avatar control resets safely and retains accessible alternatives", () => {
|
||||
const html = fs.readFileSync(path.join(project, "web/index.html"), "utf8");
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
assert.match(script, /addEventListener\("pointerup", stopJoystick\)/);
|
||||
assert.match(script, /addEventListener\("pointercancel", stopJoystick\)/);
|
||||
assert.match(script, /addEventListener\("blur", resetJoystick\)/);
|
||||
assert.match(script, /connectionState !== "online"\) resetJoystick\(\)/);
|
||||
assert.match(script, /snapshot\.phase === "paused"\) \{ resetJoystick\(\)/);
|
||||
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, /setMuted\(!state\.muted\)/);
|
||||
assert.match(script, /setReducedMotion\(!state\.reducedMotion\)/);
|
||||
assert.match(html, /id="avatar-control"/);
|
||||
assert.match(html, /aria-label="Тянуть героя для движения"/);
|
||||
assert.match(script, /onboarding-replay/);
|
||||
assert.match(html, /id="onboarding-dots"[\s\S]*?<span><\/span><span><\/span><span><\/span><span><\/span><span><\/span><span><\/span><span><\/span><span><\/span>/);
|
||||
});
|
||||
@@ -181,11 +209,12 @@ test("player assets are local, Russian, accessible, and responsive", () => {
|
||||
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(html, /id="joystick"/);
|
||||
assert.match(html, /id="avatar-control"/);
|
||||
assert.match(html, /id="onboarding"/);
|
||||
assert.match(html, /id="mute-toggle"/);
|
||||
assert.match(css, /\.joystick \{[^}]*width: 180px; height: 180px/);
|
||||
assert.match(script, /resetJoystick/);
|
||||
assert.match(css, /\.avatar-control \{[^}]*width: 96px; height: 96px/);
|
||||
assert.match(css, /\.avatar-handle \{[^}]*transition: transform/);
|
||||
assert.match(script, /resetAvatarControl/);
|
||||
assert.match(script, /playerCellSize/);
|
||||
assert.match(css, /\(hover: none\) and \(pointer: coarse\) and \(max-height: 720px\)/);
|
||||
assert.match(icons, /id="key"/);
|
||||
|
||||
Reference in New Issue
Block a user