mrpas 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Just use npm:
8
8
  npm install mrpas
9
9
  ```
10
10
 
11
- Or grab the code from the [repository](https://bitbucket.org/umbraprojekt/mrpas) and include it in your project.
11
+ Or grab the code from the [repository](https://github.com/mingos777/mrpas) and include it in your project.
12
12
 
13
13
  ## Usage
14
14
 
@@ -28,9 +28,29 @@ ES modules can import the same named export:
28
28
  import { Mrpas } from "mrpas";
29
29
  ```
30
30
 
31
+ Alternatively, instead of the callback, you can pass the map's transparency data as a flat array of `width * height` values, in row-major order (the cell at `x, y` is at index `y * width + x`). Truthy values denote transparent cells, falsy ones denote obstacles:
32
+
33
+ ```
34
+ const transparency = new Uint8Array(20 * 20); // fill with 1 for transparent cells, 0 for obstacles
35
+
36
+ const fov = new Mrpas(20, 20, transparency);
37
+ ```
38
+
39
+ Any array-like object will do: a typed array, an array of booleans, numbers, etc. `Mrpas` keeps a reference to the array rather than a copy, so changes to the map (e.g. a door opening) only require updating the array's values. Cells outside the map are treated as obstacles.
40
+
41
+ Using transparency data is usually faster than using a callback.
42
+
31
43
  ### Compute FOV
32
44
 
33
- Compute the field of view for any set of coordinates by calling the `Mrpas#compute()` method and passing it the origin coordinates, FOV calculation radius and two callbacks: one for determining whether a call has been marked as visible, and the othe for marking it as visible:
45
+ Compute the field of view for any set of coordinates by calling the `Mrpas#compute()` method and passing it the origin coordinates, the FOV calculation radius and a callback for marking a cell as visible:
46
+
47
+ ```
48
+ fov.compute(10, 10, 7, (x, y) => map[x][y].visible = true);
49
+ ```
50
+
51
+ The callback is called once for every cell in the field of view. Cells that aren't visible are never reported, so remember to clear your map's visibility before computing the FOV again.
52
+
53
+ Alternatively, you can pass two callbacks: one for determining whether a cell has been marked as visible, and the other for marking it as visible:
34
54
 
35
55
  ```
36
56
  fov.compute(
@@ -42,7 +62,9 @@ fov.compute(
42
62
  );
43
63
  ```
44
64
 
45
- To calculate the FOV on the entire map, regardless of radius, you may pass `Infinity` as the radius value. Just remember that the transparency callback (the one passed to the constructor) will need to account for this, returning `false` when a cell outside the map is checked.
65
+ In this case, the visibility check callback must only report the cells marked as visible during the current computation: the visibility has to be cleared before each call. Otherwise, cells left visible by a previous computation can affect the result. Calling `compute()` with a single callback tracks visibility internally, which avoids this problem and is usually faster.
66
+
67
+ To calculate the FOV on the entire map, regardless of radius, you may pass `Infinity` as the radius value. Just remember that a transparency callback (the one passed to the constructor) will need to account for this, returning `false` when a cell outside the map is checked. Transparency data handles this automatically.
46
68
 
47
69
  ### Map dimension changing
48
70
 
@@ -52,37 +74,38 @@ Should your map change dimensions, you can inform `Mrpas` about it by calling `M
52
74
  fov.setMapDimensions(40, 40);
53
75
  ```
54
76
 
77
+ If you're using transparency data, the array's size and layout depend on the map dimensions, so create a new `Mrpas` instance with the new array instead.
78
+
55
79
  ### TypeScript
56
80
 
57
81
  MRPAS includes a TypeScript declaration file. It can be used in TypeScript projects without additional typings:
58
82
 
59
83
  ```
60
- import { Mrpas } from "mrpas";
84
+ import { Mrpas, TransparencyData } from "mrpas";
61
85
 
62
- const fov: Mrpas = new Mrpas(20, 20, (x: number, y: number): boolean => map[x][y].transparent);
86
+ const transparency: TransparencyData = new Uint8Array(20 * 20);
87
+ const fov: Mrpas = new Mrpas(20, 20, transparency);
63
88
 
64
- fov.compute(
65
- 10,
66
- 10,
67
- 7,
68
- (x: number, y: number): boolean => map[x][y].visible,
69
- (x: number, y: number): void => map[x][y].visible = true
70
- );
89
+ fov.compute(10, 10, 7, (x: number, y: number): void => {
90
+ map[x][y].visible = true;
91
+ });
71
92
  ```
72
93
 
94
+ The callback types, `IsTransparentFunc`, `IsVisibleFunc` and `SetVisibleFunc`, are exported as well.
95
+
73
96
  ## About MRPAS
74
97
 
75
98
  MRPAS, or [Mingos' Restrictive Precise Angle Shadowcasting](http://www.roguebasin.com/index.php?title=Restrictive_Precise_Angle_Shadowcasting) (Precise Angle Shadowcasting for short) is a field of view (FOV) computation algorithm. Given a 2D grid with a source point *S* and *n* points marked as obstacles, it will determine all points visible from *S*.
76
99
 
77
100
  To visualise this, let the following be a 2D grid of 20 by 20 points. The character `@` represents the field of view source, `#` characters represent obstacles. Cells with yellow background are in the source point's field of view:
78
101
 
79
- ![fov](https://mingos.bitbucket.io/img/umbraprojekt/mrpas/fov.png)
102
+ ![fov](https://raw.githubusercontent.com/mingos777/mrpas/main/docs/fov.png)
80
103
 
81
104
  ### Real life uses
82
105
 
83
106
  This algorithm was conceived in the year 2008 for the purpose of being used in a [roguelike](http://www.roguebasin.com/index.php?title=What_a_roguelike_is) game. To this day, roguelikes remain the focus of MRPAS.
84
107
 
85
- The algorithm was originally written in C and was included in the roguelike library [libtcod](https://bitbucket.org/libtcod/libtcod). It also contained a C++ wrapper.
108
+ The algorithm was originally written in C and was included in the roguelike library [libtcod](https://github.com/libtcod/libtcod). It also contained a C++ wrapper.
86
109
 
87
110
  Since 2008, MRPAS has been implemented [in various languages](http://www.roguebasin.com/index.php?title=Restrictive_Precise_Angle_Shadowcasting#Implementations), and has been successfully used in numerous roguelike games.
88
111
 
@@ -106,7 +129,7 @@ Considering the mathematics behind determining the FOV, the line of sight from A
106
129
 
107
130
  The algorithm's asymmetry can be seen on the following image, where the `.` character has been used to mark asymmetric points (if on yellow background, they can be seen from the source, but cannot see the source; if on black, the source is visible from them, but it cannot see them):
108
131
 
109
- ![fov](https://mingos.bitbucket.io/img/umbraprojekt/mrpas/symmetry.png)
132
+ ![fov](https://raw.githubusercontent.com/mingos777/mrpas/main/docs/symmetry.png)
110
133
 
111
134
  On the above map, 1.6% of all points not containing an obstacle are asymmetric.
112
135
 
@@ -114,7 +137,7 @@ The percentage of asymmetric points is within the range of single digit numbers,
114
137
 
115
138
  For comparison, here's an "outdoor" map with 3.9% asymmetric points:
116
139
 
117
- ![fov](https://mingos.bitbucket.io/img/umbraprojekt/mrpas/symmetry-outdoor.png)
140
+ ![fov](https://raw.githubusercontent.com/mingos777/mrpas/main/docs/symmetry-outdoor.png)
118
141
 
119
142
  ### Performance
120
143
 
package/dist/cjs/index.js CHANGED
@@ -5,14 +5,17 @@ class Mrpas {
5
5
  mapWidth;
6
6
  mapHeight;
7
7
  isTransparent;
8
- constructor(mapWidth, mapHeight, isTransparent) {
8
+ constructor(mapWidth, mapHeight, transparency) {
9
9
  this.mapWidth = mapWidth;
10
10
  this.mapHeight = mapHeight;
11
- this.isTransparent = isTransparent;
11
+ this.isTransparent = typeof transparency === "function"
12
+ ? transparency
13
+ : (x, y) => x >= 0 && x < this.mapWidth && y >= 0 && y < this.mapHeight &&
14
+ !!transparency[y * this.mapWidth + x];
12
15
  }
13
16
  computeOctantY(deltaX, deltaY, data) {
14
- const startSlopes = [];
15
- const endSlopes = [];
17
+ const startSlopes = data.startSlopes;
18
+ const endSlopes = data.endSlopes;
16
19
  const minX = data.minX;
17
20
  const maxX = data.maxX;
18
21
  const minY = data.minY;
@@ -22,6 +25,8 @@ class Mrpas {
22
25
  const isTransparent = this.isTransparent;
23
26
  const isVisible = data.isVisible;
24
27
  const setVisible = data.setVisible;
28
+ const visibility = data.visibility;
29
+ const width = maxX - minX + 1;
25
30
  let iteration = 1;
26
31
  let totalObstacles = 0;
27
32
  let obstaclesInLastLine = 0;
@@ -53,11 +58,14 @@ class Mrpas {
53
58
  transparencyChecked = false;
54
59
  if (obstaclesInLastLine > 0) {
55
60
  diagonalOpen = behindOpen;
56
- behindOpen = isVisible(x, y - deltaY) && isTransparent(x, y - deltaY);
61
+ behindOpen = (visibility === null ? isVisible(x, y - deltaY) : visibility[(y - deltaY - minY) * width + x - minX] === 1) &&
62
+ isTransparent(x, y - deltaY);
57
63
  if (!behindOpen &&
58
64
  !(processedCell > firstCell
59
65
  ? diagonalOpen
60
- : isVisible(x - deltaX, y - deltaY) && isTransparent(x - deltaX, y - deltaY))) {
66
+ : (visibility === null ? isVisible(x - deltaX, y - deltaY) : x - deltaX >= minX && x - deltaX <= maxX &&
67
+ visibility[(y - deltaY - minY) * width + x - deltaX - minX] === 1) &&
68
+ isTransparent(x - deltaX, y - deltaY))) {
61
69
  visible = false;
62
70
  }
63
71
  else {
@@ -94,6 +102,9 @@ class Mrpas {
94
102
  }
95
103
  if (visible) {
96
104
  setVisible(x, y);
105
+ if (visibility !== null) {
106
+ visibility[(y - minY) * width + x - minX] = 1;
107
+ }
97
108
  if (!transparencyChecked) {
98
109
  cellTransparent = isTransparent(x, y);
99
110
  }
@@ -114,8 +125,8 @@ class Mrpas {
114
125
  }
115
126
  }
116
127
  computeOctantX(deltaX, deltaY, data) {
117
- const startSlopes = [];
118
- const endSlopes = [];
128
+ const startSlopes = data.startSlopes;
129
+ const endSlopes = data.endSlopes;
119
130
  const minX = data.minX;
120
131
  const maxX = data.maxX;
121
132
  const minY = data.minY;
@@ -125,6 +136,8 @@ class Mrpas {
125
136
  const isTransparent = this.isTransparent;
126
137
  const isVisible = data.isVisible;
127
138
  const setVisible = data.setVisible;
139
+ const visibility = data.visibility;
140
+ const width = maxX - minX + 1;
128
141
  let iteration = 1;
129
142
  let totalObstacles = 0;
130
143
  let obstaclesInLastLine = 0;
@@ -156,11 +169,14 @@ class Mrpas {
156
169
  transparencyChecked = false;
157
170
  if (obstaclesInLastLine > 0) {
158
171
  diagonalOpen = behindOpen;
159
- behindOpen = isVisible(x - deltaX, y) && isTransparent(x - deltaX, y);
172
+ behindOpen = (visibility === null ? isVisible(x - deltaX, y) : visibility[(y - minY) * width + x - deltaX - minX] === 1) &&
173
+ isTransparent(x - deltaX, y);
160
174
  if (!behindOpen &&
161
175
  !(processedCell > firstCell
162
176
  ? diagonalOpen
163
- : isVisible(x - deltaX, y - deltaY) && isTransparent(x - deltaX, y - deltaY))) {
177
+ : (visibility === null ? isVisible(x - deltaX, y - deltaY) : y - deltaY >= minY && y - deltaY <= maxY &&
178
+ visibility[(y - deltaY - minY) * width + x - deltaX - minX] === 1) &&
179
+ isTransparent(x - deltaX, y - deltaY))) {
164
180
  visible = false;
165
181
  }
166
182
  else {
@@ -197,6 +213,9 @@ class Mrpas {
197
213
  }
198
214
  if (visible) {
199
215
  setVisible(x, y);
216
+ if (visibility !== null) {
217
+ visibility[(y - minY) * width + x - minX] = 1;
218
+ }
200
219
  if (!transparencyChecked) {
201
220
  cellTransparent = isTransparent(x, y);
202
221
  }
@@ -220,17 +239,32 @@ class Mrpas {
220
239
  this.mapWidth = mapWidth;
221
240
  this.mapHeight = mapHeight;
222
241
  }
223
- compute(originX, originY, radius, isVisible, setVisible) {
242
+ compute(originX, originY, radius, isVisibleOrSetVisible, setVisibleFunc) {
243
+ const minX = Math.max(0, originX - radius);
244
+ const minY = Math.max(0, originY - radius);
245
+ const maxX = Math.min(this.mapWidth - 1, originX + radius);
246
+ const maxY = Math.min(this.mapHeight - 1, originY + radius);
247
+ const width = maxX - minX + 1;
248
+ const visibility = setVisibleFunc === undefined
249
+ ? new Uint8Array(width * (maxY - minY + 1))
250
+ : null;
251
+ const setVisible = setVisibleFunc ?? isVisibleOrSetVisible;
252
+ if (visibility !== null) {
253
+ visibility[(originY - minY) * width + originX - minX] = 1;
254
+ }
224
255
  setVisible(originX, originY);
225
256
  const data = {
226
- minX: Math.max(0, originX - radius),
227
- minY: Math.max(0, originY - radius),
228
- maxX: Math.min(this.mapWidth - 1, originX + radius),
229
- maxY: Math.min(this.mapHeight - 1, originY + radius),
257
+ minX: minX,
258
+ minY: minY,
259
+ maxX: maxX,
260
+ maxY: maxY,
230
261
  originX: originX,
231
262
  originY: originY,
232
- isVisible: isVisible,
233
- setVisible: setVisible
263
+ isVisible: isVisibleOrSetVisible,
264
+ setVisible: setVisible,
265
+ visibility: visibility,
266
+ startSlopes: [],
267
+ endSlopes: []
234
268
  };
235
269
  this.computeOctantY(1, 1, data);
236
270
  this.computeOctantX(1, 1, data);
@@ -1,13 +1,15 @@
1
1
  export type IsTransparentFunc = (x: number, y: number) => boolean;
2
2
  export type IsVisibleFunc = (x: number, y: number) => boolean;
3
3
  export type SetVisibleFunc = (x: number, y: number) => void;
4
+ export type TransparencyData = ArrayLike<unknown>;
4
5
  export declare class Mrpas {
5
6
  private mapWidth;
6
7
  private mapHeight;
7
8
  private readonly isTransparent;
8
- constructor(mapWidth: number, mapHeight: number, isTransparent: IsTransparentFunc);
9
+ constructor(mapWidth: number, mapHeight: number, transparency: IsTransparentFunc | TransparencyData);
9
10
  private computeOctantY;
10
11
  private computeOctantX;
11
12
  setMapDimensions(mapWidth: number, mapHeight: number): void;
13
+ compute(originX: number, originY: number, radius: number, setVisible: SetVisibleFunc): void;
12
14
  compute(originX: number, originY: number, radius: number, isVisible: IsVisibleFunc, setVisible: SetVisibleFunc): void;
13
15
  }
package/dist/esm/index.js CHANGED
@@ -2,14 +2,17 @@ export class Mrpas {
2
2
  mapWidth;
3
3
  mapHeight;
4
4
  isTransparent;
5
- constructor(mapWidth, mapHeight, isTransparent) {
5
+ constructor(mapWidth, mapHeight, transparency) {
6
6
  this.mapWidth = mapWidth;
7
7
  this.mapHeight = mapHeight;
8
- this.isTransparent = isTransparent;
8
+ this.isTransparent = typeof transparency === "function"
9
+ ? transparency
10
+ : (x, y) => x >= 0 && x < this.mapWidth && y >= 0 && y < this.mapHeight &&
11
+ !!transparency[y * this.mapWidth + x];
9
12
  }
10
13
  computeOctantY(deltaX, deltaY, data) {
11
- const startSlopes = [];
12
- const endSlopes = [];
14
+ const startSlopes = data.startSlopes;
15
+ const endSlopes = data.endSlopes;
13
16
  const minX = data.minX;
14
17
  const maxX = data.maxX;
15
18
  const minY = data.minY;
@@ -19,6 +22,8 @@ export class Mrpas {
19
22
  const isTransparent = this.isTransparent;
20
23
  const isVisible = data.isVisible;
21
24
  const setVisible = data.setVisible;
25
+ const visibility = data.visibility;
26
+ const width = maxX - minX + 1;
22
27
  let iteration = 1;
23
28
  let totalObstacles = 0;
24
29
  let obstaclesInLastLine = 0;
@@ -50,11 +55,14 @@ export class Mrpas {
50
55
  transparencyChecked = false;
51
56
  if (obstaclesInLastLine > 0) {
52
57
  diagonalOpen = behindOpen;
53
- behindOpen = isVisible(x, y - deltaY) && isTransparent(x, y - deltaY);
58
+ behindOpen = (visibility === null ? isVisible(x, y - deltaY) : visibility[(y - deltaY - minY) * width + x - minX] === 1) &&
59
+ isTransparent(x, y - deltaY);
54
60
  if (!behindOpen &&
55
61
  !(processedCell > firstCell
56
62
  ? diagonalOpen
57
- : isVisible(x - deltaX, y - deltaY) && isTransparent(x - deltaX, y - deltaY))) {
63
+ : (visibility === null ? isVisible(x - deltaX, y - deltaY) : x - deltaX >= minX && x - deltaX <= maxX &&
64
+ visibility[(y - deltaY - minY) * width + x - deltaX - minX] === 1) &&
65
+ isTransparent(x - deltaX, y - deltaY))) {
58
66
  visible = false;
59
67
  }
60
68
  else {
@@ -91,6 +99,9 @@ export class Mrpas {
91
99
  }
92
100
  if (visible) {
93
101
  setVisible(x, y);
102
+ if (visibility !== null) {
103
+ visibility[(y - minY) * width + x - minX] = 1;
104
+ }
94
105
  if (!transparencyChecked) {
95
106
  cellTransparent = isTransparent(x, y);
96
107
  }
@@ -111,8 +122,8 @@ export class Mrpas {
111
122
  }
112
123
  }
113
124
  computeOctantX(deltaX, deltaY, data) {
114
- const startSlopes = [];
115
- const endSlopes = [];
125
+ const startSlopes = data.startSlopes;
126
+ const endSlopes = data.endSlopes;
116
127
  const minX = data.minX;
117
128
  const maxX = data.maxX;
118
129
  const minY = data.minY;
@@ -122,6 +133,8 @@ export class Mrpas {
122
133
  const isTransparent = this.isTransparent;
123
134
  const isVisible = data.isVisible;
124
135
  const setVisible = data.setVisible;
136
+ const visibility = data.visibility;
137
+ const width = maxX - minX + 1;
125
138
  let iteration = 1;
126
139
  let totalObstacles = 0;
127
140
  let obstaclesInLastLine = 0;
@@ -153,11 +166,14 @@ export class Mrpas {
153
166
  transparencyChecked = false;
154
167
  if (obstaclesInLastLine > 0) {
155
168
  diagonalOpen = behindOpen;
156
- behindOpen = isVisible(x - deltaX, y) && isTransparent(x - deltaX, y);
169
+ behindOpen = (visibility === null ? isVisible(x - deltaX, y) : visibility[(y - minY) * width + x - deltaX - minX] === 1) &&
170
+ isTransparent(x - deltaX, y);
157
171
  if (!behindOpen &&
158
172
  !(processedCell > firstCell
159
173
  ? diagonalOpen
160
- : isVisible(x - deltaX, y - deltaY) && isTransparent(x - deltaX, y - deltaY))) {
174
+ : (visibility === null ? isVisible(x - deltaX, y - deltaY) : y - deltaY >= minY && y - deltaY <= maxY &&
175
+ visibility[(y - deltaY - minY) * width + x - deltaX - minX] === 1) &&
176
+ isTransparent(x - deltaX, y - deltaY))) {
161
177
  visible = false;
162
178
  }
163
179
  else {
@@ -194,6 +210,9 @@ export class Mrpas {
194
210
  }
195
211
  if (visible) {
196
212
  setVisible(x, y);
213
+ if (visibility !== null) {
214
+ visibility[(y - minY) * width + x - minX] = 1;
215
+ }
197
216
  if (!transparencyChecked) {
198
217
  cellTransparent = isTransparent(x, y);
199
218
  }
@@ -217,17 +236,32 @@ export class Mrpas {
217
236
  this.mapWidth = mapWidth;
218
237
  this.mapHeight = mapHeight;
219
238
  }
220
- compute(originX, originY, radius, isVisible, setVisible) {
239
+ compute(originX, originY, radius, isVisibleOrSetVisible, setVisibleFunc) {
240
+ const minX = Math.max(0, originX - radius);
241
+ const minY = Math.max(0, originY - radius);
242
+ const maxX = Math.min(this.mapWidth - 1, originX + radius);
243
+ const maxY = Math.min(this.mapHeight - 1, originY + radius);
244
+ const width = maxX - minX + 1;
245
+ const visibility = setVisibleFunc === undefined
246
+ ? new Uint8Array(width * (maxY - minY + 1))
247
+ : null;
248
+ const setVisible = setVisibleFunc ?? isVisibleOrSetVisible;
249
+ if (visibility !== null) {
250
+ visibility[(originY - minY) * width + originX - minX] = 1;
251
+ }
221
252
  setVisible(originX, originY);
222
253
  const data = {
223
- minX: Math.max(0, originX - radius),
224
- minY: Math.max(0, originY - radius),
225
- maxX: Math.min(this.mapWidth - 1, originX + radius),
226
- maxY: Math.min(this.mapHeight - 1, originY + radius),
254
+ minX: minX,
255
+ minY: minY,
256
+ maxX: maxX,
257
+ maxY: maxY,
227
258
  originX: originX,
228
259
  originY: originY,
229
- isVisible: isVisible,
230
- setVisible: setVisible
260
+ isVisible: isVisibleOrSetVisible,
261
+ setVisible: setVisible,
262
+ visibility: visibility,
263
+ startSlopes: [],
264
+ endSlopes: []
231
265
  };
232
266
  this.computeOctantY(1, 1, data);
233
267
  this.computeOctantX(1, 1, data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mrpas",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "A Field of View (FOV) computation algorithm.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -19,7 +19,8 @@
19
19
  "build": "npm run build:esm && npm run build:cjs",
20
20
  "build:esm": "tsc -p tsconfig.esm.json",
21
21
  "build:cjs": "tsc -p tsconfig.cjs.json",
22
- "watch": "tsc --watch -p tsconfig.esm.json"
22
+ "watch": "tsc --watch -p tsconfig.esm.json",
23
+ "prepublishOnly": "npm run build"
23
24
  },
24
25
  "repository": {
25
26
  "type": "git",