diff --git a/src/app/Utils.ts b/src/app/Utils.ts index e68a04cd..86053e5c 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -12,7 +12,10 @@ export function htmlEncode(str: string) { } export function hashString(s: string) { - return (s ?? '').split('').reduce((a, b) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a }, 0) + let h = 0 + for(let i = 0; i < s.length; i++) + h = Math.imul(31, h) + s.charCodeAt(i) | 0 + return h } export function stringToColor(str: string): [number, number, number] { diff --git a/src/app/preview/DecoratorPreview.ts b/src/app/preview/DecoratorPreview.ts index 32fe8f61..d67bc12e 100644 --- a/src/app/preview/DecoratorPreview.ts +++ b/src/app/preview/DecoratorPreview.ts @@ -1,26 +1,42 @@ import { DataModel, Path, ModelPath } from "@mcschema/core" import seedrandom from "seedrandom" import { App } from "../App" -import { clamp, hexId, stringToColor } from "../Utils" +import { clamp, hashString, hexId, stringToColor } from "../Utils" import { PerlinNoise } from "./noise/PerlinNoise" import { Preview } from './Preview' import { Octicon } from '../components/Octicon' import { View } from "../views/View" type BlockPos = [number, number, number] -type Placement = { pos: BlockPos, feature: string } +type Placement = { pos: BlockPos, feature: number } -const terrain = [50, 50, 50, 51, 52, 52, 53, 54, 56, 57, 57, 58, 58, 59, 60, 60, 60, 60, 59, 59, 60, 61, 62, 62, 63, 63, 63, 64, 64, 64, 65, 65, 66, 65, 65, 66, 66, 67, 67, 67, 68, 69, 71, 73, 74, 76, 79, 80, 81, 81, 82, 83, 83, 82, 82, 81, 81, 80, 80, 81, 81, 81, 82, 82] +const terrain = [50, 50, 51, 51, 52, 52, 53, 54, 56, 57, 57, 58, 58, 59, 60, 60, 60, 59, 59, 59, 60, 61, 61, 62, 63, 63, 64, 64, 64, 65, 65, 66, 66, 65, 65, 66, 66, 67, 67, 67, 68, 69, 71, 73, 74, 76, 79, 80, 81, 81, 82, 83, 83, 82, 82, 81, 81, 80, 80, 80, 81, 81, 82, 82] const seaLevel = 63 +const featureColors = [ + [255, 77, 54], // red + [59, 118, 255], // blue + [91, 207, 25], // green + [217, 32, 245], // magenta + [255, 209, 41], // yellow + [52, 204, 209], // cyan +] export class DecoratorPreview extends Preview { private seed: string private perspective: string + private size: [number, number, number] + private random: seedrandom.prng + private biomeInfoNoise: PerlinNoise + private usedFeatures: string[] constructor() { super() this.seed = hexId() this.perspective = 'top' + this.size = [64, 128, 48] + this.random = seedrandom(this.seed) + this.biomeInfoNoise = new PerlinNoise(hexId(), 0, [1]) + this.usedFeatures = [] } getName() { @@ -44,21 +60,20 @@ export class DecoratorPreview extends Preview { } getSize(): [number, number] { - return this.perspective === 'top' ? [4 * 16, 3 * 16] : [4 * 16, 128] + return this.perspective === 'top' ? [this.size[0], this.size[2]] : [this.size[0], this.size[1]] } draw(model: DataModel, img: ImageData) { const featureData = JSON.parse(JSON.stringify(model.data)) - const random = seedrandom(this.seed) + this.random = seedrandom(this.seed) + this.usedFeatures = [] let placements: Placement[] = [] - for (let x = 0; x < 4; x += 1) { - for (let z = 0; z < (this.perspective === 'top' ? 3 : 1); z += 1) { - const chunkPlacements = getPlacements(random, [x * 16, 0, z * 16], featureData) + for (let x = 0; x < this.size[0]/16; x += 1) { + for (let z = 0; z < (this.perspective === 'top' ? this.size[2]/16 : 1); z += 1) { + const chunkPlacements = this.getPlacements([x * 16, 0, z * 16], featureData) const filtered = chunkPlacements.filter(p => { - return p.pos[0] >= 0 && p.pos[0] < 4 * 16 - && p.pos[1] >= 0 && p.pos[1] < 128 - && p.pos[2] >= 0 && p.pos[2] < 3 * 16 + return p.pos.every((n, i) => n >= 0 && n < this.size[i]) }) placements = [...placements, ...filtered] } @@ -68,15 +83,15 @@ export class DecoratorPreview extends Preview { img.data.fill(255) if (this.perspective === 'side') { - for (let x = 0; x < 4 * 16; x += 1) { - for (let y = 0; y < terrain[x]; y += 1) { - const i = ((127 - y) * (img.width * 4)) + (x * 4) + for (let x = 0; x < this.size[0]; x += 1) { + for (let y = 0; y < terrain[x % terrain.length]; y += 1) { + const i = ((this.size[1] - y - 1) * (img.width * 4)) + (x * 4) for (let j = 0; j < 3; j += 1) { data[i + j] = 30 } } - for (let y = terrain[x]; y < seaLevel; y += 1) { - const i = ((127 - y) * (img.width * 4)) + (x * 4) + for (let y = terrain[x % terrain.length]; y < seaLevel; y += 1) { + const i = ((this.size[1] - y - 1) * (img.width * 4)) + (x * 4) data[i + 0] = 108 data[i + 1] = 205 data[i + 2] = 230 @@ -87,13 +102,13 @@ export class DecoratorPreview extends Preview { for (let {pos, feature} of placements) { const i = this.perspective === 'top' ? (pos[2] * (img.width * 4)) + (pos[0] * 4) - : ((127 - pos[1]) * (img.width * 4)) + (pos[0] * 4) - const color = stringToColor(feature) + : ((this.size[1] - pos[1] - 1) * (img.width * 4)) + (pos[0] * 4) + const color = feature < featureColors.length ? featureColors[feature] : stringToColor(this.usedFeatures[feature]) data.set(color.map(c => clamp(50, 205, c)), i) } - for (let x = 0; x < 4 * 16; x += 1) { - for (let y = 0; y < (this.perspective === 'top' ? 3 * 16: 128); y += 1) { + for (let x = 0; x < this.size[0]; x += 1) { + for (let y = 0; y < (this.perspective === 'top' ? this.size[2]: this.size[1]); y += 1) { if ((Math.floor(x/16) + (this.perspective === 'top' ? Math.floor(y/16) : 0)) % 2 === 0) continue const i = (y * (img.width * 4)) + (x * 4) for (let j = 0; j < 3; j += 1) { @@ -102,142 +117,147 @@ export class DecoratorPreview extends Preview { } } } -} -const biomeInfoNoise = new PerlinNoise(hexId(), 0, [1]) - -function getPlacements (random: seedrandom.prng, pos: BlockPos, feature: any): Placement[] { - if (typeof feature === 'string') { - return [{ pos, feature }] + private useFeature(s: string) { + const i = this.usedFeatures.indexOf(s) + if (i != -1) return i + this.usedFeatures.push(s) + return this.usedFeatures.length - 1 } - const type = feature?.type?.replace(/^minecraft:/, '') - const featureFn = Features[type] - if (!featureFn) { - return [{ pos, feature: JSON.stringify(feature) }] + + private getPlacements (pos: BlockPos, feature: any): Placement[] { + if (typeof feature === 'string') { + return [{ pos, feature: this.useFeature(feature) }] + } + const type = feature?.type?.replace(/^minecraft:/, '') + const featureFn = this.Features[type] + if (!featureFn) { + return [{ pos, feature: this.useFeature(JSON.stringify(feature)) }] + } + return featureFn(feature.config, pos) } - return featureFn(feature.config, random, pos) -} -function getPositions (random: seedrandom.prng, pos: BlockPos, decorator: any): BlockPos[] { - const type = decorator?.type?.replace(/^minecraft:/, '') - const decoratorFn = Decorators[type] - if (!decoratorFn) { - return [pos] + private getPositions (pos: BlockPos, decorator: any): BlockPos[] { + const type = decorator?.type?.replace(/^minecraft:/, '') + const decoratorFn = this.Decorators[type] + if (!decoratorFn) { + return [pos] + } + return decoratorFn(decorator?.config, pos) } - return decoratorFn(decorator?.config, random, pos) -} -const Features: { - [key: string]: (config: any, random: seedrandom.prng, pos: BlockPos) => Placement[] -} = { - decorated: (config, random, pos) => { - const positions = getPositions(random, pos, config?.decorator) - return positions.flatMap(p => getPlacements(random, p, config?.feature)) - }, - random_boolean_selector: (config, random, pos) => { - const feature = random() < 0.5 ? config?.feature_true : config?.feature_false - return getPlacements(random, pos, feature) - }, - random_selector: (config, random, pos) => { - for (const f of config?.features ?? []) { - if (random() < (f?.chance ?? 0)) { - return getPlacements(random, pos, f.feature) + private decorateY(pos: BlockPos, y: number): BlockPos[] { + return [[ pos[0], y, pos[2] ]] + } + + private sampleUniformInt(value: any): number { + if (typeof value === 'number') { + return value + } else { + return (value.base ?? 1) + this.nextInt(1 + (value.spread ?? 0)) + } + } + + private nextInt(max: number): number { + return Math.floor(this.random() * max) + } + + private Features: { + [key: string]: (config: any, pos: BlockPos) => Placement[] + } = { + decorated: (config, pos) => { + const positions = this.getPositions(pos, config?.decorator) + return positions.flatMap(p => this.getPlacements(p, config?.feature)) + }, + random_boolean_selector: (config, pos) => { + const feature = this.random() < 0.5 ? config?.feature_true : config?.feature_false + return this.getPlacements(pos, feature) + }, + random_selector: (config, pos) => { + for (const f of config?.features ?? []) { + if (this.random() < (f?.chance ?? 0)) { + return this.getPlacements(pos, f.feature) + } } + return this.getPlacements(pos, config?.default) + }, + simple_random_selector: (config, pos) => { + const feature = config?.features?.[this.nextInt(config?.features?.length ?? 0)] + return this.getPlacements(pos, feature) } - return getPlacements(random, pos, config?.default) - }, - simple_random_selector: (config, random, pos) => { - const feature = config?.features?.[nextInt(random, config?.features?.length ?? 0)] - return getPlacements(random, pos, feature) } -} -const Decorators: { - [key: string]: (config: any, random: seedrandom.prng, pos: BlockPos) => BlockPos[] -} = { - count: (config, random, pos) => { - return new Array(sampleUniformInt(random, config?.count ?? 1)).fill(pos) - }, - count_extra: (config, random, pos) => { - let count = config?.count ?? 1 - if (random() < config.extra_chance ?? 0){ - count += config.extra_count ?? 0 + private Decorators: { + [key: string]: (config: any, pos: BlockPos) => BlockPos[] + } = { + count: (config, pos) => { + return new Array(this.sampleUniformInt(config?.count ?? 1)).fill(pos) + }, + count_extra: (config, pos) => { + let count = config?.count ?? 1 + if (this.random() < config.extra_chance ?? 0){ + count += config.extra_count ?? 0 + } + return new Array(count).fill(pos) + }, + count_noise: (config, pos) => { + const noise = this.biomeInfoNoise.getValue(pos[0] / 200, 0, pos[2] / 200) + const count = noise < config.noise_level ? config.below_noise : config.above_noise + return new Array(count).fill(pos) + }, + count_noise_biased: (config, pos) => { + const factor = Math.max(1, config.noise_factor) + const noise = this.biomeInfoNoise.getValue(pos[0] / factor, 0, pos[2] / factor) + const count = Math.max(0, Math.ceil((noise + config.noise_offset) * config.noise_to_count_ratio)) + return new Array(count).fill(pos) + }, + decorated: (config, pos) => { + return this.getPositions(pos, config?.outer).flatMap(p => { + return this.getPositions(p, config?.inner) + }) + }, + heightmap: (config, pos) => { + const y = Math.max(seaLevel, terrain[clamp(0, 63, pos[0])]) + return this.decorateY(pos, y) + }, + heightmap_spread_double: (config, pos) => { + const y = Math.max(seaLevel, terrain[clamp(0, 63, pos[0])]) + return this.decorateY(pos, this.nextInt(y * 2)) + }, + heightmap_world_surface: (config, pos) => { + const y = Math.max(seaLevel, terrain[clamp(0, 63, pos[0])]) + return this.decorateY(pos, y) + }, + range: (config, pos) => { + const y = this.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0) + return this.decorateY(pos, y) + }, + range_biased: (config, pos) => { + const y = this.nextInt(this.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) + return this.decorateY(pos, y) + }, + range_very_biased: (config, pos) => { + const y = this.nextInt(this.nextInt(this.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) + (config?.bottom_offset ?? 0)) + return this.decorateY(pos, y) + }, + spread_32_above: (config, pos) => { + const y = this.nextInt(pos[1] + 32) + return this.decorateY(pos, y) + }, + top_solid_heightmap: (config, pos) => { + const y = terrain[clamp(0, 63, pos[0])] + return this.decorateY(pos, y) + }, + magma: (config, pos) => { + const y = this.nextInt(pos[1] + 32) + return this.decorateY(pos, y) + }, + square: (config, pos) => { + return [[ + pos[0] + this.nextInt(16), + pos[1], + pos[2] + this.nextInt(16) + ]] } - return new Array(count).fill(pos) - }, - count_noise: (config, random, pos) => { - const noise = biomeInfoNoise.getValue(pos[0] / 200, 0, pos[2] / 200) - const count = noise < config.noise_level ? config.below_noise : config.above_noise - return new Array(count).fill(pos) - }, - count_noise_biased: (config, random, pos) => { - const factor = Math.max(1, config.noise_factor) - const noise = biomeInfoNoise.getValue(pos[0] / factor, 0, pos[2] / factor) - const count = Math.max(0, Math.ceil((noise + config.noise_offset) * config.noise_to_count_ratio)) - return new Array(count).fill(pos) - }, - decorated: (config, random, pos) => { - return getPositions(random, pos, config?.outer).flatMap(p => { - return getPositions(random, p, config?.inner) - }) - }, - heightmap: (config, random, pos) => { - const y = Math.max(seaLevel, terrain[clamp(0, 63, pos[0])]) - return decorateY(pos, y) - }, - heightmap_spread_double: (config, random, pos) => { - const y = Math.max(seaLevel, terrain[clamp(0, 63, pos[0])]) - return decorateY(pos, nextInt(random, y * 2)) - }, - heightmap_world_surface: (config, random, pos) => { - const y = Math.max(seaLevel, terrain[clamp(0, 63, pos[0])]) - return decorateY(pos, y) - }, - range: (config, random, pos) => { - const y = nextInt(random, (config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0) - return decorateY(pos, y) - }, - range_biased: (config, random, pos) => { - const y = nextInt(random, nextInt(random, (config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) - return decorateY(pos, y) - }, - range_very_biased: (config, random, pos) => { - const y = nextInt(random, nextInt(random, nextInt(random, (config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) + (config?.bottom_offset ?? 0)) - return decorateY(pos, y) - }, - spread_32_above: (config, random, pos) => { - const y = nextInt(random, pos[1] + 32) - return decorateY(pos, y) - }, - top_solid_heightmap: (config, random, pos) => { - const y = terrain[clamp(0, 63, pos[0])] - return decorateY(pos, y) - }, - magma: (config, random, pos) => { - const y = nextInt(random, pos[1] + 32) - return decorateY(pos, y) - }, - square: (config, random, pos) => { - return [[ - pos[0] + nextInt(random, 16), - pos[1], - pos[2] + nextInt(random, 16) - ]] } } - -function decorateY(pos: BlockPos, y: number): BlockPos[] { - return [[ pos[0], y, pos[2] ]] -} - -function sampleUniformInt(random: seedrandom.prng, value: any): number { - if (typeof value === 'number') { - return value - } else { - return (value.base ?? 1) + nextInt(random, 1 + (value.spread ?? 0)) - } -} - -function nextInt(random: seedrandom.prng, max: number): number { - return Math.floor(random() * max) -}