Initial commit

This commit is contained in:
ryanfitzpatrickio
2026-08-03 10:28:11 -05:00
parent 65bfc3dcb4
commit 6c08153e42
63 changed files with 15790 additions and 1 deletions
+165
View File
@@ -0,0 +1,165 @@
/**
* Body style — three appearance sliders inspired by dreamfall simhuman
* global morphs (mass / muscle / fat).
*
* Plain numbers only: Party, C path, and client mesh build all share this.
* Ranges match vibe-human MODELING_CONTROLS for body.global.*:
* mass 1 lean … +1 heavy
* muscle 1 soft … +1 muscular
* fat 0 base … +1 fat (no negative target in the reference)
*/
export const BODY_STYLE_DEFAULTS = Object.freeze({
mass: 0,
muscle: 0,
fat: 0,
});
/** Slider metadata for the kit chest UI. */
export const BODY_STYLE_SLIDERS = Object.freeze([
{
id: 'mass',
label: 'Mass',
min: -1,
max: 1,
step: 0.01,
left: 'Lean',
right: 'Heavy',
},
{
id: 'muscle',
label: 'Muscle',
min: -1,
max: 1,
step: 0.01,
left: 'Soft',
right: 'Muscular',
},
{
id: 'fat',
label: 'Fat',
min: 0,
max: 1,
step: 0.01,
left: 'Base',
right: 'Heavy',
},
]);
function clamp(v, lo, hi) {
const n = Number(v);
if (!Number.isFinite(n)) return lo;
return Math.max(lo, Math.min(hi, n));
}
/**
* Normalize a partial body bag. Missing keys become 0 (average build).
* @param {unknown} raw
* @returns {{ mass: number, muscle: number, fat: number }}
*/
export function normalizeBodyStyle(raw) {
if (!raw || typeof raw !== 'object') {
return { ...BODY_STYLE_DEFAULTS };
}
return {
mass: clamp(raw.mass, -1, 1),
muscle: clamp(raw.muscle, -1, 1),
fat: clamp(raw.fat, 0, 1),
};
}
/** True when every channel is at the neutral default. */
export function isDefaultBodyStyle(style) {
const s = normalizeBodyStyle(style);
return s.mass === 0 && s.muscle === 0 && s.fat === 0;
}
/**
* Compact wire form for full-roster snapshots (two decimals).
* @returns {{ m: number, u: number, f: number }}
*/
export function packBodyStyle(style) {
const s = normalizeBodyStyle(style);
return {
m: Math.round(s.mass * 100) / 100,
u: Math.round(s.muscle * 100) / 100,
f: Math.round(s.fat * 100) / 100,
};
}
/** Inverse of packBodyStyle — also accepts full { mass, muscle, fat }. */
export function unpackBodyStyle(raw) {
if (!raw || typeof raw !== 'object') return normalizeBodyStyle(null);
if ('mass' in raw || 'muscle' in raw || 'fat' in raw) {
return normalizeBodyStyle(raw);
}
return normalizeBodyStyle({
mass: raw.m,
muscle: raw.u,
fat: raw.f,
});
}
/**
* Ease unit strength so mid-slider stays mild and the last third punches
* harder into caricature (leaner / bulkier / more cut / fatter).
* @param {number} t 0..1
* @returns {number} 0..1
*/
function easeExtreme(t) {
const a = clamp(t, 0, 1);
// ~0.35 at half travel, 1.0 at the end — more of the gain sits near the stop.
return a * 0.35 + a * a * 0.25 + a * a * a * 0.4;
}
/** Signed ease: preserves direction, applies easeExtreme on |v|. */
function shapedSigned(v) {
if (v === 0) return 0;
return Math.sign(v) * easeExtreme(Math.abs(v));
}
/**
* Map mass/muscle/fat onto the loft physique scalars used by buildBodyGeometry.
*
* Ends of each slider push hard (ease-in + large peak gains). Mid values stay
* readable so average builds do not jump. Seeded rng still adds a small natural
* variation so two fighters with the same sliders are not voxel-identical.
*
* @param {{ mass: number, muscle: number, fat: number }} style
* @param {{ range: (a: number, b: number) => number }} rng
*/
export function physiqueFromBodyStyle(style, rng) {
const s = normalizeBodyStyle(style);
// Shaped channels: mild near 0, extreme at ±1 / 1.
const mass = shapedSigned(s.mass);
const muscle = shapedSigned(s.muscle);
const fat = easeExtreme(s.fat);
// Peak gains (at shaped = ±1 / 1) — roughly 2× the first-pass response so
// full lean / tank / soft / cut / fat reads clearly under armor.
// Overall girth: mass + fat dominate; muscle adds a little.
const bulkBase = 1 + mass * 0.32 + fat * 0.26 + muscle * 0.1;
// Waist: fat fills hard, muscle nips, mass thickens.
const waistBase = 1 + fat * 0.48 + mass * 0.2 - muscle * 0.18;
// Shoulders / chest: muscle primary.
const shoulderBase = 1 + muscle * 0.42 + mass * 0.16 + fat * 0.1;
// Arms: muscle, with a little mass/fat.
const armBase = 1 + muscle * 0.48 + mass * 0.14 + fat * 0.12;
// Legs: fat + mass, muscle secondary.
const legBase = 1 + fat * 0.36 + mass * 0.2 + muscle * 0.16;
// Head: slight only — keeps helm fit.
const headBase = 1 + mass * 0.06 + fat * 0.05;
const jitter = (base, lo = 0.97, hi = 1.03) => base * (rng?.range?.(lo, hi) ?? 1);
return {
bulk: jitter(bulkBase),
// Lower floors so full lean/soft can actually go thin.
waistF: Math.max(0.52, jitter(waistBase, 0.98, 1.02)),
shoulderF: Math.max(0.62, jitter(shoulderBase, 0.98, 1.02)),
armF: Math.max(0.58, jitter(armBase, 0.97, 1.03)),
legF: Math.max(0.6, jitter(legBase, 0.97, 1.03)),
headF: Math.max(0.85, jitter(headBase, 0.98, 1.02)),
style: s,
};
}