1(function () { 2 const ROVER_MODEL_PATH = 'assets/models/human-rover.glb'; 3 const GLTF_LOADER_LOCAL_PATH = 'GLTFLoader.js'; 4 const GLTF_LOADER_CDN_BASE = 'https://cdn.jsdelivr.net/npm/three@'; 5 const ROVER_MODEL_SCALE = 1.45; 6 const ROVER_SPEED = 1.38; 7 const ROVER_RETURN_SPEED = 1.64; 8 const ROVER_TURN_RATE = 5.4; 9 const ROVER_WAYPOINT_RADIUS = 0.38; 10 const ROVER_SCAN_RADIUS = 5.8; 11 const ROVER_SCAN_TIME = 2.15; 12 const ROVER_SCAN_COOLDOWN = 8.5; 13 const ROVER_TARGET_COOLDOWN = 28.0; 14 const ROVER_MISSION_DURATION = 72.0; 15 const ROVER_ALERT_BASE = 12; 16 const ROVER_SCOUT_POINT_COUNT = 6; 17 const ROVER_SCOUT_STEP = 6.8; 18 const ROVER_SCOUT_LATERAL_MIN = 2.8; 19 const ROVER_SCOUT_LATERAL_MAX = 8.2; 20 const ROVER_SCOUT_EDGE_MARGIN = 3.2; 21 const ROVER_BLOCKED_TIME = 1.15; 22 const ROVER_BLOCKED_PAUSE = 1.4; 23 const ROVER_TERRAIN_NORMAL_SAMPLE = 0.82; 24 const ROVER_GROUND_ALIGN_SHARPNESS = 10.0; 25 let gltfLoaderPromise = null; 26 let roverTemplatePromise = null; 27 28 function clamp01(v) { 29 return Math.max(0, Math.min(1, v)); 30 } 31 32 function smoothstep01(v) { 33 const t = clamp01(v); 34 return t * t * (3 - 2 * t); 35 } 36 37 function normalizeAngle(angle) { 38 let out = angle; 39 while (out <= -Math.PI) out += Math.PI * 2; 40 while (out > Math.PI) out -= Math.PI * 2; 41 return out; 42 } 43 44 function turnToward(current, target, maxStep) { 45 const delta = normalizeAngle(target - current); 46 if (Math.abs(delta) <= maxStep) return target; 47 return normalizeAngle(current + Math.sign(delta) * maxStep); 48 } 49 50 function distance2D(a, b) { 51 const dx = a.x - b.x; 52 const dz = a.z - b.z; 53 return Math.sqrt(dx * dx + dz * dz); 54 } 55 56 function sampleTerrainNormalInto(target, api, x, z) { 57 const step = ROVER_TERRAIN_NORMAL_SAMPLE; 58 const hLeft = api.terrainHeight(x - step, z); 59 const hRight = api.terrainHeight(x + step, z); 60 const hBack = api.terrainHeight(x, z - step); 61 const hForward = api.terrainHeight(x, z + step); 62 target.set(hLeft - hRight, step * 2, hBack - hForward); 63 if (target.lengthSq() <= 0.000001) target.set(0, 1, 0); 64 return target.normalize(); 65 } 66 67 function updateGroundAlignedRoot(actor, api, stepDt, immediate) { 68 const pos = actor.root.position; 69 sampleTerrainNormalInto(actor.groundUp, api, pos.x, pos.z); 70 71 actor.groundForward.set(Math.sin(actor.heading), 0, Math.cos(actor.heading)); 72 actor.groundForward.addScaledVector(actor.groundUp, -actor.groundForward.dot(actor.groundUp)); 73 if (actor.groundForward.lengthSq() <= 0.000001) { 74 actor.groundForward.set(Math.sin(actor.heading), 0, Math.cos(actor.heading)); 75 } 76 actor.groundForward.normalize(); 77 actor.groundRight.crossVectors(actor.groundUp, actor.groundForward).normalize(); 78 actor.groundForward.crossVectors(actor.groundRight, actor.groundUp).normalize(); 79 actor.groundMatrix.makeBasis(actor.groundRight, actor.groundUp, actor.groundForward); 80 actor.groundQuaternion.setFromRotationMatrix(actor.groundMatrix); 81 82 if (immediate) { 83 actor.root.quaternion.copy(actor.groundQuaternion); 84 return; 85 } 86 const blend = 1 - Math.exp(-ROVER_GROUND_ALIGN_SHARPNESS * Math.max(0, stepDt || 0)); 87 actor.root.quaternion.slerp(actor.groundQuaternion, clamp01(blend)); 88 } 89 90 function getThreePackageVersion(THREE) { 91 const revision = Number(THREE && THREE.REVISION); 92 return Number.isFinite(revision) && revision > 0 ? `0.${revision}.0` : '0.128.0'; 93 } 94 95 function ensureGLTFLoader(THREE) { 96 if (THREE.GLTFLoader) return Promise.resolve(THREE.GLTFLoader); 97 if (gltfLoaderPromise) return gltfLoaderPromise; 98 99 gltfLoaderPromise = new Promise((resolve, reject) => { 100 const script = document.createElement('script'); 101 script.async = true; 102 script.onload = () => { 103 if (THREE.GLTFLoader) resolve(THREE.GLTFLoader); 104 else reject(new Error('GLTFLoader did not register on THREE.')); 105 }; 106 script.onerror = () => { 107 const fallback = document.createElement('script'); 108 fallback.src = `${GLTF_LOADER_CDN_BASE}${getThreePackageVersion(THREE)}/examples/js/loaders/GLTFLoader.js`; 109 fallback.async = true; 110 fallback.onload = () => { 111 if (THREE.GLTFLoader) resolve(THREE.GLTFLoader); 112 else reject(new Error('GLTFLoader did not register on THREE.')); 113 }; 114 fallback.onerror = () => reject(new Error('GLTFLoader could not be loaded.')); 115 document.head.appendChild(fallback); 116 }; 117 script.src = GLTF_LOADER_LOCAL_PATH; 118 document.head.appendChild(script); 119 }); 120 121 return gltfLoaderPromise; 122 } 123 124 function loadRoverTemplate(THREE) { 125 if (roverTemplatePromise) return roverTemplatePromise; 126 roverTemplatePromise = ensureGLTFLoader(THREE).then(Loader => new Promise((resolve, reject) => { 127 const loader = new Loader(); 128 loader.load( 129 ROVER_MODEL_PATH, 130 gltf => { 131 const scene = gltf.scene || (gltf.scenes && gltf.scenes[0]); 132 if (!scene) { 133 reject(new Error('Rover GLB did not contain a scene.')); 134 return; 135 } 136 scene.updateMatrixWorld(true); 137 scene.traverse(obj => { 138 if (!obj.isMesh) return; 139 obj.frustumCulled = false;
140 if (obj.material) { 141 const materials = Array.isArray(obj.material) ? obj.material : [obj.material]; 142 materials.forEach(material => { 143 if (material.map) { 144 material.map.magFilter = THREE.NearestFilter; 145 material.map.minFilter = THREE.NearestMipmapNearestFilter; 146 } 147 }); 148 } 149 }); 150 resolve(scene); 151 }, 152 undefined, 153 reject 154 ); 155 })); 156 return roverTemplatePromise; 157 } 158 159 function materialColor(material) { 160 if (material && material.color) return material.color; 161 return { r: 0.72, g: 0.74, b: 0.72 }; 162 } 163 164 function convertMaterial(source, api, THREE) { 165 const color = materialColor(source); 166 if (source && source.map && api.objTexEdgeMat) { 167 const material = api.objTexEdgeMat(source.map, color.r, color.g, color.b); 168 material.side = source.side || THREE.FrontSide; 169 return material; 170 } 171 const make = api.objEdgeMat || api.objMat; 172 return make(color.r, color.g, color.b); 173 } 174 175 function convertModelMaterials(root, actor, api, THREE) { 176 root.traverse(obj => { 177 if (!obj.isMesh) return; 178 const materials = Array.isArray(obj.material) ? obj.material : [obj.material]; 179 const converted = materials.map(material => convertMaterial(material, api, THREE)); 180 obj.material = Array.isArray(obj.material) ? converted : converted[0]; 181 actor.ownedMaterials.push(...converted); 182 }); 183 } 184 185 function makeFallbackRover(api, seed) { 186 const THREE = api.THREE; 187 const make = api.objEdgeMat || api.objMat; 188 const bodyMat = make(0.78, 0.80, 0.78); 189 const darkMat = make(0.12, 0.13, 0.14); 190 const blueMat = make(0.18, 0.45, 0.90); 191 const group = new THREE.Group(); 192 const wheels = []; 193 194 const body = new THREE.Mesh(new THREE.BoxGeometry(0.98, 0.28, 1.18), bodyMat); 195 body.position.y = 0.32; 196 group.add(body); 197 198 const cabin = new THREE.Mesh(new THREE.BoxGeometry(0.58, 0.32, 0.48), bodyMat); 199 cabin.position.set(0, 0.58, -0.08); 200 cabin.rotation.x = -0.05; 201 group.add(cabin); 202 203 const sensor = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.10, 0.14), blueMat); 204 sensor.position.set(0, 0.73, 0.34); 205 sensor.userData.noShadowCaster = true; 206 group.add(sensor); 207 208 for (const side of [-1, 1]) { 209 for (const z of [-0.38, 0.38]) { 210 const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.13, 8), darkMat); 211 wheel.position.set(side * 0.54, 0.19, z); 212 wheel.rotation.z = Math.PI * 0.5; 213 wheel.userData.baseZ = z; 214 wheel.userData.phase = api.hash(seed, z * 20 + side * 10) * Math.PI * 2; 215 group.add(wheel); 216 wheels.push(wheel); 217 } 218 } 219 220 group.userData.fallbackWheels = wheels; 221 return group; 222 } 223 224 function disposeFallback(actor, api) { 225 if (!actor || !actor.placeholder) return; 226 api.unmarkShadowCaster(actor.placeholder); 227 if (actor.placeholder.parent) actor.placeholder.parent.remove(actor.placeholder); 228 api.disposeObjectTree(actor.placeholder); 229 actor.placeholder = null; 230 actor.fallbackWheels = []; 231 } 232 233 function attachRoverModel(actor, template, api) { 234 if (!actor || actor.disposed || !actor.visualRoot) return; 235 const THREE = api.THREE; 236 disposeFallback(actor, api); 237 238 const model = template.clone(true); 239 model.scale.setScalar(ROVER_MODEL_SCALE); 240 model.position.y = 0; 241 convertModelMaterials(model, actor, api, THREE); 242 actor.visualRoot.add(model); 243 actor.model = model; 244 actor.modelLoaded = true; 245 api.markShadowCaster(actor.root); 246 } 247 248 function makeScanRing(THREE) { 249 const segments = 64; 250 const positions = []; 251 for (let i = 0; i <= segments; i++) { 252 const a = (i / segments) * Math.PI * 2; 253 positions.push(Math.cos(a), 0, Math.sin(a)); 254 } 255 const geometry = new THREE.BufferGeometry(); 256 geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); 257 const material = new THREE.LineBasicMaterial({ 258 color: 0x63a7ff, 259 transparent: true, 260 opacity: 0.0, 261 depthWrite: false 262 }); 263 const ring = new THREE.Line(geometry, material); 264 ring.visible = false;
265 ring.userData.noShadowCaster = true; 266 return ring; 267 } 268 269 function clampScoutPoint(point, api) { 270 const half = Number.isFinite(api.worldHalfSize) ? api.worldHalfSize : 40; 271 const margin = ROVER_SCOUT_EDGE_MARGIN; 272 return { 273 x: Math.max(-half + margin, Math.min(half - margin, point.x)), 274 z: Math.max(-half + margin, Math.min(half - margin, point.z)) 275 }; 276 } 277 278 function defaultScoutPoints(x, z, seed, api) { 279 const points = []; 280 const focus = api.getHumanScoutFocus ? api.getHumanScoutFocus({ x, z }, seed) : null; 281 const target = focus && Number.isFinite(focus.x) && Number.isFinite(focus.z) 282 ? focus 283 : { x: x - 18, z: z + 18 }; 284 const baseAngle = Math.atan2(target.z - z, target.x - x); 285 const count = Math.max(3, ROVER_SCOUT_POINT_COUNT); 286 const maxDistance = Math.max(ROVER_SCOUT_STEP * 2, distance2D({ x, z }, target)); 287 for (let i = 0; i < count; i++) { 288 const p = (i + 1) / count; 289 const travel = Math.min(maxDistance + 5.5, ROVER_SCOUT_STEP * (i + 1) * (0.92 + api.hash(seed, 1710 + i) * 0.22)); 290 const lateralScale = ROVER_SCOUT_LATERAL_MIN + (ROVER_SCOUT_LATERAL_MAX - ROVER_SCOUT_LATERAL_MIN) * p; 291 const lateral = (api.hash(seed, 1730 + i) - 0.5) * 2 * lateralScale; 292 const alongX = Math.cos(baseAngle); 293 const alongZ = Math.sin(baseAngle); 294 const sideX = -alongZ; 295 const sideZ = alongX; 296 points.push(clampScoutPoint({ 297 x: x + alongX * travel + sideX * lateral, 298 z: z + alongZ * travel + sideZ * lateral 299 }, api)); 300 } 301 return points; 302 } 303 304 function makeTooltip(actor) { 305 let stateText = 'Human scout'; 306 if (actor.state === 'scan') stateText = 'Surveying nearby ground'; 307 else if (actor.state === 'blocked') stateText = 'Blocked by bone wall'; 308 else if (actor.state === 'return') stateText = 'Returning to human hardware'; 309 310 const hasHull = Number.isFinite(actor.health) && Number.isFinite(actor.maxHealth) && actor.maxHealth > 0; 311 const meta = hasHull 312 ? `${stateText} | Hull ${Math.max(0, Math.ceil(actor.health))}/${Math.max(1, Math.ceil(actor.maxHealth))}` 313 : stateText; 314 return { 315 title: 'SCOUT ROVER', 316 body: 'Small human rover used to scout colony growth and send target data.', 317 meta 318 }; 319 } 320 321 function create(api, x, z, seed, options) { 322 options = options || {}; 323 const THREE = api.THREE; 324 const root = new THREE.Group(); 325 const visualRoot = new THREE.Group(); 326 root.add(visualRoot); 327 328 const scoutPoints = Array.isArray(options.patrolPoints) && options.patrolPoints.length 329 ? options.patrolPoints.map(point => clampScoutPoint({ x: point.x, z: point.z }, api)) 330 : defaultScoutPoints(x, z, seed, api); 331 const waypointIndex = 0; 332 const first = scoutPoints[waypointIndex] || { x, z }; 333 const startX = Number.isFinite(options.startX) ? options.startX : x; 334 const startZ = Number.isFinite(options.startZ) ? options.startZ : z; 335 const initialHeading = Math.atan2(first.x - startX, first.z - startZ); 336 337 root.position.set(startX, api.terrainHeight(startX, startZ), startZ); 338 root.rotation.y = initialHeading; 339 root.userData.tooltip = makeTooltip({ state: 'patrol' }); 340 root.userData.tooltipFn = owner => { 341 const rover = owner.userData.humanRoverActor; 342 return makeTooltip(rover || { state: 'patrol' }); 343 }; 344 root.userData.tooltipRequiresSight = true; 345 346 const placeholder = makeFallbackRover(api, seed); 347 visualRoot.add(placeholder); 348 349 const lightMat = new THREE.MeshBasicMaterial({ 350 color: 0x6aa8ff, 351 transparent: true, 352 opacity: 0.58, 353 depthWrite: false 354 }); 355 const statusLight = new THREE.Mesh(new THREE.SphereGeometry(0.085, 8, 6), lightMat); 356 statusLight.position.set(0, 0.86, 0.18); 357 statusLight.userData.noShadowCaster = true; 358 visualRoot.add(statusLight); 359 360 const scanRing = makeScanRing(THREE); 361 api.scene.add(scanRing); 362 363 const actor = { 364 root, 365 visualRoot, 366 placeholder, 367 fallbackWheels: placeholder.userData.fallbackWheels || [], 368 statusLight, 369 lightMat, 370 scanRing, 371 patrolPoints: scoutPoints, 372 waypointIndex, 373 loopRoute: !!options.loopRoute, 374 home: { x, z }, 375 homeSite: options.homeSite || null, 376 seed, 377 phase: Number.isFinite(options.phase) ? options.phase : api.hash(seed, 1901) * Math.PI * 2, 378 heading: initialHeading, 379 speed: Number.isFinite(options.speed) ? options.speed : ROVER_SPEED, 380 returnSpeed: Number.isFinite(options.returnSpeed) ? options.returnSpeed : ROVER_RETURN_SPEED, 381 missionDuration: Number.isFinite(options.missionDuration) ? options.missionDuration : ROVER_MISSION_DURATION, 382 missionAge: 0, 383 state: 'patrol', 384 stateTime: 0, 385 scanCooldown: 2.0 + api.hash(seed, 1931) * 4.0, 386 scanTarget: null, 387 scanProgress: 0, 388 targetCooldowns: {},
389 relayDeployed: false, 390 blockedTime: 0, 391 groundUp: new THREE.Vector3(0, 1, 0), 392 groundForward: new THREE.Vector3(0, 0, 1), 393 groundRight: new THREE.Vector3(1, 0, 0), 394 groundMatrix: new THREE.Matrix4(), 395 groundQuaternion: new THREE.Quaternion(), 396 model: null, 397 modelLoaded: false, 398 ownedMaterials: [], 399 disposed: false, 400 expired: false 401 }; 402 root.userData.humanRoverActor = actor; 403 updateGroundAlignedRoot(actor, api, 1, true); 404 405 api.scene.add(root); 406 api.markShadowCaster(root); 407 408 loadRoverTemplate(THREE) 409 .then(template => attachRoverModel(actor, template, api)) 410 .catch(err => { 411 actor.modelLoadFailed = true; 412 if (window && window.console) console.warn('Human rover model load failed:', err); 413 }); 414 415 return actor; 416 } 417 418 function reduceTargetCooldowns(actor, dt) { 419 for (const key of Object.keys(actor.targetCooldowns)) { 420 actor.targetCooldowns[key] -= dt; 421 if (actor.targetCooldowns[key] <= 0) delete actor.targetCooldowns[key]; 422 } 423 } 424 425 function chooseScanTarget(actor, api) { 426 if (!api.getHumanScoutTargets) return null; 427 const origin = actor.root.position; 428 const targets = api.getHumanScoutTargets({ x: origin.x, z: origin.z }, ROVER_SCAN_RADIUS); 429 if (!Array.isArray(targets) || !targets.length) return null; 430 431 let best = null; 432 let bestScore = -Infinity; 433 for (const target of targets) { 434 if (!target || !Number.isFinite(target.x) || !Number.isFinite(target.z)) continue; 435 if (target.id && actor.targetCooldowns[target.id]) continue; 436 const dist = Math.max(0.001, target.distance ?? distance2D(origin, target)); 437 const score = (target.priority || 1) * 3.0 - dist * 0.35 + api.hash(actor.seed, dist * 91) * 0.35; 438 if (score > bestScore) { 439 bestScore = score; 440 best = target; 441 } 442 } 443 return best; 444 } 445 446 function moveActorToward(actor, target, speed, dt, api) { 447 const pos = actor.root.position; 448 const dx = target.x - pos.x; 449 const dz = target.z - pos.z; 450 const dist = Math.sqrt(dx * dx + dz * dz); 451 if (dist <= 0.001) return dist; 452 453 const step = Math.min(dist, speed * dt); 454 const nx = dx / dist; 455 const nz = dz / dist; 456 const next = { 457 x: pos.x + nx * step, 458 z: pos.z + nz * step 459 }; 460 if (api.isGroundPathBlocked && api.isGroundPathBlocked({ x: pos.x, z: pos.z }, next, 0.44)) { 461 actor.lastMoveBlocked = true; 462 return dist; 463 } 464 actor.lastMoveBlocked = false; 465 actor.blockedTime = 0; 466 pos.x = next.x; 467 pos.z = next.z; 468 actor.heading = turnToward(actor.heading, Math.atan2(nx, nz), ROVER_TURN_RATE * dt); 469 return dist; 470 } 471 472 function updatePatrol(actor, stepDt, api) { 473 actor.scanCooldown = Math.max(0, actor.scanCooldown - stepDt); 474 if (actor.scanCooldown <= 0) { 475 const target = chooseScanTarget(actor, api); 476 if (target) { 477 actor.state = 'scan'; 478 actor.stateTime = 0; 479 actor.scanProgress = 0; 480 actor.scanTarget = target; 481 return; 482 } 483 } 484 485 if (actor.missionAge >= actor.missionDuration) { 486 actor.state = 'return'; 487 actor.stateTime = 0; 488 return; 489 } 490 491 const point = actor.patrolPoints[actor.waypointIndex] || actor.home; 492 const remaining = moveActorToward(actor, point, actor.speed, stepDt, api); 493 if (actor.lastMoveBlocked) { 494 actor.blockedTime += stepDt; 495 if (actor.blockedTime >= ROVER_BLOCKED_TIME) { 496 actor.state = 'blocked'; 497 actor.stateTime = 0; 498 actor.scanCooldown = Math.min(actor.scanCooldown, 0.35); 499 } 500 return; 501 }
502 if (remaining <= ROVER_WAYPOINT_RADIUS) { 503 if (actor.waypointIndex >= actor.patrolPoints.length - 1 && !actor.loopRoute) { 504 actor.state = 'return'; 505 actor.stateTime = 0; 506 } else { 507 actor.waypointIndex = (actor.waypointIndex + 1) % actor.patrolPoints.length; 508 } 509 } 510 } 511 512 function completeScan(actor, api) { 513 const target = actor.scanTarget; 514 if (target && target.id) actor.targetCooldowns[target.id] = ROVER_TARGET_COOLDOWN; 515 actor.scanCooldown = ROVER_SCAN_COOLDOWN + api.hash(actor.seed, actor.missionAge * 11.0) * 4.0; 516 actor.state = 'patrol'; 517 actor.stateTime = 0; 518 actor.scanTarget = null; 519 actor.scanProgress = 0; 520 521 if (target && api.markHumanDetection) { 522 const duration = api.getHumanRoverScanDetectionDuration 523 ? api.getHumanRoverScanDetectionDuration() 524 : 70; 525 api.markHumanDetection(target, 'rover-scan', duration, 1.0); 526 } 527 if (!actor.relayDeployed && target && api.deployHumanFieldRelay) { 528 actor.relayDeployed = !!api.deployHumanFieldRelay(target, actor); 529 } 530 const alertScore = ROVER_ALERT_BASE + Math.max(0, target && target.priority ? target.priority * 4 : 0); 531 if (api.worldSignal) api.worldSignal('HUMAN ROVER SENT SURVEY DATA', true, 1500); 532 if (api.addHumanAlertScore) api.addHumanAlertScore(alertScore); 533 } 534 535 function updateScan(actor, t, stepDt, api) { 536 actor.scanProgress = clamp01(actor.stateTime / ROVER_SCAN_TIME); 537 const target = actor.scanTarget; 538 if (target && Number.isFinite(target.x) && Number.isFinite(target.z)) { 539 const desired = Math.atan2(target.x - actor.root.position.x, target.z - actor.root.position.z); 540 actor.heading = turnToward(actor.heading, desired, ROVER_TURN_RATE * 0.82 * stepDt); 541 } 542 if (actor.stateTime >= ROVER_SCAN_TIME) completeScan(actor, api); 543 } 544 545 function updateReturn(actor, stepDt, api) { 546 const remaining = moveActorToward(actor, actor.home, actor.returnSpeed, stepDt, api); 547 if (remaining <= 0.5) actor.expired = true; 548 } 549 550 function updateBlocked(actor, stepDt, api) { 551 actor.scanCooldown = Math.max(0, actor.scanCooldown - stepDt); 552 if (actor.scanCooldown <= 0) { 553 const target = chooseScanTarget(actor, api); 554 if (target) { 555 actor.state = 'scan'; 556 actor.stateTime = 0; 557 actor.scanProgress = 0; 558 actor.scanTarget = target; 559 actor.blockedTime = 0; 560 return; 561 } 562 } 563 if (actor.stateTime >= ROVER_BLOCKED_PAUSE) { 564 actor.state = 'return'; 565 actor.stateTime = 0; 566 actor.blockedTime = 0; 567 } 568 } 569 570 function updateVisuals(actor, t, stepDt, api) { 571 const pos = actor.root.position; 572 pos.y = api.terrainHeight(pos.x, pos.z); 573 const moving = actor.state === 'patrol' || actor.state === 'return'; 574 const speedBob = moving ? 1 : 0.25; 575 updateGroundAlignedRoot(actor, api, stepDt, false); 576 actor.visualRoot.position.y = Math.sin(t * 8.0 + actor.phase) * 0.015 * speedBob; 577 actor.visualRoot.rotation.x = Math.sin(t * 3.1 + actor.phase) * 0.018 * speedBob; 578 actor.visualRoot.rotation.z = Math.cos(t * 3.5 + actor.phase) * 0.014 * speedBob; 579 580 actor.fallbackWheels.forEach((wheel, index) => { 581 wheel.rotation.x += (moving ? 8.0 : 0.9) * stepDt * (index % 2 ? 1 : -1); 582 wheel.position.z = wheel.userData.baseZ + Math.sin(t * 7.2 + wheel.userData.phase) * 0.012; 583 }); 584 585 const scanning = actor.state === 'scan'; 586 const pulse = scanning 587 ? 0.65 + Math.sin(t * 9.0 + actor.phase) * 0.24 588 : 0.36 + Math.sin(t * 3.4 + actor.phase) * 0.10; 589 actor.lightMat.opacity = pulse; 590 actor.statusLight.scale.setScalar(scanning ? 1.20 + Math.sin(t * 8.5) * 0.12 : 1.0); 591 592 const visibleHere = !api.isWorldVisible || api.isWorldVisible(pos.x, pos.z); 593 actor.root.visible = visibleHere; 594 actor.scanRing.visible = scanning && visibleHere; 595 if (actor.scanRing.visible) { 596 const p = smoothstep01(actor.scanProgress); 597 const radius = 0.86 + p * ROVER_SCAN_RADIUS; 598 actor.scanRing.position.set(pos.x, api.terrainHeight(pos.x, pos.z) + 0.045, pos.z); 599 actor.scanRing.scale.set(radius, 1, radius); 600 actor.scanRing.material.opacity = (1 - p) * 0.48; 601 actor.scanRing.rotation.y = t * 0.4; 602 } 603 } 604 605 function animate(actor, t, dt, api) { 606 if (!actor || actor.disposed || actor.expired || !actor.root) return; 607 const stepDt = Math.min(0.1, Math.max(0.0001, dt || 0.016)); 608 actor.missionAge += stepDt; 609 actor.stateTime += stepDt; 610 reduceTargetCooldowns(actor, stepDt); 611 612 if (actor.homeSite && actor.homeSite.userData && actor.homeSite.userData.assimilated) { 613 actor.state = 'return'; 614 actor.expired = true; 615 return; 616 } 617 618 if (actor.state === 'scan') updateScan(actor, t, stepDt, api); 619 else if (actor.state === 'blocked') updateBlocked(actor, stepDt, api); 620 else if (actor.state === 'return') updateReturn(actor, stepDt, api); 621 else updatePatrol(actor, stepDt, api); 622 623 updateVisuals(actor, t, stepDt, api); 624 } 625 626 function dispose(actor, api) { 627 if (!actor || actor.disposed) return; 628 actor.disposed = true; 629 if (actor.scanRing) { 630 if (actor.scanRing.parent) actor.scanRing.parent.remove(actor.scanRing); 631 if (actor.scanRing.geometry) actor.scanRing.geometry.dispose(); 632 if (actor.scanRing.material) actor.scanRing.material.dispose(); 633 } 634 disposeFallback(actor, api); 635 if (actor.ownedMaterials) {
636 actor.ownedMaterials.forEach(material => { 637 if (material && material.dispose) material.dispose(); 638 }); 639 actor.ownedMaterials.length = 0; 640 } 641 if (actor.lightMat) actor.lightMat.dispose(); 642 if (actor.statusLight && actor.statusLight.geometry) actor.statusLight.geometry.dispose(); 643 if (actor.root) { 644 api.unmarkShadowCaster(actor.root); 645 if (actor.root.parent) actor.root.parent.remove(actor.root); 646 } 647 } 648 649 window.FAR_COLONY_HUMAN_ROVER_DEFINITION = { 650 create, 651 animate, 652 dispose 653 }; 654})();
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.