Source: entity.mjs

/**
 * @module Entity
 * @version 0.1
 * @author Luca Leon Happel
 * @file Erlaubt es neue Entitäten zu erzeugen, welche bewegt werden
 * und kollidieren können
 */

/**
 * Klasse, welche eine Entität darstellt. Diese Entität kann sich
 * bewegen, hat einen [Sprite]{@link Entity#sprite} und kann
 * [kollidieren]{@link Entity#collides}
 */
export class Entity{
	/**
	 * Erstellt eine Entität
	 *
	 * @param {string} [sprite=''] - URL zu dem Spritesheet der Entität
	 * @param {string} [audio=[]] - Liste von URLs zu den Audioeffekten
	 * @param {number} [x=0] - X-Position der Entität
	 * @param {number} [y=0] - Y-Position der Entität
	 *
	 * @property {Image} sprite - Der momentane Sprite des Entity
	 * @property {Image} audio - Die momentan spielende Audiodatei
	 * @property {string} state - Der Status der Entität
	 */
	constructor(sprite='', audio=[], x=0, y=0){
		this.position = {
			x : x,
			y : y,
		}
		this.velocity = {
			x : 0,
			y : 0,
		}
		this.spritesheet = new Image();
		this.spritesheet.src = sprite // url zum Spritesheet;
		this.spriteatlas = undefined; // Speichert ein Object aus Arrays aus Sprites für jeden state
		this.audiosheet = audio;
		this.audioatlas = undefined; // Speichert ein Objekt aus Arrays aus Audiodateien für jeden state
		this.state = 'idle'; // Stadium in dem sich die Entität befindet
		this.active = true; // true if this entity should be animated and checked for events
		this.time = 0; // time the object has been active
		this.collision = undefined; // speichert alle Orte, an denen die Entität momentan kollidiert
		this.ds = 1; // kleine Änderung der Distanz zu einer Kante der Entität
		this.dt = 14; // veränderung der Position bei einer Änderung der Distanz um ds
		this.effectivleyZero = 0.04; // Grenze, ab welchem Wert eine Zahl praktisch 0 ist
	}
	/**
	 * Gibt die X-Position der Entität an
	 * @return {number} X-Position
	 */
	get x() {
		return this.position.x;
	}
	/**
	 * Setzt die X-Position der Entität
	 */
	set x(nx) {
		this.position.x = nx;
	}
	/**
	 * Gibt die Y-Position der Entität an
	 * @return {number} Y-Position
	 */
	get y() {
		// y-Position of the Entity
		return this.position.y;
	}
	/**
	 * Setzt die Y-Position der Entität
	 */
	set y(ny) {
		// y-Position of the Entity
		this.position.y = ny;
	}
	/**
	 * gibt die X-Geschwindigkeit der Entität an
	 * @return {number} X-Geschwindigkeit
	 */
	get vx() {
		// Wenn die x-Velocity praktisch 0 ist, wird diese auf 0 gesetzt
		if( Math.abs(this.velocity.x) < this.effectivleyZero )
			this.velocity.x = 0;
		return this.velocity.x;
	}
	/**
	 * Setzt die X-Geschwindigkeit der Entität
	 */
	set vx(nvx) {
		this.velocity.x = nvx;
	}
	/**
	 * gibt die Y-Geschwindigkeit der Entität an
	 * @return {number} Y-Geschwindigkeit
	 */
	get vy() {
		// Wenn die y-Velocity praktisch 0 ist, wird diese auf 0 gesetzt
		if( Math.abs(this.velocity.y) < this.effectivleyZero )
			this.velocity.y = 0;
		return this.velocity.y;
	}
	/**
	 * Setzt die Y-Geschwindigkeit der Entität
	 */
	set vy(nvy) {
		// y-Velocity of the Entity
		this.velocity.y = nvy;
	}
	/**
	 * Gibt die Breite der Entität (also des gerade angezeigten Sprites) an
	 * @see {@link Entity#sprite}
	 */
	get width() {
		return this.sprite.width
	}
	/**
	 * Gibt die Höhe der Entität (also des gerade angezeigten Sprites) an
	 * @see {@link Entity#sprite}
	 */
	get height() {
		return this.sprite.height
	}
	/**
	 * Speichere alle Ecken einer Entität.
	 * Also die Punkte an denen der Sprite anfängt/aufhört, wenn
	 * man den Sprite als ein Rechteck betrachtet
	 * @see {@link Entity#sprite}
	 */
	get corners() {
		return {
			topleft     : {
				x: this.x-this.width/2,
				y: this.y-this.height/2,
			},
			topright    : {
				x: this.x+this.width/2,
				y: this.y-this.height/2,
			},
			bottomright : {
				x: this.x+this.width/2,
				y: this.y+this.height/2,
			},
			bottomleft  : {
				x: this.x-this.width/2,
				y: this.y+this.height/2,
			},
		}
	}
	/**
	 * Gibt an, ob die Entität mit irgendetwas kollidiert
	 * @see {@link Entity#collidesTop}
	 * @see {@link Entity#collidesBottom}
	 * @see {@link Entity#collidesLeft}
	 * @see {@link Entity#collidesRight}
	 * @return {boolean} true, wenn Entität kollidiert
	 */
	get collides() {
		if( this.collision==undefined )
			return true
		return Object.entries(this.collision).some(e =>
			!e[0].includes('dx') && !e[0].includes('dy') && e[1] )
	}
	/**
	 * Gibt an, ob die Entität oben mit Etwas Kollidiert
	 * @return {boolean} true, wenn Entität oben gegen etwas kollidiert
	 */
	get collidesTop() {
		return this.collision.topright || this.collision.topright
	}
	/**
	 * Gibt an, ob die Entität unten mit Etwas Kollidiert
	 * @return {boolean} true, wenn Entität unten gegen etwas kollidiert
	 */
	get collidesBottom() {
		return this.collision.bottomright || this.collision.bottomright
	}
	/**
	 * Gibt an, ob die Entität links mit Etwas Kollidiert
	 * @return {boolean} true, wenn Entität links gegen etwas kollidiert
	 */
	get collidesLeft() {
		return this.collision.bottomleft || this.collision.topleft
	}
	/**
	 * Gibt an, ob die Entität rechts mit Etwas Kollidiert
	 * @return {boolean} true, wenn Entität rechts gegen etwas kollidiert
	 */
	get collidesRight() {
		return this.collision.bottomright || this.collision.topright
	}
	/**
	 * true, wenn Entität nach oben steigt und mit dem
	 * oberen Teil gegen etwas kollidiert
	 * @return {boolean} sprints gegen eine Decke
	 */
	get jumpingAgainstCeiling(){
		return this.collidesTop && this.vy < 0
	}
	/**
	 * true, wenn Entiät fällt und mit dem unteren Teil
	 * gegen etwas kollidiert
	 * @return {boolean} fällt auf den Boden
	 */
	get fallingOnFloor() {
		return this.collidesBottom && this.vy > 0
	}
	/**
	 * Gibt an, ob Entität gegen eine linke Wand läuft
	 * @return {boolean} true, wenn Entität links gegen eine Wand läuft
	 */
	get walkingIntoLeftWall() {
		return this.collidesLeft && this.vx < 0
	}
	/**
	 * Gibt an, ob Entität gegen eine rechte Wand läuft
	 * @return {boolean} true, wenn Entität rechts gegen eine Wand läuft
	 */
	get walkingIntoRightWall() {
		return this.collidesRight && this.vx > 0
	}
	/**
	 * Gibt an, ob die Entität irgendwo links hochläft
	 * @return {boolean} true, wenn Entität irgendwo nach links hochgehen möchte
	 */
	get walkingUpLeftSlope() {
		return this.collision.bottomleft && !this.collision.dxbottomleft && this.vx < 0
	}
	/**
	 * Gibt an, ob die Entität irgendwo rechts hochläft
	 * @return {boolean} true, wenn Entität irgendwo nach rechts hochgehen möchte
	 */
	get walkingUpRightSlope() {
		return this.collision.bottomright && !this.collision.dxbottomright && this.vx > 0
	}
	/**
	 * Gibt an, ob die Entität irgendwo links runterläuft
	 * @return {boolean} true, wenn Entität irgendwo nach links runterläuft
	 */
	get fallingDownLeftSlope() {
		return this.collision.bottomleft && !this.collision.dybottomleft && this.vy > 0
	}
	/**
	 * Gibt an, ob die Entität irgendwo rechts runterläuft
	 * @return {boolean} true, wenn Entität irgendwo nach rechts runterläuft
	 */
	get fallingDownRightSlope() {
		return this.collision.bottomright && !this.collision.dybottomright && this.vy > 0
	}
	/**
	 * Gibt den momentan verwendeten Sprite aus dem Spritesheet an an
	 * @return {image} der Sprite, der gerade verwendet wird
	 * @see {@link Entity#spritesheet}
	 */

	/**
	 * Weitere Bedingungen, die erfüllt sein müssen
	 * @callback condition
	 * @returns {boolean}
	 */
	/**
	 * Bewegt die Entität in Richtung [x,y], bis diese auf
	 * der Oberfläche ist
	 * @param {number} [x=0] - X-Richtung
	 * @param {number} [y=1] - Y-Richtung
	 * @param {condition} [condition=_=>true] - Weitere Sachen, die erfüllt sein müssen, damit weiter zur Oberfläche gegangen wird
	 */
	moveToSurvace(x=0,y=1, condition=_=>true) {
		// move the entity up the slope, until it is in the air
		while(this.collides && condition()){
			this.x += x;
			this.y += y;
			this.computeCollision(level);
		}
		// undo the last movement, so the entity is on the
		// highest point of the slope
		this.x -= x;
		this.y -= y;
		this.computeCollision(level);
	}
	/**
	 * gibt den gerade spielenden Sprite wieder
	 * dies muss für jeden Entity einzeln festgelegt werden
	 */
	get sprite() {
		console.warn('Kein benutzerdefinierter Sprite. Bitte erstelle eine eigene Spritefunktion!');
		return this.spritesheet
	}
	/**
	 * Gibt die gerade spielenden Audiodatei wieder
	 * dies muss für jeden Entity einzeln festgelegt werden
	 */
	get audio() {}
	/**
	 * Gibt an, ob alle Daten für die Entität geladen wurden.
	 * Erst dann kann die Entität auch von der Kamera gezeichnet werden
	 * @return {boolean} true, wenn alles geladen wurde, sonst false
	 */
	get loaded() {
		if( this.spriteatlas == undefined )
			this.computeSpriteatlas();
		if( this.audioatlas == undefined )
			this.computeAudioatlas();
		return this.spritesheet.complete
			&& this.spriteatlas != undefined
			&& this.audioatlas != undefined
	}
	/**
	 * Updated die Entität. Dazu gehört:
	 * timer höhersetzen
	 * tastatureingabe verarbeiten
	 * Wenn schon geladen, dann auch physik und kollisionen berechnen
	 * @param {Level} level - Das Level in dem sich die Entität befindet
	 * @fires Entity#keyboard
	 * @fires Entity#computeCollision
	 * @fires Entity#physics
	 */
	update(level) {
		// Erhöhe den Zeit-Counter
		this.time++;
		// Prüfe nach Tastatureingaben
		this.keyboard();
		if( level.loaded ){
			this.computeCollision(level);
			this.physics(level);
		}
	}
	/**
	 * führt Aktionen aus, je nachdem ob eine Taste gedrückt wurde
	 * dies muss für jeden Entity einzeln festgelegt werden
	 */
	keyboard() {
		return
	}
	/**
	 * Berechnet den Spriteatlas aus dem Spritesheet.
	 * Also werden die Bilder im Spritesheet zurechtgeschnitten,
	 * sodass man die Sprites einzeln verwenden kann
	 * muss für jede Entität selber definiert werden
	 */
	computeSpriteatlas(){
		this.spriteatlas = {};
	}
	/**
	 * Berechnet den Audioatlas aus dem Audiosheet.
	 * Der Audiosheet ist der Parameter "audio" des Generators dieses
	 * Objektes.
	 * Mittels dieser Methode wird jedem State ein Array aus
	 * Audiodateien zugeordnet, welche mittels der URLs vom
	 * Audioarray geladen werden.
	 * Diese URLs werden hier in Audiodateien Umgewandelt und in
	 * this.audioatlas gespeichert.
	 */
	computeAudioatlas(){
		this.audioatlas = {}
	}
	/**
	 * Updated this.collision, sodass neue Kollisionen nachweisbar sind
	 * Dabei wird this.corners verwendet und jeder Kante zugeordnet, ob
	 * diese kollidiert. Zusätzich werden noch Kanten mit dx oder dy
	 * vorne im Namen zusätzlich zu this.collision hinzugefügt, welche
	 * die normalen Kanten nach kollisionen überprüfen, wobei diese
	 * ein kleines bisschen verschoben wurden
	 * @param {Level} level - das Level in dem nach Kollisionen geprüft wird
	 * @param {number} [ds=this.ds] - die Größe, die ein kleiner Schritt ist
	 * @param {number} [dt=this.dt] - Veränderung in Y richtung, die eine ds Änderung in X hat, bzw Veränderung in X Richtung, die eine ds Änderung in Y hat
	 */
	computeCollision(level, ds=this.ds, dt=this.dt) {
		let dxcorners = {
			dxtopleft     : {
				x: this.corners.topleft.x-ds, // ←
				y: this.corners.topleft.y+dt, // ↓
			},
			dxtopright    : {
				x: this.corners.topright.x+ds, // →
				y: this.corners.topright.y+dt, // ↓
			},
			dxbottomright : {
				x: this.corners.bottomright.x+ds, // →
				y: this.corners.bottomright.y-dt, // ↑
			},
			dxbottomleft  : {
				x: this.corners.bottomleft.x-ds, // ←
				y: this.corners.bottomleft.y-dt, // ↑
			},
		}
		let dycorners = {
			dytopleft     : {
				x: this.corners.topleft.x+dt, // →
				y: this.corners.topleft.y-ds, // ↑
			},
			dytopright    : {
				x: this.corners.topright.x-dt, // ←
				y: this.corners.topright.y-ds, // ↑
			},
			dybottomright : {
				x: this.corners.bottomright.x-dt, // ←
				y: this.corners.bottomright.y+ds, // ↓
			},
			dybottomleft  : {
				x: this.corners.bottomleft.x+dt, // →
				y: this.corners.bottomleft.y+ds, // ↓
			},
		}
		let positionsToCompute = {...this.corners, ...dxcorners, ...dycorners};
		this.collision = Object.fromEntries(Object.entries(positionsToCompute)
			.map(c => [
				c[0],
				level.checkCollision(c[1].x,c[1].y),
			])
		)
	}
	/**
	 * Führt alles aus, was mit Physik zu tuen hat
	 */
	physics(level) {
		// Wende Gravitation auf Entität an
		this.vx += level.gravity.x;
		this.vy += level.gravity.y;
		// Stillstand bei kollision
		if( this.collides ){
			if( this.jumpingAgainstCeiling || this.fallingOnFloor )
				if( this.fallingDownLeftSlope || this.fallingDownRightSlope ){
					this.vx *= 1.01
					this.vy += 0.1
				}else
					this.vy = 0;
			if( this.walkingIntoLeftWall || this.walkingIntoRightWall )
				if( this.walkingUpLeftSlope || this.walkingUpRightSlope ){
					this.moveToSurvace(0,-1, _=>!this.collidesTop || this.collidesBottom);
					// decrease the speed, because the entity needs
					// to walk up
					this.vx *= 0.8
				}else
					this.vx = 0
		}
		// Wende die Beschläunigung auf die Position an
		this.x += this.vx;
		this.y += this.vy;
	}
}