204 lines
5.5 KiB
TypeScript
204 lines
5.5 KiB
TypeScript
import type { SeedMapDimension } from './biomes.ts'
|
|
|
|
export type SeedMapDisplayMode = 'structures' | 'ores'
|
|
|
|
export type SeedMapOreKind =
|
|
| 'diamond'
|
|
| 'iron'
|
|
| 'iron_vein'
|
|
| 'copper'
|
|
| 'copper_vein'
|
|
| 'gold'
|
|
| 'redstone'
|
|
| 'lapis'
|
|
| 'coal'
|
|
| 'netherite'
|
|
|
|
export type SeedMapOreDefinition = {
|
|
kind: SeedMapOreKind
|
|
dimension: Exclude<SeedMapDimension, 'end'>
|
|
yMin: number
|
|
yMax: number
|
|
bandRows: number
|
|
image: string
|
|
texture: string
|
|
deepslateTexture?: string
|
|
}
|
|
|
|
export type SeedMapOreHit = {
|
|
ore: SeedMapOreKind
|
|
x: number
|
|
y: number
|
|
z: number
|
|
verified: boolean
|
|
yMin: number
|
|
yMax: number
|
|
precision: number
|
|
}
|
|
|
|
export type SeedMapOreChunk = {
|
|
cx: number
|
|
cz: number
|
|
}
|
|
|
|
const ORE_ASSET_ROOT = '/seed-map-assets/ores'
|
|
|
|
export const SEED_MAP_ORE_MAX_SCALE = 0.25
|
|
export const SEED_MAP_ORE_CACHE_LIMIT = 6_000
|
|
export const SEED_MAP_ORE_CACHE_TARGET = 5_400
|
|
|
|
/**
|
|
* Ore prediction replays the vanilla 1.18+ population RNG natively, so the
|
|
* offered kinds match the scattered-ore features of those versions. The
|
|
* `bandRows` value reflects each distribution's Y span and scales the scan
|
|
* budget so wide bands do not overwhelm a single pass.
|
|
*/
|
|
export const SEED_MAP_ORES: readonly SeedMapOreDefinition[] = [
|
|
{
|
|
kind: 'diamond',
|
|
dimension: 'overworld',
|
|
yMin: -144,
|
|
yMax: 16,
|
|
bandRows: 160,
|
|
image: `${ORE_ASSET_ROOT}/diamond-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/diamond_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_diamond_ore.png`,
|
|
},
|
|
{
|
|
kind: 'iron',
|
|
dimension: 'overworld',
|
|
yMin: -64,
|
|
yMax: 319,
|
|
bandRows: 384,
|
|
image: `${ORE_ASSET_ROOT}/iron-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/iron_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_iron_ore.png`,
|
|
},
|
|
{
|
|
kind: 'iron_vein',
|
|
dimension: 'overworld',
|
|
yMin: -60,
|
|
yMax: -8,
|
|
bandRows: 128,
|
|
image: `${ORE_ASSET_ROOT}/block-of-raw-iron.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/iron_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_iron_ore.png`,
|
|
},
|
|
{
|
|
kind: 'copper',
|
|
dimension: 'overworld',
|
|
yMin: -16,
|
|
yMax: 112,
|
|
bandRows: 144,
|
|
image: `${ORE_ASSET_ROOT}/copper-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/copper_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_copper_ore.png`,
|
|
},
|
|
{
|
|
kind: 'copper_vein',
|
|
dimension: 'overworld',
|
|
yMin: 0,
|
|
yMax: 50,
|
|
bandRows: 128,
|
|
image: `${ORE_ASSET_ROOT}/block-of-raw-copper.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/copper_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_copper_ore.png`,
|
|
},
|
|
{
|
|
kind: 'gold',
|
|
dimension: 'overworld',
|
|
yMin: -64,
|
|
yMax: 32,
|
|
bandRows: 104,
|
|
image: `${ORE_ASSET_ROOT}/gold-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/gold_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_gold_ore.png`,
|
|
},
|
|
{
|
|
kind: 'redstone',
|
|
dimension: 'overworld',
|
|
yMin: -96,
|
|
yMax: 15,
|
|
bandRows: 112,
|
|
image: `${ORE_ASSET_ROOT}/redstone-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/redstone_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_redstone_ore.png`,
|
|
},
|
|
{
|
|
kind: 'lapis',
|
|
dimension: 'overworld',
|
|
yMin: -64,
|
|
yMax: 64,
|
|
bandRows: 136,
|
|
image: `${ORE_ASSET_ROOT}/lapis-lazuli-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/lapis_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_lapis_ore.png`,
|
|
},
|
|
{
|
|
kind: 'coal',
|
|
dimension: 'overworld',
|
|
yMin: 0,
|
|
yMax: 319,
|
|
bandRows: 328,
|
|
image: `${ORE_ASSET_ROOT}/coal-ore.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/coal_ore.png`,
|
|
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_coal_ore.png`,
|
|
},
|
|
{
|
|
kind: 'netherite',
|
|
dimension: 'nether',
|
|
yMin: 8,
|
|
yMax: 119,
|
|
bandRows: 128,
|
|
image: `${ORE_ASSET_ROOT}/ancient-debris.png`,
|
|
texture: `${ORE_ASSET_ROOT}/ore-flat/ancient_debris_side.png`,
|
|
},
|
|
]
|
|
|
|
const ORE_BY_KIND = new Map(SEED_MAP_ORES.map((ore) => [ore.kind, ore]))
|
|
|
|
export function seedMapOreDefinition(kind: SeedMapOreKind): SeedMapOreDefinition {
|
|
const definition = ORE_BY_KIND.get(kind)
|
|
if (!definition) throw new Error(`Unknown seed-map ore: ${kind}`)
|
|
return definition
|
|
}
|
|
|
|
export function seedMapOresForDimension(dimension: SeedMapDimension): SeedMapOreDefinition[] {
|
|
if (dimension === 'end') return []
|
|
return SEED_MAP_ORES.filter((ore) => ore.dimension === dimension)
|
|
}
|
|
|
|
export function seedMapOreScanBudget(kinds: readonly SeedMapOreKind[]): number {
|
|
const maxBandRows = Math.max(1, ...kinds.map((kind) => seedMapOreDefinition(kind).bandRows))
|
|
return Math.max(60, Math.round(33_600 / maxBandRows))
|
|
}
|
|
|
|
export function seedMapOreYRange(kinds: readonly SeedMapOreKind[]): [number, number] {
|
|
if (kinds.length === 0) return [-64, 319]
|
|
return [
|
|
Math.min(...kinds.map((kind) => seedMapOreDefinition(kind).yMin)),
|
|
Math.max(...kinds.map((kind) => seedMapOreDefinition(kind).yMax)),
|
|
]
|
|
}
|
|
|
|
export function seedMapOreKey(hit: SeedMapOreHit): string {
|
|
return `${hit.ore}:${hit.x}:${hit.y}:${hit.z}`
|
|
}
|
|
|
|
export function seedMapOreColumnKey(hit: Pick<SeedMapOreHit, 'ore' | 'x' | 'z'>): string {
|
|
return `${hit.ore}:${hit.x}:${hit.z}`
|
|
}
|
|
|
|
export function preferSeedMapOreHit(
|
|
current: SeedMapOreHit | undefined,
|
|
candidate: SeedMapOreHit,
|
|
): SeedMapOreHit {
|
|
if (!current) return candidate
|
|
if (candidate.verified !== current.verified) return candidate.verified ? candidate : current
|
|
if (candidate.precision !== current.precision)
|
|
return candidate.precision > current.precision ? candidate : current
|
|
const currentRange = current.yMax - current.yMin
|
|
const candidateRange = candidate.yMax - candidate.yMin
|
|
return candidateRange < currentRange ? candidate : current
|
|
}
|