UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

852 lines (692 loc) 29 kB
var PCTDecode = require('../../../src/textures/parsers/PCTDecode'); describe('Phaser.Textures.Parsers.PCTDecode', function () { var warnSpy; beforeEach(function () { // Silence the console.warn calls made by the decoder for invalid input so // the test runner output stays clean. Individual tests that want to assert // on the warnings read from warnSpy.mock.calls directly. warnSpy = vi.spyOn(console, 'warn').mockImplementation(function () {}); }); afterEach(function () { warnSpy.mockRestore(); }); // ------------------------------------------------------------------------- // Header / version validation // ------------------------------------------------------------------------- describe('header validation', function () { it('should return null for a non-string input', function () { expect(PCTDecode(null)).toBeNull(); expect(PCTDecode(undefined)).toBeNull(); expect(PCTDecode(42)).toBeNull(); expect(PCTDecode({})).toBeNull(); }); it('should return null for an empty string', function () { expect(PCTDecode('')).toBeNull(); }); it('should return null when the PCT: header is missing', function () { var text = 'P:atlas_0.png,RGBA8888,256,256,0\n'; expect(PCTDecode(text)).toBeNull(); }); it('should warn when the PCT: header is missing', function () { PCTDecode('P:atlas_0.png,RGBA8888,256,256,0\n'); expect(warnSpy).toHaveBeenCalled(); }); it('should return null for an unsupported major version', function () { var text = 'PCT:2.0\nP:atlas_0.png,RGBA8888,256,256,0\n'; expect(PCTDecode(text)).toBeNull(); }); it('should warn for an unsupported major version', function () { PCTDecode('PCT:2.0\nP:atlas_0.png,RGBA8888,256,256,0\n'); expect(warnSpy).toHaveBeenCalled(); }); it('should accept version 1.0', function () { var text = 'PCT:1.0\nP:atlas_0.png,RGBA8888,256,256,0\n'; expect(PCTDecode(text)).not.toBeNull(); }); it('should accept minor version greater than 0', function () { // Minor bumps must load successfully in a 1.0 parser var text = 'PCT:1.5\nP:atlas_0.png,RGBA8888,256,256,0\n'; expect(PCTDecode(text)).not.toBeNull(); }); it('should strip a trailing CR from the header line', function () { var text = 'PCT:1.0\r\nP:atlas_0.png,RGBA8888,256,256,0\r\n'; expect(PCTDecode(text)).not.toBeNull(); }); }); // ------------------------------------------------------------------------- // Page header (P:) // ------------------------------------------------------------------------- describe('page headers', function () { it('should parse a single page header into the pages array', function () { var text = 'PCT:1.0\nP:atlas_0.png,RGBA8888,1024,512,2\n'; var result = PCTDecode(text); expect(result.pages).toEqual([ { filename: 'atlas_0.png', format: 'RGBA8888', width: 1024, height: 512, padding: 2 } ]); }); it('should parse multiple page headers in order', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,2048,512,2\n' + 'P:atlas_1.png,RGBA8888,1024,256,4\n'; var result = PCTDecode(text); expect(result.pages.length).toBe(2); expect(result.pages[0].filename).toBe('atlas_0.png'); expect(result.pages[1].filename).toBe('atlas_1.png'); expect(result.pages[0].padding).toBe(2); expect(result.pages[1].padding).toBe(4); }); }); // ------------------------------------------------------------------------- // Folder dictionary (F:) // ------------------------------------------------------------------------- describe('folder dictionary', function () { it('should return an empty folders array when there are no F: lines', function () { var text = 'PCT:1.0\nP:atlas_0.png,RGBA8888,256,256,0\n'; var result = PCTDecode(text); expect(result.folders).toEqual([]); }); it('should parse folder entries in declaration order', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'F:warrior\n' + 'F:knight\n' + 'F:effects\n'; var result = PCTDecode(text); expect(result.folders).toEqual([ 'warrior', 'knight', 'effects' ]); }); it('should allow folder names with slashes', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'F:knight/idle\n'; var result = PCTDecode(text); expect(result.folders[0]).toBe('knight/idle'); }); }); // ------------------------------------------------------------------------- // Block (B:) header and block names line // ------------------------------------------------------------------------- describe('block headers and names', function () { it('should expand a simple single-row block (spec Example 1)', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,1024,256,2\n' + 'B:2,2,8,64,64\n' + 'frame#1-8\n'; var result = PCTDecode(text); var names = Object.keys(result.frames); expect(names.length).toBe(8); // Positions documented in the spec: blockX=2, padding=2, cellW=64+4=68 var expected = [ 4, 72, 140, 208, 276, 344, 412, 480 ]; for (var i = 0; i < 8; i++) { var frame = result.frames['frame' + (i + 1)]; expect(frame).toBeDefined(); expect(frame.x).toBe(expected[i]); expect(frame.y).toBe(4); expect(frame.w).toBe(64); expect(frame.h).toBe(64); expect(frame.trimmed).toBe(false); expect(frame.page).toBe(0); } }); it('should lay out multi-row blocks by col = i % cols, row = floor(i/cols)', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,512,512,0\n' + 'B:0,0,3,10,10\n' + 'a,b,c,d,e,f\n'; var result = PCTDecode(text); // padding=0 → cellW=10, cellH=10 expect(result.frames.a).toMatchObject({ x: 0, y: 0 }); expect(result.frames.b).toMatchObject({ x: 10, y: 0 }); expect(result.frames.c).toMatchObject({ x: 20, y: 0 }); expect(result.frames.d).toMatchObject({ x: 0, y: 10 }); expect(result.frames.e).toMatchObject({ x: 10, y: 10 }); expect(result.frames.f).toMatchObject({ x: 20, y: 10 }); }); it('should parse trimmed block headers and copy the trim data onto each frame', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,2048,512,2\n' + 'B:2,2,6,120,108|134,120,4,6\n' + 'frame#01-06\n'; var result = PCTDecode(text); var f1 = result.frames.frame01; expect(f1.trimmed).toBe(true); expect(f1.sourceW).toBe(134); expect(f1.sourceH).toBe(120); expect(f1.trimX).toBe(4); expect(f1.trimY).toBe(6); // The w/h on a trimmed frame still reflects the in-atlas packed size expect(f1.w).toBe(120); expect(f1.h).toBe(108); }); it('should default sourceW/H/trimX/trimY for untrimmed blocks', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,2,32,32\n' + 'a,b\n'; var result = PCTDecode(text); expect(result.frames.a.sourceW).toBe(32); expect(result.frames.a.sourceH).toBe(32); expect(result.frames.a.trimX).toBe(0); expect(result.frames.a.trimY).toBe(0); expect(result.frames.a.trimmed).toBe(false); }); it('should consume the next line as block names even if it starts with #', function () { // Edge case: the reorder fix ensures a pending block always consumes // the next non-empty line, even if it happens to start with '#'. var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,3,10,10\n' + '#1-3\n'; var result = PCTDecode(text); expect(Object.keys(result.frames).sort()).toEqual([ '1', '2', '3' ]); expect(result.frames['1'].x).toBe(0); expect(result.frames['2'].x).toBe(10); expect(result.frames['3'].x).toBe(20); }); }); // ------------------------------------------------------------------------- // Range compression // ------------------------------------------------------------------------- describe('range compression', function () { it('should zero-pad generated numbers when startStr has a leading zero', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,12,10,10\n' + 'idle_#001-012\n'; var result = PCTDecode(text); expect(result.frames.idle_001).toBeDefined(); expect(result.frames.idle_012).toBeDefined(); // idle_12 (non-padded) should NOT be emitted expect(result.frames.idle_12).toBeUndefined(); }); it('should not zero-pad when the start number has no leading zero', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,5,10,10\n' + 'frame#1-5\n'; var result = PCTDecode(text); expect(result.frames.frame1).toBeDefined(); expect(result.frames.frame5).toBeDefined(); expect(result.frames.frame01).toBeUndefined(); }); it('should emit literal comma-separated names without expansion', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,3,10,10\n' + 'alpha,beta,gamma\n'; var result = PCTDecode(text); expect(result.frames.alpha).toBeDefined(); expect(result.frames.beta).toBeDefined(); expect(result.frames.gamma).toBeDefined(); }); it('should mix literal names and ranges on the same line', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,5,10,10\n' + 'intro,frame#1-3,outro\n'; var result = PCTDecode(text); expect(Object.keys(result.frames).sort()).toEqual([ 'frame1', 'frame2', 'frame3', 'intro', 'outro' ]); }); }); // ------------------------------------------------------------------------- // Folder and extension index resolution // ------------------------------------------------------------------------- describe('name encoding', function () { it('should resolve folder indices via "N/rest" prefix', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'F:warrior\n' + 'F:knight\n' + 'B:0,0,2,10,10\n' + '0/idle,1/idle\n'; var result = PCTDecode(text); expect(result.frames['warrior/idle']).toBeDefined(); expect(result.frames['knight/idle']).toBeDefined(); }); it('should not treat a non-numeric segment before slash as a folder index', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'F:warrior\n' + 'B:0,0,1,10,10\n' + 'abc/def\n'; var result = PCTDecode(text); // abc/def should NOT be resolved through the folder table expect(result.frames['abc/def']).toBeDefined(); expect(result.frames['warrior/def']).toBeUndefined(); }); it('should apply the line-level ~N extension suffix to all block names', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'B:0,0,3,10,10\n' + 'a,b,c~1\n'; var result = PCTDecode(text); expect(result.frames['a.png']).toBeDefined(); expect(result.frames['b.png']).toBeDefined(); expect(result.frames['c.png']).toBeDefined(); }); it('should apply the hardcoded extension dictionary correctly', function () { // 1=.png, 2=.webp, 3=.jpg, 4=.jpeg, 5=.gif var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'one~1|0|0,0,1,1\n' + 'two~2|0|0,0,1,1\n' + 'three~3|0|0,0,1,1\n' + 'four~4|0|0,0,1,1\n' + 'five~5|0|0,0,1,1\n'; var result = PCTDecode(text); expect(result.frames['one.png']).toBeDefined(); expect(result.frames['two.webp']).toBeDefined(); expect(result.frames['three.jpg']).toBeDefined(); expect(result.frames['four.jpeg']).toBeDefined(); expect(result.frames['five.gif']).toBeDefined(); }); it('should compose folder index, range compression and extension suffix', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'F:warrior\n' + 'B:0,0,4,10,10\n' + '0/idle_#01-04~1\n'; var result = PCTDecode(text); expect(result.frames['warrior/idle_01.png']).toBeDefined(); expect(result.frames['warrior/idle_02.png']).toBeDefined(); expect(result.frames['warrior/idle_03.png']).toBeDefined(); expect(result.frames['warrior/idle_04.png']).toBeDefined(); }); it('should leave names with a raw .ext suffix untouched', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'heightmap.tga|0|0,0,32,32\n'; var result = PCTDecode(text); expect(result.frames['heightmap.tga']).toBeDefined(); }); }); // ------------------------------------------------------------------------- // Individual frames // ------------------------------------------------------------------------- describe('individual frames', function () { it('should parse an untrimmed individual frame', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,1\n' + 'logo|0|1,1,200,180\n'; var result = PCTDecode(text); var logo = result.frames.logo; expect(logo).toBeDefined(); expect(logo.x).toBe(1); expect(logo.y).toBe(1); expect(logo.w).toBe(200); expect(logo.h).toBe(180); expect(logo.trimmed).toBe(false); expect(logo.rotated).toBe(false); // sourceW/H default to w/h for untrimmed expect(logo.sourceW).toBe(200); expect(logo.sourceH).toBe(180); expect(logo.trimX).toBe(0); expect(logo.trimY).toBe(0); }); it('should parse a trimmed individual frame with trim data in one segment', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,1024,512,0\n' + 'shield|2|726,48,72,68|80,80,4,6\n'; var result = PCTDecode(text); var shield = result.frames.shield; expect(shield.trimmed).toBe(true); expect(shield.x).toBe(726); expect(shield.y).toBe(48); expect(shield.w).toBe(72); expect(shield.h).toBe(68); expect(shield.sourceW).toBe(80); expect(shield.sourceH).toBe(80); expect(shield.trimX).toBe(4); expect(shield.trimY).toBe(6); }); it('should set rotated=true when flag bit 0 is set', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'r|1|0,0,32,32\n'; var result = PCTDecode(text); expect(result.frames.r.rotated).toBe(true); expect(result.frames.r.trimmed).toBe(false); }); it('should set both trimmed and rotated when flags=3', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'rt|3|0,0,32,32|40,40,2,2\n'; var result = PCTDecode(text); expect(result.frames.rt.rotated).toBe(true); expect(result.frames.rt.trimmed).toBe(true); expect(result.frames.rt.sourceW).toBe(40); expect(result.frames.rt.trimX).toBe(2); }); it('should associate individual frames with the current #page', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'P:atlas_1.png,RGBA8888,256,256,0\n' + 'a|0|0,0,10,10\n' + '#1\n' + 'b|0|0,0,10,10\n'; var result = PCTDecode(text); expect(result.frames.a.page).toBe(0); expect(result.frames.b.page).toBe(1); }); it('should skip individual frame lines with fewer than 3 pipe segments', function () { // These are unknown/future record types and must be silently ignored. var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'some-future-line\n' + 'another|one\n' + 'ok|0|0,0,10,10\n'; var result = PCTDecode(text); expect(result.frames.ok).toBeDefined(); expect(Object.keys(result.frames).length).toBe(1); }); }); // ------------------------------------------------------------------------- // Page selector (#) // ------------------------------------------------------------------------- describe('page selector', function () { it('should route subsequent block frames to the selected page', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'P:atlas_1.png,RGBA8888,256,256,0\n' + '#1\n' + 'B:0,0,2,10,10\n' + 'a,b\n'; var result = PCTDecode(text); expect(result.frames.a.page).toBe(1); expect(result.frames.b.page).toBe(1); }); it('should default to page 0 when no # selector is used', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'logo|0|0,0,10,10\n'; var result = PCTDecode(text); expect(result.frames.logo.page).toBe(0); }); }); // ------------------------------------------------------------------------- // Aliases (A:) // ------------------------------------------------------------------------- describe('aliases', function () { it('should create duplicate frames that share the original\'s position', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'orig|0|42,24,16,16\n' + 'A:orig=dup1,dup2\n'; var result = PCTDecode(text); expect(result.frames.dup1).toBeDefined(); expect(result.frames.dup2).toBeDefined(); expect(result.frames.dup1.x).toBe(42); expect(result.frames.dup1.y).toBe(24); expect(result.frames.dup2.x).toBe(42); expect(result.frames.dup2.y).toBe(24); }); it('should set the duplicate\'s key to its own name, not the original\'s', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'orig|0|0,0,10,10\n' + 'A:orig=dup\n'; var result = PCTDecode(text); expect(result.frames.dup.key).toBe('dup'); expect(result.frames.orig.key).toBe('orig'); }); it('should deep-copy so mutating a duplicate does not affect the original', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'orig|0|5,5,10,10\n' + 'A:orig=dup\n'; var result = PCTDecode(text); result.frames.dup.x = 999; expect(result.frames.orig.x).toBe(5); }); it('should silently skip aliases that reference a missing original', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'A:does_not_exist=dup\n'; var result = PCTDecode(text); expect(result.frames.dup).toBeUndefined(); }); it('should silently skip malformed alias lines with no = separator', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'orig|0|0,0,10,10\n' + 'A:no_equals_sign_here\n'; var result = PCTDecode(text); expect(result.frames.orig).toBeDefined(); }); it('should resolve folder and extension indices on both sides of the = sign', function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + 'F:warrior\n' + 'B:0,0,3,10,10\n' + '0/idle_#01-03~1\n' + 'A:0/idle_01~1=0/idle_02,0/idle_03~1\n'; var result = PCTDecode(text); // Both idle_02.png and idle_03.png existed from the block expansion, // and the alias should overwrite them with copies of idle_01. expect(result.frames['warrior/idle_02.png'].x).toBe(result.frames['warrior/idle_01.png'].x); expect(result.frames['warrior/idle_03.png'].x).toBe(result.frames['warrior/idle_01.png'].x); }); }); // ------------------------------------------------------------------------- // Line ending handling // ------------------------------------------------------------------------- describe('line endings', function () { it('should handle CRLF line endings throughout the file', function () { var text = 'PCT:1.0\r\n' + 'P:atlas_0.png,RGBA8888,256,256,1\r\n' + 'logo|0|1,1,200,180\r\n'; var result = PCTDecode(text); expect(result.frames.logo).toBeDefined(); expect(result.frames.logo.x).toBe(1); expect(result.frames.logo.w).toBe(200); }); it('should skip empty lines without breaking', function () { var text = 'PCT:1.0\n' + '\n' + 'P:atlas_0.png,RGBA8888,256,256,0\n' + '\n' + 'logo|0|0,0,10,10\n' + '\n'; var result = PCTDecode(text); expect(result.frames.logo).toBeDefined(); }); }); // ------------------------------------------------------------------------- // End-to-end spec example // ------------------------------------------------------------------------- describe('full spec Example 2', function () { var result; beforeEach(function () { var text = 'PCT:1.0\n' + 'P:atlas_0.png,RGBA8888,2048,512,2\n' + 'P:atlas_1.png,RGBA8888,2048,256,2\n' + 'F:warrior\n' + 'F:knight\n' + 'F:effects\n' + '#0\n' + 'B:2,2,6,120,108|134,120,4,6\n' + '0/idle_#01-24~1\n' + 'B:2,222,6,120,108|134,120,4,6\n' + '1/idle_#01-18~1\n' + 'sword~1|0|726,2,86,42\n' + 'shield~1|2|726,48,72,68|80,80,4,6\n' + '#1\n' + 'B:2,2,10,48,48\n' + '2/spark_#01-30~1\n' + 'A:0/idle_01~1=0/idle_12,0/idle_18~1\n' + 'A:1/idle_01~1=1/idle_09~1\n'; result = PCTDecode(text); }); it('should decode both pages', function () { expect(result.pages.length).toBe(2); expect(result.pages[0].filename).toBe('atlas_0.png'); expect(result.pages[1].filename).toBe('atlas_1.png'); }); it('should decode all three folders', function () { expect(result.folders).toEqual([ 'warrior', 'knight', 'effects' ]); }); it('should produce 24 warrior + 18 knight + 30 effects + 2 individual = 74 frames', function () { expect(Object.keys(result.frames).length).toBe(74); }); it('should place warrior/idle_01.png at the documented position (4, 4)', function () { var f = result.frames['warrior/idle_01.png']; expect(f.x).toBe(4); expect(f.y).toBe(4); expect(f.trimmed).toBe(true); expect(f.sourceW).toBe(134); expect(f.sourceH).toBe(120); expect(f.trimX).toBe(4); expect(f.trimY).toBe(6); }); it('should place warrior/idle_24.png at the last cell of a 4-row block', function () { // cols=6, index 23 → col=5, row=3 // cellW = 120+4 = 124, cellH = 108+4 = 112 // x = 2 + 5*124 + 2 = 624 // y = 2 + 3*112 + 2 = 340 var f = result.frames['warrior/idle_24.png']; expect(f.x).toBe(624); expect(f.y).toBe(340); }); it('should place knight/idle_01.png in the second block at (4, 224)', function () { var f = result.frames['knight/idle_01.png']; expect(f.x).toBe(4); expect(f.y).toBe(224); }); it('should route effects/spark_01.png to page 1', function () { expect(result.frames['effects/spark_01.png'].page).toBe(1); }); it('should decode the untrimmed sword individual frame', function () { var s = result.frames['sword.png']; expect(s.x).toBe(726); expect(s.y).toBe(2); expect(s.w).toBe(86); expect(s.h).toBe(42); expect(s.trimmed).toBe(false); expect(s.page).toBe(0); }); it('should decode the trimmed shield individual frame', function () { var s = result.frames['shield.png']; expect(s.trimmed).toBe(true); expect(s.sourceW).toBe(80); expect(s.sourceH).toBe(80); expect(s.trimX).toBe(4); expect(s.trimY).toBe(6); }); it('should make the warrior aliases share the original\'s atlas position', function () { var orig = result.frames['warrior/idle_01.png']; var a1 = result.frames['warrior/idle_12.png']; var a2 = result.frames['warrior/idle_18.png']; expect(a1.x).toBe(orig.x); expect(a1.y).toBe(orig.y); expect(a2.x).toBe(orig.x); expect(a2.y).toBe(orig.y); }); }); });