Objetos del Héroe

24/27

Lectura

Actualmente nuestra vista de Hero tiene un bloque de cabecera, otro de atributos (stats) del personaje y el último, con las habilidades.

Lo siguiente que vamos a hacer es mostrar los items del personaje. La API de items nos devuelve un objeto que tiene este formato:

{
  "mainHand": {
    "id":"P6_Unique_Scythe1H_04",
    "name":"Jesseth Skullscythe",
    "icon":"p6_unique_scythe1h_04_demonhunter_male",
    "displayColor":"green",
    "tooltipParams":"/item/jesseth-skullscythe-P6_Unique_Scythe1H_04",
    "requiredLevel":70,
    "itemLevel":1,
    "stackSizeMax":0,
    "accountBound":true,
    "flavorText":"This assembly of sharpened bones will make you a true artist in combat.",
    "typeName":"Ancient Set Scythe",
    "type": {
      "twoHanded":false,
      "id":"scythe1h"
    },
    "armor":0,
    "damage":"1682-2259 Damage\n1.38 Attacks per Second",
    "dps":"2,715.3",
    "attacksPerSecond":1.3779999,
    "minDamage":1682,
    "maxDamage":2259,
    "slots":"mainHand",
    "attributes": {
      "primary": [
        "+1433-1798 Damage",
        "+998 Intelligence",
        "+899 Vitality",
        "Increases Attack Speed by 6%"
      ],
      "secondary": [
        "+19 Maximum Essence",
        "Monster kills grant +254 experience."
      ]
    },
    "openSockets":0,
    "gems": [
      {
        "item": {
          "id":"x1_Emerald_10",
          "slug":"flawless-royal-emerald",
          "name":"Flawless Royal Emerald",
          "icon":"x1_emerald_10_demonhunter_male",
          "path":"item/flawless-royal-emerald-x1_Emerald_10"
        },
        "attributes": [
          "Critical Hit Damage Increased by 130.0%"
        ],
        "isGem":true,
        "isJewel":false
      }
    ],
    "seasonRequiredToDrop":-1,
    "isSeasonRequiredToDrop":false
  }
}

Este modelo de datos corresponde al slot del arma (mano principal o main hand). Tenemos 13 tipos de objetos (items) distintos, que son los siguientes: cabeza (head), cuello (neck), pecho (torso), hombros (shoulders), piernas (legs), cintura (waist), manos (hands), brazales (bracers), pies (feet), dedo izquierdo (leftFinger), dedo derecho (rightFinger), mano dominante (mainHand) y mano secundaria (offHand).

📗 Aquí tienes más información acerca de los objetos del juego: https://eu.diablo3.com/es/item/

Según la documentación, para obtener la imagen de un objeto, tenemos que usar esta url http://media.blizzard.com/d3/icons/items/large/ + la propiedad icon + .png.

Si quisiéramos obtener la imagen del arma principal (mainHand) tendríamos que poner lo siguiente:
http://media.blizzard.com/d3/icons/items/large/p6_unique_scythe1h_04_demonhunter_male.png

Que se corresponde con esta imagen:
mainHand

Como has podido ver, cada item tiene una gran cantidad de propiedades que podríamos usar para crear nuestra app, pero para evitar complejidad y no alargar mucho el tema, no vamos a usar todas.
Aparte de la propiedad icon, vamos a usar name, displayColor (para saber la calidad del objeto) y gems (para saber si tiene gemas o joyas).

La estructura HTML que vamos a seguir para mostrar los objetos de nuestro personaje en pantalla es la misma que se ve en este boceto, en el bloque de items:

wireframe

Un pequeño problema que podríamos tener es que nuestro personaje no tenga objetos equipados en todos los huecos posibles. Por ejemplo, puede darse el caso de que el personaje no tenga equipado el objeto de legs. Si esto pasara, se nos descuadraría la cuadrícula o se mostraría vacía. Es nuestro deber, como developers, controlar estos posibles casos.


La lista de objetos de nuestro personaje descansa en la variable items del componente /views/Hero/Index.vue. Tenemos que crear el componente de HeroItems para poder usarlo.

Dentro del directorio /HeroItems creamos nuestro componente principal Index.vue y le damos este contenido:

<template>
  <div>
    <h1>HeroItems</h1>
  </div>
</template>

<script>
export default {
  name: 'HeroItems'
}
</script>

Ahora, desde el componente principal de la vista Hero, es decir, en /views/Hero/Index.vue hacemos el ritual de siempre: traer, declarar y utilizar el componente:

// Traer
import HeroItems from './HeroItems/Index'
// Declarar
components: {
  BaseLoading,
  HeroDetailHeader,
  HeroAttributes,
  HeroSkills,
  HeroItems
}

Por último, lo usamos:

<template>
  <div class="hero-view">
    <BaseLoading v-if="isLoadingHero"/>
    <HeroDetailHeader v-if="hero" :detail="detailHeader"/>

    <b-row>
      <b-col md="12" lg="8" order-lg="2">
        <BaseLoading v-if="isLoadingItems"/>
        <!-- Componente de Items del personaje -->
        <HeroItems v-if="items" :items="items"/>
      </b-col>

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

Debería verse así:
pv-1


HeroItem

Vamos a crear el componente correspondiente a un objeto, que luego reutilizaremos para pintar todos los objetos. A nivel de API, todos los objetos del personaje son iguales, por lo que no tenemos que hacer distinciones.

Empezamos creando el fichero /Hero/HeroItems/ItemDetail.vue. Continuamos con la definición básica de nuestro componente:

<template>
  <div>
    <h1>Item</h1>
  </div>
</template>

<script>

export default {
  name: 'ItemDetail',
  props: {
    item: {
      type: Object || undefined,
      required: true
    }
  }
}
</script>

<style lang="stylus">

</style>

Lo siguiente que vamos a definir son las clases CSS que necesitamos para este componente. Para ello, dentro de style agregamos lo siguiente:

.d3-icon-item
  min-height 100px
  // El borde de la caja va determinar la rareza del objeto, segun el color que tenga
  border-top-style solid
  border-top-width 4px

  &.item-none
    border-color transparent

  &.item-green
    border-color #8bc34a

  &.item-orange
    border-color #ff9800

  &.item-yellow
    border-color #ffeb3b

  &.item-blue
    border-color #03a9f4

  &.item-white
    border-color #a0aab5

Estas clases las vamos a usar para determinar la calidad de nuestro objeto. Recuerda que el objeto puede ser blanco (normal), azul (mágico), amarillo (raro), verde (de conjunto) o naranja (legendario).

En el HTML vamos a poner lo siguiente:

<template>
  <!-- Clase que determina el color -->
  <div class="text-center bg-dark h-100 pt-3 d3-icon-item" :class="itemClassColor">

    <div class="d-flex flex-column justify-content-between h-100">
      <!-- Si el item tiene ID, es que tenemos la información -->
      <!-- Es decir, que tiene un objeto equipado. -->
      <template v-if="item.id">
        <div>
          <div v-if="item" class="item-image">
            <!-- Nombre del objeto -->
            <p class="text-muted">{{ item.name }}</p>
            <!-- Imagen correspondiente al objeto -->
            <img :src="itemUrl" :alt="item.slotName + ' ' + item.name ">
          </div>
        </div>

        <div>
          <!-- No todos los objetos tienen gemas -->
          <!-- Por lo tanto, si el objeto tiene gemas engarzadas -->
          <template v-if="itemHasGems">
            <!-- Puede ser Gema o Joya -->
            <small>{{ gemOrJewel }}:</small>
            <ul class="list-inline">
              <!-- Un objeto puede tener varias gemas -->
              <li v-for="(gem, index) in item.gems" :key="'gem-'+index" class="list-inline-item">
                <!-- Componente gema -->
                <ItemDetailGem :gem="gem"/>
              </li>
            </ul>
          </template>
        </div>
      </template>
      <!-- En caso de que no tenga el objeto equipado -->
      <p v-else>
        <!-- Mostramos el nombre del slot y dejamos el contenido vacío -->
        <b-badge class="text-dark"> {{item.slotName}} </b-badge>
      </p>
    </div>

  </div>
</template>

Con los comentarios queda bastante claro que hace cada cosa.
Ahora necesitamos crear las computed properties que estamos usando en el template. Agregamos, en nuestro bloque de JavaScript, las siguientes funciones (computadas):

computed: {
  // Resuelve la URL de la imagen
  itemUrl () {
    const host = 'http://media.blizzard.com/d3/icons/items/large/'
    return `${host}${this.item.icon}.png`
  },
  // Comprueba si el item tiene gemas
  itemHasGems () {
    return Object.prototype.hasOwnProperty.call(this.item, 'gems')
  },
  // Si tiene gemas, comprueba si es Gema o Joya
  // Puede haber varias Gemas. Solo puede haber una Joya. No puede haber joyas y gemas mezcladas
  gemOrJewel () {
    return this.item.gems[0].isGem ? 'Gems' : 'Jewel'
  },
  // Clase CSS para saber la rareza
  itemClassColor () {
    if (Object.prototype.hasOwnProperty.call(this.item, 'displayColor')) {
      return `item-${this.item.displayColor}`
    }
    // Si no tiene color (es que no hay objeto equipado)
    return 'item-none'
  }
}

El código completo se ve así:

<template>
  <!-- Clase CSS que determina el color -->
  <div class="text-center bg-dark h-100 pt-3 d3-icon-item" :class="itemClassColor">

    <div class="d-flex flex-column justify-content-between h-100">
      <!-- Si el item tiene `id`, es que tenemos la información -->
      <!-- Es decir, que tiene un objeto equipado. -->
      <template v-if="item.id">
        <div>
          <div v-if="item" class="item-image">
            <!-- Nombre del objeto -->
            <p class="text-muted">{{ item.name }}</p>
            <!-- Imagen correspondiente al objeto -->
            <img :src="itemUrl" :alt="item.slotName + ' ' + item.name ">
          </div>
        </div>

        <div>
          <!-- No todos los objetos tienen gemas -->
          <!-- Por lo tanto, si el objeto tiene gemas engarzadas -->
          <template v-if="itemHasGems">
            <!-- Puede ser Gema o Joya -->
            <small>{{ gemOrJewel }}:</small>
            <ul class="list-inline">
              <!-- Un objeto puede tener varias gemas -->
              <li v-for="(gem, index) in item.gems" :key="'gem-'+index" class="list-inline-item">
                <!-- Componente gema -->
                <ItemDetailGem :gem="gem"/>
              </li>
            </ul>
          </template>
        </div>
      </template>
      <!-- En caso de que no tenga el objeto equipado -->
      <p v-else>
        <!-- Mostramos el nombre del slot y dejamos el contenido vacío -->
        <b-badge class="text-dark"> {{item.slotName}}</b-badge>
      </p>
    </div>

  </div>
</template>

<script>

export default {
  name: 'ItemDetail',
  props: {
    item: {
      type: Object || undefined,
      required: true
    }
  },
  computed: {
    itemUrl () {
      const host = 'http://media.blizzard.com/d3/icons/items/large/'
      return `${host}${this.item.icon}.png`
    },
    itemHasGems () {
      return Object.prototype.hasOwnProperty.call(this.item, 'gems')
    },
    gemOrJewel () {
      return this.item.gems[0].isGem ? 'Gems' : 'Jewel'
    },
    itemClassColor () {
      if (Object.prototype.hasOwnProperty.call(this.item, 'displayColor')) {
        return `item-${this.item.displayColor}`
      }
      return 'item-none'
    }
  }
}
</script>

<style lang="stylus">
  .d3-icon-item
    min-height 100px
    border-top-style solid
    border-top-width 4px

    &.item-none
      border-color transparent

    &.item-green
      border-color #8bc34a

    &.item-orange
      border-color #ff9800

    &.item-yellow
      border-color #ffeb3b

    &.item-blue
      border-color #03a9f4

    &.item-white
      border-color #a0aab5
</style>

Para terminar, vamos a crear el componente de la gema (o joya). Creamos un nuevo archivo ItemDetailGem.vue al mismo nivel de ItemDetail.vue, es decir, dentro de /HeroItems.

El código de este componente es muy simple:

<template>
  <img :src="gemUrl" :alt="gem.item.name" :title="gem.item.name">
</template>

<script>
export default {
  name: 'GemSlotItem',
  props: {
    gem: {
      required: true,
      type: Object
    }
  },
  computed: {
    gemUrl () {
      // Cambio de 'large' por 'small'
      const host = 'http://media.blizzard.com/d3/icons/items/small/'
      return `${host}${this.gem.item.icon}.png`
    }
  }
}
</script>

El único cambio que hay aquí es que en la URL de la imagen estamos usando el tamaño pequeño (small) en vez del grande (large) que usamos en los demás items.

Tenemos que importar este componente en el componente de detalle. Para ello, desde /Hero/HeroItems/ItemDetail.vue agregamos lo siguiente:

import ItemDetailGem from './ItemDetailGem'
export default {
  name: 'ItemDetail',
  components: { ItemDetailGem }
  // ...
}

Con esto ya tenemos los componentes de items terminados. Queda llamarlos desde la vista principal y ¡💥! Deberían aparecer todos.

Desde /Hero/HeroItems/Index.vue, cargamos el componente de ItemDetail, lo declaramos y los utilizamos:

<template>
  <section class="hero-items mb-5">
    <h2 class="font-diablo">Items</h2>

    <hr class="bg-white">

    <!-- Grid de 3 columnas. Mostramos solo una centrada -->
    <b-row>
      <b-col cols="4" offset="4">
        <ItemDetail :item="itemsData.head"/>
      </b-col>
    </b-row>

    <hr>

    <!-- Grid de 3 columnas -->
    <b-row>
      <b-col>
        <ItemDetail :item="itemsData.shoulders"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.torso"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.neck"/>
      </b-col>
    </b-row>

    <hr>

    <!-- Grid de 3 columnas -->
    <b-row>
      <b-col>
        <ItemDetail :item="itemsData.hands"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.waist"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.bracers"/>
      </b-col>
    </b-row>

    <hr>

    <!-- Grid de 3 columnas -->
    <b-row>
      <b-col>
        <ItemDetail :item="itemsData.leftFinger"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.legs"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.rightFinger"/>
      </b-col>
    </b-row>

    <hr>

    <!-- Grid de 3 columnas -->
    <b-row>
      <b-col>
        <ItemDetail :item="itemsData.mainHand"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.feet"/>
      </b-col>
      <b-col>
        <ItemDetail :item="itemsData.offHand"/>
      </b-col>
    </b-row>

  </section>

</template>

<script>
import ItemDetail from './ItemDetail'

// Objeto con las keys de los 'items' del personaje
const defaultItems = {
  head: {
    slotName: 'head'
  },
  shoulders: {
    slotName: 'Shoulders'
  },
  torso: {
    slotName: 'Torso'
  },
  neck: {
    slotName: 'Neck'
  },
  hands: {
    slotName: 'Hands'
  },
  waist: {
    slotName: 'Waist'
  },
  bracers: {
    slotName: 'Bracers'
  },
  leftFinger: {
    slotName: 'Left Finger'
  },
  legs: {
    slotName: 'Legs'
  },
  rightFinger: {
    slotName: 'Right Finger'
  },
  mainHand: {
    slotName: 'Main Hand'
  },
  feet: {
    slotName: 'Feet'
  },
  offHand: {
    slotName: 'Off Hand'
  }
}

export default {
  name: 'HeroItems',
  components: { ItemDetail },
  props: {
    items: {
      type: Object,
      required: true
    }
  },
  computed: {
    itemsData () {
      // Fusionar objetos:
      // Esto lo hacemos para mostrar el hueco vacío en caso de que ese objeto no esté equipado
      // Si NO hay item equipado, manda el valor de 'defaultItems' correspondiente
      // Si hay item equipado, manda la info del item
      return {
        ...defaultItems,
        ...this.items
      }
    }
  }
}
</script>

Si tienes todo correcto y sin errores deberías ver algo como esto:

full-pv

En mi caso, todos los personajes que tenía en el momento de crear este escrito estaban a niveles altos y con todos los huecos de equipación completos.
Sin embargo, rebuscando por internet, he encontrado un perfil a nivel 1, con varios huecos de items vacíos, y solo una habilidad activa. Así es como se vería:

low-level

Si has llegado hasta aquí… 🎉 ¡Enhorabuena! 🎉 Ya tienes una super aplicación con datos reales de personajes Diablo III.

congrats

Esta vista de la aplicación da mucho juego, pues la información que nos devuelve la API es muy extensa. En este curso no hemos trabajado con toda la información existente, ya que se haría demasiado complejo, pero te animo a que hagas extensiones y/o mejoras.

También te recuerdo que si quieres contribuir, ya sabes que el repositorio original siempre está abierto a mejoras y sugerencias.

En la siguiente lectura veremos cómo crear directivas personalizadas (custom directives).

Aportes 6

Preguntas 0

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.

Por fin un curso de Platzi, en el cual también puedo escuchar música mientras estudio. 😎😎😊😊

¡Excelente curso, una joyita!
Hemos obtenido un gran boilerplate y una seria de lineamientos para trabajar en proyectos profesionales.

Buff, me explota la cabeza el modo de trabajar, me encanta como todo se divide el pequeños componentes y cómo se van pasando datos unos a otros, es genial!

Excelente curso, me gusto mucho que fuera diferente a veces uno se cansa de ver tantos videos a 1.5X, ya que vimos la manera de cargar los componentes en lazy loading, deberíamos implementarlo en todos los componentes de nuestra app o como sabemos cuando usar lazy loading y cuando no.
Gracias Jorge excelente profesor!

Excelente clase, muy bueno!

Genial el curso, mucho mas entretenido que ver los videos. 😉