Funciones y Componentes para la Vista de Héroe en Vue.js
Clase 22 de 27 • Curso Avanzado de Vue.js 2
Ahora que tenemos la página de Profile completa, y podemos navegar a la Hero, vamos a crear las funciones y componentes necesarios para esta vista.
> 👆 Esta lectura se corresponde con este commit: 35f0bacbfe195461a8dbacac01412779d98323fa
.
> Si no has seguido el proceso desde el principio (o si no te funciona), puedes ir a la carpeta del proyecto y escribir git checkout 35f0bacbfe195461a8dbacac01412779d98323fa
en la terminal.
Antes de continuar, vamos a repasar la ruta que tenemos que gestionar en esta vista. Esta es la definición que tenemos en nuestro fichero de rutas:
{ path: '/region/:region/profile/:battleTag/hero/:heroId', name: 'Hero' }
Entra en juego un nuevo parámetro, el id
del héroe. Este parámetro lo vamos a usar para hacer una llamada a la API de items del héroe. Con esto vamos a obtener un listado de todos los objetos del personaje especificado.
Para esta vista vamos a hacer dos llamadas simultáneas a las APIs de Diablo. Aquí tienes todas las definiciones de las APIs y su documentación: https://develop.battle.net/documentation/diablo-3/community-apis
Vamos a hacer uso de getApiHero
y de getApiDetailedHeroItems
. Con esto vamos a obtener todos los datos del héroe (stats, habilidades, etc.) y los objetos que tiene equipados en ese momento.
Tenemos que definir las dos nuevas funciones que hagan las llamadas a las APIs. Para ello, vamos a la carpeta /api
y abrimos el fichero search.js
. Agregamos estas 2 funciones:
/** * Returns a single hero * GET – /d3/profile/{account}/hero/{heroId} * @param region {String} * @param account {String} * @param heroId {String} * @returns {Promise} */ function getApiHero ({ region, account, heroId }) { const resource = `d3/profile/${account}/hero/${heroId}` const API_URL = `${protocol}${region}${host}${resource}` const locale = locales[region] const params = { 'access_token': store.state.oauth.accessToken, locale } return get(API_URL, { params }) } /** * Returns a list of items for the specified hero. * GET – /d3/profile/{account}/hero/{heroId}/items * @param region {String} * @param account {String} * @param heroId {String} * @returns {Promise} */ function getApiDetailedHeroItems ({ region, account, heroId }) { const resource = `d3/profile/${account}/hero/${heroId}/items` const API_URL = `${protocol}${region}${host}${resource}` const locale = locales[region] const params = { access_token: store.state.oauth.accessToken, locale } return get(API_URL, { params }) }
No requieren mucha explicación, son similares a las que teníamos antes. Nos falta que puedan ser usadas desde fuera:
export { getApiAccount, getApiHero, getApiDetailedHeroItems }
Ya tenemos las dos funciones que llaman a las APIs preparadas, ahora solo queda llamarlas para gestionar la respuesta (promesa) desde la vista de Hero.
Volvamos a nuestra vista de Hero, al fichero /views/Hero/Index.vue
. En el bloque de JavaScript, escribimos lo siguiente:
<script> import setError from '@/mixins/setError' import BaseLoading from '@/components/BaseLoading' import { getApiHero, getApiDetailedHeroItems } from '@/api/search' export default { name: 'HeroView', mixins: [setError], components: { BaseLoading }, data () { return { isLoadingHero: false, isLoadingItems: false, hero: null, items: null } }, computed: {}, created () { this.isLoadingHero = true this.isLoadingItems = true const { region, battleTag: account, heroId } = this.$route.params getApiHero({ region, account, heroId }) .then(({ data }) => { this.hero = data }) .catch((err) => { this.hero = null const errObj = { routeParams: this.$route.params, message: err.message } if (err.response) { errObj.data = err.response.data errObj.status = err.response.status } this.setApiErr(errObj) this.$router.push({ name: 'Error' }) }) .finally(() => { this.isLoadingHero = false }) getApiDetailedHeroItems({ region, account, heroId }) .then(({ data }) => { this.items = data }) .catch((err) => { this.items = null console.log(err) }) .finally(() => { this.isLoadingItems = false }) } } </script>
Vayamos por partes. Lo primero, importar el mixin de error y el componente Loading. En caso de error, usaremos el mixin. Mientras hagamos las llamadas a las APIs, usaremos el componente Loading hasta que se carguen los datos. Como tenemos dos llamadas de APIs (hero & items) distintas vamos a tener dos componentes Loading, uno para cada llamada.
En la sección de variables, hemos definido la variable hero
y la variable items
. Las dos llamadas que vamos a hacer son independientes la una de la otra, por lo tanto se van a hacer en paralelo. Por eso hemos incluido otras dos variables de control para saber si están loading o no:
data () { return { isLoadingHero: false, isLoadingItems: false, hero: null, items: null } }
Lo siguiente es hacer las llamadas a las dos APIs. Ponemos el loading a true
, llamamos a las APIs.
En caso de error hacemos uso del mixin y si todo va bien, guardamos el resultado en la variable correspondiente.
Por último, ponemos los loading a false
. Con esto vamos a poder controlar la visibilidad del componente de Loading.
Vemos que, efectivamente, se están haciendo las dos llamadas a las APIs:
>
Personalmente, creo que esta es la página más divertida de toda la app 🤙🤩🎉.
Vamos a pintar en pantalla:
- Los atributos (fuerza, vida, inteligencia, etc.) del personaje, incluyendo sus recursos:
- Recursos:
- Recursos:
- Sus habilidades, tanto las activas como las pasivas (si las tiene, depende del nivel del personaje) y las runas (en caso de que tenga alguna, también dependen del nivel).
En esta parte veremos como crear componentes asíncronos (parecido a lo que hacíamos con las rutas y el lazy load, solo serán cargados cuando se requieran) - Los objetos, junto con las joyas o gemas que puedan tener engarzadas.
- Para los objetos, pintaremos una barra de color según la calidad de los objetos: https://eu.diablo3.com/es/game/guide/items/equipment#item-quality
- Color blanco: Normal
- Color azul: Mágico
- Color amarillo: Raro
- Color verde: Conjunto (otorgan bonificación extra cuando llevas el set completo)
- Color naranja: Legendarios
- Vamos a construir algo parecido a esto, que es como se ven los objetos del personaje (en PC, para consola cambia):
Aquí hay objetos azules (mágicos) y amarillos (raros). Además vemos algunas gemas, por ejemplo, en el arma. Algo parecido a esto es lo que vamos a construir con los datos que nos devuelva la API de items.
- Para los objetos, pintaremos una barra de color según la calidad de los objetos: https://eu.diablo3.com/es/game/guide/items/equipment#item-quality
Lo primero es crear la estructura de carpetas. ¡Empecemos!
Vamos a /views/Hero
y creamos tres carpetas: HeroAttributes
, HeroItems
y HeroSkills
. Creamos también el componente HeroDetailHeader.vue
, que no va a estar agrupado en carpetas. Deberías tener una estructura como esta:
📂 /Hero ├──📂 /HeroAttributes ├──📂 /HeroItems ├──📂 /HeroSkills ├── HeroDetailHeader.vue └── Index.vue
Ahora, en nuestro componente /Hero/Index.vue
, traemos el componente HeroDetailHeader:
import HeroDetailHeader from './HeroDetailHeader'
Y lo damos de alta para poder usarlo:
components: { BaseLoading, HeroDetailHeader }
Usamos los componentes:
<template> <div class="hero-view"> <BaseLoading v-if="isLoadingHero"/> <HeroDetailHeader v-if="hero" :detail="detailHeader"/> </div> </template>
> Por ahora, ignora los errores
Los datos que necesita el componente HeroDetailHeader
son los siguientes: name
, class
, gender
, level
, hardcore
, seasonal
, paragonLevel
, alive
y seasonCreated
. Todos estos datos los sacamos de la variable this.hero
, que es donde guardamos los datos que hemos recuperado de la API.
Creamos una computed llamada detailHeader que nos retorne estos valores:
computed: { detailHeader () { // Asignamos valores a través de const { name, // valor: alias class: classSlug, gender, level, hardcore, seasonal, paragonLevel, alive, seasonCreated } = this.hero return { name, classSlug, gender, level, hardcore, seasonal, paragonLevel, alive, seasonCreated } } }
> 📗 Asignación por desestructuración: https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Operadores/Destructuring_assignment
Esto es lo que recibe el componente HeroDetailHeader
en la prop detail
.
Ahora, abrimos el recién creado componente de HeroDetailHeader
y le damos este contenido:
- JavaScript
<script> import heroName from '@/mixins/heroName' export default { name: 'HeroDetailHeader', mixins: [heroName], props: { detail: { type: Object, required: true } }, computed: { heroClass () { const gender = this.detail.gender === 0 ? 'male' : 'female' return `hero-${this.detail.classSlug} ${gender}` } } } </script>
Con la computed heroClass
vamos a crear la clase de CSS necesaria para mostrar la cara correspondiente a nuestro personaje. Ya lo hemos usado con anterioridad; estamos usando las mismas clases para generar el avatar de nuestro héroe.
- CSS:
<style lang="stylus" scoped> .hero-detail-avatar width 138px height 105px background-size 280px .text-bone color #e8dcc2 </style>
- HTML
<template> <b-row class="hero-detail-header my-5"> <b-col cols="12"> <!-- Avatar --> <div class="d-flex justify-content-center mb-3"> <div class="hero-detail-avatar" :class="heroClass"></div> </div> <div class="text-center"> <!-- Nombre --> <h1 class="font-diablo text-truncate text-bone">{{ detail.name }}</h1> <div class="text-monospace"> <small> <!-- Nivel --> <span>{{ detail.level }}</span> <!-- Nivel de Leyenda --> <span class="text-info" v-if="detail.paragonLevel"> <span class="text-white"> · </span> ({{ detail.paragonLevel }}) </span> <!-- Clase (A través del Mixin) --> <span> · {{classToName(detail.classSlug)}}</span> <!-- ¿Es de temporada? --> <span v-if="detail.seasonal" class="text-success"> · Seasonal </span> <!-- ¿Es hardcore? --> <span v-if="detail.hardcore" class="text-danger"> · Hardcore </span> </small> <div> <!-- En qué temporada ha sido creado el héroe --> <small class="text-muted"> Season created: </small> <b-badge>{{ detail.seasonCreated }}</b-badge> </div> </div> <hr> </div> </b-col> </b-row> </template>
En el HTML lo único que estamos haciendo es pintar, en el centro, los datos que recibimos del componente padre.
Si todo va bien, deberías ver algo como esto:
Volvemos al componente /Hero/Index.vue
y ponemos lo siguiente en el HTML:
<template> <div class="hero-view"> <BaseLoading v-if="isLoadingHero"/> <HeroDetailHeader v-if="hero" :detail="detailHeader"/> <b-row> <!-- 12 columnas de 'xs' -> 'md', 8 columnas desde 'lg' hacia arriba --> <!-- En 'lg' orden 2 --> <b-col md="12" lg="8" order-lg="2"> <BaseLoading v-if="isLoadingItems"/> </b-col> <!-- 12 columnas de 'xs' -> 'md', 4 columnas desde 'lg' hacia arriba --> <!-- En 'lg' orden 1 --> <b-col md="12" lg="4" order-lg="1"> <template v-if="hero"> <HeroAttributes :attributes="detailStats"/> <HeroSkills :skills="hero.skills"/> </template> </b-col> </b-row> </div> </template>
El componente de DetailHeader está a 12 columnas (es decir el 100%). Para los demás componentes vamos a crear 2 columnas, como vimos en una imagen anteriormente. A la izquierda (atributos y habilidades) una columna de 4 unidades (sobre 12) y a la derecha (objetos) otra columna de 8 (sobre 12).
En tamaño de pantalla pequeño, lo primero que veremos es el bloque de 8 columnas, es decir, los objetos del personaje. Pero para pantallas grandes estamos alterando el orden de aparición a con la propiedad order
de flexbox: https://bootstrap-vue.js.org/docs/components/layout/#reordering.
Seguimos en /Hero/Index.vue
. Importamos los componentes de atributos y habilidades y los registramos:
import HeroAttributes from './HeroAttributes/Index' import HeroItems from './HeroItems/Index'
components: { BaseLoading, HeroDetailHeader, HeroAttributes, HeroSkills }
El componente de Skills recibe el dato intacto de hero.skills
. Para el componente de Attributes necesitamos crear una computed que haga alguna transformación. En este caso la transformación es muy simple. Cogemos los datos de hero.stats
y le agregamos la clase (tipo) de personaje, que la necesitaremos en un componente hijo más adelante:
computed: { detailStats () { // Devuelve el contenido de stats y agrega classSlug return { ...this.hero.stats, classSlug: this.hero.class } } }
Para evitar errores en consola, vamos a crear también el fichero de HeroSkills. Dentro de la carpeta creamos su Index.vue
correspondiente y le damos este contenido:
/Hero/HeroSkills/Index.vue
<template> <h1>Skills</h1> </template> <script> export default { name: 'HeroSkills' } </script>
Con esto ya podemos ir a pintar los componentes de atributos (y también de habilidades).
Dentro de HeroAttributes
tendremos tres componentes: atributos primarios, atributos secundarios y recursos.
Vamos a crear el componente Index.vue
en la carpeta /HeroAttributes
, que está vacía. Además, dentro la misma, creamos otros dos componentes: HeroAttributeList.vue
y HeroResources.vue
Abrimos /HeroAttributes/Index.vue
y agregamos lo siguiente:
<script> // Importamos los componentes import HeroAttributeList from './HeroAttributeList' import HeroResources from './HeroResources' // Definimos: // Los atributos principales const coreAttributes = ['strength', 'dexterity', 'vitality', 'intelligence'] // Los atributos secundarios const secondaryAttributes = ['damage', 'toughness', 'healing'] // Los recursos const resources = ['life', 'primaryResource', 'secondaryResource'] export default { name: 'HeroAttributes', components: { HeroResources, HeroAttributeList }, // Definimos la propiedad props: { attributes: { type: Object, required: true } }, computed: { // Creamos el objeto de atributos principales coreAttributes () { return coreAttributes.map(item => ({ name: item, val: this.attributes[item] })) }, // Creamos el objeto de atributos principales secondaryAttributes () { return secondaryAttributes.map(item => ({ name: item, val: this.attributes[item] })) }, resources () { // Creamos el objeto de recursos // Agregamos el tipo de personaje `classSlug` (necesario para los Sprites CSS) const data = { classSlug: this.attributes.classSlug, resources: {} } resources.forEach(item => { data.resources[item] = { name: item, val: this.attributes[item] } }) return data } } } </script>
Los arrays que acabamos de definir nos sirven para agrupar las claves que necesitamos en cada bloque.
Hemos agrupado en atributos principales (core), secundarios y recursos.
Todos los héroes tienen vida y recurso primario, pero solamente algunos tienen recurso secundario.
El HTML, que también es bastante sencillo, es el siguiente:
<template> <div class="h-attriubutes"> <!--Título--> <h2 class="font-diablo">Attributes</h2> <hr class="bg-white"> <div class="attributes"> <!-- Atributos Principales--> <div class="core"> <HeroAttributeList :attributes="coreAttributes"/> </div> <hr> <!-- Atributos Secundarios--> <div class="secondary"> <HeroAttributeList :attributes="secondaryAttributes"/> </div> </div> <hr> <!-- Recursos --> <div class="resources"> <HeroResources :resources="resources"/> </div> </div> </template>
Para que te hagas la idea, un ejemplo de atributos primarios serían estos:
[ { "name":"strength", "val":77 }, { "name":"dexterity", "val":77 }, { "name":"vitality", "val":4813 }, { "name":"intelligence", "val":9660 } ]
Hemos definido fuerza, destreza, vitalidad e inteligencia como atributos primarios. Solo nos quedaría mostrarlos por pantalla.
Un ejemplo de atributos secundarios serían estos:
[ { "name":"damage", "val":986514 }, { "name":"toughness", "val":15263800 }, { "name":"healing", "val":1305200 } ]
Daño, dureza y curación serían nuestros atributos secundarios. Como ves, son idénticos a los primarios (a nivel de estructura de datos) y por lo tanto podemos utilizar el mismo componente para pintar los dos tipos de atributos. Simplemente les pasamos distinta información, pero mismo formato.
El componente /Hero/HeroAttributes/HeroAttributeList.vue
es muy sencillo. Lo único que hace es mostrar el nombre del atributo (en color naranja) y su valor (en color blanco), pasado por el filtro de numeral (que ya hemos usado anteriormente):
<template> <ul class="list-unstyled"> <!-- Itera --> <li v-for="attr in attributes" :key="attr.name" class="mb-1"> <div class="d-flex justify-content-between"> <!-- Nombre atributo --> <div class="pl-2 text-capitalize name-text">{{ attr.name }}</div> <!-- Valor formateado --> <div class="px-2 text-monospace">{{ attr.val | formatNumber }}</div> </div> </li> </ul> </template> <script> import { formatNumber } from '@/filters/numeral' export default { name: 'AttributeList', filters: { formatNumber }, props: { attributes: { type: Array, required: true } } } </script> <style lang="stylus"> ul li .name-text color #efb45d font-weight 600 </style>
La app, actualmente, se ve así aunque tengamos errores:
Recursos
Todas las clases tienen, además de los puntos de vida (HP), un recurso propio. Los recursos son: furia (bárbaro), cólera (cruzado), odio y disciplina (cazador de demonios), esencia (nigromante), espíritu (monje), maná (médico brujo) y poder arcano (mago).
En este bloque, vamos a pintar los puntos de vida que tiene el personaje y su recurso correspondiente. Tenemos cargados todos los recursos (incluyendo la vida) en una imagen, a modo de sprite. Esta es la imagen que usaremos:
Vamos a crear las clases CSS correspondientes para cada tipo de héroe.
¿Recuerdas en dónde tenemos los estilos globales de CSS? Si pensaste que era /src/assets/css/main.styl
, has acertado. Abrimos el archivo y le agregamos lo siguiente:
// --------------------- // Resources // --------------------- .resource .resource-icon background-image url('../img/resources.png') width 50px height 50px &.resource-mana background-position 0 -50px &.resource-fury background-position: -50px 0 &.resource-hatred-discipline background-position: -100px 0px &.resource-spirit background-position: -50px -50px &.resource-arcane-power background-position: -100px -50px &.resource-wrath background-position: 0px -100px &.resource-essence background-position: -50px -100px
Como has podido ver, es muy sencillo este bloque de CSS. Cargamos la imagen y nos vamos moviendo de 50 en 50 por la imagen 😃 según el recurso que seleccionemos.
Al igual que hemos hecho antes con los nombres de los héroes, vamos a crear un mixin para mostrar el nombre normalizado de los recursos. Para ello vamos a la carpeta donde están los mixins y creamos un nuevo fichero. De nombre le ponemos resources.js
y el contenido va a ser el siguiente:
const names = { BARBARIAN: 'barbarian', CRUSADER: 'crusader', MONK: 'monk', WIZARD: 'wizard', WITCHDOCTOR: 'witch-doctor', NECROMANCER: 'necromancer', DEMONHUNTER: 'demon-hunter' } const resourceClassName = { [names.BARBARIAN]: 'fury', [names.CRUSADER]: 'wrath', [names.MONK]: 'spirit', [names.WIZARD]: 'arcane-power', [names.WITCHDOCTOR]: 'mana', [names.NECROMANCER]: 'essence', [names.DEMONHUNTER]: 'hatred-discipline' } const resourceDisplayName = { [names.BARBARIAN]: 'Fury', [names.CRUSADER]: 'Wrath', [names.MONK]: 'Spirit', [names.WIZARD]: 'Arcane Power', [names.WITCHDOCTOR]: 'Mana', [names.NECROMANCER]: 'Essence', [names.DEMONHUNTER]: 'Hatred / Discipline' } export default { methods: { /** * Get the name of the primary resource by class * @param classSlug {String} * @returns {String} */ resourceClassName (classSlug) { return resourceClassName[classSlug] }, /** * Resource Normalized name * @param classSlug {String} * @returns {String} */ resourceDisplayName (classSlug) { return resourceDisplayName[classSlug] } } }
Regresamos al componente de recursos, abrimos el archivo /HeroAttributes/HeroResources.vue
y ponemos lo siguiente:
<template> <div class="resource-wrapper"> <div class="resource"> <div class="d-flex justify-content-start align-items-center"> <!-- Imagen Vida --> <div class="resource-icon resource-life"/> <!-- Nombre --> <div class="ml-3 text-uppercase name-text"> <span>{{ resources.resources.life.name }}</span> </div> <!-- Valor --> <div class="ml-3"> <span class="text-monospace"> {{ resources.resources.life.val | formatNumber }} </span> </div> </div> </div> <hr> <div class="resource"> <div class="d-flex justify-content-start align-items-center"> <!-- Imagen Recurso --> <div class="resource-icon" :class="classResourceName"/> <!-- Nombre --> <div class="ml-3 text-uppercase name-text" :class="'resource-name-' + resources.classSlug"> <span>{{ displayResourceName }}</span> </div> <div class="ml-3"> <!-- Valor --> <span class="text-monospace"> {{ resources.resources.primaryResource.val | formatNumber }} <template v-if="hasSecondaryResource"> <!-- Valor recurso secundario --> <span class="mx-0 text-muted">/</span> <span> {{ resources.resources.secondaryResource.val | formatNumber }} </span> </template> </span> </div> </div> </div> </div> </template> <script> import resources from '@/mixins/resources' import { formatNumber } from '@/filters/numeral' export default { name: 'HeroResources', mixins: [resources], filters: { formatNumber }, props: { resources: { required: true, type: Object } }, computed: { classResourceName () { return `resource-${this.resourceClassName(this.resources.classSlug)}` }, displayResourceName () { return this.resourceDisplayName(this.resources.classSlug) }, // Solo demon-hunter tiene recurso secundario hasSecondaryResource () { return this.resources.classSlug === 'demon-hunter' } } } </script> <style lang="stylus"> .resource .name-text color #efb45d .resource-name-demon-hunter width auto @media (min-width: 992px) .resource .resource-name-demon-hunter width 100px </style>
Recibimos datos a través de una prop y los mostramos, eso es todo lo que hacemos aquí. Con esto funcionando, la app debería verse así:
Ahí vemos la esencia , que es el recurso del nigromante. Si probamos a cambiar de clase, vemos que se carga el recurso correspondiente. En los ejemplos de abajo vemos la ira del cruzado y el odio / disciplina del cazador de demonios.
> ✏️ Ves al navegador y prueba a cambiar los estilos CSS de la imagen de los recursos.
> ¿Qué pasa si pones background-position: -25px 75px;
al elemento un recurso cualquiera? Deja tus respuestas en el sistema de comentarios
Con esto ya hemos terminado el bloque de atributos y recursos del personaje. Vamos a seguir con la siguiente parte, que es la de habilidades.
Skills
Cada personaje puede tener hasta 6 habilidades activas, 2 de ratón (botón primario y secundario) y 4 de teclado. Las habilidades se van desbloqueando según el nivel, no tienes todas las habilidades disponibles desde el inicio.
A su vez, las habilidades activas pueden tener modificadores o runas de habilidad que mejoren dicha habilidad. Al igual que con las habilidades, las runas se van desbloqueando cuando vas subiendo de nivel.
Todo esto corresponde a las habilidades activas. Existen otro grupo de habilidades, las habilidades pasivas. Como las demás, se van ganando al subir de nivel.
Aquí puedes ver, a modo ejemplo, el progreso de niveles y habilidades del nigromante: https://eu.diablo3.com/es/class/necromancer/progression
Una vez entendido esto, podemos ir a crear los componentes necesarios. Vamos a tener componentes agrupados en habilidades activas y habilidades pasivas.
¡Hagámoslo! Dentro de nuestra carpeta de /Hero/HeroSkills
creamos los siguientes archivos: ActiveSkills.vue
, ActiveSkill.vue
, PassiveSkills.vue
y PassiveSkill.vue
.
El contenido de /Hero/HeroSkills/Index.vue
va a ser el siguiente (de momento):
<template> <div class="skills-wrapper mt-5"> <h2 class="font-diablo">Skills</h2> <hr class="bg-white"> <ActiveSkills :skills="skills.active"/> <hr> <PassiveSkills :skills="skills.passive"/> </div> </template> <script> import ActiveSkills from './ActiveSkills' import PassiveSkills from './PassiveSkills' export default { name: 'HeroSkills', components: { ActiveSkills, PassiveSkills }, props: { skills: { required: true, type: Object } } } </script>
Estamos cargando los skills activos y los pasivos, sin más.
Vamos a editar los componentes de las habilidades, empezando por el habiliades activas.
Como tenemos un array de skills lo que vamos a hacer es iterar, con v-for
, para utilizar el componente de habilidad individual, ActiveSkill
.
ActiveSkills.vue
:
<template> <div class="active-skills"> <h4 class="my-5">Active</h4> <div class="skills"> <b-row> <b-col v-for="(skill, idx) in skills" :key="idx" cols="6" lg="12"> <ActiveSkill :skill="skill.skill" :rune="skill.rune" :slot-num="idx+1"/> </b-col> </b-row> </div> </div> </template> <script> import ActiveSkill from './ActiveSkill' export default { name: 'ActiveSkills', components: { ActiveSkill }, props: { skills: { type: Array, required: true } } } </script>
Antes de cargar el listado de habilidades activas, necesitamos editar el fichero global de CSS, que es dónde estamos guardando los estilos para los Sprites de imágenes.
Para identificar qué habilidad estamos mostrando, vamos a agregar estas líneas de código en /assets/css/main.styl
:
// --------------------- // Active Skills // --------------------- .active-skills .skills .slot display block width 22px height 22px background url("../img/skill-overlays.png") 0 0 position absolute top -5px left 5px &.slot-1 background-position: 0 -1px &.slot-2 background-position: -21px -1px &.slot-3 background-position: 0 -23px &.slot-4 background-position: -23px -23px &.slot-5 background-position: 0 -46px &.slot-6 background-position: -23px -46px
Con slot
nos estamos refiriendo a qué habilidad es, siendo slot-1
el botón principal del ratón y slot-2
el botón secundario. Aguanta un poco, que con un ejemplo lo verás mejor.l
ActiveSkill.vue
:
<template> <div class="d-flex align-items-center mb-3"> <div class="mr-2"> <!-- Slot (identificador de skill) --> <span class="slot" :class="slotClass"/> <!-- La imagen · Skill URL --> <img :src="skillUrl" :alt="skill.name"> </div> <div> <!-- Nombre de la habilidad --> <p class="skill-name m-0" :title="skill.description"> {{ skill.name }} </p> <!-- Runa (si tiene) --> <small v-if="rune" class="rune-name text-muted" :title="rune.description"> {{ rune.name }} </small> </div> </div> </template> <script> export default { name: 'ActiveSkill', props: { skill: { required: true, type: Object }, rune: { required: false, type: Object }, slotNum: { required: true, type: Number || String } }, computed: { skillUrl () { // Posibles tamaños (px) const sizes = [21, 42, 64] // API URL para imágenes const host = `http://media.blizzard.com/d3/icons/skills/${sizes[1]}/` // Ejemplo: // http://media.blizzard.com/d3/icons/skills/42/p6_necro_bonespikes.png return `${host}${this.skill.icon}.png` }, // Clase CSS para los slots slotClass () { return `slot-${this.slotNum}` } } } </script>
Skill Images
En las propiedades estamos recibiendo la habilidad, la runa en caso de que la tenga (si no llega, no la mostramos) y el número de slot, que corresponde con las clases de CSS que hemos creado recientemente.
> 📗 Documentación para obtener las URLs de las habilidades y de los objetos de Diablo III: https://develop.battle.net/documentation/diablo-3/community-apis
Hacemos la composición de la URL, tenemos la base de la URL, el tipo (skills), el tamaño (42) y el nombre del icon (que nos lo da la API). Un ejemplo sería este: http://media.blizzard.com/d3/icons/skills/42/p6_necro_bonespikes.png
Que renderiza esta imagen:
Gracias al atributo title
, si pasamos el ratón por encima del elemento, podemos ver una breve descripción.
Perfecto, ya tenemos las habilidaes activas de nuestro personaje cargadas, que se ven así en nuestra app:
Hora de cargar las habilidades pasivas, que son casi lo mismo que las activas, pero más sencillas. Solo imagen y nombre de la habilidad.
PassiveSkills.vue
:
<template> <div class="passive-skills"> <h4 class="my-5">Passive</h4> <div class="skills"> <b-row> <b-col v-for="(skill, idx) in skills" :key="idx" cols="6" lg="12"> <PassiveSkill :skill="skill.skill"/> </b-col> </b-row> </div> </div> </template> <script> import PassiveSkill from './PassiveSkill' export default { name: 'PassiveSkills', components: { PassiveSkill }, props: { skills: { type: Array, required: true } } } </script>
PassiveSkill.vue
:
<template> <div class="d-flex align-items-center mb-3"> <div class="mr-2"> <!-- Imagen Habilidad Pasiva --> <img :src="skillUrl" :alt="skill.name"> </div> <div> <!-- Nombre --> <p class="skill-name m-0" :title="skill.description"> {{ skill.name }} </p> </div> </div> </template> <script> export default { name: 'PassiveSkill', props: { skill: { required: true, type: Object } }, computed: { skillUrl () { const sizes = { 21: 21, 42: 42, 64: 64 } const host = `http://media.blizzard.com/d3/icons/skills/${sizes[42]}/` return `${host}${this.skill.icon}.png` } } } </script>
Bien, ya tenemos las habilidades activas y las pasivas funcionando. Se deberían ver así:
Puede darse el caso en el que si estás usando el perfil de un personaje que no está al nivel máximo, no tengas todas las habilidades (activas, pasivas y runas) desbloqueadas y por lo tanto veas menos habilidades.
Con esto hemos terminado la parte de skills... en modo normal. En el siguiente bloque vamos a ver cómo refactorizar el bloque de habilidades y crear componentes asíncronos dinámicos 🤘.
{{ detail.name }}
Skills
Con esto ya podemos ir a pintar los componentes de atributos (y también de habilidades).Dentro de HeroAttributes tendremos tres componentes: atributos primarios, atributos secundarios y recursos.Vamos a crear el componente Index.vue en la carpeta /HeroAttributes, que está vacía. Además, dentro la misma, creamos otros dos componentes: HeroAttributeList.vue y HeroResources.vueAbrimos /HeroAttributes/Index.vue y agregamos lo siguiente:Los arrays que acabamos de definir nos sirven para agrupar las claves que necesitamos en cada bloque.Hemos agrupado en atributos principales (core), secundarios y recursos.Todos los héroes tienen vida y recurso primario, pero solamente algunos tienen recurso secundario.El HTML, que también es bastante sencillo, es el siguiente:Attributes
- {{ attr.name }}{{ attr.val | formatNumber }}
Skills
Active
{{ skill.name }}
{{ rune.name }}Passive
{{ skill.name }}