| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- import { describe, expect, it } from 'vitest';
- import { TerrainBitmap } from '../src/shared/terrain/bitmap';
- import { DetachFinder } from '../src/shared/terrain/detach';
- import { applyTerrainEvent, rasterizeChunk } from '../src/shared/terrain/events';
- import { rleDecode, rleEncode } from '../src/shared/terrain/rle';
- import { boxesForMask } from '../src/server/physics/fallingChunk';
- import { bitmapFrom, realMaterials } from './helpers';
- const materials = realMaterials();
- const all = (bm: TerrainBitmap) => ({ x0: 0, y0: 0, x1: bm.width, y1: bm.height });
- describe('detached components', () => {
- it('everything connected to a plate stays', () => {
- const bm = bitmapFrom(['......', '.11...', '.1....', '.1111.', '######']);
- expect(new DetachFinder(bm).find(all(bm))).toEqual([]);
- });
- it('a floating island detaches with its exact mask', () => {
- const bm = bitmapFrom(['.......', '..23...', '..1....', '.......', '#######']);
- const [c] = new DetachFinder(bm).find(all(bm));
- expect(c.bounds).toEqual({ x0: 2, y0: 1, x1: 4, y1: 3 });
- expect([...c.mask]).toEqual([2, 3, 1, 0]);
- expect(c.pixelCount).toBe(3);
- });
- it('diagonal contact does not anchor (4-connectivity)', () => {
- const bm = bitmapFrom(['.1...', '..1..', '..1..', '#####']);
- const comps = new DetachFinder(bm).find(all(bm));
- expect(comps).toHaveLength(1);
- expect(comps[0].pixelCount).toBe(1);
- });
- it('finds every separate island in the region, once each', () => {
- const bm = bitmapFrom(['1.1.1', '.....', '1.1.1', '.....', '#####']);
- expect(new DetachFinder(bm).find(all(bm))).toHaveLength(6);
- });
- it('only looks around the changed region', () => {
- const bm = bitmapFrom(['1......', '.......', '.....1.', '.....1.', '#######']);
- const comps = new DetachFinder(bm).find({ x0: 0, y0: 0, x1: 1, y1: 1 });
- expect(comps).toHaveLength(1);
- expect(comps[0].bounds.x0).toBe(0);
- });
- it('cutting the only support of a ledge detaches the ledge', () => {
- const rows = ['..........', '.1111111..', '.1........', '.1........', '##########'];
- const bm = bitmapFrom(rows);
- const finder = new DetachFinder(bm);
- expect(finder.find(all(bm))).toEqual([]);
- bm.data[3 * bm.width + 1] = 0; // cut the pillar
- const [c] = finder.find({ x0: 1, y0: 3, x1: 2, y1: 4 });
- expect(c.pixelCount).toBe(8);
- expect(finder.isAnchored(1 * bm.width + 3)).toBe(false);
- });
- it('a detach event removes exactly the masked pixels', () => {
- const bm = bitmapFrom(['.....', '.12..', '..1..', '.....', '#####']);
- const [c] = new DetachFinder(bm).find(all(bm));
- const { x0, y0, x1, y1 } = c.bounds;
- applyTerrainEvent(bm, { kind: 'detach', id: 1, x: x0, y: y0, w: x1 - x0, h: y1 - y0, mask: rleEncode(c.mask), debris: false }, materials);
- expect([...bm.data].filter((v) => v !== 0 && v !== 255)).toEqual([]);
- });
- });
- describe('RLE', () => {
- it('round-trips and rejects malformed input', () => {
- const data = new Uint8Array([0, 0, 0, 5, 5, 1, 0, 0]);
- expect(rleEncode(data)).toEqual([0, 3, 5, 2, 1, 1, 0, 2]);
- expect([...rleDecode(rleEncode(data), data.length)]).toEqual([...data]);
- expect(() => rleDecode([1, 3], 2)).toThrow();
- expect(() => rleDecode([1], 1)).toThrow();
- });
- });
- describe('chunk rasterization', () => {
- it('writes an unrotated chunk back where it was, into empty pixels only', () => {
- const bm = new TerrainBitmap(8, 8);
- bm.data[5 * 8 + 3] = 4; // obstacle inside the chunk area
- const mask = new Uint8Array([1, 1, 1, 1, 1, 1]); // 3 × 2
- const patch = rasterizeChunk(bm, mask, 3, 2, 2, 4, 0, 0)!;
- expect(patch.pixelCount).toBe(5);
- applyTerrainEvent(bm, { kind: 'merge', id: 1, x: patch.x, y: patch.y, w: patch.w, h: patch.h, patch: rleEncode(patch.cells) }, materials);
- expect(bm.get(2, 4)).toBe(1);
- expect(bm.get(3, 5)).toBe(4);
- expect(bm.get(4, 5)).toBe(1);
- });
- it('a quarter turn keeps the pixel count and leaves no holes', () => {
- const bm = new TerrainBitmap(32, 32);
- const mask = new Uint8Array(10 * 4).fill(1);
- const patch = rasterizeChunk(bm, mask, 10, 4, 20, 5, Math.PI / 2, 0)!;
- expect(patch.pixelCount).toBe(40);
- });
- it('glues a chunk resting a couple of pixels above the ground', () => {
- const bm = new TerrainBitmap(6, 8);
- for (let x = 0; x < 6; x++) bm.data[7 * 6 + x] = 255;
- const patch = rasterizeChunk(bm, new Uint8Array([1, 1]), 2, 1, 2, 4, 0, 3)!;
- applyTerrainEvent(bm, { kind: 'merge', id: 1, x: patch.x, y: patch.y, w: patch.w, h: patch.h, patch: rleEncode(patch.cells) }, materials);
- expect(bm.get(2, 5)).toBe(1);
- expect(bm.get(2, 6)).toBe(1);
- expect(new DetachFinder(bm).isAnchored(4 * 6 + 2)).toBe(true);
- });
- });
- describe('chunk collision boxes', () => {
- it('covers a rectangle with one box and an L with two', () => {
- expect(boxesForMask(new Uint8Array(24 * 12).fill(1), 24, 12)).toEqual([{ x0: 0, y0: 0, x1: 24, y1: 12 }]);
- const l = new Uint8Array(24 * 24);
- for (let y = 0; y < 24; y++) for (let x = 0; x < 24; x++) if (x < 12 || y >= 12) l[y * 24 + x] = 1;
- expect(boxesForMask(l, 24, 24)).toHaveLength(2);
- });
- });
|