535 lines
19 KiB
TypeScript
535 lines
19 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
getRoofBounds,
|
|
getCombinedRoofBounds,
|
|
isPointInRoofBounds,
|
|
boundsOverlap,
|
|
getPlayerBounds,
|
|
isPlayerOverlappingRoofBounds,
|
|
isPlayerInRoofCell,
|
|
isPlayerInRoofSubTile,
|
|
isPlayerUnderRoof,
|
|
stepRoofAlpha,
|
|
clusterRoofs,
|
|
RoofFadeController,
|
|
DEFAULT_MIN_ROOF_ALPHA,
|
|
DEFAULT_MAX_ROOF_ALPHA,
|
|
DEFAULT_ROOF_FADE_DURATION_MS,
|
|
DEFAULT_CLUSTER_CELL_DISTANCE,
|
|
type RoofDrawable,
|
|
} from '../src/render/roofs.ts'
|
|
|
|
/** Helper to create a minimal roof tile. */
|
|
function makeRoof(
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
cellX?: number,
|
|
cellY?: number,
|
|
roofHeight?: number,
|
|
wallType?: number,
|
|
): RoofDrawable {
|
|
return {
|
|
x,
|
|
y,
|
|
frame: { width, height },
|
|
...(cellX !== undefined ? { cellX } : {}),
|
|
...(cellY !== undefined ? { cellY } : {}),
|
|
...(roofHeight !== undefined ? { roofHeight } : {}),
|
|
...(wallType !== undefined ? { wallType } : {}),
|
|
}
|
|
}
|
|
|
|
describe('Roof Bounding Detection', () => {
|
|
it('computes correct bounds for a single roof tile', () => {
|
|
const roof = makeRoof(100, 200, 80, 60)
|
|
const bounds = getRoofBounds(roof)
|
|
expect(bounds).toEqual({
|
|
minX: 100,
|
|
minY: 200,
|
|
maxX: 180,
|
|
maxY: 260,
|
|
})
|
|
})
|
|
|
|
it('computes correct bounds with negative coordinates and offsets', () => {
|
|
const roof = makeRoof(-50, -30, 100, 70)
|
|
const bounds = getRoofBounds(roof)
|
|
expect(bounds).toEqual({
|
|
minX: -50,
|
|
minY: -30,
|
|
maxX: 50,
|
|
maxY: 40,
|
|
})
|
|
})
|
|
|
|
it('returns null for combined bounds of empty roof list', () => {
|
|
expect(getCombinedRoofBounds([])).toBeNull()
|
|
})
|
|
|
|
it('computes combined bounding box enclosing multiple roof tiles', () => {
|
|
const roofs = [
|
|
makeRoof(100, 150, 40, 40),
|
|
makeRoof(130, 180, 50, 30),
|
|
makeRoof(80, 200, 30, 60),
|
|
]
|
|
const combined = getCombinedRoofBounds(roofs)
|
|
expect(combined).toEqual({
|
|
minX: 80,
|
|
minY: 150,
|
|
maxX: 180, // 130 + 50
|
|
maxY: 260, // 80..110, 200 + 60
|
|
})
|
|
})
|
|
|
|
it('detects whether a 2D point is inside roof bounds', () => {
|
|
const bounds = { minX: 50, minY: 100, maxX: 150, maxY: 200 }
|
|
|
|
// Inside
|
|
expect(isPointInRoofBounds(100, 150, bounds)).toBe(true)
|
|
|
|
// Boundaries (inclusive)
|
|
expect(isPointInRoofBounds(50, 100, bounds)).toBe(true)
|
|
expect(isPointInRoofBounds(150, 200, bounds)).toBe(true)
|
|
|
|
// Outside
|
|
expect(isPointInRoofBounds(49, 150, bounds)).toBe(false)
|
|
expect(isPointInRoofBounds(151, 150, bounds)).toBe(false)
|
|
expect(isPointInRoofBounds(100, 99, bounds)).toBe(false)
|
|
expect(isPointInRoofBounds(100, 201, bounds)).toBe(false)
|
|
})
|
|
|
|
it('detects bounding box overlap and non-overlap', () => {
|
|
const boxA = { minX: 0, minY: 0, maxX: 100, maxY: 100 }
|
|
|
|
// Fully overlapping
|
|
expect(boundsOverlap(boxA, { minX: 20, minY: 20, maxX: 80, maxY: 80 })).toBe(true)
|
|
// Partially overlapping
|
|
expect(boundsOverlap(boxA, { minX: 50, minY: 50, maxX: 150, maxY: 150 })).toBe(true)
|
|
// Touching edge
|
|
expect(boundsOverlap(boxA, { minX: 100, minY: 0, maxX: 200, maxY: 100 })).toBe(true)
|
|
// Separated in X
|
|
expect(boundsOverlap(boxA, { minX: 101, minY: 0, maxX: 200, maxY: 100 })).toBe(false)
|
|
// Separated in Y
|
|
expect(boundsOverlap(boxA, { minX: 0, minY: 101, maxX: 100, maxY: 200 })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Player-Under-Roof Condition', () => {
|
|
const roof = makeRoof(100, 100, 80, 80, 5, 10)
|
|
|
|
describe('bounding box overlap', () => {
|
|
it('detects player point inside roof bounds', () => {
|
|
expect(isPlayerOverlappingRoofBounds({ x: 140, y: 140 }, roof)).toBe(true)
|
|
})
|
|
|
|
it('detects player sprite overlapping lower edge of roof', () => {
|
|
// Player feet at y=190, sprite extends upward to y=142, overlapping roof [100..180]
|
|
expect(isPlayerOverlappingRoofBounds({ x: 140, y: 190 }, roof)).toBe(true)
|
|
})
|
|
|
|
it('returns false when player is far away from roof', () => {
|
|
expect(isPlayerOverlappingRoofBounds({ x: 500, y: 500 }, roof)).toBe(false)
|
|
expect(isPlayerOverlappingRoofBounds({ x: 20, y: 50 }, roof)).toBe(false)
|
|
})
|
|
|
|
it('supports explicit player bounding box', () => {
|
|
expect(isPlayerOverlappingRoofBounds({ left: 110, top: 120, right: 130, bottom: 160 }, roof)).toBe(true)
|
|
expect(isPlayerOverlappingRoofBounds({ left: 300, top: 300, right: 320, bottom: 340 }, roof)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('cell and sub-tile coverage', () => {
|
|
it('matches player cell coordinates with roof tile cell', () => {
|
|
expect(isPlayerInRoofCell({ x: 5, y: 10 }, roof)).toBe(true)
|
|
expect(isPlayerInRoofCell({ x: 4, y: 10 }, roof)).toBe(false)
|
|
expect(isPlayerInRoofCell({ x: 5, y: 9 }, roof)).toBe(false)
|
|
})
|
|
|
|
it('matches player cell against an array of roof tiles', () => {
|
|
const roofTiles = [
|
|
makeRoof(0, 0, 80, 40, 5, 10),
|
|
makeRoof(0, 0, 80, 40, 5, 11),
|
|
makeRoof(0, 0, 80, 40, 6, 10),
|
|
]
|
|
expect(isPlayerInRoofCell({ x: 5, y: 11 }, roofTiles)).toBe(true)
|
|
expect(isPlayerInRoofCell({ x: 6, y: 10 }, roofTiles)).toBe(true)
|
|
expect(isPlayerInRoofCell({ x: 7, y: 12 }, roofTiles)).toBe(false)
|
|
})
|
|
|
|
it('detects player sub-tile coverage (5x5 sub-tiles per cell)', () => {
|
|
// Cell (5, 10) corresponds to sub-tiles [25..29, 50..54]
|
|
expect(isPlayerInRoofSubTile({ x: 25, y: 50 }, roof)).toBe(true)
|
|
expect(isPlayerInRoofSubTile({ x: 29, y: 54 }, roof)).toBe(true)
|
|
expect(isPlayerInRoofSubTile({ x: 27, y: 52 }, roof)).toBe(true)
|
|
|
|
// Adjacent sub-tile in cell (4, 10) or (5, 9)
|
|
expect(isPlayerInRoofSubTile({ x: 24, y: 50 }, roof)).toBe(false)
|
|
expect(isPlayerInRoofSubTile({ x: 25, y: 49 }, roof)).toBe(false)
|
|
})
|
|
|
|
it('combines cell coverage and bounding box in isPlayerUnderRoof', () => {
|
|
// Cell matches even if screen point is outside
|
|
expect(isPlayerUnderRoof({ x: 0, y: 0 }, roof, { x: 5, y: 10 })).toBe(true)
|
|
|
|
// Bounding box matches even if cell is different
|
|
expect(isPlayerUnderRoof({ x: 140, y: 140 }, roof, { x: 99, y: 99 })).toBe(true)
|
|
|
|
// Neither matches
|
|
expect(isPlayerUnderRoof({ x: 0, y: 0 }, roof, { x: 99, y: 99 })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('shelter clustering', () => {
|
|
it('groups adjacent roof tiles into a single shelter cluster', () => {
|
|
const akaraTent = [
|
|
makeRoof(100, 100, 80, 80, 10, 10),
|
|
makeRoof(140, 120, 80, 80, 10, 11),
|
|
makeRoof(120, 140, 80, 80, 11, 10),
|
|
]
|
|
const clusters = clusterRoofs(akaraTent)
|
|
expect(clusters).toHaveLength(1)
|
|
expect(clusters[0]!.roofs).toHaveLength(3)
|
|
expect(clusters[0]!.cells.has('10,10')).toBe(true)
|
|
expect(clusters[0]!.cells.has('10,11')).toBe(true)
|
|
expect(clusters[0]!.cells.has('11,10')).toBe(true)
|
|
})
|
|
|
|
it('separates distant roof groups into distinct shelters', () => {
|
|
const akaraTent = [
|
|
makeRoof(100, 100, 80, 80, 10, 10),
|
|
makeRoof(140, 120, 80, 80, 10, 11),
|
|
]
|
|
const charsiForge = [
|
|
makeRoof(800, 800, 80, 80, 50, 50),
|
|
makeRoof(840, 820, 80, 80, 50, 51),
|
|
]
|
|
const clusters = clusterRoofs([...akaraTent, ...charsiForge])
|
|
expect(clusters).toHaveLength(2)
|
|
|
|
const clusterA = clusters.find(c => c.cells.has('10,10'))!
|
|
const clusterB = clusters.find(c => c.cells.has('50,50'))!
|
|
expect(clusterA.roofs).toHaveLength(2)
|
|
expect(clusterB.roofs).toHaveLength(2)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Roof Opacity Fade', () => {
|
|
describe('stepRoofAlpha', () => {
|
|
it('fades down towards minAlpha over durationMs', () => {
|
|
// 200ms total duration, from 1.0 to 0.25
|
|
const start = 1.0
|
|
const target = 0.25
|
|
|
|
// After 50ms (25% progress) -> 1.0 - 0.75 * 0.25 = 0.8125
|
|
const step1 = stepRoofAlpha(start, target, 50, 200, 0.25, 1.0)
|
|
expect(step1).toBeCloseTo(0.8125, 4)
|
|
|
|
// After 100ms (50% progress) -> 1.0 - 0.75 * 0.5 = 0.625
|
|
const step2 = stepRoofAlpha(start, target, 100, 200, 0.25, 1.0)
|
|
expect(step2).toBeCloseTo(0.625, 4)
|
|
|
|
// After 200ms (100% progress) -> exactly 0.25
|
|
const step3 = stepRoofAlpha(start, target, 200, 200, 0.25, 1.0)
|
|
expect(step3).toBe(0.25)
|
|
|
|
// Clamps and does not overshoot below minAlpha
|
|
const step4 = stepRoofAlpha(0.25, target, 100, 200, 0.25, 1.0)
|
|
expect(step4).toBe(0.25)
|
|
})
|
|
|
|
it('fades up towards maxAlpha when exiting shelter', () => {
|
|
const start = 0.25
|
|
const target = 1.0
|
|
|
|
// After 100ms -> halfway back to 1.0 (0.625)
|
|
const step1 = stepRoofAlpha(start, target, 100, 200, 0.25, 1.0)
|
|
expect(step1).toBeCloseTo(0.625, 4)
|
|
|
|
// After 200ms -> full 1.0
|
|
const step2 = stepRoofAlpha(start, target, 200, 200, 0.25, 1.0)
|
|
expect(step2).toBe(1.0)
|
|
|
|
// Clamps at maxAlpha
|
|
const step3 = stepRoofAlpha(1.0, target, 50, 200, 0.25, 1.0)
|
|
expect(step3).toBe(1.0)
|
|
})
|
|
|
|
it('smoothly reverses direction mid-fade without discontinuities', () => {
|
|
// Player enters shelter, fades down partially to 0.70
|
|
let alpha = 1.0
|
|
alpha = stepRoofAlpha(alpha, 0.25, 80, 200, 0.25, 1.0)
|
|
expect(alpha).toBeCloseTo(0.70, 4)
|
|
|
|
// Player immediately steps back out: smoothly fades back up towards 1.0
|
|
alpha = stepRoofAlpha(alpha, 1.0, 40, 200, 0.25, 1.0)
|
|
expect(alpha).toBeGreaterThan(0.70)
|
|
expect(alpha).toBeCloseTo(0.85, 4)
|
|
|
|
// Reaches 1.0
|
|
alpha = stepRoofAlpha(alpha, 1.0, 100, 200, 0.25, 1.0)
|
|
expect(alpha).toBe(1.0)
|
|
})
|
|
|
|
it('returns currentAlpha when deltaMs is zero or negative', () => {
|
|
expect(stepRoofAlpha(0.5, 0.25, 0, 200)).toBe(0.5)
|
|
expect(stepRoofAlpha(0.5, 0.25, -10, 200)).toBe(0.5)
|
|
})
|
|
|
|
it('snaps immediately to target when durationMs is zero', () => {
|
|
expect(stepRoofAlpha(1.0, 0.25, 16, 0)).toBe(0.25)
|
|
})
|
|
})
|
|
|
|
describe('RoofFadeController', () => {
|
|
const akaraTent = [
|
|
makeRoof(100, 100, 80, 80, 10, 10),
|
|
makeRoof(140, 120, 80, 80, 10, 11),
|
|
]
|
|
const charsiForge = [
|
|
makeRoof(800, 800, 80, 80, 50, 50),
|
|
]
|
|
|
|
it('initializes all roofs to full opacity (1.0)', () => {
|
|
const controller = new RoofFadeController([...akaraTent, ...charsiForge])
|
|
expect(controller.getAlpha(0)).toBe(1.0)
|
|
expect(controller.getAlpha(1)).toBe(1.0)
|
|
expect(controller.getAlpha(2)).toBe(1.0)
|
|
expect(controller.overallAlpha).toBe(1.0)
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(false)
|
|
})
|
|
|
|
it('fades only the shelter the player is under', () => {
|
|
const controller = new RoofFadeController([...akaraTent, ...charsiForge], {
|
|
fadeDurationMs: 200,
|
|
minAlpha: 0.25,
|
|
})
|
|
|
|
// Player stands inside Akara's tent at cell (10, 10)
|
|
const playerPos = { x: 140, y: 140 }
|
|
const playerCell = { x: 10, y: 10 }
|
|
|
|
// Advance by 100ms (halfway)
|
|
controller.update(playerPos, 100, playerCell)
|
|
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(true)
|
|
// Akara's tent tiles (indices 0 and 1) fade in unison
|
|
expect(controller.getAlpha(0)).toBeCloseTo(0.625, 4)
|
|
expect(controller.getAlpha(1)).toBeCloseTo(0.625, 4)
|
|
|
|
// Charsi's forge (index 2) remains fully opaque
|
|
expect(controller.getAlpha(2)).toBe(1.0)
|
|
|
|
// Advance by another 100ms (reaches minAlpha 0.25)
|
|
controller.update(playerPos, 100, playerCell)
|
|
expect(controller.getAlpha(0)).toBe(0.25)
|
|
expect(controller.getAlpha(1)).toBe(0.25)
|
|
expect(controller.getAlpha(2)).toBe(1.0)
|
|
expect(controller.overallAlpha).toBe(0.25)
|
|
})
|
|
|
|
it('smoothly restores full opacity when player exits shelter', () => {
|
|
const controller = new RoofFadeController(akaraTent, {
|
|
fadeDurationMs: 200,
|
|
minAlpha: 0.25,
|
|
})
|
|
|
|
// Player inside -> fully faded
|
|
controller.update({ x: 140, y: 140 }, 200, { x: 10, y: 10 })
|
|
expect(controller.getAlpha(0)).toBe(0.25)
|
|
|
|
// Player exits shelter to town square
|
|
const outsidePos = { x: 500, y: 500 }
|
|
const outsideCell = { x: 25, y: 25 }
|
|
|
|
// 100ms elapsed
|
|
controller.update(outsidePos, 100, outsideCell)
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(false)
|
|
expect(controller.getAlpha(0)).toBeCloseTo(0.625, 4)
|
|
|
|
// 200ms elapsed -> fully restored
|
|
controller.update(outsidePos, 100, outsideCell)
|
|
expect(controller.getAlpha(0)).toBe(1.0)
|
|
expect(controller.overallAlpha).toBe(1.0)
|
|
})
|
|
|
|
it('handles empty roof list safely', () => {
|
|
const controller = new RoofFadeController([])
|
|
controller.update({ x: 100, y: 100 }, 16)
|
|
expect(controller.getAlpha(0)).toBe(1.0)
|
|
expect(controller.overallAlpha).toBe(1.0)
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(false)
|
|
})
|
|
|
|
it('allows manual alpha override via setAlpha', () => {
|
|
const controller = new RoofFadeController(akaraTent)
|
|
controller.setAlpha(0.2)
|
|
expect(controller.getAlpha(0)).toBe(0.2)
|
|
expect(controller.getAlpha(1)).toBe(0.2)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Issue #72: Harrogath West Gate and Overhead Occlusion Fade', () => {
|
|
it('projects elevated roof bounds down to ground footprint for occlusion detection', () => {
|
|
// A roof tile elevated by 160 px (isometric YAdjust = -roofHeight = -160)
|
|
// The visual sprite is rendered at y=200..260, but ground under it is at y=360
|
|
const elevatedRoof = makeRoof(100, 200, 80, 60, undefined, undefined, 160)
|
|
const bounds = getRoofBounds(elevatedRoof)
|
|
|
|
// Projected down: maxY = 200 + 60 + 160 = 420
|
|
expect(bounds).toEqual({
|
|
minX: 100,
|
|
minY: 200,
|
|
maxX: 180,
|
|
maxY: 420,
|
|
})
|
|
|
|
// Player standing on the ground beneath the elevated roof:
|
|
// Player feet at y=360, sprite height 48 -> player bounds y: 312..360
|
|
const playerOnGround = { x: 140, y: 360 }
|
|
|
|
// Unelevated roof (height=0) would fail to detect this player
|
|
const flatRoof = makeRoof(100, 200, 80, 60, undefined, undefined, 0)
|
|
expect(isPlayerOverlappingRoofBounds(playerOnGround, flatRoof)).toBe(false)
|
|
|
|
// Elevated roof correctly detects the player on the ground
|
|
expect(isPlayerOverlappingRoofBounds(playerOnGround, elevatedRoof)).toBe(true)
|
|
expect(isPlayerUnderRoof(playerOnGround, elevatedRoof)).toBe(true)
|
|
})
|
|
|
|
it('computes elevation from cell coordinate offset when roofHeight is omitted', () => {
|
|
// Roof placed at cell (5, 10), orthoY = (5 + 10) * 40 = 600
|
|
// Sprite top y = 440, so implicit elevation = 600 - 440 = 160
|
|
const roof = makeRoof(100, 440, 80, 60, 5, 10)
|
|
const bounds = getRoofBounds(roof)
|
|
expect(bounds.maxY).toBe(440 + 60 + 160) // 660
|
|
|
|
// Player at ground orthoY=600 is detected under roof bounds
|
|
const playerAtGround = { x: 140, y: 600 }
|
|
expect(isPlayerOverlappingRoofBounds(playerAtGround, roof)).toBe(true)
|
|
})
|
|
|
|
it('clusters Harrogath West Gate left shrine and right tower across corridor', () => {
|
|
// Harrogath West Gate layout:
|
|
// Left shrine roofs: cellX: 5..7, cellY: 27..29
|
|
// Right tower roofs: cellX: 10..12, cellY: 27..30
|
|
// Separated by 3-4 cells across the gate passage
|
|
const leftShrine = [
|
|
makeRoof(200, 600, 80, 80, 5, 27),
|
|
makeRoof(240, 640, 80, 80, 6, 28),
|
|
makeRoof(280, 680, 80, 80, 7, 29),
|
|
]
|
|
const rightTower = [
|
|
makeRoof(480, 640, 80, 80, 10, 28),
|
|
makeRoof(480, 680, 80, 80, 10, 30),
|
|
makeRoof(520, 680, 80, 80, 11, 29),
|
|
makeRoof(560, 720, 80, 80, 12, 30),
|
|
]
|
|
|
|
expect(DEFAULT_CLUSTER_CELL_DISTANCE).toBe(4)
|
|
const clusters = clusterRoofs([...leftShrine, ...rightTower])
|
|
|
|
// With distance 4, the gatehouse halves form a single connected cluster
|
|
expect(clusters).toHaveLength(1)
|
|
expect(clusters[0]!.roofs).toHaveLength(7)
|
|
|
|
// Corridor passage cells between shrine (cx: 7) and tower (cx: 10) are bridged
|
|
expect(clusters[0]!.cells.has('7,29')).toBe(true)
|
|
expect(clusters[0]!.cells.has('8,29')).toBe(true)
|
|
expect(clusters[0]!.cells.has('9,29')).toBe(true)
|
|
expect(clusters[0]!.cells.has('10,29')).toBe(true)
|
|
expect(clusters[0]!.cells.has('8,30')).toBe(true)
|
|
})
|
|
|
|
it('bridges corridor cells between gatehouse structures so passage triggers occlusion', () => {
|
|
const leftShrine = [
|
|
makeRoof(200, 600, 80, 80, 6, 28),
|
|
makeRoof(240, 640, 80, 80, 7, 29),
|
|
]
|
|
const rightTower = [
|
|
makeRoof(480, 640, 80, 80, 10, 29),
|
|
makeRoof(520, 680, 80, 80, 11, 30),
|
|
]
|
|
const controller = new RoofFadeController([...leftShrine, ...rightTower], {
|
|
fadeDurationMs: 200,
|
|
})
|
|
|
|
expect(controller.overallAlpha).toBe(1.0)
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(false)
|
|
|
|
// Player steps into the passage between shrine and tower: cell (8, 30)
|
|
const playerInPassage = { x: 340, y: 700 }
|
|
controller.update(playerInPassage, 200, { x: 8, y: 30 })
|
|
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(true)
|
|
// Left shrine and right tower both fade completely to 0.0 in unison
|
|
expect(controller.getAlpha(0)).toBe(0.0)
|
|
expect(controller.getAlpha(1)).toBe(0.0)
|
|
expect(controller.getAlpha(2)).toBe(0.0)
|
|
expect(controller.getAlpha(3)).toBe(0.0)
|
|
expect(controller.overallAlpha).toBe(0.0)
|
|
|
|
// Player steps outside into the open snowfield
|
|
const playerOutside = { x: 50, y: 300 }
|
|
controller.update(playerOutside, 200, { x: 2, y: 30 })
|
|
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(false)
|
|
expect(controller.overallAlpha).toBe(1.0)
|
|
})
|
|
|
|
it('fades all connected gatehouse roofs and overhead archways/upper walls in unison to 0.0', () => {
|
|
// Cross-passage archways (wallType: 12) and upper walls (wallType: 14)
|
|
// alongside gatehouse roofs (wallType: 15)
|
|
const gateDrawables: RoofDrawable[] = [
|
|
makeRoof(240, 640, 80, 80, 7, 28, 160, 15), // Left shrine roof
|
|
makeRoof(360, 600, 80, 80, 9, 28, 128, 12), // Overhead archway
|
|
makeRoof(400, 620, 80, 80, 10, 29, 96, 12), // Overhead archway
|
|
makeRoof(420, 580, 80, 80, 10, 30, 384, 14), // Upper wall cross beam
|
|
makeRoof(480, 640, 80, 80, 11, 28, 160, 15), // Right tower roof
|
|
]
|
|
|
|
const controller = new RoofFadeController(gateDrawables, {
|
|
fadeDurationMs: 200,
|
|
})
|
|
|
|
// All belong to the same cluster spanning the gate
|
|
expect(controller.clusters).toHaveLength(1)
|
|
expect(controller.clusters[0]!.roofs).toHaveLength(5)
|
|
|
|
// Player under archway at cell (9, 28)
|
|
controller.update({ x: 360, y: 728 }, 200, { x: 9, y: 28 })
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(true)
|
|
// Every element (roofs, archways, upper walls) fades completely to 0.0
|
|
for (let i = 0; i < gateDrawables.length; i += 1) {
|
|
expect(controller.getAlpha(i)).toBe(0.0)
|
|
}
|
|
|
|
// Player under upper wall beam at cell (10, 30)
|
|
controller.update({ x: 420, y: 964 }, 200, { x: 10, y: 30 })
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(true)
|
|
for (let i = 0; i < gateDrawables.length; i += 1) {
|
|
expect(controller.getAlpha(i)).toBe(0.0)
|
|
}
|
|
|
|
// Player exits gate
|
|
controller.update({ x: 100, y: 100 }, 200, { x: 1, y: 1 })
|
|
expect(controller.isPlayerUnderAnyRoof).toBe(false)
|
|
for (let i = 0; i < gateDrawables.length; i += 1) {
|
|
expect(controller.getAlpha(i)).toBe(1.0)
|
|
}
|
|
})
|
|
|
|
it('defaults to complete transparency (minAlpha = 0.0) in vanilla Diablo II style', () => {
|
|
expect(DEFAULT_MIN_ROOF_ALPHA).toBe(0.0)
|
|
const roof = makeRoof(100, 100, 80, 80, 10, 10)
|
|
const controller = new RoofFadeController([roof])
|
|
controller.update({ x: 140, y: 140 }, 200, { x: 10, y: 10 })
|
|
expect(controller.getAlpha(0)).toBe(0.0)
|
|
expect(controller.overallAlpha).toBe(0.0)
|
|
})
|
|
})
|
|
|