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:
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.
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} * @paramregion{String} * @paramaccount{String} * @paramheroId{String} * @returns{Promise} */functiongetApiHero({ region, account, heroId }){const resource =`d3/profile/${account}/hero/${heroId}`constAPI_URL=`${protocol}${region}${host}${resource}`const locale = locales[region]const params ={'access_token': store.state.oauth.accessToken, locale
}returnget(API_URL,{ params })}/**
* Returns a list of items for the specified hero.
* GET – /d3/profile/{account}/hero/{heroId}/items
* @paramregion{String} * @paramaccount{String} * @paramheroId{String} * @returns{Promise} */functiongetApiDetailedHeroItems({ region, account, heroId }){const resource =`d3/profile/${account}/hero/${heroId}/items`constAPI_URL=`${protocol}${region}${host}${resource}`const locale = locales[region]const params ={access_token: store.state.oauth.accessToken, locale
}returnget(API_URL,{ params })}
No requieren mucha explicación, son similares a las que teníamos antes. Nos falta que puedan ser usadas desde fuera:
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:
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:
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:
Ahora que ya tenemos los datos necesarios, vamos a empezar con los componentes de la vista. La estructura de componentes que vamos a seguir es esta:
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:
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)
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.
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:
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: aliasclass: classSlug, gender, level, hardcore, seasonal, paragonLevel, alive, seasonCreated
}=this.heroreturn{ name, classSlug, gender, level, hardcore, seasonal, paragonLevel, alive, seasonCreated
}}}
Con la computedheroClass 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.
<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:
Con esto hemos terminado el componente de detailHeader. A continuación vamos a trabajar con el componente de Atributos.
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:
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 classSlugreturn{...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:
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:
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):
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:
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'}exportdefault{methods:{/**
* Get the name of the primary resource by class
* @paramclassSlug{String} * @returns{String} */resourceClassName(classSlug){return resourceClassName[classSlug]},/**
* Resource Normalized name
* @paramclassSlug{String} * @returns{String} */resourceDisplayName(classSlug){return resourceDisplayName[classSlug]}}}
Regresamos al componente de recursos, abrimos el archivo /HeroAttributes/HeroResources.vue y ponemos lo siguiente:
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.
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):
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.
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:
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
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.
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 🤘.