mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 10:06:08 -05:00
989 lines
29 KiB
CoffeeScript
989 lines
29 KiB
CoffeeScript
RootView = require 'views/kinds/RootView'
|
|
template = require 'templates/play/world-map-view'
|
|
LevelSession = require 'models/LevelSession'
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
AudioPlayer = require 'lib/AudioPlayer'
|
|
PlayLevelModal = require 'views/play/modal/PlayLevelModal'
|
|
ThangType = require 'models/ThangType'
|
|
MusicPlayer = require 'lib/surface/MusicPlayer'
|
|
storage = require 'lib/storage'
|
|
|
|
class LevelSessionsCollection extends CocoCollection
|
|
url: ''
|
|
model: LevelSession
|
|
|
|
constructor: (model) ->
|
|
super()
|
|
@url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID"
|
|
|
|
module.exports = class WorldMapView extends RootView
|
|
id: 'world-map-view'
|
|
template: template
|
|
|
|
events:
|
|
'click .map-background': 'onClickMap'
|
|
'click .level a': 'onClickLevel'
|
|
'click .level-info-container .start-level': 'onClickStartLevel'
|
|
'mouseenter .level a': 'onMouseEnterLevel'
|
|
'mouseleave .level a': 'onMouseLeaveLevel'
|
|
'mousemove .map': 'onMouseMoveMap'
|
|
'click #volume-button': 'onToggleVolume'
|
|
|
|
constructor: (options, @terrain) ->
|
|
@terrain ?= 'dungeon' # or 'forest'
|
|
super options
|
|
@nextLevel = @getQueryVariable 'next'
|
|
@levelStatusMap = {}
|
|
@levelPlayCountMap = {}
|
|
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model
|
|
@listenToOnce @sessions, 'sync', @onSessionsLoaded
|
|
@getLevelPlayCounts()
|
|
$(window).on 'resize', @onWindowResize
|
|
@playAmbientSound()
|
|
@probablyCachedMusic = storage.load("loaded-menu-music-#{@terrain}")
|
|
musicDelay = if @probablyCachedMusic then 1000 else 10000
|
|
@playMusicTimeout = _.delay (=> @playMusic() unless @destroyed), musicDelay
|
|
@preloadTopHeroes()
|
|
@hadEverChosenHero = me.get('heroConfig')?.thangType
|
|
window.tracker?.trackEvent 'World Map', Action: 'Loaded'
|
|
|
|
destroy: ->
|
|
$(window).off 'resize', @onWindowResize
|
|
if ambientSound = @ambientSound
|
|
# Doesn't seem to work; stops immediately.
|
|
createjs.Tween.get(ambientSound).to({volume: 0.0}, 1500).call -> ambientSound.stop()
|
|
@musicPlayer?.destroy()
|
|
clearTimeout @playMusicTimeout
|
|
super()
|
|
|
|
getLevelPlayCounts: ->
|
|
return unless me.isAdmin()
|
|
success = (levelPlayCounts) =>
|
|
return if @destroyed
|
|
for level in levelPlayCounts
|
|
@levelPlayCountMap[level._id] = playtime: level.playtime, sessions: level.sessions
|
|
@render() if @supermodel.finished()
|
|
|
|
levelIDs = []
|
|
for campaign in campaigns
|
|
for level in campaign.levels
|
|
levelIDs.push level.id
|
|
levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', {
|
|
url: '/db/level/-/play_counts'
|
|
data: {ids: levelIDs}
|
|
method: 'POST'
|
|
success: success
|
|
}, 0
|
|
levelPlayCountsRequest.load()
|
|
|
|
getRenderData: (context={}) ->
|
|
context = super(context)
|
|
context.campaign = _.find campaigns, { id: @terrain }
|
|
for level, index in context.campaign.levels
|
|
level.x ?= 10 + 80 * Math.random()
|
|
level.y ?= 10 + 80 * Math.random()
|
|
level.locked = index > 0 and not me.earnedLevel 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.color = 'rgb(255, 80, 60)'
|
|
if level.practice
|
|
level.color = 'rgb(80, 130, 200)' unless me.getBranchingGroup() is 'all-practice'
|
|
level.hidden = true if me.getBranchingGroup() is 'no-practice'
|
|
context.levelStatusMap = @levelStatusMap
|
|
context.levelPlayCountMap = @levelPlayCountMap
|
|
context.isIPadApp = application.isIPadApp
|
|
context.mapType = _.string.slugify @terrain
|
|
context.nextLevel = @nextLevel
|
|
context
|
|
|
|
afterRender: ->
|
|
super()
|
|
@onWindowResize()
|
|
unless application.isIPadApp
|
|
_.defer => @$el?.find('.game-controls .btn').tooltip() # Have to defer or i18n doesn't take effect.
|
|
@$el.find('.level').tooltip()
|
|
@$el.addClass _.string.slugify @terrain
|
|
@updateVolume()
|
|
@highlightElement '.level.next', delay: 2000, duration: 60000, rotation: 0, sides: ['top']
|
|
|
|
onSessionsLoaded: (e) ->
|
|
for session in @sessions.models
|
|
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
|
|
@render()
|
|
|
|
onClickMap: (e) ->
|
|
@$levelInfo?.hide()
|
|
# Easy-ish way of figuring out coordinates for placing level dots.
|
|
x = e.offsetX / @$el.find('.map-background').width()
|
|
y = (1 - e.offsetY / @$el.find('.map-background').height())
|
|
console.log " x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n"
|
|
|
|
onClickLevel: (e) ->
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
@$levelInfo?.hide()
|
|
return if $(e.target).attr('disabled') or $(e.target).parent().hasClass 'locked'
|
|
if application.isIPadApp
|
|
levelID = $(e.target).parents('.level').data('level-id')
|
|
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
|
|
@adjustLevelInfoPosition e
|
|
@endHighlight()
|
|
else
|
|
levelElement = $(e.target).parents('.level')
|
|
levelID = levelElement.data('level-id')
|
|
@startLevel levelElement
|
|
window.tracker?.trackEvent 'World Map', Action: 'Play Level', levelID: levelID
|
|
|
|
onClickStartLevel: (e) ->
|
|
@startLevel $(e.target).parents('.level-info-container')
|
|
|
|
startLevel: (levelElement) ->
|
|
playLevelModal = new PlayLevelModal supermodel: @supermodel, levelID: levelElement.data('level-id'), levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero
|
|
@openModalView playLevelModal
|
|
@$levelInfo?.hide()
|
|
|
|
onMouseEnterLevel: (e) ->
|
|
return if application.isIPadApp
|
|
levelID = $(e.target).parents('.level').data('level-id')
|
|
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
|
|
@adjustLevelInfoPosition e
|
|
@endHighlight()
|
|
|
|
onMouseLeaveLevel: (e) ->
|
|
return if application.isIPadApp
|
|
levelID = $(e.target).parents('.level').data('level-id')
|
|
@$el.find(".level-info-container[data-level-id='#{levelID}']").hide()
|
|
|
|
onMouseMoveMap: (e) ->
|
|
return if application.isIPadApp
|
|
@adjustLevelInfoPosition e
|
|
|
|
adjustLevelInfoPosition: (e) ->
|
|
return unless @$levelInfo
|
|
@$map ?= @$el.find('.map')
|
|
mapOffset = @$map.offset()
|
|
mapX = e.pageX - mapOffset.left
|
|
mapY = e.pageY - mapOffset.top
|
|
margin = 20
|
|
width = @$levelInfo.outerWidth()
|
|
@$levelInfo.css('left', Math.min(Math.max(margin, mapX - width / 2), @$map.width() - width - margin))
|
|
height = @$levelInfo.outerHeight()
|
|
top = mapY - @$levelInfo.outerHeight() - 60
|
|
if top < 20
|
|
top = mapY + 60
|
|
@$levelInfo.css('top', top)
|
|
|
|
onWindowResize: (e) =>
|
|
mapHeight = iPadHeight = 1536
|
|
mapWidth = if @terrain is 'dungeon' then 2350 else 2500
|
|
iPadWidth = 2048
|
|
aspectRatio = mapWidth / mapHeight
|
|
iPadAspectRatio = iPadWidth / iPadHeight
|
|
pageWidth = $(window).width()
|
|
pageHeight = $(window).height()
|
|
widthRatio = pageWidth / mapWidth
|
|
heightRatio = pageHeight / mapHeight
|
|
iPadWidthRatio = pageWidth / iPadWidth
|
|
if @terrain is 'dungeon'
|
|
# Make sure we can see almost the whole map, fading to background in one dimension.
|
|
if heightRatio <= iPadWidthRatio
|
|
# Full width, full height, left and right margin
|
|
resultingHeight = pageHeight
|
|
resultingWidth = resultingHeight * aspectRatio
|
|
else if iPadWidthRatio < heightRatio * (iPadAspectRatio / aspectRatio)
|
|
# Cropped width, full height, left and right margin
|
|
resultingWidth = pageWidth
|
|
resultingHeight = resultingWidth / aspectRatio
|
|
else
|
|
# Cropped width, full height, top and bottom margin
|
|
resultingWidth = pageWidth * aspectRatio / iPadAspectRatio
|
|
resultingHeight = resultingWidth / aspectRatio
|
|
else
|
|
# Scale it in either dimension so that we're always full on one of the dimensions.
|
|
if heightRatio > widthRatio
|
|
resultingHeight = pageHeight
|
|
resultingWidth = resultingHeight * aspectRatio
|
|
else
|
|
resultingWidth = pageWidth
|
|
resultingHeight = resultingWidth / aspectRatio
|
|
resultingMarginX = (pageWidth - resultingWidth) / 2
|
|
resultingMarginY = (pageHeight - resultingHeight) / 2
|
|
@$el.find('.map').css(width: resultingWidth, height: resultingHeight, 'margin-left': resultingMarginX, 'margin-top': resultingMarginY)
|
|
|
|
playAmbientSound: ->
|
|
return if @ambientSound
|
|
return unless file = {dungeon: 'ambient-dungeon', forest: 'ambient-map-grass'}[@terrain]
|
|
src = "/file/interface/#{file}#{AudioPlayer.ext}"
|
|
unless AudioPlayer.getStatus(src)?.loaded
|
|
AudioPlayer.preloadSound src
|
|
Backbone.Mediator.subscribeOnce 'audio-player:loaded', @playAmbientSound, @
|
|
return
|
|
@ambientSound = createjs.Sound.play src, loop: -1, volume: 0.1
|
|
createjs.Tween.get(@ambientSound).to({volume: 1.0}, 1000)
|
|
|
|
playMusic: ->
|
|
@musicPlayer = new MusicPlayer()
|
|
musicFile = {dungeon: '/music/music-menu-dungeon', forest: '/music/music-menu-grass'}[@terrain]
|
|
Backbone.Mediator.publish 'music-player:play-music', play: true, file: musicFile
|
|
storage.save("loaded-menu-music-#{@terrain}", true) unless @probablyCachedMusic
|
|
|
|
preloadTopHeroes: ->
|
|
for heroID in ['captain', 'knight']
|
|
url = "/db/thang.type/#{ThangType.heroes[heroID]}/version"
|
|
continue if @supermodel.getModel url
|
|
fullHero = new ThangType()
|
|
fullHero.setURL url
|
|
@supermodel.loadModel fullHero, 'thang'
|
|
|
|
updateVolume: (volume) ->
|
|
volume ?= me.get('volume') ? 1.0
|
|
classes = ['vol-off', 'vol-down', 'vol-up']
|
|
button = $('#volume-button', @$el)
|
|
button.toggleClass 'vol-off', volume <= 0.0
|
|
button.toggleClass 'vol-down', 0.0 < volume < 1.0
|
|
button.toggleClass 'vol-up', volume >= 1.0
|
|
createjs.Sound.setVolume(if volume is 1 then 0.6 else volume) # Quieter for now until individual sound FX controls work again.
|
|
if volume isnt me.get 'volume'
|
|
me.set 'volume', volume
|
|
me.patch()
|
|
|
|
onToggleVolume: (e) ->
|
|
button = $(e.target).closest('#volume-button')
|
|
classes = ['vol-off', 'vol-down', 'vol-up']
|
|
volumes = [0, 0.4, 1.0]
|
|
for oldClass, i in classes
|
|
if button.hasClass oldClass
|
|
newI = (i + 1) % classes.length
|
|
break
|
|
else if i is classes.length - 1 # no oldClass
|
|
newI = 2
|
|
@updateVolume volumes[newI]
|
|
|
|
|
|
tutorials = [
|
|
{
|
|
name: 'Rescue Mission'
|
|
difficulty: 1
|
|
id: 'rescue-mission'
|
|
image: '/file/db/level/52740644904ac0411700067c/rescue_mission_icon.png'
|
|
description: 'Tharin has been captured! Start here.'
|
|
x: 17.23
|
|
y: 36.94
|
|
}
|
|
{
|
|
name: 'Grab the Mushroom'
|
|
difficulty: 1
|
|
id: 'grab-the-mushroom'
|
|
image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png'
|
|
description: 'Grab a powerup and smash a big ogre.'
|
|
x: 22.6
|
|
y: 35.1
|
|
}
|
|
{
|
|
name: 'Drink Me'
|
|
difficulty: 1
|
|
id: 'drink-me'
|
|
image: '/file/db/level/525dc5589a0765e496000006/drink_me_icon.png'
|
|
description: 'Drink up and slay two munchkins.'
|
|
x: 27.74
|
|
y: 35.17
|
|
}
|
|
{
|
|
name: 'Taunt the Guards'
|
|
difficulty: 1
|
|
id: 'taunt-the-guards'
|
|
image: '/file/db/level/5276c9bdcf83207a2801ff8f/taunt_icon.png'
|
|
description: 'Tharin, if clever, can escape with Phoebe.'
|
|
x: 32.7
|
|
y: 36.7
|
|
}
|
|
{
|
|
name: 'It\'s a Trap'
|
|
difficulty: 1
|
|
id: 'its-a-trap'
|
|
image: '/file/db/level/528aea2d7f37fc4e0700016b/its_a_trap_icon.png'
|
|
description: 'Organize a dungeon ambush with archers.'
|
|
x: 37.6
|
|
y: 40.0
|
|
}
|
|
{
|
|
name: 'Break the Prison'
|
|
difficulty: 1
|
|
id: 'break-the-prison'
|
|
image: '/file/db/level/5275272c69abdcb12401216e/break_the_prison_icon.png'
|
|
description: 'More comrades are imprisoned!'
|
|
x: 44.1
|
|
y: 39.5
|
|
}
|
|
{
|
|
name: 'Taunt'
|
|
difficulty: 1
|
|
id: 'taunt'
|
|
image: '/file/db/level/525f150306e1ab0962000018/taunt_icon.png'
|
|
description: 'Taunt the ogre to claim victory.'
|
|
x: 38.5
|
|
y: 44.1
|
|
}
|
|
{
|
|
name: 'Cowardly Taunt'
|
|
difficulty: 1
|
|
id: 'cowardly-taunt'
|
|
image: '/file/db/level/525abfd9b12777d78e000009/cowardly_taunt_icon.png'
|
|
description: 'Lure infuriated ogres to their doom.'
|
|
x: 39.2
|
|
y: 50.1
|
|
}
|
|
{
|
|
name: 'Commanding Followers'
|
|
difficulty: 1
|
|
id: 'commanding-followers'
|
|
image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png'
|
|
description: 'Lead allied soldiers into battle.'
|
|
x: 39.1
|
|
y: 54.7
|
|
}
|
|
{
|
|
name: 'Mobile Artillery'
|
|
difficulty: 1
|
|
id: 'mobile-artillery'
|
|
image: '/file/db/level/525085419851b83f4b000001/mobile_artillery_icon.png'
|
|
description: 'Blow ogres up!'
|
|
x: 39.5
|
|
y: 60.2
|
|
}
|
|
]
|
|
|
|
experienced = [
|
|
{
|
|
name: 'Hunter Triplets'
|
|
difficulty: 2
|
|
id: 'hunter-triplets'
|
|
image: '/file/db/level/526711d9add4f8965f000002/hunter_triplets_icon.png'
|
|
description: 'Three soldiers go ogre hunting.'
|
|
x: 51.76
|
|
y: 35.5
|
|
}
|
|
{
|
|
name: 'Emphasis on Aim'
|
|
difficulty: 2
|
|
id: 'emphasis-on-aim'
|
|
image: '/file/db/level/525f384d96cd77000000000f/munchkin_masher_icon.png'
|
|
description: 'Choose your targets carefully.'
|
|
x: 61.47
|
|
y: 33.46
|
|
}
|
|
{
|
|
name: 'Zone of Danger'
|
|
difficulty: 3
|
|
id: 'zone-of-danger'
|
|
image: '/file/db/level/526ae95c1e5cd30000000008/zone_of_danger_icon.png'
|
|
description: 'Target the ogres swarming into arrow range.'
|
|
x: 65.72
|
|
y: 26.72
|
|
}
|
|
{
|
|
name: 'Molotov Medic'
|
|
difficulty: 2
|
|
id: 'molotov-medic'
|
|
image: '/file/db/level/52602ecb026e8481e7000001/generic_1.png'
|
|
description: 'Tharin must play support in this dungeon battle.'
|
|
x: 70.95
|
|
y: 18.64
|
|
}
|
|
{
|
|
name: 'Gridmancer'
|
|
difficulty: 5
|
|
id: 'gridmancer'
|
|
image: '/file/db/level/52ae2460ef42c52f13000008/gridmancer_icon.png'
|
|
description: 'Super algorithm challenge level!'
|
|
x: 61.41
|
|
y: 17.22
|
|
}
|
|
]
|
|
|
|
arenas = [
|
|
{
|
|
name: 'Criss-Cross'
|
|
difficulty: 5
|
|
id: 'criss-cross'
|
|
image: '/file/db/level/528aea2d7f37fc4e0700016b/its_a_trap_icon.png'
|
|
description: 'Participate in a bidding war with opponents to reach the other side!'
|
|
levelPath: 'ladder'
|
|
x: 49.43
|
|
y: 21.48
|
|
}
|
|
{
|
|
name: 'Greed'
|
|
difficulty: 4
|
|
id: 'greed'
|
|
image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png'
|
|
description: 'Liked Dungeon Arena and Gold Rush? Put them together in this economic arena!'
|
|
levelPath: 'ladder'
|
|
x: 45.00
|
|
y: 23.34
|
|
}
|
|
{
|
|
name: 'Dungeon Arena'
|
|
difficulty: 3
|
|
id: 'dungeon-arena'
|
|
image: '/file/db/level/526ae95c1e5cd30000000008/zone_of_danger_icon.png'
|
|
description: 'Play head-to-head against fellow Wizards in a dungeon melee!'
|
|
levelPath: 'ladder'
|
|
x: 36.82
|
|
y: 23.17
|
|
}
|
|
{
|
|
name: 'Gold Rush'
|
|
difficulty: 3
|
|
id: 'gold-rush'
|
|
image: '/file/db/level/52602ecb026e8481e7000001/generic_1.png'
|
|
description: 'Prove you are better at collecting gold than your opponent!'
|
|
levelPath: 'ladder'
|
|
x: 30.8
|
|
y: 16.87
|
|
}
|
|
{
|
|
name: 'Brawlwood'
|
|
difficulty: 4
|
|
id: 'brawlwood'
|
|
image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png'
|
|
description: 'Combat the armies of other Wizards in a strategic forest arena! (Fast computer required.)'
|
|
levelPath: 'ladder'
|
|
x: 41.93
|
|
y: 12.79
|
|
}
|
|
{
|
|
name: 'Sky Span (Testing)'
|
|
difficulty: 3
|
|
id: 'sky-span'
|
|
image: '/file/db/level/526ae95c1e5cd30000000008/zone_of_danger_icon.png'
|
|
description: 'Preview version of an upgraded Dungeon Arena. Help us with hero balance before release!'
|
|
levelPath: 'ladder'
|
|
x: 53.12
|
|
y: 11.37
|
|
}
|
|
]
|
|
|
|
classicAlgorithms = [
|
|
{
|
|
name: 'Bubble Sort Bootcamp Battle'
|
|
difficulty: 3
|
|
id: 'bubble-sort-bootcamp-battle'
|
|
image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png'
|
|
description: 'Write a bubble sort to organize your soldiers. - by Alexandru Caciulescu'
|
|
x: 26.37
|
|
y: 93.02
|
|
}
|
|
{
|
|
name: 'Ogres of Hanoi'
|
|
difficulty: 3
|
|
id: 'ogres-of-hanoi'
|
|
image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png'
|
|
description: 'Transfer a stack of ogres while preserving their honor. - by Alexandru Caciulescu'
|
|
x: 32.39
|
|
y: 92.67
|
|
}
|
|
{
|
|
name: 'Danger! Minefield'
|
|
difficulty: 3
|
|
id: 'danger-minefield'
|
|
image: '/file/db/level/526bda3fe79aefde2a003e36/mobile_artillery_icon.png'
|
|
description: 'Learn how to find prime numbers while defusing mines! - by Alexandru Caciulescu'
|
|
x: 38.07
|
|
y: 92.76
|
|
}
|
|
{
|
|
name: 'K-means++ Cluster Wars'
|
|
difficulty: 4
|
|
id: 'k-means-cluster-wars'
|
|
image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png'
|
|
description: 'Learn cluster analysis while leading armies into battle! - by Alexandru Caciulescu'
|
|
x: 43.75
|
|
y: 90.36
|
|
}
|
|
{
|
|
name: 'Quicksort the Spiral'
|
|
difficulty: 3
|
|
id: 'quicksort-the-spiral'
|
|
image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png'
|
|
description: 'Learn Quicksort while sorting a spiral of ogres! - by Alexandru Caciulescu'
|
|
x: 48.97
|
|
y: 87.08
|
|
}
|
|
{
|
|
name: 'Minimax Tic-Tac-Toe'
|
|
difficulty: 4
|
|
id: 'minimax-tic-tac-toe'
|
|
image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png'
|
|
description: 'Learn how to make a game AI with the Minimax algorithm. - by Alexandru Caciulescu'
|
|
x: 55.96
|
|
y: 82.73
|
|
}
|
|
]
|
|
|
|
playerCreated = [
|
|
{
|
|
name: 'Extra Extrapolation'
|
|
difficulty: 2
|
|
id: 'extra-extrapolation'
|
|
image: '/file/db/level/526bda3fe79aefde2a003e36/mobile_artillery_icon.png'
|
|
description: 'Predict your target\'s position for deadly aim. - by Sootn'
|
|
x: 42.67
|
|
y: 67.98
|
|
}
|
|
{
|
|
name: 'The Right Route'
|
|
difficulty: 1
|
|
id: 'the-right-route'
|
|
image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png'
|
|
description: 'Strike at the weak point in an array of enemies. - by Aftermath'
|
|
x: 47.38
|
|
y: 70.55
|
|
}
|
|
{
|
|
name: 'Sword Loop'
|
|
difficulty: 2
|
|
id: 'sword-loop'
|
|
image: '/file/db/level/525dc5589a0765e496000006/drink_me_icon.png'
|
|
description: 'Kill the ogres and save the peasants with for-loops. - by Prabh Simran Singh Baweja'
|
|
x: 52.66
|
|
y: 69.66
|
|
}
|
|
{
|
|
name: 'Coin Mania'
|
|
difficulty: 2
|
|
id: 'coin-mania'
|
|
image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png'
|
|
description: 'Learn while-loops to grab coins and potions. - by Prabh Simran Singh Baweja'
|
|
x: 58.46
|
|
y: 66.38
|
|
}
|
|
{
|
|
name: 'Find the Spy'
|
|
difficulty: 2
|
|
id: 'find-the-spy'
|
|
image: '/file/db/level/526ae95c1e5cd30000000008/zone_of_danger_icon.png'
|
|
description: 'Identify the spies hidden among your soldiers - by Nathan Gossett'
|
|
x: 63.11
|
|
y: 62.74
|
|
}
|
|
{
|
|
name: 'Harvest Time'
|
|
difficulty: 2
|
|
id: 'harvest-time'
|
|
image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png'
|
|
description: 'Collect a hundred mushrooms in just five lines of code - by Nathan Gossett'
|
|
x: 69.19
|
|
y: 60.61
|
|
}
|
|
{
|
|
name: 'Guide Everyone Home'
|
|
difficulty: 2
|
|
id: 'guide-everyone-home'
|
|
image: '/file/db/level/52740644904ac0411700067c/rescue_mission_icon.png'
|
|
description: 'Fetch the wizards teleporting into the area - by Nathan Gossett'
|
|
x: 77.54
|
|
y: 65.94
|
|
}
|
|
{
|
|
name: "Let's go Fly a Kite"
|
|
difficulty: 3
|
|
id: 'lets-go-fly-a-kite'
|
|
image: '/file/db/level/526711d9add4f8965f000002/hunter_triplets_icon.png'
|
|
description: 'There is a horde of ogres marching on your village. Stay out of reach and use your bow to take them out! - by Danny Whittaker'
|
|
x: 84.29
|
|
y: 61.23
|
|
}
|
|
]
|
|
|
|
dungeon = [
|
|
{
|
|
name: 'Dungeons of Kithgard'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'dungeons-of-kithgard'
|
|
original: '528110f30268d018e3000001'
|
|
description: 'Grab the gem, but touch nothing else. Start here.'
|
|
x: 14
|
|
y: 15.5
|
|
nextLevels:
|
|
continue: 'gems-in-the-deep'
|
|
skip_ahead: 'shadow-guard'
|
|
}
|
|
{
|
|
name: 'Gems in the Deep'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'gems-in-the-deep'
|
|
original: '54173c90844506ae0195a0b4'
|
|
description: 'Quickly collect the gems; you will need them.'
|
|
x: 32
|
|
y: 15.5
|
|
nextLevels:
|
|
more_practice: 'gem-grabber'
|
|
continue: 'shadow-guard'
|
|
skip_ahead: 'true-names'
|
|
}
|
|
{
|
|
name: 'Gem Grabber'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'gem-grabber'
|
|
original: '54174347844506ae0195a0b8'
|
|
description: 'Grab even more gems as you practice moving.'
|
|
x: 35.49
|
|
y: 24.61
|
|
nextLevels:
|
|
continue: 'shadow-guard'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'Shadow Guard'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'shadow-guard'
|
|
original: '54174347844506ae0195a0b8'
|
|
description: 'Evade the Kithgard minion.'
|
|
x: 54
|
|
y: 9
|
|
nextLevels:
|
|
more_practice: 'munchkin-dodger'
|
|
continue: 'true-names'
|
|
}
|
|
{
|
|
name: 'Munchkin Dodger'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'munchkin-dodger'
|
|
original: '541875da4c16460000ab990f'
|
|
description: 'Practice your evasion skills with more guards.'
|
|
x: 61.19
|
|
y: 13.80
|
|
nextLevels:
|
|
continue: 'true-names'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'True Names'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'true-names'
|
|
original: '541875da4c16460000ab990f'
|
|
description: 'Learn an enemy\'s true name to defeat it.'
|
|
x: 74
|
|
y: 12
|
|
nextLevels:
|
|
more_practice: 'munchkin-slayer'
|
|
continue: 'the-raised-sword'
|
|
}
|
|
{
|
|
name: 'Munchkin Slayer'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'munchkin-slayer'
|
|
original: '5418aec24c16460000ab9aa6'
|
|
description: 'Test out your battle skills by defeating more munchkins.'
|
|
x: 80.85
|
|
y: 11.85
|
|
nextLevels:
|
|
continue: 'the-raised-sword'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'The Raised Sword'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'the-raised-sword'
|
|
original: '5418aec24c16460000ab9aa6'
|
|
description: 'Learn to equip yourself for combat.'
|
|
x: 85
|
|
y: 20
|
|
nextLevels:
|
|
continue: 'the-first-kithmaze'
|
|
}
|
|
{
|
|
name: 'The First Kithmaze'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'the-first-kithmaze'
|
|
original: '5418b9d64c16460000ab9ab4'
|
|
description: 'The builders of Kith constructed many mazes to confuse travelers.'
|
|
x: 70
|
|
y: 28
|
|
nextLevels:
|
|
more_practice: 'the-one-point-fifth-kithmaze'
|
|
continue: 'the-second-kithmaze'
|
|
skip_ahead: 'new-sight'
|
|
}
|
|
{
|
|
name: 'The One-Point-Fifth Kithmaze'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'the-one-point-fifth-kithmaze'
|
|
original: '5418cf256bae62f707c7e1c3'
|
|
description: 'Another day, another maze.'
|
|
x: 78.47
|
|
y: 34.38
|
|
nextLevels:
|
|
continue: 'the-second-kithmaze'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'The Second Kithmaze'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'the-second-kithmaze'
|
|
original: '5418cf256bae62f707c7e1c3'
|
|
description: 'Many have tried, few have found their way through this maze.'
|
|
x: 55.54
|
|
y: 26.96
|
|
nextLevels:
|
|
more_practice: 'the-two-point-fifth-kithmaze'
|
|
continue: 'new-sight'
|
|
}
|
|
{
|
|
name: 'The Two-Point-Fifth Kithmaze'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'the-two-point-fifth-kithmaze'
|
|
original: '5418d40f4c16460000ab9ac2'
|
|
description: 'You must really like doing these mazes!'
|
|
x: 49.02
|
|
y: 25.78
|
|
nextLevels:
|
|
continue: 'new-sight'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'New Sight'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'new-sight'
|
|
original: '5418d40f4c16460000ab9ac2'
|
|
description: 'A true name can only be seen with the correct lenses.'
|
|
x: 67
|
|
y: 41
|
|
nextLevels:
|
|
continue: 'lowly-kithmen'
|
|
}
|
|
{
|
|
name: 'Lowly Kithmen'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'lowly-kithmen'
|
|
original: '541b24511ccc8eaae19f3c1f'
|
|
description: 'Use your glasses to seek out and attack the Kithmen.'
|
|
x: 74
|
|
y: 48
|
|
nextLevels:
|
|
more_practice: 'still-pretty-low-kithmen'
|
|
continue: 'closing-the-distance'
|
|
skip_ahead: 'the-final-kithmaze'
|
|
}
|
|
{
|
|
name: 'Still-Pretty-Low Kithmen'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'still-pretty-low-kithmen'
|
|
original: '541b288e1ccc8eaae19f3c25'
|
|
description: 'Now that you can see them, they\'re everywhere!'
|
|
x: 80.17
|
|
y: 45.31
|
|
nextLevels:
|
|
continue: 'closing-the-distance'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'Closing the Distance'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'closing-the-distance'
|
|
original: '541b288e1ccc8eaae19f3c25'
|
|
description: 'Kithmen are not the only ones to stand in your way.'
|
|
x: 76
|
|
y: 60
|
|
nextLevels:
|
|
more_practice: 'assassinating-more-kithmen'
|
|
continue: 'the-final-kithmaze'
|
|
}
|
|
{
|
|
name: 'Assassinating More Kithmen'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'assassinating-more-kithmen'
|
|
original: '541b434e1ccc8eaae19f3c33'
|
|
description: 'They\'re, uh, coming right for us?'
|
|
x: 80.34
|
|
y: 55.60
|
|
nextLevels:
|
|
continue: 'the-final-kithmaze'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'The Final Kithmaze'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'the-final-kithmaze'
|
|
original: '541b434e1ccc8eaae19f3c33'
|
|
description: 'To escape you must find your way through an Elder Kithman\'s maze.'
|
|
x: 82
|
|
y: 70
|
|
nextLevels:
|
|
more_practice: 'okay-one-more-kithmaze'
|
|
continue: 'kithgard-gates'
|
|
}
|
|
{
|
|
name: 'Okay, One More Kithmaze'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'okay-one-more-kithmaze'
|
|
original: '541c9a30c6362edfb0f34479'
|
|
description: 'But you really gotta get outside after this, okay?'
|
|
x: 76.94
|
|
y: 74.22
|
|
nextLevels:
|
|
continue: 'kithgard-gates'
|
|
practice: true
|
|
disabled: true
|
|
}
|
|
{
|
|
name: 'Kithgard Gates'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
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'
|
|
difficulty: 1
|
|
id: 'cavern-survival'
|
|
original: '544437e0645c0c0000c3291d'
|
|
description: 'Stay alive longer than your opponent amidst hordes of ogres!'
|
|
disabled: not me.isAdmin()
|
|
x: 17.54
|
|
y: 78.39
|
|
}
|
|
]
|
|
|
|
forest = [
|
|
{
|
|
name: 'Defense of Plainswood'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'defense-of-plainswood'
|
|
original: '541b67f71ccc8eaae19f3c62'
|
|
description: 'Protect the peasants from the pursuing ogres.'
|
|
x: 29.63
|
|
y: 53.69
|
|
}
|
|
{
|
|
name: 'Winding Trail'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'winding-trail'
|
|
original: '5446cb40ce01c23e05ecf027'
|
|
description: 'Stay alive and navigate through the forest.'
|
|
x: 39.03
|
|
y: 54.97
|
|
}
|
|
{
|
|
name: 'Thornbush Farm'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'thornbush-farm'
|
|
original: '5447030525cce60000745e2a'
|
|
description: 'Determine refugee peasant from ogre when defending the farm.'
|
|
x: 44.09
|
|
y: 57.75
|
|
}
|
|
{
|
|
name: 'A Fiery Trap'
|
|
type: 'hero'
|
|
difficulty: 1
|
|
id: 'a-fiery-trap'
|
|
original: '5448330517d7283e051f9b9e'
|
|
description: 'Patrol the village entrances, but stay defensive.'
|
|
disabled: true
|
|
x: 40.14
|
|
y: 63.96
|
|
}
|
|
#{
|
|
# name: ''
|
|
# type: 'hero'
|
|
# difficulty: 1
|
|
# id: ''
|
|
# description: ''
|
|
# x: 58.46
|
|
# y: 66.38
|
|
# }
|
|
#{
|
|
# name: ''
|
|
# type: 'hero'
|
|
# difficulty: 1
|
|
# id: ''
|
|
# description: ''
|
|
# x: 63.11
|
|
# y: 62.74
|
|
# }
|
|
#{
|
|
# name: ''
|
|
# type: 'hero'
|
|
# difficulty: 1
|
|
# id: ''
|
|
# description: ''
|
|
# x: 69.19
|
|
# y: 60.61
|
|
# }
|
|
#{
|
|
# name: ''
|
|
# type: 'hero'
|
|
# difficulty: 1
|
|
# id: ''
|
|
# description: ''
|
|
# x: 77.54
|
|
# y: 65.94
|
|
#}
|
|
#{
|
|
# name: ''
|
|
# type: 'hero'
|
|
# difficulty: 1
|
|
# id: ''
|
|
# description: ''
|
|
# x: 84.29
|
|
# y: 61.23
|
|
#}
|
|
{
|
|
name: 'Dueling Grounds'
|
|
type: 'hero-ladder'
|
|
difficulty: 1
|
|
id: 'dueling-grounds'
|
|
original: '5442ba0e1e835500007eb1c7'
|
|
description: 'Battle head-to-head against another hero in this basic beginner combat arena.'
|
|
disabled: not me.isAdmin()
|
|
x: 25.5
|
|
y: 77.5
|
|
}
|
|
]
|
|
|
|
WorldMapView.campaigns = campaigns = [
|
|
#{id: 'beginner', name: 'Beginner Campaign', description: '... in which you learn the wizardry of programming.', levels: tutorials, color: "rgb(255, 80, 60)"}
|
|
#{id: 'multiplayer', name: 'Multiplayer Arenas', description: '... in which you code head-to-head against other players.', levels: arenas, color: "rgb(80, 5, 60)"}
|
|
#{id: 'dev', name: 'Random Harder Levels', description: '... in which you learn the interface while doing something a little harder.', levels: experienced, color: "rgb(80, 60, 255)"}
|
|
#{id: 'classic_algorithms' ,name: 'Classic Algorithms', description: '... in which you learn the most popular algorithms in Computer Science.', levels: classicAlgorithms, color: "rgb(110, 80, 120)"}
|
|
#{id: 'player_created', name: 'Player-Created', description: '... in which you battle against the creativity of your fellow <a href=\"/contribute#artisan\">Artisan Wizards</a>.', levels: playerCreated, color: "rgb(160, 160, 180)"}
|
|
{id: 'dungeon', name: 'Dungeon Campaign', levels: dungeon }
|
|
{id: 'forest', name: 'Forest Campaign', levels: forest }
|
|
]
|