diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index afcffb0f8..08db78355 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -167,6 +167,7 @@
     medium: "Medium"
     hard: "Hard"
     player: "Player"
+    player_level: "Level"  # Like player level 5, not like level: Dungeons of Kithgard
 
   units:
     second: "second"
diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee
index 848c82f29..789abab76 100644
--- a/app/models/ThangType.coffee
+++ b/app/models/ThangType.coffee
@@ -396,8 +396,16 @@ module.exports = class ThangType extends CocoModel
     name: name, display: display
 
   isSilhouettedItem: ->
-    # TODO: have items have actual levels instead of just going by their gem count
     return console.error "Trying to determine whether #{@get('name')} should be a silhouetted item, but it has no gem cost." unless @get 'gems'
+    console.info "Add (or make sure you have fetched) a tier for #{@get('name')} to more accurately determine whether it is silhouetted." unless @get('tier')?
+    tier = @get 'tier'
+    if tier?
+      return @levelRequiredForItem() > me.level()
     points = me.get('points')
     expectedTotalGems = (points ? 0) * 1.5   # Not actually true, but roughly kinda close for tier 0, kinda tier 1
-    @get('gems') > (100 + expectedTotalGems) * 2
+    @get('gems') > (100 + expectedTotalGems) * 1.2
+
+  levelRequiredForItem: ->
+    return console.error "Trying to determine what level is required for #{@get('name')}, but it has no tier." unless @get('tier')?
+    tier = @get 'tier'
+    me.constructor.levelForTier(Math.pow(tier, 0.7))
diff --git a/app/models/User.coffee b/app/models/User.coffee
index 9e760cc17..7fab3e260 100644
--- a/app/models/User.coffee
+++ b/app/models/User.coffee
@@ -63,9 +63,22 @@ module.exports = class User extends CocoModel
   @expForLevel: (level) ->
     if level > 1 then Math.ceil Math.exp((level - 1)/ a) * b - c else 0
 
+  @tierFromLevel: (level) ->
+    # TODO: math
+    # For now, just eyeball it.
+    tiersByLevel[Math.min(level, tiersByLevel.length - 1)]
+
+  @levelForTier: (tier) ->
+    # TODO: math
+    for tierThreshold, level in tiersByLevel
+      return level if tierThreshold >= tier
+
   level: ->
     User.levelFromExp(@get('points'))
 
+  tier: ->
+    User.tierFromLevel @level()
+
   gems: ->
     gemsEarned = @get('earned')?.gems ? 0
     gemsPurchased = @get('purchased')?.gems ? 0
@@ -146,3 +159,5 @@ module.exports = class User extends CocoModel
     @kithmazeGroup = 'haunted-kithmaze' if me.isAdmin()
     application.tracker.identify kithmazeGroup: @kithmazeGroup unless me.isAdmin()
     @kithmazeGroup
+
+tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96]
diff --git a/app/schemas/models/thang_type.coffee b/app/schemas/models/thang_type.coffee
index 08db1e67b..7d74b0ef9 100644
--- a/app/schemas/models/thang_type.coffee
+++ b/app/schemas/models/thang_type.coffee
@@ -109,6 +109,7 @@ _.extend ThangTypeSchema.properties,
   terrains: c.array {title: 'Terrains', description: 'If specified, limits this ThangType to levels with matching terrains.', uniqueItems: true}, c.terrainString
   gems: {type: 'integer', minimum: 0, title: 'Gem Cost', description: 'How many gems this item or hero costs.'}
   heroClass: {type: 'string', enum: ['Warrior', 'Ranger', 'Wizard'], title: 'Hero Class', description: 'What class this is (if a hero) or is restricted to (if an item). Leave undefined for most items.'}
+  tier: {type: 'number', minimum: 0, title: 'Tier', description: 'What tier (fractional) this item or hero is in.'}
   actions: c.object {title: 'Actions', additionalProperties: {$ref: '#/definitions/action'}}
   soundTriggers: c.object {title: 'Sound Triggers', additionalProperties: c.array({}, {$ref: '#/definitions/sound'})},
     say: c.object {format: 'slug-props', additionalProperties: {$ref: '#/definitions/sound'}},
diff --git a/app/styles/play/modal/play-items-modal.sass b/app/styles/play/modal/play-items-modal.sass
index 19e5dd548..a8a55e1c4 100644
--- a/app/styles/play/modal/play-items-modal.sass
+++ b/app/styles/play/modal/play-items-modal.sass
@@ -139,6 +139,9 @@
     margin: 4px
     text-align: center
     position: relative
+
+    &.silhouetted
+      cursor: default
     
     strong
       position: absolute
@@ -275,7 +278,19 @@
   .item-silhouette
     @include filter(contrast(0%) brightness(0%))
     opacity: 0.3
-    
+
+  .required-level
+    position: absolute
+    left: 0
+    right: 5px
+    top: 70px
+    font-size: 20px
+    line-height: 20px
+    font-family: Open Sans Condensed
+    text-transform: uppercase
+    font-weight: bold
+    z-index: 2
+
     
   //- Unlock buttons (both in list and details areas)
 
diff --git a/app/templates/play/modal/play-items-modal.jade b/app/templates/play/modal/play-items-modal.jade
index 38e4be7d9..ec6b2d21d 100644
--- a/app/templates/play/modal/play-items-modal.jade
+++ b/app/templates/play/modal/play-items-modal.jade
@@ -26,11 +26,16 @@
             .nano-content
               for item in itemCategoryCollections[category].models
                 - var hidden = item.comingSoon && !me.isAdmin()
-                div(class="item" + (hidden ? " hide" : ""), data-item-id=item.id)
+                - hidden = hidden || (!item.get('gems') && !item.owned)
+                div(class="item" + (hidden ? " hide" : "") + (item.silhouetted && !item.owned ? " silhouetted" : ""), data-item-id=item.id)
                   if item.silhouetted && !item.owned
                     span.glyphicon.glyphicon-lock.bolder
                     span.glyphicon.glyphicon-lock
                     img.item-silhouette(src=item.getPortraitURL())
+                    if item.level
+                      .required-level
+                        div(data-i18n="general.player_level")
+                        div= item.level
                   else
                     strong.big-font= item.name
                     img.item-img(src=item.getPortraitURL())
diff --git a/app/templates/play/world-map-view.jade b/app/templates/play/world-map-view.jade
index 74d0ca9e1..3290ea6d5 100644
--- a/app/templates/play/world-map-view.jade
+++ b/app/templates/play/world-map-view.jade
@@ -64,6 +64,8 @@
 .user-status.header-font
   span.gem.gem-20
   span.spr= me.gems()
+  span.spl.spr(data-i18n="general.player_level")
+  span.spr= me.level()
   if me.get('anonymous')
     span.spr(data-i18n="play.anonymous_player") Anonymous Player
     button.btn.btn-default.btn-flat.btn-sm(data-toggle='coco-modal', data-target='modal/AuthModal', data-i18n="login.log_in")
diff --git a/app/views/HomeView.coffee b/app/views/HomeView.coffee
index 7b5064e10..7bc31c09e 100644
--- a/app/views/HomeView.coffee
+++ b/app/views/HomeView.coffee
@@ -4,8 +4,6 @@ WizardLank = require 'lib/surface/WizardLank'
 ThangType = require 'models/ThangType'
 Simulator = require 'lib/simulator/Simulator'
 
-InventoryModal = require 'views/game-menu/InventoryModal'
-
 {me} = require '/lib/auth'
 
 module.exports = class HomeView extends RootView
@@ -40,4 +38,3 @@ module.exports = class HomeView extends RootView
 
   afterInsert: ->
     super(arguments...)
-    @openModalView(new InventoryModal({levelID: 'the-raised-sword'}))
\ No newline at end of file
diff --git a/app/views/game-menu/InventoryModal.coffee b/app/views/game-menu/InventoryModal.coffee
index 3da689a25..dc1931d87 100644
--- a/app/views/game-menu/InventoryModal.coffee
+++ b/app/views/game-menu/InventoryModal.coffee
@@ -44,14 +44,15 @@ module.exports = class InventoryModal extends ModalView
     # TODO: switch to item store loading system?
     @items.url = '/db/thang.type?view=items'
     @items.setProjection [
-      'name',
-      'slug',
-      'components',
-      'original',
-      'rasterIcon',
-      'gems',
-      'description',
-      'heroClass',
+      'name'
+      'slug'
+      'components'
+      'original'
+      'rasterIcon'
+      'gems'
+      'tier'
+      'description'
+      'heroClass'
       'i18n'
     ]
     @supermodel.loadCollection(@items, 'items')
diff --git a/app/views/play/modal/PlayItemsModal.coffee b/app/views/play/modal/PlayItemsModal.coffee
index 2295ed2ad..88805a346 100644
--- a/app/views/play/modal/PlayItemsModal.coffee
+++ b/app/views/play/modal/PlayItemsModal.coffee
@@ -62,6 +62,7 @@ module.exports = class PlayItemsModal extends ModalView
       'original'
       'rasterIcon'
       'gems'
+      'tier'
       'i18n'
       'heroClass'
     ]
@@ -77,7 +78,8 @@ module.exports = class PlayItemsModal extends ModalView
     gemsOwned = me.gems()
     needMore = itemFetcher.models.length is PAGE_SIZE
     for model in itemFetcher.models
-      continue unless cost = model.get('gems')
+      model.owned = me.ownsItem model.get('original')
+      continue unless (cost = model.get('gems')) or model.owned
       category = slotToCategory[model.getAllowedSlots()[0]] or 'misc'
       @itemCategoryCollections[category] ?= new Backbone.Collection()
       collection = @itemCategoryCollections[category]
@@ -85,10 +87,10 @@ module.exports = class PlayItemsModal extends ModalView
       collection.add(model)
       model.name = utils.i18n model.attributes, 'name'
       model.affordable = cost <= gemsOwned
-      model.owned = me.ownsItem model.get('original')
-      model.silhouetted = model.isSilhouettedItem()
+      model.silhouetted = not model.owned and model.isSilhouettedItem()
+      model.level = model.levelRequiredForItem() if model.get('tier')?
       model.equippable = 'Warrior' in model.getAllowedHeroClasses()  # Temp: while there are no wizards/rangers
-      model.comingSoon = not model.getFrontFacingStats().props.length and not _.size model.getFrontFacingStats().stats  # Temp: while there are placeholder items
+      model.comingSoon = not model.getFrontFacingStats().props.length and not _.size model.getFrontFacingStats().stats and not model.owned  # Temp: while there are placeholder items
       @idToItem[model.id] = model
 
     if needMore
@@ -127,7 +129,7 @@ module.exports = class PlayItemsModal extends ModalView
       item = null
     else
       item = @idToItem[itemEl.data('item-id')]
-      if item.silhouetted
+      if item.silhouetted and not item.owned
         item = null
       else
         itemEl.addClass('selected') unless wasSelected
@@ -158,4 +160,3 @@ module.exports = class PlayItemsModal extends ModalView
       button.addClass('confirm').text($.i18n.t('play.confirm'))
       @$el.one 'click', (e) ->
         button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
-
diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee
index 490a1492e..833129c13 100644
--- a/server/levels/thangs/thang_type_handler.coffee
+++ b/server/levels/thangs/thang_type_handler.coffee
@@ -40,6 +40,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
     'description'
     'gems'
     'heroClass'
+    'tier'
     'extendedName'
   ]
 
@@ -61,7 +62,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
       query = slug: {$exists: true}
       if req.query.view is 'items'
         query.kind = 'Item'
-        query.gems = {$exists: true}  # Items without gems don't show up anywhere
+        query.tier = {$exists: true}  # Items without a tier don't show up anywhere, whereas items without gems don't show up in the store
       else if req.query.view is 'heroes'
         #query.kind = 'Hero'  # TODO: when ChooseHeroView is refactored, just use this
         query.original = {$in: _.values heroes}  # TODO: when ChooseHeroView is refactored, don't do this