From c2c1d280d5efa8572273feccbdc3184d96e472e2 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Tue, 29 Sep 2026 10:29:42 +0000 Subject: [PATCH] fix(drlg): resolve M1 iteration 2 audit findings (animSpeed, crossActObjOf, strip-types, __d2Scene, D2Rng.int) --- scripts/pack-act-assets.ts | 2 +- scripts/pack-entity-assets.ts | 20 +++++-- scripts/verify-audio.ts | 4 +- src/formats/animdata.ts | 2 +- src/formats/bitstream.ts | 2 +- src/formats/cel.ts | 2 +- src/formats/cof.ts | 2 +- src/formats/dc6.ts | 2 +- src/formats/ds1.ts | 4 +- src/formats/dt1.ts | 4 +- src/formats/pal.ts | 2 +- src/formats/pcx.ts | 2 +- src/formats/pl2.ts | 2 +- src/formats/tbl.ts | 2 +- src/game/act-variants.ts | 8 ++- src/game/d2-rng.ts | 5 +- src/game/drlg/drlg-map.ts | 24 +++++++- src/game/drlg/drlg-preset.ts | 6 +- src/game/items.ts | 88 ++++++++++++++++-------------- src/game/treasure-class.ts | 28 +++++++--- src/mpq/adpcm.ts | 2 +- src/mpq/decompress.ts | 6 +- src/mpq/huffman.ts | 11 ++-- src/scene/frontend/scene-state.ts | 1 + tests/animated-tiles-speed.test.ts | 23 ++++++++ tests/d2-rng.test.ts | 7 +++ 26 files changed, 174 insertions(+), 87 deletions(-) diff --git a/scripts/pack-act-assets.ts b/scripts/pack-act-assets.ts index 97ea918..f2ac7f6 100644 --- a/scripts/pack-act-assets.ts +++ b/scripts/pack-act-assets.ts @@ -1476,7 +1476,7 @@ async function bakeDs1Variant( } : {}), }) if (pick !== null) objectsWithArt += 1 - else missingObjects.push(`${resolved.token === '' ? `id ${String(object.id)}` : resolved.token} (act ${String(object.act ?? entry.act)}) has no art members`) + else if (!resolved.artless) missingObjects.push(`${resolved.token === '' ? `id ${String(object.id)}` : resolved.token} (act ${String(object.act ?? entry.act)}) has no art members`) } const objectFiles = objectPages.encode(0) diff --git a/scripts/pack-entity-assets.ts b/scripts/pack-entity-assets.ts index a41daf2..4d8bc36 100644 --- a/scripts/pack-entity-assets.ts +++ b/scripts/pack-entity-assets.ts @@ -240,12 +240,22 @@ export interface MonsterResult { export type EntityResult = CharResult | MonsterResult class EntityWorkerContext { + public readonly archives: MountedArchives + public readonly animData: any + public readonly monstats2: any + public readonly palettes: Record + constructor( - public readonly archives: MountedArchives, - public readonly animData: any, - public readonly monstats2: any, - public readonly palettes: Record, - ) {} + archives: MountedArchives, + animData: any, + monstats2: any, + palettes: Record, + ) { + this.archives = archives + this.animData = animData + this.monstats2 = monstats2 + this.palettes = palettes + } static async create(archiveDir: string, outDir: string): Promise { const archives = new MountedArchives() diff --git a/scripts/verify-audio.ts b/scripts/verify-audio.ts index 2af28f9..98539bc 100644 --- a/scripts/verify-audio.ts +++ b/scripts/verify-audio.ts @@ -1,5 +1,5 @@ -import { decompressSector, COMPRESSION_ADPCM_MONO } from '../src/mpq/decompress.js'; -import { AudioManager } from '../src/audio/manager.js'; +import { decompressSector, COMPRESSION_ADPCM_MONO } from '../src/mpq/decompress.ts'; +import { AudioManager } from '../src/audio/manager.ts'; async function run() { console.log("Verifying Audio dependencies..."); diff --git a/src/formats/animdata.ts b/src/formats/animdata.ts index 630a5a1..d74c6b3 100644 --- a/src/formats/animdata.ts +++ b/src/formats/animdata.ts @@ -78,7 +78,7 @@ * comparing one file first-wins against the other last-wins. Comparing like for * like, the difference count is 0.) */ -import { ByteReader, InvalidFieldError } from './reader' +import { ByteReader, InvalidFieldError } from './reader.ts' /** Hash blocks in the file, one per possible value of the name hash. */ export const ANIMDATA_BLOCK_COUNT = 256 diff --git a/src/formats/bitstream.ts b/src/formats/bitstream.ts index 2567433..15a72cc 100644 --- a/src/formats/bitstream.ts +++ b/src/formats/bitstream.ts @@ -1,4 +1,4 @@ -import { InvalidFieldError, TruncatedDataError } from "./reader"; +import { InvalidFieldError, TruncatedDataError } from "./reader.ts"; /** * Least-significant-bit-first bit reader for Diablo II's packed sprite diff --git a/src/formats/cel.ts b/src/formats/cel.ts index bbef686..d2373cc 100644 --- a/src/formats/cel.ts +++ b/src/formats/cel.ts @@ -54,7 +54,7 @@ const DEFAULT_MAX_FRAMES = 20000 /** Default guard for one frame (a 4096×4096 indexed frame). */ const DEFAULT_MAX_FRAME_PIXELS = 1 << 24 -import { requireBytes } from './reader' +import { requireBytes } from './reader.ts' /** Read a little-endian uint32. */ function u32(data: Uint8Array, at: number): number { diff --git a/src/formats/cof.ts b/src/formats/cof.ts index a4617b2..12c85e1 100644 --- a/src/formats/cof.ts +++ b/src/formats/cof.ts @@ -25,7 +25,7 @@ * is exactly why a renderer must resolve them through the layer records instead * of using them directly. That resolution is `cofLayerOrder`. */ -import { ByteReader, FormatError, InvalidFieldError, TruncatedDataError } from './reader' +import { ByteReader, FormatError, InvalidFieldError, TruncatedDataError } from './reader.ts' /** Fixed part of the file, up to and including the animation-speed byte. */ const HEADER_SIZE = 25 diff --git a/src/formats/dc6.ts b/src/formats/dc6.ts index 21bf2d9..2e296eb 100644 --- a/src/formats/dc6.ts +++ b/src/formats/dc6.ts @@ -19,7 +19,7 @@ * entry unused), which is why frames carry a mask rather than relying on a * sentinel index. */ -import { FormatError, InvalidFieldError, TruncatedDataError, ByteReader, requireBytes } from './reader' +import { FormatError, InvalidFieldError, TruncatedDataError, ByteReader, requireBytes } from './reader.ts' import type { SpriteFrame, SpriteGroup, SpriteSheet } from './sprite.ts' /** Byte offset of the file header, and its size. */ diff --git a/src/formats/ds1.ts b/src/formats/ds1.ts index d8c425a..0b2fe25 100644 --- a/src/formats/ds1.ts +++ b/src/formats/ds1.ts @@ -1,4 +1,4 @@ -import { CelError } from "./sprite"; +import { CelError } from "./sprite.ts"; /** * Diablo II `.ds1` map decoder. @@ -15,7 +15,7 @@ import { CelError } from "./sprite"; * versions below 4. Both are encoded here exactly as the reference decoder * reads them, including the pre-7 orientation lookup table. */ -import { FormatError, InvalidFieldError, TruncatedDataError } from './reader' +import { FormatError, InvalidFieldError, TruncatedDataError } from './reader.ts' /** Pre-7 maps store orientations through this lookup rather than directly. */ const LEGACY_DIRECTION_LOOKUP = [ diff --git a/src/formats/dt1.ts b/src/formats/dt1.ts index a610177..8e06c57 100644 --- a/src/formats/dt1.ts +++ b/src/formats/dt1.ts @@ -1,6 +1,6 @@ -import { CelError } from "./sprite"; +import { CelError } from "./sprite.ts"; -import { FormatError, InvalidFieldError, TruncatedDataError, requireBytes } from './reader' +import { FormatError, InvalidFieldError, TruncatedDataError, requireBytes } from './reader.ts' /** Bytes of the fixed header before the tile count. */ const HEADER_PREFIX = 8 diff --git a/src/formats/pal.ts b/src/formats/pal.ts index cacbb32..f04db35 100644 --- a/src/formats/pal.ts +++ b/src/formats/pal.ts @@ -1,4 +1,4 @@ -import { FormatError } from "./reader"; +import { FormatError } from "./reader.ts"; /** diff --git a/src/formats/pcx.ts b/src/formats/pcx.ts index fc2ec63..3d07cde 100644 --- a/src/formats/pcx.ts +++ b/src/formats/pcx.ts @@ -7,7 +7,7 @@ * the trailing 769 bytes (0x0C marker + 768 bytes of VGA triples). */ -import { ByteReader, FormatError, InvalidFieldError, TruncatedDataError } from './reader' +import { ByteReader, FormatError, InvalidFieldError, TruncatedDataError } from './reader.ts' /** One decoded image, ready to blit. */ export interface PcxImage { diff --git a/src/formats/pl2.ts b/src/formats/pl2.ts index 1b3a4ee..1b53ee5 100644 --- a/src/formats/pl2.ts +++ b/src/formats/pl2.ts @@ -1,4 +1,4 @@ -import { ByteReader, FormatError, TruncatedDataError } from "./reader"; +import { ByteReader, FormatError, TruncatedDataError } from "./reader.ts"; /** * Diablo II `.pl2` palette+transform decoder. diff --git a/src/formats/tbl.ts b/src/formats/tbl.ts index b23be5a..a3ec6f8 100644 --- a/src/formats/tbl.ts +++ b/src/formats/tbl.ts @@ -1,4 +1,4 @@ -import { requireBytes } from './reader' +import { requireBytes } from './reader.ts' /** * Diablo II `.tbl` string-table decoder (hash-bucket layout). * diff --git a/src/game/act-variants.ts b/src/game/act-variants.ts index 461acf9..ca470cc 100644 --- a/src/game/act-variants.ts +++ b/src/game/act-variants.ts @@ -79,11 +79,15 @@ function normalizeMember(path: string): string { */ export class WorldVariants { private readonly pinned = new Map() + private readonly index: VariantIndex + private readonly seed: number constructor( - private readonly index: VariantIndex, - private readonly seed: number = WORLD_VARIANT_SEED, + index: VariantIndex, + seed: number = WORLD_VARIANT_SEED, ) { + this.index = index + this.seed = seed for (const [act, layouts] of Object.entries(index.actLayouts ?? {})) { if (layouts.length === 0) throw new Error(`actLayouts[${act}] is empty`) layouts.forEach((layout, at) => { diff --git a/src/game/d2-rng.ts b/src/game/d2-rng.ts index b566ccf..7184a30 100644 --- a/src/game/d2-rng.ts +++ b/src/game/d2-rng.ts @@ -175,8 +175,9 @@ export class D2Rng { * Draw an integer in an inclusive range `[min, max]`. */ int(min: number, max: number): number { - const lo = Math.ceil(Math.min(min, max)) - const hi = Math.floor(Math.max(min, max)) + if (max <= min) return Math.floor(min) + const lo = Math.ceil(min) + const hi = Math.floor(max) const span = hi - lo + 1 if (span <= 0) return lo return lo + this.rand(span) diff --git a/src/game/drlg/drlg-map.ts b/src/game/drlg/drlg-map.ts index 5db282b..5e545c9 100644 --- a/src/game/drlg/drlg-map.ts +++ b/src/game/drlg/drlg-map.ts @@ -529,6 +529,15 @@ export function buildDrlgLevelMap(input: DrlgLevelInput, ctx: DrlgMapContext): D for (let slot = DS1_OBJPRESET_SLOTS - 1; slot >= 0; slot -= 1) { objSlotOf.set(DRLGPRESET_GetObjectIndexFromObjPreset(nAct, slot), slot) // lowest slot wins } + const crossActObjOf = new Map() + for (let a = 4; a >= 0; a -= 1) { + for (let slot = DS1_OBJPRESET_SLOTS - 1; slot >= 0; slot -= 1) { + const objIdx = DRLGPRESET_GetObjectIndexFromObjPreset(a, slot) + if (objIdx >= 0 && objIdx !== OBJECT_CLASS_INVALID_PRESET) { + crossActObjOf.set(objIdx, { act: a + 1, slot }) + } + } + } const monSection = DATATBLS_GetMonPresetTxtActSection(ctx.tables, nAct) if (monSection === null) throw new Error(`level ${levelId}: MonPreset has no act ${nAct + 1} section`) const nMonStats = ctx.tables.nMonStatsTxtRecordCount @@ -574,7 +583,15 @@ export function buildDrlgLevelMap(input: DrlgLevelInput, ctx: DrlgMapContext): D if (u.index === OBJECT_CLASS_INVALID_PRESET) { skippedInvalidObjects += 1; break } if (u.index < 0) throw new Error(`level ${levelId}: object unit with class ${u.index}`) const slot = objSlotOf.get(u.index) - objects.push({ type: DS1_TYPE_OBJECT, id: slot ?? u.index + DS1_OBJPRESET_SLOTS, x: u.subX, y: u.subY, flags: u.spawned }) + const crossObj = slot === undefined ? crossActObjOf.get(u.index) : undefined + objects.push({ + type: DS1_TYPE_OBJECT, + id: slot ?? crossObj?.slot ?? u.index + DS1_OBJPRESET_SLOTS, + x: u.subX, + y: u.subY, + flags: u.spawned, + ...(crossObj !== undefined ? { act: crossObj.act } : {}), + }) nObjects += 1 // Throne of Destruction (131, wthrone.ds1) connects to The Worldstone Chamber (132, Vis1=132, // Warp1=82) via the preset object BaalsPortal (Objects.txt row 563, "The Worldstone Chamber") @@ -961,8 +978,9 @@ export function drlgLevelInputFromDump(dump: DrlgDump, levelId: number, tables: for (const room of level.rooms) { if (room.preset) { const prest = DATATBLS_GetLvlPrestTxtRecord(tables, room.preset.prest) - if (prest.dwAnimate !== 0 && prest.nAnimSpeed > 0) { - animSpeed = prest.nAnimSpeed + const speed = prest.nAnimSpeed > 0 ? prest.nAnimSpeed : prest.dwAnimate + if (speed > 0) { + animSpeed = speed break } } diff --git a/src/game/drlg/drlg-preset.ts b/src/game/drlg/drlg-preset.ts index c26d838..75e1473 100644 --- a/src/game/drlg/drlg-preset.ts +++ b/src/game/drlg/drlg-preset.ts @@ -170,10 +170,14 @@ export function DRLGPRESET_MapTileType(nId: number): number { /** `int32_t* pData` walking over a DS1 image (unaligned little-endian reads, bounds-checked). */ class Ds1Cursor { + private readonly bytes: Uint8Array + private readonly path: string private readonly view: DataView offset: number - constructor(private readonly bytes: Uint8Array, offset: number, private readonly path: string) { + constructor(bytes: Uint8Array, offset: number, path: string) { + this.bytes = bytes + this.path = path this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) this.offset = offset } diff --git a/src/game/items.ts b/src/game/items.ts index 7a42b09..037493f 100644 --- a/src/game/items.ts +++ b/src/game/items.ts @@ -186,41 +186,44 @@ export interface ItemFlagsDecoded { } /** Location / storage mode of the item in D2 bitstream (3 bits). */ -export enum ItemMode { - STORED = 0, // In inventory, stash, or Horadric Cube - EQUIPPED = 1, // Equipped on character body - BELT = 2, // Slotted in potion belt - GROUND = 3, // Dropped on the ground - CURSOR = 4, // Held on mouse cursor - DROPPING = 5, // Currently in drop motion - SOCKETED = 6, // Socketed into a parent item -} +export type ItemMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 +export const ItemMode = { + STORED: 0 as ItemMode, // In inventory, stash, or Horadric Cube + EQUIPPED: 1 as ItemMode, // Equipped on character body + BELT: 2 as ItemMode, // Slotted in potion belt + GROUND: 3 as ItemMode, // Dropped on the ground + CURSOR: 4 as ItemMode, // Held on mouse cursor + DROPPING: 5 as ItemMode, // Currently in drop motion + SOCKETED: 6 as ItemMode, // Socketed into a parent item +} as const /** Body equipment slot (4 bits). */ -export enum EquippedSlot { - NONE = 0, - HEAD = 1, - NECK = 2, - TORSO = 3, - RIGHT_HAND = 4, - LEFT_HAND = 5, - RIGHT_FINGER = 6, - LEFT_FINGER = 7, - WAIST = 8, - FEET = 9, - HANDS = 10, - ALT_RIGHT_HAND = 11, - ALT_LEFT_HAND = 12, -} +export type EquippedSlot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 +export const EquippedSlot = { + NONE: 0 as EquippedSlot, + HEAD: 1 as EquippedSlot, + NECK: 2 as EquippedSlot, + TORSO: 3 as EquippedSlot, + RIGHT_HAND: 4 as EquippedSlot, + LEFT_HAND: 5 as EquippedSlot, + RIGHT_FINGER: 6 as EquippedSlot, + LEFT_FINGER: 7 as EquippedSlot, + WAIST: 8 as EquippedSlot, + FEET: 9 as EquippedSlot, + HANDS: 10 as EquippedSlot, + ALT_RIGHT_HAND: 11 as EquippedSlot, + ALT_LEFT_HAND: 12 as EquippedSlot, +} as const /** Container / storage page (3 bits). */ -export enum StoragePage { - INVENTORY = 0, - EQUIPPED = 1, - BELT = 2, - CUBE = 4, - STASH = 5, -} +export type StoragePage = 0 | 1 | 2 | 4 | 5 +export const StoragePage = { + INVENTORY: 0 as StoragePage, + EQUIPPED: 1 as StoragePage, + BELT: 2 as StoragePage, + CUBE: 4 as StoragePage, + STASH: 5 as StoragePage, +} as const /** Item location and positioning structure. */ export interface ItemLocation { @@ -241,17 +244,18 @@ export interface ItemLocation { } /** Quality tier of the item (4 bits). */ -export enum ItemQuality { - LOW = 1, // Inferior / Low Quality / Cracked / Crude / Damaged - NORMAL = 2, // Normal base - SUPERIOR = 3, // Superior / High Quality - MAGIC = 4, // Magic - SET = 5, // Set - RARE = 6, // Rare - UNIQUE = 7, // Unique - CRAFTED = 8, // Crafted - TEMPERED = 9, // Tempered (expansion prototype remnant) -} +export type ItemQuality = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +export const ItemQuality = { + LOW: 1 as ItemQuality, // Inferior / Low Quality / Cracked / Crude / Damaged + NORMAL: 2 as ItemQuality, // Normal base + SUPERIOR: 3 as ItemQuality, // Superior / High Quality + MAGIC: 4 as ItemQuality, // Magic + SET: 5 as ItemQuality, // Set + RARE: 6 as ItemQuality, // Rare + UNIQUE: 7 as ItemQuality, // Unique + CRAFTED: 8 as ItemQuality, // Crafted + TEMPERED: 9 as ItemQuality, // Tempered (expansion prototype remnant) +} as const /** One affix slot for Rare / Crafted items (1 bit present + 11-bit id). */ export interface ItemAffixSlot { diff --git a/src/game/treasure-class.ts b/src/game/treasure-class.ts index bd3ce1c..0d020ff 100644 --- a/src/game/treasure-class.ts +++ b/src/game/treasure-class.ts @@ -195,14 +195,28 @@ export interface TreasureClassTable { * Concrete implementation of `TreasureClassTable`. */ class TreasureClassTableImpl implements TreasureClassTable { + readonly all: TreasureClassNode[] + readonly byName: Map + readonly byGroup: Map + readonly danglingReferences: ReadonlySet + readonly autoTCDanglingReferences: ReadonlySet + readonly baseItemDanglingReferences: ReadonlySet + constructor( - readonly all: TreasureClassNode[], - readonly byName: Map, - readonly byGroup: Map, - readonly danglingReferences: ReadonlySet, - readonly autoTCDanglingReferences: ReadonlySet, - readonly baseItemDanglingReferences: ReadonlySet, - ) {} + all: TreasureClassNode[], + byName: Map, + byGroup: Map, + danglingReferences: ReadonlySet, + autoTCDanglingReferences: ReadonlySet, + baseItemDanglingReferences: ReadonlySet, + ) { + this.all = all + this.byName = byName + this.byGroup = byGroup + this.danglingReferences = danglingReferences + this.autoTCDanglingReferences = autoTCDanglingReferences + this.baseItemDanglingReferences = baseItemDanglingReferences + } get(name: string): TreasureClassNode | undefined { return this.byName.get(name) diff --git a/src/mpq/adpcm.ts b/src/mpq/adpcm.ts index c964060..3d26dc8 100644 --- a/src/mpq/adpcm.ts +++ b/src/mpq/adpcm.ts @@ -1,4 +1,4 @@ -import { NEXT_STEP_TABLE, STEP_SIZE_TABLE } from './adpcm-tables.js'; +import { NEXT_STEP_TABLE, STEP_SIZE_TABLE } from './adpcm-tables.ts'; export class AdpcmError extends Error { constructor(message: string) { diff --git a/src/mpq/decompress.ts b/src/mpq/decompress.ts index 14eed56..b359989 100644 --- a/src/mpq/decompress.ts +++ b/src/mpq/decompress.ts @@ -12,9 +12,9 @@ * `implode.ts` and is called here. */ -import { ImplodeError, explode } from './implode.js'; -import { decompressHuffman } from './huffman.js'; -import { decompressAdpcm } from './adpcm.js'; +import { ImplodeError, explode } from './implode.ts'; +import { decompressHuffman } from './huffman.ts'; +import { decompressAdpcm } from './adpcm.ts'; /** Codec bit: adaptive Huffmann (FGK variant). */ export const COMPRESSION_HUFFMANN = 0x01 diff --git a/src/mpq/huffman.ts b/src/mpq/huffman.ts index d9c90a4..ea4516b 100644 --- a/src/mpq/huffman.ts +++ b/src/mpq/huffman.ts @@ -4,7 +4,7 @@ export class HuffmanError extends Error { this.name = 'HuffmanError'; } } -import { HUFFMAN_WEIGHT_TABLES } from './huffman-tables.js'; +import { HUFFMAN_WEIGHT_TABLES } from './huffman-tables.ts'; export const HUFF_DECOMPRESS_ERROR = 0x1FF; @@ -74,10 +74,11 @@ class InputStream { } } -enum InsertPoint { - InsertAfter = 1, - InsertBefore = 2 -} +const InsertPoint = { + InsertAfter: 1, + InsertBefore: 2, +} as const; +type InsertPoint = (typeof InsertPoint)[keyof typeof InsertPoint]; class THuffmanTree { listHead: THuffmanTreeItem; diff --git a/src/scene/frontend/scene-state.ts b/src/scene/frontend/scene-state.ts index d95f295..e7439a7 100644 --- a/src/scene/frontend/scene-state.ts +++ b/src/scene/frontend/scene-state.ts @@ -250,6 +250,7 @@ export const state: ActSceneState = { if (typeof window !== 'undefined') { window.__d2webAct = state window.__d2webActs = state + ;(window as unknown as { __d2Scene?: ActSceneState }).__d2Scene = state } export function monsterToTargetHudInfo(monster: Monster): MonsterTargetHudInfo { diff --git a/tests/animated-tiles-speed.test.ts b/tests/animated-tiles-speed.test.ts index 072505b..274d725 100644 --- a/tests/animated-tiles-speed.test.ts +++ b/tests/animated-tiles-speed.test.ts @@ -1,3 +1,5 @@ +import { existsSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { describe, expect, test } from 'vitest' import { DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS, @@ -143,4 +145,25 @@ describe('Animated Tile Speed from LvlPrest.txt Animate Column (Issue #85)', () expect(tile.currentFrameIndex).toBe(2) }) }) + + describe('Baked Act 4 Lava and Act 5 Hell portal scenes preserve animSpeed = 1 (10240ms)', () => { + const lavaScenes = [ + 'samples/d2-packs/act4/107-act-4-lava-1-var1/scene.json', + 'samples/d2-packs/act4/108-act-4-diablo-1-var1/scene.json', + 'samples/d2-packs/act5/125-act-5-hell-1-var1/scene.json', + 'samples/d2-packs/act5/126-act-5-hell-2-var1/scene.json', + 'samples/d2-packs/act5/127-act-5-hell-3-var1/scene.json', + ] + + test.each(lavaScenes)('%s has animSpeed === 1 and frameDurationMs === 10240', relPath => { + const fullPath = resolve(process.cwd(), relPath) + expect(existsSync(fullPath)).toBe(true) + const scene = JSON.parse(readFileSync(fullPath, 'utf8')) as { + animSpeed?: number + frameDurationMs?: number + } + expect(scene.animSpeed).toBe(1) + expect(scene.frameDurationMs).toBe(10240) + }) + }) }) diff --git a/tests/d2-rng.test.ts b/tests/d2-rng.test.ts index e422a6f..8ec022d 100644 --- a/tests/d2-rng.test.ts +++ b/tests/d2-rng.test.ts @@ -310,7 +310,14 @@ describe("1.13c Diablo II PRNG and Per-Unit Seed (D2Common!6FD510B0 / D2Game!6FC expect(v).toBeGreaterThanOrEqual(5) expect(v).toBeLessThanOrEqual(10) } + const loBeforeDegenerate = rng.lo + const hiBeforeDegenerate = rng.hi expect(rng.int(7, 7)).toBe(7) + expect(rng.lo).toBe(loBeforeDegenerate) + expect(rng.hi).toBe(hiBeforeDegenerate) + expect(rng.int(10, 5)).toBe(10) + expect(rng.lo).toBe(loBeforeDegenerate) + expect(rng.hi).toBe(hiBeforeDegenerate) const r = rng.range(10, 20) expect(r).toBeGreaterThanOrEqual(10)