v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
@@ -0,0 +1,174 @@
|
||||
/*!
|
||||
* Tectum
|
||||
* Copyright © 2026 Machiel Heinen.
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
// ============================================================
|
||||
// core/granny-kit.js — onderdelen van de easter egg (core/shooter.js):
|
||||
// oma-sprite, gibs, kreten,
|
||||
// geluidseffecten, soundtrack en de omroeper à la Unreal Tournament.
|
||||
// Plain script (geen ES-module).
|
||||
// ============================================================
|
||||
|
||||
const GrannyKit = (() => {
|
||||
// Oud vrouwtje met rollator, 16x25 pixels (twee loopbeelden).
|
||||
const LADY = [
|
||||
'......hHHh......',
|
||||
'.....HHHHHH.....',
|
||||
'....HHHHHHHH....',
|
||||
'....HSSSSSSH....',
|
||||
'....SGEsSGES....',
|
||||
'....SSSssSSS....',
|
||||
'.....SsrrsS.....',
|
||||
'......SSSS......',
|
||||
'.....WWWWWW.....',
|
||||
'...DDDDWWDDDD...',
|
||||
'..DDDDDDDDDDDD..',
|
||||
'..DdDDDDDDDDdD..',
|
||||
'..DdDDDDDDDDdD..',
|
||||
'MSSMMMMMMMMMMSSM',
|
||||
'.M.DDDDDDDDDD.M.',
|
||||
'.MRRRRRRRRRRRRM.',
|
||||
'.MRrRrRrRrRrRRM.',
|
||||
'.MRRRRRRRRRRRRM.',
|
||||
'.M..dDDDDDDd..M.',
|
||||
'.M...DDDDDD...M.',
|
||||
'.M....LL.LL...M.',
|
||||
'.M....LL..LL..M.',
|
||||
'.M....BB..BB..M.',
|
||||
'KKK..........KKK',
|
||||
'KkK..........KkK'
|
||||
];
|
||||
const LADY_B = LADY.slice(0, 20).concat([
|
||||
'.M...LL..LL...M.',
|
||||
'.M...LL...LL..M.',
|
||||
'.M...BB...BB..M.',
|
||||
'KkK..........KkK',
|
||||
'KKK..........KKK'
|
||||
]);
|
||||
const PAL = { h: '#9a9aa8', H: '#d0d0da', S: '#f0c8a0', s: '#d09878', E: '#101010', G: '#707080', r: '#a04040',
|
||||
W: '#f4f4f4', D: '#8040a0', d: '#5a2a78', M: '#b8c0c8', R: '#c03030', L: '#c8a888', B: '#402018', K: '#202020', k: '#606060' };
|
||||
|
||||
// Gibs: 8-bit stukjes oma (vlees, botjes, gebit, bril, handtas, breibol, rollatorwiel).
|
||||
const GIBS = [
|
||||
['..rr....', '.rRRr...', 'rRRRRr..', 'rRrRRRr.', '.rRRRRr.', '..rRRr..', '...rr...'],
|
||||
['.rr.rr..', 'rRRrRRr.', 'rRRRRRr.', '.rRRRr..', '..rrr...'],
|
||||
['ww....ww', 'wWw..wWw', '.wWwwWw.', '..wWWw..', '.wWwwWw.', 'wWw..wWw', 'ww....ww'],
|
||||
['.pppppp.', 'pWWWWWWp', 'pWkWkWkp', 'pWWWWWWp', '.pppppp.'],
|
||||
['GG..GG..', 'GbG.GbG.', 'GGGGGGG.', '........'],
|
||||
['..nn....', '.n..n...', 'nNNNNNn.', 'NNNNNNN.', 'NNyNNNN.', 'NNNNNNN.'],
|
||||
['.YYYY...', 'YyYYyY..', 'YYyYYy..', 'YyYYyY..', '.YYYY...', '.....Y..', '......Y.'],
|
||||
['.KKKK...', 'KkkkkK..', 'KkMMkK..', 'KkMMkK..', 'KkkkkK..', '.KKKK...']
|
||||
];
|
||||
const GIB_PAL = { r: '#6a0808', R: '#c01818', w: '#a09880', W: '#f0ecdc', p: '#e07888', k: '#303030', G: '#606070', b: '#a0c8f0',
|
||||
n: '#402018', N: '#7a4a22', y: '#d0b040', Y: '#c050a0', K: '#202020', M: '#b8c0c8' };
|
||||
|
||||
const SHOUTS = ['"Jongeman! Voeten vegen!"', '"In mijn tijd hadden we geen raketten!"', '"Ik bel je moeder!"',
|
||||
'"Waar is de bingo?"', '"Dat dak lekt, hoor!"', '"Is dit wel vergund?"', '"Ik heb een abonnement op de bus!"'];
|
||||
const DEATH_SHOUTS = ['"Oeeehh, mijn heup!"', '"Dat zeg ik tegen de wijkagent!"', '"Aaargh, de bingo..."'];
|
||||
const HIT_SHOUTS = ['*BAM* met de handtas!', '*klets* met de rollator!', '"Neem een koekje, en dan wegwezen!"'];
|
||||
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
// Pixelraster -> canvas (1 px per teken, optioneel met rand en één tintkleur;
|
||||
// bottom = figuur op de onderrand, voor sprites die op de grond staan).
|
||||
function pixelCanvas(rows, pal, opts) {
|
||||
const o = Object.assign({ pad: 0, size: 0, tint: null, bottom: false }, opts || {});
|
||||
const w = Math.max(...rows.map(r => r.length)), h = rows.length;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = o.size || w + o.pad * 2;
|
||||
c.height = o.size || h + o.pad * 2;
|
||||
const g = c.getContext('2d');
|
||||
const ox = Math.floor((c.width - w) / 2), oy = o.bottom ? c.height - h : Math.floor((c.height - h) / 2);
|
||||
rows.forEach((row, y) => [...row].forEach((ch, x) => {
|
||||
if (!pal[ch]) return;
|
||||
g.fillStyle = o.tint || pal[ch];
|
||||
g.fillRect(ox + x, oy + y, 1, 1);
|
||||
}));
|
||||
return c;
|
||||
}
|
||||
|
||||
// Geluidseffecten: gesynthetiseerde ruis via WebAudio.
|
||||
function createSfx() {
|
||||
let audio = null;
|
||||
return {
|
||||
bang(len, freq, vol) {
|
||||
try {
|
||||
audio = audio || new (window.AudioContext || window.webkitAudioContext)();
|
||||
const n = Math.floor(audio.sampleRate * len), buf = audio.createBuffer(1, n, audio.sampleRate), d = buf.getChannelData(0);
|
||||
for (let i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / n, 3);
|
||||
const src = audio.createBufferSource(), f = audio.createBiquadFilter(), g = audio.createGain();
|
||||
src.buffer = buf; f.type = 'lowpass'; f.frequency.value = freq; g.gain.value = vol;
|
||||
src.connect(f).connect(g).connect(audio.destination);
|
||||
src.start();
|
||||
} catch (e) { /* geen geluid, jammer */ }
|
||||
},
|
||||
close() { if (audio) audio.close(); audio = null; }
|
||||
};
|
||||
}
|
||||
|
||||
// Soundtrack: "Five Armies" Kevin MacLeod (incompetech.com), CC BY 4.0.
|
||||
const MUSIC_CREDIT = '♪ "Five Armies" Kevin MacLeod (incompetech.com) · CC BY 4.0';
|
||||
function createMusic() {
|
||||
const music = new Audio('assets/audio/five-armies.mp3');
|
||||
music.loop = true;
|
||||
music.volume = 0.5;
|
||||
return {
|
||||
play() { music.currentTime = 0; music.play().catch(() => {}); }, // toetsdruk = gebruikersactie
|
||||
stop() { music.pause(); }
|
||||
};
|
||||
}
|
||||
|
||||
// Omroeper (Web Speech API + grote tekst in labelEl).
|
||||
// Multi-kill: kills binnen MULTI_WINDOW s na elkaar. Spree: kills zonder geraakt te worden.
|
||||
const MULTI_WINDOW = 6;
|
||||
const MULTI = { 2: 'Double Granny!', 3: 'Multi Granny!', 4: 'Mega Granny!', 5: 'Ultra Granny!', 6: 'M M M MONSTER GRANNY!' };
|
||||
const SPREE = { 5: 'Bingo Spree!', 10: 'Rollator Rampage!', 15: 'Dominating the retirement home!', 20: 'Unstoppable!', 25: 'Godlike!', 30: 'Wicked Sick... of grannies!' };
|
||||
function createAnnouncer(labelEl) {
|
||||
let kills = 0, multi = 0, lastKillAt = -99, spree = 0, clock = 0, timer = 0;
|
||||
function say(text) {
|
||||
labelEl.textContent = text.replace(/^M M M /, 'M-M-M-');
|
||||
labelEl.classList.remove('show'); void labelEl.offsetWidth; labelEl.classList.add('show');
|
||||
timer = 2.2;
|
||||
try {
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
const v = speechSynthesis.getVoices().find(x => /^en(-|_)(US|GB)/i.test(x.lang) && /male|david|daniel|george/i.test(x.name))
|
||||
|| speechSynthesis.getVoices().find(x => /^en/i.test(x.lang));
|
||||
if (v) u.voice = v;
|
||||
u.lang = 'en-US'; u.pitch = 0.1; u.rate = 0.8; u.volume = 1;
|
||||
speechSynthesis.cancel();
|
||||
speechSynthesis.speak(u);
|
||||
} catch (e) { /* geen stem, dan alleen tekst */ }
|
||||
}
|
||||
return {
|
||||
say,
|
||||
get kills() { return kills; },
|
||||
kill() {
|
||||
kills++;
|
||||
multi = clock - lastKillAt <= MULTI_WINDOW ? multi + 1 : 1;
|
||||
lastKillAt = clock;
|
||||
spree++;
|
||||
if (kills === 1) say('First Blood!');
|
||||
else if (multi >= 2) say(MULTI[Math.min(multi, 6)]);
|
||||
else if (SPREE[spree]) say(SPREE[spree]);
|
||||
return kills;
|
||||
},
|
||||
hurt() {
|
||||
if (spree >= 5) say('Spree ended by a handbag!');
|
||||
spree = 0;
|
||||
},
|
||||
reset() { kills = multi = spree = 0; lastKillAt = -99; },
|
||||
update(dt) {
|
||||
clock += dt;
|
||||
if (timer > 0 && (timer -= dt) <= 0) labelEl.classList.remove('show');
|
||||
},
|
||||
stop() {
|
||||
try { speechSynthesis.cancel(); } catch (e) { /* */ }
|
||||
labelEl.classList.remove('show');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { LADY, LADY_B, PAL, GIBS, GIB_PAL, SHOUTS, DEATH_SHOUTS, HIT_SHOUTS, MUSIC_CREDIT,
|
||||
pick, pixelCanvas, createSfx, createMusic, createAnnouncer };
|
||||
})();
|
||||
Reference in New Issue
Block a user