Add hero selector for courses mode

Use selected hero in Course mode play

Show selected hero on Courses (in progress)

Add hero select modal

Use short names, only show warriors

Use box-shadow instead of borders

Add tests for HeroSelectModal

Refactor modal opening test

Address code review feedback
This commit is contained in:
phoenixeliot 2016-05-25 15:24:51 -07:00
parent 5e1942c0d3
commit 870ae9a8a1
13 changed files with 274 additions and 2 deletions

View file

@ -3,4 +3,9 @@ ThangType = require 'models/ThangType'
module.exports = class ThangTypeCollection extends CocoCollection
url: '/db/thang.type'
model: ThangType
model: ThangType
fetchHeroes: ->
@fetch {
url: '/db/thang.type?view=heroes'
}

View file

@ -168,6 +168,13 @@ module.exports = class LevelLoader extends CocoClass
@consolidateFlagHistory() if @opponentSession?.loaded
else if session is @opponentSession
@consolidateFlagHistory() if @session.loaded
if @level.get('type', true) in ['course'] # course-ladder is hard to handle because there's 2 sessions
heroConfig = me.get('heroConfig')
return if not heroConfig
url = "/db/thang.type/#{heroConfig.thangType}/version"
if heroResource = @maybeLoadURL(url, ThangType, 'thang')
@worldNecessities.push heroResource
return
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
heroConfig = session.get('heroConfig')
heroConfig ?= me.get('heroConfig') if session is @session and not @headless

View file

@ -1314,6 +1314,9 @@
sent_verification: "We've sent a verification email to:"
you_can_edit: "You can edit your email address in "
account_settings: "Account Settings"
select_your_hero: "Select Your Hero"
select_your_hero_description: "You can always change your hero by going to your Courses page and clicking \"Select Hero\""
select_this_hero: "Select this Hero"
teacher:
teacher_dashboard: "Teacher Dashboard" # Navbar

View file

@ -145,6 +145,11 @@ module.exports = class Level extends CocoModel
for original, placeholderComponent of placeholders when not placeholdersUsed[original]
levelThang.components.push placeholderComponent
# Load the user's chosen hero AFTER getting stats from default char
if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course', 'course-ladder']
heroThangType = me.get('heroConfig')?.thangType
levelThang.thangType = heroThangType if heroThangType
sortSystems: (levelSystems, systemModels) ->
[sorted, originalsSeen] = [[], {}]
visit = (system) ->

View file

@ -239,6 +239,25 @@ module.exports = class ThangType extends CocoModel
portraitOnly = !!options.portraitOnly
"#{@get('name')} - #{options.resolutionFactor} - #{colorConfigs} - #{portraitOnly}"
getHeroShortName: ->
map = {
"Assassin": "Ritic"
"Captain": "Anya"
"Forest Archer": "Naria"
"Goliath": "Okar"
"Guardian": "Illia"
"Knight": "Tharin"
"Librarian": "Hushbaum"
"Necromancer": "Nalfar"
"Ninja": "Amara"
"Potion Master": "Omarn"
"Raider": "Arryn"
"Samurai": "Hattori"
"Sorcerer": "Pender"
"Trapper": "Senick"
}
map[@get('name')]
getPortraitImage: (spriteOptionsOrKey, size=100) ->
src = @getPortraitSource(spriteOptionsOrKey, size)
return null unless src

View file

@ -41,3 +41,22 @@
#join-class-form
.alert, .progress
margin-top: 20px
// Hero display
.current-hero-container
display: flex
justify-content: center
.current-hero-text
font-size: 16pt
.hero-avatar
background-color: #f8f8f8
box-shadow: 0 0 0 1px gray
margin-right: 25px
.current-hero-right-col
display: flex
flex-direction: column
justify-content: space-between
align-items: flex-start

View file

@ -0,0 +1,40 @@
@import "app/styles/style-flat-variables"
#hero-select-modal
.modal-dialog
width: auto
max-width: 900px
.modal-header, .modal-body:not(.secret), .modal-footer
display: flex
flex-direction: column
align-items: center
.modal-footer
margin: 30px
h4
max-width: 500px
.hero-list
display: flex
flex-wrap: wrap
justify-content: center
margin-bottom: -50px
.hero-option
display: flex
flex-direction: column
align-items: center
margin: 0 50px 50px
.hero-avatar
margin: 6px
background-color: #f8f8f8
box-shadow: 0 0 0 1px gray
.current .hero-avatar
box-shadow: 0 0 0 6px gray
.selected .hero-avatar
box-shadow: 0 0 0 6px $gold

View file

@ -39,6 +39,18 @@ block content
.text-center
h1(data-i18n="courses.welcome_to_page") Welcome to your Courses page!
.current-hero-container.text-center.row
.hero-avatar
img(src=view.hero.getPortraitURL())
.current-hero-right-col
.semibold.current-hero-text
span.spr(data-i18n="TODO")
| Current Hero:
span.current-hero-name= view.hero.getHeroShortName()
button.change-hero-btn.btn.btn-lg.btn-forest
span(data-i18n="TODO")
| Change Hero
if view.classrooms.size()
h3.text-uppercase(data-i18n="courses.my_classes")

View file

@ -0,0 +1,27 @@
extends /templates/core/modal-base-flat
block modal-header-content
.text-center
h3(data-i18n="courses.select_your_hero")
h4(data-i18n="courses.select_your_hero_description")
block modal-body-content
.hero-list
if view.heroes.loaded
each hero in view.heroes.models
if hero.get('heroClass') === 'Warrior'
+heroOption(hero)
mixin heroOption(hero)
- var heroID = hero.id
- var selectedState = (state.get('selectedHeroID') === heroID ? 'selected' : (state.get('currentHeroID') === heroID ? 'current' : ''))
.hero-option(data-hero-id=heroID class=selectedState)
.hero-avatar
img(src=hero.getPortraitURL())
.text-h5.hero-name
span= hero.getHeroShortName()
block modal-footer-content
.select-hero-btn.btn.btn-lg.btn-forest
span(data-i18n="courses.select_this_hero")

View file

@ -4,6 +4,7 @@ template = require 'templates/courses/courses-view'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal'
HeroSelectModal = require 'views/courses/HeroSelectModal'
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
JoinClassModal = require 'views/courses/JoinClassModal'
CourseInstance = require 'models/CourseInstance'
@ -13,6 +14,7 @@ Classroom = require 'models/Classroom'
Classrooms = require 'collections/Classrooms'
LevelSession = require 'models/LevelSession'
Campaign = require 'models/Campaign'
ThangType = require 'models/ThangType'
utils = require 'core/utils'
# TODO: Test everything
@ -24,6 +26,7 @@ module.exports = class CoursesView extends RootView
events:
'click #log-in-btn': 'onClickLogInButton'
'click #start-new-game-btn': 'openSignUpModal'
'click .change-hero-btn': 'onClickChangeHeroButton'
'click #join-class-btn': 'onClickJoinClassButton'
'submit #join-class-form': 'onSubmitJoinClassForm'
'click #change-language-link': 'onClickChangeLanguageLink'
@ -43,6 +46,16 @@ module.exports = class CoursesView extends RootView
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@supermodel.loadCollection(@courses)
# TODO: Trim this section for only what's necessary
@hero = new ThangType
defaultHeroOriginal = ThangType.heroes.captain
heroOriginal = me.get('heroConfig')?.thangType or defaultHeroOriginal
@hero.url = "/db/thang.type/#{heroOriginal}/version"
# @hero.setProjection ['name','slug','soundTriggers','featureImages','gems','heroClass','description','components','extendedName','unlockLevelName','i18n']
@supermodel.loadModel(@hero, 'hero')
@listenTo @hero, 'all', ->
@render()
onCourseInstancesLoaded: ->
map = {}
for courseInstance in @courseInstances.models
@ -76,6 +89,16 @@ module.exports = class CoursesView extends RootView
@openModalView(modal)
application.tracker?.trackEvent 'Started Student Signup', category: 'Courses'
onClickChangeHeroButton: ->
modal = new HeroSelectModal({ currentHeroID: @hero.id })
@openModalView(modal)
@listenTo modal, 'hero-select:success', (newHero) =>
# @hero.url = "/db/thang.type/#{me.get('heroConfig').thangType}/version"
# @hero.fetch()
@hero.set(newHero.attributes)
@listenTo modal, 'hide', ->
@stopListening modal
onSubmitJoinClassForm: (e) ->
e.preventDefault()
@joinClass()
@ -136,7 +159,7 @@ module.exports = class CoursesView extends RootView
classroomCourseInstances.fetch({ data: {classroomID: newClassroom.id} })
@listenToOnce classroomCourseInstances, 'sync', ->
# TODO: Smoother system for joining a classroom and course instances, without requiring page reload,
# and showing which class was just joined.
# and showing which class was just joined.
document.location.search = '' # Using document.location.reload() causes an infinite loop of reloading
onClickChangeLanguageLink: ->

View file

@ -0,0 +1,42 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/courses/hero-select-modal'
Classroom = require 'models/Classroom'
ThangTypes = require 'collections/ThangTypes'
State = require 'models/State'
ThangType = require 'models/ThangType'
User = require 'models/User'
module.exports = class HeroSelectModal extends ModalView
id: 'hero-select-modal'
template: template
events:
'click .select-hero-btn': 'onClickSelectHeroButton'
'click .hero-option': 'onClickHeroOption'
initialize: ({ currentHeroID }) ->
@debouncedRender = _.debounce @render, 0
@state = new State({
currentHeroID
selectedHeroID: currentHeroID
})
@heroes = new ThangTypes({}, { project: ['original', 'name', 'heroClass'] })
@supermodel.trackRequest @heroes.fetchHeroes()
@listenTo @state, 'all', -> @debouncedRender()
@listenTo @heroes, 'all', -> @debouncedRender()
onClickHeroOption: (e) ->
heroID = $(e.currentTarget).data('hero-id')
@state.set selectedHeroID: heroID
hero = @heroes.get(heroID)
me.set(heroConfig: {}) unless me.get('heroConfig')
heroConfig = _.assign me.get('heroConfig'), { thangType: hero.get('original') }
me.set({ heroConfig })
me.save().then =>
@trigger 'hero-select:success', hero
onClickSelectHeroButton: () ->
@hide()

View file

@ -0,0 +1,32 @@
CoursesView = require 'views/courses/CoursesView'
HeroSelectModal = require 'views/courses/HeroSelectModal'
Classrooms = require 'collections/Classrooms'
CourseInstances = require 'collections/CourseInstances'
Courses = require 'collections/Courses'
auth = require 'core/auth'
factories = require 'test/app/factories'
describe 'CoursesView', ->
modal = null
view = null
describe 'Change Hero button', ->
beforeEach (done) ->
view = new CoursesView()
classrooms = new Classrooms([factories.makeClassroom()])
courseInstances = new CourseInstances([factories.makeCourseInstance()])
courses = new Courses([factories.makeCourse()])
view.classrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() })
view.ownedClassrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() })
view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: courseInstances.stringify() })
view.render()
jasmine.demoEl(view.$el)
done()
it 'opens the modal when you click Change Hero', ->
spyOn(view, 'openModalView')
view.$('.change-hero-btn').click()
expect(view.openModalView).toHaveBeenCalled()
args = view.openModalView.calls.argsFor(0)
expect(args[0] instanceof HeroSelectModal).toBe(true)

View file

@ -0,0 +1,38 @@
HeroSelectModal = require 'views/courses/HeroSelectModal'
auth = require 'core/auth'
factories = require 'test/app/factories'
describe 'HeroSelectModal', ->
modal = null
coursesView = null
user = null
hero1 = factories.makeThangType({ original: "hero1original", _id: "hero1id", heroClass: "Warrior", name: "Hero 1" })
hero2 = factories.makeThangType({ original: "hero2original", _id: "hero2id", heroClass: "Warrior", name: "Hero 2" })
heroesResponse = JSON.stringify([hero1, hero2])
beforeEach (done) ->
window.me = user = factories.makeUser({ heroConfig: { thangType: hero1.get('original') } })
auth.loginUser(user.attributes)
modal = new HeroSelectModal({ currentHeroID: hero1.id })
modal.heroes.fakeRequests[0].respondWith({ status: 200, responseText: heroesResponse })
jasmine.demoModal(modal)
_.defer ->
modal.render()
done()
afterEach ->
modal.stopListening()
it 'highlights the current hero', ->
expect(modal.$(".hero-option[data-hero-id='#{hero1.id}']")[0].className.split(" ")).toContain('selected')
it 'saves when you change heroes', (done) ->
modal.$(".hero-option[data-hero-id='#{hero2.id}']").click()
_.defer ->
expect(user.fakeRequests.length).toBe(1)
request = user.fakeRequests[0]
expect(request.method).toBe("PUT")
expect(JSON.parse(request.params).heroConfig?.thangType).toBe(hero2.get('original'))
done()