import { describe, expect, it } from "vitest"; import { BufferAttribute, BufferGeometry, Quaternion, Vector3 } from "three"; import { checkLength, longestExtent, maskToMetallicRoughness, meshArrays, readTga, TEXTURE_SIZE, weaponTarget, WEAPON_SOURCES } from "../tools/assets/weapons.mjs"; /** Baut eine TGA-Datei wie die Masken im Pack: 18 Byte Kopf, danach BGRA je Pixel in Dateireihenfolge. */ function tgaFile({ width, height, pixels, type = 2, depth = 32, originTop = false }) { const bytes = new Uint8Array(18 + pixels.length * 4); bytes[2] = type; bytes[12] = width; bytes[14] = height; bytes[16] = depth; bytes[17] = 8 | (originTop ? 0x20 : 0); pixels.forEach(([r, g, b, a], i) => bytes.set([b, g, r, a], 18 + i * 4)); return bytes; } describe("readTga", () => { // Dateireihenfolge: rot, grün, blau, weiß const pixels = [[255, 0, 0, 10], [0, 255, 0, 20], [0, 0, 255, 30], [255, 255, 255, 40]]; // Bruch: Ursprung unten links übersehen — die Maske läge kopfüber auf der Waffe. it("dreht die Zeilen um, wenn der Ursprung unten links liegt", () => { const { width, height, data } = readTga(tgaFile({ width: 2, height: 2, pixels })); expect([width, height]).toEqual([2, 2]); expect([...data]).toEqual([0, 0, 255, 30, 255, 255, 255, 40, 255, 0, 0, 10, 0, 255, 0, 20]); }); it("übernimmt die Reihenfolge, wenn der Ursprung oben liegt", () => { const { data } = readTga(tgaFile({ width: 2, height: 2, pixels, originTop: true })); expect([...data]).toEqual([255, 0, 0, 10, 0, 255, 0, 20, 0, 0, 255, 30, 255, 255, 255, 40]); }); // Bruch: stillschweigend falsch gelesene Datei statt einer verständlichen Meldung. it("lehnt RLE, 24 Bit und Farbtabellen ab", () => { expect(() => readTga(tgaFile({ width: 2, height: 2, pixels, type: 10 }))).toThrow(/Typ 10/); expect(() => readTga(tgaFile({ width: 2, height: 2, pixels, depth: 24 }))).toThrow(/24 Bit/); const withColorMap = tgaFile({ width: 2, height: 2, pixels }); withColorMap[1] = 1; expect(() => readTga(withColorMap)).toThrow(/Farbtabelle 1/); }); // Bruch: Dateigröße nicht geprüft — die Maske würde stillschweigend unvollständig gelesen. it("meldet eine zu kurze Datei", () => { expect(() => readTga(tgaFile({ width: 4, height: 4, pixels }))).toThrow(/kürzer/); }); }); describe("maskToMetallicRoughness", () => { // Bruch: Kanäle vertauscht — Holz glänzte metallisch, Metall wirkte stumpf. it("legt den Grauwert nach G (Rauheit) und Alpha nach B (Metall)", () => { const mask = { width: 2, height: 1, data: Uint8Array.from([217, 217, 217, 56, 242, 242, 242, 179]) }; expect([...maskToMetallicRoughness(mask)]).toEqual([255, 217, 56, 255, 242, 179]); }); }); describe("meshArrays", () => { function geometry() { const result = new BufferGeometry(); result.setAttribute("position", new BufferAttribute(Float32Array.from([1, 0, 0, 0, 1, 0, 0, 0, 1]), 3)); result.setAttribute("normal", new BufferAttribute(Float32Array.from([0, 0, 1, 0, 0, 1, 0, 0, 1]), 3)); result.setAttribute("uv", new BufferAttribute(Float32Array.from([0, 0, 1, 0.25, 0.5, 1]), 2)); return result; } // Bruch: v nicht gespiegelt — jede Waffentextur läge senkrecht verkehrt. it("spiegelt v, weil FBX den UV-Ursprung unten und glTF ihn oben hat", () => { expect([...meshArrays(geometry(), new Quaternion()).uvs]).toEqual([0, 1, 1, 0.75, 0.5, 0]); }); // Bruch: Knotendrehung übergangen — die L96 zeigte mit dem Lauf nach hinten. it("dreht Eckpunkte und Normalen mit der Knotendrehung", () => { const halfTurn = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI); const { positions, normals } = meshArrays(geometry(), halfTurn); expect(positions[0]).toBeCloseTo(-1, 6); expect(positions[8]).toBeCloseTo(-1, 6); expect(normals[2]).toBeCloseTo(-1, 6); }); }); describe("checkLength", () => { const pistol = WEAPON_SOURCES.find((source) => source.id === "pistol"); const along = (length) => Float32Array.from([0, 0, 0, length, 0.02, 0.01]); it("misst die längste Ausdehnung", () => { expect(longestExtent(along(0.189))).toBeCloseTo(0.189, 6); }); it("lässt eine 0,19 m lange Pistole durch", () => { expect(checkLength("pistol", along(0.189), pistol.length)).toBeCloseTo(0.189, 6); }); // Bruch: Knotenmaßstab 100 der Pistole mitgerechnet — eine 19 m lange Waffe in der Hand. it("bricht ab, wenn die Einheit nicht stimmt", () => { expect(() => checkLength("pistol", along(18.9), pistol.length)).toThrow(/falsche Einheit/); }); }); describe("WEAPON_SOURCES", () => { it("nennt die fünf Waffen und die AK47 nur als Option 2", () => { expect(WEAPON_SOURCES.map((source) => source.id)).toEqual(["pistol", "ak47", "m4", "shotgun870", "l96"]); expect(WEAPON_SOURCES.find((source) => source.id === "ak47").dir).toBe("AK47_Option2(Same Socketing)"); expect(TEXTURE_SIZE).toBe(1024); }); // Bruch: ID-Validierung übergangen — eine manipulierte ID könnte aus public/models/weapons/ herausführen. it("legt jede Waffe unter public/models/weapons ab", () => { expect(weaponTarget("pistol").replace(/\\/g, "/")).toMatch(/public\/models\/weapons\/pistol\.glb$/); expect(() => weaponTarget("../ual")).toThrow(/Ungültige Waffen-ID/); }); });