fix(drlg): resolve M1 iteration 2 audit findings (animSpeed, crossActObjOf, strip-types, __d2Scene, D2Rng.int)
This commit is contained in:
parent
8ddfbd318a
commit
c2c1d280d5
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<number, Uint8Array>
|
||||
|
||||
constructor(
|
||||
public readonly archives: MountedArchives,
|
||||
public readonly animData: any,
|
||||
public readonly monstats2: any,
|
||||
public readonly palettes: Record<number, Uint8Array>,
|
||||
) {}
|
||||
archives: MountedArchives,
|
||||
animData: any,
|
||||
monstats2: any,
|
||||
palettes: Record<number, Uint8Array>,
|
||||
) {
|
||||
this.archives = archives
|
||||
this.animData = animData
|
||||
this.monstats2 = monstats2
|
||||
this.palettes = palettes
|
||||
}
|
||||
|
||||
static async create(archiveDir: string, outDir: string): Promise<EntityWorkerContext> {
|
||||
const archives = new MountedArchives()
|
||||
|
|
|
|||
|
|
@ -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...");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { FormatError } from "./reader";
|
||||
import { FormatError } from "./reader.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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ByteReader, FormatError, TruncatedDataError } from "./reader";
|
||||
import { ByteReader, FormatError, TruncatedDataError } from "./reader.ts";
|
||||
|
||||
/**
|
||||
* Diablo II `.pl2` palette+transform decoder.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { requireBytes } from './reader'
|
||||
import { requireBytes } from './reader.ts'
|
||||
/**
|
||||
* Diablo II `.tbl` string-table decoder (hash-bucket layout).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -79,11 +79,15 @@ function normalizeMember(path: string): string {
|
|||
*/
|
||||
export class WorldVariants<E extends VariantEntry> {
|
||||
private readonly pinned = new Map<number, number>()
|
||||
private readonly index: VariantIndex<E>
|
||||
private readonly seed: number
|
||||
|
||||
constructor(
|
||||
private readonly index: VariantIndex<E>,
|
||||
private readonly seed: number = WORLD_VARIANT_SEED,
|
||||
index: VariantIndex<E>,
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<number, { readonly act: number; readonly slot: number }>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -195,14 +195,28 @@ export interface TreasureClassTable {
|
|||
* Concrete implementation of `TreasureClassTable`.
|
||||
*/
|
||||
class TreasureClassTableImpl implements TreasureClassTable {
|
||||
readonly all: TreasureClassNode[]
|
||||
readonly byName: Map<string, TreasureClassNode>
|
||||
readonly byGroup: Map<number, TreasureClassNode[]>
|
||||
readonly danglingReferences: ReadonlySet<string>
|
||||
readonly autoTCDanglingReferences: ReadonlySet<string>
|
||||
readonly baseItemDanglingReferences: ReadonlySet<string>
|
||||
|
||||
constructor(
|
||||
readonly all: TreasureClassNode[],
|
||||
readonly byName: Map<string, TreasureClassNode>,
|
||||
readonly byGroup: Map<number, TreasureClassNode[]>,
|
||||
readonly danglingReferences: ReadonlySet<string>,
|
||||
readonly autoTCDanglingReferences: ReadonlySet<string>,
|
||||
readonly baseItemDanglingReferences: ReadonlySet<string>,
|
||||
) {}
|
||||
all: TreasureClassNode[],
|
||||
byName: Map<string, TreasureClassNode>,
|
||||
byGroup: Map<number, TreasureClassNode[]>,
|
||||
danglingReferences: ReadonlySet<string>,
|
||||
autoTCDanglingReferences: ReadonlySet<string>,
|
||||
baseItemDanglingReferences: ReadonlySet<string>,
|
||||
) {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue