import { describe, expect, it } from 'vitest'; import type { DrawItem } from '../../src/viewport/items'; import { menuTargetOf, pickOn, pressOn, stackAt } from '../../src/viewport/pick'; /** Only what the pick order reads: the type, the `z`, and a map's layers with their own `z`. */ function item( id: string, type: DrawItem['type'], z: number, layers: { id: string; z: number }[] = [], ): DrawItem { return { id, type, z, tilemap: type === 'tilemap' ? { layers: layers.map((l) => ({ ...l, tiles: [] })) } : null, } as unknown as DrawItem; } describe('what lies under the pointer (#203)', () => { const map = item('map', 'tilemap', 0, [ { id: 'ground', z: -5 }, { id: 'roofs', z: 5 }, ]); const hero = item('hero', 'sprite', -1); it('puts a sprite in front of a map whose only tile there is on a layer below it', () => { expect( stackAt( [map, hero], [ { id: 'map', layers: ['ground'] }, { id: 'hero', layers: null }, ], ), ).toEqual(['hero', 'map']); }); it('puts the map in front where a layer above the sprite has a tile', () => { expect( stackAt( [map, hero], [ { id: 'map', layers: ['ground', 'roofs'] }, { id: 'hero', layers: null }, ], ), ).toEqual(['map', 'hero']); }); it('leaves out a map without a tile under the pointer', () => { expect( stackAt( [map, hero], [ { id: 'map', layers: [] }, { id: 'hero', layers: null }, ], ), ).toEqual(['hero']); }); it('orders sprites by z, ties in hierarchy order, with symbols in front of all', () => { const items = [ item('a', 'sprite', 2), item('b', 'sprite', 0), item('c', 'sprite', 2), item('box', 'collider', -9), ]; const hits = items.map((i) => ({ id: i.id, layers: null })); expect(stackAt(items, hits)).toEqual(['box', 'c', 'a', 'b']); }); }); describe('what a press picks (#203)', () => { const stack = ['top', 'middle', 'bottom']; it('selects the topmost when nothing selected is under the pointer', () => { expect(pressOn(stack, ['elsewhere'], 'elsewhere')).toEqual({ select: 'top', onClick: null }); }); it('keeps a selection hidden under others, so a drag moves it', () => { expect(pressOn(stack, ['bottom'], 'bottom')?.select).toBeNull(); }); it('passes a click on to the next one below, and round to the top again', () => { expect(pressOn(stack, ['top'], 'top')?.onClick).toBe('middle'); expect(pressOn(stack, ['middle'], 'middle')?.onClick).toBe('bottom'); expect(pressOn(stack, ['bottom'], 'bottom')?.onClick).toBe('top'); }); it('goes on from the primary one of a group', () => { expect(pressOn(stack, ['top', 'middle'], 'middle')).toEqual({ select: null, onClick: 'bottom' }); }); it('changes nothing on a click when the selected object is alone there', () => { expect(pressOn(['only'], ['only'], 'only')).toEqual({ select: null, onClick: null }); }); it('is nothing on empty space', () => { expect(pressOn([], ['top'], 'top')).toBeNull(); }); }); describe('where a click of the eyedropper moves its candidate (#276)', () => { const stack = ['top', 'middle', 'bottom']; const all = () => true; it('takes the topmost when the candidate is not under the pointer', () => { expect(pickOn(stack, all, null)).toEqual({ candidate: 'top', refused: false }); expect(pickOn(stack, all, 'elsewhere')).toEqual({ candidate: 'top', refused: false }); }); it('passes a click on the candidate on to the next one below, and round to the top again', () => { expect(pickOn(stack, all, 'top').candidate).toBe('middle'); expect(pickOn(stack, all, 'middle').candidate).toBe('bottom'); expect(pickOn(stack, all, 'bottom').candidate).toBe('top'); }); it('goes over what may be picked only', () => { const offered = (id: string) => id !== 'middle'; expect(pickOn(stack, offered, 'top').candidate).toBe('bottom'); expect(pickOn(['middle', 'bottom'], offered, null).candidate).toBe('bottom'); }); it('keeps the candidate on empty space, without a word', () => { expect(pickOn([], all, 'top')).toEqual({ candidate: 'top', refused: false }); expect(pickOn([], all, null)).toEqual({ candidate: null, refused: false }); }); it('keeps the candidate where nothing may be picked, and says so', () => { expect(pickOn(['middle'], (id) => id !== 'middle', 'top')).toEqual({ candidate: 'top', refused: true }); }); it('stays on the candidate when it is all that may be picked there', () => { expect(pickOn(['only'], all, 'only').candidate).toBe('only'); }); }); describe('what a right-click opens the menu for (#204)', () => { const stack = ['top', 'middle', 'bottom']; it('selects the topmost first when nothing selected is under the pointer', () => { expect(menuTargetOf(stack, ['elsewhere'], 'elsewhere')).toEqual({ id: 'top', select: true }); }); it('keeps a selection hidden under others and opens on it', () => { expect(menuTargetOf(stack, ['elsewhere', 'bottom'], 'elsewhere')).toEqual({ id: 'bottom', select: false, }); }); it('prefers the primary one of a group', () => { expect(menuTargetOf(stack, ['top', 'middle'], 'middle')).toEqual({ id: 'middle', select: false }); }); it('opens none on empty space', () => { expect(menuTargetOf([], ['top'], 'top')).toBeNull(); }); });