loader.test.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { describe, expect, it } from 'vitest';
  2. import { readMapFiles } from '../src/server/assets';
  3. import { buildTerrain, MapValidationError, parseMapDefinition, type MapDefinition, type RgbaImage } from '../src/shared/maps/loader';
  4. import { ASSETS, realMaterials } from './helpers';
  5. const materials = realMaterials();
  6. function def(overrides: Partial<MapDefinition> = {}): MapDefinition {
  7. return parseMapDefinition({
  8. id: 'test',
  9. name: 'Test',
  10. size: [4, 4],
  11. materialPalette: { '#000000': 0, '#F2C46D': 1, '#8899AA': 255 },
  12. spawns: [[1, 0]],
  13. cherrySpawn: [2, 0],
  14. custard: { startY: 3, speed: 1, finalRushSpeed: 2 },
  15. items: { interval: [1, 2], weights: { candle: 1 } },
  16. ...overrides,
  17. });
  18. }
  19. /** Rows of 0xRRGGBB pixels -> RGBA image. */
  20. function image(rows: number[][]): RgbaImage {
  21. const h = rows.length;
  22. const w = rows[0].length;
  23. const data = new Uint8Array(w * h * 4);
  24. rows.flat().forEach((c, i) => {
  25. data.set([c >> 16, (c >> 8) & 0xff, c & 0xff, 255], i * 4);
  26. });
  27. return { width: w, height: h, data };
  28. }
  29. const E = 0x000000;
  30. const S = 0xf2c46d;
  31. const P = 0x8899aa;
  32. const valid = [
  33. [E, E, E, E],
  34. [E, E, E, E],
  35. [S, S, S, S],
  36. [P, P, P, P],
  37. ];
  38. describe('map loader', () => {
  39. it('loads a valid map', () => {
  40. const bm = buildTerrain(def(), image(valid), materials);
  41. expect(bm.get(0, 2)).toBe(1);
  42. expect(bm.get(0, 3)).toBe(255);
  43. });
  44. it('rejects colors outside the palette', () => {
  45. const bad = valid.map((r) => [...r]);
  46. bad[2][1] = 0x123456;
  47. expect(() => buildTerrain(def(), image(bad), materials)).toThrow(/#123456.*not in the palette/);
  48. });
  49. it('rejects a map without plate', () => {
  50. const noPlate = valid.map((r) => r.map((c) => (c === P ? S : c)));
  51. expect(() => buildTerrain(def(), image(noPlate), materials)).toThrow(/no plate/);
  52. });
  53. it('rejects spawns inside terrain or above nothing', () => {
  54. expect(() => buildTerrain(def({ spawns: [[1, 2]] }), image(valid), materials)).toThrow(/inside solid/);
  55. const hole = valid.map((r) => [...r]);
  56. hole[2][0] = E;
  57. hole[3][0] = E;
  58. expect(() => buildTerrain(def({ spawns: [[0, 0]] }), image(hole), materials)).toThrow(/no ground/);
  59. });
  60. it('rejects size mismatches and malformed map.json', () => {
  61. expect(() => buildTerrain(def({ size: [5, 4] }), image(valid), materials)).toThrow(/terrain.png is 4x4/);
  62. expect(() => parseMapDefinition({ id: 'x' }, 'x')).toThrow(MapValidationError);
  63. expect(() => parseMapDefinition({ ...def(), id: 'other' }, 'test')).toThrow(/folder/);
  64. });
  65. it('the reference map gateau-etages is valid', () => {
  66. const raw = readMapFiles(ASSETS, 'gateau-etages');
  67. const map = parseMapDefinition(raw.definition, 'gateau-etages');
  68. const bm = buildTerrain(map, raw.terrain, materials);
  69. expect(bm.width).toBe(2048);
  70. expect(bm.height).toBe(1024);
  71. });
  72. });