first commit
38
bomberman.conf
Normal file
@ -0,0 +1,38 @@
|
||||
Define short_name bomberman
|
||||
Define server_name ${short_name}.zapashcanon.fr
|
||||
Define admin_mail leo@ndrs.fr
|
||||
Define cert_name ${server_name}
|
||||
Define cert_dir /etc/letsencrypt/live
|
||||
Define log_dir /var/log/apache2
|
||||
Define log_file ${log_dir}/${short_name}
|
||||
Define error_log_file ${log_file}.error.log
|
||||
Define access_log_file ${log_file}.access.log
|
||||
Define doc_root /var/www/${server_name}/src
|
||||
|
||||
<VirtualHost *:80>
|
||||
|
||||
RewriteEngine on
|
||||
RewriteCond %{SERVER_NAME} =${server_name}
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
|
||||
ServerName ${server_name}
|
||||
ServerAdmin ${admin_mail}
|
||||
|
||||
DocumentRoot ${doc_root}
|
||||
|
||||
ErrorLog ${error_log_file}
|
||||
CustomLog ${access_log_file} combined
|
||||
|
||||
Protocols h2 http/1.1
|
||||
|
||||
SSLCertificateFile ${cert_dir}/${cert_name}/fullchain.pem
|
||||
SSLCertificateKeyFile ${cert_dir}/${cert_name}/privkey.pem
|
||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||
|
||||
</VirtualHost>
|
||||
</IfModule>
|
16
scripts/install.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
( cd "$(dirname "$0")/../"
|
||||
|
||||
domain=bomberman.zapashcanon.fr
|
||||
|
||||
sudo cp ./*.conf /etc/apache2/sites-available/
|
||||
dst=/var/www/${domain}/
|
||||
sudo mkdir -p $dst
|
||||
sudo cp -r src/ $dst
|
||||
|
||||
sudo service apache2 restart
|
||||
echo "Installed !"
|
||||
)
|
BIN
src/audio/intro-screen.mp3
Normal file
BIN
src/audio/intro-screen.ogg
Normal file
17
src/css/style.css
Normal file
@ -0,0 +1,17 @@
|
||||
#canvas {
|
||||
position: relative;
|
||||
width: 1000px;
|
||||
height: 600px;
|
||||
border: 2px solid black;
|
||||
z-index:1;
|
||||
}
|
||||
|
||||
.object {
|
||||
position: absolute;
|
||||
z-index:2;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
/*#audioplayer {
|
||||
display: none;
|
||||
}*/
|
BIN
src/img/betty_bottom.png
Normal file
After Width: | Height: | Size: 706 B |
BIN
src/img/betty_dead.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
src/img/betty_left.png
Normal file
After Width: | Height: | Size: 622 B |
BIN
src/img/betty_right.png
Normal file
After Width: | Height: | Size: 623 B |
BIN
src/img/betty_roasted.png
Normal file
After Width: | Height: | Size: 673 B |
BIN
src/img/betty_top.png
Normal file
After Width: | Height: | Size: 660 B |
BIN
src/img/bettytwo_bottom.png
Normal file
After Width: | Height: | Size: 704 B |
BIN
src/img/bettytwo_dead.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
src/img/bettytwo_left.png
Normal file
After Width: | Height: | Size: 613 B |
BIN
src/img/bettytwo_right.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
src/img/bettytwo_roasted.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
src/img/bettytwo_top.png
Normal file
After Width: | Height: | Size: 655 B |
BIN
src/img/bomb_0.png
Normal file
After Width: | Height: | Size: 566 B |
BIN
src/img/bomb_1.png
Normal file
After Width: | Height: | Size: 611 B |
BIN
src/img/bomb_2.png
Normal file
After Width: | Height: | Size: 626 B |
BIN
src/img/bomb_3.png
Normal file
After Width: | Height: | Size: 611 B |
BIN
src/img/bomb_4.png
Normal file
After Width: | Height: | Size: 695 B |
BIN
src/img/bomberman_bottom.png
Normal file
After Width: | Height: | Size: 454 B |
BIN
src/img/bomberman_dead.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/img/bomberman_left.png
Normal file
After Width: | Height: | Size: 375 B |
BIN
src/img/bomberman_right.png
Normal file
After Width: | Height: | Size: 380 B |
BIN
src/img/bomberman_roasted.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/img/bomberman_top.png
Normal file
After Width: | Height: | Size: 438 B |
BIN
src/img/george_bottom.png
Normal file
After Width: | Height: | Size: 632 B |
BIN
src/img/george_dead.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
src/img/george_left.png
Normal file
After Width: | Height: | Size: 582 B |
BIN
src/img/george_right.png
Normal file
After Width: | Height: | Size: 587 B |
BIN
src/img/george_roasted.png
Normal file
After Width: | Height: | Size: 605 B |
BIN
src/img/george_top.png
Normal file
After Width: | Height: | Size: 549 B |
BIN
src/img/tile_grass.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
src/img/tile_wall.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
src/img/tile_wood.png
Normal file
After Width: | Height: | Size: 54 KiB |
54
src/index.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Bomberman</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
|
||||
<link href="https://unpkg.com/nes.css/css/nes.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid text-center">
|
||||
<br />
|
||||
<h1><i class="nes-icon star is-large"></i> Bomberman <i class="nes-icon star is-large"></i></h1>
|
||||
<br />
|
||||
</div>
|
||||
<div id="canvasdiv" class="container-fluid text-center">
|
||||
<canvas id="canvas" width="800" height="600"> </canvas>
|
||||
</div>
|
||||
<br />
|
||||
<div class="container-fluid text-center">
|
||||
<button type="button" class="nes-btn" id="new-game">New Game</button>
|
||||
<br />
|
||||
<br />
|
||||
<audio id="audioplayer" controls loop>
|
||||
<source src="audio/intro-screen.ogg" type="audio/ogg">
|
||||
<source src="audio/intro-screen.mp3" type="audio/mpeg">
|
||||
<p>Update pliiiiiz</p>
|
||||
</audio>
|
||||
</div>
|
||||
<br />
|
||||
<div class="nes-container is-dark with-title container-fluid">
|
||||
<p class="title">Commands</p>
|
||||
<p>Player 1: <kbd>e</kbd> and <kbd>d</kbd><br />
|
||||
Player 2: <kbd>o</kbd> and <kbd>l</kbd></p>
|
||||
</div>
|
||||
|
||||
<script defer type="module" src="js/main.js"></script>
|
||||
|
||||
<div style="display:none;">
|
||||
<img id="tile_grass" src="img/tile_grass.png">
|
||||
<img id="tile_wall" src="img/tile_wall.png">
|
||||
<img id="tile_wood" src="img/tile_wood.png">
|
||||
<img id="betty_bottom" src="img/betty_bottom.png">
|
||||
<img id="bomb_0" src="img/bomb_0.png">
|
||||
<img id="bomb_1" src="img/bomb_1.png">
|
||||
<img id="bomb_2" src="img/bomb_2.png">
|
||||
<img id="bomb_3" src="img/bomb_3.png">
|
||||
<img id="bomb_4" src="img/bomb_4.png">
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
25
src/js/bomb.js
Normal file
@ -0,0 +1,25 @@
|
||||
// @flow
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as point from "./point.js";
|
||||
|
||||
export class Bomb extends point.Point {
|
||||
constructor (p) {
|
||||
super(p.x, p.y);
|
||||
this.created = Date.now();
|
||||
this.power = 2; // TODO: how much ?
|
||||
};
|
||||
age () {
|
||||
return Math.floor ((Date.now() - this.created) / 1000);
|
||||
};
|
||||
is_alive () {
|
||||
return this.age() < 4; // TODO: how long ?
|
||||
};
|
||||
get_pos () {
|
||||
return new point.Point(this.x, this.y);
|
||||
};
|
||||
has_pos (p) {
|
||||
return this.x == p.x && this.y == p.y;
|
||||
}
|
||||
}
|
80
src/js/main.js
Normal file
@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as renderer from "./renderer.js";
|
||||
import * as mover from "./mover.js";
|
||||
import * as state from "./state.js";
|
||||
|
||||
const get_json_callback = function(file_name, callback) {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('GET', "json/" + file_name);
|
||||
req.responseType = 'json';
|
||||
req.send();
|
||||
req.onload = function() {
|
||||
callback(req.response);
|
||||
}
|
||||
}
|
||||
|
||||
const mv_step = 0.25;
|
||||
|
||||
window.addEventListener("load", get_json_callback("lvl_01.json", function (map) {
|
||||
|
||||
const s = new state.State(map);
|
||||
|
||||
const up = function(p) {
|
||||
s.move_p(0, -mv_step);
|
||||
};
|
||||
|
||||
const down = function(p) {
|
||||
s.move_p(0, mv_step);
|
||||
};
|
||||
|
||||
const left = function(p) {
|
||||
s.move_p(-mv_step, 0);
|
||||
};
|
||||
|
||||
const right = function(p) {
|
||||
s.move_p(mv_step, 0);
|
||||
}
|
||||
|
||||
const keyboard = function(e /*:: : KeyboardEvent */) {
|
||||
// TODO: forçage de meme, parser directement le .html avec les indications de touche
|
||||
switch (e.key) {
|
||||
case "z":
|
||||
up(s);
|
||||
break;
|
||||
case "s":
|
||||
down(s);
|
||||
break;
|
||||
case "q":
|
||||
left(s);
|
||||
break;
|
||||
case "d":
|
||||
right(s);
|
||||
break;
|
||||
case "b":
|
||||
s.add_bomb();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", keyboard);
|
||||
|
||||
// TODO: better FPS count
|
||||
//let previous = Date.now();
|
||||
let interval;
|
||||
interval = setInterval(function () {
|
||||
try {
|
||||
//const delta = (Date.now() - previous) / 1000;
|
||||
//previous = Date.now();
|
||||
//console.log(1 / delta);
|
||||
renderer.draw_state(s);
|
||||
s.rm_bombs();
|
||||
} catch (e) {
|
||||
clearInterval(interval);
|
||||
throw(e);
|
||||
}
|
||||
|
||||
}, 1000 / 60);
|
||||
|
||||
}));
|
29
src/js/mover.js
Normal file
@ -0,0 +1,29 @@
|
||||
// @flow
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as point from "./point.js";
|
||||
|
||||
const dec_x = -0.1; // TODO
|
||||
const dec_y = +0.2; // TODO
|
||||
|
||||
export class Mover extends point.Point {
|
||||
/*:: x: number */
|
||||
/*:: y: number */
|
||||
constructor (x /*: number */, y /*: number */) {
|
||||
super(x, y);
|
||||
this.alive = true;
|
||||
}
|
||||
kill () {
|
||||
this.alive = false;
|
||||
}
|
||||
copy () {
|
||||
let c = new Mover(this.x, this.y);
|
||||
c.alive = this.alive;
|
||||
return c;
|
||||
}
|
||||
move (p) {
|
||||
this.x += p.x;
|
||||
this.y += p.y;
|
||||
}
|
||||
}
|
28
src/js/point.js
Normal file
@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
|
||||
"use strict";
|
||||
|
||||
export class Point {
|
||||
/*:: x: number */
|
||||
/*:: y: number */
|
||||
constructor (x /*: number */, y /*: number */) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
};
|
||||
add (p) {
|
||||
return new Point(this.x + p.x, this.y + p.y);
|
||||
};
|
||||
round () {
|
||||
return new Point(Math.round(this.x), Math.round(this.y));
|
||||
};
|
||||
eq (p) {
|
||||
return this.x == p.x && this.y == p.y;
|
||||
};
|
||||
}
|
||||
|
||||
const left = new Point(-1, 0);
|
||||
const right = new Point(1, 0);
|
||||
const bottom = new Point(0, 1);
|
||||
const top = new Point(0, -1);
|
||||
|
||||
export {left, right, bottom, top};
|
74
src/js/renderer.js
Normal file
@ -0,0 +1,74 @@
|
||||
// @flow
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as state from "./state.js";
|
||||
|
||||
const assert_valid = function (el, t) {
|
||||
if (!(el && el instanceof t)) {
|
||||
throw "invalid object";
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
assert_valid(canvas, HTMLCanvasElement);
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
assert_valid(context, CanvasRenderingContext2D);
|
||||
|
||||
const tile_grass = document.getElementById("tile_grass");
|
||||
const tile_wall = document.getElementById("tile_wall");
|
||||
const tile_wood = document.getElementById("tile_wood");
|
||||
const betty_bottom = document.getElementById("betty_bottom");
|
||||
const bomb_0 = document.getElementById("bomb_0");
|
||||
const bomb_1 = document.getElementById("bomb_1");
|
||||
const bomb_2 = document.getElementById("bomb_2");
|
||||
const bomb_3 = document.getElementById("bomb_3");
|
||||
const bomb_4 = document.getElementById("bomb_4");
|
||||
|
||||
const img_of_block = function(b) {
|
||||
switch (b) {
|
||||
case state.block.Grass:
|
||||
return tile_grass;
|
||||
case state.block.Wood:
|
||||
return tile_wood;
|
||||
case state.block.Wall:
|
||||
return tile_wall;
|
||||
default:
|
||||
console.log("Missing block img");
|
||||
}
|
||||
}
|
||||
|
||||
const draw_state = function (s) {
|
||||
const step_x = width / s.width;
|
||||
const step_y = height / s.height;
|
||||
context.clearRect(0, 0, width, height);
|
||||
for (let i = 0; i < s.width; i++) {
|
||||
for (let j = 0; j < s.height; j++) {
|
||||
context.drawImage(img_of_block(s.grid[i][j]), i * step_x, j * step_y, step_x, step_y);
|
||||
}
|
||||
}
|
||||
// TODO: draw bombs U { p } starting with the one on the top so they don't overlap weirdly
|
||||
s.bombs.forEach(function (b) {
|
||||
let img = bomb_0;
|
||||
let a = b.age();
|
||||
if (a >= 4) {
|
||||
img = bomb_4;
|
||||
} else if (a == 3) {
|
||||
img = bomb_3;
|
||||
} else if (a == 2) {
|
||||
img = bomb_2;
|
||||
} else if (a == 1) {
|
||||
img = bomb_1;
|
||||
}
|
||||
context.drawImage(img, b.x * step_x, b.y * step_y, step_x, step_y);
|
||||
}
|
||||
);
|
||||
context.drawImage(betty_bottom, s.p.x * step_x, s.p.y * step_y, step_x, step_y);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
export { draw_state };
|
110
src/js/state.js
Normal file
@ -0,0 +1,110 @@
|
||||
// @flow
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as mover from "./mover.js";
|
||||
import * as bomb from "./bomb.js";
|
||||
import * as point from "./point.js";
|
||||
|
||||
const block = {
|
||||
Empty : 1,
|
||||
Grass : 2,
|
||||
Wood : 3,
|
||||
Wall: 4
|
||||
}
|
||||
|
||||
export class State {
|
||||
constructor (map) {
|
||||
this.width = map.width;
|
||||
this.height = map.height;
|
||||
this.grid = map.grid;
|
||||
this.p = new mover.Mover(map.p.x, map.p.y);
|
||||
this.bombs = [];
|
||||
};
|
||||
get_block(p) {
|
||||
return this.grid[p.x][p.y];
|
||||
};
|
||||
set_block(p, v) {
|
||||
this.grid[p.x][p.y] = v;
|
||||
}
|
||||
is_block_empty(p) {
|
||||
const b = this.get_block(p);
|
||||
return b == block.Empty || b == block.Grass;
|
||||
};
|
||||
is_block_in_grid(p) {
|
||||
return p.x >= 0 && p.y >= 0 && p.x < this.width && p.y < this.height;
|
||||
};
|
||||
move_p(x, y) {
|
||||
if (this.p.alive) {
|
||||
// we move a copy
|
||||
let p2 = this.p.copy();
|
||||
p2.move(new point.Point(x, y));
|
||||
let p_round = p2.round();
|
||||
// if it's valid, we use the copy
|
||||
if (this.is_block_in_grid(p_round)) {
|
||||
if (this.is_block_empty(p_round)) {
|
||||
this.p = p2;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
block_has_bomb(p) {
|
||||
let res = false;
|
||||
this.bombs.forEach(function (b) {
|
||||
if (b.has_pos(p)) {
|
||||
res = false; // TODO: escape the foreach
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
add_bomb() {
|
||||
if (this.p.alive) {
|
||||
const p_round = this.p.round();
|
||||
if (!this.block_has_bomb(p_round)) {
|
||||
this.bombs.push(new bomb.Bomb(p_round));
|
||||
}
|
||||
}
|
||||
};
|
||||
explode_block(b_pos) {
|
||||
if (this.is_block_in_grid(b_pos)) {
|
||||
let b = this.get_block(b_pos);
|
||||
if (b == block.Wood) {
|
||||
this.set_block(b_pos, block.Grass);
|
||||
} else if (b_pos.eq(this.p)) { // TODO ugly and unclear
|
||||
this.p.alive = false;
|
||||
}
|
||||
|
||||
// TODO: explode others bombs
|
||||
}
|
||||
};
|
||||
explode_dir(p, dir, power) {
|
||||
if (power > 0) {
|
||||
this.explode_block(p); // TODO: if something is destroyed or if there's an unbreakable block, return false
|
||||
this.explode_dir(p.add(dir), dir, power - 1); // TODO: if the previous function returned false, we don't make this call
|
||||
}
|
||||
};
|
||||
explode_bomb(p, power) {
|
||||
this.explode_block(p);
|
||||
this.explode_dir(p, point.left, power);
|
||||
this.explode_dir(p, point.right, power);
|
||||
this.explode_dir(p, point.top, power);
|
||||
this.explode_dir(p, point.bottom, power);
|
||||
};
|
||||
rm_bombs() {
|
||||
let bombs2 = [];
|
||||
const s = this;
|
||||
this.bombs.forEach(function (b) {
|
||||
if (b) {
|
||||
if (b.is_alive()) {
|
||||
bombs2.push(b);
|
||||
} else {
|
||||
s.explode_bomb(b.get_pos(), b.power);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.bombs = bombs2;
|
||||
};
|
||||
}
|
||||
|
||||
export { block }
|
30
src/json/lvl_01.json
Normal file
@ -0,0 +1,30 @@
|
||||
{ "width": 20,
|
||||
"height": 20,
|
||||
"grid": [
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,2,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4],
|
||||
[2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3,2,3],
|
||||
[2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4,2,4]
|
||||
],
|
||||
"p": {
|
||||
"x": 3,
|
||||
"y": 2
|
||||
}
|
||||
|
||||
}
|