PageSourceSearch

https://the-far-colony.netlify.app/human-behaviors.js

js the-far-colony.netlify.app collected 2026-10-03 11:29:57 UTC 24,699 bytes, 594 lines download raw bytes

1// Behavior registry for active human sites.
2
3var humanSiteBehaviors = {};
4
5function registerHumanSiteBehavior(kind, behavior) {
6    if (!kind || !behavior) return;
7    humanSiteBehaviors[kind] = behavior;
8}
9
10function getHumanSiteBehavior(siteOrKind) {
11    const kind = typeof siteOrKind === 'string'
12        ? siteOrKind
13        : siteOrKind && siteOrKind.userData && siteOrKind.userData.siteKind;
14    return kind ? humanSiteBehaviors[kind] || null : null;
15}
16
17function attachHumanSiteBehavior(site, seed) {
18    const behavior = getHumanSiteBehavior(site);
19    if (behavior && typeof behavior.attach === 'function') {
20        behavior.attach(site, seed);
21    }
22}
23
24function notifyHumanSiteAssimilated(site) {
25    const behavior = getHumanSiteBehavior(site);
26    if (behavior && typeof behavior.assimilate === 'function') {
27        behavior.assimilate(site);
28    }
29}
30
31function updateHumanSiteBehaviors(t, dt) {
32    if (!Array.isArray(humanSites)) return;
33    for (const site of humanSites) {
34        const behavior = getHumanSiteBehavior(site);
35        if (behavior && typeof behavior.update === 'function') {
36            behavior.update(site, t, dt);
37        }
38    }
39}
40
41// Drone gantry behavior.
42
43var HUMAN_GANTRY_REBUILD_TIME = 120;
44var HUMAN_GANTRY_LAUNCH_DELAY = 5.5;
45var HUMAN_ROVER_DEPLOY_DELAY = 6.0;
46var HUMAN_ROVER_REDEPLOY_TIME = 42.0;
47var HUMAN_ROVER_MAX_ACTIVE = 1;
48var HUMAN_ROVER_SCOUT_MIN_MISSION_TIME = 86.0;
49var HUMAN_POST_TUTORIAL_PASSIVE_TIME = 45.0;
50
51function isHumanGantryCombatUnlocked() {
52    return typeof window === 'undefined'
53        || typeof window.isFarColonyHumanCombatUnlocked !== 'function'
54        || window.isFarColonyHumanCombatUnlocked();
55}
56
57function getHumanGantryPostTutorialPassiveRemaining() {
58    if (typeof window === 'undefined'
59        || typeof window.getFarColonyHumanPostTutorialPassiveRemaining !== 'function') {
60        return 0;
61    }
62    const remaining = Number(window.getFarColonyHumanPostTutorialPassiveRemaining());
63    return Number.isFinite(remaining) ? Math.max(0, remaining) : 0;
64}
65
66function isHumanGantryBurnDroneAllowed() {
67    return isHumanGantryCombatUnlocked() && getHumanGantryPostTutorialPassiveRemaining() <= 0;
68}
69
70function isHumanGantryRoverAllowed() {
71    return isHumanGantryCombatUnlocked();
72}
73
74function isHumanGantryRoverPassiveMode() {
75    return isHumanGantryCombatUnlocked() && getHumanGantryPostTutorialPassiveRemaining() > 0;
76}
77
78function getHumanGantryDormantDelay(gantry) {
79    if (!gantry || !gantry.site || !gantry.site.userData) return 0;
80    const delay = Number(gantry.site.userData.dormantDelay);
81    return Number.isFinite(delay) ? Math.max(0, delay) : 0;
82}
83
84function getHumanGantryDormantRemaining(gantry) {
85    const delay = getHumanGantryDormantDelay(gantry);
86    if (delay <= 0 || !gantry || gantry.activated) return 0;
87    if (!isHumanGantryCombatUnlocked() || getHumanGantryPostTutorialPassiveRemaining() > 0) return Infinity;
88    const now = typeof getGameplayTime === 'function' ? getGameplayTime() : 0;
89    if (Number.isFinite(gantry.activationTime)) return Math.max(0, gantry.activationTime - now);
90    return delay;
91}
92
93function isHumanGantryDormant(gantry) {
94    return getHumanGantryDormantRemaining(gantry) > 0;
95}
96
97function setHumanGantryDormantHold(gantry) {
98    if (!gantry || gantry.site.userData.assimilated) return;
99    shutdownHumanGantryDrone(gantry);
100    gantry.state = 'dormant';
101    gantry.timer = 0;
102}
103
104function updateHumanGantryDormancy(gantry, t, activationClockUnlocked) {
105    const delay = getHumanGantryDormantDelay(gantry);
106    if (!gantry || delay <= 0 || gantry.activated) return false;
107    if (!activationClockUnlocked) return false;
108    if (!Number.isFinite(gantry.activationTime)) gantry.activationTime = t + delay;
109    if (t < gantry.activationTime) return false;
110    gantry.activated = true;
111    gantry.activationTime = t;
112    return true;
113}
114
115function getHumanGantryRoverMaxActive() {
116    const alert = typeof getHumanAlertLevel === 'function' ? getHumanAlertLevel() : HUMAN_ALERT_START_LEVEL;
117    return HUMAN_ROVER_MAX_ACTIVE + (alert >= 4 ? 1 : 0);
118}
119
120function getHumanGantryRoverRedeployTime(gantry) {
121    const alert = typeof getHumanAlertLevel === 'function' ? getHumanAlertLevel() : HUMAN_ALERT_START_LEVEL;
122    const pressureScale = Math.max(0, alert - 1) * 3.2;
123    return Math.max(24.0, HUMAN_ROVER_REDEPLOY_TIME - pressureScale)
124        + hash(gantry.seed, 2100 + gantry.roverSerial) * 10.0;
125}
126
127function getHumanGantryWorldLocal(gantry, key, fallbackX, fallbackY, fallbackZ) {
128    const local = new THREE.Vector3(fallbackX, fallbackY, fallbackZ);
129    const custom = gantry && gantry[key];
130    if (custom
131        && Number.isFinite(custom.x)
132        && Number.isFinite(custom.y)
133        && Number.isFinite(custom.z)) {
134        local.copy(custom);
135    }
136    gantry.site.localToWorld(local);
137    return local;
138}
139
140function getHumanGantryDockPosition(gantry) {
141    return getHumanGantryWorldLocal(gantry, 'droneDockLocal', 0.08, 1.16, 0.04);
142}
143
144function getHumanGantryRoverStart(gantry) {
145    const start = getHumanGantryWorldLocal(gantry, 'roverStartLocal', -1.66, 0, 1.32);
146    start.y = terrainHeight(start.x, start.z);
147    return start;
148}
149
150function getHumanGantryRoverPatrolPoints(gantry) {
151    return getHumanGantryRoverScoutPoints(gantry, getHumanGantryRoverStart(gantry), gantry.roverSerial + 1);
152}
153
154function clampHumanScoutPoint(point) {
155    const half = typeof FOW_HALF === 'number' ? FOW_HALF : 40;
156    const margin = 3.5;
157    return {
158        x: Math.max(-half + margin, Math.min(half - margin, point.x)),
159        z: Math.max(-half + margin, Math.min(half - margin, point.z))
160    };
161}
162
163function getHumanGantryPassiveRoverFocus(start) {
164    const playerFocus = typeof START_POS !== 'undefined' ? START_POS : { x: 0, z: 0 };
165    const dx = start.x - playerFocus.x;
166    const dz = start.z - playerFocus.z;
167    const d = Math.max(0.001, Math.sqrt(dx * dx + dz * dz));
168    const push = Math.min(24.0, Math.max(12.0, d * 0.42));
169    return clampHumanScoutPoint({
170        x: start.x + dx / d * push,
171        z: start.z + dz / d * push
172    });
173}
174
175function getHumanGantryRoverScoutPoints(gantry, start, serial) {
176    const points = [];
177    const passiveRoute = isHumanGantryRoverPassiveMode();
178    const focus = passiveRoute
179        ? getHumanGantryPassiveRoverFocus(start)
180        : (typeof START_POS !== 'undefined' ? START_POS : { x: 0, z: 0 });
181    const alert = typeof getHumanAlertLevel === 'function' ? getHumanAlertLevel() : HUMAN_ALERT_START_LEVEL;
182    const dx = focus.x - start.x;
183    const dz = focus.z - start.z;
184    const baseAngle = Math.atan2(dz, dx);
185    const sweepRange = passiveRoute ? 0.72 : (alert >= 4 ? 0.58 : 0.92);
186    const sweep = (hash(gantry.seed, 3200 + serial * 17) - 0.5) * sweepRange;
187    const angle = baseAngle + sweep;
188    const alongX = Math.cos(angle);
189    const alongZ = Math.sin(angle);
190    const sideX = -alongZ;
191    const sideZ = alongX;
192    const directDistance = Math.sqrt(dx * dx + dz * dz);
193    const count = passiveRoute ? 5 : (alert >= 5 ? 7 : 6);
194    const maxTravel = passiveRoute
195        ? Math.min(directDistance + 4.0, 24.0)
196        : Math.min(directDistance + 8.0 + alert * 1.5, 36.0);
197
198    for (let i = 0; i < count; i++) {
199        const p = (i + 1) / count;
200        const travel = Math.max(5.2, maxTravel * p);
201        const alternating = i % 2 === 0 ? 1 : -1;
202        const lateralBase = passiveRoute ? 1.5 + p * 4.2 : 2.2 + p * 6.6;
203        const lateral = alternating * lateralBase * (0.55 + hash(gantry.seed, 3310 + serial * 29 + i) * 0.90);
204        const jitter = (hash(gantry.seed, 3440 + serial * 31 + i) - 0.5) * 2.1;
205        points.push(clampHumanScoutPoint({
206            x: start.x + alongX * (travel + jitter) + sideX * lateral,
207            z: start.z + alongZ * (travel + jitter) + sideZ * lateral
208        }));
209    }
210
211    const focusOffsetAngle = baseAngle + (hash(gantry.seed, 3570 + serial) - 0.5) * 1.65;
212    points.push(clampHumanScoutPoint({
213        x: focus.x + Math.cos(focusOffsetAngle) * (5.2 + hash(gantry.seed, 3610 + serial) * 5.6),
214        z: focus.z + Math.sin(focusOffsetAngle) * (5.2 + hash(gantry.seed, 3650 + serial) * 5.6)
215    }));
216    return points;
217}
218
219function clearHumanGantryDockedDrone(gantry) {
220    if (!gantry || !gantry.dockedDrone) return;
221    disposeHumanDroneActor(gantry.dockedDrone, false);
222    gantry.dockedDrone = null;
223}
224
225function shutdownHumanGantryDrone(gantry) {
226    if (!gantry) return;
227    clearHumanGantryDockedDrone(gantry);
228    if (gantry.deployedDrone) {
229        despawnHumanBurnDrone(gantry.deployedDrone);
230        gantry.deployedDrone = null;
231    }
232}
233
234function shutdownHumanGantryRovers(gantry) {
235    if (!gantry) return;
236    for (const actor of humanRoverActors.slice()) {
237        if (actor && actor.homeSite === gantry.site) despawnHumanScoutRover(actor);
238    }
239    if (Array.isArray(gantry.roverActors)) gantry.roverActors.length = 0;
240}
241
242function setHumanGantryLaunchPrep(gantry, delay) {
243    if (!gantry) return;
244    shutdownHumanGantryDrone(gantry);
245    gantry.state = 'launch-prep';
246    gantry.timer = Math.max(0.1, Number.isFinite(delay) ? delay : HUMAN_GANTRY_LAUNCH_DELAY);
247    gantry.previewBob = 0;
248    if (!humanDroneDefinition || typeof humanDroneDefinition.create !== 'function') return;
249
250    const dock = getHumanGantryDockPosition(gantry);
251    const hoverHeight = Math.max(0.45, dock.y - terrainHeight(dock.x, dock.z));
252    const actor = humanDroneDefinition.create(getHumanDroneBuildApi(), dock.x, dock.z, gantry.seed + 6000, {
253        previewMode: true,
254        hoverHeight,
255        pathLength: 0,
256        pathAngle: gantry.patrolAngle,
257        phase: 0
258    });
259    if (!actor || !actor.root) return;
260    actor.root.position.copy(dock);
261    actor.root.rotation.set(0, gantry.droneHeading, 0);
262    actor.root.userData.tooltipRequiresSight = true;
263    gantry.dockedDrone = actor;
264}
265
266function startHumanGantryRebuild(gantry, duration) {
267    if (!gantry || gantry.site.userData.assimilated) return;
268    shutdownHumanGantryDrone(gantry);
269    gantry.state = 'rebuilding';
270    gantry.timer = Math.max(1, Number.isFinite(duration) ? duration : HUMAN_GANTRY_REBUILD_TIME);
271}
272
273function setHumanGantryLaunchHold(gantry) {
274    if (!gantry || gantry.site.userData.assimilated) return;
275    shutdownHumanGantryDrone(gantry);
276    gantry.state = 'holding';
277    gantry.timer = 0;
278}
279
280function deployHumanGantryDrone(gantry) {
281    if (!gantry || gantry.site.userData.assimilated || isHumanGantryDormant(gantry) || !isHumanGantryBurnDroneAllowed()) return null;
282    clearHumanGantryDockedDrone(gantry);
283    const patrolOrigin = getHumanGantryWorldLocal(gantry, 'patrolOriginLocal', 0.40, 0, 0.02);
284    const actor = spawnHumanBurnDrone(patrolOrigin.x, patrolOrigin.z, gantry.seed + 4000, {
285        pathAngle: gantry.patrolAngle,
286        pathLength: Number.isFinite(gantry.pathLength) ? gantry.pathLength : 4.6,
287        hoverHeight: Number.isFinite(gantry.hoverHeight) ? gantry.hoverHeight : 4.2,
288        phase: hash(gantry.seed, 991) * Math.PI * 2
289    });
290    if (!actor) return null;
291    actor.root.userData.gantrySite = gantry.site;
292    gantry.deployedDrone = actor;
293    gantry.state = 'deployed';
294    gantry.timer = 0;
295    if (typeof window !== 'undefined' && typeof window.FAR_COLONY_PLAY_SOUND === 'function') {
296        window.FAR_COLONY_PLAY_SOUND('human-drone');
297    }
298    return actor;
299}
300
301function deployHumanGantryRover(gantry) {
302    if (!gantry || gantry.site.userData.assimilated || isHumanGantryDormant(gantry) || !isHumanGantryRoverAllowed()) return null;
303    const start = getHumanGantryRoverStart(gantry);
304    const serial = ++gantry.roverSerial;
305    const route = getHumanGantryRoverScoutPoints(gantry, start, serial);
306    const routeLength = route.reduce((sum, point, index) => {
307        const previous = index === 0 ? start : route[index - 1];
308        const dx = point.x - previous.x;
309        const dz = point.z - previous.z;
310        return sum + Math.sqrt(dx * dx + dz * dz);
311    }, 0);
312    const actor = spawnHumanScoutRover(start.x, start.z, gantry.seed + 8200 + serial * 37, {
313        startX: start.x,
314        startZ: start.z,
315        homeSite: gantry.site,
316        patrolPoints: route,
317        missionDuration: Math.max(HUMAN_ROVER_SCOUT_MIN_MISSION_TIME, routeLength * 1.45),
318        phase: hash(gantry.seed, 1840 + serial) * Math.PI * 2
319    });
320    if (!actor) return null;
321    actor.root.userData.gantrySite = gantry.site;
322    gantry.roverActors.push(actor);
323    if (typeof window !== 'undefined' && typeof window.FAR_COLONY_PLAY_SOUND === 'function') {
324        window.FAR_COLONY_PLAY_SOUND('human-rover');
325    }
326    return actor;
327}
328
329function updateHumanGantryRoverSpawner(gantry, stepDt, assimilated) {
330    if (!gantry || assimilated || !humanRoverDefinition || typeof humanRoverDefinition.create !== 'function') return;
331    if (isHumanGantryDormant(gantry)) {
332        gantry.roverTimer = HUMAN_ROVER_DEPLOY_DELAY;
333        return;
334    }
335    if (!isHumanGantryRoverAllowed()) {
336        gantry.roverTimer = HUMAN_ROVER_DEPLOY_DELAY;
337        return;
338    }
339    gantry.roverActors = gantry.roverActors.filter(actor => actor && actor.root && actor.root.parent && !actor.expired);
340    if (gantry.roverActors.length >= getHumanGantryRoverMaxActive()) return;
341    gantry.roverTimer = Math.max(0, (gantry.roverTimer || 0) - stepDt);
342    if (gantry.roverTimer > 0) return;
343    const actor = deployHumanGantryRover(gantry);
344    gantry.roverTimer = getHumanGantryRoverRedeployTime(gantry);
345    if (!actor) gantry.roverTimer = 6.0;
346}
347
348function attachHumanGantrySite(site, seed) {
349    const gantry = site && site.userData && site.userData.gantryActor;
350    if (!gantry) return;
351    if (getHumanGantryDormantDelay(gantry) > 0 && !gantry.activated) {
352        setHumanGantryDormantHold(gantry);
353        return;
354    }
355    if (isHumanGantryCombatUnlocked()) {
356        const passiveRemaining = getHumanGantryPostTutorialPassiveRemaining();
357        setHumanGantryLaunchPrep(gantry, passiveRemaining > 0 ? passiveRemaining : HUMAN_GANTRY_LAUNCH_DELAY);
358    }
359    else setHumanGantryLaunchHold(gantry);
360}
361
362function assimilateHumanGantrySite(site) {
363    const gantry = site && site.userData && site.userData.gantryActor;
364    if (!gantry) return;
365    shutdownHumanGantryDrone(gantry);
366    shutdownHumanGantryRovers(gantry);
367}
368
369function updateHumanGantrySite(site, t, dt) {
370    const stepDt = Math.min(0.1, Math.max(0.0001, dt || 0.016));
371    const gantry = site && site.userData && site.userData.gantryActor;
372    if (!gantry) return;
373
374    const assimilated = !!site.userData.assimilated;
375    const combatUnlocked = isHumanGantryCombatUnlocked();
376    const passiveRemaining = getHumanGantryPostTutorialPassiveRemaining();
377    const burnDroneAllowed = combatUnlocked && passiveRemaining <= 0;
378    const activatedNow = !assimilated && updateHumanGantryDormancy(gantry, t, burnDroneAllowed);
379    const dormant = !assimilated && isHumanGantryDormant(gantry);
380    if (activatedNow && site.userData.fringeOutpost && typeof showSignal === 'function') {
381        showSignal('HUMAN FRINGE GANTRY ONLINE', true, 2200);
382        if (typeof window !== 'undefined' && typeof window.FAR_COLONY_PLAY_SOUND === 'function') {
383            window.FAR_COLONY_PLAY_SOUND('human-online');
384        }
385    }
386    if (!assimilated && !combatUnlocked) {
387        if (gantry.state !== 'holding' && gantry.state !== 'dormant') {
388            if (getHumanGantryDormantDelay(gantry) > 0) setHumanGantryDormantHold(gantry);
389            else setHumanGantryLaunchHold(gantry);
390        }
391    } else if (!assimilated && dormant) {
392        if (gantry.state !== 'dormant') setHumanGantryDormantHold(gantry);
393        gantry.timer = getHumanGantryDormantRemaining(gantry);
394    } else if (!assimilated && passiveRemaining > 0) {
395        if (gantry.state !== 'launch-prep' || !gantry.dockedDrone) {
396            setHumanGantryLaunchPrep(gantry, passiveRemaining);
397        } else {
398            gantry.timer = Math.max(0.1, passiveRemaining);
399        }
400    } else if (!assimilated && (gantry.state === 'holding' || gantry.state === 'dormant')) {
401        setHumanGantryLaunchPrep(gantry, HUMAN_GANTRY_LAUNCH_DELAY);
402    } else if (!assimilated && gantry.state === 'deployed') {
403        const active = gantry.deployedDrone;
404        if (!active || !active.root || !active.root.parent) {
405            gantry.deployedDrone = null;
406            startHumanGantryRebuild(gantry, HUMAN_GANTRY_REBUILD_TIME);
407        }
408    } else if (!assimilated && (gantry.state === 'launch-prep' || gantry.state === 'rebuilding')) {
409        gantry.timer = Math.max(0, gantry.timer - stepDt);
410        if (gantry.timer <= 0) {
411            if (gantry.state === 'rebuilding') setHumanGantryLaunchPrep(gantry, 3.5);
412            else deployHumanGantryDrone(gantry);
413        }
414    }
415
416    updateHumanGantryRoverSpawner(gantry, stepDt, assimilated);
417
418    const dormantVisual = !assimilated && gantry.state === 'dormant';
419    const activePulse = assimilated
420        ? 0.18
421        : dormantVisual
422            ? 0.16 + Math.sin(t * 1.6 + gantry.seed) * 0.035
423            : 0.56 + Math.sin(t * 4.6 + gantry.seed) * 0.12;
424    gantry.emitterGlow.scale.setScalar(gantry.state === 'rebuilding' ? 1.15 + Math.sin(t * 7.5 + gantry.seed) * 0.18 : dormantVisual ? 0.56 : 0.8);
425    gantry.emitterGlow.visible = !assimilated;
426    gantry.emitterGlow.material.opacity = assimilated
427        ? 0.08
428        : dormantVisual
429            ? 0.08 + Math.max(0, activePulse) * 0.20
430        : gantry.state === 'rebuilding'
431            ? activePulse + 0.18
432            : activePulse * 0.65;
433
434    gantry.guideGlowBars.forEach((bar, index) => {
435        bar.material.opacity = assimilated
436            ? 0.08
437            : dormantVisual
438                ? 0.10 + Math.sin(t * 1.2 + index) * 0.025
439            : gantry.state === 'rebuilding'
440                ? 0.62 + Math.sin(t * 6.2 + index * 0.9) * 0.16
441                : gantry.state === 'launch-prep'
442                    ? 0.48 + Math.sin(t * 4.2 + index) * 0.10
443                    : 0.34 + Math.sin(t * 2.8 + index) * 0.06;
444    });
445
446    gantry.berthMarkerLights.forEach((marker, index) => {
447        marker.material.opacity = assimilated
448            ? 0.07
449            : dormantVisual
450                ? 0.09 + Math.sin(t * 1.3 + index * 0.6) * 0.025
451            : gantry.state === 'rebuilding'
452                ? 0.70 + Math.sin(t * 7.0 + index * 0.6) * 0.18
453                : gantry.state === 'launch-prep'
454                    ? 0.56 + Math.sin(t * 5.2 + index * 0.7) * 0.12
455                    : 0.30 + Math.sin(t * 2.4 + index) * 0.06;
456        marker.scale.y = assimilated ? 0.65 : dormantVisual ? 0.72 : 1.0 + Math.sin(t * 4.8 + index) * 0.08;
457    });
458
459    gantry.dockGlowPlates.forEach((plate, index) => {
460        plate.material.opacity = assimilated
461            ? 0.06
462            : dormantVisual
463                ? 0.05
464            : gantry.state === 'launch-prep'
465                ? 0.42 + Math.sin(t * 3.8 + index) * 0.08
466                : gantry.state === 'rebuilding'
467                    ? 0.60 + Math.sin(t * 6.4 + index) * 0.12
468                    : 0.22;
469    });
470
471    gantry.lensPlanes.forEach((mesh, index) => {
472        mesh.position.y = (mesh.userData.baseY || mesh.position.y)
473            + Math.sin(t * (2.0 + index) + gantry.seed) * 0.02;
474    });
475
476    gantry.armPivot.rotation.z = 0;
477    gantry.armFore.rotation.z = 0;
478    gantry.armHead.rotation.x = assimilated ? 0 : gantry.state === 'rebuilding' ? Math.sin(t * 9.0 + gantry.seed) * 0.16 : 0.06;
479
480    if (gantry.dockedDrone && gantry.dockedDrone.root) {
481        const dock = getHumanGantryDockPosition(gantry);
482        const bob = gantry.state === 'launch-prep' ? 0.04 + Math.sin(t * 3.8 + gantry.seed) * 0.02 : 0.02;
483        gantry.dockedDrone.root.position.set(dock.x, dock.y + bob, dock.z);
484        gantry.dockedDrone.root.rotation.set(0, gantry.droneHeading, 0);
485    }
486}
487
488function updateHumanRimMegastructureVisuals(site, t) {
489    const actor = site && site.userData && site.userData.rimMegastructureActor;
490    if (!actor) return;
491    const assimilated = !!site.userData.assimilated;
492    const active = !assimilated;
493    const pulse = 0.5 + Math.sin(t * 3.1 + actor.seed * 0.013) * 0.5;
494
495    if (actor.drillHead) {
496        actor.drillHead.rotation.y = active ? t * 1.35 : actor.drillHead.rotation.y;
497        actor.drillHead.position.y = active ? 0.82 + Math.sin(t * 1.8 + actor.seed) * 0.035 : 0.80;
498    }
499    if (actor.drillCable) {
500        actor.drillCable.scale.y = active ? 1.0 + Math.sin(t * 1.8 + actor.seed) * 0.035 : 0.94;
501    }
502    if (actor.topDish) {
503        actor.topDish.rotation.z = Math.sin(t * 0.86 + actor.seed) * (active ? 0.055 : 0.015);
504    }
505    if (Array.isArray(actor.statusLights)) {
506        actor.statusLights.forEach((light, index) => {
507            if (!light || !light.material) return;
508            const localPulse = 0.5 + Math.sin(t * (3.6 + index * 0.18) + (light.userData.phase || 0)) * 0.5;
509            light.material.opacity = active ? 0.34 + localPulse * 0.34 : 0.08;
510            light.scale.y = active ? 0.82 + localPulse * 0.34 : 0.55;
511        });
512    }
513    if (Array.isArray(actor.drillFramePosts)) {
514        actor.drillFramePosts.forEach((post, index) => {
515            post.position.y = 1.88 + (active ? Math.sin(t * 1.1 + index * 0.8) * 0.008 : 0);
516        });
517    }
518    if (actor.lightMat) actor.lightMat.opacity = active ? 0.24 + pulse * 0.24 : 0.06;
519}
520
521function updateHumanRimMegastructureSite(site, t, dt) {
522    updateHumanGantrySite(site, t, dt);
523    updateHumanRimMegastructureVisuals(site, t);
524}
525
526function assimilateHumanRimMegastructureSite(site) {
527    assimilateHumanGantrySite(site);
528    updateHumanRimMegastructureVisuals(site, 0);
529}
530
531function updateHumanFieldRelaySite(site, t, dt) {
532    const actor = site && site.userData && site.userData.fieldRelayActor;
533    if (!actor) return;
534    const assimilated = !!site.userData.assimilated;
535    if (assimilated) {
536        const assimilatedAt = Number.isFinite(site.userData.assimilatedAt) ? site.userData.assimilatedAt : t;
537        const linger = typeof HUMAN_FIELD_RELAY_ASSIMILATED_LINGER_TIME === 'number'
538            ? HUMAN_FIELD_RELAY_ASSIMILATED_LINGER_TIME
539            : 15.0;
540        const age = Math.max(0, t - assimilatedAt);
541        if (age >= linger) {
542            if (typeof removeHumanSite === 'function') removeHumanSite(site);
543            return;
544        }
545        if (!Number.isFinite(site.userData.relayAssimilatedBaseY)) site.userData.relayAssimilatedBaseY = site.position.y;
546        const vanishT = Math.max(0, Math.min(1, (age - linger * 0.55) / Math.max(0.001, linger * 0.45)));
547        const settle = 1.0 - vanishT * 0.18;
548        site.scale.setScalar(settle);
549        site.position.y = site.userData.relayAssimilatedBaseY - vanishT * 0.10;
550    }
551    const visible = typeof isWorldVisible !== 'function' || isWorldVisible(site.position.x, site.position.z);
552    const active = !assimilated;
553    const pulse = 0.5 + Math.sin(t * 2.8 + actor.seed * 0.017) * 0.5;
554
555    if (actor.radiusRing && actor.radiusRing.material) {
556        actor.radiusRing.visible = active && visible;
557        actor.radiusRing.material.opacity = actor.radiusRing.visible ? 0.055 + pulse * 0.055 : 0;
558        actor.radiusRing.rotation.z = t * 0.18;
559        actor.radiusRing.scale.setScalar(1.0 + pulse * 0.012);
560    }
561    if (actor.lightMat) actor.lightMat.opacity = active ? 0.44 + pulse * 0.34 : 0.10;
562    if (actor.pulseLight) actor.pulseLight.scale.setScalar(active ? 0.86 + pulse * 0.38 : 0.55);
563    if (actor.head) actor.head.rotation.y = -0.22 + Math.sin(t * 0.72 + actor.seed) * 0.07;
564    if (actor.dish) actor.dish.rotation.z = Math.sin(t * 1.1 + actor.seed) * 0.025;
565    if (actor.status) actor.status.position.y = 0.62 + Math.sin(t * 4.1 + actor.seed) * (active ? 0.018 : 0.006);
566}
567
568function assimilateHumanFieldRelaySite(site) {
569    const actor = site && site.userData && site.userData.fieldRelayActor;
570    if (!actor) return;
571    if (!Number.isFinite(site.userData.assimilatedAt)) {
572        site.userData.assimilatedAt = typeof getGameplayTime === 'function' ? getGameplayTime() : 0;
573    }
574    site.userData.relayAssimilatedBaseY = site.position.y;
575    if (actor.radiusRing) actor.radiusRing.visible = false;
576    if (actor.lightMat) actor.lightMat.opacity = 0.10;
577}
578
579registerHumanSiteBehavior('drone-gantry', {
580    attach: attachHumanGantrySite,
581    assimilate: assimilateHumanGantrySite,
582    update: updateHumanGantrySite
583});
584
585registerHumanSiteBehavior('rim-megastructure', {
586    attach: attachHumanGantrySite,
587    assimilate: assimilateHumanRimMegastructureSite,
588    update: updateHumanRimMegastructureSite
589});
590
591registerHumanSiteBehavior('field-relay', {
592    assimilate: assimilateHumanFieldRelaySite,
593    update: updateHumanFieldRelaySite
594});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.