fix(drlg): 消除 pickVariant 45度斜向机械平铺条纹并纠正生成器虚构 Style (Issue #116)
- src/game/d2map.ts: 替换 pickVariant 中线性的 (seed + cellX) * cellY 乘法公式为 SplitMix64 空间坐标散列,消除 45 度斜向机械平铺条纹并解决 row 0 退化问题。 - src/game/wilderness-acts.ts: 确保 Mesa 关卡(104~106)峡谷地面始终使用 Mesa 真实的 style 10 地砖,杜绝仅在 Lava 存在的 style 20 引用。 - src/game/wilderness-siege.ts: 攻城木桩栅栏破坏缺口两端(type 7 pillar)改用 fence.dt1 style 2 真实存在的 sequence 10 与 11。 - tests/wilderness-siege-strips.test.ts: 更新攻城木桩断言,验证残破柱桩(type 7)匹配真实的 sequence 10 与 11。 - tests/variant-distribution.test.ts: 新增 64x64 网格变体均匀分布与对角无周期性断言,以及 Kurast、Mesa、Siege 的真实合法 DT1 样式断言。 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
370dcc05b1
commit
f111ba5a74
|
|
@ -302,7 +302,7 @@ function tileToFrame(tile: Dt1Tile): SpriteFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One tile of the merged pool. */
|
/** One tile of the merged pool. */
|
||||||
interface PoolTile {
|
export interface PoolTile {
|
||||||
readonly library: number
|
readonly library: number
|
||||||
readonly tile: number
|
readonly tile: number
|
||||||
/** Variant weight (`Dt1Tile.rarityFrameIndex`) used when picking per cell. */
|
/** Variant weight (`Dt1Tile.rarityFrameIndex`) used when picking per cell. */
|
||||||
|
|
@ -391,7 +391,7 @@ function mergeLibraries(libraries: readonly Dt1[]): TilePool {
|
||||||
* @param seed - per-level seed.
|
* @param seed - per-level seed.
|
||||||
* @returns the chosen tile, or undefined for an empty group.
|
* @returns the chosen tile, or undefined for an empty group.
|
||||||
*/
|
*/
|
||||||
function pickVariant(
|
export function pickVariant(
|
||||||
candidates: readonly PoolTile[] | undefined,
|
candidates: readonly PoolTile[] | undefined,
|
||||||
cellX: number,
|
cellX: number,
|
||||||
cellY: number,
|
cellY: number,
|
||||||
|
|
@ -399,12 +399,16 @@ function pickVariant(
|
||||||
): PoolTile | undefined {
|
): PoolTile | undefined {
|
||||||
if (candidates === undefined || candidates.length === 0) return undefined
|
if (candidates === undefined || candidates.length === 0) return undefined
|
||||||
if (candidates.length === 1) return candidates[0]
|
if (candidates.length === 1) return candidates[0]
|
||||||
let state = (BigInt(seed >>> 0) + BigInt(cellX >>> 0)) * BigInt(cellY >>> 0)
|
|
||||||
const mask = (1n << 64n) - 1n
|
const mask = (1n << 64n) - 1n
|
||||||
state &= mask
|
const x = BigInt(cellX >>> 0)
|
||||||
state = (state ^ (state << 13n)) & mask
|
const y = BigInt(cellY >>> 0)
|
||||||
state = (state ^ (state >> 17n)) & mask
|
const s = BigInt(seed >>> 0)
|
||||||
state = (state ^ (state << 5n)) & mask
|
let state = (s ^ (x * 0x9e3779b97f4a7c15n) ^ (y * 0xc6a4a7935bd1e995n)) & mask
|
||||||
|
state = (state + ((x << 32n) | y)) & mask
|
||||||
|
state = (state ^ (state >> 30n)) * 0xbf58476d1ce4e5b9n & mask
|
||||||
|
state = (state ^ (state >> 27n)) * 0x94d049bb133111ebn & mask
|
||||||
|
state = (state ^ (state >> 31n)) & mask
|
||||||
|
|
||||||
let total = 0
|
let total = 0
|
||||||
for (const candidate of candidates) total += Math.max(0, candidate.weight | 0)
|
for (const candidate of candidates) total += Math.max(0, candidate.weight | 0)
|
||||||
if (total === 0) return candidates[0]
|
if (total === 0) return candidates[0]
|
||||||
|
|
|
||||||
|
|
@ -661,8 +661,9 @@ export function initAct4OutdoorLevel(
|
||||||
}
|
}
|
||||||
rockyBridges += 1
|
rockyBridges += 1
|
||||||
} else {
|
} else {
|
||||||
// Boiling lava chasm for Act 4 - Lava, or rocky canyon floor for Act 4 - Mesa
|
// Boiling lava chasm for Act 4 - Lava, or rocky canyon floor for Act 4 - Mesa (style 10)
|
||||||
const isLava = request.levelTypeName === 'Act 4 - Lava'
|
const isMesa = request.levelTypeName === 'Act 4 - Mesa' || (request.levelId >= 104 && request.levelId <= 106)
|
||||||
|
const isLava = request.levelTypeName === 'Act 4 - Lava' && !isMesa
|
||||||
const chasmStyle = isLava ? 20 : 10
|
const chasmStyle = isLava ? 20 : 10
|
||||||
const chasmSeq = isLava ? 3 : 0
|
const chasmSeq = isLava ? 3 : 0
|
||||||
for (let dy = -2; dy <= 2; dy += 1) {
|
for (let dy = -2; dy <= 2; dy += 1) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* progression:
|
* progression:
|
||||||
* 1. Sequential 16x48 strip placement supporting dynamic variant and level selection.
|
* 1. Sequential 16x48 strip placement supporting dynamic variant and level selection.
|
||||||
* 2. Battlefield trenches (style: 5, prop1: 194) with cleared wall channels.
|
* 2. Battlefield trenches (style: 5, prop1: 194) with cleared wall channels.
|
||||||
* 3. Breached wooden barricades (style: 2, prop1: 129) with splintered end-caps (seq: 1/2)
|
* 3. Breached wooden barricades (style: 2, prop1: 129) with splintered end-caps (seq: 10/11)
|
||||||
* and traversable breach openings.
|
* and traversable breach openings.
|
||||||
* 4. Catapult defense platforms (style: 4, prop1: 2) with defensive parapet ramparts.
|
* 4. Catapult defense platforms (style: 4, prop1: 2) with defensive parapet ramparts.
|
||||||
* 5. Crumbled stone fort ruins interspersed between siege strips.
|
* 5. Crumbled stone fort ruins interspersed between siege strips.
|
||||||
|
|
@ -152,7 +152,7 @@ function stampTrench(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stamp a breached wooden barricade palisade (style: 2, prop1: 129).
|
* Stamp a breached wooden barricade palisade (style: 2, prop1: 129).
|
||||||
* Features splintered end-caps (sequence: 1 or 2) flanking a traversable breach opening.
|
* Features splintered end-caps (sequence: 10 or 11) flanking a traversable breach opening.
|
||||||
*/
|
*/
|
||||||
function stampBreachedBarricade(
|
function stampBreachedBarricade(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
|
|
@ -184,10 +184,10 @@ function stampBreachedBarricade(
|
||||||
})
|
})
|
||||||
breachFloorCount += 1
|
breachFloorCount += 1
|
||||||
} else if (y === breachY - 1 || y === breachY + breachHeight) {
|
} else if (y === breachY - 1 || y === breachY + breachHeight) {
|
||||||
// Splintered end-cap post at the breach edge (fence.dt1 type 7 pillar, seq 2)
|
// Splintered end-cap post at the breach edge (fence.dt1 type 7 pillar, seq 10 or 11)
|
||||||
setWall(canvas, x, y, {
|
setWall(canvas, x, y, {
|
||||||
style: 2,
|
style: 2,
|
||||||
sequence: 2,
|
sequence: rng.int(10, 11),
|
||||||
prop1: 129,
|
prop1: 129,
|
||||||
type: 7,
|
type: 7,
|
||||||
unknown1: 0,
|
unknown1: 0,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
import { pickVariant, type PoolTile } from '../src/game/d2map.ts'
|
||||||
|
import { generateWilderness, type WildernessPiece } from '../src/game/wilderness.ts'
|
||||||
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||||
|
import { ACT5_SIEGE_STRIP_NAMES } from '../src/game/wilderness-siege.ts'
|
||||||
|
|
||||||
|
function makeMockDs1(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
hasGapInWalls: boolean = false,
|
||||||
|
floorStyle: number = 1,
|
||||||
|
wallStyle: number = 1,
|
||||||
|
): Ds1 {
|
||||||
|
const cells: Ds1Cell[][] = []
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
const row: Ds1Cell[] = []
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
||||||
|
row.push({
|
||||||
|
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: wallStyle, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
|
floors: [{ prop1: 2, sequence: 0, style: floorStyle, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
|
shadows: [],
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cells.push(row)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: 18,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
act: 1,
|
||||||
|
substitutionType: 0,
|
||||||
|
wallLayers: 1,
|
||||||
|
floorLayers: 1,
|
||||||
|
cells,
|
||||||
|
objects: [],
|
||||||
|
npcPathOffset: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMockBorderPieces(actPrefix: string): WildernessPiece[] {
|
||||||
|
const borderDs1 = makeMockDs1(8, 8, false)
|
||||||
|
const openBorderDs1 = makeMockDs1(8, 8, true)
|
||||||
|
return [
|
||||||
|
{ name: `${actPrefix} - Border 1`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 2`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 3`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 4`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 5`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 6`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 7`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 8`, border: true, levels: [borderDs1] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMockStripDs1(width: number, height: number, tagSequence: number): Ds1 {
|
||||||
|
const cells: Ds1Cell[][] = []
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
const row: Ds1Cell[] = []
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
row.push({
|
||||||
|
walls: [],
|
||||||
|
floors: [{ prop1: 2, sequence: x === 0 && y === 0 ? tagSequence : 0, style: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
|
shadows: [],
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cells.push(row)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: 18,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
act: 5,
|
||||||
|
substitutionType: 0,
|
||||||
|
wallLayers: 1,
|
||||||
|
floorLayers: 1,
|
||||||
|
cells,
|
||||||
|
objects: [],
|
||||||
|
npcPathOffset: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockSiegePieces(): WildernessPiece[] {
|
||||||
|
return ACT5_SIEGE_STRIP_NAMES.map((name, idx) => ({
|
||||||
|
name,
|
||||||
|
border: false,
|
||||||
|
levels: [makeMockStripDs1(16, 48, 100 + idx)],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Variant Distribution & Valid DT1 Tile Styles (Issue #116)', () => {
|
||||||
|
describe('pickVariant 2D spatial coordinate hash and diagonal decorrelation', () => {
|
||||||
|
test('handles empty or single candidate groups', () => {
|
||||||
|
expect(pickVariant(undefined, 0, 0, 1)).toBeUndefined()
|
||||||
|
expect(pickVariant([], 0, 0, 1)).toBeUndefined()
|
||||||
|
|
||||||
|
const single: PoolTile = { library: 0, tile: 42, weight: 10, animated: false, frameIndex: 0 }
|
||||||
|
expect(pickVariant([single], 5, 10, 99)).toBe(single)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('distributes variants uniformly across 64x64 grid without diagonal periodicity', () => {
|
||||||
|
const candidates: PoolTile[] = [
|
||||||
|
{ library: 0, tile: 0, weight: 1, animated: false, frameIndex: 0 },
|
||||||
|
{ library: 0, tile: 1, weight: 1, animated: false, frameIndex: 0 },
|
||||||
|
{ library: 0, tile: 2, weight: 1, animated: false, frameIndex: 0 },
|
||||||
|
{ library: 0, tile: 3, weight: 1, animated: false, frameIndex: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const N = 64
|
||||||
|
const seed = 123456
|
||||||
|
const grid: number[][] = []
|
||||||
|
const counts = [0, 0, 0, 0]
|
||||||
|
|
||||||
|
for (let y = 0; y < N; y += 1) {
|
||||||
|
const row: number[] = []
|
||||||
|
for (let x = 0; x < N; x += 1) {
|
||||||
|
const picked = pickVariant(candidates, x, y, seed)
|
||||||
|
expect(picked).toBeDefined()
|
||||||
|
const tileIdx = picked!.tile
|
||||||
|
counts[tileIdx] += 1
|
||||||
|
row.push(tileIdx)
|
||||||
|
}
|
||||||
|
grid.push(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCells = N * N // 4096 cells
|
||||||
|
// 1. Uniformity: each of the 4 variants should receive ~25% of the total cells (between 20% and 30%)
|
||||||
|
for (let i = 0; i < 4; i += 1) {
|
||||||
|
const ratio = counts[i] / totalCells
|
||||||
|
expect(ratio).toBeGreaterThan(0.20)
|
||||||
|
expect(ratio).toBeLessThan(0.30)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Eliminate 45-degree diagonal periodicity:
|
||||||
|
// In 2.5D isometric projection, diagonal step (dx=1, dy=1) or (dx=1, dy=-1) projects to screen lines.
|
||||||
|
// With the old multiplicative formula (seed+x)*y, diagonal periodicity caused stripes.
|
||||||
|
// With a decorrelated 2D coordinate hash, the diagonal match rate is close to 25% (random chance).
|
||||||
|
let diagMatches1 = 0
|
||||||
|
let totalDiag1 = 0
|
||||||
|
for (let y = 0; y < N - 1; y += 1) {
|
||||||
|
for (let x = 0; x < N - 1; x += 1) {
|
||||||
|
if (grid[y][x] === grid[y + 1][x + 1]) diagMatches1 += 1
|
||||||
|
totalDiag1 += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let diagMatches2 = 0
|
||||||
|
let totalDiag2 = 0
|
||||||
|
for (let y = 1; y < N; y += 1) {
|
||||||
|
for (let x = 0; x < N - 1; x += 1) {
|
||||||
|
if (grid[y][x] === grid[y - 1][x + 1]) diagMatches2 += 1
|
||||||
|
totalDiag2 += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagRate1 = diagMatches1 / totalDiag1
|
||||||
|
const diagRate2 = diagMatches2 / totalDiag2
|
||||||
|
expect(diagRate1).toBeGreaterThan(0.20)
|
||||||
|
expect(diagRate1).toBeLessThan(0.30)
|
||||||
|
expect(diagRate2).toBeGreaterThan(0.20)
|
||||||
|
expect(diagRate2).toBeLessThan(0.30)
|
||||||
|
|
||||||
|
// 3. No degenerate rows or columns:
|
||||||
|
// The old formula produced state=0 for all cells where y=0 (row 0 was all 0s).
|
||||||
|
const row0Distinct = new Set(grid[0]).size
|
||||||
|
expect(row0Distinct).toBe(4)
|
||||||
|
|
||||||
|
const col0Distinct = new Set(grid.map(row => row[0])).size
|
||||||
|
expect(col0Distinct).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('respects candidate rarity weights', () => {
|
||||||
|
const candidates: PoolTile[] = [
|
||||||
|
{ library: 0, tile: 0, weight: 1, animated: false, frameIndex: 0 },
|
||||||
|
{ library: 0, tile: 1, weight: 3, animated: false, frameIndex: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const N = 64
|
||||||
|
let count0 = 0
|
||||||
|
let count1 = 0
|
||||||
|
for (let y = 0; y < N; y += 1) {
|
||||||
|
for (let x = 0; x < N; x += 1) {
|
||||||
|
const picked = pickVariant(candidates, x, y, 777)
|
||||||
|
if (picked?.tile === 0) count0 += 1
|
||||||
|
else count1 += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio1 = count1 / (count0 + count1)
|
||||||
|
// Theoretical ratio is 3/4 = 75%; allow +/- 5% margin
|
||||||
|
expect(ratio1).toBeGreaterThan(0.70)
|
||||||
|
expect(ratio1).toBeLessThan(0.80)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Kurast causeway valid stone floor styles (Level 82 & 79~81)', () => {
|
||||||
|
test('Kurast Causeway (82) uses valid stone floor style 20 and balustrade wall style 20', () => {
|
||||||
|
const groundDs1 = makeMockDs1(48, 16)
|
||||||
|
const pieces: WildernessPiece[] = [
|
||||||
|
{ name: 'Act 3 - Ground', border: false, levels: [groundDs1] },
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 82,
|
||||||
|
levelName: 'Kurast Causeway',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sizeX: 48,
|
||||||
|
sizeY: 16,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 82001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let foundCausewayFloor = false
|
||||||
|
let foundBalustradeWall = false
|
||||||
|
let foundInvalidStyle5 = false
|
||||||
|
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
for (const floor of cell.floors) {
|
||||||
|
if (floor.style === 5) foundInvalidStyle5 = true
|
||||||
|
if (floor.style === 20 && floor.prop1 === 194) foundCausewayFloor = true
|
||||||
|
}
|
||||||
|
for (const wall of cell.walls) {
|
||||||
|
if (wall.style === 20 && wall.prop1 === 129) foundBalustradeWall = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must NOT use fictional style 5 (not present in Kurast DT1s)
|
||||||
|
expect(foundInvalidStyle5).toBe(false)
|
||||||
|
// Must use valid Kurast stone floor style 20
|
||||||
|
expect(foundCausewayFloor).toBe(true)
|
||||||
|
// Must use valid Kurast balustrade wall style 20
|
||||||
|
expect(foundBalustradeWall).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Kurast Urban Districts (79 Lower Kurast) avenues and bridges use valid style 20', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 3')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 79,
|
||||||
|
levelName: 'Lower Kurast',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 79001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let foundAvenueFloor = false
|
||||||
|
let foundBridgeFloor = false
|
||||||
|
let foundInvalidStyle5 = false
|
||||||
|
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
for (const floor of cell.floors) {
|
||||||
|
if (floor.style === 5) foundInvalidStyle5 = true
|
||||||
|
if (floor.style === 20 && floor.prop1 === 194) foundAvenueFloor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(foundInvalidStyle5).toBe(false)
|
||||||
|
expect(foundAvenueFloor).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Mesa canyon and chasm valid tile styles (Levels 104, 105, 106)', () => {
|
||||||
|
test.each([
|
||||||
|
{ levelId: 104, levelName: 'Outer Steppes' },
|
||||||
|
{ levelId: 105, levelName: 'Plains of Despair' },
|
||||||
|
{ levelId: 106, levelName: 'City of the Damned' },
|
||||||
|
])('$levelName ($levelId) uses Mesa floor style 10 instead of Lava style 20', ({ levelId, levelName }) => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 4')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId,
|
||||||
|
levelName,
|
||||||
|
levelTypeName: 'Act 4 - Mesa',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: levelId * 1000 + 1,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let foundMesaChasmFloor = false
|
||||||
|
let foundMesaBridgeFloor = false
|
||||||
|
let foundLavaStyle20 = false
|
||||||
|
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
for (const floor of cell.floors) {
|
||||||
|
if (floor.style === 20) foundLavaStyle20 = true
|
||||||
|
if (floor.style === 10 && floor.prop1 === 2) {
|
||||||
|
if (floor.sequence === 0) foundMesaChasmFloor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mesa DT1s have no style 20 (only present in Lava.dt1)
|
||||||
|
expect(foundLavaStyle20).toBe(false)
|
||||||
|
// Valid Mesa chasm floor is style 10
|
||||||
|
expect(foundMesaChasmFloor).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Siege barricades valid fence sequence numbers (Level 110)', () => {
|
||||||
|
test('Bloody Foothills (110) barricades use fence style 2 with valid sequence numbers 10 and 11', () => {
|
||||||
|
const pieces = createMockSiegePieces()
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 110,
|
||||||
|
levelName: 'Bloody Foothills',
|
||||||
|
levelTypeName: 'Act 5 - Siege',
|
||||||
|
sizeX: 240,
|
||||||
|
sizeY: 48,
|
||||||
|
subType: -1,
|
||||||
|
subTheme: -1,
|
||||||
|
seed: 42,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let foundBarricadePalisade = false
|
||||||
|
let foundSplinteredEndCap = false
|
||||||
|
let foundInvalidSequence = false
|
||||||
|
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
for (const wall of cell.walls) {
|
||||||
|
if (wall.style === 2 && wall.prop1 === 129) {
|
||||||
|
// Intact palisade (type 1)
|
||||||
|
if (wall.type === 1) {
|
||||||
|
if (wall.sequence === 10) foundBarricadePalisade = true
|
||||||
|
else foundInvalidSequence = true
|
||||||
|
}
|
||||||
|
// Splintered end-cap post (type 7)
|
||||||
|
if (wall.type === 7) {
|
||||||
|
if (wall.sequence === 10 || wall.sequence === 11) foundSplinteredEndCap = true
|
||||||
|
else foundInvalidSequence = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(foundInvalidSequence).toBe(false)
|
||||||
|
expect(foundBarricadePalisade).toBe(true)
|
||||||
|
expect(foundSplinteredEndCap).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -246,7 +246,7 @@ describe("Act 5 Bloody Foothills Dynamic Siege Generation (DRLGOUTSIEGE)", () =>
|
||||||
)
|
)
|
||||||
if (barricadeWall) {
|
if (barricadeWall) {
|
||||||
foundBarricadeWalls += 1
|
foundBarricadeWalls += 1
|
||||||
if (barricadeWall.sequence === 1 || barricadeWall.sequence === 2) {
|
if (barricadeWall.type === 7 && (barricadeWall.sequence === 10 || barricadeWall.sequence === 11)) {
|
||||||
foundSplinteredEndCaps += 1
|
foundSplinteredEndCaps += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +254,7 @@ describe("Act 5 Bloody Foothills Dynamic Siege Generation (DRLGOUTSIEGE)", () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(foundBarricadeWalls).toBe(stats.barricadeWalls)
|
expect(foundBarricadeWalls).toBe(stats.barricadeWalls)
|
||||||
// Splintered end-caps (seq: 1/2) must flank the breaches
|
// Splintered end-caps (type: 7, seq: 10/11) must flank the breaches
|
||||||
expect(foundSplinteredEndCaps).toBeGreaterThan(0)
|
expect(foundSplinteredEndCaps).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue