1
0
mirror of https://gitlab.com/Deukhoofd/BattleSim.git synced 2025-10-28 02:00:04 +00:00

Lots of stuff

This commit is contained in:
Deukhoofd
2016-02-01 23:19:30 +01:00
commit d7316d5799
6681 changed files with 527969 additions and 0 deletions

538
client/app/css/battle.styl Normal file
View File

@@ -0,0 +1,538 @@
outline-text(clr = #000)
text-shadow 0 1px 0 clr, 0 -1px 0 clr,
-1px 0 0 clr, 1px 0 0 clr
divide(a, b)
a / b
create-projectile(clr)
trans-clr = rgba(black, 0)
background radial-gradient(ellipse at center, lighten(clr, 50%) 0%, clr 50%, trans-clr 51%, trans-clr 100%)
$padding = 10px
$battle-width = 600px
$battle-height = 300px
$pane-width = $battle-width + $padding + $padding
$screen-width = 400px
$side-width = ($battle-width - $screen-width) / 2
.battle_window
.chat
left $pane-width
.battle
position absolute
top 0
left 0
bottom 0
width $pane-width
box-sizing border-box
padding $padding
h2
font-size 0.6875em
text-transform uppercase
letter-spacing 1px
border-bottom 1px solid rgba(0, 0, 0, .10)
line-height normal
.battle_container
position relative
width $battle-width
height $battle-height
narrow-font()
&.battle_bg_0
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg.png")
&.battle_bg_1
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_1.png")
&.battle_bg_2
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_2.png")
&.battle_bg_3
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_3.png")
&.battle_bg_4
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_4.png")
&.battle_bg_5
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_5.png")
.battle_user_info
.pokemon_icons
margin 0 auto
width 80px
.icon_wrapper
width 40px
float left
&:nth-child(even)
position relative
top 6px
.fill-left, .fill-right
width $side-width
.left
bottom 20px
left 0
.right
top 20px
right 0
.left, .right
position absolute
width $side-width
color #fff
text-align center
outline-text()
font-size 13px
.icon_wrapper
position relative
&:hover
border-radius 5px
background-color rgba(187, 184, 248, .5)
.pokemon_hp_background
position absolute
right 6px
bottom 2px
width 4px
height 3px
border 1px solid rgba(32, 32, 32, .4)
background rgba(32, 32, 32, .4)
pointer-events none
.pokemon_hp
absolute bottom left
right 0
&.green
background #0f0
&.yellow
background #ff0
&.red
background #f00
.battle-timer
font-size 2em
&.battle-timer-low
color #f33
&.battle-timer-small
font-size 1em
&:before
content: '('
&:after
content: ')'
.battle_overlays
position absolute
top 0
left 0
right 0
bottom 0
pointer-events none
overflow hidden
.battle_overlay
position absolute
top 0
left 0
right 0
bottom 0
&.weather
&.rain
background rgba(0, 48, 196, .3)
&.hail
background rgba(128, 255, 255, .3)
&.sand
background linear-gradient(top, rgba(128, 64, 0, .3), rgba(64, 32, 48, .5))
&.sun
background rgba(255, 196, 64, .3)
.battle_pane
position absolute
width $screen-width
height $battle-height
top 0
left $side-width
.pokemon
&.bottom
.pokemon-info
bottom 35%
left 200px
.sprite
&.fade
position absolute
left -20px
opacity .3
&.top
.pokemon-info
top 20%
right 238px
.sprite
&.fade
position absolute
left 20px
opacity .3
.pokemon-info
position absolute
width 144px
height 6px
border 1px solid #fff
border-radius 2px
box-shadow 0 0 0 1px #000
background rgba(0, 8, 32, .5)
.hp, .hp-gradient
position absolute
top 0
left 0
bottom 0
width 100%
transition width .4s cubic-bezier(0, 1, .5, 1)
.hp-gradient
background linear-gradient(top, transparent 40%, rgba(0,32,64,0.3) 60%)
.hp
background #0f0
.hp-text
position absolute
top -1px
bottom @top
left 100%
background #333
color #fff
font-size 8px
line-height @font-size
padding 0 4px
border-radius 0 2px 2px 0
box-shadow 0 0 0 1px #222
font-family Verdana, sans-serif
.pokemon-meta
position absolute
left 0
bottom 100%
color #fff
pointer-events visible
outline-text()
.pokemon-name
font-weight 700
color #fff
.pokemon-level, .gender
font-size 75%
.pokemon-level-text
color #EFA532
.pokemon-effects
position absolute
top 9px
color #fff
normal-font()
.pokemon-effect
font-size .625em
float left
border-radius 3px
background #333
color #fff
padding 0 4px
border 1px solid rgba(0, 0, 0, .5)
&.sleep
background #535
&.burn
background #c33
&.freeze
background #38b
&.paralyze
background #b93
&.poison, &.toxic
background #b3b
&.boost
background #5a3
&.negative
background #a53
.sprite
position absolute
top 0
left 0
pointer-events visible
transition left 1s ease
img
position absolute
max-width none
.pokemon
position absolute
width 100%
height 100%
top 0
left 0
pointer-events none
overflow hidden
.pokeball
position absolute
background url("//media.pokebattle.com/img/battle/pokeballs.gif") -133px -10px
transition opacity .25s ease-in
.percentage
font-family "Helvetica Neue", sans-serif
font-weight 700
padding 4px
border-radius 3px
background #4e4
transition left 5s linear
color #fff
border 1px solid rgba(0, 0, 0, .3)
box-shadow 2px 2px 2px rgba(0, 0, 0, .3)
&.red
background #e44
.team-stealth-rock
position absolute
background url("//media.pokebattle.com/img/battle/stealth_rock.png")
width 28px
height 35px
pointer-events none
.team-toxic-spikes
position absolute
background url("//media.pokebattle.com/img/battle/toxic_spikes.png")
width 21px
height 21px
pointer-events none
.team-spikes
position absolute
background url("//media.pokebattle.com/img/battle/spikes.png")
width 21px
height 21px
pointer-events none
.team-sticky-web
position absolute
background url("//media.pokebattle.com/img/battle/sticky_web.png")
width 150px
height 151px
pointer-events none
.team-screen
position absolute
opacity .5
width 1px
height @width
pointer-events none
&.rounded
border-radius 100%
for $pair in ('blue' $water) ('yellow' $electric) ('pink' $fairy)
&.{$pair[0]}
border 1px solid darken($pair[1], 20%)
background-color $pair[1]
.projectile
position absolute
width 64px
height @width
pointer-events none
for $array in $types
&.{$array[0]}
create-projectile($array[1])
.battle_summary
position absolute
bottom 0
left $side-width
right @left
background rgba(black, .5)
color white
display none
text-shadow none
padding 4px 10px
font-size .875em
p
margin 0
font-size .8125em
&.big
font-size 1em
margin-top 4px
&:first-child
margin-top 0
.ability_activation
position absolute
width 150px
background rgba(black, .5)
text-align center
opacity 0
color white
outline-text()
pointer-events none
$radius = 4px
strong
font-size 1.2em
display block
&.front
border-radius $radius 0 0 $radius
top 64px
right 0
&.back
border-radius 0 $radius $radius 0
bottom 64px
left 0
.moves
.button
display block
float none
margin 0 0 5px
&.hidden
display none
.switches
.button
display block
float none
&.disabled
.pokemon_icon
opacity .5
.pokemon_icon
display block
.mega-evolve
text-align center
font-size .8em
.gender
&.gender_female
color #EA597A
&.gender_male
color #62CBB0
.team_icons
width 6 * 40px
.pokemon_icon
position relative
width 32px
height 32px
display inline-block
margin 0 auto
background url("//media.pokebattle.com/img/pokemon_icons_40px.png") top left
vertical-align middle
&.fainted
opacity 0.5
.battle_teams
position absolute
left $side-width
right $side-width
text-align center
margin 0 auto
width 300px
normal-font()
li
position relative
.gender
position absolute
top 0
right 2px
font-size .75em
text-shadow 0 1px 0 #333
.level
position absolute
bottom 0
right 2px
font-size .75em
line-height @font-size
outline-text(#fff)
narrow-font()
.battle_team_preview
background #eee
padding 10px
border-radius 4px
margin 10px 0
border 1px solid #aaa
.px_40, .sortable-placeholder
width 40px
margin 0 2px
padding-left 0
padding-right 0
float left
.submit_arrangement
margin 10px 0 0
.team_pokemon
position relative
width 40px
height 32px
margin 0 auto
.arrange_team
user-select none
li
cursor pointer
border-radius 4px
&.img-polaroid.active
background-color #ccc
.lead_text
font-size .8125em
min-height 0
.pokemon_icon
top 0
left 0
.sortable-placeholder
border 1px dashed #ccc
background none
height 40px
.unstyled
margin 0
.well-battle-actions
font-size 1.5em
height 3em
line-height @height
text-align center
.save-log
display block
text-align center
.drop
background linear-gradient(top, rgba($water, .7), rgba(255, 255, 255, 0.3))
width 1px
height 89px
position absolute
top 0
transform rotate(-20deg)
animation fall .63s linear infinite
@keyframes fall
to
margin-top 2 * $battle-height
margin-left 2 * $battle-height * .36
.ray
position absolute
background rgba(230, 192, 70, .5)
width ($battle-width / 10)
height 100%
transition all 1000ms ease
transform rotate(360deg) skewX(30deg)
animation fade-in-out 3s ease infinite
@keyframes fade-in-out
0%, 100%
opacity 0
50%
opacity .3
.sand_overlay
absolute top left
background url("//media.pokebattle.com/img/battle/weather_sand.png")
width 600px
height 600px
opacity .5
animation sandstorm .75s linear infinite
@keyframes sandstorm
0%
transform translate(0, 0)
100%
transform translate(200%, 100%)
.hailstone
position absolute
border-radius 4px
width 10px
height @width
background rgba(230, 200, 255, .5)
animation fall .4s linear infinite

View File

@@ -0,0 +1,59 @@
.battle-list
position absolute
top 0
bottom 0
left 0
right 0
overflow auto
h2
display block
margin-left 10px
.battle-list-collection
padding-right 10px
.empty
margin-left 10px
.battle-list-item-wrapper
float left
width 100%
padding-left 10px
padding-bottom 10px
box-sizing border-box
.battle-list-item
cursor pointer
border 1px solid #DDD
background #F5F5F5
box-shadow 1px 2px 0 rgba(0, 0, 0, 0.2)
padding 10px
&:hover
background: #FFF
.vs
font-weight bold
margin 0 5px
// Normally, our media queries use max-width to scale down, but for this page we want to scale up
@media screen and (min-width: 1000px)
.battle-list-item-wrapper
width 50%
@media screen and (min-width: 1400px)
.battle-list-item-wrapper
width 33%
@media screen and (min-width: 1800px)
.battle-list-item-wrapper
width 25%
@media screen and (min-width: 2200px)
.battle-list-item-wrapper
width 20%
@media screen and (min-width: 2600px)
.battle-list-item-wrapper
width 16.6%

100
client/app/css/buttons.styl Normal file
View File

@@ -0,0 +1,100 @@
button-bg(clr)
clr = lighten(clr, 50%)
light = lighten(clr, 30%)
dark-clr = darken(clr, 20%)
darker-clr = darken(clr, 50%)
darkest-clr = darken(darker-clr, 20%)
btn-clr = darken(darkest-clr, 30%)
background linear-gradient(top, clr, dark-clr)
border 1px solid darker-clr
border-top-color rgba(0, 0, 0, .1)
border-left-color rgba(0, 0, 0, .2)
border-right-color @border-left-color
border-bottom-color rgba(0, 0, 0, .3)
color btn-clr
&:hover
background linear-gradient(top, light, clr)
color lighten(btn-clr, 20%)
text-shadow 0 -1px 0 rgba(255, 255, 255, .10)
&:active, &.pressed
color darken(btn-clr, 50%)
text-shadow none
border-color darkest-clr
&.pressed
background darker-clr
&:active
background darkest-clr
.button
clearfix()
button-bg(#eee)
display inline-block
height 32px
line-height @height
border-radius 4px
margin-bottom 6px
padding 0 10px
cursor pointer
color #333
text-shadow 0 -1px 0 rgba(255, 255, 255, .25)
box-shadow inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.25)
position relative
&.block
display block
&:hover
box-shadow inset 0 1px 0 rgba(255,255,255,.125), 0 1px 2px rgba(0,0,0,.125)
.main_text
narrow-font()
font-weight 700
float left
.meta_info
font-size 12px
float right
font-family "Helvetica Neue", sans-serif
&.big
font-size 1.5em
height 3em
line-height @height
text-align center
.main_text
float none
.meta_info
position absolute
top 0
right 1em
&.button_blue
background linear-gradient(top, #0066cc, #0044cc)
text-shadow 0 -1px rgba(0, 0, 0, .3)
color #fff
&:hover
background linear-gradient(top, #0088cc, #0066cc)
&.button_red
background linear-gradient(top, #cc6600, #cc4400)
text-shadow 0 -1px rgba(0, 0, 0, .3)
color #fff
&:hover
background linear-gradient(top, #cc8800, #cc6600)
for $array in $types
&.{$array[0]}
button-bg($array[1])
&.disabled, &.disabled:hover
background #333
border-color #000
cursor default
color #666
text-shadow none
box-shadow none
.hover_info_moves
.hover_info
width 196px
display block
margin 0
&:first-child
border-radius 4px 4px 0 0
&:last-child
border-radius 0 0 4px 4px
border-top none
&:not(:first-child):not(:last-child)
border-radius 0
border-top none

100
client/app/css/chat.styl Normal file
View File

@@ -0,0 +1,100 @@
.chat_window .chat
left 350px
.chat
position absolute
top 0
right 0
bottom 0
&.without_spectators
.message_pane
right 0
.user_list
display none
&.without_chat_input
.messages
bottom 0
.chat_input_pane
display none
.user_list
position absolute
top 0
right 0
bottom 0
width 25%
overflow-y auto
p
padding 2px 6px
margin 0
ul
margin 0
padding 0
list-style none
li
padding 2px 6px
&:hover
background #fb7
.message_pane
position absolute
top 0
left 0
right 25%
bottom 0
box-shadow 0 0 5px rgba(0, 8, 16, .2)
.messages
overflow-y auto
position absolute
top 0
left 0
right 0
bottom 30px
border-left 1px solid rgba(0, 0, 0, .2)
border-right @border-left
background white
word-wrap break-word
h2, h3, p
padding 0 16px 2px
p
font-size .875em
text-rendering optimizeLegibility
&.move_message
font-size 1em
margin-top 10px
a.pokemon-link
color inherit
.alert
margin 0.75em 16px
.chat_input_pane
position absolute
width 100%
bottom 0
height 30px
.chat_input_wrapper
overflow: hidden // Make it take the remainder of the space not used by the button
.chat_input
box-sizing border-box
border 2px solid #384035
padding 2px 6px
margin 0
width 100%
height 30px
border-radius 2px
&:hover
border-color #5D736B
&:focus
border-color #F27405
outline none
.chat_input_send
box-sizing border-box
float right
margin 0
height 30px

View File

@@ -0,0 +1,54 @@
$body-color = rgba(223, 231, 232, .5)
$background-color = #f3f3f3
$positive-color = #38AB46
$negative-color = #DC4E48
$off-white = #f5f5f5
$grey = #999
$yellow = #b92
$normal = #A9A57A
$fire = #E28544
$water = #6A97ED
$electric = #F0C944
$grass = #8FC052
$ice = #A2CFD0
$fighting = #B04137
$poison = #9253A0
$ground = #DCBE6F
$flying = #A09AEE
$psychic = #E0698B
$bug = #AEB22D
$rock = #B69D42
$ghost = #6A5F97
$dragon = #5C59F1
$dark = #685447
$steel = #B7BACF
$fairy = #F7B5F7
$types = ('normal' $normal) ('fire' $fire) ('water' $water) ('electric' $electric) ('grass' $grass) ('ice' $ice) ('fighting' $fighting) ('poison' $poison) ('ground' $ground) ('flying' $flying) ('psychic' $psychic) ('bug' $bug) ('rock' $rock) ('ghost' $ghost) ('dragon' $dragon) ('dark' $dark) ('steel' $steel) ('fairy' $fairy)
.bg-blue
background-color lighten($ice, 50%)
.bg-faded-white
background rgba(white, .5)
.bg-off-white
background $off-white
.hover-bg-white
&:hover
background white
.bg-faded-blue
background rgba(0, 20, 64, .5)
.red
color $negative-color
.yellow
color $yellow
.grey
color $grey

131
client/app/css/core.styl Normal file
View File

@@ -0,0 +1,131 @@
narrow-font()
font-family "PT Sans Narrow", "Helvetica Neue", sans-serif
normal-font()
font-family 'proxima-nova', 'Helvetica Neue', Calibri, 'Droid Sans', Helvetica, Arial, sans-serif
$body_wrapper = 30px
$nav_size = 150px
$header_size = 52px
html, body
color #333332
padding 0
margin 0
font-size 100%
normal-font()
background $background-color url("//media.pokebattle.com/img/bg.png")
line-height normal
ul, ol
list-style none
ul, ol, p
margin 0
padding 0
#content
position absolute
top 0
left $nav_size
right 0
bottom 0
box-shadow 0 0 10px #000
.no-sidebar
#content
left 0
#navigation
display none
#main-section
position absolute
top $header_size
left 0
right 0
bottom 0
.window
position absolute
top 0
left 0
right 0
bottom 0
a, .fake_link
color #F27405
text-decoration none
cursor pointer
&:hover
color #BD4F00
.nav
clearfix()
list-style none
padding 0
margin 0
li
float left
border-radius 4px 4px 0 0
margin-left 2px
margin-right @margin-left
&:hover
background-color #d9d7d7
&.active
border 1px solid #d9d7d7
border-bottom none
background white
a, .fake_link, &.fake_link
color #333
text-decoration none
cursor default
a, .fake_link
padding 5px 15px
.popup-absolute
position absolute
top 20px
left 10px
right @left
text-align center
z-index 100
.popover
normal-font()
.popover-title
font-size 16px
.popover-content
font-size 14px
.loading-container
position absolute
top 0
left 0
right 0
bottom 0
background rgba(0, 0, 0, .7)
color white
font-size 4em
text-align center
z-index 99
display table
width 100%
height 100%
.loading-message
display table-cell
vertical-align middle
.team-pbv
font-size 0.75em
// Typography
.monospace
font-family "Monaco", monospace
abbr
border-bottom 1px dotted #333
.italic
font-style italic

103
client/app/css/header.styl Normal file
View File

@@ -0,0 +1,103 @@
$header_size = 52px
#header
height $header_size
line-height @height
width 100%
background #38342B
z-index 10
position absolute
ul
margin 0
.icon
position relative
top 3px
font-size 20px
#main-nav
padding 0 18px
#main-nav, #header li
line-height 42px
#logo
float left
width 120px
margin-right 15px
#logo img
position relative
z-index: 100
top 3px
#sections
margin-left 30px
margin-right 120px
visibility visible
#leftmenu
display none
#leftmenu a
color #fff
padding 5px
margin-left 15px
&:hover
border-radius 5px
background #D94E47
text-decoration none
#rightmenu {
margin-left: -40px;
}
nav ul {
list-style: none;
font-size: .95em;
}
nav li {
display: block;
float: left;
text-align: left;
}
nav a {
display: block;
padding-left: .75em;
padding-right: .75em;
color: #fff;
text-decoration: none;
}
nav a:hover, nav li:hover {
background: #D94E47;
color: #6C2724;
text-decoration none
}
#sub-nav {
height: 10px;
background: #D94E47;
z-index: 6;
position absolute
bottom 0
left 0
right @left
}
@keyframes anim-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.spinner-anim {
display: inline-block;
animation: anim-rotate 1s infinite linear;
}
.spinner-anim.hidden {
display: none;
}

105
client/app/css/layout.styl Normal file
View File

@@ -0,0 +1,105 @@
for $i in (1 2 3)
.z{$i}
z-index $i
for $i in (1 2 3 4)
.m{$i}
margin $i * 10px
.p{$i}
padding $i * 10px
for $i in (1 2 3 4)
// margin
.ml{$i}
margin-left $i * 10px
.mr{$i}
margin-right $i * 10px
.mt{$i}
margin-top $i * 10px
.mb{$i}
margin-bottom $i * 10px
// padding
.pl{$i}
padding-left $i * 10px
.pr{$i}
padding-right $i * 10px
.pt{$i}
padding-top $i * 10px
.pb{$i}
padding-bottom $i * 10px
.mt-header
margin-top $header_size
.left
float left
.right
float right
.center
text-align center
.align-left
text-align left
.align-right
text-align right
.flex-center
display flex
align-items center
justify-content center
.flex-column
flex-direction column
.hidden
display none
.absolute
position absolute
.fill
absolute top left
bottom 0
right 0
.fill-left
absolute top left
bottom 0
.fill-right
absolute top right
bottom 0
.relative
position relative
.rounded
border-radius 3px
.border
border 1px solid #ddd
.block
display block
.inline-block
display inline-block
.inline
display inline
.box-shadow
box-shadow 1px 2px 0 rgba(0, 0, 0, .2)
.tiny-type
font-size .75em
.clickable-box
@extend .border
@extend .bg-off-white
@extend .hover-bg-white
@extend .rounded
@extend .box-shadow

15
client/app/css/main.styl Normal file
View File

@@ -0,0 +1,15 @@
@import 'nib'
@import 'colors'
@import 'core'
@import 'layout'
@import 'header'
@import 'buttons'
@import 'navigation'
@import 'chat'
@import 'main_buttons'
@import 'battle'
@import 'teambuilder'
@import 'battle_list'
@import 'messages'
@import 'miscellaneous'
@import 'responsive'

View File

@@ -0,0 +1,34 @@
.main_buttons
position absolute
top 0
left 0
bottom 0
box-sizing border-box
padding 30px 30px 0 30px
width 350px
overflow auto
.button
display block
float none
text-align center
margin 0 auto
p
margin 1em 0 0 0
.section
width 100%
box-sizing border-box
background rgba(64, 64, 64, .15)
margin-bottom 15px
padding 8px
border-radius 8px
.select-team
.team-pbv
margin-top -2px
.no-alt-label
margin-left 5px
color #aaa

View File

@@ -0,0 +1,110 @@
#messages
position absolute
bottom 0
right 0
width 100%
font-size .875em
.popup
position fixed
bottom 0
float right
width 270px
margin-left 10px
box-shadow 0 2px 6px rgba(0, 0, 0, .2)
&.new_message
.title
background #d94e47
.popup_messages
background white
height 270px
border-bottom 1px solid #ccc
border-left 1px solid #ccc
border-right @border-left
overflow-y scroll
line-height 1.3em
&.small
height 150px
p
margin 0 6px
overflow hidden
word-wrap break-word
.popup_menu
background linear-gradient(top, #f0f0f0, #eee)
border-left 1px solid #ccc
border-right @border-left
border-bottom @border-left
.popup_menu_button
border-right 1px solid #ddd
float left
padding 8px
cursor pointer
&:hover
background #ddd
.chat_input_pane
position relative
background #eee
height 36px
.chat_input_wrapper
padding 3px
border-left 1px solid #ccc
border-right @border-left
.title
background #333
border-radius 3px 3px 0 0
padding 8px 0 8px 12px
color white
&.new
background #d94e47
.title_buttons
position absolute
top 0
right 2px
.title_button
display inline-block
cursor pointer
padding 5px
margin 4px
&:hover
background #706666
.icon-minus
position relative
top 4px
.challenge
background #f0f0f0
border-right 1px solid #ccc
border-left @border-right
border-bottom @border-right
transition bottom .25s ease-out
.challenge_data
padding 8px
.challenge_clauses
height 50px
overflow-y auto
p
margin 1em 0 0
&:first-child
margin-top 0
.challenge_buttons
padding 8px
border-top 1px solid #ccc
.button
margin-right 8px

View File

@@ -0,0 +1,93 @@
.clearfix:before, .clearfix:after
content ""
display table
line-height 0
.clearfix:after
clear both
::selection
background #F27405
.select
cursor pointer
border 1px solid #aaa
border-radius 3px
background #f0f0f0
padding 10px
&:hover
background #fff
border-color #bbb
&.disabled
background #ccc
cursor auto
.alt-input
$button-size = 65px
$num-buttons = 2
$alt-input-height = 38px
width 100%
padding-right ($button-size * $num-buttons)
box-sizing border-box
.input-wrapper
width 100%
float left
input
width 100%
box-sizing border-box
height $alt-input-height
border-radius 4px 0 0 4px
margin-bottom 0
.buttons-wrapper
margin-right -($button-size * $num-buttons)
float left
.button
float left
width $button-size
box-sizing border-box
height $alt-input-height
padding 0
border-radius 0
margin-left -1px
margin-bottom 0
.button:last-child
border-radius 0 4px 4px 0
.challenge_clauses
list-style none
margin 0
input
vertical-align top
margin-top 3px
label
display block
margin-bottom 0
&.disabled
cursor not-allowed
color #bbb
.achievement
img
float left
width 75px
h2
margin-top 0
.achievement-info
padding-left 90px
#achievements-modal
border none
animation: achievement-glow 2s infinite
$animation-glow-color = #4EABDA
@keyframes achievement-glow
0%
box-shadow 1px 1px 30px 2px $animation-glow-color
50%
box-shadow 1px 1px 45px 2px lighten($animation-glow-color, 20%)
100%
box-shadow 1px 1px 30px 2px $animation-glow-color

View File

@@ -0,0 +1,113 @@
h1
narrow-font()
font-size 2em
font-weight 700
line-height 50px
margin 0
#header h1
text-transform lowercase
margin 0 $body_wrapper
#navigation
$clr = #44484e
position absolute
top 0
left 0
bottom 0
width $nav_size
background $clr
font-size .875em
overflow hidden
.logo
display block
margin 0
font-size 32px
color rgba(255, 255, 255, .8)
text-shadow 0 1px 0 #333
text-align center
height $header_size
line-height $header_size
.logo:hover
cursor pointer
.nav
margin-bottom 1em
a, .fake_link
display block
padding 4px 16px
line-height 1.5em
color #ccd
border none
background none
float none
margin 0
border-radius 0
&:hover
background lighten($clr, 10%)
text-decoration none
color #fff
h2
text-transform uppercase
line-height normal
margin 0
font-size .929em
padding 4px 16px
letter-spacing 1px
color lighten($clr, 60%)
background darken($clr, 20%)
text-shadow 0 1px 0 rgba(0, 0, 0, .3)
color #777
border-bottom 1px solid #555
border-top 1px solid #111
.nav_item
position relative
&.active, &.active:hover
background #d94e47
color #fff
cursor default
.nav_meta
position absolute
right 0
font-size .714em
.notifications, .close
height 1.3em * (1em / .714em) // 1.3 height adjusted by the nav_meta font size
width @height
line-height @height
text-align center
margin-top 0.1em // the container is 1.5em and the contents are 1.3em
margin-right 5px
float left
opacity 1
font-size 1em
text-shadow none
.notifications
border-radius 4px
background #a30
color #fff
&:hover .notifications
background #b41
.close
background #333
border-radius 1em
color #fff
cursor pointer
&:hover
background #555
.user-info
float right
margin 0 16px
.team-dropdown
max-height 300px
overflow-y auto
.team-pbv
margin-top -5px
.dropdown
li
border-bottom 1px solid #aaa
li:last-of-type
border none !important

View File

@@ -0,0 +1,122 @@
@media screen and (max-width: 319px)
#logo
visibility hidden
@media screen and (max-width: 480px)
.modal
top 10px
left 10px
right 10px
.modal-header .close
padding 10px
margin -10px
@media screen and (max-width: 640px)
.popover
display none !important
#main-nav
padding 0
#logo
float none
margin 0 auto!important
#sections
visibility hidden
#leftmenu
display block
width 0
#navigation
display none
top $header_size
z-index 100
#navigation .logo
display none
#navigation.active
display block
animation slidenav 0.3s
@keyframes slidenav
0%
left -100%
100%
left 0
#content
left 0
@media screen and (max-width: 767px)
.chat_window
.main_buttons
width 100%
padding 10px
.chat
display none
// Modals
.modal
position fixed
top 20px
left 20px
right 20px
width auto
margin 0
&.fade
top -100px
&.fade.in
top 20px
// Shrink battle window
.battle_window
.battle
width 420px
margin 0 auto
right 0
padding 10px 0 0
.battle_summary
bottom 50px
left 0
right 0
.battle_container
width 400px
height 400px
margin 0 auto
.battle_pane
left 0
top 50px
.battle_teams
left 50px
right @left
top 50px
.battle_user_info
.left
bottom 0
.right
top 0
.left, .right
width 100%
left 0
.battle-timer
display inline-block
margin-right 10px
.pokemon_icons
display inline-block
vertical-align bottom
width auto
position relative
top -4px
.icon_wrapper:nth-child(even)
top 0
.moves.span8, .switches.span4
width 100%
margin-left 0
@media screen and (max-width: 980px)
.battle_window
.chat
display none

View File

@@ -0,0 +1,457 @@
$header = 50px
$pokemon-list-height = 50px
// TODO: Make this a global style
.teambuilder input
.teambuilder select
box-sizing border-box
height 28px
.teambuilder .selectize-input input
height 18px
.teambuilder
position absolute
top 0
bottom 0
left 0
right 0
select, input
margin-bottom 0!important
width 100%
.teambuilder .meta-info
max-width 1368px
.teambuilder .meta-info .left-side, .teambuilder .meta-info .right-side
float left
box-sizing border-box
.teambuilder .navigation
position absolute
top $header
bottom 0
background-color $body-color
width: 150px
ul
list-style-type none
padding 0
margin 0
li, .nav-button
display block
box-sizing border-box
padding-left 1em
height $pokemon-list-height
line-height $pokemon-list-height
font-size 14px
li:hover, .nav-button:hover
cursor pointer
background-color rgba(0, 0, 0, 0.1)
li.active
background-color rgba(200, 210, 255, 0.5)
.pokemon_icon
float none
display inline-block
margin 0
vertical-align middle
.pokemon-pbv
margin-top 7px
font-size 0.75em
.pokemon-middle
line-height 50%
display inline-block
vertical-align middle
.teambuilder .meta-info .left-side
padding-left 180px
width 45%
.teambuilder .meta-info .right-side
width 55%
padding-left 20px
.teambuilder .species
float left
width 180px
margin-left -185px
.species_list
width 100%
.pbv
float right
color #888
.teambuilder .shiny-switch
display inline-block
width 14px
height 14px
margin-right 10px
margin-left -(@margin-right + @width)
vertical-align middle
cursor pointer
background-image URL('http://91.121.152.74:8000/Sprites/images/noshiny.png')
background-size cover
&.selected
background-image URL('http://91.121.152.74:8000/Sprites/images/shiny.png')
.teambuilder .happiness-switch
display inline-block
position relative
width 14px
height 14px
margin-left 10px
margin-right -(@margin-left + @width)
cursor pointer
vertical-align middle
$happy = #FF3C3C
$unhappy = #333
&:before, &:after
position absolute
content ""
left 8px
top 0
width 8px
height 13px
background $happy
border-radius 7px 7px 0 0
transform rotate(-45deg)
transform-origin 0 100%
&:after
left 0
transform rotate(45deg)
transform-origin 100% 100%
&:hover:before, &:hover:after
background darken($happy, 30%)
&.selected:before, &.selected:after
background $unhappy
&.selected:hover:before, &.selected:hover:after
background lighten($unhappy, 30%)
.teambuilder .team_meta
position absolute
top 0
left 0
right 0
height $header
line-height @height
.team_name
font-weight 700
font-size 1.5em
height $header
line-height @height
float left
margin 0
width auto
padding 0 (1em/@font-size)
box-sizing border-box
.team_name:hover:not(:focus)
text-decoration underline
cursor pointer
.team_meta_buttons
float right
margin-top 7px
.button
float left
margin-right 10px
.teambuilder .meta-info .non-stats
display table
width 100%
.teambuilder_row, .pbv-row
display table-row
line-height 1.5em
.teambuilder_col
display table-cell
box-sizing border-box
height 2em
margin 0
&:first-child
text-align right
padding-right 10px
.pbv-row .left, .pbv-row .right
box-sizing border-box
height 2em
.non-stat-label
width 1px // minimum possible size
.teambuilder .stats
width 100%
margin-bottom 1.5em
td, th
text-align left
padding-right 10px
white-space nowrap
line-height 1.5em
height 1.5em
.stat-label, .stat-total, .base-stat
width 1px // minimum possible size
td:first-child
text-align right
input
width 100%
text-align right
th.ev-cell, th.iv-cell
text-align right
padding-right 17px // match with value in textfield (10px from the main cell + the input padding)
td.ev-cell, td.iv-cell
width 50px
.plus-nature
color $positive-color
font-weight bold
.minus-nature
color $negative-color
.remaining-evs
float left
.hidden-power
float right
.ev-lock
float left
margin-right 5px
cursor pointer
.icon-lock
color #444
.icon-unlocked
color #888
.remaining-evs-amount.over-limit
color $negative-color
font-weight bold
.teambuilder select.select-hidden-power
width auto
.teambuilder .pokemon_edit
position absolute
overflow auto
left 150px
top $header
bottom 0
right 0
background white
padding 15px
overflow-y scroll
.teambuilder .forme-sprite-box
height 120px
line-height @height
text-align center
.forme-sprite
display inline-block
.teambuilder .species-types
text-align center
margin-bottom 5px
margin-top 50px
img
margin 0 2px
.teambuilder .selected_moves
.header
margin-bottom 8px
.moves-label
font-weight bold
font-size 2em
line-height 1em
a
line-height 2em
.move-button, input
display block
margin 0
box-sizing border-box
height 32px
.close
opacity .4
line-height 30px
width 20px
text-align center
float right
.close:hover, .close:focus
opacity .8
font-weight bold
.table-moves
border-collapse separate
border solid 1px #DDD
thead th
background whitesmoke
border-bottom solid 1px #DDD
tbody tr
cursor pointer
td
border 1px solid transparent
&.active td
background #f0f0f9
&:first-child
text-decoration underline
&.selected td
background #f0f0f9
&:first-child
font-weight bold
img
margin-bottom 3px
.name
white-space nowrap
.teambuilder .table-moves
margin-top 8px
.teambuilder .description
width 60%
.teambuilder .display_teams
position absolute
top 0
left 0
bottom 0
right 0
overflow auto
h2
margin-left 10px
margin-bottom 0
.select-team
h2
text-overflow ellipsis
white-space nowrap
overflow hidden
width 192px
margin 0
font-size 1.5em
.add-new-team
margin 10px
.team-meta
font-size .75em
text-align right
margin-top 10px
.team-pbv
margin-top -5px
textarea.textarea_modal
box-sizing border-box
width 100%
height 300px
@media screen and (max-width: 1268px)
// stack non-stats and stats
.teambuilder .meta-info .left-side, .teambuilder .meta-info .right-side
display block
width 100%
// stack non-stats and stats
.teambuilder .non-stats
margin 0!important
// stack non-stats and stats
.teambuilder .meta-info .right-side
padding-left 0
// add some separation between stats and non-stats
.teambuilder .stats
margin-top 1.5em
@media screen and (max-width: 1000px)
// Hide move descriptions
.table-moves .description
display none
// Make selected moves take up the entire row
.teambuilder .selected_moves .span3
width 100%
margin-left 0
@media screen and (max-width: 800px)
// Move side navigation to above
.teambuilder .navigation li, teambuilder .navigation .nav-button
float left
width (100/6)%
font-size 0 // Hide the text
text-align center
// Move side navigation to above
.teambuilder .pokemon_edit
left 0
top $header + $pokemon-list-height
// Move side navigation to above
.teambuilder .navigation
width 100%
@media screen and (max-width: 600px)
// Scroll at the teambuilder level instead of the pokemon_edit level
// TODO: This should kick in on tablet landscape modes,
// with large widths but small heights (ex: nexus 7)
.teambuilder
overflow auto
.team_meta
position static
height auto
.navigation
position static
.pokemon_edit
position static
overflow visible
textarea.textarea_modal
height auto
@media screen and (max-width: 480px)
// split species and non-stats
.teambuilder .meta-info .left-side
padding-left 0
// split species and non-stats
.teambuilder .species, .teambuilder .non-stats
margin-left 0
width 100%
margin-bottom 1.5em
// hide ev-bars
.teambuilder .ev-range-cell
display none
// Hide pp and acc cells
.table-moves
.pp, .acc
display none

View File

@@ -0,0 +1,46 @@
return if PokeBattle.autoConnect == false
PokeBattle.primus = Primus.connect()
PokeBattle.primus.on 'listChatroom', (id, users) ->
if room = PokeBattle.rooms.get(id: id)
room.get('users').reset(users)
else
room = PokeBattle.rooms.add(id: id, users: users)
new ChatView(model: room, el: $('#chat-section .chat')).render()
PokeBattle.primus.on 'userMessage', (id, username, data) ->
room = PokeBattle.rooms.get(id)
room.userMessage(username, data)
PokeBattle.primus.on 'rawMessage', (id, message) ->
room = PokeBattle.rooms.get(id)
room.rawMessage(message)
PokeBattle.primus.on 'announce', (id, klass, message) ->
room = PokeBattle.rooms.get(id)
room.announce(klass, message)
PokeBattle.primus.on 'joinChatroom', (id, user) ->
room = PokeBattle.rooms.get(id)
room.get('users').add(user)
PokeBattle.primus.on 'leaveChatroom', (id, user) ->
room = PokeBattle.rooms.get(id)
room.get('users').remove(user)
PokeBattle.primus.on 'topic', (topic) ->
# TODO: Hardcoded
room = PokeBattle.rooms.get("Lobby")
room.setTopic(topic)
PokeBattle.userList = new UserList()
PokeBattle.battles = new BattleCollection([])
PokeBattle.messages = new PrivateMessages([])
PokeBattle.rooms = new Rooms([])
$ ->
PokeBattle.navigation = new SidebarView(el: $('#navigation'))
PokeBattle.teambuilder = new TeambuilderView(el: $("#teambuilder-section"))
PokeBattle.battleList = new BattleListView(el: $("#battle-list-section"))
new PrivateMessagesView(el: $("#messages"), collection: PokeBattle.messages)

View File

@@ -0,0 +1,8 @@
class @BattleCollection extends Backbone.Collection
model: Battle
isPlaying: ->
@find((battle) -> battle.isPlaying())?
playingBattles: ->
@filter((battle) -> battle.isPlaying())

View File

@@ -0,0 +1,2 @@
class @PrivateMessages extends Backbone.Collection
model: PrivateMessage

View File

@@ -0,0 +1,9 @@
class @Rooms extends Backbone.Collection
model: Room
# Delegate room events to every single room in this collection.
for eventName in Room::EVENTS
do (eventName) =>
this::[eventName] = (args...) ->
@each (room) ->
room[eventName].apply(room, args)

View File

@@ -0,0 +1,15 @@
class @UserList extends Backbone.Collection
model: User
comparator: (a, b) =>
aAuthority = a.get('authority')
bAuthority = b.get('authority')
aName = "#{a.id}".toLowerCase()
bName = "#{b.id}".toLowerCase()
if aAuthority < bAuthority then 1
else if aAuthority > bAuthority then -1
else if aName < bName then -1
else if aName > bName then 1
else 0
initialize: =>

View File

@@ -0,0 +1,2 @@
class @Replays extends Backbone.Collection
model: Replay

View File

@@ -0,0 +1,5 @@
$currentModal = null
PokeBattle.primus.on 'achievementsEarned', (achievements) ->
# TODO: Add achievements to the current modal if one is already open
$currentModal = PokeBattle.modal('modals/achievements', window: window, achievements: achievements)

View File

@@ -0,0 +1,10 @@
PokeBattle.primus.on 'altList', (list) ->
PokeBattle.alts.list = list
PokeBattle.primus.on 'altCreated', (altName) ->
PokeBattle.alts.list.push(altName)
PokeBattle.alts =
list: []
createAlt: (altName) ->
PokeBattle.primus.send('createAlt', altName)

View File

@@ -0,0 +1,52 @@
# Send primus event when leaving battles.
PokeBattle.battles.on 'remove', (battle) ->
PokeBattle.primus.send('leaveChatroom', battle.id)
# Event listeners
PokeBattle.primus.on 'updateBattle', (id, queue) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.update(queue)
# Create a BattleView when spectating a battle
PokeBattle.primus.on 'spectateBattle', (id, format, numActive, index, playerIds, log) ->
if PokeBattle.battles.get(id)
console.log "Already spectating battle #{id}!"
return
battle = new Battle({id, format, numActive, index, playerIds})
# Create BattleView
$battle = $(JST['battle_window']({battle, window}))
$('#main-section').append($battle)
battle.view = new BattleView(el: $battle, model: battle)
battle.view.skip = 0
battle.view.$('.battle_pane').hide()
# Add to collection
PokeBattle.battles.add(battle)
# Update log
battle.update(log)
PokeBattle.primus.on 'updateTimers', (id, timers) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.view.updateTimers(timers)
PokeBattle.primus.on 'resumeTimer', (id, player) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.view.resumeTimer(player)
PokeBattle.primus.on 'pauseTimer', (id, player, timeSinceLastAction) ->
battle = PokeBattle.battles.get(id)
if !battle
console.log "Received events for #{id}, but no longer in battle!"
return
battle.view.pauseTimer(player, timeSinceLastAction)

View File

@@ -0,0 +1,2 @@
PokeBattle.primus.on 'battleList', (battles) ->
PokeBattle.battleList.refreshListComplete(battles)

View File

@@ -0,0 +1,290 @@
PokeBattle.commands ?= {}
Commands = {}
desc = (description) ->
desc.lastDescription = description
makeCommand = (commandNames..., func) ->
for commandName in commandNames
Commands[commandName] = func
# Generate description
description = ""
if commandNames.length > 1
aliases = commandNames[1...].map((n) -> "/#{n}").join(', ')
description += " <i>Also #{aliases}. </i>"
description += desc.lastDescription
# TODO: Hardcoded user level
HelpDescriptions['1'][commandNames[0]] = description
delete desc.lastDescription
parseCommand = (line) ->
[ commandName, args... ] = line.split(/\s+/)
if commandName[0] == '/'
# It's a command. Remove leading slash
commandName = commandName[1...]
args = args.join(' ').split(/,/g)
return [commandName, args]
return null
PokeBattle.commands.execute = (room, line) ->
result = parseCommand(line)
return false if !result
[commandName, args] = result
command = Commands[commandName]
if !command
# Fall-through to server.
return false
command(room, args...)
return true
desc 'Displays a list of all commands.'
makeCommand "commands", "help", "h", (room) ->
user = room.get('users').get(PokeBattle.username)
for level, descriptions of HelpDescriptions
level = Number(level)
continue if user.get('authority') < level
message = []
# TODO: Hardcoded levels
authLevels = {1: "USER", 2: "DRIVER", 3: "MOD", 4: "ADMIN", 5: "OWNER"}
humanLevel = authLevels[level]
message.push("<b>#{humanLevel} COMMANDS:</b>")
for name, description of descriptions
message.push("<b>/#{name}:</b> #{description}")
message = message.join("<br>")
room.announce('success', message)
true
desc 'Opens the challenge for a specific user. Usage: /challenge username'
makeCommand "challenge", "chall", "c", (room, username) ->
if !username
PokeBattle.events.trigger("errorMessage", "Usage: /challenge username")
return
message = PokeBattle.messages.add(id: username)
message.openChallenge(username)
desc 'Private messages a certain user. Usage: /message username, message'
makeCommand "message", "msg", "pm", "whisper", "w", (room, username, messages...) ->
username = username?.trim()
if !username
PokeBattle.events.trigger("errorMessage", "Usage: /message username, msg")
return
message = PokeBattle.messages.add(id: username)
if messages.length > 0
text = messages.join(',')
PokeBattle.primus.send('privateMessage', message.id, text)
else
# The PM is opened without a message.
message.trigger('open', message)
desc 'Clears the chat.'
makeCommand "clear", (room) ->
room.clear()
desc 'Displays a Pokemon\'s PokeBattle value, or displays all Pokemon at or under a particular PBV. Usage: /pbv pkmn1, pkmn2, OR /pbv number'
makeCommand "pbv", (room, pokemon...) ->
pbv = Number(pokemon[0])
if !isNaN(pbv)
messages = findPokemonAtPBV(pbv)
else
messages = findTotalPBV(pokemon)
if messages.length == 0
room.announce('error', "<b>PBV error:</b> Enter valid Pokemon or PBV.")
else
room.announce('success', "<b>PBV:</b> #{messages.join('; ')}")
findPokemonAtPBV = (pbv) ->
messages = []
counter = 0
for speciesName, formes of window.Generations.XY.FormeData
for formeName, formeData of formes
if formeData.pokeBattleValue <= pbv
counter += 1
dexEntry = "pokemon/#{slugify(speciesName)}/#{slugify(formeName)}"
icon = pokemonIcon(speciesName, formeName)
formattedName = formatName(speciesName, formeName)
messages.push("#{linkToDex(dexEntry, icon + formattedName)}:
#{formeData.pokeBattleValue}")
if messages.length > 10
messages = _.sample(messages, 10)
messages.push(linkToDex("pokemon/?pbv=<#{pbv + 1}",
"See more Pokemon &raquo;"))
if messages.length > 0
plural = if messages.length == 1 then "is" else "are"
messages.unshift("There #{plural} #{counter} Pokemon with a PBV of
#{pbv} or less")
messages
findTotalPBV = (pokemon) ->
pokemon = _(pokemon).map(findPokemon)
messages = []
total = 0
for array in pokemon
continue unless array
[speciesName, formeName] = array
pbv = PokeBattle.PBV.determinePBV(window.Generations.XY,
species: speciesName, forme: formeName)
total += pbv
dexEntry = "pokemon/#{slugify(speciesName)}/#{slugify(formeName)}"
icon = pokemonIcon(speciesName, formeName)
formattedName = formatName(speciesName, formeName)
messages.push("#{linkToDex(dexEntry, icon + formattedName)}: #{pbv}")
messages.push("Total: #{total}") if messages.length > 1
messages
desc 'Looks up information about a Pokemon, move, item, or ability.'
makeCommand "data", "dex", (room, query) ->
if (pokemon = findPokemon(query))
message = dataPokemon(pokemon)
else if (item = findItem(query))
message = dataItem(item)
else if (move = findMove(query))
message = dataMove(move)
else if (ability = findAbility(query))
message = dataAbility(ability)
else
room.announce("error", "<b>Data error:</b> Enter a valid Pokemon, item,
move, or ability.</div>")
return
room.announce('success', message)
dataPokemon = (pokemon) ->
[speciesName, formeName] = pokemon
[speciesSlug, formeSlug] = [slugify(speciesName), slugify(formeName)]
forme = window.Generations.XY.FormeData[speciesName][formeName]
{types, abilities, hiddenAbility, stats, pokeBattleValue} = forme
# Format abilities
abilities = _.clone(abilities)
abilities.push(hiddenAbility) if hiddenAbility?
abilities = _(abilities).map((a) -> linkToDex("abilities/#{slugify(a)}", a))
abilities = abilities.join('/')
abilities += " (H)" if hiddenAbility?
# Format types, stats, and icon
types = _(types).map (t) ->
linkToDex("types/#{slugify(t)}",
"<img src='#{window.TypeSprite(t)}' alt='#{t}'/>")
statNames = [ 'HP', 'Attack', 'Defense', 'Sp.Attack', 'Sp.Defense', 'Speed']
stats = [ stats.hp, stats.attack, stats.defense,
stats.specialAttack, stats.specialDefense, stats.speed ]
statsText = _.map(_.zip(statNames, stats), (a) -> a.join(': ')).join(' / ')
# Build data
message = """#{pokemonIcon(speciesName, formeName, "left")}
<p class="ml3">
<b>#{formatName(speciesName, formeName)}:</b> #{types.join('')} |
#{abilities}<br />#{statsText} |
#{_(stats).reduce((a, b) -> a + b)} <abbr title="Base Stat Total">BST</abbr>
| <abbr title="PokeBattle Value">PBV</abbr>: #{pokeBattleValue}
#{linkToDex("pokemon/#{speciesSlug}/#{formeSlug}", "See dex entry &raquo;")}
</p>
"""
message
dataItem = (itemName) ->
item = window.Generations.XY.ItemData[itemName]
message = "<b>#{itemName}:</b> #{item.description}"
message += " Natural Gift is #{item.naturalGift.type} type
and has #{item.naturalGift.power} base power." if item.naturalGift
message += " Fling has #{item.flingPower} base power." if item.flingPower
message += " Currently unreleased in Gen 6." if item.unreleased
message
dataMove = (moveName) ->
move = window.Generations.XY.MoveData[moveName]
type = linkToDex("types/#{slugify(move.type)}",
"<img src='#{window.TypeSprite(move.type)}' alt='#{move.type}'/>")
category = """<img src="#{CategorySprite(move.damage)}"
alt="#{move.damage}"/>"""
target = """<img src="#{TargetSprite(move)}"
alt="#{move.target}"/>"""
power = move.power || "&mdash;"
acc = move.accuracy || "&mdash;"
maxpp = Math.floor(move.pp * 8/5)
if move.priority > 0
priority = "+#{move.priority}"
else if move.priority < 0
priority = move.priority
message = """<b>#{moveName}:</b> #{type} #{category} #{target} """
message += "<b>Power:</b> #{power} <b>Acc:</b> #{acc} <b>PP:</b> #{move.pp} (max #{maxpp})"
message += "<br />"
message += "Priority #{priority}. " if priority
message += move.description
message += " "
message += linkToDex("moves/#{slugify(moveName)}",
"See who learns this move &raquo;")
message
dataAbility = (abilityName) ->
ability = window.Generations.XY.AbilityData[abilityName]
message = """<b>#{abilityName}:</b> #{ability.description}
#{linkToDex("abilities/#{slugify(abilityName)}",
"See who obtains this ability &raquo;")}"""
message
# Finds the most lenient match possible.
findPokemon = (pokemonName) ->
pokemonName = normalize(pokemonName)
for speciesName, speciesData of window.Generations.XY.FormeData
for formeName of speciesData
name = speciesName
name += formeName unless formeName == 'default'
name = normalize(name)
name += name
return [speciesName, formeName] if name.indexOf(pokemonName) != -1
# Return blank match
null
# Finds the most lenient match possible.
findItem = (itemName) ->
normalized = normalize(itemName)
for name of window.Generations.XY.ItemData
return name if normalized == normalize(name)
# Return blank match
null
# Finds the most lenient match possible.
findMove = (moveName) ->
normalized = normalize(moveName)
for name of window.Generations.XY.MoveData
return name if normalized == normalize(name)
# Return blank match
null
# Finds the most lenient match possible.
findAbility = (abilityName) ->
normalized = normalize(abilityName)
for name of window.Generations.XY.AbilityData
return name if normalized == normalize(name)
# Return blank match
null
slugify = (str) ->
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
normalize = (str) ->
str.trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, '')
formatName = (speciesName, formeName) ->
if formeName == 'default'
pokemonName = speciesName
else
pokemonName = speciesName
pokemonName += ' '
pokemonName += formeName.split('-')
.map((n) -> n[0].toUpperCase() + n[1...])
.join('-')
return pokemonName
linkToDex = (slug, text) ->
"<a href='//pokebattle.com/dex/#{slug}' target='_blank'>#{text}</a>"
pokemonIcon = (speciesName, formeName, classes="") ->
style = window.PokemonIconBackground(speciesName, formeName)
"""<span class="pokemon_icon #{classes}" style="#{style}"></span>"""

View File

@@ -0,0 +1,39 @@
$body = $("body")
$popup = $('#popup')
PokeBattle.primus.on 'open', ->
$popup.hide()
PokeBattle.rooms.rawMessage("Connected to the server!", class: "yellow italic")
PokeBattle.primus.on 'reconnecting', ->
PokeBattle.rooms.rawMessage("Lost connection to the server...", class: "red italic")
PokeBattle.primus.on 'end', ->
PokeBattle.rooms.rawMessage("Connection terminated!", class: "red italic")
PokeBattle.primus.on 'reconnecting', (opts) ->
seconds = Math.floor(opts.timeout / 1000)
if $popup.length == 0
$popup = $('<div id="popup" class="reconnect popup-absolute"/>').hide()
$div = $('<span/>')
.addClass('alert')
.html("<strong>You lost your connection to PokeBattle!</strong>
<span class='reconnect-text'></span>")
.appendTo($popup)
$popup.appendTo($body)
$popup.find('.reconnect-text').html("""Reconnecting in
<span class='reconnect-timer'>#{seconds}</span> seconds...""")
$popup.fadeIn(250)
$body.data('seconds', seconds)
reconnectTimer($popup)
reconnectTimer = ($popup) ->
seconds = $body.data('seconds') ? 1
if seconds == 0
$popup.find('.reconnect-text').text("Reconnecting...")
else
time = "#{Math.floor(seconds / 60)}:"
time += "00#{seconds % 60}".substr(-2)
$popup.find('.reconnect-timer').text(time)
$body.data('seconds', seconds - 1)
setTimeout(reconnectTimer.bind(this, $popup), 1000)

View File

@@ -0,0 +1,57 @@
PokeBattle.primus.on 'errorMessage', (args...) ->
PokeBattle.events.trigger('errorMessage', args...)
PokeBattle.events.on "errorMessage", (type, args...) ->
e = PokeBattle.errors
switch type
when e.INVALID_SESSION
$('#errors-modal').remove() if $('#errors-modal').length > 0
options =
title: "Your login timed out!"
body: """To access the simulator, you need to
<a href="http://91.121.152.74/">login again</a>."""
$modal = PokeBattle.modal('modals/errors', options)
$modal.find('.modal-footer button').first().focus()
PokeBattle.primus.end()
when e.BANNED
$('#errors-modal').remove() if $('#errors-modal').length > 0
[reason, length] = args
if length < 0
length = "is permanent"
else
length = "lasts for #{Math.round(length / 60)} minute(s)"
body = "This ban #{length}."
if reason
body += "You were banned for the following reason: #{reason}"
options =
title: "You have been banned!"
body: body
$modal = PokeBattle.modal('modals/errors', options)
$modal.find('.modal-footer button').first().focus()
PokeBattle.primus.end()
when e.FIND_BATTLE
PokeBattle.events.trigger("findBattleCanceled")
# Show errors
[errors] = args
alert(errors)
when e.BATTLE_DNE
[battleId] = args
message = 'This battle no longer exists.'
PokeBattle.rooms.get(battleId)?.announce('error', message)
when e.COMMAND_ERROR
[ roomId, message ] = args
PokeBattle.rooms.get(roomId).announce('error', message)
when e.PRIVATE_MESSAGE
[ toUser, messageText ] = args
message = PokeBattle.messages.get(toUser)
message.add(toUser, messageText, type: "error")
when e.INVALID_ALT_NAME
[ messageText ] = args
alert(messageText)
PokeBattle.events.trigger("invalidAltName")
else
console.log("Received error: #{type}")
console.log(" with content: #{args}")

View File

@@ -0,0 +1,7 @@
# Prevents a clean escape while you're in a battle.
$(window).on 'beforeunload', ->
if PokeBattle.battles.isPlaying()
"You are currently in a battle."
else
# Do not prompt
return

View File

@@ -0,0 +1,4 @@
PokeBattle.primus.on 'data', (args...) ->
try
console.log(args...) if window.localStorage.debug == 'true'
catch

View File

@@ -0,0 +1,50 @@
$ ->
$mainButtons = $('.main_buttons')
$mainButtons.on 'click', '.teambuilder_button', (e) ->
PokeBattle.navigation.showTeambuilder()
createChallengePane
eventName: "findBattle"
populate: $mainButtons.find('.find_battle_select_team')
button: $mainButtons.find('.find_battle')
defaultClauses: [
Conditions.SLEEP_CLAUSE
Conditions.EVASION_CLAUSE
Conditions.SPECIES_CLAUSE
Conditions.OHKO_CLAUSE
Conditions.PRANKSTER_SWAGGER_CLAUSE
Conditions.UNRELEASED_BAN
Conditions.RATED_BATTLE
Conditions.TIMED_BATTLE
]
blockedClauses: true
$mainButtons.find('.find_battle').on 'challenge', ->
$this = $(this)
$this.find('.find-icon')
.addClass('icon-spinner spinner-anim')
.removeClass("icon-earth")
$mainButtons.find('.display_credits').click ->
$modal = PokeBattle.modal('modals/credits')
$modal.find('.modal-footer button').first().focus()
# Depresss Find Battle once one is found
depressFindBattle = ->
$mainButtons = $('.main_buttons')
$button = $mainButtons.find('.find_battle')
$button.removeClass("disabled")
$button.find('.find-icon')
.removeClass("icon-spinner spinner-anim")
.addClass("icon-earth")
$mainButtons.find('.find_battle_select_team .select').removeClass('disabled')
$(window).load ->
$mainButtons = $('.main_buttons')
PokeBattle.battles.on 'add', (battle) ->
if !battle.get('spectating')
depressFindBattle()
PokeBattle.primus.on 'findBattleCanceled', depressFindBattle
PokeBattle.events.on 'findBattleCanceled', depressFindBattle

View File

@@ -0,0 +1,31 @@
# Clicking a person's name will open a private message session with that person.
$(document).on 'click', '.open_pm', ->
$this = $(this)
message = PokeBattle.messages.add(id: $this.data('user-id'))
message.trigger('open', message)
message.trigger('focus', message)
# Receive private message events
PokeBattle.primus.on 'privateMessage', (messageId, fromUserId, messageText) ->
message = PokeBattle.messages.add(id: messageId)
message.add(fromUserId, messageText)
# Challenges
PokeBattle.primus.on 'challenge', (fromUserId, generation, conditions) ->
message = PokeBattle.messages.add(id: fromUserId)
message.add(fromUserId, "You have been challenged!", type: "alert")
message.openChallenge(fromUserId, generation, conditions)
PokeBattle.primus.on 'cancelChallenge', (fromUserId) ->
message = PokeBattle.messages.add(id: fromUserId)
message.add(fromUserId, "The challenge was canceled!", type: "alert")
message.closeChallenge(fromUserId)
PokeBattle.primus.on 'rejectChallenge', (fromUserId) ->
message = PokeBattle.messages.add(id: fromUserId)
message.add(fromUserId, "The challenge was rejected!", type: "alert")
message.closeChallenge(fromUserId)
PokeBattle.primus.on 'challengeSuccess', (fromUserId) ->
message = PokeBattle.messages.add(id: fromUserId)
message.closeChallenge(fromUserId)

View File

@@ -0,0 +1,21 @@
class PokeBattleRouter extends Backbone.Router
routes:
"" : "main"
"battles/:id" : "spectateBattle"
main: =>
$navigation = $('#navigation')
$navigation.find('.nav_item').first().click()
spectateBattle: (id) =>
if PokeBattle.battles.get(id)
PokeBattle.navigation.changeWindowToBattle(id)
else
PokeBattle.primus.send('spectateBattle', id)
PokeBattle.router = new PokeBattleRouter()
PokeBattle.primus.once "loginSuccess", ->
return if Backbone.History.started
PokeBattle.events.trigger("ready")
routed = Backbone.history.start(pushState: true)

View File

@@ -0,0 +1,4 @@
$(document).on 'click', '.spectate', ->
battleId = $(this).data('battle-id')
PokeBattle.router.navigate("battles/#{battleId}", trigger: true)
return false

View File

@@ -0,0 +1,6 @@
PokeBattle.primus.on 'version', (version) ->
if version != PokeBattle.CLIENT_VERSION
$modal = PokeBattle.modal 'modals/new_client', ($modal) ->
$modal.on 'click', '.button_refresh', ->
window.location.reload(true)
$modal.find('button').first().focus()

View File

@@ -0,0 +1,57 @@
DEFAULT_INTERVAL = 1000
original = document.title
timeoutId = undefined
$.flashTitle = (newMsg, interval) ->
if newMsg == false
# stop flashing and reset title
clearTimeout(timeoutId)
document.title = original
else
# loop flashing
interval = interval || DEFAULT_INTERVAL
timeoutId = setTimeout( ->
clearTimeout(timeoutId)
document.title = if (document.title == original) then newMsg else original
timeoutId = setTimeout(arguments.callee, interval)
, interval)
PokeBattle.NotificationTypes =
PRIVATE_MESSAGE:
showDesktop: false
BATTLE_STARTED:
showDesktop: true
prefix: "bs"
title: "Battle Started"
body: "Your battle has started!"
ACTION_REQUESTED:
showDesktop: true
prefix: "ar"
title: "Battle Action Requested"
body: "A battle is ready for your input!"
notifications = []
# Currently called in concerns/find_battle.coffee
PokeBattle.requestNotifyPermission = =>
if notify.permissionLevel() == notify.PERMISSION_DEFAULT
notify.requestPermission()
# TODO: Count the notifications by unique type/identifier combos
PokeBattle.notifyUser = (type, identifier) =>
return if document.hasFocus()
$.flashTitle "You have new notification(s)"
if type.showDesktop
notification = notify.createNotification type.title,
icon: "//media.pokebattle.com/logo/pb_red.png"
body: type.body
tag: "PokeBattle_#{type.prefix}_#{identifier}"
notifications.push notification
$(window).focus ->
$.flashTitle(false)
notification.close() for notification in notifications
notifications = []

View File

@@ -0,0 +1,70 @@
class TeamStore extends Backbone.Collection
model: Team
initialize: ->
@on('add remove reset remoteSync', @saveLocally)
@loadLocally()
# Only locally save teams without an id or is trying to save.
unsavedTeams: =>
@filter((team) -> !team.id || team.get('saving'))
saveLocally: =>
teams = @unsavedTeams()
json = _.map(teams, (team) -> team.toJSON())
try
window.localStorage.setItem('local_teams', JSON.stringify(json))
catch
console.error("Failed to save teams locally.")
loadLocally: =>
try
json = window.localStorage.getItem('local_teams')
return unless json
teams = JSON.parse(json)
@add(teams) if teams.length > 0
catch
console.error("Failed to load teams locally.")
saveRemotely: =>
teams = @unsavedTeams()
team.save() for team in teams
PokeBattle.TeamStore = new TeamStore()
PokeBattle.primus.on 'receiveTeams', (remoteTeams) ->
remoteTeams = remoteTeams.map (team) ->
team.teambuilder = true
new Team(team)
# First, find teams that are already saved locally -- these exclude deleted
# teams on either side. The remote copy of the team is checked against the
# local copy of the team. If they differ, display a modal asking whether to
# override or keep the local changes.
ids = PokeBattle.TeamStore.pluck('id')
for remoteTeam in remoteTeams when remoteTeam.id in ids
remoteJSON = remoteTeam.toJSON()
localTeam = PokeBattle.TeamStore.get(remoteTeam.id)
unsavedTeam = localTeam.clone()
unsavedTeam.set(localTeam.previousAttributes(), silent: true)
localJSON = unsavedTeam.toJSON()
if !_.isEqual(remoteJSON, localJSON)
# Whoa! Versions are different! Let's ask the user what to do.
teamText = PokeBattle.exportTeam(remoteJSON.pokemon)
domId = "teams-differ-#{remoteTeam.id}"
$modal = PokeBattle.modal('modals/teams_differ', domId, {teamText})
$modal.find('textarea').first().focus()
do (localTeam, remoteJSON) ->
# We want to override the current version with the one on the server.
# This is extremely hacky due to hidden state and clones everywhere on
# the teambuilder.
$modal.find('.button_override').one 'click', ->
localTeam.set(remoteJSON, silent: true)
localTeam.trigger('render', localTeam)
$modal.modal('hide')
# Now, add teams we haven't seen yet to the store.
PokeBattle.TeamStore.add(remoteTeams)
PokeBattle.primus.on 'loginSuccess', ->
PokeBattle.primus.send('requestTeams')

View File

@@ -0,0 +1,9 @@
# Used to handle the show navigation button in the header, which is visible when the browser
# window becomes small enough.
$ ->
$navigation = $("#navigation")
$(".show-navigation").click =>
active = $navigation.hasClass('active')
$navigation.toggleClass('active', !active)
$navigation.on 'click', '.nav_item', -> $navigation.removeClass("active")

View File

@@ -0,0 +1,199 @@
# eventName should be one of "challenge" or "find battle"
# opts may include whether to enable clauses, for example
@createChallengePane = (opts) ->
$wrapper = opts.populate
$button = opts.button
$accept = opts.acceptButton || $()
$reject = opts.rejectButton || $()
$buttons = $button.add($accept).add($reject)
eventName = opts.eventName
capitalizedEventName = "#{eventName[0].toUpperCase()}#{eventName.substr(1)}"
acceptEventName = "accept#{capitalizedEventName}"
rejectEventName = "reject#{capitalizedEventName}"
cancelEventName = "cancel#{capitalizedEventName}"
generation = opts.generation
personId = opts.personId
defaultClauses = opts.defaultClauses || []
blockedClauses = opts.blockedClauses ? false
selectedTeamId = null
selectedAlt = null
getSelectedTeam = ->
PokeBattle.TeamStore.get(selectedTeamId) || PokeBattle.TeamStore.at(0)
renderCurrentTeam = ($context) ->
$selectTeam = $context.find('.select-team')
if PokeBattle.TeamStore.length > 0
currentTeam = getSelectedTeam()
html = JST['team_dropdown'](window: window, team: currentTeam)
$selectTeam.html(html)
else
$selectTeam.html("You have no teams!")
cancelChallenge = ->
enableButtons()
if personId
PokeBattle.primus.send(cancelEventName, personId)
else
format = $selectFormat.data('format')
PokeBattle.primus.send(cancelEventName, format)
$button.trigger('cancelChallenge')
disableButtons = ->
$wrapper.find('.select').addClass('disabled')
$buttons.addClass('disabled')
# Enable buttons
enableButtons = ->
$buttons.removeClass('disabled')
toggleAltInput = (visible) ->
$wrapper.find('.alt-input').toggleClass("hidden", !visible)
$wrapper.find('.alt-dropdown-section').toggleClass("hidden", visible)
$wrapper.find('.alt-input input').focus() if visible
isAttachedToDom = ->
$.contains(document, $wrapper.get(0))
altCreatedEvent = ->
return PokeBattle.primus.off('altCreated', altCreatedEvent) unless isAttachedToDom()
$wrapper.find('.alt-input input').val("")
toggleAltInput(false)
PokeBattle.primus.on 'altCreated', altCreatedEvent
enableButtons()
$wrapper.html(JST['new_battle']({window, defaultClauses}))
$selectFormat = $wrapper.find(".select-format")
# Implement finding battle/challenging
$button.on 'click.challenge', ->
# Start requesting for notify permission here
PokeBattle.requestNotifyPermission()
format = $selectFormat.data('format')
# Toggle state when you press the button.
if !$button.hasClass('disabled')
team = getSelectedTeam()
unless team
alert("You need to create a team using the Teambuilder before you can battle.")
PokeBattle.navigation.showTeambuilder()
return
disableButtons()
teamJSON = team.toNonNullJSON().pokemon
# Send the event
if personId
$clauses = $wrapper.find('input:checked[type="checkbox"]')
clauses = []
$clauses.each(-> clauses.push(parseInt($(this).val(), 10)))
PokeBattle.primus.send(eventName, personId, format, teamJSON, clauses, selectedAlt)
else
PokeBattle.primus.send(eventName, format, teamJSON, selectedAlt)
$button.addClass('disabled').trigger('challenge')
else
cancelChallenge()
# Implement accept/reject buttons.
$accept.on 'click.challenge', ->
return if $(this).hasClass('disabled')
team = getSelectedTeam()
unless team
alert("You need to create a team using the Teambuilder before you can battle.")
PokeBattle.navigation.showTeambuilder()
return
disableButtons()
teamJSON = team.toNonNullJSON().pokemon
PokeBattle.primus.send(acceptEventName, personId, teamJSON, selectedAlt)
$reject.on 'click.challenge', ->
return if $(this).hasClass('disabled')
disableButtons()
PokeBattle.primus.send(rejectEventName, personId)
# Clicking the alts dropdown brings down an alt selection dropdown menu
$wrapper.find('.select-alt').click (e) ->
html = JST['alt_dropdown'](alts: PokeBattle.alts.list, username: PokeBattle.username)
$wrapper.find('.alt-dropdown').html(html)
# Selecting an alt from the dropdown
$wrapper.find('.alt-dropdown').on 'click', '.select-alt-dropdown-item', (e) ->
selectedAlt = $(this).data('alt-name')
$wrapper.find('.select-alt').html($(this).html())
# When add alt is clicked, show the alt input form
$wrapper.find('.alt-dropdown').on 'click', '.add-alt-dropdown-item', (e) ->
toggleAltInput(true)
# Clicking the Add Alt Button
$wrapper.find('.alt-input .add-button').click (e) ->
altName = $wrapper.find('.alt-input input').val().trim()
PokeBattle.alts.createAlt(altName)
# Clicking the Cancel Add Alt Button
$wrapper.find('.alt-input .cancel-button').click (e) ->
toggleAltInput(false)
# Clicking the team dropdown brings down a team selection menu.
# Also updates the allTeams collection
$wrapper.find('.select-team').click (e) ->
allTeams = PokeBattle.TeamStore.models || []
html = JST['team_dropdown'](window: window, teams: allTeams)
$wrapper.find('.team-dropdown').html(html)
# Selecting a team from the menu
$wrapper.find('.team-dropdown').on 'click', '.select-team-dropdown-item', (e) ->
slot = $(e.currentTarget).data('slot')
selectedTeamId = PokeBattle.TeamStore.at(slot).id
renderCurrentTeam($wrapper)
# Selecting build team from the menu
$wrapper.find('.team-dropdown').on 'click', '.build-team-option', (e) ->
PokeBattle.navigation.showTeambuilder()
# Selecting the format changes the dropdown.
$wrapper.find('.format-dropdown').on 'click', '.select-format-dropdown-item', (e) ->
$target = $(e.currentTarget)
format = $target.data('format')
$selectFormat.text($target.text())
$selectFormat.data('format', format)
# Select non-alt option
$wrapper.find('.select-alt').html(JST['alt_dropdown'](alt: null, username: PokeBattle.username))
# Auto-select format.
if generation
# If a generation is passed, auto-select it.
$format = $wrapper.find(".format-dropdown a[data-format='#{generation}']")
$format.first().click()
$wrapper.find('.select-format').addClass('disabled')
else
# Auto-select first available format.
$wrapper.find('.format-dropdown a').first().click()
if blockedClauses
$checkboxes = $wrapper.find('input[type="checkbox"]')
if blockedClauses != true
$checkboxes = $checkboxes.filter ->
clause = Number($(this).data('clause'))
clause in blockedClauses
$checkboxes.prop('disabled', true)
$checkboxes.closest('label').addClass('disabled')
renderCurrentTeam($wrapper)
# Called when a team has been updated
teamUpdated = ->
# If this challenge panel no longer exists, remove the callback
if not isAttachedToDom()
PokeBattle.TeamStore.off 'add remove reset saved', teamUpdated
return
# Rerender the current team
renderCurrentTeam($wrapper)
# Start listening for team updated events
PokeBattle.TeamStore.on 'add remove reset saved', teamUpdated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
# domId is optional:
#
# PokeBattle.modal(modalPath, [domId], options, initialize)
PokeBattle.modal = (modalPath, domId, options, initialize) ->
[domId, options, initialize] = [null, domId, options] if !_.isString(domId)
[options, initialize] = [{}, options] if _.isFunction(options)
$modal = $(JST[modalPath](options))
id = '#' + (domId || $modal.prop('id'))
if $(id).length == 0
$modal.appendTo($('body'))
initialize?($modal)
else
$modal = $(id).last()
$modal.modal('show')
return $modal

View File

@@ -0,0 +1,262 @@
HiddenPower = (if module? then require('../../../../shared/hidden_power') else window.HiddenPower ?= {})
@PokeBattle ?= {}
@PokeBattle.parseTeam = (teamString) ->
text = teamString.split('\n')
team = []
pokemonRegex = /^(.*?)\s*(\(M\)|\(F\)|)?(?:\s*@\s*(.*))?$/
pokemon = null
for line in text
line = line.trim()
if line.length == 0
pokemon = null
else if !pokemon
[ all, pokemonLine, gender, item ] = line.match(pokemonRegex)
pokemon = {}
team.push(pokemon)
if pokemonLine.match(/(.*?)\s*\((.*)\)/)
pokemon.name = RegExp.$1
pokemonLine = RegExp.$2
convertNameToSpeciesAndForme(pokemon, pokemonLine.trim())
pokemon.gender = gender[1] if gender # (M) and (F)
pokemon.item = item if item
for olditem, newitem of Aliases.items
if olditem is pokemon.item
pokemon.item = newitem
else if line.match(/^(?:Trait|Ability):\s+(.*)$/i)
pokemon.ability = RegExp.$1
for oldability, newability of Aliases.abilities
if pokemon.ability is oldability
pokemon.ability = newability
else if line.match(/^Level:\s+(.*)$/i)
pokemon.level = Number(RegExp.$1) || 100
else if line.match(/^Happiness:\s+(.*)$/i)
pokemon.happiness = Number(RegExp.$1) || 0
else if line.match(/^Shiny: Yes$/i)
pokemon.shiny = true
else if line.match(/^EVs: (.*)$/i)
evs = RegExp.$1.split(/\//g)
pokemon.evs = {}
for ev in evs
ev = ev.trim()
[ numberString, rawStat ] = ev.split(/\s+/)
pokemon.evs[statsHash[rawStat]] = Number(numberString) || 0
else if line.match(/^IVs: (.*)$/i)
ivs = RegExp.$1.split(/\//g)
pokemon.ivs = {}
for iv in ivs
iv = iv.trim()
[ numberString, rawStat ] = iv.split(/\s+/)
pokemon.ivs[statsHash[rawStat]] = Number(numberString) || 0
else if line.match(/^([A-Za-z]+) nature/i)
pokemon.nature = RegExp.$1
else if line.match(/^[\-\~]\s*(.*)/)
moveName = RegExp.$1
for oldmove, newmove of Aliases.moves
if moveName is oldmove
moveName = newmove
if /Hidden Power /.test(moveName)
if !pokemon.ivs
moveName.match(/Hidden Power (.*)/i)
hiddenPowerType = RegExp.$1.trim().toLowerCase().replace(/\W+/g, '')
pokemon.ivs = HiddenPower.BW.ivs[hiddenPowerType] || {}
moveName = 'Hidden Power'
pokemon.moves ?= []
pokemon.moves.push(moveName)
return team
@PokeBattle.exportTeam = (json) ->
s = []
for pokemon in json
s.push("")
species = pokemon.species
if pokemon.forme && pokemon.forme != "default"
species += "-#{pokemon.forme[0].toUpperCase()}"
mainLine = []
if pokemon.name
mainLine.push(pokemon.name)
mainLine.push("(#{species})")
else
mainLine.push(species)
mainLine.push("(#{pokemon.gender})") if pokemon.gender
mainLine.push("@ #{pokemon.item}") if pokemon.item
s.push(mainLine.join(' '))
# Ability
s.push("Ability: #{pokemon.ability}") if pokemon.ability
# EVs
if pokemon.evs
evArray = for stat, amount of pokemon.evs when amount > 0
"#{amount} #{reverseStatsHash[stat]}"
s.push("EVs: #{evArray.join(" / ")}") if evArray.length > 0
# IVs
if pokemon.ivs
ivArray = for stat, amount of pokemon.ivs when amount < 31
"#{amount} #{reverseStatsHash[stat]}"
s.push("IVs: #{ivArray.join(" / ")}") if ivArray.length > 0
# Nature
s.push("#{pokemon.nature} nature") if pokemon.nature
# Level
s.push("Level: #{pokemon.level}") if pokemon.level && pokemon.level != 100
# Shiny
s.push("Shiny: Yes") if pokemon.shiny
# Happiness
if pokemon.happiness && pokemon.happiness != 100
s.push("Happiness: #{pokemon.happiness}")
# Moves
if pokemon.moves
s.push("- #{moveName}") for moveName in pokemon.moves
s.push("\n") # Trailing newlines, just in case.
s.join("\n")
Aliases =
moves:
"Ancient Power" : "AncientPower"
"Bubble Beam" : "BubbleBeam"
"Double Slap" : "DoubleSlap"
"Dragon Breath" : "DragonBreath"
"Dynamic Punch" : "DynamicPunch"
"Extreme Speed" : "ExtremeSpeed"
"Feint Attack" : "Faint Attack"
"Feather Dance" : "FeatherDance"
"Grass Whistle" : "GrassWhistle"
"High Jump Kick" : "Hi Jump Kick"
"Poison Powder" : "PoisonPowder"
"Sand Attack" : "Sand-Attack"
"Self-Destruct" : "Selfdestruct"
"Smelling Salts" : "SmellingSalt"
"Smokescreen" : "SmokeScreen"
"Soft-Boiled" : "Softboiled"
"Solar Beam" : "SolarBeam"
"Sonic Boom" : "SonicBoom"
"Thunder Punch" : "ThunderPunch"
"Thunder Shock" : "ThunderShock"
"Vice Grip" : "ViceGrip"
abilities:
"Compound Eyes" : "Compoundeyes"
"Lightning Rod" : "Lightningrod"
items:
"Balm Mushroom" : "BalmMushroom"
"Black Glasses" : "BlackGlasses"
"Bright Powder" : "BrightPowder"
"Deep Sea Scale" : "DeepSeaScale"
"Deep Sea Tooth" : "DeepSeaTooth"
"Energy Powder" : "EnergyPowder"
"Never-Melt Ice" : "NeverMeltIce"
"Paralyze Heal" : "Parlyz Heal"
"Rage Candy Bar" : "RageCandyBar"
"Silver Powder" : "SilverPowder"
"Thunder Stone" : "Thunderstone"
"Tiny Mushroom" : "TinyMushroom"
"Twisted Spoon" : "TwistedSpoon"
"X Defense" : "X Defend"
"X Sp. Atk" : "X Special"
statsHash =
'hp' : 'hp'
'Hp' : 'hp'
'HP' : 'hp'
'Atk' : 'attack'
'Def' : 'defense'
'SAtk' : 'specialAttack'
'SpA' : 'specialAttack'
'SDef' : 'specialDefense'
'SpD' : 'specialDefense'
'Spe' : 'speed'
'Spd' : 'speed'
reverseStatsHash =
'hp' : 'HP'
'attack' : 'Atk'
'defense' : 'Def'
'specialAttack' : 'SAtk'
'specialDefense' : 'SDef'
'speed' : 'Spe'
convertNameToSpeciesAndForme = (pokemon, species) ->
if species.match(/(Thundurus|Landorus|Tornadus)\-T(herian)?/i)
pokemon.species = RegExp.$1
pokemon.forme = 'therian'
else if species.match(/Shaymin-S(ky)?/i)
pokemon.species = "Shaymin"
pokemon.forme = 'sky'
else if species.match(/Giratina-O(rigin)?/i)
pokemon.species = "Giratina"
pokemon.forme = 'origin'
else if species.match(/Arceus(\-.*)?/)
pokemon.species = "Arceus"
else if species.match(/Kyurem-B(lack)?/)
pokemon.species = "Kyurem"
pokemon.forme = "black"
else if species.match(/Kyurem-W(hite)?/)
pokemon.species = "Kyurem"
pokemon.forme = "white"
else if species.match(/Rotom-W|Rotom-Wash/)
pokemon.species = "Rotom"
pokemon.forme = "wash"
else if species.match(/Rotom-S|Rotom-Fan/)
pokemon.species = "Rotom"
pokemon.forme = "fan"
else if species.match(/Rotom-H|Rotom-Heat/)
pokemon.species = "Rotom"
pokemon.forme = "heat"
else if species.match(/Rotom-F|Rotom-Frost/)
pokemon.species = "Rotom"
pokemon.forme = "frost"
else if species.match(/Rotom-C|Rotom-Mow/)
pokemon.species = "Rotom"
pokemon.forme = "mow"
else if species.match(/Deoxys-A|Deoxys-Attack/)
pokemon.species = "Deoxys"
pokemon.forme = "attack"
else if species.match(/Deoxys-D|Deoxys-Defense/)
pokemon.species = "Deoxys"
pokemon.forme = "defense"
else if species.match(/Deoxys-S|Deoxys-Speed/)
pokemon.species = "Deoxys"
pokemon.forme = "speed"
else if species.match(/Basculin-Blue-Striped|Basculin-A/)
pokemon.species = "Basculin"
pokemon.forme = "blue-striped"
else if species.match(/Keldeo-Resolute|Keldeo-R/)
pokemon.species = "Keldeo"
pokemon.forme = "resolute"
else if species.match(/Shellos-East/)
pokemon.species = "Shellos"
# TODO: Read east forme
pokemon.forme = "default"
else if species.match(/Gastrodon-East/)
pokemon.species = "Gastrodon"
# TODO: Read east forme
pokemon.forme = "default"
else if species.match(/Wormadam-Sandy|Wormadam-G/)
pokemon.species = "Wormadam"
pokemon.forme = "sandy"
else if species.match(/Wormadam-Trash|Wormadam-S/)
pokemon.species = "Wormadam"
pokemon.forme = "trash"
else if species.match(/Deerling-.*/)
pokemon.species = "Deerling"
# TODO: Read other formes
pokemon.forme = null
else if species.match(/Sawsbuck-.*/)
pokemon.species = "Sawsbuck"
# TODO: Read other formes
pokemon.forme = null
else if species.match(/Unown-.*/)
pokemon.species = "Unown"
# TODO: Read other formes
pokemon.forme = null
else
pokemon.species = species

View File

@@ -0,0 +1,17 @@
# TODO: Move more timer functionality into here
# TODO: Encapsulate this better. Maybe put humanizeTime in a time object with more functions?
PokeBattle.humanizeTime = (unixTime) =>
unixTime = 0 if !unixTime? || unixTime < 0
seconds = Math.floor(unixTime / 1000) % 60
minutes = Math.floor(unixTime / 1000 / 60)
seconds = String(seconds)
return minutes + ":" + "00".substr(seconds.length) + seconds
$ ->
window.setInterval( ->
$(".elapsed-time").each ->
$el = $(this)
elapsedTime = Date.now() - $el.data("time-start")
$el.text(PokeBattle.humanizeTime(elapsedTime))
, 500)

View File

@@ -0,0 +1,4 @@
@PokeBattle ?= {}
PokeBattle.battles = null
PokeBattle.events = {}
_.extend(PokeBattle.events, Backbone.Events)

View File

@@ -0,0 +1,3 @@
PokeBattle.events.once "ready", ->
$loading = $(".loading-container")
$loading.fadeOut(-> $loading.remove())

View File

@@ -0,0 +1,3 @@
# This is turned off by default, due to performance reasons.
# But we'd like to listen to this event for the teambuilder.
Backbone.Associations.EVENTS_NC = true

View File

@@ -0,0 +1,210 @@
@PokeBattle.mixins.BattleProtocolParser =
update: (actions) ->
return if actions.length == 0
@notify()
hadStuff = (@updateQueue.length > 0)
@updateQueue.push(actions...)
@_update() unless hadStuff
_update: (wasAtBottom) ->
view = @view
queue = @updateQueue
return if !queue # closed battle in the middle of getting updates
if queue.length == 0
view.renderUserInfo()
view.resetPopovers()
if wasAtBottom || view.skip? then view.chatView.scrollToBottom()
if view.skip?
delete view.skip
view.$('.battle_pane').show()
return
wasAtBottom ||= view.chatView.isAtBottom()
action = queue.shift()
[ type, rest... ] = action
protocol = (key for key, value of Protocol when value == type)[0]
try
if window.localStorage.debug == 'true'
console.log "Received protocol: #{protocol} with args: #{rest}"
catch
done = () =>
return if done.called
done.called = true
if view.skip?
@_update.call(this, wasAtBottom)
else
# setTimeout 0 lets the browser breathe.
setTimeout(@_update.bind(this, wasAtBottom), 0)
doneTimeout = ->
setTimeout(done, 0)
doneSpeedTimeout = () =>
if view.skip? || view.speed <= 1
done()
else
setTimeout(done, (view.speed - 1) * 1000)
try
switch type
when Protocol.CHANGE_HP
[player, slot, newPercent] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('percent', newPercent)
if view.skip? then done() else setTimeout(done, 500)
when Protocol.CHANGE_EXACT_HP
[player, slot, newHP] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('hp', newHP)
done()
when Protocol.SWITCH_OUT
[player, slot] = rest
view.switchOut(player, slot, done)
when Protocol.SWITCH_IN
# TODO: Get Pokemon data, infer which Pokemon it is.
# Currently, it cheats with `fromSlot`.
[player, toSlot, fromSlot] = rest
team = @getTeam(player).get('pokemon').models
[team[toSlot], team[fromSlot]] = [team[fromSlot], team[toSlot]]
# TODO: Again, automatic.
view.switchIn(player, toSlot, fromSlot, doneSpeedTimeout)
when Protocol.CHANGE_PP
[player, slot, moveIndex, newPP] = rest
pokemon = @getPokemon(player, slot)
pokemon.setPP(moveIndex, newPP)
done()
when Protocol.REQUEST_ACTIONS
[validActions] = rest
view.enableButtons(validActions)
PokeBattle.notifyUser(PokeBattle.NotificationTypes.ACTION_REQUESTED, @id + "_" + @get('turn'))
done()
when Protocol.START_TURN
[turn] = rest
view.beginTurn(turn, doneTimeout)
when Protocol.CONTINUE_TURN
view.continueTurn(doneTimeout)
when Protocol.RAW_MESSAGE
[message] = rest
view.addLog("#{message}<br>")
done()
when Protocol.FAINT
[player, slot] = rest
view.faint(player, slot, done)
when Protocol.MAKE_MOVE
# TODO: Send move id instead
[player, slot, moveName] = rest
view.logMove(player, slot, moveName, done)
when Protocol.END_BATTLE
[winner] = rest
view.announceWinner(winner, done)
when Protocol.FORFEIT_BATTLE
[forfeiter] = rest
view.announceForfeit(forfeiter, done)
when Protocol.TIMER_WIN
[winner] = rest
view.announceTimer(winner, done)
when Protocol.BATTLE_EXPIRED
view.announceExpiration(done)
when Protocol.MOVE_SUCCESS
[player, slot, targetSlots, moveName] = rest
view.moveSuccess(player, slot, targetSlots, moveName, done)
when Protocol.CANNED_TEXT
cannedInteger = rest.splice(0, 1)
view.parseCannedText(cannedInteger, rest, done)
when Protocol.EFFECT_END
[player, slot, effect] = rest
view.endEffect(player, slot, effect, done)
when Protocol.POKEMON_ATTACH
[player, slot, attachment] = rest
view.attachPokemon(player, slot, attachment, done)
when Protocol.TEAM_ATTACH
[player, attachment] = rest
view.attachTeam(player, attachment, done)
when Protocol.BATTLE_ATTACH
[attachment] = rest
view.attachBattle(attachment, done)
when Protocol.POKEMON_UNATTACH
[player, slot, attachment] = rest
view.unattachPokemon(player, slot, attachment, done)
when Protocol.TEAM_UNATTACH
[player, attachment] = rest
view.unattachTeam(player, attachment, done)
when Protocol.BATTLE_UNATTACH
[attachment] = rest
view.unattachBattle(attachment, done)
when Protocol.INITIALIZE
# TODO: Handle non-team-preview
[teams] = rest
@receiveTeams(teams)
view.preloadImages()
if !@get('spectating')
PokeBattle.notifyUser(PokeBattle.NotificationTypes.BATTLE_STARTED, @id)
done()
when Protocol.START_BATTLE
view.removeTeamPreview()
view.renderBattle()
done()
when Protocol.REARRANGE_TEAMS
arrangements = rest
@get('teams').forEach (team, i) ->
team.rearrange(arrangements[i])
done()
when Protocol.RECEIVE_TEAM
[team] = rest
@receiveTeam(team)
done()
when Protocol.SPRITE_CHANGE
[player, slot, newSpecies, newForme] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('species', newSpecies)
pokemon.set('forme', newForme)
view.changeSprite(player, slot, newSpecies, newForme, done)
when Protocol.NAME_CHANGE
[player, slot, newName] = rest
pokemon = @getPokemon(player, slot)
view.changeName(player, slot, newName, done)
when Protocol.BOOSTS
[player, slot, deltaBoosts] = rest
view.boost(player, slot, deltaBoosts, floatText: true)
done()
when Protocol.SET_BOOSTS
[player, slot, boosts] = rest
view.setBoosts(player, slot, boosts)
done()
when Protocol.RESET_BOOSTS
[player, slot] = rest
view.resetBoosts(player, slot)
done()
when Protocol.MOVESET_UPDATE
[player, slot, movesetJSON] = rest
pokemon = @getPokemon(player, slot)
pokemon.set(movesetJSON)
done()
when Protocol.WEATHER_CHANGE
[newWeather] = rest
view.changeWeather(newWeather, done)
when Protocol.TEAM_PREVIEW
view.renderTeamPreview()
done()
when Protocol.ACTIVATE_ABILITY
[player, slot, ability] = rest
pokemon = @getPokemon(player, slot)
pokemon.set('ability', ability)
view.activateAbility(player, slot, ability, done)
when Protocol.END_MOVE
doneSpeedTimeout()
when Protocol.ILLUSION_CHANGE
[player, slot, change] = rest
pokemon = @getPokemon(player, slot)
pokemon.setIllu(change)
done()
when Protocol.VISIBLE_TEAM
view.setVisibleTeam()
else
done()
catch e
console.error(e)
console.error(e.stack)
done()
if wasAtBottom && !view.chatView.isAtBottom()
view.chatView.scrollToBottom()

View File

@@ -0,0 +1,2 @@
@PokeBattle ?= {}
@PokeBattle.mixins ?= {}

View File

@@ -0,0 +1,85 @@
class Teams extends Backbone.Collection
model: Team
class @Battle extends Backbone.AssociatedModel
relations: [
type: Backbone.Many
key: 'teams'
relatedModel: Team
collectionType: Teams
]
defaults:
spectating: true
finished: false
_.extend(this.prototype, PokeBattle.mixins.BattleProtocolParser)
initialize: (attributes) =>
@updateQueue = []
{@numActive, spectators} = attributes
@spectators = new UserList(spectators) unless !spectators
@set('generation', Formats[@get('format')].generation)
@set('notifications', 0)
@set('turn', 0)
@set('teams', [{hidden: true}, {hidden: true}])
@set('spectating', !@has('index'))
@set('index', Math.floor(2 * Math.random())) unless @has('index')
receiveTeams: (receivedTeams) =>
teams = @get('teams')
for receivedTeam, i in receivedTeams
receivedTeam.hidden = true
team = teams.at(i)
team.set(receivedTeam) if team.get('hidden')
receiveTeam: (team) =>
teams = @get('teams')
teams.at(@get('index')).unset('hidden', silent: true).set(team)
makeMove: (moveName, forSlot, callback) =>
pokemon = @getPokemon(@get('index'), forSlot)
options = {}
options['megaEvolve'] = pokemon.get('megaEvolve') if pokemon.get('megaEvolve')
PokeBattle.primus.send(
'sendMove', @id, moveName, forSlot,
@get('turn'), options, callback,
)
makeSwitch: (toSlot, forSlot, callback) =>
PokeBattle.primus.send(
'sendSwitch', @id, toSlot, forSlot, @get('turn'), callback
)
makeCancel: =>
PokeBattle.primus.send 'sendCancelAction', @id, @get('turn')
arrangeTeam: (arrangement) =>
PokeBattle.primus.send 'arrangeTeam', @id, arrangement
switch: (fromIndex, toIndex) =>
you = @getTeam().pokemon
[you[fromIndex], you[toIndex]] = [you[toIndex], you[fromIndex]]
getTeam: (playerIndex = @get('index')) =>
@get("teams").at(playerIndex)
getOpponentTeam: (playerIndex = @get('index')) =>
@get("teams").at(1 - playerIndex)
getPokemon: (playerIndex, slot = 0) =>
team = @getTeam(playerIndex)
team.at(slot)
isPlaying: =>
!@get('finished') && !@get('spectating')
forfeit: =>
PokeBattle.primus.send('forfeit', @id)
# TODO: Opponent switch. Use some logic to determine whether the switch is
# to a previously seen Pokemon or a new Pokemon. In the latter case, we
# should reveal a previously unknown Pokeball if it's not a Wi-Fi battle.
notify: =>
@set('notifications', @get('notifications') + 1)

View File

@@ -0,0 +1,350 @@
class @Pokemon extends Backbone.Model
defaults: =>
moves: []
percent: 100
ivs:
hp: 31
attack: 31
defense: 31
specialAttack: 31
specialDefense: 31
speed: 31
evs:
hp: 0
attack: 0
defense: 0
specialAttack: 0
specialDefense: 0
speed: 0
initialize: (attributes={}) ->
# History lesson: We stored species under `name`. Now that we support
# nicknames, we need the `name` freed up. However, teams are saved to the
# server using the old scheme. Therefore we need to do a simple check for
# the existence of `species`; if it exists, do nothing. If not, use `name`.
@set('species', @get('name')) if !@has('species') && @has('name')
@set('forme', 'default') unless @has('forme')
@set('illu', false)
@normalizeStats(@get('ivs'), 31)
@normalizeStats(@get('evs'), 0)
@resetBoosts()
@isNull = false
# Skip teambuilder-specific properties.
return if @get('teambuilder') != true
@on 'change:ivs', (model, ivs)=>
type = HiddenPower.BW.type(ivs).toLowerCase()
@set("hiddenPowerType", type, silent: true)
@on 'change:hiddenPowerType', (model, type) =>
hpIVs = HiddenPower.BW.ivs[type.toLowerCase()]
ivs = @get('ivs')
for stat, iv of ivs
ivs[stat] = hpIVs[stat] || 31
@set('ivs', ivs, silent: true)
@set('ability', @getAbilities()[0]) unless attributes.ability
@set('level', 100) unless attributes.level
@set('happiness', 100) if isNaN(attributes.happiness)
@set('nature', 'Hardy') unless attributes.nature
hiddenPowerType = HiddenPower.BW.type(@get('ivs')).toLowerCase()
@set('hiddenPowerType', hiddenPowerType, silent: true)
# If there is no gender set and only one possiblity, set the gender
unless @has('gender')
genders = @getGenders()
@set('gender', genders[0], silent: true) if genders.length == 1
resetBoosts: ->
@set 'stages',
hp: 0
attack: 0
defense: 0
specialAttack: 0
specialDefense: 0
speed: 0
accuracy: 0
evasion: 0
normalizeStats: (hash, defaultValue) ->
stats = [ "hp", "attack", "defense", "specialAttack",
"specialDefense", "speed"]
for stat in stats
hash[stat] ?= defaultValue
getName: ->
sanitizedName = $('<div/>').text(@get('name')).html()
sanitizedName = sanitizedName.replace(
/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
sanitizedName
getGeneration: (generation) ->
gen = generation || @collection?.generation || DEFAULT_GENERATION
gen = gen.toUpperCase()
window.Generations[gen]
getSpecies: ->
@getGeneration().SpeciesData[@get('species')]
getItem: ->
@getGeneration().ItemData[@get('item')]
getForme: (forme, generation) ->
forme ||= @get('forme')
@getGeneration(generation).FormeData[@get('species')]?[forme]
getFormes: ->
(forme for forme of @getGeneration().FormeData[@get('species')])
# Returns all non-battle only formes
getSelectableFormes: ->
_(@getFormes()).reject((forme) => @getForme(forme).isBattleOnly)
# Returns all mega formes
getMegaFormes: ->
_(@getFormes()).reject((forme) => forme.indexOf('mega') != 0)
getAbilities: ->
forme = @getForme()
abilities = _.clone(forme.abilities)
abilities.push(forme.hiddenAbility) if forme.hiddenAbility
_.unique(abilities)
getGenders: ->
species = @getSpecies()
genders = []
switch species.genderRatio
when -1
genders.push("Genderless")
when 0
genders.push("M")
when 8
genders.push("F")
else
genders.push("M", "F")
genders
hasSelectedMove: (moveName) ->
moveName && moveName in @moves
getMovepool: ->
{SpeciesData, MoveData} = @getGeneration()
generation = GENERATION_TO_INT[@collection?.generation || DEFAULT_GENERATION]
learnset = learnableMoves(window.Generations, @attributes, generation)
# Map each move name to a move object
return _(learnset).map (moveName) ->
console.log(moveName)
move = _(MoveData[moveName]).clone()
move['name'] = moveName
move
getTotalEVs: (options = {}) ->
total = 0
for stat, value of @get("evs")
total += value if stat != options.exclude
total
getTeam: =>
@collection?.parents[0]
setIv: (stat, value) ->
ivs = _.clone(@get("ivs"))
ivs[stat] = value
@set("ivs", ivs) # trigger change event
setEv: (stat, value) ->
evs = _.clone(@get("evs"))
value = value - (value % 4)
evs[stat] = value
@set("evs", evs) # trigger change event
value
iv: (stat) ->
@get("ivs")[stat] ? 31
ev: (stat) ->
@get("evs")[stat] ? 0
natureBoost: (stat) ->
nature = @get('nature')?.toLowerCase()
if nature of natures
natures[nature][stat] || 1
else
1
base: (key) ->
forme = @getForme()
base = forme["stats"][key]
stat: (key) ->
base = @base(key)
return 1 if base == 1 # For Shedinja. key doesn't have to be hp.
level = @get('level') || 100
iv = @iv(key)
ev = Math.floor(@ev(key) / 4)
total = if key == 'hp'
Math.floor((2 * base + iv + ev) * (level / 100) + level + 10)
else
Math.floor(((2 * base + iv + ev) * (level / 100) + 5) * @natureBoost(key))
# Returns the natures that this pokemon can use
# The natures are returned as a list of [id, value] values
# to populate a dropdown field.
# TODO: Should this be needed in more places, return Nature objects instead
getNatures: ->
natureResults = []
for nature, stats of natures
name = nature[0].toUpperCase() + nature.substr(1)
invertedStats = _(stats).invert()
label = name
if invertedStats[PLUS]
# This nature has an effect, so update the label
plusStat = statAbbreviations[invertedStats[PLUS]]
minusStat = statAbbreviations[invertedStats[MINUS]]
label = "#{name} (+#{plusStat}, -#{minusStat})"
natureResults.push [name, label]
return natureResults
getPBV: ->
gen = @getGeneration()
PokeBattle.PBV.determinePBV(gen, @attributes)
setPP: (moveIndex, newPP) ->
array = _.clone(@get('pp'))
array[moveIndex] = newPP
@set('pp', array)
getPercentHP: ->
Math.max(@get('percent'), 0)
getHPColor: ->
percent = @getPercentHP()
switch
when percent <= 20 then 'red'
when percent <= 50 then 'yellow'
else 'green'
isFainted: ->
@get('percent') <= 0
getStatus: ->
status = @get('status')
if status
"#{status[0].toUpperCase()}#{status.substr(1)}"
else
"Healthy"
canMegaEvolve: ->
# TODO: Refactor this to use getPossibleMegaForme()
# I didn't feel like making the change and testing it while implementing getPossibleMegaForme()
item = @getItem()
return false if item.type != 'megastone'
[ species, forme ] = item.mega
return false if @get('species') != species || @get('forme') != 'default'
return true
getPossibleMegaForme: ->
item = @getItem()
return null if item?.type != 'megastone'
[ species, forme ] = item.mega
return forme if @get('species') == species && @get('forme') == 'default'
return null
# Returns the complete web address to the pokedex link for this pokemon.
# For this project, this leads to our website at http://www.pokebattle.com,
# but if you want it to lead somewhere else, edit this function.
getPokedexUrl: ->
# todo: move this function to /shared, or use an actual slugify library
slugify = (str) ->
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
slugSpecies = slugify(@get('species'))
slugForme = slugify(@get('forme'))
"//pokebattle.com/dex/pokemon/#{slugSpecies}/#{slugForme}"
getIllu: ->
@get('illu')
setIllu: (y) ->
@set('illu', y)
toJSON: ->
attributes = _.clone(@attributes)
delete attributes.gender if attributes.gender == 'Genderless'
delete attributes.hiddenPowerType
delete attributes.teambuilder
attributes
# TODO: These shortenings really should be stored somewhere else.
statAbbreviations =
'hp' : 'HP'
'attack' : 'Atk'
'defense' : 'Def'
'specialAttack' : 'SAtk'
'specialDefense' : 'SDef'
'speed' : 'Spe'
# A hash that keys a nature with the stats that it boosts.
# Neutral natures are ignored.
# TODO: .yml-ify these.
PLUS = 1.1
MINUS = 0.9
natures =
lonely: {attack: PLUS, defense: MINUS}
brave: {attack: PLUS, speed: MINUS}
adamant: {attack: PLUS, specialAttack: MINUS}
naughty: {attack: PLUS, specialDefense: MINUS}
bold: {defense: PLUS, attack: MINUS}
relaxed: {defense: PLUS, speed: MINUS}
impish: {defense: PLUS, specialAttack: MINUS}
lax: {defense: PLUS, specialDefense: MINUS}
timid: {speed: PLUS, attack: MINUS}
hasty: {speed: PLUS, defense: MINUS}
jolly: {speed: PLUS, specialAttack: MINUS}
naive: {speed: PLUS, specialDefense: MINUS}
modest: {specialAttack: PLUS, attack: MINUS}
mild: {specialAttack: PLUS, defense: MINUS}
quiet: {specialAttack: PLUS, speed: MINUS}
rash: {specialAttack: PLUS, specialDefense: MINUS}
calm: {specialDefense: PLUS, attack: MINUS}
gentle: {specialDefense: PLUS, defense: MINUS}
sassy: {specialDefense: PLUS, speed: MINUS}
careful: {specialDefense: PLUS, specialAttack: MINUS}
hardy: {}
docile: {}
serious: {}
bashful: {}
quirky: {}
class @NullPokemon extends Pokemon
initialize: ->
@set('species', null)
@set('forme', 'default')
@isNull = true
getNatures: -> []
getPBV: -> 0
base: -> 0
stat: -> null
iv: -> null
ev: -> null
getSpecies: ->
id: 0
genderRatio: -1
generation: 1
getForme: ->
@getFormes()['default']
getFormes: ->
default:
abilities: []
hiddenAbility: null
isBattleOnly: false
learnset: {}
types: []

View File

@@ -0,0 +1,128 @@
class PokemonCollection extends Backbone.Collection
model: (attrs, options) =>
# History lesson: We stored species under `name`. Now that we support
# nicknames, we need the `name` freed up. However, teams are saved to the
# server using the old scheme. Therefore we need to do a simple check for
# the existence of `species`; if it exists, do nothing. If not, use `name`.
if attrs.name || attrs.species
attrs.teambuilder = @parents[0].get('teambuilder')
return new Pokemon(attrs, options)
else
return new NullPokemon()
class @Team extends Backbone.AssociatedModel
relations: [
type: Backbone.Many
key: 'pokemon'
collectionType: PokemonCollection
]
initialize: (attrs={}, options={}) =>
@owner = attrs.owner
@set('generation', DEFAULT_GENERATION) unless attrs.generation
@set('teambuilder', true) if options.teambuilder
@set('pokemon', []) unless attrs.pokemon
getName: =>
@get('name') || "Untitled team"
toJSON: =>
json = {}
json['id'] = @id if @id
json['name'] = @get('name')
json['generation'] = @get('generation')
json['pokemon'] = @get('pokemon').toJSON()
json
# Returns the pokemon at a particular index. Delegates to the internal pokemon collection
at: (idx) => @get('pokemon').at(idx)
# Returns which index the pokemon is in
indexOf: (idx) => @get('pokemon').indexOf(idx)
# Replace a pokemon at a particular index for another
replace: (idx, newPokemon) =>
@get('pokemon').remove(@get('pokemon').at(idx))
@get('pokemon').add(newPokemon, at: idx)
# Equivalent to toJSON, but omits NullPokemon
toNonNullJSON: =>
id: @id
name: @get('name')
generation: @get('generation')
pokemon: @get('pokemon')
.reject((pokemon) -> pokemon.isNull)
.map((pokemon) -> pokemon.toJSON())
clone: =>
attrs = _(@attributes).clone()
attrs.pokemon = @get('pokemon').toJSON()
new Team(attrs)
rearrange: (arrangement) ->
pokemon = @get('pokemon')
pokemon.reset((pokemon.models[index] for index in arrangement))
return true
getFormat: =>
format = @get('generation') # TODO: Migrate to format
format = DEFAULT_FORMAT if format not of Formats
Formats[format]
getGeneration: (generation) ->
gen = generation || @getFormat().generation
gen = gen.toUpperCase()
window.Generations[gen]
getPBV: =>
gen = @getGeneration()
pokemon = @get('pokemon').toJSON()
PokeBattle.PBV.determinePBV(gen, pokemon)
getMaxPBV: =>
{conditions} = @getFormat()
if Conditions.PBV_1000 in conditions
1000
else if Conditions.PBV_500 in conditions
500
else
0
hasPBV: =>
@getMaxPBV() > 0
getNonNullPokemon: =>
@get('pokemon').where(isNull: false)
hasNonNullPokemon: =>
@get('pokemon').some((pokemon) -> not pokemon.isNull)
sync: (method) =>
switch method
when 'create', 'patch', 'update'
@set('saving', true, silent: true)
@trigger('remoteSync', this)
PokeBattle.primus.send 'saveTeam', @toJSON(), (id) =>
# Note: If this model is saved multiple times, then this won't
# tell you if some of the saves failed.
@set('id', id)
@set('saving', false, silent: true)
@trigger('remoteSync', this)
when 'delete'
PokeBattle.primus.send('destroyTeam', @id) if @id
getRandomOrder: =>
order = @get('randomorder')
if typeof order is 'undefined'
@setRandomOrder()
order = @get('randomorder')
order
else
order = @get('randomorder')
order
setRandomOrder: =>
#random order for the icons with team preview when in battle
team = @get('pokemon').toJSON()
teamshuffled = _.shuffle(team)
@set('randomorder', teamshuffled)

View File

@@ -0,0 +1,50 @@
MAX_LOG_LENGTH = 50
class @PrivateMessage extends Backbone.Model
initialize: =>
@loadLog()
@set('notifications', 0)
add: (username, message, opts = {}) =>
@set('notifications', @get('notifications') + 1) if username == @id
@trigger("receive", this, @id, username, message, opts)
log = @get('log')
log.push({username, message, opts})
# Trim the log size. Use 2x log length to reduce how often this happens
if log.length > (2 * MAX_LOG_LENGTH)
log.splice(0, log.length - MAX_LOG_LENGTH)
@saveLog()
openChallenge: (args...) =>
@trigger("openChallenge", args...)
cancelChallenge: (args...) =>
@trigger("cancelChallenge", args...)
closeChallenge: (args...) =>
@trigger("closeChallenge", args...)
getLog: =>
log = @get('log')
if log.length > 50
log.splice(0, log.length - 50)
return log
loadLog: =>
try
log = JSON.parse(window.localStorage.getItem(@logKey())) || []
@set('log', log)
catch
@set('log', [])
saveLog: =>
try
window.localStorage.setItem(@logKey(), JSON.stringify(@getLog()))
logKey: =>
key = [ @id, PokeBattle.username ]
key.sort()
key.join(':')

View File

@@ -0,0 +1,20 @@
class @Room extends Backbone.AssociatedModel
relations: [
type: Backbone.Many
key: 'users'
relatedModel: 'User'
collectionType: 'UserList'
]
EVENTS: "userMessage rawMessage announce clear setTopic".split(/\s+/)
for eventName in this::EVENTS
do (eventName) =>
this::[eventName] = (args...) ->
@trigger(eventName, args...)
sendChat: (message) ->
return false unless message?.replace(/\s+$/).length > 0
if !PokeBattle.commands.execute(this, message)
PokeBattle.primus.send('sendChat', @id, message)
return true

View File

@@ -0,0 +1,16 @@
AuthorityMap =
"1": ""
"2": "+"
"3": "%"
"4": "@"
"5": "~"
class @User extends Backbone.Model
initialize: (attributes) =>
getDisplayName: =>
authorityString = AuthorityMap[@get('authority')] ? ""
"#{authorityString}#{@get('id')}"
isAlt: =>
@get('isAlt')

View File

@@ -0,0 +1,19 @@
class @Replay extends Backbone.Model
urlRoot: '/replays'
getFormat: ->
window.Formats[@get('format')].humanName
getCreatedAt: ->
date = new Date(@get('created_at'))
day = date.getDate()
month = date.getMonth() + 1
year = date.getFullYear()
timeOfDay = 'a.m.'
hours = date.getHours()
minutes = "00#{date.getMinutes()}"[-2...]
seconds = "00#{date.getSeconds()}"[-2...]
if hours > 12
hours = hours % 12
timeOfDay = 'p.m.'
"#{year}/#{month}/#{day} #{hours}:#{minutes}:#{seconds} #{timeOfDay}"

View File

@@ -0,0 +1,14 @@
class @BattleListView extends Backbone.View
template: JST['battle_list']
initialize: (attributes) =>
@battles = []
@render()
refreshList: =>
PokeBattle.primus.send "getBattleList", (battles) =>
@battles = _(battles).sortBy((battle) => battle[3])
@render()
render: =>
@$el.html @template(battles: @battles)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
class @ChatView extends Backbone.View
template: JST['chat']
userListTemplate: JST['user_list']
events:
'click': 'focusChat'
'keydown .chat_input': 'handleKeys'
'click .chat_input_send': 'sendChat'
'scroll_to_bottom': 'scrollToBottom'
MAX_USERNAME_HISTORY = 10
MAX_MESSAGES_LENGTH = 500
# Takes a `model`, which is a Room instance.
initialize: (options) =>
{@noisy} = options
if @model
@listenTo(@model.get('users'), 'add remove reset', @renderUserList)
if @noisy
@listenTo(@model.get('users'), 'add', @userJoin)
@listenTo(@model.get('users'), 'remove', @userLeave)
for eventName in Room::EVENTS
callback = this[eventName] || throw new Error("ChatView must implement #{eventName}.")
@listenTo(@model, eventName, callback)
@chatHistory = []
@mostRecentNames = []
@tabCompleteIndex = -1
@tabCompleteNames = []
# Sets the channel topic
setTopic: (topic) =>
topic = @sanitize(topic)
@rawMessage("<div class='alert alert-info'><b>Topic:</b> #{topic}</div>")
render: =>
@$el.html @template()
if @model
@$el.removeClass('without_spectators')
@$el.removeClass('without_chat_input')
@renderUserList()
this
renderUserList: =>
@$('.user_count').text "Users (#{@model.get('users').length})"
@$('.users').html @userListTemplate(userList: @model.get('users').models)
this
getSelectedText: =>
text = ""
if window.getSelection
text = window.getSelection().toString()
else if document.selection && document.selection.type != "Control"
text = document.selection.createRange().text
return text
focusChat: =>
selectedText = @getSelectedText()
@$('.chat_input').focus() if selectedText.length == 0
sendChat: =>
$this = @$('.chat_input')
message = $this.val()
if @model.sendChat(message)
@chatHistory.push(message)
delete @chatHistoryIndex
$this.val('')
tabComplete: ($input, options = {}) =>
cursorIndex = $input.prop('selectionStart')
text = $input.val()
if @tabCompleteNames.length > 0 && @tabCompleteCursorIndex == cursorIndex
if options.reverse
@tabCompleteIndex -= 1
if @tabCompleteIndex < 0
@tabCompleteIndex = @tabCompleteNames.length - 1
else
@tabCompleteIndex = (@tabCompleteIndex + 1) % @tabCompleteNames.length
else
delete @tabCompleteCursorIndex
pieces = text[0...cursorIndex].split(' ')
possibleName = pieces.pop()
rest = pieces.join(' ')
rest += ' ' if pieces.length > 0 # Append a space if a word exists
length = possibleName.length
return if length == 0
candidates = _.union(@mostRecentNames, @model.get('users').pluck('id'))
candidates = candidates.filter (name) ->
name[...length].toLowerCase() == possibleName.toLowerCase()
return if candidates.length == 0
if options.reverse
@tabCompleteIndex = candidates.length - 1
else
@tabCompleteIndex = 0
@tabCompleteNames = candidates
@tabCompletePrefix = rest
@tabCompleteCursorIndex = cursorIndex
tabbedName = @tabCompleteNames[@tabCompleteIndex]
newPrefix = @tabCompletePrefix + tabbedName
newPrefixLength = newPrefix.length
$input.val(newPrefix + text[cursorIndex...])
$input[0].setSelectionRange(newPrefixLength, newPrefixLength)
@tabCompleteCursorIndex = newPrefixLength
handleKeys: (e) =>
$input = $(e.currentTarget)
switch e.which
when 13 # [Enter]
e.preventDefault()
@sendChat()
when 9 # [Tab]
e.preventDefault()
@tabComplete($input, reverse: e.shiftKey)
when 38 # [Up arrow]
e.preventDefault()
return if @chatHistory.length == 0
if !@chatHistoryIndex?
@chatHistoryIndex = @chatHistory.length
@chatHistoryText = $input.val()
if @chatHistoryIndex > 0
@chatHistoryIndex -= 1
$input.val(@chatHistory[@chatHistoryIndex])
when 40 # [Down arrow]
e.preventDefault()
return unless @chatHistoryIndex?
@chatHistoryIndex += 1
if @chatHistoryIndex == @chatHistory.length
$input.val(@chatHistoryText)
delete @chatHistoryIndex
else
$input.val(@chatHistory[@chatHistoryIndex])
userMessage: (username, message) =>
user = @model.get('users').get(username)
displayName = user?.getDisplayName() || username
yourName = PokeBattle.username
highlight = (new RegExp("\\b#{yourName}\\b", 'i').test(message))
# Render the chat message
u = "<b class='open_pm fake_link' data-user-id='#{username}'
style='color: #{@userColor(username)}'>#{displayName}:</b>"
@rawMessage("#{@timestamp()} #{u} #{@sanitize(message)}", {highlight})
# We might want to run something based on the message, e.g. !pbv from a mod.
@handleMessage(user, message)
# Record last few usernames who chatted
index = @mostRecentNames.indexOf(username)
@mostRecentNames.splice(index, 1) if index != -1
@mostRecentNames.push(username)
@mostRecentNames.shift() if @mostRecentNames.length > MAX_USERNAME_HISTORY
userColor: (username) =>
# Same hashing algorithm as in Java
hash = 0
for c, i in username
chr = username.charCodeAt(i)
hash = ((hash << 5) - hash) + chr
hash |= 0
h = hash % 360
hash /= 360
s = (hash % 25) + 75
l = 50
"hsl(#{h}, #{s}%, #{l}%)"
handleMessage: (user, message) =>
authority = user?.get('authority')
printableCommands = ['/pbv', '/data']
# TODO: no magic constants. '1' is a regular user.
if authority > 1 && message.split(/\s/, 1)[0] in printableCommands
PokeBattle.commands.execute(@model, message)
userJoin: (user) =>
@rawMessage("#{@timestamp()} #{user.id} joined!")
userLeave: (user) =>
@rawMessage("#{@timestamp()} #{user.id} left!")
rawMessage: (message, options = {}) =>
wasAtBottom = @isAtBottom()
klass = []
klass.push('bg-blue') if options.highlight
klass.push(options.class) if options.class
@print("<p class='chat_message #{klass.join(' ')}'>#{message}</p>")
@cleanChat()
if wasAtBottom then @scrollToBottom()
cleanChat: =>
$messages = @$('.chat_message')
numToRemove = ($messages.length - MAX_MESSAGES_LENGTH)
if numToRemove > 0
$messages.slice(0, numToRemove).remove()
announce: (klass, message) =>
wasAtBottom = @isAtBottom()
message = @linkify(message)
@print("<div class='alert alert-#{klass} clearfix'>#{message}</div>")
if wasAtBottom then @scrollToBottom()
print: (message) =>
@$('.messages').append(message)
clear: =>
@$('.messages').empty()
timestamp: =>
date = new Date()
hours = date.getHours()
minutes = date.getMinutes()
seconds = date.getSeconds()
minutes = "00#{minutes}".substr(-2)
seconds = "00#{seconds}".substr(-2)
"<span class='monospace'>[#{hours}:#{minutes}:#{seconds}]</span>"
# Escapes all HTML, but also converts links to clickable links.
sanitize: (message) =>
sanitizedMessage = $('<div/>').text(message).html()
@linkify(sanitizedMessage)
linkify: (message) =>
message = URI.withinString message, (url) ->
uri = URI(url)
[host, path] = [uri.host(), uri.path()]
battleRegex = /^\/battles\/([a-fA-F0-9]+)$/i
$a = $("<a/>").prop('href', url).prop('target', '_blank').text(url)
if host == URI(window.location.href).host() && battleRegex.test(path)
battleId = path.match(battleRegex)[1]
$a.addClass('spectate').attr('data-battle-id', battleId)
return $a.wrap("<div/>").parent().html()
message
# Returns true if the chat is scrolled to the bottom of the screen.
# This also returns true if the messages are hidden.
isAtBottom: =>
$el = @$('.messages')
($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight())
scrollToBottom: =>
messages = @$('.messages')[0]
messages.scrollTop = messages.scrollHeight
false

View File

@@ -0,0 +1,259 @@
class @PrivateMessagesView extends Backbone.View
messageTemplate: JST['private_message']
events:
"keypress .chat_input" : "keyPressEvent"
"keyup .chat_input" : "keyUpEvent"
"click .challenge_button, .cancel_challenge" : "toggleChallengeEvent"
"click .popup_messages" : "focusChatEvent"
"click .title_minimize" : "minimizePopupEvent"
"click .title_close" : "closePopupEvent"
"challenge .popup" : "sendChallengeEvent"
"cancelChallenge .popup" : "challengeCanceledEvent"
"focus .popup" : "focusPopupEvent"
initialize: =>
@listenTo(@collection, 'open', @createPopup)
@listenTo(@collection, 'focus', @focusPopup)
@listenTo(@collection, 'receive', @receiveMessage)
@listenTo(@collection, 'close', @closePopup)
@listenTo(@collection, 'minimize', @minimizePopup)
@listenTo(@collection, 'show', @showPopup)
@listenTo(@collection, 'openChallenge', @openChallenge)
@listenTo(@collection, 'cancelChallenge', @cancelChallenge)
@listenTo(@collection, 'closeChallenge', @closeChallenge)
@listenTo(@collection, 'focus show', @resetNotifications)
# @listenTo(PokeBattle.userList, 'add', @notifyJoin)
# @listenTo(PokeBattle.userList, 'remove', @notifyLeave)
createPopup: (message) =>
title = id = message.id
$html = @$findPopup(id)
if @$findPopup(id).length == 0
$html = $(@messageTemplate({window, id, title}))
@$el.append($html)
@positionPopup($html, @$(".popup:visible").length - 1)
@addLogMessages($html, message.getLog())
$html
focusPopup: (message) =>
id = message.id
$popup = @$findPopup(id)
$popup.find('.chat_input').focus()
closePopup: (message) =>
username = message.id
@$findPopup(username).remove()
@repositionPopups()
minimizePopup: (message) =>
username = message.id
$popup = @$findPopup(username)
$popup.addClass('hidden')
@repositionPopups()
showPopup: (message) =>
username = message.id
$popup = @$findPopup(username)
@$el.append($popup)
$popup.removeClass('hidden')
@scrollToBottom($popup)
@repositionPopups()
addMessage: ($popup, message) =>
$messages = $popup.find('.popup_messages')
$messages.append(message)
# todo: make this and receiveMessage construct messages from a common source
addLogMessages: ($popup, log) =>
messageHtml = ""
for {username, message, opts} in log
message = _.escape(message)
username = "Me" if username == PokeBattle.username
if opts.type in [ 'error', 'alert' ]
messageHtml += "<p class='grey'>#{message}</p>"
else
messageHtml += "<p class='grey'><strong>#{username}:</strong> #{message}</p>"
@addMessage($popup, messageHtml)
@scrollToBottom($popup)
# todo: make this and addLogMessages construct messages from a common source
receiveMessage: (messageModel, messageId, username, message, options) =>
message = _.escape(message)
$popup = @$findOrCreatePopup(messageId)
wasAtBottom = @isAtBottom($popup)
username = "Me" if username == PokeBattle.username
if options.type == 'error'
@addMessage($popup, "<p class='red italic'>#{message}</p>")
else if options.type == 'alert'
@addMessage($popup, "<p class='yellow italic'>#{message}</p>")
else
if username != "Me" && !$popup.find('.chat_input').is(":focus")
$popup.addClass('new_message')
PokeBattle.notifyUser(PokeBattle.NotificationTypes.PRIVATE_MESSAGE, username)
else
@resetNotifications(messageModel)
@addMessage($popup, "<p><strong>#{username}:</strong> #{message}</p>")
if wasAtBottom then @scrollToBottom($popup)
openChallenge: (messageId, generation, conditions) =>
$popup = @$findOrCreatePopup(messageId)
$challenge = @createChallenge($popup, generation, conditions)
if generation
$challenge.find('.is_not_challenger').addClass('hidden')
$challenge.find('.is_challenger').removeClass('hidden')
cancelChallenge: (messageId) =>
$popup = @$findOrCreatePopup(messageId)
$challenge = $popup.find('.challenge')
$challenge.find('.icon-spinner').addClass('hidden')
$challenge.find('.send_challenge, .select').removeClass('disabled')
$challenge.find('.challenge_text').text("Challenge")
$challenge.find(".cancel_challenge").text('Close')
closeChallenge: (messageId) =>
$popup = @$findOrCreatePopup(messageId)
$challenge = $popup.find('.challenge')
$challenge.addClass('hidden')
$popup.find('.popup_messages').removeClass('small')
resetNotifications: (message) =>
message.set('notifications', 0)
notifyJoin: (user) =>
message = @collection.get(user.id)
return unless @isOpen(message)
message?.add(user.id, "#{user.id} is now online!", type: "alert")
notifyLeave: (user) =>
message = @collection.get(user.id)
return unless @isOpen(message)
message?.add(user.id, "#{user.id} is now offline.", type: "alert")
isOpen: (message) =>
message && @$findPopup(message.id).length > 0
# Returns true if the chat is scrolled to the bottom of the screen.
# This also returns true if the messages are hidden.
isAtBottom: ($popup) =>
$el = $popup.find('.popup_messages')
($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight())
scrollToBottom: ($popup) =>
messages = $popup.find('.popup_messages')[0]
return unless messages
messages.scrollTop = messages.scrollHeight
false
positionPopup: ($popup, index) =>
leftOffset = $('#content').position().left
$popup.css(left: leftOffset + index * $popup.outerWidth(true))
repositionPopups: =>
@$(".popup:visible").each (index, self) =>
@positionPopup($(self), index)
$findPopup: (id) =>
@$(".popup[data-user-id='#{id}']")
$findOrCreatePopup: (messageId) =>
$popup = @$findPopup(messageId)
$popup = @createPopup(@collection.get(messageId)) if $popup.length == 0
$popup
$closestPopup: (target) =>
$target = $(target)
return $target if $target.hasClass("popup")
return $target.closest(".popup")
messageFromPopup: (target) =>
$popup = @$closestPopup(target)
message = @collection.get($popup.data('user-id'))
return message
createChallenge: ($popup, generation, conditions) =>
$challenge = $popup.find('.challenge')
$challenge.html(JST['challenge']())
createChallengePane
eventName: "challenge"
button: $popup.find('.send_challenge')
acceptButton: $popup.find('.accept_challenge')
rejectButton: $popup.find('.reject_challenge')
populate: $popup.find(".challenge_data")
generation: generation
personId: $popup.data('user-id')
defaultClauses: conditions || [
Conditions.TEAM_PREVIEW
Conditions.PBV_1000
Conditions.SLEEP_CLAUSE
Conditions.EVASION_CLAUSE
Conditions.SPECIES_CLAUSE
Conditions.OHKO_CLAUSE
Conditions.PRANKSTER_SWAGGER_CLAUSE
Conditions.UNRELEASED_BAN
]
blockedClauses: conditions? || [Conditions.RATED_BATTLE]
$popup.find('.popup_messages').addClass('small')
$challenge.removeClass('hidden')
$challenge
##########
# EVENTS #
##########
keyPressEvent: (e) =>
switch e.which
when 13 # [ Enter ]
$input = $(e.currentTarget)
message = @messageFromPopup(e.currentTarget)
text = $input.val()
return if text.length == 0
PokeBattle.primus.send('privateMessage', message.id, text)
$input.val('')
keyUpEvent: (e) =>
switch e.which
when 27 # [ Esc ]
@closePopupEvent(e)
minimizePopupEvent: (e) =>
message = @messageFromPopup(e.currentTarget)
message.trigger('minimize', message)
closePopupEvent: (e) =>
message = @messageFromPopup(e.currentTarget)
message.trigger('close', message)
focusChatEvent: (e) =>
@$closestPopup(e.currentTarget).find('input').focus()
toggleChallengeEvent: (e) =>
$popup = @$closestPopup(e.currentTarget)
$challenge = $popup.find('.challenge')
wasAtBottom = @isAtBottom($popup)
if $challenge.hasClass("hidden")
@createChallenge($popup)
else if $challenge.find('.cancel_challenge').text() == 'Cancel'
$popup.find('.send_challenge').click()
else
@closeChallenge(@messageFromPopup($popup))
if wasAtBottom then @scrollToBottom($popup)
sendChallengeEvent: (e) =>
$popup = @$closestPopup(e.currentTarget)
$challenge = $popup.find('.challenge')
$challenge.find(".icon-spinner").removeClass('hidden')
$challenge.find(".challenge_text").text('Challenging...')
$challenge.find(".cancel_challenge").text('Cancel')
challengeCanceledEvent: (e) =>
message = @messageFromPopup(e.currentTarget)
message.trigger('cancelChallenge', message.id)
focusPopupEvent: (e) =>
$popup = @$closestPopup(e.currentTarget)
$popup.removeClass('new_message')
@resetNotifications(@collection.get($popup.data('user-id')))

View File

@@ -0,0 +1,34 @@
class @ReplayView extends Backbone.View
replayTemplate: JST['replay']
events:
'click .delete-replay': 'deleteReplay'
render: =>
@$el.empty()
templates = @collection.map((replay) => @replayTemplate({window, replay}))
groups = for x in [0...templates.length] by 3
_.compact([ templates[x], templates[x + 1], templates[x + 2] ])
for groupHTML in groups
$row = $('<div/>').addClass('row-fluid')
$row.append(groupHTML)
$row.appendTo(@$el)
if @collection.length == 0
@$el.append($("<p/>").text("You have not saved any replays."))
this
deleteReplay: (e) =>
return unless confirm("Do you really want to delete this replay?")
$target = $(e.currentTarget)
$spinner = $target.closest('.clickable-box').find('.show_spinner')
$spinner.removeClass('hidden')
cid = $target.data('cid')
replay = @collection.get(cid)
replay
.destroy()
.complete(@render)

View File

@@ -0,0 +1,188 @@
class @SidebarView extends Backbone.View
template: JST['navigation']
events:
"click .logo" : "focusLobbyEvent"
"click .nav_rooms li" : 'focusRoomEvent'
"click .nav_battles li" : 'focusBattleEvent'
"click .nav_messages li": 'focusMessageEvent'
"click .nav_battles .close" : 'leaveRoomEvent'
"click .nav_messages .close" : 'closeMessageEvent'
"click .nav_teambuilder": 'showTeambuilder'
"click .nav_battle_list": 'showBattleList'
initialize: (attributes) =>
@currentWindow = null
@listenTo(PokeBattle.battles, 'add', @addBattle)
@listenTo(PokeBattle.battles, 'remove', @removeBattle)
@listenTo(PokeBattle.battles, 'reset', @resetBattles)
@listenTo(PokeBattle.battles, 'change:notifications', @renderNotifications)
@listenTo(PokeBattle.messages, 'open receive', @addMessage)
@listenTo(PokeBattle.messages, 'close', @removeMessage)
@listenTo(PokeBattle.messages, 'reset', @resetMessages)
@listenTo(PokeBattle.messages, 'change:notifications', @renderMessageNotifications)
@render()
showTeambuilder: =>
@changeWindowTo($("#teambuilder-section"), $(".nav_teambuilder"))
showBattleList: =>
@changeWindowTo($("#battle-list-section"), $(".nav_battle_list"))
PokeBattle.battleList.refreshList()
render: =>
@$el.html @template(battles: PokeBattle.battles)
renderNotifications: (battle) =>
$notifications = @$("[data-battle-id='#{battle.id}'] .notifications")
# We don't want to display notifications if this window is already focused.
if @currentWindow.data('battle-id') == battle.id
battle.set('notifications', 0, silent: true)
$notifications.addClass('hidden')
return
# Show notification count.
notificationCount = battle.get('notifications')
if notificationCount > 0
$notifications.text(notificationCount)
$notifications.removeClass('hidden')
else
$notifications.addClass('hidden')
addBattle: (battle) =>
@$(".header_battles, .nav_battles").removeClass("hidden")
$li = $("""<li class="nav_item fake_link" data-battle-id="#{battle.id}">
<div class="nav_meta">
<div class="notifications hidden">0</div>
<div class="close">&times;</div>
</div>#{battle.get('playerIds').join(' VS ')}</li>""")
$li.appendTo(@$('.nav_battles'))
$li.click()
removeBattle: (battle) =>
$navItems = @$(".nav_item")
$battle = @$(".nav_item[data-battle-id='#{battle.id}']")
index = $navItems.index($battle)
$battle.remove()
if PokeBattle.battles.size() == 0
@$(".header_battles, .nav_battles").addClass('hidden')
PokeBattle.navigation.focusLobby()
else
$next = $navItems.eq(index).add($navItems.eq(index - 1))
$next.first().click()
resetBattles: (battles) =>
for battle in battles
@addBattle(battle)
addMessage: (message) =>
# This event can trigger on already opened messages, so we need to verify
return if @$(".nav_item[data-message-id='#{message.id}']").length
@$(".header_messages, .nav_messages").removeClass("hidden")
$li = $("""<li class="nav_item fake_link" data-message-id="#{message.id}">
<div class="nav_meta">
<div class="notifications hidden">0</div>
<div class="close">&times;</div>
</div>#{message.id}</li>""")
$li.appendTo(@$('.nav_messages'))
@renderMessageNotifications(message)
removeMessage: (message) =>
@$(".nav_item[data-message-id='#{message.id}']").remove()
# If there are no messages, remove the header
# Note: We can't check the collection directly since messages are never actually removed from it
if @$('.nav_messages li').length == 0
@$(".header_messages").addClass("hidden")
resetMessages: (messages) =>
@addMessage(message) for message in messages
renderMessageNotifications: (message) =>
$notifications = @$("[data-message-id='#{message.id}'] .notifications")
notificationCount = message.get('notifications')
if notificationCount > 0
$notifications.text(notificationCount)
$notifications.removeClass('hidden')
else
$notifications.addClass('hidden')
focusLobby: =>
# TODO: Clean this up once rooms are implemented
# right now it duplicates part of focusRoom()
$lobbyLink = @$(".nav_rooms li").first()
@resetNotifications($lobbyLink)
$room = $('.chat_window')
@changeWindowTo($room, $lobbyLink)
PokeBattle.router.navigate("")
leaveRoomEvent: (e) =>
$navItem = $(e.currentTarget).closest('.nav_item')
battleId = $navItem.data('battle-id')
battle = PokeBattle.battles.get(battleId)
if battle.isPlaying()
return if !confirm("Are you sure you want to forfeit this battle?")
battle.forfeit()
PokeBattle.battles.remove(battle)
false
closeMessageEvent: (e) =>
$navItem = $(e.currentTarget).closest('.nav_item')
messageId = $navItem.data('message-id')
message = PokeBattle.messages.get(messageId)
message.trigger('close', message)
focusBattleEvent: (e) =>
$this = $(e.currentTarget)
@resetNotifications($this)
battleId = $this.data('battle-id')
@changeWindowToBattle(battleId)
focusLobbyEvent: (e) =>
@focusLobby()
focusRoomEvent: (e) =>
$this = $(e.currentTarget)
@resetNotifications($this)
# TODO: Remove hardcoding once rooms are implemented
$room = $('.chat_window')
@changeWindowTo($room, $this)
PokeBattle.router.navigate("")
focusMessageEvent: (e) =>
$navItem = $(e.currentTarget).closest('.nav_item')
messageId = $navItem.data('message-id')
message = PokeBattle.messages.get(messageId)
message.trigger('show', message)
message.trigger('focus', message)
changeWindowTo: ($toSelector, $navItem) =>
# Show window, hide others
$mainContent = $('#main-section')
$mainContent.children().addClass("hidden")
@currentWindow = $toSelector.first()
@currentWindow.removeClass("hidden")
@currentWindow.find('.chat').trigger('scroll_to_bottom')
# Add .active to navigation, remove from others
@$('.nav_item').removeClass('active')
$navItem.addClass('active')
changeWindowToBattle: (battleId) =>
$battle = $(""".battle_window[data-battle-id='#{battleId}']""")
$navItem = @$("[data-battle-id='#{battleId}']")
@changeWindowTo($battle, $navItem)
PokeBattle.router.navigate("battles/#{battleId}")
resetNotifications: ($link) =>
$link = $link.first()
$link = $link.closest('li') if $link[0].tagName != 'li'
if battleId = $link.data('battle-id')
battle = PokeBattle.battles.get(battleId)
battle.set('notifications', 0)

View File

@@ -0,0 +1,484 @@
isMobileOrAndroid = ->
return true if /Mobile/i.test(window.navigator.userAgent)
return true if /Android/i.test(window.navigator.userAgent)
return false
# helper which attaches selectize
attachSelectize = ($element, options) ->
# Block selectize on mobile and all android operating systems (All androids are blocked due to a bug)
return if isMobileOrAndroid()
$element.selectize(options)
setSelectizeValue = ($element, value) ->
if isMobileOrAndroid()
$element.val(value)
else
$element.each ->
@selectize?.setValue(value)
setSelectizeDisabled = ($element, disabled) ->
$element.filter(".selectized").each ->
return unless @selectize
if disabled then @selectize.disable() else @selectize.enable()
class @PokemonEditView extends Backbone.View
editTemplate: JST['teambuilder/pokemon']
speciesTemplate: JST['teambuilder/species']
nonStatsTemplate: JST['teambuilder/non_stats']
movesTemplate: JST['teambuilder/moves']
events:
'change .sortSpecies': 'changeSort'
'change .species_list': 'changeSpecies'
'change .selected_nickname': 'changeNickname'
'click .selected_shininess': 'changeShiny'
'click .selected_happiness': 'changeHappiness'
'change .selected-forme': 'changeForme'
'change .selected_nature': 'changeNature'
'change .selected_ability': 'changeAbility'
'change .selected_item': 'changeItem'
'change .selected_gender': 'changeGender'
'change .selected_level': 'changeLevel'
'change .iv-entry': 'changeIv'
'focus .ev-entry': 'focusEv'
'blur .ev-entry': 'changeEv'
'change .ev-entry': 'changeEv'
'input .ev-entry[type=range]': 'changeEv' # fix for firefox
'change .select-hidden-power': 'changeHiddenPower'
'keydown .selected_moves input': 'keydownMoves'
'blur .selected_moves input': 'blurMoves'
'click .table-moves tbody tr': 'clickMoveName'
'mousedown .table-moves': 'preventBlurMoves'
'click .move-button': 'clickSelectedMove'
'click .move-button .close': 'removeSelectedMove'
initialize: (attributes={}) =>
@onPokemonChange = attributes.onPokemonChange
setFormat: (format) =>
format = Formats[format] || Formats[DEFAULT_FORMAT]
@setGeneration(format.generation)
# TODO: Set PBV limit based on conditions
changeSort:(e) =>
sort = $(e.currentTarget).val()
console.log(sort)
if sort =="Default Sort"
@sortSpecieslist("Default")
else if sort == "Sort by Dexnumber"
@sortSpecieslist("id", false)
else if sort == "Invert by Dexnumber"
@sortSpecieslist("id", true)
else if sort == "Sort Alphabetically"
@sortSpecieslist("pokename", false)
else if sort == "Invert Alphabetically"
@sortSpecieslist("pokename", true)
sortSpecieslist: (option, reverse) =>
{MoveData, SpeciesData, ItemData} = @generation
if option == "Default"
sortedlist = @getSpecies
else
sortedlist = @sortObject(SpeciesData, option, reverse)
@speciesList = (species for species, data of sortedlist)
@render()
sortObject: (data, option, reverse) ->
arr = []
for key, val of data
val.pokename = key
arr.push(val)
arr = _.sortBy(arr, option)
if reverse == true
arr.reverse()
newobj = {}
for thing in arr
newobj[thing.pokename] = thing
finished = newobj
setGeneration: (generation) =>
@generation = window.Generations[generation.toUpperCase()]
{MoveData, SpeciesData, ItemData} = @generation
@moveData = MoveData
@speciesList = (species for species, data of SpeciesData)
# TODO: filter irrelevant items
@itemList = (_(itemName for itemName, data of ItemData).sort())
@render()
setPokemon: (pokemon) =>
# Stop listening for change events on the previously set pokemon
@stopListening(@pokemon) if @pokemon
@pokemon = pokemon
@listenTo(pokemon, 'change:level', @renderStats)
@listenTo(pokemon, 'change:ivs', @renderStats)
@listenTo(pokemon, 'change:evs', @renderStats)
@listenTo(pokemon, 'change:nature', @renderStats)
@listenTo(pokemon, 'change:hiddenPowerType', @renderStats)
@listenTo(pokemon, 'change:shiny', @renderSpecies)
@renderPokemon()
setTeamPBV: (pbv) =>
@teamPBV = pbv
changeSpecies: (e) =>
return if not @onPokemonChange
species = $(e.currentTarget).val()
@pokemon = if species
new Pokemon(teambuilder: true, species: species)
else
new NullPokemon()
@onPokemonChange(@pokemon)
changeNickname: (e) =>
$input = $(e.currentTarget)
@pokemon.set("name", $input.val())
changeShiny: (e) =>
$switch = $(e.currentTarget).toggleClass("selected")
@pokemon.set("shiny", $switch.is(".selected"))
changeHappiness: (e) =>
$switch = $(e.currentTarget).toggleClass("selected")
happiness = if $switch.is(".selected") then 0 else 100
@pokemon.set("happiness", happiness)
changeForme: (e) =>
$forme = $(e.currentTarget)
@pokemon.set('forme', $forme.val())
# Forme changes may have different abilities, so we have to change this.
@pokemon.set('ability', @pokemon.getAbilities()[0])
changeNature: (e) =>
$list = $(e.currentTarget)
@pokemon.set("nature", $list.val())
changeAbility: (e) =>
$list = $(e.currentTarget)
@pokemon.set("ability", $list.val())
changeItem: (e) =>
$list = $(e.currentTarget)
@pokemon.set("item", $list.val())
changeGender: (e) =>
$list = $(e.currentTarget)
@pokemon.set("gender", $list.val())
changeLevel: (e) =>
$input = $(e.currentTarget)
value = parseInt($input.val(), 10)
value = 1120 if isNaN(value) || value > 120
value = 1 if value < 1
$input.val(value)
@pokemon.set("level", value)
changeIv: (e) =>
# todo: make changeIv and changeEv DRY
$input = $(e.currentTarget)
stat = $input.data("stat")
value = parseInt($input.val(), 10)
if isNaN(value) || value > 31 || value < 0
value = 31
@pokemon.setIv(stat, value)
focusEv: (e) =>
$input = $(e.currentTarget)
return if $input.is("[type=range]")
value = parseInt($input.val(), 10)
$input.val("") if value == 0
changeEv: (e) =>
# todo: make changeIv and changeEv DRY
$input = $(e.currentTarget)
stat = $input.data("stat")
value = parseInt($input.val(), 10)
value = 252 if value > 252
value = 0 if isNaN(value) || value < 0
value = @pokemon.setEv(stat, value)
$input.val(value) if not $input.is("[type=range]")
changeHiddenPower: (e) =>
$input = $(e.currentTarget)
type = $input.val()
@pokemon.set('hiddenPowerType', type.toLowerCase())
# Prevents the blurMoves event from activating for the duration of
# the remaining javascript events. This allows the click event to not fire
# the blur event.
preventBlurMoves: (e) =>
@_preventBlur = true
_.defer =>
@_preventBlur = false
blurMoves: (e) =>
$input = $(e.currentTarget)
if @_preventBlur
previousScrollPosition = @$el.scrollTop()
$input.focus()
e.preventDefault()
@$el.scrollTop(previousScrollPosition) # prevent scroll from refocus
return
$selectedMove = @$selectedMove()
moveName = $selectedMove.data('move-id')
# Remove filtering and row selection
@filterMovesBy("")
$(".table-moves .active").removeClass("active")
if $input.val().length == 0
@recordMoves()
else
@insertMove($input, moveName)
clickMoveName: (e) =>
$this = $(e.currentTarget)
moveName = $this.data('move-id')
$moves = @$el.find('.selected_moves')
$input = $moves.find('input:focus').first()
$input = $moves.find('input').first() if $input.length == 0
return if $input.length == 0
@insertMove($input, moveName)
insertMove: ($input, moveName) =>
currentScrollPosition = @$el.scrollTop()
@preventBlurMoves()
return if !@buttonify($input, moveName)
$moves = @$el.find('.selected_moves')
$firstInput = $moves.find('input').first()
if $firstInput.length > 0
$firstInput.focus()
@$el.scrollTop(currentScrollPosition)
else
@$el.scrollTop(0)
@recordMoves()
recordMoves: =>
movesArray = []
$moves = @$el.find('.selected_moves')
$moves.find('.move-button').each ->
moveName = $(this).find("span").text().trim()
if moveName != ""
movesArray.push(moveName)
@pokemon.set("moves", movesArray)
$selectedMove: =>
$table = @$el.find('.table-moves')
$allMoves = $table.find('tbody tr')
$allMoves.filter('.active').first()
clickSelectedMove: (e) =>
$this = $(e.currentTarget)
moveName = $this.find('span').text()
$input = $("<input type='text' value='#{moveName}'/>")
$this.replaceWith($input)
$input.focus().select()
# Set the current move row to active
$(".table-moves tr[data-move-id='#{moveName}']").addClass("active")
removeSelectedMove: (e) =>
$this = $(e.currentTarget).parent()
$input = $("<input type='text'/>")
$this.replaceWith($input)
$input.focus()
e.stopPropagation()
buttonify: ($input, moveName) =>
return false if moveName not of @moveData
# The blur event may have been cancelled, so when removing the input also
# remove the filter
if $input.is(":focus")
@filterMovesBy("")
$(".table-moves .active").removeClass("active")
type = @moveData[moveName].type.toLowerCase()
$input.replaceWith("""
<div class="button move-button #{type}"><span>#{moveName}</span><div class='close'>&times;</div></div>
""")
return true
keydownMoves: (e) =>
$input = $(e.currentTarget)
$table = @$el.find('.table-moves')
$allMoves = $table.find('tbody tr')
switch e.which
when 13 # [Enter]; we're selecting the active move.
$activeMove = @$selectedMove()
$activeMove.click()
when 38 # [Up arrow]; selects move above
$activeMove = $allMoves.filter('.active').first()
$prevMove = $activeMove.prevAll(":visible").first()
if $prevMove.length > 0
$activeMove.removeClass('active')
$prevMove.addClass('active')
when 40 # [Down arrow]; selects move below
$activeMove = $allMoves.filter('.active').first()
$nextMove = $activeMove.nextAll(":visible").first()
if $nextMove.length > 0
$activeMove.removeClass('active')
$nextMove.addClass('active')
else
# Otherwise we're filtering moves
# We defer since $input may not have updated yet
_.defer =>
return unless $input.is(":focus")
moveName = $input.val()
@filterMovesBy(moveName)
filterMovesBy: (moveName) =>
moveName = moveName.replace(/\s+|-/g, "")
$table = @$el.find('.table-moves')
$allMoves = $table.find('tbody tr')
moveRegex = new RegExp(moveName, "i")
$moves = $allMoves.filter ->
$move = $(this)
moveName = $move.data('move-search-id')
moveRegex.test(moveName)
$table.addClass('hidden')
$moves.removeClass('hidden')
$allMoves.not($moves).addClass('hidden')
$allMoves.removeClass('active')
$moves.first().addClass('active')
$table.removeClass('hidden')
disableEventsAndExecute: (callback) =>
isOutermost = !@_eventsDisabled
@_eventsDisabled = true
@undelegateEvents() if isOutermost # disable events
callback()
@delegateEvents() if isOutermost
@_eventsDisabled = false if isOutermost
render: =>
@$el.html @editTemplate(window: window, speciesList: @speciesList, itemList: @itemList)
attachSelectize(@$el.find(".species_list"),
render:
option: (item, escape) =>
pbv = PokeBattle.PBV.determinePBV(@generation, species: item.value)
return "<div class='clearfix'>#{item.text}<div class='pbv'>#{pbv}</div></div>"
)
attachSelectize(@$el.find(".selected_item"))
return this
renderPokemon: =>
@renderSpecies()
@renderNonStats()
@renderStats()
@renderMoves()
@renderPBV()
# Disable entering values if this is a NullPokemon
$elements = @$el.find("input, select").not(".species input, .species select")
$elements.prop("disabled", @pokemon.isNull)
setSelectizeDisabled($elements, @pokemon.isNull)
return this
renderPBV: =>
individualPBV = @pokemon.getPBV()
@$(".individual-pbv").text(individualPBV)
team = @pokemon.getTeam()
if team && team.hasPBV()
pbv = team.getPBV()
maxPBV = team.getMaxPBV()
@$(".total-pbv").text(pbv).toggleClass("red", pbv > maxPBV)
@$(".max-pbv").text(maxPBV)
renderSpecies: =>
@disableEventsAndExecute =>
setSelectizeValue(@$(".species_list"), @pokemon.get("species"))
html = if @pokemon.isNull then "" else @speciesTemplate(window: window, pokemon: @pokemon)
@$(".species-info").html(html)
@$(".selected_shininess").toggleClass("selected", @pokemon.get('shiny') == true)
@$(".selected_happiness").toggleClass("selected", @pokemon.get("happiness") == 0)
renderNonStats: =>
$nonStats = @$el.find(".non-stats")
populateSelect = (searchStr, valueTextPairs, selectedValue) ->
$select = $nonStats.find(searchStr).empty()
for pair in valueTextPairs
value = text = pair
if pair instanceof Array
value = pair[0]
text = pair[1]
$select.append($("<option>").attr("value", value).text(text))
$select.val(selectedValue)
displayedGenders =
F: "Female"
M: "Male"
Genderless: "Genderless"
@disableEventsAndExecute =>
genders = ([g, displayedGenders[g]] for g in @pokemon.getGenders())
$nonStats.find(".selected_nickname").val(@pokemon.get("name"))
populateSelect ".selected_ability", @pokemon.getAbilities(), @pokemon.get("ability")
populateSelect ".selected_nature", @pokemon.getNatures(), @pokemon.get("nature")
setSelectizeValue(@$(".selected_item"), @pokemon.get("item"))
populateSelect ".selected_gender", genders, @pokemon.get("gender")
$nonStats.find(".selected_level").val(@pokemon.get("level"))
renderStats: =>
pokemon = @pokemon
@$(".iv-entry").each ->
$input = $(this)
stat = $input.data("stat")
$input.val(pokemon.iv(stat))
@$(".ev-entry").each ->
return if $(this).is(":focus")
$input = $(this)
stat = $input.data("stat")
$input.val(pokemon.ev(stat))
@$('.base-stat').each ->
$this = $(this)
stat = $this.data("stat")
$this.text(pokemon.base(stat))
@$('.stat-total').each ->
$this = $(this)
stat = $this.data("stat")
$this.text(pokemon.stat(stat))
$this.removeClass('plus-nature minus-nature')
if pokemon.natureBoost(stat) > 1
$this.addClass('plus-nature')
$this.text($this.text() + '+')
if pokemon.natureBoost(stat) < 1
$this.addClass('minus-nature')
$this.text($this.text() + '-')
remainingEvs = 508 - @pokemon.getTotalEVs()
@$('.remaining-evs-amount')
.text(remainingEvs)
.toggleClass("over-limit", remainingEvs < 0)
@$('.select-hidden-power').val(@pokemon.get('hiddenPowerType'))
renderMoves: =>
# TODO: Cache the resultant html
$moveSection = @$el.find(".moves-section")
if @pokemon.isNull
$moveSection.html ""
return
$moveSection.html @movesTemplate(window: window, pokemon: @pokemon)
$moveSection.find('.selected_moves input').each (i, el) =>
$this = $(el)
moveName = $this.val()
@buttonify($this, moveName)

View File

@@ -0,0 +1,301 @@
class @TeambuilderView extends Backbone.View
template: JST['teambuilder/main']
teamTemplate: JST['teambuilder/team']
teamsTemplate: JST['teambuilder/teams']
pokemonListTemplate: JST['teambuilder/pokemon_list']
events:
# Team view
'click .add-new-team': 'addNewTeamEvent'
'click .export-team': 'exportTeam'
'click .clone-team': 'cloneTeam'
'click .delete-team': 'deleteTeamEvent'
'click .go-to-team': 'clickTeam'
'click .import-team': 'renderImportTeamModal'
# Teambuild view
'click .change-format-dropdown a': 'changeTeamFormat'
'blur .team_name': 'blurTeamName'
'keypress .team_name': 'keypressTeamName'
'click .go_back': 'goBackToOverview'
'click .pokemon_list li': 'clickPokemon'
'click .add_pokemon': 'addNewPokemonEvent'
'click .save_team': 'saveTeam'
initialize: (attributes) =>
@selectedPokemon = 0
@selectedTeam = null
@render()
@listenTo(PokeBattle.TeamStore, 'reset', @resetTeams)
@listenTo(PokeBattle.TeamStore, 'add', @addNewTeam)
@listenTo(PokeBattle.TeamStore, 'remove', @deleteTeam)
@listenTo(PokeBattle.TeamStore, 'change:id', @changeTeamId)
@listenTo(PokeBattle.TeamStore, 'reset', @renderTeams)
@listenTo(PokeBattle.TeamStore, 'saving', @renderSaving)
@listenTo(PokeBattle.TeamStore, 'saved', @renderSaved)
@listenTo PokeBattle.TeamStore, 'render', (team) =>
@renderTeams()
if @getSelectedTeam() && team.id == @getSelectedTeam().id
@setSelectedTeam(team)
@pokemonEditView = new PokemonEditView(
el: @$('.pokemon_edit')
onPokemonChange: (newPokemon) =>
team = @getSelectedTeam()
team.replace(@selectedPokemon, newPokemon)
@renderPBV()
)
clickTeam: (e) =>
$team = $(e.currentTarget).closest('.select-team')
team = PokeBattle.TeamStore.get($team.data('cid'))
@setSelectedTeam(team)
clickPokemon: (e) =>
$listItem = $(e.currentTarget)
index = @$('.pokemon_list li').index($listItem)
@setSelectedPokemonIndex(index)
attachEventsToTeam: (team) =>
return if team.attachedTeambuildEvents
@listenTo(team, 'add:pokemon', @renderPokemon)
# Todo: Make this perform better
@listenTo(team, 'change:pokemon[*].species change:pokemon[*].forme', (pokemon) =>
@renderPokemonList()
@renderPokemon(pokemon)
)
@listenTo(team, 'add:pokemon remove:pokemon', @renderPokemonList)
@listenTo(team, 'reset:pokemon', (=> @changeTeam(team)))
@listenTo(team, 'change nested-change reset:pokemon add:pokemon remove:pokemon', @dirty)
@listenTo(team, 'change:pokemon[*] reset:pokemon add:pokemon remove:pokemon', @renderPBV)
# A temporary flag to attach until the teambuilder view is refactored
team.attachedTeambuildEvents = true
addEmptyPokemon: (team) =>
team.get('pokemon').add(new NullPokemon())
addNewTeamEvent: (e) =>
team = new Team()
PokeBattle.TeamStore.add(team)
team.save()
addNewTeam: (team) =>
@addEmptyPokemon(team) while team.get('pokemon').length < 6
@$('.teambuilder_teams').append @teamTemplate({team, window})
@attachEventsToTeam(team)
resetTeams: (teamStore) =>
teamStore.forEach (team) =>
@attachEventsToTeam(team)
cloneTeam: (e) =>
$team = $(e.currentTarget).closest('.select-team')
cid = $team.data('cid')
clone = @getTeam(cid).clone().set("id", null)
PokeBattle.TeamStore.add(clone)
clone.save()
return false
deleteTeamEvent: (e) =>
return false if !confirm("Do you really want to delete this team?")
$team = $(e.currentTarget).closest('.select-team')
team = @getTeam($team.data('cid'))
PokeBattle.TeamStore.remove(team)
team.destroy()
return false
deleteTeam: (team) =>
@$(".select-team[data-cid=#{team.cid}]").remove()
changeTeam: (team) =>
html = $(@teamTemplate({team, window})).html()
@$(".select-team[data-cid=#{team.cid}]").html(html)
changeTeamId: (team) =>
@$(".select-team[data-cid=#{team.cid}]").attr('data-id', team.id)
exportTeam: (e) =>
$team = $(e.currentTarget).closest('.select-team')
id = $team.data('id')
if not @getTeam(id).hasNonNullPokemon()
alert("You cannot export empty teams. Please add some pokemon first.")
return false
teamJSON = @getTeam(id).toNonNullJSON()
teamString = PokeBattle.exportTeam(teamJSON.pokemon)
$modal = PokeBattle.modal('modals/export_team')
$modal.find('.exported-team').val(teamString)
$modal.find('textarea, input').first().focus().select()
return false
addNewPokemonEvent: =>
@addNewPokemon(@getSelectedTeam())
addNewPokemon: (team) =>
@addEmptyPokemon(team)
@$('.pokemon_list li').last().click()
saveTeam: =>
clone = @getSelectedTeam()
team = PokeBattle.TeamStore.get(clone.id)
team.save(clone.toJSON(), silent: true)
@resetHeaderButtons()
changeTeamFormat: (e) =>
$link = $(e.currentTarget)
format = $link.data('format')
team = @getSelectedTeam()
if format != team.get('generation')
team.set('generation', format)
@renderTeam()
@dirty() # renderTeam() removes dirty, so call it again
setSelectedPokemonIndex: (index) =>
pokemon = @getSelectedTeam().at(index)
@selectedPokemon = index
# Render the pokemon
@pokemonEditView.setPokemon(pokemon)
@renderPokemon(pokemon)
# Set the correct list item to active
@$(".navigation li").removeClass("active")
@$(".navigation li").eq(index).addClass("active")
getSelectedPokemon: =>
@getSelectedTeam().at(@selectedPokemon)
setSelectedTeam: (team) =>
# Duplicate the team, so that changes don't stick until saved
@selectedTeam = team.clone()
@selectedTeam.id = team.id
@selectedTeam.cid = team.cid
@selectedPokemon = 0
@attachEventsToTeam(@selectedTeam)
@renderTeam()
getAllTeams: =>
PokeBattle.TeamStore.models
getSelectedTeam: =>
@selectedTeam
getTeam: (idx) =>
PokeBattle.TeamStore.get(idx)
blurTeamName: =>
teamName = @$('.team_name').text()
@getSelectedTeam().set('name', teamName)
keypressTeamName: (e) =>
if e.which == 13 # [Enter]
@$('.team_name').blur()
goBackToOverview: =>
@renderTeams()
dirty: =>
@$('.go_back').text('Discard changes')
@$('.save_team').removeClass('disabled')
resetHeaderButtons: =>
@$('.go_back').text('Back')
@$('.save_team').addClass('disabled')
render: =>
@$el.html @template(pokemon: @getSelectedTeam(), selected: @selectedPokemon)
@renderTeams()
renderTeams: =>
@$('.display_teams').html @teamsTemplate(teams: @getAllTeams(), window: window)
@$('.display_teams').removeClass('hidden')
@$('.display_pokemon').addClass('hidden')
this
renderTeam: =>
team = @getSelectedTeam()
@pokemonEditView.setFormat(team.get('generation') || DEFAULT_FORMAT)
@resetHeaderButtons()
@renderFormat()
@renderPokemonList()
@setSelectedPokemonIndex(@selectedPokemon)
@$('.team_name').text(team.getName())
@$('.display_teams').addClass('hidden')
@$('.display_pokemon').removeClass('hidden')
renderPokemonList: =>
team = @getSelectedTeam()
$pokemon_list = @$(".pokemon_list").empty()
$pokemon_list.html @pokemonListTemplate(window: window, pokemonList: team.get('pokemon').models)
$pokemon_list.find("li[data-pokemon-index=#{@selectedPokemon}]").addClass("active")
# NOTE: this isn't be used, and just amounts to hiding the button, however
# we may re-enable this functionality in the future
# Hide add pokemon if there's 6 pokemon
if team.length < 6
@$(".add_pokemon").show()
else
@$(".add_pokemon").hide()
renderPokemon: (pokemon) =>
@pokemonEditView.setPokemon(pokemon)
renderPBV: (pokemon) =>
if pokemon
individualPBV = pokemon.getPBV()
$listItem = @$(".pokemon_list li[data-pokemon-cid=#{pokemon.cid}]")
$listItem.find(".pbv-value").text(individualPBV)
totalPBV = @getSelectedTeam().getPBV()
@pokemonEditView.setTeamPBV(totalPBV)
@pokemonEditView.renderPBV()
renderFormat: =>
format = @getSelectedTeam().get("generation")
format = DEFAULT_FORMAT if format not of Formats
text = @$(".change-format-dropdown a[data-format='#{format}']").text()
@$(".current-format").text(text)
renderImportTeamModal: =>
$modal = PokeBattle.modal 'modals/import_team', ($modal) =>
$modal.on 'click', '.import-team-submit', (e) =>
teamString = $modal.find('.imported-team').val()
pokemonJSON = PokeBattle.parseTeam(teamString)
errors = @validateImportedTeam(pokemonJSON)
if errors.length > 0
listErrors = errors.map((e) -> "<li>#{e}</li>").join('')
$errors = $modal.find('.form-errors')
$errors.html("<ul>#{listErrors}</ul>").removeClass('hidden')
else
team = new Team(pokemon: pokemonJSON, teambuilder: true)
PokeBattle.TeamStore.add(team)
team.save()
$modal.find('.imported-team').val("")
$modal.modal('hide')
return false
$modal.find('.imported-team').first().focus()
validateImportedTeam: (json) =>
errors = []
pokemonSpecies = (pokemon.species for pokemon in json)
{SpeciesData} = window.Generations[DEFAULT_GENERATION.toUpperCase()]
pokemonSpecies = pokemonSpecies.filter((s) -> s not of SpeciesData)
if pokemonSpecies.length > 0
errors.push(pokemonSpecies.map((n) -> "#{n} is not a valid Pokemon.")...)
return errors
return errors
renderSaving: (team) =>
$team = $(".select-team[data-cid='#{team.cid}']")
$team.find('.show_spinner').removeClass('hidden')
renderSaved: (team) =>
$team = $(".select-team[data-cid='#{team.cid}']")
$team.find('.show_spinner').addClass('hidden')