feat: add factory sample machines and robot pricing conversations

This commit is contained in:
2026-10-28 12:53:07 +03:30
parent d0da416923
commit 55d4123fe5
71 changed files with 8179 additions and 3978 deletions
+538 -390
View File
@@ -2,310 +2,410 @@ import * as THREE from 'three';
// Lightweight articulated characters; all coordinates below are in the group.
// createFamily() builds the father and child; createFamily({robot: true}) builds the Robot Market guide alone.
/**
* Creates family resources for the game.
* @param {*} options - Operation options.
* @returns {*} Operation result, when applicable.
*/
export function createFamily({ robot: robotOnly = false } = {}) {
const group = new THREE.Group();
const mat = (color) => new THREE.MeshStandardMaterial({ color, roughness: 0.82 });
const skin = mat('#edbd98'),
hair = mat('#49382d'),
shoes = mat('#273c36');
const build = {
robotOnly,
};
for (const [name, method] of Object.entries(FAMILY_METHODS))
build[name] = (...args) => method(build, ...args);
for (const initialize of FAMILY_STEPS) initialize(build);
return build.result;
}
/**
* Runs the family shape operation for this subsystem.
* @param {*} build - build.
* @param {*} parent - parent.
* @param {*} geometry - geometry.
* @param {*} material - material.
* @param {*} position - position.
* @returns {*} Operation result, when applicable.
*/
function familyShape(build, parent, geometry, material, position) {
const m = new THREE.Mesh(geometry, material);
m.position.set(...position);
m.castShadow = true;
m.receiveShadow = true;
parent.add(m);
return m;
}
/**
* Runs the family sphere operation for this subsystem.
* @param {*} build - build.
* @param {*} parent - parent.
* @param {*} r - r.
* @param {*} material - material.
* @param {*} pos - pos.
* @param {*} scale - scale.
* @returns {*} Operation result, when applicable.
*/
function familySphere(build, parent, r, material, pos, scale = [1, 1, 1]) {
const m = build.shape(parent, new THREE.SphereGeometry(r, 16, 12), material, pos);
m.scale.set(...scale);
return m;
}
/**
* Runs the family segment operation for this subsystem.
* @param {*} build - build.
* @param {*} parent - parent.
* @param {*} a - a.
* @param {*} b - b.
* @param {*} r - r.
* @param {*} material - material.
* @returns {*} Operation result, when applicable.
*/
function familySegment(build, parent, a, b, r, material) {
const mesh = build.shape(parent, new THREE.CylinderGeometry(r, r, 1, 10), material, [0, 0, 0]);
return {
mesh,
a,
b,
};
}
/**
* Runs the family update segment operation for this subsystem.
* @param {*} build - build.
* @param {*} bone - bone.
* @param {*} a - a.
* @param {*} b - b.
* @returns {*} Operation result, when applicable.
*/
function familyUpdateSegment(build, bone, a, b) {
const delta = b.clone().sub(a);
bone.mesh.position.copy(a).add(b).multiplyScalar(0.5);
bone.mesh.scale.y = delta.length();
bone.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
}
function shape(parent, geometry, material, position) {
const m = new THREE.Mesh(geometry, material);
m.position.set(...position);
m.castShadow = true;
m.receiveShadow = true;
parent.add(m);
return m;
}
function sphere(parent, r, material, pos, scale = [1, 1, 1]) {
const m = shape(parent, new THREE.SphereGeometry(r, 16, 12), material, pos);
m.scale.set(...scale);
return m;
}
function segment(parent, a, b, r, material) {
const mesh = shape(parent, new THREE.CylinderGeometry(r, r, 1, 10), material, [0, 0, 0]);
return { mesh, a, b };
}
function updateSegment(bone, a, b) {
const delta = b.clone().sub(a);
bone.mesh.position.copy(a).add(b).multiplyScalar(0.5);
bone.mesh.scale.y = delta.length();
bone.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
}
// Procedural Robot Market mascot: a hovering guide that follows the father and child.
function makeRobot(z) {
const scale = 0.69,
root = new THREE.Group(),
torso = new THREE.Group();
root.name = 'robot-market-mascot';
root.position.z = z;
group.add(root);
root.add(torso);
const teal = new THREE.MeshStandardMaterial({
color: '#079f98',
metalness: 0.4,
roughness: 0.28,
});
const white = new THREE.MeshStandardMaterial({
color: '#edf6f4',
metalness: 0.22,
roughness: 0.32,
});
const joint = new THREE.MeshStandardMaterial({
color: '#102b30',
metalness: 0.5,
roughness: 0.35,
});
const glow = new THREE.MeshStandardMaterial({
color: '#c6f7ff',
emissive: '#23bcff',
emissiveIntensity: 2.5,
roughness: 0.2,
});
const visor = new THREE.MeshStandardMaterial({
color: '#020d16',
metalness: 0.5,
roughness: 0.16,
});
sphere(torso, 0.25, teal, [0, 0.76, 0], [1, 0.65, 0.75]);
sphere(torso, 0.31, white, [0, 0.97, 0], [1, 1.05, 0.72]);
for (const side of [-1, 1]) sphere(torso, 0.17, teal, [side * 0.24, 1.03, -0.04], [0.55, 1, 1]);
shape(torso, new THREE.CylinderGeometry(0.09, 0.09, 0.12, 12), joint, [0, 1.27, 0]);
const head = new THREE.Group();
head.position.y = 1.61;
torso.add(head);
sphere(head, 0.43, teal, [0, 0, 0], [1.12, 0.91, 0.88]);
sphere(head, 0.4, white, [0, -0.015, 0.12], [1.12, 0.86, 0.77]);
sphere(head, 0.35, visor, [0, -0.015, 0.255], [1.13, 0.83, 0.48]);
const eyes = [];
function arc(parent, radius, thickness, pos, rotation = 0, material = glow) {
const line = shape(
parent,
new THREE.TorusGeometry(radius, thickness, 8, 24, Math.PI),
material,
pos,
);
line.rotation.z = rotation;
return line;
}
for (const side of [-1, 1]) {
eyes.push(arc(head, 0.065, 0.016, [side * 0.16, 0.025, 0.423]));
const ear = shape(head, new THREE.CylinderGeometry(0.14, 0.14, 0.085, 24), joint, [
side * 0.47,
0,
0,
]);
ear.rotation.z = Math.PI / 2;
const ring = shape(head, new THREE.TorusGeometry(0.105, 0.012, 8, 24), glow, [
side * 0.518,
0,
0,
]);
ring.rotation.y = Math.PI / 2;
sphere(head, 0.12, teal, [side * 0.49, 0, 0], [0.4, 1, 1]);
}
arc(head, 0.075, 0.016, [0, -0.085, 0.425], Math.PI);
sphere(head, 0.1, teal, [0, 0.38, 0], [1, 0.55, 1]);
shape(head, new THREE.CylinderGeometry(0.017, 0.022, 0.18, 10), teal, [0, 0.49, 0]);
sphere(head, 0.055, teal, [0, 0.6, 0]);
// Draw the chest mark as a texture so it remains attached to the 3D body.
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const c = canvas.getContext('2d');
if (c) {
c.strokeStyle = '#102b30';
c.fillStyle = '#102b30';
c.lineWidth = 12;
c.lineJoin = 'round';
c.beginPath();
c.moveTo(65, 40);
c.lineTo(82, 40);
c.lineTo(99, 110);
c.lineTo(170, 110);
c.lineTo(186, 58);
c.lineTo(89, 58);
c.stroke();
for (const x of [111, 162]) {
c.beginPath();
c.arc(x, 130, 9, 0, Math.PI * 2);
c.fill();
}
c.textAlign = 'center';
c.font = 'bold 34px sans-serif';
c.fillText('ROBOT', 128, 181);
c.fillText('MARKET', 128, 218);
shape(
torso,
new THREE.PlaneGeometry(0.31, 0.31),
new THREE.MeshBasicMaterial({
map: new THREE.CanvasTexture(canvas),
transparent: true,
depthWrite: false,
}),
[0, 0.99, 0.224],
);
}
}
const legs = [-1, 1].map((side) => {
const hip = new THREE.Group();
hip.position.set(side * 0.15, 0.65, 0);
root.add(hip);
sphere(hip, 0.09, joint, [0, 0, 0]);
shape(hip, new THREE.CapsuleGeometry(0.095, 0.12, 4, 12), white, [0, -0.12, 0]);
const knee = new THREE.Group();
knee.position.y = -0.25;
hip.add(knee);
sphere(knee, 0.105, teal, [0, 0, 0.035]);
shape(knee, new THREE.CapsuleGeometry(0.1, 0.12, 4, 12), white, [0, -0.13, 0]);
const foot = sphere(knee, 0.13, teal, [0, -0.29, 0.07], [1, 0.65, 1.6]);
sphere(knee, 0.13, joint, [0, -0.34, 0.07], [1.06, 0.22, 1.65]);
return { hip, knee, foot };
});
const arms = [-1, 1].map(() => ({
upper: segment(group, null, null, 0.09, teal),
lower: segment(group, null, null, 0.085, white),
hand: sphere(group, 0.09, joint, [0, 0, 0], [1, 1.1, 1]),
}));
return { root, torso, head, legs, arms, scale, z, eyes };
}
function makePerson(isChild, z) {
const scale = isChild ? 0.69 : 1.05,
root = new THREE.Group();
root.position.z = z;
group.add(root);
const torso = new THREE.Group();
root.add(torso);
const cloth = mat(isChild ? '#d98150' : '#e4c695'),
pants = mat(isChild ? '#527463' : '#496474');
// Tapered jacket, hips and neck replace the single rounded toy-like body.
shape(torso, new THREE.CylinderGeometry(0.255 * scale, 0.22 * scale, 0.62 * scale, 16), cloth, [
0,
1.2 * scale,
0,
]).scale.z = 0.72;
sphere(torso, 0.23, pants, [0, 0.88 * scale, 0], [scale, 0.55 * scale, 0.72 * scale]);
shape(torso, new THREE.CylinderGeometry(0.075 * scale, 0.09 * scale, 0.18 * scale, 12), skin, [
0,
1.59 * scale,
0,
]);
shape(torso, new THREE.BoxGeometry(0.014 * scale, 0.5 * scale, 0.014 * scale), mat('#bda77f'), [
0,
1.2 * scale,
0.19 * scale,
]);
for (const side of [-1, 1]) {
const collar = shape(
torso,
new THREE.BoxGeometry(0.12 * scale, 0.09 * scale, 0.025 * scale),
cloth,
[side * 0.07 * scale, 1.49 * scale, 0.15 * scale],
);
collar.rotation.z = side * 0.4;
}
const head = new THREE.Group();
head.position.y = 1.77 * scale;
torso.add(head);
head.scale.setScalar(isChild ? 0.9 : 0.77);
const eyes = [];
sphere(head, 0.27 * scale, skin, [0, 0, 0]);
sphere(head, 0.275 * scale, hair, [0, 0.11 * scale, -0.035 * scale], [1, 0.65, 1]);
sphere(head, 0.058 * scale, skin, [0, -0.02 * scale, 0.25 * scale]);
for (const side of [-1, 1]) {
sphere(head, 0.06 * scale, skin, [side * 0.26 * scale, 0, 0]);
sphere(
head,
0.037 * scale,
mat('#f6f1e7'),
[side * 0.09 * scale, 0.03 * scale, 0.241 * scale],
[1, 0.65, 0.5],
);
const eye = sphere(
head,
0.022 * scale,
shoes,
[side * 0.09 * scale, 0.03 * scale, 0.258 * scale],
[0.8, 1, 0.45],
);
eyes.push(eye);
const brow = shape(
head,
new THREE.BoxGeometry(0.075 * scale, 0.015 * scale, 0.015 * scale),
hair,
[side * 0.09 * scale, 0.087 * scale, 0.237 * scale],
);
brow.rotation.z = side * -0.08;
sphere(head, 0.04 * scale, mat('#d79379'), [
side * 0.15 * scale,
-0.055 * scale,
0.215 * scale,
]);
}
const smile = shape(
head,
new THREE.TorusGeometry(0.068 * scale, 0.012 * scale, 6, 14, Math.PI),
shoes,
[0, -0.075 * scale, 0.252 * scale],
// Procedural Robot Market mascot: a hovering guide that follows the father and child.
/**
* Runs the family make robot operation for this subsystem.
* @param {*} build - build.
* @param {*} z - z.
* @returns {*} Operation result, when applicable.
*/
function familyMakeRobot(build, z) {
const scale = 0.69,
root = new THREE.Group(),
torso = new THREE.Group();
root.name = 'robot-market-mascot';
root.position.z = z;
build.group.add(root);
root.add(torso);
const teal = new THREE.MeshStandardMaterial({
color: '#079f98',
metalness: 0.4,
roughness: 0.28,
});
const white = new THREE.MeshStandardMaterial({
color: '#edf6f4',
metalness: 0.22,
roughness: 0.32,
});
const joint = new THREE.MeshStandardMaterial({
color: '#102b30',
metalness: 0.5,
roughness: 0.35,
});
const glow = new THREE.MeshStandardMaterial({
color: '#c6f7ff',
emissive: '#23bcff',
emissiveIntensity: 2.5,
roughness: 0.2,
});
const visor = new THREE.MeshStandardMaterial({
color: '#020d16',
metalness: 0.5,
roughness: 0.16,
});
build.sphere(torso, 0.25, teal, [0, 0.76, 0], [1, 0.65, 0.75]);
build.sphere(torso, 0.31, white, [0, 0.97, 0], [1, 1.05, 0.72]);
for (const side of [-1, 1])
build.sphere(torso, 0.17, teal, [side * 0.24, 1.03, -0.04], [0.55, 1, 1]);
build.shape(torso, new THREE.CylinderGeometry(0.09, 0.09, 0.12, 12), joint, [0, 1.27, 0]);
const head = new THREE.Group();
head.position.y = 1.61;
torso.add(head);
build.sphere(head, 0.43, teal, [0, 0, 0], [1.12, 0.91, 0.88]);
build.sphere(head, 0.4, white, [0, -0.015, 0.12], [1.12, 0.86, 0.77]);
build.sphere(head, 0.35, visor, [0, -0.015, 0.255], [1.13, 0.83, 0.48]);
const eyes = [];
/**
* Runs the arc operation for this subsystem.
* @param {*} parent - parent.
* @param {*} radius - radius.
* @param {*} thickness - thickness.
* @param {*} pos - pos.
* @param {*} rotation - rotation.
* @param {*} material - material.
* @returns {*} Operation result, when applicable.
*/
function arc(parent, radius, thickness, pos, rotation = 0, material = glow) {
const line = build.shape(
parent,
new THREE.TorusGeometry(radius, thickness, 8, 24, Math.PI),
material,
pos,
);
smile.rotation.z = Math.PI;
if (isChild) {
sphere(head, 0.29 * scale, mat('#e8b857'), [0, 0.18 * scale, 0], [1, 0.5, 1]);
shape(head, new THREE.BoxGeometry(0.55 * scale, 0.04 * scale, 0.34 * scale), mat('#e8b857'), [
0,
0.13 * scale,
0.12 * scale,
]);
} else {
sphere(torso, 0.22, mat('#a77950'), [0, 1.2 * scale, -0.24 * scale], [1, 1.2, 0.5]);
}
const legs = [-1, 1].map((side) => {
const hip = new THREE.Group();
hip.position.set(side * 0.15 * scale, 0.83 * scale, 0);
root.add(hip);
shape(hip, new THREE.CapsuleGeometry(0.09 * scale, 0.23 * scale, 4, 10), pants, [
0,
-0.16 * scale,
0,
]);
const knee = new THREE.Group();
knee.position.y = -0.34 * scale;
hip.add(knee);
shape(knee, new THREE.CapsuleGeometry(0.08 * scale, 0.25 * scale, 4, 10), pants, [
0,
-0.16 * scale,
0,
]);
const foot = shape(knee, new THREE.SphereGeometry(0.12 * scale, 12, 8), shoes, [
0,
-0.32 * scale,
0.065 * scale,
]);
foot.scale.set(0.85, 0.65, 1.5);
return { hip, knee, foot };
});
const arms = [-1, 1].map(() => ({
upper: segment(group, null, null, 0.072 * scale, cloth),
lower: segment(group, null, null, 0.061 * scale, skin),
hand: sphere(group, 0.075 * scale, skin, [0, 0, 0]),
}));
return { root, torso, head, legs, arms, scale, z, eyes };
line.rotation.z = rotation;
return line;
}
const father = robotOnly ? null : makePerson(false, -0.45),
child = robotOnly ? null : makePerson(true, 0.45);
const robot = robotOnly ? makeRobot(0) : null;
const actors = robotOnly ? [robot] : [father, child];
// The group faces +X; `facing` is the actors' own yaw inside it (π/2 = along +X).
// `wave` raises the robot's right hand, `point` stretches it forward.
function update({
for (const side of [-1, 1]) {
eyes.push(arc(head, 0.065, 0.016, [side * 0.16, 0.025, 0.423]));
const ear = build.shape(head, new THREE.CylinderGeometry(0.14, 0.14, 0.085, 24), joint, [
side * 0.47,
0,
0,
]);
ear.rotation.z = Math.PI / 2;
const ring = build.shape(head, new THREE.TorusGeometry(0.105, 0.012, 8, 24), glow, [
side * 0.518,
0,
0,
]);
ring.rotation.y = Math.PI / 2;
build.sphere(head, 0.12, teal, [side * 0.49, 0, 0], [0.4, 1, 1]);
}
arc(head, 0.075, 0.016, [0, -0.085, 0.425], Math.PI);
build.sphere(head, 0.1, teal, [0, 0.38, 0], [1, 0.55, 1]);
build.shape(head, new THREE.CylinderGeometry(0.017, 0.022, 0.18, 10), teal, [0, 0.49, 0]);
build.sphere(head, 0.055, teal, [0, 0.6, 0]);
// Draw the chest mark as a texture so it remains attached to the 3D body.
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const c = canvas.getContext('2d');
if (c) {
c.strokeStyle = '#102b30';
c.fillStyle = '#102b30';
c.lineWidth = 12;
c.lineJoin = 'round';
c.beginPath();
c.moveTo(65, 40);
c.lineTo(82, 40);
c.lineTo(99, 110);
c.lineTo(170, 110);
c.lineTo(186, 58);
c.lineTo(89, 58);
c.stroke();
for (const x of [111, 162]) {
c.beginPath();
c.arc(x, 130, 9, 0, Math.PI * 2);
c.fill();
}
c.textAlign = 'center';
c.font = 'bold 34px sans-serif';
c.fillText('ROBOT', 128, 181);
c.fillText('MARKET', 128, 218);
build.shape(
torso,
new THREE.PlaneGeometry(0.31, 0.31),
new THREE.MeshBasicMaterial({
map: new THREE.CanvasTexture(canvas),
transparent: true,
depthWrite: false,
}),
[0, 0.99, 0.224],
);
}
}
const legs = [-1, 1].map((side) => {
const hip = new THREE.Group();
hip.position.set(side * 0.15, 0.65, 0);
root.add(hip);
build.sphere(hip, 0.09, joint, [0, 0, 0]);
build.shape(hip, new THREE.CapsuleGeometry(0.095, 0.12, 4, 12), white, [0, -0.12, 0]);
const knee = new THREE.Group();
knee.position.y = -0.25;
hip.add(knee);
build.sphere(knee, 0.105, teal, [0, 0, 0.035]);
build.shape(knee, new THREE.CapsuleGeometry(0.1, 0.12, 4, 12), white, [0, -0.13, 0]);
const foot = build.sphere(knee, 0.13, teal, [0, -0.29, 0.07], [1, 0.65, 1.6]);
build.sphere(knee, 0.13, joint, [0, -0.34, 0.07], [1.06, 0.22, 1.65]);
return {
hip,
knee,
foot,
};
});
const arms = [-1, 1].map(() => ({
upper: build.segment(build.group, null, null, 0.09, teal),
lower: build.segment(build.group, null, null, 0.085, white),
hand: build.sphere(build.group, 0.09, joint, [0, 0, 0], [1, 1.1, 1]),
}));
return {
root,
torso,
head,
legs,
arms,
scale,
z,
eyes,
};
}
/**
* Runs the family make person operation for this subsystem.
* @param {*} build - build.
* @param {*} isChild - is Child.
* @param {*} z - z.
* @returns {*} Operation result, when applicable.
*/
function familyMakePerson(build, isChild, z) {
const scale = isChild ? 0.69 : 1.05,
root = new THREE.Group();
root.position.z = z;
build.group.add(root);
const torso = new THREE.Group();
root.add(torso);
const cloth = build.mat(isChild ? '#d98150' : '#e4c695'),
pants = build.mat(isChild ? '#527463' : '#496474');
// Tapered jacket, hips and neck replace the single rounded toy-like body.
build.shape(
torso,
new THREE.CylinderGeometry(0.255 * scale, 0.22 * scale, 0.62 * scale, 16),
cloth,
[0, 1.2 * scale, 0],
).scale.z = 0.72;
build.sphere(torso, 0.23, pants, [0, 0.88 * scale, 0], [scale, 0.55 * scale, 0.72 * scale]);
build.shape(
torso,
new THREE.CylinderGeometry(0.075 * scale, 0.09 * scale, 0.18 * scale, 12),
build.skin,
[0, 1.59 * scale, 0],
);
build.shape(
torso,
new THREE.BoxGeometry(0.014 * scale, 0.5 * scale, 0.014 * scale),
build.mat('#bda77f'),
[0, 1.2 * scale, 0.19 * scale],
);
for (const side of [-1, 1]) {
const collar = build.shape(
torso,
new THREE.BoxGeometry(0.12 * scale, 0.09 * scale, 0.025 * scale),
cloth,
[side * 0.07 * scale, 1.49 * scale, 0.15 * scale],
);
collar.rotation.z = side * 0.4;
}
const head = new THREE.Group();
head.position.y = 1.77 * scale;
torso.add(head);
head.scale.setScalar(isChild ? 0.9 : 0.77);
const eyes = [];
build.sphere(head, 0.27 * scale, build.skin, [0, 0, 0]);
build.sphere(head, 0.275 * scale, build.hair, [0, 0.11 * scale, -0.035 * scale], [1, 0.65, 1]);
build.sphere(head, 0.058 * scale, build.skin, [0, -0.02 * scale, 0.25 * scale]);
for (const side of [-1, 1]) {
build.sphere(head, 0.06 * scale, build.skin, [side * 0.26 * scale, 0, 0]);
build.sphere(
head,
0.037 * scale,
build.mat('#f6f1e7'),
[side * 0.09 * scale, 0.03 * scale, 0.241 * scale],
[1, 0.65, 0.5],
);
const eye = build.sphere(
head,
0.022 * scale,
build.shoes,
[side * 0.09 * scale, 0.03 * scale, 0.258 * scale],
[0.8, 1, 0.45],
);
eyes.push(eye);
const brow = build.shape(
head,
new THREE.BoxGeometry(0.075 * scale, 0.015 * scale, 0.015 * scale),
build.hair,
[side * 0.09 * scale, 0.087 * scale, 0.237 * scale],
);
brow.rotation.z = side * -0.08;
build.sphere(head, 0.04 * scale, build.mat('#d79379'), [
side * 0.15 * scale,
-0.055 * scale,
0.215 * scale,
]);
}
const smile = build.shape(
head,
new THREE.TorusGeometry(0.068 * scale, 0.012 * scale, 6, 14, Math.PI),
build.shoes,
[0, -0.075 * scale, 0.252 * scale],
);
smile.rotation.z = Math.PI;
if (isChild) {
build.sphere(head, 0.29 * scale, build.mat('#e8b857'), [0, 0.18 * scale, 0], [1, 0.5, 1]);
build.shape(
head,
new THREE.BoxGeometry(0.55 * scale, 0.04 * scale, 0.34 * scale),
build.mat('#e8b857'),
[0, 0.13 * scale, 0.12 * scale],
);
} else {
build.sphere(torso, 0.22, build.mat('#a77950'), [0, 1.2 * scale, -0.24 * scale], [1, 1.2, 0.5]);
}
const legs = [-1, 1].map((side) => {
const hip = new THREE.Group();
hip.position.set(side * 0.15 * scale, 0.83 * scale, 0);
root.add(hip);
build.shape(hip, new THREE.CapsuleGeometry(0.09 * scale, 0.23 * scale, 4, 10), pants, [
0,
-0.16 * scale,
0,
]);
const knee = new THREE.Group();
knee.position.y = -0.34 * scale;
hip.add(knee);
build.shape(knee, new THREE.CapsuleGeometry(0.08 * scale, 0.25 * scale, 4, 10), pants, [
0,
-0.16 * scale,
0,
]);
const foot = build.shape(knee, new THREE.SphereGeometry(0.12 * scale, 12, 8), build.shoes, [
0,
-0.32 * scale,
0.065 * scale,
]);
foot.scale.set(0.85, 0.65, 1.5);
return {
hip,
knee,
foot,
};
});
const arms = [-1, 1].map(() => ({
upper: build.segment(build.group, null, null, 0.072 * scale, cloth),
lower: build.segment(build.group, null, null, 0.061 * scale, build.skin),
hand: build.sphere(build.group, 0.075 * scale, build.skin, [0, 0, 0]),
}));
return {
root,
torso,
head,
legs,
arms,
scale,
z,
eyes,
};
}
// The group faces +X; `facing` is the actors' own yaw inside it (π/2 = along +X).
// `wave` raises the robot's right hand, `point` stretches it forward.
/**
* Runs the family update operation for this subsystem.
* @param {*} build - build.
* @param {*} options1 - Operation options.
* @returns {*} Operation result, when applicable.
*/
function familyUpdate(
build,
{
time,
gait,
moving,
@@ -317,95 +417,143 @@ export function createFamily({ robot: robotOnly = false } = {}) {
wave = false,
point = false,
dt = 1 / 60,
}) {
actors.forEach((actor, index) => {
const blend = 1 - Math.exp(-dt * 7),
isRobot = actor === robot;
actor.root.position.x = THREE.MathUtils.lerp(
actor.root.position.x,
finished ? (index === 0 ? -0.55 : 0.55) : shopping && !isRobot ? [-0.55, 0.25][index] : 0,
blend,
);
// The robot hovers; it bobs faster while it is speaking.
const hover = isRobot ? 0.35 + Math.sin(time * (talking ? 6 : 2.2)) * 0.07 : 0;
actor.root.position.y = THREE.MathUtils.lerp(
actor.root.position.y,
finished ? -0.6 * actor.scale : hover,
isRobot ? 1 : blend,
);
actor.root.position.z = finished ? 0 : actor.z;
actor.root.rotation.y = facing;
const bob = moving ? Math.abs(Math.sin(gait)) * 0.022 : Math.sin(time * 2) * 0.006;
actor.torso.position.y = bob;
actor.torso.rotation.z = moving ? Math.sin(gait) * 0.018 : 0;
const blinking = Math.sin(time * 1.3 + index) > 0.993;
actor.eyes.forEach((eye) => (eye.scale.y = blinking ? 0.12 : 1));
actor.head.rotation.y = shopping
? -0.65
: isRobot
? Math.sin(time * 0.9) * 0.35
: Math.sin(time * 0.7) * 0.06;
actor.torso.rotation.x = delivery && actor === child ? 0.22 : 0;
actor.legs.forEach((leg, i) => {
const phase = gait + i * Math.PI;
const stride = isRobot ? 0.08 : 0.34;
leg.hip.rotation.x = finished ? -1.4 : moving ? Math.sin(phase) * stride : 0;
leg.foot.rotation.x = moving ? Math.sin(phase + 0.5) * 0.12 : 0;
leg.knee.rotation.x = finished
? 2.5
: isRobot
? 0.25
: moving
? Math.max(0, -Math.sin(phase)) * 0.5
: 0.05;
});
});
group.updateMatrixWorld(true);
const toFamily = (actor, point) => point.applyMatrix4(actor.root.matrix).clone();
const shoulder = (actor, side) =>
toFamily(
actor,
new THREE.Vector3(
side * 0.29 * actor.scale,
(actor === robot ? 1.12 : 1.43) * actor.scale,
0,
),
);
// With characters facing +X, father's left and child's right hands meet.
const contact = new THREE.Vector3(0, 1.06, 0);
actors.forEach((actor) =>
actor.arms.forEach((arm, i) => {
const side = i === 0 ? -1 : 1,
inside = actor === father ? side === -1 : actor === child && side === 1;
const a = shoulder(actor, side);
let target;
if (inside && !shopping && !finished) target = contact.clone();
else if (actor === child && shopping && side === -1)
target = new THREE.Vector3(0.5, delivery ? 0.65 : 1.1, -0.15);
else if (actor === robot && side === 1 && wave)
target = toFamily(actor, new THREE.Vector3(0.42, 1.62 + Math.sin(time * 9) * 0.07, 0.12));
else if (actor === robot && side === 1 && point)
target = toFamily(actor, new THREE.Vector3(0.3, 1.25, 0.62));
else
target = toFamily(
actor,
new THREE.Vector3(
side * 0.34 * actor.scale,
0.88 * actor.scale,
moving ? Math.sin(gait + (i ? Math.PI : 0)) * 0.2 : 0,
),
);
const elbow = a
.clone()
.lerp(target, 0.5)
.add(new THREE.Vector3(0.07, 0, inside ? 0 : 0.07));
updateSegment(arm.upper, a, elbow);
updateSegment(arm.lower, elbow, target);
arm.hand.position.copy(target);
}),
},
) {
build.actors.forEach((actor, index) => {
const blend = 1 - Math.exp(-dt * 7),
isRobot = actor === build.robot;
actor.root.position.x = THREE.MathUtils.lerp(
actor.root.position.x,
finished ? (index === 0 ? -0.55 : 0.55) : shopping && !isRobot ? [-0.55, 0.25][index] : 0,
blend,
);
}
update({ time: 0, gait: 0, moving: false });
return { group, update };
// The robot hovers; it bobs faster while it is speaking.
const hover = isRobot ? 0.35 + Math.sin(time * (talking ? 6 : 2.2)) * 0.07 : 0;
actor.root.position.y = THREE.MathUtils.lerp(
actor.root.position.y,
finished ? -0.6 * actor.scale : hover,
isRobot ? 1 : blend,
);
actor.root.position.z = finished ? 0 : actor.z;
actor.root.rotation.y = facing;
const bob = moving ? Math.abs(Math.sin(gait)) * 0.022 : Math.sin(time * 2) * 0.006;
actor.torso.position.y = bob;
actor.torso.rotation.z = moving ? Math.sin(gait) * 0.018 : 0;
const blinking = Math.sin(time * 1.3 + index) > 0.993;
actor.eyes.forEach((eye) => (eye.scale.y = blinking ? 0.12 : 1));
actor.head.rotation.y = shopping
? -0.65
: isRobot
? Math.sin(time * 0.9) * 0.35
: Math.sin(time * 0.7) * 0.06;
actor.torso.rotation.x = delivery && actor === build.child ? 0.22 : 0;
actor.legs.forEach((leg, i) => {
const phase = gait + i * Math.PI;
const stride = isRobot ? 0.08 : 0.34;
leg.hip.rotation.x = finished ? -1.4 : moving ? Math.sin(phase) * stride : 0;
leg.foot.rotation.x = moving ? Math.sin(phase + 0.5) * 0.12 : 0;
leg.knee.rotation.x = finished
? 2.5
: isRobot
? 0.25
: moving
? Math.max(0, -Math.sin(phase)) * 0.5
: 0.05;
});
});
build.group.updateMatrixWorld(true);
/**
* Runs the to family operation for this subsystem.
* @param {*} actor - actor.
* @param {*} point - point.
* @returns {*} Operation result, when applicable.
*/
const toFamily = (actor, point) => point.applyMatrix4(actor.root.matrix).clone();
/**
* Runs the shoulder operation for this subsystem.
* @param {*} actor - actor.
* @param {*} side - side.
* @returns {*} Operation result, when applicable.
*/
const shoulder = (actor, side) =>
toFamily(
actor,
new THREE.Vector3(
side * 0.29 * actor.scale,
(actor === build.robot ? 1.12 : 1.43) * actor.scale,
0,
),
);
// With characters facing +X, father's left and child's right hands meet.
const contact = new THREE.Vector3(0, 1.06, 0);
build.actors.forEach((actor) =>
actor.arms.forEach((arm, i) => {
const side = i === 0 ? -1 : 1,
inside = actor === build.father ? side === -1 : actor === build.child && side === 1;
const a = shoulder(actor, side);
let target;
if (inside && !shopping && !finished) target = contact.clone();
else if (actor === build.child && shopping && side === -1)
target = new THREE.Vector3(0.5, delivery ? 0.65 : 1.1, -0.15);
else if (actor === build.robot && side === 1 && wave)
target = toFamily(actor, new THREE.Vector3(0.42, 1.62 + Math.sin(time * 9) * 0.07, 0.12));
else if (actor === build.robot && side === 1 && point)
target = toFamily(actor, new THREE.Vector3(0.3, 1.25, 0.62));
else
target = toFamily(
actor,
new THREE.Vector3(
side * 0.34 * actor.scale,
0.88 * actor.scale,
moving ? Math.sin(gait + (i ? Math.PI : 0)) * 0.2 : 0,
),
);
const elbow = a
.clone()
.lerp(target, 0.5)
.add(new THREE.Vector3(0.07, 0, inside ? 0 : 0.07));
build.updateSegment(arm.upper, a, elbow);
build.updateSegment(arm.lower, elbow, target);
arm.hand.position.copy(target);
}),
);
}
/**
* Runs the family build group operation for this subsystem.
* @param {*} build - build.
* @returns {*} Operation result, when applicable.
*/
function familyBuildGroup(build) {
build.group = new THREE.Group();
build.mat = (color) =>
new THREE.MeshStandardMaterial({
color,
roughness: 0.82,
});
build.skin = build.mat('#edbd98');
build.hair = build.mat('#49382d');
build.shoes = build.mat('#273c36');
build.father = build.robotOnly ? null : build.makePerson(false, -0.45);
build.child = build.robotOnly ? null : build.makePerson(true, 0.45);
build.robot = build.robotOnly ? build.makeRobot(0) : null;
build.actors = build.robotOnly ? [build.robot] : [build.father, build.child];
build.update({
time: 0,
gait: 0,
moving: false,
});
build.result = {
group: build.group,
update: build.update,
};
}
const FAMILY_METHODS = {
shape: familyShape,
sphere: familySphere,
segment: familySegment,
updateSegment: familyUpdateSegment,
makeRobot: familyMakeRobot,
makePerson: familyMakePerson,
update: familyUpdate,
};
const FAMILY_STEPS = [familyBuildGroup];