1export function createWorldStationsController(params) { 2 const { 3 THREE, 4 scene, 5 state, 6 createWorldNpcHosts, 7 extractBakedNpcStations, 8 remapLocalStationProxies, 9 mergeStations 10 } = params; 11 12 let worldRoot = null; 13 let npcHosts = null; 14 15 function guideProfileForRole(role) { 16 switch (String(role || '')) { 17 case 'cashier': 18 return { 19 displayName: 'Cashier Guide', 20 title: 'Cashier Guide', 21 inspect: 'Rex is live on the east rail. Fund, withdraw, or move balance there before you walk back into the action.', 22 useLabel: 'Open cashier', 23 use: 'Opening Rex on the east rail.', 24 roleLabel: 'cashier' 25 }; 26 case 'coinflip': 27 return { 28 displayName: 'Coinflip Runner', 29 title: 'Coinflip Runner', 30 inspect: 'Jade is taking coinflip rounds right now. Best first stop if you want one clean decision and a fast result.', 31 useLabel: 'Open coinflip', 32 use: 'Opening Jade at the coinflip table.', 33 roleLabel: 'coinflip' 34 }; 35 case 'rps': 36 return { 37 displayName: 'RPS Runner', 38 title: 'RPS Runner', 39 inspect: 'Axel is live on Rock Paper Scissors. If you want reads instead of luck, step to his table.', 40 useLabel: 'Open RPS', 41 use: 'Opening Axel at the RPS table.', 42 roleLabel: 'RPS' 43 }; 44 case 'dice': 45 return { 46 displayName: 'Dice Runner', 47 title: 'Dice Runner', 48 inspect: 'Zara is still rolling on the south line. Pick a face there if you want the fastest swing in the arena.', 49 useLabel: 'Open dice', 50 use: 'Opening Zara at the dice table.', 51 roleLabel: 'dice' 52 }; 53 case 'prediction': 54 return { 55 displayName: 'Market Runner', 56 title: 'Market Runner', 57 inspect: 'Kai is posting live prediction rounds on the north rise. If you have a view, that board is where it pays.', 58 useLabel: 'Open markets', 59 use: 'Opening Kai at the live market board.', 60 roleLabel: 'prediction' 61 }; 62 case 'blackjack': 63 return { 64 displayName: 'Blackjack Runner', 65 title: 'Blackjack Runner', 66 inspect: 'Vera is live on blackjack. Step there when you want a longer hand and a real dealer read.', 67 useLabel: 'Open blackjack', 68 use: 'Opening Vera at the blackjack table.', 69 roleLabel: 'blackjack' 70 }; 71 default: 72 return { 73 displayName: 'Floor Host', 74 title: 'Floor Host', 75 inspect: 'Live hosts are working this section now. Step to a named table and the panel will open with a real round, not filler.', 76 useLabel: 'Find live table', 77 use: 'Routing you to the nearest live host.', 78 roleLabel: 'live host' 79 }; 80 } 81 } 82 83 function createDegradedBakedInteraction(baked, nearestSameRoleHost, nearestHost) { 84 // Prefer a host that matches the baked station's game type so players are 85 // routed to a coinflip dealer for coinflip kiosks, RPS dealer for RPS kiosks, etc. 86 const routeHost = nearestSameRoleHost || nearestHost; 87 const sectionRole = String(baked.hostRole || routeHost?.hostRole || 'info'); 88 const profile = guideProfileForRole(sectionRole); 89 const destination = routeHost?.displayName || 'the nearest live host'; 90 return { 91 title: profile.title, 92 inspect: `${profile.inspect} ${routeHost ? `${destination} is the live stop for this side of the arena.` : 'No live stop is mapped here yet, so follow the nearest named host.'}`, 93 useLabel: profile.useLabel, 94 use: routeHost ? profile.use : 'Move to the nearest named host to enter a real round.', 95 routeStationId: routeHost?.id || '', 96 routeHostName: destination 97 }; 98 } 99 100 function setupWorldNpcStations() { 101 if (!worldRoot) return; 102 if (npcHosts) { 103 npcHosts.dispose(); 104 npcHosts = null; 105 } 106 npcHosts = createWorldNpcHosts({ THREE, scene }); 107 state.hostStations = new Map(npcHosts.hostStations); 108 state.bakedStations = extractBakedNpcStations({ THREE, worldRoot }); 109 remapLocalStationProxies(); 110 const OVERLAP_SUPPRESSION_DISTANCE = 10; 111 for (const [bakedId, baked] of state.bakedStations.entries()) { 112 let overlapsLiveHost = false;
113 for (const host of state.hostStations.values()) { 114 const sameKind = String(host.kind || '') === String(baked.kind || ''); 115 if (!sameKind) continue; 116 const dist = Math.hypot(Number(host.x || 0) - Number(baked.x || 0), Number(host.z || 0) - Number(baked.z || 0)); 117 if (dist <= OVERLAP_SUPPRESSION_DISTANCE) { 118 overlapsLiveHost = true; 119 break; 120 } 121 } 122 if (overlapsLiveHost) { 123 state.bakedStations.delete(bakedId); 124 } 125 } 126 for (const baked of state.bakedStations.values()) { 127 let nearestHost = null; 128 let nearestDistance = Number.POSITIVE_INFINITY; 129 let nearestSameRoleHost = null; 130 let nearestSameRoleDistance = Number.POSITIVE_INFINITY; 131 for (const host of state.hostStations.values()) { 132 const dist = Math.hypot(Number(host.x || 0) - Number(baked.x || 0), Number(host.z || 0) - Number(baked.z || 0)); 133 if (dist < nearestDistance) { 134 nearestDistance = dist; 135 nearestHost = host; 136 } 137 // Also track the nearest host that matches this baked station's game type 138 const sameRole = String(host.hostRole || '') === String(baked.hostRole || '') && baked.hostRole; 139 if (sameRole && dist < nearestSameRoleDistance) { 140 nearestSameRoleDistance = dist; 141 nearestSameRoleHost = host; 142 } 143 } 144 if (nearestHost) { 145 baked.hostRole = baked.hostRole || nearestHost.hostRole || ''; 146 if (baked.kind === 'world_interactable' && nearestHost.localInteraction) { 147 baked.localInteraction = { ...nearestHost.localInteraction }; 148 } 149 } 150 const hasLiveProxy = Boolean(String(baked.proxyStationId || '').trim()); 151 const degradableKind = baked.kind !== 'world_interactable'; 152 if (degradableKind && !hasLiveProxy) { 153 baked.originalKind = baked.kind; 154 baked.kind = 'world_interactable'; 155 baked.degradedToLocal = true; 156 baked.radius = 8; 157 baked.actions = ['interact_open', 'interact_use']; 158 baked.interactionTag = `baked_info_${String(baked.hostRole || 'info')}`; 159 baked.localInteraction = createDegradedBakedInteraction(baked, nearestSameRoleHost, nearestHost); 160 baked.displayName = baked.localInteraction?.title || guideProfileForRole(baked.hostRole).displayName; 161 } else { 162 baked.degradedToLocal = false;
163 } 164 } 165 remapLocalStationProxies(); 166 mergeStations(); 167 } 168 169 function getWorldRoot() { 170 return worldRoot; 171 } 172 173 function setWorldRoot(nextRoot) { 174 worldRoot = nextRoot; 175 } 176 177 function updateHosts() { 178 npcHosts?.updateHosts?.(); 179 } 180 181 return { 182 setupWorldNpcStations, 183 getWorldRoot, 184 setWorldRoot, 185 updateHosts 186 }; 187}
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.