Skip to content

Commit c1e3ad2

Browse files
feat: add rhythm game
Co-authored-by: Rob Cohen <rmacohen@users.noreply.github.com> Co-authored-by: SG-SWE073 <SG-SWE073@users.noreply.github.com> Co-authored-by: Gina Castromonte <GinaCastromonte@users.noreply.github.com> https://replit.com/@remarkablemark/Rhythm-Jam-2024
1 parent dde913b commit c1e3ad2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+820
-108
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
🎶 Rhythmism is a web rhythm game.
44

5-
The game was made for the [Rhythm Jam 2024](https://itch.io/jam/rhythm-jam-2024), which the theme was `Divergence`.
5+
The game was made for the [Rhythm Jam 2024](https://itch.io/jam/rhythm-jam-2024), which the theme was `Divergence`:
6+
7+
> a difference between two or more things, attitudes, or opinions.
68
79
Play the game on:
810

@@ -25,6 +27,7 @@ Play the game on:
2527

2628
- [Excalidraw](https://excalidraw.com/#json=4xsd2WQeFiejrdBQkalCR,GLv9eoVsLsWhkXYKI1gCLA)
2729
- [Prototype 1](https://replit.com/@remarkablemark/Rhythm-Game)
30+
- [Prototype 2](https://replit.com/@remarkablemark/Rhythm-Jam-2024)
2831

2932
## Stack
3033

index.html

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import kaboom from 'kaboom';
4747

4848
kaboom({
49+
background: [255, 255, 255],
4950
width: 360,
5051
height: 480,
5152
});

package-lock.json

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "rhythmism",
33
"version": "1.0.0-alpha",
4-
"description": "A template for building Kaboom games.",
4+
"description": "Rhythmism is a web rhythm game.",
55
"author": "Mark <mark@remarkablemark.org>",
66
"scripts": {
77
"build": "npm run clean && vite build",
@@ -23,6 +23,7 @@
2323
"@commitlint/config-conventional": "18.6.2",
2424
"@typescript-eslint/eslint-plugin": "7.0.1",
2525
"@typescript-eslint/parser": "7.0.1",
26+
"easytimer.js": "4.6.0",
2627
"esbuild": "0.20.0",
2728
"eslint": "8.56.0",
2829
"eslint-plugin-prettier": "5.1.3",

public/sounds/longPress.mp3

282 KB
Binary file not shown.
File renamed without changes.
File renamed without changes.

public/sounds/tranquil-wave.mp3

1.63 MB
Binary file not shown.

public/sounds/tranquil-wave.webm

983 KB
Binary file not shown.
279 KB
Loading
8.2 KB
Loading
6.46 KB
Loading
7.03 KB
Loading
7.1 KB
Loading
6.94 KB
Loading
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

public/sprites/keys/GreyDownArrow.png

7.03 KB
Loading

public/sprites/keys/GreyLeftArrow.png

7.1 KB
Loading
7.06 KB
Loading

public/sprites/keys/GreyUpArrow.png

7.06 KB
Loading

public/sprites/keys/down.png

1.74 KB
Loading

public/sprites/keys/left.png

1.7 KB
Loading

public/sprites/keys/right.png

1.74 KB
Loading

public/sprites/keys/up.png

1.71 KB
Loading

src/constants.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export enum Direction {
2+
left = 'directions/PurpleLeftArrow',
3+
down = 'directions/BlueDownArrow',
4+
up = 'directions/GreenUpArrow',
5+
right = 'directions/RedRightArrow',
6+
}
7+
8+
export type DirectionKey = 'left' | 'down' | 'up' | 'right';
9+
10+
export const directions = Object.keys(Direction);
11+
12+
export enum Key {
13+
left = 'keys/GreyLeftArrow',
14+
down = 'keys/GreyDownArrow',
15+
up = 'keys/GreyUpArrow',
16+
right = 'keys/GreyRightArrow',
17+
}
18+
19+
export enum Scene {
20+
game = 'game',
21+
load = 'load',
22+
pause = 'pause',
23+
preload = 'preload',
24+
start = 'start',
25+
}
26+
27+
export enum Sprite {
28+
background = 'background',
29+
}
30+
31+
export enum Sound {
32+
score = 'score',
33+
music = 'music',
34+
longPress = 'longPress',
35+
}
36+
37+
export enum Tag {
38+
left = 'left',
39+
down = 'down',
40+
up = 'up',
41+
right = 'right',
42+
direction = 'direction',
43+
}
44+
45+
export enum accuracyRadii {
46+
perfect = 10,
47+
excellent = 20,
48+
good = 30,
49+
}

src/gameobjects/buttons.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Vec2 } from 'kaboom';
2+
3+
export function addButton(txt: string, position: Vec2, callback: () => void) {
4+
// add a parent background object
5+
const button = add([
6+
rect(300, 80, { radius: 8 }),
7+
pos(position),
8+
area(),
9+
scale(1),
10+
anchor('center'),
11+
outline(4),
12+
color(),
13+
]);
14+
15+
// add a child object that displays the text
16+
button.add([text(txt), anchor('center'), color(0, 0, 0)]);
17+
18+
// onHoverUpdate() comes from area() component
19+
// it runs every frame when the object is being hovered
20+
button.onHoverUpdate(() => {
21+
const t = time() * 10;
22+
button.color = hsl2rgb((t / 10) % 1, 0.6, 0.7);
23+
button.scale = vec2(1.1);
24+
setCursor('pointer');
25+
});
26+
27+
// onHoverEnd() comes from area() component
28+
// it runs once when the object stopped being hovered
29+
button.onHoverEnd(() => {
30+
button.scale = vec2(1);
31+
button.color = rgb();
32+
});
33+
34+
// onClick() comes from area() component
35+
// it runs once when the object is clicked
36+
button.onClick(callback);
37+
38+
return button;
39+
}

src/gameobjects/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './buttons';
2+
export * from './keys';
3+
export * from './pause';
4+
export * from './score';
5+
export * from './timer';

src/gameobjects/keys.ts

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { AudioPlay } from 'kaboom';
2+
3+
import { DirectionKey, directions, Key, Sound, Tag } from '../constants';
4+
import { getPosition } from '../helpers';
5+
import { addScore } from './score';
6+
7+
const LONG_PRESS_MILLISECONDS = 500;
8+
9+
export function addKeys() {
10+
const keyMap = {
11+
left: add([
12+
sprite(Key.left),
13+
getPosition('left'),
14+
anchor('center'),
15+
area(),
16+
opacity(0.5),
17+
Tag.left,
18+
]),
19+
20+
down: add([
21+
sprite(Key.down),
22+
getPosition('down'),
23+
anchor('center'),
24+
area(),
25+
opacity(0.5),
26+
Tag.down,
27+
]),
28+
29+
up: add([
30+
sprite(Key.up),
31+
getPosition('up'),
32+
anchor('center'),
33+
area(),
34+
opacity(0.5),
35+
Tag.up,
36+
]),
37+
38+
right: add([
39+
sprite(Key.right),
40+
getPosition('right'),
41+
anchor('center'),
42+
area(),
43+
opacity(0.5),
44+
Tag.right,
45+
]),
46+
};
47+
48+
const incrementScore = addScore();
49+
let music: AudioPlay | undefined;
50+
51+
(directions as DirectionKey[]).forEach((key) => {
52+
onCollideUpdate(key, Tag.direction, () => {
53+
if (isKeyPressed(key)) {
54+
addKaboom(center());
55+
incrementScore();
56+
}
57+
});
58+
59+
const releaseMap: Record<string, number> = {};
60+
61+
onKeyPress(key, () => {
62+
keyMap[key].opacity = 1;
63+
64+
releaseMap[key] = Date.now();
65+
play(Sound.score, {
66+
volume: 0.5,
67+
});
68+
});
69+
70+
onKeyDown(key, () => {
71+
if (!music && Date.now() - releaseMap[key] > LONG_PRESS_MILLISECONDS) {
72+
music = play(Sound.longPress, {
73+
volume: 0.5,
74+
});
75+
}
76+
});
77+
78+
onKeyRelease(key, () => {
79+
keyMap[key].opacity = 0.5;
80+
81+
if (music) {
82+
music.paused = true;
83+
music = undefined;
84+
}
85+
});
86+
});
87+
}

src/gameobjects/pause.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { AudioPlay, GameObj, TimerComp, TweenController } from 'kaboom';
2+
3+
export function addPause(game: GameObj<TimerComp>, music: AudioPlay) {
4+
let curTween: TweenController;
5+
6+
onKeyPress((key) => {
7+
if (!['escape', 'p'].includes(key)) {
8+
return;
9+
}
10+
11+
music.paused = !music.paused;
12+
game.paused = !game.paused;
13+
14+
if (curTween) {
15+
curTween.cancel();
16+
}
17+
18+
curTween = tween(
19+
pauseMenu.pos,
20+
game.paused ? center() : center().add(0, 700),
21+
1,
22+
(p) => (pauseMenu.pos = p),
23+
easings.easeOutElastic,
24+
);
25+
26+
if (game.paused) {
27+
pauseMenu.hidden = false;
28+
} else {
29+
curTween.onEnd(() => {
30+
pauseMenu.hidden = true;
31+
});
32+
}
33+
});
34+
35+
const pauseMenu = add([
36+
rect(300, 400),
37+
color(255, 255, 255),
38+
outline(4),
39+
anchor('center'),
40+
pos(center().add(0, 700)),
41+
]);
42+
43+
pauseMenu.hidden = true;
44+
45+
pauseMenu.add([
46+
text('Paused', {
47+
size: 36,
48+
}),
49+
color(0, 0, 0),
50+
center(),
51+
anchor('center'),
52+
]);
53+
}

src/gameobjects/score.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function addScore() {
2+
const scoreDisplay = add([
3+
text('0', { size: 32 }),
4+
pos(center().x, 5),
5+
color(255, 255, 255),
6+
]);
7+
8+
return (incrementBy = 1) => {
9+
scoreDisplay.text = String(parseInt(scoreDisplay.text) + incrementBy);
10+
};
11+
}

src/gameobjects/timer.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getTime, startTimer } from '../helpers';
2+
3+
export function addTimer() {
4+
const timerDisplay = add([
5+
text('00:00:00:0', { size: 16 }),
6+
pos(width() - 100, 0),
7+
color(255, 255, 255),
8+
]);
9+
10+
startTimer((time) => {
11+
timerDisplay.text = time;
12+
});
13+
14+
const map: Record<
15+
string,
16+
{ direction: string; time: number; duration?: number }
17+
> = {};
18+
19+
onKeyPress((key) => {
20+
const direction = {
21+
direction: key,
22+
time: getTime(),
23+
};
24+
map[key] = direction;
25+
});
26+
27+
onKeyRelease((key) => {
28+
const time = getTime();
29+
const direction = {
30+
direction: key,
31+
time,
32+
duration: Number((time - map[key].time).toFixed(1)),
33+
};
34+
35+
if (debug.inspect) {
36+
// eslint-disable-next-line no-console
37+
console.log(JSON.stringify(direction));
38+
}
39+
});
40+
}

0 commit comments

Comments
 (0)