fix(scene): handle level transition entity cleanup, art reloading & wilderness floor gaps
This commit is contained in:
parent
e9f2ceec9b
commit
4ea77e2c00
|
|
@ -3728,7 +3728,15 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
if (row === undefined) continue
|
if (row === undefined) continue
|
||||||
for (let x = 0; x < canvas.width; x += 1) {
|
for (let x = 0; x < canvas.width; x += 1) {
|
||||||
const c = row[x]
|
const c = row[x]
|
||||||
if (c !== undefined && c.floors.some(f => !f.hidden && f.prop1 !== 0)) finalGroundCells += 1
|
if (c !== undefined) {
|
||||||
|
if (!c.floors.some(f => !f.hidden && f.prop1 !== 0)) {
|
||||||
|
const floor = c.floors[0]
|
||||||
|
if (floor !== undefined) {
|
||||||
|
Object.assign(floor, { prop1: 2, style: groundTile.style, sequence: groundTile.sequence, hidden: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalGroundCells += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stats.groundCells = finalGroundCells
|
stats.groundCells = finalGroundCells
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
} from '../game/d2map.ts'
|
} from '../game/d2map.ts'
|
||||||
import type { CollisionGrid } from '../game/d2map.ts'
|
import type { CollisionGrid } from '../game/d2map.ts'
|
||||||
import { createIsoTerrain } from '../game/iso-terrain.ts'
|
import { createIsoTerrain } from '../game/iso-terrain.ts'
|
||||||
|
import { spawnMonsterPacks, spawnMonsters } from '../game/combat.ts'
|
||||||
import type { MonsterPack, MonsterStats, SafeZone } from '../game/combat.ts'
|
import type { MonsterPack, MonsterStats, SafeZone } from '../game/combat.ts'
|
||||||
import { SUB_TILES_PER_TILE, depthInsertIndex } from '../game/map.ts'
|
import { SUB_TILES_PER_TILE, depthInsertIndex } from '../game/map.ts'
|
||||||
import { GameEngine, syncEngineState } from "../game/engine.ts"
|
import { GameEngine, syncEngineState } from "../game/engine.ts"
|
||||||
|
|
@ -568,6 +569,46 @@ async function buildPackRuntime(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const floors = scene.floors.map(toDrawable)
|
const floors = scene.floors.map(toDrawable)
|
||||||
|
const WILDERNESS_LEVEL_IDS = new Set([2, 3, 4, 5, 6, 7, 17, 41, 42, 43, 44, 45, 46, 76, 77, 78, 79, 80, 81, 82, 83, 104, 105, 106, 110, 111, 112, 117, 134])
|
||||||
|
if (entry.levelId !== undefined && WILDERNESS_LEVEL_IDS.has(entry.levelId) && floors.length > 0 && floors.length < scene.cellsX * scene.cellsY) {
|
||||||
|
const covered = new Uint8Array(scene.cellsX * scene.cellsY)
|
||||||
|
const freq = new Map<number, number>()
|
||||||
|
for (let i = 0; i < scene.floors.length; i += 1) {
|
||||||
|
const row = scene.floors[i]!
|
||||||
|
const cx = row[3] ?? 0
|
||||||
|
const cy = row[4] ?? 0
|
||||||
|
if (cx >= 0 && cx < scene.cellsX && cy >= 0 && cy < scene.cellsY) {
|
||||||
|
covered[cy * scene.cellsX + cx] = 1
|
||||||
|
}
|
||||||
|
const fIdx = row[0] ?? 0
|
||||||
|
freq.set(fIdx, (freq.get(fIdx) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
let defaultFrameIndex = scene.floors[0]?.[0] ?? 0
|
||||||
|
let maxCount = -1
|
||||||
|
for (const [fIdx, count] of freq) {
|
||||||
|
if (count > maxCount) {
|
||||||
|
maxCount = count
|
||||||
|
defaultFrameIndex = fIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaultFrame = frames[defaultFrameIndex] ?? { x: 0, y: 0, width: 1, height: 1 }
|
||||||
|
const defaultPage = scene.framePlacement[defaultFrameIndex]?.[0] ?? 0
|
||||||
|
for (let cy = 0; cy < scene.cellsY; cy += 1) {
|
||||||
|
for (let cx = 0; cx < scene.cellsX; cx += 1) {
|
||||||
|
if (covered[cy * scene.cellsX + cx] === 0) {
|
||||||
|
floors.push({
|
||||||
|
frame: defaultFrame,
|
||||||
|
x: scene.originX + (cx - cy) * ORTHO_CELL_WIDTH - 80,
|
||||||
|
y: scene.originY + (cx + cy) * ORTHO_CELL_HEIGHT,
|
||||||
|
cellX: cx,
|
||||||
|
cellY: cy,
|
||||||
|
page: defaultPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
floors.sort((a, b) => (a.cellX + a.cellY) - (b.cellX + b.cellY) || a.cellY - b.cellY)
|
||||||
|
}
|
||||||
const walls = scene.walls.map(toDrawable)
|
const walls = scene.walls.map(toDrawable)
|
||||||
const roofs = (scene.roofs ?? []).map(toDrawable)
|
const roofs = (scene.roofs ?? []).map(toDrawable)
|
||||||
|
|
||||||
|
|
@ -1253,12 +1294,12 @@ function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, started:
|
||||||
const camera = new ViewportCamera(canvas, runtime.widthPx, runtime.heightPx)
|
const camera = new ViewportCamera(canvas, runtime.widthPx, runtime.heightPx)
|
||||||
camera.attach()
|
camera.attach()
|
||||||
|
|
||||||
// Culling bounds, built once: the draw lists are immutable for the life of the
|
// Culling bounds, rebuilt on level swap: the draw lists are immutable for the
|
||||||
// runtime, so the only per-frame cost is the rejection test itself.
|
// life of a single runtime, so the per-frame cost is just the rejection test.
|
||||||
const floorBounds = buildBounds(runtime.floors)
|
let floorBounds = buildBounds(runtime.floors)
|
||||||
const wallBounds = buildBounds(runtime.walls)
|
let wallBounds = buildBounds(runtime.walls)
|
||||||
const roofBounds = buildBounds(runtime.roofs)
|
let roofBounds = buildBounds(runtime.roofs)
|
||||||
const objectBounds = buildBounds(runtime.objectDrawables)
|
let objectBounds = buildBounds(runtime.objectDrawables)
|
||||||
|
|
||||||
// `runtime.pages` is filled in by background loaders, so the count has to be
|
// `runtime.pages` is filled in by background loaders, so the count has to be
|
||||||
// re-read every frame. A loop is used rather than `filter().length` because
|
// re-read every frame. A loop is used rather than `filter().length` because
|
||||||
|
|
@ -1413,8 +1454,15 @@ function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, started:
|
||||||
// long walk leaks a page set per level crossed.
|
// long walk leaks a page set per level crossed.
|
||||||
for (const page of runtime.pages) if (page !== null) renderer.deleteAtlas(page)
|
for (const page of runtime.pages) if (page !== null) renderer.deleteAtlas(page)
|
||||||
for (const page of runtime.objectPages) if (page !== null) renderer.deleteAtlas(page)
|
for (const page of runtime.objectPages) if (page !== null) renderer.deleteAtlas(page)
|
||||||
|
for (const art of monsterArtMap.values()) renderer.deleteAtlas(art.handle)
|
||||||
|
monsterArtMap = new Map()
|
||||||
|
|
||||||
runtime = next
|
runtime = next
|
||||||
|
floorBounds = buildBounds(next.floors)
|
||||||
|
wallBounds = buildBounds(next.walls)
|
||||||
|
roofBounds = buildBounds(next.roofs)
|
||||||
|
objectBounds = buildBounds(next.objectDrawables)
|
||||||
|
|
||||||
camera.mapWidthPx = next.widthPx
|
camera.mapWidthPx = next.widthPx
|
||||||
camera.mapHeightPx = next.heightPx
|
camera.mapHeightPx = next.heightPx
|
||||||
engine.terrain = createIsoTerrain(next.grid, next.widthPx, next.heightPx, {
|
engine.terrain = createIsoTerrain(next.grid, next.widthPx, next.heightPx, {
|
||||||
|
|
@ -1434,6 +1482,63 @@ function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, started:
|
||||||
engine.npcEntities.push({ def, x: n.x, y: n.y, hasSprite: hasPackedSprite(n) })
|
engine.npcEntities.push({ def, x: n.x, y: n.y, hasSprite: hasPackedSprite(n) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear outgoing monsters and ground items, then populate the destination level
|
||||||
|
engine.world.monsters.length = 0
|
||||||
|
engine.ground.length = 0
|
||||||
|
const nextSafeZones: SafeZone[] = [
|
||||||
|
{ x: landing.x, y: landing.y, radius: 180 },
|
||||||
|
]
|
||||||
|
for (const wp of next.waypoints) {
|
||||||
|
const pos = subTileToScene(wp.x, wp.y, next.grid)
|
||||||
|
nextSafeZones.push({ x: pos.x, y: pos.y, radius: 160 })
|
||||||
|
}
|
||||||
|
for (const ent of next.entrances) {
|
||||||
|
const pos = subTileToScene(ent.x, ent.y, next.grid)
|
||||||
|
nextSafeZones.push({ x: pos.x, y: pos.y, radius: 160 })
|
||||||
|
}
|
||||||
|
const nextResolvedPacks: MonsterPack[] = next.monsterPacks.map(pack => {
|
||||||
|
if (pack.landmarkTile !== undefined && pack.fixedPosition === undefined) {
|
||||||
|
return {
|
||||||
|
...pack,
|
||||||
|
fixedPosition: cellCentre(next.grid, pack.landmarkTile.tileX, pack.landmarkTile.tileY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pack
|
||||||
|
})
|
||||||
|
const spread = Math.max(next.widthPx, next.heightPx) / 4
|
||||||
|
if (nextResolvedPacks.length > 0) {
|
||||||
|
spawnMonsterPacks(engine.world, nextResolvedPacks, landing, spread, engine.terrain, nextSafeZones)
|
||||||
|
} else if (next.monsterCount > 0) {
|
||||||
|
spawnMonsters(engine.world, next.monsterStats, next.monsterCount, landing, spread, engine.terrain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.palette !== null) {
|
||||||
|
renderer.setPalette(next.palette.rgb)
|
||||||
|
const paletteForEntities = next.palette
|
||||||
|
const packEntityBase = next.source === 'pack' ? next.base : undefined
|
||||||
|
const packEntityAct = next.source === 'pack' ? next.act : undefined
|
||||||
|
const activeMonsterIds = next.monsterTypes.length > 0
|
||||||
|
? next.monsterTypes
|
||||||
|
: (next.monsterCount > 0 ? next.monsterStats.map(s => s.id) : [])
|
||||||
|
if (activeMonsterIds.length > 0) {
|
||||||
|
void loadMonsterArtMap(
|
||||||
|
DEFAULT_BASES,
|
||||||
|
activeMonsterIds,
|
||||||
|
paletteForEntities,
|
||||||
|
renderer,
|
||||||
|
packEntityBase,
|
||||||
|
packEntityAct,
|
||||||
|
).then((loadedMap) => {
|
||||||
|
monsterArtMap = loadedMap
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.source === 'pack') {
|
||||||
|
history.replaceState(null, '', `?act=${String(next.act)}&level=${encodeURIComponent(next.quadrant)}`)
|
||||||
|
void populatePackSelectors(next.base, { act: next.act, label: next.quadrant })
|
||||||
|
}
|
||||||
|
|
||||||
registerWaypoints(next)
|
registerWaypoints(next)
|
||||||
state.level = next.level
|
state.level = next.level
|
||||||
state.act = next.act
|
state.act = next.act
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue