131 lines
4.4 KiB
TypeScript
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]])
|
|
})
|
|
})
|