Redesign the crafting page

This introduces the new crafting page design featuring a new vertical
layout and list display of all crafting steps. It also makes it easy
to move items between the have/need sections and even from each step
into your inventory (causing a new crafting plan to be created which
doesn't plan on making that item).

* Convert the `BaseController.hide` and `show` methods to be much
  more robust in the use of CSS transitions (using a timer in case a
  transition doesn't actually fire).
* Change `hide`/`show` to use events for communicating animation
  state instead of callbacks
* Convert several controllers to fire events for communication with
  parent controllers instead of accepting functions.
* Rewrite the `CraftPageController`, `CraftPage` model and related
  Jade/SCSS files to implement the new design.
* Alter the `InventoryController` and `StackController` to allow a
  variety of action buttons, and for the events from those action
  buttons to bubble up to parent controllers
* Add the `StepController` and its related model, template, and style
  sheet as part of the new craft page.
* Tweak the `ItemSelectorController` and related files to allow for
  a variety of styles for the launcher button
This commit is contained in:
Andrew Miner
2015-04-20 09:17:58 -07:00
parent daaeb523d9
commit 5b014c9921
61 changed files with 855 additions and 401 deletions
+32 -19
View File
@@ -37,32 +37,45 @@ Duration.fast = 200
Duration.normal = 400
Duration.slow = 1200
exports.Event = Event = {}
Event.add = 'add' # collection, item...
Event.change = 'change' # model
Event.click = 'click' # event
Event.load = {}
Event.load.started = 'load:started' # controller, url
Event.load.succeeded = 'load:succeeded' # controller, book
Event.load.failed = 'load:failed' # controller, error message
Event.load.finished = 'load:finished' # controller
Event.remove = 'remove' # collection, item...
Event.request = 'request' # model
Event.route = 'route'
Event.sort = 'sort'
Event.sync = 'sync' # model, response
Event.transitionEnd = (->
exports.Event = Event = {}
Event.add = 'add' # collection, item...
Event.animate = {}
Event.animate.hide = {}
Event.animate.hide.start = 'animate:hide:start' # controller, selector
Event.animate.hide.finish = 'animate:hide:finish' # controller, selector
Event.animate.show = {}
Event.animate.show.start = 'animate:show:start' # controller, selector
Event.animate.show.finish = 'animate:show:finish' # controller, selector
Event.button = {}
Event.button.complete = 'button:complete' # controller
Event.button.first = 'button:first' # controller, buttonType
Event.button.second = 'button:second' # controller, buttonType
Event.change = 'change' # model
Event.click = 'click' # event
Event.load = {}
Event.load.started = 'load:started' # controller, url
Event.load.succeeded = 'load:succeeded' # controller, book
Event.load.failed = 'load:failed' # controller, error message
Event.load.finished = 'load:finished' # controller
Event.remove = 'remove' # collection, item...
Event.request = 'request' # model
Event.route = 'route'
Event.sort = 'sort'
Event.sync = 'sync' # model, response
Event.transitionEnd = (->
return unless document?
transitions =
'transition': 'transitionend',
'OTransition': 'oTransitionEnd',
'MSTransition': 'msTransitionEnd',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd'
'MozTransition': 'transitionend'
'MSTransition': 'msTransitionEnd'
'OTransition': 'oTransitionEnd'
'transition': 'transitionend'
el = document.createElement 'fakeelement'
for styleName, eventName of transitions
return eventName if el.style[styleName]?
throw new Error 'cannot determine transitionEnd event'
)()
exports.Key = Key = {}
+52 -44
View File
@@ -5,22 +5,22 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
views = require '../views'
{Event} = require '../constants'
views = require '../views'
{Duration} = require '../constants'
{Event} = require '../constants'
########################################################################################################################
module.exports = class BaseController extends Backbone.View
constructor: (options={})->
options.useAnimations ?= true
@tryRefresh = _.debounce @_tryRefresh, 100
@_model = null
@_rendered = false
@_parent = options.parent
@_children = []
@_useAnimations = options.useAnimations
@_model = null
@_parent = options.parent
@_rendered = false
@_useAnimations = options.useAnimations ?= true
@_loadTemplate options.templateName
super options
@@ -36,33 +36,39 @@ module.exports = class BaseController extends Backbone.View
@_children.push child
return child
hide: (args...)->
{$el, callback} = @_resolveShowHideArgs args
hide: ($el)->
$el ?= @$el
$el.addClass 'hideable' unless $el.hasClass 'hideable'
if not $el.hasClass 'hideable'
$el.addClass 'hideable'
_.defer => @hide $el
return
logger.verbose => "#{this} is hiding #{$el.selector}"
if $el.hasClass('hiding') or $el.hasClass('hidden')
_.defer => callback this
else
$el.off Event.transitionEnd
$el.one Event.transitionEnd, =>
if not ($el.hasClass('hiding') or $el.hasClass('hidden'))
logger.verbose => "#{this} is hiding \"#{$el.selector}\""
@_onAnimationComplete $el, Duration.normal, =>
$el.addClass 'hidden'
$el.removeClass 'hiding'
callback this
logger.verbose => "#{this} is finished hiding #{$el.selector}"
@trigger Event.animate.hide.finish, this, $el
$el.addClass 'hiding'
@trigger Event.animate.hide.start, this, $el
refresh: ->
logger.verbose => "#{this} refreshing"
remove: ->
if @_useAnimations
@hide =>
logger.verbose => "#{this} is hiding element before removing it"
@once Event.animate.hide.finish, =>
logger.verbose => "#{this} is removing its element from the DOM"
@$el.remove()
@off()
@hide()
else
@$el.remove()
@off()
routeLinkClick: (event)->
event.preventDefault()
@@ -75,19 +81,22 @@ module.exports = class BaseController extends Backbone.View
else
router.navigate href, trigger:true
show: (args...)->
{$el, callback} = @_resolveShowHideArgs args
show: ($el)->
$el ?= @$el
$el.addClass 'hideable'
$el.addClass 'hideable' unless $el.hasClass 'hideable'
if $el.hasClass('hidden') or $el.hasClass('hiding')
logger.verbose => "#{this} is showing \"#{$el.selector}\""
logger.verbose => "#{this} is showing #{$el.selector}"
if $el.hasClass('hiding') or $el.hasClass('hidden')
$el.off Event.transitionEnd
$el.one Event.transitionEnd, => callback this
$el.removeClass 'hiding'
@_onAnimationComplete $el, Duration.normal, =>
logger.verbose => "#{this} is finished showing #{$el.selector}"
@trigger Event.animate.show.finish, this, $el
$el.addClass 'hiding'
$el.removeClass 'hidden'
else
_.defer => callback this
_.defer =>
$el.removeClass 'hiding'
@trigger Event.animate.show.start, this, $el
unrender: ->
@undelegateEvents()
@@ -164,14 +173,12 @@ module.exports = class BaseController extends Backbone.View
$renderedEl = $($(@_template(data))[0])
@onWillRender()
@hide() if @_useAnimations
@unrender()
@$el.append $renderedEl.children()
@$el.addClass $renderedEl.attr 'class'
@$el.data $renderedEl.data()
@delegateEvents()
@show() if options.show and @_useAnimations
@_rendered = true
@onDidRender()
@@ -189,19 +196,20 @@ module.exports = class BaseController extends Backbone.View
if templateName?
@_template = views[templateName]
_resolveShowHideArgs: (args)->
if args.length is 0
return $el:@$el, callback:->
else if args.length is 1
if _.isFunction args[0]
return $el:@$el, callback:args[0]
else
return $el:args[0], callback:->
else if args.length is 2
return $el:args[0], callback:args[1]
else
throw new Error "expected to get 0, 1, or 2 args"
_tryRefresh: ->
return unless @_rendered
@refresh()
_onAnimationComplete: ($el, maxDuration, callback)->
$el ?= @$el
maxDuration ?= Duration.slow
callback ?= -> # do nothing
$el.off Event.transitionEnd
$el.one Event.transitionEnd, callback
# One would very much like this not to be necessary, but because of the way CSS transitions work, they are
# frequently not triggered. Rather than try to avoid the multitude of corner cases where this can happen, this
# simply sets a timer to ensure the event does eventually trigger. Since we're only listening for the first
# event, if the transition end event *did* happen, then this is a no-op. -- Andrew 2015-04-18
setTimeout (-> $el.trigger Event.transitionEnd), maxDuration
@@ -5,17 +5,13 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
CraftingTableController = require './crafting_table_controller'
CraftPage = require '../models/craft_page'
{Event} = require '../constants'
ImageLoader = require './image_loader'
InventoryController = require './inventory_controller'
ModPackController = require './mod_pack_controller'
NameFinder = require '../models/name_finder'
PageController = require './page_controller'
Storage = require '../models/storage'
{Text} = require '../constants'
{Url} = require '../constants'
AdsenseController = require './adsense_controller'
CraftPage = require '../models/craft_page'
InventoryController = require './inventory_controller'
PageController = require './page_controller'
StepController = require './step_controller'
{Event} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
@@ -24,20 +20,56 @@ module.exports = class CraftPageController extends PageController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
if not options.storage? then throw new Error 'options.storage is required'
options.model ?= new CraftPage modPack:options.modPack
options.storage ?= new Storage storage:window.localStorage
options.templateName = 'craft_page'
options.model ?= new CraftPage modPack:options.modPack
options.templateName = 'craft_page'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
@storage = options.storage
@model.plan.on Event.change, => @refresh()
# Event Methods ################################################################################
onToolsBoxToggled: ->
@model.plan.includingTools = @$('.includeTools:checked').length isnt 0
onCraftTool: (itemSlug)->
# TODO: implement this
onHaveInventoryChanged: ->
@storage.store 'crafting-plan:have', @model.plan.have.unparse()
onMoveNeedToHave: (itemSlug)->
quantity = @model.plan.need.quantityOf itemSlug
@model.plan.have.add itemSlug, quantity
@_saveHaveInventory()
onRemoveFromHaveInventory: (itemSlug)->
@model.plan.have.remove itemSlug
@onHaveInventoryChanged()
onRemoveFromWant: (itemSlug)->
@model.plan.want.remove itemSlug
@onWantInventoryChange()
onStopUsingTool: (itemSlug)->
# TODO: implement this
onStepComplete: (stepController)->
step = stepController.model
for stack in step.recipe.output
@model.plan.have.add stack.itemSlug, stack.quantity * step.multiplier
@onHaveInventoryChanged()
onWantInventoryChange: ->
text = @model.plan.want.unparse()
url = Url.crafting inventoryText:text
router.navigate url
if @model.plan.want.isEmpty
@model.plan.have.clear()
@onHaveInventoryChanged()
# PageController Overrides #####################################################################
@@ -46,69 +78,105 @@ module.exports = class CraftPageController extends PageController
# BaseController Overrides #####################################################################
onDidRender: ->
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
@wantInventoryController = @addChild InventoryController, '.want .view__inventory',
imageLoader: @imageLoader
isAcceptable: (item)=> item.isCraftable
model: @model.plan.want
modPack: @modPack
firstButtonType: 'remove'
@wantInventoryController.on Event.button.first, (c, s)=> @onRemoveFromWant(s)
@wantInventoryController.on Event.change, (c)=> @onWantInventoryChange()
# @toolsInUseController = @addChild InventoryController, '.tools .view__inventory',
# imageLoader: @imageLoader
# model: @model.plan.toolsInUse
# modPack: @modPack
# firstButtonType: 'down'
# secondButtonType: 'up'
# @toolsInUseController.on Event.button.first, (c, s)=> @onStopUsingTool(s)
# @toolsInUseController.of Event.button.second, (c, s)=> @onCraftTool(s)
@haveInventoryController = @addChild InventoryController, '.have .view__inventory',
imageLoader: @imageLoader
model: @model.plan.have
modPack: @modPack
firstButtonType: 'down'
@haveInventoryController.on Event.button.first, (c, s)=>
@onRemoveFromHaveInventory(s)
@haveInventoryController.on Event.change, (c)=> @onHaveInventoryChanged()
@needInventoryController = @addChild InventoryController, '.need .view__inventory',
editable: false
imageLoader: @imageLoader
model: @model.plan.need
modPack: @modPack
firstButtonType: 'up'
@needInventoryController.on Event.button.first, (c, s)=> @onMoveNeedToHave(s)
@$instructionsSection = @$('.instructions.section')
@$makeSection = @$('.make.section')
@$toolsSection = @$('.tools.section')
@$ingredientsSection = @$('.ingredients.section')
@$stepsSection = @$('.steps.section')
@$stepsContainer = @$('.steps.section .panel')
super
onWillRender: ->
@storage.register 'crafting-plan', @model.plan, 'includingTools'
@model.plan.have.clear()
@model.plan.have.parse @storage.load('crafting-plan:have')
super
onDidRender: ->
@wantController = @addChild InventoryController, '.want',
editable: true
icon: '/images/fishing_rod.png'
imageLoader: @imageLoader
isAcceptable: (item)-> item.isCraftable
model: @model.plan.want
modPack: @model.modPack
onChange: => @_updateLocation()
title: 'Items you want'
@haveController = @addChild InventoryController, '.have',
editable: true,
imageLoader: @imageLoader
model: @model.plan.have
modPack: @model.modPack
onChange: => @_saveHaveInventory()
title: 'Items you have'
@needController = @addChild InventoryController, '.need',
editable: false
icon: '/images/boots.png'
imageLoader: @imageLoader
model: @model.plan.need
modPack: @model.modPack
title: "Items you'll need"
@craftingTableController = @addChild CraftingTableController, '.view__crafting_table',
imageLoader: @imageLoader
model: @model.table
modPack: @model.modPack
@$('.want .toolbar').append '<label><input class="includeTools" type="checkbox"> include tools</label>'
@$includeToolsBox = @$('.includeTools')
super
refresh: ->
if @model.plan.includingTools
@$includeToolsBox.attr 'checked', 'checked'
else
@$includeToolsBox.removeAttr 'checked'
@_refreshSectionVisibility()
@_refreshSteps()
super
# Backbone.View Overrides ######################################################################
# Backbone.View Overrides ########################################################################
events: ->
return _.extend super,
'change .includeTools': 'onToolsBoxToggled'
'click .instructions a': 'routeLinkClick'
# Private Methods ##############################################################################
# Private Methods ################################################################################
_saveHaveInventory: ->
@storage.store 'crafting-plan:have', @model.plan.have.unparse()
_isStepCompletable: (controller)->
return not @model.plan.want.hasAtLeast controller.model.outputItemSlug
_updateLocation: ->
text = @model.plan.want.unparse()
url = Url.crafting inventoryText:text
router.navigate url
_refreshSectionVisibility: ->
if @model.plan.want.isEmpty
for $el in [@$toolsSection, @$ingredientsSection, @$stepsSection]
@hide $el
@show @$instructionsSection
else
for $el in [@$toolsSection, @$ingredientsSection, @$stepsSection]
@show $el
@hide @$instructionsSection
_refreshSteps: ->
@_stepControllers ?= []
index = 0
for step in @model.plan.steps
controller = @_stepControllers[index]
if not controller?
controller = new StepController
canComplete: (controller)=> @_isStepCompletable(controller)
imageLoader: @imageLoader
model: step
modPack: @modPack
controller.on Event.button.complete, (c)=> @onStepComplete(c)
controller.render()
@$stepsContainer.append controller.$el
@_stepControllers.push controller
else
controller.model = step
index += 1
while @_stepControllers.length > index
@_stepControllers.pop().remove()
@@ -81,7 +81,7 @@ module.exports = class HeaderController extends BaseController
addThisUrl = '//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-54f3c9717b2e530d'
$('body').append "<script async src=\"#{addThisUrl}\"></script>"
@_itemSelector = @addChild ItemSelectorController, '.view__item_selector',
@_itemSelector = @addChild ItemSelectorController, '.view__item_selector.search',
modPack: @modPack
onChoseItem: (itemSlug)=> @_goToItem(itemSlug)
@@ -16,6 +16,14 @@ StackController = require './stack_controller'
########################################################################################################################
###
Events:
clear(this)
change(this)
add(this, itemSlug)
button:first(this, itemSlug)
button:second(this, itemSlug)
###
module.exports = class InventoryController extends BaseController
@MAX_QUANTITY = 9999
@@ -27,13 +35,16 @@ module.exports = class InventoryController extends BaseController
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
@editable = options.editable ?= true
@icon = options.icon ?= '/images/chest_front.png'
@imageLoader = options.imageLoader
@isAcceptable = options.isAcceptable ?= null
@modPack = options.modPack
@onChange = options.onChange ?= -> # do nothing
@title = options.title ?= 'Inventory'
@imageLoader = options.imageLoader
@modPack = options.modPack
@editable = options.editable ?= true
@icon = options.icon ?= '/images/chest_front.png'
@firstButtonType = options.firstButtonType ?= null
@isAcceptable = options.isAcceptable ?= null
@secondButtonType = options.secondButtonType ?= null
@shouldEnableButton = options.shouldEnableButton ?= (model, button)-> true
@title = options.title ?= 'Inventory'
options.templateName = 'inventory'
super options
@@ -46,11 +57,19 @@ module.exports = class InventoryController extends BaseController
onClearButtonClicked: ->
@model.clear()
@onChange()
@trigger Event.clear, this
@trigger Event.change, this
onFirstButtonClicked: (stackController)->
@trigger Event.button.first, this, stackController?.model?.itemSlug
onItemChosen: (itemSlug)->
@model.add itemSlug, 1
@onChange()
@trigger Event.add, this, itemSlug
@trigger Event.change, this
onSecondButtonClicked: (stackController)->
@trigger Event.button.second, this, stackController?.model?.itemSlug
# BaseController Overrides #####################################################################
@@ -60,32 +79,23 @@ module.exports = class InventoryController extends BaseController
modPack: @modPack,
onChoseItem: (itemSlug)=> @onItemChosen(itemSlug)
@$clearButton = @$('button[name="clear"]')
@$clearButton = @$('button.clear')
@$icon = @$('.icon')
@$editPanel = @$('.edit')
@$scrollbox = @$('.scrollbox')
@$table = @$('table')
@$toolbar = @$('.toolbar')
@$itemContainer = @$('.item_container')
@$title = @$('h2 p')
super
refresh: ->
if @editable
@show @$editPanel
@show @$toolbar, =>
@$scrollbox.css bottom:@$toolbar.height()
@selector.show()
else
if not @$editPanel? then throw new Error "no edit panel"
if not @$toolbar? then throw new Error "no toolbar"
@hide @$editPanel
@hide @$toolbar
@$scrollbox.css bottom:0
@selector.hide()
@$icon.attr 'src', @icon
@$title.html @title
@$clearButton.disabled = @model.isEmpty
@_refreshStacks()
@_refreshButtonState()
super
@@ -93,38 +103,37 @@ module.exports = class InventoryController extends BaseController
events: ->
return _.extend super,
'click button[name="clear"]': 'onClearButtonClicked'
'click button.clear': 'onClearButtonClicked'
'click button.first': 'onFirstButtonClicked'
'click button.second': 'onSecondButtonClicked'
# Private Methods ##############################################################################
_refreshButtonState: ->
if @model.isEmpty then @$clearButton.attr('disabled', 'disabled') else @$clearButton.removeAttr('disabled')
_refreshStacks: ->
@_stackControllers ?= []
index = 0
$lastRow = @$table.find 'tr:last-child'
@model.each (stack)=>
controller = @_stackControllers[index]
if not controller?
controller = new StackController
editable: @editable
imageLoader: @imageLoader
model: stack
modPack: @modPack
onChange: @onChange
onRemove: if not @editable then null else (stack)=> @_removeStack(stack)
editable: @editable
firstButtonType: @firstButtonType
imageLoader: @imageLoader
model: stack
modPack: @modPack
secondButtonType: @secondButtonType
shouldEnableButton: @shouldEnableButton
controller.on Event.change, => @trigger Event.change, this
controller.on Event.button.first, (c)=> @onFirstButtonClicked(c)
controller.on Event.button.second, (c)=> @onSecondButtonClicked(c)
controller.render()
@_stackControllers.push controller
controller.$el.insertBefore $lastRow
controller.render()
@$itemContainer.append controller.$el
else
controller.model = stack
index += 1
while @_stackControllers.length > index
@_stackControllers.pop().remove()
_removeStack: (stack)->
@model.remove stack.itemSlug, stack.quantity
@@ -65,7 +65,6 @@ module.exports = class ItemGroupController extends BaseController
_refreshItems: ->
@_itemControllers ?= []
controllerIndex = 0
delay = 0
for item in @model
controller = @_itemControllers[controllerIndex]
@@ -24,6 +24,7 @@ module.exports = class MinimalRecipeController extends BaseController
@imageLoader = options.imageLoader
@modPack = options.modPack
@multiplier = options.multiplier ?= null
# BaseController Overrides #####################################################################
@@ -36,6 +37,7 @@ module.exports = class MinimalRecipeController extends BaseController
imageLoader: @imageLoader
modPack: @modPack
@$multiplier = @$('.multiplier')
@$outputImg = @$('.output img')
@$outputLink = @$('.output a')
@$outputQuantity = @$('.quantity')
@@ -46,6 +48,8 @@ module.exports = class MinimalRecipeController extends BaseController
@gridController.model = @model
@outputSlotController.model = @model?.output?[0]
@$el.tooltip show:{delay:Duration.snap, duration:Duration.fast}
@_refreshMultiplier()
@_refreshTools()
super
@@ -57,6 +61,12 @@ module.exports = class MinimalRecipeController extends BaseController
# Private Methods ##############################################################################
_refreshMultiplier: ->
if @multiplier? and @multiplier > 1
@$multiplier.html "x#{@multiplier}"
else
@$multiplier.html ''
_refreshTools: ->
@$toolContainer.empty()
return unless @model?
@@ -159,7 +159,7 @@ module.exports = class ModPageController extends PageController
@$tutorialsContainer.append controller.$el
controller.render()
else
controller.model = model
controller.model = tutorial
index += 1
+70 -37
View File
@@ -6,13 +6,20 @@ All rights reserved.
###
BaseController = require './base_controller'
ImageLoader = require './image_loader'
{Duration} = require '../constants'
{Event} = require '../constants'
ImageLoader = require './image_loader'
{Key} = require '../constants'
########################################################################################################################
###
Events:
change(this)
change:quantity(this, oldQuantity, newQuantity)
button:first(this, type)
button:second(this, type)
###
module.exports = class StackController extends BaseController
@MAX_QUANTITY = 9999
@@ -22,60 +29,67 @@ module.exports = class StackController extends BaseController
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.editable ?= false
options.onChange ?= -> # do nothing
options.onRemove ?= (stack)-> # do nothing
options.tagName = 'tr'
options.templateName = 'stack'
super options
@editable = options.editable
@modPack = options.modPack
@onChange = options.onChange
@onRemove = options.onRemove
@_imageLoader = options.imageLoader
@editable = options.editable ?= false
@firstButtonType = options.firstButtonType ?= null
@imageLoader = options.imageLoader
@modPack = options.modPack
@secondButtonType = options.secondButtonType ?= null
@shouldEnableButton = options.shouldEnableButton ?= (model, button)-> true
@modPack.on Event.change, => @tryRefresh()
# Event Methods ################################################################################
onFirstButtonClicked: (event)->
@trigger Event.button.first, this, @firstButtonType
return false
onQuantityFieldBlur: ->
return unless @editable
@$bubble.removeClass 'editing'
oldQuantity = @_priorValue
quantityText = @$quantityField.val().trim()
if quantityText.length is 0
quantity = @_priorValue
newQuantity = oldQuantity
else if not quantityText.match /^[0-9]*$/
quantity = 1
newQuantity = 1
else
quantity = parseInt quantityText, 10
if _.isNaN quantity then quantity = 1
newQuantity = parseInt quantityText, 10
if _.isNaN newQuantity then newQuantity = 1
quantity = Math.min quantity, StackController.MAX_QUANTITY
quantity = Math.max 1, quantity
newQuantity = Math.min newQuantity, StackController.MAX_QUANTITY
newQuantity = Math.max 1, newQuantity
if quantity?
@$quantityField.val "#{quantity}"
@$quantityField.removeClass 'error', Duration.snap
@$quantityField.removeClass 'error-new', Duration.snap
if newQuantity? and newQuantity isnt oldQuantity
@$quantityField.val "#{newQuantity}"
@$bubble.removeClass 'error', Duration.snap
@$bubble.removeClass 'error-new', Duration.snap
@model.quantity = quantity
@onChange()
@model.quantity = newQuantity
@trigger Event.change + ':quantity', this, oldQuantity, newQuantity
@trigger Event.change, this
onQuantityFieldChanged: ->
return unless @editable
quantityText = @$quantityField.val().trim()
if not quantityText.match /^[0-9]*$/
@$quantityField.addClass 'error', 0
@$quantityField.addClass 'error-new', 0
@$quantityField.removeClass 'error-new', Duration.fast
@$bubble.addClass 'error', 0
@$bubble.addClass 'error-new', 0
@$bubble.removeClass 'error-new', Duration.fast
else
@$quantityField.removeClass 'error', Duration.snap
@$quantityField.removeClass 'error-new', Duration.snap
@$bubble.removeClass 'error', Duration.snap
@$bubble.removeClass 'error-new', Duration.snap
onQuantityFieldFocused: ->
if @editable
@$bubble.addClass 'editing'
@_priorValue = @model.quantity
@$quantityField.val ''
else
@@ -87,18 +101,20 @@ module.exports = class StackController extends BaseController
if event.which is Key.Return
@$quantityField.blur()
onRemoveClicked: ->
@onRemove @model
@onChange()
onSecondButtonClicked: (event)->
event.preventDefault()
@trigger Event.button.second, this, @secondButtonType
# BaseController Overrides #####################################################################
onDidRender: ->
@$action = @$('.action')
@$bubble = @$('.bubble')
@$firstButton = @$('button.first')
@$image = @$('.icon img')
@$nameLink = @$('.name a')
@$quantityField = @$('.quantity input')
@$removeButton = @$('button.remove')
@$secondButton = @$('button.second')
super
refresh: ->
@@ -106,19 +122,23 @@ module.exports = class StackController extends BaseController
display = @modPack.findItemDisplay @model.itemSlug
@_imageLoader.load display.iconUrl, @$image
@imageLoader.load display.iconUrl, @$image
@$nameLink.html display.itemName
@$nameLink.attr 'href', display.itemUrl
@$quantityField.val @model.quantity
@_updateButtonType @$firstButton, @firstButtonType
@_updateButtonType @$secondButton, @secondButtonType
@$firstButton.prop('disabled', not @shouldEnableButton(@model, 1)) if @firstButtonType?
@$secondButton.prop('disabled', not @shouldEnableButton(@model, 2)) if @secondButtonType?
if @editable
@$quantityField.removeAttr 'readonly'
@$quantityField.addClass 'editable'
@$bubble.addClass 'editable'
else
@$quantityField.attr 'readonly', 'readonly'
@$quantityField.removeClass 'editable'
@$action.css display:(if @editable then 'table-cell' else 'none')
@$bubble.removeClass 'editable'
super
@@ -127,8 +147,21 @@ module.exports = class StackController extends BaseController
events: ->
return _.extend super,
'blur .quantity input': 'onQuantityFieldBlur'
'click button.remove': 'onRemoveClicked'
'click .name a': 'routeLinkClick'
'click button.first': 'onFirstButtonClicked'
'click button.second': 'onSecondButtonClicked'
'focus .quantity input': 'onQuantityFieldFocused'
'input .quantity input': 'onQuantityFieldChanged'
'keyup .quantity input': 'onQuantityKeyUp'
# Private Methods ##############################################################################
_updateButtonType: ($button, type)->
for currentType in ['check', 'down', 'remove', 'up']
$button.removeClass currentType
if type?
$button.css 'display', null
$button.addClass type
else
$button.css 'display', 'none'
@@ -0,0 +1,75 @@
###
Crafting Guide - step_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
InventoryController = require './inventory_controller'
MinimalRecipeController = require './minimal_recipe_controller'
{Event} = require '../constants'
########################################################################################################################
module.exports = class StepController extends BaseController
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
options.templateName = 'step'
super options
@canComplete = options.canComplete or (controller)-> true
@imageLoader = options.imageLoader
@modPack = options.modPack
# Event Methods ################################################################################
onCompleteButtonClicked: (event)->
event.preventDefault()
@trigger Event.button.complete, this
# BaseController Overrides #####################################################################
onDidRender: ->
@inventoryController = @addChild InventoryController, '.view__inventory',
editable: false
imageLoader: @imageLoader
model: @model.inventory
modPack: @modPack
@recipeController = @addChild MinimalRecipeController, '.view__minimal_recipe',
imageLoader: @imageLoader
model: @model.recipe
modPack: @modPack
@$header = @$('h3')
@$completeButton = @$('button.complete')
super
onWillChangeModel: (oldModel, newModel)->
if not newModel? then throw new Error 'model cannot be null'
return super
refresh: ->
itemDisplay = @modPack.findItemDisplay @model.outputItemSlug
@$header.html "#{@model.number}. #{itemDisplay.itemName}"
@inventoryController.model = @model.inventory
@recipeController.model = @model.recipe
@recipeController.multiplier = @model.multiplier
if @canComplete(this)
@$completeButton.removeProp 'disabled'
else
@$completeButton.prop 'disabled', true
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click button.complete': 'onCompleteButtonClicked'
+3 -1
View File
@@ -224,9 +224,11 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
controller.onWillShow()
controller.$el = $pageContent
controller.render()
controller.show()
if @_controller?
@_controller.hide -> switchToNextController()
@_controller.once Event.animate.hide.finish, switchToNextController
@_controller.hide()
else
switchToNextController()
+1
View File
@@ -25,6 +25,7 @@ module.exports = class CraftPage extends BaseModel
@modPack.on Event.change, => @_consumeParams()
@on Event.change + ':params', => @_consumeParams()
@plan.on Event.change, => @trigger Event.change, this
# Private Methods ##############################################################################
+18 -7
View File
@@ -6,9 +6,10 @@ All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Inventory = require './inventory'
ItemSlug = require './item_slug'
Step = require './step'
{Event} = require '../constants'
########################################################################################################################
@@ -26,7 +27,7 @@ module.exports = class CraftingPlan extends BaseModel
recraft = _.debounce (=> @craft()), 100
for inventory in [@have, @want]
inventory.on 'change', recraft
inventory.on Event.change, recraft
@on Event.change + ':includingTools', recraft
@@ -140,7 +141,7 @@ module.exports = class CraftingPlan extends BaseModel
@_findSteps inputStack.itemSlug, parentSteps
logger.verbose -> "adding step for: #{recipe.slug}"
@steps[recipe.slug] = recipe:recipe, itemSlug:item.slug
@steps[recipe.slug] = new Step outputItemSlug:item.slug, recipe:recipe
foundValidRecipe = true
break
catch error
@@ -166,16 +167,26 @@ module.exports = class CraftingPlan extends BaseModel
return itemSlug
_removeExtraSteps: ->
result = (step for step in @steps when step.multiplier > 0)
result = []
number = 1
for step in @steps
if step.multiplier > 0
result.push step
step.number = number
number += 1
@steps = result
_resolveNeeds: ->
for i in [@steps.length-1..0] by -1
step = @steps[i]
recipe = step.recipe
outputQuantity = recipe.getQuantityProducedOf step.itemSlug
step.number = i + 1
step.multiplier = Math.ceil(@need.quantityOf(step.itemSlug) / outputQuantity)
recipe = step.recipe
outputQuantity = recipe.getQuantityProducedOf step.outputItemSlug
step.multiplier = Math.ceil(@need.quantityOf(step.outputItemSlug) / outputQuantity)
if @includingTools
recipe.eachToolStack (stack)=>
+9 -4
View File
@@ -20,9 +20,6 @@ module.exports = class Inventory extends BaseModel
attributes.modPack ?= null
@clear()
Object.defineProperties this,
isEmpty: { get:-> @_itemSlugs.length is 0 }
# Class Methods ################################################################################
@Delimiters =
@@ -161,6 +158,14 @@ module.exports = class Inventory extends BaseModel
return parts.join Inventory.Delimiters.Stack
# Property Methods #############################################################################
getIsEmpty: ->
return @_itemSlugs.length is 0
Object.defineProperties @prototype,
isEmpty: { get:@prototype.getIsEmpty }
# Object Overrides #############################################################################
toString: ->
@@ -185,7 +190,7 @@ module.exports = class Inventory extends BaseModel
stack = @_stacks[itemSlug]
if not stack?
stack = new Stack itemSlug:itemSlug, quantity:quantity
@listenTo stack, Event.change, => @trigger Event.change, stack
@listenTo stack, Event.change, => @trigger Event.change, this
@_stacks[itemSlug] = stack
@_itemSlugs.push itemSlug
@_sort()
+1 -1
View File
@@ -14,7 +14,7 @@ module.exports = class ItemSelector extends BaseModel
constructor: (attributes={}, options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
options.isAcceptable ?= (itemSlug)-> return true # accept everything by default
options.isAcceptable ?= (item)-> return true # accept everything by default
super attributes, options
@_isAcceptable = options.isAcceptable
+35
View File
@@ -0,0 +1,35 @@
###
Crafting Guide - step.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
Inventory = require './inventory'
{Event} = require '../constants'
########################################################################################################################
module.exports = class Step extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.outputItemSlug? then throw new Error 'attributes.outputItemSlug is required'
if not attributes.recipe? then throw new Error 'attributes.recipe is required'
attributes.inventory = new Inventory
attributes.number ?= 1
attributes.outputItemSlug ?= null
attributes.multiplier ?= 1
attributes.recipe ?= null
super attributes, options
@_computeInventory()
@on Event.change + ':multiplier', => @_computeInventory()
# Private Methods ##############################################################################
_computeInventory: ->
@inventory.clear()
@recipe.eachInputStack (stack)=>
@inventory.add stack.itemSlug, stack.quantity * @multiplier
+1 -1
View File
@@ -30,4 +30,4 @@
p Craft
.dot
.view__item_selector: p.placeholder find an item...
.view__item_selector.search: p find an item...
+46 -4
View File
@@ -6,8 +6,50 @@
//-
.view__craft_page
.view__inventory.want
.view__inventory.have
.view__inventory.need
.sidebar
ins.view__adsense
.view__crafting_table
.mainBody
.want.section
h2 Items to Make
.panel
.view__inventory.large.editable
//- .tools.section.hideable.hidden
//- h2 Tools
//- .panel
//- .inUse
//- h3 In Use
//- .view__inventory.large
//-
//- .options
//- h3 More Options
//- .view__inventory.large
.ingredients.section.hideable.hidden
h2 Ingredients
.panel
.have
h3 Already In Inventory
.view__inventory.large
.need
h3 Need to Gather
.view__inventory.large
.steps.section.hideable.hidden
h2 Steps
.panel
.instructions.section
h2 Instructions
.panel
p.
Welcome to the Craft page. Here you can get detailed instructions for making any item in your
current mod pack (use the <a href="/configure">Configure</a> page to change it). Making a crafting
plan is very simple:
ol
li Click the "add an item..." button to choose items to craft
li Use the <img src="/images/up.png"> button to mark any items you already possess
li Use the <img src="/images/check.png"> button to complete any step if you already have that item
+3 -10
View File
@@ -6,13 +6,6 @@
//-
.view__inventory
h2
img.icon
p
.panel
.scrollbox
table
tr.edit.hideable.hidden
td.name(colspan="4", width="*"): .view__item_selector: p.button-label add an item...
.toolbar.hideable.hidden
button(name="clear") clear
.item_container
.empty_placeholder.hideable.hidden Empty
.view__item_selector.hideable.hidden.button: p add an item...
+1 -1
View File
@@ -5,7 +5,7 @@
//- All rights reserved.
//-
.view__item.hideable.hidden
.view__item
a
table
tr
-2
View File
@@ -6,8 +6,6 @@
//-
.view__item_selector
p.placeholder find items...
.view__item_selector_popup.hideable.hiding
.search
input
+2 -1
View File
@@ -7,6 +7,7 @@
.view__minimal_recipe
.input
table.view__crafting_grid
.view__crafting_grid
.tool
.output.view__slot
.multiplier
+7 -6
View File
@@ -5,9 +5,10 @@
//- All rights reserved.
//-
tr.view__stack
td.quantity: input
td.icon: img(src='')
td.name(width='*'): a
td.action
button.remove &nbsp;
.view__stack
.quantity: .bubble: input
.icon: img(src='')
.name: a
.action
button.second &nbsp;
button.first &nbsp;
+12
View File
@@ -0,0 +1,12 @@
//-
//- Crafting Guide - step.jade
//-
//- Copyright (c) 2015 by Redwood Labs
//- All rights reserved.
//-
.view__step
h3
.view__inventory
.view__minimal_recipe
button.complete
+12
View File
@@ -101,12 +101,20 @@ All rights reserved.
font-family: $font-family-header;
font-size: $font-size-large;
margin-bottom: 0.5em;
&:first-child {
margin-top: 0;
}
}
h3 {
font-family: $font-family-header;
font-size: $font-size-normal;
margin: 2em 0 0 0.5em;
&:first-child {
margin-top: 0;
}
}
.entry {
@@ -118,6 +126,10 @@ All rights reserved.
background: white;
padding: 1em;
}
&:first-child {
margin-top: 0;
}
}
.slideable {
+35 -11
View File
@@ -7,20 +7,44 @@ All rights reserved.
.view__craft_page {
& > div {
display: inline-block;
margin-top: 2em;
padding: 0 2em;
position: relative;
vertical-align: top;
.want.section {
.view__inventory {
position: relative; width: 100%;
}
}
.view__inventory { width: 50%; height: 30em; }
.tools.section {
.options {
margin-top: 4em;
}
}
.view__crafting_table { width: 50%; height: 30em; }
.ingredients.section {
.need {
margin-top: 4em;
}
}
.want label {
position: absolute; top: 50%; left: 0.5em;
@include transform(translateY(-50%));
.instructions.section {
p {
font-size: $font-size-large;
margin-top: 1em;
&:first-child {
margin-top: 0;
}
}
ol {
margin: 2em 3em 0 3em;
li {
font-family: $font-family-normal;
font-size: $font-size-large;
line-height: $font-size-normal + 0.15em;
list-style: decimal;
margin: 0.66em 0 0 2em;
}
}
}
}
+1
View File
@@ -27,5 +27,6 @@ All rights reserved.
@import 'mod_selector';
@import 'stack';
@import 'slot';
@import 'step';
@import 'tutorial';
@import 'video';
+54 -74
View File
@@ -6,92 +6,72 @@ All rights reserved.
*/
.view__inventory {
min-height: 14em;
width: 100%;
.edit td {
padding-top: 1em;
}
.item_container {
position: relative; width: 100%;
.panel {
position: absolute; top: 3.6em; right: 2em; bottom: 0; left: 2em;
background: $color-background-panel;
text-align: right;
}
.view__stack > div {
height: 4.2em;
padding: 0 1em 0.5em 0;
vertical-align: middle;
.scrollbox {
position: absolute; top: 0em; right: 0em; bottom: 3em; left: 0em;
padding: 0.5em 1em;
overflow: auto;
}
.toolbar {
position: absolute; right: 0; bottom: 0; left: 0; height: 3.5em;
background: $color-background-toolbar;
border-top: 1px solid $color-gray-light;
}
.view__item_selector {
height: 2.5em; width: 80%;
background: $color-gray-faint;
margin: 0 auto;
transition: background $animate-normal;
&:hover {
background: $color-background-panel;
&:last-child {
padding: 0 0 0.5em 0;
}
}
}
button[name="clear"] {
position: absolute; bottom: 0.5em; right: 0.5em;
}
input {
border: 1px solid $color-gray-light;
}
button:disabled {
background: white;
color: $color-gray-light;
}
table {
width: 100%;
}
td {
height: 2.4em;
vertical-align: middle;
}
td.icon {
width: 4.8em;
padding: 0 1em;
img {
width: 100%; height: auto;
max-height: 4.8em;
.icon {
img {
height: 3.6em; width: 3.6em;
}
}
}
td.name {
text-align: left;
padding-right: 1em;
input {
.name {
width: 100%;
border: 0;
border-bottom: 1px dashed $color-gray-light;
outline: 0;
}
&:focus {
border: 1px solid $color-gray-light;
.action {
white-space: nowrap;
button:first-child {
margin-right: 0.5em;
}
}
}
td.action {
width: 4em;
.view__item_selector {
position: relative; width: 80%;
margin: 1em auto;
}
&.large {
.item_container {
.view__stack > div {
height: 5em;
}
.quantity {
.bubble {
border-radius: 3em;
input {
font-size: $font-size-large;
}
}
}
.icon {
img {
height: 4.8em; width: 4.8em;
}
}
.name {
a {
font-size: $font-size-large;
font-weight: bold;
}
}
}
}
}
+20 -8
View File
@@ -6,30 +6,42 @@ All rights reserved.
*/
.view__item_selector {
background: $color-background-panel;
position: relative; min-height: $font-size-normal * 2; min-width: 20em;
border: 0.1em solid $color-gray-medium;
border-radius: 1.5em;
cursor: pointer;
padding: 0 0.75em;
p {
position: relative; top: 50%; width: 100%;
@include transform(translateY(-50%));
&.search {
background: $color-background-panel;
&.placeholder {
p {
color: $color-gray-medium;
font-family: $font-family-input;
font-size: $font-size-normal;
}
}
&.button {
background: $color-gray-faint;
&:hover {
background: $color-background-panel;
}
&.button-label {
p {
color: $color-active;
font-family: $font-family-normal;
font-size: $font-size-normal;
font-weight: bold;
text-align: center;
}
}
p {
position: absolute; top: 50%; width: 100%;
@include transform(translateY(-50%));
font-size: $font-size-normal;
}
}
.view__item_selector_popup {
+8 -7
View File
@@ -44,13 +44,6 @@ All rights reserved.
@include transform(translate(-50%, -50%));
}
.multiplier {
position: absolute; bottom: -2.0em; left: 0; right: 0; height: 1.5em;
font-family: $font-family-header;
font-size: $font-size-large;
text-align: center;
}
.quantity {
position: absolute; right: 0.1em; bottom: 0;
color: white;
@@ -59,4 +52,12 @@ All rights reserved.
text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
}
}
.multiplier {
position: absolute; top: 50%; right: 0; width: 2.7em;
@include transform(translateY(-50%) translateY(2.5em));
font-family: $font-family-header;
font-size: $font-size-large;
text-align: center;
}
}
+4
View File
@@ -29,6 +29,10 @@ All rights reserved.
font-size: $font-size-large;
}
}
.itemGroups {
margin-top: 2em;
}
}
.view__item {
+110 -53
View File
@@ -7,38 +7,36 @@ All rights reserved.
.view__stack {
font-family: $font-family-normal;
display: table-row;
td {
padding: 0.2em 0;
& > div {
display: table-cell;
vertical-align: middle;
}
td.quantity {
position: relative;
width: 5em;
input {
position: absolute; top: 50%; height: $font-size-small; width: 100%;
@include transform(translateY(-50%));
border-radius: 0.75em;
.quantity {
.bubble {
border: 0.1em solid $color-gray-light;
border-radius: 1.5em;
background: $color-ice;
color: $color-background-panel;
font-family: $font-family-normal;
font-size: $font-size-normal;
font-weight: bold;
text-align: center;
&.editable {
&:focus {
background: $color-background-panel;
color: $color-text;
font-family: $font-family-input;
font-weight: normal;
outline: none;
}
input {
width: 3em;
background: none;
border: none;
color: $color-background-panel;
font-family: $font-family-normal;
font-size: $font-size-normal;
font-weight: bold;
line-height: $font-size-normal;
outline: none;
text-align: center;
}
&:hover {
background: $color-background-panel;
&.editable:hover, &.editing {
background: $color-background-panel;
input {
color: $color-text;
font-family: $font-family-input;
font-weight: normal;
@@ -47,10 +45,7 @@ All rights reserved.
}
}
td.icon {
width: 4.6em;
padding: 0.2em 0.5em;
.icon {
img {
width: 100%; height: auto;
max-height: 4.8em;
@@ -58,37 +53,99 @@ All rights reserved.
}
}
td.name a {
color: $color-text;
display: inline;
font-size: $font-size-normal;
text-decoration: none;
.name {
a {
color: $color-text;
font-size: $font-size-normal;
text-decoration: none;
&:hover {
color: $color-active-hover;
text-decoration: underline;
&:hover {
color: $color-active-hover;
text-decoration: underline;
}
}
}
td.action {
.action {
text-align: right;
}
button.remove {
opacity: 0;
position: relative; width: 1.8em; height: 1.8em;
background: url('/images/remove.png') no-repeat center;
border: 0;
outline: none;
button {
position: relative; width: 3.0em; height: 3.0em;
display: inline-block;
&:hover {
background: url('/images/remove-hover.png') no-repeat center;
}
}
border: 0;
font-size: $font-size-small;
margin: 0;
outline: none;
padding: 0;
&:hover {
button.remove {
opacity: 1;
&.check {
background: url('/images/check.png') no-repeat center;
&:active {
background: url('/images/check_active.png') no-repeat center;
}
&:disabled {
background: url('/images/check_disabled.png') no-repeat center !important;
cursor: inherit;
}
&:hover {
background: url('/images/check_hover.png') no-repeat center;
}
}
&.down {
background: url('/images/down.png') no-repeat center;
&:active {
background: url('/images/down_active.png') no-repeat center;
}
&:disabled {
background: url('/images/down_disabled.png') no-repeat center !important;
cursor: inherit;
}
&:hover {
background: url('/images/down_hover.png') no-repeat center;
}
}
&.remove {
background: url('/images/remove.png') no-repeat center;
&:active {
background: url('/images/remove_active.png') no-repeat center;
}
&:disabled {
background: url('/images/remove_disabled.png') no-repeat center !important;
cursor: inherit;
}
&:hover {
background: url('/images/remove_hover.png') no-repeat center;
}
}
&.up {
background: url('/images/up.png') no-repeat center;
&:active {
background: url('/images/up_active.png') no-repeat center;
}
&:disabled {
background: url('/images/up_disabled.png') no-repeat center !important;
cursor: inherit;
}
&:hover {
background: url('/images/up_hover.png') no-repeat center;
}
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
/*
Crafting Page - step.scss
Copyright (C) 2015 by Redwood Labs
All rights reserved.
*/
.view__step {
position: relative; min-height: 24em;
border-top: 0.1em solid $color-gray-medium;
padding: 1em 0;
&:first-child {
border-top: 0;
}
.view__inventory {
margin: 1em 0 0 3em;
}
.view__minimal_recipe {
position: absolute; top: 2em; right: 4.8em;
}
button.complete {
position: absolute; top: 1em; right: 0; height: 3em; width: 3em;
background: url("/images/check.png") no-repeat center;
border: 0;
font-size: $font-size-small;
outline: none;
&:active {
background: url('/images/check_active.png') no-repeat center;
}
&:disabled {
background: url('/images/check_disabled.png') no-repeat center !important;
cursor: inherit;
}
&:hover {
background: url('/images/check_hover.png') no-repeat center;
}
}
}