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:
538
client/app/css/battle.styl
Normal file
538
client/app/css/battle.styl
Normal 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
|
||||
59
client/app/css/battle_list.styl
Normal file
59
client/app/css/battle_list.styl
Normal 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
100
client/app/css/buttons.styl
Normal 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
100
client/app/css/chat.styl
Normal 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
|
||||
54
client/app/css/colors.styl
Normal file
54
client/app/css/colors.styl
Normal 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
131
client/app/css/core.styl
Normal 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
103
client/app/css/header.styl
Normal 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
105
client/app/css/layout.styl
Normal 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
15
client/app/css/main.styl
Normal 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'
|
||||
34
client/app/css/main_buttons.styl
Normal file
34
client/app/css/main_buttons.styl
Normal 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
|
||||
110
client/app/css/messages.styl
Normal file
110
client/app/css/messages.styl
Normal 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
|
||||
93
client/app/css/miscellaneous.styl
Normal file
93
client/app/css/miscellaneous.styl
Normal 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
|
||||
113
client/app/css/navigation.styl
Normal file
113
client/app/css/navigation.styl
Normal 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
|
||||
122
client/app/css/responsive.styl
Normal file
122
client/app/css/responsive.styl
Normal 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
|
||||
457
client/app/css/teambuilder.styl
Normal file
457
client/app/css/teambuilder.styl
Normal 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
|
||||
46
client/app/js/client.coffee
Normal file
46
client/app/js/client.coffee
Normal 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)
|
||||
@@ -0,0 +1,8 @@
|
||||
class @BattleCollection extends Backbone.Collection
|
||||
model: Battle
|
||||
|
||||
isPlaying: ->
|
||||
@find((battle) -> battle.isPlaying())?
|
||||
|
||||
playingBattles: ->
|
||||
@filter((battle) -> battle.isPlaying())
|
||||
2
client/app/js/collections/chats/private_messages.coffee
Normal file
2
client/app/js/collections/chats/private_messages.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
class @PrivateMessages extends Backbone.Collection
|
||||
model: PrivateMessage
|
||||
9
client/app/js/collections/chats/rooms.coffee
Normal file
9
client/app/js/collections/chats/rooms.coffee
Normal 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)
|
||||
15
client/app/js/collections/chats/user_list.coffee
Normal file
15
client/app/js/collections/chats/user_list.coffee
Normal 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: =>
|
||||
2
client/app/js/collections/replays/replays.coffee
Normal file
2
client/app/js/collections/replays/replays.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
class @Replays extends Backbone.Collection
|
||||
model: Replay
|
||||
5
client/app/js/concerns/achievements.coffee
Normal file
5
client/app/js/concerns/achievements.coffee
Normal 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)
|
||||
10
client/app/js/concerns/alts.coffee
Normal file
10
client/app/js/concerns/alts.coffee
Normal 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)
|
||||
52
client/app/js/concerns/battle_consumer.coffee
Normal file
52
client/app/js/concerns/battle_consumer.coffee
Normal 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)
|
||||
2
client/app/js/concerns/battle_list.coffee
Normal file
2
client/app/js/concerns/battle_list.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
PokeBattle.primus.on 'battleList', (battles) ->
|
||||
PokeBattle.battleList.refreshListComplete(battles)
|
||||
290
client/app/js/concerns/commands.coffee
Normal file
290
client/app/js/concerns/commands.coffee
Normal 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 »"))
|
||||
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 »")}
|
||||
</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 || "—"
|
||||
acc = move.accuracy || "—"
|
||||
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 »")
|
||||
message
|
||||
|
||||
dataAbility = (abilityName) ->
|
||||
ability = window.Generations.XY.AbilityData[abilityName]
|
||||
message = """<b>#{abilityName}:</b> #{ability.description}
|
||||
#{linkToDex("abilities/#{slugify(abilityName)}",
|
||||
"See who obtains this ability »")}"""
|
||||
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>"""
|
||||
39
client/app/js/concerns/connections.coffee
Normal file
39
client/app/js/concerns/connections.coffee
Normal 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)
|
||||
57
client/app/js/concerns/errors.coffee
Normal file
57
client/app/js/concerns/errors.coffee
Normal 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}")
|
||||
7
client/app/js/concerns/exit_while_in_battle.coffee
Normal file
7
client/app/js/concerns/exit_while_in_battle.coffee
Normal 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
|
||||
4
client/app/js/concerns/logging.coffee
Normal file
4
client/app/js/concerns/logging.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
PokeBattle.primus.on 'data', (args...) ->
|
||||
try
|
||||
console.log(args...) if window.localStorage.debug == 'true'
|
||||
catch
|
||||
50
client/app/js/concerns/main_buttons.coffee
Normal file
50
client/app/js/concerns/main_buttons.coffee
Normal 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
|
||||
31
client/app/js/concerns/private_messages.coffee
Normal file
31
client/app/js/concerns/private_messages.coffee
Normal 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)
|
||||
21
client/app/js/concerns/router.coffee
Normal file
21
client/app/js/concerns/router.coffee
Normal 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)
|
||||
4
client/app/js/concerns/spectating.coffee
Normal file
4
client/app/js/concerns/spectating.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
$(document).on 'click', '.spectate', ->
|
||||
battleId = $(this).data('battle-id')
|
||||
PokeBattle.router.navigate("battles/#{battleId}", trigger: true)
|
||||
return false
|
||||
6
client/app/js/concerns/stale_assets.coffee
Normal file
6
client/app/js/concerns/stale_assets.coffee
Normal 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()
|
||||
57
client/app/js/concerns/tab_notifications.coffee
Normal file
57
client/app/js/concerns/tab_notifications.coffee
Normal 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 = []
|
||||
70
client/app/js/concerns/team_store.coffee
Normal file
70
client/app/js/concerns/team_store.coffee
Normal 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')
|
||||
9
client/app/js/concerns/toggle_navigation.coffee
Normal file
9
client/app/js/concerns/toggle_navigation.coffee
Normal 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")
|
||||
199
client/app/js/helpers/challenge_pane.coffee
Normal file
199
client/app/js/helpers/challenge_pane.coffee
Normal 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
|
||||
2281
client/app/js/helpers/images.coffee
Normal file
2281
client/app/js/helpers/images.coffee
Normal file
File diff suppressed because it is too large
Load Diff
15
client/app/js/helpers/modals.coffee
Normal file
15
client/app/js/helpers/modals.coffee
Normal 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
|
||||
262
client/app/js/helpers/team_parsing.coffee
Normal file
262
client/app/js/helpers/team_parsing.coffee
Normal 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
|
||||
17
client/app/js/helpers/time.coffee
Normal file
17
client/app/js/helpers/time.coffee
Normal 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)
|
||||
4
client/app/js/initializers/index.coffee
Normal file
4
client/app/js/initializers/index.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
@PokeBattle ?= {}
|
||||
PokeBattle.battles = null
|
||||
PokeBattle.events = {}
|
||||
_.extend(PokeBattle.events, Backbone.Events)
|
||||
3
client/app/js/initializers/loading_message.coffee
Normal file
3
client/app/js/initializers/loading_message.coffee
Normal file
@@ -0,0 +1,3 @@
|
||||
PokeBattle.events.once "ready", ->
|
||||
$loading = $(".loading-container")
|
||||
$loading.fadeOut(-> $loading.remove())
|
||||
@@ -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
|
||||
210
client/app/js/mixins/battles/battle_protocol_parser.coffee
Normal file
210
client/app/js/mixins/battles/battle_protocol_parser.coffee
Normal 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()
|
||||
2
client/app/js/mixins/index.coffee
Normal file
2
client/app/js/mixins/index.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
@PokeBattle ?= {}
|
||||
@PokeBattle.mixins ?= {}
|
||||
85
client/app/js/models/battles/battle.coffee
Normal file
85
client/app/js/models/battles/battle.coffee
Normal 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)
|
||||
350
client/app/js/models/battles/pokemon.coffee
Normal file
350
client/app/js/models/battles/pokemon.coffee
Normal 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: []
|
||||
128
client/app/js/models/battles/team.coffee
Normal file
128
client/app/js/models/battles/team.coffee
Normal 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)
|
||||
50
client/app/js/models/chats/private_message.coffee
Normal file
50
client/app/js/models/chats/private_message.coffee
Normal 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(':')
|
||||
20
client/app/js/models/chats/room.coffee
Normal file
20
client/app/js/models/chats/room.coffee
Normal 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
|
||||
16
client/app/js/models/chats/user.coffee
Normal file
16
client/app/js/models/chats/user.coffee
Normal 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')
|
||||
19
client/app/js/models/replays/replay.coffee
Normal file
19
client/app/js/models/replays/replay.coffee
Normal 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}"
|
||||
14
client/app/js/views/battle_list_view.coffee
Normal file
14
client/app/js/views/battle_list_view.coffee
Normal 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)
|
||||
1390
client/app/js/views/battles/battle_view.coffee
Normal file
1390
client/app/js/views/battles/battle_view.coffee
Normal file
File diff suppressed because it is too large
Load Diff
244
client/app/js/views/battles/chat_view.coffee
Normal file
244
client/app/js/views/battles/chat_view.coffee
Normal 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
|
||||
259
client/app/js/views/private_messages_view.coffee
Normal file
259
client/app/js/views/private_messages_view.coffee
Normal 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')))
|
||||
34
client/app/js/views/replays/replay_view.coffee
Normal file
34
client/app/js/views/replays/replay_view.coffee
Normal 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)
|
||||
188
client/app/js/views/sidebar_view.coffee
Normal file
188
client/app/js/views/sidebar_view.coffee
Normal 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">×</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">×</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)
|
||||
484
client/app/js/views/teambuilder/pokemon_edit_view.coffee
Normal file
484
client/app/js/views/teambuilder/pokemon_edit_view.coffee
Normal 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'>×</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)
|
||||
301
client/app/js/views/teambuilder/teambuilder_view.coffee
Normal file
301
client/app/js/views/teambuilder/teambuilder_view.coffee
Normal 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')
|
||||
Reference in New Issue
Block a user