Hero View

22/27

Lectura

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:

req-1.png
req-2.pngAhora 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:

> wireframe.png📗 Si nunca has jugado a Diablo III, te recomiendo que leas esta guía de controles de combate: https://eu.diablo3.com/es/game/guide/gameplay/combat-skills

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:
      resources-preview.jpg
  • 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):
      items.jpg
      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:

📂 /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:

preview-1.pngCon 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:

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:

preview-2.png

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:

resources

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í:

preview-3

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.

cruzado
demon-hunter

> ✏️ 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:
p6_necro_bonespikes.png

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:
preview-4

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í:

preview-5

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 🤘.

Aportes 9

Preguntas 1

Ordenar por:

Los aportes, preguntas y respuestas son vitales para aprender en comunidad. Regístrate o inicia sesión para participar.

En respuesta a la pregunta de la clase: Si se le coloca esa propiedad a algún elemento de recurso sucede que la imagen se descoloca y se visualizan 4 recursos pero todos recordados de mala forma. Al estar trabajando con 1 sola imagen para todos los recursos el posicionamiento debería ser el adecuado y la linea que se le añade lo distorsiona.

Bufff, este bloque estuvo largo pero logré entenderlo, hubo un punto en el que me frustré porque habían componentes sin contenido y no podía ver los resultados:( Pero todo genial hasta ahora, aunque en la página en producción no se ven las habilidades pasivas ni las imágenes…

Vaya fumada de clase, casi que no la saco adelante :c

Pensé que el tema se refería a algo parecido a Hero Widget como en Flutter, me alegré muchísimo pensando eso, pero nada que ver

Vi que en utilizas el forEach y map, ¿En qué casos debo utilizar uno el otro? , Siempre me causa conflicto en cual situación debo utilizarlo

Vaya clase! bastante contenido, pero al final de tanto analizar se entiende bien…
Solo una observación…

Cuando nos piden que importemos los componentes de atributos y habilidades y los registremos los mismos en el /Hero/Index.vue.

Nos indican importar el ‘HeroItems’

import HeroAttributes from './HeroAttributes/Index'
import HeroItems from './HeroItems/Index'

Pero registramos el ‘HeroSkills’

components: {
  BaseLoading,
  HeroDetailHeader,
  HeroAttributes,
  HeroSkills
}

En mi caso imaginé que la importación pertenecia al HeroSkills ya que comenzamos a trabajar con el posteriormente, de igual forma está un poco confuso, si lo pudieran aclarar les agredezco!

Si las imagenes no les cargan es porque en el la url falto un / justo en esta parte

const host = `http://media.blizzard.com/d3/icons/skills/${sizes[1]}`

al final de la url debe ir el / ya que la url debe ser asi

http://media.blizzard.com/d3/icons/<type>/<size>/<icon>.png

no asi

http://media.blizzard.com/d3/icons/<type>/<size><icon>.png

Cada vez se ha vuelto mas complejo, pero todo bien!

Crei que nunca acabaria esta leccion no entiendo nada del juego pero si el codigo casi todo 😄