diff --git a/UI/dialog.js b/UI/dialog.js new file mode 100644 index 0000000..9401705 --- /dev/null +++ b/UI/dialog.js @@ -0,0 +1,15 @@ +Quintus.Dialog = function(Q) { + /** + * Clase que representa la ventana de diálogo. + */ + Q.Sprite.extend('Dialog', { + init: function(p) { + this._super(p, { + asset: 'dialog_box.png', + type: Q.SPRITE_UI, + x: Q.width / 2, + y: Q.height - 80 + }); + } + }); +}; \ No newline at end of file diff --git a/UI/heart.js b/UI/heart.js new file mode 100644 index 0000000..c9d165c --- /dev/null +++ b/UI/heart.js @@ -0,0 +1,39 @@ +Quintus.Heart = function(Q) { + /** + * Clase que representa las vidas que tiene Link. + * Se representan por corazones, cada uno con un num. + */ + Q.Sprite.extend('Heart', { + init: function(p) { + this._super(p, { + sheet: 'heart', + sprite: 'heartAnim', + gravity: 0, + x: 20, + y: 20, + scale: 2, + type: Q.SPRITE_UI, + actualLives: 3 + }); + this.add('animation'); + + Q.state.on('change.lives', this, 'lives'); + }, + + /** + * Si cambia el número de vidas, se anima el último corazón. + */ + lives: function(lives) { + if (this.p.actualLives > lives) { + if (this.p.num === lives) { + this.play('getHit'); + } + } + this.p.actualLives = lives; + } + }); + + Q.animations('heartAnim', { + 'getHit': { frames: [1, 2], rate: 1 / 4, loop: false }, + }); +}; \ No newline at end of file diff --git a/UI/hud.js b/UI/hud.js new file mode 100644 index 0000000..c628d54 --- /dev/null +++ b/UI/hud.js @@ -0,0 +1,18 @@ +Quintus.Hud = function(Q) { + /** + * Clase que representa los elementos de HUD, + * como son las rupias y la vida. + */ + Q.scene('hud', function(stage) { + var hearts = []; + for (var i = 0; i < 3; i++) { + hearts[i] = new Q.Heart({ x: 20 + i * 20, num: i }); + if (Q.state.get('maxLives') > Q.state.get('lives') && i >= Q.state.get('lives')) { + hearts[i].p.frame = 2; + } + stage.insert(hearts[i]); + } + stage.insert(new Q.RupeeCount()); + stage.insert(new Q.Score()); + }, { stage: 1 }); +}; \ No newline at end of file diff --git a/UI/rupeecount.js b/UI/rupeecount.js new file mode 100644 index 0000000..b33acb9 --- /dev/null +++ b/UI/rupeecount.js @@ -0,0 +1,42 @@ +Quintus.RupeeCount = function(Q) { + /** + * Clase que representa el icono de contador de rupias + */ + Q.Sprite.extend('RupeeCount', { + init: function(p) { + this._super(p, { + asset: 'rupee_icon.png', + gravity: 0, + type: Q.SPRITE_UI, + x: Q.width - 100, + y: 20, + scale: 2 + }); + } + }); + + /** + * Contador de rupias como tal + */ + Q.UI.Text.extend('Score', { + init: function(p) { + this._super(p, { + label: 'x ' + Q.state.get('score'), + x: Q.width - 50, + y: 10, + color: 'white', + size: 18 + }); + + Q.state.on('change.score', this, 'score'); + }, + + /** + * Si cambia el número de rupias, se activa + */ + score: function(score) { + Q.audio.play('rupee_counter.mp3'); + this.p.label = 'x ' + score; + } + }); +}; \ No newline at end of file diff --git a/enemies/deadrock.js b/enemies/deadrock.js new file mode 100644 index 0000000..6a46345 --- /dev/null +++ b/enemies/deadrock.js @@ -0,0 +1,34 @@ +Quintus.Deadrock = function(Q) { + + /** + * Creación de enemigo Deadrock con su movimiento + */ + + Q.Sprite.extend('Deadrock', { + init: function(p) { + this._super(p, { + sheet: 'deadrockWalk', + sprite: 'deadrockAnim', + hp: 1, + vx: 50, + direction: 'left', + score: 5 + }); + this.add('defaultEnemy, 2d, aiBounce'); + }, + + step: function(dt) { + this.p.invicible -= dt; + this.p.direction = (this.p.vx > 0) ? 'right' : 'left'; + this.play('walk_' + this.p.direction + '_foot'); + if (Q.state.get(this.p.id_enemy)) { + this.destroy(); + } + } + }); + + Q.animations('deadrockAnim', { + walk_left_foot: { frames: [0, 1], rate: 1 / 10, loop: true }, + walk_right_foot: { frames: [0, 1], flip: 'x', rate: 1 / 10, loop: true } + }); +}; \ No newline at end of file diff --git a/enemies/defaultenemy.js b/enemies/defaultenemy.js new file mode 100644 index 0000000..b6f322d --- /dev/null +++ b/enemies/defaultenemy.js @@ -0,0 +1,79 @@ +Quintus.LoadDefaultEnemy = function(Q) { + + /** + * Componente defaultEnemy, necesario para todos los enemigos + */ + Q.component('defaultEnemy', { + defaults: { + dead: false, + gravity: 0, + invicibleTime: 1, + invicible: 0, + score: 1 + }, + + added: function() { + this.entity.p.type = Q.SPRITE_ENEMY; + Q._defaults(this.entity.p, this.defaults); + this.entity.add('animation'); + this.entity.on('kicked', this, 'kicked'); + this.entity.on('dead', this, 'dead'); + }, + + /** + * Se activa cuando el enemigo es golpeado. + * Es invulnerable durante un tiempo y retrocede + */ + kicked: function(enemy, direction) { + if (this.entity.p.invicible < 0) { + this.entity.p.invicible = this.entity.p.invicibleTime; + this.entity.p.hp--; + if(enemy=='ganonAnim'){ + this.stun(direction); + } + if (this.entity.p.hp == 0) { + Q.audio.play('enemy_killed.mp3'); + this.entity.trigger('dead'); + } else { + Q.audio.play('enemy_hurt.mp3'); + this.entity.animate({ opacity: 0 }, 0.5); + this.entity.animate({ opacity: 1 }, 0.5, { delay: 0.5 }); + } + } + }, + + /** + * Se activa si el enemigo tiene 0 PV, + * inserta la animación de su muerte + */ + dead: function() { + var obj = this.entity.stage.insert(new Q.EnemyKilled({ x: this.entity.p.x, y: this.entity.p.y })); + Q.state.inc('score', this.entity.p.score); + Q.state.set(this.entity.p.id_enemy, true); + if(this.entity.destroyEffect) + this.entity.destroyEffect(); + this.entity.destroy(); + }, + + /** + * Indica la dirección hacia la que debe retroceder el enemigo + * cuando es golpeado + */ + stun: function(direction){ + switch(direction){ + case '_up': + this.entity.animate({ y: this.entity.p.y-50}); + break; + case '_down': + this.entity.animate({ y: this.entity.p.y+50}); + break; + case '_left': + this.entity.animate({ x: this.entity.p.x-50 }); + break; + default: + this.entity.animate({ x: this.entity.p.x+50}); + } + + } + }); +}; diff --git a/enemies/enemykilled.js b/enemies/enemykilled.js new file mode 100644 index 0000000..f6e0960 --- /dev/null +++ b/enemies/enemykilled.js @@ -0,0 +1,28 @@ +Quintus.EnemyKilled = function(Q) { + + /** + * Clase auxiliar que contiene + * la animación de un enemigo muerto + */ + + Q.Sprite.extend('EnemyKilled', { + init: function(p) { + this._super(p, { + sheet: 'enemyKilled', + sprite: 'enemyKilledAnim', + }); + this.add('animation'); + this.on('end', this, 'end'); + this.play('play'); + }, + step: function(dt) {}, + + end: function() { + this.destroy(); + } + }); + + Q.animations('enemyKilledAnim', { + play: { frames: [0, 1, 2, 3, 4, 5], rate: 1 / 10, loop: false, trigger: 'end' }, + }); +}; \ No newline at end of file diff --git a/enemies/fire.js b/enemies/fire.js new file mode 100644 index 0000000..f475db0 --- /dev/null +++ b/enemies/fire.js @@ -0,0 +1,49 @@ +Quintus.Fire = function(Q) { + + /** + * Sprite que representa los ataques de fuego de Ganon + */ + Q.Sprite.extend('fire', { + init: function(p) { + this._super(p, { + asset: "explosion.png", + collisionMask: Q.SPRITE_PLAYER | Q.SPRITE_WALL | Q.SPRITE_FIRE, + type: Q.SPRITE_FIRE, + gravity:0, + target: undefined, + vx: 0, + vy: 0, + lifeTime: 8, + delay:1, + }) + this.add('2d, aiBounce'); + this.p.target = (Q('Link').first()).p; + }, + + /** + * Se define el rebote + * y la desaparición de las bolas de fuego + */ + step: function(dt) { + if(this.p.delay - dt > 0) + this.p.delay -= dt; + else{ + if(this.p.vx == 0 && this.p.vy == 0){ + deltaX = this.p.target.x - this.p.x; + deltaY = this.p.target.y - this.p.y; + scale = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + total = Math.abs(deltaX) + Math.abs(deltaY); + this.p.vx = (deltaX / scale) * 75; + this.p.vy = (deltaY / scale) * 75; + }else{ + this.p.lifeTime -=dt; + if(this.p.lifeTime <= 0){ + this.destroy(); + } + } + } + }, + }); + + +}; \ No newline at end of file diff --git a/enemies/ganon.js b/enemies/ganon.js new file mode 100644 index 0000000..f585b77 --- /dev/null +++ b/enemies/ganon.js @@ -0,0 +1,85 @@ +var lock = true; +Quintus.Ganon = function(Q) { + + /** + * Clase de Ganon, enemigo final + */ + Q.Sprite.extend('Ganon', { + init: function(p) { + this._super(p, { + sheet: 'ganonWalk', + sprite: 'ganonAnim', + score: 100, + hp: 3, + invokeFireTime: 0, + hideousLaughter:false, + }); + this.add('defaultEnemy'); + this.on('kicked', this, 'kicked'); + this.on('dead', this, 'dead'); + }, + + /** + * Se define el teletransporte y los ataques de fuego de Ganon + */ + step: function(dt) { + this.p.invicible -= dt; + if (!this.p.dead) { + this.play('stand'); + } + if(this.p.hideousLaughter){ + this.p.invokeFireTime = 1; + if(!Q('Deadrock').first() && !Q('Soldier').first()){ + this.p.hideousLaughter = false; + this.p.y = 300 + } + }else{ + if(this.p.invokeFireTime <= 0){ + this.p.invokeFireTime = 1; + this.stage.insert(new Q.fire({x:this.p.x, y:this.p.y})) + }else{ + this.p.invokeFireTime -= dt; + } + } + }, + + /** + * Al morir, se desbloquea el camino hasta Zelda y desaparecen las bolas de fuego + */ + destroyEffect: function(){ + stair = Q('stair').first(); + stair.destroy(); + fire = Q('fire'); + for(f of fire.items){ + f.destroy(); + } + }, + + /** + * Al ser atacado, Ganon libera su horda de enemigos + * y desaparecen sus ataques de fuego + */ + kicked: function(){ + if(!this.p.hideousLaughter){ + this.p.hideousLaughter = true; + this.p.y=100; + this.stage.insert(new Q.Deadrock({id_enemy: 10,x:150,y:250})); + this.stage.insert(new Q.Soldier({id_enemy: 11,x:200,y:180})); + this.stage.insert(new Q.Soldier({id_enemy: 12,x:280,y:180})); + this.stage.insert(new Q.Deadrock({id_enemy: 13,x:325,y:250})); + this.stage.insert(new Q.Deadrock({id_enemy: 14,x:150,y:400})); + this.stage.insert(new Q.Deadrock({id_enemy: 15,x:325,y:400})); + fire = Q('fire') + for(f of fire.items){ + f.destroy(); + } + } + } + + }); + + Q.animations('ganonAnim', { + walk: { frames: [5, 6], rate: 1 / 5, loop: true }, + stand: { frames: [9], loop: true } + }); +}; \ No newline at end of file diff --git a/enemies/soldier.js b/enemies/soldier.js new file mode 100644 index 0000000..54ba44a --- /dev/null +++ b/enemies/soldier.js @@ -0,0 +1,152 @@ +Quintus.Soldier = function(Q) { + + /** + * Creación de enemigo soldado + */ + Q.Sprite.extend('Soldier', { + init: function(p) { + this._super(p, { + sheet: 'soldierWalk', + sprite: 'soldierAnim', + hp: 2, + viewRange: 10, + direction: 'down', + vSoldier: 40, + tileSize: 8, + score: 10, + collisionMask: Q.SPRITE_DEFAULT, + type: Q.SPRITE_ENEMY, + }); + this.add('defaultEnemy, 2d'); + this.on('check', this, 'check'); + this.on('move', this, 'move'); + this.on('moveX', this, 'moveX'); + this.on('moveY', this, 'moveY'); + }, + + step: function(dt) { + this.p.invicible -= dt; + if (Q.state.get(this.p.id_enemy)) { + this.destroy(); + } + this.check(); + + }, + + /** + * Comportamiento de soldado. + * Se mantiene quieto hasta que viene link + */ + check: function() { + this.p.vx = 0; + this.p.vy = 0; + + if (!this.p.trackClass) { + this.p.trackClass = Q('Link').first(); + } + + var distanceX = Math.pow((this.p.x - this.p.trackClass.p.x), 2); + var distanceY = Math.pow((this.p.y - this.p.trackClass.p.y), 2); + var distance = Math.sqrt(distanceX + distanceY) / this.p.tileSize; + + if (distance <= this.p.viewRange) { + this.move(); + } else { + this.play('stand_' + this.p.direction); + } + }, + + /** + * Se define el movimiento de seguimiento del soldado + */ + move: function() { + var objective = this.p.trackClass.p; + var dir = this.p.direction; + this.moveX(this.p.x, objective.x); + this.moveY(this.p.y, objective.y); + if (this.p.vx != 0 || this.p.vy != 0) { + if (Math.abs(this.p.vx) >= Math.abs(this.p.vy)) { + if (this.p.vx < 0) { + this.p.direction = 'left'; + } else if (this.p.vx > 0) { + this.p.direction = 'right'; + } + if (dir == 'up') { + this.play('walking_up_' + this.p.direction); + } else if (dir == 'down') { + this.play('walking_down_' + this.p.direction); + } else { + this.play('walking_' + this.p.direction); + } + } else { + if (this.p.vy < 0) { + this.p.direction = 'up'; + } else if (this.p.vy > 0) { + this.p.direction = 'down'; + } + if (dir == 'left') { + this.play('walking_left_' + this.p.direction); + } else if (dir == 'right') { + this.play('walking_right_' + this.p.direction); + } else { + this.play('walking_' + this.p.direction); + } + } + } + }, + + moveX: function(xSoldier, xObjective) { + if (xSoldier < xObjective) { + this.p.vx = this.p.vSoldier; + if (xSoldier + this.p.vx > xObjective) { + this.p.vx = xObjective - xSoldier; + } + } else if (xSoldier > xObjective) { + this.p.vx = -this.p.vSoldier; + if (xSoldier + this.p.vx < xObjective) { + this.p.vx = xObjective - xSoldier; + } + } else { + this.p.vx = 0; + } + }, + + moveY: function(ySoldier, yObjective) { + if (ySoldier < yObjective) { + this.p.vy = this.p.vSoldier; + if (ySoldier + this.p.vy > yObjective) { + this.p.vy = yObjective - ySoldier; + } + } else if (ySoldier > yObjective) { + this.p.vy = -this.p.vSoldier; + if (ySoldier + this.p.vy < yObjective) { + this.p.vy = yObjective - ySoldier; + } + } else { + this.p.vy = 0; + } + } + }); + + Q.animations('soldierAnim', { + 'walking_down': { frames: [0, 1], rate: 1 / 10, loop: true }, + 'stand_down': { frames: [0] }, + 'walking_down_right': { frames: [2], rate: 1 / 12, next: 'walking_right' }, + 'walking_down_left': { frames: [3], rate: 1 / 12, next: 'walking_left' }, + + 'walking_up': { frames: [12, 13], rate: 1 / 10, loop: true }, + 'stand_up': { frames: [12] }, + 'walking_up_right': { frames: [15], rate: 1 / 12, next: 'walking_right' }, + 'walking_up_left': { frames: [14], rate: 1 / 12, next: 'walking_left' }, + + 'walking_right': { frames: [4, 5], rate: 1 / 10, loop: true }, + 'stand_right': { frames: [4] }, + 'walking_right_up': { frames: [6], rate: 1 / 12, next: 'walking_up' }, + 'walking_right_down': { frames: [7], rate: 1 / 12, next: 'walking_down' }, + + 'walking_left': { frames: [8, 9], rate: 1 / 10, loop: true }, + 'stand_left': { frames: [9] }, + 'walking_left_up': { frames: [11], rate: 1 / 12, next: 'walking_up' }, + 'walking_left_down': { frames: [10], rate: 1 / 12, next: 'walking_down' } + }); +}; \ No newline at end of file diff --git a/items/chest.js b/items/chest.js new file mode 100644 index 0000000..0ac0077 --- /dev/null +++ b/items/chest.js @@ -0,0 +1,43 @@ +Quintus.Chest = function(Q) { + /** + * Clase que representa un cofre. + */ + Q.Sprite.extend('Chest', { + init: function(p) { + this._super(p, { + sheet: 'chestSmall', + sprite: 'chestAnim', + type: Q.SPRITE_CHEST, + sensor: true + }); + this.add('animation'); + + this.on('sensor', this, 'sensor'); + }, + + /** + * Si el cofre está cerrado (sensor: true), crea un objeto. + */ + sensor: function() { + if (this.p.sensor) { + Q.audio.play('chest_open.mp3'); + Q.state.set(this.p.id_chest, true); + this.p.sensor = false; + var obj = this.stage.insert(new Q.Item({ object: this.p.object, x: this.p.x, y: this.p.y - 10 })); + } + }, + + step: function(dt) { + if (Q.state.get(this.p.id_chest)) { + this.play('open'); + } else { + this.play('close'); + } + } + }); + + Q.animations('chestAnim', { + close: { frames: [0] }, + open: { frames: [1] } + }); +}; \ No newline at end of file diff --git a/items/item.js b/items/item.js new file mode 100644 index 0000000..b4e4d89 --- /dev/null +++ b/items/item.js @@ -0,0 +1,30 @@ +Quintus.Item = function(Q) { + /** + * Clase que representa un objeto + */ + Q.Sprite.extend('Item', { + init: function(p) { + this._super(p, { + asset: 'item_' + p.object + '.png' + }); + this.add('animation, tween'); + }, + + /** + * Al recoger un objeto, se suma la puntuación + */ + step: function(dt) { + var get = function() { + this.play('picked_item.mp3'); + Q.state.inc('score', 20); + if (Q.state.get(this.p.object)) { + Q.state.inc(this.p.object, 1); + } else { + Q.state.set(this.p.object, 1); + } + this.destroy(); + }; + this.animate({ y: this.p.y - 10 }, 1, { callback: get }); + } + }); +}; \ No newline at end of file diff --git a/items/rupee.js b/items/rupee.js new file mode 100644 index 0000000..64b5a52 --- /dev/null +++ b/items/rupee.js @@ -0,0 +1,42 @@ +Quintus.Rupee = function(Q) { + /** + * Clase que representa las rupias + * que se pueden recoger por el mapa. + */ + Q.Sprite.extend('Rupee', { + init: function(p) { + this._super(p, { + sheet: 'rupee', + sprite: 'rupeeAnim', + type: Q.SPRITE_RUPEE, + collisionMask: Q.SPRITE_DEFAULT, + sensor: true, + get: false + }); + this.add('animation'); + this.on('sensor', this, 'sensor'); + this.play('live'); + }, + + /** + * Se recogen y se marcan las rupias + * para que no vuelvan a aparecer al cambiar de mapa + */ + sensor: function() { + this.destroy(); + Q.state.inc('score', 1); + Q.state.set(this.p.id_rupee, true); + + }, + + step: function(dt) { + if (Q.state.get(this.p.id_rupee)) { + this.destroy(); + } + } + }); + + Q.animations('rupeeAnim', { + live: { frames: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], rate: 1 / 12, loop: true }, + }); +}; \ No newline at end of file diff --git a/items/stair.js b/items/stair.js new file mode 100644 index 0000000..d57ff9c --- /dev/null +++ b/items/stair.js @@ -0,0 +1,27 @@ + +Quintus.Stair = function(Q) { + /** + * Clase del muro que crea Ganon, + * e impide llegar a Link hasta Zelda + */ + Q.Sprite.extend('stair', { + init: function(p) { + this._super(p, { + sheet: 'stairStand', + sprite: 'stairAnim', + gravity: 0, + type: Q.SPRITE_WALL + }); + this.add(' animation, tween'); + }, + + step: function(dt) { + this.play('stand'); + } + }); + + Q.animations('stairAnim', { + stand: { frames: [0], loop: true } + }); + +}; \ No newline at end of file diff --git a/link/link.js b/link/link.js new file mode 100644 index 0000000..39da84a --- /dev/null +++ b/link/link.js @@ -0,0 +1,176 @@ +Quintus.Link = function(Q) { + + /** + * Clase que representa a Link, controlado por el jugador + */ + Q.Sprite.extend('Link', { + init: function(p) { + this._super(p, { + sheet: 'link', + sprite: 'linkAnim', + gravity: 0, + stepDistance: 16, + stepDelay: 0.2, + points: [ + [-8, -3], + [8, -3], + [8, 12], + [-8, 12] + ], + type: Q.SPRITE_PLAYER, + collisionMask: Q.SPRITE_FIRE | Q.SPRITE_WALL | Q.SPRITE_DEFAULT | Q.SPRITE_ENEMY | Q.SPRITE_CHEST | Q.SPRITE_COLLIDER | Q.SPRITE_RUPEE | Q.SPRITE_NPC, + invulnerabilityTime: 1, + invulnerability: false, + talking: false, + talkingNext: 0, + talkingNPC: 0, + }); + + this.add('stepControls, animation'); + + this.on('hit', 'hit'); + this.on('dead', 'dead'); + this.on('restart', 'restart'); + }, + + /** + * Aparece la escena de final cuando muere Link + */ + restart: function() { + Q.audio.stop(); + Q.clearStages(); + Q.stageScene('endGame'); + }, + + /** + * Describe el comportamiento de Link + * cuando colisiona con un objeto. + */ + hit: function(col) { + switch (col.obj.p.type) { + /** + * Al chocar contra un enemigo o contra una bola de fuego, + * se vuelve invulnerable durante un tiempo y pierde una vida. + */ + case Q.SPRITE_FIRE: + col.obj.destroy(); + case Q.SPRITE_ENEMY: + if (!this.p.invulnerability) { + this.p.invulnerabilityTime = 1; + this.p.invulnerability = true; + Q.state.dec("lives", 1); + Q.audio.play("hero_hurt.mp3"); + if (Q.state.get('lives') === 0) { + this.trigger('dead'); + } + } + break; + /** + * Activa el sensor de los objetos. + */ + case Q.SPRITE_CHEST: + case Q.SPRITE_COLLIDER: + case Q.SPRITE_RUPEE: + col.obj.sensor(); + break; + /** + * Comienza el diálogo con los personajes y + * deja de moverse. + */ + case Q.SPRITE_NPC: + if(!this.p.talking){ + this.p.talking = true; + this.p.stepDistance = 0; + this.p.talkingNPC = col.obj; + this.children[0].destroy(); + col.obj.trigger('sensor'); + } + break; + /** + * La pared impide que Link pase. + */ + case Q.SPRITE_WALL: + break; + } + }, + + /** + * Activa la animación de muerte de Link. + */ + dead: function() { + this.p.stepDistance = 0; + Q.audio.stop(); + Q.audio.play("hero_dying.mp3"); + this.p.sheet = 'dying'; + this.play('dying', 1); + }, + + /** + * Modifica los sprites de Link en función de su movimiento, + * y cambia su comportamiento durante la conversación con NPCs. + */ + step: function(dt) { + if(this.p.talking){ + this.p.talkingNext += dt; + if(Q.inputs.confirm && this.p.talkingNext > 0.3){ + this.p.talkingNext = 0; + if(this.p.talkingNPC.p.continue){ + this.p.talkingNPC.trigger('talk'); + }else{ + this.p.stepDistance = 16; + this.stage.insert(new Q.Sword(), this); + this.p.talkingNPC.trigger('endTalk', this); + } + } + }else{ + this.p.reloadSword -= dt; + var dir = 'walking'; + + if (Q.inputs.up) { + dir += '_up'; + } else if (Q.inputs.down) { + dir += '_down'; + } + if (Q.inputs.left) { + dir += '_left'; + } else if (Q.inputs.right) { + dir += '_right'; + } + if (dir !== 'walking') { + this.play(dir); + } + if (this.p.invulnerability) { + this.p.invulnerabilityTime -= dt; + if (this.p.invulnerabilityTime < 0) { + this.p.invulnerability = false; + } + } + this.stage.collide(this); + } + } + }); + + Q.animations('linkAnim', { + 'walking_right': { frames: [0, 1, 2, 3, 4, 5, 6, 7], rate: 1 / 16, next: 'stand_right' }, + 'stand_right': { frames: [0] }, + + 'walking_up': { frames: [11, 12, 13, 14, 15, 16, 17, 18], rate: 1 / 16, next: 'stand_up' }, + 'walking_up_right': { frames: [8, 9, 10], rate: 1 / 12, next: 'stand_up_right' }, + 'walking_up_left': { frames: [19, 20, 21], rate: 1 / 12, next: 'stand_up_left' }, + 'stand_up': { frames: [11] }, + 'stand_up_right': { frames: [8] }, + 'stand_up_left': { frames: [19] }, + + 'walking_down': { frames: [33, 34, 35, 36, 37, 38, 39, 40], rate: 1 / 16, next: 'stand_down' }, + 'walking_down_right': { frames: [41, 42, 43], rate: 1 / 12, next: 'stand_down_right' }, + 'walking_down_left': { frames: [30, 31, 32], rate: 1 / 12, next: 'stand_down_left' }, + 'stand_down': { frames: [33] }, + 'stand_down_right': { frames: [41] }, + 'stand_down_left': { frames: [30] }, + + 'walking_left': { frames: [22, 23, 24, 25, 26, 27, 28, 29], rate: 1 / 16, next: 'stand_left' }, + 'stand_left': { frames: [22] }, + + 'dying': { frames: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], rate: 1 / 8, loop: false, trigger: "restart" } + }); +}; diff --git a/link/sword.js b/link/sword.js new file mode 100644 index 0000000..c7b5de3 --- /dev/null +++ b/link/sword.js @@ -0,0 +1,136 @@ +Quintus.SwordLink = function(Q) { + /** + * Clase que controla la espada de Link + */ + Q.Sprite.extend('Sword', { + init: function(p) { + this._super(p, { + sheet: 'sword', + sprite: 'swordAnim', + gravity: 0, + type: Q.SPRITE_SWORD, + collisionMask: Q.SPRITE_ENEMY, + stepDistance: 8, + stepDelay: 0.1, + atack: false, + direction: '_right', + atackResetMax: 2 / 3, + atackReset: 2 / 3, + originX: 0, + originY: 0, + dmg: 1, + }); + this.add('animation'); + this.on('hit', 'hit'); + this.on('restart', this, 'restart'); + }, + + /** + * Modifica el sprite de la espada en función de su posición. + * También se encarga de las animaciones de ataque y de su + * comportamiento durante el mismo. + */ + step: function(dt) { + dir = 'sword'; + if (!this.p.atack) { + if (Q.inputs.up) { + this.p.direction = '_up'; + dir += '_up'; + } else if (Q.inputs.down) { + this.p.direction = '_down'; + dir += '_down'; + } + if (Q.inputs.left) { + this.p.direction = '_left'; + dir += '_left'; + } else if (Q.inputs.right) { + this.p.direction = '_right'; + dir += '_right'; + } + if (dir !== 'sword') { + this.play(dir); + } + } + if (Q.inputs.fire && !this.p.atack) { + this.p.atack = true; + Q.audio.play('sword1.mp3'); + this.p.sheet = "atack_sword"; + this.p.originX = this.p.cx; + this.p.originY = this.p.cy; + setSwordPos(this.p); + this.play('sword_atack' + this.p.direction); + } + if (this.p.atack) { + this.stage.collide(this); + } + if (Q.state.get('lives') === 0) { + this.destroy(); + } + }, + + /** + * Golpea a los enemigos + */ + hit: function(col) { + col.obj.trigger('kicked', [col.obj.p.sprite, this.p.direction]); + }, + + /** + * La espada vuelve a su posición inicial tras atacar + */ + restart: function() { + this.p.atack = false; + this.p.sheet = 'sword'; + this.p.cx = this.p.originX; + this.p.cy = this.p.originY; + } + }); + + Q.animations('swordAnim', { + 'sword_atack_right': { frames: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], rate: 1 / 18, loop: false, trigger: 'restart', next: 'sword_right_stop' }, + 'sword_atack_up': { frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], rate: 1 / 18, loop: false, trigger: 'restart', next: 'sword_up_stop' }, + 'sword_atack_left': { frames: [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35], loop: false, trigger: 'restart', rate: 1 / 18, next: 'sword_left_stop' }, + 'sword_atack_down': { frames: [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47], rate: 1 / 18, loop: false, trigger: 'restart', next: 'sword_down_stop' }, + 'sword_right': { frames: [0, 1, 2, 3, 4, 5], rate: 1 / 16, next: 'sword_right_stop' }, + 'sword_right_stop': { frames: [0] }, + 'sword_up': { frames: [6, 7, 8, 9, 10, 11], rate: 1 / 16, next: 'sword_up_stop' }, + 'sword_up_stop': { frames: [6] }, + 'sword_left': { frames: [12, 13, 14, 15, 16, 17], rate: 1 / 16, next: 'sword_left_stop' }, + 'sword_left_stop': { frames: [12] }, + 'sword_down': { frames: [18, 19, 20, 21, 22, 23], rate: 1 / 16, next: 'sword_down_stop' }, + 'sword_down_stop': { frames: [18] }, + 'sword_up_right': { frames: [0, 1, 2, 3, 4, 5], rate: 1 / 12, next: 'sword_up_right_stop' }, + 'sword_up_right_stop': { frames: [0] }, + 'sword_up_left': { frames: [6, 7, 8, 9, 10, 11], rate: 1 / 12, next: 'sword_up_left_stop' }, + 'sword_up_left_stop': { frames: [6] }, + 'sword_down_right': { frames: [0, 1, 2, 3, 4, 5], rate: 1 / 12, next: 'sword_down_right_stop' }, + 'sword_down_right_stop': { frames: [0] }, + 'sword_down_left': { frames: [18, 19, 20, 21, 22, 23], rate: 1 / 12, next: 'sword_down_left_stop' }, + 'sword_down_left_stop': { frames: [18] }, + }); +}; + +/** + * Coloca la posición de la espada + * @param {*} p + */ +function setSwordPos(p){ + switch(p.direction){ + case "_right": + p.cx += 20; + p.cy += 8; + break + case "_left": + p.cx += 14; + p.cy += 8; + break; + case "_up": + p.cx += 16; + p.cy += 8; + break; + case "_down": + p.cx += 16; + p.cy += 12; + break; + } +} \ No newline at end of file diff --git a/maps/boss.js b/maps/boss.js new file mode 100644 index 0000000..ad1ef9c --- /dev/null +++ b/maps/boss.js @@ -0,0 +1,24 @@ +Quintus.BossMap = function(Q) { + /** + * Escena que representa la sala de batalla con Ganon, + * carga el mapa, la música y a Link con su espada. + */ + Q.scene('boss', function(stage) { + + Q.stageTMX('interior_1_map.tmx', stage); + var player = Q('Link').first(); + if (stage.options.xLink) { + player.p.x = stage.options.xLink; + player.p.y = stage.options.yLink; + } + var sword = stage.insert(new Q.Sword(), player); + + stage.add('viewport').follow(player, { x: true, y: true }, { + minY: 0, + maxY: 464, + minX: 0, + maxX: 496, + }); + Q.audio.play('forest.mp3', { loop: true }); + }); +}; \ No newline at end of file diff --git a/maps/castlemap.js b/maps/castlemap.js new file mode 100644 index 0000000..993e25d --- /dev/null +++ b/maps/castlemap.js @@ -0,0 +1,24 @@ +Quintus.CastleMap = function(Q) { + /** + * Escena que representa el castillo, + * carga el mapa, la música y a Link con su espada. + */ + Q.scene('castleMap', function(stage) { + + Q.stageTMX('castle_map.tmx', stage); + var player = Q('Link').first(); + if (stage.options.xLink) { + player.p.x = stage.options.xLink; + player.p.y = stage.options.yLink; + } + var sword = stage.insert(new Q.Sword(), player); + + stage.add('viewport').follow(player, { x: true, y: true }, { + minY: 0, + maxY: 1024, + minX: 0, + maxX: 1024, + }); + Q.audio.play('forest.mp3', { loop: true }); + }); +}; \ No newline at end of file diff --git a/maps/endgame.js b/maps/endgame.js new file mode 100644 index 0000000..68aa10d --- /dev/null +++ b/maps/endgame.js @@ -0,0 +1,38 @@ +Quintus.EndGame = function(Q) { + + /** + * Escena que representa el final de juego malo (game over), + * al pulsar ENTER se reinicia el juego en la casa de Link. + */ + Q.scene('endGame', function(stage) { + console.log('Game over'); + var confirm = true; + var over = stage.insert(new Q.GameOver()); + var container = stage.insert(new Q.UI.Container({ + x: Q.width / 100, + y: Q.height / 2.5, + fill: 'rgba(0,0,0,0)', + type: Q.SPRITE_UI + })); + + var label = container.insert(new Q.UI.Text({ + x: 0, + y: 0, + color: 'white', + label: 'Press enter' + })); + + Q.input.on('confirm', this, function() { + if (confirm) { + Q.audio.stop(); + Q.clearStages(); + Q.state.reset({ score: 0, lives: 3, maxLives: 3, dialog: "" }); + Q.stageScene('initialMenu'); + confirm = false; + } + }); + Q.audio.play('game_over.mp3'); + stage.add('viewport').follow(over); + container.fit(15, 25); + }); +}; \ No newline at end of file diff --git a/maps/houselink.js b/maps/houselink.js new file mode 100644 index 0000000..5ed8549 --- /dev/null +++ b/maps/houselink.js @@ -0,0 +1,25 @@ +Quintus.HouseLinkMap = function(Q) { + /** + * Escena que representa la casa de Link, + * carga el mapa, la música y a Link con su espada. + */ + Q.scene('houseLinkMap', function(stage) { + + Q.stageTMX('house_link.tmx', stage); + + var player = Q('Link').first(); + if (stage.options.xLink) { + player.p.x = stage.options.xLink; + player.p.y = stage.options.yLink; + } + var sword = stage.insert(new Q.Sword(), player); + + stage.add('viewport').follow(player, { x: true, y: true }, { + minY: -125, + maxY: 200, + minX: -125, + maxX: 100 + }); + Q.audio.play('forest.mp3', { loop: true }); + }); +}; \ No newline at end of file diff --git a/maps/houselinkforest.js b/maps/houselinkforest.js new file mode 100644 index 0000000..81a55ff --- /dev/null +++ b/maps/houselinkforest.js @@ -0,0 +1,24 @@ +Quintus.HouseLinkForestMap = function(Q) { + /** + * Escena que representa el bosque, + * carga el mapa, la música y a Link con su espada. + */ + Q.scene('houseLinkForestMap', function(stage) { + + Q.stageTMX('house_link_forest.tmx', stage); + var player = Q('Link').first(); + if (stage.options.xLink) { + player.p.x = stage.options.xLink; + player.p.y = stage.options.yLink; + } + var sword = stage.insert(new Q.Sword(), player); + + stage.add('viewport').follow(player, { x: true, y: true }, { + minY: 0, + maxY: 512, + minX: 0, + maxX: 1024, + }); + Q.audio.play('forest.mp3', { loop: true }); + }); +}; \ No newline at end of file diff --git a/maps/initialMenu.js b/maps/initialMenu.js new file mode 100644 index 0000000..5e45b9f --- /dev/null +++ b/maps/initialMenu.js @@ -0,0 +1,37 @@ +Quintus.InitialMenu = function(Q) { + /** + * Escena que representa el inicio del juego, + * al pulsar ENTER se reinicia el juego en la casa de Link. + */ + Q.scene('initialMenu', function(stage) { + var confirm = true; + var intro = stage.insert(new Q.Intro()); + var container = stage.insert(new Q.UI.Container({ + x: Q.width / 100, + y: Q.height / 2.5, + fill: 'rgba(0,0,0,0)', + type: Q.SPRITE_UI + })); + + var label = container.insert(new Q.UI.Text({ + x: 0, + y: 0, + color: 'white', + label: 'Press enter' + })); + + Q.input.on('confirm', this, function() { + if (confirm) { + Q.audio.stop(); + Q.clearStages(); + Q.state.reset({ score: 0, lives: 3, maxLives: 3, dialog: "" }); + Q.stageScene('hud'); + Q.stageScene('houseLinkMap'); + confirm = false; + } + }); + Q.audio.play('title_screen.mp3'); + stage.add('viewport').follow(intro); + container.fit(15, 25); + }); +}; \ No newline at end of file diff --git a/maps/wingame.js b/maps/wingame.js new file mode 100644 index 0000000..a12017e --- /dev/null +++ b/maps/wingame.js @@ -0,0 +1,35 @@ +Quintus.WinGame = function(Q) { + /** + * Escena que representa el final de juego bueno (you win), + * al pulsar ENTER se reinicia el juego en la pantalla de título. + */ + Q.scene('wingame', function(stage) { + var confirm = true; + var over = stage.insert(new Q.winGame()); + var container = stage.insert(new Q.UI.Container({ + x: Q.width / 100, + y: Q.height / 2.5, + fill: 'rgba(0,0,0,0)', + type: Q.SPRITE_UI + })); + + var label = container.insert(new Q.UI.Text({ + x: 0, + y: 0, + color: 'white', + label: 'Press enter' + })); + + Q.input.on('confirm', this, function() { + if (confirm) { + Q.audio.stop(); + Q.clearStages(); + Q.state.reset({ score: 0, lives: 3, maxLives: 3, dialog:""}); + Q.stageScene('initialMenu'); + confirm = false; + } + }); + stage.add('viewport').follow(over); + container.fit(15, 25); + }); +}; \ No newline at end of file diff --git a/maps/zelda.js b/maps/zelda.js new file mode 100644 index 0000000..971d36d --- /dev/null +++ b/maps/zelda.js @@ -0,0 +1,24 @@ +Quintus.ZeldaMap = function(Q) { + /** + * Escena que representa la sala en la que se encuentra Zelda, + * carga el mapa, la música y a Link con su espada. + */ + Q.scene('zelda', function(stage) { + + Q.stageTMX('zelda_map.tmx', stage); + var player = Q('Link').first(); + if (stage.options.xLink) { + player.p.x = stage.options.xLink; + player.p.y = stage.options.yLink; + } + var sword = stage.insert(new Q.Sword(), player); + + stage.add('viewport').follow(player, { x: true, y: true }, { + minY: 0, + maxY: 450, + minX: 0, + maxX: 416, + }); + Q.audio.play('forest.mp3', { loop: true }); + }); +}; \ No newline at end of file