Merged in campaign editor and campaign view. Destroyed WorldMapView, CampaignOptions, and LevelOptions. Lots of stuff is now stored in the database instead of code. Cleaned up a few unneeded old features. Fixed some problems with checking permissions on first rather than latest versions of documents.

This commit is contained in:
Nick Winter 2014-12-28 13:25:20 -08:00
parent a9c1846e41
commit 744d30737e
44 changed files with 333 additions and 2681 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -86,11 +86,11 @@ module.exports = class CocoRouter extends Backbone.Router
'play-old': go('play/MainPlayView') # This used to be 'play'.
'play': go('play/CampaignView')
'play/:map': go('play/CampaignView')
'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView')
'play/spectate/:levelID': go('play/SpectateView')
'play/:map': go('play/WorldMapView')
'preview': go('HomeView')

View file

@ -140,3 +140,24 @@ if document?
module.exports.replaceText = (elems, text) ->
elem[TEXT] = text for elem in elems
null
# Add a stylesheet rule
# http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript/26230472#26230472
# Don't use wantonly, or we'll have to implement a simple mechanism for clearing out old rules.
if document?
module.exports.injectCSS = ((doc) ->
# wrapper for all injected styles and temp el to create them
wrap = doc.createElement("div")
temp = doc.createElement("div")
# rules like "a {color: red}" etc.
return (cssRules) ->
# append wrapper to the body on the first call
unless wrap.id
wrap.id = "injected-css"
wrap.style.display = "none"
doc.body.appendChild wrap
# <br> for IE: http://goo.gl/vLY4x7
temp.innerHTML = "<br><style>" + cssRules + "</style>"
wrap.appendChild temp.children[1]
return
)(document)

View file

@ -1,27 +0,0 @@
CampaignList = require('views/play/WorldMapView').campaigns
# TODO: Is this file structured correctly?
# Per-campaign options, with default fallback set
options =
'default':
autocompleteFontSizePx: 16
backspaceThrottle: false
lockDefaultCode: false
'dungeon':
autocompleteFontSizePx: 20
backspaceThrottle: true
lockDefaultCode: true
module.exports = CampaignOptions =
getCampaignForSlug: (slug) ->
return unless slug
for campaign in CampaignList
for level in campaign.levels
return campaign.id if level.id is slug
getOption: (levelSlug, option) ->
return unless levelSlug and option
return unless campaign = CampaignOptions.getCampaignForSlug levelSlug
return options[campaign]?[option] if options[campaign]?[option]?
return options.default[option] if options.default[option]?

View file

@ -1,367 +0,0 @@
module.exports = LevelOptions =
'dungeons-of-kithgard':
disableSpaces: true
helpVideos: [
{style: 'original', URL: '//player.vimeo.com/video/114921603'}
{style: 'scripted', URL: '//player.vimeo.com/video/114729726'}
{style: 'eccentric', URL: '//player.vimeo.com/video/114729725'}
{style: 'edited', URL: '//player.vimeo.com/video/114729724'}
]
hidesSubmitUntilRun: true
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots'}
restrictedGear: {feet: 'leather-boots'}
requiredCode: ['moveRight']
'gems-in-the-deep':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114730449'}]
hidesSubmitUntilRun: true
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots'}
restrictedGear: {feet: 'leather-boots'}
'shadow-guard':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734163'}]
hidesSubmitUntilRun: true
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots'}
restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword'}
'kounter-kithwise':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734160'}]
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots'}
restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
'crawlways-of-kithgard':
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734162'}]
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots'}
restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
'forgetful-gemsmith':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734165'}]
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots'}
restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
'true-names':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734166'}]
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'}
restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
requiredCode: ['Brak']
'favorable-odds':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734656'}]
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
'the-raised-sword':
disableSpaces: true
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114734655'}]
hidesPlayButton: true
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
'riddling-kithmaze':
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
requiredCode: ['loop']
'haunted-kithmaze':
helpVideos: [
{style: 'original', URL: '//player.vimeo.com/video/114921605'}
{style: 'scripted', URL: '//player.vimeo.com/video/114730074'}
{style: 'eccentric', URL: '//player.vimeo.com/video/114729727'}
{style: 'edited', URL: '//player.vimeo.com/video/114729723'}
]
hidesRunShortcut: true
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
moveRightLoopSnippet: true
requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
requiredCode: ['loop']
'descending-further':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
'the-second-kithmaze':
helpVideos: [{style: 'original', URL: '//player.vimeo.com/video/114899761'}]
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
moveRightLoopSnippet: true
requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
'dread-door':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
'known-enemy':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {feet: 'leather-boots'}
suspectCode: [{name: 'enemy-in-quotes', pattern: /['"]enemy/m}] # '
'master-of-names':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {feet: 'leather-boots'}
requiredCode: ['findNearestEnemy']
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'lowly-kithmen':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {feet: 'leather-boots'}
requiredCode: ['findNearestEnemy']
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'closing-the-distance':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
restrictedGear: {feet: 'leather-boots'}
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'tactical-strike':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
restrictedGear: {feet: 'leather-boots'}
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'the-final-kithmaze':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'the-gauntlet':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
restrictedGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'}
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'kithgard-gates':
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {'right-hand': 'simple-sword'}
'defense-of-plainswood':
hidesRealTimePlayback: true
hidesCodeToolbar: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'}
restrictedGear: {'right-hand': 'simple-sword'}
'winding-trail':
hidesRealTimePlayback: true
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
'patrol-buster':
hidesRealTimePlayback: true
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'endangered-burl':
hidesRealTimePlayback: true
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'village-guard':
hidesCodeToolbar: true
lockDefaultCode: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'thornbush-farm':
hidesCodeToolbar: true
lockDefaultCode: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
requiredCode: ['topEnemy']
'back-to-back':
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'ogre-encampment':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'woodland-cleaver':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
'shield-rush':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {'left-hand': 'wooden-shield', 'programming-book': 'programmaticon-i'}
# Warrior branch
'peasant-protection':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
'munchkin-swarm':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# Ranger branch
'munchkin-harvest':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {'programming-book': 'programmaticon-i'}
allowedHeroes: ['captain', 'knight', 'samurai']
'swift-dagger':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'}
restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
allowedHeroes: ['ninja', 'trapper', 'forest-archer']
'shrapnel':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'}
restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger', 'programming-book': 'programmaticon-i'}
allowedHeroes: ['ninja', 'trapper', 'forest-archer']
# Wizard branch
'arcane-ally':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
allowedHeroes: ['captain', 'knight', 'samurai']
'touch-of-death':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}
restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
'bonemender':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'}
restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
requiredCode: ['canCast']
allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
'coinucopia':
requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'}
restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
'copper-meadows':
requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'}
restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
'drop-the-flag':
requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
'deadly-pursuit':
requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
'rich-forager':
requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
'multiplayer-treasure-grove':
requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
'siege-of-stonehold':
requiredGear: {}
restrictedGear: {}
# Desert
'the-dunes':
requiredGear: {}
restrictedGear: {}
'the-mighty-sand-yak':
requiredGear: {neck: 'rough-sense-stone'}
restrictedGear: {flag: 'basic-flags'}
'oasis':
requiredGear: {neck: 'rough-sense-stone'}
restrictedGear: {flag: 'basic-flags'}
'sarven-road':
requiredGear: {neck: 'rough-sense-stone'}
restrictedGear: {flag: 'basic-flags'}
'sarven-gaps':
requiredGear: {'right-hand': 'crude-builders-hammer', neck: 'rough-sense-stone'}
restrictedGear: {flag: 'basic-flags'}
'thunderhooves':
requiredGear: {'right-hand': 'crude-builders-hammer', neck: 'rough-sense-stone'}
restrictedGear: {flag: 'basic-flags'}
'medical-attention':
requiredGear: {'right-hand': 'long-sword', neck: 'polished-sense-stone'}
restrictedGear: {'right-hand': 'crude-builders-hammer', flag: 'basic-flags', neck: 'rough-sense-stone'}
'minesweeper':
requiredGear: {neck: 'polished-sense-stone'}
restrictedGear: {flag: 'basic-flags', neck: 'rough-sense-stone'}
'sarven-sentry':
requiredGear: {'right-hand': 'crude-builders-hammer', flag: 'basic-flags', neck: 'polished-sense-stone'}
restrictedGear: {}
'keeping-time':
requiredGear: {wrists: 'simple-wristwatch'}
restrictedGear: {wrists: 'sundial-wristwatch'}
'hoarding-gold':
requiredGear: {neck: 'quartz-sense-stone'}
restrictedGear: {neck: 'polished-sense-stone'}
'decoy-drill':
requiredGear: {'right-hand': 'wooden-builders-hammer', neck: 'quartz-sense-stone'}
restrictedGear: {neck: 'polished-sense-stone'}
'yakstraction':
requiredGear: {'right-hand': 'wooden-builders-hammer', flag: 'basic-flags'}
restrictedGear: {'right-hand': 'crude-builders-hammer'}
'sarven-brawl':
requiredGear: {}
restrictedGear: {}

View file

@ -1,10 +1,10 @@
CocoClass = require 'core/CocoClass'
PlayHeroesModal = require 'views/play/modal/PlayHeroesModal'
InventoryModal = require 'views/play/menu/InventoryModal'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
SuperModel = require 'models/SuperModel'
ThangType = require 'models/ThangType'
LevelOptions = require 'lib/LevelOptions'
lastHeroesEarned = me.get('earned')?.heroes ? []
lastHeroesPurchased = me.get('purchased')?.heroes ? []
@ -22,27 +22,39 @@ module.exports = class LevelSetupManager extends CocoClass
@loadSession()
loadSession: ->
url = "/db/level/#{@options.levelID}/session"
#url += "?team=#{@team}" if @options.team # TODO: figure out how to get the teams for multiplayer PVP hero style
@session = new LevelSession().setURL url
levelURL = "/db/level/#{@options.levelID}"
@level = new Level().setURL levelURL
@level = @supermodel.loadModel(@level, 'level').model
onLevelSync = ->
return if @destroyed
if @waitingToLoadModals
@waitingToLoadModals = false
@loadModals()
onLevelSync.call @ if @level.loaded
sessionURL = "#{levelURL}/session"
#sessionURL += "?team=#{@team}" if @options.team # TODO: figure out how to get the teams for multiplayer PVP hero style
@session = new LevelSession().setURL sessionURL
onSessionSync = ->
return if @destroyed
@session.url = -> '/db/level.session/' + @id
@fillSessionWithDefaults()
@listenToOnce @session, 'sync', onSessionSync
@session = @supermodel.loadModel(@session, 'level_session').model
if @session.loaded
onSessionSync.call @
onSessionSync.call @ if @session.loaded
fillSessionWithDefaults: ->
heroConfig = _.merge {}, me.get('heroConfig'), @session.get('heroConfig')
@session.set('heroConfig', heroConfig)
@loadModals()
if @level.loaded
@loadModals()
else
@waitingToLoadModals = true
loadModals: ->
# build modals and prevent them from disappearing.
@heroesModal = new PlayHeroesModal({supermodel: @supermodel, session: @session, confirmButtonI18N: 'play.next', levelID: @options.levelID, hadEverChosenHero: @options.hadEverChosenHero})
@inventoryModal = new InventoryModal({supermodel: @supermodel, session: @session, levelID: @options.levelID})
@heroesModal = new PlayHeroesModal({supermodel: @supermodel, session: @session, confirmButtonI18N: 'play.next', level: @level, hadEverChosenHero: @options.hadEverChosenHero})
@inventoryModal = new InventoryModal({supermodel: @supermodel, session: @session, level: @level})
@heroesModalDestroy = @heroesModal.destroy
@inventoryModalDestroy = @inventoryModal.destroy
@heroesModal.destroy = @inventoryModal.destroy = _.noop
@ -62,7 +74,7 @@ module.exports = class LevelSetupManager extends CocoClass
not _.isEqual(lastHeroesPurchased, me.get('purchased')?.heroes ? []))
console.log 'Showing hero picker because heroes earned/purchased has changed.'
firstModal = @heroesModal
else if allowedHeroSlugs = LevelOptions[@options.levelID]?.allowedHeroes
else if allowedHeroSlugs = @level.get 'allowedHeroes'
unless _.find(allowedHeroSlugs, (slug) -> ThangType.heroes[slug] is me.get('heroConfig')?.thangType)
firstModal = @heroesModal
lastHeroesEarned = me.get('earned')?.heroes ? []

View file

@ -5,11 +5,11 @@ c.extendNamedProperties CampaignSchema # name first
_.extend CampaignSchema.properties, {
i18n: {type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body']}
ambientSound: c.object {},
mp3: { type: 'string', format: 'sound-file' }
ogg: { type: 'string', format: 'sound-file' }
backgroundImage: c.array {}, {
type: 'object'
additionalProperties: false
@ -20,7 +20,7 @@ _.extend CampaignSchema.properties, {
}
backgroundColor: { type: 'string' }
backgroundColorTransparent: { type: 'string' }
adjacentCampaigns: { type: 'object', format: 'campaigns', additionalProperties: {
title: 'Campaign'
type: 'object'
@ -32,7 +32,7 @@ _.extend CampaignSchema.properties, {
description: { type: 'string', format: 'hidden' }
i18n: { type: 'object', format: 'hidden' }
slug: { type: 'string', format: 'hidden' }
#- normal properties
position: c.point2d()
rotation: { type: 'number', format: 'degrees' }
@ -40,13 +40,13 @@ _.extend CampaignSchema.properties, {
showIfUnlocked: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' }
}
}}
levels: { type: 'object', format: 'levels', additionalProperties: {
title: 'Level'
type: 'object'
format: 'level'
additionalProperties: false
# key is the original property
properties: {
#- denormalized from Level
@ -58,6 +58,7 @@ _.extend CampaignSchema.properties, {
original: { type: 'string', format: 'hidden' }
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
adminOnly: { type: 'boolean' }
disableSpaces: { type: 'boolean' }
hidesSubmitUntilRun: { type: 'boolean' }
hidesPlayButton: { type: 'boolean' }
@ -72,8 +73,8 @@ _.extend CampaignSchema.properties, {
realTimeSpeedFactor: { type: 'number' }
autocompleteFontSizePx: { type: 'number' }
requiredCode: c.array {}, {
type: 'string'
requiredCode: c.array {}, {
type: 'string'
}
suspectCode: c.array {}, {
type: 'object'
@ -82,17 +83,17 @@ _.extend CampaignSchema.properties, {
pattern: { type: 'string' }
}
}
requiredGear: { type: 'object', additionalProperties: {
type: 'array'
items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
}}
restrictedGear: { type: 'object', additionalProperties: {
type: 'array'
type: 'array'
items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
}}
allowedHeroes: { type: 'array', items: {
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
allowedHeroes: { type: 'array', items: {
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
}}
#- denormalized from Achievements
@ -106,6 +107,7 @@ _.extend CampaignSchema.properties, {
level: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' }
type: { enum: ['heroes', 'items', 'levels'] }
}}
campaign: c.shortString title: 'Campaign', description: 'Which campaign this level is part of (like "desert").', format: 'hidden' # Automatically set by campaign editor.
#- normal properties
position: c.point2d()

View file

@ -295,10 +295,15 @@ _.extend LevelSchema.properties,
showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always'])
requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'}
tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this level.', default: (name: t for t in defaultTasks)}, c.task
helpVideos: c.array {title: 'Help Videos'}, c.object {default: {style: 'eccentric', url: '', free: false}},
style: c.shortString title: 'Style', description: 'Like: original, eccentric, scripted, edited, etc.'
free: {type: 'boolean', title: 'Free', description: 'Whether this video is freely available to all players without a subscription.'}
url: c.url {title: 'URL', description: 'Link to the video on Vimeo.'}
# Admin flags
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
adminOnly: { type: 'boolean' }
disableSpaces: { type: 'boolean' }
hidesSubmitUntilRun: { type: 'boolean' }
hidesPlayButton: { type: 'boolean' }
@ -323,15 +328,18 @@ _.extend LevelSchema.properties,
}
}
requiredGear: { type: 'object', additionalProperties: {
type: 'string'
type: 'array'
items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
}}
restrictedGear: { type: 'object', additionalProperties: {
type: 'string'
type: 'array'
items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
}}
allowedHeroes: { type: 'array', items: {
type: 'string'
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
}}
campaign: c.shortString title: 'Campaign', description: 'Which campaign this level is part of (like "desert").', format: 'hidden' # Automatically set by campaign editor.
c.extendBasicProperties LevelSchema, 'level'
c.extendSearchableProperties LevelSchema
c.extendVersionedProperties LevelSchema, 'level'

View file

@ -105,8 +105,6 @@ module.exports =
'playback:ended-changed': c.object {required: ['ended']},
ended: {type: 'boolean'}
'level:play-next-level': c.object {}
'level:toggle-playing': c.object {}
'level:toggle-grid': c.object {}

View file

@ -2,17 +2,9 @@
@import "app/styles/bootstrap/variables"
$mapHeight: 1536
$forestMapWidth: 2500
$dungeonMapWidth: 2350
$desertMapWidth: 2350
$desertMapSeaBackground: rgba(113, 186, 208, 1)
$desertMapSeaBackgroundTransparent: rgba(113, 186, 208, 0)
$forestMapSeaBackground: rgba(113, 186, 208, 1)
$forestMapSeaBackgroundTransparent: rgba(113, 186, 208, 0)
$dungeonMapCaveBackground: rgba(68, 54, 45, 1)
$dungeonMapCaveBackgroundTransparent: rgba(68, 54, 45, 0)
$mapWidth: 2350
$levelDotWidth: 2%
$levelDotHeight: $levelDotWidth * $forestMapWidth / $mapHeight
$levelDotHeight: $levelDotWidth * $mapWidth / $mapHeight
$levelDotZ: $levelDotHeight * 0.25
$levelDotHoverZ: $levelDotZ * 2
$levelDotShadowWidth: 0.8 * $levelDotWidth
@ -63,51 +55,6 @@ $gameControlMargin: 30px
&.left-gradient
left: 0
&.desert
background-color: $desertMapSeaBackground
.top-gradient
background: linear-gradient(to bottom, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
.right-gradient
background: linear-gradient(to left, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
.bottom-gradient
background: linear-gradient(to top, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
.left-gradient
background: linear-gradient(to right, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
&.forest
background-color: $forestMapSeaBackground
.top-gradient
background: linear-gradient(to bottom, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
.right-gradient
background: linear-gradient(to left, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
.bottom-gradient
background: linear-gradient(to top, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
.left-gradient
background: linear-gradient(to right, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
&.dungeon
background-color: $dungeonMapCaveBackground
.top-gradient
background: linear-gradient(to bottom, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.right-gradient
background: linear-gradient(to left, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.bottom-gradient
background: linear-gradient(to top, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.left-gradient
background: linear-gradient(to right, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.map
position: relative
@ -117,21 +64,6 @@ $gameControlMargin: 30px
background-size: 100%
@include user-select(none)
&.map-dungeon
background-image: url('/images/pages/play/map_dungeon_1920.jpg')
@media screen and ( max-width: 1366px )
background-image: url('/images/pages/play/map_dungeon_1366.jpg')
&.map-forest
background-image: url('/images/pages/play/map_forest_1920.jpg')
@media screen and ( max-width: 1366px )
background-image: url('/images/pages/play/map_forest_1366.jpg')
&.map-desert
background-image: url('/images/pages/play/map_desert_1920.jpg')
@media screen and ( max-width: 1366px )
background-image: url('/images/pages/play/map_desert_1366.jpg')
.level, .level-shadow
position: absolute
border-radius: 100%
@ -301,27 +233,6 @@ $gameControlMargin: 30px
&:hover
text-decoration: none
&#desert-link
left: 90%
top: 18.5%
transform: scaleY(-1.5) scaleX(1.5)
&#forest-back-link
left: 2%
top: 70.5%
transform: rotate(216deg)
&#forest-link
left: 94.5%
top: 7%
transform: rotate(-35deg)
&#dungeon-link
left: 9%
top: 54.5%
transform: rotate(180deg)
color: fuchsia
.game-controls
position: absolute

View file

@ -19,10 +19,6 @@
float: right
margin-left: 10px
.next-level-button, .world-map-button
float: right
margin-left: 10px
.rating
float: left
position: relative

View file

@ -3,24 +3,23 @@
.gradient.vertical-gradient.right-gradient
.gradient.horizontal-gradient.bottom-gradient
.gradient.vertical-gradient.left-gradient
.map-background(class="map-"+mapType alt="", draggable="false")
.map-background(alt="", draggable="false")
each level in levels
if !level.hidden
- var next = nextLevel && level.slug === nextLevel;
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.original] || "", data-level-id=level.original, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.slug] || "", data-level-slug=level.slug, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
if level.unlocksHero && !level.unlockedHero
img.hero-portrait(src=level.unlocksHero.img)
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.original}", disabled=level.disabled, data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name)
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
if level.requiresSubscription
img.star(src="/images/pages/play/star.png")
if levelStatusMap[level.original] === 'complete'
if levelStatusMap[level.slug] === 'complete'
img.banner(src="/images/pages/play/level-banner-complete.png")
if levelStatusMap[level.original] === 'started'
if levelStatusMap[level.slug] === 'started'
img.banner(src="/images/pages/play/level-banner-started.png")
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.original] || "")
.level-info-container(data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name)
div(class="level-info " + (levelStatusMap[level.original] || ""))
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + levelStatusMap[level.slug] || "")
.level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
div(class="level-info " + (levelStatusMap[level.slug] || ""))
h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
.level-description= level.description
if level.disabled
@ -30,7 +29,7 @@
strong(data-i18n="play.awaiting_levels_adventurer") Sign up as an Adventurer
span.spl(data-i18n="play.awaiting_levels_adventurer_suffix") to be the first to play new levels.
- var playCount = levelPlayCountMap[level.original]
- var playCount = levelPlayCountMap[level.slug]
if playCount && playCount.sessions > 20
div
span.spr #{playCount.sessions}
@ -42,8 +41,8 @@
button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play
for adjacentCampaign in adjacentCampaigns
a
span.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/"+adjacentCampaign.slug, style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug)
span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
.game-controls.header-font
button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")

View file

@ -15,10 +15,6 @@ block modal-footer-content
.ladder-submission-view
else if level.get('type') === 'ladder'
a.btn.btn-primary(href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder
else if level.get('type', true) === 'hero'
a.btn.btn-success.world-map-button(href="/play-hero", data-dismiss="modal", data-i18n="play_level.victory_play_continue") Continue
else if hasNextLevel
button.btn.btn-success.next-level-button(data-dismiss="modal", data-i18n="play_level.victory_play_next_level") Play Next Level
else
a.btn.btn-primary(href="/", data-dismiss="modal", data-i18n="play_level.victory_go_home") Go Home
if me.get('anonymous')
@ -44,9 +40,3 @@ block modal-footer-content
.fb-like(data-href="https://www.facebook.com/codecombat", data-send="false", data-layout="button_count", data-width="350", data-show-faces="true", data-ref="coco_victory_#{fbRef}")
a.twitter-follow-button(href="https://twitter.com/CodeCombat", data-show-count="true", data-show-screen-name="false", data-dnt="true", data-align="right", data-i18n="nav.twitter_follow") Follow
iframe.github-star-button(src="http://ghbtns.com/github-btn.html?user=codecombat&repo=codecombat&type=watch&count=true", allowtransparency="true", frameborder="0", scrolling="0", width="110", height="20")
if showHourOfCodeDoneButton
h3.pull-left(data-i18n="play_level.victory_hour_of_code_done") Are You Done?
a(href="http://code.org/api/hour/finish")
strong(data-i18n="play_level.victory_hour_of_code_done_yes") Yes, I'm finished with my Hour of Code!
img(src="/images/level/csedweek-logo-final-small.jpg", alt="CS Ed Week Hour of Code", title="I'm finished with my Hour of Code", width=80)

View file

@ -53,6 +53,6 @@ module.exports = class HomeView extends RootView
if elapsed < 5 * 60 * 1000
me.set 'hourOfCode', true
me.patch()
# We may also insert the tracking pixel for everyone on the WorldMapView so as to count directly-linked visitors.
# We may also insert the tracking pixel for everyone on the CampaignView so as to count directly-linked visitors.
$('body').append($('<img src="http://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
application.tracker?.trackEvent 'Hour of Code Begin', {}

View file

@ -92,7 +92,7 @@ module.exports = class AuthModal extends ModalView
Backbone.Mediator.publish "auth:signed-up", {}
window.tracker?.trackEvent 'Finished Signup', label: 'CodeCombat'
@enableModalInProgress(@$el)
createUser userObject, null, window.nextLevelURL
createUser userObject, null, window.nextURL
onLoggingInWithFacebook: (e) ->
modal = $('.modal:visible', @$el)

View file

@ -12,61 +12,19 @@ RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollecti
CampaignLevelView = require './CampaignLevelView'
achievementProject = ['related', 'rewards', 'name', 'slug']
thangTypeProject = ['name', 'original', 'slug']
thangTypeProject = ['name', 'original']
module.exports = class CampaignEditorView extends RootView
id: "campaign-editor-view"
template: require 'templates/editor/campaign/campaign-editor-view'
className: 'editor'
events:
'click #save-button': 'onClickSaveButton'
constructor: (options, @campaignHandle) ->
super(options)
# MIGRATION CODE
# for level in levels
# _.extend level, options[level.id]
# level.slug = level.id
# delete level.id
# delete level.nextLevels
# level.position = { x: level.x, y: level.y }
# delete level.x
# delete level.y
# if level.unlocksHero
# level.unlocks = [{
# original: level.unlocksHero.originalID
# type: 'hero'
# }]
# delete level.unlocksHero
# campaign.levels[level.original] = level
# @campaign = new Campaign(campaign)
#------------------------------------------------
@campaign = new Campaign({_id:@campaignHandle})
#--------------- temporary migration to change thang type slugs to originals
#- should keep around though for loading the names of items and heroes that are referenced
#- just load names instead of slugs, though
@sluggyThangs = new Backbone.Collection()
@listenToOnce @campaign, 'sync', ->
slugs = []
for level in _.values(@campaign.get('levels'))
slugs = slugs.concat(_.values(level.requiredGear)) if level.requiredGear
slugs = slugs.concat(_.values(level.restrictedGear)) if level.restrictedGear
slugs = slugs.concat(level.allowedHeroes) if level.allowedHeroes
slugs = _.uniq _.flatten slugs
for slug in slugs
thangType = new ThangType()
thangType.setProjection(thangTypeProject)
if utils.isID slug
thangType.setURL("/db/thang.type/#{slug}/version")
else
thangType.setURL("/db/thang.type/#{slug}")
@supermodel.loadModel(thangType, 'thang')
@sluggyThangs.add(thangType)
#---------------
@supermodel.loadModel(@campaign, 'campaign')
@levels = new CocoCollection([], {
@ -82,28 +40,41 @@ module.exports = class CampaignEditorView extends RootView
project: achievementProject
})
@supermodel.loadCollection(@achievements, 'achievements')
@toSave = new Backbone.Collection()
@listenToOnce @campaign ,'sync', @loadThangTypeNames
@listenToOnce @campaign, 'sync', @onFundamentalLoaded
@listenToOnce @levels, 'sync', @onFundamentalLoaded
@listenToOnce @achievements, 'sync', @onFundamentalLoaded
loadThangTypeNames: ->
# Load the names of the ThangTypes that this level's Treema nodes might want to display.
originals = []
for level in _.values(@campaign.get('levels'))
originals = originals.concat(_.values(level.requiredGear)) if level.requiredGear
originals = originals.concat(_.values(level.restrictedGear)) if level.restrictedGear
originals = originals.concat(level.allowedHeroes) if level.allowedHeroes
originals = _.uniq _.flatten originals
for original in originals
thangType = new ThangType()
thangType.setProjection(thangTypeProject)
thangType.setURL("/db/thang.type/#{original}/version")
@supermodel.loadModel(thangType, 'thang')
onFundamentalLoaded: ->
# load any levels which
# Load any levels which haven't been denormalized into our campaign.
return unless @campaign.loaded and @levels.loaded and @achievements.loaded
for level in _.values(@campaign.get('levels'))
model = @levels.findWhere(original: level.original)
if not model
model = new Level({})
model.setProjection Campaign.denormalizedLevelProperties
model.setURL("/db/level/#{level.original}/version")
@levels.add @supermodel.loadModel(model, 'level').model
achievements = new RelatedAchievementsCollection level.original
achievements.setProjection achievementProject
@supermodel.loadCollection achievements, 'achievements'
@listenToOnce achievements, 'sync', ->
@achievements.add(achievements.models)
continue if model = @levels.findWhere(original: level.original)
model = new Level({})
model.setProjection Campaign.denormalizedLevelProperties
model.setURL("/db/level/#{level.original}/version")
@levels.add @supermodel.loadModel(model, 'level').model
achievements = new RelatedAchievementsCollection level.original
achievements.setProjection achievementProject
@supermodel.loadCollection achievements, 'achievements'
@listenToOnce achievements, 'sync', ->
@achievements.add(achievements.models)
onLoaded: ->
@toSave.add @campaign if @campaign.hasLocalChanges()
@ -113,38 +84,6 @@ module.exports = class CampaignEditorView extends RootView
campaignLevel = campaignLevels[levelOriginal]
continue if not campaignLevel
#--------------- temporary migrations
if campaignLevel.restrictedGear
for slot, value of campaignLevel.restrictedGear
if _.isString(value)
campaignLevel.restrictedGear[slot] = [value]
#
if campaignLevel.requiredGear
for slot, value of campaignLevel.requiredGear
if _.isString(value)
campaignLevel.requiredGear[slot] = [value]
#
if campaignLevel.requiredGear
for gear in _.values(campaignLevel.requiredGear)
for slug, index in gear
thang = @sluggyThangs.findWhere({slug: slug})
continue unless thang
gear[index] = thang.get('original')
#
if campaignLevel.restrictedGear
for gear in _.values(campaignLevel.restrictedGear)
for slug, index in gear
thang = @sluggyThangs.findWhere({slug: slug})
continue unless thang
gear[index] = thang.get('original')
#
if campaignLevel.allowedHeroes
for slug, index in campaignLevel.allowedHeroes
thang = @sluggyThangs.findWhere({slug: slug})
continue unless thang
level.allowedHeroes[index] = thang.get('original')
#---------------
$.extend campaignLevel, _.omit(level.attributes, '_id')
achievements = @achievements.where {'related': levelOriginal}
rewards = []
@ -152,38 +91,39 @@ module.exports = class CampaignEditorView extends RootView
for rewardType, rewardArray of achievement.get('rewards')
for reward in rewardArray
rewardObject = { achievement: achievement.id }
if rewardType is 'heroes'
rewardObject.hero = reward
thangType = new ThangType({}, {project: thangTypeProject})
thangType.setURL("/db/thang.type/#{reward}/version")
@supermodel.loadModel(thangType, 'thang')
if rewardType is 'levels'
rewardObject.level = reward
if not @levels.findWhere({original: reward})
level = new Level({}, {project: Campaign.denormalizedLevelProperties})
level.setURL("/db/level/#{reward}/version")
@supermodel.loadModel(level, 'level')
if rewardType is 'items'
rewardObject.item = reward
thangType = new ThangType({}, {project: thangTypeProject})
thangType.setURL("/db/thang.type/#{reward}/version")
@supermodel.loadModel(thangType, 'thang')
rewards.push rewardObject
campaignLevel.rewards = rewards
delete campaignLevel.unlocks
campaignLevel.campaign = @campaign.get 'slug'
campaignLevels[levelOriginal] = campaignLevel
@campaign.set('levels', campaignLevels)
for level in _.values campaignLevels
model = @levels.findWhere {original: level.original}
model.set key, level[key] for key in Campaign.denormalizedLevelProperties
# @toSave.add model if model.hasLocalChanges()
# @updateRewardsForLevel model, level.rewards
@toSave.add model if model.hasLocalChanges()
@updateRewardsForLevel model, level.rewards
super()
@ -195,12 +135,13 @@ module.exports = class CampaignEditorView extends RootView
onClickSaveButton: ->
@toSave.set @toSave.filter (m) -> m.hasLocalChanges()
@openModalView new SaveCampaignModal({}, @toSave)
afterRender: ->
super()
treemaOptions =
schema: Campaign.schema
data: $.extend({}, @campaign.attributes)
filePath: "db/campaign/#{@campaign.get('_id')}"
callbacks:
change: @onTreemaChanged
select: @onTreemaSelectionChanged
@ -224,7 +165,7 @@ module.exports = class CampaignEditorView extends RootView
@listenTo @campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved
@listenTo @campaignView, 'level-clicked', @onCampaignLevelClicked
@insertSubView @campaignView
onTreemaChanged: (e, nodes) =>
for node in nodes
path = node.getPath()
@ -233,12 +174,12 @@ module.exports = class CampaignEditorView extends RootView
original = parts[2]
level = @supermodel.getModelByOriginal Level, original
campaignLevel = @treema.get "/levels/#{original}"
@updateRewardsForLevel level, campaignLevel.rewards
level.set key, campaignLevel[key] for key in Campaign.denormalizedLevelProperties
@toSave.add level if level.hasLocalChanges()
@toSave.add @campaign
@campaign.set key, value for key, value of @treema.data
@campaignView.setCampaign(@campaign)
@ -270,16 +211,16 @@ module.exports = class CampaignEditorView extends RootView
rewardSubset = (r for r in rewards when r.achievement is achievement.id)
oldRewards = achievement.get 'rewards'
newRewards = {}
heroes = _.compact((r.hero for r in rewardSubset))
newRewards.heroes = heroes if heroes.length
items = _.compact((r.item for r in rewardSubset))
newRewards.items = items if items.length
levels = _.compact((r.level for r in rewardSubset))
newRewards.levels = levels if levels.length
newRewards.gems = oldRewards.gems if oldRewards.gems
achievement.set 'rewards', newRewards
if achievement.hasLocalChanges()
@ -288,7 +229,7 @@ module.exports = class CampaignEditorView extends RootView
class LevelsNode extends TreemaObjectNode
valueClass: 'treema-levels'
@levels: {}
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, ''+_.size(data)
@ -310,7 +251,7 @@ class LevelNode extends TreemaObjectNode
valueClass: 'treema-level'
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, data.name
populateData: ->
return if @data.name?
data = _.pick LevelsNode.levels[@keyForParent].attributes, Campaign.denormalizedLevelProperties
@ -328,7 +269,7 @@ class CampaignsNode extends TreemaObjectNode
childSource: (req, res) =>
s = new Backbone.Collection([], {model:Campaign})
s.url = '/db/campaign'
s.fetch({data: {term:req.term, project: campaign.denormalizedCampaignProperties}})
s.fetch({data: {term:req.term, project: Campaign.denormalizedCampaignProperties}})
s.once 'sync', (collection) ->
CampaignsNode.campaigns[campaign.id] = campaign for campaign in collection.models
mapped = ({label: r.get('name'), value: r.id} for r in collection.models)
@ -344,601 +285,6 @@ class CampaignNode extends TreemaObjectNode
return if @data.name?
data = _.pick CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties
_.extend @data, data
class AchievementNode extends treemaExt.IDReferenceNode
buildSearchURL: (term) -> "#{@url}?term=#{term}&project=#{achievementProject.join(',')}"
#campaign = {
# name: 'Dungeon'
# levels: {}
#}
#
#
#levels = [
# {
# name: 'Dungeons of Kithgard'
# type: 'hero'
# id: 'dungeons-of-kithgard'
# original: '5411cb3769152f1707be029c'
# description: 'Grab the gem, but touch nothing else. Start here.'
# x: 14
# y: 15.5
# nextLevels:
# continue: 'gems-in-the-deep'
# }
# {
# name: 'Gems in the Deep'
# type: 'hero'
# id: 'gems-in-the-deep'
# original: '54173c90844506ae0195a0b4'
# description: 'Quickly collect the gems; you will need them.'
# x: 29
# y: 12
# nextLevels:
# continue: 'shadow-guard'
# }
# {
# name: 'Shadow Guard'
# type: 'hero'
# id: 'shadow-guard'
# original: '54174347844506ae0195a0b8'
# description: 'Evade the Kithgard minion.'
# x: 40.54
# y: 11.03
# nextLevels:
# continue: 'forgetful-gemsmith'
# }
# {
# name: 'Kounter Kithwise'
# type: 'hero'
# id: 'kounter-kithwise'
# original: '54527a6257e83800009730c7'
# description: 'Practice your evasion skills with more guards.'
# x: 35.37
# y: 20.61
# nextLevels:
# continue: 'crawlways-of-kithgard'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Crawlways of Kithgard'
# type: 'hero'
# id: 'crawlways-of-kithgard'
# original: '545287ef57e83800009730d5'
# description: 'Dart in and grab the gemat the right moment.'
# x: 36.48
# y: 29.03
# nextLevels:
# continue: 'forgetful-gemsmith'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Forgetful Gemsmith'
# type: 'hero'
# id: 'forgetful-gemsmith'
# original: '544a98f62d002f0000fe331a'
# description: 'Grab even more gems as you practice moving.'
# x: 54.98
# y: 10.53
# nextLevels:
# continue: 'true-names'
# }
# {
# name: 'True Names'
# type: 'hero'
# id: 'true-names'
# original: '541875da4c16460000ab990f'
# description: 'Learn an enemy\'s true name to defeat it.'
# x: 68.44
# y: 10.70
# nextLevels:
# continue: 'the-raised-sword'
# unlocksHero: {
# img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png'
# originalID: '53e12be0d042f23505c3023b'
# }
# }
# {
# name: 'Favorable Odds'
# type: 'hero'
# id: 'favorable-odds'
# original: '5452972f57e83800009730de'
# description: 'Test out your battle skills by defeating more munchkins.'
# x: 88.25
# y: 14.92
# nextLevels:
# continue: 'the-raised-sword'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'The Raised Sword'
# type: 'hero'
# id: 'the-raised-sword'
# original: '5418aec24c16460000ab9aa6'
# description: 'Learn to equip yourself for combat.'
# x: 81.51
# y: 17.92
# nextLevels:
# continue: 'haunted-kithmaze'
# }
# {
# name: 'Haunted Kithmaze'
# type: 'hero'
# id: 'haunted-kithmaze'
# original: '545a5914d820eb0000f6dc0a'
# description: 'The builders of Kithgard constructed many mazes to confuse travelers.'
# x: 78
# y: 29
# nextLevels:
# continue: 'the-second-kithmaze'
# }
# {
# name: 'Riddling Kithmaze'
# type: 'hero'
# id: 'riddling-kithmaze'
# original: '5418b9d64c16460000ab9ab4'
# description: 'If at first you go astray, change your loop to find the way.'
# x: 69.97
# y: 28.03
# nextLevels:
# continue: 'descending-further'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Descending Further'
# type: 'hero'
# id: 'descending-further'
# original: '5452a84d57e83800009730e4'
# description: 'Another day, another maze.'
# x: 61.68
# y: 22.80
# nextLevels:
# continue: 'the-second-kithmaze'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'The Second Kithmaze'
# type: 'hero'
# id: 'the-second-kithmaze'
# original: '5418cf256bae62f707c7e1c3'
# description: 'Many have tried, few have found their way through this maze.'
# x: 54.49
# y: 26.49
# nextLevels:
# continue: 'dread-door'
# }
# {
# name: 'Dread Door'
# type: 'hero'
# id: 'dread-door'
# original: '5418d40f4c16460000ab9ac2'
# description: 'Behind a dread door lies a chest full of riches.'
# x: 60.52
# y: 33.70
# nextLevels:
# continue: 'known-enemy'
# }
# {
# name: 'Known Enemy'
# type: 'hero'
# id: 'known-enemy'
# original: '5452adea57e83800009730ee'
# description: 'Begin to use variables in your battles.'
# x: 67
# y: 39
# nextLevels:
# continue: 'master-of-names'
# }
# {
# name: 'Master of Names'
# type: 'hero'
# id: 'master-of-names'
# original: '5452c3ce57e83800009730f7'
# description: 'Use your glasses to defend yourself from the Kithmen.'
# x: 75
# y: 46
# nextLevels:
# continue: 'lowly-kithmen'
# }
# {
# name: 'Lowly Kithmen'
# type: 'hero'
# id: 'lowly-kithmen'
# original: '541b24511ccc8eaae19f3c1f'
# description: 'Now that you can see them, they\'re everywhere!'
# x: 85
# y: 40
# nextLevels:
# continue: 'closing-the-distance'
# }
# {
# name: 'Closing the Distance'
# type: 'hero'
# id: 'closing-the-distance'
# original: '541b288e1ccc8eaae19f3c25'
# description: 'Kithmen are not the only ones to stand in your way.'
# x: 93
# y: 47
# nextLevels:
# continue: 'the-final-kithmaze'
# }
# {
# name: 'Tactical Strike'
# type: 'hero'
# id: 'tactical-strike'
# original: '5452cfa706a59e000067e4f5'
# description: 'They\'re, uh, coming right for us! Sneak up behind them.'
# x: 83.23
# y: 52.73
# nextLevels:
# continue: 'the-final-kithmaze'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'The Final Kithmaze'
# type: 'hero'
# id: 'the-final-kithmaze'
# original: '541b434e1ccc8eaae19f3c33'
# description: 'To escape you must find your way through an Elder Kithman\'s maze.'
# x: 86.95
# y: 64.70
# nextLevels:
# continue: 'kithgard-gates'
# }
# {
# name: 'The Gauntlet'
# type: 'hero'
# id: 'the-gauntlet'
# original: '5452d8b906a59e000067e4fa'
# description: 'Rush for the stairs, battling foes at every turn.'
# x: 76.50
# y: 72.69
# nextLevels:
# continue: 'kithgard-gates'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Kithgard Gates'
# type: 'hero'
# id: 'kithgard-gates'
# original: '541c9a30c6362edfb0f34479'
# description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.'
# x: 89
# y: 82
# nextLevels:
# continue: 'defense-of-plainswood'
# }
# {
# name: 'Cavern Survival'
# type: 'hero-ladder'
# id: 'cavern-survival'
# original: '544437e0645c0c0000c3291d'
# description: 'Stay alive longer than your opponent amidst hordes of ogres!'
# x: 17.54
# y: 78.39
# }
#]
#
#options =
# 'dungeons-of-kithgard':
# disableSpaces: true
# hidesSubmitUntilRun: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['moveRight']
# 'gems-in-the-deep':
# disableSpaces: true
# hidesSubmitUntilRun: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots'}
# 'shadow-guard':
# disableSpaces: true
# hidesSubmitUntilRun: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword'}
# 'kounter-kithwise':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# 'crawlways-of-kithgard':
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# 'forgetful-gemsmith':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# 'true-names':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# requiredCode: ['Brak']
# 'favorable-odds':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# 'the-raised-sword':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# 'the-first-kithmaze':
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['loop']
# 'haunted-kithmaze':
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# moveRightLoopSnippet: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['loop']
# 'descending-further':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# 'the-second-kithmaze':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# moveRightLoopSnippet: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# 'dread-door':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# 'known-enemy':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots'}
# suspectCode: [{name: 'enemy-in-quotes', pattern: '[\'"]enemy'}] # '
# 'master-of-names':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['findNearestEnemy']
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'lowly-kithmen':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['findNearestEnemy']
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'closing-the-distance':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'leather-boots'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'tactical-strike':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'leather-boots'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'the-final-kithmaze':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'the-gauntlet':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'kithgard-gates':
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {'right-hand': 'simple-sword'}
# 'defense-of-plainswood':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {'right-hand': 'simple-sword'}
# 'winding-trail':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
# 'patrol-buster':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'endangered-burl':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'village-guard':
# hidesCodeToolbar: true
# lockDefaultCode: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'thornbush-farm':
# hidesCodeToolbar: true
# lockDefaultCode: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# requiredCode: ['topEnemy']
# 'back-to-back':
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'ogre-encampment':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'woodland-cleaver':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# 'shield-rush':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {'left-hand': 'wooden-shield', 'programming-book': 'programmaticon-i'}
#
## Warrior branch
# 'peasant-protection':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
# 'munchkin-swarm':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
#
## Ranger branch
# 'munchkin-harvest':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {'programming-book': 'programmaticon-i'}
# allowedHeroes: ['captain', 'knight', 'samurai']
# 'swift-dagger':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
# allowedHeroes: ['ninja', 'trapper', 'forest-archer']
# 'shrapnel':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger', 'programming-book': 'programmaticon-i'}
# allowedHeroes: ['ninja', 'trapper', 'forest-archer']
#
## Wizard branch
# 'arcane-ally':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
# allowedHeroes: ['captain', 'knight', 'samurai']
# 'touch-of-death':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
# 'bonemender':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'}
# restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# requiredCode: ['canCast']
# allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
#
# 'coinucopia':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'copper-meadows':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'drop-the-flag':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'deadly-pursuit':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'rich-forager':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
# restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'multiplayer-treasure-grove':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'siege-of-stonehold':
# requiredGear: {}
# restrictedGear: {}
#
## Desert
# 'the-dunes':
# requiredGear: {}
# restrictedGear: {}
# 'the-mighty-sand-yak':
# requiredGear: {}
# restrictedGear: {}
# 'oasis':
# requiredGear: {}
# restrictedGear: {}

View file

@ -15,7 +15,7 @@ module.exports = class SettingsTabView extends CocoView
editableSettings: [
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
'tasks'
'tasks', 'helpVideos'
]
subscriptions:

View file

@ -24,7 +24,7 @@ class LevelSessionsCollection extends CocoCollection
super()
@url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID"
module.exports = class WorldMapView extends RootView
module.exports = class CampaignView extends RootView
id: 'campaign-view'
template: template
@ -41,16 +41,13 @@ module.exports = class WorldMapView extends RootView
'click #volume-button': 'onToggleVolume'
constructor: (options, @terrain='dungeon') ->
if options and application.isIPAdApp # TODO: later only clear the SuperModel if it has received a memory warning (not in app store yet)
options.supermodel = null
super options
options ?= {}
@campaign = new Campaign({_id:@terrain})
@campaign = @supermodel.loadModel(@campaign, 'campaign').model
@editorMode = options.editorMode
@nextLevel = @getQueryVariable 'next'
@levelStatusMap = {}
@levelPlayCountMap = {}
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model
@ -71,9 +68,8 @@ module.exports = class WorldMapView extends RootView
@supermodel.loadCollection(@earnedAchievements, 'achievements')
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@getLevelPlayCounts()
@listenToOnce @campaign, 'sync', @getLevelPlayCounts
$(window).on 'resize', @onWindowResize
@playAmbientSound()
@probablyCachedMusic = storage.load("loaded-menu-music")
musicDelay = if @probablyCachedMusic then 1000 else 10000
@playMusicTimeout = _.delay (=> @playMusic() unless @destroyed), musicDelay
@ -103,7 +99,6 @@ module.exports = class WorldMapView extends RootView
super()
getLevelPlayCounts: ->
return # TODO: Either use the campaign object instead of hardcoded data or get the data some other way
return unless me.isAdmin()
success = (levelPlayCounts) =>
return if @destroyed
@ -111,13 +106,10 @@ module.exports = class WorldMapView extends RootView
@levelPlayCountMap[level._id] = playtime: level.playtime, sessions: level.sessions
@render() if @fullyRendered
levelIDs = []
for campaign in campaigns
for level in campaign.levels
levelIDs.push level.id
levelSlugs = (level.slug for levelID, level of @campaign.get 'levels')
levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', {
url: '/db/level/-/play_counts'
data: {ids: levelIDs}
data: {ids: levelSlugs}
method: 'POST'
success: success
}, 0
@ -128,7 +120,7 @@ module.exports = class WorldMapView extends RootView
@fullyRendered = true
@render()
@preloadTopHeroes() unless me.get('heroConfig')?.thangType
setCampaign: (@campaign) ->
@render()
@ -143,12 +135,10 @@ module.exports = class WorldMapView extends RootView
for level in context.levels
level.position ?= { x: 10, y: 10 }
level.locked = not me.ownsLevel level.original
window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary
level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal
level.locked = false if @levelStatusMap[level.id] in ['started', 'complete']
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
level.locked = false if me.get('slug') is 'nick'
level.locked = false if @editorMode
level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
level.disabled = true if not me.isAdmin() and level.adminOnly and not @levelStatusMap[level.slug] in ['started', 'complete']
level.color = 'rgb(255, 80, 60)'
if level.requiresSubscription
level.color = 'rgb(80, 130, 200)'
@ -163,13 +153,10 @@ module.exports = class WorldMapView extends RootView
context.levelPlayCountMap = @levelPlayCountMap
context.isIPadApp = application.isIPadApp
context.mapType = _.string.slugify @terrain
context.nextLevel = @nextLevel
context.forestIsAvailable = Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or [])
context.desertIsAvailable = Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or [])
context.requiresSubscription = @requiresSubscription
context.editorMode = @editorMode
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign.get('adjacentCampaigns') or {})), (ac) ->
return false if ac.showIfUnlocked and ac.showIfUnlocked not in (me.get('unlocked')?.levels or [])
return false if ac.showIfUnlocked and ac.showIfUnlocked not in me.levels()
ac.name = utils.i18n ac, 'name'
ac.description = utils.i18n ac, 'description'
styles = []
@ -194,19 +181,14 @@ module.exports = class WorldMapView extends RootView
bg = $('.map-background')
x = ($(@).offset().left - bg.offset().left + $(@).outerWidth() / 2) / bg.width()
y = 1 - ($(@).offset().top - bg.offset().top + $(@).outerHeight() / 2) / bg.height()
e = { position: { x: (100 * x), y: (100 * y) }, levelOriginal: $(@).data('level-id'), campaignID: $(@).data('campaign-id') }
e = { position: { x: (100 * x), y: (100 * y) }, levelOriginal: $(@).data('level-slug'), campaignID: $(@).data('campaign-id') }
view.trigger 'level-moved', e if e.levelOriginal
view.trigger 'adjacent-campaign-moved', e if e.campaignID
@$el.addClass _.string.slugify @terrain
@updateVolume()
@updateHero()
unless window.currentModal or not @fullyRendered
@highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top']
if levelID = @$el.find('.level.next').data('level-id')
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() unless @editorMode
pos = @$el.find('.level.next').offset()
@adjustLevelInfoPosition pageX: pos.left, pageY: pos.top
@manuallyPositionedLevelInfoID = levelID
@applyCampaignStyles()
afterInsert: ->
super()
@ -217,12 +199,29 @@ module.exports = class WorldMapView extends RootView
authModal.mode = 'signup'
@openModalView authModal
applyCampaignStyles: ->
return unless @campaign.loaded
if (backgrounds = @campaign.get 'backgroundImage') and backgrounds.length
backgrounds = _.sortBy backgrounds, 'width'
backgrounds.reverse()
rules = []
for background, i in backgrounds
rule = "#campaign-view .map-background { background-image: url(/file/#{background.image}); }"
rule = "@media screen and (max-width: #{background.width}px) { #{rule} }" if i
rules.push rule
utils.injectCSS rules.join('\n')
if backgroundColor = @campaign.get 'backgroundColor'
backgroundColorTransparent = @campaign.get 'backgroundColorTransparent'
@$el.css 'background-color', backgroundColor
for pos in ['top', 'right', 'bottom', 'left']
@$el.find(".#{pos}-gradient").css 'background-image', "linear-gradient(to #{pos}, #{backgroundColorTransparent} 0%, #{backgroundColor} 100%)"
@playAmbientSound()
onSessionsLoaded: (e) ->
return if @editorMode
for session in @sessions.models
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
if @nextLevel and @levelStatusMap[@nextLevel] is 'complete'
@nextLevel = null
# TODO: add level.next = true for the next level they should do
@render()
onClickMap: (e) ->
@ -237,18 +236,18 @@ module.exports = class WorldMapView extends RootView
e.stopPropagation()
@$levelInfo?.hide()
levelElement = $(e.target).parents('.level')
levelID = levelElement.data('level-id')
levelSlug = levelElement.data('level-slug')
if @editorMode
return @trigger 'level-clicked', levelID
level = _.find _.values(@campaign.get('levels')), id: levelID
return @trigger 'level-clicked', levelSlug
level = _.find _.values(@campaign.get('levels')), slug: levelSlug
if application.isIPadApp
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
@$levelInfo = @$el.find(".level-info-container[data-level-slug=#{levelSlug}]").show()
@adjustLevelInfoPosition e
@endHighlight()
else
if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer
if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.slug] and not level.adventurer
@openModalView new SubscribeModal()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelID
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelSlug
else if $(e.target).attr('disabled')
Backbone.Mediator.publish 'router:navigate', route: '/contribute/adventurer'
return
@ -256,40 +255,36 @@ module.exports = class WorldMapView extends RootView
return
else
@startLevel levelElement
window.tracker?.trackEvent 'Clicked Level', category: 'World Map', levelID: levelID, ['Google Analytics']
window.tracker?.trackEvent 'Clicked Level', category: 'World Map', levelID: levelSlug, ['Google Analytics']
onClickStartLevel: (e) ->
levelElement = $(e.target).parents('.level-info-container')
@startLevel levelElement
window.tracker?.trackEvent 'Clicked Start Level', category: 'World Map', levelID: levelElement.data('level-id'), ['Google Analytics']
window.tracker?.trackEvent 'Clicked Start Level', category: 'World Map', levelID: levelElement.data('level-slug'), ['Google Analytics']
startLevel: (levelElement) ->
@setupManager?.destroy()
@setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelElement.data('level-id'), levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @
@setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelElement.data('level-slug'), levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @
@setupManager.open()
@$levelInfo?.hide()
onMouseEnterLevel: (e) ->
return if application.isIPadApp
return if @editorMode
levelID = $(e.target).parents('.level').data('level-id')
return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
levelSlug = $(e.target).parents('.level').data('level-slug')
@$levelInfo = @$el.find(".level-info-container[data-level-slug=#{levelSlug}]").show()
@adjustLevelInfoPosition e
@endHighlight()
@manuallyPositionedLevelInfoID = false
onMouseLeaveLevel: (e) ->
return if application.isIPadApp
levelID = $(e.target).parents('.level').data('level-id')
return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID
@$el.find(".level-info-container[data-level-id='#{levelID}']").hide()
@manuallyPositionedLevelInfoID = null
levelSlug = $(e.target).parents('.level').data('level-slug')
@$el.find(".level-info-container[data-level-slug='#{levelSlug}']").hide()
@$levelInfo = null
onMouseMoveMap: (e) ->
return if application.isIPadApp
@adjustLevelInfoPosition e unless @manuallyPositionedLevelInfoID
@adjustLevelInfoPosition e
adjustLevelInfoPosition: (e) ->
return unless @$levelInfo
@ -329,8 +324,8 @@ module.exports = class WorldMapView extends RootView
playAmbientSound: ->
return if @ambientSound
return unless file = {dungeon: 'ambient-dungeon', forest: 'ambient-map-grass', desert: 'ambient-desert'}[@terrain]
src = "/file/interface/#{file}#{AudioPlayer.ext}"
return unless file = @campaign.get('ambientSound')?[AudioPlayer.ext.substr 1]
src = "/file/#{file}"
unless AudioPlayer.getStatus(src)?.loaded
AudioPlayer.preloadSound src
Backbone.Mediator.subscribeOnce 'audio-player:loaded', @playAmbientSound, @
@ -381,4 +376,4 @@ module.exports = class WorldMapView extends RootView
for slug, original of ThangType.heroes when original is hero
@$el.find('.player-hero-icon').removeClass().addClass('player-hero-icon ' + slug)
return
console.error "WorldMapView hero update couldn't find hero slug for original:", hero
console.error "CampaignView hero update couldn't find hero slug for original:", hero

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,6 @@ RealTimeModel = require 'models/RealTimeModel'
RealTimeCollection = require 'collections/RealTimeCollection'
LevelSetupManager = require 'lib/LevelSetupManager'
GameMenuModal = require 'views/play/menu/GameMenuModal'
CampaignOptions = require 'lib/CampaignOptions'
module.exports = class ControlBarView extends CocoView
id: 'control-bar-view'
@ -69,8 +68,8 @@ module.exports = class ControlBarView extends CocoView
@homeViewArgs.push levelID
else if @level.get('type', true) in ['hero', 'hero-coop']
@homeLink = c.homeLink = '/play'
@homeViewClass = 'views/play/WorldMapView'
campaign = CampaignOptions.getCampaignForSlug @level.get 'slug'
@homeViewClass = 'views/play/CampaignView'
campaign = @level.get 'campaign'
if campaign isnt 'dungeon'
@homeLink += '/' + campaign
@homeViewArgs.push campaign

View file

@ -1,7 +1,6 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/hud'
prop_template = require 'templates/play/level/hud_prop'
LevelOptions = require 'lib/LevelOptions'
utils = require 'core/utils'
module.exports = class LevelHUDView extends CocoView
@ -23,7 +22,7 @@ module.exports = class LevelHUDView extends CocoView
afterRender: ->
super()
@$el.addClass 'no-selection'
if LevelOptions[@options.level.get('slug')]?.hidesHUD
if @options.level.get('hidesHUD')
@hidesHUD = true
@$el.addClass 'hide-hud-properties'

View file

@ -1,7 +1,6 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/playback'
{me} = require 'core/auth'
LevelOptions = require 'lib/LevelOptions'
module.exports = class LevelPlaybackView extends CocoView
id: 'playback-view'
@ -67,7 +66,7 @@ module.exports = class LevelPlaybackView extends CocoView
@goto = t 'play_level.time_goto'
@current = t 'play_level.time_current'
@total = t 'play_level.time_total'
@$el.find('#play-button').css('visibility', 'hidden') if LevelOptions[@options.levelID]?.hidesPlayButton # Don't show for first few levels, confuses new players.
@$el.find('#play-button').css('visibility', 'hidden') if @options.level.get 'hidesPlayButton' # Don't show for first few levels, confuses new players.
updatePopupContent: ->
@timePopup?.updateContent "<h2>#{@timeToString @newTime}</h2>#{@formatTime(@current, @currentTime)}<br/>#{@formatTime(@total, @totalTime)}"

View file

@ -62,7 +62,6 @@ module.exports = class PlayLevelView extends RootView
'god:infinite-loop': 'onInfiniteLoop'
'level:reload-from-data': 'onLevelReloadFromData'
'level:reload-thang-type': 'onLevelReloadThangType'
'level:play-next-level': 'onPlayNextLevel'
'level:session-will-save': 'onSessionWillSave'
'level:started': 'onLevelStarted'
'level:loading-view-unveiling': 'onLoadingViewUnveiling'
@ -235,7 +234,7 @@ module.exports = class PlayLevelView extends RootView
insertSubviews: ->
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level
@insertSubView new LevelPlaybackView session: @session, levelID: @levelID, level: @level
@insertSubView new LevelPlaybackView session: @session, level: @level
@insertSubView new GoalsView {}
@insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags'
@insertSubView new GoldView {}
@ -427,7 +426,7 @@ module.exports = class PlayLevelView extends RootView
victoryModal = new ModalClass(options)
@openModalView(victoryModal)
if me.get('anonymous')
window.nextLevelURL = @getNextLevelURL() # Signup will go here on completion instead of reloading.
window.nextURL = '/play/' + (@level.get('campaign') ? '') # Signup will go here on completion instead of reloading.
onRestartLevel: ->
@tome.reloadAllCode()
@ -440,23 +439,6 @@ module.exports = class PlayLevelView extends RootView
@openModalView new InfiniteLoopModal()
application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name')
onPlayNextLevel: ->
nextLevelID = @getNextLevelID()
nextLevelURL = @getNextLevelURL()
Backbone.Mediator.publish 'router:navigate', {
route: nextLevelURL,
viewClass: PlayLevelView,
viewArgs: [{supermodel: if @hasReceivedMemoryWarning then null else @supermodel}, nextLevelID]}
getNextLevelID: ->
for campaign in require('views/play/WorldMapView').campaigns
for level in campaign.levels
return level.nextLevels?.continue if level.id is @level.get('slug')
getNextLevelURL: ->
return null unless @getNextLevelID()
"/play/level/#{@getNextLevelID()}"
onHighlightDOM: (e) -> @highlightElement e.selector, delay: e.delay, sides: e.sides, offset: e.offset, rotation: e.rotation
onEndHighlight: -> @endHighlight()
@ -554,7 +536,7 @@ module.exports = class PlayLevelView extends RootView
delete window.world # not sure where this is set, but this is one way to clean it up
@bus?.destroy()
#@instance.save() unless @instance.loading
delete window.nextLevelURL
delete window.nextURL
console.profileEnd?() if PROFILE_ME
@onRealTimeMultiplayerLevelUnloaded()
super()

View file

@ -9,7 +9,6 @@ utils = require 'core/utils'
ThangType = require 'models/ThangType'
LadderSubmissionView = require 'views/play/common/LadderSubmissionView'
AudioPlayer = require 'lib/AudioPlayer'
CampaignOptions = require 'lib/CampaignOptions'
User = require 'models/User'
utils = require 'core/utils'
@ -115,7 +114,6 @@ module.exports = class HeroVictoryModal extends ModalView
c.me = me
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()
c.level = @level
@continueLevelLink = @getNextLevelLink 'continue'
elapsed = (new Date() - new Date(me.get('dateCreated')))
isHourOfCode = me.get('hourOfCode') or elapsed < 120 * 60 * 1000
@ -301,34 +299,21 @@ module.exports = class HeroVictoryModal extends ModalView
else
AudioPlayer.playSound name, 1
getLevelInfoForSlug: (slug) ->
for campaign in require('views/play/WorldMapView').campaigns
for level in campaign.levels
return level if level.id is slug
getNextLevelCampaign: ->
# Wouldn't handle skipping/more practice across campaign boundaries, but we don't do that.
campaign = CampaignOptions.getCampaignForSlug @level.get 'slug'
if nextLevelSlug = @getNextLevel 'continue'
campaign = CampaignOptions.getCampaignForSlug nextLevelSlug
campaign or 'dungeon'
{'kithgard-gates': 'forest', 'siege-of-stonehold': 'desert'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out.
link += '/' + nextCampaign unless nextCampaign is 'dungeon'
getNextLevelLink: (type) ->
getNextLevelLink: ->
link = '/play'
nextCampaign = @getNextLevelCampaign()
link += '/' + nextCampaign unless nextCampaign is 'dungeon'
return link unless nextLevel = @getNextLevel type
"#{link}?next=#{nextLevel}"
getNextLevel: (type) ->
levelInfo = @getLevelInfoForSlug @level.get 'slug'
levelInfo?.nextLevels?[type] # 'continue'; TODO: refactor to not have the object and just use single nextLevel property
link
onClickContinue: (e) ->
@playSound 'menu-button-click'
nextLevelLink = @continueLevelLink
nextLevelLink = @getNextLevelLink()
# Preserve the supermodel as we navigate back to the world map.
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/CampaignView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
onClickReturnToLadder: (e) ->
@playSound 'menu-button-click'

View file

@ -14,8 +14,6 @@ module.exports = class VictoryModal extends ModalView
'ladder:game-submitted': 'onGameSubmitted'
events:
'click .next-level-button': 'onPlayNextLevel'
'click .world-map-button': 'onClickWorldMap'
'click .sign-up-button': 'onClickSignupButton'
# review events
@ -26,9 +24,6 @@ module.exports = class VictoryModal extends ModalView
@$el.find('.review').show()
'keypress .review textarea': -> @saveReviewEventually()
shortcuts:
'enter': -> 'onPlayNextLevel'
constructor: (options) ->
application.router.initializeSocialMediaServices()
victory = options.level.get('victory', true)
@ -61,15 +56,6 @@ module.exports = class VictoryModal extends ModalView
@feedback.set('level', {majorVersion: @level.get('version').major, original: @level.get('original')})
@showStars()
onPlayNextLevel: ->
@saveReview() if @$el.find('.review textarea').val()
Backbone.Mediator.publish 'level:play-next-level', {}
onClickWorldMap: (e) ->
e.preventDefault()
e.stopImmediatePropagation()
Backbone.Mediator.publish 'router:navigate', route: '/play', viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: @supermodel}]
onClickSignupButton: (e) ->
e.preventDefault()
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Victory Modal', level: @level.get('slug')
@ -83,23 +69,10 @@ module.exports = class VictoryModal extends ModalView
c = super()
c.body = @body
c.me = me
c.hasNextLevel = _.isObject(@level.get('nextLevel'))
c.levelName = utils.i18n @level.attributes, 'name'
c.level = @level
if c.level.get('type') in ['ladder', 'hero-ladder']
if c.level.get('type') is 'ladder'
c.readyToRank = @session.readyToRank()
if me.get 'hourOfCode'
# Show the Hour of Code "I'm Done" tracking pixel after they played for 30 minutes
elapsed = (new Date() - new Date(me.get('dateCreated')))
enough = not c.hasNextLevel or elapsed >= 30 * 60 * 1000
if enough and not me.get('hourOfCodeComplete')
$('body').append($('<img src="http://code.org/api/hour/finish_codecombat.png" style="visibility: hidden;">'))
me.set 'hourOfCodeComplete', true
me.patch()
window.tracker?.trackEvent 'Hour of Code Finish', {}
# Show the "I'm done" button if they get to the end, unless it's been over two hours
tooMuch = elapsed >= 120 * 60 * 1000
c.showHourOfCodeDoneButton = not c.hasNextLevel and not tooMuch
c
afterRender: ->

View file

@ -1,7 +1,6 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/tome/cast_button'
{me} = require 'core/auth'
LevelOptions = require 'lib/LevelOptions'
module.exports = class CastButtonView extends CocoView
id: 'cast-button-view'
@ -25,9 +24,7 @@ module.exports = class CastButtonView extends CocoView
constructor: (options) ->
super options
@spells = options.spells
@levelID = options.levelID
@castShortcut = '⇧↵'
@levelOptions = LevelOptions[@options.levelID] ? {}
getRenderData: (context={}) ->
context = super context
@ -46,11 +43,11 @@ module.exports = class CastButtonView extends CocoView
#delay = me.get('autocastDelay') # No more autocast
delay = 90019001
@setAutocastDelay delay
if @levelOptions.hidesSubmitUntilRun or @levelOptions.hidesRealTimePlayback
if @options.level.get('hidesSubmitUntilRun') or @options.level.get('hidesRealTimePlayback')
@$el.find('.submit-button').hide() # Hide Submit for the first few until they run it once.
if @options.session.get('state')?.complete and @levelOptions.hidesRealTimePlayback
if @options.session.get('state')?.complete and @options.level.get 'hidesRealTimePlayback'
@$el.find('.done-button').show()
if @options.levelID is 'thornbush-farm'# and not @options.session.get('state')?.complete
if @options.level.get('slug') is 'thornbush-farm'# and not @options.session.get('state')?.complete
@$el.find('.submit-button').hide() # Hide submit until first win so that script can explain it.
attachTo: (spellView) ->
@ -92,16 +89,16 @@ module.exports = class CastButtonView extends CocoView
@winnable = winnable
@$el.toggleClass 'winnable', @winnable
Backbone.Mediator.publish 'tome:winnability-updated', winnable: @winnable
if @levelOptions.hidesRealTimePlayback
if @options.level.get 'hidesRealTimePlayback'
@$el.find('.done-button').toggle @winnable
else if @winnable and @options.levelID is 'thornbush-farm'
else if @winnable and @options.level.get('slug') is 'thornbush-farm'
@$el.find('.submit-button').show() # Hide submit until first win so that script can explain it.
onGoalsCalculated: (e) ->
# When preloading, with real-time playback enabled, we highlight the submit button when we think they'll win.
return unless e.preload
return if @levelOptions.hidesRealTimePlayback
return if @options.levelID is 'thornbush-farm' # Don't show it until they actually win for this first one.
return if @options.level.get 'hidesRealTimePlayback'
return if @options.level.get('slug') is 'thornbush-farm' # Don't show it until they actually win for this first one.
@onNewGoalStates e
updateCastButton: ->
@ -116,7 +113,7 @@ module.exports = class CastButtonView extends CocoView
castText = $.i18n.t('play_level.tome_cast_button_running')
else if castable or true
castText = $.i18n.t('play_level.tome_cast_button_run')
unless @levelOptions.hidesRunShortcut # Hide for first few.
unless @options.level.get 'hidesRunShortcut' # Hide for first few.
castText += ' ' + @castShortcut
else
castText = $.i18n.t('play_level.tome_cast_button_ran')

View file

@ -5,7 +5,6 @@ filters = require 'lib/image_filter'
SpellPaletteEntryView = require './SpellPaletteEntryView'
LevelComponent = require 'models/LevelComponent'
ThangType = require 'models/ThangType'
LevelOptions = require 'lib/LevelOptions'
GameMenuModal = require 'views/play/menu/GameMenuModal'
N_ROWS = 4
@ -221,7 +220,7 @@ module.exports = class SpellPaletteView extends CocoView
# Assign any unassigned properties to the hero itself.
for owner, storage of propStorage
for prop in _.reject(@thang[storage] ? [], (prop) -> itemsByProp[prop] or prop[0] is '_') # no private properties
if prop is 'say' and LevelOptions[@options.level.get('slug')]?.hidesSay # Hide for Dungeon Campaign
if prop is 'say' and @options.level.get 'hidesSay' # Hide for Dungeon Campaign
continue
propsByItem['Hero'] ?= []
propsByItem['Hero'].push owner: owner, prop: prop, item: itemThangTypes[@thang.spriteName]
@ -284,7 +283,7 @@ module.exports = class SpellPaletteView extends CocoView
entry.destroy() for entry in @entries
@createPalette()
@render()
onClickHelp: (e) ->
application.tracker?.trackEvent 'Spell palette help clicked', levelID: @level.get('slug')
@openModalView new GameMenuModal showTab: 'guide', level: @level, session: @session, supermodel: @supermodel

View file

@ -9,8 +9,6 @@ SpellDebugView = require './SpellDebugView'
SpellToolbarView = require './SpellToolbarView'
LevelComponent = require 'models/LevelComponent'
UserCodeProblem = require 'models/UserCodeProblem'
CampaignOptions = require 'lib/CampaignOptions'
LevelOptions = require 'lib/LevelOptions'
module.exports = class SpellView extends CocoView
id: 'spell-view'
@ -199,7 +197,7 @@ module.exports = class SpellView extends CocoView
bindKey: {win: 'Ctrl-Shift-M', mac: 'Command-Shift-M|Ctrl-Shift-M'}
exec: -> Backbone.Mediator.publish 'tome:toggle-maximize', {}
addCommand
# TODO: Restrict to beginner campaign levels, possibly with a CampaignOptions similar to LevelOptions
# TODO: Restrict to beginner campaign levels like we do backspaceThrottle
name: 'enter-skip-delimiters'
bindKey: 'Enter|Return'
exec: =>
@ -216,40 +214,36 @@ module.exports = class SpellView extends CocoView
name: 'disable-spaces'
bindKey: 'Space'
exec: =>
return @ace.execCommand 'insertstring', ' ' unless LevelOptions[@options.level.get('slug')]?.disableSpaces
return @ace.execCommand 'insertstring', ' ' unless @options.level.get 'disableSpaces'
line = @aceDoc.getLine @ace.getCursorPosition().row
return @ace.execCommand 'insertstring', ' ' if @singleLineCommentRegex().test line
addCommand
name: 'throttle-backspaces'
bindKey: 'Backspace'
exec: =>
# Throttle the backspace speed
# Slow to 500ms when whitespace at beginning of line is first encountered
# Slow to 100ms for remaining whitespace at beginning of line
# Rough testing showed backspaces happen at 150ms when tapping.
# Backspace speed varies by system when holding, 30ms on fastest Macbook setting.
unless CampaignOptions?.getOption?(@options?.level?.get?('slug'), 'backspaceThrottle')
if @options.level.get 'backspaceThrottle'
addCommand
name: 'throttle-backspaces'
bindKey: 'Backspace'
exec: =>
# Throttle the backspace speed
# Slow to 500ms when whitespace at beginning of line is first encountered
# Slow to 100ms for remaining whitespace at beginning of line
# Rough testing showed backspaces happen at 150ms when tapping.
# Backspace speed varies by system when holding, 30ms on fastest Macbook setting.
nowDate = Date.now()
if @aceSession.selection.isEmpty()
cursor = @ace.getCursorPosition()
line = @aceDoc.getLine(cursor.row)
if /^\s*$/.test line.substring(0, cursor.column)
@backspaceThrottleMs ?= 500
# console.log "SpellView @backspaceThrottleMs=#{@backspaceThrottleMs}"
# console.log 'SpellView lastBackspace diff', nowDate - @lastBackspace if @lastBackspace?
if not @lastBackspace? or nowDate - @lastBackspace > @backspaceThrottleMs
@backspaceThrottleMs = 100
@lastBackspace = nowDate
@ace.remove "left"
return
@backspaceThrottleMs = null
@lastBackspace = nowDate
@ace.remove "left"
return
nowDate = Date.now()
if @aceSession.selection.isEmpty()
cursor = @ace.getCursorPosition()
line = @aceDoc.getLine(cursor.row)
if /^\s*$/.test line.substring(0, cursor.column)
@backspaceThrottleMs ?= 500
# console.log "SpellView @backspaceThrottleMs=#{@backspaceThrottleMs}"
# console.log 'SpellView lastBackspace diff', nowDate - @lastBackspace if @lastBackspace?
if not @lastBackspace? or nowDate - @lastBackspace > @backspaceThrottleMs
@backspaceThrottleMs = 100
@lastBackspace = nowDate
@ace.remove "left"
return
@backspaceThrottleMs = null
@lastBackspace = nowDate
@ace.remove "left"
fillACE: ->
@ -259,7 +253,7 @@ module.exports = class SpellView extends CocoView
lockDefaultCode: (force=false) ->
# TODO: Lock default indent for an empty line?
return unless LevelOptions[@options.level.get('slug')]?.lockDefaultCode or CampaignOptions?.getOption?(@options?.level?.get?('slug'), 'lockDefaultCode')
return unless @options.level.get('lockDefaultCode')
return unless @spell.source is @spell.originalSource or force
console.info 'Locking down default code.'
@ -374,7 +368,7 @@ module.exports = class SpellView extends CocoView
# TODO: Turn on more autocompletion based on level sophistication
# TODO: E.g. using the language default snippets yields a bunch of crazy non-beginner suggestions
# TODO: Options logic shouldn't exist both here and in updateAutocomplete()
popupFontSizePx = CampaignOptions.getOption(@options.level.get('slug'), 'autocompleteFontSizePx') ? 16
popupFontSizePx = @options.level.get('autocompleteFontSizePx') ? 16
@zatanna = new Zatanna @ace,
basic: false
liveCompletion: false
@ -411,7 +405,7 @@ module.exports = class SpellView extends CocoView
return (owner is 'this' or owner is 'more') and (not doc.owner? or doc.owner is 'this')
if doc?.snippets?[e.language]
content = doc.snippets[e.language].code
if /loop/.test(content) and LevelOptions[@options.level.get('slug')]?.moveRightLoopSnippet
if /loop/.test(content) and @options.level.get 'moveRightLoopSnippet'
# Replace default loop snippet with an embedded moveRight()
content = switch e.language
when 'python' then 'loop:\n self.moveRight()\n ${1:}'
@ -618,8 +612,8 @@ module.exports = class SpellView extends CocoView
_.throttle @updateLines, 500
_.throttle @hideProblemAlert, 500
]
onSignificantChange.push _.debounce @checkRequiredCode, 750 if LevelOptions[@options.level.get('slug')]?.requiredCode
onSignificantChange.push _.debounce @checkSuspectCode, 750 if LevelOptions[@options.level.get('slug')]?.suspectCode
onSignificantChange.push _.debounce @checkRequiredCode, 750 if @options.level.get 'requiredCode'
onSignificantChange.push _.debounce @checkSuspectCode, 750 if @options.level.get 'suspectCode'
@onCodeChangeMetaHandler = =>
return if @eventsSuppressed
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'code-change', volume: 0.5
@ -924,7 +918,7 @@ module.exports = class SpellView extends CocoView
@aceSession.removeGutterDecoration row, 'executed'
@decoratedGutter[row] = ''
lastExecuted = _.last executed
showToolbarView = executed.length and @spellThang.castAether.metrics.statementsExecuted > 3 and not LevelOptions[@options.level.get('slug')]?.hidesCodeToolbar # Hide for a while
showToolbarView = executed.length and @spellThang.castAether.metrics.statementsExecuted > 3 and not @options.level.get 'hidesCodeToolbar' # Hide for a while
showToolbarView = false # TODO: fix toolbar styling in new design to have some space for it
if showToolbarView
@ -1059,7 +1053,7 @@ module.exports = class SpellView extends CocoView
checkRequiredCode: =>
return if @destroyed
source = @getSource().replace @singleLineCommentRegex(), ''
requiredCodeFragments = LevelOptions[@options.level.get('slug')].requiredCode
requiredCodeFragments = @options.level.get 'requiredCode'
for requiredCodeFragment in requiredCodeFragments
# Could make this obey regular expressions like suspectCode if needed
if source.indexOf(requiredCodeFragment) is -1
@ -1071,10 +1065,11 @@ module.exports = class SpellView extends CocoView
checkSuspectCode: =>
return if @destroyed
source = @getSource().replace @singleLineCommentRegex(), ''
suspectCodeFragments = LevelOptions[@options.level.get('slug')].suspectCode
suspectCodeFragments = @options.level.get 'suspectCode'
detectedSuspectCodeFragmentNames = []
for suspectCodeFragment in suspectCodeFragments
if suspectCodeFragment.pattern.test source
pattern = new RegExp suspectCodeFragment.pattern, 'm'
if pattern.test source
@warnedCodeFragments ?= {}
unless @warnedCodeFragments[suspectCodeFragment.name]
Backbone.Mediator.publish 'tome:suspect-code-fragment-added', codeFragment: suspectCodeFragment.name, codeLanguage: @spell.language

View file

@ -62,7 +62,7 @@ module.exports = class TomeView extends CocoView
programmableThangs = _.filter @options.thangs, 'isProgrammable'
@createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
@castButton = @insertSubView new CastButtonView spells: @spells, levelID: @options.levelID, session: @options.session
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session
@teamSpellMap = @generateTeamSpellMap(@spells)
unless programmableThangs.length
@cast()

View file

@ -2,7 +2,6 @@ CocoView = require 'views/core/CocoView'
template = require 'templates/play/menu/guide-view'
Article = require 'models/Article'
utils = require 'core/utils'
LevelOptions = require 'lib/LevelOptions'
# let's implement this once we have the docs database schema set up
@ -15,12 +14,12 @@ module.exports = class LevelGuideView extends CocoView
constructor: (options) ->
@levelID = options.level.get('slug')
@helpVideos = LevelOptions[@levelID]?.helpVideos ? []
@helpVideos = options.level.get 'helpVideos'
@trackedHelpVideoStart = @trackedHelpVideoFinish = false
# A/B Testing video tutorial styles
@helpVideosIndex = me.getVideoTutorialStylesIndex(@helpVideos.length)
@firstOnly = options.firstOnly
@docs = options?.docs ? options.level.get('documentation') ? {}
general = @docs.generalArticles or []
@ -88,7 +87,7 @@ module.exports = class LevelGuideView extends CocoView
unless @trackedHelpVideoStart
window.tracker?.trackEvent 'Start help video', level: @levelID, style: @helpVideos[@helpVideosIndex].style
@trackedHelpVideoStart = true
onFinishHelpVideo: ->
unless @trackedHelpVideoFinish
window.tracker?.trackEvent 'Finish help video', level: @levelID, style: @helpVideos[@helpVideosIndex].style
@ -96,56 +95,8 @@ module.exports = class LevelGuideView extends CocoView
setupVideoPlayer: () ->
return unless @helpVideos.length > 0
# TODO: run A/B test for different video styles
helpVideoURL = @helpVideos[@helpVideosIndex].URL
if helpVideoURL.toLowerCase().indexOf('youtube') >= 0
@setupYouTubeVideoPlayer helpVideoURL
else if helpVideoURL.toLowerCase().indexOf('vimeo') >= 0
@setupVimeoVideoPlayer helpVideoURL
setupYouTubeVideoPlayer: (helpVideoURL) ->
# Setup YouTube iframe player
# https://developers.google.com/youtube/iframe_api_reference
# TODO: Can't load a YouTube video twice in one level
# TODO: window.onYouTubeIframeAPIReady is only called once
# TODO: Consider ripping out YouTube support and migrating all videos to Vimeo
onPlayerStateChange = (e) =>
if e.data is 1
@onStartHelpVideo()
else if e.data is 0
@onFinishHelpVideo()
createPlayer = =>
new YT.Player 'help-video-player', {
height: @helpVideoHeight,
width: @helpVideoWidth,
videoId: videoID,
events: {
'onStateChange': onPlayerStateChange
}
}
if matchVideoID = helpVideoURL.match /www\.youtube\.com\/embed\/(bHaeKdMPZrA)/
videoID = matchVideoID[1]
else
console.warn "Unable to read video ID from help video."
# TODO: Default to dungeons-of-kithgard?
videoID = 'bHaeKdMPZrA'
# Add method that will be called by YouTube iframe player when ready
window.onYouTubeIframeAPIReady = =>
createPlayer()
# Add YouTube video player iframe script if necessary
if YT?.Player?
createPlayer()
else
tag = document.createElement('script')
tag.src = "https://www.youtube.com/iframe_api"
@$el.find('#help-video-heading').after(tag)
helpVideoURL = @helpVideos[@helpVideosIndex].url
@setupVimeoVideoPlayer helpVideoURL
setupVimeoVideoPlayer: (helpVideoURL) ->
# Setup Vimeo player
@ -166,7 +117,6 @@ module.exports = class LevelGuideView extends CocoView
# Vimeo player is ready, can now hook up other events
# https://developer.vimeo.com/player/js-api#events
player = $('#help-video-player')[0]
helpVideoURL = 'http:' + helpVideoURL unless helpVideoURL.indexOf('http') is 0
player.contentWindow.postMessage JSON.stringify(method: 'addEventListener', value: 'play'), helpVideoURL
player.contentWindow.postMessage JSON.stringify(method: 'addEventListener', value: 'finish'), helpVideoURL
else if data.event is 'play'

View file

@ -8,7 +8,6 @@ ItemView = require './ItemView'
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
ItemDetailsView = require 'views/play/modal/ItemDetailsView'
Purchase = require 'models/Purchase'
LevelOptions = require 'lib/LevelOptions'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
hasGoneFullScreenOnce = false
@ -94,9 +93,13 @@ module.exports = class InventoryModal extends ModalView
locked = not (item.get('original') in me.items())
#locked = false if me.get('slug') is 'nick'
if not item.getFrontFacingStats().props.length and not _.size(item.getFrontFacingStats().stats) and locked # Temp: while there are placeholder items
null # Don't put into a collection
if locked and item.get('slug') in _.values(LevelOptions[@options.levelID]?.requiredGear ? {})
required = item.get('slug') in _.flatten(_.values(@options.level.get('requiredGear') ? {}))
restricted = item.get('slug') in _.flatten(_.values(@options.level.get('restrictedGear') ? {}))
placeholder = not item.getFrontFacingStats().props.length and not _.size(item.getFrontFacingStats().stats)
if placeholder and locked # The item is not complete, so don't put it into a collection.
null
else if locked and required
item.classes.push 'locked'
@itemGroups.requiredPurchaseItems.add item
else if locked and item.get('slug') isnt 'simple-boots'
@ -106,7 +109,7 @@ module.exports = class InventoryModal extends ModalView
null
else
@itemGroups.lockedItems.add(item)
else if item.get('slug') in _.values(LevelOptions[@options.levelID]?.restrictedGear ? {})
else if restricted
@itemGroups.restrictedItems.add(item)
item.classes.push 'restricted'
else
@ -361,8 +364,8 @@ module.exports = class InventoryModal extends ModalView
requireLevelEquipment: ->
# This is temporary, until we have a more general way of awarding items and configuring required/restricted items per level.
requiredGear = LevelOptions[@options.levelID]?.requiredGear ? {}
restrictedGear = LevelOptions[@options.levelID]?.restrictedGear ? {}
requiredGear = @options.level.get('requiredGear') ? {}
restrictedGear = @options.level.get('restrictedGear') ? {}
if @inserted
if @supermodel.finished()
equipment = @getCurrentEquipmentConfig() # Make sure @equipment is updated
@ -379,33 +382,43 @@ module.exports = class InventoryModal extends ModalView
console.log 'Unequipping', itemModel.get('heroClass'), 'item', itemModel.get('name'), 'from slot due to class restrictions.'
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
delete equipment[slot]
for slot, item of restrictedGear
equipped = equipment[slot]
if equipped and equipped is gear[restrictedGear[slot]]
console.log 'Unequipping restricted item', restrictedGear[slot], 'for', slot, 'before level', @options.levelID
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
delete equipment[slot]
for slot, item of requiredGear
for slot, items of restrictedGear
items = [items] if _.isString items
for item in items
item = gear[item] unless item.length is 24 # Temp: until migration to DB data is done
equipped = equipment[slot]
if equipped and equipped is item
console.log 'Unequipping restricted item', equipped, 'for', slot, 'before level', @options.level.get('slug')
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
delete equipment[slot]
for slot, items of requiredGear
items = [items] if _.isString items # Temp: until migration to arrays is done
item = items[0] # TODO: look for the last one that they own, or the first one if they don't own any.
# TODO: require them to have one of the given items, not just either the item or anything except all these exceptions.
slug = gearSlugs[item]
if item.length isnt 24 # Temp: until migration to DB data is done
[item, slug] = [gear[item], item]
#console.log 'requiring', item, slug, 'for', slot, 'and have', equipment[slot]
if (slot in ['right-hand', 'left-hand', 'head', 'torso']) and not (heroClass is 'Warrior' or
(heroClass is 'Ranger' and @options.levelID in ['swift-dagger', 'shrapnel']) or
(heroClass is 'Wizard' and @options.levelID in ['touch-of-death', 'bonemender'])) and not (item in ['crude-builders-hammer', 'wooden-builders-hammer'])
(heroClass is 'Ranger' and @options.level.get('slug') in ['swift-dagger', 'shrapnel']) or
(heroClass is 'Wizard' and @options.level.get('slug') in ['touch-of-death', 'bonemender'])) and not (slug in ['crude-builders-hammer', 'wooden-builders-hammer'])
# After they switch to a ranger or wizard, we stop being so finicky about class-specific gear.
continue
continue if item is 'tarnished-bronze-breastplate' and inWorldMap and @options.levelID is 'the-raised-sword' # Don't tell them they need it until they need it in the level
continue if slug is 'tarnished-bronze-breastplate' and inWorldMap and @options.level.get('slug') is 'the-raised-sword' # Don't tell them they need it until they need it in the level
equipped = equipment[slot]
continue if equipped and not (
(item is 'crude-builders-hammer' and equipped in [gear['simple-sword'], gear['long-sword'], gear['sharpened-sword'], gear['roughedge']]) or
(item in ['simple-sword', 'long-sword', 'roughedge', 'sharpened-sword'] and equipped is gear['crude-builders-hammer']) or
(item is 'leather-boots' and equipped is gear['simple-boots']) or
(item is 'simple-boots' and equipped is gear['leather-boots'])
(slug is 'crude-builders-hammer' and equipped in [gear['simple-sword'], gear['long-sword'], gear['sharpened-sword'], gear['roughedge']]) or
(slug in ['simple-sword', 'long-sword', 'roughedge', 'sharpened-sword'] and equipped is gear['crude-builders-hammer']) or
(slug is 'leather-boots' and equipped is gear['simple-boots']) or
(slug is 'simple-boots' and equipped is gear['leather-boots'])
)
itemModel = @items.findWhere {slug: item}
itemModel = @items.findWhere {slug: slug}
continue unless itemModel
availableSlotSelector = "#unequipped .item[data-item-id='#{itemModel.id}']"
@highlightElement availableSlotSelector, delay: 500, sides: ['right'], rotation: Math.PI / 2
@$el.find(availableSlotSelector).addClass 'should-equip'
@$el.find("#equipped div[data-slot='#{slot}']").addClass 'should-equip'
@remainingRequiredEquipment.push slot: slot, item: gear[item]
@remainingRequiredEquipment.push slot: slot, item: item
if hadRequired and not @remainingRequiredEquipment.length
@endHighlight()
@highlightElement '#play-level-button', duration: 5000
@ -639,3 +652,5 @@ gear =
'quartz-sense-stone': '54693240a2b1f53ce79443c5'
'wooden-builders-hammer': '54694ba3a2b1f53ce794444d'
'simple-wristwatch': '54693797a2b1f53ce79443e9'
gearSlugs = _.invert gear

View file

@ -8,7 +8,6 @@ AudioPlayer = require 'lib/AudioPlayer'
utils = require 'core/utils'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
Purchase = require 'models/Purchase'
LevelOptions = require 'lib/LevelOptions'
module.exports = class PlayHeroesModal extends ModalView
className: 'modal fade play-modal'
@ -55,8 +54,8 @@ module.exports = class PlayHeroesModal extends ModalView
original = hero.get('original')
hero.locked = not me.ownsHero(original)
hero.purchasable = hero.locked and (original in (me.get('earned')?.heroes ? []))
if @options.levelID and allowedHeroSlugs = LevelOptions[@options.levelID]?.allowedHeroes
hero.restricted = not (hero.get('slug') in allowedHeroSlugs)
if @options.level and allowedHeroes = @options.level.get 'allowedHeroes'
hero.restricted = not (hero.get('original') in allowedHeroes)
hero.class = (hero.get('heroClass') or 'warrior').toLowerCase()
hero.stats = hero.getHeroStats()

View file

@ -36,23 +36,30 @@ CampaignHandler = class CampaignHandler extends Handler
return @getRelatedAchievements(req, res, campaign, projection) if relationship is 'achievements'
else
super(arguments...)
getRelatedLevels: (req, res, campaign, projection) ->
extraProjectionProps = []
if projection
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
extraProjectionProps.push 'permissions' unless projection.permissions
extraProjectionProps.push 'version' unless projection.version
projection.permissions = 1
projection.version = 1
levels = campaign.get('levels') or []
f = (levelOriginal) ->
(callback) ->
query = { original: mongoose.Types.ObjectId(levelOriginal) }
sort = { 'version.major': -1, 'version.minor': -1 }
Level.findOne(query, projection).sort(sort).exec callback
fetches = (f(level.original) for level in _.values(levels))
async.parallel fetches, (err, levels) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (level.toObject() for level in levels))
filteredLevels = (_.omit(level.toObject(), extraProjectionProps) for level in levels)
return @sendSuccess(res, filteredLevels)
getRelatedAchievements: (req, res, campaign, projection) ->
levels = campaign.get('levels') or []
@ -67,4 +74,8 @@ CampaignHandler = class CampaignHandler extends Handler
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (achievement.toObject() for achievement in achievements))
onPutSuccess: (req, doc) ->
docLink = "http://codecombat.com#{req.headers['x-current-path']}"
@sendChangedHipChatMessage creator: req.user, target: doc, docLink: docLink
module.exports = new CampaignHandler()

View file

@ -314,10 +314,17 @@ module.exports = class Handler
projection = {}
fields = if req.query.project is 'true' then _.keys(PROJECT) else req.query.project.split(',')
projection[field] = 1 for field in fields
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
extraProjectionProps = []
extraProjectionProps.push 'permissions' unless projection.permissions
extraProjectionProps.push 'version' unless projection.version
projection.permissions = 1
projection.version = 1
args.push projection
@modelClass.findOne(args...).sort(sort).exec (err, doc) =>
return @sendNotFoundError(res) unless doc?
return @sendForbiddenError(res) unless @hasAccessToDocument(req, doc)
doc = _.omit doc, extraProjectionProps if extraProjectionProps?
res.send(doc)
res.end()
@ -343,6 +350,7 @@ module.exports = class Handler
return @sendBadInputError(res, err.errors) if err?.valid is false
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document))
@onPutSuccess(req, document)
post: (req, res) ->
if @modelClass.schema.uses_coco_versions
@ -362,6 +370,7 @@ module.exports = class Handler
@onPostSuccess(req, document)
onPostSuccess: (req, doc) ->
onPutSuccess: (req, doc) ->
###
TODO: think about pulling some common stuff out of postFirstVersion/postNewVersion

View file

@ -33,6 +33,7 @@ LevelHandler = class LevelHandler extends Handler
'requiresSubscription'
'adventurer'
'practice'
'adminOnly'
'disableSpaces'
'hidesSubmitUntilRun'
'hidesPlayButton'
@ -52,6 +53,8 @@ LevelHandler = class LevelHandler extends Handler
'restrictedGear'
'allowedHeroes'
'tasks'
'helpVideos'
'campaign'
]
postEditableProperties: ['name']
@ -93,7 +96,7 @@ LevelHandler = class LevelHandler extends Handler
Session.findOne(sessionQuery).exec (err, doc) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, doc) if doc?
return @sendPaymentRequiredError(res, err) if (not req.user.isPremium()) and level.get('requiresSubscription')
return @sendPaymentRequiredError(res, err) if (not req.user.isPremium()) and level.get('requiresSubscription') and not level.get('adventurer')
@createAndSaveNewSession sessionQuery, req, res
createAndSaveNewSession: (sessionQuery, req, res) =>

View file

@ -309,17 +309,17 @@ module.exports.SearchablePlugin = (schema, options) ->
next()
module.exports.TranslationCoveragePlugin = (schema, options) ->
schema.uses_coco_translation_coverage = true
schema.set('autoIndex', true)
index = {}
if schema.uses_coco_versions
if not schema.uses_coco_names
throw Error('If using translation coverage and versioning, should also use names for indexing.')
index.slug = 1
index.i18nCoverage = 1
schema.index(index, {sparse: true, name: 'translation coverage index', background: true})
schema.index(index, {sparse: true, name: 'translation coverage index', background: true})

View file

@ -23,7 +23,7 @@ campaign = {
name: 'Campaign'
levels: {}
}
levelURL = getURL('/db/level')
achievementURL = getURL('/db/achievement')
campaignURL = getURL('/db/campaign')
@ -47,7 +47,7 @@ describe '/db/campaign', ->
request.post {uri: achievementURL, json: achievement}, (err, res, body) ->
achievement = body
done()
it 'can create campaigns', (done) ->
for level in levels.reverse()
campaign.levels[level.original] = _.pick level, campaignLevelProperties
@ -55,7 +55,7 @@ describe '/db/campaign', ->
expect(res.statusCode).toBe(200)
campaign = body
done()
describe '/db/campaign/.../levels', ->
it 'fetches the levels in a campaign', (done) ->
url = getURL("/db/campaign/#{campaign._id}/levels")
@ -65,7 +65,7 @@ describe '/db/campaign/.../levels', ->
expect(body.length).toBe(2)
expect(_.difference(['level-1', 'level-2'],(level.slug for level in body)).length).toBe(0)
done()
describe '/db/campaign/.../achievements', ->
it 'fetches the achievements in the levels in a campaign', (done) ->
url = getURL("/db/campaign/#{campaign._id}/achievements")
@ -74,4 +74,3 @@ describe '/db/campaign/.../achievements', ->
body = JSON.parse(body)
expect(body.length).toBe(1)
done()