mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
4a85290af8
18 changed files with 541 additions and 169 deletions
|
@ -96,6 +96,8 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
|
||||
'github/*path': 'routeToServer'
|
||||
|
||||
'hoc': go('courses/CoursesView')
|
||||
|
||||
'i18n': go('i18n/I18NHomeView')
|
||||
'i18n/thang/:handle': go('i18n/I18NEditThangTypeView')
|
||||
'i18n/component/:handle': go('i18n/I18NEditComponentView')
|
||||
|
|
98
app/lib/sprites/SpriteExporter.coffee
Normal file
98
app/lib/sprites/SpriteExporter.coffee
Normal file
|
@ -0,0 +1,98 @@
|
|||
SpriteBuilder = require('./SpriteBuilder')
|
||||
ThangType = require('models/ThangType')
|
||||
CocoClass = require('core/CocoClass')
|
||||
|
||||
|
||||
class SpriteExporter extends CocoClass
|
||||
'''
|
||||
To be used by the ThangTypeEditView to export ThangTypes to single sprite sheets which can be uploaded to
|
||||
GridFS and used in gameplay, avoiding rendering vector images.
|
||||
|
||||
Code has been copied and reworked and simplified from LayerAdapter. Some shared code has been refactored into
|
||||
ThangType, but more work could be done to rethink and reorganize Sprite rendering.
|
||||
'''
|
||||
|
||||
constructor: (thangType, options) ->
|
||||
@thangType = thangType
|
||||
options ?= {}
|
||||
@colorConfig = options.colorConfig or {}
|
||||
@resolutionFactor = options.resolutionFactor or 1
|
||||
@actionNames = options.actionNames or (action.name for action in @thangType.getDefaultActions())
|
||||
super()
|
||||
|
||||
build: (renderType) ->
|
||||
spriteSheetBuilder = new createjs.SpriteSheetBuilder()
|
||||
if (renderType or @thangType.get('spriteType') or 'segmented') is 'segmented'
|
||||
@renderSegmentedThangType(spriteSheetBuilder)
|
||||
else
|
||||
@renderSingularThangType(spriteSheetBuilder)
|
||||
try
|
||||
spriteSheetBuilder.buildAsync()
|
||||
catch e
|
||||
@resolutionFactor *= 0.9
|
||||
return @build()
|
||||
spriteSheetBuilder.on 'complete', @onBuildSpriteSheetComplete, @, true, spriteSheetBuilder
|
||||
@asyncBuilder = spriteSheetBuilder
|
||||
|
||||
renderSegmentedThangType: (spriteSheetBuilder) ->
|
||||
containersToRender = @thangType.getContainersForActions(@actionNames)
|
||||
spriteBuilder = new SpriteBuilder(@thangType, {colorConfig: @colorConfig})
|
||||
for containerGlobalName in containersToRender
|
||||
container = spriteBuilder.buildContainerFromStore(containerGlobalName)
|
||||
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (@thangType.get('scale') or 1))
|
||||
spriteSheetBuilder.addAnimation(containerGlobalName, [frame], false)
|
||||
|
||||
renderSingularThangType: (spriteSheetBuilder) ->
|
||||
actionObjects = _.values(@thangType.getActions())
|
||||
animationActions = []
|
||||
for a in actionObjects
|
||||
continue unless a.animation
|
||||
continue unless a.name in @actionNames
|
||||
animationActions.push(a)
|
||||
|
||||
spriteBuilder = new SpriteBuilder(@thangType, {colorConfig: @colorConfig})
|
||||
|
||||
animationGroups = _.groupBy animationActions, (action) -> action.animation
|
||||
for animationName, actions of animationGroups
|
||||
scale = actions[0].scale or @thangType.get('scale') or 1
|
||||
mc = spriteBuilder.buildMovieClip(animationName, null, null, null, {'temp':0})
|
||||
spriteSheetBuilder.addMovieClip(mc, null, scale * @resolutionFactor)
|
||||
frames = spriteSheetBuilder._animations['temp'].frames
|
||||
framesMap = _.zipObject _.range(frames.length), frames
|
||||
for action in actions
|
||||
if action.frames
|
||||
frames = (framesMap[parseInt(frame)] for frame in action.frames.split(','))
|
||||
else
|
||||
frames = _.sortBy(_.values(framesMap))
|
||||
next = @nextForAction(action)
|
||||
spriteSheetBuilder.addAnimation(action.name, frames, next)
|
||||
|
||||
containerActions = []
|
||||
for a in actionObjects
|
||||
continue unless a.container
|
||||
continue unless a.name in @actionNames
|
||||
containerActions.push(a)
|
||||
|
||||
containerGroups = _.groupBy containerActions, (action) -> action.container
|
||||
for containerName, actions of containerGroups
|
||||
container = spriteBuilder.buildContainerFromStore(containerName)
|
||||
scale = actions[0].scale or @thangType.get('scale') or 1
|
||||
frame = spriteSheetBuilder.addFrame(container, null, scale * @resolutionFactor)
|
||||
for action in actions
|
||||
spriteSheetBuilder.addAnimation(action.name, [frame], false)
|
||||
|
||||
onBuildSpriteSheetComplete: (e, builder) ->
|
||||
if builder.spriteSheet._images.length > 1
|
||||
total = 0
|
||||
# get a rough estimate of how much smaller the spritesheet needs to be
|
||||
for image, index in builder.spriteSheet._images
|
||||
total += image.height / builder.maxHeight
|
||||
@resolutionFactor /= (Math.max(1.1, Math.sqrt(total)))
|
||||
@_renderNewSpriteSheet(e.async)
|
||||
return
|
||||
|
||||
@trigger 'build', { spriteSheet: builder.spriteSheet }
|
||||
|
||||
|
||||
|
||||
module.exports = SpriteExporter
|
|
@ -22,6 +22,7 @@ SpriteBuilder = require 'lib/sprites/SpriteBuilder'
|
|||
CocoClass = require 'core/CocoClass'
|
||||
SegmentedSprite = require './SegmentedSprite'
|
||||
SingularSprite = require './SingularSprite'
|
||||
ThangType = require 'models/ThangType'
|
||||
|
||||
NEVER_RENDER_ANYTHING = false # set to true to test placeholders
|
||||
|
||||
|
@ -42,7 +43,6 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
buildAutomatically: true
|
||||
buildAsync: true
|
||||
resolutionFactor: SPRITE_RESOLUTION_FACTOR
|
||||
defaultActions: ['idle', 'die', 'move', 'attack']
|
||||
numThingsLoading: 0
|
||||
lanks: null
|
||||
spriteSheet: null
|
||||
|
@ -196,7 +196,7 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
@upsertActionToRender(lank.thangType)
|
||||
else
|
||||
for action in _.values(lank.thangType.getActions())
|
||||
continue unless _.any @defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
|
||||
continue unless _.any ThangType.defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
|
||||
@upsertActionToRender(lank.thangType, action.name, lank.options.colorConfig)
|
||||
|
||||
upsertActionToRender: (thangType, actionName, colorConfig) ->
|
||||
|
@ -346,17 +346,9 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
#- Rendering containers for segmented thang types
|
||||
|
||||
renderSegmentedThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
|
||||
containersToRender = {}
|
||||
for actionName in actionNames
|
||||
action = _.find(thangType.getActions(), {name: actionName})
|
||||
if action.container
|
||||
containersToRender[action.container] = true
|
||||
else if action.animation
|
||||
animationContainers = @getContainersForAnimation(thangType, action.animation, action)
|
||||
containersToRender[container.gn] = true for container in animationContainers
|
||||
|
||||
containersToRender = thangType.getContainersForActions(actionNames)
|
||||
spriteBuilder = new SpriteBuilder(thangType, {colorConfig: colorConfig})
|
||||
for containerGlobalName in _.keys(containersToRender)
|
||||
for containerGlobalName in containersToRender
|
||||
containerKey = @renderGroupingKey(thangType, containerGlobalName, colorConfig)
|
||||
if @spriteSheet?.resolutionFactor is @resolutionFactor and containerKey in @spriteSheet.getAnimations()
|
||||
container = new createjs.Sprite(@spriteSheet)
|
||||
|
@ -367,15 +359,6 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
|
|||
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (thangType.get('scale') or 1))
|
||||
spriteSheetBuilder.addAnimation(containerKey, [frame], false)
|
||||
|
||||
getContainersForAnimation: (thangType, animation, action) ->
|
||||
rawAnimation = thangType.get('raw').animations[animation]
|
||||
if not rawAnimation
|
||||
console.error 'thang type', thangType.get('name'), 'is missing animation', animation, 'from action', action
|
||||
containers = rawAnimation.containers
|
||||
for animation in thangType.get('raw').animations[animation].animations
|
||||
containers = containers.concat(@getContainersForAnimation(thangType, animation.gn, action))
|
||||
return containers
|
||||
|
||||
#- Rendering sprite sheets for singular thang types
|
||||
|
||||
renderSingularThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
|
||||
|
|
|
@ -913,7 +913,7 @@
|
|||
see_the: "See the"
|
||||
more_info: "for more information."
|
||||
choose_course: "Choose Your Course:"
|
||||
enter_code: "Enter an unlock code"
|
||||
enter_code: "Enter an unlock code to join an existing class" # {change}
|
||||
enter_code1: "Enter unlock code"
|
||||
enroll: "Enroll"
|
||||
pick_from_classes: "Pick from your current classes"
|
||||
|
|
|
@ -602,7 +602,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
intro_2: "無需經驗!"
|
||||
free_title: "要多少錢?"
|
||||
cost_china: "CodeCombat的前5個關卡在中國是免費的,在這之後需花費每月9.99美元來訪問我們架設在中國專屬服務器上的190多個關卡。" # Deprecated
|
||||
# cost_premium_server: "CodeCombat is free for the first five levels, after which it costs $9.99 USD per month for access to our other 190+ levels on our exclusive country-specific servers."
|
||||
cost_premium_server: "CodeCombat的前5個關卡在中國是免費的,在這之後需花費每月9.99美元來訪問我們架設在中國專屬服務器上的190多個關卡。"
|
||||
free_1: "有110多個覆蓋了所有理論的免費關卡。"
|
||||
free_2: "包月訂閱可以訪問視頻教程和額外的練習關卡。"
|
||||
teacher_subs_title: "教師可免費訂閱!"
|
||||
|
@ -835,93 +835,94 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
last_played: "最後玩了"
|
||||
leagues_explanation: "在部落裡與其他成員組成聯盟一起參加下面的多人競技場。"
|
||||
|
||||
# courses:
|
||||
# course: "Course"
|
||||
# courses: "courses"
|
||||
# not_enrolled: "You are not enrolled in this course."
|
||||
# visit_pref: "Please visit the"
|
||||
# visit_suf: "page to enroll."
|
||||
# select_class: "Select one of your classes"
|
||||
# unnamed: "*unnamed*"
|
||||
# select: "Select"
|
||||
# unnamed_class: "Unnamed Class"
|
||||
# edit_settings: "edit class settings"
|
||||
# edit_settings1: "Edit Class Settings"
|
||||
# progress: "Class Progress"
|
||||
# add_students: "Add Students"
|
||||
# stats: "Statistics"
|
||||
# total_students: "Total students:"
|
||||
# average_time: "Average level play time:"
|
||||
# total_time: "Total play time:"
|
||||
# average_levels: "Average levels completed:"
|
||||
# total_levels: "Total levels completed:"
|
||||
# furthest_level: "Furthest level completed:"
|
||||
# concepts_covered: "Concepts Covered"
|
||||
# students: "Students"
|
||||
# students1: "students"
|
||||
# expand_details: "Expand details"
|
||||
# concepts: "Concepts"
|
||||
# levels: "levels"
|
||||
# played: "Played"
|
||||
# play_time: "Play time:"
|
||||
# completed: "Completed:"
|
||||
# invite_students: "Invite students to join this class."
|
||||
# invite_link_header: "Link to join course"
|
||||
# invite_link_p_1: "Give this link to students you would like to have join the course."
|
||||
# invite_link_p_2: "Or have us email them directly:"
|
||||
# capacity_used: "Course slots used:"
|
||||
# enter_emails: "Enter student emails to invite, one per line"
|
||||
# send_invites: "Send Invites"
|
||||
# title: "Title"
|
||||
# description: "Description"
|
||||
# languages_available: "Select programming languages available to the class:"
|
||||
# all_lang: "All Languages"
|
||||
# show_progress: "Show student progress to everyone in the class"
|
||||
# creating_class: "Creating class..."
|
||||
# purchasing_course: "Purchasing course..."
|
||||
# buy_course: "Buy Course"
|
||||
# buy_course1: "Buy this course"
|
||||
# create_class: "Create Class"
|
||||
# select_all_courses: "Select 'All Courses' for a 50% discount!"
|
||||
# all_courses: "All Courses"
|
||||
# number_students: "Number of students"
|
||||
# enter_number_students: "Enter the number of students you need for this class."
|
||||
# name_class: "Name your class"
|
||||
# displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later."
|
||||
# buy: "Buy"
|
||||
# purchasing_for: "You are purchasing a license for"
|
||||
# creating_for: "You are creating a class for"
|
||||
# for: "for" # Like in 'for 30 students'
|
||||
# receive_code: "Afterwards you will receive an unlock code to distribute to your students, which they can use to enroll in your class."
|
||||
# free_trial: "Free trial for teachers!"
|
||||
# get_access: "to get individual access to all courses for evalutaion purposes."
|
||||
# questions: "Questions?"
|
||||
# faq: "Courses FAQ"
|
||||
# question: "Q:" # Like in 'Question'
|
||||
# question1: "What's the difference between these courses and the single player game?"
|
||||
# answer: "A:" # Like in 'Answer'
|
||||
# answer1: "The single player game is designed for individuals, while the courses are designed for classes."
|
||||
# answer2: "The single player game has items, gems, hero selection, leveling up, and in-app purchases. Courses have classroom management features and streamlined student-focused level pacing."
|
||||
# teachers_click: "Teachers Click Here"
|
||||
# students_click: "Students Click Here"
|
||||
# courses_on_coco: "Courses on CodeCombat"
|
||||
# designed_to: "Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours."
|
||||
# more_in_less: "Learn more in less time"
|
||||
# no_experience: "No coding experience necesssary"
|
||||
# easy_monitor: "Easily monitor student progress"
|
||||
# purchase_for_class: "Purchase a course for your entire class. It's easy to sign up your students!"
|
||||
# see_the: "See the"
|
||||
# more_info: "for more information."
|
||||
# choose_course: "Choose Your Course:"
|
||||
# enter_code: "Enter an unlock code"
|
||||
# enter_code1: "Enter unlock code"
|
||||
# enroll: "Enroll"
|
||||
# pick_from_classes: "Pick from your current classes"
|
||||
# enter: "Enter"
|
||||
# or: "Or"
|
||||
# topics: "Topics"
|
||||
# hours_content: "Hours of content:"
|
||||
# get_free: "Get FREE course"
|
||||
|
||||
courses:
|
||||
course: "課程"
|
||||
courses: "課程"
|
||||
not_enrolled: "您没有註冊這一節課。"
|
||||
visit_pref: "請到這個"
|
||||
visit_suf: "網頁註冊。"
|
||||
select_class: "請選其中一門課堂"
|
||||
unnamed: "*未命名*"
|
||||
select: "選擇"
|
||||
unnamed_class: "課堂未命名"
|
||||
edit_settings: "編輯課堂設定"
|
||||
edit_settings1: "編輯課堂設定"
|
||||
progress: "課堂進度"
|
||||
add_students: "添加學生"
|
||||
stats: "統計"
|
||||
total_students: "學生人數:"
|
||||
average_time: "平均遊戲時間:"
|
||||
total_time: "總計遊戲時間:"
|
||||
average_levels: "平均完成關卡:"
|
||||
total_levels: "總共完成關卡:"
|
||||
furthest_level: "最高關卡完成:"
|
||||
concepts_covered: "課目覆蓋"
|
||||
students: "學生"
|
||||
students1: "個學生。"
|
||||
expand_details: "展開細節"
|
||||
concepts: "課目"
|
||||
levels: "關卡"
|
||||
played: "已通關"
|
||||
play_time: "遊戲時間:"
|
||||
completed: "遊戲時間:"
|
||||
invite_students: "邀請學生加入此課堂。"
|
||||
invite_link_header: "參與課堂的縺結"
|
||||
invite_link_p_1: "把這個參與課堂的連結發給你認可的學生。" # there has few problem of my translation
|
||||
invite_link_p_2: "或讓我們代你直接發送電郵:"
|
||||
capacity_used: "Course slots used:"
|
||||
enter_emails: "輸入學生電郵地址來邀請,每行一個"
|
||||
send_invites: "發送邀請"
|
||||
title: "發送邀請"
|
||||
description: "描述"
|
||||
languages_available: "選擇編程語言:"
|
||||
all_lang: "所有編程語言"
|
||||
show_progress: "向所有該課堂的人展示學生的進度"
|
||||
creating_class: "課堂創建中···"
|
||||
purchasing_course: "購買課程中···"
|
||||
buy_course: "購買課程"
|
||||
buy_course1: "購買這個課程"
|
||||
create_class: "創建課堂"
|
||||
select_all_courses: "可半價一次購買所有課程!"
|
||||
all_courses: "所有課程"
|
||||
number_students: "學生人數"
|
||||
enter_number_students: "輪入該課堂的學生上限人數"
|
||||
name_class: "命名您的課堂"
|
||||
displayed_course_page: "這將會在課程頁面顯示,可被修改。"
|
||||
buy: "購買"
|
||||
purchasing_for: "你正在購買許可證,課程為"
|
||||
creating_for: "您正在創建一個課程為"
|
||||
for: ",人數上限為" # Like in 'for 30 students'
|
||||
receive_code: "然後您會收到一個解鎖碼,把它分發給你的學生用來註冊你的課堂。"
|
||||
free_trial: "老師可免費試用!"
|
||||
get_access: "獲得個人使用權在評估目的下來使用所有課程。"
|
||||
questions: "有疑問?"
|
||||
faq: "課程FAQ"
|
||||
question: "問:" # Like in 'Question'
|
||||
question1: "這些課程和單人遊戲之間的有什麼區別?"
|
||||
answer: "答:" # Like in 'Answer'
|
||||
answer1: "單人遊戲是專為個人而設,而課程是專為課堂而設。"
|
||||
answer2: "在單人遊戲中有物品、寶石、英雄選擇、練級、和內購應用。課程有課堂管理功能,和老師可根據學生的水平而調整教學進度。"
|
||||
teachers_click: "老師點擊這裡"
|
||||
students_click: "學生點擊這裡"
|
||||
courses_on_coco: "CodeCombat上的課程"
|
||||
designed_to: "CodeCombat課程的宗旨是在使用CodeCombat生動有趣的環境下教授計算機科學的課目。整個CodeCombat的關卡是圍繞著計算機科學的重點並激勵學生們自主向上學習在5小時的過程。"
|
||||
more_in_less: "以最少的時間學習最多的知識"
|
||||
no_experience: "無需編程經驗"
|
||||
easy_monitor: "容易管理學生的進程"
|
||||
purchase_for_class: "為你的班級購買CodeCombat課程,讓簽到和管理變得更容易!"
|
||||
see_the: "詳細資訊請看"
|
||||
more_info: "。"
|
||||
choose_course: "選擇您的課程:"
|
||||
enter_code: "輸入一個解銷碼"
|
||||
enter_code1: "輸入解銷碼"
|
||||
enroll: "註冊"
|
||||
pick_from_classes: "從目前的課程選擇"
|
||||
enter: "輪入"
|
||||
or: "或"
|
||||
topics: "題目"
|
||||
hours_content: "內容時間:"
|
||||
get_free: "取得免費課程!"
|
||||
|
||||
classes:
|
||||
archmage_title: "大法師"
|
||||
|
@ -1207,7 +1208,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
recently_played: "最近玩過"
|
||||
no_recent_games: "在過去兩個星期沒有玩過遊戲。"
|
||||
payments: "付款"
|
||||
# prepaid: "Prepaid"
|
||||
prepaid: "充值"
|
||||
purchased: "已購買"
|
||||
sale: "促銷"
|
||||
subscription: "訂閱"
|
||||
|
@ -1238,13 +1239,13 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
retrying: "服務器錯誤,重試中。"
|
||||
success: "支付成功。謝謝!"
|
||||
|
||||
# account_prepaid:
|
||||
# purchase_code: "Purchase a Subscription Code"
|
||||
# purchase_amount: "Amount"
|
||||
# purchase_total: "Total"
|
||||
# purchase_button: "Submit Purchase"
|
||||
# your_codes: "Your Codes:"
|
||||
# redeem_codes: "Redeem a Subscription Code"
|
||||
account_prepaid:
|
||||
purchase_code: "購買訂閱碼"
|
||||
purchase_amount: "數量"
|
||||
purchase_total: "總共"
|
||||
purchase_button: "提交購買"
|
||||
your_codes: "你的訂閱碼:"
|
||||
redeem_codes: "兌換訂閱碼"
|
||||
|
||||
loading_error:
|
||||
could_not_load: "從伺服器載入失敗"
|
||||
|
@ -1258,7 +1259,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
bad_input: "錯誤輸入。"
|
||||
server_error: "伺服器錯誤。"
|
||||
unknown: "未知錯誤。"
|
||||
# error: "ERROR"
|
||||
error: "錯誤"
|
||||
|
||||
resources:
|
||||
sessions: "會話"
|
||||
|
|
|
@ -35,6 +35,7 @@ module.exports = class ThangType extends CocoModel
|
|||
urlRoot: '/db/thang.type'
|
||||
building: {}
|
||||
editableByArtisans: true
|
||||
@defaultActions: ['idle', 'die', 'move', 'attack']
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
@ -78,6 +79,14 @@ module.exports = class ThangType extends CocoModel
|
|||
return {} unless @isFullyLoaded()
|
||||
return @actions or @buildActions()
|
||||
|
||||
getDefaultActions: ->
|
||||
actions = []
|
||||
for action in _.values(@getActions())
|
||||
continue unless _.any ThangType.defaultActions, (prefix) ->
|
||||
_.string.startsWith(action.name, prefix)
|
||||
actions.push(action)
|
||||
return actions
|
||||
|
||||
buildActions: ->
|
||||
return null unless @isFullyLoaded()
|
||||
@actions = $.extend(true, {}, @get('actions'))
|
||||
|
@ -479,3 +488,24 @@ module.exports = class ThangType extends CocoModel
|
|||
playerLevel = me.constructor.levelForTier playerTier
|
||||
#console.log 'Level required for', @get('name'), 'is', playerLevel, 'player tier', playerTier, 'because it is itemTier', itemTier, 'which is normally level', me.constructor.levelForTier(itemTier)
|
||||
playerLevel
|
||||
|
||||
getContainersForAnimation: (animation, action) ->
|
||||
rawAnimation = @get('raw').animations[animation]
|
||||
if not rawAnimation
|
||||
console.error 'thang type', @get('name'), 'is missing animation', animation, 'from action', action
|
||||
containers = rawAnimation.containers
|
||||
for animation in @get('raw').animations[animation].animations
|
||||
containers = containers.concat(@getContainersForAnimation(animation.gn, action))
|
||||
return containers
|
||||
|
||||
getContainersForActions: (actionNames) ->
|
||||
containersToRender = {}
|
||||
actions = @getActions()
|
||||
for actionName in actionNames
|
||||
action = _.find(actions, {name: actionName})
|
||||
if action.container
|
||||
containersToRender[action.container] = true
|
||||
else if action.animation
|
||||
animationContainers = @getContainersForAnimation(action.animation, action)
|
||||
containersToRender[container.gn] = true for container in animationContainers
|
||||
return _.keys(containersToRender)
|
||||
|
|
|
@ -169,6 +169,42 @@ _.extend ThangTypeSchema.properties,
|
|||
extendedName: {type: 'string', title: 'Extended Hero Name', description: 'The long form of the hero\'s name. Ex.: "Captain Anya Weston".'}
|
||||
unlockLevelName: {type: 'string', title: 'Unlock Level Name', description: 'The name of the level in which the hero is unlocked.'}
|
||||
tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this ThangType.'}, c.task
|
||||
spriteSheets: c.array {title: 'SpriteSheets'},
|
||||
c.object {title: 'SpriteSheet'},
|
||||
actionNames: { type: 'array' }
|
||||
animations:
|
||||
type: 'object'
|
||||
description: 'Third EaselJS SpriteSheet animations format'
|
||||
additionalProperties: {
|
||||
description: 'EaselJS animation'
|
||||
type: 'object'
|
||||
properties: {
|
||||
frames: { type: 'array' }
|
||||
next: { type: ['string', 'null'] }
|
||||
speed: { type: 'number' }
|
||||
}
|
||||
}
|
||||
colorConfig: c.colorConfig()
|
||||
colorLabel: { enum: ['red', 'green', 'blue'] }
|
||||
frames:
|
||||
type: 'array'
|
||||
description: 'Second EaselJS SpriteSheet frames format'
|
||||
items:
|
||||
type: 'array'
|
||||
items: [
|
||||
{ type: 'number', title: 'x' }
|
||||
{ type: 'number', title: 'y' }
|
||||
{ type: 'number', title: 'width' }
|
||||
{ type: 'number', title: 'height' }
|
||||
{ type: 'number', title: 'imageIndex' }
|
||||
{ type: 'number', title: 'regX' }
|
||||
{ type: 'number', title: 'regY' }
|
||||
]
|
||||
image: { type: 'string', format: 'image-file' }
|
||||
resolutionFactor: {
|
||||
type: 'number'
|
||||
}
|
||||
spriteType: { enum: ['singular', 'segmented'], title: 'Sprite Type' }
|
||||
|
||||
|
||||
ThangTypeSchema.required = []
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
#courses-view
|
||||
|
||||
.logged_out
|
||||
font-size: 24px
|
||||
|
||||
.signup-button
|
||||
background: red
|
||||
color: white
|
||||
font-size: 18px
|
||||
font-variant: small-caps
|
||||
line-height: 27px
|
||||
text-transform: uppercase
|
||||
margin-right: 20px
|
||||
|
||||
.login-button
|
||||
background: white
|
||||
color: black
|
||||
font-size: 18px
|
||||
font-variant: small-caps
|
||||
line-height: 27px
|
||||
text-transform: uppercase
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
|
@ -25,9 +45,6 @@
|
|||
margin-bottom: 20px
|
||||
font-size: 14pt
|
||||
|
||||
.btn-enroll
|
||||
margin-top: 20px
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
|
|
|
@ -10,29 +10,61 @@ block content
|
|||
if state === 'enrolling'
|
||||
.alert.alert-info Enrolling in course..
|
||||
else if state === 'ppc_logged_out'
|
||||
.alert.alert-success Log in or create an account to join this course.
|
||||
.alert.alert-danger.logged_out Create account or log in to join this course.
|
||||
button.btn.btn-sm.btn-primary.header-font.signup-button(data-i18n="login.sign_up")
|
||||
button.btn.btn-sm.btn-default.header-font.login-button(data-i18n="login.log_in")
|
||||
else
|
||||
if state === 'unknown_error'
|
||||
.alert.alert-danger.alert-dismissible= stateMessage
|
||||
|
||||
if studentMode
|
||||
+student-main
|
||||
if hocLandingPage
|
||||
+hoc-landing
|
||||
else
|
||||
+teacher-main
|
||||
.container-fluid
|
||||
- var i = 0
|
||||
while i < courses.length
|
||||
.row
|
||||
+course-block(courses[i], instances)
|
||||
- i++
|
||||
if i < courses.length
|
||||
if studentMode
|
||||
+student-main
|
||||
else
|
||||
if hocMode
|
||||
+teacher-hoc
|
||||
else
|
||||
+teacher-main
|
||||
.container-fluid
|
||||
- var i = 0
|
||||
while i < courses.length
|
||||
.row
|
||||
+course-block(courses[i], instances)
|
||||
- i++
|
||||
if i < courses.length
|
||||
+course-block(courses[i], instances)
|
||||
- i++
|
||||
|
||||
mixin hoc-landing
|
||||
h1.center Welcome Hour of Code!
|
||||
br
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6.center
|
||||
button.btn.btn-lg.btn-success.btn-student(data-i18n="courses.students_click")
|
||||
.col-md-6.center
|
||||
button.btn.btn-lg.btn-default.btn-teacher(data-i18n="courses.teachers_click")
|
||||
|
||||
mixin student-main
|
||||
button.btn.btn-warning.btn-teacher(data-i18n="courses.teachers_click")
|
||||
h1.center(data-i18n="courses.courses_on_coco")
|
||||
|
||||
mixin teacher-hoc
|
||||
button.btn.btn-warning.btn-student(data-i18n="courses.students_click")
|
||||
h1.center Welcome Hour of Code!
|
||||
p
|
||||
strong How to use CodeCombat with your students:
|
||||
ol
|
||||
li Click the green 'Get FREE course' button below
|
||||
li Follow the enrollment instructions
|
||||
li Add students via the 'Add Students' tab
|
||||
p
|
||||
span.spr If you have any problems, please email
|
||||
a(href='mailto:team@codecombat.com') team@codecombat.com
|
||||
br
|
||||
|
||||
mixin teacher-main
|
||||
button.btn.btn-warning.btn-student(data-i18n="courses.students_click")
|
||||
h1.center(data-i18n="courses.courses_on_coco")
|
||||
|
@ -87,6 +119,14 @@ mixin student-dialog(course)
|
|||
input.code-input(type='text', data-course-id="#{course.id}", data-i18n="[placeholder]courses.enter_code1", placeholder="Enter unlock code")
|
||||
.col-md-4
|
||||
button.btn.btn-success.btn-enroll(data-course-id="#{course.id}", data-i18n="courses.enroll")
|
||||
if hocMode && course.get('pricePerSeat') === 0
|
||||
.row.button-row.center.row-pick-class
|
||||
.col-md-12
|
||||
br
|
||||
div.or(data-i18n="courses.or")
|
||||
.row.button-row.center
|
||||
.col-md-12
|
||||
button.btn.btn-success.btn-lg.btn-hoc-student-continue(data-course-id="#{course.id}") Continue by yourself
|
||||
|
||||
mixin teacher-dialog(course)
|
||||
.modal.continue-dialog(id="continueModal#{course.id}")
|
||||
|
|
34
app/templates/editor/thang/export-thang-type-modal.jade
Normal file
34
app/templates/editor/thang/export-thang-type-modal.jade
Normal file
|
@ -0,0 +1,34 @@
|
|||
extends /templates/core/modal-base
|
||||
|
||||
block modal-header-content
|
||||
h4.modal-title Export #{view.thangType.get('name')} SpriteSheet
|
||||
|
||||
block modal-body-content
|
||||
.form-horizontal
|
||||
.form-group
|
||||
label.col-sm-3.control-label Team Color
|
||||
.col-sm-9
|
||||
select#color-config-select.form-control
|
||||
option(value='') None
|
||||
option(value="red") Red
|
||||
option(value="blue") Blue
|
||||
option(value="green") Green
|
||||
|
||||
.form-group
|
||||
label.col-sm-3.control-label Resolution Factor
|
||||
.col-sm-9
|
||||
input#resolution-input.form-control(value=3)
|
||||
.form-group
|
||||
label.col-sm-3.control-label Actions
|
||||
.col-sm-9
|
||||
- var defaultActionNames = _.pluck(view.thangType.getDefaultActions(), 'name')
|
||||
- var actions = view.thangType.getActions()
|
||||
for action in actions
|
||||
.checkbox
|
||||
label
|
||||
input(type="checkbox" name="action" value=action.name checked=_.contains(defaultActionNames, action.name))
|
||||
| #{action.name}
|
||||
|
||||
block modal-footer-content
|
||||
button.btn.btn-default(data-dismiss="modal") Cancel
|
||||
button#save-btn.btn.btn-primary Save
|
|
@ -79,19 +79,25 @@ block outer_content
|
|||
div.tab-pane#editor-thang-colors-tab-view
|
||||
div.tab-pane#editor-thang-main-tab-view.active
|
||||
div#settings-col.well
|
||||
img#portrait.img-thumbnail
|
||||
div.file-controls
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
|
||||
span.glyphicon.glyphicon-upload
|
||||
span.spl Upload Animation
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
|
||||
span.glyphicon.glyphicon-remove
|
||||
span.spl Clear Data
|
||||
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
|
||||
span.glyphicon.glyphicon-gbp
|
||||
span.spl Vector Icon Setup
|
||||
input#real-upload-button(type="file")
|
||||
#thang-type-file-size= fileSizeString
|
||||
.row
|
||||
.col-sm-3
|
||||
img#portrait.img-thumbnail
|
||||
.col-sm-9
|
||||
div.file-controls
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
|
||||
span.glyphicon.glyphicon-upload
|
||||
span.spl Upload Animation
|
||||
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
|
||||
span.glyphicon.glyphicon-remove
|
||||
span.spl Clear Data
|
||||
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
|
||||
span.glyphicon.glyphicon-gbp
|
||||
span.spl Vector Icon Setup
|
||||
button#export-sprite-sheet-btn.btn.btn-sm(disabled=authorized === true ? undefined : "true")
|
||||
span.glyphicon.glyphicon-export
|
||||
span.spl Export SpriteSheet
|
||||
input#real-upload-button(type="file")
|
||||
#thang-type-file-size= fileSizeString
|
||||
div#thang-type-treema
|
||||
.clearfix
|
||||
div#display-col.well
|
||||
|
|
|
@ -2,37 +2,24 @@ extends /templates/base
|
|||
|
||||
block content
|
||||
|
||||
h2(style='color:#CC0000;') Try CodeCombat Courses!
|
||||
p
|
||||
strong What are CodeCombat Courses?
|
||||
h2 Hour of Code(Combat)
|
||||
p
|
||||
a.spr(href='/courses') Courses
|
||||
span organize the same great levels into groups. They make it easier for you to manage a class of students and monitor their progress.
|
||||
strong Hi Teachers!
|
||||
p We're excited to participate in Hour of Code this year!
|
||||
p We've set up an Introduction to Computer Science course, just for you.
|
||||
p
|
||||
strong How to use them:
|
||||
strong How to use CodeCombat with your students:
|
||||
ol
|
||||
li
|
||||
span.spr Navigate to the
|
||||
a.spr(href='/courses') Courses
|
||||
a.spr(href='/courses/teachers?hoc=true') Courses
|
||||
span page
|
||||
li Click the green 'Get FREE course' button
|
||||
li Click the green 'Get FREE course' button under Introduction to Computer Science
|
||||
li Follow the enrollment instructions
|
||||
li Add students via the 'Add Students' tab
|
||||
p
|
||||
strong We Need Your Help!
|
||||
p Courses are still in early development, and we need your feedback to make them great for the classroom.
|
||||
p
|
||||
strong How you can help:
|
||||
ul
|
||||
li
|
||||
spa.spr Spend 5 minutes checking out
|
||||
a.spr(href='/courses') Courses
|
||||
li Enroll in a course and add a student
|
||||
li Monitor a group of students working through the first course
|
||||
p
|
||||
span.spr Send your feedback to
|
||||
span.spr If you have any problems, please email
|
||||
a(href='mailto:team@codecombat.com') team@codecombat.com
|
||||
p Thanks!
|
||||
br
|
||||
|
||||
h2 More Info for Teachers
|
||||
|
|
|
@ -51,7 +51,7 @@ module.exports = class CocoView extends Backbone.View
|
|||
@listenTo(@supermodel, 'failed', @onResourceLoadFailed)
|
||||
@warnConnectionError = _.throttle(@warnConnectionError, 3000)
|
||||
|
||||
super options
|
||||
super arguments...
|
||||
|
||||
destroy: ->
|
||||
@stopListening()
|
||||
|
@ -130,6 +130,7 @@ module.exports = class CocoView extends Backbone.View
|
|||
context.moment = moment
|
||||
context.translate = $.i18n.t
|
||||
context.view = @
|
||||
context._ = _
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
|
|
@ -22,7 +22,7 @@ module.exports = class ModalView extends CocoView
|
|||
@className = @className.replace ' fade', '' if options.instant or @instant
|
||||
@closeButton = options.closeButton if options.closeButton?
|
||||
@modalWidthPercent = options.modalWidthPercent if options.modalWidthPercent
|
||||
super options
|
||||
super arguments...
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super(context)
|
||||
|
|
|
@ -7,6 +7,8 @@ RootView = require 'views/core/RootView'
|
|||
template = require 'templates/courses/courses'
|
||||
utils = require 'core/utils'
|
||||
|
||||
# TODO: Hour of Code (HoC) integration is a mess
|
||||
|
||||
module.exports = class CoursesView extends RootView
|
||||
id: 'courses-view'
|
||||
template: template
|
||||
|
@ -15,12 +17,15 @@ module.exports = class CoursesView extends RootView
|
|||
'click .btn-buy': 'onClickBuy'
|
||||
'click .btn-enroll': 'onClickEnroll'
|
||||
'click .btn-enter': 'onClickEnter'
|
||||
'click .btn-hoc-student-continue': 'onClickHocStudentContinue'
|
||||
'click .btn-student': 'onClickStudent'
|
||||
'click .btn-teacher': 'onClickTeacher'
|
||||
|
||||
constructor: (options) ->
|
||||
super(options)
|
||||
@praise = utils.getCoursePraise()
|
||||
@hocLandingPage = Backbone.history.getFragment()?.indexOf('hoc') >= 0
|
||||
@hocMode = utils.getQueryVariable('hoc', false)
|
||||
@studentMode = Backbone.history.getFragment()?.indexOf('courses/students') >= 0
|
||||
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||
@supermodel.loadCollection(@courses, 'courses')
|
||||
|
@ -38,6 +43,8 @@ module.exports = class CoursesView extends RootView
|
|||
context = super()
|
||||
context.courses = @courses.models ? []
|
||||
context.enrolledCourses = @enrolledCourses ? {}
|
||||
context.hocLandingPage = @hocLandingPage
|
||||
context.hocMode = @hocMode
|
||||
context.instances = @courseInstances.models ? []
|
||||
context.praise = @praise
|
||||
context.state = @state
|
||||
|
@ -95,14 +102,58 @@ module.exports = class CoursesView extends RootView
|
|||
navigationEvent = route: route, viewClass: viewClass, viewArgs: viewArgs
|
||||
Backbone.Mediator.publish 'router:navigate', navigationEvent
|
||||
|
||||
onClickHocStudentContinue: (e) ->
|
||||
$('.continue-dialog').modal('hide')
|
||||
return @openModalView new AuthModal() if me.isAnonymous()
|
||||
courseID = $(e.target).data('course-id')
|
||||
|
||||
@state = 'enrolling'
|
||||
@stateMessage = undefined
|
||||
@render?()
|
||||
|
||||
# TODO: Copied from CourseEnrollView
|
||||
|
||||
data =
|
||||
name: 'Single Player'
|
||||
seats: 9999
|
||||
courseID: courseID
|
||||
jqxhr = $.post('/db/course_instance/-/create', data)
|
||||
jqxhr.done (data, textStatus, jqXHR) =>
|
||||
application.tracker?.trackEvent 'Finished HoC student course creation', {courseID: courseID}
|
||||
# TODO: handle fetch errors
|
||||
me.fetch(cache: false).always =>
|
||||
courseID = courseID
|
||||
route = "/courses/#{courseID}"
|
||||
viewArgs = [{}, courseID]
|
||||
if data?.length > 0
|
||||
courseInstanceID = data[0]._id
|
||||
route += "/#{courseInstanceID}"
|
||||
viewArgs[0].courseInstanceID = courseInstanceID
|
||||
Backbone.Mediator.publish 'router:navigate',
|
||||
route: route
|
||||
viewClass: 'views/courses/CourseDetailsView'
|
||||
viewArgs: viewArgs
|
||||
jqxhr.fail (xhr, textStatus, errorThrown) =>
|
||||
console.error 'Got an error purchasing a course:', textStatus, errorThrown
|
||||
application.tracker?.trackEvent 'Failed HoC student course creation', status: textStatus
|
||||
if xhr.status is 402
|
||||
@state = 'declined'
|
||||
@stateMessage = arguments[2]
|
||||
else
|
||||
@state = 'unknown_error'
|
||||
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
|
||||
@render?()
|
||||
|
||||
onClickStudent: (e) ->
|
||||
route = "/courses/students"
|
||||
route += "?hoc=true" if @hocLandingPage or @hocMode
|
||||
viewClass = require 'views/courses/CoursesView'
|
||||
navigationEvent = route: route, viewClass: viewClass, viewArgs: []
|
||||
Backbone.Mediator.publish 'router:navigate', navigationEvent
|
||||
|
||||
onClickTeacher: (e) ->
|
||||
route = "/courses/teachers"
|
||||
route += "?hoc=true" if @hocLandingPage or @hocMode
|
||||
viewClass = require 'views/courses/CoursesView'
|
||||
navigationEvent = route: route, viewClass: viewClass, viewArgs: []
|
||||
Backbone.Mediator.publish 'router:navigate', navigationEvent
|
||||
|
|
78
app/views/editor/thang/ExportThangTypeModal.coffee
Normal file
78
app/views/editor/thang/ExportThangTypeModal.coffee
Normal file
|
@ -0,0 +1,78 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/editor/thang/export-thang-type-modal'
|
||||
SpriteExporter = require 'lib/sprites/SpriteExporter'
|
||||
|
||||
module.exports = class ExportThangTypeModal extends ModalView
|
||||
id: "export-thang-type-modal"
|
||||
template: template
|
||||
plain: true
|
||||
|
||||
events:
|
||||
'click #save-btn': 'onClickSaveButton'
|
||||
|
||||
initialize: (options, @thangType) ->
|
||||
@builder = null
|
||||
@getFilename = _.once(@getFilename)
|
||||
|
||||
colorMap: {
|
||||
red: { hue: 0, saturation: 0.75, lightness: 0.5 }
|
||||
blue: { hue: 0.66, saturation: 0.75, lightness: 0.5 }
|
||||
green: { hue: 0.33, saturation: 0.75, lightness: 0.5 }
|
||||
}
|
||||
getColorLabel: -> @$('#color-config-select').val()
|
||||
getColorConfig: -> @colorMap[@getColorLabel()]
|
||||
getActionNames: -> _.map @$('input[name="action"]:checked'), (el) -> $(el).val()
|
||||
getResolutionFactor: -> parseInt(@$('#resolution-input').val()) or SPRITE_RESOLUTION_FACTOR
|
||||
getFilename: -> 'spritesheet-'+_.string.slugify(moment().format())+'.png'
|
||||
|
||||
onClickSaveButton: ->
|
||||
options = {
|
||||
resolutionFactor: @getResolutionFactor()
|
||||
actionNames: @getActionNames()
|
||||
colorConfig: @getColorConfig()
|
||||
}
|
||||
console.log 'options?', options
|
||||
@exporter = new SpriteExporter(@thangType, options)
|
||||
@exporter.build()
|
||||
@listenToOnce @exporter, 'build', @onExporterBuild
|
||||
|
||||
onExporterBuild: (e) ->
|
||||
@spriteSheet = e.spriteSheet
|
||||
$('body').empty().append(@spriteSheet._images[0])
|
||||
return
|
||||
src = @spriteSheet._images[0].toDataURL()
|
||||
src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+')
|
||||
body =
|
||||
filename: @getFilename()
|
||||
mimetype: 'image/png'
|
||||
path: "db/thang.type/#{@thangType.get('original')}"
|
||||
b64png: src
|
||||
$.ajax('/file', {type: 'POST', data: body, success: @onSpriteSheetUploaded})
|
||||
|
||||
onSpriteSheetUploaded: =>
|
||||
spriteSheetData = {
|
||||
actionNames: @getActionNames()
|
||||
animations: @spriteSheet._data
|
||||
frames: ([
|
||||
f.rect.x
|
||||
f.rect.y
|
||||
f.rect.width
|
||||
f.rect.height
|
||||
0
|
||||
f.regX
|
||||
f.regY
|
||||
] for f in @spriteSheet._frames)
|
||||
image: "db/thang.type/#{@thangType.get('original')}/"+@getFilename()
|
||||
resolutionFactor: @getResolutionFactor()
|
||||
}
|
||||
if config = @getColorConfig()
|
||||
spriteSheetData.colorConfig = config
|
||||
if label = @getColorLabel()
|
||||
spriteSheetData.colorLabel = label
|
||||
spriteSheets = _.clone(@thangType.get('spriteSheets') or [])
|
||||
spriteSheets.push(spriteSheetData)
|
||||
@thangType.set('spriteSheets', spriteSheets)
|
||||
@thangType.save()
|
||||
@listenToOnce @thangType, 'sync', @hide
|
||||
|
||||
window.SomeModal = module.exports
|
|
@ -20,6 +20,7 @@ VectorIconSetupModal = require 'views/editor/thang/VectorIconSetupModal'
|
|||
SaveVersionModal = require 'views/editor/modal/SaveVersionModal'
|
||||
template = require 'templates/editor/thang/thang-type-edit-view'
|
||||
storage = require 'core/storage'
|
||||
ExportThangTypeModal = require './ExportThangTypeModal'
|
||||
|
||||
CENTER = {x: 200, y: 400}
|
||||
|
||||
|
@ -157,6 +158,7 @@ module.exports = class ThangTypeEditView extends RootView
|
|||
'mousedown #canvas': 'onCanvasMouseDown'
|
||||
'mouseup #canvas': 'onCanvasMouseUp'
|
||||
'mousemove #canvas': 'onCanvasMouseMove'
|
||||
'click #export-sprite-sheet-btn': 'onClickExportSpriteSheetButton'
|
||||
|
||||
onClickSetVectorIcon: ->
|
||||
modal = new VectorIconSetupModal({}, @thangType)
|
||||
|
@ -208,6 +210,7 @@ module.exports = class ThangTypeEditView extends RootView
|
|||
@patchesView = @insertSubView(new PatchesView(@thangType), @$el.find('.patches-view'))
|
||||
@showReadOnly() if me.get('anonymous')
|
||||
@updatePortrait()
|
||||
@onClickExportSpriteSheetButton()
|
||||
|
||||
initComponents: =>
|
||||
options =
|
||||
|
@ -651,6 +654,10 @@ module.exports = class ThangTypeEditView extends RootView
|
|||
@canvasDragOffset = null
|
||||
node.set '/', offset
|
||||
|
||||
onClickExportSpriteSheetButton: ->
|
||||
modal = new ExportThangTypeModal({}, @thangType)
|
||||
@openModalView(modal)
|
||||
|
||||
destroy: ->
|
||||
@camera?.destroy()
|
||||
super()
|
||||
|
|
|
@ -35,6 +35,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
|
|||
'unlockLevelName'
|
||||
'tasks'
|
||||
'terrains'
|
||||
'spriteSheets'
|
||||
]
|
||||
|
||||
hasAccess: (req) ->
|
||||
|
|
Loading…
Reference in a new issue