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
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
} from '../game/d2map.ts'
|
||||
import type { CollisionGrid } from '../game/d2map.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 { SUB_TILES_PER_TILE, depthInsertIndex } from '../game/map.ts'
|
||||
import { GameEngine, syncEngineState } from "../game/engine.ts"
|
||||
|
|
@ -568,6 +569,46 @@ async function buildPackRuntime(
|
|||
}
|
||||
}
|
||||
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 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)
|
||||
camera.attach()
|
||||
|
||||
// Culling bounds, built once: the draw lists are immutable for the life of the
|
||||
// runtime, so the only per-frame cost is the rejection test itself.
|
||||
const floorBounds = buildBounds(runtime.floors)
|
||||
const wallBounds = buildBounds(runtime.walls)
|
||||
const roofBounds = buildBounds(runtime.roofs)
|
||||
const objectBounds = buildBounds(runtime.objectDrawables)
|
||||
// Culling bounds, rebuilt on level swap: the draw lists are immutable for the
|
||||
// life of a single runtime, so the per-frame cost is just the rejection test.
|
||||
let floorBounds = buildBounds(runtime.floors)
|
||||
let wallBounds = buildBounds(runtime.walls)
|
||||
let roofBounds = buildBounds(runtime.roofs)
|
||||
let objectBounds = buildBounds(runtime.objectDrawables)
|
||||
|
||||
// `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
|
||||
|
|
@ -1413,8 +1454,15 @@ function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, started:
|
|||
// 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.objectPages) if (page !== null) renderer.deleteAtlas(page)
|
||||
for (const art of monsterArtMap.values()) renderer.deleteAtlas(art.handle)
|
||||
monsterArtMap = new Map()
|
||||
|
||||
runtime = next
|
||||
floorBounds = buildBounds(next.floors)
|
||||
wallBounds = buildBounds(next.walls)
|
||||
roofBounds = buildBounds(next.roofs)
|
||||
objectBounds = buildBounds(next.objectDrawables)
|
||||
|
||||
camera.mapWidthPx = next.widthPx
|
||||
camera.mapHeightPx = 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) })
|
||||
}
|
||||
|
||||
// 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)
|
||||
state.level = next.level
|
||||
state.act = next.act
|
||||
|
|
|
|||
Loading…
Reference in New Issue