diablo2-web/tests/warp-tiles.test.ts

131 lines
4.4 KiB
TypeScript

/**
* The warp-tile scanner.
*
* The important behaviour is not "finds a tile" but "groups a multi-tile
* staircase into one warp and keeps slots apart", because getting either wrong
* puts a level's exit in the wrong place without failing anything else.
*/
import { describe, expect, it } from 'vitest'
import { findWarpTiles } from '../src/game/warp-tiles.ts'
import type { Ds1, Ds1Cell, Ds1Wall } from '../src/formats/ds1.ts'
/**
* A wall entry.
*
* @param type - DS1 wall type; 10 and 11 are the special markers.
* @param style - the marker's style, which for a warp is the `Warp0..7` slot.
* @param sequence - the tile index within a multi-tile marker.
* @returns the wall.
*/
function wall(type: number, style: number, sequence = 0): Ds1Wall {
return { prop1: 0, sequence, style, type, unknown1: 0, unknown2: 0, hidden: false }
}
/**
* An empty map with walls placed at the given cells.
*
* @param width - map width in cells.
* @param height - map height in cells.
* @param placements - what to put where.
* @returns the map.
*/
function mapWith(
width: number,
height: number,
placements: readonly { x: number; y: number; wall: Ds1Wall }[],
): 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: [], shadows: [], substitutions: [] })
}
cells.push(row)
}
for (const placement of placements) {
const cell = cells[placement.y]![placement.x]!
;(cell.walls as Ds1Wall[]).push(placement.wall)
}
return {
version: 18,
width,
height,
act: 1,
substitutionType: 0,
wallLayers: 1,
floorLayers: 1,
cells,
objects: [],
groups: [],
files: [],
} as unknown as Ds1
}
describe('findWarpTiles', () => {
it('finds nothing in a map with no markers', () => {
expect(findWarpTiles(mapWith(4, 4, [{ x: 1, y: 1, wall: wall(1, 0) }]))).toEqual([])
})
it('reads the slot from the style, for both special wall types', () => {
const found = findWarpTiles(mapWith(8, 8, [
{ x: 1, y: 1, wall: wall(10, 3) },
{ x: 5, y: 5, wall: wall(11, 6) },
]))
expect(found.map(tile => tile.slot)).toEqual([3, 6])
})
it('collapses a two-tile staircase into one warp at its centre', () => {
// This is the shape almost every warp in the game has: `LvlWarp.Tiles` is 2
// and the DS1 marks both cells with a rising sequence.
const found = findWarpTiles(mapWith(8, 8, [
{ x: 3, y: 4, wall: wall(11, 1, 0) },
{ x: 4, y: 4, wall: wall(11, 1, 1) },
]))
expect(found).toHaveLength(1)
expect(found[0]).toMatchObject({ slot: 1, cellX: 3, cellY: 4, tiles: 2, minCellX: 3, minCellY: 4 })
})
it('collapses a four-tile staircase too', () => {
const found = findWarpTiles(mapWith(8, 8, [
{ x: 2, y: 2, wall: wall(10, 0, 0) },
{ x: 3, y: 2, wall: wall(10, 0, 1) },
{ x: 2, y: 3, wall: wall(10, 0, 2) },
{ x: 3, y: 3, wall: wall(10, 0, 3) },
]))
expect(found).toHaveLength(1)
expect(found[0]).toMatchObject({ slot: 0, tiles: 4, cellX: 2, cellY: 2 })
})
it('keeps two staircases of the same slot apart when they are far apart', () => {
// A generator can stamp the same stair piece twice; the caller needs both
// so it can choose, rather than silently averaging them into a wall.
const found = findWarpTiles(mapWith(20, 20, [
{ x: 1, y: 1, wall: wall(10, 2) },
{ x: 15, y: 15, wall: wall(10, 2) },
]))
expect(found).toHaveLength(2)
expect(found.map(tile => [tile.cellX, tile.cellY])).toEqual([[1, 1], [15, 15]])
})
it('ignores markers outside the eight warp slots', () => {
// Styles 30..34 are town entry points and player start positions; there is
// no `Warp30`, so treating them as warps would invent destinations.
const found = findWarpTiles(mapWith(8, 8, [
{ x: 1, y: 1, wall: wall(10, 30) },
{ x: 2, y: 2, wall: wall(10, 8) },
{ x: 3, y: 3, wall: wall(10, 7) },
]))
expect(found.map(tile => tile.slot)).toEqual([7])
})
it('sorts by slot then position, so a bake is reproducible', () => {
const found = findWarpTiles(mapWith(20, 20, [
{ x: 10, y: 10, wall: wall(10, 4) },
{ x: 1, y: 1, wall: wall(10, 4) },
{ x: 5, y: 5, wall: wall(11, 0) },
]))
expect(found.map(tile => [tile.slot, tile.cellX, tile.cellY]))
.toEqual([[0, 5, 5], [4, 1, 1], [4, 10, 10]])
})
})