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