File size: 9,474 Bytes
7a2b109
d32ddf2
 
 
 
 
 
e6ecfb1
 
7a2b109
 
 
 
d32ddf2
 
 
e6ecfb1
d32ddf2
cae3dc6
d32ddf2
 
 
9eded26
d32ddf2
 
842eae1
2a07a69
755b556
17641e3
cce54f5
 
406718a
cce54f5
9eded26
 
 
 
 
7a2b109
4f2ab31
 
 
 
de0a7f9
 
 
 
 
3f927e2
cce54f5
de0a7f9
17641e3
cce54f5
17641e3
2d6f3fe
 
17641e3
7a2b109
cce54f5
17641e3
d90ceb8
 
 
 
 
 
 
 
cce54f5
d90ceb8
 
 
 
 
 
 
 
2d6f3fe
 
 
 
 
cce54f5
2d6f3fe
 
 
 
d088d6d
 
 
e6ecfb1
 
 
cce54f5
e6ecfb1
d088d6d
 
 
 
 
e6ecfb1
 
 
 
cce54f5
e6ecfb1
 
 
 
 
 
cae3dc6
cce54f5
cae3dc6
2d6f3fe
 
cae3dc6
 
cce54f5
cae3dc6
d32ddf2
 
 
cce54f5
d32ddf2
cae3dc6
a4bbde9
 
 
 
d32ddf2
cae3dc6
 
d32ddf2
cae3dc6
6fe5e5b
4e300be
 
 
cce54f5
4e300be
d32ddf2
4e300be
a4bbde9
d32ddf2
4e300be
 
 
 
 
 
 
e7bc56d
 
d32ddf2
 
 
cce54f5
d088d6d
e7bc56d
0fdd1b1
d32ddf2
 
 
 
 
 
2d6f3fe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/* ============================================================
   Entry point / composition root. The game is split across focused modules โ€”
   dom (element handles), session (the live state/input holder), setup (splash),
   boot (the run loop + frame events), hud (selection panel + readouts),
   overlays (chips/objectives/help), saveload (localStorage). Importing them here
   evaluates each module, which self-wires its own listeners; this file owns the
   remaining top-level chrome (mute, volume, the canvases' resize/backing store,
   the minimap commands, the panel-fold toggles (portrait sheet + desktop side panel),
   the touch-mode flag) and kicks the first screen off.
   ============================================================ */

"use strict";

import { game } from "./session.js";
import {
  canvas, ctx, minimapCanvas, minimapCtx, muteBtn, volumeEl,
  idleWorkersEl, idleProductionEl, sheetToggleEl, panelToggleEl, MINIMAP_W, MINIMAP_H, isTouchMode,
} from "./dom.js";
import { clampCamera } from "./camera.js";
import { minimapToWorld } from "./minimap.js";
import { issueMove, issueAttackMove } from "./engine/commands.js";
import { UNITS } from "./engine/entities.js";
import * as sound from "./sound.js";
import { buildHelpOverlay } from "./overlays.js";
import { renderMapSelect } from "./setup.js";
import "./starmap.js";   // self-wires the galaxy-map button + M key
import "./techChart.js"; // self-wires the Tech & Industry Chart button + T key
import "./update.js";    // self-wires the version chip + auto-update check

if (idleWorkersEl) idleWorkersEl.addEventListener("click", () => { if (game.input) game.input.focusIdleWorker(); });
if (idleProductionEl) idleProductionEl.addEventListener("click", () => { if (game.input) game.input.focusIdleProducer(); });

if (muteBtn) muteBtn.addEventListener("click", () => {
  const next = !sound.isMuted();
  sound.setMuted(next);
  muteBtn.setAttribute("aria-pressed", String(next));
  muteBtn.textContent = next ? "๐Ÿ”‡" : "๐Ÿ”Š";
});

if (volumeEl) {
  volumeEl.addEventListener("input", () => sound.setVolume(Number(volumeEl.value) / 100));
}

// Right-click is a game command (move / attack / gather / queue a waypoint),
// so the browser's own context menu must never pop over the game. The canvas
// already suppresses it for clicks that land squarely on it, but the view has
// padding around the canvas and the minimap sits on top of it โ€” a right-click
// on either of those would otherwise open the native menu. One window-level
// listener covers the whole window in one place.
if (typeof window !== "undefined") window.addEventListener("contextmenu", e => e.preventDefault());

function resizeCanvas() {
  if (typeof window === "undefined" || !canvas) return;   // import-safe under Node (dom.js idiom)
  const dpr = window.devicePixelRatio || 1;
  canvas.width = Math.round(canvas.clientWidth * dpr);    // round so a fractional DPR (e.g. 1.25) doesn't
  canvas.height = Math.round(canvas.clientHeight * dpr);  // leave a sub-pixel sliver unrendered at the edges
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
if (typeof window !== "undefined") window.addEventListener("resize", resizeCanvas);

// The canvas fills a flex region, so its box can change from a LAYOUT shift that fires NO window
// `resize` event โ€” most visibly the topbar wrapping to a second line on a narrow window (its
// `flex-wrap` grows it, shrinking the view below), but also the Home button appearing or the
// portrait sheet collapsing. Without a refit, resizeCanvas never ran: the backing store kept its
// old, taller size, so the never-redrawn strip showed as a thick band at the bottom AND โ€” because
// the render viewport and the clickโ†’world transform then disagreed on the canvas height โ€” every
// selection landed offset. A ResizeObserver refits the backing store the instant the box changes,
// in one place (superseding the ad-hoc triggers above), and re-clamps the camera into the map.
if (typeof window !== "undefined" && window.ResizeObserver) {
  new ResizeObserver(() => {
    resizeCanvas();
    if (game.state && game.input) {
      clampCamera(game.input.getCamera(), game.state.map, canvas.clientWidth, canvas.clientHeight);
    }
  }).observe(canvas);
}

// A monitor move (or an OS scaling change) can change devicePixelRatio WITHOUT firing a resize
// event โ€” the CSS layout size is unchanged โ€” leaving the backing store at the old DPR (blurry on
// the sharper screen, oversampled on the other). matchMedia on the current dppx fires once when
// the ratio leaves it; refit both canvases and re-arm for the new ratio.
function watchDPR() {
  if (typeof window === "undefined" || !window.matchMedia) return;
  window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
    .addEventListener("change", () => { resizeCanvas(); resizeMinimap(); watchDPR(); }, { once: true });
}

// Portrait bottom-sheet collapse: hide the panel to reclaim the whole screen,
// then resize the canvas into the freed space. The button only shows in the
// portrait layout (style.css); the class is scoped to that layout too, so it's
// inert on desktop/landscape. Clears panel-collapsed (panelToggleEl's own class, just below)
// so resizing across the portrait/landscape breakpoint can never leave the OTHER toggle's
// button showing an "expanded" glyph while its class still has the panel hidden.
if (sheetToggleEl) sheetToggleEl.addEventListener("click", () => {
  document.body.classList.remove("panel-collapsed");
  const collapsed = document.body.classList.toggle("sheet-collapsed");
  sheetToggleEl.textContent = collapsed ? "โ–ด" : "โ–พ";
  requestAnimationFrame(resizeCanvas);   // the view grew/shrank โ€” refit the backing store
});

// Side-panel collapse: the same fold-it-away idea as the portrait handle above, for the
// normal (non-portrait) side-column layout โ€” hide the panel to widen the view, then resize the
// canvas into the freed space. The button only shows outside the portrait layout (style.css);
// see sheetToggleEl's own handler above for why each toggle clears the other's class.
if (panelToggleEl) panelToggleEl.addEventListener("click", () => {
  document.body.classList.remove("sheet-collapsed");
  const collapsed = document.body.classList.toggle("panel-collapsed");
  panelToggleEl.textContent = collapsed ? "โ—‚" : "โ–ธ";
  requestAnimationFrame(resizeCanvas);
});

function resizeMinimap() {
  if (typeof window === "undefined" || !minimapCanvas) return;   // import-safe under Node (dom.js idiom)
  const dpr = window.devicePixelRatio || 1;
  minimapCanvas.width = Math.round(MINIMAP_W * dpr);
  minimapCanvas.height = Math.round(MINIMAP_H * dpr);
  minimapCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
if (typeof window !== "undefined") window.addEventListener("resize", resizeMinimap);

// Attached once, not per-game: state/input are read fresh from the shared session
// at click time, so this stays correct across a "choose another battlefield"
// restart without needing to re-wire on every startGame().
if (minimapCanvas) minimapCanvas.addEventListener("click", e => {
  if (!game.state || !game.input) return;
  const rect = minimapCanvas.getBoundingClientRect();
  // Convert against the minimap's ACTUAL rendered size, not the fixed logical MINIMAP_W/H:
  // the CSS shrinks the element on small/portrait viewports (style.css), so using the
  // constant sent a phone tap on the right edge to ~56% of the map instead of the edge.
  const world = minimapToWorld(game.state.map, rect.width, rect.height, e.clientX - rect.left, e.clientY - rect.top);
  const camera = game.input.getCamera();
  camera.x = world.x;
  camera.y = world.y;
  clampCamera(camera, game.state.map, canvas.clientWidth, canvas.clientHeight);
});

// Right-click the minimap to command the current selection to that spot without
// scrolling the main view first โ€” move for workers, attack-move for combat, so
// you can respond to a raid on a far flank straight from the map.
if (minimapCanvas) minimapCanvas.addEventListener("contextmenu", e => {
  e.preventDefault();
  if (!game.state || !game.input) return;
  const rect = minimapCanvas.getBoundingClientRect();
  const world = minimapToWorld(game.state.map, rect.width, rect.height, e.clientX - rect.left, e.clientY - rect.top);   // actual size, not MINIMAP_W/H (see the click handler)
  const selected = game.state.selection.map(id => game.state.units.get(id)).filter(Boolean);
  if (!selected.length) return;
  const combatants = selected.filter(u => UNITS[u.type].role === "combat");
  const others = selected.filter(u => UNITS[u.type].role !== "combat");
  if (combatants.length) issueAttackMove(combatants, world.x, world.y);
  if (others.length) issueMove(others, world.x, world.y);
});

// Touch mode: the first finger anywhere grows the tap targets (style.css) and
// switches the HUD hints to touch phrasing. input.js sets the class on a canvas
// touch too; doing it here covers a first touch that lands on a HUD button. The
// panel signature includes isTouchMode(), so the loop's next renderHUD rebuilds
// the legend/hints in touch phrasing on its own.
if (typeof window !== "undefined") window.addEventListener("touchstart", () => {
  if (!isTouchMode()) document.body.classList.add("touch");
}, { passive: true });

// ---- kickoff (all modules are evaluated by now, so these cross-module calls
// are safe) ----
buildHelpOverlay();
renderMapSelect();
resizeCanvas();
resizeMinimap();
watchDPR();