@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.
- package/MISSING_COMPONENTS.md +109 -0
- package/README.md +171 -0
- package/package.json +59 -0
- package/src/components/AgentTrace/AgentTrace.jsx +128 -0
- package/src/components/AgentTrace/AgentTrace.module.css +115 -0
- package/src/components/AppMock/AppMock.jsx +86 -0
- package/src/components/AppMock/AppMock.module.css +629 -0
- package/src/components/AppMock/variants/DeciDeskMock.jsx +71 -0
- package/src/components/AppMock/variants/DocuDeskMock.jsx +69 -0
- package/src/components/AppMock/variants/LarpingAppMock.jsx +59 -0
- package/src/components/AppMock/variants/MyDashBiMock.jsx +135 -0
- package/src/components/AppMock/variants/MyDashMock.jsx +96 -0
- package/src/components/AppMock/variants/MyDashTilesMock.jsx +103 -0
- package/src/components/AppMock/variants/MyDashWidgetsMock.jsx +123 -0
- package/src/components/AppMock/variants/NLDesignMock.jsx +70 -0
- package/src/components/AppMock/variants/OpenCatalogiMock.jsx +61 -0
- package/src/components/AppMock/variants/OpenConnectorMock.jsx +83 -0
- package/src/components/AppMock/variants/OpenRegisterMock.jsx +100 -0
- package/src/components/AppMock/variants/OpenWooMock.jsx +61 -0
- package/src/components/AppMock/variants/PipelinQMock.jsx +88 -0
- package/src/components/AppMock/variants/ProcestMock.jsx +87 -0
- package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +71 -0
- package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +71 -0
- package/src/components/AppsGrid/AppsGrid.jsx +84 -0
- package/src/components/AppsGrid/AppsGrid.module.css +46 -0
- package/src/components/AppsPreview/AppsPreview.jsx +85 -0
- package/src/components/AppsPreview/AppsPreview.module.css +128 -0
- package/src/components/Clients/Clients.jsx +205 -0
- package/src/components/Clients/Clients.module.css +166 -0
- package/src/components/ComposeBlock/ComposeBlock.jsx +70 -0
- package/src/components/ComposeBlock/ComposeBlock.module.css +74 -0
- package/src/components/ConductionBg/ConductionBg.jsx +150 -0
- package/src/components/ConductionBg/ConductionBg.module.css +41 -0
- package/src/components/ContentCard/ContentCard.jsx +126 -0
- package/src/components/ContentCard/ContentCard.module.css +84 -0
- package/src/components/ContentDetailHero/ContentDetailHero.jsx +136 -0
- package/src/components/ContentDetailHero/ContentDetailHero.module.css +96 -0
- package/src/components/ContentTypeFilter/ContentTypeFilter.jsx +103 -0
- package/src/components/ContentTypeFilter/ContentTypeFilter.module.css +60 -0
- package/src/components/ContentTypeFilter/contentTypes.js +58 -0
- package/src/components/CookieCli/CookieCli.jsx +223 -0
- package/src/components/CookieCli/CookieCli.module.css +166 -0
- package/src/components/CtaBanner/CtaBanner.jsx +61 -0
- package/src/components/CtaBanner/CtaBanner.module.css +65 -0
- package/src/components/DetailHero/DetailHero.jsx +143 -0
- package/src/components/DetailHero/DetailHero.module.css +154 -0
- package/src/components/Diagrams/Diagrams.jsx +148 -0
- package/src/components/EmployeeCard/EmployeeCard.jsx +127 -0
- package/src/components/EmployeeCard/EmployeeCard.module.css +144 -0
- package/src/components/ExternalAppShelf/ExternalAppShelf.jsx +61 -0
- package/src/components/ExternalAppShelf/ExternalAppShelf.module.css +90 -0
- package/src/components/FAQ/FAQ.jsx +42 -0
- package/src/components/FAQ/FAQ.module.css +74 -0
- package/src/components/FacetedFilters/FacetedFilters.jsx +125 -0
- package/src/components/FacetedFilters/FacetedFilters.module.css +133 -0
- package/src/components/FeatureGrid/FeatureGrid.jsx +94 -0
- package/src/components/FeatureGrid/FeatureGrid.module.css +114 -0
- package/src/components/FeatureList/FeatureList.jsx +54 -0
- package/src/components/FeatureList/FeatureList.module.css +52 -0
- package/src/components/FeaturedCard/FeaturedCard.jsx +101 -0
- package/src/components/FeaturedCard/FeaturedCard.module.css +98 -0
- package/src/components/GameModal/GameModal.jsx +197 -0
- package/src/components/GameModal/GameModal.module.css +184 -0
- package/src/components/Hero/Hero.jsx +101 -0
- package/src/components/Hero/Hero.module.css +95 -0
- package/src/components/HexBackground/HexBackground.jsx +56 -0
- package/src/components/HexBackground/HexBackground.module.css +73 -0
- package/src/components/HexNetwork/HexNetwork.jsx +141 -0
- package/src/components/HexNetwork/HexNetwork.module.css +187 -0
- package/src/components/HexRain/HexRain.jsx +81 -0
- package/src/components/HowSteps/HowSteps.jsx +57 -0
- package/src/components/HowSteps/HowSteps.module.css +52 -0
- package/src/components/ManagedCommonGround/ManagedCommonGround.jsx +78 -0
- package/src/components/ManagedCommonGround/ManagedCommonGround.module.css +16 -0
- package/src/components/NewsletterCta/NewsletterCta.jsx +83 -0
- package/src/components/NewsletterCta/NewsletterCta.module.css +103 -0
- package/src/components/PairCard/PairCard.jsx +58 -0
- package/src/components/PairCard/PairCard.module.css +54 -0
- package/src/components/PartnerCard/PartnerCard.jsx +130 -0
- package/src/components/PartnerCard/PartnerCard.module.css +198 -0
- package/src/components/PartnerDirectory/PartnerDirectory.jsx +122 -0
- package/src/components/PartnerDirectory/PartnerDirectory.module.css +25 -0
- package/src/components/PartnerSidecard/PartnerSidecard.jsx +116 -0
- package/src/components/PartnerSidecard/PartnerSidecard.module.css +185 -0
- package/src/components/Pipeline/Pipeline.jsx +198 -0
- package/src/components/Pipeline/Pipeline.module.css +206 -0
- package/src/components/PlatformDiagram/PlatformDiagram.jsx +110 -0
- package/src/components/PlatformOverview/PlatformOverview.jsx +68 -0
- package/src/components/PlatformOverview/PlatformOverview.module.css +71 -0
- package/src/components/ReferenceCard/ReferenceCard.jsx +44 -0
- package/src/components/ReferenceCard/ReferenceCard.module.css +57 -0
- package/src/components/RelatedPosts/RelatedPosts.jsx +58 -0
- package/src/components/RelatedPosts/RelatedPosts.module.css +51 -0
- package/src/components/RotatingCards/RotatingCards.jsx +98 -0
- package/src/components/RotatingCards/RotatingCards.module.css +153 -0
- package/src/components/Showcase/Showcase.jsx +129 -0
- package/src/components/Showcase/Showcase.module.css +168 -0
- package/src/components/SolutionCard/SolutionCard.jsx +83 -0
- package/src/components/SolutionCard/SolutionCard.module.css +99 -0
- package/src/components/StatsStrip/StatsStrip.jsx +38 -0
- package/src/components/StatsStrip/StatsStrip.module.css +53 -0
- package/src/components/WidgetShelf/WidgetShelf.jsx +67 -0
- package/src/components/WidgetShelf/WidgetShelf.module.css +73 -0
- package/src/components/index.js +96 -0
- package/src/components/primitives/AuthorByline.jsx +85 -0
- package/src/components/primitives/AuthorByline.module.css +57 -0
- package/src/components/primitives/BrandCitation.jsx +71 -0
- package/src/components/primitives/Button.jsx +46 -0
- package/src/components/primitives/Button.module.css +88 -0
- package/src/components/primitives/Card.jsx +42 -0
- package/src/components/primitives/Card.module.css +42 -0
- package/src/components/primitives/Eyebrow.jsx +37 -0
- package/src/components/primitives/Eyebrow.module.css +19 -0
- package/src/components/primitives/HexBullet.jsx +37 -0
- package/src/components/primitives/HexBullet.module.css +16 -0
- package/src/components/primitives/HexThumbnail.jsx +70 -0
- package/src/components/primitives/HexThumbnail.module.css +45 -0
- package/src/components/primitives/Pill.jsx +42 -0
- package/src/components/primitives/Pill.module.css +30 -0
- package/src/components/primitives/Section.jsx +51 -0
- package/src/components/primitives/Section.module.css +31 -0
- package/src/components/primitives/SectionHead.jsx +36 -0
- package/src/components/primitives/SectionHead.module.css +43 -0
- package/src/components/primitives/index.js +22 -0
- package/src/css/brand.css +158 -0
- package/src/css/tokens.css +12 -0
- package/src/data/app-downloads.js +42 -0
- package/src/diagrams/README.md +74 -0
- package/src/diagrams/cn-domain-tree.js +105 -0
- package/src/diagrams/cn-hex-prism.js +163 -0
- package/src/diagrams/cn-hex.js +181 -0
- package/src/diagrams/cn-honeycomb-bg.js +135 -0
- package/src/diagrams/cn-pipeline.js +150 -0
- package/src/diagrams/cn-platform.js +156 -0
- package/src/diagrams/cn-side-box.js +104 -0
- package/src/diagrams/index.js +28 -0
- package/src/index.js +183 -0
- package/src/theme/Footer/index.jsx +516 -0
- package/src/theme/MDXPage/index.jsx +134 -0
- package/src/theme/Navbar/index.jsx +120 -0
- package/src/theme/Navbar/styles.module.css +114 -0
- package/src/theme/brand.jsx +63 -0
- package/src/theme.js +45 -0
- package/src/utils/lazyScript.js +37 -0
- package/static/img/favicon.svg +14 -0
- package/static/img/honeycomb-scatter.svg +23 -0
- package/static/img/honeycomb-watermark.svg +108 -0
- package/static/img/logo-dark.svg +11 -0
- package/static/img/logo.svg +14 -0
- package/static/img/nextcloud-logo.svg +5 -0
- package/static/lib/canal-footer.css +418 -0
- package/static/lib/canal-footer.js +499 -0
- package/static/lib/clients-flow.js +317 -0
- package/static/lib/conduction-bg.css +50 -0
- package/static/lib/conduction-bg.js +122 -0
- package/static/lib/hex-rain.css +128 -0
- package/static/lib/hex-rain.js +284 -0
- package/static/lib/kade-cyclist.css +264 -0
- package/static/lib/kade-cyclist.js +420 -0
- package/static/lib/logo-memory.css +219 -0
- package/static/lib/logo-memory.js +540 -0
- package/static/lib/platform-diagram.css +458 -0
- 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
|
+
})();
|