mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
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:
parent
5e1942c0d3
commit
870ae9a8a1
13 changed files with 274 additions and 2 deletions
|
@ -4,3 +4,8 @@ ThangType = require 'models/ThangType'
|
||||||
module.exports = class ThangTypeCollection extends CocoCollection
|
module.exports = class ThangTypeCollection extends CocoCollection
|
||||||
url: '/db/thang.type'
|
url: '/db/thang.type'
|
||||||
model: ThangType
|
model: ThangType
|
||||||
|
|
||||||
|
fetchHeroes: ->
|
||||||
|
@fetch {
|
||||||
|
url: '/db/thang.type?view=heroes'
|
||||||
|
}
|
||||||
|
|
|
@ -168,6 +168,13 @@ module.exports = class LevelLoader extends CocoClass
|
||||||
@consolidateFlagHistory() if @opponentSession?.loaded
|
@consolidateFlagHistory() if @opponentSession?.loaded
|
||||||
else if session is @opponentSession
|
else if session is @opponentSession
|
||||||
@consolidateFlagHistory() if @session.loaded
|
@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']
|
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
|
||||||
heroConfig = session.get('heroConfig')
|
heroConfig = session.get('heroConfig')
|
||||||
heroConfig ?= me.get('heroConfig') if session is @session and not @headless
|
heroConfig ?= me.get('heroConfig') if session is @session and not @headless
|
||||||
|
|
|
@ -1314,6 +1314,9 @@
|
||||||
sent_verification: "We've sent a verification email to:"
|
sent_verification: "We've sent a verification email to:"
|
||||||
you_can_edit: "You can edit your email address in "
|
you_can_edit: "You can edit your email address in "
|
||||||
account_settings: "Account Settings"
|
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:
|
||||||
teacher_dashboard: "Teacher Dashboard" # Navbar
|
teacher_dashboard: "Teacher Dashboard" # Navbar
|
||||||
|
|
|
@ -145,6 +145,11 @@ module.exports = class Level extends CocoModel
|
||||||
for original, placeholderComponent of placeholders when not placeholdersUsed[original]
|
for original, placeholderComponent of placeholders when not placeholdersUsed[original]
|
||||||
levelThang.components.push placeholderComponent
|
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) ->
|
sortSystems: (levelSystems, systemModels) ->
|
||||||
[sorted, originalsSeen] = [[], {}]
|
[sorted, originalsSeen] = [[], {}]
|
||||||
visit = (system) ->
|
visit = (system) ->
|
||||||
|
|
|
@ -239,6 +239,25 @@ module.exports = class ThangType extends CocoModel
|
||||||
portraitOnly = !!options.portraitOnly
|
portraitOnly = !!options.portraitOnly
|
||||||
"#{@get('name')} - #{options.resolutionFactor} - #{colorConfigs} - #{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) ->
|
getPortraitImage: (spriteOptionsOrKey, size=100) ->
|
||||||
src = @getPortraitSource(spriteOptionsOrKey, size)
|
src = @getPortraitSource(spriteOptionsOrKey, size)
|
||||||
return null unless src
|
return null unless src
|
||||||
|
|
|
@ -41,3 +41,22 @@
|
||||||
#join-class-form
|
#join-class-form
|
||||||
.alert, .progress
|
.alert, .progress
|
||||||
margin-top: 20px
|
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
|
||||||
|
|
40
app/styles/courses/hero-select-modal.sass
Normal file
40
app/styles/courses/hero-select-modal.sass
Normal 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
|
|
@ -40,6 +40,18 @@ block content
|
||||||
.text-center
|
.text-center
|
||||||
h1(data-i18n="courses.welcome_to_page") Welcome to your Courses page!
|
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()
|
if view.classrooms.size()
|
||||||
h3.text-uppercase(data-i18n="courses.my_classes")
|
h3.text-uppercase(data-i18n="courses.my_classes")
|
||||||
hr
|
hr
|
||||||
|
|
27
app/templates/courses/hero-select-modal.jade
Normal file
27
app/templates/courses/hero-select-modal.jade
Normal 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")
|
||||||
|
|
|
@ -4,6 +4,7 @@ template = require 'templates/courses/courses-view'
|
||||||
AuthModal = require 'views/core/AuthModal'
|
AuthModal = require 'views/core/AuthModal'
|
||||||
CreateAccountModal = require 'views/core/CreateAccountModal'
|
CreateAccountModal = require 'views/core/CreateAccountModal'
|
||||||
ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal'
|
ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal'
|
||||||
|
HeroSelectModal = require 'views/courses/HeroSelectModal'
|
||||||
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
|
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
|
||||||
JoinClassModal = require 'views/courses/JoinClassModal'
|
JoinClassModal = require 'views/courses/JoinClassModal'
|
||||||
CourseInstance = require 'models/CourseInstance'
|
CourseInstance = require 'models/CourseInstance'
|
||||||
|
@ -13,6 +14,7 @@ Classroom = require 'models/Classroom'
|
||||||
Classrooms = require 'collections/Classrooms'
|
Classrooms = require 'collections/Classrooms'
|
||||||
LevelSession = require 'models/LevelSession'
|
LevelSession = require 'models/LevelSession'
|
||||||
Campaign = require 'models/Campaign'
|
Campaign = require 'models/Campaign'
|
||||||
|
ThangType = require 'models/ThangType'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
|
|
||||||
# TODO: Test everything
|
# TODO: Test everything
|
||||||
|
@ -24,6 +26,7 @@ module.exports = class CoursesView extends RootView
|
||||||
events:
|
events:
|
||||||
'click #log-in-btn': 'onClickLogInButton'
|
'click #log-in-btn': 'onClickLogInButton'
|
||||||
'click #start-new-game-btn': 'openSignUpModal'
|
'click #start-new-game-btn': 'openSignUpModal'
|
||||||
|
'click .change-hero-btn': 'onClickChangeHeroButton'
|
||||||
'click #join-class-btn': 'onClickJoinClassButton'
|
'click #join-class-btn': 'onClickJoinClassButton'
|
||||||
'submit #join-class-form': 'onSubmitJoinClassForm'
|
'submit #join-class-form': 'onSubmitJoinClassForm'
|
||||||
'click #change-language-link': 'onClickChangeLanguageLink'
|
'click #change-language-link': 'onClickChangeLanguageLink'
|
||||||
|
@ -43,6 +46,16 @@ module.exports = class CoursesView extends RootView
|
||||||
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||||
@supermodel.loadCollection(@courses)
|
@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: ->
|
onCourseInstancesLoaded: ->
|
||||||
map = {}
|
map = {}
|
||||||
for courseInstance in @courseInstances.models
|
for courseInstance in @courseInstances.models
|
||||||
|
@ -76,6 +89,16 @@ module.exports = class CoursesView extends RootView
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
application.tracker?.trackEvent 'Started Student Signup', category: 'Courses'
|
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) ->
|
onSubmitJoinClassForm: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@joinClass()
|
@joinClass()
|
||||||
|
|
42
app/views/courses/HeroSelectModal.coffee
Normal file
42
app/views/courses/HeroSelectModal.coffee
Normal 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()
|
32
test/app/views/courses/CoursesView.spec.coffee
Normal file
32
test/app/views/courses/CoursesView.spec.coffee
Normal 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)
|
38
test/app/views/courses/HeroSelectModal.spec.coffee
Normal file
38
test/app/views/courses/HeroSelectModal.spec.coffee
Normal 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()
|
Loading…
Reference in a new issue