Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // 3 types of Assets
- const ASSET_TYPE = {
- IMAGE : 0x000001,
- SPRITE : 0x000002,
- AUDIO : 0x000003
- };
- // 7 types of game state
- const GAME_STATE = {
- SETUP : 0x000000,
- WIN : 0x000001,
- GAME_OVER : 0x000002,
- CHOICE : 0x000003,
- DIALOG : 0x000004,
- ATTACK_PLAYER : 0x000005,
- ATTACK_ENTITY : 0x000006,
- };
- // 2 types of draw mode
- const SPRITE_DRAW = {
- AUTO_INDEX : 0x1, // draw sprite automatically with index table (spriteSheet where sprite are all same dimension, need width and height opt)
- AUTO_NAME : 0x2 // draw sprite automatically with name table (spriteSheet where sprite are NOT all same dimension, need sprites opt)
- };
- // 4 types of UI Buttons
- const UI_BUTTON = {
- FIGHT : 0x1,
- ACTION : 0x2,
- ITEM : 0x3,
- MERCY : 0x4
- };
- // 3 State when Fight
- const FIGHT_STATE = {
- FIGHTBAR_MOVE : 0x1,
- ANIMATION_ATTACK : 0x2,
- SHOW_DAMAGE : 0x3
- };
- // 3 STATE OF LIFE
- const LIFE_STATE = {
- ALIVE : 0x1,
- DYING : 0x2,
- DIE : 0x3
- };
- // Add ProtoType
- CanvasRenderingContext2D.prototype.fillTextM = function(text, x, y) {
- var lineHeight = this.measureText("M").width * 1.2;
- var lines = text.split('\n');
- for (var i = 0; i < lines.length; ++i) {
- this.fillText(lines[i], x, y); y += lineHeight*1.8;
- }
- }
- CanvasRenderingContext2D.prototype.roundRect = function(x, y, ww, hh, r) {
- if (ww < 2 * r) r = ww / 2;
- if (hh < 2 * r) r = hh / 2;
- this.beginPath();
- this.moveTo(x+r, y);
- this.arcTo(x+ww, y, x+ww, y+hh, r);
- this.arcTo(x+ww, y+hh, x, y+hh, r);
- this.arcTo(x, y+hh, x, y, r);
- this.arcTo(x, y, x+ww, y, r);
- this.closePath();
- this.fill();
- }
- var Utils = {
- getRandomIntInclusive: function(min, max) {
- min = Math.ceil(min);
- max = Math.floor(max);
- return Math.floor(Math.random() * (max - min +1)) + min;
- }
- };
- class Asset {
- constructor(type,path) {
- // le type de l'asset : image, son, ...etc...
- this.type = type;
- // le chemin vers le fichier (exemple: son/exemple.wav ou image/sprite.png ...etc..)
- this.path = path;
- // permet de savoir si notre asset est chargé ou pas (de base l'asset n'est pas chargé)
- this.loaded = false;
- }
- /**
- * isLoad: renvoie true si l'asset est chargé, sinon renvoie false
- */
- isLoad() {
- return this.loaded;
- }
- /**
- * load: on charge l'asset
- */
- load() {
- // Cette classe est une classe de base, on laisse les classes enfants définir cette méthode (elles vont overwrite cette méthode)
- return new Promise(function(resolve, reject) {resolve();});
- }
- /**
- * destroy: on détruit l'asset (ce qui peut soulager la mémoire)
- */
- destroy() {} // même remarque que pour la méthode load
- }
- class AudioAsset extends Asset {
- constructor(...args) {
- // nouvelle façon (avec le ES6) d'envoyer tout les arguments vers le constructeur de la classe parent (ici la classe Asset)
- // on est obligé d'appeler le constructeur avec super (sinon ça renvoie une erreur)
- super(...args);
- // on créé notre élément audio (on ne l'ajoute pas au dom, ce n'est pas nécessaire)
- this.audio = document.createElement('audio');
- }
- /**
- * load: on charge le son
- */
- load() {
- // on stocke le scope actuelle (le this correspond à l'objet qui a été créé avec new)
- var _this = this;
- // j'expliquerai plus tard comme l'utiliser, mais en gros, ça ce comporte comme un callback
- return new Promise(function(resolve, reject) {
- // on load le son
- _this.audio.src = _this.path;
- _this.loaded = true;
- _this.audio.load();
- // on appelle le "callback"
- resolve();
- });
- }
- /**
- * destroy: on détruit l'asset (ce qui peut soulager la mémoire)
- */
- destroy() {
- // on stop correctement le son
- this.audio.pause();
- this.audio.currentTime = 0;
- // on détruit l'objet
- this.audio = null;
- // du coup, l'asset n'est plus charger
- this.loaded = false;
- }
- }
- class ImageAsset extends Asset {
- constructor(...args) {
- super(...args);
- // on créé simplement un objet Image
- this.image = new Image();
- }
- /**
- * load: on charge l'image
- */
- load() {
- var _this = this;
- return new Promise(function(resolve, reject) {
- // on utilise l'event onload pour savoir quand l'image a fini de se charger
- _this.image.onload = function() {
- _this.loaded = true; // l'image a finit de se charger
- // on appele le "callback"
- resolve();
- };
- // s'il y a une erreur lors du chargement (l'image n'existe plus, ...etc...)
- _this.image.onerror = function() {
- _this.loaded = false; // on rédfinit loaded à false pour être sûr qu'elle soit bien à false
- // (imagine que c'est la 2ème fois qu'on la charge et que la 1ère fois, il n'y avais pas d'erreur)
- // on appelle la fonction qui gère les erreurs
- reject();
- };
- // on commence a charger l'image
- _this.image.src = _this.path;
- });
- }
- /**
- * destroy: on détruit l'asset (ce qui peut soulager la mémoire)
- */
- destroy() {
- // on détruit simplement l'image
- this.image = null;
- // du coup, l'asset n'est plus charger
- this.loaded = false;
- }
- }
- class SpriteAsset extends ImageAsset {
- constructor(...args) {
- super(...args);
- // on essai de récupérer les options qui sont dans le dernier argument (on vérifie bien que le dernier argument est bien un objet)
- var opt = args.length > 0 ? typeof args[args.length-1] === 'object' ? args[args.length-1] : {} : {};
- // ces paramètres sont uniquement pour les spritesheets avec tous les sprites à la même taille
- this.width = typeof opt.width === 'number' ? opt.width : 1; // on définit la largeur d'une "sous-image"
- this.height = typeof opt.height === 'number' ? opt.height : 1; // on définit la hauteur d'une "sous-image"
- // on définit le temps minimal avant de passer à la prochaine image dans le cas d'une animation
- this.tick = typeof opt.tick === 'number' ? opt.tick : 1000/30; // par défaut 30FPS
- // ces paramètres sont uniquement pour les spritesheets avec des sprites de tailles différentes
- // ils sont stockés dans un tableau comme ceci :
- // [{
- // name: "sous-image-1"
- // x: 0, y: 0, w: 50, h: 50
- // },{....
- // x, y, w, h : c'est pour localiser la "sous-image" dans l'image
- this.sprites = typeof opt.sprites !== 'undefined' ? opt.sprites : [];
- this.spritesobj = {};
- for (var i = 0; i < this.sprites.length; i++) {
- this.spritesobj[this.sprites[i].name] = this.sprites[i]; // on stocke ça dans un objet pour faciliter l'accès
- }
- // contient des index (dans le cas où les "sous-images" sont de même taille),
- // ou les noms des "sous-images" (dans le cas où les "sous-images" sont de tailles différentes)
- this.animation = typeof opt.animation === 'undefined' ? [] : opt.animation;
- this.animationIndex = 0; // index qui nous servira de repère dans le tableau animation
- // variable "temporaire" qui rend possible l'animation
- this.time = Date.now();
- }
- /**
- * getPosition: on récupère les positions x et y de la "sous-image" seulement avec son index
- * (uniquement à utiliser dans le cas où tous les sprites sont de même tailles, ce qui est le cas la pluspart du temps)
- * @param index {int} : l'indice de la "sous-image"
- * @return {object} : on retourne la position en x et y de la "sous-image"
- */
- getPosition(index) {
- return {
- x:(index%(Math.ceil(this.image.width/this.width)))*this.width,
- y:Math.floor(index/(Math.ceil(this.image.width/this.width)))*this.height
- }
- }
- /**
- * getMaxIndex: on récupère l'indice maximale à partir de la taille des "sous-images" et de l'image
- * (uniquement à utiliser dans le cas où tous les sprites sont de même tailles, ce qui est le cas la pluspart du temps)
- * @return {int} : on retourne l'indice maximale
- */
- getMaxIndex() {
- return Math.ceil(this.image.width/this.width) * Math.ceil(this.image.height/this.height) - 1;
- }
- /**
- * drawSpriteByIndex: on dessine la n-ème "sous-image" seulement avec l'indice
- * (uniquement à utiliser dans le cas où tous les sprites sont de même tailles, ce qui est le cas la pluspart du temps)
- * (ce qui automatise l'opération, on est quitte de chercher et de mettre nous-même les coordonnées)
- * @param ctx {CanvasRenderingContext2D} : le context 2D du canvas
- * @param index {int} : l'indice de la "sous-image"
- * @params ...args {array} : x, y, width, height : endroit où on dessine l'image
- */
- drawSpriteByIndex(ctx,index,...args) {
- var point = this.getPosition(index); // on récupère les coordonées à partir de l'indice
- ctx.drawImage(this.image, point.x, point.y, this.width, this.height, ...args); // on dessine la "sous-image"
- }
- /**
- * getSprite: on récupère simplement la "sous-image" à parti de son nom
- * (uniquement à utiliser dans le cas où tous les sprites sont de tailles différentes)
- * @param name {string} : nom de la "sous-image"
- * @return {object} : on retourne l'objet associé
- */
- getSprite(name) {
- return this.spritesobj[name];
- }
- /**
- * drawSprite: on dessine la "sous-image" définit par son nom
- * (uniquement à utiliser dans le cas où tous les sprites sont de tailles différentes)
- * (ce qui automatise l'opération, on est quitte de chercher et de mettre nous-même les coordonnées)
- * @param ctx {CanvasRenderingContext2D} : le context 2D du canvas
- * @param name {string} : le nom de la "sous-image"
- * @params ...args {array} : x, y, width, height : endroit où on dessine l'image
- */
- drawSprite(ctx,name,...args) {
- ctx.drawImage(this.image, this.spritesobj[name].x, this.spritesobj[name].y, this.spritesobj[name].w, this.spritesobj[name].h, ...args);
- }
- /**
- * draw: on dessine et on anime la "sous-image" en précisant le mode avec les indices ou avec les noms
- * cette fonction permet d'animer automatiquement un spritesheet
- * @param ctx {CanvasRenderingContext2D} : le context 2D du canvas
- * @param mode {SPRITE_DRAW} : le mode utilisé (avec les indices ou avec les noms)
- * @params ...args {array} : x, y, width, height : endroit où on dessine l'image
- */
- draw(mode,ctx,...args) {
- // this.animation est censé contenir les indices ou les noms pour pouvoir animer
- // (on dessine la "sous-image" 'image-1' 'image-2' ...etc...., or si c'est vide, j'ai décidé arbitrairement qu'on ne dessine rien)
- if (this.animation.length == 0 && mode == SPRITE_DRAW.AUTO_NAME) return;
- // si on est là, c'est que le tableau this.animation contient les noms des "sous-images" à dessiner à la suite
- // sinon elle est vide et on est dans le mode SPRITE_DRAW.AUTO_INDEX
- // donc si le tableau est vide et qu'on est dans le mode SPRITE_DRAW.AUTO_INDEX, je dessine les "sous-images"
- // dans l'ordre croissant 0,1,2,3....
- // si le tableau est vide, j'utilise les indices dans l'odre croissant
- // sinon je suis les indices (ou les noms) du tableau et this.animationIndex devient un indice pour itérer dans le tableau
- var index = this.animation.length == 0 ? this.animationIndex : this.animation[this.animationIndex];
- // suivant le mode, on utilise la fonction adapté
- switch (mode) {
- case SPRITE_DRAW.AUTO_INDEX:
- this.drawSpriteByIndex(ctx,index,...args);
- break;
- case SPRITE_DRAW.AUTO_NAME:
- this.drawSprite(ctx,index,...args);
- break;
- default:
- }
- // on anime
- if (Date.now() - this.time > this.tick) {
- this.time = Date.now(); // tu connais déjà ce principe :)
- // on incrémente l'indice pour dessiner la prochaine "sous-image"
- this.animationIndex++;
- // si le tableau est vide, on est dans le mode SPRITE_DRAW.AUTO_INDEX, donc l'animation est fini quand on arrive au dernier indice (this.getMaxIndex())
- if (this.animation.length == 0 && (this.getMaxIndex() == this.animationIndex - 1)) {
- this.animationIndex = 0; // on recommence l'animation
- // on regarde si notre indice correspond à la longueur du tableau, au quel cas, on recommence l'animation
- } else if (this.animationIndex == this.animation.length) {
- this.animationIndex = 0; // on recommence l'animation
- }
- }
- }
- /**
- * isAnimationEnded: permet de savoir si l'animation est à la dernière "sous-image"
- * @return {boolean} : on renvoie true si l'animation est terminé, sinon en renvoie false
- */
- isAnimationEnded() {
- return (this.animation.length == 0 && (this.getMaxIndex() - 1 == this.animationIndex)) || (this.animationIndex == this.animation.length - 1);
- }
- }
- class AssetManager {
- constructor(game) {
- this.game = game; // l'objet qui contiendra tout
- this.assets = {}; // toutes les assets seront stockés ici
- this.countLoadedSuccess = 0; // le nombre d'assets chargés avec succès
- this.countLoadedError = 0; // le nombre d'assets non-chargés
- this.countLoadedTotal = 0; // le nombre total d'assets chargés
- this.totalToLoaded = 0; // le nombre total d'assets à charger
- }
- /**
- * loadAll: permet de charger toutes les assets
- * @param next {function} : le callback qui sera appelé une fois le chargement de toutes les assets terminés
- */
- loadAll(next) {
- var _this = this;
- // on réinitialise les variables
- this.countLoadedSuccess = 0; // le nombre d'assets chargés avec succès
- this.countLoadedError = 0; // le nombre d'assets non-chargés
- this.countLoadedTotal = 0; // le nombre total d'assets chargés
- // on itère sur toutes les assets
- for (var name in this.assets) {
- if (this.assets.hasOwnProperty(name)) {
- // on charge l'asset
- this.assets[name].load()
- // l'interêt de la promise est ici, si le chargement est un succès,
- // alors on appelle la fonction resolve (de la promise) qui nous envoie dans le "then"
- .then(function(){
- // load success
- _this.countLoadedSuccess++;
- })
- // sinon, c'est la fonction reject (de la promise) qui est appelé, et on arrive dans le catch
- .catch(function(e){
- // load fail
- _this.countLoadedError++;
- console.warn(e);
- })
- // dans tout les cas, que ce soit un succès ou pas, on exécute le finally
- .finally(function(){
- // finally
- _this.countLoadedTotal++;
- if (_this.countLoadedTotal==_this.totalToLoaded) {
- next(); // si tout est chargé on appelle le callback
- }
- });
- }
- }
- }
- /**
- * addAsset: permet d'ajouter un asset
- * @param name {string} : le nom de l'asset
- * @param type {ASSET_TYPE} : le type de l'asset
- * @params ...args {array} : le reste des arguments
- */
- addAsset(name,type,...args) {
- // on créé automatiquement l'asset adéquat selon le type
- switch (type) {
- case ASSET_TYPE.IMAGE:
- this.assets[name] = new ImageAsset(type,...args);
- break;
- case ASSET_TYPE.AUDIO:
- this.assets[name] = new AudioAsset(type,...args);
- break;
- case ASSET_TYPE.SPRITE:
- this.assets[name] = new SpriteAsset(type,...args);
- break;
- default:
- this.assets[name] = new Asset(type,...args);
- }
- // on incrémente le nombre d'assets
- this.totalToLoaded++;
- }
- /**
- * removeAsset: permet de supprimer un asset
- * @param name {string} : le nom de l'asset
- */
- removeAsset(name) {
- // on désincrémente le nombre d'assets
- this.totalToLoaded--;
- // si l'asset a bien été chargé, on désincrémente this.countLoadedSuccess sinon this.countLoadedError
- if (this.assets[name].isLoad()) {
- this.countLoadedSuccess--;
- } else {
- this.countLoadedError--;
- }
- // on détruit l'asset proprement
- this.assets[name].destroy();
- // on l'enlève de l'objet
- this.assets[name] = null;
- delete this.assets[name];
- }
- /**
- * getAsset: permet de récupérer un asset
- * @param name {string} : le nom de l'asset
- */
- getAsset(name) {
- return this.assets[name];
- }
- }
- // Undertale UI Like
- class UI {
- /**
- * constructor
- * @param game {Game} : un instancier de la classe Game
- * @return {UI}
- */
- constructor(game) {
- this.game = game;
- this.assetManager = this.game.assetManager;
- this.canvas = this.game.canvas;
- this.ctx = this.game.ctx;
- // on stockera ici l'asset de l'UI
- this.spriteSheet = null;
- // l'indice
- this.currentChoice = 0;
- // le nombre de boutons
- this.countButtons = 4;
- // le temps que dure le moment où le coeur du joueur se retrouve tout seul sur le fond noir (sans être briser)
- this.gameOverKeepHeartDuration = 1200;
- this.gameOverKeepHeartTime = Date.now();
- // le temps que dure le moment où le coeur se brise
- this.gameOverKeepBreakHeartDuration = 500;
- this.gameOverKeepBreakHeartTime = Date.now();
- // on stockera ici les différentes étapes de l'affiche du game over
- this.gameOverState = 0x0;
- }
- /**
- * reset: permet de reset l'UI
- */
- reset() {
- this.gameOverState = 0x0;
- this.currentChoice = 0;
- }
- /**
- * gameOverPrepare: permet de préparer le game over
- */
- gameOverPrepare() {
- // on passe à l'état 0x1 (j'aurais pu utiliser une énumération pour ça)
- this.gameOverKeepHeartTime = Date.now();
- this.gameOverKeepBreakHeartTime = Date.now();
- this.gameOverState = 0x1;
- }
- /**
- * init: permet d'initialiser l'UI
- */
- init() {
- // on récupère simplement l'asset
- this.spriteSheet = this.assetManager.getAsset('interface.sprite');
- }
- /**
- * draw: permet de dessiner l'UI
- */
- draw() {
- // pour l'instant, on dessine les boutons
- this.drawChoiceButton();
- }
- /**
- * disable: permet de désactiver le choix du menu
- */
- disable() {
- this.currentChoice = 0;
- }
- /**
- * enable: permet d'activer le choix du menu
- * @param keyCodeToSelect {array} : contient la touche qui permet de naviguer vers la gauche, et la touche qui permet de naviguer vers la droite
- * @param keyCodeToValidate {array} : contient les touches qui permettent de valider le choix
- * @param next {function} : callback qui sera appelé une fois le choix fait
- */
- enable(keyCodeToSelect,keyCodeToValidate,next) {
- // par défaut, on sélectionne le 1er bouton
- this.currentChoice = 1;
- // on stocke le scope "this"
- var _this = this;
- // on prépare la création d'un event local (un event qui ne s'exécute qu'une seule fois)
- var localevent;
- localevent = function(event) {
- // si on arrive ici, c'est que l'utilisateur a appuyé sur une touche
- // on regarde si le joueur a appuyé sur une touche permettant de valider le choix
- for (var i = 0; i < keyCodeToValidate.length; i++) {
- if (keyCodeToValidate[i] == event.keyCode) {
- // si on arrive là, c'est que le joueur a validé son choix
- // on supprime toutes les touches
- for (var j = 0; j < keyCodeToValidate.length; j++) {
- _this.game.keyState[keyCodeToValidate[j]] = false;
- delete _this.game.keyState[keyCodeToValidate[j]];
- }
- // on appelle le callback
- // si ce callback renvoie true, on supprime cette event, sinon on fait rien
- if (next(_this.currentChoice)) {
- document.removeEventListener('keydown', localevent);
- }
- // on arrete la fonction ici
- return;
- }
- }
- // on regarde si l'utilisateur tente de naviguer dans le menu
- for (var i = 0; i < keyCodeToSelect.length; i++) {
- if (keyCodeToSelect[i] == event.keyCode) {
- // si on arrive là, c'est que l'utilisateur appuie sur des bouttons permettant de naviguer dans le menu
- // comme on a que 2 bouttons dans le tableau, on définit arbitrairement que le 1er c'est pour aller à gauche
- // et le 2ème pour aller à droite
- if (i==0) {
- _this.currentChoice--; // on va vers la gauche
- } else {
- _this.currentChoice++; // on va vers la droite
- }
- // si on va trop à gauche, on revient tout à droite, si on va trop à droite, on revient tout à gauche (sinon on fait rien)
- _this.currentChoice = _this.currentChoice < 1 ? _this.countButtons : _this.currentChoice > _this.countButtons ? 1 : _this.currentChoice;
- }
- }
- };
- // on ajoute l'event
- document.addEventListener('keydown', localevent);
- }
- /**
- * drawChoiceButton: permet de dessiner les bouttons
- */
- drawChoiceButton() {
- // on récupère le canvas et le contexte
- var canvas = this.canvas;
- var ctx = this.ctx;
- // j'ai littéralement copier/coller ton code ici :D
- var a = '#FF7E24';
- var b = '#FF7E24';
- var c = '#FF7E24';
- var d = '#FF7E24';
- if (this.currentChoice == 1) a = '#FFFF00';
- if (this.currentChoice == 2) b = '#FFFF00';
- if (this.currentChoice == 3) c = '#FFFF00';
- if (this.currentChoice == 4) d = '#FFFF00';
- ctx.lineWidth = 1;
- ctx.strokeStyle = a;
- ctx.strokeRect(40, 400, 100, 40);
- ctx.strokeStyle = b;
- ctx.strokeRect(180, 400, 100, 40);
- ctx.strokeStyle = c;
- ctx.strokeRect(320, 400, 100, 40);
- ctx.strokeStyle = d;
- ctx.strokeRect(460, 400, 100, 40);
- ctx.font = '23px Verdana';
- ctx.textAlign = 'right';
- ctx.fillStyle = a;
- ctx.fillText('FIGHT', 136, 429);
- ctx.fillStyle = b;
- ctx.fillText('ACT', 262, 429);
- ctx.fillStyle = c;
- ctx.fillText('ITEM', 410, 429);
- ctx.font = '22px Verdana';
- ctx.fillStyle = d;
- ctx.fillText('MERCY', 557, 429);
- // là, on utilise les méthodes des assets (je te laisse faire des aller-retour pour comprendre ce qu'il se passe)
- // ne tkt pas, on fait la classe Player juste après :)
- if (this.currentChoice == 1) { this.game.player.drawHeart(52, 420); } else { this.spriteSheet.drawSprite(ctx, 'fight', 45, 405, 15, 28); }
- if (this.currentChoice == 2) { this.game.player.drawHeart(199, 420); } else { this.spriteSheet.drawSprite(ctx, 'act', 192, 412, 15, 28); }
- if (this.currentChoice == 3) { this.game.player.drawHeart(337, 420); } else { this.spriteSheet.drawSprite(ctx, 'item', 330, 408, 15, 28); }
- if (this.currentChoice == 4) { this.game.player.drawHeart(471, 420); } else { this.spriteSheet.drawSprite(ctx, 'mercy', 463, 410, 15, 28); }
- }
- }
- class Area {
- constructor(game) {
- this.game = game;
- this.canvas = this.game.canvas;
- this.ctx = this.game.ctx;
- this.reset();
- }
- /**
- * reset: on reset la zone
- */
- reset() {
- this.x = 0;
- this.y = 0;
- this.w = 520;
- this.h = 130;
- this.xm = 0;
- this.ym = 0;
- this.wm = 0;
- this.hm = 0;
- this.rw = 0;
- this.rh = 0;
- // on simplifie l'utilisation d'affichage du message de status
- this.statusMessageText = '';
- this.statusMessageDisplay = true;
- this.playerHeartControl = false; // si c'est true, on dessine le coeur du joueur et on active les controls clavier
- this.resizenext = null; // callback appelé une fois que la zone a fini de se redimensionner
- this.resetFightBar(); // on reset la "fight bar"
- }
- /**
- * resize: permet de redimensionner la zone
- * @param w {int} : la nouvelle longueur
- * @param h {int} : la nouvelle hauteur
- * @param next {function} : cette fonction sera appelée une fois que la zone aura fini de se redimensionner
- */
- resize(w,h,next) {
- this.rw = w;
- this.rh = h;
- this.resizenext = next;
- }
- /**
- * changeStatusMessage: permet de changer le message status
- * @param text {string} : le nouveau status
- */
- changeStatusMessage(text) {
- this.statusMessageText = text;
- }
- /**
- * showStatusMessage: on affiche le message status
- */
- showStatusMessage() {
- this.statusMessageDisplay = true;
- }
- /**
- * hideStatusMessage: on cache le message status
- */
- hideStatusMessage() {
- this.statusMessageDisplay = false;
- }
- /**
- * draw: on dessine la zone
- */
- draw() {
- // on récupère le contexte 2D du canvas
- var ctx = this.ctx;
- // on dessine le rectangle
- ctx.strokeStyle = 'rgb(255,255,255)';
- ctx.lineWidth = 2;
- ctx.strokeRect(this.x, this.y, this.w, this.h);
- // si le message status est "activé", on le dessine
- if (this.statusMessageDisplay) {
- ctx.font = '14px KulminoituvaRegular';
- ctx.fillStyle = 'rgb(255,255,255)';
- ctx.textAlign = 'left';
- ctx.fillTextM(this.statusMessageText, 50, 265);
- }
- // on propage l'event
- if (typeof this.ondraw === 'function') this.ondraw();
- // si on aactivé le control du coeur du joueur
- if (this.playerHeartControl) {
- var s = ctx.globalAlpha; // on sauvegarde cette valeur
- // si le joueur est invulnerable, c'est qu'il s'est pris des dégats, on le rend transparent temporairement
- if (!this.game.player.isVulnerable()) {
- ctx.globalAlpha = 0.4;
- }
- // on dessine le coeur du joueur
- this.game.player.drawHeart(this.game.player.x+this.x,this.game.player.y+this.y);
- // on réinitialise le globalAlpha
- if (!this.game.player.isVulnerable()) {
- ctx.globalAlpha = s;
- }
- // on fais bouger le coeur du joueur
- this.game.player.update();
- }
- }
- /**
- * resetFightBar: permet de reset la barre de fight
- */
- resetFightBar() {
- this.fightBar = {
- speed: 6,
- x: 0,
- y: 0,
- hit: false,
- miss: false,
- state: FIGHT_STATE.FIGHTBAR_MOVE, // l'état de la barre
- time: Date.now(),
- tick: 125
- };
- }
- /**
- * enablePlayer: permet d'activer les controles du coeur du joueur
- */
- enablePlayer() {
- this.playerHeartControl = true;
- // on replace le coeur du joueur au centre
- this.game.player.x = this.w/2;
- this.game.player.y = this.h/2;
- }
- /**
- * enablePlayer: permet de désactiver les controles du coeur du joueur
- */
- disablePlayer() {
- this.playerHeartControl = false;
- }
- /**
- * update: permet de mettre à jour la zone
- */
- update() {
- // comme c'est toi qui à fait ce code, je n'ai pas besoin de l'expliquer :)
- // pour éviter de rendre le tout trop compliquer d'un coup, j'ai pas cherché à l'optimiser
- var y = 0;
- this.x = 300-this.w/2;
- this.y = 300-this.h/2 + y;
- this.xm = 300-this.rw/2;
- this.ym = 300-this.rh/2 + y;
- this.wm = this.rw;
- this.hm = this.rh;
- var v = 10;
- for (var i = 0; i < v; i++) {
- if (this.xm < this.x) this.x -= 1;
- if (this.xm > this.x) this.x += 1;
- if (this.ym < this.y) this.y -= 1;
- if (this.ym > this.y) this.y += 1;
- if (this.wm < this.w) this.w -= 1;
- if (this.wm > this.w) this.w += 1;
- if (this.hm < this.h) this.h -= 1;
- if (this.hm > this.h) this.h += 1;
- }
- // si la zone est redimensionner correctement, on appelle le callback this.resizenext s'il existe
- if (this.xm == this.x && this.ym == this.y && this.wm == this.w && this.hm == this.h) {
- if (typeof this.resizenext === 'function') {
- this.resizenext();
- this.resizenext = null;
- }
- }
- }
- }
- class Player {
- /**
- * constructor
- * @param game {Game} : un instancier de la classe Game
- * @param name {string} : le nom du joueur
- * @return {Player}
- */
- constructor(game,name) {
- this.game = game;
- this.assetManager = this.game.assetManager;
- this.canvas = this.game.canvas;
- this.ctx = this.game.ctx;
- // on stocke ici l'asset du coeur
- this.heartAsset = null;
- // on stocke les différents assets d'attaques (si jamais on veut une animation différente d'attaque)
- this.attackAssets = {};
- // dégat de l'attaque lorsque le joueur vise à une précision de 100%
- this.attack = 15;
- // servira pour la position du coeur et la taille du coeur
- this.x = 0;
- this.y = 0;
- this.w = 13;
- this.h = 13;
- // la vitesse du coeur
- this.speed = 2;
- // le nom du joueur
- this.name = name || 'Frisk';
- // son niveau
- this.lvl = 1;
- // sa vie
- this.health = 42;
- // sa vie maximale
- this.healthMax = 42;
- // le temps d'invulnerabilité après avoir reçu des dégats
- this.notTakeDamageDuration = 500; // 0.5 secondes
- this.notTakeDamageTime = Date.now();
- // on stocke ici si le joueur est en vie ou pas
- this.isDead = false;
- }
- /**
- * reset: permet de reset le joueur
- */
- reset() {
- // on reset sa vie
- this.health = this.healthMax;
- // on le remet à la vie
- this.isDead = false;
- // on pourrait tout aussi bien reset son niveau mais je n'ais pas implémenté cette partie, et je laisserai le faire :)
- }
- /**
- * addAttack: permet d'ajouter un asset d'attaque avec le nom de l'asset
- * @param name {string} : le nom de l'asset
- */
- addAttack(name) {
- // on récupère simplement l'asset
- this.attackAssets[name] = this.assetManager.getAsset(name);
- }
- /**
- * drawAttack: permet de dessiner (en animant) une attaque et appelle le callback next une fois l'animation terminée
- * @param name {string} : nom de l'attaque (correspond aussi au nom de l'asset)
- * @param mode {SPRITE_DRAW} : mode de dessin
- * @param next {function} : callback qui sera appelé après que l'animation soit terminée
- */
- drawAttack(name,mode,next,...args) {
- // on anime automatiquement l'asset attaque
- this.attackAssets[name].draw(mode,this.ctx,...args);
- // on regarde si elle est finie
- if (this.attackAssets[name].isAnimationEnded()) {
- // si oui, on appelle le callback
- next();
- }
- }
- /**
- * getRandomAttack: renvoie le nom d'une attaque de manière aléatoire
- * @return {string} : on retourne le nom de l'attaque
- */
- getRandomAttack() {
- var index = Utils.getRandomIntInclusive(0,Object.keys(this.attackAssets).length-1);
- return Object.keys(this.attackAssets)[index]; // on récupère le nom des clés (ça renvoie un tableau des noms), et on a plus qu'à récuper le nom avec l'indice choisi au hasard
- }
- /**
- * drawState: permet de dessiner l'état du joueur
- * on dessine sa barre de vie, son nom, ..etc..
- */
- drawState() {
- var ctx = this.ctx; // on récupère le contexte 2D
- // comme tu peux le voir j'ai simplement copier/coller ton code
- ctx.font = '12px KulminoituvaRegular';
- ctx.fillStyle = 'rgb(255,255,255)';
- ctx.textAlign = 'left';
- ctx.fillText(this.name, 40, 388);
- ctx.fillText('LV ' + this.lvl, 150, 388);
- ctx.fillText(this.health + ' / ' + this.healthMax, 280 + this.healthMax, 388);
- ctx.font = '10px KulminoituvaRegular';
- ctx.fillText('HP', 238, 387);
- ctx.fillStyle = 'rgb(255,0,0)';
- ctx.fillRect(258,376,this.healthMax,13);
- ctx.fillStyle = 'rgb(255,255,0)';
- ctx.fillRect(258,376,this.health,13);
- }
- /**
- * init: permet d'initialiser l'objet (elle est appelée une seule fois lorsque le jeu s'initialise)
- */
- init() {
- // on récupère simplement l'asset du coeur
- this.heartAsset = this.assetManager.getAsset('heart');
- }
- /**
- * isAlive: permet de savoir si le joueur est en vie
- * @return {boolean} : renvoie true si joueur est en vie sinon false
- */
- isAlive() {
- return !this.isDead;
- }
- /**
- * takeDamage: permet de faire prendre des dégats au joueur
- * @param damage {int} : les dégats
- */
- takeDamage(damage) {
- // si le joueur est invulnerable, on fait rien
- if (Date.now() - this.notTakeDamageTime <= this.notTakeDamageDuration) return;
- // sinon on soustrait les dégats à sa vie
- this.health -= damage;
- // si sa vie est égale ou plus petit que zéro, on considère le joueur comme mort (logique mdr)
- if (this.health <= 0) {
- this.health = 0; // on remet sa vie bien à 0
- this.isDead = true; // on notifie que le joueur est bien mort
- this.game.changeState(GAME_STATE.GAME_OVER); // on change l'état du jeu et on le met à l'état "GAME_OVER" pour afficher le screen game over
- }
- }
- /**
- * drawHeart: permet de dessiner le coeur du joueur
- * @param x {int} : position x du coeur
- * @param y {int} : position y du coeur
- * @param w {int} : largeur du coeur
- * @param h {int} : hauteur du coeur
- * @return {object} : on retourne l'objet {x:x,y:y,w:w,h:h}
- */
- drawHeart(x,y,w,h) {
- var ctx = this.ctx; // on récupère le Contexte 2D
- // si les paramètres x, y, w, ou h sont indéfinis, on les remplace par des valeurs "par défaut"
- // par conséquent, on peut très bien appeler cette méthode sans aucun arguments comme ceci : player.drawHeart()
- x = typeof x !== 'undefined' ? x : this.x;
- y = typeof y !== 'undefined' ? y : this.y;
- w = typeof w !== 'undefined' ? w : this.w;
- h = typeof h !== 'undefined' ? h : this.h;
- // comme le coeur est simplement une image, on a pas besoin de l'animer
- ctx.drawImage(this.heartAsset.image, x-w/2, y-h/2, w, h);
- // on retourne les valeurs utiliser pour dessiner le coeur
- return {x:x,y:y,w:w,h:h};
- }
- /**
- * isInArea: permet de tester si un rectangle est bien dans strictement dans la zone
- * @param x {int} : position x du rectangle
- * @param y {int} : position y du rectangle
- * @param w {int} : largeur du rectangle
- * @param h {int} : hauteur du rectangle
- * @return {boolean} : on retourne true si le rectangle est dans la zone, sinon on retourne fasle
- */
- isInArea(x,y,w,h) {
- w = typeof w !== 'undefined' ? w : this.w;
- h = typeof h !== 'undefined' ? h : this.h;
- var area = this.game.area;
- return x-w/2 > 0 && x+w/2 < area.w && y-h/2 > 0 && y+h/2 < area.h;
- }
- /**
- * move: permet de déplacer le coeur du joueur de point.x en x et de point.y en y
- * @param point {object} : point qui est sous la fome {x:...,y:...}
- */
- move(point) {
- // si le coeur reste dans la zone, on déplace le coeur sinon on fait rien
- if (this.isInArea(this.x+point.x,this.y+point.y)) {
- this.x += point.x;
- this.y += point.y;
- }
- }
- /**
- * isVulnerable: permet de savoir si le joueur est vulnerable
- * @return {boolean} : on renvoie true si le joueur est vulnerable sinon on renvoie false
- */
- isVulnerable() {
- return !this.invulnerable;
- }
- /**
- * checkHitArea: permet de savoir si le coeur est rentré en collision avec l'attaque de l'ennemi
- * @param attack {Attack} : un instancier de la classe Attack
- */
- checkHitArea(attack) {
- // si le joueur est invulnerable, on fait rien
- if (Date.now() - this.notTakeDamageTime <= this.notTakeDamageDuration) return;
- // si on arrive ici, alors le joueur n'est plus invulnerable
- this.invulnerable = false;
- // on récupère ce qui nous intéresse
- var ctx = this.game.ctx;
- var canvas = this.game.canvas;
- var area = this.game.area;
- // on récupère la zone du coeur (il faut faire attention à inverser le scale et le translate)
- var pixel = ctx.getImageData(this.game.pointTranslate.x+(this.x+area.x+1-(this.w-2)/2)*this.game.pointScale.x, this.game.pointTranslate.y+(this.y+area.y+1-(this.h-2)/2)*this.game.pointScale.y, (this.w-2)*this.game.retrieveData('e'), (this.h-2)*this.game.retrieveData('e'));
- // on teste s'il y a un pixel blanc dans cette zone
- for (var i = 0; i < pixel.data.length; i+=4) {
- if (pixel.data[i] > 200 && pixel.data[i+1] > 200 && pixel.data[i+2] > 200) {
- // si oui, on applique les dégats au joueur
- this.takeDamage(attack.damage);
- // on "active" l'invulnerabilité
- this.notTakeDamageTime = Date.now();
- // on notifie le fait que le joueur soit invulnerable
- this.invulnerable = true;
- }
- }
- }
- /**
- * update: permet d'update le joueur
- * en particulier, on déplace le coeur à l'appui des touches
- */
- update() {
- // LEFT
- if (this.game.isKeyDown(37) || this.game.isKeyDown(81)) {
- this.move({x:-this.speed,y:0});
- }
- // RIGHT
- if (this.game.isKeyDown(39) || this.game.isKeyDown(68)) {
- this.move({x:this.speed,y:0});
- }
- // UP
- if (this.game.isKeyDown(38) || this.game.isKeyDown(90)) {
- this.move({x:0,y:-this.speed});
- }
- // DOWN
- if (this.game.isKeyDown(40) || this.game.isKeyDown(83)) {
- this.move({x:0,y:this.speed});
- }
- }
- }
- class Text {
- constructor(text,tick,keyCodeSkip) {
- this.text = text;
- this.tick = tick;
- this.keyCodeSkip = keyCodeSkip || [];
- }
- }
- // Undertale Dialog Like
- class Dialog {
- /**
- * constructor
- * @param textObject {Text} : l'objet Text créer avec new Text()
- * @param next {function} : callback qui sera appelé une fois que le dialogue sera fini
- */
- constructor(textObject,next) {
- // notre objet Text
- this.textObject = textObject;
- // le texte qui sera affiché
- this.displayText = '';
- // on stocke ici l'indice
- this.index = 0;
- // on stocke le callback
- this.next = next || function(){};
- // tu sais à quoi sert cette variable :)
- this.time = Date.now();
- // une variable qui indiquera si le dialogue est fini ou pas
- this.ended = false;
- }
- /**
- * end: permet de terminer le dialogue (par exemple, à l'appui d'une touche, on appelle cette fonction et ça termine le dialogue)
- */
- end() {
- this.displayText = this.textObject.text; // on affiche le texte finale
- this.ended = true; // le dialogue est fini
- if (typeof this.next === 'function') this.next(); // on appelle le callback
- }
- /**
- * update: permet de mettre à jour le dialogue
- * @param game {Game} : un instancier de la classe Game
- */
- update(game) {
- // si le dialogue est fini, on a plus rien a mettre à jour
- if (this.ended) return;
- // à l'appui d'une touche, on termine le dialogue
- if (this.textObject.keyCodeSkip.length > 0) {
- if (game.isKeyDown(this.textObject.keyCodeSkip)) {
- this.end(); // force the dialog to end
- }
- }
- // on "anime" l'écriture du dialogue tous les "this.textObject.tick" millisecondes
- if (Date.now() - this.time > this.textObject.tick) {
- this.time = Date.now();
- this.displayText += this.textObject.text[this.index];
- this.index++;
- if (this.index == this.textObject.text.length) {
- this.ended = true;
- if (typeof this.next === 'function') this.next();
- }
- }
- }
- /**
- * draw: permet de dessiner le dialogue
- * @param ctx {CanvasRenderingContext2D} : le Contexte 2D du canvas
- */
- draw(ctx) {
- ctx.fillStyle = 'rgb(255,255,255)';
- ctx.beginPath();
- ctx.moveTo(416, 85);
- ctx.lineTo(380, 95);
- ctx.lineTo(416, 105);
- ctx.fill();
- ctx.fillStyle = 'rgb(255,255,255)';
- ctx.roundRect(415,50,155,80,13);
- ctx.font = '9px KulminoituvaRegular';
- ctx.fillStyle = 'rgb(0,0,0)';
- ctx.textAlign = 'left';
- ctx.fillTextM(this.displayText, 423, 69);
- }
- }
- // Undertale Attack Like
- class Attack {
- constructor(data) {
- // si aucun argument n'est passé, data sera indéfiné, donc si on fait data['quelque_chose'] le script renverra une erreur,
- // pour empêcher cette erreur, on remplace data par {} donc le cas où data n'est pas défini
- data = data || {};
- // data.spriteSheets est un tableau qui contient le nom des assets
- this.spriteSheetsData = data.spriteSheets || [];
- // on organise ça dans un objet pour faciliter l'accès
- this.spriteSheets = {};
- for (var i = 0; i < this.spriteSheetsData.length; i++) {
- this.spriteSheets[this.spriteSheetsData[i]] = null;
- }
- // les dégats de l'attaque
- this.damage = typeof data.damage !== 'undefined' ? data.damage : 4;
- // tous les "events"
- this.oncreate = typeof data.oncreate === 'function' ? data.oncreate : function(){};
- this.onstart = typeof data.onstart === 'function' ? data.onstart.bind({parent:this,caller:'onstart'}) : function(){};
- this.onend = typeof data.onend === 'function' ? data.onend.bind({parent:this,caller:'onend'}) : function(){};
- this.ondraw = typeof data.ondraw === 'function' ? data.ondraw.bind({parent:this,caller:'ondraw'}) : function(){};
- this.onupdate = typeof data.onupdate === 'function' ? data.onupdate : function(){};
- // l'état local de l'attaque
- this.state = 'onstart';
- // on stocke les données temporaires de l'état ici
- this.stateData = {};
- // on propose à l'utilisateur de mettre ces variables dedans (ce qui est conseillé pour éviter de réécrire des variables existantes)
- this.data = {};
- }
- /**
- * reset: permet de reset l'attaque
- */
- reset() {
- this.state = 'onstart'; // on revient à l'état initiale de l'attaque
- this.stateData = {};
- this.oncreate();
- }
- /**
- * init: permet d'initialiser l'attaque
- * @param game {Game} : un instancier de la classe Game
- */
- init(game) {
- // on stocke les variables dont on aura besoin
- this.game = game;
- this.assetManager = this.game.assetManager;
- this.area = this.game.area;
- this.player = this.game.player;
- // on "reset" l'attaque
- this.oncreate();
- }
- /**
- * run: permet de faire "tourner l'attaque", cette fonction est exécuté à 60FPS
- */
- run() {
- // on récupère le scope actuel, le "this" correspond à notre objet attaque (construit avec new Attack())
- var _this = this;
- // on ecécute la fonction "onstart", "ondraw", ou "onend" (à 60FPS)
- this[this.state](function(__this){
- // ici le "__this", correspond au scope lorsque qu'on ".bind" les 3 fonctions,
- // donc le "__this" contient ceci : {parent:_this,caller:...} (le "_this" est le même que celui juste au-dessus)
- // ainsi, comme leur variable "caller" porte leur nom, on est en mesure de savoir quelle fonction est appelée
- // l'objectif ici, c'est de faire en sorte que cette fonction ne soit appelé seulement une fois
- // (je rappele qu'on est dans une fonction qui est exécuté une centaine de fois par seconde)
- // si "_this.stateData[__this.caller]" est défini, c'est que la fonction a déjà été appelé à l'état "__this.caller"
- if (typeof _this.stateData[__this.caller] !== 'undefined') return;
- // si on arrive là, c'est la fonction next n'a jamais été appelé à cet état de l'attaque, donc on fais en sorte,
- // que ce soit le 1er appelle mais aussi surtout le dernier appelle
- _this.stateData[__this.caller] = true;
- // on passe à la fonction suivante
- if (_this.state == 'onstart') {
- _this.state = 'ondraw';
- } else if (_this.state == 'ondraw') {
- _this.state = 'onend';
- } else if (_this.state == 'onend') {
- _this.reset(); // reset attack
- }
- });
- }
- draw() {
- if (typeof this.ondraw === 'function') this.ondraw();
- }
- update() {
- if (typeof this.onupdate === 'function') this.onupdate();
- }
- }
- // Undertale Entity Like
- class Entity {
- /**
- * constructor : permet de construire un ennemi
- * @param data {object}
- * @return {Entity}
- */
- constructor(data) {
- // si aucun argument n'est passé, data sera indéfiné, donc si on fait data['quelque_chose'] le script renverra une erreur,
- // pour empêcher cette erreur, on remplace data par {} donc le cas où data n'est pas défini
- data = data || {};
- // la vie de l'entité
- this.health = data.health || 0;
- // la vie maximale que l'entité peut avoir
- this.healthMax = data.healthMax || this.health;
- // les différents sprites de l'entité (un sprite où il est en vie, un sprite où il est en colère, un sprite où il est mort ... etc ...)
- this.spriteSheetsData = data.spriteSheets || [];
- // on stocke ça dans un objet pour faciliter l'accès
- this.spriteSheets = {};
- for (var i = 0; i < this.spriteSheetsData.length; i++) {
- this.spriteSheets[this.spriteSheetsData[i]] = null;
- }
- // le nom de l'entité (ne tkt pas, il y a une méthode setName pour définir le nom)
- this.name = '';
- // quelques events
- // quand l'entité est créé (lors du reset du jeu et au début du jeu)
- this.oncreate = typeof data.oncreate === 'function' ? data.oncreate : function(){};
- // lorsque l'entité choisi une attaque, cette méthode est appelé, on a juste à retourner une attaque
- // par défaut, on renvoie une attaque de manière aléatoire
- this.onselectattack = typeof data.onselectattack === 'function' ? data.onselectattack : function(){
- var index = Utils.getRandomIntInclusive(0,Object.keys(this.attacks).length-1);
- return this.attacks[Object.keys(this.attacks)[index]];
- };
- // lorsque l'entité est dessinée, cette méthode est appelé
- this.ondraw = typeof data.ondraw === 'function' ? data.ondraw : function(){};
- // lorsque l'entité est mis à jour cette méthode est appelée
- this.onupdate = typeof data.onupdate === 'function' ? data.onupdate : function(){};
- // lorsque l'état global du jeu change, cette méthode est appelée
- this.onchangestate = typeof data.onchangestate === 'function' ? data.onchangestate : function(){};
- // on stocke toutes les autres données ici (ça évite de réécrire une variable déjà existente)
- this.data = {};
- // on stocke toutes les attaques ici (ce sont des instancier de la classe Attack)
- this.attacks = {};
- // on reset les variables lié à l'animation de l'entité lorsqu'elle se prend des dégats du joueur
- this.resetDamageVariable();
- // l'état se sa vie
- this.lifeState = LIFE_STATE.ALIVE;
- // son opacité
- this.opacity = 1;
- // on stocke ici l'objet dialogue (un instancier de la classe Dialog)
- this.dialog = null;
- // on créé l'entité
- this.oncreate();
- }
- /**
- * reset: on reste l'entité
- */
- reset() {
- // on reset sa vie
- this.health = this.healthMax;
- // on le remet en vie
- this.lifeState = LIFE_STATE.ALIVE;
- // on "détruit" l'objet dialogue s'il y en a un
- this.dialog = null;
- // on propage l'event reset aux attaques pour reset toutes les attaques
- for (var i in this.attacks) {
- if (this.attacks.hasOwnProperty(i)) {
- this.attacks[i].reset();
- }
- }
- // on reset les variables lié à l'animation de l'entité lorsqu'elle se prend des dégats du joueur
- this.resetDamageVariable();
- // on recréé l'entité
- this.oncreate();
- }
- /**
- * resetDamageVariable: permet de reset les variables lié à l'animation de l'entité lorsqu'elle se prend des dégats du joueur
- */
- resetDamageVariable() {
- this.damageTime = Date.now();
- this.damageShake = 0; // si cette variable vaut 1, alors l'entité se déplace à gauche, si cette variable vaut 0, alors l'entité se déplace à droite
- this.damageTransition = 0;
- this.damageLock = false;
- this.damageX = 0; // on fait "trembler" l'entité, on aura besoin d'une variable temporaire pour le déplacement en x
- this.opacity = 1;
- }
- /**
- * setName: permet de définir le nom de l'entité
- * @param name {string} : le nom de l'entité
- */
- setName(name) {
- this.name = name;
- }
- /**
- * getName: permet de récupérer le nom de l'entité
- * @return {string} : retourne le nom de l'entité
- */
- getName() {
- return this.name;
- }
- /**
- * init: permet d'initialiser l'entité (exécuter une seule fois lorsque le jeu s'initialise)
- * @param game {Game} : un instancier de la classe Game
- */
- init(game) {
- // on stocke uniquement ce qui nous intéresse
- this.game = game;
- this.assetManager = this.game.assetManager;
- this.canvas = this.game.canvas;
- this.ctx = this.game.ctx;
- // on récupère tous les assets
- for (var i = 0; i < this.spriteSheetsData.length; i++) {
- this.spriteSheets[this.spriteSheetsData[i]] = this.assetManager.getAsset(this.spriteSheetsData[i]);
- }
- // on récupère toutes les attaques (instancier de la classe Attack)
- // et on propage l'initialisation
- for (var name in this.attacks) {
- if (this.attacks.hasOwnProperty(name)) {
- this.attacks[name] = this.game.getAttack(name);
- this.attacks[name].init(this.game);
- }
- }
- }
- /**
- * isAlive: permet de savoir si l'entité est en vie
- */
- isAlive() {
- return this.lifeState == LIFE_STATE.ALIVE;
- }
- /**
- * addAttack: permet d'ajouter une attaque à l'entité
- * j'ai choisi arbitrairement le fait qu'il faut ajouter les attaques avant d'initialiser l'entité
- */
- addAttack(name) {
- this.attacks[name] = null;
- }
- /**
- * removeAttack: permet d'enlver une attaque
- */
- removeAttack(name) {
- // on supprime simplement l'objet
- this.attacks[name] = null;
- delete this.attacks[name];
- }
- /**
- * selectAttack: permet de sélectionner une attaque
- * @params ...args : correspond à tous les arguments
- */
- selectAttack(...args) {
- // on propage l'event et on propage aussi les arguments
- var attack = this.onselectattack(...args);
- // si l'objet retourné n'est pas un instancier de la class Attack, on fait planter le script avec une erreur pour éviter des bugs
- if (!(attack instanceof Attack)) throw 'Thre returned value of "onselectattack" must be a "new Attack()" Object';
- // si on arrive ici, c'est que l'objet est bien un instancier de la classe Attack, dans ce cas on retourne l'attaque sélectionée
- return attack;
- }
- /**
- * takeDamage: permet d'appliquer les dégâts reçu sur la vie de l'entité
- * @param damage {int} : des dégâts
- * @param next {function} : ce callback sera appelé une fois que l'animation de se prendre les dégâts sera finie
- */
- takeDamage(damage,next) {
- // on stocke le scope actuel
- var _this = this;
- var ctx = this.game.ctx;
- // on change l'opacité de l'entité
- this.opacity = 0.8;
- // on fait "trembler" l'entité
- if (Date.now() - this.damageTime > 50) {
- this.damageTime = Date.now();
- if (this.damageShake == 0) {
- this.damageShake = 1;
- this.damageX -= 10;
- } else {
- this.damageShake = 0;
- this.damageX += 10;
- }
- }
- // j'ai simplement repris ton code ici
- ctx.font = '20px KulminoituvaRegular';
- ctx.fillStyle = 'rgb(255,0,0)';
- ctx.textAlign = 'center';
- ctx.fillText(damage, 380 + this.healthMax/2, 99);
- ctx.fillStyle = 'rgb(255,0,0)';
- ctx.fillRect(380,60,this.healthMax,10);
- ctx.fillStyle = 'rgb(0,255,0)';
- ctx.fillRect(380,60,this.health-this.damageTransition,10);
- // si this.damageTransition n'est pas égale à damage, alors l'entité n'a pas fini de se prendre les dégâts, on continue
- if (this.damageTransition < damage) {
- this.damageTransition++;
- } else if (!this.damageLock) {
- // comme cette méthode est exécuté une centaine de fois par seconde, si on bloque pas la fonction, il va se prendre 1 million de dégâts mdr
- this.damageLock = true; // donc on bloque
- // on attend un tout petit pour laisser le temps au joueur de bien voir la barre de vie de l'entité
- setTimeout(function () {
- // on enlève les dégâts à la vie
- _this.health -= damage;
- if (_this.health <= 0) {
- _this.health = 0;
- // pour faire en sorte que l'entité dise ces derniers mots, on met l'entité dans un état "entrain de mourrir"
- _this.lifeState = LIFE_STATE.DYING;
- }
- // on reset toutes les variables liés à l'animation des dégâts
- _this.resetDamageVariable();
- // on appelle le callback pour dire que l'animation où l'entité prend des dégâts est finie
- next();
- }, 600);
- }
- }
- /**
- * waitKeyDown: permet d'attendre l'appui d'une touche (attention, ceci ne doit pas être exécuté à 60FPS)
- * @param keyCodeSkip {array} : tableau des touches
- */
- waitKeyDown(keyCodeSkip) {
- // on stocke le scope this
- var _this = this;
- _this.waitKeyDownEventFn = function(event) {
- // si on arrive ici, c'est que l'utilisateur a appuyé sur une touche
- // on vérifie s'il a appuyé sur la touche attendu
- for (var i = 0; i < keyCodeSkip.length; i++) {
- if (keyCodeSkip[i] == event.keyCode) {
- // si oui, on supprime cet event
- document.removeEventListener('keydown', _this.waitKeyDownEventFn);
- // on "réinitialise" les touches
- for (var j = 0; j < keyCodeSkip.length; j++) {
- _this.game.keyState[keyCodeSkip[j]] = false;
- delete _this.game.keyState[keyCodeSkip[j]];
- }
- // on appelle la fonction qui se trouve dans le .then
- _this.resolvetmp();
- break;
- }
- }
- };
- // on pourra faire un .then
- return new Promise(function(resolve, reject) {
- _this.resolvetmp = resolve; // on stocke la fonction du .then ici
- // on démarre l'event
- document.addEventListener('keydown', _this.waitKeyDownEventFn);
- });
- }
- /**
- * draw: permet de dessiner l'entité
- */
- draw() {
- // dans un 1er temps, on propage l'event
- if (typeof this.ondraw === 'function') this.ondraw();
- // et on dessine le dialogue s'il y en a un
- if (this.dialog != null) {
- this.dialog.update(this.game);
- this.dialog.draw(this.game.ctx);
- }
- }
- /**
- * update: permet d'update l'entité
- */
- update() {
- // on propage simplement l'event
- if (typeof this.onupdate === 'function') this.onupdate();
- }
- /**
- * destroyDialog: permet de détruire le dialogue
- */
- destroyDialog() {
- this.dialog = null;
- }
- /**
- * displayDialog: permet d'afficher un dialogue
- * @param textObject {Text} : instancier de la classe Text
- * @param next {function} : callback qui sera exécuté une fois que le dialogue sera fini
- */
- displayDialog(textObject,next) {
- // si le dialogue existe déjà, on fait rien
- if (this.dialog != null) {
- return;
- }
- // on créé le dialogue
- this.dialog = new Dialog(textObject,next);
- }
- }
- class Game {
- constructor() {
- // on créé le canvas
- this.canvas = document.createElement('canvas');
- // on met le background en noir
- this.canvas.style.background = '#000000';
- // on récupère le contexte 2D
- this.ctx = this.canvas.getContext('2d');
- // contiendra toutes sortes de données
- this.data = {};
- // contiendra l'état des touches
- this.keyState = {};
- // contiendra tous les ennemies
- this.ennemies = {};
- // contiendra toutes les attaques
- this.attacks = {};
- // variable temporaire pour mieux gérer les transitons (j'expliquerai ça un peu plus tard)
- this.transitions = {};
- this.transitionstext = {};
- // on stock simplement le translate et le scale
- this.pointTranslate = {
- x: 0,
- y: 0
- };
- this.pointScale = {
- x: 1,
- y: 1
- };
- // on stocke ici l'ennemy courant
- this.currentEnnemy = null;
- // l'état global du jeu
- this.state = null;
- // l'asset manager
- this.assetManager = new AssetManager(this);
- // la zone où seront dessinés les attaques et le coeur du joueur
- this.area = new Area(this);
- // le joueur
- this.player = new Player(this);
- // l'UI qui gérera les boutons fight, act, item et mercy
- this.UI = new UI(this);
- // on ajoute enfin le canvas dans le DOM
- document.body.appendChild(this.canvas);
- }
- reset() {
- this.transitions = {};
- this.transitionstext = {};
- this.keyState = {};
- for (var i in this.ennemies) {
- if (this.ennemies.hasOwnProperty(i)) {
- this.ennemies[i].reset();
- }
- }
- this.player.reset();
- this.area.reset();
- this.UI.reset();
- }
- isKeyDown(code) {
- if (typeof code === 'object') {
- for (var i in code) {
- if (code.hasOwnProperty(i)) {
- if (this.keyState[code[i]]) return true;
- }
- }
- return false;
- }
- return this.keyState[code];
- }
- startEvents() {
- var _this = this;
- document.addEventListener('keydown',function(event){
- _this.keyState[event.keyCode || event.which] = true;
- });
- document.addEventListener('keyup',function(event){
- _this.keyState[event.keyCode || event.which] = false;
- });
- }
- addAttack(name,attack) {
- this.attacks[name] = attack;
- }
- removeAttack(name) {
- this.attacks[name] = null;
- delete this.attacks[name];
- }
- getAttack(name) {
- return this.attacks[name];
- }
- addEnnemy(name,entity) {
- this.ennemies[name] = entity;
- this.ennemies[name].setName(name);
- }
- removeEnnemy(name) {
- this.ennemies[name] = null;
- delete this.ennemies[name];
- }
- getEnnemy(name) {
- return this.ennemies[name];
- }
- getCurrentEnnemy() {
- return this.ennemies[this.currentEnnemy];
- }
- setCurrentEnnemy(name) {
- this.currentEnnemy = name;
- }
- /**
- * transition: permet d'effectuer une transition (doit être exécutée à 60FPS pour fonctionner)
- * @param name {string} : le nom de la transition (son ID)
- * @param object {object} : un objet de la forme {from:...,to:...,step:...}
- * @param draw {function} : function qui sera exécutée pour dessiner la transtion (son paramètre est la valeur actuelle de la transtion)
- * la fonction est donc de cette forme:
- * function (value) {
- * console.log('valeur actuelle de la transtion:',value)
- * }
- * @param next {function} : function qui sera exécutée lorsque la transtion est finie (cette fonction est exécutée à 60FPS)
- * (remarque: la fonction draw continuera aussi d'être exécutée)
- */
- transition(name,object,draw,next) {
- if (typeof this.transitions[name] === 'undefined') {
- this.transitions[name] = object.from;
- }
- this.transitions[name] += (object.from < object.to ? Math.abs(object.step) : -Math.abs(object.step)) || 1;
- if (object.from < object.to) {
- if (this.transitions[name] > object.to) {
- this.transitions[name] = object.to;
- if (typeof next === 'function') next();
- }
- } else {
- if (this.transitions[name] < object.to) {
- this.transitions[name] = object.to;
- if (typeof next === 'function') next();
- }
- }
- draw(this.transitions[name]);
- }
- /**
- * drawTextTransition: permet d'effectuer une transition (doit être exécutée à 60FPS pour fonctionner)
- * @param name {string} : le nom de la transition (son ID)
- * @param object {object} : un objet de la forme {text:...,tick:...}
- * @param draw {function} : function qui sera exécutée pour dessiner la transtion (son paramètre est la valeur actuelle de la transtion)
- * la fonction est donc de cette forme:
- * function (value) {
- * console.log('valeur actuelle de la transtion:',value)
- * }
- * @param next {function} : function qui sera exécutée lorsque la transtion est finie (cette fonction est exécutée à 60FPS)
- * (remarque: la fonction draw continuera aussi d'être exécutée)
- */
- drawTextTransition(name,object,draw,next) {
- if (typeof this.transitionstext[name] === 'undefined') {
- this.transitionstext[name] = {
- text: '',
- index: 0,
- time: Date.now()
- };
- }
- if (Date.now() - this.transitionstext[name].time > (object.tick || 100)) {
- this.transitionstext[name].time = Date.now();
- if (this.transitionstext[name].index < (object.text||'undefined').length) {
- this.transitionstext[name].text += (object.text||'undefined')[this.transitionstext[name].index];
- this.transitionstext[name].index++;
- }
- }
- draw(this.transitionstext[name].text);
- if (this.transitionstext[name].index == (object.text||'undefined').length) {
- if (typeof next === 'function') next();
- }
- }
- translate() {
- this.ctx.translate(this.pointTranslate.x,this.pointTranslate.y);
- }
- scale() {
- this.ctx.scale(this.pointScale.x,this.pointScale.y);
- }
- /**
- * changeState: permet de changer l'état du jeu
- * @param state {GAME_STATE} : l'état du jeu
- */
- changeState(state) {
- // on stocke le nouvelle état
- this.state = state;
- // on propage le changement de l'état partout
- for (var name in this.ennemies) {
- if (this.ennemies.hasOwnProperty(name)) {
- if (typeof this.ennemies[name].onchangestate === 'function') this.ennemies[name].onchangestate(state);
- }
- }
- if (typeof this.onchangestate === 'function') this.onchangestate(state);
- }
- /**
- * run: permet de lancer le jeu
- */
- run() {
- // on initialise tout
- for (var name in this.ennemies) {
- if (this.ennemies.hasOwnProperty(name)) {
- this.ennemies[name].init(this);
- }
- }
- this.player.init(this);
- this.UI.init(this);
- // on lance la fonction qui fera tourner notre jeu (cette fonction sera exécuté une centaine de fois par seconde)
- this.update();
- }
- waitKeyDown(keyCodeSkip) {
- var _this = this;
- _this.waitKeyDownEventFn = function(event) {
- if (keyCodeSkip.length == 0) {
- document.removeEventListener('keydown', _this.waitKeyDownEventFn);
- _this.keyState[event.keyCode] = false;
- delete _this.keyState[event.keyCode];
- _this.resolvetmp();
- return;
- }
- for (var i = 0; i < keyCodeSkip.length; i++) {
- if (keyCodeSkip[i] == event.keyCode) {
- document.removeEventListener('keydown', _this.waitKeyDownEventFn);
- for (var j = 0; j < keyCodeSkip.length; j++) {
- _this.keyState[keyCodeSkip[j]] = false;
- delete _this.keyState[keyCodeSkip[j]];
- }
- _this.resolvetmp();
- break;
- }
- }
- };
- return new Promise(function(resolve, reject) {
- _this.resolvetmp = resolve;
- document.addEventListener('keydown', _this.waitKeyDownEventFn);
- });
- }
- registerData(name, value) {
- this.data[name] = value;
- }
- retrieveData(name) {
- return (typeof this.data[name] === 'undefined' ? null : this.data[name]);
- }
- addAsset(...args) {
- this.assetManager.addAsset(...args);
- }
- removeAsset(...args) {
- this.assetManager.removeAsset(...args);
- }
- load(next) {
- this.assetManager.loadAll(next);
- }
- update() {
- var _this = this;
- window.requestAnimationFrame(function(){_this.update();});
- if (typeof this.onUpdate === 'function') this.onUpdate();
- }
- }
Add Comment
Please, Sign In to add comment