Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
              
            
!

CSS

              
                html,
body {
    margin: 0;
    padding: 0;
    height: 100%;
}
body {
    background-color: #000;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  font-family:arial;
  
}
canvas {
    flex-shrink: 0;
    background-color: #000;
    image-rendering: -moz-crisp-edges;
    image-rendering: -webkit-crisp-edges;
    image-rendering: pixelated;
    object-fit: contain;
}

#info_link{
  position:absolute;
  right:10px;
  bottom:10px;  
}

a{
    text-decoration:none;
  color:lightgray;
}
              
            
!

JS

              
                
Utl = {};
Utl.timeStamp = function() {
	//	return window.performance && window.performance.now ? window.performance.now() : new Date().getTime();
	return window.performance.now();
};
Utl.between = function(valeur, min, max) {
	return (valeur - min) * (valeur - max) < 0;
};
Utl.distance = function(p1, p2) {
	return Math.hypot(p1.x - p2.x, p1.y - p2.y);
};
Utl.angleFrom = function(p1,p2){
	return Math.atan2(p2.y - p1.y, p2.x - p1.x)
};
Utl.pointToRectangle = function(point, rectangle) {
	return Utl.between(point.x, rectangle.pos.x, rectangle.pos.x + rectangle.size.x) && Utl.between(point.y, rectangle.pos.y, rectangle.pos.y + rectangle.size.y);
};
Utl.lerp = function(value1, value2, amount) {
	return value1 + (value2 - value1) * amount;
};
Utl.map = function(a,b,c,d,e){
	return(a-b)/(c-b)*(e-d)+d;
};
Utl.array2D = function(tableau, largeur){
	var result = [];
	for (var i = 0; i < tableau.length; i += largeur) result.push(tableau.slice(i, i + largeur));
		return result;
};
Utl.random = function(min, max) {
	return min + Math.random() * (max - min);
};
Utl.percent = function(percent,of_number){
	return (percent*of_number)/100;
};
Utl.linearTween = function(currentTime, start, degreeOfChange, duration) {
	return degreeOfChange * currentTime / duration + start;
};
Utl.easeInOutQuad = function (t, b, c, d) {
	t /= d/2;
	if (t < 1) return c/2*t*t + b;
	t--;
	return -c/2 * (t*(t-2) - 1) + b;
};

class Vector{
	constructor(x,y){
		this.x = x;
		this.y = y;
	}
	add(vector){
		this.x += vector.x;
		this.y += vector.y;
	}
	sub(vector){
		this.x -= vector.x;
		this.y -= vector.y;
	}
	mult(vector){
		this.x *= vector.x;
		this.y *= vector.y;	
	}
	multBy(value){
		this.x *= value;
		this.y *= value;	
	}
	copy(vector){
		this.x = vector.x;
		this.y = vector.y;
	}
	setAngle(angle){
		let length = this.getLength();
		this.x = Math.cos(angle) * length;
		this.y = Math.sin(angle) * length;
	}
	setLength(length){
		let angle = this.getAngle();
		this.x = Math.cos(angle) * length;
		this.y = Math.sin(angle) * length;
	}
	getAngle(){
		return Math.atan2(this.y,this.x)
	}
	getLength(){
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}
}

class Tile{
	constructor(x,y,tile_size,tile_info){
		this.pos = {
			x:x,
			y:y
		}
		this.scaled_pos = {
			x:x*tile_size,
			y:y*tile_size
		}
		this.tile_info = tile_info;
	}
}

class Camera {
	constructor(world, target) {
		this.world = world;
		this.ctx = world.ctx;
		// viewport size
		this.W = this.world.W;
		this.H = this.world.H;
		this.halfW = (this.W/2) + this.world.tile_size;
		this.halfH = (this.H/2) + this.world.tile_size;
		this.empty = {
			pos: {
				x: this.W / 2,
				y: this.H / 2
			}
		}
		this.target = target || this.empty;
		this.pos = {
			x: (this.W / 2) - this.target.pos.x,
			y: (this.H / 2) - this.target.pos.y
		}
		this.constraint = {
			x:false,
			y:false,
		}
		this.offset = {
			x:0,
			y:-100,
		}
		this.ts = this.world.tile_size;
		this.get = {
			top:()=>{
				return -this.pos.y;
			},
			bottom:()=>{
				return -this.pos.y + this.H;
			},
		}
	}
	visible(x,y){
		if(Utl.between(x,-this.pos.x-this.ts,-this.pos.x+this.W) &&
			Utl.between(y,-this.pos.y-this.ts,-this.pos.y+this.H)){
			return true;
		}else{
			return false;
		}
	}
	setConstraint(x,y){
		this.constraint.x = x;
		this.constraint.y = y;
	}
	setTarget(target) {
		this.target = target;
		this.setPos();
	}
	setOffset(x,y){
		this.offset.x = x;
		this.offset.y = y;
	}
	resetTarget() {
		this.pos.x = 0;
		this.pos.y = 0;
		this.target = this.empty;
	}
	setPos(){
		if(!this.constraint.x){this.pos.x = (this.W / 2) - this.target.pos.x - this.offset.x;}
		if(!this.constraint.y){this.pos.y = (this.H / 2) - this.target.pos.y - this.offset.y;}
	}
	update() {
		this.setPos();
		// map limit
		let camera_bottom = this.get.bottom();
		if(camera_bottom > this.world.terrain.reelLimit.y){
			this.pos.y -= this.world.terrain.reelLimit.y - camera_bottom;
		}
	}
}

class Scene {
	constructor(world, name) {
		this.name = name;
		this.world = world;
		this.ctx = world.ctx;
		this.loop = true;
		this.init_once = false;
	}
	pointerMove(event) {

	}
	pointerDown(event) {

	}
	pointerUp(event) {

	}
	keyEvents(event) {

	}
	init() {

	}
	update(delta) {

	}
	render() {

	}
};

class Diorama {
	constructor(parameters) {
		// Game Info
		this.game_info = {
			name: parameters.game_name || "Untitled",
			version: parameters.version || "0",
			author: parameters.author || "Anonymous",
		};
		// Touch and keyboard data
		this.event_needs = parameters.event_needs;
		this.keys = [];
		this.pointer = {
			pos: {
				x: 0,
				y: 0
			},
			active: false,
		};
		// Scenes
		this.scenes = {};
		this.start_screen = parameters.start_screen;
		this.current_scene = "";
		// Maps
		this.gravity = parameters.gravity || new Vector(0, 0);
		this.tile_size = parameters.tile_size || 16;
		this.maps = {};
		// Image and audio are stocked here :)
		this.ressources = {};
		// Minimal system for font and Cursor
		this.alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ?!:',.()<>[]";
		this.font_name = parameters.font || "bitmap_font";
		this.cursor = parameters.cursor || "cursor";
		// FPS
		this.FPS = {
			now: 0,
			delta: 0,
			last: Utl.timeStamp(),
			step: 1 / (parameters.frame_rate || 30),
		};
		this.requestChange = {
			value: false,
			action: ""
		};
		this.main_loop = undefined;
		this.pause = false;
		// Creates the canvas and load the ressources
		this.canvasSetup(parameters);
		// loading info
		this.load_info = {
			loaded: 0,
			toLoad: 0,
			complete: false,
			game_ready: false,
		};
		this.dataProcessing(parameters);
		// Camera
		this.camera = new Camera(this);
		// Quick functions 
		this.audio_muted = false;
		this.audio = {
			setLoop:(name,value)=>{
				this.ressources.audio[name].url.loop = value;
			},
			volume:(name,value)=>{
				this.ressources.audio[name].url.volume = value;
			},
			play: (name) => {
				this.ressources.audio[name].url.play();
			},
			pause: (name) => {
				this.ressources.audio[name].url.pause();
			},
			stop: (name) => {
				this.ressources.audio[name].url.pause();
				this.ressources.audio[name].url.currentTime = 0;
			},
			getCurrentTime: (name) => {
				return this.ressources.audio[name].url.currentTime;
			}
		};
		this.image = {
			draw: (name, x, y) => {
				this.ctx.drawImage(this.ressources.images[name].img, x, y);
			},
		}
	}

	canvasSetup(parameters) {
		this.canvas = document.createElement("canvas");
		this.ctx = this.canvas.getContext('2d');
		this.W = this.canvas.width = parameters.width || 128;
		this.H = this.canvas.height = parameters.height || 128;
		this.scale = parameters.scale || 1;
		this.full = false;
		this.ctx.imageSmoothingEnabled = false;
		document.title = this.game_info.name;
		document.body.appendChild(this.canvas);
		this.setScale();
	}
	//  _      ____          _____  ______ _____  
	// | |    / __ \   /\   |  __ \|  ____|  __ \ 
	// | |   | |  | | /  \  | |  | | |__  | |__) |
	// | |   | |  | |/ /\ \ | |  | |  __| |  _  / 
	// | |___| |__| / ____ \| |__| | |____| | \ \ 
	// |______\____/_/    \_\_____/|______|_|  \_\


	dataProcessing(parameters) {
		// check if there is image or audio to load
		let image_length = 0,
			audio_length = 0;
		(parameters.images) ? image_length = parameters.images.length: 0;
		(parameters.audio) ? audio_length = parameters.audio.length: 0;
		this.load_info.toLoad = image_length + audio_length;
		if (this.load_info.toLoad !== 0) {
			this.load_info.complete = false;
			// Processing Images
			let IM = {};
			for (let i = 0; i < image_length; i++) {
				let subject = parameters.images[i];
				let name = subject.name;
				subject.img = this.loadImage(parameters.images[i].img);
				IM[name] = subject;
			}
			this.ressources.images = IM;
			// Processing Audio
			let IS = {};
			for (let i = 0; i < audio_length; i++) {
				let subject = parameters.audio[i];
				let name = subject.name;
				subject.url = this.loadAudio(parameters.audio[i].url);
				IS[name] = subject;
			}
			this.ressources.audio = IS;
		} else {
			this.load_info.complete = true;
		}
		// check if there is tiles to assign
		if (parameters.tiles) {
			let CM = {};
			for (let i = 0; i < parameters.tiles.length; i++) {
				let subject = parameters.tiles[i];
				let name = subject.id;
				CM[name] = subject;
			}
			this.tiles_data = CM;
		}
		// give names to maps
		if (parameters.maps) {
			this.mapsCount = parameters.maps.length;
			let maps_result = {};
			for (let i = 0; i < parameters.maps.length; i++) {
				let subject = parameters.maps[i];
				let name = subject.name;
				maps_result[name] = subject;
			}
			this.maps = maps_result;
		}

	}

	load() {
		this.load_info.loaded += 1;
		if (this.load_info.loaded === this.load_info.toLoad) {
			// loading finished
			this.load_info.complete = true;
			this.launch();
		} else {
			// loading screen
			this.clearCanvas();
			this.ctx.fillStyle = "#888";
			this.ctx.fillRect(18, Math.round(Utl.percent(90, this.H) - 8), this.W - 38, 8);
			this.ctx.fillStyle = "#555";
			this.ctx.fillRect(20, Math.round(Utl.percent(90, this.H) - 8) + 2, Math.round(((this.load_info.loaded * this.W) / this.load_info.toLoad)) - 10, 4);
		}
	}

	loadImage(url) {
		let img = new Image();
		img.onload = () => {
			this.load();
		};
		img.src = url;
		return img;
	}

	loadAudio(url) {
		let audio = new Audio(url);
		audio.addEventListener('canplaythrough', this.load(), false);
		return audio;
	}

	ready() {
		this.load_info.game_ready = true;
		this.launch();
	}

	launch() {
		if (this.load_info.game_ready && this.load_info.complete) {
			this.font = this.ressources.images[this.font_name];
			this.eventSetup();
			this.clearCanvas();
			this.startScene(this.start_screen);
		}
	}

	//  ________      ________ _   _ _______ _____ 
	// |  ____\ \    / /  ____| \ | |__   __/ ____|
	// | |__   \ \  / /| |__  |  \| |  | | | (___  
	// |  __|   \ \/ / |  __| | . ` |  | |  \___ \ 
	// | |____   \  /  | |____| |\  |  | |  ____) |
	// |______|   \/   |______|_| \_|  |_| |_____/ 

	eventSetup() {
		if (this.event_needs.keyboard) {
			document.addEventListener("keydown", event => this.keyDown(event), false);
			document.addEventListener("keyup", event => this.keyUp(event), false);
		}
		if (this.event_needs.touch) {
			document.addEventListener("pointerdown", event => this.pointerDown(event), false);
			document.addEventListener("pointerup", event => this.pointerUp(event), false);
			document.addEventListener("pointermove", event => this.pointerMove(event), false);
		}
	}

	keyDown(event) {
		this.keys[event.keyCode] = true;
		if (this.keys[70]) {
			this.fullScreen();
		}
		this.current_scene.keyEvents(event);
	}
	keyUp(event) {
		this.keys[event.keyCode] = false;
	}
	updatePointerPosition(event) {
		this.pointer.pos.x = event.pageX - this.canvas.offsetLeft;
		this.pointer.pos.y = event.pageY - this.canvas.offsetTop;
	}
	pointerMove(event) {
		this.updatePointerPosition(event);
		this.current_scene.pointerMove(event);
	}
	pointerDown(event) {
		this.pointer.active = true;
		this.updatePointerPosition(event);
		// send information to current scene
		this.current_scene.pointerDown(event);
	}
	pointerUp(event) {
		this.pointer.active = false;
		// send information to current scene
		this.current_scene.pointerUp(event);
	}

	//  _______                  _          __                  _   _                 
	// |__   __|                (_)        / _|                | | (_)                
	//    | | ___ _ __ _ __ __ _ _ _ __   | |_ _   _ _ __   ___| |_ _  ___  _ __  ___ 
	//    | |/ _ \ '__| '__/ _` | | '_ \  |  _| | | | '_ \ / __| __| |/ _ \| '_ \/ __|
	//    | |  __/ |  | | | (_| | | | | | | | | |_| | | | | (__| |_| | (_) | | | \__ \
	//    |_|\___|_|  |_|  \__,_|_|_| |_| |_|  \__,_|_| |_|\___|\__|_|\___/|_| |_|___/

	changeTile(x, y, new_ID) {
		this.terrain.data[y][x] = new_ID;
	}

	tileByID(tile_ID) {
		let result = [];
		for (let y = 0; y < this.terrain.limit.y; y++) {
			for (let x = 0; x < this.terrain.limit.x; x++) {
				if (this.terrain.data[y][x] === tile_ID) {
					result.push(new Tile(x, y, this.tile_size, this.tiles_data[tile_ID]));
				}
			}
		}
		return result;
	}
	getTileData(x, y) {
		if (x < 0 || y < 0) return false;
		if (x > this.terrain.reelLimit.x || y > this.terrain.reelLimit.y - 1) return false;
		let NewX = Math.floor(x / this.tile_size),
			NewY = Math.floor(y / this.tile_size);
		let tile_ID = this.terrain.data[NewY][NewX];
		let tile_data = {
			id: this.terrain.data[NewY][NewX],
			pos: {
				x: NewX * this.tile_size,
				y: NewY * this.tile_size
			}
		}
		if (this.tiles_data[tile_ID]) tile_data.data = this.tiles_data[tile_ID];

		return tile_data;
	}

	getTileCollision(x, y) {
		let tile = this.getTileData(x, y);
		if (!tile.data) return false;
		if (!tile.data.collision) return false;
		return tile.data.collision;
	}

	getTileCollisionData(x, y) {
		let tile = this.getTileData(x, y);
		let collision = this.getTileCollision(x, y);
		if (!collision) return false;
		let neighbors = {
			left: this.getTileCollision(tile.pos.x - this.tile_size, tile.pos.y),
			right: this.getTileCollision(tile.pos.x + this.tile_size, tile.pos.y),
			top: this.getTileCollision(tile.pos.x, tile.pos.y - this.tile_size),
			bottom: this.getTileCollision(tile.pos.x, tile.pos.y + this.tile_size),
		}
		let collision_data = {
			collision: true,
			neighbors: neighbors,
			pos: tile.pos,
		}
		return collision_data;
	}
	setTerrainLimit(){
		this.terrain.limit = {
			x: this.terrain.data[0].length,
			y: this.terrain.data.length
		};
		this.terrain.reelLimit = {
			x: this.terrain.data[0].length * this.tile_size,
			y: this.terrain.data.length * this.tile_size
		};
	}
	initMap(terrain_id) {
		this.terrain = {};
		this.terrain.tileset = this.ressources.images[this.maps[terrain_id].tileset].img;

		this.terrain.tileset_data = {
			width: (this.terrain.tileset.width / this.tile_size),
			height: (this.terrain.tileset.height / this.tile_size) + 1,
		}
		this.arret = false;
		this.terrain.data = this.maps[terrain_id].data.slice(0);
		this.setTerrainLimit();
		this.terrain.bitMask = [];
	}
	bitMasking() {
		this.terrain.bitMask = [];
		for (let y = 0; y < this.terrain.limit.y; y++) {
			for (let x = 0; x < this.terrain.limit.x; x++) {
				let id = this.terrain.data[y][x];
				// haut gauche droit bas
				let voisine = [0, 0, 0, 0];
				if (y - 1 > -1) {
					if (id === this.terrain.data[y - 1][x]) {
						//haut
						voisine[0] = 1;
					}
				}
				if (id === this.terrain.data[y][x - 1]) {
					// gauche
					voisine[1] = 1;
				}
				if (id === this.terrain.data[y][x + 1]) {
					// droite
					voisine[2] = 1;
				}
				if (y + 1 < this.terrain.limit.y) {
					if (id === this.terrain.data[y + 1][x]) {
						//bas
						voisine[3] = 1;
					}
				}
				id = 1 * voisine[0] + 2 * voisine[1] + 4 * voisine[2] + 8 * voisine[3];
				this.terrain.bitMask.push(id);
			}
		}
		this.terrain.bitMask = Utl.array2D(this.terrain.bitMask, this.terrain.limit.x);
	}
	renderMap() {
		for (let j = 0; j < this.terrain.limit.y; j++) {
			for (let i = 0; i < this.terrain.limit.x; i++) {
				let id = this.terrain.data[j][i];
				let positionX = i * this.tile_size,
					positionY = j * this.tile_size;

				let sourceX = Math.floor(id % this.terrain.tileset_data.width) * this.tile_size,
					sourceY = Math.floor(id / this.terrain.tileset_data.width) * this.tile_size;

				if (this.camera.visible(positionX, positionY)) {
					if (this.tiles_data[id] !== undefined) {
						if (this.tiles_data[id].bitMask === "auto") {
							sourceX = Math.floor(this.terrain.bitMask[j][i]) * this.tile_size;
							sourceY = this.tiles_data[id].line * this.tile_size;
						} else if (this.tiles_data[id].bitMask !== undefined) {
							sourceX = Math.floor(this.tiles_data[id].bitMask % this.terrain.tileset_data.width) * this.tile_size;
							sourceY = Math.floor(this.tiles_data[id].bitMask / this.terrain.tileset_data.width) * this.tile_size;
						}
						if (this.tiles_data[id].bitMask === false) continue;
					}
					this.ctx.drawImage(this.terrain.tileset, sourceX, sourceY, this.tile_size, this.tile_size, positionX, positionY, this.tile_size, this.tile_size);
				}
			}
		}
	}

	//  ______                _   _                 
	// |  ____|              | | (_)                
	// | |__ _   _ _ __   ___| |_ _  ___  _ __  ___ 
	// |  __| | | | '_ \ / __| __| |/ _ \| '_ \/ __|
	// | |  | |_| | | | | (__| |_| | (_) | | | \__ \
	// |_|   \__,_|_| |_|\___|\__|_|\___/|_| |_|___/

	mute() {
		if (this.audio_muted) {
			this.audio_muted = false;
		} else {
			this.audio_muted = true;
		}
		for (const [k, v] of Object.entries(this.ressources.audio)) {
			v.url.muted = this.audio_muted;
		}
	}
	setScale() {
		this.canvas.style.width = this.W * this.scale + "px";
		this.canvas.style.height = this.H * this.scale + "px";
	}
	fullScreen() {
		if (!this.full) {
			this.full = true;
			this.canvas.style.width = "100%";
			this.canvas.style.height = "100%";
		} else {
			this.full = false;
			this.setScale();
		}
	}

	write(text, x, y, align, color) {
		let multiply = color || 0;
		let offset_text = 0;
		if (typeof(align) === "string") {
			switch (align) {
				case "center":
					offset_text = (text.length * this.font.size.x) / 2;
					break;
				case "right":
					offset_text = (text.length * this.font.size.y);
					break;
				default:
					offset_text = 0
			}
			this.writeLine(text, x, y, offset_text, multiply);
		} else {
			// wrap text width
			let y_offset = 0,
				line_height = this.font.size.y + 5;

			let words = text.split(' '),
				line = "";
			for (let i = 0; i < words.length; i++) {
				line += words[i] + " ";
				// check for next word length
				let nextword_width = 0;
				(words[i + 1]) ? nextword_width = words[i + 1].length * this.font.size.x: 0;
				let line_width = line.length * this.font.size.x;
				if (line_width + nextword_width > align) {
					// write this line
					this.writeLine(line, x, y + y_offset, 0, multiply);
					// offset for next line
					y_offset += line_height;
					line = "";
				} else {
					// write last line
					this.writeLine(line, x, y + y_offset, 0, multiply);
				}
			}
		}
	}

	writeLine(text, x, y, offset, color) {
		// write line
		for (let i = 0; i < text.length; i++) {
			let index = this.alphabet.indexOf(text.charAt(i)),
				clipX = this.font.size.x * index,
				posX = (x - offset) + (i * this.font.size.x);
			this.ctx.drawImage(this.font.img, clipX, (color * this.font.size.y), this.font.size.x, this.font.size.y, posX, y, this.font.size.x, this.font.size.y);
		}
	}

	drawBox(x, y, l, h, color) {
		this.ctx.fillStyle = color || "white";
		this.ctx.fillRect(x + 1, y + 1, l - 2, h - 2);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 32, 16, 16, 16, x, y, 16, 16);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 32 + 8, 16, 16, 16, x + l - 16, y, 16, 16);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 32, 16 + 8, 16, 16, x, y + h - 16, 16, 16);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 32 + 8, 16 + 8, 16, 16, x + l - 16, y + h - 16, 16, 16);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 40, 16, 8, 16, x + 16, y, l - 32, 16);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 40, 24, 8, 16, x + 16, y + h - 16, l - 32, 16);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 32, 24, 16, 8, x, y + 16, 16, h - 32);
		this.ctx.drawImage(this.ressources.images[this.cursor].img, 48, 24, 16, 8, x + l - 8, y + 16, 16, h - 32);
	}

	clearCanvas() {
		this.ctx.fillStyle = "#000";
		this.ctx.fillRect(0, 0, this.W, this.H);
	}

	//   _____                        _                       
	//  / ____|                      | |                      
	// | |  __  __ _ _ __ ___   ___  | |     ___   ___  _ __  
	// | | |_ |/ _` | '_ ` _ \ / _ \ | |    / _ \ / _ \| '_ \ 
	// | |__| | (_| | | | | | |  __/ | |___| (_) | (_) | |_) |
	//  \_____|\__,_|_| |_| |_|\___| |______\___/ \___/| .__/ 
	//                                                 | |    
	//                                                 |_|    

	addScene(scene) {
		this.scenes[scene.name] = scene;
	}
	startScene(scene_name) {
		if(this.scenes[scene_name] === undefined){
			console.log("Sorry, This scene doesn't exist")
			return false;
		}
		// request the change of scene if this.main_loop is active
		if (this.main_loop !== undefined) {
			this.requestChange.value = true;
			this.requestChange.action = scene_name;
			return false;
		}
		this.requestChange.value = false;
		this.requestChange.action = "";
		this.FPS.last = Utl.timeStamp();

		this.current_scene = this.scenes[scene_name];
		this.initScene();
		// does this scenes needs a gameloop ?
		if (this.current_scene.loop === true) {
			this.gameLoop();
		} else {
			this.mainRender();
		}

	}
	initScene() {
		this.camera.resetTarget();
		if (!this.current_scene.init_once) {
			this.current_scene.init();
		}
	}
	mainRender() {
		this.clearCanvas();
		this.ctx.save();
		this.ctx.translate(this.camera.pos.x, this.camera.pos.y);
		this.current_scene.render();
		this.ctx.restore();
	}
	mainUpdate(delta) {
		this.current_scene.update(delta);

	}
	loopCheck() {
		if (this.requestChange.value === false) {
			this.main_loop = requestAnimationFrame(() => this.gameLoop());
		} else {
			cancelAnimationFrame(this.main_loop);
			this.main_loop = undefined;
			this.startScene(this.requestChange.action);
		}
	}
	gameLoop() {
		/*
		this.FPS.now = Utl.timeStamp();
		this.FPS.delta += Math.min(1, (this.FPS.now - this.FPS.last) / 1000)
		while (this.FPS.delta > this.FPS.step) {
			this.mainUpdate(this.FPS.step);
			this.mainRender();
			this.FPS.delta -= this.FPS.step;
		}
		this.FPS.last = this.FPS.now;
		this.loopCheck();
		*/
		this.mainUpdate(this.FPS.step);
		this.mainRender();
		this.loopCheck();
	}
};

// Entity
class Entity {
	constructor(scene, x, y) {
		this.scene = scene;
		this.world = this.scene.world;
		this.ctx = this.world.ctx;
		this.size = this.world.tile_size;
		this.half = this.size * 0.5;
		this.pos = new Vector(x, y);
		this.vel = new Vector(0, 0);
		this.friction = new Vector(0.94, 0.94);
		this.bounce = 0.2;
		this.gravity = this.world.gravity;
		this.collision = {
			left: false,
			top: false,
			right: false,
			bottom: false,
		}
	}
	update(delta) {
		this.vel.x += this.world.gravity.x * delta;
		this.vel.y += this.world.gravity.y * delta;
		this.vel.x *= this.friction.x;
		this.vel.y *= this.friction.y;
		this.pos.x += this.vel.x * delta;
		this.pos.y += this.vel.y * delta;
	}
	render() {
		this.world.ctx.fillRect(this.pos.x, this.pos.y, this.size, this.size);
	}
	mapCollision() {

		let tX = this.pos.x + this.vel.x;
		let tY = this.pos.y + this.vel.y;

		let top_left = this.world.getTileCollisionData(tX, tY);
		let top_right = this.world.getTileCollisionData(tX + this.size, tY);
		let bottom_left = this.world.getTileCollisionData(tX, tY + this.size);
		let bottom_right = this.world.getTileCollisionData(tX + this.size, tY + this.size);
		this.collision.left = false;
		this.collision.top = false;
		this.collision.right = false;
		this.collision.bottom = false;
		if (top_left) {
			this.AABB(top_left)
		}
		if (top_right) {
			this.AABB(top_right)
		}
		if (bottom_left) {
			this.AABB(bottom_left)
		}
		if (bottom_right) {
			this.AABB(bottom_right)
		}
	}
	AABB(tile) {
		// Distance from the center of a box
		let distX = Math.abs(this.pos.x - tile.pos.x);
		let distY = Math.abs(this.pos.y - tile.pos.y);
		// Gap between each boxes
		let gapX = distX - this.half - (this.world.tile_size / 2);
		let gapY = distY - this.half - (this.world.tile_size / 2);
		//collision on the X or Y axis
		let offset = this.world.tile_size;
		if (gapX < 0 || gapY < 0) {
			// prevent equality if square
			if (gapX === gapY) {
				gapY = -1;
			}

			if (gapX < 0 && gapX > gapY) {
				if (this.pos.x > tile.pos.x) {
					if (tile.neighbors.right) return false;
					this.vel.x *= -this.bounce;
					this.pos.x -= gapX;
					this.collision.left = true;

				} else {
					if (tile.neighbors.left) return false;
					this.vel.x *= -this.bounce;
					this.pos.x += gapX;
					this.collision.right = true;
				}
			}

			if (gapY < 0 && gapY > gapX) {
				if (this.pos.y > tile.pos.y) {
					if (tile.neighbors.bottom) return false;
					this.vel.y *= -this.bounce;
					this.pos.y -= gapY;
					this.collision.top = true;
				} else {
					if (tile.neighbors.top) return false;
					this.vel.y *= -this.bounce;
					this.pos.y += gapY;
					this.collision.bottom = true;
				}
			}
		}
	}

}


class Taxi extends Entity {
	constructor(scene, x, y, sprite) {
		super(scene, x, y);
		this.angle = 0;
		this.turnSpeed = 0;
		this.thrust = 0;
		this.topSpeed = 0.15;
		this.friction = 0.92;
		this.addhesion = 0.6;
		this.direction = 1;
		// 3D
		this.sprite = this.world.ressources.images[sprite];
		this.frameCourante = 0;
		this.nombreFrame = 0;
		this.sep = this.sprite.img.width / this.sprite.size.x;
		this.halfx = this.sprite.size.x / 2;
		this.halfy = this.sprite.size.y / 2;
	}
	draw() {
		for (let i = 0; i < this.sep; i++) {
			this.ctx.save();
			this.ctx.translate(this.pos.x + this.halfx, (this.pos.y - i) + this.halfy);
			this.ctx.rotate(this.angle);
			this.ctx.drawImage(this.sprite.img, i * this.sprite.size.x, 0, this.sprite.size.x, this.sprite.size.y, -this.halfx, -this.halfy, this.sprite.size.x, this.sprite.size.y);
			this.ctx.restore();
		}
	}

	render() {
		this.draw();
	}
	control() {
		if (this.world.keys[38]) {
			this.thrust = this.topSpeed;
			this.direction = 1;
		} else if (this.world.keys[40]) {
			this.thrust = -this.topSpeed * 0.5;
			this.direction = -1;
		} else {
			this.thrust = 0;
		}

		if (this.world.keys[37]) {
			if (this.turnSpeed > -0.1) {
				this.turnSpeed -= 0.01;
			}
		} else if (this.world.keys[39]) {
			if (this.turnSpeed < 0.1) {
				this.turnSpeed += 0.01;
			}
		}

	}
	checkTile() {
		let tile = this.world.getTileData(
			this.pos.x + this.size / 2, this.pos.y + this.size / 2);
		if (!tile.data) return false;
		if (tile.data.addhesion !== undefined) {
			this.addhesion = tile.data.addhesion;
		}
		if (tile.data.friction !== undefined) {
			this.friction = tile.data.friction;
		}
		// actions
		if (tile.data.action !== undefined) {
			switch (tile.data.action) {
				case "drown":
					this.world.cause_of_death = "Drowned in the water";
					this.scene.handleDeath();
					break;
			}
		}
	}
	update() {
		this.checkTile();

		this.vel.x += Math.cos(this.angle) * this.thrust;
		this.vel.y += Math.sin(this.angle) * this.thrust;

		let speed = this.vel.getLength();

		let turnPercent = Utl.map(speed, 0, 2, 0, 1);
		if (speed > 2) {
			turnPercent = 1
		}
		this.turnSpeed *= this.friction;


		this.angle += this.turnSpeed * turnPercent;

		speed *= this.direction;

		let assisted_directionX = Math.cos(this.angle) * speed,
			assisted_directionY = Math.sin(this.angle) * speed;

		this.vel.x = Utl.lerp(this.vel.x, assisted_directionX, this.addhesion);
		this.vel.y = Utl.lerp(this.vel.y, assisted_directionY, this.addhesion);

		this.vel.x *= this.friction;
		this.vel.y *= this.friction;

		this.pos.x += this.vel.x;
		this.pos.y += this.vel.y;

		// bound

		if (this.pos.x < 0) {
			this.pos.x = 0;
			this.vel.x = 0;
		}

		if (this.pos.x > this.world.W - this.size) {
			this.pos.x = this.world.W - this.size;
			this.vel.x = 0;
		}

		if (this.pos.y > this.world.terrain.reelLimit.y) {
			this.pos.y = this.world.terrain.reelLimit.y;
			this.vel.y = 0;
		}


	}

}

class Sprite {
	constructor(world, sprite) {
		this.world = world;
		this.ctx = this.world.ctx;

		this.sprite = this.world.ressources.images[sprite];
		this.size = this.world.tile_size;
		this.width = this.sprite.size.x;
		this.height = this.sprite.size.y;
		this.frame = 0;
		this.maxFrame = (this.sprite.img.width / this.width);
		this.animation_speed = 0.3;
	}
	setSpeed(speed) {
		this.animation_speed = speed;
	}
	updateFrames() {
		this.frame += this.animation_speed;
		if (this.frame >= this.maxFrame) {
			this.frame = 0;
		}
	}
	render(x, y) {
		this.updateFrames();
		this.ctx.drawImage(this.sprite.img, Math.floor(this.frame) * this.width, 0, this.width, this.height, x, y, this.width, this.height);
	}
}

class Lava {
	constructor(scene, sprite) {
		this.world = scene.world;
		this.W = this.world.W;
		this.scene = scene;
		this.ctx = this.world.ctx;
		this.size = this.world.tile_size;
		this.sprite = new Sprite(this.world, sprite);
		this.number_sprite = Math.floor(this.W / this.size);
		this.sprite.setSpeed(0.06);
	}
	render(x, y) {
		for (let i = 0; i < this.number_sprite; i++) {
			this.sprite.render(x + (i * this.size), y);

		}
	}
}


class Meteor {
	constructor(scene, target) {
		this.world = scene.world;
		this.scene = scene;
		this.entity_array = this.scene.entity_array;
		this.ctx = this.world.ctx;
		this.target = target;
		this.pos = new Vector(
			Utl.random(0, this.world.W),
			Utl.random(target.pos.y - this.world.H, target.pos.y + this.world.H / 2)
		);
		this.m_pos = new Vector(
			this.pos.x, -this.world.H
		);
		this.start_time = new Date();
		this.duration = Utl.random(500, 1000);
		this.start_value = 0;
		this.value = this.start_value;
		this.goal = 1;

		this.explose = false;
		// impact sprite
		this.impact_sprite = new Sprite(this.world, "impact_sprite");
		this.explosion = new Sprite(this.world, "explosion");
		this.explosion.setSpeed(0.4);
		// meteor sprite
		this.meteor_sprite = new Sprite(this.world, "meteor_sprite");

	}
	update() {
		let time = new Date() - this.start_time;
		if (time < this.duration) {
			this.value = Utl.linearTween(time, this.start_value, this.goal - this.start_value, this.duration);
		} else if (!this.explose) {
			this.explose = true;
			if (Utl.distance(this.target.pos, this.pos) < 16) {
				this.world.cause_of_death = "Crushed by a meteor ";
				this.scene.handleDeath();
			}
		}
	}
	render() {
		if (this.explose) {
			if (Math.floor(this.explosion.frame) + 2 > this.explosion.maxFrame) {
				this.complete();
			} else {
				this.explosion.render(this.pos.x - 8, this.pos.y - 8);
			}
		} else {
			this.ctx.fillStyle = "red";
			// zone
			this.ctx.globalAlpha = Utl.map(this.value, 0, 1, 0, 4);
			this.impact_sprite.render(this.pos.x, this.pos.y);
			// Meteor
			let M_x = Utl.lerp(this.m_pos.x, this.pos.x, this.value),
				M_y = Utl.lerp(this.pos.y + this.m_pos.y, this.pos.y, this.value);
			this.ctx.globalAlpha = this.value;
			this.meteor_sprite.render(M_x-4, M_y-8);
			this.ctx.globalAlpha = 1;
		}
		
	}
	complete() {
		if (this.entity_array !== undefined) {
			this.entity_array.splice(this.entity_array.indexOf(this), 1);
		}
	}
}


class Transition {
	constructor(scene, callback, duration, mode) {
		this.scene = scene;
		this.world = this.scene.world;
		this.sfx_array = this.scene.sfx_array;
		this.ctx = this.world.ctx;
		this.callback = callback;
		this.mode = mode;
		this.duration = duration;
		this.speed = 6;
		if (this.mode === "in") {
			this.start_value = (this.world.H / 2) + 20;
			this.goal = 0;
			this.step = -this.speed;
		} else {
			this.start_value = 0;
			this.goal = (this.world.H / 2) + 20;
			this.step = this.speed;
		}
		this.start_time = new Date();
		this.value = this.start_value;

	}
	update() {
		let time = new Date() - this.start_time;
		if (time < this.duration) {
			this.value = Utl.easeInOutQuad(time, this.start_value, this.goal - this.start_value, this.duration);
		} else {
			this.complete();
		}
	}
	render() {
		this.ctx.fillStyle = "black";
		this.ctx.fillRect(0, -this.world.camera.pos.y, this.world.W, this.value);
		this.ctx.fillRect(0, this.world.H - this.world.camera.pos.y, this.world.W, -this.value);
	}
	complete() {
		if (this.sfx_array !== undefined) {
			this.sfx_array.splice(this.sfx_array.indexOf(this), 1);
		}
		this.callback();
	}
}

class Effect {
	constructor(scene, x, y, sprite) {
		this.scene = scene;
		this.world = this.scene.world;
		this.entity_array = this.scene.entity_array;
		this.sprite = new Sprite(this.world, sprite);
		this.pos = new Vector(x, y);
	}
	update() {

	}
	render() {
		if (Math.floor(this.sprite.frame) + 2 > this.sprite.maxFrame) {
			this.delete();
		} else {
			this.sprite.render(this.pos.x - 8, this.pos.y - 16);
		}
	}
	delete() {
		if (this.entity_array !== undefined) {
			this.entity_array.splice(this.entity_array.indexOf(this), 1);
		}
	}
}

// Basic Init file
let parameters = {
    game_name: "Taxi Apocalypse",
    version: 0.1,
    width: 144,
    height: 176,
    start_screen: "menu",
    scale: 2,
    frame_rate: 60,
    // Default 16
    tile_size: 16,
    gravity: {
        x: 0,
        y: 0
    },
    // Custom font and selector
    font: "bitmap_font",
    cursor: "cursor",
    // do i need touch and keys events ?
    event_needs: {
        touch: false,
        keyboard: true
    },
    images: [
        // Important files for the text-display and box system.
        {
            img: "https://image.ibb.co/nx0QyG/bitmap_font.png",
            name: "bitmap_font",
            size: {
                x: 6,
                y: 9
            }
        },
        {
            img: "https://image.ibb.co/iNQQyG/cursor.png",
            name: "cursor"
        },
        // Custom files
        {
            img: "https://image.ibb.co/iTu7Cb/title.png",
            name: "title"
        },
        {
            img: "https://image.ibb.co/mm19Qw/tileset_taxi.png",
            name: "tileset_taxi"
        },
        {
            img: "https://image.ibb.co/eBtpQw/taxi_sprite.png",
            name: "taxi_sprite",
            size: {
                x: 19,
                y: 12
            }
        },
        {
            img: "https://image.ibb.co/bNuw5w/taxi_sprite_broken.png",
            name: "taxi_sprite_broken",
            size: {
                x: 19,
                y: 12
            }
        },
        {
            img: "https://image.ibb.co/fRVsdG/lava_sprite.png",
            name: "lava_sprite",
            size: {
                x: 16,
                y: 16
            }
        },
        {
            img: "https://image.ibb.co/gSSG5w/impact.png",
            name: "impact_sprite",
            size: {
                x: 16,
                y: 16
            }
        },
        {
            img: "https://image.ibb.co/g3mnCb/explosion.png",
            name: "explosion",
            size: {
                x: 32,
                y: 32
            }
        },
        {
            img: "https://image.ibb.co/hR3R8G/meteor_sprite.png",
            name: "meteor_sprite",
            size: {
                x: 25,
                y: 25
            }
        },
    ],
    audio: [{
            url: "https://vocaroo.com/media_command.php?media=s1J5GMshjgOj&command=download_mp3",
            name: "selection"
        },
        {
            url: "https://vocaroo.com/media_command.php?media=s0myGN0GVRDM&command=download_mp3",
            name: "theme"
        },
        {
            url: "https://vocaroo.com/media_command.php?media=s0CgTmndvdI4&command=download_mp3",
            name: "death"
        },
    ],

    tiles: [{
            name: "wall",
            id: 0,
            collision: true,
            bitMask: "auto",
            line: 4
        },
        {
            name: "water",
            id: 1,
            action: "drown",
            bitMask: "auto",
            line: 3
        },
        {
            name: "grass",
            addhesion: 0.1,
            friction: 0.92,
            id: 2
        },
        {
            name: "ice",
            addhesion: 0,
            friction: 0.96,
            id: 3,
            bitMask: "auto",
            line: 1
        },
        {
            name: "sand",
            addhesion: 1,
            friction: 0.85,
            id: 4,
            bitMask: "auto",
            line: 2
        },
    ],
    maps: [

        {
            name: "map_0",
            tileset: "tileset_taxi",
            data: [
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
            ]
        },
        {
            name: "map_1",
            tileset: "tileset_taxi",
            data: [
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [1, 1, 1, 1, 1, 3, 3, 3, 3],
                [1, 1, 1, 1, 1, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 1, 1, 1, 1, 1],
                [3, 3, 3, 3, 1, 1, 1, 1, 1],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3]
            ],
        },
        {
            name: "map_2",
            tileset: "tileset_taxi",
            data: [
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [1, 1, 1, 2, 2, 2, 1, 1, 1],
                [1, 1, 1, 2, 2, 2, 1, 1, 1],
                [3, 3, 3, 2, 2, 2, 4, 4, 4],
                [3, 3, 3, 2, 2, 2, 4, 4, 4],
                [3, 3, 3, 1, 1, 1, 4, 4, 4],
                [3, 3, 1, 1, 1, 1, 1, 4, 4],
                [3, 3, 1, 1, 1, 1, 1, 4, 4],
                [3, 3, 1, 1, 1, 1, 1, 4, 4],
                [3, 3, 3, 1, 1, 1, 4, 4, 4],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2]
            ],
        },
        {
            name: "map_3",
            tileset: "tileset_taxi",
            data: [
                [0, 0, 0, 2, 2, 2, 0, 0, 0],
                [3, 3, 3, 2, 2, 2, 3, 3, 3],
                [3, 3, 3, 2, 2, 2, 3, 3, 3],
                [3, 3, 0, 2, 2, 2, 0, 3, 3],
                [3, 3, 0, 2, 2, 2, 0, 3, 3],
                [3, 3, 0, 2, 2, 2, 0, 3, 3],
                [3, 3, 3, 2, 2, 2, 3, 3, 3],
                [3, 3, 3, 2, 2, 2, 3, 3, 3],
                [3, 3, 3, 2, 2, 2, 3, 3, 3],
                [0, 0, 0, 2, 2, 2, 0, 0, 0],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 0, 0, 0, 0, 0, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2]
            ],
        },
        {
            name: "map_4",
            tileset: "tileset_taxi",
            data: [
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 4, 4, 0, 0, 0, 4, 4, 4],
                [4, 4, 4, 2, 2, 2, 4, 4, 4],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [0, 0, 0, 2, 2, 2, 0, 0, 0],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 0, 0, 0, 0, 0, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2]
            ],
        },
        {
            name: "map_5",
            tileset: "tileset_taxi",
            data: [
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [1, 3, 3, 3, 3, 3, 3, 3, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 1, 3, 3, 3, 3, 3, 1, 1],
                [1, 3, 3, 3, 3, 3, 3, 3, 1],
                [3, 3, 3, 3, 3, 3, 3, 3, 3]
            ],
        },
        {
            name: "map_6",
            tileset: "tileset_taxi",
            data: [
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2],
                [2, 2, 2, 2, 2, 2, 2, 2, 2]
            ],
        },
        {
            name: "map_7",
            tileset: "tileset_taxi",
            data: [
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [0, 0, 3, 3, 3, 3, 3, 0, 0],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3]
            ],
        },

    ],

};

// throw everything in a new Diorama

let game = new Diorama(parameters);


let best_score = 0;

if (localStorage.taxi_apocalypse) {
      best_score = JSON.parse(localStorage.taxi_apocalypse);
    } else {
      // s'il n'y a rien on genere une mémoire
      localStorage.setItem("taxi_apocalypse", JSON.stringify(0));
    }


game.best_score = best_score;

let menu = new Scene(game, "menu");

menu.keyEvents = function(event) {
	if (this.world.keys[38] && this.selection > 0) {
		this.world.audio.play("selection");
		this.selection -= 1;
		this.world.mainRender();
	} else if (this.world.keys[40] && this.selection < this.max) {
		this.world.audio.play("selection");
		this.selection += 1;
		this.world.mainRender();
	}
	if (this.world.keys[88]) {
		this.world.startScene(this.buttons[this.selection].link);
	}
}
menu.init = function() {
	this.init_once = true;
	this.loop = false;
	this.pos = {
		x: this.world.W / 2,
		y: 80
	};
	this.selection = 0;
	this.buttons = [{
		name: "Start",
		link: "inGame"
	}, {
		name: "About",
		link: "about"
	}, {
		name: "How to play ?",
		link: "rules"
	}, ];

	let valeur = [];
	for (var i = 0; i < this.buttons.length; i++) {
		valeur.push(this.buttons[i].name.length);
	}
	this.texteMax = Math.max(...valeur) * 6 + 20;
	this.max = this.buttons.length - 1;
}
menu.render = function() {
	this.world.image.draw("title", 0, 5);
	// score 
	this.world.write("Best Score : " + this.world.best_score + " M", this.world.W / 2, 50, "center", 0);

	this.world.drawBox(this.pos.x - this.texteMax / 2, this.pos.y - 15, this.texteMax, 22 * this.buttons.length, "#50576b");
	this.ctx.fillStyle = "#101e29";
	this.ctx.fillRect((this.pos.x - this.texteMax / 2) + 2, (this.pos.y + (13 * this.selection)) - 2, this.texteMax - 4, 13)
	for (var i = 0; i < this.buttons.length; i++) {
		let color = 2;
		if (this.selection === i) {
			color = 1;
		}
		this.world.write(this.buttons[i].name, this.pos.x, this.pos.y + 13 * i, "center", color);
	}
	this.world.write("Arrow keys to select", this.world.W / 2, this.world.H - 30, "center", 1);
	this.world.write("[x] to confirm", this.world.W / 2, this.world.H - 15, "center", 1);
}
game.addScene(menu);

// in-game.js
let inGame = new Scene(game, "inGame");

inGame.keyEvents = function(event) {
	if (game.keys[69]) {
		game.startScene("menu");
	}
};

inGame.init = function() {
	this.W = this.world.W;
	this.H = this.world.H;
	this.camera = this.world.camera;
	this.world.initMap("map_0");
	this.world.bitMasking();
	this.player = new Taxi(this,
		(this.W / 2) - 8,
		this.world.terrain.reelLimit.y - 40, "taxi_sprite");
	this.player.angle = -Math.PI / 2;
	this.camera.setConstraint(true, false);
	this.camera.setOffset(0, -20);
	this.camera.setTarget(this.player);
	// lava
	this.dist_bottom = 0;
	this.lava_height = 100;
	this.lava = new Lava(this, "lava_sprite");
	// global Variables
	this.world.cause_of_death = "";
	this.world.score = 0;
	// Effects Missile / explosions array
	this.entity_array = [];
	this.number_meteor = 0;
	// logic
	this.isDead = false;
	// Transition array
	this.sfx_array = [];
	let transitionIn_callBack = () => {};
	this.sfx_array.push(new Transition(this, transitionIn_callBack, 500, 'in'));
	this.world.audio.setLoop("theme", true);
	this.world.audio.volume("death", 0.1);
	this.world.audio.volume("theme", 0.1);
	this.world.audio.play("theme");

};

inGame.update = function(delta) {

	//check camera position to delete map section
	if (this.camera.get.bottom() + this.world.tile_size < this.world.terrain.reelLimit.y) {
		this.updateScore();
		this.cutMap();
	}
	if (this.camera.get.top() - this.world.tile_size < 0) {
		this.addMap();
	}
	if (this.sfx_array.length > 0) {
		for (var i = this.sfx_array.length - 1; i >= 0; i--) {
			this.sfx_array[i].update();
		}
	}
	//
	this.camera.update(delta);
	this.meteorLogic();
	if (!this.isDead) {
		this.player.control();
	}
	this.player.update();

	this.player.mapCollision();
	// entity update
	for (var i = this.entity_array.length - 1; i >= 0; i--) {
		this.entity_array[i].update();
	}
	this.dist_bottom = Math.round((this.player.pos.y - this.camera.get.bottom()));
	// lava logic 
	if (Math.round(this.player.vel.y) < 0 && this.dist_bottom < -50) {
		this.lava_height -= this.player.vel.y;
	}
	if (this.lava_height > 100) {
		this.lava_height = 100;
	} else if (this.world.score > 10) {
		this.lava_height -= 1;
	}
	if (this.lava_height < this.dist_bottom) {
		this.world.cause_of_death = "burned by the lava :(";
		this.handleDeath();
	}

};

inGame.render = function() {
	this.world.renderMap();
	this.player.render();
	// render lava
	let y = 0;
	if (this.lava_height < 0) {
		y = this.lava_height;
	}
	this.ctx.fillStyle = "#db362c";
	this.ctx.fillRect(0, this.camera.get.bottom(), this.W, Math.round(y));
	this.lava.render(0, (this.camera.get.bottom() + y) - 16);
	// Entity Render
	for (var i = this.entity_array.length - 1; i >= 0; i--) {
		this.entity_array[i].render();
	}
	this.world.write(this.world.score + " m", this.W / 2, -this.camera.pos.y + 10, "center");

	if (this.sfx_array.length > 0) {
		for (var i = this.sfx_array.length - 1; i >= 0; i--) {
			this.sfx_array[i].render();
		}
	}
};

inGame.meteorLogic = function() {
	if (this.entity_array.length < this.number_meteor) {
		this.entity_array.push(new Meteor(this, this.player));
	}
};

inGame.updateScore = function() {
	this.world.score += 1;
	if (this.world.score < 100) {
		this.number_meteor = Math.ceil(this.world.score * 0.1);
	}
};

inGame.cutMap = function() {
	this.world.terrain.data.splice(this.world.terrain.data.length - 1, 1);
	this.world.setTerrainLimit();
};

inGame.addMap = function() {
	let randomMapName = "map_" + Math.floor(Utl.random(0, this.world.mapsCount)),
		data_array = this.world.maps[randomMapName].data,
		added_height = data_array.length;

	this.world.terrain.data = data_array.concat(this.world.terrain.data);
	this.world.setTerrainLimit();
	// offset camera and player position
	this.camera.pos.y += (added_height * this.world.tile_size);
	this.player.pos.y += (added_height * this.world.tile_size);
	for (var i = 0; i < this.entity_array.length; i++) {
		this.entity_array[i].pos.y += (added_height * this.world.tile_size);
	}
	this.world.bitMasking();
};

inGame.handleDeath = function() {
	if (this.isDead) return false;
	this.world.audio.stop("theme");
	this.world.audio.play("death");
	this.isDead = true;
	this.player.thrust = 0;
	this.player.vel.x = 0;
	this.player.vel.y = 0;
	this.player.sprite = this.world.ressources.images["taxi_sprite_broken"];
	this.entity_array.push(new Effect(this, this.player.pos.x, this.player.pos.y, "explosion"))
	let transitionOut_callBack = () => {
		this.world.startScene("death");
	};
	this.sfx_array.push(new Transition(this, transitionOut_callBack, 1000, 'out'));
}

game.addScene(inGame);



// About
let about = new Scene(game, "about");

about.keyEvents = function(event) {
    if (this.world.keys[69]) {
        this.world.startScene("menu");
    }
}

about.init = function() {
	this.init_once = true;
	this.loop = false;
}

about.render = function() {
    this.world.clearCanvas();
    this.world.write("About", game.W/2, 20,"center");
    let about_text = "Made with Html5 canvas, by Gtibo on Codepen. Thank you for playing :)"
    this.world.write(about_text, 10, 40,this.world.W-20,1);
    this.world.write("Credits", game.W/2, 100,"center");

    this.world.write("Sound: noiseforfun.com", game.W/2, 120,"center",1);
    this.world.write("Theme: Trevor Lentz", game.W/2, 135,"center",1);

    this.world.write("[e] to return to menu", this.world.W/2, this.world.H-20, "center",1);
}

game.addScene(about);

// Rules
let rules = new Scene(game, "rules");

rules.keyEvents = function(event) {
    if (this.world.keys[69]) {
        this.world.startScene("menu");
    }
}

rules.init = function() {
	this.init_once = true;
    this.loop = false;
}

rules.render = function() {
    this.world.clearCanvas();
    this.world.write("Rules", this.world.W/2, 20,"center");
    let text = "Conduct the taxi with the arrow keys, avoid the obstacles and try not being submerged by the lava.";
    this.world.write(text,10,40,this.world.W - 20,1);

    this.world.write("[e] to return to menu", this.world.W/2, this.world.H-20, "center",1);
}
game.addScene(rules);


// Death
let death = new Scene(game, "death");

death.keyEvents = function(event) {
    if (this.world.keys[69]) {
        this.world.startScene("menu");
    }
}

death.init = function() {
	this.init_once = true;
    this.loop = false;
    if(this.world.score > this.world.best_score){
        this.world.best_score = this.world.score;
        localStorage.setItem("taxi_apocalypse", JSON.stringify(this.world.score));
    }
}

death.render = function() {
    this.world.clearCanvas();
    this.world.write("Game over", this.world.W/2, 20,"center");
    let text = "You're dead, " + this.world.cause_of_death;
    this.world.write(text,10,40,this.world.W - 20,1);


    this.world.write("You've traveled", this.world.W/2, 100,"center");
    this.world.write(this.world.score + " meters", this.world.W/2, 120,"center",1);

    this.world.write("[e] to return to menu", this.world.W/2, this.world.H-20, "center",1);
}
game.addScene(death);

game.ready();
game.fullScreen();

              
            
!
999px

Console