refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,82 @@
/**
* @esengine/procgen
*
* @zh 程序化生成工具包
* @en Procedural Generation Toolkit
*
* @zh 提供噪声函数、随机工具和蓝图节点
* @en Provides noise functions, random utilities, and blueprint nodes
*/
// =============================================================================
// Noise Functions | 噪声函数
// =============================================================================
export {
PerlinNoise,
createPerlinNoise,
SimplexNoise,
createSimplexNoise,
WorleyNoise,
createWorleyNoise,
FBM,
createFBM
} from './noise';
export type {
DistanceFunction,
INoise2D,
INoise3D,
FBMConfig
} from './noise';
// =============================================================================
// Random Utilities | 随机工具
// =============================================================================
export {
SeededRandom,
createSeededRandom,
WeightedRandom,
weightedPick,
weightedPickFromMap,
createWeightedRandom,
shuffle,
shuffleCopy,
pickOne,
sample,
sampleWithReplacement,
randomIntegers,
weightedSample
} from './random';
export type { WeightedItem } from './random';
// =============================================================================
// Blueprint Nodes | 蓝图节点
// =============================================================================
export {
// Templates
SampleNoise2DTemplate,
SampleFBMTemplate,
SeededRandomTemplate,
SeededRandomIntTemplate,
WeightedPickTemplate,
ShuffleArrayTemplate,
PickRandomTemplate,
SampleArrayTemplate,
RandomPointInCircleTemplate,
// Executors
SampleNoise2DExecutor,
SampleFBMExecutor,
SeededRandomExecutor,
SeededRandomIntExecutor,
WeightedPickExecutor,
ShuffleArrayExecutor,
PickRandomExecutor,
SampleArrayExecutor,
RandomPointInCircleExecutor,
// Collection
ProcGenNodeDefinitions
} from './nodes';

View File

@@ -0,0 +1,475 @@
/**
* @zh 程序化生成蓝图节点
* @en Procedural Generation Blueprint Nodes
*/
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
import { PerlinNoise } from '../noise/PerlinNoise';
import { SimplexNoise } from '../noise/SimplexNoise';
import { WorleyNoise } from '../noise/WorleyNoise';
import { FBM } from '../noise/FBM';
import { SeededRandom } from '../random/SeededRandom';
import { weightedPick } from '../random/WeightedRandom';
import type { WeightedItem } from '../random/WeightedRandom';
import { shuffle, pickOne, sample } from '../random/Shuffle';
// =============================================================================
// 噪声缓存 | Noise Cache
// =============================================================================
const noiseCache = new Map<string, PerlinNoise | SimplexNoise | WorleyNoise>();
const rngCache = new Map<number, SeededRandom>();
function getPerlinNoise(seed: number): PerlinNoise {
const key = `perlin_${seed}`;
if (!noiseCache.has(key)) {
noiseCache.set(key, new PerlinNoise(seed));
}
return noiseCache.get(key) as PerlinNoise;
}
function getSimplexNoise(seed: number): SimplexNoise {
const key = `simplex_${seed}`;
if (!noiseCache.has(key)) {
noiseCache.set(key, new SimplexNoise(seed));
}
return noiseCache.get(key) as SimplexNoise;
}
function getWorleyNoise(seed: number): WorleyNoise {
const key = `worley_${seed}`;
if (!noiseCache.has(key)) {
noiseCache.set(key, new WorleyNoise(seed));
}
return noiseCache.get(key) as WorleyNoise;
}
function getSeededRandom(seed: number): SeededRandom {
if (!rngCache.has(seed)) {
rngCache.set(seed, new SeededRandom(seed));
}
return rngCache.get(seed)!;
}
// =============================================================================
// 执行上下文接口 | Execution Context Interface
// =============================================================================
interface ProcGenContext {
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
}
// =============================================================================
// SampleNoise2D 节点 | SampleNoise2D Node
// =============================================================================
export const SampleNoise2DTemplate: BlueprintNodeTemplate = {
type: 'SampleNoise2D',
title: 'Sample Noise 2D',
category: 'math',
description: 'Sample 2D noise at coordinates / 在坐标处采样 2D 噪声',
keywords: ['noise', 'perlin', 'simplex', 'random', 'procedural'],
menuPath: ['Procedural', 'Noise', 'Sample Noise 2D'],
isPure: true,
inputs: [
{ name: 'x', displayName: 'X', type: 'float' },
{ name: 'y', displayName: 'Y', type: 'float' },
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'noiseType', displayName: 'Type', type: 'string' }
],
outputs: [
{ name: 'value', displayName: 'Value', type: 'float' }
],
color: '#9c27b0'
};
export class SampleNoise2DExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const x = ctx.evaluateInput(node.id, 'x', 0) as number;
const y = ctx.evaluateInput(node.id, 'y', 0) as number;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const noiseType = ctx.evaluateInput(node.id, 'noiseType', 'perlin') as string;
let value = 0;
switch (noiseType.toLowerCase()) {
case 'simplex':
value = getSimplexNoise(seed).noise2D(x, y);
break;
case 'worley':
value = getWorleyNoise(seed).noise2D(x, y);
break;
case 'perlin':
default:
value = getPerlinNoise(seed).noise2D(x, y);
break;
}
return { outputs: { value } };
}
}
// =============================================================================
// SampleFBM 节点 | SampleFBM Node
// =============================================================================
export const SampleFBMTemplate: BlueprintNodeTemplate = {
type: 'SampleFBM',
title: 'Sample FBM',
category: 'math',
description: 'Sample Fractal Brownian Motion noise / 采样分形布朗运动噪声',
keywords: ['noise', 'fbm', 'fractal', 'octave', 'terrain'],
menuPath: ['Procedural', 'Noise', 'Sample FBM'],
isPure: true,
inputs: [
{ name: 'x', displayName: 'X', type: 'float' },
{ name: 'y', displayName: 'Y', type: 'float' },
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'octaves', displayName: 'Octaves', type: 'int' },
{ name: 'frequency', displayName: 'Frequency', type: 'float' },
{ name: 'persistence', displayName: 'Persistence', type: 'float' }
],
outputs: [
{ name: 'value', displayName: 'Value', type: 'float' }
],
color: '#9c27b0'
};
export class SampleFBMExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const x = ctx.evaluateInput(node.id, 'x', 0) as number;
const y = ctx.evaluateInput(node.id, 'y', 0) as number;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const octaves = ctx.evaluateInput(node.id, 'octaves', 6) as number;
const frequency = ctx.evaluateInput(node.id, 'frequency', 1) as number;
const persistence = ctx.evaluateInput(node.id, 'persistence', 0.5) as number;
const noise = getPerlinNoise(seed);
const fbm = new FBM(noise, { octaves, frequency, persistence });
const value = fbm.noise2D(x, y);
return { outputs: { value } };
}
}
// =============================================================================
// SeededRandom 节点 | SeededRandom Node
// =============================================================================
export const SeededRandomTemplate: BlueprintNodeTemplate = {
type: 'SeededRandom',
title: 'Seeded Random',
category: 'math',
description: 'Generate deterministic random number / 生成确定性随机数',
keywords: ['random', 'seed', 'deterministic', 'procedural'],
menuPath: ['Procedural', 'Random', 'Seeded Random'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'index', displayName: 'Index', type: 'int' }
],
outputs: [
{ name: 'value', displayName: 'Value', type: 'float' }
],
color: '#9c27b0'
};
export class SeededRandomExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
// Create deterministic value from seed and index
const rng = new SeededRandom(seed + index * 12345);
const value = rng.next();
return { outputs: { value } };
}
}
// =============================================================================
// SeededRandomInt 节点 | SeededRandomInt Node
// =============================================================================
export const SeededRandomIntTemplate: BlueprintNodeTemplate = {
type: 'SeededRandomInt',
title: 'Seeded Random Int',
category: 'math',
description: 'Generate deterministic random integer / 生成确定性随机整数',
keywords: ['random', 'seed', 'integer', 'deterministic'],
menuPath: ['Procedural', 'Random', 'Seeded Random Int'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'index', displayName: 'Index', type: 'int' },
{ name: 'min', displayName: 'Min', type: 'int' },
{ name: 'max', displayName: 'Max', type: 'int' }
],
outputs: [
{ name: 'value', displayName: 'Value', type: 'int' }
],
color: '#9c27b0'
};
export class SeededRandomIntExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
const min = ctx.evaluateInput(node.id, 'min', 0) as number;
const max = ctx.evaluateInput(node.id, 'max', 100) as number;
const rng = new SeededRandom(seed + index * 12345);
const value = rng.nextInt(min, max);
return { outputs: { value } };
}
}
// =============================================================================
// WeightedPick 节点 | WeightedPick Node
// =============================================================================
export const WeightedPickTemplate: BlueprintNodeTemplate = {
type: 'WeightedPick',
title: 'Weighted Pick',
category: 'math',
description: 'Pick from weighted options / 从加权选项中选择',
keywords: ['random', 'weight', 'pick', 'select', 'loot'],
menuPath: ['Procedural', 'Random', 'Weighted Pick'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'index', displayName: 'Index', type: 'int' },
{ name: 'items', displayName: 'Items', type: 'array' },
{ name: 'weights', displayName: 'Weights', type: 'array' }
],
outputs: [
{ name: 'value', displayName: 'Value', type: 'any' },
{ name: 'selectedIndex', displayName: 'Index', type: 'int' }
],
color: '#9c27b0'
};
export class WeightedPickExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
const items = ctx.evaluateInput(node.id, 'items', []) as unknown[];
const weights = ctx.evaluateInput(node.id, 'weights', []) as number[];
if (items.length === 0) {
return { outputs: { value: null, selectedIndex: -1 } };
}
const rng = new SeededRandom(seed + index * 12345);
// Build weighted items
const weightedItems: WeightedItem<{ value: unknown; index: number }>[] = items.map((item, i) => ({
value: { value: item, index: i },
weight: weights[i] ?? 1
}));
const result = weightedPick(weightedItems, rng);
return { outputs: { value: result.value, selectedIndex: result.index } };
}
}
// =============================================================================
// ShuffleArray 节点 | ShuffleArray Node
// =============================================================================
export const ShuffleArrayTemplate: BlueprintNodeTemplate = {
type: 'ShuffleArray',
title: 'Shuffle Array',
category: 'math',
description: 'Shuffle array with seed / 使用种子洗牌数组',
keywords: ['random', 'shuffle', 'array', 'order'],
menuPath: ['Procedural', 'Random', 'Shuffle Array'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'array', displayName: 'Array', type: 'array' }
],
outputs: [
{ name: 'result', displayName: 'Result', type: 'array' }
],
color: '#9c27b0'
};
export class ShuffleArrayExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const array = ctx.evaluateInput(node.id, 'array', []) as unknown[];
const rng = new SeededRandom(seed);
const result = shuffle([...array], rng);
return { outputs: { result } };
}
}
// =============================================================================
// PickRandom 节点 | PickRandom Node
// =============================================================================
export const PickRandomTemplate: BlueprintNodeTemplate = {
type: 'PickRandom',
title: 'Pick Random',
category: 'math',
description: 'Pick random element from array / 从数组中随机选择元素',
keywords: ['random', 'pick', 'array', 'select'],
menuPath: ['Procedural', 'Random', 'Pick Random'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'index', displayName: 'Index', type: 'int' },
{ name: 'array', displayName: 'Array', type: 'array' }
],
outputs: [
{ name: 'value', displayName: 'Value', type: 'any' }
],
color: '#9c27b0'
};
export class PickRandomExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
const array = ctx.evaluateInput(node.id, 'array', []) as unknown[];
if (array.length === 0) {
return { outputs: { value: null } };
}
const rng = new SeededRandom(seed + index * 12345);
const value = pickOne(array, rng);
return { outputs: { value } };
}
}
// =============================================================================
// SampleArray 节点 | SampleArray Node
// =============================================================================
export const SampleArrayTemplate: BlueprintNodeTemplate = {
type: 'SampleArray',
title: 'Sample Array',
category: 'math',
description: 'Sample N unique elements from array / 从数组中采样 N 个不重复元素',
keywords: ['random', 'sample', 'array', 'unique'],
menuPath: ['Procedural', 'Random', 'Sample Array'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'array', displayName: 'Array', type: 'array' },
{ name: 'count', displayName: 'Count', type: 'int' }
],
outputs: [
{ name: 'result', displayName: 'Result', type: 'array' }
],
color: '#9c27b0'
};
export class SampleArrayExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const array = ctx.evaluateInput(node.id, 'array', []) as unknown[];
const count = ctx.evaluateInput(node.id, 'count', 1) as number;
if (array.length === 0 || count <= 0) {
return { outputs: { result: [] } };
}
const rng = new SeededRandom(seed);
const actualCount = Math.min(count, array.length);
const result = sample(array, actualCount, rng);
return { outputs: { result } };
}
}
// =============================================================================
// RandomPointInCircle 节点 | RandomPointInCircle Node
// =============================================================================
export const RandomPointInCircleTemplate: BlueprintNodeTemplate = {
type: 'RandomPointInCircle',
title: 'Random Point In Circle',
category: 'math',
description: 'Generate random point inside circle / 在圆内生成随机点',
keywords: ['random', 'point', 'circle', 'position'],
menuPath: ['Procedural', 'Random', 'Random Point In Circle'],
isPure: true,
inputs: [
{ name: 'seed', displayName: 'Seed', type: 'int' },
{ name: 'index', displayName: 'Index', type: 'int' },
{ name: 'centerX', displayName: 'Center X', type: 'float' },
{ name: 'centerY', displayName: 'Center Y', type: 'float' },
{ name: 'radius', displayName: 'Radius', type: 'float' }
],
outputs: [
{ name: 'x', displayName: 'X', type: 'float' },
{ name: 'y', displayName: 'Y', type: 'float' }
],
color: '#9c27b0'
};
export class RandomPointInCircleExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as ProcGenContext;
const seed = ctx.evaluateInput(node.id, 'seed', 0) as number;
const index = ctx.evaluateInput(node.id, 'index', 0) as number;
const centerX = ctx.evaluateInput(node.id, 'centerX', 0) as number;
const centerY = ctx.evaluateInput(node.id, 'centerY', 0) as number;
const radius = ctx.evaluateInput(node.id, 'radius', 1) as number;
const rng = new SeededRandom(seed + index * 12345);
const point = rng.nextPointInCircle(radius);
return {
outputs: {
x: centerX + point.x,
y: centerY + point.y
}
};
}
}
// =============================================================================
// 节点定义集合 | Node Definition Collection
// =============================================================================
export const ProcGenNodeDefinitions = {
templates: [
SampleNoise2DTemplate,
SampleFBMTemplate,
SeededRandomTemplate,
SeededRandomIntTemplate,
WeightedPickTemplate,
ShuffleArrayTemplate,
PickRandomTemplate,
SampleArrayTemplate,
RandomPointInCircleTemplate
],
executors: new Map<string, INodeExecutor>([
['SampleNoise2D', new SampleNoise2DExecutor()],
['SampleFBM', new SampleFBMExecutor()],
['SeededRandom', new SeededRandomExecutor()],
['SeededRandomInt', new SeededRandomIntExecutor()],
['WeightedPick', new WeightedPickExecutor()],
['ShuffleArray', new ShuffleArrayExecutor()],
['PickRandom', new PickRandomExecutor()],
['SampleArray', new SampleArrayExecutor()],
['RandomPointInCircle', new RandomPointInCircleExecutor()]
])
};

View File

@@ -0,0 +1,29 @@
/**
* @zh 程序化生成蓝图节点模块
* @en Procedural Generation Blueprint Nodes Module
*/
export {
// Templates
SampleNoise2DTemplate,
SampleFBMTemplate,
SeededRandomTemplate,
SeededRandomIntTemplate,
WeightedPickTemplate,
ShuffleArrayTemplate,
PickRandomTemplate,
SampleArrayTemplate,
RandomPointInCircleTemplate,
// Executors
SampleNoise2DExecutor,
SampleFBMExecutor,
SeededRandomExecutor,
SeededRandomIntExecutor,
WeightedPickExecutor,
ShuffleArrayExecutor,
PickRandomExecutor,
SampleArrayExecutor,
RandomPointInCircleExecutor,
// Collection
ProcGenNodeDefinitions
} from './ProcGenNodes';

View File

@@ -0,0 +1,214 @@
/**
* @zh 分形布朗运动 (FBM)
* @en Fractal Brownian Motion (FBM)
*
* @zh 通过叠加多层噪声创建更自然的效果
* @en Creates more natural effects by layering multiple noise octaves
*/
/**
* @zh 噪声函数接口
* @en Noise function interface
*/
export interface INoise2D {
noise2D(x: number, y: number): number;
}
/**
* @zh 噪声函数接口 (3D)
* @en Noise function interface (3D)
*/
export interface INoise3D {
noise3D(x: number, y: number, z: number): number;
}
/**
* @zh FBM 配置
* @en FBM configuration
*/
export interface FBMConfig {
/**
* @zh 八度数(层数)
* @en Number of octaves (layers)
*/
octaves: number;
/**
* @zh 频率倍增因子
* @en Frequency multiplier per octave
*/
lacunarity: number;
/**
* @zh 振幅衰减因子
* @en Amplitude decay per octave
*/
persistence: number;
/**
* @zh 初始频率
* @en Initial frequency
*/
frequency: number;
/**
* @zh 初始振幅
* @en Initial amplitude
*/
amplitude: number;
}
const DEFAULT_CONFIG: FBMConfig = {
octaves: 6,
lacunarity: 2.0,
persistence: 0.5,
frequency: 1.0,
amplitude: 1.0
};
/**
* @zh FBM 噪声生成器
* @en FBM noise generator
*/
export class FBM {
private readonly _noise: INoise2D & Partial<INoise3D>;
private readonly _config: FBMConfig;
/**
* @zh 创建 FBM 噪声生成器
* @en Create FBM noise generator
*
* @param noise - @zh 基础噪声函数 @en Base noise function
* @param config - @zh 配置 @en Configuration
*/
constructor(noise: INoise2D & Partial<INoise3D>, config?: Partial<FBMConfig>) {
this._noise = noise;
this._config = { ...DEFAULT_CONFIG, ...config };
}
/**
* @zh 2D FBM 噪声
* @en 2D FBM noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @returns @zh 噪声值 @en Noise value
*/
noise2D(x: number, y: number): number {
let value = 0;
let frequency = this._config.frequency;
let amplitude = this._config.amplitude;
let maxValue = 0;
for (let i = 0; i < this._config.octaves; i++) {
value += this._noise.noise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= this._config.persistence;
frequency *= this._config.lacunarity;
}
return value / maxValue;
}
/**
* @zh 3D FBM 噪声
* @en 3D FBM noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param z - @zh Z 坐标 @en Z coordinate
* @returns @zh 噪声值 @en Noise value
*/
noise3D(x: number, y: number, z: number): number {
if (!this._noise.noise3D) {
throw new Error('Base noise does not support 3D');
}
let value = 0;
let frequency = this._config.frequency;
let amplitude = this._config.amplitude;
let maxValue = 0;
for (let i = 0; i < this._config.octaves; i++) {
value += this._noise.noise3D(x * frequency, y * frequency, z * frequency) * amplitude;
maxValue += amplitude;
amplitude *= this._config.persistence;
frequency *= this._config.lacunarity;
}
return value / maxValue;
}
/**
* @zh Ridged FBM脊状适合山脉
* @en Ridged FBM (suitable for mountains)
*/
ridged2D(x: number, y: number): number {
let value = 0;
let frequency = this._config.frequency;
let amplitude = this._config.amplitude;
let weight = 1;
for (let i = 0; i < this._config.octaves; i++) {
let signal = this._noise.noise2D(x * frequency, y * frequency);
signal = 1 - Math.abs(signal);
signal *= signal;
signal *= weight;
weight = Math.max(0, Math.min(1, signal * 2));
value += signal * amplitude;
frequency *= this._config.lacunarity;
amplitude *= this._config.persistence;
}
return value;
}
/**
* @zh Turbulence湍流使用绝对值
* @en Turbulence (using absolute value)
*/
turbulence2D(x: number, y: number): number {
let value = 0;
let frequency = this._config.frequency;
let amplitude = this._config.amplitude;
let maxValue = 0;
for (let i = 0; i < this._config.octaves; i++) {
value += Math.abs(this._noise.noise2D(x * frequency, y * frequency)) * amplitude;
maxValue += amplitude;
amplitude *= this._config.persistence;
frequency *= this._config.lacunarity;
}
return value / maxValue;
}
/**
* @zh Billowed膨胀适合云朵
* @en Billowed (suitable for clouds)
*/
billowed2D(x: number, y: number): number {
let value = 0;
let frequency = this._config.frequency;
let amplitude = this._config.amplitude;
let maxValue = 0;
for (let i = 0; i < this._config.octaves; i++) {
const n = this._noise.noise2D(x * frequency, y * frequency);
value += (Math.abs(n) * 2 - 1) * amplitude;
maxValue += amplitude;
amplitude *= this._config.persistence;
frequency *= this._config.lacunarity;
}
return value / maxValue;
}
}
/**
* @zh 创建 FBM 噪声生成器
* @en Create FBM noise generator
*/
export function createFBM(noise: INoise2D & Partial<INoise3D>, config?: Partial<FBMConfig>): FBM {
return new FBM(noise, config);
}

View File

@@ -0,0 +1,160 @@
/**
* @zh Perlin 噪声实现
* @en Perlin Noise Implementation
*
* @zh 基于 Ken Perlin 的改进版噪声算法
* @en Based on Ken Perlin's improved noise algorithm
*/
/**
* @zh Perlin 噪声生成器
* @en Perlin noise generator
*/
export class PerlinNoise {
private readonly _perm: Uint8Array;
private readonly _gradP: Float32Array;
/**
* @zh 创建 Perlin 噪声生成器
* @en Create Perlin noise generator
*
* @param seed - @zh 随机种子 @en Random seed
*/
constructor(seed: number = 0) {
this._perm = new Uint8Array(512);
this._gradP = new Float32Array(512 * 3);
this._seed(seed);
}
private _seed(seed: number): void {
const p = new Uint8Array(256);
// Initialize permutation array
for (let i = 0; i < 256; i++) {
p[i] = i;
}
// Shuffle using seed
let n = seed;
for (let i = 255; i > 0; i--) {
n = (n * 16807) % 2147483647;
const j = n % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
// Duplicate permutation array
for (let i = 0; i < 512; i++) {
this._perm[i] = p[i & 255];
}
// Precompute gradient vectors
const grad3 = [
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0,
1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1,
0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1
];
for (let i = 0; i < 512; i++) {
const gi = (this._perm[i] % 12) * 3;
this._gradP[i * 3] = grad3[gi];
this._gradP[i * 3 + 1] = grad3[gi + 1];
this._gradP[i * 3 + 2] = grad3[gi + 2];
}
}
private _fade(t: number): number {
return t * t * t * (t * (t * 6 - 15) + 10);
}
private _lerp(a: number, b: number, t: number): number {
return a + t * (b - a);
}
private _dot2(gi: number, x: number, y: number): number {
return this._gradP[gi * 3] * x + this._gradP[gi * 3 + 1] * y;
}
private _dot3(gi: number, x: number, y: number, z: number): number {
return this._gradP[gi * 3] * x + this._gradP[gi * 3 + 1] * y + this._gradP[gi * 3 + 2] * z;
}
/**
* @zh 2D Perlin 噪声
* @en 2D Perlin noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
*/
noise2D(x: number, y: number): number {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
const u = this._fade(x);
const v = this._fade(y);
const A = this._perm[X] + Y;
const B = this._perm[X + 1] + Y;
return this._lerp(
this._lerp(this._dot2(this._perm[A], x, y), this._dot2(this._perm[B], x - 1, y), u),
this._lerp(this._dot2(this._perm[A + 1], x, y - 1), this._dot2(this._perm[B + 1], x - 1, y - 1), u),
v
);
}
/**
* @zh 3D Perlin 噪声
* @en 3D Perlin noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param z - @zh Z 坐标 @en Z coordinate
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
*/
noise3D(x: number, y: number, z: number): number {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const Z = Math.floor(z) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
z -= Math.floor(z);
const u = this._fade(x);
const v = this._fade(y);
const w = this._fade(z);
const A = this._perm[X] + Y;
const AA = this._perm[A] + Z;
const AB = this._perm[A + 1] + Z;
const B = this._perm[X + 1] + Y;
const BA = this._perm[B] + Z;
const BB = this._perm[B + 1] + Z;
return this._lerp(
this._lerp(
this._lerp(this._dot3(this._perm[AA], x, y, z), this._dot3(this._perm[BA], x - 1, y, z), u),
this._lerp(this._dot3(this._perm[AB], x, y - 1, z), this._dot3(this._perm[BB], x - 1, y - 1, z), u),
v
),
this._lerp(
this._lerp(this._dot3(this._perm[AA + 1], x, y, z - 1), this._dot3(this._perm[BA + 1], x - 1, y, z - 1), u),
this._lerp(this._dot3(this._perm[AB + 1], x, y - 1, z - 1), this._dot3(this._perm[BB + 1], x - 1, y - 1, z - 1), u),
v
),
w
);
}
}
/**
* @zh 创建 Perlin 噪声生成器
* @en Create Perlin noise generator
*/
export function createPerlinNoise(seed: number = 0): PerlinNoise {
return new PerlinNoise(seed);
}

View File

@@ -0,0 +1,222 @@
/**
* @zh Simplex 噪声实现
* @en Simplex Noise Implementation
*
* @zh 比 Perlin 噪声更快且没有方向性伪影
* @en Faster than Perlin noise with no directional artifacts
*/
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const F3 = 1 / 3;
const G3 = 1 / 6;
/**
* @zh Simplex 噪声生成器
* @en Simplex noise generator
*/
export class SimplexNoise {
private readonly _perm: Uint8Array;
private readonly _permMod12: Uint8Array;
private static readonly _grad3 = new Float32Array([
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0,
1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1,
0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1
]);
/**
* @zh 创建 Simplex 噪声生成器
* @en Create Simplex noise generator
*
* @param seed - @zh 随机种子 @en Random seed
*/
constructor(seed: number = 0) {
this._perm = new Uint8Array(512);
this._permMod12 = new Uint8Array(512);
this._seed(seed);
}
private _seed(seed: number): void {
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) {
p[i] = i;
}
let n = seed;
for (let i = 255; i > 0; i--) {
n = (n * 16807) % 2147483647;
const j = n % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
for (let i = 0; i < 512; i++) {
this._perm[i] = p[i & 255];
this._permMod12[i] = this._perm[i] % 12;
}
}
/**
* @zh 2D Simplex 噪声
* @en 2D Simplex noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
*/
noise2D(x: number, y: number): number {
const grad3 = SimplexNoise._grad3;
let n0 = 0, n1 = 0, n2 = 0;
const s = (x + y) * F2;
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const t = (i + j) * G2;
const X0 = i - t;
const Y0 = j - t;
const x0 = x - X0;
const y0 = y - Y0;
let i1: number, j1: number;
if (x0 > y0) {
i1 = 1;
j1 = 0;
} else {
i1 = 0;
j1 = 1;
}
const x1 = x0 - i1 + G2;
const y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2;
const y2 = y0 - 1 + 2 * G2;
const ii = i & 255;
const jj = j & 255;
const gi0 = this._permMod12[ii + this._perm[jj]] * 3;
const gi1 = this._permMod12[ii + i1 + this._perm[jj + j1]] * 3;
const gi2 = this._permMod12[ii + 1 + this._perm[jj + 1]] * 3;
let t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0) {
t0 *= t0;
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0);
}
let t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 >= 0) {
t1 *= t1;
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
}
let t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 >= 0) {
t2 *= t2;
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
}
return 70 * (n0 + n1 + n2);
}
/**
* @zh 3D Simplex 噪声
* @en 3D Simplex noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param z - @zh Z 坐标 @en Z coordinate
* @returns @zh 噪声值 [-1, 1] @en Noise value [-1, 1]
*/
noise3D(x: number, y: number, z: number): number {
const grad3 = SimplexNoise._grad3;
let n0 = 0, n1 = 0, n2 = 0, n3 = 0;
const s = (x + y + z) * F3;
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const k = Math.floor(z + s);
const t = (i + j + k) * G3;
const X0 = i - t;
const Y0 = j - t;
const Z0 = k - t;
const x0 = x - X0;
const y0 = y - Y0;
const z0 = z - Z0;
let i1: number, j1: number, k1: number;
let i2: number, j2: number, k2: number;
if (x0 >= y0) {
if (y0 >= z0) {
i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0;
} else if (x0 >= z0) {
i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1;
} else {
i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1;
}
} else {
if (y0 < z0) {
i1 = 0; j1 = 0; k1 = 1; i2 = 0; j2 = 1; k2 = 1;
} else if (x0 < z0) {
i1 = 0; j1 = 1; k1 = 0; i2 = 0; j2 = 1; k2 = 1;
} else {
i1 = 0; j1 = 1; k1 = 0; i2 = 1; j2 = 1; k2 = 0;
}
}
const x1 = x0 - i1 + G3;
const y1 = y0 - j1 + G3;
const z1 = z0 - k1 + G3;
const x2 = x0 - i2 + 2 * G3;
const y2 = y0 - j2 + 2 * G3;
const z2 = z0 - k2 + 2 * G3;
const x3 = x0 - 1 + 3 * G3;
const y3 = y0 - 1 + 3 * G3;
const z3 = z0 - 1 + 3 * G3;
const ii = i & 255;
const jj = j & 255;
const kk = k & 255;
const gi0 = this._permMod12[ii + this._perm[jj + this._perm[kk]]] * 3;
const gi1 = this._permMod12[ii + i1 + this._perm[jj + j1 + this._perm[kk + k1]]] * 3;
const gi2 = this._permMod12[ii + i2 + this._perm[jj + j2 + this._perm[kk + k2]]] * 3;
const gi3 = this._permMod12[ii + 1 + this._perm[jj + 1 + this._perm[kk + 1]]] * 3;
let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
if (t0 >= 0) {
t0 *= t0;
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0 + grad3[gi0 + 2] * z0);
}
let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
if (t1 >= 0) {
t1 *= t1;
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1 + grad3[gi1 + 2] * z1);
}
let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
if (t2 >= 0) {
t2 *= t2;
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2 + grad3[gi2 + 2] * z2);
}
let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
if (t3 >= 0) {
t3 *= t3;
n3 = t3 * t3 * (grad3[gi3] * x3 + grad3[gi3 + 1] * y3 + grad3[gi3 + 2] * z3);
}
return 32 * (n0 + n1 + n2 + n3);
}
}
/**
* @zh 创建 Simplex 噪声生成器
* @en Create Simplex noise generator
*/
export function createSimplexNoise(seed: number = 0): SimplexNoise {
return new SimplexNoise(seed);
}

View File

@@ -0,0 +1,196 @@
/**
* @zh Worley (Cellular) 噪声实现
* @en Worley (Cellular) Noise Implementation
*
* @zh 基于 Voronoi 图的噪声,适合生成细胞、石头纹理
* @en Voronoi-based noise, suitable for cellular and stone textures
*/
/**
* @zh 距离函数类型
* @en Distance function type
*/
export type DistanceFunction = 'euclidean' | 'manhattan' | 'chebyshev';
/**
* @zh Worley 噪声生成器
* @en Worley noise generator
*/
export class WorleyNoise {
private readonly _seed: number;
private readonly _distanceFunc: DistanceFunction;
/**
* @zh 创建 Worley 噪声生成器
* @en Create Worley noise generator
*
* @param seed - @zh 随机种子 @en Random seed
* @param distanceFunc - @zh 距离函数 @en Distance function
*/
constructor(seed: number = 0, distanceFunc: DistanceFunction = 'euclidean') {
this._seed = seed;
this._distanceFunc = distanceFunc;
}
private _hash(x: number, y: number, z: number = 0): number {
let h = this._seed;
h ^= x * 374761393;
h ^= y * 668265263;
h ^= z * 1274126177;
h = Math.imul(h ^ (h >>> 13), 1274126177);
return h;
}
private _randomPoint(cellX: number, cellY: number, index: number): { x: number; y: number } {
const h1 = this._hash(cellX, cellY, index);
const h2 = this._hash(cellX, cellY, index + 1000);
return {
x: cellX + (h1 & 0xFFFF) / 65536,
y: cellY + (h2 & 0xFFFF) / 65536
};
}
private _randomPoint3D(cellX: number, cellY: number, cellZ: number, index: number): { x: number; y: number; z: number } {
const h1 = this._hash(cellX, cellY, cellZ * 1000 + index);
const h2 = this._hash(cellX, cellY, cellZ * 1000 + index + 1000);
const h3 = this._hash(cellX, cellY, cellZ * 1000 + index + 2000);
return {
x: cellX + (h1 & 0xFFFF) / 65536,
y: cellY + (h2 & 0xFFFF) / 65536,
z: cellZ + (h3 & 0xFFFF) / 65536
};
}
private _distance2D(x1: number, y1: number, x2: number, y2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
switch (this._distanceFunc) {
case 'manhattan':
return Math.abs(dx) + Math.abs(dy);
case 'chebyshev':
return Math.max(Math.abs(dx), Math.abs(dy));
case 'euclidean':
default:
return Math.sqrt(dx * dx + dy * dy);
}
}
private _distance3D(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
const dz = z2 - z1;
switch (this._distanceFunc) {
case 'manhattan':
return Math.abs(dx) + Math.abs(dy) + Math.abs(dz);
case 'chebyshev':
return Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
case 'euclidean':
default:
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
}
/**
* @zh 2D Worley 噪声
* @en 2D Worley noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param pointsPerCell - @zh 每个单元格的点数 @en Points per cell
* @returns @zh 到最近点的距离 [0, ~1.4] @en Distance to nearest point [0, ~1.4]
*/
noise2D(x: number, y: number, pointsPerCell: number = 1): number {
const cellX = Math.floor(x);
const cellY = Math.floor(y);
let minDist = Infinity;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const cx = cellX + dx;
const cy = cellY + dy;
for (let i = 0; i < pointsPerCell; i++) {
const point = this._randomPoint(cx, cy, i);
const dist = this._distance2D(x, y, point.x, point.y);
minDist = Math.min(minDist, dist);
}
}
}
return minDist;
}
/**
* @zh 3D Worley 噪声
* @en 3D Worley noise
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param z - @zh Z 坐标 @en Z coordinate
* @param pointsPerCell - @zh 每个单元格的点数 @en Points per cell
* @returns @zh 到最近点的距离 @en Distance to nearest point
*/
noise3D(x: number, y: number, z: number, pointsPerCell: number = 1): number {
const cellX = Math.floor(x);
const cellY = Math.floor(y);
const cellZ = Math.floor(z);
let minDist = Infinity;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
for (let dz = -1; dz <= 1; dz++) {
const cx = cellX + dx;
const cy = cellY + dy;
const cz = cellZ + dz;
for (let i = 0; i < pointsPerCell; i++) {
const point = this._randomPoint3D(cx, cy, cz, i);
const dist = this._distance3D(x, y, z, point.x, point.y, point.z);
minDist = Math.min(minDist, dist);
}
}
}
}
return minDist;
}
/**
* @zh 获取到第 N 近点的距离(用于更复杂的纹理)
* @en Get distance to Nth nearest point (for more complex textures)
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param n - @zh 第 N 近 (1 = 最近) @en Nth nearest (1 = nearest)
* @returns @zh 距离值 @en Distance value
*/
nthNearest2D(x: number, y: number, n: number = 1): number {
const cellX = Math.floor(x);
const cellY = Math.floor(y);
const distances: number[] = [];
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const cx = cellX + dx;
const cy = cellY + dy;
const point = this._randomPoint(cx, cy, 0);
distances.push(this._distance2D(x, y, point.x, point.y));
}
}
distances.sort((a, b) => a - b);
return distances[Math.min(n - 1, distances.length - 1)];
}
}
/**
* @zh 创建 Worley 噪声生成器
* @en Create Worley noise generator
*/
export function createWorleyNoise(seed: number = 0, distanceFunc: DistanceFunction = 'euclidean'): WorleyNoise {
return new WorleyNoise(seed, distanceFunc);
}

View File

@@ -0,0 +1,11 @@
/**
* @zh 噪声函数模块
* @en Noise Functions Module
*/
export { PerlinNoise, createPerlinNoise } from './PerlinNoise';
export { SimplexNoise, createSimplexNoise } from './SimplexNoise';
export { WorleyNoise, createWorleyNoise } from './WorleyNoise';
export type { DistanceFunction } from './WorleyNoise';
export { FBM, createFBM } from './FBM';
export type { INoise2D, INoise3D, FBMConfig } from './FBM';

View File

@@ -0,0 +1,192 @@
/**
* @zh 种子随机数生成器
* @en Seeded Random Number Generator
*
* @zh 基于 xorshift128+ 算法的确定性伪随机数生成器
* @en Deterministic PRNG based on xorshift128+ algorithm
*/
/**
* @zh 种子随机数生成器
* @en Seeded random number generator
*/
export class SeededRandom {
private _s0: number;
private _s1: number;
private readonly _initialS0: number;
private readonly _initialS1: number;
/**
* @zh 创建种子随机数生成器
* @en Create seeded random number generator
*
* @param seed - @zh 随机种子 @en Random seed
*/
constructor(seed: number = Date.now()) {
// Initialize with MurmurHash3 mixing
let h = seed | 0;
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
h ^= h >>> 16;
this._s0 = h >>> 0;
this._s1 = (h * 0x9e3779b9) >>> 0;
// Ensure non-zero state
if (this._s0 === 0) this._s0 = 1;
if (this._s1 === 0) this._s1 = 1;
this._initialS0 = this._s0;
this._initialS1 = this._s1;
// Warm up
for (let i = 0; i < 10; i++) {
this.next();
}
}
/**
* @zh 重置到初始状态
* @en Reset to initial state
*/
reset(): void {
this._s0 = this._initialS0;
this._s1 = this._initialS1;
for (let i = 0; i < 10; i++) {
this.next();
}
}
/**
* @zh 生成下一个随机数 [0, 1)
* @en Generate next random number [0, 1)
*/
next(): number {
let s1 = this._s0;
const s0 = this._s1;
this._s0 = s0;
s1 ^= s1 << 23;
s1 ^= s1 >>> 17;
s1 ^= s0;
s1 ^= s0 >>> 26;
this._s1 = s1;
return ((this._s0 + this._s1) >>> 0) / 4294967296;
}
/**
* @zh 生成整数 [min, max]
* @en Generate integer [min, max]
*/
nextInt(min: number, max: number): number {
return Math.floor(this.next() * (max - min + 1)) + min;
}
/**
* @zh 生成浮点数 [min, max)
* @en Generate float [min, max)
*/
nextFloat(min: number, max: number): number {
return this.next() * (max - min) + min;
}
/**
* @zh 生成布尔值
* @en Generate boolean
*
* @param probability - @zh 为 true 的概率 [0, 1] @en Probability of true [0, 1]
*/
nextBool(probability: number = 0.5): boolean {
return this.next() < probability;
}
/**
* @zh 生成正态分布随机数 (Box-Muller 变换)
* @en Generate normally distributed random number (Box-Muller transform)
*
* @param mean - @zh 均值 @en Mean
* @param stdDev - @zh 标准差 @en Standard deviation
*/
nextGaussian(mean: number = 0, stdDev: number = 1): number {
const u1 = this.next();
const u2 = this.next();
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
return z0 * stdDev + mean;
}
/**
* @zh 生成指数分布随机数
* @en Generate exponentially distributed random number
*
* @param lambda - @zh 率参数 @en Rate parameter
*/
nextExponential(lambda: number = 1): number {
return -Math.log(1 - this.next()) / lambda;
}
/**
* @zh 在圆内生成均匀分布的随机点
* @en Generate uniformly distributed random point in circle
*
* @param radius - @zh 半径 @en Radius
*/
nextPointInCircle(radius: number = 1): { x: number; y: number } {
const r = Math.sqrt(this.next()) * radius;
const theta = this.next() * 2 * Math.PI;
return {
x: r * Math.cos(theta),
y: r * Math.sin(theta)
};
}
/**
* @zh 在圆环上生成随机点
* @en Generate random point on circle
*
* @param radius - @zh 半径 @en Radius
*/
nextPointOnCircle(radius: number = 1): { x: number; y: number } {
const theta = this.next() * 2 * Math.PI;
return {
x: radius * Math.cos(theta),
y: radius * Math.sin(theta)
};
}
/**
* @zh 在球内生成均匀分布的随机点
* @en Generate uniformly distributed random point in sphere
*
* @param radius - @zh 半径 @en Radius
*/
nextPointInSphere(radius: number = 1): { x: number; y: number; z: number } {
const r = Math.cbrt(this.next()) * radius;
const theta = this.next() * 2 * Math.PI;
const phi = Math.acos(2 * this.next() - 1);
return {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.sin(phi) * Math.sin(theta),
z: r * Math.cos(phi)
};
}
/**
* @zh 生成随机方向向量
* @en Generate random direction vector
*/
nextDirection2D(): { x: number; y: number } {
const theta = this.next() * 2 * Math.PI;
return {
x: Math.cos(theta),
y: Math.sin(theta)
};
}
}
/**
* @zh 创建种子随机数生成器
* @en Create seeded random number generator
*/
export function createSeededRandom(seed?: number): SeededRandom {
return new SeededRandom(seed);
}

View File

@@ -0,0 +1,193 @@
/**
* @zh 洗牌和采样工具
* @en Shuffle and Sampling Utilities
*/
import type { SeededRandom } from './SeededRandom';
/**
* @zh Fisher-Yates 洗牌算法(原地修改)
* @en Fisher-Yates shuffle algorithm (in-place)
*
* @param array - @zh 要洗牌的数组 @en Array to shuffle
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 洗牌后的数组(同一数组)@en Shuffled array (same array)
*/
export function shuffle<T>(array: T[], rng: SeededRandom | { next(): number }): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(rng.next() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
/**
* @zh 创建洗牌后的副本(不修改原数组)
* @en Create shuffled copy (does not modify original)
*
* @param array - @zh 原数组 @en Original array
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 洗牌后的新数组 @en New shuffled array
*/
export function shuffleCopy<T>(array: readonly T[], rng: SeededRandom | { next(): number }): T[] {
return shuffle([...array], rng);
}
/**
* @zh 从数组中随机选择一个元素
* @en Pick a random element from array
*
* @param array - @zh 数组 @en Array
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 随机元素 @en Random element
*/
export function pickOne<T>(array: readonly T[], rng: SeededRandom | { next(): number }): T {
if (array.length === 0) {
throw new Error('Cannot pick from empty array');
}
return array[Math.floor(rng.next() * array.length)];
}
/**
* @zh 从数组中随机采样 N 个不重复元素
* @en Sample N unique elements from array
*
* @param array - @zh 数组 @en Array
* @param count - @zh 采样数量 @en Sample count
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 采样结果 @en Sample result
*/
export function sample<T>(array: readonly T[], count: number, rng: SeededRandom | { next(): number }): T[] {
if (count > array.length) {
throw new Error('Sample count exceeds array length');
}
if (count === array.length) {
return shuffleCopy(array, rng);
}
// For small sample sizes relative to array, use selection
if (count < array.length / 2) {
const result: T[] = [];
const indices = new Set<number>();
while (result.length < count) {
const index = Math.floor(rng.next() * array.length);
if (!indices.has(index)) {
indices.add(index);
result.push(array[index]);
}
}
return result;
}
// For large sample sizes, shuffle and take first N
return shuffleCopy(array, rng).slice(0, count);
}
/**
* @zh 从数组中随机采样 N 个元素(可重复)
* @en Sample N elements from array (with replacement)
*
* @param array - @zh 数组 @en Array
* @param count - @zh 采样数量 @en Sample count
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 采样结果 @en Sample result
*/
export function sampleWithReplacement<T>(
array: readonly T[],
count: number,
rng: SeededRandom | { next(): number }
): T[] {
if (array.length === 0) {
throw new Error('Cannot sample from empty array');
}
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(pickOne(array, rng));
}
return result;
}
/**
* @zh 生成范围内的随机整数数组(不重复)
* @en Generate array of random unique integers in range
*
* @param min - @zh 最小值(包含)@en Minimum (inclusive)
* @param max - @zh 最大值(包含)@en Maximum (inclusive)
* @param count - @zh 数量 @en Count
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 随机整数数组 @en Array of random integers
*/
export function randomIntegers(
min: number,
max: number,
count: number,
rng: SeededRandom | { next(): number }
): number[] {
const range = max - min + 1;
if (count > range) {
throw new Error('Count exceeds range');
}
// Generate all numbers and sample
const numbers: number[] = [];
for (let i = min; i <= max; i++) {
numbers.push(i);
}
return sample(numbers, count, rng);
}
/**
* @zh 按权重从数组中采样(不重复)
* @en Sample from array by weight (without replacement)
*
* @param items - @zh 项目数组 @en Item array
* @param weights - @zh 权重数组 @en Weight array
* @param count - @zh 采样数量 @en Sample count
* @param rng - @zh 随机数生成器 @en Random number generator
*/
export function weightedSample<T>(
items: readonly T[],
weights: readonly number[],
count: number,
rng: SeededRandom | { next(): number }
): T[] {
if (items.length !== weights.length) {
throw new Error('Items and weights must have same length');
}
if (count > items.length) {
throw new Error('Sample count exceeds array length');
}
const result: T[] = [];
const remaining = [...items];
const remainingWeights = [...weights];
for (let i = 0; i < count; i++) {
let totalWeight = 0;
for (const w of remainingWeights) {
totalWeight += w;
}
let r = rng.next() * totalWeight;
let selectedIndex = 0;
for (let j = 0; j < remainingWeights.length; j++) {
r -= remainingWeights[j];
if (r <= 0) {
selectedIndex = j;
break;
}
}
result.push(remaining[selectedIndex]);
remaining.splice(selectedIndex, 1);
remainingWeights.splice(selectedIndex, 1);
}
return result;
}

View File

@@ -0,0 +1,177 @@
/**
* @zh 加权随机工具
* @en Weighted Random Utilities
*/
import type { SeededRandom } from './SeededRandom';
/**
* @zh 加权项
* @en Weighted item
*/
export interface WeightedItem<T> {
/**
* @zh 项目值
* @en Item value
*/
value: T;
/**
* @zh 权重(> 0
* @en Weight (> 0)
*/
weight: number;
}
/**
* @zh 加权随机选择器
* @en Weighted random selector
*/
export class WeightedRandom<T> {
private readonly _items: WeightedItem<T>[];
private readonly _cumulativeWeights: number[];
private readonly _totalWeight: number;
/**
* @zh 创建加权随机选择器
* @en Create weighted random selector
*
* @param items - @zh 加权项数组 @en Array of weighted items
*/
constructor(items: WeightedItem<T>[]) {
if (items.length === 0) {
throw new Error('Items array cannot be empty');
}
this._items = [...items];
this._cumulativeWeights = [];
let total = 0;
for (const item of this._items) {
if (item.weight <= 0) {
throw new Error('Weights must be positive');
}
total += item.weight;
this._cumulativeWeights.push(total);
}
this._totalWeight = total;
}
/**
* @zh 随机选择一个项目
* @en Randomly select an item
*
* @param rng - @zh 随机数生成器 @en Random number generator
*/
pick(rng: SeededRandom | { next(): number }): T {
const r = rng.next() * this._totalWeight;
// Binary search for the selected item
let left = 0;
let right = this._cumulativeWeights.length - 1;
while (left < right) {
const mid = (left + right) >>> 1;
if (this._cumulativeWeights[mid] < r) {
left = mid + 1;
} else {
right = mid;
}
}
return this._items[left].value;
}
/**
* @zh 使用 Math.random 选择
* @en Pick using Math.random
*/
pickRandom(): T {
return this.pick({ next: () => Math.random() });
}
/**
* @zh 获取项目的选中概率
* @en Get selection probability of an item
*/
getProbability(index: number): number {
if (index < 0 || index >= this._items.length) {
throw new Error('Index out of bounds');
}
return this._items[index].weight / this._totalWeight;
}
/**
* @zh 获取所有项目数量
* @en Get total item count
*/
get size(): number {
return this._items.length;
}
/**
* @zh 获取总权重
* @en Get total weight
*/
get totalWeight(): number {
return this._totalWeight;
}
}
/**
* @zh 从加权数组中随机选择
* @en Pick from weighted array
*
* @param items - @zh 加权项数组 @en Array of weighted items
* @param rng - @zh 随机数生成器 @en Random number generator
*/
export function weightedPick<T>(
items: WeightedItem<T>[],
rng: SeededRandom | { next(): number }
): T {
if (items.length === 0) {
throw new Error('Items array cannot be empty');
}
let totalWeight = 0;
for (const item of items) {
totalWeight += item.weight;
}
let r = rng.next() * totalWeight;
for (const item of items) {
r -= item.weight;
if (r <= 0) {
return item.value;
}
}
return items[items.length - 1].value;
}
/**
* @zh 从权重映射中随机选择
* @en Pick from weight map
*
* @param weights - @zh 值到权重的映射 @en Map of values to weights
* @param rng - @zh 随机数生成器 @en Random number generator
*/
export function weightedPickFromMap<T extends string | number>(
weights: Record<T, number>,
rng: SeededRandom | { next(): number }
): T {
const items: WeightedItem<T>[] = [];
for (const key in weights) {
items.push({ value: key as T, weight: weights[key] });
}
return weightedPick(items, rng);
}
/**
* @zh 创建加权随机选择器
* @en Create weighted random selector
*/
export function createWeightedRandom<T>(items: WeightedItem<T>[]): WeightedRandom<T> {
return new WeightedRandom(items);
}

View File

@@ -0,0 +1,17 @@
/**
* @zh 随机工具模块
* @en Random Utilities Module
*/
export { SeededRandom, createSeededRandom } from './SeededRandom';
export { WeightedRandom, weightedPick, weightedPickFromMap, createWeightedRandom } from './WeightedRandom';
export type { WeightedItem } from './WeightedRandom';
export {
shuffle,
shuffleCopy,
pickOne,
sample,
sampleWithReplacement,
randomIntegers,
weightedSample
} from './Shuffle';