; ; /* ============================================================================ BATTLEFIELD INTEL — vision, radar, detection, stealth ---------------------------------------------------------------------------- Fog already hid models, bars, FX and the minimap. What it did not do was read the intel data the roster already carried: TYPES[].scout Kestrel (and faction scouts) used the same air bubble as a Wasp. The card said "150 sight"; fog stamped 14 cells. umode === 4 GHOST: findEnemy skipped them, firing broke cover, hulls went translucent — and the player still SAW them the moment they entered a sensor circle. "Unseen until you fire" was targeting-only. BT.uplink Targeting Array / radar dish. Boosted towers and the build grid. Vision was the generic 10-cell building stamp, so a radar mast saw the same as a silo. WC.dark "vision suffers" only forced night. Fog Bank (fogb) was the only wildcard that actually shortened sensors. Jamming has no authored radius/flag. Ghost Net already stuns and scans; a dedicated radar-denial layer is a later plan item, not invented here. Radar is a COMMAND-MAP layer, not a second 3D reveal. Uplink/techlab stamp fogRadar; the minimap draws those contacts; render3d keeps using fogEntityVisible (visual). That split is why this file must not live in the renderer — and why it never writes GL state. Detection is the answer to GHOST. Scouts, uplinks, techlabs, HQs and live survey scans stamp fogDetect. intelCanTarget (sim.js seam) lets a detector lock a silent-running hull; fogEntityVisible hides it from the player until then. Same numbers both ways, so a cloak that fools the eye also fools the gun until a sensor is in range. ============================================================================ */ const INTEL_VIS_GROUND=10, INTEL_VIS_AIR=14, INTEL_VIS_SCOUT=18, INTEL_VIS_GHOST=4; const INTEL_VIS_HQ=22, INTEL_VIS_TURRET=12, INTEL_VIS_BLD=10, INTEL_VIS_CARRIER=24; /* Radar cells are larger than visual on purpose: the mast hears further than it sees. Contacts are minimap-only (see intelRadarContact). */ const INTEL_RADAR_UPLINK=22, INTEL_RADAR_TECHLAB=16; /* Detect is shorter than radar. Omni-style pierce, not a second map hack. */ const INTEL_DET_SCOUT=16, INTEL_DET_UPLINK=14, INTEL_DET_TECHLAB=12, INTEL_DET_HQ=8; const fogRadar=new Uint8Array(FN*FN); const fogDetect=new Uint8Array(FN*FN); const intelGhostIdx=[]; let intelGhostStamp=-1; const intelHasGhost=[0,0,0]; /* Last-known contacts are a derived command-intel ledger, not gameplay or save authority. `stats.t` is the only clock: replays therefore age the same way at every render rate and pausing the renderer cannot create or expire intel. Unit slot + generation is the stable identity because slots are recycled. */ const INTEL_CONTACT_CAP=256; const INTEL_CONTACT_TTL={visual:10,radar:18,scan:14,aerial:16,manual:10}; const INTEL_CONTACT_CONF={visual:1,radar:.72,scan:.86,aerial:.92,manual:.65}; const intelContacts=[]; let intelContactClock=0; function intelContactNow(){ return typeof stats!=='undefined'&&stats&&Number.isFinite(stats.t)?stats.t:0; } function intelContactAdvanceClock(now){ now=Number.isFinite(now)?now:intelContactNow(); if(now>intelContactClock) intelContactClock=now; return intelContactClock; } function intelContactDefaults(source){ source=typeof source==='string'&&source?source:'manual'; return {source, confidence:INTEL_CONTACT_CONF[source]===undefined?INTEL_CONTACT_CONF.manual:INTEL_CONTACT_CONF[source], ttl:INTEL_CONTACT_TTL[source]===undefined?INTEL_CONTACT_TTL.manual:INTEL_CONTACT_TTL[source]}; } function intelContactSnapshot(C){ return {team:C.team,target:C.target,generation:C.generation,x:C.x,y:C.y, source:C.source,confidence:C.confidence,age:C.age,timestamp:C.timestamp, expiresAt:C.expiresAt}; } function intelContactTick(now){ now=intelContactAdvanceClock(now); for(let i=intelContacts.length-1;i>=0;i--){ const C=intelContacts[i]; /* A living slot with another generation is a different chassis. A dead slot with the same generation remains valid last-known intelligence. */ if(C.target<0||C.target>=MAXU||(ualive[C.target]&&ugen[C.target]!==C.generation)){ intelContacts.splice(i,1);continue; } C.age=Math.max(0,now-C.timestamp); const span=Math.max(.000001,C.expiresAt-C.timestamp); C.confidence=C.baseConfidence*clamp((C.expiresAt-now)/span,0,1); if(now>=C.expiresAt||C.confidence<=0) intelContacts.splice(i,1); } return intelContacts.length; } function intelContactFind(team,target,generation){ for(let i=0;i30||target<0||target>=MAXU) return null; const sampledAt=Number.isFinite(now)?now:intelContactNow(); const staleSample=sampledAt=0;i--){ const C=intelContacts[i]; if(C.team===team&&C.target===target&&C.generation!==generation) intelContacts.splice(i,1); } let at=intelContactFind(team,target,generation),C=at>=0?intelContacts[at]:null; /* Late packets/probe calls may describe an older simulation tick. They are allowed to age the ledger to the current monotonic clock, but never to refresh confidence or replace the newer sampled position. */ if(staleSample) return C?intelContactSnapshot(C):null; if(C&&now=INTEL_CONTACT_CAP){ /* Deterministic pressure trim: weakest first, then oldest, then the lowest stable identity. Active observations are not insertion-order random and equal runs retain the same contact set. */ let worst=0; for(let i=1;i=0&&targetC.team===team&&C.confidence>=minConfidence) .sort((A,B)=>A.target-B.target||A.generation-B.generation) .map(intelContactSnapshot); } function intelArtillerySolution(team,request,now){ team=team|0;request=request&&typeof request==='object'?request:{}; now=intelContactAdvanceClock(now); intelContactTick(now); if(request.source==='player'&&Number.isFinite(request.x)&&Number.isFinite(request.y)){ return {eligible:true,team,target:-1,generation:-1, x:clamp(request.x,0,MAP),y:clamp(request.y,0,MAP),source:'player', confidence:1,age:0,timestamp:now,expiresAt:now}; } const target=request.target|0; if(target<0||target>=MAXU) return {eligible:false,team,reason:'invalid-target'}; const generation=Number.isFinite(request.generation)?request.generation|0: (ualive[target]?ugen[target]|0:undefined); const at=intelContactFind(team,target,generation); if(at<0) return {eligible:false,team,target,generation,reason:'missing-contact'}; const C=intelContacts[at]; return {eligible:true,team,target:C.target,generation:C.generation,x:C.x,y:C.y, source:C.source,confidence:C.confidence,age:C.age,timestamp:C.timestamp, expiresAt:C.expiresAt}; } function intelArtilleryScatterRadius(solution,weaponRadius){ if(!solution||!solution.eligible) return 0; weaponRadius=Number.isFinite(weaponRadius)?Math.max(1,weaponRadius):64; const sourceBias=solution.source==='visual'?.08:solution.source==='player'?.12: solution.source==='aerial'?.14:solution.source==='scan'?.2:solution.source==='radar'?.28:.34; const confidence=clamp(Number.isFinite(solution.confidence)?solution.confidence:0,0,1); const ttl=Math.max(.001,(Number.isFinite(solution.expiresAt)?solution.expiresAt:0)- (Number.isFinite(solution.timestamp)?solution.timestamp:0)); const ageRatio=clamp((Number.isFinite(solution.age)?solution.age:ttl)/ttl,0,1); return weaponRadius*(sourceBias+(1-confidence)*.65+ageRatio*.25); } function intelArtilleryHash32(value){ value=(value|0)+0x6d2b79f5; value=Math.imul(value^(value>>>15),value|1); value^=value+Math.imul(value^(value>>>7),value|61); return (value^(value>>>14))>>>0; } function intelArtilleryScatterPoint(solution,weaponRadius,shellIndex,seed){ if(!solution||!solution.eligible) return {x:NaN,y:NaN,radius:0}; const radius=intelArtilleryScatterRadius(solution,weaponRadius); const identity=Math.imul((solution.target|0)+2,0x45d9f3b)^Math.imul((solution.generation|0)+2,0x119de1f3)^ Math.imul(Math.round(solution.x*16)|0,0x27d4eb2d)^Math.imul(Math.round(solution.y*16)|0,0x165667b1); const h0=intelArtilleryHash32(identity^(seed|0)^Math.imul((shellIndex|0)+1,0x9e3779b1)); const h1=intelArtilleryHash32(h0^0x85ebca6b); const angle=h0/4294967296*Math.PI*2; const distance=Math.sqrt(h1/4294967296)*radius; return {x:clamp(solution.x+Math.cos(angle)*distance,0,MAP), y:clamp(solution.y+Math.sin(angle)*distance,0,MAP),radius}; } function intelContactRefreshSensors(){ const active=typeof fogGameplayActive!=='function'||fogGameplayActive(); const now=intelContactNow(); intelContactTick(now); if(!active) return; const viewTeam=typeof mfFogViewTeam==='function'?mfFogViewTeam():0; for(let i=0;i