commit 1563bb67890aac942a95b10401e607cb18415a1e
parent 751a75cb7d3496d1586e0da1c01ecf877891bf22
Author: Kyle Barlow <kb@kylebarlow.com>
Date: Mon, 17 Aug 2026 21:15:51 -0700
Lengthen music
Diffstat:
4 files changed, 175 insertions(+), 80 deletions(-)
diff --git a/games/animal-letter/game.js b/games/animal-letter/game.js
@@ -2,29 +2,43 @@ const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6'];
const GOAL = 10;
const CDN = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/';
-// F major — circus-like leaps and animal energy (32-note loop, ~7.5s cycle at 128 BPM)
-const MELODY = [
- // bar 1: fanfare — big octave leaps
- 'F4','C5','F5','C5', 'F4','A4','C5','A4',
- // bar 2: bounding up to Bb
- 'Bb4','D5','F5','Bb5', 'F5','D5','Bb4','F4',
- // bar 3: pivot to C — chase scene
- 'C5','E5','G5','C6', 'G5','E5','C5','E5',
- // bar 4: swinging close
- 'F5','D5','Bb4','F4', 'C5','F5','A5','F5',
-];
-const HARMONY = [
- // diatonic 3rds above in F major
- 'A4','E5','A5','E5', 'A4','C5','E5','C5',
- 'D5','F5','A5','D6', 'A5','F5','D5','A4',
- 'E5','G5','Bb5','E6', 'Bb5','G5','E5','G5',
- 'A5','F5','D5','A4', 'E5','A5','C6','A5',
-];
-const BASS = [
- // 2-bar ostinato (loops independently of 4-bar melody)
- 'F2', null, null, null, null, null, null, null,
- 'Bb2',null, null, null, 'C3', null, null, null,
-];
+// F major — circus-like leaps and animal energy, extended to a ~1-minute song:
+// 32-step sections (eighth notes at 128 BPM) concatenated so the loop is long
+// enough not to grate. 'A' is the original fanfare; B–D are new sections in
+// the same character. '.' = rest. Harmony is a diatonic third above the
+// melody (it plays during the success jingle).
+const N = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t));
+
+const MEL = {
+ A: N(`F4 C5 F5 C5 F4 A4 C5 A4 Bb4 D5 F5 Bb5 F5 D5 Bb4 F4
+ C5 E5 G5 C6 G5 E5 C5 E5 F5 D5 Bb4 F4 C5 F5 A5 F5`),
+ B: N(`F5 E5 D5 C5 D5 E5 F5 A5 G5 F5 E5 D5 C5 D5 E5 C5
+ A5 G5 F5 G5 A5 C6 A5 F5 G5 E5 C5 E5 F5 A5 G5 F5`),
+ C: N(`D5 F5 A5 D6 A5 F5 D5 F5 C5 E5 G5 C6 G5 E5 C5 E5
+ Bb4 D5 F5 Bb5 F5 D5 Bb4 D5 C5 F5 A5 C6 A5 G5 F5 F5`),
+ D: N(`A4 Bb4 C5 D5 C5 Bb4 A4 F4 G4 A4 Bb4 C5 Bb4 A4 G4 E4
+ F4 A4 C5 F5 E5 C5 A4 C5 D5 C5 Bb4 A4 G4 C5 E5 F5`),
+};
+const HAR = {
+ A: N(`A4 E5 A5 E5 A4 C5 E5 C5 D5 F5 A5 D6 A5 F5 D5 A4
+ E5 G5 Bb5 E6 Bb5 G5 E5 G5 A5 F5 D5 A4 E5 A5 C6 A5`),
+ B: N(`A5 G5 F5 E5 F5 G5 A5 C6 Bb5 A5 G5 F5 E5 F5 G5 E5
+ C6 Bb5 A5 Bb5 C6 E6 C6 A5 Bb5 G5 E5 G5 A5 C6 Bb5 A5`),
+ C: N(`F5 A5 C6 F6 C6 A5 F5 A5 E5 G5 Bb5 E6 Bb5 G5 E5 G5
+ D5 F5 A5 D6 A5 F5 D5 F5 E5 A5 C6 E6 C6 Bb5 A5 A5`),
+ D: N(`C5 D5 E5 F5 E5 D5 C5 A4 Bb4 C5 D5 E5 D5 C5 Bb4 G4
+ A4 C5 E5 A5 G5 E5 C5 E5 F5 E5 D5 C5 Bb4 E5 G5 A5`),
+};
+const BAS = {
+ A: N(`F2 . . . . . . . Bb2 . . . C3 . . . F2 . . . . . . . Bb2 . . . C3 . . .`),
+ B: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . C3 . . . F2 . . .`),
+ C: N(`D3 . . . . . . . C3 . . . . . . . Bb2 . . . . . . . C3 . . . F2 . . .`),
+ D: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . Bb2 . . . C3 . . .`),
+};
+const ORDER = ['A','B','A','C','D','B','C','A'];
+const MELODY = ORDER.flatMap(k => MEL[k]);
+const HARMONY = ORDER.flatMap(k => HAR[k]);
+const BASS = ORDER.flatMap(k => BAS[k]);
class AnimalLetterScene extends Phaser.Scene {
constructor() {
diff --git a/games/animal-spell/game.js b/games/animal-spell/game.js
@@ -2,22 +2,42 @@ const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6'];
const GOAL = 10;
const CDN = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/';
-const MELODY = [
- 'F4','C5','F5','C5', 'F4','A4','C5','A4',
- 'Bb4','D5','F5','Bb5', 'F5','D5','Bb4','F4',
- 'C5','E5','G5','C6', 'G5','E5','C5','E5',
- 'F5','D5','Bb4','F4', 'C5','F5','A5','F5',
-];
-const HARMONY = [
- 'A4','E5','A5','E5', 'A4','C5','E5','C5',
- 'D5','F5','A5','D6', 'A5','F5','D5','A4',
- 'E5','G5','Bb5','E6', 'Bb5','G5','E5','G5',
- 'A5','F5','D5','A4', 'E5','A5','C6','A5',
-];
-const BASS = [
- 'F2', null, null, null, null, null, null, null,
- 'Bb2',null, null, null, 'C3', null, null, null,
-];
+// ~1-minute song: 32-step sections (eighth notes at 128 BPM) concatenated so
+// the loop is long enough not to grate. 'A' is the original circus fanfare;
+// B–D are new sections in the same F-major character. '.' = rest. Harmony is
+// a diatonic third above the melody (it plays during the success jingle).
+const N = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t));
+
+const MEL = {
+ A: N(`F4 C5 F5 C5 F4 A4 C5 A4 Bb4 D5 F5 Bb5 F5 D5 Bb4 F4
+ C5 E5 G5 C6 G5 E5 C5 E5 F5 D5 Bb4 F4 C5 F5 A5 F5`),
+ B: N(`F5 E5 D5 C5 D5 E5 F5 A5 G5 F5 E5 D5 C5 D5 E5 C5
+ A5 G5 F5 G5 A5 C6 A5 F5 G5 E5 C5 E5 F5 A5 G5 F5`),
+ C: N(`D5 F5 A5 D6 A5 F5 D5 F5 C5 E5 G5 C6 G5 E5 C5 E5
+ Bb4 D5 F5 Bb5 F5 D5 Bb4 D5 C5 F5 A5 C6 A5 G5 F5 F5`),
+ D: N(`A4 Bb4 C5 D5 C5 Bb4 A4 F4 G4 A4 Bb4 C5 Bb4 A4 G4 E4
+ F4 A4 C5 F5 E5 C5 A4 C5 D5 C5 Bb4 A4 G4 C5 E5 F5`),
+};
+const HAR = {
+ A: N(`A4 E5 A5 E5 A4 C5 E5 C5 D5 F5 A5 D6 A5 F5 D5 A4
+ E5 G5 Bb5 E6 Bb5 G5 E5 G5 A5 F5 D5 A4 E5 A5 C6 A5`),
+ B: N(`A5 G5 F5 E5 F5 G5 A5 C6 Bb5 A5 G5 F5 E5 F5 G5 E5
+ C6 Bb5 A5 Bb5 C6 E6 C6 A5 Bb5 G5 E5 G5 A5 C6 Bb5 A5`),
+ C: N(`F5 A5 C6 F6 C6 A5 F5 A5 E5 G5 Bb5 E6 Bb5 G5 E5 G5
+ D5 F5 A5 D6 A5 F5 D5 F5 E5 A5 C6 E6 C6 Bb5 A5 A5`),
+ D: N(`C5 D5 E5 F5 E5 D5 C5 A4 Bb4 C5 D5 E5 D5 C5 Bb4 G4
+ A4 C5 E5 A5 G5 E5 C5 E5 F5 E5 D5 C5 Bb4 E5 G5 A5`),
+};
+const BAS = {
+ A: N(`F2 . . . . . . . Bb2 . . . C3 . . . F2 . . . . . . . Bb2 . . . C3 . . .`),
+ B: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . C3 . . . F2 . . .`),
+ C: N(`D3 . . . . . . . C3 . . . . . . . Bb2 . . . . . . . C3 . . . F2 . . .`),
+ D: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . Bb2 . . . C3 . . .`),
+};
+const ORDER = ['A','B','A','C','D','B','C','A'];
+const MELODY = ORDER.flatMap(k => MEL[k]);
+const HARMONY = ORDER.flatMap(k => HAR[k]);
+const BASS = ORDER.flatMap(k => BAS[k]);
class AnimalSpellScene extends Phaser.Scene {
constructor() {
diff --git a/games/fire-truck/game.js b/games/fire-truck/game.js
@@ -42,39 +42,78 @@ const PALM_TRUNK = 0x9a6b43;
const PALM_FROND = 0x46b063;
const BIKE_LANE_COLOR = 0x3e8e5a; // painted protected bike lanes
-const FT_BPM = 158;
-const FT_MELODY = [
- 'E5','G5','A5','B5','G5','E5','D5','E5',
- 'A4','C5','D5','E5','C5','A4','G4','A4',
-];
-const FT_BASS = [
- 'E2','B2','E2','B2','A2','C3','E3','A2',
- 'G2','B2','G2','D3','A2','E2','A2','E2',
-];
-const FT_CHORDS = [
- ['E3','G3','B3'], null, null, null,
- ['A2','C3','E3'], null, null, null,
- ['G2','B2','D3'], null, null, null,
- ['A2','C3','E3'], null, null, null,
-];
+// Both songs are ~1-minute arrangements: 16-step phrases (eighth notes)
+// concatenated so the loop is long enough not to grate. 'A' is the original
+// theme; the other phrases are new sections in the same E-minor character.
+// '.' = rest in the string form; chord rows place a triad every 4 steps
+// (main theme) or every 2 steps (end fanfare).
+const ftN = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t));
+const FTC = {
+ Em: ['E3','G3','B3'], Am: ['A2','C3','E3'], G: ['G2','B2','D3'],
+ C: ['C3','E3','G3'], D: ['D3','F#3','A3'], Bm: ['B2','D3','F#3'],
+};
+const ftChd4 = (a, b, c, d) =>
+ [a, null, null, null, b, null, null, null, c, null, null, null, d, null, null, null];
+const ftChd8 = (...cs) => cs.flatMap(c => [c, null]);
+
+const FT_BPM = 158;
+const FT_MEL = {
+ A: ftN('E5 G5 A5 B5 G5 E5 D5 E5 A4 C5 D5 E5 C5 A4 G4 A4'),
+ B: ftN('B4 D5 E5 G5 E5 D5 B4 D5 E5 G5 B5 A5 G5 E5 D5 B4'),
+ C: ftN('E4 G4 A4 B4 A4 G4 E4 G4 A4 B4 C5 B4 A4 G4 F#4 E4'),
+ D: ftN('B5 A5 G5 A5 B5 E6 D6 B5 C6 B5 A5 G5 A5 B5 A5 G5'),
+ E: ftN('G5 B5 D6 B5 G5 D5 B4 D5 C5 E5 G5 E5 D5 F#5 A5 F#5'),
+ F: ftN('E5 D5 C5 B4 A4 B4 C5 A4 B4 C5 D5 E5 F#5 D5 B4 E5'),
+};
+const FT_BAS = {
+ A: ftN('E2 B2 E2 B2 A2 C3 E3 A2 G2 B2 G2 D3 A2 E2 A2 E2'),
+ B: ftN('E2 B2 E2 B2 G2 B2 G2 B2 C3 G2 C3 G2 D3 A2 D3 B2'),
+ C: ftN('E2 E3 E2 E3 E2 E3 E2 B2 A2 A3 A2 A3 B2 F#2 B2 E2'),
+ D: ftN('E2 B2 E3 B2 E2 B2 E3 B2 A2 E3 A2 E3 D3 A2 D3 A2'),
+ E: ftN('G2 D3 G2 D3 G2 B2 G2 B2 C3 G2 C3 G2 D3 A2 D3 C3'),
+ F: ftN('A2 E3 A2 E3 A2 C3 A2 C3 B2 F#2 B2 F#2 B2 D3 B2 E2'),
+};
+const FT_CHD = {
+ A: ftChd4(FTC.Em, FTC.Am, FTC.G, FTC.Am),
+ B: ftChd4(FTC.Em, FTC.G, FTC.C, FTC.D),
+ C: ftChd4(FTC.Em, FTC.Em, FTC.Am, FTC.Bm),
+ D: ftChd4(FTC.Em, FTC.Em, FTC.Am, FTC.D),
+ E: ftChd4(FTC.G, FTC.G, FTC.C, FTC.D),
+ F: ftChd4(FTC.Am, FTC.Am, FTC.Bm, FTC.Bm),
+};
+const FT_ORDER = ['A','B','A','C','D','B','E','F','A','C',
+ 'D','E','B','F','C','D','A','B','F','A'];
+const FT_MELODY = FT_ORDER.flatMap(k => FT_MEL[k]);
+const FT_BASS = FT_ORDER.flatMap(k => FT_BAS[k]);
+const FT_CHORDS = FT_ORDER.flatMap(k => FT_CHD[k]);
// Celebration song for the endgame screen — related to the main theme
// (same Em/Am/G harmonic language, brighter octave, triumphant ascending phrases)
const FT_END_BPM = 170;
-const FT_END_MELODY = [
- 'E5','G5','B5','E6', 'D6','B5','G5','A5',
- 'A5','C6','E6','A6', 'G6','E6','D6','E6',
-];
-const FT_END_BASS = [
- 'E3','G3','B3','E4', 'D4','B3','G3','A3',
- 'A3','C4','E4','A4', 'G4','E4','B3','E4',
-];
-const FT_END_CHORDS = [
- ['E3','G3','B3'], null, ['B2','D3','G3'], null,
- ['G2','B2','D3'], null, ['A2','E3','A3'], null,
- ['A2','C3','E3'], null, ['E3','A3','C4'], null,
- ['G2','B2','D3'], null, ['E3','G3','B3'], null,
-];
+const FT_END_MEL = {
+ A: ftN('E5 G5 B5 E6 D6 B5 G5 A5 A5 C6 E6 A6 G6 E6 D6 E6'),
+ B: ftN('B5 G5 E5 G5 B5 D6 E6 D6 C6 A5 E5 A5 C6 E6 D6 B5'),
+ C: ftN('G5 B5 D6 G6 E6 C6 G5 C6 D6 B5 G5 B5 E6 D6 B5 A5'),
+ D: ftN('E6 D6 B5 G5 A5 B5 C6 A5 B5 G5 E5 G5 B5 D6 E6 E6'),
+};
+const FT_END_BAS = {
+ A: ftN('E3 G3 B3 E4 D4 B3 G3 A3 A3 C4 E4 A4 G4 E4 B3 E4'),
+ B: ftN('E3 B3 G3 B3 E4 B3 G3 B3 A3 E4 C4 E4 D4 A3 D4 B3'),
+ C: ftN('G3 B3 D4 G4 C4 E4 G4 C4 D4 B3 G3 B3 E4 B3 G3 A3'),
+ D: ftN('E4 B3 G3 B3 A3 E3 A3 C4 B3 E3 G3 E3 B3 G3 E4 E3'),
+};
+const FT_END_CHD = {
+ A: ftChd8(FTC.Em, ['B2','D3','G3'], FTC.G, ['A2','E3','A3'],
+ FTC.Am, ['E3','A3','C4'], FTC.G, FTC.Em),
+ B: ftChd8(FTC.Em, FTC.Em, FTC.G, FTC.Em, FTC.Am, FTC.Am, FTC.Am, FTC.G),
+ C: ftChd8(FTC.G, FTC.G, FTC.C, FTC.C, FTC.G, FTC.Em, FTC.Em, FTC.Am),
+ D: ftChd8(FTC.Em, FTC.Em, FTC.Am, FTC.Am, FTC.Em, FTC.Em, FTC.G, FTC.Em),
+};
+const FT_END_ORDER = ['A','B','A','C','A','B','D','C','A','B',
+ 'C','D','A','C','B','D','A','B','D','A'];
+const FT_END_MELODY = FT_END_ORDER.flatMap(k => FT_END_MEL[k]);
+const FT_END_BASS = FT_END_ORDER.flatMap(k => FT_END_BAS[k]);
+const FT_END_CHORDS = FT_END_ORDER.flatMap(k => FT_END_CHD[k]);
function arrowLabel(dir) {
const ARROWS = { left: '←', right: '→', straight: '↑' };
diff --git a/games/letter-find/game.js b/games/letter-find/game.js
@@ -2,18 +2,40 @@ const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6'];
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const GOAL = 10;
-const MELODY = [
- 'C5','E5','G5','A5','G5','E5','C5','E5',
- 'F5','A5','C6','A5','G5','B4','C5','G4',
-];
-const HARMONY = [
- 'E5','G5','B5','C6','B5','G5','E5','G5',
- 'A5','C6','E6','C6','B5','D5','E5','B4',
-];
-const BASS = [
- 'C3',null,null,null,null,null,null,null,
- 'F2',null,null,null,'G2',null,null,null,
-];
+// ~1-minute song: 16-step phrases (eighth notes at 128 BPM) concatenated so the
+// loop is long enough not to grate. 'A' is the original theme; B–F are new
+// sections in the same C-major character. '.' = rest. Harmony is a diatonic
+// third above the melody (it plays during the success jingle).
+const N = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t));
+
+const MEL = {
+ A: N('C5 E5 G5 A5 G5 E5 C5 E5 F5 A5 C6 A5 G5 B4 C5 G4'),
+ B: N('E5 D5 C5 D5 E5 G5 E5 C5 D5 F5 A5 F5 E5 D5 C5 G4'),
+ C: N('G5 A5 B5 C6 B5 A5 G5 E5 A5 G5 F5 E5 D5 E5 F5 D5'),
+ D: N('C5 D5 E5 F5 E5 D5 C5 A4 B4 C5 D5 E5 D5 C5 B4 G4'),
+ E: N('C6 B5 A5 G5 A5 B5 C6 E6 D6 C6 B5 A5 G5 F5 E5 D5'),
+ F: N('E5 G5 C6 G5 E5 C5 D5 E5 F5 E5 D5 C5 D5 B4 C5 C5'),
+};
+const HAR = {
+ A: N('E5 G5 B5 C6 B5 G5 E5 G5 A5 C6 E6 C6 B5 D5 E5 B4'),
+ B: N('G5 F5 E5 F5 G5 B5 G5 E5 F5 A5 C6 A5 G5 F5 E5 B4'),
+ C: N('B5 C6 D6 E6 D6 C6 B5 G5 C6 B5 A5 G5 F5 G5 A5 F5'),
+ D: N('E5 F5 G5 A5 G5 F5 E5 C5 D5 E5 F5 G5 F5 E5 D5 B4'),
+ E: N('E6 D6 C6 B5 C6 D6 E6 G6 F6 E6 D6 C6 B5 A5 G5 F5'),
+ F: N('G5 B5 E6 B5 G5 E5 F5 G5 A5 G5 F5 E5 F5 D5 E5 E5'),
+};
+const BAS = {
+ A: N('C3 . . . . . . . F2 . . . G2 . . .'),
+ B: N('C3 . . . . . . . D3 . . . G2 . . .'),
+ C: N('G2 . . . . . . . F2 . . . G2 . . .'),
+ D: N('C3 . . . A2 . . . G2 . . . G2 . . .'),
+ E: N('C3 . . . F2 . . . G2 . . . G2 . . .'),
+ F: N('C3 . . . F2 . . . G2 . C3 . C3 . . .'),
+};
+const ORDER = ['A','B','A','C','D','B','E','F','A','C','D','B','E','C','F','A'];
+const MELODY = ORDER.flatMap(k => MEL[k]);
+const HARMONY = ORDER.flatMap(k => HAR[k]);
+const BASS = ORDER.flatMap(k => BAS[k]);
class LetterFindScene extends Phaser.Scene {
constructor() {