import { Euler, Quaternion, Vector3 } from "three"; import { describe, expect, it } from "vitest"; import type { DungeonTransform, Vector3Tuple } from "./dungeonTypes"; import { RUNEWAKER_NATIVE_LIGHTING_SOURCE } from "./generated/runewakerNativeLighting.generated"; import { resolveRunewakerNativeLighting, runewakerRuntimePositionToSource, runewakerSourcePositionToRuntime, } from "./runewakerNativeLighting"; const identityTransform: DungeonTransform = { position: [0, 0, 0], rotation: [0, 0, 0], scale: 1, }; function definition(id: string, transform = identityTransform) { return { id, transform }; } function toTuple(vector: Vector3): Vector3Tuple { return [vector.x, vector.y, vector.z]; } describe("native Runewaker environment resolver", () => { it("covers every registered RoM WDB and matches the source engine's entry trigger/settings result", () => { const dungeons = RUNEWAKER_NATIVE_LIGHTING_SOURCE.dungeons; expect(Object.keys(dungeons)).toHaveLength(30); for (const [id, data] of Object.entries(dungeons)) { const entry = data.authoredEntry; expect(entry, `${id} should retain its audited source entry`).not.toBeNull(); const runtimePosition = runewakerSourcePositionToRuntime( definition(id), entry!.sourcePosition as Vector3Tuple, ); expect(runtimePosition, `${id} source entry should map to runtime space`).not.toBeNull(); if (!runtimePosition) throw new Error(`Missing runtime lighting position for ${id}`); const sourceRoundTrip = runewakerRuntimePositionToSource(definition(id), runtimePosition!); sourceRoundTrip!.forEach((value, axis) => expect(value, `${id} axis ${axis}, runtime ${runtimePosition.join(",")}`).toBeCloseTo(entry!.sourcePosition[axis], 4)); const resolved = resolveRunewakerNativeLighting(definition(id), runtimePosition!); const expectedTriggerId = entry!.selection.triggerZoneId; const expectedSettingsId = entry!.selection.settingsZoneId; if (expectedTriggerId == null || expectedSettingsId == null) { expect(resolved, `${id} should retain its existing profile when no source zone resolves`).toBeNull(); } else { expect(resolved, `${id} should resolve source lighting`).not.toBeNull(); expect(resolved!.triggerZone.id).toBe(expectedTriggerId); expect(resolved!.settingsZone.id).toBe(expectedSettingsId); expect(resolved!.timeGroup).toBe(entry!.selection.selectedTimeGroup); expect(resolved!.weatherGroup).toBe(entry!.selection.selectedWeatherGroup); expect(resolved!.sourceWdbSha256).toBe(data.source.wdbSha256); } } }); it("uses the reviewed Origin path node's source zone instead of the raw Lua entrance", () => { const data = RUNEWAKER_NATIVE_LIGHTING_SOURCE.dungeons["the-origin"]; const reviewed = data.reviewedRuntimeEntry; expect(reviewed?.covered).toBe(true); const runtimePosition = runewakerSourcePositionToRuntime( definition("the-origin"), reviewed!.sourcePosition as Vector3Tuple, ); const resolved = resolveRunewakerNativeLighting(definition("the-origin"), runtimePosition!); expect(resolved?.triggerZone.id).toBe(reviewed!.selection.triggerZoneId); expect(resolved?.settingsZone.id).toBe(3); }); it("inherits a child trigger's parent settings and applies native time/weather fallbacks", () => { const data = RUNEWAKER_NATIVE_LIGHTING_SOURCE.dungeons["grafu-castle"]; const childTrigger = data.zoneTriggers.find((trigger) => trigger.zoneId === 3)!; const runtimePosition = runewakerSourcePositionToRuntime(definition("grafu-castle"), childTrigger.obb.center as Vector3Tuple); const resolved = resolveRunewakerNativeLighting(definition("grafu-castle"), runtimePosition!, { timeGroup: 2, weatherGroup: 7, }); expect(resolved?.triggerZone.id).toBe(3); expect(resolved?.settingsZone.id).toBe(1); expect(resolved?.timeGroup).toBe(2); expect(resolved?.weatherGroup).toBe(0); }); it("preserves Rune's byte-normalized linear colors, three diffuse directionals, and distinct specular values", () => { const data = RUNEWAKER_NATIVE_LIGHTING_SOURCE.dungeons["mystic-altar"]; const runtimePosition = runewakerSourcePositionToRuntime( definition("mystic-altar"), data.authoredEntry!.sourcePosition as Vector3Tuple, ); const resolved = resolveRunewakerNativeLighting(definition("mystic-altar"), runtimePosition!, { cameraFarDistance: 1000, })!; const settings = data.zones.find((zone) => zone.id === resolved.settingsZone.id)!.settings[0]; if (!settings) throw new Error("Missing source lighting settings fixture"); expect(resolved.ambient.colorLinear).toEqual(settings.ambient.rgba.slice(0, 3)); expect(resolved.directionalLights).toHaveLength(3); expect(resolved.directionalLights.map((light) => light.intensity)).toEqual([1, 0.8, 0.8]); expect(resolved.directionalLights[0].colorLinear).toEqual(settings.primary.rgba.slice(0, 3)); expect(resolved.directionalLights[1].colorLinear).toEqual(settings.secondary.rgba.slice(0, 3)); expect(resolved.specular.colorLinear).toEqual(settings.specular.rgba.slice(0, 3)); expect(resolved.fog.startFactor).toBeCloseTo(400 / 1600); expect(resolved.fog.endFactor).toBeCloseTo(1300 / 1600); expect(resolved.fog.startDistance).toBeCloseTo(250); expect(resolved.fog.endDistance).toBeCloseTo(812.5); }); it("transforms authored directions through the inverse primary placement and runtime dungeon rotation", () => { const data = RUNEWAKER_NATIVE_LIGHTING_SOURCE.dungeons["mystic-altar"]; const transform: DungeonTransform = { position: [8, -3, 4], rotation: [0.2, -0.35, 0.5], scale: 1.6, }; const runtimePosition = runewakerSourcePositionToRuntime( definition("mystic-altar", transform), data.authoredEntry!.sourcePosition as Vector3Tuple, ); const resolved = resolveRunewakerNativeLighting(definition("mystic-altar", transform), runtimePosition!)!; const settings = data.zones.find((zone) => zone.id === resolved.settingsZone.id)!.settings[0]; if (!settings) throw new Error("Missing source lighting settings fixture"); const sourceDirection = new Vector3(...settings.nativeDirectionsLeftHandedYUp.primary); const primaryRotation = new Quaternion(...data.source.primaryPlacement.rotation).normalize(); const primaryScale = new Vector3(...data.source.primaryPlacement.scale); sourceDirection.applyQuaternion(primaryRotation.invert()).divide(primaryScale); sourceDirection.z *= -1; sourceDirection.normalize(); sourceDirection.applyQuaternion(new Quaternion().setFromEuler(new Euler(...transform.rotation, "XYZ"))); const actual = new Vector3(...resolved.directionalLights[0].rayDirection); expect(actual.distanceTo(sourceDirection.normalize())).toBeLessThan(1e-6); expect(new Vector3(...resolved.directionalLights[0].toLightDirection).distanceTo(actual.clone().negate())).toBeLessThan(1e-8); }); it("returns null for unknown dungeons and uncovered source points", () => { expect(resolveRunewakerNativeLighting(definition("not-a-rom-dungeon"), [0, 0, 0])).toBeNull(); const noCoverage = RUNEWAKER_NATIVE_LIGHTING_SOURCE.dungeons["grafu-castle"].authoredEntry!; const runtimePosition = runewakerSourcePositionToRuntime(definition("grafu-castle"), noCoverage.sourcePosition as Vector3Tuple); expect(resolveRunewakerNativeLighting(definition("grafu-castle"), runtimePosition!)).toBeNull(); }); });