Skip to content

Commit d96cd0b

Browse files
committed
added game
1 parent 90de9ff commit d96cd0b

21 files changed

+731
-0
lines changed

aseprites/Butterfly.aseprite

2.32 KB
Binary file not shown.

aseprites/Flower1.aseprite

586 Bytes
Binary file not shown.

aseprites/Flower2.aseprite

601 Bytes
Binary file not shown.

aseprites/RetroColors.ase

348 Bytes
Binary file not shown.

aseprites/Surface1.aseprite

2.77 KB
Binary file not shown.

assets/Butterfly.gif

1.41 KB
Loading

assets/Flower1.png

468 Bytes
Loading

assets/Flower2.png

528 Bytes
Loading

assets/Surface1.png

7.4 KB
Loading

game/FlyButterfly.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class FlyButterflyGame extends Game {
2+
start() {
3+
super.start();
4+
5+
const lifeInterval = setInterval(() => {
6+
this.life.points -= this.level / 5;
7+
if (this.over) {
8+
clearInterval(lifeInterval);
9+
}
10+
}, 100);
11+
12+
const raisePoints = () => window.requestAnimationFrame(() => {
13+
this.score.points += 0.1 * this.level;
14+
this.level = Math.ceil(this.score.points / 1000);
15+
16+
if (!this.over) {
17+
raisePoints();
18+
}
19+
});
20+
21+
raisePoints();
22+
}
23+
24+
end() {
25+
super.end();
26+
27+
const gameOverScreen = document.createElement("div");
28+
gameOverScreen.id = "gameover";
29+
gameOverScreen.innerHTML = `<h1>GAME OVER</h1>Score: <span>${Math.floor(this.score.points)}</span><button onclick='window.location.reload()'>PLAY AGAIN</button>`;
30+
this.scene.renderGameElement(new GameElement(gameOverScreen));
31+
}
32+
}
33+
34+
new FlyButterflyGame({
35+
sceneElement: document.querySelector("#game"),
36+
playerElement: document.querySelector("#player"),
37+
spawners: [FlowerSpawner]
38+
});

game/engine/Controls.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
class Controls {
2+
keyEventListeners = {};
3+
mouseEventListeners = {};
4+
enabled = true;
5+
sceneElement = null;
6+
7+
constructor({ sceneElement }) {
8+
window.addEventListener("keydown", event => {
9+
if (!this.enabled) {
10+
return;
11+
}
12+
13+
(this.keyEventListeners[event.key] || []).forEach(callback => window.requestAnimationFrame(callback));
14+
});
15+
16+
sceneElement.addEventListener("mousemove", event => {
17+
if (!this.enabled) {
18+
return;
19+
}
20+
21+
(this.mouseEventListeners["MouseMove"] || []).forEach(callback => window.requestAnimationFrame(() => callback(event)));
22+
});
23+
}
24+
25+
onMouseMove(callback) {
26+
this.mouseEventListeners["MouseMove"] = [...(this.mouseEventListeners["MouseMove"] || []), callback];
27+
}
28+
29+
onMouseDown(callback) {
30+
this.mouseEventListeners["MouseDown"] = [...(this.mouseEventListeners["MouseDown"] || []), callback];
31+
}
32+
33+
onArrowDown(callback) {
34+
this.keyEventListeners["ArrowDown"] = [...(this.keyEventListeners["ArrowDown"] || []), callback];
35+
}
36+
37+
onArrowUp(callback) {
38+
this.keyEventListeners["ArrowUp"] = [...(this.keyEventListeners["ArrowUp"] || []), callback];
39+
}
40+
41+
onArrowLeft(callback) {
42+
this.keyEventListeners["ArrowLeft"] = [...(this.keyEventListeners["ArrowLeft"] || []), callback];
43+
}
44+
45+
onArrowRight(callback) {
46+
this.keyEventListeners["ArrowRight"] = [...(this.keyEventListeners["ArrowRight"] || []), callback];
47+
}
48+
}

game/engine/Game.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
class Game {
2+
over = false;
3+
4+
/**
5+
* @type {Player}
6+
*/
7+
player = null;
8+
9+
/**
10+
* @type {Controls}
11+
*/
12+
controls = null;
13+
14+
/**
15+
* @type {Score}
16+
*/
17+
score = null;
18+
19+
level = 1;
20+
21+
constructor({
22+
playerElement, sceneElement, spawners = []
23+
}) {
24+
this.controls = new Controls({ sceneElement });
25+
this.scene = new GameScene(sceneElement, { spawners: spawners.map(spawner => new spawner(this)) });
26+
this.player = new Player({
27+
playerGameElement: new GameElement(playerElement),
28+
gameControls: this.controls,
29+
gameScene: this.scene
30+
});
31+
this.score = new Score(this.scene);
32+
this.life = new PlayerLife(this.scene, () => this.end());
33+
34+
this.start();
35+
}
36+
37+
end() {
38+
this.over = true;
39+
40+
this.controls.enabled = false;
41+
42+
this.scene._element.style.cursor = "default";
43+
this.scene.destroy();
44+
this.player.destroy();
45+
}
46+
47+
start() {}
48+
}

game/engine/GameElement.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
class GameElement {
2+
/**
3+
* @type {HTMLElement}
4+
*/
5+
_element = null;
6+
7+
id = `element${randomId()}`;
8+
isDestroyed = false;
9+
10+
/**
11+
* @param {HTMLElement} element
12+
*/
13+
constructor(element) {
14+
this._element = element;
15+
this._element.setAttribute("data-game-element-id", this.id);
16+
}
17+
18+
get x() {
19+
return this._element.offsetLeft;
20+
}
21+
22+
set x(value) {
23+
this._element.style.left = `${value}px`;
24+
}
25+
26+
get y() {
27+
return this._element.offsetTop;
28+
}
29+
30+
set y(value) {
31+
this._element.style.top = `${value}px`;
32+
}
33+
34+
get width() {
35+
return this._element.offsetWidth;
36+
}
37+
38+
get height() {
39+
return this._element.offsetHeight;
40+
}
41+
42+
getElementBox() {
43+
return { width: this.width, height: this.height, x: this.x, y: this.y }
44+
}
45+
46+
/**
47+
* @param {GameScene} scene
48+
*/
49+
renderIn(scene) {
50+
scene.renderGameElement(this);
51+
}
52+
53+
setContent(value) {
54+
this._element.innerHTML = value;
55+
}
56+
57+
setAttribute(attribute, value) {
58+
this._element.setAttribute(attribute, value);
59+
}
60+
61+
setData(key, value) {
62+
this._element.dataset[key] = value;
63+
}
64+
65+
setAlpha(value) {
66+
this._element.style.opacity = value / 100;
67+
}
68+
69+
getData(key) {
70+
return this._element.dataset[key];
71+
}
72+
73+
destroy() {
74+
this._element.remove();
75+
this.isDestroyed = true;
76+
this._element = null;
77+
}
78+
79+
/**
80+
* @param {GameElement} gameElement
81+
*/
82+
collidesWith(gameElement) {
83+
if (gameElement.isDestroyed || this.isDestroyed) {
84+
return false;
85+
}
86+
87+
return this.x >= gameElement.x &&
88+
this.y >= gameElement.y &&
89+
this.x <= gameElement.x + gameElement.width &&
90+
this.y <= gameElement.y + gameElement.height;
91+
}
92+
}

game/engine/GameScene.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
class GameScene {
2+
/**
3+
* @type {HTMLElement}
4+
*/
5+
_element = null;
6+
7+
/**
8+
* @type {Spawner[]}
9+
*/
10+
_spawners = null;
11+
12+
/**
13+
* @type {Object.<string,GameElement>}
14+
*/
15+
_elements = {};
16+
17+
/**
18+
* @type {MutationObserver}
19+
*/
20+
_mutationObserver = {};
21+
22+
_spawnerTimers = {};
23+
24+
/**
25+
* @param {HTMLElement} element
26+
*/
27+
constructor(element, { spawners = [] } = {}) {
28+
this._element = element;
29+
this._spawners = spawners;
30+
31+
this._element.addEventListener("contextmenu", e => e.preventDefault());
32+
33+
this.setupSpawners();
34+
this.setupMutationObserver();
35+
}
36+
37+
get width() {
38+
return this._element.offsetWidth;
39+
}
40+
41+
get height() {
42+
return this._element.offsetHeight;
43+
}
44+
45+
/**
46+
* @param {GameElement} element
47+
*/
48+
elementIsInScene(element) {
49+
const isInX = element.x >= 0 && element.x + element.width <= this.width;
50+
const isInY = element.y >= 0 && element.y + element.height <= this.height;
51+
52+
return isInX && isInY;
53+
}
54+
55+
setupSpawners() {
56+
this._spawners.forEach(spawner => {
57+
const spawnLoop = () => {
58+
this._spawnerTimers[spawner.id] = setTimeout(() => {
59+
spawner.spawnInto(this);
60+
spawnLoop();
61+
}, spawner.frequency());
62+
};
63+
spawnLoop();
64+
});
65+
}
66+
67+
destroy() {
68+
Object.values(this._spawnerTimers).forEach(timer => clearTimeout(timer));
69+
this._spawners = [];
70+
this._mutationObserver.disconnect();
71+
}
72+
73+
setupMutationObserver() {
74+
this._mutationObserver = new MutationObserver(mutations => {
75+
mutations.forEach(mutation => {
76+
if (mutation.type !== "childList") {
77+
return;
78+
}
79+
80+
if (mutation.addedNodes.length) {
81+
mutation.addedNodes.forEach(node => {
82+
if (node.nodeType !== Node.TEXT_NODE) {
83+
this._elements[node.dataset.gameElementId] = new GameElement(node);
84+
}
85+
});
86+
}
87+
88+
if (mutation.removedNodes.length) {
89+
mutation.removedNodes.forEach(node => {
90+
if (node.nodeType !== Node.TEXT_NODE) {
91+
delete this._elements[node.dataset.gameElementId];
92+
}
93+
});
94+
}
95+
});
96+
});
97+
98+
this._mutationObserver.observe(this._element, { childList: true, subtree: true });
99+
}
100+
101+
/**
102+
* @param {GameElement} element
103+
*/
104+
renderGameElement(element) {
105+
this._element.append(element._element);
106+
}
107+
108+
getElement(id) {
109+
return this._elements[id];
110+
}
111+
}

0 commit comments

Comments
 (0)