@forgeax/engine-picking 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +168 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/glyph-text-pick.test.d.ts +2 -0
- package/dist/__tests__/glyph-text-pick.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts +3 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts.map +1 -0
- package/dist/__tests__/pick-errors.test-d.d.ts +2 -0
- package/dist/__tests__/pick-errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/pick-errors.test.d.ts +2 -0
- package/dist/__tests__/pick-errors.test.d.ts.map +1 -0
- package/dist/__tests__/pick-tile.test.d.ts +2 -0
- package/dist/__tests__/pick-tile.test.d.ts.map +1 -0
- package/dist/__tests__/pick-vertex.unit.test.d.ts +2 -0
- package/dist/__tests__/pick-vertex.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick.test.d.ts +2 -0
- package/dist/__tests__/pick.test.d.ts.map +1 -0
- package/dist/__tests__/visibility-regression.integration.test.d.ts +2 -0
- package/dist/__tests__/visibility-regression.integration.test.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +440 -0
- package/dist/index.mjs.map +1 -0
- package/dist/pick-core.d.ts +39 -0
- package/dist/pick-core.d.ts.map +1 -0
- package/dist/pick-errors.d.ts +39 -0
- package/dist/pick-errors.d.ts.map +1 -0
- package/dist/pick-tile.d.ts +44 -0
- package/dist/pick-tile.d.ts.map +1 -0
- package/dist/pick-vertex.d.ts +87 -0
- package/dist/pick-vertex.d.ts.map +1 -0
- package/dist/pick.d.ts +34 -0
- package/dist/pick.d.ts.map +1 -0
- package/dist/viewport-to-world.d.ts +12 -0
- package/dist/viewport-to-world.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/glyph-text-pick.test.ts +132 -0
- package/src/__tests__/helpers/mock-shader-registry.ts +118 -0
- package/src/__tests__/pick-errors.test-d.ts +66 -0
- package/src/__tests__/pick-errors.test.ts +71 -0
- package/src/__tests__/pick-tile.test.ts +239 -0
- package/src/__tests__/pick-vertex.unit.test.ts +1607 -0
- package/src/__tests__/pick.test.ts +538 -0
- package/src/__tests__/visibility-regression.integration.test.ts +73 -0
- package/src/index.ts +21 -0
- package/src/pick-core.ts +114 -0
- package/src/pick-errors.ts +69 -0
- package/src/pick-tile.ts +157 -0
- package/src/pick-vertex.ts +593 -0
- package/src/pick.ts +148 -0
- package/src/viewport-to-world.ts +23 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// pick-errors.test-d.ts — PickError code-owner and closed-union declaration guard.
|
|
2
|
+
//
|
|
3
|
+
// The guard proves the package barrel keeps the public PickErrorCode projection
|
|
4
|
+
// equal to the PickError.code owner, rejects an invalid literal, and remains
|
|
5
|
+
// exhaustively switchable without a `default` arm. Adding a member later without
|
|
6
|
+
// extending the switch breaks the declaration typecheck.
|
|
7
|
+
//
|
|
8
|
+
// Both closed unions the picking barrel exports are covered:
|
|
9
|
+
// - PickErrorCode — single member ('camera-component-missing'), thrown by pick().
|
|
10
|
+
// - PickTileError — two-member discriminated union (code), returned by pickTile().
|
|
11
|
+
//
|
|
12
|
+
// Charter P3 (tension): closed unions must retain zero-loss exhaustive-switch
|
|
13
|
+
// capability across the package boundary — this file is the compile-time proof.
|
|
14
|
+
|
|
15
|
+
// (b) external-consumer import path: the type resolves from the package barrel.
|
|
16
|
+
import type { PickError, PickErrorCode, PickTileError } from '@forgeax/engine-picking';
|
|
17
|
+
import { expectTypeOf, test } from 'vitest';
|
|
18
|
+
|
|
19
|
+
function assertNever(_x: never): never {
|
|
20
|
+
throw new Error('exhaustive');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// PickErrorCode exhaustive switch, no default — assertNever traps a future
|
|
24
|
+
// member that is added without extending this switch.
|
|
25
|
+
function describePickErrorCode(code: PickErrorCode): string {
|
|
26
|
+
switch (code) {
|
|
27
|
+
case 'camera-component-missing':
|
|
28
|
+
return 'camera missing';
|
|
29
|
+
default:
|
|
30
|
+
return assertNever(code);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// (c) PickTileError exhaustive switch over the discriminant, no default.
|
|
35
|
+
function describePickTileError(err: PickTileError): string {
|
|
36
|
+
switch (err.code) {
|
|
37
|
+
case 'tilemap-not-found':
|
|
38
|
+
return 'tilemap not found';
|
|
39
|
+
case 'tilemap-component-missing':
|
|
40
|
+
return 'tilemap component missing';
|
|
41
|
+
default:
|
|
42
|
+
return assertNever(err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('PickErrorCode derives the exact closed surface from PickError', () => {
|
|
47
|
+
expectTypeOf<PickErrorCode>().toEqualTypeOf<PickError['code']>();
|
|
48
|
+
expectTypeOf<PickErrorCode>().toEqualTypeOf<'camera-component-missing'>();
|
|
49
|
+
|
|
50
|
+
const acceptPickErrorCode = (code: PickErrorCode): void => {
|
|
51
|
+
void code;
|
|
52
|
+
};
|
|
53
|
+
acceptPickErrorCode('camera-component-missing');
|
|
54
|
+
// @ts-expect-error invalid codes must not be accepted.
|
|
55
|
+
acceptPickErrorCode('not-a-pick-error');
|
|
56
|
+
|
|
57
|
+
// Compile-time exhaustiveness (run-time noop).
|
|
58
|
+
void describePickErrorCode;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('PickTileError is the closed two-member discriminated union', () => {
|
|
62
|
+
expectTypeOf<PickTileError['code']>().toEqualTypeOf<
|
|
63
|
+
'tilemap-not-found' | 'tilemap-component-missing'
|
|
64
|
+
>();
|
|
65
|
+
void describePickTileError;
|
|
66
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// pick-errors.test.ts — PickErrorCode closed union + PickError structured surface.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from packages/runtime/src/__tests__/errors.unit.test.ts (the
|
|
4
|
+
// "from pick-errors.test.ts" block) in feat-20260705 M2 / w25 when the pick
|
|
5
|
+
// cluster moved to @forgeax/engine-picking. Runtime can no longer import the
|
|
6
|
+
// picking package (AC-203: no runtime -> picking edge), so these unit tests
|
|
7
|
+
// live alongside the code they exercise.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { PickError, type PickErrorCode } from '../pick-errors';
|
|
11
|
+
|
|
12
|
+
describe('pick-errors', () => {
|
|
13
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
14
|
+
|
|
15
|
+
describe('w8 — PickErrorCode closed union (AC-13)', () => {
|
|
16
|
+
it('camera-component-missing is a valid PickErrorCode literal', () => {
|
|
17
|
+
const code: PickErrorCode = 'camera-component-missing';
|
|
18
|
+
expect(code).toBe('camera-component-missing');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('camera-component-missing is valid kebab-case', () => {
|
|
22
|
+
const code: PickErrorCode = 'camera-component-missing';
|
|
23
|
+
expect(code).toMatch(KEBAB_REGEX);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('exhaustive switch over PickErrorCode compiles without default', () => {
|
|
27
|
+
function exhaustive(code: PickErrorCode): string {
|
|
28
|
+
switch (code) {
|
|
29
|
+
case 'camera-component-missing':
|
|
30
|
+
return 'camera missing';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
expect(exhaustive('camera-component-missing')).toBe('camera missing');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('w8 — PickError structured 3-field surface (AC-11)', () => {
|
|
38
|
+
it('PickError has .code === camera-component-missing', () => {
|
|
39
|
+
const e = new PickError(7);
|
|
40
|
+
expect(e.code).toBe('camera-component-missing');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('PickError .expected is a non-empty string', () => {
|
|
44
|
+
const e = new PickError(7);
|
|
45
|
+
expect(typeof e.expected).toBe('string');
|
|
46
|
+
expect(e.expected.length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('PickError .hint contains a world.set recovery directive', () => {
|
|
50
|
+
const e = new PickError(7);
|
|
51
|
+
expect(e.hint.length).toBeGreaterThan(0);
|
|
52
|
+
expect(e.hint).toContain('world.set');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('PickError super message (Error.message) is non-empty', () => {
|
|
56
|
+
const e = new PickError(7);
|
|
57
|
+
expect(e.message.length).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('PickError is an instanceof Error and carries .name', () => {
|
|
61
|
+
const e = new PickError(7);
|
|
62
|
+
expect(e).toBeInstanceOf(Error);
|
|
63
|
+
expect(e.name).toBe('PickError');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('PickError .detail records the offending camera entity', () => {
|
|
67
|
+
const e = new PickError(42);
|
|
68
|
+
expect(e.detail).toEqual({ cameraEntity: 42 });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// pick-tile.test - pickTile cell-level query (worldX/Y -> tileId)
|
|
2
|
+
// (feat-20260608 M0 baseline rebuild).
|
|
3
|
+
//
|
|
4
|
+
// Anchors: plan-tasks m0-t7 / m0-t8; plan-strategy §D-5 / §M0 targetFiles
|
|
5
|
+
// (pickTile); feat-20260604 baseline; charter P3 explicit failure.
|
|
6
|
+
|
|
7
|
+
import { World } from '@forgeax/engine-ecs';
|
|
8
|
+
import { TileLayer, Tilemap } from '@forgeax/engine-render/authoring';
|
|
9
|
+
import { ChildOf, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { pickTile } from '../pick-tile';
|
|
12
|
+
|
|
13
|
+
function makeFixture(opts: {
|
|
14
|
+
cols: number;
|
|
15
|
+
rows: number;
|
|
16
|
+
layers: ReadonlyArray<{ layerOrder: number; tiles: readonly number[] }>;
|
|
17
|
+
}) {
|
|
18
|
+
const world = new World();
|
|
19
|
+
const tilemap = world
|
|
20
|
+
.spawn(
|
|
21
|
+
{ component: Tilemap, data: { cols: opts.cols, rows: opts.rows, tileset: 'test/tileset' } },
|
|
22
|
+
{ component: Transform, data: {} },
|
|
23
|
+
)
|
|
24
|
+
.unwrap();
|
|
25
|
+
const layerEntities = opts.layers.map((spec) => {
|
|
26
|
+
const tilesArr = new Uint32Array(opts.cols * opts.rows);
|
|
27
|
+
for (let i = 0; i < spec.tiles.length; i++) tilesArr[i] = spec.tiles[i] ?? 0;
|
|
28
|
+
return world
|
|
29
|
+
.spawn(
|
|
30
|
+
{ component: TileLayer, data: { tiles: tilesArr, layerOrder: spec.layerOrder } },
|
|
31
|
+
{ component: ChildOf, data: { parent: tilemap } },
|
|
32
|
+
)
|
|
33
|
+
.unwrap();
|
|
34
|
+
});
|
|
35
|
+
return { world, tilemap, layerEntities };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('pickTile (M0 baseline)', () => {
|
|
39
|
+
it('hits the cell containing worldX/Y on a single-layer tilemap', () => {
|
|
40
|
+
const { world, tilemap } = makeFixture({
|
|
41
|
+
cols: 4,
|
|
42
|
+
rows: 4,
|
|
43
|
+
layers: [{ layerOrder: 0, tiles: [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }],
|
|
44
|
+
});
|
|
45
|
+
const r = pickTile(world, tilemap, 1.5, 1.5);
|
|
46
|
+
expect(r.ok).toBe(true);
|
|
47
|
+
if (r.ok) {
|
|
48
|
+
expect(r.value).not.toBeNull();
|
|
49
|
+
if (r.value !== null) {
|
|
50
|
+
expect(r.value.cellX).toBe(1);
|
|
51
|
+
expect(r.value.cellY).toBe(1);
|
|
52
|
+
expect(r.value.tileId).toBe(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns Result.ok(null) when worldX/Y is out of bounds', () => {
|
|
58
|
+
const { world, tilemap } = makeFixture({
|
|
59
|
+
cols: 2,
|
|
60
|
+
rows: 2,
|
|
61
|
+
layers: [{ layerOrder: 0, tiles: [1, 0, 0, 0] }],
|
|
62
|
+
});
|
|
63
|
+
const r = pickTile(world, tilemap, 100, 100);
|
|
64
|
+
expect(r.ok).toBe(true);
|
|
65
|
+
if (r.ok) expect(r.value).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns Result.ok(null) when no non-zero tile occupies the cell', () => {
|
|
69
|
+
const { world, tilemap } = makeFixture({
|
|
70
|
+
cols: 2,
|
|
71
|
+
rows: 2,
|
|
72
|
+
layers: [{ layerOrder: 0, tiles: [1, 0, 0, 0] }],
|
|
73
|
+
});
|
|
74
|
+
const r = pickTile(world, tilemap, 1.5, 1.5);
|
|
75
|
+
expect(r.ok).toBe(true);
|
|
76
|
+
if (r.ok) expect(r.value).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('walks layers in layerOrder-descending order (high layer wins)', () => {
|
|
80
|
+
// layer 0: cell (0,0) has tile 1
|
|
81
|
+
// layer 5: cell (0,0) has tile 7
|
|
82
|
+
const { world, tilemap } = makeFixture({
|
|
83
|
+
cols: 2,
|
|
84
|
+
rows: 2,
|
|
85
|
+
layers: [
|
|
86
|
+
{ layerOrder: 0, tiles: [1, 0, 0, 0] },
|
|
87
|
+
{ layerOrder: 5, tiles: [7, 0, 0, 0] },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
const r = pickTile(world, tilemap, 0.5, 0.5);
|
|
91
|
+
expect(r.ok).toBe(true);
|
|
92
|
+
if (r.ok && r.value !== null) {
|
|
93
|
+
expect(r.value.tileId).toBe(7);
|
|
94
|
+
} else {
|
|
95
|
+
expect.fail('expected a hit');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('uses the propagated inverse affine transform for translated rotated scaled maps', () => {
|
|
100
|
+
const world = new World();
|
|
101
|
+
const tilemap = world
|
|
102
|
+
.spawn(
|
|
103
|
+
{
|
|
104
|
+
component: Tilemap,
|
|
105
|
+
data: { cols: 3, rows: 2, tileSize: [1, 1], tileset: 'test/tileset' },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
component: Transform,
|
|
109
|
+
data: {
|
|
110
|
+
pos: [10, 20, 0],
|
|
111
|
+
// +90 degrees around Z: local cell (1.5, 0.5) maps to world (8.5, 23).
|
|
112
|
+
quat: [0, 0, Math.SQRT1_2, Math.SQRT1_2],
|
|
113
|
+
scale: [2, 3, 1],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
.unwrap();
|
|
118
|
+
const lowerLayer = world
|
|
119
|
+
.spawn(
|
|
120
|
+
{
|
|
121
|
+
component: TileLayer,
|
|
122
|
+
data: { tiles: new Uint32Array([0, 3, 0, 0, 0, 0]), layerOrder: 0 },
|
|
123
|
+
},
|
|
124
|
+
{ component: ChildOf, data: { parent: tilemap } },
|
|
125
|
+
)
|
|
126
|
+
.unwrap();
|
|
127
|
+
const upperLayer = world
|
|
128
|
+
.spawn(
|
|
129
|
+
{
|
|
130
|
+
component: TileLayer,
|
|
131
|
+
data: { tiles: new Uint32Array([0, 9, 0, 0, 0, 0]), layerOrder: 7 },
|
|
132
|
+
},
|
|
133
|
+
{ component: ChildOf, data: { parent: tilemap } },
|
|
134
|
+
)
|
|
135
|
+
.unwrap();
|
|
136
|
+
|
|
137
|
+
propagateTransforms(world).unwrap();
|
|
138
|
+
const r = pickTile(world, tilemap, 8.5, 23);
|
|
139
|
+
|
|
140
|
+
expect(r.ok).toBe(true);
|
|
141
|
+
if (r.ok) {
|
|
142
|
+
expect(r.value).toEqual({ layerEntity: upperLayer, cellX: 1, cellY: 0, tileId: 9 });
|
|
143
|
+
}
|
|
144
|
+
expect(lowerLayer).not.toBe(upperLayer);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('falls through to lower layer when higher layer has 0 at the cell', () => {
|
|
148
|
+
const { world, tilemap } = makeFixture({
|
|
149
|
+
cols: 2,
|
|
150
|
+
rows: 2,
|
|
151
|
+
layers: [
|
|
152
|
+
{ layerOrder: 0, tiles: [3, 0, 0, 0] },
|
|
153
|
+
{ layerOrder: 5, tiles: [0, 0, 0, 0] },
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
const r = pickTile(world, tilemap, 0.5, 0.5);
|
|
157
|
+
expect(r.ok).toBe(true);
|
|
158
|
+
if (r.ok && r.value !== null) {
|
|
159
|
+
expect(r.value.tileId).toBe(3);
|
|
160
|
+
} else {
|
|
161
|
+
expect.fail('expected a hit');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns tilemap-not-found for a dead handle', () => {
|
|
166
|
+
const { world } = makeFixture({
|
|
167
|
+
cols: 1,
|
|
168
|
+
rows: 1,
|
|
169
|
+
layers: [{ layerOrder: 0, tiles: [1] }],
|
|
170
|
+
});
|
|
171
|
+
const dead = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
172
|
+
world.despawn(dead).unwrap();
|
|
173
|
+
const r = pickTile(world, dead, 1, 1);
|
|
174
|
+
expect(r.ok).toBe(false);
|
|
175
|
+
if (!r.ok) expect(r.error.code).toBe('tilemap-not-found');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns Result.err when the entity does not carry Tilemap', () => {
|
|
179
|
+
const { world } = makeFixture({
|
|
180
|
+
cols: 1,
|
|
181
|
+
rows: 1,
|
|
182
|
+
layers: [{ layerOrder: 0, tiles: [1] }],
|
|
183
|
+
});
|
|
184
|
+
const e = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
185
|
+
const r = pickTile(world, e, 1, 1);
|
|
186
|
+
expect(r.ok).toBe(false);
|
|
187
|
+
if (!r.ok) {
|
|
188
|
+
expect(r.error.code).toBe('tilemap-component-missing');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('repairs a missing Tilemap on the same World and retries successfully', () => {
|
|
193
|
+
const world = new World();
|
|
194
|
+
const entity = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
195
|
+
|
|
196
|
+
const missing = pickTile(world, entity, 0.5, 0.5);
|
|
197
|
+
expect(missing.ok).toBe(false);
|
|
198
|
+
if (!missing.ok) expect(missing.error.code).toBe('tilemap-component-missing');
|
|
199
|
+
|
|
200
|
+
world
|
|
201
|
+
.addComponent(entity, {
|
|
202
|
+
component: Tilemap,
|
|
203
|
+
data: { cols: 1, rows: 1, tileSize: [1, 1], tileset: 'test/tileset' },
|
|
204
|
+
})
|
|
205
|
+
.unwrap();
|
|
206
|
+
const layer = world
|
|
207
|
+
.spawn(
|
|
208
|
+
{ component: TileLayer, data: { tiles: new Uint32Array([11]), layerOrder: 0 } },
|
|
209
|
+
{ component: ChildOf, data: { parent: entity } },
|
|
210
|
+
)
|
|
211
|
+
.unwrap();
|
|
212
|
+
|
|
213
|
+
const repaired = pickTile(world, entity, 0.5, 0.5);
|
|
214
|
+
expect(repaired.ok).toBe(true);
|
|
215
|
+
if (repaired.ok) {
|
|
216
|
+
expect(repaired.value).toEqual({ layerEntity: layer, cellX: 0, cellY: 0, tileId: 11 });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('keeps the documented mat4 singular fallback deterministic', () => {
|
|
221
|
+
const { world, tilemap } = makeFixture({
|
|
222
|
+
cols: 1,
|
|
223
|
+
rows: 1,
|
|
224
|
+
layers: [{ layerOrder: 0, tiles: [5] }],
|
|
225
|
+
});
|
|
226
|
+
world.set(tilemap, Transform, { pos: [7, 9, 0], scale: [0, 2, 1] }).unwrap();
|
|
227
|
+
propagateTransforms(world).unwrap();
|
|
228
|
+
|
|
229
|
+
// mat4.invert defines singular input as identity; pickTile keeps that
|
|
230
|
+
// central math fallback instead of inventing another error arm.
|
|
231
|
+
const first = pickTile(world, tilemap, 0.5, 0.5);
|
|
232
|
+
const second = pickTile(world, tilemap, 0.5, 0.5);
|
|
233
|
+
expect(first).toEqual(second);
|
|
234
|
+
expect(first.ok).toBe(true);
|
|
235
|
+
if (first.ok) {
|
|
236
|
+
expect(first.value).toMatchObject({ cellX: 0, cellY: 0, tileId: 5 });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|