@conduction/docusaurus-preset 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/MISSING_COMPONENTS.md +109 -0
  2. package/README.md +171 -0
  3. package/package.json +59 -0
  4. package/src/components/AgentTrace/AgentTrace.jsx +128 -0
  5. package/src/components/AgentTrace/AgentTrace.module.css +115 -0
  6. package/src/components/AppMock/AppMock.jsx +86 -0
  7. package/src/components/AppMock/AppMock.module.css +629 -0
  8. package/src/components/AppMock/variants/DeciDeskMock.jsx +71 -0
  9. package/src/components/AppMock/variants/DocuDeskMock.jsx +69 -0
  10. package/src/components/AppMock/variants/LarpingAppMock.jsx +59 -0
  11. package/src/components/AppMock/variants/MyDashBiMock.jsx +135 -0
  12. package/src/components/AppMock/variants/MyDashMock.jsx +96 -0
  13. package/src/components/AppMock/variants/MyDashTilesMock.jsx +103 -0
  14. package/src/components/AppMock/variants/MyDashWidgetsMock.jsx +123 -0
  15. package/src/components/AppMock/variants/NLDesignMock.jsx +70 -0
  16. package/src/components/AppMock/variants/OpenCatalogiMock.jsx +61 -0
  17. package/src/components/AppMock/variants/OpenConnectorMock.jsx +83 -0
  18. package/src/components/AppMock/variants/OpenRegisterMock.jsx +100 -0
  19. package/src/components/AppMock/variants/OpenWooMock.jsx +61 -0
  20. package/src/components/AppMock/variants/PipelinQMock.jsx +88 -0
  21. package/src/components/AppMock/variants/ProcestMock.jsx +87 -0
  22. package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +71 -0
  23. package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +71 -0
  24. package/src/components/AppsGrid/AppsGrid.jsx +84 -0
  25. package/src/components/AppsGrid/AppsGrid.module.css +46 -0
  26. package/src/components/AppsPreview/AppsPreview.jsx +85 -0
  27. package/src/components/AppsPreview/AppsPreview.module.css +128 -0
  28. package/src/components/Clients/Clients.jsx +205 -0
  29. package/src/components/Clients/Clients.module.css +166 -0
  30. package/src/components/ComposeBlock/ComposeBlock.jsx +70 -0
  31. package/src/components/ComposeBlock/ComposeBlock.module.css +74 -0
  32. package/src/components/ConductionBg/ConductionBg.jsx +150 -0
  33. package/src/components/ConductionBg/ConductionBg.module.css +41 -0
  34. package/src/components/ContentCard/ContentCard.jsx +126 -0
  35. package/src/components/ContentCard/ContentCard.module.css +84 -0
  36. package/src/components/ContentDetailHero/ContentDetailHero.jsx +136 -0
  37. package/src/components/ContentDetailHero/ContentDetailHero.module.css +96 -0
  38. package/src/components/ContentTypeFilter/ContentTypeFilter.jsx +103 -0
  39. package/src/components/ContentTypeFilter/ContentTypeFilter.module.css +60 -0
  40. package/src/components/ContentTypeFilter/contentTypes.js +58 -0
  41. package/src/components/CookieCli/CookieCli.jsx +223 -0
  42. package/src/components/CookieCli/CookieCli.module.css +166 -0
  43. package/src/components/CtaBanner/CtaBanner.jsx +61 -0
  44. package/src/components/CtaBanner/CtaBanner.module.css +65 -0
  45. package/src/components/DetailHero/DetailHero.jsx +143 -0
  46. package/src/components/DetailHero/DetailHero.module.css +154 -0
  47. package/src/components/Diagrams/Diagrams.jsx +148 -0
  48. package/src/components/EmployeeCard/EmployeeCard.jsx +127 -0
  49. package/src/components/EmployeeCard/EmployeeCard.module.css +144 -0
  50. package/src/components/ExternalAppShelf/ExternalAppShelf.jsx +61 -0
  51. package/src/components/ExternalAppShelf/ExternalAppShelf.module.css +90 -0
  52. package/src/components/FAQ/FAQ.jsx +42 -0
  53. package/src/components/FAQ/FAQ.module.css +74 -0
  54. package/src/components/FacetedFilters/FacetedFilters.jsx +125 -0
  55. package/src/components/FacetedFilters/FacetedFilters.module.css +133 -0
  56. package/src/components/FeatureGrid/FeatureGrid.jsx +94 -0
  57. package/src/components/FeatureGrid/FeatureGrid.module.css +114 -0
  58. package/src/components/FeatureList/FeatureList.jsx +54 -0
  59. package/src/components/FeatureList/FeatureList.module.css +52 -0
  60. package/src/components/FeaturedCard/FeaturedCard.jsx +101 -0
  61. package/src/components/FeaturedCard/FeaturedCard.module.css +98 -0
  62. package/src/components/GameModal/GameModal.jsx +197 -0
  63. package/src/components/GameModal/GameModal.module.css +184 -0
  64. package/src/components/Hero/Hero.jsx +101 -0
  65. package/src/components/Hero/Hero.module.css +95 -0
  66. package/src/components/HexBackground/HexBackground.jsx +56 -0
  67. package/src/components/HexBackground/HexBackground.module.css +73 -0
  68. package/src/components/HexNetwork/HexNetwork.jsx +141 -0
  69. package/src/components/HexNetwork/HexNetwork.module.css +187 -0
  70. package/src/components/HexRain/HexRain.jsx +81 -0
  71. package/src/components/HowSteps/HowSteps.jsx +57 -0
  72. package/src/components/HowSteps/HowSteps.module.css +52 -0
  73. package/src/components/ManagedCommonGround/ManagedCommonGround.jsx +78 -0
  74. package/src/components/ManagedCommonGround/ManagedCommonGround.module.css +16 -0
  75. package/src/components/NewsletterCta/NewsletterCta.jsx +83 -0
  76. package/src/components/NewsletterCta/NewsletterCta.module.css +103 -0
  77. package/src/components/PairCard/PairCard.jsx +58 -0
  78. package/src/components/PairCard/PairCard.module.css +54 -0
  79. package/src/components/PartnerCard/PartnerCard.jsx +130 -0
  80. package/src/components/PartnerCard/PartnerCard.module.css +198 -0
  81. package/src/components/PartnerDirectory/PartnerDirectory.jsx +122 -0
  82. package/src/components/PartnerDirectory/PartnerDirectory.module.css +25 -0
  83. package/src/components/PartnerSidecard/PartnerSidecard.jsx +116 -0
  84. package/src/components/PartnerSidecard/PartnerSidecard.module.css +185 -0
  85. package/src/components/Pipeline/Pipeline.jsx +198 -0
  86. package/src/components/Pipeline/Pipeline.module.css +206 -0
  87. package/src/components/PlatformDiagram/PlatformDiagram.jsx +110 -0
  88. package/src/components/PlatformOverview/PlatformOverview.jsx +68 -0
  89. package/src/components/PlatformOverview/PlatformOverview.module.css +71 -0
  90. package/src/components/ReferenceCard/ReferenceCard.jsx +44 -0
  91. package/src/components/ReferenceCard/ReferenceCard.module.css +57 -0
  92. package/src/components/RelatedPosts/RelatedPosts.jsx +58 -0
  93. package/src/components/RelatedPosts/RelatedPosts.module.css +51 -0
  94. package/src/components/RotatingCards/RotatingCards.jsx +98 -0
  95. package/src/components/RotatingCards/RotatingCards.module.css +153 -0
  96. package/src/components/Showcase/Showcase.jsx +129 -0
  97. package/src/components/Showcase/Showcase.module.css +168 -0
  98. package/src/components/SolutionCard/SolutionCard.jsx +83 -0
  99. package/src/components/SolutionCard/SolutionCard.module.css +99 -0
  100. package/src/components/StatsStrip/StatsStrip.jsx +38 -0
  101. package/src/components/StatsStrip/StatsStrip.module.css +53 -0
  102. package/src/components/WidgetShelf/WidgetShelf.jsx +67 -0
  103. package/src/components/WidgetShelf/WidgetShelf.module.css +73 -0
  104. package/src/components/index.js +96 -0
  105. package/src/components/primitives/AuthorByline.jsx +85 -0
  106. package/src/components/primitives/AuthorByline.module.css +57 -0
  107. package/src/components/primitives/BrandCitation.jsx +71 -0
  108. package/src/components/primitives/Button.jsx +46 -0
  109. package/src/components/primitives/Button.module.css +88 -0
  110. package/src/components/primitives/Card.jsx +42 -0
  111. package/src/components/primitives/Card.module.css +42 -0
  112. package/src/components/primitives/Eyebrow.jsx +37 -0
  113. package/src/components/primitives/Eyebrow.module.css +19 -0
  114. package/src/components/primitives/HexBullet.jsx +37 -0
  115. package/src/components/primitives/HexBullet.module.css +16 -0
  116. package/src/components/primitives/HexThumbnail.jsx +70 -0
  117. package/src/components/primitives/HexThumbnail.module.css +45 -0
  118. package/src/components/primitives/Pill.jsx +42 -0
  119. package/src/components/primitives/Pill.module.css +30 -0
  120. package/src/components/primitives/Section.jsx +51 -0
  121. package/src/components/primitives/Section.module.css +31 -0
  122. package/src/components/primitives/SectionHead.jsx +36 -0
  123. package/src/components/primitives/SectionHead.module.css +43 -0
  124. package/src/components/primitives/index.js +22 -0
  125. package/src/css/brand.css +158 -0
  126. package/src/css/tokens.css +12 -0
  127. package/src/data/app-downloads.js +42 -0
  128. package/src/diagrams/README.md +74 -0
  129. package/src/diagrams/cn-domain-tree.js +105 -0
  130. package/src/diagrams/cn-hex-prism.js +163 -0
  131. package/src/diagrams/cn-hex.js +181 -0
  132. package/src/diagrams/cn-honeycomb-bg.js +135 -0
  133. package/src/diagrams/cn-pipeline.js +150 -0
  134. package/src/diagrams/cn-platform.js +156 -0
  135. package/src/diagrams/cn-side-box.js +104 -0
  136. package/src/diagrams/index.js +28 -0
  137. package/src/index.js +183 -0
  138. package/src/theme/Footer/index.jsx +516 -0
  139. package/src/theme/MDXPage/index.jsx +134 -0
  140. package/src/theme/Navbar/index.jsx +120 -0
  141. package/src/theme/Navbar/styles.module.css +114 -0
  142. package/src/theme/brand.jsx +63 -0
  143. package/src/theme.js +45 -0
  144. package/src/utils/lazyScript.js +37 -0
  145. package/static/img/favicon.svg +14 -0
  146. package/static/img/honeycomb-scatter.svg +23 -0
  147. package/static/img/honeycomb-watermark.svg +108 -0
  148. package/static/img/logo-dark.svg +11 -0
  149. package/static/img/logo.svg +14 -0
  150. package/static/img/nextcloud-logo.svg +5 -0
  151. package/static/lib/canal-footer.css +418 -0
  152. package/static/lib/canal-footer.js +499 -0
  153. package/static/lib/clients-flow.js +317 -0
  154. package/static/lib/conduction-bg.css +50 -0
  155. package/static/lib/conduction-bg.js +122 -0
  156. package/static/lib/hex-rain.css +128 -0
  157. package/static/lib/hex-rain.js +284 -0
  158. package/static/lib/kade-cyclist.css +264 -0
  159. package/static/lib/kade-cyclist.js +420 -0
  160. package/static/lib/logo-memory.css +219 -0
  161. package/static/lib/logo-memory.js +540 -0
  162. package/static/lib/platform-diagram.css +458 -0
  163. package/static/lib/platform-diagram.js +414 -0
@@ -0,0 +1,499 @@
1
+ /* Canal footer — randomised skyline + drifting boats + a click-to-sink
2
+ mini-game. Boats use a CSS keyframe drift on page load; spawned boats
3
+ (added by the mini-game once it starts) use Web Animations API so the
4
+ drift duration can scale with the per-game speed multiplier.
5
+
6
+ LEVELS (driven by the score countdown from 100):
7
+ 100→81 drift fleet only calm, ~1.4s/spawn
8
+ 80→61 + sailing ships (HP 2) ~1.0s/spawn
9
+ 60→41 + cargo ships (HP 3) ~0.7s/spawn
10
+ 40→21 + frigates (HP 4) ~0.5s/spawn, 2 at a time
11
+ 20→11 + cruise-bosses (HP 5) ~0.3s/spawn, 2-3 at a time
12
+ 10→2 cruise swarm + chaos ~0.2s/spawn, 3 at a time
13
+ 1 battleship boss (HP 10) regular spawns paused
14
+ */
15
+ /* On Docusaurus SPA navigation React unmounts the old <Footer/> and
16
+ mounts a new one. The IIFE pattern (run once at script load, bind to
17
+ the first .canal-footer found) bound listeners and timers to the
18
+ detached old DOM — so on the new page the skyline placeholder stayed
19
+ empty and boat clicks didn't register, until a hard reload re-ran
20
+ the script. The fix: wrap setup in init(), expose it as
21
+ window.CanalFooter.hydrate, and have the Footer swizzle call it on
22
+ every mount. The data-canal-footer-ready attribute keeps repeated
23
+ calls on the same root idempotent; _cleanup tears down the previous
24
+ instance's timers so they don't leak across route changes. */
25
+ (function () {
26
+ function init() {
27
+ const root = document.querySelector('.canal-footer');
28
+ if (!root) return;
29
+ /* Same DOM node we already wired? Bail. The dataset attribute is
30
+ attached to the LIVE root; React replaces the whole footer node
31
+ on SPA navigation, so a fresh root has no attribute and falls
32
+ through to setup. */
33
+ if (root.dataset.canalFooterReady === '1') return;
34
+ /* Genuinely fresh root, so the *previous* instance (if any) was
35
+ bound to a now-detached DOM. Stop its timers and unhook its
36
+ window listeners before binding new ones. */
37
+ if (window.CanalFooter && window.CanalFooter._cleanup) {
38
+ try { window.CanalFooter._cleanup(); } catch (e) { /* ignore */ }
39
+ }
40
+ root.dataset.canalFooterReady = '1';
41
+
42
+ // ===== Skyline (random row of trapgevels) =====
43
+ const skyline = root.querySelector('.skyline');
44
+ const houseTypes = ['h-a', 'h-b', 'h-c', 'h-d', 'h-e'];
45
+ const houseWidths = { 'h-a': 48, 'h-b': 60, 'h-c': 42, 'h-d': 54, 'h-e': 57 };
46
+
47
+ function buildSkyline() {
48
+ skyline.innerHTML = '';
49
+ let total = 0;
50
+ const target = window.innerWidth + 200;
51
+ let prev = null;
52
+ while (total < target) {
53
+ let pick;
54
+ do {
55
+ pick = houseTypes[(Math.random() * houseTypes.length) | 0];
56
+ } while (pick === prev && Math.random() < 0.7);
57
+ prev = pick;
58
+ const tpl = document.getElementById('tpl-' + pick);
59
+ if (!tpl) continue;
60
+ const wrap = document.createElement('div');
61
+ wrap.className = 'house-wrap';
62
+ wrap.appendChild(tpl.content.cloneNode(true));
63
+ if (Math.random() < 0.30) wrap.querySelector('svg').classList.add('flipped');
64
+ skyline.appendChild(wrap);
65
+ total += houseWidths[pick];
66
+ }
67
+ const all = skyline.querySelectorAll('.house-wrap');
68
+ if (all.length) all[Math.floor(all.length / 2)].classList.add('house-conduction');
69
+ }
70
+
71
+ // ===== Random per-item drift speeds for the initial fleet =====
72
+ const initialDriftSpeeds = {
73
+ 'ki-bike-1': [55, 95],
74
+ 'ki-bike-2': [60, 100],
75
+ 'ki-car-1': [70, 120],
76
+ 'ki-car-2': [75, 130],
77
+ 'ci-cruise': [180, 280],
78
+ 'ci-sloep': [120, 200],
79
+ 'ci-row': [200, 320],
80
+ 'ci-swim': [320, 480],
81
+ 'ci-hover': [90, 150],
82
+ 'ci-periscope': [240, 380],
83
+ 'ci-whale': [220, 340],
84
+ };
85
+ function rollSpeeds() {
86
+ Object.entries(initialDriftSpeeds).forEach(([cls, [lo, hi]]) => {
87
+ const el = root.querySelector('.' + cls);
88
+ if (!el) return;
89
+ const dur = lo + Math.random() * (hi - lo);
90
+ el.style.animationDuration = dur + 's';
91
+ el.style.animationDelay = '-' + (Math.random() * dur).toFixed(1) + 's';
92
+ });
93
+ }
94
+
95
+ // ===== Conduction office (#14) opens Google Maps =====
96
+ const MAPS_URL = 'https://maps.google.com/?q=Lauriergracht+14h+Amsterdam';
97
+ function wireConductionHouse() {
98
+ const house = root.querySelector('.house-conduction');
99
+ if (!house) return;
100
+ house.title = 'Conduction — Lauriergracht 14h, 1016 RL Amsterdam';
101
+ house.addEventListener('click', () => window.open(MAPS_URL, '_blank', 'noopener'));
102
+ }
103
+
104
+ // ===== Drift-fleet templates =====
105
+ // Cache a detached clone of each drift-boat SVG at page-load. Without this,
106
+ // spawnBoat() would call querySelector('.ci-<type>') which returns null
107
+ // once the original is sunk — and the spawn loop would go silent after
108
+ // the player cleared the initial fleet.
109
+ const DRIFT_TYPES = ['cruise', 'sloep', 'row', 'swim', 'hover', 'periscope', 'whale'];
110
+ const driftTemplates = {};
111
+ function captureDriftTemplates() {
112
+ DRIFT_TYPES.forEach(type => {
113
+ const el = root.querySelector('.ci-' + type);
114
+ if (el && !driftTemplates[type]) driftTemplates[type] = el.cloneNode(true);
115
+ });
116
+ }
117
+
118
+ const canalItems = root.querySelector('.canal-items');
119
+ const hud = root.querySelector('.game-hud');
120
+ const hudCounter = root.querySelector('[data-counter]');
121
+ const hudTimer = root.querySelector('[data-timer]');
122
+ const hudCounterBlock = root.querySelector('.hud-counter');
123
+ const hudTimerBlock = root.querySelector('.hud-timer');
124
+ const goPanel = root.querySelector('.game-over');
125
+ const goTitle = root.querySelector('[data-go-title]');
126
+ const goSunk = root.querySelector('[data-go-sunk]');
127
+ const goRestart = root.querySelector('[data-restart]');
128
+
129
+ let game;
130
+
131
+ function newGameState() {
132
+ return {
133
+ started: false,
134
+ over: false,
135
+ score: 100,
136
+ timeLeft: 60,
137
+ sunkTotal: 0,
138
+ swarmTriggered: false,
139
+ bossTriggered: false,
140
+ bossActive: false,
141
+ timerInt: null,
142
+ spawnTimer: null,
143
+ };
144
+ }
145
+ game = newGameState();
146
+ window.__minigame = game; // exposed for testing
147
+
148
+ function updateHud() {
149
+ hudCounter.textContent = String(Math.max(0, game.score));
150
+ hudTimer.textContent = String(Math.max(0, game.timeLeft));
151
+ if (game.timeLeft <= 5 && game.started) hudTimerBlock.classList.add('urgent');
152
+ else hudTimerBlock.classList.remove('urgent');
153
+ }
154
+
155
+ function flashCounter() {
156
+ hudCounterBlock.classList.remove('tick');
157
+ void hudCounterBlock.offsetWidth;
158
+ hudCounterBlock.classList.add('tick');
159
+ }
160
+
161
+ function startGame() {
162
+ if (game.started || game.over) return;
163
+ game.started = true;
164
+ hud.classList.add('active');
165
+ updateHud();
166
+ game.timerInt = setInterval(() => {
167
+ if (game.over) return;
168
+ game.timeLeft -= 1;
169
+ updateHud();
170
+ if (game.timeLeft <= 0) endGame(false);
171
+ }, 1000);
172
+ scheduleNextSpawn();
173
+ }
174
+
175
+ function endGame(victory) {
176
+ if (game.over) return;
177
+ game.over = true;
178
+ if (game.timerInt) clearInterval(game.timerInt);
179
+ if (game.spawnTimer) clearTimeout(game.spawnTimer);
180
+ goTitle.textContent = victory ? 'Victory!' : "Time's up";
181
+ goSunk.textContent = String(game.sunkTotal);
182
+ goPanel.classList.add('show');
183
+ hud.classList.remove('active');
184
+ // Notify the gaming modal (if mounted) so it can update the cookie
185
+ // and reveal the cross-site progress panel.
186
+ window.dispatchEvent(new CustomEvent('connext:gameend', {
187
+ detail: {
188
+ id: 'boats',
189
+ won: victory,
190
+ score: game.sunkTotal,
191
+ summary: game.sunkTotal + ' boat' + (game.sunkTotal === 1 ? '' : 's') + ' sunk',
192
+ },
193
+ }));
194
+ }
195
+
196
+ function resetGame() {
197
+ if (game.timerInt) clearInterval(game.timerInt);
198
+ if (game.spawnTimer) clearTimeout(game.spawnTimer);
199
+ canalItems.querySelectorAll('.ci[data-spawned], .ci[data-sinking]').forEach(el => el.remove());
200
+ game = newGameState();
201
+ window.__minigame = game;
202
+ hudTimerBlock.classList.remove('urgent');
203
+ goPanel.classList.remove('show');
204
+ updateHud();
205
+ wireOriginalBoats();
206
+ // Restart kicks off immediately — without this the player would face an
207
+ // empty canal until the spawn loop produced its first boat.
208
+ startGame();
209
+ }
210
+ goRestart.addEventListener('click', resetGame);
211
+ /* The connext:gamereplay listener is bound near the bottom of init()
212
+ so the cleanup handler can remove it on the next route change. */
213
+
214
+ // ===== Pace knobs — driven by the further of (time elapsed) and (score lost) =====
215
+ function progress() {
216
+ const tTime = game.started ? (60 - game.timeLeft) / 60 : 0;
217
+ const tScore = (100 - game.score) / 100;
218
+ return Math.max(tTime, tScore);
219
+ }
220
+ function getSpawnInterval() {
221
+ if (game.bossActive) return 9999;
222
+ return Math.max(140, 1400 - progress() * 1260);
223
+ }
224
+ function getSpeedMult() {
225
+ return 1 + progress() * 4; // 1× → 5×
226
+ }
227
+ function getSpawnCount() {
228
+ const s = game.score;
229
+ if (s <= 10) return 3;
230
+ if (s <= 40) return 2;
231
+ return 1;
232
+ }
233
+
234
+ // ===== Spawn pool — bigger ships unlock as the score drops =====
235
+ function pickSpawnType() {
236
+ const s = game.score;
237
+ const pool = [];
238
+ pool.push({ type: 'sloep', hp: 1, w: 4 });
239
+ pool.push({ type: 'row', hp: 1, w: 3 });
240
+ pool.push({ type: 'swim', hp: 1, w: 1 });
241
+ pool.push({ type: 'hover', hp: 1, w: 2 });
242
+ pool.push({ type: 'periscope', hp: 1, w: 1 });
243
+ pool.push({ type: 'whale', hp: 1, w: 1 });
244
+ pool.push({ type: 'cruise', hp: 1, w: 2 });
245
+ if (s <= 80) pool.push({ type: 'sailing', hp: 2, w: 3 });
246
+ if (s <= 60) pool.push({ type: 'cargo', hp: 3, w: 3 });
247
+ if (s <= 40) pool.push({ type: 'frigate', hp: 4, w: 3 });
248
+ if (s <= 20) pool.push({ type: 'cruise', hp: 5, w: 2, boss: true });
249
+ let total = 0;
250
+ for (const x of pool) total += x.w;
251
+ let r = Math.random() * total;
252
+ for (const x of pool) {
253
+ r -= x.w;
254
+ if (r <= 0) return x;
255
+ }
256
+ return pool[0];
257
+ }
258
+
259
+ function scheduleNextSpawn() {
260
+ if (game.over) return;
261
+ const interval = getSpawnInterval();
262
+ game.spawnTimer = setTimeout(() => {
263
+ if (!game.over && !game.bossActive) {
264
+ const count = getSpawnCount();
265
+ for (let i = 0; i < count; i++) {
266
+ const pick = pickSpawnType();
267
+ spawnBoat(pick.type, { hp: pick.hp, boss: pick.boss });
268
+ }
269
+ }
270
+ scheduleNextSpawn();
271
+ }, interval);
272
+ }
273
+
274
+ // ===== Spawn a single boat (clone or template), drive drift via WAAPI =====
275
+ function spawnBoat(type, opts) {
276
+ opts = opts || {};
277
+ let clone;
278
+ if (type === 'battleship' || type === 'sailing' || type === 'cargo' || type === 'frigate') {
279
+ const tplId = type === 'battleship' ? 'tpl-battleship' : ('tpl-ship-' + type);
280
+ const tpl = document.getElementById(tplId);
281
+ if (!tpl) return null;
282
+ clone = tpl.content.firstElementChild.cloneNode(true);
283
+ } else {
284
+ // Drift-fleet types are cloned from the cached templates so the spawn
285
+ // loop keeps working after the initial fleet has been sunk.
286
+ const tmpl = driftTemplates[type];
287
+ if (!tmpl) return null;
288
+ clone = tmpl.cloneNode(true);
289
+ clone.style.animation = 'none';
290
+ }
291
+ clone.dataset.spawned = '1';
292
+ delete clone.dataset.wired;
293
+ delete clone.dataset.sinking;
294
+
295
+ if (opts.boss) {
296
+ clone.classList.add('boss');
297
+ // Cruise-boss = upscale the regular cruise SVG so it reads as bigger.
298
+ if (type === 'cruise') {
299
+ clone.setAttribute('width', '184');
300
+ clone.setAttribute('height', '44');
301
+ }
302
+ }
303
+ if (opts.hp && opts.hp > 1) {
304
+ clone.dataset.hp = String(opts.hp);
305
+ addHpPips(clone, opts.hp);
306
+ }
307
+
308
+ canalItems.appendChild(clone);
309
+
310
+ // Stagger small-drift items vertically so they don't line up; bigger
311
+ // hulled ships keep their template-prescribed waterline.
312
+ if (!opts.boss && type !== 'cruise' && type !== 'sailing' && type !== 'cargo' && type !== 'frigate') {
313
+ clone.style.bottom = (4 + Math.random() * 28) + 'px';
314
+ } else if (type === 'sailing' || type === 'cargo' || type === 'frigate') {
315
+ clone.style.bottom = '4px';
316
+ }
317
+
318
+ const w = window.innerWidth;
319
+ const fromRight = Math.random() < 0.5;
320
+ const startX = fromRight ? (w + 100) : -300;
321
+ const endX = fromRight ? -300 : (w + 100);
322
+ const dur = (baseDurationFor(type, opts) * 1000) / getSpeedMult();
323
+ const anim = clone.animate(
324
+ [{ transform: 'translateX(' + startX + 'px)' },
325
+ { transform: 'translateX(' + endX + 'px)' }],
326
+ { duration: dur, easing: 'linear', fill: 'forwards' }
327
+ );
328
+ clone._anim = anim;
329
+ anim.onfinish = () => { if (!clone.dataset.sinking) clone.remove(); };
330
+
331
+ clone.addEventListener('click', () => onShipClick(clone));
332
+ return clone;
333
+ }
334
+
335
+ function baseDurationFor(type, opts) {
336
+ if (type === 'battleship') return 32;
337
+ if (opts && opts.boss) return 30; // cruise-boss
338
+ if (type === 'cargo') return 26;
339
+ if (type === 'sailing') return 22;
340
+ if (type === 'frigate') return 18;
341
+ if (type === 'whale' || type === 'periscope') return 24;
342
+ if (type === 'swim') return 30;
343
+ if (type === 'cruise') return 22;
344
+ if (type === 'sloep' || type === 'row') return 18;
345
+ if (type === 'hover') return 14;
346
+ return 18;
347
+ }
348
+
349
+ // Pip row inside the ship's SVG — placed near the top of the viewBox so
350
+ // they read as little orange "health" dots above the deck.
351
+ function addHpPips(el, hp) {
352
+ if (el.querySelector('.hp-pips')) return; // template already provides them
353
+ const ns = 'http://www.w3.org/2000/svg';
354
+ const vb = (el.getAttribute('viewBox') || '0 0 100 30').split(/\s+/).map(Number);
355
+ const vx = vb[0], vy = vb[1], vw = vb[2];
356
+ const g = document.createElementNS(ns, 'g');
357
+ g.setAttribute('class', 'hp-pips');
358
+ const pipR = 1.6;
359
+ const pipGap = 4.4;
360
+ const totalW = (hp - 1) * pipGap;
361
+ const startPx = vx + (vw - totalW) / 2;
362
+ const py = vy + 2.6;
363
+ for (let i = 0; i < hp; i++) {
364
+ const c = document.createElementNS(ns, 'circle');
365
+ c.setAttribute('class', 'pip');
366
+ c.setAttribute('cx', String(startPx + i * pipGap));
367
+ c.setAttribute('cy', String(py));
368
+ c.setAttribute('r', String(pipR));
369
+ c.setAttribute('fill', 'var(--c-orange-knvb)');
370
+ g.appendChild(c);
371
+ }
372
+ el.appendChild(g);
373
+ }
374
+
375
+ // ===== Click handling: damage on multi-HP, sink on HP→0 =====
376
+ function onShipClick(el) {
377
+ if (game.over || el.dataset.sinking) return;
378
+ let hp = parseInt(el.dataset.hp || '1', 10);
379
+ if (hp > 1) {
380
+ hp -= 1;
381
+ el.dataset.hp = String(hp);
382
+ const pips = el.querySelectorAll('.hp-pips .pip');
383
+ for (let i = pips.length - 1; i >= 0; i--) {
384
+ if (!pips[i].classList.contains('hit')) {
385
+ pips[i].classList.add('hit');
386
+ break;
387
+ }
388
+ }
389
+ el.classList.remove('damaged');
390
+ void el.offsetWidth;
391
+ el.classList.add('damaged');
392
+ setTimeout(() => el.classList.remove('damaged'), 240);
393
+ if (!game.started) startGame();
394
+ return;
395
+ }
396
+ sinkBoat(el);
397
+ onShipSunk();
398
+ }
399
+
400
+ function onShipSunk() {
401
+ game.sunkTotal += 1;
402
+ game.score = Math.max(0, game.score - 1);
403
+ updateHud();
404
+ flashCounter();
405
+ if (!game.started && !game.over) startGame();
406
+ if (game.score === 10 && !game.swarmTriggered) {
407
+ game.swarmTriggered = true;
408
+ triggerCruiseSwarm();
409
+ }
410
+ if (game.score === 1 && !game.bossTriggered) {
411
+ game.bossTriggered = true;
412
+ triggerBattleship();
413
+ }
414
+ if (game.score <= 0) endGame(true);
415
+ }
416
+
417
+ function triggerCruiseSwarm() {
418
+ let i = 0;
419
+ (function next() {
420
+ if (game.over) return;
421
+ spawnBoat('cruise', { boss: true, hp: 5 });
422
+ i++;
423
+ if (i < 5) setTimeout(next, 600);
424
+ })();
425
+ }
426
+
427
+ function triggerBattleship() {
428
+ game.bossActive = true;
429
+ spawnBoat('battleship', { boss: true, hp: 10 });
430
+ }
431
+
432
+ // ===== Sink animation (drop + tilt + fade) =====
433
+ function sinkBoat(el) {
434
+ if (el.dataset.sinking) return;
435
+ el.dataset.sinking = '1';
436
+ let x = 0;
437
+ try {
438
+ const m = new DOMMatrixReadOnly(getComputedStyle(el).transform);
439
+ x = m.m41 || 0;
440
+ } catch (e) { /* keep x=0 */ }
441
+ el.style.animation = 'none';
442
+ if (el._anim) try { el._anim.cancel(); } catch (e) {}
443
+ el.style.transform = 'translateX(' + x + 'px)';
444
+ void el.offsetWidth;
445
+ const dur = el.classList.contains('boss') ? 3000 : 2200;
446
+ const anim = el.animate(
447
+ [
448
+ { transform: 'translateX(' + x + 'px) translateY(0) rotate(0deg)', opacity: 1 },
449
+ { transform: 'translateX(' + x + 'px) translateY(8px) rotate(5deg)', opacity: 1, offset: 0.25 },
450
+ { transform: 'translateX(' + x + 'px) translateY(40px) rotate(-7deg)', opacity: 0.5, offset: 0.7 },
451
+ { transform: 'translateX(' + x + 'px) translateY(80px) rotate(15deg)', opacity: 0 }
452
+ ],
453
+ { duration: dur, easing: 'cubic-bezier(.4,.1,.7,.4)', fill: 'forwards' }
454
+ );
455
+ anim.finished.then(() => el.remove()).catch(() => {});
456
+ }
457
+
458
+ function wireOriginalBoats() {
459
+ root.querySelectorAll('.canal-items .ci').forEach((b) => {
460
+ if (b.dataset.wired) return;
461
+ b.dataset.wired = '1';
462
+ b.title = 'click to sink';
463
+ b.addEventListener('click', () => onShipClick(b));
464
+ });
465
+ }
466
+
467
+ // ===== Bootstrap =====
468
+ buildSkyline();
469
+ rollSpeeds();
470
+ wireConductionHouse();
471
+ captureDriftTemplates();
472
+ wireOriginalBoats();
473
+ updateHud();
474
+
475
+ let resizeT;
476
+ const onResize = () => {
477
+ clearTimeout(resizeT);
478
+ resizeT = setTimeout(() => { buildSkyline(); rollSpeeds(); wireConductionHouse(); }, 200);
479
+ };
480
+ const onReplay = (e) => { if (e.detail?.id === 'boats') resetGame(); };
481
+ window.addEventListener('resize', onResize);
482
+ window.addEventListener('connext:gamereplay', onReplay);
483
+
484
+ /* Tear-down for the *previous* root: stop its timers and unhook
485
+ its window listeners. Saved on the global so the next init()
486
+ call can run it before binding fresh handlers. */
487
+ window.CanalFooter._cleanup = function () {
488
+ if (game.timerInt) clearInterval(game.timerInt);
489
+ if (game.spawnTimer) clearTimeout(game.spawnTimer);
490
+ clearTimeout(resizeT);
491
+ window.removeEventListener('resize', onResize);
492
+ window.removeEventListener('connext:gamereplay', onReplay);
493
+ };
494
+ }
495
+
496
+ window.CanalFooter = window.CanalFooter || {};
497
+ window.CanalFooter.hydrate = init;
498
+ init();
499
+ })();