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
|
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)
|
const objectFiles = objectPages.encode(0)
|
||||||
|
|
|
||||||
|
|
@ -240,12 +240,22 @@ export interface MonsterResult {
|
||||||
export type EntityResult = CharResult | MonsterResult
|
export type EntityResult = CharResult | MonsterResult
|
||||||
|
|
||||||
class EntityWorkerContext {
|
class EntityWorkerContext {
|
||||||
|
public readonly archives: MountedArchives
|
||||||
|
public readonly animData: any
|
||||||
|
public readonly monstats2: any
|
||||||
|
public readonly palettes: Record<number, Uint8Array>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly archives: MountedArchives,
|
archives: MountedArchives,
|
||||||
public readonly animData: any,
|
animData: any,
|
||||||
public readonly monstats2: any,
|
monstats2: any,
|
||||||
public readonly palettes: Record<number, Uint8Array>,
|
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> {
|
static async create(archiveDir: string, outDir: string): Promise<EntityWorkerContext> {
|
||||||
const archives = new MountedArchives()
|
const archives = new MountedArchives()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { decompressSector, COMPRESSION_ADPCM_MONO } from '../src/mpq/decompress.js';
|
import { decompressSector, COMPRESSION_ADPCM_MONO } from '../src/mpq/decompress.ts';
|
||||||
import { AudioManager } from '../src/audio/manager.js';
|
import { AudioManager } from '../src/audio/manager.ts';
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
console.log("Verifying Audio dependencies...");
|
console.log("Verifying Audio dependencies...");
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
* comparing one file first-wins against the other last-wins. Comparing like for
|
* comparing one file first-wins against the other last-wins. Comparing like for
|
||||||
* like, the difference count is 0.)
|
* 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. */
|
/** Hash blocks in the file, one per possible value of the name hash. */
|
||||||
export const ANIMDATA_BLOCK_COUNT = 256
|
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
|
* 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). */
|
/** Default guard for one frame (a 4096×4096 indexed frame). */
|
||||||
const DEFAULT_MAX_FRAME_PIXELS = 1 << 24
|
const DEFAULT_MAX_FRAME_PIXELS = 1 << 24
|
||||||
|
|
||||||
import { requireBytes } from './reader'
|
import { requireBytes } from './reader.ts'
|
||||||
|
|
||||||
/** Read a little-endian uint32. */
|
/** Read a little-endian uint32. */
|
||||||
function u32(data: Uint8Array, at: number): number {
|
function u32(data: Uint8Array, at: number): number {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
* is exactly why a renderer must resolve them through the layer records instead
|
* is exactly why a renderer must resolve them through the layer records instead
|
||||||
* of using them directly. That resolution is `cofLayerOrder`.
|
* 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. */
|
/** Fixed part of the file, up to and including the animation-speed byte. */
|
||||||
const HEADER_SIZE = 25
|
const HEADER_SIZE = 25
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
* entry unused), which is why frames carry a mask rather than relying on a
|
* entry unused), which is why frames carry a mask rather than relying on a
|
||||||
* sentinel index.
|
* 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'
|
import type { SpriteFrame, SpriteGroup, SpriteSheet } from './sprite.ts'
|
||||||
|
|
||||||
/** Byte offset of the file header, and its size. */
|
/** 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.
|
* 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
|
* versions below 4. Both are encoded here exactly as the reference decoder
|
||||||
* reads them, including the pre-7 orientation lookup table.
|
* 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. */
|
/** Pre-7 maps store orientations through this lookup rather than directly. */
|
||||||
const LEGACY_DIRECTION_LOOKUP = [
|
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. */
|
/** Bytes of the fixed header before the tile count. */
|
||||||
const HEADER_PREFIX = 8
|
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).
|
* 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. */
|
/** One decoded image, ready to blit. */
|
||||||
export interface PcxImage {
|
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.
|
* 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).
|
* Diablo II `.tbl` string-table decoder (hash-bucket layout).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,15 @@ function normalizeMember(path: string): string {
|
||||||
*/
|
*/
|
||||||
export class WorldVariants<E extends VariantEntry> {
|
export class WorldVariants<E extends VariantEntry> {
|
||||||
private readonly pinned = new Map<number, number>()
|
private readonly pinned = new Map<number, number>()
|
||||||
|
private readonly index: VariantIndex<E>
|
||||||
|
private readonly seed: number
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly index: VariantIndex<E>,
|
index: VariantIndex<E>,
|
||||||
private readonly seed: number = WORLD_VARIANT_SEED,
|
seed: number = WORLD_VARIANT_SEED,
|
||||||
) {
|
) {
|
||||||
|
this.index = index
|
||||||
|
this.seed = seed
|
||||||
for (const [act, layouts] of Object.entries(index.actLayouts ?? {})) {
|
for (const [act, layouts] of Object.entries(index.actLayouts ?? {})) {
|
||||||
if (layouts.length === 0) throw new Error(`actLayouts[${act}] is empty`)
|
if (layouts.length === 0) throw new Error(`actLayouts[${act}] is empty`)
|
||||||
layouts.forEach((layout, at) => {
|
layouts.forEach((layout, at) => {
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,9 @@ export class D2Rng {
|
||||||
* Draw an integer in an inclusive range `[min, max]`.
|
* Draw an integer in an inclusive range `[min, max]`.
|
||||||
*/
|
*/
|
||||||
int(min: number, max: number): number {
|
int(min: number, max: number): number {
|
||||||
const lo = Math.ceil(Math.min(min, max))
|
if (max <= min) return Math.floor(min)
|
||||||
const hi = Math.floor(Math.max(min, max))
|
const lo = Math.ceil(min)
|
||||||
|
const hi = Math.floor(max)
|
||||||
const span = hi - lo + 1
|
const span = hi - lo + 1
|
||||||
if (span <= 0) return lo
|
if (span <= 0) return lo
|
||||||
return lo + this.rand(span)
|
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) {
|
for (let slot = DS1_OBJPRESET_SLOTS - 1; slot >= 0; slot -= 1) {
|
||||||
objSlotOf.set(DRLGPRESET_GetObjectIndexFromObjPreset(nAct, slot), slot) // lowest slot wins
|
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)
|
const monSection = DATATBLS_GetMonPresetTxtActSection(ctx.tables, nAct)
|
||||||
if (monSection === null) throw new Error(`level ${levelId}: MonPreset has no act ${nAct + 1} section`)
|
if (monSection === null) throw new Error(`level ${levelId}: MonPreset has no act ${nAct + 1} section`)
|
||||||
const nMonStats = ctx.tables.nMonStatsTxtRecordCount
|
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 === OBJECT_CLASS_INVALID_PRESET) { skippedInvalidObjects += 1; break }
|
||||||
if (u.index < 0) throw new Error(`level ${levelId}: object unit with class ${u.index}`)
|
if (u.index < 0) throw new Error(`level ${levelId}: object unit with class ${u.index}`)
|
||||||
const slot = objSlotOf.get(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
|
nObjects += 1
|
||||||
// Throne of Destruction (131, wthrone.ds1) connects to The Worldstone Chamber (132, Vis1=132,
|
// 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")
|
// 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) {
|
for (const room of level.rooms) {
|
||||||
if (room.preset) {
|
if (room.preset) {
|
||||||
const prest = DATATBLS_GetLvlPrestTxtRecord(tables, room.preset.prest)
|
const prest = DATATBLS_GetLvlPrestTxtRecord(tables, room.preset.prest)
|
||||||
if (prest.dwAnimate !== 0 && prest.nAnimSpeed > 0) {
|
const speed = prest.nAnimSpeed > 0 ? prest.nAnimSpeed : prest.dwAnimate
|
||||||
animSpeed = prest.nAnimSpeed
|
if (speed > 0) {
|
||||||
|
animSpeed = speed
|
||||||
break
|
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). */
|
/** `int32_t* pData` walking over a DS1 image (unaligned little-endian reads, bounds-checked). */
|
||||||
class Ds1Cursor {
|
class Ds1Cursor {
|
||||||
|
private readonly bytes: Uint8Array
|
||||||
|
private readonly path: string
|
||||||
private readonly view: DataView
|
private readonly view: DataView
|
||||||
offset: number
|
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.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
||||||
this.offset = offset
|
this.offset = offset
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,41 +186,44 @@ export interface ItemFlagsDecoded {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Location / storage mode of the item in D2 bitstream (3 bits). */
|
/** Location / storage mode of the item in D2 bitstream (3 bits). */
|
||||||
export enum ItemMode {
|
export type ItemMode = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||||
STORED = 0, // In inventory, stash, or Horadric Cube
|
export const ItemMode = {
|
||||||
EQUIPPED = 1, // Equipped on character body
|
STORED: 0 as ItemMode, // In inventory, stash, or Horadric Cube
|
||||||
BELT = 2, // Slotted in potion belt
|
EQUIPPED: 1 as ItemMode, // Equipped on character body
|
||||||
GROUND = 3, // Dropped on the ground
|
BELT: 2 as ItemMode, // Slotted in potion belt
|
||||||
CURSOR = 4, // Held on mouse cursor
|
GROUND: 3 as ItemMode, // Dropped on the ground
|
||||||
DROPPING = 5, // Currently in drop motion
|
CURSOR: 4 as ItemMode, // Held on mouse cursor
|
||||||
SOCKETED = 6, // Socketed into a parent item
|
DROPPING: 5 as ItemMode, // Currently in drop motion
|
||||||
}
|
SOCKETED: 6 as ItemMode, // Socketed into a parent item
|
||||||
|
} as const
|
||||||
|
|
||||||
/** Body equipment slot (4 bits). */
|
/** Body equipment slot (4 bits). */
|
||||||
export enum EquippedSlot {
|
export type EquippedSlot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
|
||||||
NONE = 0,
|
export const EquippedSlot = {
|
||||||
HEAD = 1,
|
NONE: 0 as EquippedSlot,
|
||||||
NECK = 2,
|
HEAD: 1 as EquippedSlot,
|
||||||
TORSO = 3,
|
NECK: 2 as EquippedSlot,
|
||||||
RIGHT_HAND = 4,
|
TORSO: 3 as EquippedSlot,
|
||||||
LEFT_HAND = 5,
|
RIGHT_HAND: 4 as EquippedSlot,
|
||||||
RIGHT_FINGER = 6,
|
LEFT_HAND: 5 as EquippedSlot,
|
||||||
LEFT_FINGER = 7,
|
RIGHT_FINGER: 6 as EquippedSlot,
|
||||||
WAIST = 8,
|
LEFT_FINGER: 7 as EquippedSlot,
|
||||||
FEET = 9,
|
WAIST: 8 as EquippedSlot,
|
||||||
HANDS = 10,
|
FEET: 9 as EquippedSlot,
|
||||||
ALT_RIGHT_HAND = 11,
|
HANDS: 10 as EquippedSlot,
|
||||||
ALT_LEFT_HAND = 12,
|
ALT_RIGHT_HAND: 11 as EquippedSlot,
|
||||||
}
|
ALT_LEFT_HAND: 12 as EquippedSlot,
|
||||||
|
} as const
|
||||||
|
|
||||||
/** Container / storage page (3 bits). */
|
/** Container / storage page (3 bits). */
|
||||||
export enum StoragePage {
|
export type StoragePage = 0 | 1 | 2 | 4 | 5
|
||||||
INVENTORY = 0,
|
export const StoragePage = {
|
||||||
EQUIPPED = 1,
|
INVENTORY: 0 as StoragePage,
|
||||||
BELT = 2,
|
EQUIPPED: 1 as StoragePage,
|
||||||
CUBE = 4,
|
BELT: 2 as StoragePage,
|
||||||
STASH = 5,
|
CUBE: 4 as StoragePage,
|
||||||
}
|
STASH: 5 as StoragePage,
|
||||||
|
} as const
|
||||||
|
|
||||||
/** Item location and positioning structure. */
|
/** Item location and positioning structure. */
|
||||||
export interface ItemLocation {
|
export interface ItemLocation {
|
||||||
|
|
@ -241,17 +244,18 @@ export interface ItemLocation {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Quality tier of the item (4 bits). */
|
/** Quality tier of the item (4 bits). */
|
||||||
export enum ItemQuality {
|
export type ItemQuality = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
|
||||||
LOW = 1, // Inferior / Low Quality / Cracked / Crude / Damaged
|
export const ItemQuality = {
|
||||||
NORMAL = 2, // Normal base
|
LOW: 1 as ItemQuality, // Inferior / Low Quality / Cracked / Crude / Damaged
|
||||||
SUPERIOR = 3, // Superior / High Quality
|
NORMAL: 2 as ItemQuality, // Normal base
|
||||||
MAGIC = 4, // Magic
|
SUPERIOR: 3 as ItemQuality, // Superior / High Quality
|
||||||
SET = 5, // Set
|
MAGIC: 4 as ItemQuality, // Magic
|
||||||
RARE = 6, // Rare
|
SET: 5 as ItemQuality, // Set
|
||||||
UNIQUE = 7, // Unique
|
RARE: 6 as ItemQuality, // Rare
|
||||||
CRAFTED = 8, // Crafted
|
UNIQUE: 7 as ItemQuality, // Unique
|
||||||
TEMPERED = 9, // Tempered (expansion prototype remnant)
|
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). */
|
/** One affix slot for Rare / Crafted items (1 bit present + 11-bit id). */
|
||||||
export interface ItemAffixSlot {
|
export interface ItemAffixSlot {
|
||||||
|
|
|
||||||
|
|
@ -195,14 +195,28 @@ export interface TreasureClassTable {
|
||||||
* Concrete implementation of `TreasureClassTable`.
|
* Concrete implementation of `TreasureClassTable`.
|
||||||
*/
|
*/
|
||||||
class TreasureClassTableImpl implements 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(
|
constructor(
|
||||||
readonly all: TreasureClassNode[],
|
all: TreasureClassNode[],
|
||||||
readonly byName: Map<string, TreasureClassNode>,
|
byName: Map<string, TreasureClassNode>,
|
||||||
readonly byGroup: Map<number, TreasureClassNode[]>,
|
byGroup: Map<number, TreasureClassNode[]>,
|
||||||
readonly danglingReferences: ReadonlySet<string>,
|
danglingReferences: ReadonlySet<string>,
|
||||||
readonly autoTCDanglingReferences: ReadonlySet<string>,
|
autoTCDanglingReferences: ReadonlySet<string>,
|
||||||
readonly baseItemDanglingReferences: 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 {
|
get(name: string): TreasureClassNode | undefined {
|
||||||
return this.byName.get(name)
|
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 {
|
export class AdpcmError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
* `implode.ts` and is called here.
|
* `implode.ts` and is called here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ImplodeError, explode } from './implode.js';
|
import { ImplodeError, explode } from './implode.ts';
|
||||||
import { decompressHuffman } from './huffman.js';
|
import { decompressHuffman } from './huffman.ts';
|
||||||
import { decompressAdpcm } from './adpcm.js';
|
import { decompressAdpcm } from './adpcm.ts';
|
||||||
|
|
||||||
/** Codec bit: adaptive Huffmann (FGK variant). */
|
/** Codec bit: adaptive Huffmann (FGK variant). */
|
||||||
export const COMPRESSION_HUFFMANN = 0x01
|
export const COMPRESSION_HUFFMANN = 0x01
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export class HuffmanError extends Error {
|
||||||
this.name = 'HuffmanError';
|
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;
|
export const HUFF_DECOMPRESS_ERROR = 0x1FF;
|
||||||
|
|
||||||
|
|
@ -74,10 +74,11 @@ class InputStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InsertPoint {
|
const InsertPoint = {
|
||||||
InsertAfter = 1,
|
InsertAfter: 1,
|
||||||
InsertBefore = 2
|
InsertBefore: 2,
|
||||||
}
|
} as const;
|
||||||
|
type InsertPoint = (typeof InsertPoint)[keyof typeof InsertPoint];
|
||||||
|
|
||||||
class THuffmanTree {
|
class THuffmanTree {
|
||||||
listHead: THuffmanTreeItem;
|
listHead: THuffmanTreeItem;
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,7 @@ export const state: ActSceneState = {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.__d2webAct = state
|
window.__d2webAct = state
|
||||||
window.__d2webActs = state
|
window.__d2webActs = state
|
||||||
|
;(window as unknown as { __d2Scene?: ActSceneState }).__d2Scene = state
|
||||||
}
|
}
|
||||||
|
|
||||||
export function monsterToTargetHudInfo(monster: Monster): MonsterTargetHudInfo {
|
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 { describe, expect, test } from 'vitest'
|
||||||
import {
|
import {
|
||||||
DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS,
|
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)
|
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).toBeGreaterThanOrEqual(5)
|
||||||
expect(v).toBeLessThanOrEqual(10)
|
expect(v).toBeLessThanOrEqual(10)
|
||||||
}
|
}
|
||||||
|
const loBeforeDegenerate = rng.lo
|
||||||
|
const hiBeforeDegenerate = rng.hi
|
||||||
expect(rng.int(7, 7)).toBe(7)
|
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)
|
const r = rng.range(10, 20)
|
||||||
expect(r).toBeGreaterThanOrEqual(10)
|
expect(r).toBeGreaterThanOrEqual(10)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue