PageSourceSearch

https://stratigosserver-production.up.railway.app/js/views/screens/screen-helpers.js

js stratigosserver-production.up.railway.app collected 2026-10-03 12:15:16 UTC 11,062 bytes, 274 lines download raw bytes

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.