1function renderSetupHeader(current) { 2 const stepper = renderSetupStepper(current); 3 const countdown = (typeof renderPhaseCountdownRow === 'function') ? renderPhaseCountdownRow() : ''; 4 return `<div class="setup-header-row">${stepper}${countdown}</div>`; 5} 6window.renderSetupHeader = renderSetupHeader; 7 8/** In-place DOM update â full render only when MY readiness flips (forces layout swap). */ 9function updateReadinessIndicator() { 10 if (typeof game === 'undefined' || !game || !game.onlineMatchContext) return; 11 const indicator = document.querySelector('.readiness-opp-indicator'); 12 if (!indicator) return; 13 const oppReady = game.onlineMatchContext.opponentReady === true; 14 const tt = (k, fb) => (typeof t === 'function' && t(k)) || fb; 15 let label; 16 if (oppReady) { 17 label = tt(game.phase === Phase.ONLINE_DEPLOY ? 'deploy.ready' : 'recruit.ready', 'Ready'); 18 } else if (game.phase === Phase.ONLINE_DEPLOY) { 19 label = tt('deploy.online.deploying', 'Deployingâ¦'); 20 } else { 21 label = tt('recruit.online.recruiting', 'Recruitingâ¦'); 22 } 23 const dot = indicator.querySelector('.readiness-opp-dot'); 24 if (dot) { 25 dot.classList.toggle('is-ready', oppReady); 26 dot.classList.toggle('is-waiting', !oppReady); 27 } 28 indicator.setAttribute('title', label); 29} 30window.updateReadinessIndicator = updateReadinessIndicator; 31 32// Toggles `progress-pill-visible` on body so CSS hides mobile sticky badge when inline pill is on-screen. 33let _progressPillObserver = null; 34function watchProgressPillVisibility() { 35 if (_progressPillObserver) { 36 _progressPillObserver.disconnect(); 37 _progressPillObserver = null; 38 } 39 if (typeof document === 'undefined' || typeof IntersectionObserver === 'undefined') return; 40 const pill = document.querySelector('.readiness-tag.progress'); 41 if (!pill) { 42 document.body.classList.remove('progress-pill-visible'); 43 return; 44 } 45 _progressPillObserver = new IntersectionObserver((entries) => { 46 for (const e of entries) { 47 document.body.classList.toggle('progress-pill-visible', e.isIntersecting); 48 } 49 }); 50 _progressPillObserver.observe(pill); 51} 52window.watchProgressPillVisibility = watchProgressPillVisibility; 53
54function renderSetupStepper(current) { 55 const steps = [ 56 { key: 'map', label: t('setupStep.map') || 'Map' }, 57 { key: 'recruit', label: t('setupStep.recruit') || 'Recruit' }, 58 { key: 'deploy', label: t('setupStep.deploy') || 'Deploy' }, 59 ]; 60 const idx = steps.findIndex(s => s.key === current); 61 return ` 62 <ol class="setup-stepper" aria-label="${t('setupStep.aria') || 'Setup progress'}"> 63 ${steps.map((s, i) => { 64 const cls = i < idx ? 'is-done' : i === idx ? 'is-current' : ''; 65 return ` 66 <li class="setup-step ${cls}" aria-current="${i === idx ? 'step' : 'false'}"> 67 <span class="setup-step-num">${i < idx ? 'â' : (i + 1)}</span> 68 <span class="setup-step-label">${s.label}</span> 69 </li> 70 ${i < steps.length - 1 ? `<li class="setup-step-connector ${i < idx ? 'is-done' : ''}" aria-hidden="true"></li>` : ''}`; 71 }).join('')} 72 </ol>`; 73} 74 75function renderMiniMap(data) { 76 let h = ''; 77 for (let r = 0; r < BOARD_SIZE; r++) 78 for (let c = 0; c < BOARD_SIZE; c++) { 79 // color===background so CSS `box-shadow: 0 0 0 1px currentColor` masks pixel-rounding seams. 80 const color = TERRAIN_INFO[data[r][c]].color; 81 h += `<div class="mini-cell" style="background:${color};color:${color}"></div>`; 82 } 83 return h; 84} 85 86function getGroupedUnits(army) { 87 const groups = {}; 88 army.forEach(u => { 89 if (!groups[u.type]) groups[u.type] = { all: [], undeployed: [], sample: u }; 90 groups[u.type].all.push(u); 91 if (!u.deployed) groups[u.type].undeployed.push(u); 92 }); 93 return Object.values(groups).sort((a, b) => { 94 if (a.sample.isStratigos) return -1; 95 if (b.sample.isStratigos) return 1; 96 return b.sample.value - a.sample.value; 97 }); 98} 99 100// Shared by deploy-tray listing AND handleDeployClick auto-pick. 101const DEPLOY_TYPE_ORDER = { 102 [UnitType.HEAVY_INFANTRY]: 1, 103 [UnitType.LIGHT_INFANTRY]: 2, 104 [UnitType.SKIRMISHER]: 3, 105 [UnitType.HEAVY_CAVALRY]: 4, 106 [UnitType.LIGHT_CAVALRY]: 5, 107 [UnitType.STRATIGOS]: 6, 108}; 109function getDeployOrderedGroups(army) { 110 return getGroupedUnits(army).slice().sort((a, b) => 111 (DEPLOY_TYPE_ORDER[a.sample.type] ?? 99) - 112 (DEPLOY_TYPE_ORDER[b.sample.type] ?? 99)); 113} 114 Object.assign(window, { DEPLOY_TYPE_ORDER, getDeployOrderedGroups }); 115 116/** online-match.js's 1s DOM updater ticks .ready-phase-timer text between renders. */ 117function renderPhaseCountdownRow() { 118 const ctx = (typeof game !== 'undefined' && game && game.onlineMatchContext) || null; 119 const pill = ctx && typeof renderPhaseCountdownPill === 'function' 120 ? renderPhaseCountdownPill(ctx.phaseDeadline) 121 : ''; 122 return pill ? `<div class="phase-countdown-row">${pill}</div>` : ''; 123} 124window.renderPhaseCountdownRow = renderPhaseCountdownRow; 125 126function renderPhaseCountdownPill(phaseDeadline) { 127 if (!phaseDeadline || typeof phaseDeadline.localDeadlineMs !== 'number') return ''; 128 const remainingMs = Math.max(0, phaseDeadline.localDeadlineMs - Date.now()); 129 const text = OnlineMatch.format(remainingMs); 130 const labelKey = 131 phaseDeadline.kind === 'DEPLOY' ? 'deploy.online.phaseTimer.label' 132 : phaseDeadline.kind === 'MAP_SELECT' ? 'mapSelect.online.phaseTimer.label' 133 : 'recruit.online.phaseTimer.label'; 134 const label = (typeof t === 'function') ? t(labelKey) : 'Phase ends in'; 135 const warning = remainingMs <= 30_000 && remainingMs > 10_000; 136 const critical = remainingMs <= 10_000; 137 const cls = `ready-phase-timer ${warning ? 'tb-timer-warning' : ''} ${critical ? 'tb-timer-critical' : ''}`; 138 return ` 139 <span class="${cls}" 140 title="${label}" 141 data-phase-deadline-ms="${phaseDeadline.localDeadlineMs}">${text}</span>`; 142} 143 144/** Cap tray height or it stretches board-wrap and rotation pivots off-centre; rAF defers 145 * measurement past layout. */ 146function lockDeployTrayHeight() { 147 requestAnimationFrame(() => { 148 const boardWrap = document.querySelector('.online-deploy .board-wrap'); 149 const trayHeader = document.querySelector('.online-deploy .deploy-tray-header'); 150 const trayContent = document.querySelector('.online-deploy .deploy-tray-content'); 151 if (boardWrap && trayContent) { 152 // Tray shares the BOARD row only (layout.css: grid-row 2; the toggle takes row 1). 153 const boardH = boardWrap.offsetHeight; 154 const trayHeaderH = trayHeader ? trayHeader.offsetHeight : 36; 155 const reserved = 14 + trayHeaderH + 8 + 14; 156 const cap = Math.max(0, boardH - reserved); 157 trayContent.style.maxHeight = cap + 'px'; 158 trayContent.style.overflowY = 'auto'; 159 } 160 }); 161} 162if (!window._deployTrayResizeBound) { 163 window._deployTrayResizeBound = true; 164 window.addEventListener('resize', () => { 165 if (typeof game !== 'undefined' && game && game.phase === Phase.ONLINE_DEPLOY) { 166 lockDeployTrayHeight(); 167 } 168 }); 169} 170
171function _clearArmy(pIdx) { 172 const p = game.players[pIdx]; 173 p.army.length = 0; 174 p.points = STARTING_POINTS; 175 p.spec = null; 176} 177 178/** Apply preset â triggers a full re-render. */ 179function applyPreset(pIdx, preset) { 180 if (!preset || typeof ARMY_PRESETS === 'undefined' || !ARMY_PRESETS[preset]) return; 181 _clearArmy(pIdx); 182 const def = ARMY_PRESETS[preset]; 183 for (const t of def.units) game.addUnit(pIdx, t); 184 game.setSpec(pIdx, def.spec); 185 render(); 186} 187 188/** Apply random army â triggers a full re-render. */ 189function applyRandomArmy(pIdx) { 190 _clearArmy(pIdx); 191 const p = game.players[pIdx]; 192 const types = [UnitType.LIGHT_INFANTRY, UnitType.HEAVY_INFANTRY, UnitType.SKIRMISHER, 193 UnitType.LIGHT_CAVALRY, UnitType.HEAVY_CAVALRY]; 194 for (const tp of types) game.addUnit(pIdx, tp); 195 const affordable = () => types.filter(tp => p.points >= UNIT_DEFS[tp].cost); 196 let choices = affordable(); 197 while (choices.length > 0) { 198 const tp = choices[Math.floor(Math.random() * choices.length)]; 199 game.addUnit(pIdx, tp); 200 choices = affordable(); 201 } 202 const specKeys = Object.keys(STRATIGO_SPECS); 203 game.setSpec(pIdx, specKeys[Math.floor(Math.random() * specKeys.length)]); 204 render(); 205} 206 207/** Reset army â triggers a full re-render. */ 208function resetArmy(pIdx) { 209 _clearArmy(pIdx); 210 render(); 211} 212window.resetArmy = resetArmy; 213 214/** Reset deployment â triggers a full re-render. */ 215function resetDeployment(pIdx) { 216 if (typeof game === 'undefined' || !game.players) return; 217 const p = game.players[pIdx]; 218 if (!p || !Array.isArray(p.army)) return; 219 for (const unit of p.army.filter((u) => u.deployed).slice()) { 220 if (typeof game.undeployUnit === 'function') game.undeployUnit(pIdx, unit.id); 221 } 222 game.selectedUnit = null; 223 if (typeof render === 'function') render(); 224} 225window.resetDeployment = resetDeployment; 226 227/** 228 * THE preferences reset footer â one markup for the Profile and Settings screens. 229 * 230 * @param {string} onclick Code string inlined into the button's onclick; pass ids, never a 231 * user-supplied string (double-context escaping). 232 * @param {string} [leftHtml] Optional control placed left of the reset button. 233 * @param {string} [statusHtml] Optional status line above the button. 234 */ 235function prefsResetFootHtml(onclick, leftHtml, statusHtml) { 236 return ` 237 <div class="profile-prefs-reset${leftHtml ? ' has-left' : ''}"> 238 ${leftHtml || ''} 239 <div class="profile-prefs-reset-main"> 240 ${statusHtml || ''} 241 <button class="btn btn-bronze" onclick="${onclick}">${t('settings.reset') || 'Reset to default'}</button> 242 </div> 243 </div>`; 244} 245 246/** 247 * THE readiness bar for both pre-play setup screens â my progress or â, and the opponent's dot. 248 * 249 * @param {{done: number, total: number, progressKey: string, readyKey: string, 250 * waitKey: string, mySubmitted: boolean, opponent: string}} o 251 * `progressKey` takes `{n}` and `{total}`; only NUMBERS are interpolated, since the result 252 * reaches an innerHTML sink. `opponent` must already be ESCAPED. 253 * @returns {string} Markup. 254 */ 255function readinessBarHtml(o) { 256 const atCap = o.done >= o.total; 257 const pointsCls = 'readiness-points' + (atCap ? ' is-full' : ''); 258 const myStatus = o.mySubmitted 259 ? `<span class="readiness-tag ready">â ${t(o.readyKey)}</span>` 260 : `<span class="readiness-tag progress">${t(o.progressKey, { 261 n: `<strong class="${pointsCls}">${o.done | 0}</strong>`, total: o.total | 0, 262 })}</span>`; 263 const oppDot = o.oppReady ? 'readiness-opp-dot is-ready' : 'readiness-opp-dot is-waiting'; 264 const oppLabel = o.oppReady ? t(o.readyKey) : t(o.waitKey); 265 return ` 266 <div class="readiness-bar${o.mySubmitted ? ' is-submitted' : ''}"> 267 ${myStatus} 268 <span class="readiness-opp-indicator" title="${oppLabel}"> 269 <span class="${oppDot}" aria-hidden="true"></span> 270 <span class="readiness-opp-name">${o.opponent}</span> 271 </span> 272 </div>`; 273} 274window.readinessBarHtml = readinessBarHtml;
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.