162 lines
4.4 KiB
JavaScript
162 lines
4.4 KiB
JavaScript
import { createAnchorGrid, allocateLayerCounts } from './anchors.js'
|
|
import { evaluateBoard } from './evaluate-board.js'
|
|
import { buildOverlapGraph } from './overlap-graph.js'
|
|
import { createRng, shuffleWithRng } from './random.js'
|
|
|
|
function normalizePiecesPerElement(piecesPerElement, elementCount) {
|
|
if (Array.isArray(piecesPerElement)) {
|
|
return piecesPerElement.slice(0, elementCount)
|
|
}
|
|
|
|
return Array.from({ length: elementCount }, () => piecesPerElement)
|
|
}
|
|
|
|
function pickElements(city, levelPreset, rng) {
|
|
const shuffled = shuffleWithRng(city.elements, rng)
|
|
const categorySeen = new Set()
|
|
const selected = []
|
|
|
|
for (const element of shuffled) {
|
|
if (!categorySeen.has(element.category)) {
|
|
selected.push(element)
|
|
categorySeen.add(element.category)
|
|
}
|
|
if (selected.length === levelPreset.elementCount) {
|
|
return selected
|
|
}
|
|
}
|
|
|
|
for (const element of shuffled) {
|
|
if (!selected.includes(element)) {
|
|
selected.push(element)
|
|
}
|
|
if (selected.length === levelPreset.elementCount) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return selected
|
|
}
|
|
|
|
function createPiecePool(selectedElements, counts) {
|
|
const pieces = []
|
|
let sequence = 1
|
|
|
|
selectedElements.forEach((element, elementIndex) => {
|
|
const count = counts[elementIndex]
|
|
for (let offset = 0; offset < count; offset += 1) {
|
|
pieces.push({
|
|
id: `piece_${String(sequence).padStart(4, '0')}`,
|
|
elementId: element.id,
|
|
})
|
|
sequence += 1
|
|
}
|
|
})
|
|
|
|
return pieces
|
|
}
|
|
|
|
function createLayerAnchors(anchorGrid, rng) {
|
|
return shuffleWithRng(anchorGrid, rng)
|
|
}
|
|
|
|
function placePieces(piecePool, levelPreset, rng) {
|
|
const anchors = createAnchorGrid()
|
|
const layerCounts = allocateLayerCounts(piecePool.length, levelPreset.density, levelPreset.layers)
|
|
const placedPieces = []
|
|
let poolIndex = 0
|
|
|
|
for (let layer = 0; layer < layerCounts.length; layer += 1) {
|
|
const layerAnchors = createLayerAnchors(anchors, rng)
|
|
const previousLayerPieces = placedPieces.filter((piece) => piece.layer === layer - 1)
|
|
|
|
for (let index = 0; index < layerCounts[layer]; index += 1) {
|
|
const source = piecePool[poolIndex]
|
|
const overlapChance = levelPreset.density === 'high'
|
|
? 0.85
|
|
: levelPreset.density === 'medium_high'
|
|
? 0.7
|
|
: levelPreset.density === 'medium'
|
|
? 0.55
|
|
: 0.35
|
|
|
|
let anchor = layerAnchors[index % layerAnchors.length]
|
|
|
|
if (layer > 0 && previousLayerPieces.length > 0 && rng() < overlapChance) {
|
|
const target = previousLayerPieces[Math.floor(rng() * previousLayerPieces.length)]
|
|
anchor = { x: target.x + 6, y: target.y + 6 }
|
|
}
|
|
|
|
placedPieces.push({
|
|
...source,
|
|
layer,
|
|
x: Math.round(anchor.x + (rng() - 0.5) * 12),
|
|
y: Math.round(anchor.y + (rng() - 0.5) * 12),
|
|
width: 64,
|
|
height: 64,
|
|
rotation: Math.round((rng() - 0.5) * 12),
|
|
removed: false,
|
|
})
|
|
|
|
poolIndex += 1
|
|
}
|
|
}
|
|
|
|
return placedPieces
|
|
}
|
|
|
|
export function generateBoardFromDefinition({
|
|
boardId,
|
|
cityId,
|
|
levelId,
|
|
seed,
|
|
elements,
|
|
levelPreset,
|
|
extraState = {},
|
|
}) {
|
|
const effectiveSeed = seed ?? levelPreset.seedBase
|
|
const rng = createRng(effectiveSeed)
|
|
const selectedElements = [...elements]
|
|
const counts = normalizePiecesPerElement(levelPreset.piecesPerElement, levelPreset.elementCount)
|
|
const piecePool = createPiecePool(selectedElements, counts)
|
|
const pieces = placePieces(shuffleWithRng(piecePool, rng), levelPreset, rng)
|
|
const overlapGraph = buildOverlapGraph(pieces)
|
|
const boardState = {
|
|
boardId: boardId ?? `${cityId}-${levelId}-${effectiveSeed}`,
|
|
cityId,
|
|
levelId,
|
|
seed: effectiveSeed,
|
|
pieces,
|
|
overlapGraph,
|
|
metrics: {},
|
|
...extraState,
|
|
}
|
|
|
|
boardState.metrics = evaluateBoard(boardState, levelPreset)
|
|
|
|
return boardState
|
|
}
|
|
|
|
export function generateBoard({ cityId, levelId, seed, contentSystem }) {
|
|
const city = contentSystem.getCity(cityId)
|
|
const levelPreset = contentSystem.getLevelPreset(cityId, levelId)
|
|
|
|
if (!city || !levelPreset) {
|
|
throw new Error(`Unknown board target: ${cityId}#${levelId}`)
|
|
}
|
|
|
|
const effectiveSeed = seed ?? levelPreset.seedBase
|
|
const rng = createRng(effectiveSeed)
|
|
const selectedElements = pickElements(city, levelPreset, rng)
|
|
|
|
return generateBoardFromDefinition({
|
|
cityId,
|
|
levelId,
|
|
seed: effectiveSeed,
|
|
elements: selectedElements,
|
|
levelPreset,
|
|
})
|
|
}
|
|
|
|
export default generateBoard
|