mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
fix(mapview): preserve map proportions at minimum zoom
This commit is contained in:
1 parent
8b7be3de4b
commit
e504d6adb3
3 files changed
+154
-26
No files matched your search
+1
-1
@@ -6,7 +6,7 @@
|
||||
"docs:api": "bun sdk/generate-api-docs.ts",
|
||||
"docs:api:check": "bun sdk/generate-api-docs.ts --check",
|
||||
"fetch-collision-data": "bun sdk/fetch-collision-data.ts",
|
||||
"test": "bun test sdk/test server/webclient/src/bot server/webclient/src/util server/webclient/src/lite server/engine/test/market.test.ts server/engine/test/market-web.test.ts server/engine/test/item-search.test.ts server/engine/test/file-stream.test.ts server/engine/test/get-ip.test.ts server/engine/test/bank-hiscores.test.ts server/engine/test/reconnect.test.ts server/engine/test/worldfeed.test.ts server/gateway/state-fanout.test.ts",
|
||||
"test": "bun test sdk/test server/webclient/src/bot server/webclient/src/util server/webclient/src/lite server/webclient/src/mapview server/engine/test/market.test.ts server/engine/test/market-web.test.ts server/engine/test/item-search.test.ts server/engine/test/file-stream.test.ts server/engine/test/get-ip.test.ts server/engine/test/bank-hiscores.test.ts server/engine/test/reconnect.test.ts server/engine/test/worldfeed.test.ts server/gateway/state-fanout.test.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:webclient": "tsc --noEmit -p server/webclient/tsconfig.json",
|
||||
"test:market:integration": "cd server/engine && bun test/market-integration.ts && bun test/market-recovery.ts && bun test/market-protocol.ts && bun test/market-disabled.ts"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import '../lite/dom-shim.js';
|
||||
import Pix2D from '#/graphics/Pix2D.js';
|
||||
import { MapView } from './MapView.js';
|
||||
|
||||
const GROUND = 0x336699;
|
||||
const LANDMARK = 0xffcc00;
|
||||
let flags: boolean[];
|
||||
|
||||
beforeEach(() => {
|
||||
flags = [MapView.shouldDrawHistory, MapView.shouldDrawPlayers, MapView.shouldDrawLabels,
|
||||
MapView.shouldDrawFreemap, MapView.shouldDrawMultimap, MapView.shouldDrawItems, MapView.shouldDrawNpcs];
|
||||
MapView.shouldDrawHistory = MapView.shouldDrawPlayers = MapView.shouldDrawLabels = false;
|
||||
MapView.shouldDrawFreemap = MapView.shouldDrawMultimap = MapView.shouldDrawItems = MapView.shouldDrawNpcs = false;
|
||||
});
|
||||
afterEach(() => {
|
||||
[MapView.shouldDrawHistory, MapView.shouldDrawPlayers, MapView.shouldDrawLabels,
|
||||
MapView.shouldDrawFreemap, MapView.shouldDrawMultimap, MapView.shouldDrawItems, MapView.shouldDrawNpcs] = flags;
|
||||
});
|
||||
|
||||
function fixture(width: number, height: number, mapWidth: number, mapHeight: number, zoom: number) {
|
||||
const grid = () => Array.from({ length: mapWidth }, () => new Int32Array(mapHeight));
|
||||
const ground = grid();
|
||||
for (let x = 0; x < mapWidth; x++) {
|
||||
ground[x]!.fill(GROUND);
|
||||
for (let y = 0; y < mapHeight; y++) {
|
||||
if (x >= mapWidth / 2 - 4 && x < mapWidth / 2 + 4 &&
|
||||
y >= mapHeight / 2 - 4 && y < mapHeight / 2 + 4) ground[x]![y] = LANDMARK;
|
||||
}
|
||||
}
|
||||
// Avoid starting GameShell's loop: the actual redraw and rasterizer run
|
||||
// against a tiny synthetic map, with only peripheral controls stubbed.
|
||||
const view = Object.assign(Object.defineProperties(Object.create(MapView.prototype), {
|
||||
sWid: { value: width }, sHei: { value: height }
|
||||
}), {
|
||||
mapWidth, mapHeight, zoom,
|
||||
focusX: mapWidth / 2, focusZ: mapHeight / 2,
|
||||
redraw: true, redrawTimer: 0, showOverview: false, showKey: false,
|
||||
blendedGroundColour: ground, floort2: grid(), floorsr: grid(),
|
||||
locWall: grid(), locMapscene: grid(), locMapfunction: grid(),
|
||||
freePos: grid(), multiPos: grid(), objPos: grid(), npcPos: grid(),
|
||||
mapLabelCount: 0, flashTimer: 0, visibleMapFunctions: [],
|
||||
visibleMapFunctionsX: [], visibleMapFunctionsY: [],
|
||||
drawStringBox() {}, drawArea: null
|
||||
});
|
||||
Pix2D.setPixels(new Int32Array(width * height), width, height);
|
||||
return view;
|
||||
}
|
||||
|
||||
function landmarkSize(width: number, height: number) {
|
||||
const xs: number[] = [], ys: number[] = [];
|
||||
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
||||
if (Pix2D.pixels[y * width + x] === LANDMARK) { xs.push(x); ys.push(y); }
|
||||
}
|
||||
return { width: Math.max(...xs) - Math.min(...xs) + 1, height: Math.max(...ys) - Math.min(...ys) + 1 };
|
||||
}
|
||||
|
||||
describe('map viewport scaling', () => {
|
||||
for (const [label, width, height, mapWidth, mapHeight] of [
|
||||
['wide', 120, 48, 60, 100],
|
||||
['tall', 48, 120, 100, 60],
|
||||
['both axes beyond the map', 120, 120, 60, 60]
|
||||
] as const) {
|
||||
test(`minimum zoom keeps an 8-tile square square in a ${label} viewport`, async () => {
|
||||
const view = fixture(width, height, mapWidth, mapHeight, 1.5);
|
||||
await view.mainredraw();
|
||||
expect(landmarkSize(width, height)).toEqual({ width: 6, height: 6 });
|
||||
expect(Pix2D.pixels[0]).toBe(0); // unused viewport stays black
|
||||
expect(Pix2D.pixels[(height / 2) * width + width / 2]).toBe(LANDMARK);
|
||||
});
|
||||
}
|
||||
|
||||
test('ordinary zoom preserves existing tile scale', async () => {
|
||||
const view = fixture(120, 48, 200, 200, 4);
|
||||
await view.mainredraw();
|
||||
expect(landmarkSize(120, 48)).toEqual({ width: 16, height: 16 });
|
||||
expect(Pix2D.pixels[0]).toBe(GROUND);
|
||||
});
|
||||
|
||||
test('blank margins do not hide labels that were visible when the map fit', async () => {
|
||||
const view = fixture(120, 48, 60, 100, 1.5);
|
||||
const labels: unknown[][] = [];
|
||||
Object.assign(view, {
|
||||
mapLabelCount: 1, mapLabel: ['Landmark'], mapLabelX: [30], mapLabelY: [50],
|
||||
mapLabelSize: [0], mapOriginX: 0, mapOriginZ: 0,
|
||||
f11: { getHeight: () => 10, getYOffset: () => 0,
|
||||
centreString: (...args: unknown[]) => labels.push(args) }
|
||||
});
|
||||
MapView.shouldDrawLabels = true;
|
||||
await view.mainredraw();
|
||||
expect(labels).toEqual([['Landmark', 60, 24, 0xffffff, true]]);
|
||||
});
|
||||
|
||||
test('player and history layers receive the same uncropped viewport', async () => {
|
||||
const view = fixture(120, 48, 60, 100, 1.5);
|
||||
const calls: number[][] = [];
|
||||
view.drawHistory = (...args: number[]) => calls.push(args);
|
||||
view.drawPlayers = (...args: number[]) => calls.push(args);
|
||||
MapView.shouldDrawHistory = MapView.shouldDrawPlayers = true;
|
||||
await view.mainredraw();
|
||||
expect(calls).toEqual([
|
||||
[-50, 18, 110, 82, 0, 0, 120, 48],
|
||||
[-50, 18, 110, 82, 0, 0, 120, 48]
|
||||
]);
|
||||
});
|
||||
|
||||
test('detail and region overlays safely clip tile iteration at every edge', async () => {
|
||||
const view = fixture(120, 120, 40, 40, 4);
|
||||
MapView.shouldDrawFreemap = MapView.shouldDrawMultimap = MapView.shouldDrawItems = MapView.shouldDrawNpcs = true;
|
||||
await view.mainredraw();
|
||||
expect(landmarkSize(120, 120)).toEqual({ width: 16, height: 16 });
|
||||
expect(Pix2D.pixels[0]).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -465,10 +465,12 @@ export class MapView extends GameShell {
|
||||
|
||||
Pix2D.cls();
|
||||
|
||||
const left: number = Math.max(0, this.focusX - ((this.sWid / this.zoom) | 0));
|
||||
const top: number = Math.max(0, this.focusZ - ((this.sHei / this.zoom) | 0));
|
||||
const right: number = Math.min(this.mapWidth, this.focusX + ((this.sWid / this.zoom) | 0));
|
||||
const bottom: number = Math.min(this.mapHeight, this.focusZ + ((this.sHei / this.zoom) | 0));
|
||||
// Keep the full viewport even beyond the map edges. Cropping these
|
||||
// bounds then stretching them to the canvas changes each axis's scale.
|
||||
const left: number = this.focusX - ((this.sWid / this.zoom) | 0);
|
||||
const top: number = this.focusZ - ((this.sHei / this.zoom) | 0);
|
||||
const right: number = this.focusX + ((this.sWid / this.zoom) | 0);
|
||||
const bottom: number = this.focusZ + ((this.sHei / this.zoom) | 0);
|
||||
this.renderWorldMap(left, top, right, bottom, 0, 0, this.sWid, this.sHei);
|
||||
|
||||
// custom: long-term movement traces under the live player layer
|
||||
@@ -489,19 +491,24 @@ export class MapView extends GameShell {
|
||||
if (this.showOverview) {
|
||||
this.overview?.quickPlotSprite(this.overviewX, this.overviewY);
|
||||
|
||||
const overviewLeft = Math.max(0, left);
|
||||
const overviewTop = Math.max(0, top);
|
||||
const overviewRight = Math.min(this.mapWidth, right);
|
||||
const overviewBottom = Math.min(this.mapHeight, bottom);
|
||||
|
||||
Pix2D.fillRectTrans(
|
||||
(this.overviewX + (this.overviewWidth * left) / this.mapWidth) | 0,
|
||||
(this.overviewY + (this.overviewHeight * top) / this.mapHeight) | 0,
|
||||
(((right - left) * this.overviewWidth) / this.mapWidth) | 0,
|
||||
(((bottom - top) * this.overviewHeight) / this.mapHeight) | 0,
|
||||
(this.overviewX + (this.overviewWidth * overviewLeft) / this.mapWidth) | 0,
|
||||
(this.overviewY + (this.overviewHeight * overviewTop) / this.mapHeight) | 0,
|
||||
(((overviewRight - overviewLeft) * this.overviewWidth) / this.mapWidth) | 0,
|
||||
(((overviewBottom - overviewTop) * this.overviewHeight) / this.mapHeight) | 0,
|
||||
0xff0000,
|
||||
0x80
|
||||
);
|
||||
Pix2D.drawRect(
|
||||
(this.overviewX + (this.overviewWidth * left) / this.mapWidth) | 0,
|
||||
(this.overviewY + (this.overviewHeight * top) / this.mapHeight) | 0,
|
||||
(((right - left) * this.overviewWidth) / this.mapWidth) | 0,
|
||||
(((bottom - top) * this.overviewHeight) / this.mapHeight) | 0,
|
||||
(this.overviewX + (this.overviewWidth * overviewLeft) / this.mapWidth) | 0,
|
||||
(this.overviewY + (this.overviewHeight * overviewTop) / this.mapHeight) | 0,
|
||||
(((overviewRight - overviewLeft) * this.overviewWidth) / this.mapWidth) | 0,
|
||||
(((overviewBottom - overviewTop) * this.overviewHeight) / this.mapHeight) | 0,
|
||||
0xff0000
|
||||
);
|
||||
|
||||
@@ -1210,8 +1217,13 @@ export class MapView extends GameShell {
|
||||
const visibleY: number = bottom - top;
|
||||
const widthRatio: number = (((width - widthOffset) << 16) / visibleX) | 0;
|
||||
const heightRatio: number = (((height - heightOffset) << 16) / visibleY) | 0;
|
||||
// Clip tile iteration, rather than the viewport used for projection.
|
||||
const startTileX = Math.max(0, -left);
|
||||
const endTileX = Math.min(visibleX, this.mapWidth - left);
|
||||
const startTileY = Math.max(0, -top);
|
||||
const endTileY = Math.min(visibleY, this.mapHeight - top);
|
||||
|
||||
for (let x: number = 0; x < visibleX; x++) {
|
||||
for (let x: number = startTileX; x < endTileX; x++) {
|
||||
let startX: number = (widthRatio * x) >> 16;
|
||||
let endX: number = (widthRatio * (x + 1)) >> 16;
|
||||
const lengthX: number = endX - startX;
|
||||
@@ -1226,7 +1238,7 @@ export class MapView extends GameShell {
|
||||
const overlays = this.floort2[x + left];
|
||||
const shapes = this.floorsr[x + left];
|
||||
|
||||
for (let y: number = 0; y < visibleY; y++) {
|
||||
for (let y: number = startTileY; y < endTileY; y++) {
|
||||
let startY: number = (heightRatio * y) >> 16;
|
||||
let endY: number = (heightRatio * (y + 1)) >> 16;
|
||||
const lengthY: number = endY - startY;
|
||||
@@ -1252,12 +1264,14 @@ export class MapView extends GameShell {
|
||||
}
|
||||
}
|
||||
|
||||
if (right - left > width - widthOffset) {
|
||||
// Keep the existing detail cutoff based on actual map tiles, excluding
|
||||
// blank space beyond the edges of the viewport.
|
||||
if (endTileX - startTileX > width - widthOffset) {
|
||||
return;
|
||||
}
|
||||
|
||||
let visibleMapFunctionCount: number = 0;
|
||||
for (let x: number = 0; x < visibleX; x++) {
|
||||
for (let x: number = startTileX; x < endTileX; x++) {
|
||||
let startX: number = (widthRatio * x) >> 16;
|
||||
let endX: number = (widthRatio * (x + 1)) >> 16;
|
||||
const lengthX: number = endX - startX;
|
||||
@@ -1269,7 +1283,7 @@ export class MapView extends GameShell {
|
||||
const mapscenes = this.locMapscene[x + left];
|
||||
const mapfunctions = this.locMapfunction[x + left];
|
||||
|
||||
for (let y: number = 0; y < visibleY; y++) {
|
||||
for (let y: number = startTileY; y < endTileY; y++) {
|
||||
let startY: number = (heightRatio * y) >> 16;
|
||||
let endY: number = (heightRatio * (y + 1)) >> 16;
|
||||
const lengthY: number = endY - startY;
|
||||
@@ -1363,7 +1377,7 @@ export class MapView extends GameShell {
|
||||
}
|
||||
|
||||
if (MapView.shouldDrawFreemap) {
|
||||
for (let x = 0; x < visibleX; x++) {
|
||||
for (let x = startTileX; x < endTileX; x++) {
|
||||
let startX = widthRatio * x >> 16;
|
||||
let endX = widthRatio * (x + 1) >> 16;
|
||||
let lengthX = endX - startX;
|
||||
@@ -1375,7 +1389,7 @@ export class MapView extends GameShell {
|
||||
endX += widthOffset;
|
||||
|
||||
let multi = this.freePos[x + left];
|
||||
for (let y = 0; y < visibleY; y++) {
|
||||
for (let y = startTileY; y < endTileY; y++) {
|
||||
let startY = heightRatio * y >> 16;
|
||||
let endY = heightRatio * (y + 1) >> 16;
|
||||
let lengthY = endY - startY;
|
||||
@@ -1394,7 +1408,7 @@ export class MapView extends GameShell {
|
||||
}
|
||||
|
||||
if (MapView.shouldDrawMultimap) {
|
||||
for (let x = 0; x < visibleX; x++) {
|
||||
for (let x = startTileX; x < endTileX; x++) {
|
||||
let startX = widthRatio * x >> 16;
|
||||
let endX = widthRatio * (x + 1) >> 16;
|
||||
let lengthX = endX - startX;
|
||||
@@ -1406,7 +1420,7 @@ export class MapView extends GameShell {
|
||||
endX += widthOffset;
|
||||
|
||||
let multi = this.multiPos[x + left];
|
||||
for (let y = 0; y < visibleY; y++) {
|
||||
for (let y = startTileY; y < endTileY; y++) {
|
||||
let startY = heightRatio * y >> 16;
|
||||
let endY = heightRatio * (y + 1) >> 16;
|
||||
let lengthY = endY - startY;
|
||||
@@ -1425,7 +1439,7 @@ export class MapView extends GameShell {
|
||||
}
|
||||
|
||||
if (MapView.shouldDrawItems) {
|
||||
for (let x: number = 0; x < visibleX; x++) {
|
||||
for (let x: number = startTileX; x < endTileX; x++) {
|
||||
let startX: number = (widthRatio * x) >> 16;
|
||||
let endX: number = (widthRatio * (x + 1)) >> 16;
|
||||
const lengthX: number = endX - startX;
|
||||
@@ -1436,7 +1450,7 @@ export class MapView extends GameShell {
|
||||
startX += widthOffset;
|
||||
endX += widthOffset;
|
||||
|
||||
for (let y: number = 0; y < visibleY; y++) {
|
||||
for (let y: number = startTileY; y < endTileY; y++) {
|
||||
let startY: number = (heightRatio * y) >> 16;
|
||||
let endY: number = (heightRatio * (y + 1)) >> 16;
|
||||
const lengthY: number = endY - startY;
|
||||
@@ -1455,7 +1469,7 @@ export class MapView extends GameShell {
|
||||
}
|
||||
|
||||
if (MapView.shouldDrawNpcs) {
|
||||
for (let x: number = 0; x < visibleX; x++) {
|
||||
for (let x: number = startTileX; x < endTileX; x++) {
|
||||
let startX: number = (widthRatio * x) >> 16;
|
||||
let endX: number = (widthRatio * (x + 1)) >> 16;
|
||||
const lengthX: number = endX - startX;
|
||||
@@ -1466,7 +1480,7 @@ export class MapView extends GameShell {
|
||||
startX += widthOffset;
|
||||
endX += widthOffset;
|
||||
|
||||
for (let y: number = 0; y < visibleY; y++) {
|
||||
for (let y: number = startTileY; y < endTileY; y++) {
|
||||
let startY: number = (heightRatio * y) >> 16;
|
||||
let endY: number = (heightRatio * (y + 1)) >> 16;
|
||||
const lengthY: number = endY - startY;
|
||||
|
||||
Reference in new issue
Block a user