File size: 3,430 Bytes
f746a82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// ==========================================
// SEEDED RANDOMNESS (LCG)
// ==========================================
const LCG_MULTIPLIER = 16807;
const LCG_MODULUS = 2147483647; // 2^31 - 1
let _seed = 123456789;

export const normalizeSeed = (newSeed) => {
  const numeric = Math.floor(Number(newSeed) || 0);
  const normalized = Math.abs(numeric) % (LCG_MODULUS - 1);
  return normalized + 1;
};

export const setSeed = (newSeed) => {
  _seed = normalizeSeed(newSeed);
};

export const createSeededRandom = (seed) => {
  let localSeed = normalizeSeed(seed);
  return () => {
    localSeed = (localSeed * LCG_MULTIPLIER) % LCG_MODULUS;
    return (localSeed - 1) / (LCG_MODULUS - 1);
  };
};

export const createRandomRange = (rng) => (min, max) => min + rng() * (max - min);

export const createRandomStream = (seed) => {
  const next = createSeededRandom(seed);
  return {
    next,
    range: createRandomRange(next),
  };
};

export const mixSeed = (...parts) => {
  let hash = 2166136261;
  for (const part of parts) {
    const chunk = String(part ?? '');
    for (let i = 0; i < chunk.length; i += 1) {
      hash ^= chunk.charCodeAt(i);
      hash = Math.imul(hash, 16777619);
    }
    hash ^= 124;
    hash = Math.imul(hash, 16777619);
  }
  return normalizeSeed(hash >>> 0);
};

export const createRandomSeed = () => {
  const cryptoObj = globalThis.crypto;
  if (cryptoObj?.getRandomValues) {
    const buffer = new Uint32Array(1);
    cryptoObj.getRandomValues(buffer);
    return normalizeSeed(buffer[0]);
  }
  return normalizeSeed(Math.floor(Math.random() * (LCG_MODULUS - 1)));
};

export const seededRandom = () => {
  _seed = (_seed * LCG_MULTIPLIER) % LCG_MODULUS;
  return (_seed - 1) / (LCG_MODULUS - 1);
};

export const randomRange = (min, max) => min + seededRandom() * (max - min);

// ==========================================
// MATH UTILITIES
// ==========================================
export const clamp = (value, min, max) => Math.min(max, Math.max(min, value));

export const lerp = (a, b, t) => a + (b - a) * t;

export const damp = (current, target, smoothing, dt) => 
  lerp(current, target, 1 - Math.exp(-smoothing * dt));

// Avoids branching (if statements) using continuous modulo logic
export const wrap01 = (value) => ((value % 1) + 1) % 1;

// ==========================================
// ANGLE UTILITIES
// ==========================================
const PI2 = Math.PI * 2;

export const angleDifference = (a, b) => {
  let diff = (b - a) % PI2;
  diff = (diff + PI2) % PI2;
  return diff > Math.PI ? diff - PI2 : diff;
};

export const signedAngleXZ = (from, to) => {
  const cross = from.x * to.z - from.z * to.x;
  const dot = from.x * to.x + from.z * to.z;
  return Math.atan2(cross, dot);
};

// ==========================================
// FORMATTING UTILITIES
// ==========================================
export const formatOrdinal = (index) => {
  const n = Math.max(1, index);
  const mod100 = n % 100;
  
  if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
  
  const suffixes = ['th', 'st', 'nd', 'rd'];
  const mod10 = n % 10;
  
  return `${n}${suffixes[mod10 < 4 ? mod10 : 0]}`;
};

export const formatRaceTime = (seconds) => {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
  const millis = Math.floor((seconds % 1) * 1000).toString().padStart(3, '0');
  
  return `${mins}:${secs}.${millis}`;
};