feat(city): add humanoid worker robots as city agents

Seed five worker robot agents (TR-01 to TR-05) with Portuguese
system prompts and active status. Extend the 3D humanoid renderer
with metallic materials, glowing visor, antenna beacon, and chest
status light for the worker_robot type. Add feature tests covering
payload serialization and city bounds validation.
This commit is contained in:
2026-10-31 22:31:03 -03:00
parent 46b15ec7be
commit ecf5291027
4 changed files with 181 additions and 6 deletions
+62 -6
View File
@@ -1817,14 +1817,17 @@ function buildHumanoid(agent, showLabel = true) {
surfer: { skin: 0xf1c27d, hair: 0x3b2314, shirt: 0x3fb6c9, pants: 0x1c1c1c, scale: 1 },
basket: { skin: 0xd2956b, hair: 0x111111, shirt: 0xe8802a, pants: 0xf2f2f2, scale: 1 },
atc: { skin: 0xe0ac69, hair: 0x1a1a1a, shirt: 0x223366, pants: 0x0f1420, scale: 1.02 },
// Humanoid worker robots: brushed metal body + safety-orange vest.
worker_robot: { skin: 0xc9d1d9, hair: 0x9aa4b2, shirt: 0xd97706, pants: 0x47515e, scale: 1, metalness: 0.5 },
};
const cfgc = configs[type] || configs.pedestrian;
const s = cfgc.scale;
const metalness = cfgc.metalness ?? 0;
const skinMat = new THREE.MeshStandardMaterial({ color: cfgc.skin, roughness: 0.7 });
const hairMat = new THREE.MeshStandardMaterial({ color: cfgc.hair, roughness: 0.6 });
const shirtMat = new THREE.MeshStandardMaterial({ color: cfgc.shirt, roughness: 0.8 });
const pantsMat = new THREE.MeshStandardMaterial({ color: cfgc.pants, roughness: 0.85 });
const skinMat = new THREE.MeshStandardMaterial({ color: cfgc.skin, roughness: 0.7 - metalness * 0.4, metalness });
const hairMat = new THREE.MeshStandardMaterial({ color: cfgc.hair, roughness: 0.6, metalness });
const shirtMat = new THREE.MeshStandardMaterial({ color: cfgc.shirt, roughness: 0.8 - metalness * 0.3, metalness });
const pantsMat = new THREE.MeshStandardMaterial({ color: cfgc.pants, roughness: 0.85 - metalness * 0.3, metalness });
const shoeMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.7 });
const head = new THREE.Mesh(new THREE.SphereGeometry(0.28 * s, 12, 12), skinMat);
@@ -1837,6 +1840,50 @@ function buildHumanoid(agent, showLabel = true) {
hair.castShadow = true;
g.add(hair);
if (type === 'worker_robot') {
// Glowing visor across the face.
const visor = new THREE.Mesh(
new THREE.BoxGeometry(0.34 * s, 0.1 * s, 0.06 * s),
new THREE.MeshStandardMaterial({
color: 0x0e7490, emissive: 0x22d3ee, emissiveIntensity: 1.5, roughness: 0.3,
})
);
visor.position.set(0, 1.63 * s, 0.255 * s);
g.add(visor);
// Antenna with a blinking beacon (ramps/pulses with the sign system).
const antennaRod = new THREE.Mesh(
new THREE.CylinderGeometry(0.014, 0.02, 0.24, 6),
new THREE.MeshStandardMaterial({ color: 0x6b7280, roughness: 0.5, metalness: 0.7 })
);
antennaRod.position.set(0, 1.97 * s, 0);
g.add(antennaRod);
const beacon = new THREE.Mesh(
new THREE.SphereGeometry(0.045, 8, 8),
new THREE.MeshStandardMaterial({ color: 0xff3333, emissive: 0xff3333, emissiveIntensity: 2 })
);
beacon.position.set(0, 2.1 * s, 0);
g.add(beacon);
signMaterials.push({
mat: beacon.material, base: 1.2, nightBoost: 1.0, pulse: 0.9,
phase: Math.random() * Math.PI * 2,
});
// Chest status light (amber, softer pulse).
const chest = new THREE.Mesh(
new THREE.BoxGeometry(0.15, 0.11, 0.05),
new THREE.MeshStandardMaterial({
color: 0x78350f, emissive: 0xf59e0b, emissiveIntensity: 1.1, roughness: 0.4,
})
);
chest.position.set(0, 1.2 * s, 0.21 * s);
g.add(chest);
signMaterials.push({
mat: chest.material, base: 0.8, nightBoost: 0.7, pulse: 0.35,
phase: Math.random() * Math.PI * 2,
});
}
const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.24 * s, 0.55 * s, 4, 8), shirtMat);
torso.position.y = 1.12 * s;
torso.castShadow = true;
@@ -2029,7 +2076,7 @@ function initPedestrians(g, agents) {
if (i % 3 === 0) routes.push({ route: crossingRoute(sq, g), kind: 'cross' });
});
const pedAgents = agents.filter(a => a.type === 'pedestrian');
const pedAgents = agents.filter(a => a.type === 'pedestrian' || a.type === 'worker_robot');
pedAgents.forEach((a, i) => {
const pick = routes[i % routes.length];
const mesh = buildHumanoid(a, true);
@@ -2038,6 +2085,7 @@ function initPedestrians(g, agents) {
const w = {
mesh, route: pick.route, wp: 0, speed: 1.15 + Math.random() * 0.4,
wait: Math.random() * 2, pauseAt: 4 + Math.random() * 8, pauseTimer: 0,
robot: a.type === 'worker_robot',
};
const start = w.route.waypoints[0];
mesh.position.set(start.x, 0, start.z);
@@ -4909,7 +4957,7 @@ function buildCity(data) {
roofBatch.flush(worldGroup);
// ---- Agents (mayor, shopkeepers handled in their systems; pedestrians walk) ----
const walkingTypes = new Set(['pedestrian', 'mayor', 'shopkeeper']);
const walkingTypes = new Set(['pedestrian', 'mayor', 'shopkeeper', 'worker_robot']);
const placed = new Set();
// Shopkeepers + mayor + pedestrians get behaviours; any leftover static agents render in place.
@@ -6643,6 +6691,14 @@ window.__city10 = {
sidewalkSegments: sidewalkSegmentCount,
beach: beachSystem ? beachSystem.counts() : null,
flights: airplaneSystem ? airplaneSystem.flights() : null,
robots: pedestrianSystem
? pedestrianSystem.walkers.filter(w => w.robot).length
: 0,
robotWalkers: pedestrianSystem
? pedestrianSystem.walkers
.filter(w => w.robot)
.map(w => ({ x: +w.mesh.position.x.toFixed(1), z: +w.mesh.position.z.toFixed(1) }))
: [],
signs: {
stores: signCounts.stores,
plazas: signCounts.plazas,