From 735845746e54546b6b473d0dcfa8c38d23fcd2b9 Mon Sep 17 00:00:00 2001 From: Erik McClure Date: Wed, 28 Aug 2019 21:15:02 -0700 Subject: [PATCH] Archive commit --- .gitattributes | 8 + CHANGELOG.md | 520 + LICENSE_ART.txt | 364 + LICENSE_CODE.txt | 22 + LICENSE_MUSIC.txt | 334 + README.md | 240 + .../Halloween_Ghost_Anim/A_Ghost_Anim_1.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_2.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_3.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_4.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_5.gif | 3 + .../A_Ghost_Anim_5_body .gif | 3 + .../A_Ghost_Anim_5_hooves.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_6.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_7.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_8.gif | 3 + .../Halloween_Ghost_Anim/A_Ghost_Anim_9.gif | 3 + .../Halloween_Ghost_Anim/B_Ghost_Anim_1.gif | 3 + .../Halloween_Ghost_Anim/B_Ghost_Anim_2.gif | 3 + .../Halloween_Ghost_Anim/B_Ghost_Anim_3.gif | 3 + .../Halloween_Ghost_Anim/B_Ghost_Anim_4.gif | 3 + .../Halloween_Ghost_Anim/B_Ghost_Anim_5.gif | 3 + .../Halloween_Ghost_Anim/B_Ghost_Anim_6.gif | 3 + assets-source/animations/bat.psd | 3 + assets-source/animations/boat_wake.psd | 3 + assets-source/animations/bunny.psd | 3 + assets-source/animations/butterfly-4x1.png | 3 + assets-source/animations/cat.psd | 3 + assets-source/animations/firefly.psd | 3 + assets-source/animations/ghost1.psd | 3 + assets-source/animations/ghost1_hooves.psd | 3 + assets-source/animations/lantern.psd | 3 + assets-source/animations/magic.psd | 3 + assets-source/animations/magic2.psd | 3 + assets-source/animations/magic3.psd | 3 + assets-source/animations/pier_leg.psd | 3 + assets-source/animations/pony_wake/1.psd | 3 + assets-source/animations/pony_wake/2.psd | 3 + assets-source/animations/pony_wake/3.psd | 3 + assets-source/animations/pony_wake/4.psd | 3 + assets-source/animations/pony_wake/5.psd | 3 + assets-source/animations/pony_wake/6.psd | 3 + assets-source/animations/poof.psd | 3 + assets-source/animations/poof2-9x1.png | 3 + assets-source/animations/rain-4x1.png | 3 + assets-source/animations/raindrop-4x1.png | 3 + assets-source/animations/splash.psd | 3 + assets-source/animations/splash_boop.psd | 3 + assets-source/animations/torch.psd | 3 + assets-source/animations/torch2.psd | 3 + assets-source/animations/water_crystal/1.psd | 3 + assets-source/animations/water_crystal/2.psd | 3 + assets-source/animations/water_crystal/3.psd | 3 + assets-source/animations/water_rock/1.psd | 3 + assets-source/animations/water_rock/10.psd | 3 + assets-source/animations/water_rock/11.psd | 3 + assets-source/animations/water_rock/2.psd | 3 + assets-source/animations/water_rock/3.psd | 3 + assets-source/animations/water_rock/4.psd | 3 + assets-source/animations/water_rock/5.psd | 3 + assets-source/animations/water_rock/6.psd | 3 + assets-source/animations/water_rock/7.psd | 3 + assets-source/animations/water_rock/8.psd | 3 + assets-source/animations/water_rock/9.psd | 3 + assets-source/buttons/bubble-2.png | 3 + assets-source/buttons/bubble2-2.png | 3 + assets-source/emotes/emote_cry.psd | 3 + assets-source/emotes/emote_cry2.psd | 3 + assets-source/emotes/emote_hearts.psd | 3 + assets-source/emotes/emote_sleep1.psd | 3 + assets-source/emotes/emote_sleep1_flip.psd | 3 + assets-source/emotes/emote_sleep2.psd | 3 + assets-source/emotes/emote_sleep2_flip.psd | 3 + assets-source/emotes/emote_sneeze.psd | 3 + assets-source/emotes/emote_tears.psd | 3 + assets-source/emotes/hold_poof.psd | 3 + assets-source/fonts/cjk.psd | 3 + assets-source/fonts/emoji.psd | 3 + assets-source/fonts/flags.psd | 3 + assets-source/fonts/main.psd | 3 + assets-source/fonts/mono.psd | 3 + assets-source/fonts/tiny.psd | 3 + assets-source/icons/arrow_down.png | 3 + assets-source/icons/arrow_left.png | 3 + assets-source/icons/arrow_right.png | 3 + assets-source/icons/arrow_up.png | 3 + assets-source/icons/magic_icon.png | 3 + assets-source/lights/light1.png | 3 + assets-source/lights/light16.png | 3 + assets-source/lights/light24.png | 3 + assets-source/lights/light32.png | 3 + assets-source/lights/light6.png | 3 + .../lights/light_crystal_lantern.png | 3 + assets-source/lights/light_crystals_1.png | 3 + assets-source/lights/light_crystals_10.png | 3 + assets-source/lights/light_crystals_2.png | 3 + assets-source/lights/light_crystals_3.png | 3 + assets-source/lights/light_crystals_4.png | 3 + assets-source/lights/light_crystals_5.png | 3 + assets-source/lights/light_crystals_6.png | 3 + assets-source/lights/light_crystals_7.png | 3 + assets-source/lights/light_crystals_8.png | 3 + assets-source/lights/light_crystals_9.png | 3 + .../lights/light_crystals_cart_pile.png | 3 + assets-source/lights/light_crystals_held.png | 3 + assets-source/objects/acorn.psd | 3 + assets-source/objects/apple/1.psd | 3 + assets-source/objects/apple/2.psd | 3 + assets-source/objects/apple/palette_1_1.psd | 3 + assets-source/objects/banana.psd | 3 + assets-source/objects/barrel.psd | 3 + assets-source/objects/basket_bin.psd | 3 + assets-source/objects/bench/1.png | 3 + assets-source/objects/bench/1.psd | 3 + assets-source/objects/bench/back.png | 3 + assets-source/objects/bench/back.psd | 3 + assets-source/objects/bench/backh.png | 3 + assets-source/objects/bench/backh.psd | 3 + assets-source/objects/bench/backh2.png | 3 + assets-source/objects/bench/backh2.psd | 3 + assets-source/objects/bench/seat.png | 3 + assets-source/objects/bench/seat.psd | 3 + assets-source/objects/bench/seath.png | 3 + assets-source/objects/bench/seath.psd | 3 + assets-source/objects/boat.psd | 3 + assets-source/objects/boat_front.psd | 3 + assets-source/objects/boat_rope.psd | 3 + assets-source/objects/boat_sail.psd | 3 + assets-source/objects/bookshelf.psd | 3 + assets-source/objects/box/_base.psd | 3 + assets-source/objects/box/_gifts.psd | 3 + assets-source/objects/box/_lanterns.psd | 3 + assets-source/objects/box/baskets.psd | 3 + assets-source/objects/box/empty.psd | 3 + assets-source/objects/box/fruits.psd | 3 + assets-source/objects/box/gifts.psd | 3 + assets-source/objects/box/lanterns.psd | 3 + assets-source/objects/broom.psd | 3 + assets-source/objects/candy.psd | 3 + assets-source/objects/candy_cane/1.psd | 3 + assets-source/objects/candy_cane/2.psd | 3 + assets-source/objects/carrot/1.psd | 3 + assets-source/objects/carrot/1b.psd | 3 + assets-source/objects/carrot/2.psd | 3 + assets-source/objects/carrot/2b.psd | 3 + assets-source/objects/carrot/3.psd | 3 + assets-source/objects/carrot/4.psd | 3 + assets-source/objects/carrot/hold.psd | 3 + .../objects/carrot/palette_1_hold.psd | 3 + assets-source/objects/christmastree.psd | 3 + assets-source/objects/cloud.psd | 3 + assets-source/objects/clover/1.psd | 3 + assets-source/objects/clover/2.psd | 3 + assets-source/objects/clover/3.psd | 3 + assets-source/objects/clover/4.psd | 3 + assets-source/objects/clover/5.psd | 3 + assets-source/objects/clover/mouth.psd | 3 + assets-source/objects/clover/patch3.psd | 3 + assets-source/objects/clover/patch4.psd | 3 + assets-source/objects/clover/patch5.psd | 3 + assets-source/objects/clover/patch6.psd | 3 + assets-source/objects/clover/patch7.psd | 3 + assets-source/objects/clover/pick.psd | 3 + assets-source/objects/cookie.psd | 3 + assets-source/objects/cookie_pony.psd | 3 + assets-source/objects/cookie_table/1.psd | 3 + assets-source/objects/cookie_table/2.psd | 3 + assets-source/objects/crate/1.psd | 3 + assets-source/objects/crate/2.psd | 3 + assets-source/objects/crate/3.psd | 3 + assets-source/objects/crate/palette_1_1.psd | 3 + assets-source/objects/crystal_lantern.psd | 3 + assets-source/objects/crystals/1.psd | 3 + assets-source/objects/crystals/10.psd | 3 + assets-source/objects/crystals/2.psd | 3 + assets-source/objects/crystals/3.psd | 3 + assets-source/objects/crystals/4.psd | 3 + assets-source/objects/crystals/5.psd | 3 + assets-source/objects/crystals/6.psd | 3 + assets-source/objects/crystals/7.psd | 3 + assets-source/objects/crystals/8.psd | 3 + assets-source/objects/crystals/9.psd | 3 + assets-source/objects/crystals/cart_pile.psd | 3 + assets-source/objects/crystals/held.psd | 3 + assets-source/objects/cushion/1.psd | 3 + assets-source/objects/cushion/palette_1_1.psd | 3 + assets-source/objects/cushion/palette_2_1.psd | 3 + assets-source/objects/direction/down_left.psd | 3 + .../objects/direction/down_right.psd | 3 + .../objects/direction/left_right.psd | 3 + assets-source/objects/direction/pole_3.psd | 3 + assets-source/objects/direction/pole_4.psd | 3 + assets-source/objects/direction/pole_5.psd | 3 + .../objects/direction/shadow_down_left.psd | 3 + .../objects/direction/shadow_down_right.psd | 3 + .../objects/direction/shadow_left.psd | 3 + .../objects/direction/shadow_right.psd | 3 + .../objects/direction/shadow_up_left.psd | 3 + .../objects/direction/shadow_up_right.psd | 3 + assets-source/objects/direction/up_left.psd | 3 + assets-source/objects/direction/up_right.psd | 3 + assets-source/objects/egg/1.psd | 3 + assets-source/objects/egg/10.psd | 3 + assets-source/objects/egg/11.psd | 3 + assets-source/objects/egg/12.psd | 3 + assets-source/objects/egg/13.psd | 3 + assets-source/objects/egg/14.psd | 3 + assets-source/objects/egg/15.psd | 3 + assets-source/objects/egg/16.psd | 3 + assets-source/objects/egg/17.psd | 3 + assets-source/objects/egg/18.psd | 3 + assets-source/objects/egg/19.psd | 3 + assets-source/objects/egg/2.psd | 3 + assets-source/objects/egg/20.psd | 3 + assets-source/objects/egg/21.psd | 3 + assets-source/objects/egg/22.psd | 3 + assets-source/objects/egg/23.psd | 3 + assets-source/objects/egg/3.psd | 3 + assets-source/objects/egg/4.psd | 3 + assets-source/objects/egg/5.psd | 3 + assets-source/objects/egg/6.psd | 3 + assets-source/objects/egg/7.psd | 3 + assets-source/objects/egg/8.psd | 3 + assets-source/objects/egg/9.psd | 3 + assets-source/objects/egg/palette_1_1.psd | 3 + assets-source/objects/egg/palette_2_1.psd | 3 + assets-source/objects/egg/palette_3_3.psd | 3 + assets-source/objects/egg/palette_4_3.psd | 3 + assets-source/objects/egg/palette_5_6.psd | 3 + assets-source/objects/egg/palette_6_7.psd | 3 + assets-source/objects/egg/palette_7_8.psd | 3 + assets-source/objects/egg/palette_8_5.psd | 3 + assets-source/objects/egg_basket/1.psd | 3 + assets-source/objects/egg_basket/2.psd | 3 + assets-source/objects/egg_basket/3.psd | 3 + assets-source/objects/egg_basket/4.psd | 3 + assets-source/objects/fence/1.psd | 3 + assets-source/objects/fence/2.psd | 3 + assets-source/objects/fence/3.psd | 3 + assets-source/objects/fence_winter/1.psd | 3 + assets-source/objects/fence_winter/2.psd | 3 + assets-source/objects/fence_winter/3.psd | 3 + assets-source/objects/flower/1.psd | 3 + assets-source/objects/flower/2.psd | 3 + assets-source/objects/flower/3.psd | 3 + assets-source/objects/flower/patch1.psd | 3 + assets-source/objects/flower/patch2.psd | 3 + assets-source/objects/flower/patch3.psd | 3 + assets-source/objects/flower/patch4.psd | 3 + assets-source/objects/flower/patch5.psd | 3 + assets-source/objects/flower/patch6.psd | 3 + assets-source/objects/flower/patch7.psd | 3 + assets-source/objects/flower/pick.psd | 3 + assets-source/objects/gift/1.psd | 3 + assets-source/objects/gift/2.psd | 3 + assets-source/objects/gift/palette_1_1.psd | 3 + assets-source/objects/giftpile/1.psd | 3 + assets-source/objects/giftpile/2.psd | 3 + assets-source/objects/giftpile/3.psd | 3 + assets-source/objects/giftpile/4.psd | 3 + assets-source/objects/giftpile/5.psd | 3 + assets-source/objects/giftpile/6.psd | 3 + assets-source/objects/giftpile/pine.psd | 3 + assets-source/objects/giftpile/sign.psd | 3 + assets-source/objects/giftpile/tree.psd | 3 + assets-source/objects/gifts/1.psd | 3 + assets-source/objects/gifts/2.psd | 3 + assets-source/objects/gifts/3.psd | 3 + assets-source/objects/gifts/4.psd | 3 + assets-source/objects/gifts/5.psd | 3 + assets-source/objects/gifts/6.psd | 3 + assets-source/objects/gifts/palette_1_1.psd | 3 + assets-source/objects/gifts/palette_2_1.psd | 3 + assets-source/objects/gifts/palette_3_1.psd | 3 + assets-source/objects/grapes/1.psd | 3 + assets-source/objects/grapes/2.psd | 3 + assets-source/objects/grapes/3.psd | 3 + assets-source/objects/grapes/4.psd | 3 + assets-source/objects/grapes/5.psd | 3 + assets-source/objects/grapes/6.psd | 3 + assets-source/objects/grapes/7.psd | 3 + assets-source/objects/grapes/one.psd | 3 + assets-source/objects/grapes/palette_1_1.psd | 3 + assets-source/objects/grass_tile.psd | 3 + assets-source/objects/hammer.psd | 3 + assets-source/objects/holly.psd | 3 + assets-source/objects/house.psd | 3 + assets-source/objects/jacko_lantern/off.psd | 3 + assets-source/objects/jacko_lantern/on.psd | 3 + assets-source/objects/large_leafed_bush/1.psd | 3 + assets-source/objects/large_leafed_bush/2.psd | 3 + assets-source/objects/large_leafed_bush/3.psd | 3 + assets-source/objects/large_leafed_bush/4.psd | 3 + .../objects/large_leafed_bush/palette_1_1.psd | 3 + assets-source/objects/leaf/1.psd | 3 + assets-source/objects/leaf/2.psd | 3 + assets-source/objects/leaf/3.psd | 3 + assets-source/objects/leaf/4.psd | 3 + assets-source/objects/leaf/5.psd | 3 + assets-source/objects/leaf/6.psd | 3 + assets-source/objects/leaf/palette_1_6.psd | 3 + assets-source/objects/leaf/palette_2_6.psd | 3 + assets-source/objects/leafpile/big.psd | 3 + assets-source/objects/leafpile/bigstick.psd | 3 + assets-source/objects/leafpile/medium.psd | 3 + assets-source/objects/leafpile/mediumalt.psd | 3 + .../objects/leafpile/palette_1_stick.psd | 3 + .../objects/leafpile/palette_2_stick.psd | 3 + assets-source/objects/leafpile/small.psd | 3 + assets-source/objects/leafpile/stick.psd | 3 + assets-source/objects/leaves/1.psd | 3 + assets-source/objects/leaves/2.psd | 3 + assets-source/objects/leaves/3.psd | 3 + assets-source/objects/leaves/4.psd | 3 + assets-source/objects/leaves/5.psd | 3 + assets-source/objects/lemon/1.psd | 3 + assets-source/objects/lemon/palette_1_1.psd | 3 + assets-source/objects/letter.psd | 3 + assets-source/objects/mango.psd | 3 + assets-source/objects/mine_cart.psd | 3 + assets-source/objects/mine_cart_back.psd | 3 + assets-source/objects/mine_cart_front.psd | 3 + assets-source/objects/mine_closed.psd | 3 + assets-source/objects/mine_entrance.psd | 3 + assets-source/objects/mine_rail_fade_up.psd | 3 + assets-source/objects/mine_rails/end_left.psd | 3 + .../objects/mine_rails/end_right.psd | 3 + assets-source/objects/mine_rails/end_top.psd | 3 + assets-source/objects/mine_rails/h.psd | 3 + assets-source/objects/mine_rails/ne.psd | 3 + assets-source/objects/mine_rails/nse.psd | 3 + assets-source/objects/mine_rails/nsw.psd | 3 + assets-source/objects/mine_rails/nw.psd | 3 + assets-source/objects/mine_rails/nwe.psd | 3 + assets-source/objects/mine_rails/se.psd | 3 + assets-source/objects/mine_rails/sw.psd | 3 + assets-source/objects/mine_rails/swe.psd | 3 + assets-source/objects/mine_rails/v.psd | 3 + assets-source/objects/mistletoe.psd | 3 + assets-source/objects/orange/1.psd | 3 + assets-source/objects/orange/2.psd | 3 + assets-source/objects/pear.psd | 3 + assets-source/objects/pickaxe.psd | 3 + assets-source/objects/picture/1.psd | 3 + assets-source/objects/picture/palette_1_1.psd | 3 + assets-source/objects/plank/1.psd | 3 + assets-source/objects/plank/2.psd | 3 + assets-source/objects/plank/3.psd | 3 + assets-source/objects/plank/4.psd | 3 + assets-source/objects/plank/palette_1_1.psd | 3 + assets-source/objects/plank/shadow.psd | 3 + assets-source/objects/plank/shadow2.psd | 3 + assets-source/objects/plank/shadow_short.psd | 3 + assets-source/objects/plank/short_1.psd | 3 + assets-source/objects/plank/short_2.psd | 3 + assets-source/objects/plank/short_3.psd | 3 + assets-source/objects/pumpkin/default.psd | 3 + assets-source/objects/pumpkin/off.psd | 3 + assets-source/objects/pumpkin/on.psd | 3 + assets-source/objects/rainfall.psd | 3 + assets-source/objects/rake.psd | 3 + assets-source/objects/rock/1.psd | 3 + assets-source/objects/rock/2.psd | 3 + assets-source/objects/rock/3.psd | 3 + assets-source/objects/rock/palette_1_1.psd | 3 + assets-source/objects/rope_rack.psd | 3 + assets-source/objects/rose.psd | 3 + assets-source/objects/route_pole.psd | 3 + assets-source/objects/saw.psd | 3 + assets-source/objects/shovel.psd | 3 + assets-source/objects/sign/1.psd | 3 + assets-source/objects/sign/2.psd | 3 + assets-source/objects/sign/3.psd | 3 + assets-source/objects/sign/4.psd | 3 + assets-source/objects/sign/blank.psd | 3 + assets-source/objects/sign_winter.psd | 3 + assets-source/objects/snowpile/big.psd | 3 + assets-source/objects/snowpile/medium.psd | 3 + .../objects/snowpile/palette_1_big.psd | 3 + assets-source/objects/snowpile/small.psd | 3 + assets-source/objects/snowpile/tinier.psd | 3 + assets-source/objects/snowpile/tiny.psd | 3 + assets-source/objects/snowpony/1.psd | 3 + assets-source/objects/snowpony/2.psd | 3 + assets-source/objects/snowpony/3.psd | 3 + assets-source/objects/snowpony/4.psd | 3 + assets-source/objects/snowpony/5.psd | 3 + assets-source/objects/snowpony/6.psd | 3 + assets-source/objects/snowpony/7.psd | 3 + assets-source/objects/snowpony/8.psd | 3 + assets-source/objects/snowpony/9.psd | 3 + assets-source/objects/spider.psd | 3 + assets-source/objects/stalactite/1.psd | 3 + assets-source/objects/stalactite/2.psd | 3 + assets-source/objects/stalactite/3.psd | 3 + assets-source/objects/stone_wall/full.psd | 3 + .../objects/stone_wall/horizontal1.psd | 3 + assets-source/objects/stone_wall/pole1.psd | 3 + .../objects/stone_wall/vertical1.psd | 3 + .../objects/stone_wall_winter/horizontal1.psd | 3 + .../objects/stone_wall_winter/pole1.psd | 3 + .../objects/stone_wall_winter/vertical1.psd | 3 + assets-source/objects/table/1.psd | 3 + assets-source/objects/table/2.psd | 3 + assets-source/objects/table_3.psd | 3 + assets-source/objects/tile.psd | 3 + assets-source/objects/tile_none.psd | 3 + assets-source/objects/tombstone/1.psd | 3 + assets-source/objects/tombstone/2.psd | 3 + assets-source/objects/toolbox/empty.psd | 3 + assets-source/objects/toolbox/full.psd | 3 + assets-source/objects/tools_icon.psd | 3 + assets-source/objects/wall_h_placeholder.psd | 3 + assets-source/objects/wall_map.psd | 3 + assets-source/objects/wall_v_placeholder.psd | 3 + assets-source/objects/web.psd | 3 + assets-source/objects/well.psd | 3 + assets-source/objects/window/1.psd | 3 + .../objects/wooden_fence/horizontal1.psd | 3 + .../objects/wooden_fence/horizontal2.psd | 3 + .../objects/wooden_fence/horizontal3.psd | 3 + .../objects/wooden_fence/horizontal4.psd | 3 + .../objects/wooden_fence/horizontal5.psd | 3 + .../objects/wooden_fence/horizontal6.psd | 3 + .../objects/wooden_fence/palette_1_pole1.psd | 3 + assets-source/objects/wooden_fence/pole1.psd | 3 + assets-source/objects/wooden_fence/pole2.psd | 3 + assets-source/objects/wooden_fence/pole3.psd | 3 + assets-source/objects/wooden_fence/pole4.psd | 3 + assets-source/objects/wooden_fence/pole5.psd | 3 + .../objects/wooden_fence/vertical1.psd | 3 + .../objects/wooden_fence/vertical2.psd | 3 + .../objects/wooden_fence/vertical3.psd | 3 + .../wooden_fence_winter/horizontal1.psd | 3 + .../wooden_fence_winter/horizontal2.psd | 3 + .../wooden_fence_winter/horizontal3.psd | 3 + .../wooden_fence_winter/horizontal4.psd | 3 + .../wooden_fence_winter/horizontal5.psd | 3 + .../wooden_fence_winter/horizontal6.psd | 3 + .../objects/wooden_fence_winter/pole1.psd | 3 + .../objects/wooden_fence_winter/pole2.psd | 3 + .../objects/wooden_fence_winter/pole3.psd | 3 + .../objects/wooden_fence_winter/pole4.psd | 3 + .../objects/wooden_fence_winter/pole5.psd | 3 + .../objects/wooden_fence_winter/vertical1.psd | 3 + .../objects/wooden_fence_winter/vertical2.psd | 3 + .../objects/wooden_fence_winter/vertical3.psd | 3 + assets-source/pony/animations/base.psd | 3 + assets-source/pony/animations/boop.psd | 3 + assets-source/pony/animations/fly-bat.psd | 3 + assets-source/pony/animations/fly-bug.psd | 3 + assets-source/pony/animations/fly-griffon.psd | 3 + assets-source/pony/animations/fly-pegasus.psd | 3 + assets-source/pony/animations/lie.psd | 3 + assets-source/pony/animations/sit.psd | 3 + assets-source/pony/animations/trot.psd | 3 + assets-source/pony/back-legs-accessories.psd | 3 + assets-source/pony/back-legs-cms.psd | 3 + assets-source/pony/back-legs-hooves.psd | 3 + assets-source/pony/back-legs-sleeves.psd | 3 + assets-source/pony/back-legs.psd | 3 + assets-source/pony/blush.psd | 3 + assets-source/pony/body-back-accessory.psd | 3 + assets-source/pony/body-chest-accessory.psd | 3 + assets-source/pony/body-waist-accessory.psd | 3 + assets-source/pony/body-wings.psd | 3 + assets-source/pony/body.psd | 3 + assets-source/pony/ear-accessory.psd | 3 + assets-source/pony/ears.psd | 3 + assets-source/pony/extra-accessory.psd | 3 + assets-source/pony/eyes.psd | 3 + assets-source/pony/face-accessory.psd | 3 + assets-source/pony/facial-hair.psd | 3 + assets-source/pony/front-legs-accessories.psd | 3 + assets-source/pony/front-legs-hooves.psd | 3 + assets-source/pony/front-legs-sleeves.psd | 3 + assets-source/pony/front-legs.psd | 3 + assets-source/pony/head-accessory.psd | 3 + assets-source/pony/head.psd | 3 + assets-source/pony/horns.psd | 3 + assets-source/pony/irises.psd | 3 + assets-source/pony/manes.psd | 3 + assets-source/pony/muzzles.psd | 3 + assets-source/pony/neck-accessory.psd | 3 + assets-source/pony/shadows.psd | 3 + assets-source/pony/tails.psd | 3 + .../sprites-animations/cat_light.psd | 3 + .../ghost1_hooves_light.psd | 3 + .../sprites-animations/ghost1_light.psd | 3 + .../sprites-animations/lantern_light.psd | 3 + .../sprites-animations/magic2_light.psd | 3 + .../sprites-animations/magic_light.psd | 3 + .../sprites-animations/torch2_light.psd | 3 + .../water_crystal_1_light.psd | 3 + .../water_crystal_2_light.psd | 3 + .../water_crystal_3_light.psd | 3 + assets-source/sprites-palette/grass.png | 3 + assets-source/sprites-palette/nipple.png | 3 + assets-source/sprites-palette/nipple_alt.png | 3 + assets-source/sprites/dirt_ice_heightmap.png | 3 + .../sprites/dirt_stone_cave_height_map.png | 3 + .../sprites/dirt_water_heightmap.png | 3 + assets-source/sprites/firefly_light.png | 3 + assets-source/sprites/grass.png | 3 + assets-source/sprites/jacko_lantern_light.png | 3 + assets-source/sprites/nipple.png | 3 + assets-source/sprites/pumpkin_light.png | 3 + assets-source/tiles/cave-walls.png | 3 + assets-source/tiles/cliffs-grass-autumn.png | 3 + assets-source/tiles/cliffs-grass-winter.png | 3 + assets-source/tiles/cliffs-grass.png | 3 + assets-source/tiles/dirt-grass-autumn.png | 3 + assets-source/tiles/dirt-grass.png | 3 + assets-source/tiles/dirt-ice-autumn.png | 3 + assets-source/tiles/dirt-ice-winter.png | 3 + assets-source/tiles/dirt-ice.png | 3 + assets-source/tiles/dirt-snow.png | 3 + assets-source/tiles/dirt-stone-cave.png | 3 + assets-source/tiles/dirt-water-1-autumn.png | 3 + assets-source/tiles/dirt-water-1-cave.png | 3 + assets-source/tiles/dirt-water-1-winter.png | 3 + assets-source/tiles/dirt-water-1.png | 3 + assets-source/tiles/dirt-water-2.png | 3 + assets-source/tiles/dirt-water-3.png | 3 + assets-source/tiles/dirt-water-4.png | 3 + assets-source/tiles/grass.png | 3 + assets-source/tiles/grass.psd | 3 + assets-source/tiles/ice-snow.png | 3 + assets-source/tiles/stone-tiles.png | 3 + assets-source/tiles/stone2-tiles.png | 3 + assets-source/tiles/water.png | 3 + assets-source/tiles/wood-tiles-1.png | 3 + assets-source/tiles/wood-tiles.png | 3 + assets-source/tiles/wood.png | 3 + assets-source/trees/pine/1.psd | 3 + assets-source/trees/pine/2.psd | 3 + assets-source/trees/pine/3.psd | 3 + assets-source/trees/pine/4.psd | 3 + assets-source/trees/pine/5.psd | 3 + assets-source/trees/pine/6.psd | 3 + assets-source/trees/pine/palette_1_6.psd | 3 + assets-source/trees/pine/palette_2_6.psd | 3 + assets-source/trees/tree/1.psd | 3 + assets-source/trees/tree/2.psd | 3 + assets-source/trees/tree/3.psd | 3 + assets-source/trees/tree/4.psd | 3 + assets-source/trees/tree/5.psd | 3 + assets-source/trees/tree/6.psd | 3 + assets-source/trees/tree/palette_1_6.psd | 3 + assets-source/trees/tree/palette_2_6.psd | 3 + assets-source/trees/tree/palette_3_6.psd | 3 + assets-source/trees/tree/palette_4_6.psd | 3 + assets-source/walls/stone.psd | 3 + assets-source/walls/wood.psd | 3 + assets/images/avatars.jpg | 3 + assets/images/logo-120.png | 3 + assets/images/logo-64.png | 3 + assets/images/logo-gray.png | 3 + assets/images/logo-large.png | 3 + assets/images/logo-small.png | 3 + assets/images/logo.png | 3 + assets/images/pony.png | 3 + assets/images/pony2.png | 3 + assets/images/pony2a.png | 3 + assets/music/ambient.mp3 | 3 + assets/music/ambient.webm | 3 + assets/music/bossanova.mp3 | 3 + assets/music/bossanova.webm | 3 + assets/music/building.mp3 | 3 + assets/music/building.webm | 3 + assets/music/cave-crystals.mp3 | 3 + assets/music/cave-crystals.webm | 3 + assets/music/cave-secrets.mp3 | 3 + assets/music/cave-secrets.webm | 3 + assets/music/clop.mp3 | 3 + assets/music/clop.webm | 3 + assets/music/falling.mp3 | 3 + assets/music/falling.webm | 3 + assets/music/fivefour.mp3 | 3 + assets/music/fivefour.webm | 3 + assets/music/ghost.mp3 | 3 + assets/music/ghost.webm | 3 + assets/music/happy-house.mp3 | 3 + assets/music/happy-house.webm | 3 + assets/music/hypnosis.mp3 | 3 + assets/music/hypnosis.webm | 3 + assets/music/island.mp3 | 3 + assets/music/island.webm | 3 + assets/music/largo.mp3 | 3 + assets/music/largo.webm | 3 + assets/music/musicbox.mp3 | 3 + assets/music/musicbox.webm | 3 + assets/music/orchid.mp3 | 3 + assets/music/orchid.webm | 3 + assets/music/pumpkin.mp3 | 3 + assets/music/pumpkin.webm | 3 + assets/music/reindeer-winter.mp3 | 3 + assets/music/reindeer-winter.webm | 3 + assets/music/reindeer.mp3 | 3 + assets/music/reindeer.webm | 3 + assets/music/scherzo.mp3 | 3 + assets/music/scherzo.webm | 3 + assets/music/school.mp3 | 3 + assets/music/school.webm | 3 + assets/music/season.mp3 | 3 + assets/music/season.webm | 3 + assets/music/sunny-island.mp3 | 3 + assets/music/sunny-island.webm | 3 + assets/music/sweet-home.mp3 | 3 + assets/music/sweet-home.webm | 3 + assets/music/tio.mp3 | 3 + assets/music/tio.webm | 3 + assets/music/trees-winter.mp3 | 3 + assets/music/trees-winter.webm | 3 + assets/music/trees.mp3 | 3 + assets/music/trees.webm | 3 + assets/music/trills.mp3 | 3 + assets/music/trills.webm | 3 + assets/music/unrest.mp3 | 3 + assets/music/unrest.webm | 3 + assets/music/waltzalt.mp3 | 3 + assets/music/waltzalt.webm | 3 + assets/music/xmas-air.mp3 | 3 + assets/music/xmas-air.webm | 3 + assets/music/xmas-horns.mp3 | 3 + assets/music/xmas-horns.webm | 3 + assets/music/xmas-presents.mp3 | 3 + assets/music/xmas-presents.webm | 3 + cli.js | 319 + config-template.json | 33 + favicons/android-chrome-192x192.png | 3 + favicons/android-chrome-512x512.png | 3 + favicons/apple-touch-icon-152x152.png | 3 + favicons/apple-touch-icon-precomposed.png | 3 + favicons/apple-touch-icon.png | 3 + favicons/browserconfig.xml | 9 + favicons/favicon-16x16.png | 3 + favicons/favicon-194x194.png | 3 + favicons/favicon-32x32.png | 3 + favicons/favicon.ico | 3 + favicons/manifest.json | 21 + favicons/mstile-150x150.png | 3 + favicons/safari-pinned-tab.svg | 32 + gulpfile.js | 339 + package-lock.json | 23105 ++++++++++++++++ package.json | 203 + pony-town-offline.js | 8 + pony-town.js | 2 + public/images/logo-120.png | 3 + public/images/logo-240.png | 3 + public/images/offline-pony.png | 3 + public/offline.html | 38 + public/privacypolicy.html | 95 + public/reload.html | 39 + public/reset.html | 64 + public/robots.txt | 4 + public/termsofservice.html | 111 + src/fixes/empty.js | 0 src/maps/cave.json | 5 + src/maps/custom.json | 15 + src/maps/island.json | 22 + src/maps/main.json | 5 + src/styles/partials/_admin.scss | 398 + src/styles/partials/_animations.scss | 19 + src/styles/partials/_base.scss | 78 + src/styles/partials/_bootstrap-admin.scss | 35 + src/styles/partials/_bootstrap.scss | 35 + src/styles/partials/_chat.scss | 77 + src/styles/partials/_common.scss | 305 + src/styles/partials/_controls.scss | 107 + src/styles/partials/_custom.scss | 351 + src/styles/partials/_game.scss | 64 + src/styles/partials/_loading.scss | 13 + src/styles/partials/_mixins.scss | 91 + src/styles/partials/_shared.scss | 9 + src/styles/partials/_tiny.scss | 23 + src/styles/partials/_tools.scss | 47 + src/styles/partials/_variables.scss | 205 + src/styles/style-admin.scss | 4 + src/styles/style-inline.scss | 1 + src/styles/style-tools.scss | 4 + src/styles/style.scss | 3 + src/tests/font/ascii.png | 3 + src/tests/font/chinese.png | 3 + src/tests/font/emoji.png | 3 + src/tests/font/greek.png | 3 + src/tests/font/hiragana.png | 3 + src/tests/font/kanji.png | 3 + src/tests/font/katakana.png | 3 + src/tests/font/newlines.png | 3 + src/tests/font/punctuation.png | 3 + src/tests/font/romaji.png | 3 + src/tests/font/russian.png | 3 + src/tests/font/shapes.png | 3 + src/tests/font/special.png | 3 + src/tests/font/tiny.png | 3 + src/tests/font/variants.png | 3 + src/tests/graphics/admin.png | 3 + src/tests/graphics/announcement.png | 3 + src/tests/graphics/bounds-none.png | 3 + src/tests/graphics/bounds.png | 3 + src/tests/graphics/break-chat.png | 3 + src/tests/graphics/break-think.png | 3 + src/tests/graphics/fade-in-0.png | 3 + src/tests/graphics/fade-in-1.png | 3 + src/tests/graphics/fade-in-2.png | 3 + src/tests/graphics/fade-out-0.png | 3 + src/tests/graphics/fade-out-1.png | 3 + src/tests/graphics/fade-out-2.png | 3 + src/tests/graphics/hello-apple.png | 3 + src/tests/graphics/hello.png | 3 + src/tests/graphics/mod.png | 3 + src/tests/graphics/move-left-chat.png | 3 + src/tests/graphics/move-left-think.png | 3 + src/tests/graphics/move-right-chat.png | 3 + src/tests/graphics/move-right-nipple-chat.png | 3 + .../graphics/move-right-nipple-think.png | 3 + src/tests/graphics/move-right-think.png | 3 + src/tests/graphics/name-apple.png | 3 + src/tests/graphics/name-dev.png | 3 + src/tests/graphics/name-mod.png | 3 + src/tests/graphics/name-party.png | 3 + src/tests/graphics/name-sup1.png | 3 + src/tests/graphics/name-sup2.png | 3 + src/tests/graphics/name-sup3.png | 3 + src/tests/graphics/name.png | 3 + src/tests/graphics/newline.png | 3 + src/tests/graphics/outline.png | 3 + src/tests/graphics/outside-chat.png | 3 + src/tests/graphics/outside-think.png | 3 + src/tests/graphics/outside.png | 3 + src/tests/graphics/party-announcement.png | 3 + src/tests/graphics/party-thinking.png | 3 + src/tests/graphics/party.png | 3 + src/tests/graphics/pixel-numbers.png | 3 + src/tests/graphics/sup1.png | 3 + src/tests/graphics/sup2.png | 3 + src/tests/graphics/sup3.png | 3 + src/tests/graphics/system.png | 3 + src/tests/graphics/thinking.png | 3 + src/tests/ponies/base.json | 818 + src/tests/ponies/default.json | 818 + src/tests/ponies/perf.json | 844 + src/tests/ponies/school.json | 818 + src/tests/ponies/wizard.json | 844 + src/tests/pony/all.png | 3 + src/tests/pony/base.png | 3 + src/tests/pony/blinking-angry.png | 3 + src/tests/pony/blinking-skip.png | 3 + src/tests/pony/blinking.png | 3 + src/tests/pony/blush.png | 3 + src/tests/pony/cape.png | 3 + src/tests/pony/cm-flip.png | 3 + src/tests/pony/deer.png | 3 + src/tests/pony/dragon.png | 3 + src/tests/pony/extra.png | 3 + src/tests/pony/eyes-0.png | 3 + src/tests/pony/face-extra-holding-letter.png | 3 + src/tests/pony/face-extra.png | 3 + src/tests/pony/freckles-flip.png | 3 + src/tests/pony/glasses.png | 3 + src/tests/pony/griffon-lantern.png | 3 + src/tests/pony/griffon-socks.png | 3 + src/tests/pony/griffon.png | 3 + src/tests/pony/hair-behind-neck-accessory.png | 3 + src/tests/pony/hair-in-front-of-wing.png | 3 + src/tests/pony/hat-bald.png | 3 + src/tests/pony/hat-horns.png | 3 + src/tests/pony/holding-letter.png | 3 + src/tests/pony/holding.png | 3 + src/tests/pony/laughing.png | 3 + src/tests/pony/lie.png | 3 + src/tests/pony/mustache.png | 3 + src/tests/pony/no-body.png | 3 + src/tests/pony/none.png | 3 + src/tests/pony/offline.png | 3 + src/tests/pony/paws-socks.png | 3 + src/tests/pony/paws.png | 3 + src/tests/pony/selected.png | 3 + src/tests/pony/sit-fetlocks.png | 3 + src/tests/pony/sit-neck-accessory.png | 3 + src/tests/pony/sit-paws.png | 3 + src/tests/pony/sit-socks-cm.png | 3 + src/tests/pony/sit-socks.png | 3 + src/tests/pony/sit-sword.png | 3 + src/tests/pony/sit-wing.png | 3 + src/tests/pony/sit.png | 3 + .../pony/sitting-with-skirt-and-socks.png | 3 + src/tests/pony/trot.png | 3 + src/tests/pony/wing-open.png | 3 + src/ts/bootstrap-admin.ts | 5 + src/ts/bootstrap-common.ts | 17 + src/ts/bootstrap-es.ts | 8 + src/ts/bootstrap-tools.ts | 5 + src/ts/bootstrap.ts | 13 + src/ts/client/buttonActions.ts | 676 + src/ts/client/canvasUtils.ts | 101 + src/ts/client/clientActions.ts | 304 + src/ts/client/clientAdminActions.ts | 34 + src/ts/client/clientUtils.ts | 502 + src/ts/client/credits.ts | 75 + src/ts/client/data.ts | 73 + src/ts/client/draw.ts | 297 + src/ts/client/emoji.ts | 194 + src/ts/client/fonts.ts | 39 + src/ts/client/game.ts | 1776 ++ src/ts/client/gameLoop.ts | 89 + src/ts/client/gameUtils.ts | 49 + src/ts/client/handlers.ts | 802 + src/ts/client/htmlUtils.ts | 174 + src/ts/client/icons.ts | 206 + src/ts/client/input/gamepad.ts | 160 + src/ts/client/input/input.ts | 144 + src/ts/client/input/inputManager.ts | 170 + src/ts/client/input/keyboard.ts | 96 + src/ts/client/input/mouse.ts | 87 + src/ts/client/input/touch.ts | 203 + src/ts/client/partyUtils.ts | 44 + src/ts/client/playerActions.ts | 229 + src/ts/client/polyfils.ts | 33 + src/ts/client/ponyAnimations.ts | 547 + src/ts/client/ponyDraw.ts | 790 + src/ts/client/ponyHelpers.ts | 56 + src/ts/client/ponyStates.ts | 155 + src/ts/client/ponyUtils.ts | 176 + src/ts/client/rev.ts | 12 + src/ts/client/sec.ts | 44 + src/ts/client/spriteAnimations.ts | 31 + src/ts/client/spriteUtils.ts | 81 + src/ts/client/tileUtils.ts | 602 + src/ts/client/timing.ts | 107 + src/ts/client/webgl.ts | 136 + src/ts/common/accountUtils.ts | 80 + src/ts/common/adminInterfaces.ts | 923 + src/ts/common/adminUtils.ts | 570 + src/ts/common/animationPlayer.ts | 135 + src/ts/common/animator.ts | 183 + src/ts/common/binaryUtils.ts | 20 + src/ts/common/bitUtils.ts | 114 + src/ts/common/camera.ts | 132 + src/ts/common/collision.ts | 306 + src/ts/common/color.ts | 495 + src/ts/common/colors.ts | 173 + src/ts/common/compress.ts | 157 + src/ts/common/compressPony.ts | 624 + src/ts/common/constants.ts | 190 + src/ts/common/countries.ts | 251 + src/ts/common/debugData.ts | 49 + src/ts/common/encoders/expressionEncoder.ts | 34 + src/ts/common/encoders/updateDecoder.ts | 152 + src/ts/common/encoders/updateEncoder.ts | 167 + src/ts/common/entities.ts | 2393 ++ src/ts/common/entityUtils.ts | 225 + src/ts/common/errors.ts | 12 + src/ts/common/expressionUtils.ts | 352 + src/ts/common/expressions.ts | 295 + src/ts/common/filterUtils.ts | 164 + src/ts/common/interfaces.ts | 1765 ++ src/ts/common/mat2d.ts | 122 + src/ts/common/mat4.ts | 33 + src/ts/common/mixins.ts | 850 + src/ts/common/movementUtils.ts | 123 + src/ts/common/offsets.ts | 138 + src/ts/common/pony.ts | 678 + src/ts/common/ponyInfo.ts | 587 + src/ts/common/positionUtils.ts | 70 + src/ts/common/rect.ts | 50 + src/ts/common/region.ts | 202 + src/ts/common/rollbar.ts | 102 + src/ts/common/security.ts | 140 + src/ts/common/sheets.ts | 882 + src/ts/common/stringUtils.ts | 90 + src/ts/common/swears.ts | 1595 ++ src/ts/common/tags.ts | 54 + src/ts/common/timeUtils.ts | 127 + src/ts/common/utils.ts | 541 + src/ts/common/worldMap.ts | 741 + .../admin-account-details.pug | 447 + .../admin-account-details.scss | 17 + .../admin-account-details.ts | 582 + .../admin/admin-accounts/admin-accounts.pug | 88 + .../admin/admin-accounts/admin-accounts.ts | 123 + .../admin/admin-events/admin-events.pug | 55 + .../admin/admin-events/admin-events.ts | 99 + .../admin-origin-details.pug | 33 + .../admin-origin-details.ts | 36 + .../admin/admin-origins/admin-origins.pug | 59 + .../admin/admin-origins/admin-origins.ts | 53 + .../admin/admin-other/admin-other.pug | 52 + .../admin/admin-other/admin-other.ts | 90 + .../admin/admin-ponies/admin-ponies.pug | 65 + .../admin/admin-ponies/admin-ponies.scss | 4 + .../admin/admin-ponies/admin-ponies.ts | 97 + .../admin-reports-perf/admin-reports-perf.pug | 41 + .../admin-reports-perf/admin-reports-perf.ts | 291 + .../admin/admin-reports/admin-reports.pug | 4 + .../admin/admin-reports/admin-reports.ts | 8 + .../admin/admin-sign-in/admin-sign-in.pug | 2 + .../admin/admin-sign-in/admin-sign-in.ts | 8 + .../admin/admin-state/admin-state.pug | 161 + .../admin/admin-state/admin-state.scss | 14 + .../admin/admin-state/admin-state.ts | 111 + src/ts/components/admin/admin.module.ts | 139 + src/ts/components/admin/admin.pug | 24 + src/ts/components/admin/admin.scss | 14 + src/ts/components/admin/admin.ts | 75 + src/ts/components/admin/base-table.ts | 100 + src/ts/components/admin/pipes/keysPipe.ts | 10 + src/ts/components/admin/pipes/orderByPipe.ts | 10 + src/ts/components/admin/pipes/translitPipe.ts | 15 + .../account-info-remote.pug | 5 + .../account-info-remote.ts | 44 + .../shared/account-info/account-info.pug | 179 + .../shared/account-info/account-info.scss | 98 + .../admin/shared/account-info/account-info.ts | 292 + .../shared/account-status/account-status.pug | 10 + .../shared/account-status/account-status.ts | 28 + .../account-tooltip/account-tooltip.pug | 27 + .../shared/account-tooltip/account-tooltip.ts | 20 + .../shared/admin-chat-log/admin-chat-log.pug | 38 + .../shared/admin-chat-log/admin-chat-log.scss | 1 + .../shared/admin-chat-log/admin-chat-log.ts | 182 + .../shared/auth-info-edit/auth-info-edit.pug | 22 + .../shared/auth-info-edit/auth-info-edit.ts | 64 + .../auth-info-remote/auth-info-remote.pug | 1 + .../auth-info-remote/auth-info-remote.ts | 31 + .../admin/shared/auth-info/auth-info.pug | 13 + .../admin/shared/auth-info/auth-info.scss | 4 + .../admin/shared/auth-info/auth-info.ts | 28 + .../auth-list-remote/auth-list-remote.pug | 5 + .../auth-list-remote/auth-list-remote.scss | 3 + .../auth-list-remote/auth-list-remote.ts | 37 + .../admin/shared/auth-list/auth-list.pug | 4 + .../admin/shared/auth-list/auth-list.scss | 7 + .../admin/shared/auth-list/auth-list.ts | 19 + .../admin/shared/ban-icon/ban-icon.pug | 16 + .../admin/shared/ban-icon/ban-icon.scss | 26 + .../admin/shared/ban-icon/ban-icon.ts | 77 + .../admin/shared/email-list/email-list.pug | 6 + .../admin/shared/email-list/email-list.ts | 16 + .../shared/events-table/events-table.pug | 51 + .../shared/events-table/events-table.scss | 8 + .../admin/shared/events-table/events-table.ts | 56 + src/ts/components/admin/shared/from-now.ts | 36 + .../shared/on-off-switch/on-off-switch.pug | 8 + .../shared/on-off-switch/on-off-switch.ts | 20 + .../origin-info-remote/origin-info-remote.pug | 1 + .../origin-info-remote/origin-info-remote.ts | 31 + .../admin/shared/origin-info/origin-info.pug | 9 + .../admin/shared/origin-info/origin-info.scss | 17 + .../admin/shared/origin-info/origin-info.ts | 23 + .../origin-list-remote/origin-list-remote.pug | 4 + .../origin-list-remote/origin-list-remote.ts | 32 + .../pony-info-remote/pony-info-remote.pug | 1 + .../pony-info-remote/pony-info-remote.ts | 32 + .../admin/shared/pony-info/pony-info.pug | 12 + .../admin/shared/pony-info/pony-info.scss | 3 + .../admin/shared/pony-info/pony-info.ts | 42 + .../pony-list-remote/pony-list-remote.pug | 17 + .../pony-list-remote/pony-list-remote.scss | 14 + .../pony-list-remote/pony-list-remote.ts | 75 + .../admin/shared/time-field/time-field.pug | 5 + .../admin/shared/time-field/time-field.ts | 10 + .../admin/shared/ua-info/ua-info.pug | 6 + .../admin/shared/ua-info/ua-info.ts | 45 + src/ts/components/app/about/about.pug | 102 + src/ts/components/app/about/about.scss | 33 + src/ts/components/app/about/about.ts | 31 + src/ts/components/app/account/account.pug | 134 + src/ts/components/app/account/account.scss | 17 + src/ts/components/app/account/account.ts | 130 + src/ts/components/app/app.module.ts | 66 + src/ts/components/app/app.pug | 61 + src/ts/components/app/app.scss | 204 + src/ts/components/app/app.ts | 210 + src/ts/components/app/character/character.pug | 389 + .../components/app/character/character.scss | 62 + src/ts/components/app/character/character.ts | 501 + .../components/app/editor-box/editor-box.pug | 138 + .../components/app/editor-box/editor-box.ts | 205 + src/ts/components/app/help/help.pug | 288 + src/ts/components/app/help/help.scss | 21 + src/ts/components/app/help/help.ts | 19 + src/ts/components/app/home/home.pug | 32 + src/ts/components/app/home/home.scss | 26 + src/ts/components/app/home/home.ts | 56 + src/ts/components/services/adminModel.ts | 617 + src/ts/components/services/audio.ts | 251 + src/ts/components/services/authGuard.ts | 22 + src/ts/components/services/errorReporter.ts | 47 + src/ts/components/services/frameService.ts | 38 + src/ts/components/services/gameService.ts | 312 + src/ts/components/services/installService.ts | 41 + .../services/intervalUpdateService.ts | 44 + src/ts/components/services/liveCollection.ts | 145 + src/ts/components/services/model.ts | 488 + src/ts/components/services/modelSubscriber.ts | 107 + .../services/rollbarErrorHandler.ts | 61 + .../services/rollbarErrorReporter.ts | 39 + src/ts/components/services/settingsService.ts | 43 + src/ts/components/services/storageService.ts | 84 + .../shared/action-bar/action-bar.pug | 19 + .../shared/action-bar/action-bar.scss | 72 + .../shared/action-bar/action-bar.ts | 84 + .../shared/action-button/action-button.pug | 6 + .../shared/action-button/action-button.scss | 95 + .../shared/action-button/action-button.ts | 46 + .../shared/actions-modal/actions-modal.pug | 97 + .../shared/actions-modal/actions-modal.scss | 40 + .../shared/actions-modal/actions-modal.ts | 154 + .../shared/bitmap-box/bitmap-box.pug | 2 + .../shared/bitmap-box/bitmap-box.scss | 27 + .../shared/bitmap-box/bitmap-box.ts | 45 + .../butt-mark-editor/butt-mark-editor.pug | 20 + .../butt-mark-editor/butt-mark-editor.ts | 30 + .../shared/character-list/character-list.pug | 29 + .../shared/character-list/character-list.scss | 108 + .../shared/character-list/character-list.ts | 172 + .../character-preview/character-preview.ts | 192 + .../character-select/character-select.pug | 36 + .../character-select/character-select.scss | 14 + .../character-select/character-select.ts | 128 + .../components/shared/chat-box/chat-box.pug | 14 + .../components/shared/chat-box/chat-box.scss | 88 + src/ts/components/shared/chat-box/chat-box.ts | 388 + .../components/shared/chat-log/chat-log.pug | 27 + .../components/shared/chat-log/chat-log.scss | 159 + src/ts/components/shared/chat-log/chat-log.ts | 620 + .../components/shared/check-box/check-box.pug | 8 + .../shared/check-box/check-box.scss | 30 + .../components/shared/check-box/check-box.ts | 22 + .../shared/color-picker/color-picker.pug | 26 + .../shared/color-picker/color-picker.scss | 123 + .../shared/color-picker/color-picker.ts | 125 + .../custom-checkbox/custom-checkbox.pug | 11 + .../custom-checkbox/custom-checkbox.scss | 8 + .../shared/custom-checkbox/custom-checkbox.ts | 16 + .../shared/date-picker/date-picker.pug | 10 + .../shared/date-picker/date-picker.ts | 55 + .../shared/directives/agAutoFocus.ts | 12 + src/ts/components/shared/directives/agDrag.ts | 140 + src/ts/components/shared/directives/anchor.ts | 17 + .../shared/directives/btnHighlight.ts | 30 + .../components/shared/directives/draggable.ts | 207 + .../components/shared/directives/dropdown.ts | 227 + .../components/shared/directives/fixToTop.ts | 22 + .../shared/directives/focusTitle.ts | 15 + .../components/shared/directives/focusTrap.ts | 57 + .../shared/directives/hasFeature.ts | 60 + .../shared/directives/labelledBy.ts | 21 + .../shared/directives/linkCurrent.ts | 14 + src/ts/components/shared/directives/revSrc.ts | 12 + .../shared/directives/saveActiveTab.ts | 26 + .../components/shared/emote-box/emote-box.ts | 80 + .../shared/fill-outline/fill-outline.pug | 29 + .../shared/fill-outline/fill-outline.scss | 18 + .../shared/fill-outline/fill-outline.ts | 47 + .../shared/friends-box/friends-box.pug | 55 + .../shared/friends-box/friends-box.scss | 107 + .../shared/friends-box/friends-box.ts | 61 + .../shared/install-button/install-button.pug | 6 + .../shared/install-button/install-button.scss | 5 + .../shared/install-button/install-button.ts | 27 + .../shared/invites-modal/invites-modal.pug | 22 + .../shared/invites-modal/invites-modal.ts | 32 + src/ts/components/shared/kbd-key/kbd-key.pug | 3 + src/ts/components/shared/kbd-key/kbd-key.ts | 9 + .../components/shared/menu-bar/menu-bar.pug | 62 + .../components/shared/menu-bar/menu-bar.scss | 59 + src/ts/components/shared/menu-bar/menu-bar.ts | 61 + .../components/shared/menu-item/menu-item.pug | 5 + .../shared/menu-item/menu-item.scss | 26 + .../components/shared/menu-item/menu-item.ts | 13 + src/ts/components/shared/mod-box/mod-box.pug | 72 + src/ts/components/shared/mod-box/mod-box.scss | 38 + src/ts/components/shared/mod-box/mod-box.ts | 97 + .../shared/notification/notification-item.pug | 20 + .../notification/notification-item.scss | 3 + .../shared/notification/notification-item.ts | 68 + .../shared/notification/notification-list.pug | 7 + .../notification/notification-list.scss | 4 + .../shared/notification/notification-list.ts | 33 + .../shared/page-loader/page-loader.pug | 18 + .../shared/page-loader/page-loader.scss | 20 + .../shared/page-loader/page-loader.ts | 30 + .../components/shared/party-box/party-box.pug | 6 + .../shared/party-box/party-box.scss | 33 + .../components/shared/party-box/party-box.ts | 24 + .../shared/party-list/party-list.pug | 20 + .../shared/party-list/party-list.scss | 18 + .../shared/party-list/party-list.ts | 93 + src/ts/components/shared/pipes/siteName.ts | 11 + .../components/shared/play-box/play-box.pug | 121 + .../components/shared/play-box/play-box.scss | 26 + src/ts/components/shared/play-box/play-box.ts | 183 + .../shared/play-notice/play-notice.pug | 4 + .../shared/play-notice/play-notice.ts | 12 + .../components/shared/pony-box/pony-box.pug | 63 + .../components/shared/pony-box/pony-box.scss | 113 + src/ts/components/shared/pony-box/pony-box.ts | 113 + .../shared/portrait-box/portrait-box.pug | 3 + .../shared/portrait-box/portrait-box.scss | 60 + .../shared/portrait-box/portrait-box.ts | 86 + .../shared/scale-picker/scale-picker.pug | 5 + .../shared/scale-picker/scale-picker.ts | 21 + .../shared/set-selection/set-selection.pug | 47 + .../shared/set-selection/set-selection.scss | 3 + .../shared/set-selection/set-selection.ts | 73 + .../shared/settings-box/settings-box.pug | 49 + .../shared/settings-box/settings-box.scss | 17 + .../shared/settings-box/settings-box.ts | 131 + .../shared/settings-modal/settings-modal.pug | 132 + .../shared/settings-modal/settings-modal.scss | 12 + .../shared/settings-modal/settings-modal.ts | 122 + src/ts/components/shared/shared.module.ts | 161 + .../shared/sign-in-box/sign-in-box.pug | 43 + .../shared/sign-in-box/sign-in-box.scss | 55 + .../shared/sign-in-box/sign-in-box.ts | 26 + .../components/shared/site-info/site-info.pug | 3 + .../shared/site-info/site-info.scss | 10 + .../components/shared/site-info/site-info.ts | 19 + .../shared/site-links/site-links.pug | 4 + .../shared/site-links/site-links.ts | 10 + .../shared/slider-bar/slider-bar.pug | 5 + .../shared/slider-bar/slider-bar.scss | 80 + .../shared/slider-bar/slider-bar.ts | 89 + .../shared/sprite-box/sprite-box.pug | 4 + .../shared/sprite-box/sprite-box.scss | 30 + .../shared/sprite-box/sprite-box.ts | 186 + .../sprite-selection/sprite-selection.pug | 17 + .../sprite-selection/sprite-selection.scss | 56 + .../sprite-selection/sprite-selection.ts | 97 + .../shared/support-button/support-button.pug | 10 + .../shared/support-button/support-button.scss | 45 + .../shared/support-button/support-button.ts | 17 + .../shared/supporter-pony/supporter-pony.pug | 3 + .../shared/supporter-pony/supporter-pony.ts | 88 + .../components/shared/swap-box/swap-box.pug | 7 + .../components/shared/swap-box/swap-box.scss | 7 + src/ts/components/shared/swap-box/swap-box.ts | 43 + src/ts/components/shared/tabset/tabset.pug | 30 + src/ts/components/shared/tabset/tabset.ts | 93 + .../shared/virtual-list/virtual-list.scss | 5 + .../shared/virtual-list/virtual-list.ts | 167 + .../tools/shared/tools-frame/tools-frame.pug | 13 + .../tools/shared/tools-frame/tools-frame.scss | 16 + .../tools/shared/tools-frame/tools-frame.ts | 76 + .../shared/tools-offset/tools-offset.pug | 14 + .../shared/tools-offset/tools-offset.scss | 17 + .../tools/shared/tools-offset/tools-offset.ts | 29 + .../tools/shared/tools-range/tools-range.pug | 8 + .../tools/shared/tools-range/tools-range.scss | 7 + .../tools/shared/tools-range/tools-range.ts | 55 + .../tools/shared/tools-xy/tools-xy.pug | 9 + .../tools/shared/tools-xy/tools-xy.scss | 9 + .../tools/shared/tools-xy/tools-xy.ts | 32 + src/ts/components/tools/sheetExport.ts | 358 + .../tools/tools-animation/tools-animation.pug | 313 + .../tools-animation/tools-animation.scss | 99 + .../tools/tools-animation/tools-animation.ts | 804 + .../tools/tools-chat/tools-chat.pug | 11 + .../components/tools/tools-chat/tools-chat.ts | 191 + .../tools-collisions/tools-collisions.pug | 5 + .../tools-collisions/tools-collisions.ts | 781 + .../tools/tools-entity/tools-entity.pug | 75 + .../tools/tools-entity/tools-entity.ts | 423 + .../tools-expressions/tools-expressions.pug | 17 + .../tools-expressions/tools-expressions.ts | 107 + .../tools/tools-index/tools-index.pug | 16 + .../tools/tools-index/tools-index.ts | 8 + .../components/tools/tools-map/tools-map.pug | 34 + .../components/tools/tools-map/tools-map.ts | 210 + .../tools/tools-palette/tools-palette.pug | 24 + .../tools/tools-palette/tools-palette.ts | 78 + src/ts/components/tools/tools-perf/methods.ts | 156 + .../tools/tools-perf/tools-perf.pug | 9 + .../components/tools/tools-perf/tools-perf.ts | 404 + .../tools/tools-regions/tools-regions.pug | 26 + .../tools/tools-regions/tools-regions.ts | 172 + .../tools/tools-sheet/tools-sheet.pug | 50 + .../tools/tools-sheet/tools-sheet.scss | 30 + .../tools/tools-sheet/tools-sheet.ts | 76 + .../tools/tools-states/tools-states.pug | 25 + .../tools/tools-states/tools-states.scss | 57 + .../tools/tools-states/tools-states.ts | 139 + src/ts/components/tools/tools-ui/tools-ui.pug | 285 + src/ts/components/tools/tools-ui/tools-ui.ts | 243 + .../tools/tools-variants/tools-variants.pug | 44 + .../tools/tools-variants/tools-variants.scss | 10 + .../tools/tools-variants/tools-variants.ts | 97 + .../tools/tools-webgl/tools-webgl.pug | 38 + .../tools/tools-webgl/tools-webgl.ts | 928 + src/ts/components/tools/tools.module.ts | 99 + src/ts/components/tools/tools.pug | 2 + src/ts/components/tools/tools.ts | 22 + src/ts/generated/changelog.ts | 693 + src/ts/generated/fa-icons.ts | 132 + src/ts/generated/gamepad-mappings.ts | 274 + src/ts/generated/gamepad-template.ts | 177 + src/ts/generated/hash.json | 1 + src/ts/generated/hash.ts | 2 + src/ts/generated/pony.bin | Bin 0 -> 4194304 bytes src/ts/generated/rev.ts | 76 + src/ts/generated/rollbarConfig.ts | 2 + src/ts/generated/shaders.ts | 17 + src/ts/generated/sprites.ts | 3478 +++ src/ts/graphics/baseSpriteBatch.ts | 202 + src/ts/graphics/baseStateBatch.ts | 76 + src/ts/graphics/contextSpriteBatch.ts | 359 + src/ts/graphics/graphicsUtils.ts | 586 + src/ts/graphics/paletteManager.ts | 247 + src/ts/graphics/paletteSpriteBatch.ts | 243 + src/ts/graphics/shaders/basic.glsl | 25 + src/ts/graphics/shaders/light.glsl | 30 + src/ts/graphics/shaders/palette-depth.glsl | 39 + .../shaders/palette-layers-instanced.glsl | 51 + src/ts/graphics/shaders/palette-layers.glsl | 51 + src/ts/graphics/shaders/palette.glsl | 34 + src/ts/graphics/shaders/sprite.glsl | 31 + src/ts/graphics/shaders/sprite2.glsl | 29 + src/ts/graphics/spriteBatch.ts | 84 + src/ts/graphics/spriteBatchUtils.ts | 12 + src/ts/graphics/spriteFont.ts | 276 + src/ts/graphics/spriteSheetUtils.ts | 16 + src/ts/graphics/webgl/frameBuffer.ts | 48 + src/ts/graphics/webgl/glFbo.ts | 308 + src/ts/graphics/webgl/glVao.ts | 155 + src/ts/graphics/webgl/shader.ts | 100 + src/ts/graphics/webgl/texture2d.ts | 98 + src/ts/graphics/webgl/vaoAttributes.ts | 44 + src/ts/graphics/webgl/webglUtils.ts | 67 + src/ts/lodash.ts | 102 + src/ts/server/accountUtils.ts | 249 + src/ts/server/adminEncoders.ts | 45 + src/ts/server/adminServerActions.ts | 598 + src/ts/server/api/account.ts | 258 + src/ts/server/api/admin-accounts.ts | 195 + src/ts/server/api/admin-auths.ts | 36 + src/ts/server/api/admin.ts | 298 + src/ts/server/api/duplicates.ts | 187 + src/ts/server/api/game.ts | 94 + src/ts/server/api/internal-admin.ts | 14 + src/ts/server/api/internal-common.ts | 4 + src/ts/server/api/internal-login.ts | 62 + src/ts/server/api/internal.ts | 333 + src/ts/server/api/merge.ts | 272 + src/ts/server/api/origins.ts | 113 + src/ts/server/api/ponies.ts | 112 + src/ts/server/api/pony.ts | 148 + src/ts/server/authUtils.ts | 106 + src/ts/server/boot.ts | 19 + src/ts/server/canvasUtilsNode.ts | 22 + src/ts/server/characterUtils.ts | 220 + src/ts/server/chat.ts | 305 + src/ts/server/cmUtils.ts | 518 + src/ts/server/commands.ts | 648 + src/ts/server/config.ts | 70 + src/ts/server/controllerUtils.ts | 139 + src/ts/server/controllers.ts | 10 + src/ts/server/controllers/cloudController.ts | 55 + .../controllers/collectableController.ts | 72 + .../controllers/fakeClientController.ts | 150 + .../controllers/flyingCritterController.ts | 89 + src/ts/server/controllers/perfController.ts | 128 + src/ts/server/controllers/plantController.ts | 89 + src/ts/server/controllers/testController.ts | 87 + src/ts/server/controllers/torchController.ts | 33 + src/ts/server/controllers/updateController.ts | 28 + src/ts/server/controllers/wallController.ts | 178 + src/ts/server/db.ts | 485 + src/ts/server/entityUtils.ts | 346 + src/ts/server/internal.ts | 182 + src/ts/server/ipc.ts | 98 + src/ts/server/liveEndPoint.ts | 170 + src/ts/server/liveSettings.ts | 6 + src/ts/server/logger.ts | 105 + src/ts/server/mapUtils.ts | 294 + src/ts/server/maps/caveMap.ts | 983 + src/ts/server/maps/customMap.ts | 42 + src/ts/server/maps/houseMap.ts | 189 + src/ts/server/maps/islandMap.ts | 386 + src/ts/server/maps/mainMap.ts | 4646 ++++ src/ts/server/maps/paletteMap.ts | 44 + src/ts/server/move.ts | 219 + src/ts/server/oauth.ts | 150 + src/ts/server/originUtils.ts | 45 + src/ts/server/paths.ts | 8 + src/ts/server/patreon.ts | 237 + src/ts/server/playerUtils.ts | 811 + src/ts/server/polling.ts | 244 + src/ts/server/pool.ts | 30 + src/ts/server/regionUtils.ts | 301 + src/ts/server/reporter.ts | 103 + src/ts/server/reporting.ts | 100 + src/ts/server/requestUtils.ts | 217 + src/ts/server/routes/api-account.ts | 36 + src/ts/server/routes/api-game.ts | 35 + src/ts/server/routes/api-pony.ts | 37 + src/ts/server/routes/api-tools.ts | 95 + src/ts/server/routes/api.ts | 20 + src/ts/server/routes/api1.ts | 60 + src/ts/server/routes/api2.ts | 71 + src/ts/server/routes/auth.ts | 317 + src/ts/server/routes/index.ts | 167 + src/ts/server/server.ts | 420 + src/ts/server/serverActions.ts | 762 + src/ts/server/serverActionsManager.ts | 118 + src/ts/server/serverInterfaces.ts | 308 + src/ts/server/serverMap.ts | 529 + src/ts/server/serverRegion.ts | 242 + src/ts/server/serverUtils.ts | 145 + src/ts/server/services/actionLimiter.ts | 44 + src/ts/server/services/adminService.ts | 542 + src/ts/server/services/counter.ts | 51 + src/ts/server/services/friends.ts | 207 + src/ts/server/services/hiding.ts | 364 + src/ts/server/services/liveList.ts | 200 + src/ts/server/services/notification.ts | 71 + src/ts/server/services/observableList.ts | 48 + src/ts/server/services/party.ts | 317 + src/ts/server/services/supporterInvites.ts | 127 + src/ts/server/settings.ts | 45 + src/ts/server/spamChecker.ts | 148 + src/ts/server/start.ts | 105 + src/ts/server/stats.ts | 224 + src/ts/server/timing.ts | 65 + src/ts/server/userError.ts | 52 + src/ts/server/utils/byteSize.ts | 31 + src/ts/server/utils/socketErrorHandler.ts | 156 + src/ts/server/utils/taskQueue.ts | 60 + src/ts/server/world.ts | 999 + src/ts/tests/client/canvasUtils.spec.ts | 78 + src/ts/tests/client/clientActions.spec.ts | 388 + src/ts/tests/client/clientUtils.spec.ts | 232 + src/ts/tests/client/emoji.spec.ts | 114 + src/ts/tests/client/gameLoop.spec.ts | 103 + src/ts/tests/client/htmlUtils.spec.ts | 222 + src/ts/tests/client/paletteManager.spec.ts | 110 + src/ts/tests/client/partyUtils.spec.ts | 193 + src/ts/tests/client/ponyDraw.spec.ts | 153 + src/ts/tests/client/ponyStates.spec.ts | 121 + src/ts/tests/client/spriteFont.spec.ts | 67 + src/ts/tests/client/webglUtils.spec.ts | 49 + src/ts/tests/client/worldMap.spec.ts | 244 + src/ts/tests/common/accountUtils.spec.ts | 153 + src/ts/tests/common/adminUtils.spec.ts | 125 + src/ts/tests/common/animator.spec.ts | 384 + src/ts/tests/common/bitUtils.spec.ts | 198 + src/ts/tests/common/camera.spec.ts | 172 + src/ts/tests/common/collision.spec.ts | 148 + src/ts/tests/common/color.spec.ts | 259 + src/ts/tests/common/colors.spec.ts | 46 + src/ts/tests/common/compress.spec.ts | 23 + src/ts/tests/common/compressPony.spec.ts | 696 + .../common/encoders/updateEncoder.spec.ts | 280 + src/ts/tests/common/entityUtils.spec.ts | 39 + src/ts/tests/common/expressionUtils.spec.ts | 103 + src/ts/tests/common/mat4.spec.ts | 23 + src/ts/tests/common/movementUtils.spec.ts | 136 + src/ts/tests/common/ponyHelpers.spec.ts | 15 + src/ts/tests/common/ponyInfo.spec.ts | 359 + src/ts/tests/common/rect.spec.ts | 11 + src/ts/tests/common/timeUtils.spec.ts | 54 + src/ts/tests/common/utils.spec.ts | 456 + src/ts/tests/generated/sprites.spec.ts | 52 + src/ts/tests/graphics/graphicsUtils.spec.ts | 189 + .../tests/graphics/spriteSheetUtils.spec.ts | 72 + src/ts/tests/lib.ts | 136 + src/ts/tests/mocks.ts | 150 + src/ts/tests/server/accountUtils.spec.ts | 89 + src/ts/tests/server/api/account.spec.ts | 445 + src/ts/tests/server/api/game.spec.ts | 176 + .../tests/server/api/internal-common.spec.ts | 21 + src/ts/tests/server/api/internal.spec.ts | 570 + src/ts/tests/server/api/pony.spec.ts | 400 + src/ts/tests/server/authUtils.spec.ts | 86 + src/ts/tests/server/characterUtils.spec.ts | 365 + src/ts/tests/server/chat.spec.ts | 954 + src/ts/tests/server/cmUtils.spec.ts | 450 + src/ts/tests/server/commands.spec.ts | 615 + src/ts/tests/server/entityUtils.spec.ts | 141 + src/ts/tests/server/liveEndPoint.spec.ts | 259 + src/ts/tests/server/mapUtils.spec.ts | 137 + src/ts/tests/server/move.spec.ts | 321 + src/ts/tests/server/originUtils.spec.ts | 46 + src/ts/tests/server/other.spec.ts | 12 + src/ts/tests/server/patreon.spec.ts | 650 + src/ts/tests/server/playerUtils.spec.ts | 1056 + src/ts/tests/server/regionUtils.spec.ts | 389 + src/ts/tests/server/reporting.spec.ts | 250 + src/ts/tests/server/serverActions.spec.ts | 1041 + src/ts/tests/server/serverMap.spec.ts | 53 + src/ts/tests/server/serverRegion.spec.ts | 121 + src/ts/tests/server/serverUtils.spec.ts | 171 + src/ts/tests/server/services/friends.spec.ts | 4 + src/ts/tests/server/services/hiding.spec.ts | 608 + .../server/services/notification.spec.ts | 207 + src/ts/tests/server/services/party.spec.ts | 873 + .../server/services/supporterInvites.spec.ts | 381 + src/ts/tests/server/spamChecker.spec.ts | 272 + src/ts/tests/server/utils/byteSize.spec.ts | 102 + src/ts/tests/server/utils/taskQueue.spec.ts | 119 + src/ts/tests/server/world.spec.ts | 347 + src/ts/tests/services/errorReporter.spec.ts | 64 + src/ts/tests/services/gameService.spec.ts | 69 + src/ts/tests/services/liveCollection.spec.ts | 462 + src/ts/tests/services/liveList.spec.ts | 463 + src/ts/tests/services/model.spec.ts | 233 + src/ts/tests/services/settingsService.spec.ts | 105 + src/ts/tools/canvas-utils.ts | 283 + src/ts/tools/common.ts | 234 + src/ts/tools/convert-tiles.ts | 41 + src/ts/tools/create-font.ts | 385 + src/ts/tools/create-sprites.ts | 1399 + src/ts/tools/name-tester.ts | 54 + src/ts/tools/palette-utils.ts | 128 + src/ts/tools/psd-utils.ts | 68 + src/ts/tools/sprite-sheet.ts | 492 + src/ts/tools/sprites-template.ts | 195 + src/ts/tools/trigger.txt | 1 + src/ts/tools/types.ts | 112 + src/typings/my.d.ts | 4 + src/typings/my/canvas.d.ts | 22 + src/typings/my/fix.d.ts | 66 + src/typings/my/node-async.d.ts | 22 + src/typings/my/patreon.d.ts | 62 + tsconfig-aot-es.json | 10 + tsconfig-aot.json | 27 + tsconfig.json | 29 + tslint.json | 81 + views/index.pug | 79 + webpack.common.js | 74 + webpack.dev.js | 67 + webpack.prod.js | 89 + workbox-config.js | 45 + 1435 files changed, 140724 insertions(+) create mode 100644 .gitattributes create mode 100644 CHANGELOG.md create mode 100644 LICENSE_ART.txt create mode 100644 LICENSE_CODE.txt create mode 100644 LICENSE_MUSIC.txt create mode 100644 README.md create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_1.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_2.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_3.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_4.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_body .gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_hooves.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_6.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_7.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_8.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_9.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_1.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_2.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_3.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_4.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_5.gif create mode 100644 assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_6.gif create mode 100644 assets-source/animations/bat.psd create mode 100644 assets-source/animations/boat_wake.psd create mode 100644 assets-source/animations/bunny.psd create mode 100644 assets-source/animations/butterfly-4x1.png create mode 100644 assets-source/animations/cat.psd create mode 100644 assets-source/animations/firefly.psd create mode 100644 assets-source/animations/ghost1.psd create mode 100644 assets-source/animations/ghost1_hooves.psd create mode 100644 assets-source/animations/lantern.psd create mode 100644 assets-source/animations/magic.psd create mode 100644 assets-source/animations/magic2.psd create mode 100644 assets-source/animations/magic3.psd create mode 100644 assets-source/animations/pier_leg.psd create mode 100644 assets-source/animations/pony_wake/1.psd create mode 100644 assets-source/animations/pony_wake/2.psd create mode 100644 assets-source/animations/pony_wake/3.psd create mode 100644 assets-source/animations/pony_wake/4.psd create mode 100644 assets-source/animations/pony_wake/5.psd create mode 100644 assets-source/animations/pony_wake/6.psd create mode 100644 assets-source/animations/poof.psd create mode 100644 assets-source/animations/poof2-9x1.png create mode 100644 assets-source/animations/rain-4x1.png create mode 100644 assets-source/animations/raindrop-4x1.png create mode 100644 assets-source/animations/splash.psd create mode 100644 assets-source/animations/splash_boop.psd create mode 100644 assets-source/animations/torch.psd create mode 100644 assets-source/animations/torch2.psd create mode 100644 assets-source/animations/water_crystal/1.psd create mode 100644 assets-source/animations/water_crystal/2.psd create mode 100644 assets-source/animations/water_crystal/3.psd create mode 100644 assets-source/animations/water_rock/1.psd create mode 100644 assets-source/animations/water_rock/10.psd create mode 100644 assets-source/animations/water_rock/11.psd create mode 100644 assets-source/animations/water_rock/2.psd create mode 100644 assets-source/animations/water_rock/3.psd create mode 100644 assets-source/animations/water_rock/4.psd create mode 100644 assets-source/animations/water_rock/5.psd create mode 100644 assets-source/animations/water_rock/6.psd create mode 100644 assets-source/animations/water_rock/7.psd create mode 100644 assets-source/animations/water_rock/8.psd create mode 100644 assets-source/animations/water_rock/9.psd create mode 100644 assets-source/buttons/bubble-2.png create mode 100644 assets-source/buttons/bubble2-2.png create mode 100644 assets-source/emotes/emote_cry.psd create mode 100644 assets-source/emotes/emote_cry2.psd create mode 100644 assets-source/emotes/emote_hearts.psd create mode 100644 assets-source/emotes/emote_sleep1.psd create mode 100644 assets-source/emotes/emote_sleep1_flip.psd create mode 100644 assets-source/emotes/emote_sleep2.psd create mode 100644 assets-source/emotes/emote_sleep2_flip.psd create mode 100644 assets-source/emotes/emote_sneeze.psd create mode 100644 assets-source/emotes/emote_tears.psd create mode 100644 assets-source/emotes/hold_poof.psd create mode 100644 assets-source/fonts/cjk.psd create mode 100644 assets-source/fonts/emoji.psd create mode 100644 assets-source/fonts/flags.psd create mode 100644 assets-source/fonts/main.psd create mode 100644 assets-source/fonts/mono.psd create mode 100644 assets-source/fonts/tiny.psd create mode 100644 assets-source/icons/arrow_down.png create mode 100644 assets-source/icons/arrow_left.png create mode 100644 assets-source/icons/arrow_right.png create mode 100644 assets-source/icons/arrow_up.png create mode 100644 assets-source/icons/magic_icon.png create mode 100644 assets-source/lights/light1.png create mode 100644 assets-source/lights/light16.png create mode 100644 assets-source/lights/light24.png create mode 100644 assets-source/lights/light32.png create mode 100644 assets-source/lights/light6.png create mode 100644 assets-source/lights/light_crystal_lantern.png create mode 100644 assets-source/lights/light_crystals_1.png create mode 100644 assets-source/lights/light_crystals_10.png create mode 100644 assets-source/lights/light_crystals_2.png create mode 100644 assets-source/lights/light_crystals_3.png create mode 100644 assets-source/lights/light_crystals_4.png create mode 100644 assets-source/lights/light_crystals_5.png create mode 100644 assets-source/lights/light_crystals_6.png create mode 100644 assets-source/lights/light_crystals_7.png create mode 100644 assets-source/lights/light_crystals_8.png create mode 100644 assets-source/lights/light_crystals_9.png create mode 100644 assets-source/lights/light_crystals_cart_pile.png create mode 100644 assets-source/lights/light_crystals_held.png create mode 100644 assets-source/objects/acorn.psd create mode 100644 assets-source/objects/apple/1.psd create mode 100644 assets-source/objects/apple/2.psd create mode 100644 assets-source/objects/apple/palette_1_1.psd create mode 100644 assets-source/objects/banana.psd create mode 100644 assets-source/objects/barrel.psd create mode 100644 assets-source/objects/basket_bin.psd create mode 100644 assets-source/objects/bench/1.png create mode 100644 assets-source/objects/bench/1.psd create mode 100644 assets-source/objects/bench/back.png create mode 100644 assets-source/objects/bench/back.psd create mode 100644 assets-source/objects/bench/backh.png create mode 100644 assets-source/objects/bench/backh.psd create mode 100644 assets-source/objects/bench/backh2.png create mode 100644 assets-source/objects/bench/backh2.psd create mode 100644 assets-source/objects/bench/seat.png create mode 100644 assets-source/objects/bench/seat.psd create mode 100644 assets-source/objects/bench/seath.png create mode 100644 assets-source/objects/bench/seath.psd create mode 100644 assets-source/objects/boat.psd create mode 100644 assets-source/objects/boat_front.psd create mode 100644 assets-source/objects/boat_rope.psd create mode 100644 assets-source/objects/boat_sail.psd create mode 100644 assets-source/objects/bookshelf.psd create mode 100644 assets-source/objects/box/_base.psd create mode 100644 assets-source/objects/box/_gifts.psd create mode 100644 assets-source/objects/box/_lanterns.psd create mode 100644 assets-source/objects/box/baskets.psd create mode 100644 assets-source/objects/box/empty.psd create mode 100644 assets-source/objects/box/fruits.psd create mode 100644 assets-source/objects/box/gifts.psd create mode 100644 assets-source/objects/box/lanterns.psd create mode 100644 assets-source/objects/broom.psd create mode 100644 assets-source/objects/candy.psd create mode 100644 assets-source/objects/candy_cane/1.psd create mode 100644 assets-source/objects/candy_cane/2.psd create mode 100644 assets-source/objects/carrot/1.psd create mode 100644 assets-source/objects/carrot/1b.psd create mode 100644 assets-source/objects/carrot/2.psd create mode 100644 assets-source/objects/carrot/2b.psd create mode 100644 assets-source/objects/carrot/3.psd create mode 100644 assets-source/objects/carrot/4.psd create mode 100644 assets-source/objects/carrot/hold.psd create mode 100644 assets-source/objects/carrot/palette_1_hold.psd create mode 100644 assets-source/objects/christmastree.psd create mode 100644 assets-source/objects/cloud.psd create mode 100644 assets-source/objects/clover/1.psd create mode 100644 assets-source/objects/clover/2.psd create mode 100644 assets-source/objects/clover/3.psd create mode 100644 assets-source/objects/clover/4.psd create mode 100644 assets-source/objects/clover/5.psd create mode 100644 assets-source/objects/clover/mouth.psd create mode 100644 assets-source/objects/clover/patch3.psd create mode 100644 assets-source/objects/clover/patch4.psd create mode 100644 assets-source/objects/clover/patch5.psd create mode 100644 assets-source/objects/clover/patch6.psd create mode 100644 assets-source/objects/clover/patch7.psd create mode 100644 assets-source/objects/clover/pick.psd create mode 100644 assets-source/objects/cookie.psd create mode 100644 assets-source/objects/cookie_pony.psd create mode 100644 assets-source/objects/cookie_table/1.psd create mode 100644 assets-source/objects/cookie_table/2.psd create mode 100644 assets-source/objects/crate/1.psd create mode 100644 assets-source/objects/crate/2.psd create mode 100644 assets-source/objects/crate/3.psd create mode 100644 assets-source/objects/crate/palette_1_1.psd create mode 100644 assets-source/objects/crystal_lantern.psd create mode 100644 assets-source/objects/crystals/1.psd create mode 100644 assets-source/objects/crystals/10.psd create mode 100644 assets-source/objects/crystals/2.psd create mode 100644 assets-source/objects/crystals/3.psd create mode 100644 assets-source/objects/crystals/4.psd create mode 100644 assets-source/objects/crystals/5.psd create mode 100644 assets-source/objects/crystals/6.psd create mode 100644 assets-source/objects/crystals/7.psd create mode 100644 assets-source/objects/crystals/8.psd create mode 100644 assets-source/objects/crystals/9.psd create mode 100644 assets-source/objects/crystals/cart_pile.psd create mode 100644 assets-source/objects/crystals/held.psd create mode 100644 assets-source/objects/cushion/1.psd create mode 100644 assets-source/objects/cushion/palette_1_1.psd create mode 100644 assets-source/objects/cushion/palette_2_1.psd create mode 100644 assets-source/objects/direction/down_left.psd create mode 100644 assets-source/objects/direction/down_right.psd create mode 100644 assets-source/objects/direction/left_right.psd create mode 100644 assets-source/objects/direction/pole_3.psd create mode 100644 assets-source/objects/direction/pole_4.psd create mode 100644 assets-source/objects/direction/pole_5.psd create mode 100644 assets-source/objects/direction/shadow_down_left.psd create mode 100644 assets-source/objects/direction/shadow_down_right.psd create mode 100644 assets-source/objects/direction/shadow_left.psd create mode 100644 assets-source/objects/direction/shadow_right.psd create mode 100644 assets-source/objects/direction/shadow_up_left.psd create mode 100644 assets-source/objects/direction/shadow_up_right.psd create mode 100644 assets-source/objects/direction/up_left.psd create mode 100644 assets-source/objects/direction/up_right.psd create mode 100644 assets-source/objects/egg/1.psd create mode 100644 assets-source/objects/egg/10.psd create mode 100644 assets-source/objects/egg/11.psd create mode 100644 assets-source/objects/egg/12.psd create mode 100644 assets-source/objects/egg/13.psd create mode 100644 assets-source/objects/egg/14.psd create mode 100644 assets-source/objects/egg/15.psd create mode 100644 assets-source/objects/egg/16.psd create mode 100644 assets-source/objects/egg/17.psd create mode 100644 assets-source/objects/egg/18.psd create mode 100644 assets-source/objects/egg/19.psd create mode 100644 assets-source/objects/egg/2.psd create mode 100644 assets-source/objects/egg/20.psd create mode 100644 assets-source/objects/egg/21.psd create mode 100644 assets-source/objects/egg/22.psd create mode 100644 assets-source/objects/egg/23.psd create mode 100644 assets-source/objects/egg/3.psd create mode 100644 assets-source/objects/egg/4.psd create mode 100644 assets-source/objects/egg/5.psd create mode 100644 assets-source/objects/egg/6.psd create mode 100644 assets-source/objects/egg/7.psd create mode 100644 assets-source/objects/egg/8.psd create mode 100644 assets-source/objects/egg/9.psd create mode 100644 assets-source/objects/egg/palette_1_1.psd create mode 100644 assets-source/objects/egg/palette_2_1.psd create mode 100644 assets-source/objects/egg/palette_3_3.psd create mode 100644 assets-source/objects/egg/palette_4_3.psd create mode 100644 assets-source/objects/egg/palette_5_6.psd create mode 100644 assets-source/objects/egg/palette_6_7.psd create mode 100644 assets-source/objects/egg/palette_7_8.psd create mode 100644 assets-source/objects/egg/palette_8_5.psd create mode 100644 assets-source/objects/egg_basket/1.psd create mode 100644 assets-source/objects/egg_basket/2.psd create mode 100644 assets-source/objects/egg_basket/3.psd create mode 100644 assets-source/objects/egg_basket/4.psd create mode 100644 assets-source/objects/fence/1.psd create mode 100644 assets-source/objects/fence/2.psd create mode 100644 assets-source/objects/fence/3.psd create mode 100644 assets-source/objects/fence_winter/1.psd create mode 100644 assets-source/objects/fence_winter/2.psd create mode 100644 assets-source/objects/fence_winter/3.psd create mode 100644 assets-source/objects/flower/1.psd create mode 100644 assets-source/objects/flower/2.psd create mode 100644 assets-source/objects/flower/3.psd create mode 100644 assets-source/objects/flower/patch1.psd create mode 100644 assets-source/objects/flower/patch2.psd create mode 100644 assets-source/objects/flower/patch3.psd create mode 100644 assets-source/objects/flower/patch4.psd create mode 100644 assets-source/objects/flower/patch5.psd create mode 100644 assets-source/objects/flower/patch6.psd create mode 100644 assets-source/objects/flower/patch7.psd create mode 100644 assets-source/objects/flower/pick.psd create mode 100644 assets-source/objects/gift/1.psd create mode 100644 assets-source/objects/gift/2.psd create mode 100644 assets-source/objects/gift/palette_1_1.psd create mode 100644 assets-source/objects/giftpile/1.psd create mode 100644 assets-source/objects/giftpile/2.psd create mode 100644 assets-source/objects/giftpile/3.psd create mode 100644 assets-source/objects/giftpile/4.psd create mode 100644 assets-source/objects/giftpile/5.psd create mode 100644 assets-source/objects/giftpile/6.psd create mode 100644 assets-source/objects/giftpile/pine.psd create mode 100644 assets-source/objects/giftpile/sign.psd create mode 100644 assets-source/objects/giftpile/tree.psd create mode 100644 assets-source/objects/gifts/1.psd create mode 100644 assets-source/objects/gifts/2.psd create mode 100644 assets-source/objects/gifts/3.psd create mode 100644 assets-source/objects/gifts/4.psd create mode 100644 assets-source/objects/gifts/5.psd create mode 100644 assets-source/objects/gifts/6.psd create mode 100644 assets-source/objects/gifts/palette_1_1.psd create mode 100644 assets-source/objects/gifts/palette_2_1.psd create mode 100644 assets-source/objects/gifts/palette_3_1.psd create mode 100644 assets-source/objects/grapes/1.psd create mode 100644 assets-source/objects/grapes/2.psd create mode 100644 assets-source/objects/grapes/3.psd create mode 100644 assets-source/objects/grapes/4.psd create mode 100644 assets-source/objects/grapes/5.psd create mode 100644 assets-source/objects/grapes/6.psd create mode 100644 assets-source/objects/grapes/7.psd create mode 100644 assets-source/objects/grapes/one.psd create mode 100644 assets-source/objects/grapes/palette_1_1.psd create mode 100644 assets-source/objects/grass_tile.psd create mode 100644 assets-source/objects/hammer.psd create mode 100644 assets-source/objects/holly.psd create mode 100644 assets-source/objects/house.psd create mode 100644 assets-source/objects/jacko_lantern/off.psd create mode 100644 assets-source/objects/jacko_lantern/on.psd create mode 100644 assets-source/objects/large_leafed_bush/1.psd create mode 100644 assets-source/objects/large_leafed_bush/2.psd create mode 100644 assets-source/objects/large_leafed_bush/3.psd create mode 100644 assets-source/objects/large_leafed_bush/4.psd create mode 100644 assets-source/objects/large_leafed_bush/palette_1_1.psd create mode 100644 assets-source/objects/leaf/1.psd create mode 100644 assets-source/objects/leaf/2.psd create mode 100644 assets-source/objects/leaf/3.psd create mode 100644 assets-source/objects/leaf/4.psd create mode 100644 assets-source/objects/leaf/5.psd create mode 100644 assets-source/objects/leaf/6.psd create mode 100644 assets-source/objects/leaf/palette_1_6.psd create mode 100644 assets-source/objects/leaf/palette_2_6.psd create mode 100644 assets-source/objects/leafpile/big.psd create mode 100644 assets-source/objects/leafpile/bigstick.psd create mode 100644 assets-source/objects/leafpile/medium.psd create mode 100644 assets-source/objects/leafpile/mediumalt.psd create mode 100644 assets-source/objects/leafpile/palette_1_stick.psd create mode 100644 assets-source/objects/leafpile/palette_2_stick.psd create mode 100644 assets-source/objects/leafpile/small.psd create mode 100644 assets-source/objects/leafpile/stick.psd create mode 100644 assets-source/objects/leaves/1.psd create mode 100644 assets-source/objects/leaves/2.psd create mode 100644 assets-source/objects/leaves/3.psd create mode 100644 assets-source/objects/leaves/4.psd create mode 100644 assets-source/objects/leaves/5.psd create mode 100644 assets-source/objects/lemon/1.psd create mode 100644 assets-source/objects/lemon/palette_1_1.psd create mode 100644 assets-source/objects/letter.psd create mode 100644 assets-source/objects/mango.psd create mode 100644 assets-source/objects/mine_cart.psd create mode 100644 assets-source/objects/mine_cart_back.psd create mode 100644 assets-source/objects/mine_cart_front.psd create mode 100644 assets-source/objects/mine_closed.psd create mode 100644 assets-source/objects/mine_entrance.psd create mode 100644 assets-source/objects/mine_rail_fade_up.psd create mode 100644 assets-source/objects/mine_rails/end_left.psd create mode 100644 assets-source/objects/mine_rails/end_right.psd create mode 100644 assets-source/objects/mine_rails/end_top.psd create mode 100644 assets-source/objects/mine_rails/h.psd create mode 100644 assets-source/objects/mine_rails/ne.psd create mode 100644 assets-source/objects/mine_rails/nse.psd create mode 100644 assets-source/objects/mine_rails/nsw.psd create mode 100644 assets-source/objects/mine_rails/nw.psd create mode 100644 assets-source/objects/mine_rails/nwe.psd create mode 100644 assets-source/objects/mine_rails/se.psd create mode 100644 assets-source/objects/mine_rails/sw.psd create mode 100644 assets-source/objects/mine_rails/swe.psd create mode 100644 assets-source/objects/mine_rails/v.psd create mode 100644 assets-source/objects/mistletoe.psd create mode 100644 assets-source/objects/orange/1.psd create mode 100644 assets-source/objects/orange/2.psd create mode 100644 assets-source/objects/pear.psd create mode 100644 assets-source/objects/pickaxe.psd create mode 100644 assets-source/objects/picture/1.psd create mode 100644 assets-source/objects/picture/palette_1_1.psd create mode 100644 assets-source/objects/plank/1.psd create mode 100644 assets-source/objects/plank/2.psd create mode 100644 assets-source/objects/plank/3.psd create mode 100644 assets-source/objects/plank/4.psd create mode 100644 assets-source/objects/plank/palette_1_1.psd create mode 100644 assets-source/objects/plank/shadow.psd create mode 100644 assets-source/objects/plank/shadow2.psd create mode 100644 assets-source/objects/plank/shadow_short.psd create mode 100644 assets-source/objects/plank/short_1.psd create mode 100644 assets-source/objects/plank/short_2.psd create mode 100644 assets-source/objects/plank/short_3.psd create mode 100644 assets-source/objects/pumpkin/default.psd create mode 100644 assets-source/objects/pumpkin/off.psd create mode 100644 assets-source/objects/pumpkin/on.psd create mode 100644 assets-source/objects/rainfall.psd create mode 100644 assets-source/objects/rake.psd create mode 100644 assets-source/objects/rock/1.psd create mode 100644 assets-source/objects/rock/2.psd create mode 100644 assets-source/objects/rock/3.psd create mode 100644 assets-source/objects/rock/palette_1_1.psd create mode 100644 assets-source/objects/rope_rack.psd create mode 100644 assets-source/objects/rose.psd create mode 100644 assets-source/objects/route_pole.psd create mode 100644 assets-source/objects/saw.psd create mode 100644 assets-source/objects/shovel.psd create mode 100644 assets-source/objects/sign/1.psd create mode 100644 assets-source/objects/sign/2.psd create mode 100644 assets-source/objects/sign/3.psd create mode 100644 assets-source/objects/sign/4.psd create mode 100644 assets-source/objects/sign/blank.psd create mode 100644 assets-source/objects/sign_winter.psd create mode 100644 assets-source/objects/snowpile/big.psd create mode 100644 assets-source/objects/snowpile/medium.psd create mode 100644 assets-source/objects/snowpile/palette_1_big.psd create mode 100644 assets-source/objects/snowpile/small.psd create mode 100644 assets-source/objects/snowpile/tinier.psd create mode 100644 assets-source/objects/snowpile/tiny.psd create mode 100644 assets-source/objects/snowpony/1.psd create mode 100644 assets-source/objects/snowpony/2.psd create mode 100644 assets-source/objects/snowpony/3.psd create mode 100644 assets-source/objects/snowpony/4.psd create mode 100644 assets-source/objects/snowpony/5.psd create mode 100644 assets-source/objects/snowpony/6.psd create mode 100644 assets-source/objects/snowpony/7.psd create mode 100644 assets-source/objects/snowpony/8.psd create mode 100644 assets-source/objects/snowpony/9.psd create mode 100644 assets-source/objects/spider.psd create mode 100644 assets-source/objects/stalactite/1.psd create mode 100644 assets-source/objects/stalactite/2.psd create mode 100644 assets-source/objects/stalactite/3.psd create mode 100644 assets-source/objects/stone_wall/full.psd create mode 100644 assets-source/objects/stone_wall/horizontal1.psd create mode 100644 assets-source/objects/stone_wall/pole1.psd create mode 100644 assets-source/objects/stone_wall/vertical1.psd create mode 100644 assets-source/objects/stone_wall_winter/horizontal1.psd create mode 100644 assets-source/objects/stone_wall_winter/pole1.psd create mode 100644 assets-source/objects/stone_wall_winter/vertical1.psd create mode 100644 assets-source/objects/table/1.psd create mode 100644 assets-source/objects/table/2.psd create mode 100644 assets-source/objects/table_3.psd create mode 100644 assets-source/objects/tile.psd create mode 100644 assets-source/objects/tile_none.psd create mode 100644 assets-source/objects/tombstone/1.psd create mode 100644 assets-source/objects/tombstone/2.psd create mode 100644 assets-source/objects/toolbox/empty.psd create mode 100644 assets-source/objects/toolbox/full.psd create mode 100644 assets-source/objects/tools_icon.psd create mode 100644 assets-source/objects/wall_h_placeholder.psd create mode 100644 assets-source/objects/wall_map.psd create mode 100644 assets-source/objects/wall_v_placeholder.psd create mode 100644 assets-source/objects/web.psd create mode 100644 assets-source/objects/well.psd create mode 100644 assets-source/objects/window/1.psd create mode 100644 assets-source/objects/wooden_fence/horizontal1.psd create mode 100644 assets-source/objects/wooden_fence/horizontal2.psd create mode 100644 assets-source/objects/wooden_fence/horizontal3.psd create mode 100644 assets-source/objects/wooden_fence/horizontal4.psd create mode 100644 assets-source/objects/wooden_fence/horizontal5.psd create mode 100644 assets-source/objects/wooden_fence/horizontal6.psd create mode 100644 assets-source/objects/wooden_fence/palette_1_pole1.psd create mode 100644 assets-source/objects/wooden_fence/pole1.psd create mode 100644 assets-source/objects/wooden_fence/pole2.psd create mode 100644 assets-source/objects/wooden_fence/pole3.psd create mode 100644 assets-source/objects/wooden_fence/pole4.psd create mode 100644 assets-source/objects/wooden_fence/pole5.psd create mode 100644 assets-source/objects/wooden_fence/vertical1.psd create mode 100644 assets-source/objects/wooden_fence/vertical2.psd create mode 100644 assets-source/objects/wooden_fence/vertical3.psd create mode 100644 assets-source/objects/wooden_fence_winter/horizontal1.psd create mode 100644 assets-source/objects/wooden_fence_winter/horizontal2.psd create mode 100644 assets-source/objects/wooden_fence_winter/horizontal3.psd create mode 100644 assets-source/objects/wooden_fence_winter/horizontal4.psd create mode 100644 assets-source/objects/wooden_fence_winter/horizontal5.psd create mode 100644 assets-source/objects/wooden_fence_winter/horizontal6.psd create mode 100644 assets-source/objects/wooden_fence_winter/pole1.psd create mode 100644 assets-source/objects/wooden_fence_winter/pole2.psd create mode 100644 assets-source/objects/wooden_fence_winter/pole3.psd create mode 100644 assets-source/objects/wooden_fence_winter/pole4.psd create mode 100644 assets-source/objects/wooden_fence_winter/pole5.psd create mode 100644 assets-source/objects/wooden_fence_winter/vertical1.psd create mode 100644 assets-source/objects/wooden_fence_winter/vertical2.psd create mode 100644 assets-source/objects/wooden_fence_winter/vertical3.psd create mode 100644 assets-source/pony/animations/base.psd create mode 100644 assets-source/pony/animations/boop.psd create mode 100644 assets-source/pony/animations/fly-bat.psd create mode 100644 assets-source/pony/animations/fly-bug.psd create mode 100644 assets-source/pony/animations/fly-griffon.psd create mode 100644 assets-source/pony/animations/fly-pegasus.psd create mode 100644 assets-source/pony/animations/lie.psd create mode 100644 assets-source/pony/animations/sit.psd create mode 100644 assets-source/pony/animations/trot.psd create mode 100644 assets-source/pony/back-legs-accessories.psd create mode 100644 assets-source/pony/back-legs-cms.psd create mode 100644 assets-source/pony/back-legs-hooves.psd create mode 100644 assets-source/pony/back-legs-sleeves.psd create mode 100644 assets-source/pony/back-legs.psd create mode 100644 assets-source/pony/blush.psd create mode 100644 assets-source/pony/body-back-accessory.psd create mode 100644 assets-source/pony/body-chest-accessory.psd create mode 100644 assets-source/pony/body-waist-accessory.psd create mode 100644 assets-source/pony/body-wings.psd create mode 100644 assets-source/pony/body.psd create mode 100644 assets-source/pony/ear-accessory.psd create mode 100644 assets-source/pony/ears.psd create mode 100644 assets-source/pony/extra-accessory.psd create mode 100644 assets-source/pony/eyes.psd create mode 100644 assets-source/pony/face-accessory.psd create mode 100644 assets-source/pony/facial-hair.psd create mode 100644 assets-source/pony/front-legs-accessories.psd create mode 100644 assets-source/pony/front-legs-hooves.psd create mode 100644 assets-source/pony/front-legs-sleeves.psd create mode 100644 assets-source/pony/front-legs.psd create mode 100644 assets-source/pony/head-accessory.psd create mode 100644 assets-source/pony/head.psd create mode 100644 assets-source/pony/horns.psd create mode 100644 assets-source/pony/irises.psd create mode 100644 assets-source/pony/manes.psd create mode 100644 assets-source/pony/muzzles.psd create mode 100644 assets-source/pony/neck-accessory.psd create mode 100644 assets-source/pony/shadows.psd create mode 100644 assets-source/pony/tails.psd create mode 100644 assets-source/sprites-animations/cat_light.psd create mode 100644 assets-source/sprites-animations/ghost1_hooves_light.psd create mode 100644 assets-source/sprites-animations/ghost1_light.psd create mode 100644 assets-source/sprites-animations/lantern_light.psd create mode 100644 assets-source/sprites-animations/magic2_light.psd create mode 100644 assets-source/sprites-animations/magic_light.psd create mode 100644 assets-source/sprites-animations/torch2_light.psd create mode 100644 assets-source/sprites-animations/water_crystal_1_light.psd create mode 100644 assets-source/sprites-animations/water_crystal_2_light.psd create mode 100644 assets-source/sprites-animations/water_crystal_3_light.psd create mode 100644 assets-source/sprites-palette/grass.png create mode 100644 assets-source/sprites-palette/nipple.png create mode 100644 assets-source/sprites-palette/nipple_alt.png create mode 100644 assets-source/sprites/dirt_ice_heightmap.png create mode 100644 assets-source/sprites/dirt_stone_cave_height_map.png create mode 100644 assets-source/sprites/dirt_water_heightmap.png create mode 100644 assets-source/sprites/firefly_light.png create mode 100644 assets-source/sprites/grass.png create mode 100644 assets-source/sprites/jacko_lantern_light.png create mode 100644 assets-source/sprites/nipple.png create mode 100644 assets-source/sprites/pumpkin_light.png create mode 100644 assets-source/tiles/cave-walls.png create mode 100644 assets-source/tiles/cliffs-grass-autumn.png create mode 100644 assets-source/tiles/cliffs-grass-winter.png create mode 100644 assets-source/tiles/cliffs-grass.png create mode 100644 assets-source/tiles/dirt-grass-autumn.png create mode 100644 assets-source/tiles/dirt-grass.png create mode 100644 assets-source/tiles/dirt-ice-autumn.png create mode 100644 assets-source/tiles/dirt-ice-winter.png create mode 100644 assets-source/tiles/dirt-ice.png create mode 100644 assets-source/tiles/dirt-snow.png create mode 100644 assets-source/tiles/dirt-stone-cave.png create mode 100644 assets-source/tiles/dirt-water-1-autumn.png create mode 100644 assets-source/tiles/dirt-water-1-cave.png create mode 100644 assets-source/tiles/dirt-water-1-winter.png create mode 100644 assets-source/tiles/dirt-water-1.png create mode 100644 assets-source/tiles/dirt-water-2.png create mode 100644 assets-source/tiles/dirt-water-3.png create mode 100644 assets-source/tiles/dirt-water-4.png create mode 100644 assets-source/tiles/grass.png create mode 100644 assets-source/tiles/grass.psd create mode 100644 assets-source/tiles/ice-snow.png create mode 100644 assets-source/tiles/stone-tiles.png create mode 100644 assets-source/tiles/stone2-tiles.png create mode 100644 assets-source/tiles/water.png create mode 100644 assets-source/tiles/wood-tiles-1.png create mode 100644 assets-source/tiles/wood-tiles.png create mode 100644 assets-source/tiles/wood.png create mode 100644 assets-source/trees/pine/1.psd create mode 100644 assets-source/trees/pine/2.psd create mode 100644 assets-source/trees/pine/3.psd create mode 100644 assets-source/trees/pine/4.psd create mode 100644 assets-source/trees/pine/5.psd create mode 100644 assets-source/trees/pine/6.psd create mode 100644 assets-source/trees/pine/palette_1_6.psd create mode 100644 assets-source/trees/pine/palette_2_6.psd create mode 100644 assets-source/trees/tree/1.psd create mode 100644 assets-source/trees/tree/2.psd create mode 100644 assets-source/trees/tree/3.psd create mode 100644 assets-source/trees/tree/4.psd create mode 100644 assets-source/trees/tree/5.psd create mode 100644 assets-source/trees/tree/6.psd create mode 100644 assets-source/trees/tree/palette_1_6.psd create mode 100644 assets-source/trees/tree/palette_2_6.psd create mode 100644 assets-source/trees/tree/palette_3_6.psd create mode 100644 assets-source/trees/tree/palette_4_6.psd create mode 100644 assets-source/walls/stone.psd create mode 100644 assets-source/walls/wood.psd create mode 100644 assets/images/avatars.jpg create mode 100644 assets/images/logo-120.png create mode 100644 assets/images/logo-64.png create mode 100644 assets/images/logo-gray.png create mode 100644 assets/images/logo-large.png create mode 100644 assets/images/logo-small.png create mode 100644 assets/images/logo.png create mode 100644 assets/images/pony.png create mode 100644 assets/images/pony2.png create mode 100644 assets/images/pony2a.png create mode 100644 assets/music/ambient.mp3 create mode 100644 assets/music/ambient.webm create mode 100644 assets/music/bossanova.mp3 create mode 100644 assets/music/bossanova.webm create mode 100644 assets/music/building.mp3 create mode 100644 assets/music/building.webm create mode 100644 assets/music/cave-crystals.mp3 create mode 100644 assets/music/cave-crystals.webm create mode 100644 assets/music/cave-secrets.mp3 create mode 100644 assets/music/cave-secrets.webm create mode 100644 assets/music/clop.mp3 create mode 100644 assets/music/clop.webm create mode 100644 assets/music/falling.mp3 create mode 100644 assets/music/falling.webm create mode 100644 assets/music/fivefour.mp3 create mode 100644 assets/music/fivefour.webm create mode 100644 assets/music/ghost.mp3 create mode 100644 assets/music/ghost.webm create mode 100644 assets/music/happy-house.mp3 create mode 100644 assets/music/happy-house.webm create mode 100644 assets/music/hypnosis.mp3 create mode 100644 assets/music/hypnosis.webm create mode 100644 assets/music/island.mp3 create mode 100644 assets/music/island.webm create mode 100644 assets/music/largo.mp3 create mode 100644 assets/music/largo.webm create mode 100644 assets/music/musicbox.mp3 create mode 100644 assets/music/musicbox.webm create mode 100644 assets/music/orchid.mp3 create mode 100644 assets/music/orchid.webm create mode 100644 assets/music/pumpkin.mp3 create mode 100644 assets/music/pumpkin.webm create mode 100644 assets/music/reindeer-winter.mp3 create mode 100644 assets/music/reindeer-winter.webm create mode 100644 assets/music/reindeer.mp3 create mode 100644 assets/music/reindeer.webm create mode 100644 assets/music/scherzo.mp3 create mode 100644 assets/music/scherzo.webm create mode 100644 assets/music/school.mp3 create mode 100644 assets/music/school.webm create mode 100644 assets/music/season.mp3 create mode 100644 assets/music/season.webm create mode 100644 assets/music/sunny-island.mp3 create mode 100644 assets/music/sunny-island.webm create mode 100644 assets/music/sweet-home.mp3 create mode 100644 assets/music/sweet-home.webm create mode 100644 assets/music/tio.mp3 create mode 100644 assets/music/tio.webm create mode 100644 assets/music/trees-winter.mp3 create mode 100644 assets/music/trees-winter.webm create mode 100644 assets/music/trees.mp3 create mode 100644 assets/music/trees.webm create mode 100644 assets/music/trills.mp3 create mode 100644 assets/music/trills.webm create mode 100644 assets/music/unrest.mp3 create mode 100644 assets/music/unrest.webm create mode 100644 assets/music/waltzalt.mp3 create mode 100644 assets/music/waltzalt.webm create mode 100644 assets/music/xmas-air.mp3 create mode 100644 assets/music/xmas-air.webm create mode 100644 assets/music/xmas-horns.mp3 create mode 100644 assets/music/xmas-horns.webm create mode 100644 assets/music/xmas-presents.mp3 create mode 100644 assets/music/xmas-presents.webm create mode 100644 cli.js create mode 100644 config-template.json create mode 100644 favicons/android-chrome-192x192.png create mode 100644 favicons/android-chrome-512x512.png create mode 100644 favicons/apple-touch-icon-152x152.png create mode 100644 favicons/apple-touch-icon-precomposed.png create mode 100644 favicons/apple-touch-icon.png create mode 100644 favicons/browserconfig.xml create mode 100644 favicons/favicon-16x16.png create mode 100644 favicons/favicon-194x194.png create mode 100644 favicons/favicon-32x32.png create mode 100644 favicons/favicon.ico create mode 100644 favicons/manifest.json create mode 100644 favicons/mstile-150x150.png create mode 100644 favicons/safari-pinned-tab.svg create mode 100644 gulpfile.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pony-town-offline.js create mode 100644 pony-town.js create mode 100644 public/images/logo-120.png create mode 100644 public/images/logo-240.png create mode 100644 public/images/offline-pony.png create mode 100644 public/offline.html create mode 100644 public/privacypolicy.html create mode 100644 public/reload.html create mode 100644 public/reset.html create mode 100644 public/robots.txt create mode 100644 public/termsofservice.html create mode 100644 src/fixes/empty.js create mode 100644 src/maps/cave.json create mode 100644 src/maps/custom.json create mode 100644 src/maps/island.json create mode 100644 src/maps/main.json create mode 100644 src/styles/partials/_admin.scss create mode 100644 src/styles/partials/_animations.scss create mode 100644 src/styles/partials/_base.scss create mode 100644 src/styles/partials/_bootstrap-admin.scss create mode 100644 src/styles/partials/_bootstrap.scss create mode 100644 src/styles/partials/_chat.scss create mode 100644 src/styles/partials/_common.scss create mode 100644 src/styles/partials/_controls.scss create mode 100644 src/styles/partials/_custom.scss create mode 100644 src/styles/partials/_game.scss create mode 100644 src/styles/partials/_loading.scss create mode 100644 src/styles/partials/_mixins.scss create mode 100644 src/styles/partials/_shared.scss create mode 100644 src/styles/partials/_tiny.scss create mode 100644 src/styles/partials/_tools.scss create mode 100644 src/styles/partials/_variables.scss create mode 100644 src/styles/style-admin.scss create mode 100644 src/styles/style-inline.scss create mode 100644 src/styles/style-tools.scss create mode 100644 src/styles/style.scss create mode 100644 src/tests/font/ascii.png create mode 100644 src/tests/font/chinese.png create mode 100644 src/tests/font/emoji.png create mode 100644 src/tests/font/greek.png create mode 100644 src/tests/font/hiragana.png create mode 100644 src/tests/font/kanji.png create mode 100644 src/tests/font/katakana.png create mode 100644 src/tests/font/newlines.png create mode 100644 src/tests/font/punctuation.png create mode 100644 src/tests/font/romaji.png create mode 100644 src/tests/font/russian.png create mode 100644 src/tests/font/shapes.png create mode 100644 src/tests/font/special.png create mode 100644 src/tests/font/tiny.png create mode 100644 src/tests/font/variants.png create mode 100644 src/tests/graphics/admin.png create mode 100644 src/tests/graphics/announcement.png create mode 100644 src/tests/graphics/bounds-none.png create mode 100644 src/tests/graphics/bounds.png create mode 100644 src/tests/graphics/break-chat.png create mode 100644 src/tests/graphics/break-think.png create mode 100644 src/tests/graphics/fade-in-0.png create mode 100644 src/tests/graphics/fade-in-1.png create mode 100644 src/tests/graphics/fade-in-2.png create mode 100644 src/tests/graphics/fade-out-0.png create mode 100644 src/tests/graphics/fade-out-1.png create mode 100644 src/tests/graphics/fade-out-2.png create mode 100644 src/tests/graphics/hello-apple.png create mode 100644 src/tests/graphics/hello.png create mode 100644 src/tests/graphics/mod.png create mode 100644 src/tests/graphics/move-left-chat.png create mode 100644 src/tests/graphics/move-left-think.png create mode 100644 src/tests/graphics/move-right-chat.png create mode 100644 src/tests/graphics/move-right-nipple-chat.png create mode 100644 src/tests/graphics/move-right-nipple-think.png create mode 100644 src/tests/graphics/move-right-think.png create mode 100644 src/tests/graphics/name-apple.png create mode 100644 src/tests/graphics/name-dev.png create mode 100644 src/tests/graphics/name-mod.png create mode 100644 src/tests/graphics/name-party.png create mode 100644 src/tests/graphics/name-sup1.png create mode 100644 src/tests/graphics/name-sup2.png create mode 100644 src/tests/graphics/name-sup3.png create mode 100644 src/tests/graphics/name.png create mode 100644 src/tests/graphics/newline.png create mode 100644 src/tests/graphics/outline.png create mode 100644 src/tests/graphics/outside-chat.png create mode 100644 src/tests/graphics/outside-think.png create mode 100644 src/tests/graphics/outside.png create mode 100644 src/tests/graphics/party-announcement.png create mode 100644 src/tests/graphics/party-thinking.png create mode 100644 src/tests/graphics/party.png create mode 100644 src/tests/graphics/pixel-numbers.png create mode 100644 src/tests/graphics/sup1.png create mode 100644 src/tests/graphics/sup2.png create mode 100644 src/tests/graphics/sup3.png create mode 100644 src/tests/graphics/system.png create mode 100644 src/tests/graphics/thinking.png create mode 100644 src/tests/ponies/base.json create mode 100644 src/tests/ponies/default.json create mode 100644 src/tests/ponies/perf.json create mode 100644 src/tests/ponies/school.json create mode 100644 src/tests/ponies/wizard.json create mode 100644 src/tests/pony/all.png create mode 100644 src/tests/pony/base.png create mode 100644 src/tests/pony/blinking-angry.png create mode 100644 src/tests/pony/blinking-skip.png create mode 100644 src/tests/pony/blinking.png create mode 100644 src/tests/pony/blush.png create mode 100644 src/tests/pony/cape.png create mode 100644 src/tests/pony/cm-flip.png create mode 100644 src/tests/pony/deer.png create mode 100644 src/tests/pony/dragon.png create mode 100644 src/tests/pony/extra.png create mode 100644 src/tests/pony/eyes-0.png create mode 100644 src/tests/pony/face-extra-holding-letter.png create mode 100644 src/tests/pony/face-extra.png create mode 100644 src/tests/pony/freckles-flip.png create mode 100644 src/tests/pony/glasses.png create mode 100644 src/tests/pony/griffon-lantern.png create mode 100644 src/tests/pony/griffon-socks.png create mode 100644 src/tests/pony/griffon.png create mode 100644 src/tests/pony/hair-behind-neck-accessory.png create mode 100644 src/tests/pony/hair-in-front-of-wing.png create mode 100644 src/tests/pony/hat-bald.png create mode 100644 src/tests/pony/hat-horns.png create mode 100644 src/tests/pony/holding-letter.png create mode 100644 src/tests/pony/holding.png create mode 100644 src/tests/pony/laughing.png create mode 100644 src/tests/pony/lie.png create mode 100644 src/tests/pony/mustache.png create mode 100644 src/tests/pony/no-body.png create mode 100644 src/tests/pony/none.png create mode 100644 src/tests/pony/offline.png create mode 100644 src/tests/pony/paws-socks.png create mode 100644 src/tests/pony/paws.png create mode 100644 src/tests/pony/selected.png create mode 100644 src/tests/pony/sit-fetlocks.png create mode 100644 src/tests/pony/sit-neck-accessory.png create mode 100644 src/tests/pony/sit-paws.png create mode 100644 src/tests/pony/sit-socks-cm.png create mode 100644 src/tests/pony/sit-socks.png create mode 100644 src/tests/pony/sit-sword.png create mode 100644 src/tests/pony/sit-wing.png create mode 100644 src/tests/pony/sit.png create mode 100644 src/tests/pony/sitting-with-skirt-and-socks.png create mode 100644 src/tests/pony/trot.png create mode 100644 src/tests/pony/wing-open.png create mode 100644 src/ts/bootstrap-admin.ts create mode 100644 src/ts/bootstrap-common.ts create mode 100644 src/ts/bootstrap-es.ts create mode 100644 src/ts/bootstrap-tools.ts create mode 100644 src/ts/bootstrap.ts create mode 100644 src/ts/client/buttonActions.ts create mode 100644 src/ts/client/canvasUtils.ts create mode 100644 src/ts/client/clientActions.ts create mode 100644 src/ts/client/clientAdminActions.ts create mode 100644 src/ts/client/clientUtils.ts create mode 100644 src/ts/client/credits.ts create mode 100644 src/ts/client/data.ts create mode 100644 src/ts/client/draw.ts create mode 100644 src/ts/client/emoji.ts create mode 100644 src/ts/client/fonts.ts create mode 100644 src/ts/client/game.ts create mode 100644 src/ts/client/gameLoop.ts create mode 100644 src/ts/client/gameUtils.ts create mode 100644 src/ts/client/handlers.ts create mode 100644 src/ts/client/htmlUtils.ts create mode 100644 src/ts/client/icons.ts create mode 100644 src/ts/client/input/gamepad.ts create mode 100644 src/ts/client/input/input.ts create mode 100644 src/ts/client/input/inputManager.ts create mode 100644 src/ts/client/input/keyboard.ts create mode 100644 src/ts/client/input/mouse.ts create mode 100644 src/ts/client/input/touch.ts create mode 100644 src/ts/client/partyUtils.ts create mode 100644 src/ts/client/playerActions.ts create mode 100644 src/ts/client/polyfils.ts create mode 100644 src/ts/client/ponyAnimations.ts create mode 100644 src/ts/client/ponyDraw.ts create mode 100644 src/ts/client/ponyHelpers.ts create mode 100644 src/ts/client/ponyStates.ts create mode 100644 src/ts/client/ponyUtils.ts create mode 100644 src/ts/client/rev.ts create mode 100644 src/ts/client/sec.ts create mode 100644 src/ts/client/spriteAnimations.ts create mode 100644 src/ts/client/spriteUtils.ts create mode 100644 src/ts/client/tileUtils.ts create mode 100644 src/ts/client/timing.ts create mode 100644 src/ts/client/webgl.ts create mode 100644 src/ts/common/accountUtils.ts create mode 100644 src/ts/common/adminInterfaces.ts create mode 100644 src/ts/common/adminUtils.ts create mode 100644 src/ts/common/animationPlayer.ts create mode 100644 src/ts/common/animator.ts create mode 100644 src/ts/common/binaryUtils.ts create mode 100644 src/ts/common/bitUtils.ts create mode 100644 src/ts/common/camera.ts create mode 100644 src/ts/common/collision.ts create mode 100644 src/ts/common/color.ts create mode 100644 src/ts/common/colors.ts create mode 100644 src/ts/common/compress.ts create mode 100644 src/ts/common/compressPony.ts create mode 100644 src/ts/common/constants.ts create mode 100644 src/ts/common/countries.ts create mode 100644 src/ts/common/debugData.ts create mode 100644 src/ts/common/encoders/expressionEncoder.ts create mode 100644 src/ts/common/encoders/updateDecoder.ts create mode 100644 src/ts/common/encoders/updateEncoder.ts create mode 100644 src/ts/common/entities.ts create mode 100644 src/ts/common/entityUtils.ts create mode 100644 src/ts/common/errors.ts create mode 100644 src/ts/common/expressionUtils.ts create mode 100644 src/ts/common/expressions.ts create mode 100644 src/ts/common/filterUtils.ts create mode 100644 src/ts/common/interfaces.ts create mode 100644 src/ts/common/mat2d.ts create mode 100644 src/ts/common/mat4.ts create mode 100644 src/ts/common/mixins.ts create mode 100644 src/ts/common/movementUtils.ts create mode 100644 src/ts/common/offsets.ts create mode 100644 src/ts/common/pony.ts create mode 100644 src/ts/common/ponyInfo.ts create mode 100644 src/ts/common/positionUtils.ts create mode 100644 src/ts/common/rect.ts create mode 100644 src/ts/common/region.ts create mode 100644 src/ts/common/rollbar.ts create mode 100644 src/ts/common/security.ts create mode 100644 src/ts/common/sheets.ts create mode 100644 src/ts/common/stringUtils.ts create mode 100644 src/ts/common/swears.ts create mode 100644 src/ts/common/tags.ts create mode 100644 src/ts/common/timeUtils.ts create mode 100644 src/ts/common/utils.ts create mode 100644 src/ts/common/worldMap.ts create mode 100644 src/ts/components/admin/admin-account-details/admin-account-details.pug create mode 100644 src/ts/components/admin/admin-account-details/admin-account-details.scss create mode 100644 src/ts/components/admin/admin-account-details/admin-account-details.ts create mode 100644 src/ts/components/admin/admin-accounts/admin-accounts.pug create mode 100644 src/ts/components/admin/admin-accounts/admin-accounts.ts create mode 100644 src/ts/components/admin/admin-events/admin-events.pug create mode 100644 src/ts/components/admin/admin-events/admin-events.ts create mode 100644 src/ts/components/admin/admin-origin-details/admin-origin-details.pug create mode 100644 src/ts/components/admin/admin-origin-details/admin-origin-details.ts create mode 100644 src/ts/components/admin/admin-origins/admin-origins.pug create mode 100644 src/ts/components/admin/admin-origins/admin-origins.ts create mode 100644 src/ts/components/admin/admin-other/admin-other.pug create mode 100644 src/ts/components/admin/admin-other/admin-other.ts create mode 100644 src/ts/components/admin/admin-ponies/admin-ponies.pug create mode 100644 src/ts/components/admin/admin-ponies/admin-ponies.scss create mode 100644 src/ts/components/admin/admin-ponies/admin-ponies.ts create mode 100644 src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug create mode 100644 src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts create mode 100644 src/ts/components/admin/admin-reports/admin-reports.pug create mode 100644 src/ts/components/admin/admin-reports/admin-reports.ts create mode 100644 src/ts/components/admin/admin-sign-in/admin-sign-in.pug create mode 100644 src/ts/components/admin/admin-sign-in/admin-sign-in.ts create mode 100644 src/ts/components/admin/admin-state/admin-state.pug create mode 100644 src/ts/components/admin/admin-state/admin-state.scss create mode 100644 src/ts/components/admin/admin-state/admin-state.ts create mode 100644 src/ts/components/admin/admin.module.ts create mode 100644 src/ts/components/admin/admin.pug create mode 100644 src/ts/components/admin/admin.scss create mode 100644 src/ts/components/admin/admin.ts create mode 100644 src/ts/components/admin/base-table.ts create mode 100644 src/ts/components/admin/pipes/keysPipe.ts create mode 100644 src/ts/components/admin/pipes/orderByPipe.ts create mode 100644 src/ts/components/admin/pipes/translitPipe.ts create mode 100644 src/ts/components/admin/shared/account-info-remote/account-info-remote.pug create mode 100644 src/ts/components/admin/shared/account-info-remote/account-info-remote.ts create mode 100644 src/ts/components/admin/shared/account-info/account-info.pug create mode 100644 src/ts/components/admin/shared/account-info/account-info.scss create mode 100644 src/ts/components/admin/shared/account-info/account-info.ts create mode 100644 src/ts/components/admin/shared/account-status/account-status.pug create mode 100644 src/ts/components/admin/shared/account-status/account-status.ts create mode 100644 src/ts/components/admin/shared/account-tooltip/account-tooltip.pug create mode 100644 src/ts/components/admin/shared/account-tooltip/account-tooltip.ts create mode 100644 src/ts/components/admin/shared/admin-chat-log/admin-chat-log.pug create mode 100644 src/ts/components/admin/shared/admin-chat-log/admin-chat-log.scss create mode 100644 src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts create mode 100644 src/ts/components/admin/shared/auth-info-edit/auth-info-edit.pug create mode 100644 src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts create mode 100644 src/ts/components/admin/shared/auth-info-remote/auth-info-remote.pug create mode 100644 src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts create mode 100644 src/ts/components/admin/shared/auth-info/auth-info.pug create mode 100644 src/ts/components/admin/shared/auth-info/auth-info.scss create mode 100644 src/ts/components/admin/shared/auth-info/auth-info.ts create mode 100644 src/ts/components/admin/shared/auth-list-remote/auth-list-remote.pug create mode 100644 src/ts/components/admin/shared/auth-list-remote/auth-list-remote.scss create mode 100644 src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts create mode 100644 src/ts/components/admin/shared/auth-list/auth-list.pug create mode 100644 src/ts/components/admin/shared/auth-list/auth-list.scss create mode 100644 src/ts/components/admin/shared/auth-list/auth-list.ts create mode 100644 src/ts/components/admin/shared/ban-icon/ban-icon.pug create mode 100644 src/ts/components/admin/shared/ban-icon/ban-icon.scss create mode 100644 src/ts/components/admin/shared/ban-icon/ban-icon.ts create mode 100644 src/ts/components/admin/shared/email-list/email-list.pug create mode 100644 src/ts/components/admin/shared/email-list/email-list.ts create mode 100644 src/ts/components/admin/shared/events-table/events-table.pug create mode 100644 src/ts/components/admin/shared/events-table/events-table.scss create mode 100644 src/ts/components/admin/shared/events-table/events-table.ts create mode 100644 src/ts/components/admin/shared/from-now.ts create mode 100644 src/ts/components/admin/shared/on-off-switch/on-off-switch.pug create mode 100644 src/ts/components/admin/shared/on-off-switch/on-off-switch.ts create mode 100644 src/ts/components/admin/shared/origin-info-remote/origin-info-remote.pug create mode 100644 src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts create mode 100644 src/ts/components/admin/shared/origin-info/origin-info.pug create mode 100644 src/ts/components/admin/shared/origin-info/origin-info.scss create mode 100644 src/ts/components/admin/shared/origin-info/origin-info.ts create mode 100644 src/ts/components/admin/shared/origin-list-remote/origin-list-remote.pug create mode 100644 src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts create mode 100644 src/ts/components/admin/shared/pony-info-remote/pony-info-remote.pug create mode 100644 src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts create mode 100644 src/ts/components/admin/shared/pony-info/pony-info.pug create mode 100644 src/ts/components/admin/shared/pony-info/pony-info.scss create mode 100644 src/ts/components/admin/shared/pony-info/pony-info.ts create mode 100644 src/ts/components/admin/shared/pony-list-remote/pony-list-remote.pug create mode 100644 src/ts/components/admin/shared/pony-list-remote/pony-list-remote.scss create mode 100644 src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts create mode 100644 src/ts/components/admin/shared/time-field/time-field.pug create mode 100644 src/ts/components/admin/shared/time-field/time-field.ts create mode 100644 src/ts/components/admin/shared/ua-info/ua-info.pug create mode 100644 src/ts/components/admin/shared/ua-info/ua-info.ts create mode 100644 src/ts/components/app/about/about.pug create mode 100644 src/ts/components/app/about/about.scss create mode 100644 src/ts/components/app/about/about.ts create mode 100644 src/ts/components/app/account/account.pug create mode 100644 src/ts/components/app/account/account.scss create mode 100644 src/ts/components/app/account/account.ts create mode 100644 src/ts/components/app/app.module.ts create mode 100644 src/ts/components/app/app.pug create mode 100644 src/ts/components/app/app.scss create mode 100644 src/ts/components/app/app.ts create mode 100644 src/ts/components/app/character/character.pug create mode 100644 src/ts/components/app/character/character.scss create mode 100644 src/ts/components/app/character/character.ts create mode 100644 src/ts/components/app/editor-box/editor-box.pug create mode 100644 src/ts/components/app/editor-box/editor-box.ts create mode 100644 src/ts/components/app/help/help.pug create mode 100644 src/ts/components/app/help/help.scss create mode 100644 src/ts/components/app/help/help.ts create mode 100644 src/ts/components/app/home/home.pug create mode 100644 src/ts/components/app/home/home.scss create mode 100644 src/ts/components/app/home/home.ts create mode 100644 src/ts/components/services/adminModel.ts create mode 100644 src/ts/components/services/audio.ts create mode 100644 src/ts/components/services/authGuard.ts create mode 100644 src/ts/components/services/errorReporter.ts create mode 100644 src/ts/components/services/frameService.ts create mode 100644 src/ts/components/services/gameService.ts create mode 100644 src/ts/components/services/installService.ts create mode 100644 src/ts/components/services/intervalUpdateService.ts create mode 100644 src/ts/components/services/liveCollection.ts create mode 100644 src/ts/components/services/model.ts create mode 100644 src/ts/components/services/modelSubscriber.ts create mode 100644 src/ts/components/services/rollbarErrorHandler.ts create mode 100644 src/ts/components/services/rollbarErrorReporter.ts create mode 100644 src/ts/components/services/settingsService.ts create mode 100644 src/ts/components/services/storageService.ts create mode 100644 src/ts/components/shared/action-bar/action-bar.pug create mode 100644 src/ts/components/shared/action-bar/action-bar.scss create mode 100644 src/ts/components/shared/action-bar/action-bar.ts create mode 100644 src/ts/components/shared/action-button/action-button.pug create mode 100644 src/ts/components/shared/action-button/action-button.scss create mode 100644 src/ts/components/shared/action-button/action-button.ts create mode 100644 src/ts/components/shared/actions-modal/actions-modal.pug create mode 100644 src/ts/components/shared/actions-modal/actions-modal.scss create mode 100644 src/ts/components/shared/actions-modal/actions-modal.ts create mode 100644 src/ts/components/shared/bitmap-box/bitmap-box.pug create mode 100644 src/ts/components/shared/bitmap-box/bitmap-box.scss create mode 100644 src/ts/components/shared/bitmap-box/bitmap-box.ts create mode 100644 src/ts/components/shared/butt-mark-editor/butt-mark-editor.pug create mode 100644 src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts create mode 100644 src/ts/components/shared/character-list/character-list.pug create mode 100644 src/ts/components/shared/character-list/character-list.scss create mode 100644 src/ts/components/shared/character-list/character-list.ts create mode 100644 src/ts/components/shared/character-preview/character-preview.ts create mode 100644 src/ts/components/shared/character-select/character-select.pug create mode 100644 src/ts/components/shared/character-select/character-select.scss create mode 100644 src/ts/components/shared/character-select/character-select.ts create mode 100644 src/ts/components/shared/chat-box/chat-box.pug create mode 100644 src/ts/components/shared/chat-box/chat-box.scss create mode 100644 src/ts/components/shared/chat-box/chat-box.ts create mode 100644 src/ts/components/shared/chat-log/chat-log.pug create mode 100644 src/ts/components/shared/chat-log/chat-log.scss create mode 100644 src/ts/components/shared/chat-log/chat-log.ts create mode 100644 src/ts/components/shared/check-box/check-box.pug create mode 100644 src/ts/components/shared/check-box/check-box.scss create mode 100644 src/ts/components/shared/check-box/check-box.ts create mode 100644 src/ts/components/shared/color-picker/color-picker.pug create mode 100644 src/ts/components/shared/color-picker/color-picker.scss create mode 100644 src/ts/components/shared/color-picker/color-picker.ts create mode 100644 src/ts/components/shared/custom-checkbox/custom-checkbox.pug create mode 100644 src/ts/components/shared/custom-checkbox/custom-checkbox.scss create mode 100644 src/ts/components/shared/custom-checkbox/custom-checkbox.ts create mode 100644 src/ts/components/shared/date-picker/date-picker.pug create mode 100644 src/ts/components/shared/date-picker/date-picker.ts create mode 100644 src/ts/components/shared/directives/agAutoFocus.ts create mode 100644 src/ts/components/shared/directives/agDrag.ts create mode 100644 src/ts/components/shared/directives/anchor.ts create mode 100644 src/ts/components/shared/directives/btnHighlight.ts create mode 100644 src/ts/components/shared/directives/draggable.ts create mode 100644 src/ts/components/shared/directives/dropdown.ts create mode 100644 src/ts/components/shared/directives/fixToTop.ts create mode 100644 src/ts/components/shared/directives/focusTitle.ts create mode 100644 src/ts/components/shared/directives/focusTrap.ts create mode 100644 src/ts/components/shared/directives/hasFeature.ts create mode 100644 src/ts/components/shared/directives/labelledBy.ts create mode 100644 src/ts/components/shared/directives/linkCurrent.ts create mode 100644 src/ts/components/shared/directives/revSrc.ts create mode 100644 src/ts/components/shared/directives/saveActiveTab.ts create mode 100644 src/ts/components/shared/emote-box/emote-box.ts create mode 100644 src/ts/components/shared/fill-outline/fill-outline.pug create mode 100644 src/ts/components/shared/fill-outline/fill-outline.scss create mode 100644 src/ts/components/shared/fill-outline/fill-outline.ts create mode 100644 src/ts/components/shared/friends-box/friends-box.pug create mode 100644 src/ts/components/shared/friends-box/friends-box.scss create mode 100644 src/ts/components/shared/friends-box/friends-box.ts create mode 100644 src/ts/components/shared/install-button/install-button.pug create mode 100644 src/ts/components/shared/install-button/install-button.scss create mode 100644 src/ts/components/shared/install-button/install-button.ts create mode 100644 src/ts/components/shared/invites-modal/invites-modal.pug create mode 100644 src/ts/components/shared/invites-modal/invites-modal.ts create mode 100644 src/ts/components/shared/kbd-key/kbd-key.pug create mode 100644 src/ts/components/shared/kbd-key/kbd-key.ts create mode 100644 src/ts/components/shared/menu-bar/menu-bar.pug create mode 100644 src/ts/components/shared/menu-bar/menu-bar.scss create mode 100644 src/ts/components/shared/menu-bar/menu-bar.ts create mode 100644 src/ts/components/shared/menu-item/menu-item.pug create mode 100644 src/ts/components/shared/menu-item/menu-item.scss create mode 100644 src/ts/components/shared/menu-item/menu-item.ts create mode 100644 src/ts/components/shared/mod-box/mod-box.pug create mode 100644 src/ts/components/shared/mod-box/mod-box.scss create mode 100644 src/ts/components/shared/mod-box/mod-box.ts create mode 100644 src/ts/components/shared/notification/notification-item.pug create mode 100644 src/ts/components/shared/notification/notification-item.scss create mode 100644 src/ts/components/shared/notification/notification-item.ts create mode 100644 src/ts/components/shared/notification/notification-list.pug create mode 100644 src/ts/components/shared/notification/notification-list.scss create mode 100644 src/ts/components/shared/notification/notification-list.ts create mode 100644 src/ts/components/shared/page-loader/page-loader.pug create mode 100644 src/ts/components/shared/page-loader/page-loader.scss create mode 100644 src/ts/components/shared/page-loader/page-loader.ts create mode 100644 src/ts/components/shared/party-box/party-box.pug create mode 100644 src/ts/components/shared/party-box/party-box.scss create mode 100644 src/ts/components/shared/party-box/party-box.ts create mode 100644 src/ts/components/shared/party-list/party-list.pug create mode 100644 src/ts/components/shared/party-list/party-list.scss create mode 100644 src/ts/components/shared/party-list/party-list.ts create mode 100644 src/ts/components/shared/pipes/siteName.ts create mode 100644 src/ts/components/shared/play-box/play-box.pug create mode 100644 src/ts/components/shared/play-box/play-box.scss create mode 100644 src/ts/components/shared/play-box/play-box.ts create mode 100644 src/ts/components/shared/play-notice/play-notice.pug create mode 100644 src/ts/components/shared/play-notice/play-notice.ts create mode 100644 src/ts/components/shared/pony-box/pony-box.pug create mode 100644 src/ts/components/shared/pony-box/pony-box.scss create mode 100644 src/ts/components/shared/pony-box/pony-box.ts create mode 100644 src/ts/components/shared/portrait-box/portrait-box.pug create mode 100644 src/ts/components/shared/portrait-box/portrait-box.scss create mode 100644 src/ts/components/shared/portrait-box/portrait-box.ts create mode 100644 src/ts/components/shared/scale-picker/scale-picker.pug create mode 100644 src/ts/components/shared/scale-picker/scale-picker.ts create mode 100644 src/ts/components/shared/set-selection/set-selection.pug create mode 100644 src/ts/components/shared/set-selection/set-selection.scss create mode 100644 src/ts/components/shared/set-selection/set-selection.ts create mode 100644 src/ts/components/shared/settings-box/settings-box.pug create mode 100644 src/ts/components/shared/settings-box/settings-box.scss create mode 100644 src/ts/components/shared/settings-box/settings-box.ts create mode 100644 src/ts/components/shared/settings-modal/settings-modal.pug create mode 100644 src/ts/components/shared/settings-modal/settings-modal.scss create mode 100644 src/ts/components/shared/settings-modal/settings-modal.ts create mode 100644 src/ts/components/shared/shared.module.ts create mode 100644 src/ts/components/shared/sign-in-box/sign-in-box.pug create mode 100644 src/ts/components/shared/sign-in-box/sign-in-box.scss create mode 100644 src/ts/components/shared/sign-in-box/sign-in-box.ts create mode 100644 src/ts/components/shared/site-info/site-info.pug create mode 100644 src/ts/components/shared/site-info/site-info.scss create mode 100644 src/ts/components/shared/site-info/site-info.ts create mode 100644 src/ts/components/shared/site-links/site-links.pug create mode 100644 src/ts/components/shared/site-links/site-links.ts create mode 100644 src/ts/components/shared/slider-bar/slider-bar.pug create mode 100644 src/ts/components/shared/slider-bar/slider-bar.scss create mode 100644 src/ts/components/shared/slider-bar/slider-bar.ts create mode 100644 src/ts/components/shared/sprite-box/sprite-box.pug create mode 100644 src/ts/components/shared/sprite-box/sprite-box.scss create mode 100644 src/ts/components/shared/sprite-box/sprite-box.ts create mode 100644 src/ts/components/shared/sprite-selection/sprite-selection.pug create mode 100644 src/ts/components/shared/sprite-selection/sprite-selection.scss create mode 100644 src/ts/components/shared/sprite-selection/sprite-selection.ts create mode 100644 src/ts/components/shared/support-button/support-button.pug create mode 100644 src/ts/components/shared/support-button/support-button.scss create mode 100644 src/ts/components/shared/support-button/support-button.ts create mode 100644 src/ts/components/shared/supporter-pony/supporter-pony.pug create mode 100644 src/ts/components/shared/supporter-pony/supporter-pony.ts create mode 100644 src/ts/components/shared/swap-box/swap-box.pug create mode 100644 src/ts/components/shared/swap-box/swap-box.scss create mode 100644 src/ts/components/shared/swap-box/swap-box.ts create mode 100644 src/ts/components/shared/tabset/tabset.pug create mode 100644 src/ts/components/shared/tabset/tabset.ts create mode 100644 src/ts/components/shared/virtual-list/virtual-list.scss create mode 100644 src/ts/components/shared/virtual-list/virtual-list.ts create mode 100644 src/ts/components/tools/shared/tools-frame/tools-frame.pug create mode 100644 src/ts/components/tools/shared/tools-frame/tools-frame.scss create mode 100644 src/ts/components/tools/shared/tools-frame/tools-frame.ts create mode 100644 src/ts/components/tools/shared/tools-offset/tools-offset.pug create mode 100644 src/ts/components/tools/shared/tools-offset/tools-offset.scss create mode 100644 src/ts/components/tools/shared/tools-offset/tools-offset.ts create mode 100644 src/ts/components/tools/shared/tools-range/tools-range.pug create mode 100644 src/ts/components/tools/shared/tools-range/tools-range.scss create mode 100644 src/ts/components/tools/shared/tools-range/tools-range.ts create mode 100644 src/ts/components/tools/shared/tools-xy/tools-xy.pug create mode 100644 src/ts/components/tools/shared/tools-xy/tools-xy.scss create mode 100644 src/ts/components/tools/shared/tools-xy/tools-xy.ts create mode 100644 src/ts/components/tools/sheetExport.ts create mode 100644 src/ts/components/tools/tools-animation/tools-animation.pug create mode 100644 src/ts/components/tools/tools-animation/tools-animation.scss create mode 100644 src/ts/components/tools/tools-animation/tools-animation.ts create mode 100644 src/ts/components/tools/tools-chat/tools-chat.pug create mode 100644 src/ts/components/tools/tools-chat/tools-chat.ts create mode 100644 src/ts/components/tools/tools-collisions/tools-collisions.pug create mode 100644 src/ts/components/tools/tools-collisions/tools-collisions.ts create mode 100644 src/ts/components/tools/tools-entity/tools-entity.pug create mode 100644 src/ts/components/tools/tools-entity/tools-entity.ts create mode 100644 src/ts/components/tools/tools-expressions/tools-expressions.pug create mode 100644 src/ts/components/tools/tools-expressions/tools-expressions.ts create mode 100644 src/ts/components/tools/tools-index/tools-index.pug create mode 100644 src/ts/components/tools/tools-index/tools-index.ts create mode 100644 src/ts/components/tools/tools-map/tools-map.pug create mode 100644 src/ts/components/tools/tools-map/tools-map.ts create mode 100644 src/ts/components/tools/tools-palette/tools-palette.pug create mode 100644 src/ts/components/tools/tools-palette/tools-palette.ts create mode 100644 src/ts/components/tools/tools-perf/methods.ts create mode 100644 src/ts/components/tools/tools-perf/tools-perf.pug create mode 100644 src/ts/components/tools/tools-perf/tools-perf.ts create mode 100644 src/ts/components/tools/tools-regions/tools-regions.pug create mode 100644 src/ts/components/tools/tools-regions/tools-regions.ts create mode 100644 src/ts/components/tools/tools-sheet/tools-sheet.pug create mode 100644 src/ts/components/tools/tools-sheet/tools-sheet.scss create mode 100644 src/ts/components/tools/tools-sheet/tools-sheet.ts create mode 100644 src/ts/components/tools/tools-states/tools-states.pug create mode 100644 src/ts/components/tools/tools-states/tools-states.scss create mode 100644 src/ts/components/tools/tools-states/tools-states.ts create mode 100644 src/ts/components/tools/tools-ui/tools-ui.pug create mode 100644 src/ts/components/tools/tools-ui/tools-ui.ts create mode 100644 src/ts/components/tools/tools-variants/tools-variants.pug create mode 100644 src/ts/components/tools/tools-variants/tools-variants.scss create mode 100644 src/ts/components/tools/tools-variants/tools-variants.ts create mode 100644 src/ts/components/tools/tools-webgl/tools-webgl.pug create mode 100644 src/ts/components/tools/tools-webgl/tools-webgl.ts create mode 100644 src/ts/components/tools/tools.module.ts create mode 100644 src/ts/components/tools/tools.pug create mode 100644 src/ts/components/tools/tools.ts create mode 100644 src/ts/generated/changelog.ts create mode 100644 src/ts/generated/fa-icons.ts create mode 100644 src/ts/generated/gamepad-mappings.ts create mode 100644 src/ts/generated/gamepad-template.ts create mode 100644 src/ts/generated/hash.json create mode 100644 src/ts/generated/hash.ts create mode 100644 src/ts/generated/pony.bin create mode 100644 src/ts/generated/rev.ts create mode 100644 src/ts/generated/rollbarConfig.ts create mode 100644 src/ts/generated/shaders.ts create mode 100644 src/ts/generated/sprites.ts create mode 100644 src/ts/graphics/baseSpriteBatch.ts create mode 100644 src/ts/graphics/baseStateBatch.ts create mode 100644 src/ts/graphics/contextSpriteBatch.ts create mode 100644 src/ts/graphics/graphicsUtils.ts create mode 100644 src/ts/graphics/paletteManager.ts create mode 100644 src/ts/graphics/paletteSpriteBatch.ts create mode 100644 src/ts/graphics/shaders/basic.glsl create mode 100644 src/ts/graphics/shaders/light.glsl create mode 100644 src/ts/graphics/shaders/palette-depth.glsl create mode 100644 src/ts/graphics/shaders/palette-layers-instanced.glsl create mode 100644 src/ts/graphics/shaders/palette-layers.glsl create mode 100644 src/ts/graphics/shaders/palette.glsl create mode 100644 src/ts/graphics/shaders/sprite.glsl create mode 100644 src/ts/graphics/shaders/sprite2.glsl create mode 100644 src/ts/graphics/spriteBatch.ts create mode 100644 src/ts/graphics/spriteBatchUtils.ts create mode 100644 src/ts/graphics/spriteFont.ts create mode 100644 src/ts/graphics/spriteSheetUtils.ts create mode 100644 src/ts/graphics/webgl/frameBuffer.ts create mode 100644 src/ts/graphics/webgl/glFbo.ts create mode 100644 src/ts/graphics/webgl/glVao.ts create mode 100644 src/ts/graphics/webgl/shader.ts create mode 100644 src/ts/graphics/webgl/texture2d.ts create mode 100644 src/ts/graphics/webgl/vaoAttributes.ts create mode 100644 src/ts/graphics/webgl/webglUtils.ts create mode 100644 src/ts/lodash.ts create mode 100644 src/ts/server/accountUtils.ts create mode 100644 src/ts/server/adminEncoders.ts create mode 100644 src/ts/server/adminServerActions.ts create mode 100644 src/ts/server/api/account.ts create mode 100644 src/ts/server/api/admin-accounts.ts create mode 100644 src/ts/server/api/admin-auths.ts create mode 100644 src/ts/server/api/admin.ts create mode 100644 src/ts/server/api/duplicates.ts create mode 100644 src/ts/server/api/game.ts create mode 100644 src/ts/server/api/internal-admin.ts create mode 100644 src/ts/server/api/internal-common.ts create mode 100644 src/ts/server/api/internal-login.ts create mode 100644 src/ts/server/api/internal.ts create mode 100644 src/ts/server/api/merge.ts create mode 100644 src/ts/server/api/origins.ts create mode 100644 src/ts/server/api/ponies.ts create mode 100644 src/ts/server/api/pony.ts create mode 100644 src/ts/server/authUtils.ts create mode 100644 src/ts/server/boot.ts create mode 100644 src/ts/server/canvasUtilsNode.ts create mode 100644 src/ts/server/characterUtils.ts create mode 100644 src/ts/server/chat.ts create mode 100644 src/ts/server/cmUtils.ts create mode 100644 src/ts/server/commands.ts create mode 100644 src/ts/server/config.ts create mode 100644 src/ts/server/controllerUtils.ts create mode 100644 src/ts/server/controllers.ts create mode 100644 src/ts/server/controllers/cloudController.ts create mode 100644 src/ts/server/controllers/collectableController.ts create mode 100644 src/ts/server/controllers/fakeClientController.ts create mode 100644 src/ts/server/controllers/flyingCritterController.ts create mode 100644 src/ts/server/controllers/perfController.ts create mode 100644 src/ts/server/controllers/plantController.ts create mode 100644 src/ts/server/controllers/testController.ts create mode 100644 src/ts/server/controllers/torchController.ts create mode 100644 src/ts/server/controllers/updateController.ts create mode 100644 src/ts/server/controllers/wallController.ts create mode 100644 src/ts/server/db.ts create mode 100644 src/ts/server/entityUtils.ts create mode 100644 src/ts/server/internal.ts create mode 100644 src/ts/server/ipc.ts create mode 100644 src/ts/server/liveEndPoint.ts create mode 100644 src/ts/server/liveSettings.ts create mode 100644 src/ts/server/logger.ts create mode 100644 src/ts/server/mapUtils.ts create mode 100644 src/ts/server/maps/caveMap.ts create mode 100644 src/ts/server/maps/customMap.ts create mode 100644 src/ts/server/maps/houseMap.ts create mode 100644 src/ts/server/maps/islandMap.ts create mode 100644 src/ts/server/maps/mainMap.ts create mode 100644 src/ts/server/maps/paletteMap.ts create mode 100644 src/ts/server/move.ts create mode 100644 src/ts/server/oauth.ts create mode 100644 src/ts/server/originUtils.ts create mode 100644 src/ts/server/paths.ts create mode 100644 src/ts/server/patreon.ts create mode 100644 src/ts/server/playerUtils.ts create mode 100644 src/ts/server/polling.ts create mode 100644 src/ts/server/pool.ts create mode 100644 src/ts/server/regionUtils.ts create mode 100644 src/ts/server/reporter.ts create mode 100644 src/ts/server/reporting.ts create mode 100644 src/ts/server/requestUtils.ts create mode 100644 src/ts/server/routes/api-account.ts create mode 100644 src/ts/server/routes/api-game.ts create mode 100644 src/ts/server/routes/api-pony.ts create mode 100644 src/ts/server/routes/api-tools.ts create mode 100644 src/ts/server/routes/api.ts create mode 100644 src/ts/server/routes/api1.ts create mode 100644 src/ts/server/routes/api2.ts create mode 100644 src/ts/server/routes/auth.ts create mode 100644 src/ts/server/routes/index.ts create mode 100644 src/ts/server/server.ts create mode 100644 src/ts/server/serverActions.ts create mode 100644 src/ts/server/serverActionsManager.ts create mode 100644 src/ts/server/serverInterfaces.ts create mode 100644 src/ts/server/serverMap.ts create mode 100644 src/ts/server/serverRegion.ts create mode 100644 src/ts/server/serverUtils.ts create mode 100644 src/ts/server/services/actionLimiter.ts create mode 100644 src/ts/server/services/adminService.ts create mode 100644 src/ts/server/services/counter.ts create mode 100644 src/ts/server/services/friends.ts create mode 100644 src/ts/server/services/hiding.ts create mode 100644 src/ts/server/services/liveList.ts create mode 100644 src/ts/server/services/notification.ts create mode 100644 src/ts/server/services/observableList.ts create mode 100644 src/ts/server/services/party.ts create mode 100644 src/ts/server/services/supporterInvites.ts create mode 100644 src/ts/server/settings.ts create mode 100644 src/ts/server/spamChecker.ts create mode 100644 src/ts/server/start.ts create mode 100644 src/ts/server/stats.ts create mode 100644 src/ts/server/timing.ts create mode 100644 src/ts/server/userError.ts create mode 100644 src/ts/server/utils/byteSize.ts create mode 100644 src/ts/server/utils/socketErrorHandler.ts create mode 100644 src/ts/server/utils/taskQueue.ts create mode 100644 src/ts/server/world.ts create mode 100644 src/ts/tests/client/canvasUtils.spec.ts create mode 100644 src/ts/tests/client/clientActions.spec.ts create mode 100644 src/ts/tests/client/clientUtils.spec.ts create mode 100644 src/ts/tests/client/emoji.spec.ts create mode 100644 src/ts/tests/client/gameLoop.spec.ts create mode 100644 src/ts/tests/client/htmlUtils.spec.ts create mode 100644 src/ts/tests/client/paletteManager.spec.ts create mode 100644 src/ts/tests/client/partyUtils.spec.ts create mode 100644 src/ts/tests/client/ponyDraw.spec.ts create mode 100644 src/ts/tests/client/ponyStates.spec.ts create mode 100644 src/ts/tests/client/spriteFont.spec.ts create mode 100644 src/ts/tests/client/webglUtils.spec.ts create mode 100644 src/ts/tests/client/worldMap.spec.ts create mode 100644 src/ts/tests/common/accountUtils.spec.ts create mode 100644 src/ts/tests/common/adminUtils.spec.ts create mode 100644 src/ts/tests/common/animator.spec.ts create mode 100644 src/ts/tests/common/bitUtils.spec.ts create mode 100644 src/ts/tests/common/camera.spec.ts create mode 100644 src/ts/tests/common/collision.spec.ts create mode 100644 src/ts/tests/common/color.spec.ts create mode 100644 src/ts/tests/common/colors.spec.ts create mode 100644 src/ts/tests/common/compress.spec.ts create mode 100644 src/ts/tests/common/compressPony.spec.ts create mode 100644 src/ts/tests/common/encoders/updateEncoder.spec.ts create mode 100644 src/ts/tests/common/entityUtils.spec.ts create mode 100644 src/ts/tests/common/expressionUtils.spec.ts create mode 100644 src/ts/tests/common/mat4.spec.ts create mode 100644 src/ts/tests/common/movementUtils.spec.ts create mode 100644 src/ts/tests/common/ponyHelpers.spec.ts create mode 100644 src/ts/tests/common/ponyInfo.spec.ts create mode 100644 src/ts/tests/common/rect.spec.ts create mode 100644 src/ts/tests/common/timeUtils.spec.ts create mode 100644 src/ts/tests/common/utils.spec.ts create mode 100644 src/ts/tests/generated/sprites.spec.ts create mode 100644 src/ts/tests/graphics/graphicsUtils.spec.ts create mode 100644 src/ts/tests/graphics/spriteSheetUtils.spec.ts create mode 100644 src/ts/tests/lib.ts create mode 100644 src/ts/tests/mocks.ts create mode 100644 src/ts/tests/server/accountUtils.spec.ts create mode 100644 src/ts/tests/server/api/account.spec.ts create mode 100644 src/ts/tests/server/api/game.spec.ts create mode 100644 src/ts/tests/server/api/internal-common.spec.ts create mode 100644 src/ts/tests/server/api/internal.spec.ts create mode 100644 src/ts/tests/server/api/pony.spec.ts create mode 100644 src/ts/tests/server/authUtils.spec.ts create mode 100644 src/ts/tests/server/characterUtils.spec.ts create mode 100644 src/ts/tests/server/chat.spec.ts create mode 100644 src/ts/tests/server/cmUtils.spec.ts create mode 100644 src/ts/tests/server/commands.spec.ts create mode 100644 src/ts/tests/server/entityUtils.spec.ts create mode 100644 src/ts/tests/server/liveEndPoint.spec.ts create mode 100644 src/ts/tests/server/mapUtils.spec.ts create mode 100644 src/ts/tests/server/move.spec.ts create mode 100644 src/ts/tests/server/originUtils.spec.ts create mode 100644 src/ts/tests/server/other.spec.ts create mode 100644 src/ts/tests/server/patreon.spec.ts create mode 100644 src/ts/tests/server/playerUtils.spec.ts create mode 100644 src/ts/tests/server/regionUtils.spec.ts create mode 100644 src/ts/tests/server/reporting.spec.ts create mode 100644 src/ts/tests/server/serverActions.spec.ts create mode 100644 src/ts/tests/server/serverMap.spec.ts create mode 100644 src/ts/tests/server/serverRegion.spec.ts create mode 100644 src/ts/tests/server/serverUtils.spec.ts create mode 100644 src/ts/tests/server/services/friends.spec.ts create mode 100644 src/ts/tests/server/services/hiding.spec.ts create mode 100644 src/ts/tests/server/services/notification.spec.ts create mode 100644 src/ts/tests/server/services/party.spec.ts create mode 100644 src/ts/tests/server/services/supporterInvites.spec.ts create mode 100644 src/ts/tests/server/spamChecker.spec.ts create mode 100644 src/ts/tests/server/utils/byteSize.spec.ts create mode 100644 src/ts/tests/server/utils/taskQueue.spec.ts create mode 100644 src/ts/tests/server/world.spec.ts create mode 100644 src/ts/tests/services/errorReporter.spec.ts create mode 100644 src/ts/tests/services/gameService.spec.ts create mode 100644 src/ts/tests/services/liveCollection.spec.ts create mode 100644 src/ts/tests/services/liveList.spec.ts create mode 100644 src/ts/tests/services/model.spec.ts create mode 100644 src/ts/tests/services/settingsService.spec.ts create mode 100644 src/ts/tools/canvas-utils.ts create mode 100644 src/ts/tools/common.ts create mode 100644 src/ts/tools/convert-tiles.ts create mode 100644 src/ts/tools/create-font.ts create mode 100644 src/ts/tools/create-sprites.ts create mode 100644 src/ts/tools/name-tester.ts create mode 100644 src/ts/tools/palette-utils.ts create mode 100644 src/ts/tools/psd-utils.ts create mode 100644 src/ts/tools/sprite-sheet.ts create mode 100644 src/ts/tools/sprites-template.ts create mode 100644 src/ts/tools/trigger.txt create mode 100644 src/ts/tools/types.ts create mode 100644 src/typings/my.d.ts create mode 100644 src/typings/my/canvas.d.ts create mode 100644 src/typings/my/fix.d.ts create mode 100644 src/typings/my/node-async.d.ts create mode 100644 src/typings/my/patreon.d.ts create mode 100644 tsconfig-aot-es.json create mode 100644 tsconfig-aot.json create mode 100644 tsconfig.json create mode 100644 tslint.json create mode 100644 views/index.pug create mode 100644 webpack.common.js create mode 100644 webpack.dev.js create mode 100644 webpack.prod.js create mode 100644 workbox-config.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5068683 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.ico filter=lfs diff=lfs merge=lfs -text diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9580eea --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,520 @@ +#### v0.53.0 +- Added custom servers (about page) +- Added export option for ponies (character page) +- Added editing house floor tiles +- Added more placeable object for the house +- Added way to cycle backwards through item list by using Shift + E while holding the hammer tool +- Added /lockhouse and /unlockhouse commands for preventing other people from modifying the house +- Added /removetoolbox and /restoretoolbox commands for removing and restoring toolbox object from house map +- Added switching placeable items and tiles by using mouse wheel +- Added switching tools using Shift + scroll wheel when you're inside the house +- Unlocked other objects in the house for removing +- Unlocked walls in front of windows from being placed +- Increased house object limit +- Fixed issues with house map saving using /savehouse command +- Fixed being able to plain object in house behind wall + +#### v0.52.0 +- Added cave/mines map +- Added 2 new cave/mine sound tracks +- Added new cave/mine objects and decorations +- Added bunnies around the map +- Added direction signs +- Added pickable ropes in mines +- Added building and removing walls in party house using saw tool +- Added removing furniture in party house using broom tool +- Added placing furniture in party house using hammer tool +- Added "Switch tool" and "Switch item to place" actions +- Added carrot field in top right corner of the map +- Added more fruits to fruit box +- Added sort tags to character descriptions (the same names are sorted by description) +- Added character description help tooltip +- Added some new emotes +- Improved sorting of character list +- Improved tags dropdown + +#### v0.51.2 +- Added character descriptions and tags +- Added song switching when changing maps +- Fixed performance issues with login server +- Fixed issues with rendering avatars on friend list +- Fixed issues with displaying load error messages +- Improved character searching on character list +- Improved map compression + +#### v0.51.1 +- Fixed multiple issues with party maps +- Fixed being able to permanently hide your own character + +#### v0.51.0 +- Added party house on party island +- Added 2 new house sound tracks +- Added new objects for party house +- Added magic action and /magic command (usable by unicorns) +- Added "darken locked outlines" option to character editor +- Added permanent hides +- Added list of permanent hides to account settings +- Changed ignore party invites to not apply to friends +- Updated look of cliffs +- Fixed socks resetting if only back leg socks are set +- Fixed issues with invisible or duplicate player character +- Fixed issues with blurring of action buttons when action bar is scrolled +- Fixed issues with collision on boat bridge +- Fixed some cyrillic characters not working for expressions correctly +- Fixed rendering issues in character editor +- Fixed issues with water animation when holding lantern +- Fixed some rock animations overlapping with terrain +- Fixed apples spawning on water + +#### v0.50.0 +- Combined portuguese and spanish safe servers into one +- Added new 18+ russian server +- Added swimming +- Added new sock patterns +- Added setting separate accessory for each leg +- Added switch for disabling outline darkening +- Added customizable eyelash color +- Added character count to character selection list +- Added pickable flower on party island +- Added rocks in water +- Added benches +- Added smaller island bit +- Added box of fruits on party island +- Added rare pickable pear and banana +- Increased party size to 30 +- Increased friend limit to 100 +- Fixed not updating last used pony when using swap pony option +- Fixed game update issues after server restart + +#### v0.49.0 +- Added new 18+ non-ERP server +- Added new Safe Spanish server +- Added new Safe Portuguese server +- Added character swapping +- Added online/offline status switch to account dropdown +- Added scrolling of action bar on PC using mouse wheel +- Added setting separate eye white color for each eye +- Increased base character limit to 200 and supporter character limits to +200/+300/+450 +- Fixed issues with game joining when spawning at the edge of the map +- Fixed gamepad X button causing pony to sit down +- Fixed player hiding getting reset on server restart +- Fixed connection issues in some cases +- Improved game performance + +#### v0.48.2 +- Added flying over cliffs, fences and some other objects +- Increased hide limit to 1000 +- Fixed issues with collision detection +- Fixed issues with switching maps +- Fixed issues with grass on party island map + +#### v0.48.1 +- Added toy stash and item stash to party island +- Increased party size to 20 +- Fixed issues with server restart +- Fixed issues with issues with party island map in some cases + +#### v0.48.0 +- Added harbor +- Added party island map +- Added party island music tracks +- Added bright night settings option, that makes game brighter during in-game night +- Added past supporter status +- Added extra pony slots for past supporters +- Fixed issues with collision inconsistencies +- Fixed friend tag and name color showing even when switched to "show as offline" +- Fixed commands not working in whispers +- Fixed camera issue after joining to the game +- Fixed - and = shortcuts not working on Firefox +- Fixed issues when selecting player from chatlog +- Fixed shortcut for hiding public chat +- Improved displaying party and whisper commands in chatlog +- Improved collision handling +- Improved game performance +- Increased friend limit +- Updated swear filters + +#### v0.47.0 +- Added friend list +- Added whispers (private messages) +- Added /w <name> and /whisper <name> command +- Added /r and /reply command +- Added "Ignore friend requests" settings option +- Added "Only allow whispers from friends" settings option +- Fixed issues with duplicate characters in certain cases +- Fixed issues with game not rendering after switching apps or tabs in some cases +- Fixed game loading performance issues +- Fixed account name not saving properly when entering name of maximum allowed length +- Improved performance +- Improved networking +- Improved server performance +- Updated swear filters + +#### v0.46.6 +- Reverted performance improvement leading to issues with sprite rendering on some browsers + +#### v0.46.5 +- Moved rose sign +- Fixed some commands not working properly when using uppercase letters +- Fixed issues with map regions not loading correctly in certain cases +- Fixed issues with map not loading in certain cases +- Fixed server-side performance issues +- Fixed issues with AFK check +- Fixed issues with colors missing on held items in certain cases +- Updated swear filters + +#### v0.46.4 +- Fixed issues with incorrect color for chat type indicator in certain cases +- Fixed issues with joining after disconnect resulting in missing or duplicated characters +- Fixed issues with error reporting +- Improved some settings labels +- Improved server performance + +#### v0.46.3 +- Changed longer hide to 10 days instead of 7 +- Increased hide limit to 25 +- Updated swear filters +- Fixed critical server-side issues + +#### v0.46.2 +- Fixed items spawning on cliffs +- Fixed issues with date of birth month list +- Updated swear filters + +#### v0.46.1 +- Fixed issues with collisions +- Fixed issues with disappearing player character in certain cases +- Fixed items spawning on cliffs +- Fixed some misplaced objects +- Fixed some issues with object sorting + +#### v0.46.0 +- Increased map size +- Added new locations to the map +- Added new plants +- Increased party size limit to 10 players +- Increased hide limit to 20 +- Fixed multiple issues with party UI +- Fixed issues with music playback +- Fixed issues with disappearing player character in certain cases +- Fixed missing range limitation for changing tiles +- Improved server performance +- Improved rendering reliability and performance +- Updated swear filters +- Updated server infrastructure + +#### v0.45.3 +- Fixed issues with saving account settings in some cases +- Fixed server performance issues +- Fixed some styling issues +- Updated swear filters +- Improved graphics performance +- Improved server logging and performance analysis tools + +#### v0.45.2 +- Added 25 more pony slots for all players +- Removed collectable eggs +- Fixed graphics memory leak issue +- Fixed graphics initialization issues in certain cases +- Fixed performance issues when joining to the game +- Improved networking performance + +#### v0.45.1 +- Added egg donation barrel +- Fixed issues when saving settings in certain cases +- Fixed issues with action bar on desktop PCs with touchscreens +- Fixed disconnect issues in some cases +- Fixed broken link in about page + +#### v0.45.0 +- Added collectable eggs +- Added egg emoji +- Added /eggs command +- Added eggs command action +- Added keyboard shortcut F3 for toggling public chat +- Fixed custom filters not working for player names +- Fixed styling issues on Yandex browser +- Fixed tongue not visible on expression action buttons +- Reduced memory usage +- Improved graphics resource allocation management +- Improved memory usage for graphics resources +- Improved chatlog performance + +#### v0.44.2 +- Fixed actions modal styling issues on Internet explorer +- Fixed error when running out of memory in some cases +- Fixed not being able to join server in some cases + +#### v0.44.1 +- Changed command actions to use last used chat type (party/public) +- Fixed actions modal scrolling by itself on mobile +- Fixed some closed eyes not counted correctly as closed for sleeping emotes + +#### v0.44.0 +- Added action bar +- Added action bar customization via "Actions" option in in-game settings dropdown +- Fixed issues with showing incorrect party state after disconnect in certain cases + +#### v0.43.2 +- Added filters section to in-game settings +- Added custom chat filter to in-game settings +- Improved rendering performance +- Fixed chatlog range indicator showing incorrectly in some cases +- Fixed incorrect errors when failed to initialize graphics device +- Fixed connection issues when cancelling joining to the game +- Updated swear filters + +#### v0.43.1 +- Added ignore public chat option to in-game settings +- Added button shortcut for opening chat on controller +- Added date of birth field to account settings +- Changed eyes when using uppercase Q letters in expressions QcQ +- Changed character name validation +- Improved camera shifting when opening mobile keyboard +- Improved some letters in pixel font +- Fixed issues when mobile keyboard is opened +- Fixed incorrect error messages for party invites in some cases +- Updated swear filters + +#### v0.43.0 +- Added new manes +- Added chatlog range setting +- Added fps counter (option in settings) +- Changed "see through objects" and "chatlog opacity" settings to be saved on server +- Changed night to be lighter on low graphics settings to compensate for missing lights +- Improved party invite error messages +- Updated map with new flower and clover patches +- Updated swear filters +- Removed some expression combinations from PG rated servers +- Fixed command messages in party chat being colored white instead of yellow +- Fixed issues with part of game screen being outside the screen bounds on mobile devices in some cases +- Fixed issues with pixel scaling on low graphics mode +- Fixed issues with game screen turning green or black +- Fixed issues with game loading +- Fixed issues with music playing before the game loads +- Fixed issue with reloading game in some cases +- Fixed styling issues + +#### v0.42.4 +- Changed season to spring +- Updated swear filters +- Fixed some issues with account alerts + +#### v0.42.3 +- Added :thinking: emoji +- Added account alerts for notifying players of account related issues +- Removed some expression combinations from PG rated servers +- Updated swear filters +- Fixed graphical glitch with some expressions when using beak muzzle option +- Fixed sign-in error showing off-screen on some devices +- Fixed some disconnect issues +- Improved performance + +#### v0.42.2 +- Fixed issue with game breaking in some cases when the connection is interrupted + +#### v0.42.1 +- Fixed issues with color pickers on some mobile devices + +#### v0.42.0 +- Added rose sign +- Added /love, /<3 command +- Updated common issues help section +- Updated in-game settings window +- Fixed some issues with game screen positioning on mobile devices +- Fixed some issues with chatlog scrolling +- Fixed issues with switching chat mode + +#### v0.41.0 +- Added new facial hair styles and patterns +- Added cookie stand +- Fixed issue with chatlog tab switching on Firefox +- Fixed issue with bats getting stuck when returning to trees +- Fixed issue with characters with no wings appear to hover above ground in certain cases +- Fixed issue with delay when joining to the game +- Fixed errors when using remote buttons while playing the game +- Fixed some off-screen messages appearing in chatlog +- Fixed some styling issues + +#### v0.40.1 +- Remove loading performance optimization due to Yandex browser bug + +#### v0.40.0 +- Added chatlog +- Added chatlog settings +- Added settings for walking by default +- Fixed graphical glitch when booping while sitting +- Fixed issues with critter movement +- Fixed being able to be hidden by yourself in certain situations +- Fixed incorrect error message displayed in certain situations +- Improved game loading performance + +#### v0.39.4 +- Added number in queue indicator when joining to the game takes longer than a second +- Fixed issue with game not loading when graphics initialization takes longer than connecting to the server +- Fixed issues with gamepad dpad moving the character +- Fixed issues with iPad keyboards where each key would only work once, the keys will still get stuck occasionally due to iOS bug, we recommend to use arrow keys for movement instead of WASD keys. + +#### v0.39.3 +- Fixed issues with sending long messages on Edge and Internet Explorer browsers + +#### v0.39.2 +- Removed Xmas decorations +- Changed support contact email +- Fixed some issues with disconnects +- Fixed party chat messages visible when party member is on another map +- Improved networking performance +- Improved server performance + +#### v0.39.1 +- Added gift donation spot where you can donate gifts and reduce your gift count +- Added additional restart buttons to avoid getting stuck in game updating state +- Added remembering of /s1, /s2 and /s3 chat mode for next message +- Added more logging for analyzing sign-in issues +- Fixed issues with mobile keyboard when the game is installed to home screen +- Fixed issues with game updating +- Fixed issues with missing files during game update process +- Fixed focusing pony name input field on mobile devices causing mobile keyboard to open +- Fixed being able to see lantern lights of lanterns held by hidden players in some cases + +#### v0.39.0 +- Added xmas decorations +- Added gifts with collectable toys (use E or /open command or gift button on mobile to open the gift) +- Added /droptoy command for getting rid of toy on your head +- Added /toys for listing amount of collected toys +- Added candy/cookie stands +- Added picking up apples and oranges +- Added set of most common Chinese characters +- Added Japanese Kanji characters +- Added cookie emoji +- Changed plushie sign to toy stash (keeps all your collected toys) +- Changed unicode symbol assigned for some emoji characters +- Fixed some issues with updating the game +- Fixed some issues with height difference when standing on ice +- Improved rendering performance + +#### v0.38.0 +- Added winter environment details +- Changed season to winter +- Fixed some performance issues + +#### v0.37.3 +- Fixed issues with character selection list on mobile devices +- Fixed issues with keyboard on mobile devices + +#### v0.37.2 +- Fixed issues with character selection +- Fixed issues when using mouse with more than 3 buttons + +#### v0.37.1 +- Added /leave command +- Removed Halloween decorations +- Fixed some duplicate objects on the map +- Fixed issues with cloudflare verification window not working properly +- Fixed issues with some sprites +- Fixed issues with opening mobile keyboard on some devices +- Fixed issues with drawing lights in certain cases +- Improved networking performance +- Improved server performance + +#### v0.37.0 +- Added 2 new music tracks +- Added new head, face, neck, chest and back accessories +- Added new sleeve options for chest accessories +- Added new manes +- Added new tails +- Added new face markings +- Added new horn and horn patterns +- Added new eyelashes +- Added new ears +- Added new hooves +- Added ear patterns +- Added pants back accessory +- Added Halloween decorations +- Added spooks +- Changed google sign-in icon +- Updated team and contributors lists +- Updated head accessory positions for each mane +- Fixed issues with install button +- Fixed issues with mobile keyboard in app mode +- Fixed issues with map loading in certain cases +- Fixed some sprite layering issues +- Fixed some site links not working in certain cases +- Fixed cutting off some pixels when exporting fly animation as PNG +- Fixed color in butt mark editor changing when switching tabs +- Improved rendering performance + +#### v0.36.1 +- Updated facebook sign-in +- Fixed some lighting issues +- Improved lighting performance + +#### v0.36.0 +- Changed season to autumn +- Increased map size +- Added new locations and road system +- Added some rule explanations to Common issues section on Help page +- Improved lighting engine +- Fixed issues with character not shown as flying in some cases +- Fixed issues with updating Patreon data +- Fixed issues with game update process +- Fixed issues with loosing item counters when merging accounts in certain situations +- Fixed issues with getting kicked out of the party in certain situations +- Fixed incorrect symbol used for :trans: emoji + +#### v0.35.7 +- Added warning message for some outdated browsers +- Improved networking performance +- Fixed "Um." and "Um-" text causing expression to change +- Fixed some issues with character selection +- Fixed some issues with name display +- Fixed issues with Cloudflare reloading + +#### v0.35.6 +- Improved server performance +- Improved network usage + +#### v0.35.5 +- Added option to install the game on android devices +- Improved startup performance +- Improved server performance +- Fixed party list covering chat buttons in some cases on small screens +- Fixed issues with experimental progressive map loading + +#### v0.35.4 +- Added experimental progressive map loading +- Added power saving option to settings +- Improved performance of map loading +- Reworked internal server communication +- Fixed layering issues with some accessories +- Fixed incorrect hitbox for flying ponies +- Fixed issues with drawing ponies at the edge of the screen +- Fixed some unhelpful error messages +- Fixed some words interpreted as expressions +- Fixed some issues with movement on slow networks +- Fixed some issues with boop animation when lying down +- Fixed some issues with error reporting + +#### v0.35.3 +- Added b.d / 6.6 expression +- Added fly animation to character editor +- Improved visibility checking +- Improved networking performance +- Fixed issue with disappearing party member portraits +- Fixed server-side performance issues +- Fixed update notification not showing when joined during an update +- Fixed server shutdown issues +- Fixed incorrect hitbox for flying characters +- Fixed server-side timing issues + +#### v0.35.2 +- Fixed performance issues when a lot of users are joining the server at the same time + +#### v0.35.1 +- Fixed performance issues with ignore system and hiding system +- Fixed performance issues when a lot of users are joining the server at the same time + +#### v0.35.0 +- Added flying - C key or
button or /fly command +- Added booping while flying +- Added flying above water diff --git a/LICENSE_ART.txt b/LICENSE_ART.txt new file mode 100644 index 0000000..2a7873d --- /dev/null +++ b/LICENSE_ART.txt @@ -0,0 +1,364 @@ +creator(s): + Shino (https://www.deviantart.com/shinodage) + ChiraChan (https://www.deviantart.com/chiramii-chan, https://chirachan-art.tumblr.com/) + Velenor (https://www.deviantart.com/velenor) + ShareMyShipment (https://www.deviantart.com/sharemyshipment) + Paulpeoples (https://www.deviantart.com/paulpeopless) + Meno (https://www.deviantart.com/menojar) + Goodly (https://www.deviantart.com/goodlyay) + TioRafaJP (https://www.deviantart.com/tiorafajp, https://www.youtube.com/user/RafaelJP2) + CyberPon3 (https://www.deviantart.com/cyberpon3) + OtakuAP (https://www.deviantart.com/otakuap) + Disastral + Velvet-Frost (https://www.deviantart.com/velvet-frost) + Jet7Wave (https://www.deviantart.com/jetwave) + Lalieri (https://lalieri.tumblr.com/) + Ruef-bae (https://www.deviantart.com/ruef-bae) + Alchemist3rd + Firecracker + ZippySqrl (https://www.deviantart.com/zippysqrl) + Karnel333 + Wellfugzee + ScribblesHeart (https://www.deviantart.com/scribblesdesu) + dsp2003 (https://dsp2003.tumblr.com/, http://www.deviantart.com/dsp2003) + MysticBlare (https://twitter.com/MysticBlare) + Towmacow Waffles (https://www.deviantart.com/towmacowwaffles) + OrchidPony (https://www.deviantart.com/orchidpony) + Cherry Cerise (https://www.deviantart.com/cherryceriseart) + Radio + Ultimate Fluff + SC (https://0somecunt0.tumblr.com/tagged/sfw) + SailorDolpin (https://vk.com/id324582699) + Deeraw (https://www.deviantart.com/deerdraw, https://twitter.com/TheOnlyDeeraw) + + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. diff --git a/LICENSE_CODE.txt b/LICENSE_CODE.txt new file mode 100644 index 0000000..b3dbff0 --- /dev/null +++ b/LICENSE_CODE.txt @@ -0,0 +1,22 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE_MUSIC.txt b/LICENSE_MUSIC.txt new file mode 100644 index 0000000..9a8a25b --- /dev/null +++ b/LICENSE_MUSIC.txt @@ -0,0 +1,334 @@ +creator(s): + Wandering Artist (https://wanderingartist.bandcamp.com/, https://www.youtube.com/user/WanderingArtistMusic) + + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ce6ccd --- /dev/null +++ b/README.md @@ -0,0 +1,240 @@ +# Pony Town + +A game of ponies building a town + +## Prerequisites + +* [Node.js](https://nodejs.org/download/release/v9.11.2/) (version 9) +* [gulp](http://gulpjs.com/) `npm install -g gulp` +* MongoDB: [download link](https://www.mongodb.com/download-center/community) and [installation instructions](https://docs.mongodb.com/manual/administration/install-community/) +* [ImageMagick](https://imagemagick.org/script/download.php#windows) (optional, required for generating preview gifs in animation tool) + +## Installation + +```bash +npm install +``` + +## Setting up Database + +- Install MongoDB +- Start `mongo` from command line (you may need to go to `C:\Program Files\MongoDB\Server\4.0\bin` path on windows to run the command) +- Type `use your_database_name` to create database +- Type `db.new_collection.insert({ some_key: "some_value" })` to initialize database +- Type + ```javascript + db.createUser( + { + user: "your_username", + pwd: "your_password", + roles: [ { role: "readWrite", db: "your_database_name" } ] + } + ) + ``` + to create database user. +- Type `quit()` to exit mongo + +## Setting up OAuth keys + +Get OAuth keys for authentication platform of your choice (github, google, twitter, facebook, vkontakte, patreon) + +### Github + +- Go to https://github.com/settings/developers create new OAuth app. +- Set authorization callback URL to `http:///auth/github/callback` or `http://localhost:8090/auth/github/callback` for localhost server. +- Add this to `oauth` field in your `config.json` + +```json +"github": { + "clientID": "", + "clientSecret": "" +} +``` + +### Twitter + +- Go to https://developer.twitter.com/en/apps create new app. +- Set callback URL to `http:///auth/twitter/callback` or `http://localhost:8090/auth/twitter/callback` for localhost server. +- Add this to `oauth` field in your `config.json` + +```json +"twitter": { + "consumerKey": "", + "consumerSecret": "" +} +``` + +### Google + +- Go to https://console.developers.google.com/apis/dashboard create new project from dropdown at the top, go to credentials and create new entry. +- Add to Authorized JavaScript origins `http://` or `http://localhost:8090/` for localhost server. +- Add to Authorized redirect URIs `http:///auth/google/callback` or `http://localhost:8090/auth/google/callback` for localhost server. +- Add this to `oauth` field in your `config.json` + +```json +"google": { + "clientID": "", + "clientSecret": "" +} +``` + +### Facebook + +- Go to https://developers.facebook.com/apps/ add a new app. +- Add "Facebook Login" product to your app +- Enable "Web OAuth Login" +- Add `https:///auth/facebook/callback` to Valid OAuth Redirect URIs +- Add this to `oauth` field in your `config.json` (You can find App ID and App Secret in Settings > Basic section) + +```json +"facebook": { + "clientID": "", + "clientSecret": "", + "graphApiVersion": "v3.1" +} +``` + +### VKontakte + +- Go to https://vk.com/apps?act=manage and create new app +- Set Authorized redirect URI to `http:///auth/vkontakte/callback` or `http://localhost:8090/auth/vkontakte/callback` for localhost server. +- Add this to `oauth` field in your `config.json` + +```json +"vkontakte": { + "clientID": "", + "clientSecret": "" +}, +``` + +### Other + +If you want to add other sign-in methods you need to find appropriate [passport](http://www.passportjs.org/) package and add it in `src/ts/server/oauth.ts` and add correct entry in `config.json`. + +## Configuration + +Add `config.json` file in root directory with following content. You can use `config-template.json` as a starting point for your own config. (do not include comments in your `config.json` file) + +```javascript +{ + "title": "Pony Town", + "twitterLink": "https://twitter.com/", // optional + "contactEmail": "", + "port": 8090, + "host": "http://localhost:8090/", + "secret": "", + "token": "", + "db": "mongodb://:@localhost:27017/", // use values you used when setting up database + "analytics": { // optional google analytics + "trackingID": "" + }, + "facebookAppId": "", // optional facebook app link + "assetsPath": "", // optional, for asset generation + "oauth": { + "google": { + "clientID": "", + "clientSecret": "" + } + // other oauth entries here + }, + "servers": [ + { + "id": "dev", + "port": 8090, + "path": "/s00/ws", + "local": "localhost:8090", + "name": "Dev server", + "desc": "Development server", + "flag": "test", // optional flag ("test", "star" or space separated list of country flags) + "flags": { // optional feature flags + "test": true, // test server + "objects": true, // in-game editor + }, + "alert": "18+", // optional 18+ alert (also blocks underage players) + }, + ] +} +``` + +## Running + +Production environment + +```bash +npm run build +npm start +``` + +Adding/removing roles + +```bash +node cli.js --addrole # roles: superadmin, admin, mod, dev +node cli.js --removerole +``` + +To setup superadmin role use following command + +```bash +node cli.js --addrole superadmin +``` + +Admin panel is accessible at `/admin/` (requires admin or superadmin role to access) +Tools are accessible at `/tools/` (only available in dev mode or when started with --tools flag) + +Starting as multiple processes + +```bash +node pony-town.js --login # login server +node pony-town.js --game main # game server 1 ('main' has to match id from config.json) +node pony-town.js --game safe # game server 2 ('safe' has to match id from config.json) +node pony-town.js --admin --standaloneadmin # admin server +``` + +For these to work on the same URL, paths to game servers and admin server need to be bound to correct ports, using http proxy. + +It is recommended to run processes with larger memory pool for large user bases (especially admin and game processes), example: + +```bash +node --max_old_space_size=8192 pony-town.js --game main +``` + +Beta environment (with dev tools and in-development features) + +```bash +npm run build-beta +node pony-town.js --login --admin --game --tools --beta +``` + +Running in development + +```bash +npm run ts-watch # terminal 1 +npm run wds # terminal 2 +gulp dev # terminal 3 +gulp test # terminal 4 (optional) +``` + +```bash +gulp dev --sprites # run with generation of sprite sheets (use src/ts/tools/trigger.txt to trigger sprite generation without restarting gulp) +gulp dev --test # run with tests +gulp dev --coverage # run with tests and code coverage +``` + +## Customization + +- `package.json` - settings for title and description of the website +- `assets/images` - logos and team avatars +- `public/images` - additional logos +- `public` - privacy policy and terms of service +- `favicons` - icons +- `src/ts/common/constants.ts` - global settings +- `src/ts/server/maps/*` - maps configuration and setup +- `src/ts/server/start.ts` - world setup +- `src/ts/components/services/audio.ts` - adding/removing sound tracks +- `src/ts/client/credits` - credits and contributors +- `src/style/partials/_variables.scss` - page style configuration + +### Custom map introduction + +- `src/ts/server/start.ts:35` - adding custom map to the world +- `src/ts/server/map/customMap.ts` - commented introduction to customizing maps diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_1.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_1.gif new file mode 100644 index 0000000..690a454 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_1.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8f7eabc734440d4cf2371cb17be0908835cc4aacca221976dce209ba15870a6 +size 10535 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_2.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_2.gif new file mode 100644 index 0000000..5464a2e --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_2.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d73fba4319f0807030cb0d188151b1ac17d140dc1f909124b0a6f8f3f7903290 +size 9050 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_3.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_3.gif new file mode 100644 index 0000000..c0a190b --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_3.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3786003c5b364dc1ada61d0e9b65f3621ced25ade5ddfc4ff70fcfbd3ba4340f +size 10339 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_4.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_4.gif new file mode 100644 index 0000000..662f7d1 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_4.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58ea5fb9e48ae3e6872c4bd587300e18e3e0ab53cf2b805d49982c4127b32626 +size 8671 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5.gif new file mode 100644 index 0000000..fd5ada5 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b4e35b663129bbf27406f97a90ee0a17c3fcdb29fddbb5c8bac924fd733026b +size 15428 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_body .gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_body .gif new file mode 100644 index 0000000..9bd17b0 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_body .gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0660eb7789563fe2f46ad021ab45b16194d8a9f0c34e3edf6e718b02571a9124 +size 4129 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_hooves.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_hooves.gif new file mode 100644 index 0000000..5de9ab8 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_5_hooves.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9d2eedfc57bafb68a6bd71c0184c80a75a7a3da10ad2d5e97dd984f69459c69 +size 1676 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_6.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_6.gif new file mode 100644 index 0000000..532054f --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_6.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0aa229356601ba7d22c153d2d5a75213399dabd498501c3ffdeaa94886e20696 +size 13090 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_7.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_7.gif new file mode 100644 index 0000000..ae3eb9d --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_7.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e104ac45b63c8c233d4631e5815974f56fee924a3baeb4468b5e1386f6228f20 +size 8441 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_8.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_8.gif new file mode 100644 index 0000000..2cdda4c --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_8.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a0ac50d36a629cbd0391ec734c979ac66d4888f7c08d1c863aa440bbe0cbacb +size 8484 diff --git a/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_9.gif b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_9.gif new file mode 100644 index 0000000..69f471c --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/A_Ghost_Anim_9.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9dc64e513206ca44bd8b297c592802e2db4b805aca467fa1eda68f39350c358 +size 8564 diff --git a/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_1.gif b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_1.gif new file mode 100644 index 0000000..61b2919 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_1.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43fc01567642a4e339b00443cdb817d79ec3415e1460e0b46e036f76e55fd014 +size 4159 diff --git a/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_2.gif b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_2.gif new file mode 100644 index 0000000..5313c3c --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_2.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3caa8069f56cf683a4b726b79579c6b00f03ae22da3ab2af47ba05310b6f36ca +size 4051 diff --git a/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_3.gif b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_3.gif new file mode 100644 index 0000000..9c61043 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_3.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197d4b08accd51d2e37056d3c8788d8632895bfe943a9ab1b127eeaefc282d23 +size 5482 diff --git a/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_4.gif b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_4.gif new file mode 100644 index 0000000..613fadf --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_4.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24789de5a73037d69beb164ac39df51053203bc5601dbe97764be75077fd28e1 +size 3330 diff --git a/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_5.gif b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_5.gif new file mode 100644 index 0000000..cd262d3 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_5.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4b8281c133fd8f7dc071316d9dd733202d81a8fa07d50224241eecb4ce4b94b +size 2168 diff --git a/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_6.gif b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_6.gif new file mode 100644 index 0000000..e481c75 --- /dev/null +++ b/assets-source/animations/Halloween_Ghost_Anim/B_Ghost_Anim_6.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ded3e50cdc23d34b4fdb18e323e0336437317030ec4e1740123a4b2cfc3058dd +size 4211 diff --git a/assets-source/animations/bat.psd b/assets-source/animations/bat.psd new file mode 100644 index 0000000..3bbd9c3 --- /dev/null +++ b/assets-source/animations/bat.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b756c910fb67d01ca6937574b05dd2191503d32c15ddbcf0cb9462c41f925ee +size 31240 diff --git a/assets-source/animations/boat_wake.psd b/assets-source/animations/boat_wake.psd new file mode 100644 index 0000000..568529c --- /dev/null +++ b/assets-source/animations/boat_wake.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f5523db4d5c088c6139d6c1031a79f11d9156db71be32f318224de5c9ffbcdb +size 42799 diff --git a/assets-source/animations/bunny.psd b/assets-source/animations/bunny.psd new file mode 100644 index 0000000..3f6be06 --- /dev/null +++ b/assets-source/animations/bunny.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04434505e309394ad87e6b29522e3b8bcc7b2ad8d22f1e24d496ce7943a6cbf7 +size 101112 diff --git a/assets-source/animations/butterfly-4x1.png b/assets-source/animations/butterfly-4x1.png new file mode 100644 index 0000000..1627942 --- /dev/null +++ b/assets-source/animations/butterfly-4x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53a8425d805875d80b39dc390cc44912de50a8e0baeadd908066e394d829d4ba +size 2949 diff --git a/assets-source/animations/cat.psd b/assets-source/animations/cat.psd new file mode 100644 index 0000000..ebc7614 --- /dev/null +++ b/assets-source/animations/cat.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27642b8dc86617fecd6ad47e8fe1cc66509c6c06634186a2a49637884ab42d27 +size 62709 diff --git a/assets-source/animations/firefly.psd b/assets-source/animations/firefly.psd new file mode 100644 index 0000000..80988c2 --- /dev/null +++ b/assets-source/animations/firefly.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7db2bca6807aca5f9bbe70d95c70cd27ac7972f7492905a816b904ddfb16f0be +size 26820 diff --git a/assets-source/animations/ghost1.psd b/assets-source/animations/ghost1.psd new file mode 100644 index 0000000..1422ce4 --- /dev/null +++ b/assets-source/animations/ghost1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f086626133cb818a398975e6c6ae27b9c4a856ab36d59ed554edb47c5af55db4 +size 214496 diff --git a/assets-source/animations/ghost1_hooves.psd b/assets-source/animations/ghost1_hooves.psd new file mode 100644 index 0000000..52b4bd1 --- /dev/null +++ b/assets-source/animations/ghost1_hooves.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:740d3aa7933c0a7ac2b94106a733075927158bf1f726ea6801a14fbfcfa6ee7c +size 158016 diff --git a/assets-source/animations/lantern.psd b/assets-source/animations/lantern.psd new file mode 100644 index 0000000..fea20b3 --- /dev/null +++ b/assets-source/animations/lantern.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68e630285a09fb21f84ea0430c608c18e9b3931d48e1c666c61bea2278283078 +size 35784 diff --git a/assets-source/animations/magic.psd b/assets-source/animations/magic.psd new file mode 100644 index 0000000..075e44f --- /dev/null +++ b/assets-source/animations/magic.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b251f93e07504e97d33b31e5a54d74686952536d9c1ffb695e786eba8023734e +size 31502 diff --git a/assets-source/animations/magic2.psd b/assets-source/animations/magic2.psd new file mode 100644 index 0000000..18ed5c3 --- /dev/null +++ b/assets-source/animations/magic2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00bdbe3c11272488152e226cedd5a7eac8e404c3816b9a6a026e36332872e087 +size 36404 diff --git a/assets-source/animations/magic3.psd b/assets-source/animations/magic3.psd new file mode 100644 index 0000000..b847ca3 --- /dev/null +++ b/assets-source/animations/magic3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e2b1dab637aeb7d931f1cf53d0b30900206d1be3f5eccaff13b856ef226008 +size 37948 diff --git a/assets-source/animations/pier_leg.psd b/assets-source/animations/pier_leg.psd new file mode 100644 index 0000000..4492a4c --- /dev/null +++ b/assets-source/animations/pier_leg.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b6dec1fcc8cc5f5dc6ea5d46209bdeaa73aef1d65517e5a7fd6f97fa49eab78 +size 32406 diff --git a/assets-source/animations/pony_wake/1.psd b/assets-source/animations/pony_wake/1.psd new file mode 100644 index 0000000..95e6057 --- /dev/null +++ b/assets-source/animations/pony_wake/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15abaae1b0e60ebe66f22a4521bb1ddd20484ed7e672baf4c45a67847405b5cd +size 37208 diff --git a/assets-source/animations/pony_wake/2.psd b/assets-source/animations/pony_wake/2.psd new file mode 100644 index 0000000..414590b --- /dev/null +++ b/assets-source/animations/pony_wake/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a30d05ba0476f1d87831207bfe92f5d643ea1991a5bfaed3584864c43df03c2 +size 31362 diff --git a/assets-source/animations/pony_wake/3.psd b/assets-source/animations/pony_wake/3.psd new file mode 100644 index 0000000..b905172 --- /dev/null +++ b/assets-source/animations/pony_wake/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:972e4386254bdf5cd423154b412adc49d2b7fdf1b47c3e5bd59667451e51a2e1 +size 33702 diff --git a/assets-source/animations/pony_wake/4.psd b/assets-source/animations/pony_wake/4.psd new file mode 100644 index 0000000..50d451a --- /dev/null +++ b/assets-source/animations/pony_wake/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c050934d87a169406939c9970db8330034ca1699a155dd823f68b6bb451488ca +size 30846 diff --git a/assets-source/animations/pony_wake/5.psd b/assets-source/animations/pony_wake/5.psd new file mode 100644 index 0000000..883b25c --- /dev/null +++ b/assets-source/animations/pony_wake/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dee94b5c34e219a42a06c70de260710feb00334a8d3921582d5947ac3816721f +size 33751 diff --git a/assets-source/animations/pony_wake/6.psd b/assets-source/animations/pony_wake/6.psd new file mode 100644 index 0000000..cc02694 --- /dev/null +++ b/assets-source/animations/pony_wake/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a264d4e55d93d3c725e8ed2fd44286ee7f00e9c5f3a1cb6320c60d5ab95e153 +size 31146 diff --git a/assets-source/animations/poof.psd b/assets-source/animations/poof.psd new file mode 100644 index 0000000..3598709 --- /dev/null +++ b/assets-source/animations/poof.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8d02d8c3a1ce66ee52b2d805320394ea491b184e42614fcf66c203a73493d01 +size 31188 diff --git a/assets-source/animations/poof2-9x1.png b/assets-source/animations/poof2-9x1.png new file mode 100644 index 0000000..52010e4 --- /dev/null +++ b/assets-source/animations/poof2-9x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:732fb444c77d9d42c7ff51e6034f271bb35d3c8e96a5735fe951a9cd767210b9 +size 3014 diff --git a/assets-source/animations/rain-4x1.png b/assets-source/animations/rain-4x1.png new file mode 100644 index 0000000..dc38621 --- /dev/null +++ b/assets-source/animations/rain-4x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bde5a79d1d4dc3eb1703dd5ec3029ebda9dc01734c624d22ffa947a042dd1b84 +size 4002 diff --git a/assets-source/animations/raindrop-4x1.png b/assets-source/animations/raindrop-4x1.png new file mode 100644 index 0000000..10e0124 --- /dev/null +++ b/assets-source/animations/raindrop-4x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ad24dcefaaf6422a75cc9e615cc74b63f90c848d728307369c0feab0f2a8844 +size 2853 diff --git a/assets-source/animations/splash.psd b/assets-source/animations/splash.psd new file mode 100644 index 0000000..58b139e --- /dev/null +++ b/assets-source/animations/splash.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45469f0eb18bac691f3a5aaad7d112aefa0c555bcf90950d401be439563038e2 +size 47350 diff --git a/assets-source/animations/splash_boop.psd b/assets-source/animations/splash_boop.psd new file mode 100644 index 0000000..324f811 --- /dev/null +++ b/assets-source/animations/splash_boop.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b51d786f4076026ee383deea99885adbbad128e440a8dbe8fa88a6b842689310 +size 59156 diff --git a/assets-source/animations/torch.psd b/assets-source/animations/torch.psd new file mode 100644 index 0000000..72ded7e --- /dev/null +++ b/assets-source/animations/torch.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90010d5122eb4989b7f248d30068f04bca93415d8d9b501a9504afde9c44b546 +size 30084 diff --git a/assets-source/animations/torch2.psd b/assets-source/animations/torch2.psd new file mode 100644 index 0000000..7701545 --- /dev/null +++ b/assets-source/animations/torch2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5c23d8972ccc4cc6e1b7322ea614e3f852ede0d7a40e0ee3a6a313da5db1ce3 +size 28268 diff --git a/assets-source/animations/water_crystal/1.psd b/assets-source/animations/water_crystal/1.psd new file mode 100644 index 0000000..106de6e --- /dev/null +++ b/assets-source/animations/water_crystal/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d02827eaa7a80804cfba6b589ad4456e3fb612625f3c3cad88de55500af2c984 +size 29180 diff --git a/assets-source/animations/water_crystal/2.psd b/assets-source/animations/water_crystal/2.psd new file mode 100644 index 0000000..d0c3f79 --- /dev/null +++ b/assets-source/animations/water_crystal/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49331e6098f3b536169f049ef0c28dad316aab7de1ac68d4140bfb3624aefbbe +size 30479 diff --git a/assets-source/animations/water_crystal/3.psd b/assets-source/animations/water_crystal/3.psd new file mode 100644 index 0000000..da19c81 --- /dev/null +++ b/assets-source/animations/water_crystal/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38ce3a138c8deedae4ec51b7ef69f55a4f26827eaacd56427438d62019cb44f7 +size 28806 diff --git a/assets-source/animations/water_rock/1.psd b/assets-source/animations/water_rock/1.psd new file mode 100644 index 0000000..9c60ad2 --- /dev/null +++ b/assets-source/animations/water_rock/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:135f7b416675c4e1e213c6403a745fa544f216d10ec19765047e84bd7a994b20 +size 29234 diff --git a/assets-source/animations/water_rock/10.psd b/assets-source/animations/water_rock/10.psd new file mode 100644 index 0000000..68ca6a5 --- /dev/null +++ b/assets-source/animations/water_rock/10.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5055472c06fce39412224b32b55c2b08d9f33e6f973ffd732728c01116c8a883 +size 27292 diff --git a/assets-source/animations/water_rock/11.psd b/assets-source/animations/water_rock/11.psd new file mode 100644 index 0000000..48f35c9 --- /dev/null +++ b/assets-source/animations/water_rock/11.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f5f0c8c609b3b942901c43e1cd3975dcd6d679772414e798eca39c839980b7b +size 32080 diff --git a/assets-source/animations/water_rock/2.psd b/assets-source/animations/water_rock/2.psd new file mode 100644 index 0000000..c949d52 --- /dev/null +++ b/assets-source/animations/water_rock/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d77dee5457a3a7058ea98c0acc1facb1122a1d88dc662dba340c25147cd8f78 +size 29423 diff --git a/assets-source/animations/water_rock/3.psd b/assets-source/animations/water_rock/3.psd new file mode 100644 index 0000000..ccd6006 --- /dev/null +++ b/assets-source/animations/water_rock/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25cbfa226d7a9a6c74bc2e89306123083ae0eade656fb773e0668781dfa8d832 +size 28620 diff --git a/assets-source/animations/water_rock/4.psd b/assets-source/animations/water_rock/4.psd new file mode 100644 index 0000000..0801524 --- /dev/null +++ b/assets-source/animations/water_rock/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd7d8900d86a9288e0278c65c5aaca68d85a6a6f44bd2cd5969d570077c9593d +size 26475 diff --git a/assets-source/animations/water_rock/5.psd b/assets-source/animations/water_rock/5.psd new file mode 100644 index 0000000..1002311 --- /dev/null +++ b/assets-source/animations/water_rock/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a441f3c79e20eaf5a1e002947d2662ac7eaf452724ac131592e3c7b5dc2112b +size 28685 diff --git a/assets-source/animations/water_rock/6.psd b/assets-source/animations/water_rock/6.psd new file mode 100644 index 0000000..fa47c5d --- /dev/null +++ b/assets-source/animations/water_rock/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7220a4222977c819ef5636832aaf94f1225769b00d5617f9a748b5284c51a29f +size 28378 diff --git a/assets-source/animations/water_rock/7.psd b/assets-source/animations/water_rock/7.psd new file mode 100644 index 0000000..e4a9f07 --- /dev/null +++ b/assets-source/animations/water_rock/7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0803da010735148ff35c234e5aa40886c98cfd07222aa100fb78b826fbcc49a1 +size 29565 diff --git a/assets-source/animations/water_rock/8.psd b/assets-source/animations/water_rock/8.psd new file mode 100644 index 0000000..70a1b29 --- /dev/null +++ b/assets-source/animations/water_rock/8.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:628648782e04eee23b4fd646002d97c0760ee4fa07645d8b0bdff055542c896c +size 26346 diff --git a/assets-source/animations/water_rock/9.psd b/assets-source/animations/water_rock/9.psd new file mode 100644 index 0000000..a288b76 --- /dev/null +++ b/assets-source/animations/water_rock/9.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4a1b06461c9d8fa7ba878c15f05fc3b6ef9a3899fc9cdbdbe14f04f3e26306d +size 28472 diff --git a/assets-source/buttons/bubble-2.png b/assets-source/buttons/bubble-2.png new file mode 100644 index 0000000..8bbe354 --- /dev/null +++ b/assets-source/buttons/bubble-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad33aabefdde172081cbbd2cc1bdb8fe1612ec1b11735e44b5902bb32faf9a6c +size 2817 diff --git a/assets-source/buttons/bubble2-2.png b/assets-source/buttons/bubble2-2.png new file mode 100644 index 0000000..e817ecd --- /dev/null +++ b/assets-source/buttons/bubble2-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa9a651e4af354b6f2a0725bebb8ab0456ea70d47618505026d0ee08bb8cfcdf +size 2818 diff --git a/assets-source/emotes/emote_cry.psd b/assets-source/emotes/emote_cry.psd new file mode 100644 index 0000000..465c654 --- /dev/null +++ b/assets-source/emotes/emote_cry.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d7adde73d37af91903b571a4bcf3bfd1436d1efc8b48f9bb1d48377d6e22724 +size 63354 diff --git a/assets-source/emotes/emote_cry2.psd b/assets-source/emotes/emote_cry2.psd new file mode 100644 index 0000000..558ed66 --- /dev/null +++ b/assets-source/emotes/emote_cry2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0934a1b1f9ff2112273e2525d59a2c9eb60b67d3ae54811e9c2b2e8a6e3e35cc +size 57438 diff --git a/assets-source/emotes/emote_hearts.psd b/assets-source/emotes/emote_hearts.psd new file mode 100644 index 0000000..f4cff2c --- /dev/null +++ b/assets-source/emotes/emote_hearts.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:784d18109364d274c80d8054208af2e12953fddf18b6eeeb631e7e59445b5d55 +size 98436 diff --git a/assets-source/emotes/emote_sleep1.psd b/assets-source/emotes/emote_sleep1.psd new file mode 100644 index 0000000..156986a --- /dev/null +++ b/assets-source/emotes/emote_sleep1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:101a969a0ae244a6377219d3966f4c374f1b94386f214aaa870cfed7809b433e +size 88774 diff --git a/assets-source/emotes/emote_sleep1_flip.psd b/assets-source/emotes/emote_sleep1_flip.psd new file mode 100644 index 0000000..6cae1a2 --- /dev/null +++ b/assets-source/emotes/emote_sleep1_flip.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16d1108deb8c86eb138a4db2080b3373b404ba347564ef194c8244e66cbd5f27 +size 88760 diff --git a/assets-source/emotes/emote_sleep2.psd b/assets-source/emotes/emote_sleep2.psd new file mode 100644 index 0000000..02cbeb7 --- /dev/null +++ b/assets-source/emotes/emote_sleep2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58d29c2932bf4f5590509a6c40cb7cc71ba1c8fbaa42af9bfb5e7785abb5e206 +size 116890 diff --git a/assets-source/emotes/emote_sleep2_flip.psd b/assets-source/emotes/emote_sleep2_flip.psd new file mode 100644 index 0000000..5a56850 --- /dev/null +++ b/assets-source/emotes/emote_sleep2_flip.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e09e50199633923b152833c2010ac32a6f7227bb6d65e816130c3517f934b6c0 +size 117116 diff --git a/assets-source/emotes/emote_sneeze.psd b/assets-source/emotes/emote_sneeze.psd new file mode 100644 index 0000000..1760197 --- /dev/null +++ b/assets-source/emotes/emote_sneeze.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad70334f8f3500105811bb11756c146e0fe7ddf67c39bfec4e186b914bc2d645 +size 39384 diff --git a/assets-source/emotes/emote_tears.psd b/assets-source/emotes/emote_tears.psd new file mode 100644 index 0000000..6488a78 --- /dev/null +++ b/assets-source/emotes/emote_tears.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af262c75a14e9f86dc37cb3b764a2b6036489b61d89076f88bc04e1f89c1fabc +size 37892 diff --git a/assets-source/emotes/hold_poof.psd b/assets-source/emotes/hold_poof.psd new file mode 100644 index 0000000..e965fca --- /dev/null +++ b/assets-source/emotes/hold_poof.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:457235d5c5a818b08ba61516265a764399ca595e54c69c450d7b986ace7a2d2a +size 44468 diff --git a/assets-source/fonts/cjk.psd b/assets-source/fonts/cjk.psd new file mode 100644 index 0000000..6f1e92e --- /dev/null +++ b/assets-source/fonts/cjk.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b93aa3370bf45155bffdb1aa8aed3bc089b32abbdbb3f3e303185e9733ce17e +size 2832213 diff --git a/assets-source/fonts/emoji.psd b/assets-source/fonts/emoji.psd new file mode 100644 index 0000000..a0415fc --- /dev/null +++ b/assets-source/fonts/emoji.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeee55925c4a0aa9d01102ab108ae404a9998b392193addfb5ed5643338f05da +size 125370 diff --git a/assets-source/fonts/flags.psd b/assets-source/fonts/flags.psd new file mode 100644 index 0000000..a91c980 --- /dev/null +++ b/assets-source/fonts/flags.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f11d5223afe1a777e9335d982b1c0a7a09698c8aa291718b3acf4f553d7717a3 +size 142850 diff --git a/assets-source/fonts/main.psd b/assets-source/fonts/main.psd new file mode 100644 index 0000000..a59ced7 --- /dev/null +++ b/assets-source/fonts/main.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db12482f0b5d2b31c080352c06e7335bce8a908123bd696a153baa591d870a73 +size 1847600 diff --git a/assets-source/fonts/mono.psd b/assets-source/fonts/mono.psd new file mode 100644 index 0000000..67d6283 --- /dev/null +++ b/assets-source/fonts/mono.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:849010a6773b091127fa1b8e1a948ee0e869727890a37e0bf71ffb7b9981ad95 +size 98116 diff --git a/assets-source/fonts/tiny.psd b/assets-source/fonts/tiny.psd new file mode 100644 index 0000000..84f47ef --- /dev/null +++ b/assets-source/fonts/tiny.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ded9fba36c1a8fcb468efb7c1329b563becb80dab8ac54974dce05277504073a +size 115656 diff --git a/assets-source/icons/arrow_down.png b/assets-source/icons/arrow_down.png new file mode 100644 index 0000000..de230a2 --- /dev/null +++ b/assets-source/icons/arrow_down.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89e99d8cd644d0b6074ea11ff789876e9c8461b3153de0bbc5eeac778e322e3c +size 949 diff --git a/assets-source/icons/arrow_left.png b/assets-source/icons/arrow_left.png new file mode 100644 index 0000000..7c02df5 --- /dev/null +++ b/assets-source/icons/arrow_left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:771745554208e0d42a0f9620c807f232da2f6eb7335d963514dedbe157a22e26 +size 950 diff --git a/assets-source/icons/arrow_right.png b/assets-source/icons/arrow_right.png new file mode 100644 index 0000000..2489be6 --- /dev/null +++ b/assets-source/icons/arrow_right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e3275158cd39cda7ab3e876deb5717ec06a98f954fe96702c240a32ba27b04c +size 951 diff --git a/assets-source/icons/arrow_up.png b/assets-source/icons/arrow_up.png new file mode 100644 index 0000000..bb8b986 --- /dev/null +++ b/assets-source/icons/arrow_up.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e14762faec33fbcf6d4383bf1f9502f50d3d9a99d766ba5071d3664cd618ee4a +size 946 diff --git a/assets-source/icons/magic_icon.png b/assets-source/icons/magic_icon.png new file mode 100644 index 0000000..242c664 --- /dev/null +++ b/assets-source/icons/magic_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d08efd993ed5a909291654f599097ad6fed890a1d859ba7794bf7d53c792d5b +size 2834 diff --git a/assets-source/lights/light1.png b/assets-source/lights/light1.png new file mode 100644 index 0000000..a986e3e --- /dev/null +++ b/assets-source/lights/light1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba48c1a139fe510b6949e58dcdefbd2c632bb15d118cc88f9b9b78f6cfd4a7d5 +size 3268 diff --git a/assets-source/lights/light16.png b/assets-source/lights/light16.png new file mode 100644 index 0000000..a251cbd --- /dev/null +++ b/assets-source/lights/light16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cd0b31fcbdefea7b4c14a6502f99e87a0bf0a2b9c2742a029783cb6426ab886 +size 973 diff --git a/assets-source/lights/light24.png b/assets-source/lights/light24.png new file mode 100644 index 0000000..a73ec32 --- /dev/null +++ b/assets-source/lights/light24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4689e216becffb34522dc549476601b7fcbdf004f0cb5a5447072f11996a873a +size 1003 diff --git a/assets-source/lights/light32.png b/assets-source/lights/light32.png new file mode 100644 index 0000000..085a053 --- /dev/null +++ b/assets-source/lights/light32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b647b402e7fcc4af16c9951c3d65f1ade184175ac4e3ec31f7c0c10f09302d3e +size 1025 diff --git a/assets-source/lights/light6.png b/assets-source/lights/light6.png new file mode 100644 index 0000000..a17326f --- /dev/null +++ b/assets-source/lights/light6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12f81b8c6ed93901f0c4f479e85e24b5509449649c65a3a31c7d6ae25fc65e67 +size 885 diff --git a/assets-source/lights/light_crystal_lantern.png b/assets-source/lights/light_crystal_lantern.png new file mode 100644 index 0000000..923831a --- /dev/null +++ b/assets-source/lights/light_crystal_lantern.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62efa6a0dc2666778851cd6a812407c54d48359feed49393734adc0f3fbd5efa +size 1021 diff --git a/assets-source/lights/light_crystals_1.png b/assets-source/lights/light_crystals_1.png new file mode 100644 index 0000000..209c818 --- /dev/null +++ b/assets-source/lights/light_crystals_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8c37c84a66e211e10ff25e07d8702d07c250b8d00c51ef760b26c8353631913 +size 174 diff --git a/assets-source/lights/light_crystals_10.png b/assets-source/lights/light_crystals_10.png new file mode 100644 index 0000000..8d8cd32 --- /dev/null +++ b/assets-source/lights/light_crystals_10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47d46c846078dcc290b6227f169bfbdb94d1fc5a46960184f0870c836cc0424f +size 120 diff --git a/assets-source/lights/light_crystals_2.png b/assets-source/lights/light_crystals_2.png new file mode 100644 index 0000000..657d0a3 --- /dev/null +++ b/assets-source/lights/light_crystals_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ae81834c1cbf3c1c45b357ade9ff4094160e11433dc18352ff9435ddb289c7 +size 198 diff --git a/assets-source/lights/light_crystals_3.png b/assets-source/lights/light_crystals_3.png new file mode 100644 index 0000000..f60274a --- /dev/null +++ b/assets-source/lights/light_crystals_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56f057a22bfb20f7e2907f90b10f1db17e67a9c8efcf868267b75cc613413de5 +size 179 diff --git a/assets-source/lights/light_crystals_4.png b/assets-source/lights/light_crystals_4.png new file mode 100644 index 0000000..8cc905a --- /dev/null +++ b/assets-source/lights/light_crystals_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0621e9d195906cdaaa3c00b19676a7978323d4963eb6a2193d75dfa7c5b9251 +size 173 diff --git a/assets-source/lights/light_crystals_5.png b/assets-source/lights/light_crystals_5.png new file mode 100644 index 0000000..e04258c --- /dev/null +++ b/assets-source/lights/light_crystals_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da06b153aecedfa077006a39b6f4ba44ea7bd014115fafb24aa045008eef3056 +size 202 diff --git a/assets-source/lights/light_crystals_6.png b/assets-source/lights/light_crystals_6.png new file mode 100644 index 0000000..b51b8c4 --- /dev/null +++ b/assets-source/lights/light_crystals_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0867646ab3a8fbb3919557fb2ffea99bb501af3a2264e2f8e00d572d9d1c57c5 +size 159 diff --git a/assets-source/lights/light_crystals_7.png b/assets-source/lights/light_crystals_7.png new file mode 100644 index 0000000..441b7ff --- /dev/null +++ b/assets-source/lights/light_crystals_7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70577a060dd194dab83e89e1cf1108c30d34123794f368fcff7704d7fcddce18 +size 183 diff --git a/assets-source/lights/light_crystals_8.png b/assets-source/lights/light_crystals_8.png new file mode 100644 index 0000000..7e5c9cb --- /dev/null +++ b/assets-source/lights/light_crystals_8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f37ae3081dfceeea68e794c373ed9a72b5d17d8c5a3d5700270439d7de08bbe1 +size 137 diff --git a/assets-source/lights/light_crystals_9.png b/assets-source/lights/light_crystals_9.png new file mode 100644 index 0000000..baaa341 --- /dev/null +++ b/assets-source/lights/light_crystals_9.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4be7800f037bd08837a030407e234759097ee9a09cb4deb2ee4764957024249 +size 133 diff --git a/assets-source/lights/light_crystals_cart_pile.png b/assets-source/lights/light_crystals_cart_pile.png new file mode 100644 index 0000000..257a244 --- /dev/null +++ b/assets-source/lights/light_crystals_cart_pile.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d8b562223f9befe5ad145eaec4a6511b5fc07a46425d21bfddb9694f0fa1347 +size 177 diff --git a/assets-source/lights/light_crystals_held.png b/assets-source/lights/light_crystals_held.png new file mode 100644 index 0000000..856789a --- /dev/null +++ b/assets-source/lights/light_crystals_held.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0eecba76e1dafe4b42f75725ef33a1b4990ac7ca90b6cfc53f078741eca1f76b +size 108 diff --git a/assets-source/objects/acorn.psd b/assets-source/objects/acorn.psd new file mode 100644 index 0000000..7c1150a --- /dev/null +++ b/assets-source/objects/acorn.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e6508e8cd65d2155a57aaf6b0ae5e6ac6bcbcf50d2c1fc8937623a3edc9df29 +size 22912 diff --git a/assets-source/objects/apple/1.psd b/assets-source/objects/apple/1.psd new file mode 100644 index 0000000..be0290c --- /dev/null +++ b/assets-source/objects/apple/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ab997251f0e9d66f92ba456f4fa494481d73ef5ee86cdf58fcc5a961c673d87 +size 26296 diff --git a/assets-source/objects/apple/2.psd b/assets-source/objects/apple/2.psd new file mode 100644 index 0000000..c6fe93c --- /dev/null +++ b/assets-source/objects/apple/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3658733bdee77f3a9e3bd8ad4a0344bca84c07682c9d39690b8df24232577840 +size 23068 diff --git a/assets-source/objects/apple/palette_1_1.psd b/assets-source/objects/apple/palette_1_1.psd new file mode 100644 index 0000000..c34e6f6 --- /dev/null +++ b/assets-source/objects/apple/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0529e1478a038b6ac0d444310321821f03c1d90bfa8b36833f427c35e76226a1 +size 26276 diff --git a/assets-source/objects/banana.psd b/assets-source/objects/banana.psd new file mode 100644 index 0000000..62134c2 --- /dev/null +++ b/assets-source/objects/banana.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c05d04dadb3e0b3642ea8e41c8f144602f282105524489a521539042ce1c0d81 +size 22376 diff --git a/assets-source/objects/barrel.psd b/assets-source/objects/barrel.psd new file mode 100644 index 0000000..6c19aaa --- /dev/null +++ b/assets-source/objects/barrel.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cdb7e841d9e8457012d34c99075b525f2987efde83a1ed4f15251f8e7083e15 +size 27041 diff --git a/assets-source/objects/basket_bin.psd b/assets-source/objects/basket_bin.psd new file mode 100644 index 0000000..947933f --- /dev/null +++ b/assets-source/objects/basket_bin.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b4fabeea3374a2d8c51e37575de119435c35769b9775cbc44958c783abe8afb +size 35549 diff --git a/assets-source/objects/bench/1.png b/assets-source/objects/bench/1.png new file mode 100644 index 0000000..9bdb3c1 --- /dev/null +++ b/assets-source/objects/bench/1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:342ab09a8304a610bbcbe80a1cec0e75d3e04ad9ba9c5e10a90e84f6f34fe521 +size 635 diff --git a/assets-source/objects/bench/1.psd b/assets-source/objects/bench/1.psd new file mode 100644 index 0000000..19fef33 --- /dev/null +++ b/assets-source/objects/bench/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03076b8acc58c1a362d74115c906c5354c07506fd334bad4758fb6f036304664 +size 27801 diff --git a/assets-source/objects/bench/back.png b/assets-source/objects/bench/back.png new file mode 100644 index 0000000..21d22fd --- /dev/null +++ b/assets-source/objects/bench/back.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b12b1d3c778d2a8b20bc98895cd6745afbd1348d986f564da8bae258966878a +size 495 diff --git a/assets-source/objects/bench/back.psd b/assets-source/objects/bench/back.psd new file mode 100644 index 0000000..2e69ca7 --- /dev/null +++ b/assets-source/objects/bench/back.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afa07b1919fc92f1b46ccdcf8bbba0f9f5ec228aebe2659b6bbf045d27d41483 +size 29098 diff --git a/assets-source/objects/bench/backh.png b/assets-source/objects/bench/backh.png new file mode 100644 index 0000000..b68a67b --- /dev/null +++ b/assets-source/objects/bench/backh.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c750df25af6adc3ba9b6359782639ebc921b7a4590289deb8208aeef291fb5b +size 484 diff --git a/assets-source/objects/bench/backh.psd b/assets-source/objects/bench/backh.psd new file mode 100644 index 0000000..d81385f --- /dev/null +++ b/assets-source/objects/bench/backh.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efa53a721828503dae257807433f74e17004a6b900bdbae14709de9242b9e835 +size 35966 diff --git a/assets-source/objects/bench/backh2.png b/assets-source/objects/bench/backh2.png new file mode 100644 index 0000000..53a91c8 --- /dev/null +++ b/assets-source/objects/bench/backh2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4427e259d2b646cb221097c611e7a3ec15d1671a7adc25377dd1c4974130a7f5 +size 477 diff --git a/assets-source/objects/bench/backh2.psd b/assets-source/objects/bench/backh2.psd new file mode 100644 index 0000000..1e449ae --- /dev/null +++ b/assets-source/objects/bench/backh2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75baafbffae697563c57d64e295c870b9b8b931430fe236d29a5ffe2805521de +size 33130 diff --git a/assets-source/objects/bench/seat.png b/assets-source/objects/bench/seat.png new file mode 100644 index 0000000..ed8b29b --- /dev/null +++ b/assets-source/objects/bench/seat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79ab7b716062973d6183aa0e233f06b9469d89db0c44a073fa4dd7fdb9146138 +size 393 diff --git a/assets-source/objects/bench/seat.psd b/assets-source/objects/bench/seat.psd new file mode 100644 index 0000000..aa76cc8 --- /dev/null +++ b/assets-source/objects/bench/seat.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11f3074c9cb2ec3f0b88a4f847ae89ad96ac0e317d9c440457f597f9a0b26b44 +size 25034 diff --git a/assets-source/objects/bench/seath.png b/assets-source/objects/bench/seath.png new file mode 100644 index 0000000..227dc3e --- /dev/null +++ b/assets-source/objects/bench/seath.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bbf1a09923da1fd4125403dad766ecfed611e9f66d02a37a5ffd5dc5285c726 +size 398 diff --git a/assets-source/objects/bench/seath.psd b/assets-source/objects/bench/seath.psd new file mode 100644 index 0000000..b7d32ab --- /dev/null +++ b/assets-source/objects/bench/seath.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c22fa03ebd8a87fea56adea5a1758b46bc0473486b598cbd21beb80563f65cff +size 30785 diff --git a/assets-source/objects/boat.psd b/assets-source/objects/boat.psd new file mode 100644 index 0000000..200e5e6 --- /dev/null +++ b/assets-source/objects/boat.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c391b615a9d7e416c88ac00677e957a36243a3616ea83bb92af804426cfc7d5 +size 55266 diff --git a/assets-source/objects/boat_front.psd b/assets-source/objects/boat_front.psd new file mode 100644 index 0000000..f749865 --- /dev/null +++ b/assets-source/objects/boat_front.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce5534485df04a0f6b536a7f26b6f258882a1931328c7443d701e4a3d8e9e10e +size 28325 diff --git a/assets-source/objects/boat_rope.psd b/assets-source/objects/boat_rope.psd new file mode 100644 index 0000000..2dcdf06 --- /dev/null +++ b/assets-source/objects/boat_rope.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:525fa449db62c7507abfa425d00df6b4c18caf684c2cbc083257552ab1fe94b2 +size 22878 diff --git a/assets-source/objects/boat_sail.psd b/assets-source/objects/boat_sail.psd new file mode 100644 index 0000000..c2c61ae --- /dev/null +++ b/assets-source/objects/boat_sail.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d4c336d39b4e0ccc8e05630143f5ea0aa3299102ea30086ab6318080c7079e0 +size 74821 diff --git a/assets-source/objects/bookshelf.psd b/assets-source/objects/bookshelf.psd new file mode 100644 index 0000000..396367c --- /dev/null +++ b/assets-source/objects/bookshelf.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3887dbd0a1018583a0e7860f48dd26edab9305ff20eca34b208027dccdd7007 +size 39259 diff --git a/assets-source/objects/box/_base.psd b/assets-source/objects/box/_base.psd new file mode 100644 index 0000000..97e5cc0 --- /dev/null +++ b/assets-source/objects/box/_base.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afc68c1579a88cd1aa14eda0418beb94f3dfe2ee6cb31168dc4d2a75a6a12909 +size 25788 diff --git a/assets-source/objects/box/_gifts.psd b/assets-source/objects/box/_gifts.psd new file mode 100644 index 0000000..ae4ed16 --- /dev/null +++ b/assets-source/objects/box/_gifts.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05822dc9a6c4a1c7ed8482d146b78a60f6b83441bc0a60da06da9b29fbb8da67 +size 32460 diff --git a/assets-source/objects/box/_lanterns.psd b/assets-source/objects/box/_lanterns.psd new file mode 100644 index 0000000..38453da --- /dev/null +++ b/assets-source/objects/box/_lanterns.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e91138688a08f89fd55e33f3a23619d763d6b3fa58e014718add31f65e74170c +size 34964 diff --git a/assets-source/objects/box/baskets.psd b/assets-source/objects/box/baskets.psd new file mode 100644 index 0000000..9f55f67 --- /dev/null +++ b/assets-source/objects/box/baskets.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65a1d16f7171cb0c79079fe52673e956e721e926a2c241a4c2a682aeabf04861 +size 27792 diff --git a/assets-source/objects/box/empty.psd b/assets-source/objects/box/empty.psd new file mode 100644 index 0000000..0640315 --- /dev/null +++ b/assets-source/objects/box/empty.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e03f87cd3b3ab3fdc6c35d43446e2aae2bb9b290351e2c247814309eac4d35a +size 23590 diff --git a/assets-source/objects/box/fruits.psd b/assets-source/objects/box/fruits.psd new file mode 100644 index 0000000..ea370ed --- /dev/null +++ b/assets-source/objects/box/fruits.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:367b57245b12c32e3f3d9023c6f8177032d70892ef465a9ae203ba57377577a0 +size 28135 diff --git a/assets-source/objects/box/gifts.psd b/assets-source/objects/box/gifts.psd new file mode 100644 index 0000000..9c7390a --- /dev/null +++ b/assets-source/objects/box/gifts.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b09d5889fe524eed8db6e5886d083a8b5f5e36a51e472b4428ce24cfe23dd581 +size 28731 diff --git a/assets-source/objects/box/lanterns.psd b/assets-source/objects/box/lanterns.psd new file mode 100644 index 0000000..4f2dab3 --- /dev/null +++ b/assets-source/objects/box/lanterns.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:750d9b68cae8bda395e6e63f985bd4b5291342fe6d57e6ef7495e6377c0b6ef1 +size 26182 diff --git a/assets-source/objects/broom.psd b/assets-source/objects/broom.psd new file mode 100644 index 0000000..c983fd2 --- /dev/null +++ b/assets-source/objects/broom.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9ba260baf06c21a3740213896bc2bbf9afe6cf4ed8318e1dd2b47effe3af5c4 +size 23192 diff --git a/assets-source/objects/candy.psd b/assets-source/objects/candy.psd new file mode 100644 index 0000000..27dc10c --- /dev/null +++ b/assets-source/objects/candy.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ecc80a1437192613a69639e8c4c510e277cd93f5c8eb06cb09ae3859803be4a +size 22902 diff --git a/assets-source/objects/candy_cane/1.psd b/assets-source/objects/candy_cane/1.psd new file mode 100644 index 0000000..3773b10 --- /dev/null +++ b/assets-source/objects/candy_cane/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91510087fce28bf39ae3b574129e7ece66d33ba934de4cf76674839738fbc306 +size 22516 diff --git a/assets-source/objects/candy_cane/2.psd b/assets-source/objects/candy_cane/2.psd new file mode 100644 index 0000000..2e2c442 --- /dev/null +++ b/assets-source/objects/candy_cane/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ef52bf322863f48bf07da3a60035f0484456b3f50d6d022237aac9784aa2dad +size 22638 diff --git a/assets-source/objects/carrot/1.psd b/assets-source/objects/carrot/1.psd new file mode 100644 index 0000000..a24b1ac --- /dev/null +++ b/assets-source/objects/carrot/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76efeb3abbdd011efd0bea0e6568ec269f6044f7f7c97d6b1c22a75f65508430 +size 26422 diff --git a/assets-source/objects/carrot/1b.psd b/assets-source/objects/carrot/1b.psd new file mode 100644 index 0000000..7a4e004 --- /dev/null +++ b/assets-source/objects/carrot/1b.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ccd3225fbfcd6ad57a23fbdeae6d23bd058c5686366f383f6c57742915cb4ef +size 23266 diff --git a/assets-source/objects/carrot/2.psd b/assets-source/objects/carrot/2.psd new file mode 100644 index 0000000..d0cce5a --- /dev/null +++ b/assets-source/objects/carrot/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a64be7041505b421a393c0c7272c24aa0ab3505b4e1ff160922411f4b99c14d9 +size 23134 diff --git a/assets-source/objects/carrot/2b.psd b/assets-source/objects/carrot/2b.psd new file mode 100644 index 0000000..121cc47 --- /dev/null +++ b/assets-source/objects/carrot/2b.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:716356d35bfda7b6d2c891faa80ed0226bc86a1cab28cb587415bf415c4050e6 +size 23144 diff --git a/assets-source/objects/carrot/3.psd b/assets-source/objects/carrot/3.psd new file mode 100644 index 0000000..338f1d8 --- /dev/null +++ b/assets-source/objects/carrot/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8d1c9b37b4c285b0b23891bf0c90418627ec310248fc7e8c6842883ddb7571a +size 23054 diff --git a/assets-source/objects/carrot/4.psd b/assets-source/objects/carrot/4.psd new file mode 100644 index 0000000..855d88a --- /dev/null +++ b/assets-source/objects/carrot/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bde7f17a3b81620ddc29890a012ca14b42b5a5802e93dc840f3d5649bdb988c9 +size 22732 diff --git a/assets-source/objects/carrot/hold.psd b/assets-source/objects/carrot/hold.psd new file mode 100644 index 0000000..206256e --- /dev/null +++ b/assets-source/objects/carrot/hold.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34a81dd47b640340bc1492ccad9bdfe902226b9c28aa3b849aa83a2a740a134b +size 22946 diff --git a/assets-source/objects/carrot/palette_1_hold.psd b/assets-source/objects/carrot/palette_1_hold.psd new file mode 100644 index 0000000..ffb5f53 --- /dev/null +++ b/assets-source/objects/carrot/palette_1_hold.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dbdce661f6f0c14be5d6462359e3aa9c47ea518bbd0e39b1295528a5e6a6f8c +size 23336 diff --git a/assets-source/objects/christmastree.psd b/assets-source/objects/christmastree.psd new file mode 100644 index 0000000..e6780d9 --- /dev/null +++ b/assets-source/objects/christmastree.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a40c2929035f962ac5aef65cf71245436f92a13606ea84045ec789ae255ca7 +size 93119 diff --git a/assets-source/objects/cloud.psd b/assets-source/objects/cloud.psd new file mode 100644 index 0000000..b56e8f2 --- /dev/null +++ b/assets-source/objects/cloud.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:611a3d67b27bfdbc1fb0a15a55fe140f239bfc908defad1855f670862a4f0362 +size 30064 diff --git a/assets-source/objects/clover/1.psd b/assets-source/objects/clover/1.psd new file mode 100644 index 0000000..c488f08 --- /dev/null +++ b/assets-source/objects/clover/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c52227202b5244bb70eac316108d7ab8c1561d0662ecf76cb6f25e40e43f3d2d +size 23385 diff --git a/assets-source/objects/clover/2.psd b/assets-source/objects/clover/2.psd new file mode 100644 index 0000000..bd98be3 --- /dev/null +++ b/assets-source/objects/clover/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc4ecffa5c8bb8f5c0c6d67486619da0b2c473300008d83486df05c4570a5384 +size 24616 diff --git a/assets-source/objects/clover/3.psd b/assets-source/objects/clover/3.psd new file mode 100644 index 0000000..c9f4fd2 --- /dev/null +++ b/assets-source/objects/clover/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13fe44140f4413582b62d5aec3bbd0d2449e3da5ac4098e442c094143f8a2e4d +size 24718 diff --git a/assets-source/objects/clover/4.psd b/assets-source/objects/clover/4.psd new file mode 100644 index 0000000..bc98db3 --- /dev/null +++ b/assets-source/objects/clover/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdf63dd99e05314df52275bcf6b6c2d0816d05cd18f264efcca4f9efa808fdbb +size 24270 diff --git a/assets-source/objects/clover/5.psd b/assets-source/objects/clover/5.psd new file mode 100644 index 0000000..d2b60b1 --- /dev/null +++ b/assets-source/objects/clover/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577a2652c64e4129c981e43e70f5b7ccf63235749a5efb624273aec93982314b +size 24582 diff --git a/assets-source/objects/clover/mouth.psd b/assets-source/objects/clover/mouth.psd new file mode 100644 index 0000000..3a947ff --- /dev/null +++ b/assets-source/objects/clover/mouth.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1537d68d95805d66a9258c963a8b2b12e9007e22788bb77f4b8a4316f348877e +size 23146 diff --git a/assets-source/objects/clover/patch3.psd b/assets-source/objects/clover/patch3.psd new file mode 100644 index 0000000..e47cf53 --- /dev/null +++ b/assets-source/objects/clover/patch3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bc9bb4d6c6063b7c5b0246221a6c699d6e15279114e20dc358a9cfa704a0649 +size 31304 diff --git a/assets-source/objects/clover/patch4.psd b/assets-source/objects/clover/patch4.psd new file mode 100644 index 0000000..6efc004 --- /dev/null +++ b/assets-source/objects/clover/patch4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a30d62a31840e3cebbee1bfb2d98901b8b52852a4c822db134768398b133be2 +size 31376 diff --git a/assets-source/objects/clover/patch5.psd b/assets-source/objects/clover/patch5.psd new file mode 100644 index 0000000..8a1eac5 --- /dev/null +++ b/assets-source/objects/clover/patch5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce86217c1964a80e8f56d08c68335d198cb54cdbd1cb665f54da4eb402e004d6 +size 26374 diff --git a/assets-source/objects/clover/patch6.psd b/assets-source/objects/clover/patch6.psd new file mode 100644 index 0000000..bffd864 --- /dev/null +++ b/assets-source/objects/clover/patch6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6ddcefb8b41da486637bec85d49671c656b64a26bcb9e42d9617df356d8a8a4 +size 27456 diff --git a/assets-source/objects/clover/patch7.psd b/assets-source/objects/clover/patch7.psd new file mode 100644 index 0000000..24f2bb1 --- /dev/null +++ b/assets-source/objects/clover/patch7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ca46b1e48130775251d3de30f0153ba7200558c99bcec5d3cb49f0aa33f0035 +size 25040 diff --git a/assets-source/objects/clover/pick.psd b/assets-source/objects/clover/pick.psd new file mode 100644 index 0000000..421ae23 --- /dev/null +++ b/assets-source/objects/clover/pick.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47b9888953724e1e873a6c3d262a309f4b8839647fda78f13a6cba3f49370713 +size 21542 diff --git a/assets-source/objects/cookie.psd b/assets-source/objects/cookie.psd new file mode 100644 index 0000000..4de85b2 --- /dev/null +++ b/assets-source/objects/cookie.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53a9cddbb19230dfb865aacf6ea596bd8b04edf79fdb0156dadd0feabafbb8ad +size 22588 diff --git a/assets-source/objects/cookie_pony.psd b/assets-source/objects/cookie_pony.psd new file mode 100644 index 0000000..8016b91 --- /dev/null +++ b/assets-source/objects/cookie_pony.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7fca00aae33b9e80cfa36541bc3c744c6a2bc6f67c8934c331e1a0bc3c076ed +size 18206 diff --git a/assets-source/objects/cookie_table/1.psd b/assets-source/objects/cookie_table/1.psd new file mode 100644 index 0000000..026c2aa --- /dev/null +++ b/assets-source/objects/cookie_table/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0430dc1026448f9777ddc381ff78e51edb4dfd07a3e057a40e200ee646165382 +size 30462 diff --git a/assets-source/objects/cookie_table/2.psd b/assets-source/objects/cookie_table/2.psd new file mode 100644 index 0000000..b15631d --- /dev/null +++ b/assets-source/objects/cookie_table/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51771ef5e37139ff14a75c3ed6b6889ebc84d53ddbd3cd789a488c3717332056 +size 30477 diff --git a/assets-source/objects/crate/1.psd b/assets-source/objects/crate/1.psd new file mode 100644 index 0000000..8995620 --- /dev/null +++ b/assets-source/objects/crate/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1372679977b65a5b661c086b30b3d34b1a50a247ad41ef6630ebb464ef645dd0 +size 29794 diff --git a/assets-source/objects/crate/2.psd b/assets-source/objects/crate/2.psd new file mode 100644 index 0000000..1f321ce --- /dev/null +++ b/assets-source/objects/crate/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d468b7f9931dd60634b4a085d4d70c27f1d63a2bac9d33698461e14d2a65a4 +size 24712 diff --git a/assets-source/objects/crate/3.psd b/assets-source/objects/crate/3.psd new file mode 100644 index 0000000..4098a38 --- /dev/null +++ b/assets-source/objects/crate/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89592d99e241f84ed95c4b13d1a62e8e4fe9784ada99db64fccb88316841f874 +size 24612 diff --git a/assets-source/objects/crate/palette_1_1.psd b/assets-source/objects/crate/palette_1_1.psd new file mode 100644 index 0000000..16b3ae0 --- /dev/null +++ b/assets-source/objects/crate/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf621736b2d7b5a99b6a69bafc77769525692fe1e5d660d9af0055d0111f545c +size 30572 diff --git a/assets-source/objects/crystal_lantern.psd b/assets-source/objects/crystal_lantern.psd new file mode 100644 index 0000000..fbfbd9d --- /dev/null +++ b/assets-source/objects/crystal_lantern.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8507c7ff77b7a0e64ad196d3381dc6faf24f5c38c70efb048c47cbb19a1bc8db +size 24694 diff --git a/assets-source/objects/crystals/1.psd b/assets-source/objects/crystals/1.psd new file mode 100644 index 0000000..580a9bc --- /dev/null +++ b/assets-source/objects/crystals/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3330f9f7573b85072fb32dccbd8a53699fe73b79ca96b57b5f9ed191cb6a162f +size 24988 diff --git a/assets-source/objects/crystals/10.psd b/assets-source/objects/crystals/10.psd new file mode 100644 index 0000000..deab2d5 --- /dev/null +++ b/assets-source/objects/crystals/10.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:789fc35f33206f37dfa69352a73531f8b565d456e92e5281ab111a41b3356948 +size 23928 diff --git a/assets-source/objects/crystals/2.psd b/assets-source/objects/crystals/2.psd new file mode 100644 index 0000000..9a41021 --- /dev/null +++ b/assets-source/objects/crystals/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ea933025dc74775379d88ed47c3c14def19647687d9e6f1f11c7397953a9b2c +size 25569 diff --git a/assets-source/objects/crystals/3.psd b/assets-source/objects/crystals/3.psd new file mode 100644 index 0000000..28b4bfb --- /dev/null +++ b/assets-source/objects/crystals/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d10b95ea8605a8ffe9a5c49bc50754914b2935f224dc3dd48e1433572cb800bc +size 25426 diff --git a/assets-source/objects/crystals/4.psd b/assets-source/objects/crystals/4.psd new file mode 100644 index 0000000..a3b2dfb --- /dev/null +++ b/assets-source/objects/crystals/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e72b6fc8e5f75a0403c9a17ddda43a679722664deb0b3ca107929ed2e3760e3 +size 24963 diff --git a/assets-source/objects/crystals/5.psd b/assets-source/objects/crystals/5.psd new file mode 100644 index 0000000..70e9bd7 --- /dev/null +++ b/assets-source/objects/crystals/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebba630eb727f75e989901a221748216b6e9346ae5fc9950ebf5af6718f285f4 +size 25737 diff --git a/assets-source/objects/crystals/6.psd b/assets-source/objects/crystals/6.psd new file mode 100644 index 0000000..8818f4b --- /dev/null +++ b/assets-source/objects/crystals/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01d654855d8a73ca3010b92e4fe1ee135a7ab724cb8c37a82dbb50602d2930b7 +size 24327 diff --git a/assets-source/objects/crystals/7.psd b/assets-source/objects/crystals/7.psd new file mode 100644 index 0000000..c53cef0 --- /dev/null +++ b/assets-source/objects/crystals/7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6ace9fece3a4e2cf92b554e19cfc3b3368fb9697ffc58d29397a32a3b6fd481 +size 25144 diff --git a/assets-source/objects/crystals/8.psd b/assets-source/objects/crystals/8.psd new file mode 100644 index 0000000..0f25536 --- /dev/null +++ b/assets-source/objects/crystals/8.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dedb7f7a2698c43882344dbbc1287a70e886e2553ff3affd26e4a8c16c482338 +size 24840 diff --git a/assets-source/objects/crystals/9.psd b/assets-source/objects/crystals/9.psd new file mode 100644 index 0000000..10c9d4e --- /dev/null +++ b/assets-source/objects/crystals/9.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a3094461d34ba164d967cad99fc5e7fc3ec8ac5de60cf63ddf1e6a0abda4b59 +size 23861 diff --git a/assets-source/objects/crystals/cart_pile.psd b/assets-source/objects/crystals/cart_pile.psd new file mode 100644 index 0000000..9a47179 --- /dev/null +++ b/assets-source/objects/crystals/cart_pile.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8076d069cb6f49fc8c75fbcc595ab284d554e31eaae5ad59a9ebd635ed2e0897 +size 27038 diff --git a/assets-source/objects/crystals/held.psd b/assets-source/objects/crystals/held.psd new file mode 100644 index 0000000..7c2a50f --- /dev/null +++ b/assets-source/objects/crystals/held.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9c387f05e4f2f2b5d394c86cbdd91cd18ff284b0cc119173a34cd99e8023c7d +size 23778 diff --git a/assets-source/objects/cushion/1.psd b/assets-source/objects/cushion/1.psd new file mode 100644 index 0000000..d225a66 --- /dev/null +++ b/assets-source/objects/cushion/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e472a5472c58f8110a82bf0638c7eac1c520ad6eab863d5bd386e678dd33b029 +size 24798 diff --git a/assets-source/objects/cushion/palette_1_1.psd b/assets-source/objects/cushion/palette_1_1.psd new file mode 100644 index 0000000..e0b3bef --- /dev/null +++ b/assets-source/objects/cushion/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cc268e84150aecfc13aee693f391289575bca274a0ed712ab7e540171c31e63 +size 25664 diff --git a/assets-source/objects/cushion/palette_2_1.psd b/assets-source/objects/cushion/palette_2_1.psd new file mode 100644 index 0000000..19c0591 --- /dev/null +++ b/assets-source/objects/cushion/palette_2_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b218b8fa711c46f74716feda49e19c48521f1ed214b3be698df3b8ac5052286 +size 25672 diff --git a/assets-source/objects/direction/down_left.psd b/assets-source/objects/direction/down_left.psd new file mode 100644 index 0000000..afbfdfd --- /dev/null +++ b/assets-source/objects/direction/down_left.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a9e2c9f1422130625e2954b49ceb6aebf693dbbdd489a0c0a7119f620be32b4 +size 22998 diff --git a/assets-source/objects/direction/down_right.psd b/assets-source/objects/direction/down_right.psd new file mode 100644 index 0000000..aa69adf --- /dev/null +++ b/assets-source/objects/direction/down_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b88df09bfac66d746c0cafd9ad98fcf21cd442f0ca5babf4b68c5990d91fc1e1 +size 22992 diff --git a/assets-source/objects/direction/left_right.psd b/assets-source/objects/direction/left_right.psd new file mode 100644 index 0000000..d53dac0 --- /dev/null +++ b/assets-source/objects/direction/left_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da48f74f21dbdcc9874f187a44fde113f0c965a1ad2a7bbf6ed7871488659876 +size 48092 diff --git a/assets-source/objects/direction/pole_3.psd b/assets-source/objects/direction/pole_3.psd new file mode 100644 index 0000000..1011648 --- /dev/null +++ b/assets-source/objects/direction/pole_3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e64c61b3310999557f57515fab32338f8c0f7c574cd7293b456396b50fefaf4 +size 28866 diff --git a/assets-source/objects/direction/pole_4.psd b/assets-source/objects/direction/pole_4.psd new file mode 100644 index 0000000..4498e42 --- /dev/null +++ b/assets-source/objects/direction/pole_4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfd5b64cd3e6a4876350e4c2c1066b7fe668df53546beba68f904200070bea31 +size 26386 diff --git a/assets-source/objects/direction/pole_5.psd b/assets-source/objects/direction/pole_5.psd new file mode 100644 index 0000000..5613662 --- /dev/null +++ b/assets-source/objects/direction/pole_5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:505497209baf009e6d7eab5b5e6202514d5720fe3b2d91b8c0e40a3b355d4708 +size 27012 diff --git a/assets-source/objects/direction/shadow_down_left.psd b/assets-source/objects/direction/shadow_down_left.psd new file mode 100644 index 0000000..62ab2e2 --- /dev/null +++ b/assets-source/objects/direction/shadow_down_left.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:208aa998ee3a82ccb15c2894a171915fc921fcab8f8fe80da83f737032a14cce +size 22310 diff --git a/assets-source/objects/direction/shadow_down_right.psd b/assets-source/objects/direction/shadow_down_right.psd new file mode 100644 index 0000000..b046bf1 --- /dev/null +++ b/assets-source/objects/direction/shadow_down_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fad0717ae5e7437a19e529ed3b1ea67746b910a8ca783c79ea858bf634a8a9b +size 22318 diff --git a/assets-source/objects/direction/shadow_left.psd b/assets-source/objects/direction/shadow_left.psd new file mode 100644 index 0000000..bf9e611 --- /dev/null +++ b/assets-source/objects/direction/shadow_left.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36682baef8a0f3c74a01502159e226fefe3d1f17f6605be031e5839948539720 +size 22976 diff --git a/assets-source/objects/direction/shadow_right.psd b/assets-source/objects/direction/shadow_right.psd new file mode 100644 index 0000000..a646c77 --- /dev/null +++ b/assets-source/objects/direction/shadow_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ddb9a41ca181b85bb6c34745acb2a3dd073b30922c12dd5a69bc3a67c1ecc97 +size 23000 diff --git a/assets-source/objects/direction/shadow_up_left.psd b/assets-source/objects/direction/shadow_up_left.psd new file mode 100644 index 0000000..d29dd21 --- /dev/null +++ b/assets-source/objects/direction/shadow_up_left.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efb6cf6700677b2b476a6655d5efa7bbf4803f9a7dd4e842613e2f4f9bdb147f +size 22226 diff --git a/assets-source/objects/direction/shadow_up_right.psd b/assets-source/objects/direction/shadow_up_right.psd new file mode 100644 index 0000000..9d6b890 --- /dev/null +++ b/assets-source/objects/direction/shadow_up_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b01dd55066bb3eb3f59d633ea449ccd5e8f5595cdc8508e2e077e45799c25a2 +size 22226 diff --git a/assets-source/objects/direction/up_left.psd b/assets-source/objects/direction/up_left.psd new file mode 100644 index 0000000..b153c8f --- /dev/null +++ b/assets-source/objects/direction/up_left.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a34618c0c5846a35d3f63486c8e47b0f532e2a6ead4dc9a2059849eddce1d738 +size 22686 diff --git a/assets-source/objects/direction/up_right.psd b/assets-source/objects/direction/up_right.psd new file mode 100644 index 0000000..e338184 --- /dev/null +++ b/assets-source/objects/direction/up_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a596cf1d99278418f45c36a51e49cc23eae2838c76c8cd3677689c03e7e85782 +size 22682 diff --git a/assets-source/objects/egg/1.psd b/assets-source/objects/egg/1.psd new file mode 100644 index 0000000..607acde --- /dev/null +++ b/assets-source/objects/egg/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a93f1325b69dbdcc614b9fe34fbe39180ede8097d507b61552639361f27636a +size 22502 diff --git a/assets-source/objects/egg/10.psd b/assets-source/objects/egg/10.psd new file mode 100644 index 0000000..09701b0 --- /dev/null +++ b/assets-source/objects/egg/10.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:550abf9cf8da3d66879c32c51025f45632edf17438156710709efecfb1e4632d +size 26102 diff --git a/assets-source/objects/egg/11.psd b/assets-source/objects/egg/11.psd new file mode 100644 index 0000000..6393aca --- /dev/null +++ b/assets-source/objects/egg/11.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc62da871fc67baff99d0d9ac819172bcab94ff156ee6edceefc3730d05cb6c8 +size 22974 diff --git a/assets-source/objects/egg/12.psd b/assets-source/objects/egg/12.psd new file mode 100644 index 0000000..9542b7c --- /dev/null +++ b/assets-source/objects/egg/12.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdecb1e256ee347557565e2b8dee1ce5ea8b379c53f6d64ae524b8f67f31156 +size 22954 diff --git a/assets-source/objects/egg/13.psd b/assets-source/objects/egg/13.psd new file mode 100644 index 0000000..ca66c47 --- /dev/null +++ b/assets-source/objects/egg/13.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62ca0e4807192efd71eb2af704560a20fb998f7219684bc4ebade030e80d5d8a +size 26108 diff --git a/assets-source/objects/egg/14.psd b/assets-source/objects/egg/14.psd new file mode 100644 index 0000000..29fe090 --- /dev/null +++ b/assets-source/objects/egg/14.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:501802251081e88d45046c9a1353fd6ad9a7610ab17e1626ae3c9271a59fd4f0 +size 22926 diff --git a/assets-source/objects/egg/15.psd b/assets-source/objects/egg/15.psd new file mode 100644 index 0000000..bbe43b3 --- /dev/null +++ b/assets-source/objects/egg/15.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69873cf32673d20f7cf67db87fae36f4ea6337d31b456a43195efbced120133a +size 22954 diff --git a/assets-source/objects/egg/16.psd b/assets-source/objects/egg/16.psd new file mode 100644 index 0000000..7b91ad8 --- /dev/null +++ b/assets-source/objects/egg/16.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6c8e4bb3c9bac167bd32afe12568523f169e7fcd46b1d3177ad427523afc560 +size 22948 diff --git a/assets-source/objects/egg/17.psd b/assets-source/objects/egg/17.psd new file mode 100644 index 0000000..795d95c --- /dev/null +++ b/assets-source/objects/egg/17.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f6315f5e9444684abdfd5e6c41fe6b0b66157f9019578f892d1e1fef9abeb5f +size 22966 diff --git a/assets-source/objects/egg/18.psd b/assets-source/objects/egg/18.psd new file mode 100644 index 0000000..8aa3ca7 --- /dev/null +++ b/assets-source/objects/egg/18.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2eec3ba50a208281a977d0b4f730a3343c2b996dbafa0af3a3dd384ee8315f7 +size 22958 diff --git a/assets-source/objects/egg/19.psd b/assets-source/objects/egg/19.psd new file mode 100644 index 0000000..08c8d98 --- /dev/null +++ b/assets-source/objects/egg/19.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b136ef9aae287493cc75df65adb7b58a6b49cb804a3ea0251b59b355856dd2e1 +size 22944 diff --git a/assets-source/objects/egg/2.psd b/assets-source/objects/egg/2.psd new file mode 100644 index 0000000..744fe9a --- /dev/null +++ b/assets-source/objects/egg/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d5f3a68094aab6f2857146393e51959bb1150ab0204da22623d04f28f13fbef +size 22984 diff --git a/assets-source/objects/egg/20.psd b/assets-source/objects/egg/20.psd new file mode 100644 index 0000000..55a4b1e --- /dev/null +++ b/assets-source/objects/egg/20.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dfa494a97e65f7c172f32295e4c33445262b29fc0c5ced20f0fba81ab42e9bb +size 22958 diff --git a/assets-source/objects/egg/21.psd b/assets-source/objects/egg/21.psd new file mode 100644 index 0000000..923f068 --- /dev/null +++ b/assets-source/objects/egg/21.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe231877ba3456ee947e9380f74350dd7fae0e5834483227a0009a26a3eb2e25 +size 22956 diff --git a/assets-source/objects/egg/22.psd b/assets-source/objects/egg/22.psd new file mode 100644 index 0000000..1784f7a --- /dev/null +++ b/assets-source/objects/egg/22.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fedc537bee14949dbd63a7515d874ef6695d59f06c38b60a43dcd80c9cd59b2b +size 22950 diff --git a/assets-source/objects/egg/23.psd b/assets-source/objects/egg/23.psd new file mode 100644 index 0000000..f77606a --- /dev/null +++ b/assets-source/objects/egg/23.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0e082da30fd1dcf6bf7fbc31999fafd2669942c0f4b195266ed69a412b9fd48 +size 22954 diff --git a/assets-source/objects/egg/3.psd b/assets-source/objects/egg/3.psd new file mode 100644 index 0000000..fdbc154 --- /dev/null +++ b/assets-source/objects/egg/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a192962682c649d00869dc9c5fd90d4e63c430e49b7a4721e08c447b575ed94 +size 22970 diff --git a/assets-source/objects/egg/4.psd b/assets-source/objects/egg/4.psd new file mode 100644 index 0000000..1f429a6 --- /dev/null +++ b/assets-source/objects/egg/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36a6cd1b663b11fdb8613af9f1fe2ddee8753a584da8cc3bef7abf6ec3cd8fcf +size 22952 diff --git a/assets-source/objects/egg/5.psd b/assets-source/objects/egg/5.psd new file mode 100644 index 0000000..0947243 --- /dev/null +++ b/assets-source/objects/egg/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ea29c69ed31c1b1f77e29e44d4f86990aa710d9aecee647d0b8d57d922dbd0f +size 22940 diff --git a/assets-source/objects/egg/6.psd b/assets-source/objects/egg/6.psd new file mode 100644 index 0000000..466eb34 --- /dev/null +++ b/assets-source/objects/egg/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6b5224284e65c2e22e207e9b70867faf99681f8a10130e33fae0bbba82ea40c +size 22964 diff --git a/assets-source/objects/egg/7.psd b/assets-source/objects/egg/7.psd new file mode 100644 index 0000000..1501f84 --- /dev/null +++ b/assets-source/objects/egg/7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9efcc4337f94d906daf7d865e5bc1b888db95cd1328334c4ca04fa31ef4cd962 +size 22944 diff --git a/assets-source/objects/egg/8.psd b/assets-source/objects/egg/8.psd new file mode 100644 index 0000000..23e6086 --- /dev/null +++ b/assets-source/objects/egg/8.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81e04e3925d08635c632528ed3198cfe7b24e44925201c134c5bb37d176dfe36 +size 22952 diff --git a/assets-source/objects/egg/9.psd b/assets-source/objects/egg/9.psd new file mode 100644 index 0000000..4f957f7 --- /dev/null +++ b/assets-source/objects/egg/9.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2acea5bedc18bcc799883b232ba1bdeefdf73cb18d561b8eed373c645a108bb +size 22940 diff --git a/assets-source/objects/egg/palette_1_1.psd b/assets-source/objects/egg/palette_1_1.psd new file mode 100644 index 0000000..5728085 --- /dev/null +++ b/assets-source/objects/egg/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9be19a4fccdbad73fb08b8ed0a993ee614613da5fc5269c147a8bd11a805e4d0 +size 26114 diff --git a/assets-source/objects/egg/palette_2_1.psd b/assets-source/objects/egg/palette_2_1.psd new file mode 100644 index 0000000..6d6015e --- /dev/null +++ b/assets-source/objects/egg/palette_2_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43c3de56f929f3b473a1547615124ca3a05093f9047c34ec19a0fdd3a6aa4f33 +size 26140 diff --git a/assets-source/objects/egg/palette_3_3.psd b/assets-source/objects/egg/palette_3_3.psd new file mode 100644 index 0000000..4a01243 --- /dev/null +++ b/assets-source/objects/egg/palette_3_3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be9fee3d2c0f736a85eb5efb20fb2842c01086f0da16170e872f471f5cdf1419 +size 22950 diff --git a/assets-source/objects/egg/palette_4_3.psd b/assets-source/objects/egg/palette_4_3.psd new file mode 100644 index 0000000..df83bdd --- /dev/null +++ b/assets-source/objects/egg/palette_4_3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bff81a3cf9b320cd0430cf658d1e06bac568ad27051bd88088782ed3d296137 +size 22986 diff --git a/assets-source/objects/egg/palette_5_6.psd b/assets-source/objects/egg/palette_5_6.psd new file mode 100644 index 0000000..13a6850 --- /dev/null +++ b/assets-source/objects/egg/palette_5_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:890e3cd70df5f07777ca28aced2c44ae92c586ed73e57bfe842e3929003e6226 +size 22982 diff --git a/assets-source/objects/egg/palette_6_7.psd b/assets-source/objects/egg/palette_6_7.psd new file mode 100644 index 0000000..079acfb --- /dev/null +++ b/assets-source/objects/egg/palette_6_7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:377c2e638c089dd8085ababa870734b4156889277c44fe6c214b905ff1cf1ba0 +size 22986 diff --git a/assets-source/objects/egg/palette_7_8.psd b/assets-source/objects/egg/palette_7_8.psd new file mode 100644 index 0000000..34bb481 --- /dev/null +++ b/assets-source/objects/egg/palette_7_8.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc2fa76b779d8cc87a6bf7590b31fcb03997dee99dc7186a4b6baadacded419f +size 22980 diff --git a/assets-source/objects/egg/palette_8_5.psd b/assets-source/objects/egg/palette_8_5.psd new file mode 100644 index 0000000..b99270a --- /dev/null +++ b/assets-source/objects/egg/palette_8_5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e441f06dbda5151fbeb74ef718912f90aedd7d2046ed4a68876c15f6718ed19 +size 22980 diff --git a/assets-source/objects/egg_basket/1.psd b/assets-source/objects/egg_basket/1.psd new file mode 100644 index 0000000..621c2d9 --- /dev/null +++ b/assets-source/objects/egg_basket/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a20ae22abf915e17ecaad84bb525de5bb2ce09eb6cc1b09ee2ecbc459c017e7e +size 20840 diff --git a/assets-source/objects/egg_basket/2.psd b/assets-source/objects/egg_basket/2.psd new file mode 100644 index 0000000..a22f824 --- /dev/null +++ b/assets-source/objects/egg_basket/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337c24cee4d863d6d9b4f8eb70ba8ecead6b373faa9bd1b7664abb66c0de5707 +size 20858 diff --git a/assets-source/objects/egg_basket/3.psd b/assets-source/objects/egg_basket/3.psd new file mode 100644 index 0000000..3cb01e7 --- /dev/null +++ b/assets-source/objects/egg_basket/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3bd065fb1bfd995a3cc6272d4bb0339f36f3583702a084e637241769725cc20 +size 20866 diff --git a/assets-source/objects/egg_basket/4.psd b/assets-source/objects/egg_basket/4.psd new file mode 100644 index 0000000..907873b --- /dev/null +++ b/assets-source/objects/egg_basket/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04545e0793a8dc16fdc27177c980d2b6c8f38e0eb902a4094e1aacc5591c3576 +size 20828 diff --git a/assets-source/objects/fence/1.psd b/assets-source/objects/fence/1.psd new file mode 100644 index 0000000..6ee186f --- /dev/null +++ b/assets-source/objects/fence/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c41f3385d22653471c21d23fe20b760d4cf1a723b5ff354afced1f72739d0eb9 +size 24548 diff --git a/assets-source/objects/fence/2.psd b/assets-source/objects/fence/2.psd new file mode 100644 index 0000000..7c2ed64 --- /dev/null +++ b/assets-source/objects/fence/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46e61327aec3113ed602dd6fd4dd0d35c29d81c205b044251a1ce3a26a159725 +size 26010 diff --git a/assets-source/objects/fence/3.psd b/assets-source/objects/fence/3.psd new file mode 100644 index 0000000..dd436fe --- /dev/null +++ b/assets-source/objects/fence/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4262aad3f93f5fc510cf67cf41cf45d11c093a7170bcfaf09b0420619b6b7e31 +size 26870 diff --git a/assets-source/objects/fence_winter/1.psd b/assets-source/objects/fence_winter/1.psd new file mode 100644 index 0000000..f0d803c --- /dev/null +++ b/assets-source/objects/fence_winter/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8b8c3584cb41240d2c5c259b99a10ea299e22aacfd04d1a5ae4c8fb5c7ac77c +size 23681 diff --git a/assets-source/objects/fence_winter/2.psd b/assets-source/objects/fence_winter/2.psd new file mode 100644 index 0000000..bfb1930 --- /dev/null +++ b/assets-source/objects/fence_winter/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b3f3e610296b8ad0dd4959dab954a99968bf3077d1694306b8f195891cb0591 +size 25709 diff --git a/assets-source/objects/fence_winter/3.psd b/assets-source/objects/fence_winter/3.psd new file mode 100644 index 0000000..d344128 --- /dev/null +++ b/assets-source/objects/fence_winter/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1223d892673036abd2147d0292883ad4d6440b0463d68b6cdd8e7a07e5f5245 +size 27877 diff --git a/assets-source/objects/flower/1.psd b/assets-source/objects/flower/1.psd new file mode 100644 index 0000000..28c8444 --- /dev/null +++ b/assets-source/objects/flower/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c554a52fab91417670fdf35a8556abe4d144a5cb938c3f9c8ea938ee0c9fec5 +size 23746 diff --git a/assets-source/objects/flower/2.psd b/assets-source/objects/flower/2.psd new file mode 100644 index 0000000..99a8f25 --- /dev/null +++ b/assets-source/objects/flower/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:473cba467f74eeeb9c91b4b838f2504199c0bb800f560fa45156252b8215ef2b +size 23580 diff --git a/assets-source/objects/flower/3.psd b/assets-source/objects/flower/3.psd new file mode 100644 index 0000000..d780537 --- /dev/null +++ b/assets-source/objects/flower/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73bdfbfbc59d2bdfd505318b7fa6d10742b67db48819595d1b7c1e5f07a9f72f +size 23670 diff --git a/assets-source/objects/flower/patch1.psd b/assets-source/objects/flower/patch1.psd new file mode 100644 index 0000000..46b3060 --- /dev/null +++ b/assets-source/objects/flower/patch1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d4c0fe73f7360985b71a4776658f7e0965b41dd95ca5b919ae6bf377f45e30f +size 54790 diff --git a/assets-source/objects/flower/patch2.psd b/assets-source/objects/flower/patch2.psd new file mode 100644 index 0000000..ce66176 --- /dev/null +++ b/assets-source/objects/flower/patch2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbff9190e93ec8f0592cd95128e096f742ea515facb47d0205f8db7232e80dce +size 47411 diff --git a/assets-source/objects/flower/patch3.psd b/assets-source/objects/flower/patch3.psd new file mode 100644 index 0000000..34bff22 --- /dev/null +++ b/assets-source/objects/flower/patch3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:300e97c74bee053de880e2b7333112573a9cad0b77cab3337e50d6460d9dec2b +size 30676 diff --git a/assets-source/objects/flower/patch4.psd b/assets-source/objects/flower/patch4.psd new file mode 100644 index 0000000..55ca151 --- /dev/null +++ b/assets-source/objects/flower/patch4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67f663a237a5ba488703f13af1019c58aba8d610e4ffd46125fa35154f2bbd55 +size 30117 diff --git a/assets-source/objects/flower/patch5.psd b/assets-source/objects/flower/patch5.psd new file mode 100644 index 0000000..bb3ea5c --- /dev/null +++ b/assets-source/objects/flower/patch5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38adf468c721ab0ea5806202e33622ff29db5b1a105f6c0b03bcdc8edbb34c8a +size 26260 diff --git a/assets-source/objects/flower/patch6.psd b/assets-source/objects/flower/patch6.psd new file mode 100644 index 0000000..076bc2c --- /dev/null +++ b/assets-source/objects/flower/patch6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b417d4c13f413e7682092f9cca9cebebdd74ef2a6b3d0467a4b17404ba1e45e5 +size 26065 diff --git a/assets-source/objects/flower/patch7.psd b/assets-source/objects/flower/patch7.psd new file mode 100644 index 0000000..7e08e32 --- /dev/null +++ b/assets-source/objects/flower/patch7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8ebe24973ab73a97d3ffabd05d880a7d95cf134c1b752a845ffd4d4b11815c0 +size 25041 diff --git a/assets-source/objects/flower/pick.psd b/assets-source/objects/flower/pick.psd new file mode 100644 index 0000000..2231f6d --- /dev/null +++ b/assets-source/objects/flower/pick.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4745a460d37de100674b2762c693bd8f16d42e163b6934792a1f85a22eaa4d7a +size 22112 diff --git a/assets-source/objects/gift/1.psd b/assets-source/objects/gift/1.psd new file mode 100644 index 0000000..438db06 --- /dev/null +++ b/assets-source/objects/gift/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a57014cfb6a8c1ae3450bf37e7d237f522b7c873d4a5f485b34eaa69c0ab66d7 +size 23824 diff --git a/assets-source/objects/gift/2.psd b/assets-source/objects/gift/2.psd new file mode 100644 index 0000000..9a7f16f --- /dev/null +++ b/assets-source/objects/gift/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab3ec01c27f79b3cddfdd535cdec6b00a29425896bde9af0491b393b5055859f +size 24692 diff --git a/assets-source/objects/gift/palette_1_1.psd b/assets-source/objects/gift/palette_1_1.psd new file mode 100644 index 0000000..0ba5a31 --- /dev/null +++ b/assets-source/objects/gift/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec90010ca1464ac84b08fb27ae4cc88d2b1fe88c89a94463315edff0432296e1 +size 23818 diff --git a/assets-source/objects/giftpile/1.psd b/assets-source/objects/giftpile/1.psd new file mode 100644 index 0000000..0b72f71 --- /dev/null +++ b/assets-source/objects/giftpile/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2480bde28b6ebae01c10c0478a1e20808918ec50b27fe5c974bbb3fb85fa6d03 +size 38866 diff --git a/assets-source/objects/giftpile/2.psd b/assets-source/objects/giftpile/2.psd new file mode 100644 index 0000000..ff2eb03 --- /dev/null +++ b/assets-source/objects/giftpile/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9f47a3511370f69e2ccdf5b5facd458a1981ecb64583733f52dee215d39b7cb +size 41115 diff --git a/assets-source/objects/giftpile/3.psd b/assets-source/objects/giftpile/3.psd new file mode 100644 index 0000000..dc9b313 --- /dev/null +++ b/assets-source/objects/giftpile/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1b1f8cbeafcda16ad507772163aa26ccf90fcf1591439e9353e26ba5118c735 +size 38938 diff --git a/assets-source/objects/giftpile/4.psd b/assets-source/objects/giftpile/4.psd new file mode 100644 index 0000000..671a255 --- /dev/null +++ b/assets-source/objects/giftpile/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c80fe1674ce2c53a0d070934a63087333d86c3ca6d8064e1a6c78aeb580cfb0c +size 33683 diff --git a/assets-source/objects/giftpile/5.psd b/assets-source/objects/giftpile/5.psd new file mode 100644 index 0000000..4434e61 --- /dev/null +++ b/assets-source/objects/giftpile/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3847feb9bfdefaa39ae0699b476db49c6250a09193f33ae1cf8ec1bfc582175e +size 32213 diff --git a/assets-source/objects/giftpile/6.psd b/assets-source/objects/giftpile/6.psd new file mode 100644 index 0000000..735d7aa --- /dev/null +++ b/assets-source/objects/giftpile/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ed80a275e8b662cb4adaf632edbb866738e8c03e6df465b1ca19f6e773d9b0f +size 32827 diff --git a/assets-source/objects/giftpile/pine.psd b/assets-source/objects/giftpile/pine.psd new file mode 100644 index 0000000..5ec4f74 --- /dev/null +++ b/assets-source/objects/giftpile/pine.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc0e1cf4338d02b2c340f43eee21957e5d7dd50c17cb3fe068d3e0ca39a1164a +size 46817 diff --git a/assets-source/objects/giftpile/sign.psd b/assets-source/objects/giftpile/sign.psd new file mode 100644 index 0000000..5bde640 --- /dev/null +++ b/assets-source/objects/giftpile/sign.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9dbf1007dc5e2649a278fc63e51a307a990d3ac19509e16002aca645d5c97ef +size 59391 diff --git a/assets-source/objects/giftpile/tree.psd b/assets-source/objects/giftpile/tree.psd new file mode 100644 index 0000000..3ed8963 --- /dev/null +++ b/assets-source/objects/giftpile/tree.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b18a22b6d9910661d86b37a1c32eee1b476b89d5d2d0bbd6233f172eba4dfc29 +size 37514 diff --git a/assets-source/objects/gifts/1.psd b/assets-source/objects/gifts/1.psd new file mode 100644 index 0000000..551cd58 --- /dev/null +++ b/assets-source/objects/gifts/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:914a7872f3c989afaa1e7da622107a3d3bf328710a9ded793d7022126bcc2629 +size 24703 diff --git a/assets-source/objects/gifts/2.psd b/assets-source/objects/gifts/2.psd new file mode 100644 index 0000000..4921076 --- /dev/null +++ b/assets-source/objects/gifts/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fb259fd314e2612fd35213311f6eb12bf5d109a1329d22c1ab8245887672e2c +size 23879 diff --git a/assets-source/objects/gifts/3.psd b/assets-source/objects/gifts/3.psd new file mode 100644 index 0000000..153d897 --- /dev/null +++ b/assets-source/objects/gifts/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50116a06ca2947635b9095f81b3e65f1135b7dc900827aa738348deb154fc67d +size 23524 diff --git a/assets-source/objects/gifts/4.psd b/assets-source/objects/gifts/4.psd new file mode 100644 index 0000000..94a9044 --- /dev/null +++ b/assets-source/objects/gifts/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:070b4124dfba95619bc3e2efc3d8fd5165058e4dadfc4452a2f7b99404d9333f +size 24741 diff --git a/assets-source/objects/gifts/5.psd b/assets-source/objects/gifts/5.psd new file mode 100644 index 0000000..58b8eb9 --- /dev/null +++ b/assets-source/objects/gifts/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4459f21bdd248e4dfd35dad98757362ba8d7f6d44c2c07525e393b2cdf61e4f +size 23899 diff --git a/assets-source/objects/gifts/6.psd b/assets-source/objects/gifts/6.psd new file mode 100644 index 0000000..475fa2b --- /dev/null +++ b/assets-source/objects/gifts/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f20d53ba3958185fe1a6bb8691ef2e13cb421a7c1bcb73d6f61c52ad2c954a7a +size 23510 diff --git a/assets-source/objects/gifts/palette_1_1.psd b/assets-source/objects/gifts/palette_1_1.psd new file mode 100644 index 0000000..7e086b8 --- /dev/null +++ b/assets-source/objects/gifts/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d2d9404fac998df7e64afb0990709b84f42e7dde8773bfe78fc1c45bd544d45 +size 24708 diff --git a/assets-source/objects/gifts/palette_2_1.psd b/assets-source/objects/gifts/palette_2_1.psd new file mode 100644 index 0000000..a151be3 --- /dev/null +++ b/assets-source/objects/gifts/palette_2_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97a12514b61ef7a607dd1183a381e6c60c047bd65c1ae3d438bb25f6b23f2a18 +size 27836 diff --git a/assets-source/objects/gifts/palette_3_1.psd b/assets-source/objects/gifts/palette_3_1.psd new file mode 100644 index 0000000..44d6cb9 --- /dev/null +++ b/assets-source/objects/gifts/palette_3_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd72e82a639c2ce3a068e2f4b887dab1d1564ba62615a55004da59e7feef3254 +size 24721 diff --git a/assets-source/objects/grapes/1.psd b/assets-source/objects/grapes/1.psd new file mode 100644 index 0000000..0b65e4b --- /dev/null +++ b/assets-source/objects/grapes/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a26139c2660870834f6f90fe0ce0cd984fa05cd04aa9829d86132435e8859937 +size 23864 diff --git a/assets-source/objects/grapes/2.psd b/assets-source/objects/grapes/2.psd new file mode 100644 index 0000000..864fb6e --- /dev/null +++ b/assets-source/objects/grapes/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb27d108963d5a173843c5bf3a7e62826c25855296fd1d1f870ab0664500c466 +size 27062 diff --git a/assets-source/objects/grapes/3.psd b/assets-source/objects/grapes/3.psd new file mode 100644 index 0000000..34eb7f4 --- /dev/null +++ b/assets-source/objects/grapes/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5f7b2ac29a4de4c3abcaadeee8769dfc996f5359b1639db01a75cc7449a3e5 +size 23844 diff --git a/assets-source/objects/grapes/4.psd b/assets-source/objects/grapes/4.psd new file mode 100644 index 0000000..58a4a0d --- /dev/null +++ b/assets-source/objects/grapes/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cd468d39e3532c202894c18fd48c119c2e744541501f42d07664c88b0558ee2 +size 23842 diff --git a/assets-source/objects/grapes/5.psd b/assets-source/objects/grapes/5.psd new file mode 100644 index 0000000..46d0969 --- /dev/null +++ b/assets-source/objects/grapes/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fd9930899005006442f463f475f2c55a7c9915fb1cf8a1e445f55042b41cabb +size 23844 diff --git a/assets-source/objects/grapes/6.psd b/assets-source/objects/grapes/6.psd new file mode 100644 index 0000000..da07cb0 --- /dev/null +++ b/assets-source/objects/grapes/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35230644b3f564a8ba43f0f4bf672c00a160fb4d5bfcbc4c1e5e3d86b9b23e43 +size 23780 diff --git a/assets-source/objects/grapes/7.psd b/assets-source/objects/grapes/7.psd new file mode 100644 index 0000000..5e06b6f --- /dev/null +++ b/assets-source/objects/grapes/7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9f21c71583a9720f4c149362bbf840bf86510a445871d49d9243958152b1f27 +size 23776 diff --git a/assets-source/objects/grapes/one.psd b/assets-source/objects/grapes/one.psd new file mode 100644 index 0000000..dd871bf --- /dev/null +++ b/assets-source/objects/grapes/one.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93befba752c5a96f0744991d20bbdf28144807417b55c5b769790899712fc261 +size 22044 diff --git a/assets-source/objects/grapes/palette_1_1.psd b/assets-source/objects/grapes/palette_1_1.psd new file mode 100644 index 0000000..f808a82 --- /dev/null +++ b/assets-source/objects/grapes/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1190bba6f707da86aeeed50bc2977d5d28a64a0f5423c1f804353eb93aa0f613 +size 22992 diff --git a/assets-source/objects/grass_tile.psd b/assets-source/objects/grass_tile.psd new file mode 100644 index 0000000..b27d474 --- /dev/null +++ b/assets-source/objects/grass_tile.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed02f6813247880c22ae5808607216fa19a39f22acd8798c653e13e75e600cc2 +size 23301 diff --git a/assets-source/objects/hammer.psd b/assets-source/objects/hammer.psd new file mode 100644 index 0000000..08ca787 --- /dev/null +++ b/assets-source/objects/hammer.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:124c9dac91b2147e89fe1bd08dc906a1b5de29830705a18633e9260b5cef6bbd +size 22630 diff --git a/assets-source/objects/holly.psd b/assets-source/objects/holly.psd new file mode 100644 index 0000000..7f52ab2 --- /dev/null +++ b/assets-source/objects/holly.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9c6462a73397a52c06c8f69c4604e580fcec13cfa7c24ad42d74dc2db7941d +size 21520 diff --git a/assets-source/objects/house.psd b/assets-source/objects/house.psd new file mode 100644 index 0000000..eb1aa78 --- /dev/null +++ b/assets-source/objects/house.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c61eac2640a468f88c41fd78c5411b97ba448d16b8d144677081cb45d5703ce8 +size 85944 diff --git a/assets-source/objects/jacko_lantern/off.psd b/assets-source/objects/jacko_lantern/off.psd new file mode 100644 index 0000000..37a7e35 --- /dev/null +++ b/assets-source/objects/jacko_lantern/off.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad594d836ef40f889a319fe39fdf1994941d24d08f8f0abd2faa60c9601e81d1 +size 23268 diff --git a/assets-source/objects/jacko_lantern/on.psd b/assets-source/objects/jacko_lantern/on.psd new file mode 100644 index 0000000..0e3bdfa --- /dev/null +++ b/assets-source/objects/jacko_lantern/on.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7c2f67e5cddc1b88d9c1f069c18c47477fb40fd19348bdc46c9261b4c8ff50a +size 23444 diff --git a/assets-source/objects/large_leafed_bush/1.psd b/assets-source/objects/large_leafed_bush/1.psd new file mode 100644 index 0000000..17321fb --- /dev/null +++ b/assets-source/objects/large_leafed_bush/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1a5fbe009f8f10fe07d5e74c05471a63f8885c57d5ea67fa73375fd94aa7e17 +size 32454 diff --git a/assets-source/objects/large_leafed_bush/2.psd b/assets-source/objects/large_leafed_bush/2.psd new file mode 100644 index 0000000..fb68db3 --- /dev/null +++ b/assets-source/objects/large_leafed_bush/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9831d0bda89d4018ca820802241a0c494d5ef616afacab634d331e62fa736995 +size 33308 diff --git a/assets-source/objects/large_leafed_bush/3.psd b/assets-source/objects/large_leafed_bush/3.psd new file mode 100644 index 0000000..35e01fa --- /dev/null +++ b/assets-source/objects/large_leafed_bush/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6adc736e126ca4277d2eed887f35bea325cd15938613cf60a829357d2eca1ec +size 29949 diff --git a/assets-source/objects/large_leafed_bush/4.psd b/assets-source/objects/large_leafed_bush/4.psd new file mode 100644 index 0000000..e37b56b --- /dev/null +++ b/assets-source/objects/large_leafed_bush/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da9d81adec3ed404ce9687c38b85bce6729fa481078a842929414813be3529e6 +size 29949 diff --git a/assets-source/objects/large_leafed_bush/palette_1_1.psd b/assets-source/objects/large_leafed_bush/palette_1_1.psd new file mode 100644 index 0000000..ed99231 --- /dev/null +++ b/assets-source/objects/large_leafed_bush/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:695962a979d8ae1564995c692eee057c36a6ed84bbc0e96f348a3a4a36e2b31e +size 33000 diff --git a/assets-source/objects/leaf/1.psd b/assets-source/objects/leaf/1.psd new file mode 100644 index 0000000..4afe1ae --- /dev/null +++ b/assets-source/objects/leaf/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bbf9bbbc1d468f8600bb4ada38ef5f820aa4488f60e4f44e8e5b91312413642 +size 23364 diff --git a/assets-source/objects/leaf/2.psd b/assets-source/objects/leaf/2.psd new file mode 100644 index 0000000..ed2754d --- /dev/null +++ b/assets-source/objects/leaf/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47dfc74c247ff8402b225514e2cc89a5fd155711f2175323c1dfd7228c0e732c +size 23384 diff --git a/assets-source/objects/leaf/3.psd b/assets-source/objects/leaf/3.psd new file mode 100644 index 0000000..414d50c --- /dev/null +++ b/assets-source/objects/leaf/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eab677272b0abffa7d3750807aedfffb6efe64f49e75b4ddfdb1653d312af224 +size 23390 diff --git a/assets-source/objects/leaf/4.psd b/assets-source/objects/leaf/4.psd new file mode 100644 index 0000000..5de9cd8 --- /dev/null +++ b/assets-source/objects/leaf/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45cc4ef5daa2e65eec8263bb85ba3cb2db0706bf0b571138ef04647e3795a74b +size 23388 diff --git a/assets-source/objects/leaf/5.psd b/assets-source/objects/leaf/5.psd new file mode 100644 index 0000000..83d9f34 --- /dev/null +++ b/assets-source/objects/leaf/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:615d545210a2cf983702167a5d728628b6cf41f4a147f8cb20e7ad2ff9b3eed6 +size 23284 diff --git a/assets-source/objects/leaf/6.psd b/assets-source/objects/leaf/6.psd new file mode 100644 index 0000000..e7937f3 --- /dev/null +++ b/assets-source/objects/leaf/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85407cdcb48570d615923e3e4b0e6676d46684597f965699d85126ebb00c1ca8 +size 23284 diff --git a/assets-source/objects/leaf/palette_1_6.psd b/assets-source/objects/leaf/palette_1_6.psd new file mode 100644 index 0000000..485cc37 --- /dev/null +++ b/assets-source/objects/leaf/palette_1_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b4e094a989ecb99a1e6bde7c0bcd11c0d86d2b1c4c6114546f23895818ee9ce +size 23282 diff --git a/assets-source/objects/leaf/palette_2_6.psd b/assets-source/objects/leaf/palette_2_6.psd new file mode 100644 index 0000000..3472814 --- /dev/null +++ b/assets-source/objects/leaf/palette_2_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3520984505318801d3b0d7ebd4c6cb4f0e42dc2d89b6a8d7af4b2d7032ad72df +size 23286 diff --git a/assets-source/objects/leafpile/big.psd b/assets-source/objects/leafpile/big.psd new file mode 100644 index 0000000..5d2331c --- /dev/null +++ b/assets-source/objects/leafpile/big.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db1109992094513e616b9461e7f4856f72a7d48de261e16a00e0988ed7affeb7 +size 39202 diff --git a/assets-source/objects/leafpile/bigstick.psd b/assets-source/objects/leafpile/bigstick.psd new file mode 100644 index 0000000..e9d30e1 --- /dev/null +++ b/assets-source/objects/leafpile/bigstick.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5a9b90da6be2c2dc1b625be655d9f73c10d080a60ef109decd1869b309e7da1 +size 39839 diff --git a/assets-source/objects/leafpile/medium.psd b/assets-source/objects/leafpile/medium.psd new file mode 100644 index 0000000..99446e6 --- /dev/null +++ b/assets-source/objects/leafpile/medium.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:207d590bee3c1ac2cf96cf35a48c3d4f078ebe7d011eceb6c44e0b51981b6048 +size 31617 diff --git a/assets-source/objects/leafpile/mediumalt.psd b/assets-source/objects/leafpile/mediumalt.psd new file mode 100644 index 0000000..16e5c70 --- /dev/null +++ b/assets-source/objects/leafpile/mediumalt.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd723bcca5aa9ae1864596018856f9a873f6339b1e193c659cdd50991fefafd8 +size 30956 diff --git a/assets-source/objects/leafpile/palette_1_stick.psd b/assets-source/objects/leafpile/palette_1_stick.psd new file mode 100644 index 0000000..16533f5 --- /dev/null +++ b/assets-source/objects/leafpile/palette_1_stick.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10b064f1e7ba3bb3088e854436965b6d11fb204109bec9923fcc943f1f384ac8 +size 27692 diff --git a/assets-source/objects/leafpile/palette_2_stick.psd b/assets-source/objects/leafpile/palette_2_stick.psd new file mode 100644 index 0000000..acaf8fd --- /dev/null +++ b/assets-source/objects/leafpile/palette_2_stick.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e34efd62d40a7a08b6aa9cab534f1fd1bb00993f2f722e021f2cb3a49b0d266 +size 27494 diff --git a/assets-source/objects/leafpile/small.psd b/assets-source/objects/leafpile/small.psd new file mode 100644 index 0000000..33d0f96 --- /dev/null +++ b/assets-source/objects/leafpile/small.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e472ba9b4e33c6b2368e119e5ba21724605359c1827067f27bd0dc72cd4f2122 +size 27300 diff --git a/assets-source/objects/leafpile/stick.psd b/assets-source/objects/leafpile/stick.psd new file mode 100644 index 0000000..6827506 --- /dev/null +++ b/assets-source/objects/leafpile/stick.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bdbd299ebcdc5427b5329aabb3f81e6b1ad24961b92de16da36fa0f100a0ebb +size 27758 diff --git a/assets-source/objects/leaves/1.psd b/assets-source/objects/leaves/1.psd new file mode 100644 index 0000000..fda01cf --- /dev/null +++ b/assets-source/objects/leaves/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:262873ae48f8e201ab860512b9de8495fb1d15098b09cc5728c099219db184c8 +size 23962 diff --git a/assets-source/objects/leaves/2.psd b/assets-source/objects/leaves/2.psd new file mode 100644 index 0000000..3fce726 --- /dev/null +++ b/assets-source/objects/leaves/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c0a8a60d3c8c2a2610102e135c047a8f2bf4717c0bbe4204c95fd965f8b9887 +size 24963 diff --git a/assets-source/objects/leaves/3.psd b/assets-source/objects/leaves/3.psd new file mode 100644 index 0000000..9961da0 --- /dev/null +++ b/assets-source/objects/leaves/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d56f88ea00d01469926c421cdb5765790bbc569513e22dea24774ff042ea43e +size 24448 diff --git a/assets-source/objects/leaves/4.psd b/assets-source/objects/leaves/4.psd new file mode 100644 index 0000000..60f5c65 --- /dev/null +++ b/assets-source/objects/leaves/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a2c574ef4460018d660fca89ad870b192d252f9c12b8ba7cd5daab682a86cf +size 26814 diff --git a/assets-source/objects/leaves/5.psd b/assets-source/objects/leaves/5.psd new file mode 100644 index 0000000..01de840 --- /dev/null +++ b/assets-source/objects/leaves/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f032ed5b4c5b31b20941b7cc55799b77d123523d4e2eea413d9940213760909 +size 27714 diff --git a/assets-source/objects/lemon/1.psd b/assets-source/objects/lemon/1.psd new file mode 100644 index 0000000..ae171a0 --- /dev/null +++ b/assets-source/objects/lemon/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b75687ecbdc6a0e7e388efacfa4843fb7c35dcba38b86768a3b00a3bbac3dfd9 +size 21912 diff --git a/assets-source/objects/lemon/palette_1_1.psd b/assets-source/objects/lemon/palette_1_1.psd new file mode 100644 index 0000000..7ffa1a3 --- /dev/null +++ b/assets-source/objects/lemon/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24645806d3f5dadade654b3d5002e1f45812d4305259da0c50db00324ccdf53a +size 22786 diff --git a/assets-source/objects/letter.psd b/assets-source/objects/letter.psd new file mode 100644 index 0000000..de33344 --- /dev/null +++ b/assets-source/objects/letter.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:747f335ece8c0997267a8e00dc1c8971f26c53099b9f7907882d7d20b50ebaea +size 24638 diff --git a/assets-source/objects/mango.psd b/assets-source/objects/mango.psd new file mode 100644 index 0000000..ee04aaa --- /dev/null +++ b/assets-source/objects/mango.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:015de74032fd7dc9bdb81b2f720fbac89c100777e3801bd3ec34f0fb9ec8948e +size 22180 diff --git a/assets-source/objects/mine_cart.psd b/assets-source/objects/mine_cart.psd new file mode 100644 index 0000000..b5304f8 --- /dev/null +++ b/assets-source/objects/mine_cart.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b300d5692739b6ca84fa694847a281661406d120456c0b7a14d1f0ad3939d190 +size 32552 diff --git a/assets-source/objects/mine_cart_back.psd b/assets-source/objects/mine_cart_back.psd new file mode 100644 index 0000000..6247eed --- /dev/null +++ b/assets-source/objects/mine_cart_back.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d4f0f0ce04f018c643d2489bcf8916134c3918e80545cdee940af8fbdff2292 +size 25408 diff --git a/assets-source/objects/mine_cart_front.psd b/assets-source/objects/mine_cart_front.psd new file mode 100644 index 0000000..d88d74d --- /dev/null +++ b/assets-source/objects/mine_cart_front.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56342bacca6a84c88014f95b986a701c6a42e1b3b6a62f9d15a77f5f66966fb5 +size 31221 diff --git a/assets-source/objects/mine_closed.psd b/assets-source/objects/mine_closed.psd new file mode 100644 index 0000000..4433565 --- /dev/null +++ b/assets-source/objects/mine_closed.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac5d6e95b20b44b6f4023ce024f5cb93795fd090e8843270b1f24522da51ceb2 +size 27579 diff --git a/assets-source/objects/mine_entrance.psd b/assets-source/objects/mine_entrance.psd new file mode 100644 index 0000000..4f6e364 --- /dev/null +++ b/assets-source/objects/mine_entrance.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e35137484d5d5ed555e63c4ec93219bcb34600f6b0ff86f987dc29b061333fa +size 37194 diff --git a/assets-source/objects/mine_rail_fade_up.psd b/assets-source/objects/mine_rail_fade_up.psd new file mode 100644 index 0000000..c7589d1 --- /dev/null +++ b/assets-source/objects/mine_rail_fade_up.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a5c0a222e109d3c2f43bff0da54b3bb955c1abfab3e30cc79890d32e44cc709 +size 27408 diff --git a/assets-source/objects/mine_rails/end_left.psd b/assets-source/objects/mine_rails/end_left.psd new file mode 100644 index 0000000..8dfb6a1 --- /dev/null +++ b/assets-source/objects/mine_rails/end_left.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9debd451b675265226ff888abf08b7f60a6b8c0805793a7d3d97c02780bad201 +size 31001 diff --git a/assets-source/objects/mine_rails/end_right.psd b/assets-source/objects/mine_rails/end_right.psd new file mode 100644 index 0000000..5c0b4c9 --- /dev/null +++ b/assets-source/objects/mine_rails/end_right.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcc901804e77361566dfcb923bc3c27ea1498ab722e555c6523b4b6d21c5dc1d +size 30469 diff --git a/assets-source/objects/mine_rails/end_top.psd b/assets-source/objects/mine_rails/end_top.psd new file mode 100644 index 0000000..3e4d09d --- /dev/null +++ b/assets-source/objects/mine_rails/end_top.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21da209a2aeae5b18a831db2851446f6ef7c33b8a58751e165e4534c878600de +size 25889 diff --git a/assets-source/objects/mine_rails/h.psd b/assets-source/objects/mine_rails/h.psd new file mode 100644 index 0000000..b2ab7a7 --- /dev/null +++ b/assets-source/objects/mine_rails/h.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65752758af9fdb4c72f84d537fbd42d7ec076a4c61bc81196b47db20c62e0cbe +size 24996 diff --git a/assets-source/objects/mine_rails/ne.psd b/assets-source/objects/mine_rails/ne.psd new file mode 100644 index 0000000..46e628d --- /dev/null +++ b/assets-source/objects/mine_rails/ne.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22e603555397612cefa6b7b686f7b5daae08145832695bc7db2ca1faaf25c37e +size 27183 diff --git a/assets-source/objects/mine_rails/nse.psd b/assets-source/objects/mine_rails/nse.psd new file mode 100644 index 0000000..3d65635 --- /dev/null +++ b/assets-source/objects/mine_rails/nse.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d7933cfb7e3472a1cda6c20873c223db206a9c2e10fff6e6f5dcaf0474a2d48 +size 26898 diff --git a/assets-source/objects/mine_rails/nsw.psd b/assets-source/objects/mine_rails/nsw.psd new file mode 100644 index 0000000..1e8db38 --- /dev/null +++ b/assets-source/objects/mine_rails/nsw.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8095f5e67f11a2b64a66573e2845c3b9571353cdb90f7eca364f09402f6e4f6 +size 26822 diff --git a/assets-source/objects/mine_rails/nw.psd b/assets-source/objects/mine_rails/nw.psd new file mode 100644 index 0000000..8337470 --- /dev/null +++ b/assets-source/objects/mine_rails/nw.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1afc871a62f3b14fb8786d7c865729b7de7b543bfaca6a10812ce64d88dbe74 +size 27079 diff --git a/assets-source/objects/mine_rails/nwe.psd b/assets-source/objects/mine_rails/nwe.psd new file mode 100644 index 0000000..87fc42b --- /dev/null +++ b/assets-source/objects/mine_rails/nwe.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6758614719a0e8d8541b5d29b777e048cd411898e0b18a9ab4277518972bc5fa +size 26831 diff --git a/assets-source/objects/mine_rails/se.psd b/assets-source/objects/mine_rails/se.psd new file mode 100644 index 0000000..ff0fd38 --- /dev/null +++ b/assets-source/objects/mine_rails/se.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13bab5e059fcfda1067d373c33c46ebe3f5c66d3c2efe64bf6c5c259e4a0d852 +size 30241 diff --git a/assets-source/objects/mine_rails/sw.psd b/assets-source/objects/mine_rails/sw.psd new file mode 100644 index 0000000..05fe0f6 --- /dev/null +++ b/assets-source/objects/mine_rails/sw.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c34809ca81a203070c0a4e17c2c230b6a26b08fddb9c1dc0ae242eccda10136 +size 26559 diff --git a/assets-source/objects/mine_rails/swe.psd b/assets-source/objects/mine_rails/swe.psd new file mode 100644 index 0000000..c2c9cc6 --- /dev/null +++ b/assets-source/objects/mine_rails/swe.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17dda6c86acd4df9e8f99b20797e2570ab418b501aaccd4531565b964248bfbe +size 26315 diff --git a/assets-source/objects/mine_rails/v.psd b/assets-source/objects/mine_rails/v.psd new file mode 100644 index 0000000..f5e84bb --- /dev/null +++ b/assets-source/objects/mine_rails/v.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b70530f6ce8e809bee295c9b82bdd72046c5f8dbadb0035d00065c1390e1f0fa +size 27512 diff --git a/assets-source/objects/mistletoe.psd b/assets-source/objects/mistletoe.psd new file mode 100644 index 0000000..c74e1bd --- /dev/null +++ b/assets-source/objects/mistletoe.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a15cbe850e18e2a0afd2fba250873e635d6b925c91e754c29ee3d605addda29f +size 22060 diff --git a/assets-source/objects/orange/1.psd b/assets-source/objects/orange/1.psd new file mode 100644 index 0000000..dfd459f --- /dev/null +++ b/assets-source/objects/orange/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c391faa8d5d97a6505a7a1db8d0bfba9aa05d49cd46ca0a35703bca3bcbacb6 +size 23182 diff --git a/assets-source/objects/orange/2.psd b/assets-source/objects/orange/2.psd new file mode 100644 index 0000000..4805af6 --- /dev/null +++ b/assets-source/objects/orange/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef4851c54ba115835021558eada284c12409668d0eb0e6f8644d28d26e557a8e +size 26104 diff --git a/assets-source/objects/pear.psd b/assets-source/objects/pear.psd new file mode 100644 index 0000000..a4be123 --- /dev/null +++ b/assets-source/objects/pear.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:190c9eacd0e85f4d1e1d7d9fbb165741919ae6fc65a93e74e769b73d4b2254fa +size 24907 diff --git a/assets-source/objects/pickaxe.psd b/assets-source/objects/pickaxe.psd new file mode 100644 index 0000000..9ac5698 --- /dev/null +++ b/assets-source/objects/pickaxe.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31e2dc57d782c6a18209d8008d3c3481d89ef835131a04e397a574d90734d00e +size 22420 diff --git a/assets-source/objects/picture/1.psd b/assets-source/objects/picture/1.psd new file mode 100644 index 0000000..8f4af17 --- /dev/null +++ b/assets-source/objects/picture/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dc58fc9c3fab7c22f778e5240e0a1a045924860bf03eb509207c056cc1ec44b +size 26464 diff --git a/assets-source/objects/picture/palette_1_1.psd b/assets-source/objects/picture/palette_1_1.psd new file mode 100644 index 0000000..69a2ddb --- /dev/null +++ b/assets-source/objects/picture/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df99f799115dd7080c75e6c293eac014628c33f7aa3cefb214902fb2c18cb61e +size 27494 diff --git a/assets-source/objects/plank/1.psd b/assets-source/objects/plank/1.psd new file mode 100644 index 0000000..bdc201d --- /dev/null +++ b/assets-source/objects/plank/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77d3dc1e49fabad5456b256a93f5eca57e44a497798959ac982bdc0fc56eec37 +size 23433 diff --git a/assets-source/objects/plank/2.psd b/assets-source/objects/plank/2.psd new file mode 100644 index 0000000..10c0dfc --- /dev/null +++ b/assets-source/objects/plank/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44f92211df4e5ee27156d8fa8e2877809bea248a10f2c23567d38c6541e74a8e +size 23842 diff --git a/assets-source/objects/plank/3.psd b/assets-source/objects/plank/3.psd new file mode 100644 index 0000000..203e1f8 --- /dev/null +++ b/assets-source/objects/plank/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e210926bc5f9d613d2663b1c3d3a3dd03b0299bef5bc3554c7aae9ab941a942 +size 23978 diff --git a/assets-source/objects/plank/4.psd b/assets-source/objects/plank/4.psd new file mode 100644 index 0000000..76a6acf --- /dev/null +++ b/assets-source/objects/plank/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77d69c88d8970055c68726eabe3e2ae1aee2e01e86f34a9090dbafe4c7fd842 +size 26866 diff --git a/assets-source/objects/plank/palette_1_1.psd b/assets-source/objects/plank/palette_1_1.psd new file mode 100644 index 0000000..7338edf --- /dev/null +++ b/assets-source/objects/plank/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df586af2c4fe5b53b579d20f41ed871ff6552f45663ac86b5c8ee98a7812ed7a +size 26983 diff --git a/assets-source/objects/plank/shadow.psd b/assets-source/objects/plank/shadow.psd new file mode 100644 index 0000000..b7a84ef --- /dev/null +++ b/assets-source/objects/plank/shadow.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fcfdffabbe117686ff5f1f42dd8c0c5a94d4e895c617e1a2ced24148f08200e +size 22508 diff --git a/assets-source/objects/plank/shadow2.psd b/assets-source/objects/plank/shadow2.psd new file mode 100644 index 0000000..b45ead6 --- /dev/null +++ b/assets-source/objects/plank/shadow2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a60850db67ec26add6d16fb366d09d0367993ff7178dd470b9c12d161154308f +size 22574 diff --git a/assets-source/objects/plank/shadow_short.psd b/assets-source/objects/plank/shadow_short.psd new file mode 100644 index 0000000..f9525d8 --- /dev/null +++ b/assets-source/objects/plank/shadow_short.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:449a5e54c39e4e9060ef78be266e27660efdc95195b6ac5da3791f770f89d600 +size 22556 diff --git a/assets-source/objects/plank/short_1.psd b/assets-source/objects/plank/short_1.psd new file mode 100644 index 0000000..b88205a --- /dev/null +++ b/assets-source/objects/plank/short_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a2534cb9cd7c9d2454c64a786f2290080093f64e826842595948dcc57c11887 +size 23427 diff --git a/assets-source/objects/plank/short_2.psd b/assets-source/objects/plank/short_2.psd new file mode 100644 index 0000000..bebef5b --- /dev/null +++ b/assets-source/objects/plank/short_2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07e8b6147e9022df13830a3f018af8069c3c981e25075546788213e5b5ddd286 +size 23519 diff --git a/assets-source/objects/plank/short_3.psd b/assets-source/objects/plank/short_3.psd new file mode 100644 index 0000000..ce9c737 --- /dev/null +++ b/assets-source/objects/plank/short_3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e85c191a93aa42c05f5da51a2411e99328798313b5ece6d2076cf8d7c131457 +size 23253 diff --git a/assets-source/objects/pumpkin/default.psd b/assets-source/objects/pumpkin/default.psd new file mode 100644 index 0000000..c6dfa25 --- /dev/null +++ b/assets-source/objects/pumpkin/default.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:978daa96b6aba0f3ecbdec9465e4c19591e1882b1286059284c3b21737616a8b +size 24579 diff --git a/assets-source/objects/pumpkin/off.psd b/assets-source/objects/pumpkin/off.psd new file mode 100644 index 0000000..8da7287 --- /dev/null +++ b/assets-source/objects/pumpkin/off.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce7e8854ec06ca39a0f27e9deda1115b2e8838cb1e2891ab772070aab29d762a +size 21491 diff --git a/assets-source/objects/pumpkin/on.psd b/assets-source/objects/pumpkin/on.psd new file mode 100644 index 0000000..dd9a2cf --- /dev/null +++ b/assets-source/objects/pumpkin/on.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cb7cb00cfc1bdc3446c3a4ce5721739f79a34f84e1333bebe9777e0c3ed0eaf +size 21606 diff --git a/assets-source/objects/rainfall.psd b/assets-source/objects/rainfall.psd new file mode 100644 index 0000000..17e1193 --- /dev/null +++ b/assets-source/objects/rainfall.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4de08479d12e45d2aae9ae5c3604b9c56a2019c575659adbd4ee738e73caaebf +size 41130 diff --git a/assets-source/objects/rake.psd b/assets-source/objects/rake.psd new file mode 100644 index 0000000..61ce853 --- /dev/null +++ b/assets-source/objects/rake.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e175285e7ac0a3a5d0a2e299add600ebf70bee3437ee97f53d3346dc90421b3 +size 22667 diff --git a/assets-source/objects/rock/1.psd b/assets-source/objects/rock/1.psd new file mode 100644 index 0000000..7720687 --- /dev/null +++ b/assets-source/objects/rock/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2ca7ffc681dfba13a7dc35004b35128815e61415eafc1abd5b22bc78353a812 +size 26788 diff --git a/assets-source/objects/rock/2.psd b/assets-source/objects/rock/2.psd new file mode 100644 index 0000000..3f25fe1 --- /dev/null +++ b/assets-source/objects/rock/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d617c51368ddb85562907c9aa7c34c6bcb65d281fae307867a30b018db5a45b5 +size 24252 diff --git a/assets-source/objects/rock/3.psd b/assets-source/objects/rock/3.psd new file mode 100644 index 0000000..0875207 --- /dev/null +++ b/assets-source/objects/rock/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0790f67d67c92a880aa7509de818bd5ad9b78c1d72dd986176bd51246b557a5 +size 27341 diff --git a/assets-source/objects/rock/palette_1_1.psd b/assets-source/objects/rock/palette_1_1.psd new file mode 100644 index 0000000..1830fcb --- /dev/null +++ b/assets-source/objects/rock/palette_1_1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffa270dec3ea9d350c723c1024ae8bd452334fa537fb7016fd1b0f7df926bd6e +size 24804 diff --git a/assets-source/objects/rope_rack.psd b/assets-source/objects/rope_rack.psd new file mode 100644 index 0000000..79af216 --- /dev/null +++ b/assets-source/objects/rope_rack.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5338dac815ed20d3f3f40f8e41cc0e2e11ec3eea90a25694644da6a230b5f356 +size 25402 diff --git a/assets-source/objects/rose.psd b/assets-source/objects/rose.psd new file mode 100644 index 0000000..806f2f3 --- /dev/null +++ b/assets-source/objects/rose.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0c9c761c3a828785ae93f413fe8f1a0203afd4358125fb562aa71e4e389629c +size 23388 diff --git a/assets-source/objects/route_pole.psd b/assets-source/objects/route_pole.psd new file mode 100644 index 0000000..527b21c --- /dev/null +++ b/assets-source/objects/route_pole.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84df4894d8119ac0a80231a9ee191dd38efd0d0fc965024547b93bcc91c93cd7 +size 22836 diff --git a/assets-source/objects/saw.psd b/assets-source/objects/saw.psd new file mode 100644 index 0000000..18c6969 --- /dev/null +++ b/assets-source/objects/saw.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df3ccca9a2ef2797c84231bd1d7d1f4c36571df93e9eaaa31c4435aa4be180cd +size 22099 diff --git a/assets-source/objects/shovel.psd b/assets-source/objects/shovel.psd new file mode 100644 index 0000000..c174ced --- /dev/null +++ b/assets-source/objects/shovel.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db3cc11b3cc5b2939f0ca0e5816c7ec04599d9a33dc3d8146eb9b985c5482ddb +size 22773 diff --git a/assets-source/objects/sign/1.psd b/assets-source/objects/sign/1.psd new file mode 100644 index 0000000..f1f8e52 --- /dev/null +++ b/assets-source/objects/sign/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2612c2a2a6c505038df0c9df2fbc750e38f6b29b44601da703856458f40e4050 +size 21859 diff --git a/assets-source/objects/sign/2.psd b/assets-source/objects/sign/2.psd new file mode 100644 index 0000000..babed21 --- /dev/null +++ b/assets-source/objects/sign/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc254cf295ca19a97b3bea38700aaf993ae35e3b1102d4c2568da6b4444ed992 +size 22012 diff --git a/assets-source/objects/sign/3.psd b/assets-source/objects/sign/3.psd new file mode 100644 index 0000000..8bd3a5d --- /dev/null +++ b/assets-source/objects/sign/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2636bf42a6cd3d688367bc3200f66ab6a96e266921f80281c6010630ead35458 +size 25148 diff --git a/assets-source/objects/sign/4.psd b/assets-source/objects/sign/4.psd new file mode 100644 index 0000000..37ec33c --- /dev/null +++ b/assets-source/objects/sign/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13793aea443a7e7e10df373e485c4640f527a37ece6e2f09ab251a1c9d28ee0b +size 22088 diff --git a/assets-source/objects/sign/blank.psd b/assets-source/objects/sign/blank.psd new file mode 100644 index 0000000..91ced50 --- /dev/null +++ b/assets-source/objects/sign/blank.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7eb13a5402936210f6647b2d979fd083794be9da7d2020578dde5d0e685ec5bb +size 21804 diff --git a/assets-source/objects/sign_winter.psd b/assets-source/objects/sign_winter.psd new file mode 100644 index 0000000..00114e7 --- /dev/null +++ b/assets-source/objects/sign_winter.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ae931f7a6a97f5ce58c6febdd575a3b799111d7f816ea95558f155366bf9bf5 +size 23193 diff --git a/assets-source/objects/snowpile/big.psd b/assets-source/objects/snowpile/big.psd new file mode 100644 index 0000000..6568119 --- /dev/null +++ b/assets-source/objects/snowpile/big.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28834478d744cbfefe9c5a2759da519f49ec6bffeef9d969dd1cde827e929912 +size 32995 diff --git a/assets-source/objects/snowpile/medium.psd b/assets-source/objects/snowpile/medium.psd new file mode 100644 index 0000000..9a46376 --- /dev/null +++ b/assets-source/objects/snowpile/medium.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4798c8db2a64e4486d865e1f64db3d80f66a9e04c762820cfaf54d626868ebd +size 27956 diff --git a/assets-source/objects/snowpile/palette_1_big.psd b/assets-source/objects/snowpile/palette_1_big.psd new file mode 100644 index 0000000..22ddf7b --- /dev/null +++ b/assets-source/objects/snowpile/palette_1_big.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:684e9f1a4a74a0b0cf105303ef6c225774321d2e2cc9c3b9bbe3285e83c34ae1 +size 33512 diff --git a/assets-source/objects/snowpile/small.psd b/assets-source/objects/snowpile/small.psd new file mode 100644 index 0000000..06e362b --- /dev/null +++ b/assets-source/objects/snowpile/small.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f8193a1df1c9cd3091cbde003ab92969709bd55221d99f708476f409d96cdba +size 23980 diff --git a/assets-source/objects/snowpile/tinier.psd b/assets-source/objects/snowpile/tinier.psd new file mode 100644 index 0000000..11ea408 --- /dev/null +++ b/assets-source/objects/snowpile/tinier.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1adf019b0aa62d978e3fbff716502210fee18dea166623905422b0739eaae2e8 +size 19870 diff --git a/assets-source/objects/snowpile/tiny.psd b/assets-source/objects/snowpile/tiny.psd new file mode 100644 index 0000000..bbb5e87 --- /dev/null +++ b/assets-source/objects/snowpile/tiny.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e188d11882562dedf1fd3db89e19c23c15af1f4e15c0f00c78698526b69ca120 +size 20323 diff --git a/assets-source/objects/snowpony/1.psd b/assets-source/objects/snowpony/1.psd new file mode 100644 index 0000000..f7e29c3 --- /dev/null +++ b/assets-source/objects/snowpony/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71f9c246a23ad07ebf2e2dd4f1b92be38d52bf04a9620122a6b5f917f3400218 +size 24348 diff --git a/assets-source/objects/snowpony/2.psd b/assets-source/objects/snowpony/2.psd new file mode 100644 index 0000000..8cb2f56 --- /dev/null +++ b/assets-source/objects/snowpony/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22b0a6877c176062766eed5d292a4b51481918a4c9489bbeb37a194fbef2986b +size 25658 diff --git a/assets-source/objects/snowpony/3.psd b/assets-source/objects/snowpony/3.psd new file mode 100644 index 0000000..e059902 --- /dev/null +++ b/assets-source/objects/snowpony/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:238ebe8feb39d48d7a560475852f3300587f891261a05c3f436cfc43b3834f5c +size 41329 diff --git a/assets-source/objects/snowpony/4.psd b/assets-source/objects/snowpony/4.psd new file mode 100644 index 0000000..9c0e514 --- /dev/null +++ b/assets-source/objects/snowpony/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3e328f1baa6b5b615cb45733b6c75b818395a48cafb5180243b0b30bfb8cf3a +size 41954 diff --git a/assets-source/objects/snowpony/5.psd b/assets-source/objects/snowpony/5.psd new file mode 100644 index 0000000..692d3be --- /dev/null +++ b/assets-source/objects/snowpony/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a858adea333d54d666061a9acdc902bf06d9c429e178c4fe8021c6bd1574f090 +size 29347 diff --git a/assets-source/objects/snowpony/6.psd b/assets-source/objects/snowpony/6.psd new file mode 100644 index 0000000..b5665ef --- /dev/null +++ b/assets-source/objects/snowpony/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad7dccf5e3c990d66554e8e7ecf750af4b63384d2b18b99fdd7a80488ae90f30 +size 39873 diff --git a/assets-source/objects/snowpony/7.psd b/assets-source/objects/snowpony/7.psd new file mode 100644 index 0000000..35161e3 --- /dev/null +++ b/assets-source/objects/snowpony/7.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d2a0048148ffb2ef69d7cbd69b9b69f88e8840a044cabfe5408618af62ec197 +size 40335 diff --git a/assets-source/objects/snowpony/8.psd b/assets-source/objects/snowpony/8.psd new file mode 100644 index 0000000..96666b3 --- /dev/null +++ b/assets-source/objects/snowpony/8.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c41116353d199ae74beca6a113da063e9dfd4c2c4437c39ca508252c14edee49 +size 40271 diff --git a/assets-source/objects/snowpony/9.psd b/assets-source/objects/snowpony/9.psd new file mode 100644 index 0000000..fa9f7fe --- /dev/null +++ b/assets-source/objects/snowpony/9.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39d57ef50f334479c040c4aa249ea77772a9d8512051f369fe052299a0fb08d +size 40132 diff --git a/assets-source/objects/spider.psd b/assets-source/objects/spider.psd new file mode 100644 index 0000000..42f7507 --- /dev/null +++ b/assets-source/objects/spider.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e76c7583c5dfe62c0e3d6e504169e0676fe8f17b863a40f145dd254d3122410c +size 22297 diff --git a/assets-source/objects/stalactite/1.psd b/assets-source/objects/stalactite/1.psd new file mode 100644 index 0000000..dc438f4 --- /dev/null +++ b/assets-source/objects/stalactite/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64065c75dca141f07b83bcf4a0a3b26d8ea813a8c444049b2da6483ed1df4942 +size 23038 diff --git a/assets-source/objects/stalactite/2.psd b/assets-source/objects/stalactite/2.psd new file mode 100644 index 0000000..b3a187a --- /dev/null +++ b/assets-source/objects/stalactite/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5690111a21ae548e960b128c86074937a0b538b2fe5be1e100b84846c7062967 +size 24532 diff --git a/assets-source/objects/stalactite/3.psd b/assets-source/objects/stalactite/3.psd new file mode 100644 index 0000000..e1e22da --- /dev/null +++ b/assets-source/objects/stalactite/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6be01784de005d1f0cd648d1f7154f79f6412167425ff98683e59a18cab6a965 +size 27084 diff --git a/assets-source/objects/stone_wall/full.psd b/assets-source/objects/stone_wall/full.psd new file mode 100644 index 0000000..a7da744 --- /dev/null +++ b/assets-source/objects/stone_wall/full.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21199ec70259a228f7452114895bc055f3e9b4466972e95b1d6e47bb716b73c6 +size 33379 diff --git a/assets-source/objects/stone_wall/horizontal1.psd b/assets-source/objects/stone_wall/horizontal1.psd new file mode 100644 index 0000000..44b2511 --- /dev/null +++ b/assets-source/objects/stone_wall/horizontal1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f5044f888be9b73b6d65c6056fdc391cdd7754903eee951f061e9c7efbe8aa4 +size 4390 diff --git a/assets-source/objects/stone_wall/pole1.psd b/assets-source/objects/stone_wall/pole1.psd new file mode 100644 index 0000000..c3fc796 --- /dev/null +++ b/assets-source/objects/stone_wall/pole1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14429b53cabce770f695d5fdd90948f3628d6acd6c95dad1da74597cdcba8b3f +size 2257 diff --git a/assets-source/objects/stone_wall/vertical1.psd b/assets-source/objects/stone_wall/vertical1.psd new file mode 100644 index 0000000..9cfca48 --- /dev/null +++ b/assets-source/objects/stone_wall/vertical1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4590849194d04adbb3ada217db7a76effc34ec3921e311a0fd41ab224328f592 +size 24173 diff --git a/assets-source/objects/stone_wall_winter/horizontal1.psd b/assets-source/objects/stone_wall_winter/horizontal1.psd new file mode 100644 index 0000000..10affa7 --- /dev/null +++ b/assets-source/objects/stone_wall_winter/horizontal1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44cc213abe4c97026102ae0fbfbca6a250fef03281d12d28196a88bd7e3ddb88 +size 23673 diff --git a/assets-source/objects/stone_wall_winter/pole1.psd b/assets-source/objects/stone_wall_winter/pole1.psd new file mode 100644 index 0000000..b6240af --- /dev/null +++ b/assets-source/objects/stone_wall_winter/pole1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74434e54ed17c5e36a053c710fef585bf1f1be2086bdbde47ee0aef400e90601 +size 21290 diff --git a/assets-source/objects/stone_wall_winter/vertical1.psd b/assets-source/objects/stone_wall_winter/vertical1.psd new file mode 100644 index 0000000..9606f5a --- /dev/null +++ b/assets-source/objects/stone_wall_winter/vertical1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd290d0b82047d0e4e3040b5e1b537d4865f0472d95d440595aaf6aa6e440671 +size 24960 diff --git a/assets-source/objects/table/1.psd b/assets-source/objects/table/1.psd new file mode 100644 index 0000000..2135830 --- /dev/null +++ b/assets-source/objects/table/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04cef616a81140aa95e48cc00315ed89ec08867ead692cd43dbae492d271c3ef +size 30761 diff --git a/assets-source/objects/table/2.psd b/assets-source/objects/table/2.psd new file mode 100644 index 0000000..e997074 --- /dev/null +++ b/assets-source/objects/table/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f9009c31a41e77896f69280a4ede3aefe3378760c9f4a038292779ff45fd89c +size 38463 diff --git a/assets-source/objects/table_3.psd b/assets-source/objects/table_3.psd new file mode 100644 index 0000000..1503dbf --- /dev/null +++ b/assets-source/objects/table_3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b94513ef107f7040206ea31cf5614da86ccbaa8b59039d3ced9e4acb549f1215 +size 24382 diff --git a/assets-source/objects/tile.psd b/assets-source/objects/tile.psd new file mode 100644 index 0000000..991d130 --- /dev/null +++ b/assets-source/objects/tile.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e673db9a99fb15b4ed41360911b652aa373f704d697d63f1d244fc0425552fa +size 25130 diff --git a/assets-source/objects/tile_none.psd b/assets-source/objects/tile_none.psd new file mode 100644 index 0000000..ef3ee82 --- /dev/null +++ b/assets-source/objects/tile_none.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e63b2a42540b9573dc81eee9ef7c6bdcddc5841023308e82817fda08e9c3b0f +size 22224 diff --git a/assets-source/objects/tombstone/1.psd b/assets-source/objects/tombstone/1.psd new file mode 100644 index 0000000..253e8ec --- /dev/null +++ b/assets-source/objects/tombstone/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b93b064bc9660675c6b414b2cb69c22d939c6de52df807f45966acc86c84dee4 +size 24654 diff --git a/assets-source/objects/tombstone/2.psd b/assets-source/objects/tombstone/2.psd new file mode 100644 index 0000000..39db203 --- /dev/null +++ b/assets-source/objects/tombstone/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c5de2f59055dc761b0992c8d34fab38ffa44a1d8b9b1f7b63480ac3e6362abc +size 26183 diff --git a/assets-source/objects/toolbox/empty.psd b/assets-source/objects/toolbox/empty.psd new file mode 100644 index 0000000..2b32e1c --- /dev/null +++ b/assets-source/objects/toolbox/empty.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5180fe6a45d8c55f43869e821ec5407df3ce389a12aaccb85b1356de5cb69d55 +size 23887 diff --git a/assets-source/objects/toolbox/full.psd b/assets-source/objects/toolbox/full.psd new file mode 100644 index 0000000..d0d1e3b --- /dev/null +++ b/assets-source/objects/toolbox/full.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:234423c78ae021036d3d5438ef636b7762350012452d9cf311ab0d9117ecc5b7 +size 25969 diff --git a/assets-source/objects/tools_icon.psd b/assets-source/objects/tools_icon.psd new file mode 100644 index 0000000..d9c2518 --- /dev/null +++ b/assets-source/objects/tools_icon.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5088fcff6610831a8a8ff887a9bc711172880ccbc987a821440c3ad370bb5ca9 +size 22276 diff --git a/assets-source/objects/wall_h_placeholder.psd b/assets-source/objects/wall_h_placeholder.psd new file mode 100644 index 0000000..63acde8 --- /dev/null +++ b/assets-source/objects/wall_h_placeholder.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42d0382b8f52a01016d464077ecee73e907419b4d6e2454e71a7b10c8aef5f2a +size 22662 diff --git a/assets-source/objects/wall_map.psd b/assets-source/objects/wall_map.psd new file mode 100644 index 0000000..9320b7e --- /dev/null +++ b/assets-source/objects/wall_map.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9575a22fe14332ffdfc5a9a0c279bcbae020c3b7cae669a5147a7f4c5168d428 +size 26800 diff --git a/assets-source/objects/wall_v_placeholder.psd b/assets-source/objects/wall_v_placeholder.psd new file mode 100644 index 0000000..9020980 --- /dev/null +++ b/assets-source/objects/wall_v_placeholder.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7b74abeb61a0e98e79a6fe49c87e7c9eeed1a7234a0eba3ae4cabfdc929579d +size 23382 diff --git a/assets-source/objects/web.psd b/assets-source/objects/web.psd new file mode 100644 index 0000000..b2a4c18 --- /dev/null +++ b/assets-source/objects/web.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65d3b7f23d414f288aa073b64afdad7a75858bfdbbfab9e7e8dedf82ae68b67c +size 26760 diff --git a/assets-source/objects/well.psd b/assets-source/objects/well.psd new file mode 100644 index 0000000..a955b13 --- /dev/null +++ b/assets-source/objects/well.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a831057ec1e9ba8eb729d7a0806d943f24b6b413d1b134e53a312eb76328fc5 +size 37806 diff --git a/assets-source/objects/window/1.psd b/assets-source/objects/window/1.psd new file mode 100644 index 0000000..0003e00 --- /dev/null +++ b/assets-source/objects/window/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3219b0d5497fbaf2a3812633435a60603baa8b7919b1ff549f68647c323699f2 +size 27196 diff --git a/assets-source/objects/wooden_fence/horizontal1.psd b/assets-source/objects/wooden_fence/horizontal1.psd new file mode 100644 index 0000000..29bf5f8 --- /dev/null +++ b/assets-source/objects/wooden_fence/horizontal1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90fd6873f9b2423d97611486f2fbd11472b7aaaac0e7120de37ed781c42d9b6c +size 22592 diff --git a/assets-source/objects/wooden_fence/horizontal2.psd b/assets-source/objects/wooden_fence/horizontal2.psd new file mode 100644 index 0000000..5c0a508 --- /dev/null +++ b/assets-source/objects/wooden_fence/horizontal2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b57544225ef58b5f9e659a07ccdfd8809839ca291c631b1c9b2fc1ed128b4c55 +size 19658 diff --git a/assets-source/objects/wooden_fence/horizontal3.psd b/assets-source/objects/wooden_fence/horizontal3.psd new file mode 100644 index 0000000..b86d8bb --- /dev/null +++ b/assets-source/objects/wooden_fence/horizontal3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:672cbd63a3677db7bab7b554fb4e172fde3304af100c84fe7d3662988ec28b8a +size 19536 diff --git a/assets-source/objects/wooden_fence/horizontal4.psd b/assets-source/objects/wooden_fence/horizontal4.psd new file mode 100644 index 0000000..17d8fa9 --- /dev/null +++ b/assets-source/objects/wooden_fence/horizontal4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:422fe1401bc00feb7098151170098d3b9a3c061d851b0ee8242d00a5133b4895 +size 19717 diff --git a/assets-source/objects/wooden_fence/horizontal5.psd b/assets-source/objects/wooden_fence/horizontal5.psd new file mode 100644 index 0000000..4341503 --- /dev/null +++ b/assets-source/objects/wooden_fence/horizontal5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ab75a210064a123b358509af3c1bbab246047e0087f8a1e52895ebed0ed5562 +size 20336 diff --git a/assets-source/objects/wooden_fence/horizontal6.psd b/assets-source/objects/wooden_fence/horizontal6.psd new file mode 100644 index 0000000..02b46ee --- /dev/null +++ b/assets-source/objects/wooden_fence/horizontal6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b9119a04ad65e2173fdb7cc7075c8392656f59c7f230cb5b437794a8b667665 +size 20475 diff --git a/assets-source/objects/wooden_fence/palette_1_pole1.psd b/assets-source/objects/wooden_fence/palette_1_pole1.psd new file mode 100644 index 0000000..b4a99ef --- /dev/null +++ b/assets-source/objects/wooden_fence/palette_1_pole1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fab1d3301cbc357202d1d272ff8d1d21cf29cde1e21b7ddcd6a72f2743072341 +size 25596 diff --git a/assets-source/objects/wooden_fence/pole1.psd b/assets-source/objects/wooden_fence/pole1.psd new file mode 100644 index 0000000..5a255a7 --- /dev/null +++ b/assets-source/objects/wooden_fence/pole1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b16d488da347de02c155d5c8d2e6cab1372417f0c186600cf36a7f3726287b7 +size 25094 diff --git a/assets-source/objects/wooden_fence/pole2.psd b/assets-source/objects/wooden_fence/pole2.psd new file mode 100644 index 0000000..48b2a4c --- /dev/null +++ b/assets-source/objects/wooden_fence/pole2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a26015b7894826e7c26402c0867c53497eba4003c28666ae2e7613c40f964235 +size 23882 diff --git a/assets-source/objects/wooden_fence/pole3.psd b/assets-source/objects/wooden_fence/pole3.psd new file mode 100644 index 0000000..b4314f4 --- /dev/null +++ b/assets-source/objects/wooden_fence/pole3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc275543afa6f1b991e74b6a1afbc903780bbea7f5d386d5abe150e3a7c24b0b +size 20716 diff --git a/assets-source/objects/wooden_fence/pole4.psd b/assets-source/objects/wooden_fence/pole4.psd new file mode 100644 index 0000000..5c297c5 --- /dev/null +++ b/assets-source/objects/wooden_fence/pole4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:956a67f9fe9524c939420048d5d31c46e973a3b67c391d943cc3b0e00aa274bf +size 23886 diff --git a/assets-source/objects/wooden_fence/pole5.psd b/assets-source/objects/wooden_fence/pole5.psd new file mode 100644 index 0000000..717a943 --- /dev/null +++ b/assets-source/objects/wooden_fence/pole5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78f5f985b76accd442aa7e5e38423c71d9adc5e7384eb146b65262106046d015 +size 20726 diff --git a/assets-source/objects/wooden_fence/vertical1.psd b/assets-source/objects/wooden_fence/vertical1.psd new file mode 100644 index 0000000..7751193 --- /dev/null +++ b/assets-source/objects/wooden_fence/vertical1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1869d65507bb3faf2a39b0bde55e53fb80baaf615140c7fcfde1aa2b38fbc351 +size 21786 diff --git a/assets-source/objects/wooden_fence/vertical2.psd b/assets-source/objects/wooden_fence/vertical2.psd new file mode 100644 index 0000000..b43c09d --- /dev/null +++ b/assets-source/objects/wooden_fence/vertical2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dc009972a26d182ab09bc80fdc99132349b209b953d06d31a96341ab48b74a1 +size 18708 diff --git a/assets-source/objects/wooden_fence/vertical3.psd b/assets-source/objects/wooden_fence/vertical3.psd new file mode 100644 index 0000000..7a446eb --- /dev/null +++ b/assets-source/objects/wooden_fence/vertical3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f184bc9b63aae46cce4c6f895a0e2339347aba24f1ed60a2083936c23240f87 +size 18704 diff --git a/assets-source/objects/wooden_fence_winter/horizontal1.psd b/assets-source/objects/wooden_fence_winter/horizontal1.psd new file mode 100644 index 0000000..1f33617 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/horizontal1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fe2cd8eefbf176f3d538df81983da93a2f0ad6b46dd61a89c93fd215ac76e0f +size 24631 diff --git a/assets-source/objects/wooden_fence_winter/horizontal2.psd b/assets-source/objects/wooden_fence_winter/horizontal2.psd new file mode 100644 index 0000000..9b7d5b3 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/horizontal2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cbc4df361a4c029e2727a785a81f2df428398052d88eed6b93ae6a90d8ca788 +size 21701 diff --git a/assets-source/objects/wooden_fence_winter/horizontal3.psd b/assets-source/objects/wooden_fence_winter/horizontal3.psd new file mode 100644 index 0000000..71769aa --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/horizontal3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0685f7d6e0a5caed3b50ff3a55f62f16e2b2aa309886e5cfc608f9a5a27ee7bd +size 21274 diff --git a/assets-source/objects/wooden_fence_winter/horizontal4.psd b/assets-source/objects/wooden_fence_winter/horizontal4.psd new file mode 100644 index 0000000..3a4056b --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/horizontal4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620fb6881cef575b6d2ec80a253ef908ff4bf97ef942f91a3de8e6559acd8f98 +size 21366 diff --git a/assets-source/objects/wooden_fence_winter/horizontal5.psd b/assets-source/objects/wooden_fence_winter/horizontal5.psd new file mode 100644 index 0000000..422366f --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/horizontal5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8db18f55fe15d41a6a25eb9df86821a99b95fcb784f60171c7903583272f8ad1 +size 20712 diff --git a/assets-source/objects/wooden_fence_winter/horizontal6.psd b/assets-source/objects/wooden_fence_winter/horizontal6.psd new file mode 100644 index 0000000..0eea185 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/horizontal6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e5c48866695439a699aded0404391560c59d1e8558b1800756a0c49d6019340 +size 22282 diff --git a/assets-source/objects/wooden_fence_winter/pole1.psd b/assets-source/objects/wooden_fence_winter/pole1.psd new file mode 100644 index 0000000..8f59d8a --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/pole1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce8d56d3aab75378a1ec4987f65e25bd2ae7d6b289c6d3fca897d67e104c70ce +size 26312 diff --git a/assets-source/objects/wooden_fence_winter/pole2.psd b/assets-source/objects/wooden_fence_winter/pole2.psd new file mode 100644 index 0000000..b12a5c1 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/pole2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9802ea8cb07ffb9d09e16eacf31cb9b893d4f22d7d6c96562b4dd0c8da0f7983 +size 21946 diff --git a/assets-source/objects/wooden_fence_winter/pole3.psd b/assets-source/objects/wooden_fence_winter/pole3.psd new file mode 100644 index 0000000..f6d8bd7 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/pole3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f40aac44247e7c15286aa49d5670e40ec596384534b5ee017ef4a8b16e72c043 +size 22542 diff --git a/assets-source/objects/wooden_fence_winter/pole4.psd b/assets-source/objects/wooden_fence_winter/pole4.psd new file mode 100644 index 0000000..7469615 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/pole4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:537be35d60915dff80e9f91a5d7dc0ee4ce2f648f9bccde1ea09d88d9fbdb4e0 +size 22542 diff --git a/assets-source/objects/wooden_fence_winter/pole5.psd b/assets-source/objects/wooden_fence_winter/pole5.psd new file mode 100644 index 0000000..711450b --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/pole5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4410b52f5bdfb46a5c5a401dde426d07fc6ceb2e7b472baf3ee850dc5d31acd7 +size 22544 diff --git a/assets-source/objects/wooden_fence_winter/vertical1.psd b/assets-source/objects/wooden_fence_winter/vertical1.psd new file mode 100644 index 0000000..6a50285 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/vertical1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02f4aa961994de03e35377c0707357af4c411ee440b3d058ffb5554795926d1f +size 23348 diff --git a/assets-source/objects/wooden_fence_winter/vertical2.psd b/assets-source/objects/wooden_fence_winter/vertical2.psd new file mode 100644 index 0000000..e793ea6 --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/vertical2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46dd91c693c7d6ef90ef193432e40babb54603b8bd9bf8852960166094049e61 +size 19864 diff --git a/assets-source/objects/wooden_fence_winter/vertical3.psd b/assets-source/objects/wooden_fence_winter/vertical3.psd new file mode 100644 index 0000000..faf2bbf --- /dev/null +++ b/assets-source/objects/wooden_fence_winter/vertical3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6398ac1fe62a14072b1a69804a8edf582fdadcf8826003f3ea6f223871078d40 +size 19872 diff --git a/assets-source/pony/animations/base.psd b/assets-source/pony/animations/base.psd new file mode 100644 index 0000000..8924b3e --- /dev/null +++ b/assets-source/pony/animations/base.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5227b69cd9059716c815b35e862fdf5bb14b4c238e18d20f7960ea17dd22fe7d +size 51134 diff --git a/assets-source/pony/animations/boop.psd b/assets-source/pony/animations/boop.psd new file mode 100644 index 0000000..ff078a5 --- /dev/null +++ b/assets-source/pony/animations/boop.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28fc3db63e6c9757a113126e1011563d64693472fd81fb118e6ba1d0aade876c +size 254154 diff --git a/assets-source/pony/animations/fly-bat.psd b/assets-source/pony/animations/fly-bat.psd new file mode 100644 index 0000000..80402ca --- /dev/null +++ b/assets-source/pony/animations/fly-bat.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:214572661341f0fd368b1028c78d5070ce3c8e009bef33ef5bb62e994fcbb14f +size 57355 diff --git a/assets-source/pony/animations/fly-bug.psd b/assets-source/pony/animations/fly-bug.psd new file mode 100644 index 0000000..e33fec3 --- /dev/null +++ b/assets-source/pony/animations/fly-bug.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28cbe6df39e0081cfa890d49ccf9f876587e2f68a99e2da38f8222593addb855 +size 45548 diff --git a/assets-source/pony/animations/fly-griffon.psd b/assets-source/pony/animations/fly-griffon.psd new file mode 100644 index 0000000..dcfd9a2 --- /dev/null +++ b/assets-source/pony/animations/fly-griffon.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d91e76d684c3bffea8b312f5c89587ba34dd823ea88446de09efc9f6b8b92e1a +size 65758 diff --git a/assets-source/pony/animations/fly-pegasus.psd b/assets-source/pony/animations/fly-pegasus.psd new file mode 100644 index 0000000..670f94c --- /dev/null +++ b/assets-source/pony/animations/fly-pegasus.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16c806b8c158c1efc2e0a8396f778ee2c34a1d1a8e9d10665cd76ec7ad374ddb +size 56134 diff --git a/assets-source/pony/animations/lie.psd b/assets-source/pony/animations/lie.psd new file mode 100644 index 0000000..8f7a529 --- /dev/null +++ b/assets-source/pony/animations/lie.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b66a96da9dbf0ccf4d1f32ead31804cd2d1055e6cd71cb5fa2b5d0e880a819c8 +size 170897 diff --git a/assets-source/pony/animations/sit.psd b/assets-source/pony/animations/sit.psd new file mode 100644 index 0000000..c4ff328 --- /dev/null +++ b/assets-source/pony/animations/sit.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:985511d963a194569eea7db950c89f12507424d1c60e2af72c396c451a9ee2d2 +size 153732 diff --git a/assets-source/pony/animations/trot.psd b/assets-source/pony/animations/trot.psd new file mode 100644 index 0000000..9b002e7 --- /dev/null +++ b/assets-source/pony/animations/trot.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6506b084d8ed453c20b977eccd0dcd0f5da785d90f640c8403a446f78e33f617 +size 272612 diff --git a/assets-source/pony/back-legs-accessories.psd b/assets-source/pony/back-legs-accessories.psd new file mode 100644 index 0000000..c06d181 --- /dev/null +++ b/assets-source/pony/back-legs-accessories.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f123e73905df058dfae6c0036bd9a44ce6f971b0da9ce92bd3fb7c2c91b9b73e +size 647718 diff --git a/assets-source/pony/back-legs-cms.psd b/assets-source/pony/back-legs-cms.psd new file mode 100644 index 0000000..c94da07 --- /dev/null +++ b/assets-source/pony/back-legs-cms.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d3164cefcabd9592f89fe84179bc05ae276f78fe7c716e16e477a21b1e61997 +size 184547 diff --git a/assets-source/pony/back-legs-hooves.psd b/assets-source/pony/back-legs-hooves.psd new file mode 100644 index 0000000..58a0445 --- /dev/null +++ b/assets-source/pony/back-legs-hooves.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b7617a88e18d7f57914df507d7b86e1090300eedc239710016658ce3efc687b +size 909856 diff --git a/assets-source/pony/back-legs-sleeves.psd b/assets-source/pony/back-legs-sleeves.psd new file mode 100644 index 0000000..dc38644 --- /dev/null +++ b/assets-source/pony/back-legs-sleeves.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33f8d0108a1e91363e993733e6ac50d584258031191de509274ca11eea8f95df +size 382401 diff --git a/assets-source/pony/back-legs.psd b/assets-source/pony/back-legs.psd new file mode 100644 index 0000000..fa71b93 --- /dev/null +++ b/assets-source/pony/back-legs.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc239280013eb97001f3b35ab3935ff4d41e9d701a36379f633085910f0c6d5a +size 216417 diff --git a/assets-source/pony/blush.psd b/assets-source/pony/blush.psd new file mode 100644 index 0000000..a0410ed --- /dev/null +++ b/assets-source/pony/blush.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a28ebc85dc9a4bc134d87e3fafcff1c2fb1cb4d0323fb5507a2faca61a1f0cdf +size 30287 diff --git a/assets-source/pony/body-back-accessory.psd b/assets-source/pony/body-back-accessory.psd new file mode 100644 index 0000000..b242bbd --- /dev/null +++ b/assets-source/pony/body-back-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21e850f2342bce3d9e675e589e20303f1708a0768e507bd4769dcf4f8eac2131 +size 772940 diff --git a/assets-source/pony/body-chest-accessory.psd b/assets-source/pony/body-chest-accessory.psd new file mode 100644 index 0000000..a2f5cec --- /dev/null +++ b/assets-source/pony/body-chest-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:637178eaf8c6b6441fc8c0db5ac7912548c2382407b3bd66847c9b3e786e2f56 +size 820877 diff --git a/assets-source/pony/body-waist-accessory.psd b/assets-source/pony/body-waist-accessory.psd new file mode 100644 index 0000000..d4cb679 --- /dev/null +++ b/assets-source/pony/body-waist-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d1b5a263717972bcfa768aaeba8fb728f152a2333f2a9ba6a24e9bc51d10594 +size 622532 diff --git a/assets-source/pony/body-wings.psd b/assets-source/pony/body-wings.psd new file mode 100644 index 0000000..a456217 --- /dev/null +++ b/assets-source/pony/body-wings.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b02529863e6f30b3d9711ab757f26d28da84091762b58672fee3eb733398cca5 +size 841446 diff --git a/assets-source/pony/body.psd b/assets-source/pony/body.psd new file mode 100644 index 0000000..6c4dc73 --- /dev/null +++ b/assets-source/pony/body.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4caef5d306a619283f7d048956f4145f4b7713702e1ab60a4761966327550e8a +size 130454 diff --git a/assets-source/pony/ear-accessory.psd b/assets-source/pony/ear-accessory.psd new file mode 100644 index 0000000..12a09a2 --- /dev/null +++ b/assets-source/pony/ear-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04c1872fd06e5f2e21f8d55abbd43856f93a49680fb37406c07afd33955da2ab +size 148829 diff --git a/assets-source/pony/ears.psd b/assets-source/pony/ears.psd new file mode 100644 index 0000000..bc9deb7 --- /dev/null +++ b/assets-source/pony/ears.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea48d24f0e2ece488ff70dd1964d222aca73520e09bbcffd969461dd5579d3b1 +size 146844 diff --git a/assets-source/pony/extra-accessory.psd b/assets-source/pony/extra-accessory.psd new file mode 100644 index 0000000..3fdebf6 --- /dev/null +++ b/assets-source/pony/extra-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a5c4234e4de1d02e7138f863d48e5cb3df221ef1d7ae9c2862fe2bde65235e6 +size 316343 diff --git a/assets-source/pony/eyes.psd b/assets-source/pony/eyes.psd new file mode 100644 index 0000000..97215f0 --- /dev/null +++ b/assets-source/pony/eyes.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bed3e8cd2a499b90dada8ca966d2b6ae41c2da9eb59895ff8df6b1277507f51c +size 100394 diff --git a/assets-source/pony/face-accessory.psd b/assets-source/pony/face-accessory.psd new file mode 100644 index 0000000..c9ee931 --- /dev/null +++ b/assets-source/pony/face-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeb74fd4d348a804db4d0855418b416176ae1db0dc87493e3ea22c56406d09dc +size 233982 diff --git a/assets-source/pony/facial-hair.psd b/assets-source/pony/facial-hair.psd new file mode 100644 index 0000000..88be123 --- /dev/null +++ b/assets-source/pony/facial-hair.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:015c5131a159940517226996884ee9172a60afb2328dab5f4b95ff3fa1bd6eb3 +size 216107 diff --git a/assets-source/pony/front-legs-accessories.psd b/assets-source/pony/front-legs-accessories.psd new file mode 100644 index 0000000..adde616 --- /dev/null +++ b/assets-source/pony/front-legs-accessories.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8064dad0027a016693ae748975aad1b064509749bdeb52ce75c732211b62a93 +size 961340 diff --git a/assets-source/pony/front-legs-hooves.psd b/assets-source/pony/front-legs-hooves.psd new file mode 100644 index 0000000..37687c2 --- /dev/null +++ b/assets-source/pony/front-legs-hooves.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc3486c18b9a11bb56efa5ffded53c5e412e136387a04da984b03881da931ece +size 2045603 diff --git a/assets-source/pony/front-legs-sleeves.psd b/assets-source/pony/front-legs-sleeves.psd new file mode 100644 index 0000000..b4696f4 --- /dev/null +++ b/assets-source/pony/front-legs-sleeves.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dee4a5d726f585b634244ad281189e350e0a1814d753de5499e4a91b2a057647 +size 1775372 diff --git a/assets-source/pony/front-legs.psd b/assets-source/pony/front-legs.psd new file mode 100644 index 0000000..9e6f9a0 --- /dev/null +++ b/assets-source/pony/front-legs.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d46e4d67ffc2085b82c71f6749f0917f14177c4ddec886bf26599642ca1e8ae0 +size 320928 diff --git a/assets-source/pony/head-accessory.psd b/assets-source/pony/head-accessory.psd new file mode 100644 index 0000000..ff2db22 --- /dev/null +++ b/assets-source/pony/head-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb080a612d4da7078add8704a8c81315e2887dc246924c5db7440da7633e01eb +size 357200 diff --git a/assets-source/pony/head.psd b/assets-source/pony/head.psd new file mode 100644 index 0000000..e3aa54f --- /dev/null +++ b/assets-source/pony/head.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d5e1f95703febafca23170cd5b36fd95ea91401d009c7eb06a4cb374899cca5 +size 90431 diff --git a/assets-source/pony/horns.psd b/assets-source/pony/horns.psd new file mode 100644 index 0000000..e3793b7 --- /dev/null +++ b/assets-source/pony/horns.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88ffd6d2a55a661b8caf816eef4879a02af873a2a0eb058aa6749cbed991340f +size 236185 diff --git a/assets-source/pony/irises.psd b/assets-source/pony/irises.psd new file mode 100644 index 0000000..e8cf189 --- /dev/null +++ b/assets-source/pony/irises.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50309584405af462cdbbca16cf018f807a9224de3035593b925b9b7a8a8283a4 +size 35274 diff --git a/assets-source/pony/manes.psd b/assets-source/pony/manes.psd new file mode 100644 index 0000000..6f3af22 --- /dev/null +++ b/assets-source/pony/manes.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e48f00a59f16c0f24ea8582689246ab9a87d3609f0f30e2a0598377b349f4ff8 +size 890685 diff --git a/assets-source/pony/muzzles.psd b/assets-source/pony/muzzles.psd new file mode 100644 index 0000000..3df76ef --- /dev/null +++ b/assets-source/pony/muzzles.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a97290b3ead81cb4730982870f34f50833aa383b11ba6a7e071612d91ea0f2c +size 158312 diff --git a/assets-source/pony/neck-accessory.psd b/assets-source/pony/neck-accessory.psd new file mode 100644 index 0000000..8f7f6ef --- /dev/null +++ b/assets-source/pony/neck-accessory.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3feddf840fd6da12612d767b12d217f883e33cf084a53b0b4255c8e0993e7f41 +size 2299299 diff --git a/assets-source/pony/shadows.psd b/assets-source/pony/shadows.psd new file mode 100644 index 0000000..5f70d07 --- /dev/null +++ b/assets-source/pony/shadows.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb234d447966743f8d4157b2610a84f55c90095fd311ddd0aeabc6315176463 +size 28335 diff --git a/assets-source/pony/tails.psd b/assets-source/pony/tails.psd new file mode 100644 index 0000000..9396965 --- /dev/null +++ b/assets-source/pony/tails.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f21d494e28be8991f771fa8f9b98f59bc900f222857d6de24ba24884d476cf2 +size 1196810 diff --git a/assets-source/sprites-animations/cat_light.psd b/assets-source/sprites-animations/cat_light.psd new file mode 100644 index 0000000..0e69e54 --- /dev/null +++ b/assets-source/sprites-animations/cat_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b9b9c99ee53929216d0e521f736d2090d509a8459999211e7e5c26e091ca291 +size 51604 diff --git a/assets-source/sprites-animations/ghost1_hooves_light.psd b/assets-source/sprites-animations/ghost1_hooves_light.psd new file mode 100644 index 0000000..b2f8146 --- /dev/null +++ b/assets-source/sprites-animations/ghost1_hooves_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:414768affd406e837d5771e37978996aa5f09baadcd79f2cd024c65797cf5621 +size 148960 diff --git a/assets-source/sprites-animations/ghost1_light.psd b/assets-source/sprites-animations/ghost1_light.psd new file mode 100644 index 0000000..38a7d79 --- /dev/null +++ b/assets-source/sprites-animations/ghost1_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de626c936c77b4e9c1e7f780fa9d151cd9fce43ca700e8d2355f07020a5789cd +size 179134 diff --git a/assets-source/sprites-animations/lantern_light.psd b/assets-source/sprites-animations/lantern_light.psd new file mode 100644 index 0000000..53124a2 --- /dev/null +++ b/assets-source/sprites-animations/lantern_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e33b889432b04682eee1f94670337144fbb75bf8493ff6208582262bff94e81 +size 30786 diff --git a/assets-source/sprites-animations/magic2_light.psd b/assets-source/sprites-animations/magic2_light.psd new file mode 100644 index 0000000..0b5d167 --- /dev/null +++ b/assets-source/sprites-animations/magic2_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d78f630a2e0e25620be002c2d7b74d94076d87a6e4bfaca9921c722336435553 +size 31648 diff --git a/assets-source/sprites-animations/magic_light.psd b/assets-source/sprites-animations/magic_light.psd new file mode 100644 index 0000000..075e44f --- /dev/null +++ b/assets-source/sprites-animations/magic_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b251f93e07504e97d33b31e5a54d74686952536d9c1ffb695e786eba8023734e +size 31502 diff --git a/assets-source/sprites-animations/torch2_light.psd b/assets-source/sprites-animations/torch2_light.psd new file mode 100644 index 0000000..0c135e7 --- /dev/null +++ b/assets-source/sprites-animations/torch2_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a111496e2bb7e8e37928b4699f6daf3ad80787c0ef8d779a56b986763ed8a014 +size 26630 diff --git a/assets-source/sprites-animations/water_crystal_1_light.psd b/assets-source/sprites-animations/water_crystal_1_light.psd new file mode 100644 index 0000000..37b26c9 --- /dev/null +++ b/assets-source/sprites-animations/water_crystal_1_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9d7f6f32d0b8d9e605777e62454359591564973963787deeb1436b91f8176c2 +size 26962 diff --git a/assets-source/sprites-animations/water_crystal_2_light.psd b/assets-source/sprites-animations/water_crystal_2_light.psd new file mode 100644 index 0000000..87f6aa6 --- /dev/null +++ b/assets-source/sprites-animations/water_crystal_2_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86cd79b5c7c589690ffadb05c8b12549238b88819544296f4c6f0b2d107d1679 +size 26948 diff --git a/assets-source/sprites-animations/water_crystal_3_light.psd b/assets-source/sprites-animations/water_crystal_3_light.psd new file mode 100644 index 0000000..82b6147 --- /dev/null +++ b/assets-source/sprites-animations/water_crystal_3_light.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bc26f1d3c2ef79f60ec76daa74ea039adf5cd0776e123380dcacd9f592b7810 +size 26852 diff --git a/assets-source/sprites-palette/grass.png b/assets-source/sprites-palette/grass.png new file mode 100644 index 0000000..180efe5 --- /dev/null +++ b/assets-source/sprites-palette/grass.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94481ef11c675073e6043a8aa37a55650763254912ae91b83b3baee4cf743374 +size 1208 diff --git a/assets-source/sprites-palette/nipple.png b/assets-source/sprites-palette/nipple.png new file mode 100644 index 0000000..c06e2ee --- /dev/null +++ b/assets-source/sprites-palette/nipple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:846de8773474c1c5741f99c5aa2cf36b474c9078a965ad9c54525bb1e6475f03 +size 2808 diff --git a/assets-source/sprites-palette/nipple_alt.png b/assets-source/sprites-palette/nipple_alt.png new file mode 100644 index 0000000..622a0d5 --- /dev/null +++ b/assets-source/sprites-palette/nipple_alt.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c5cbed682493a573a0a4e2d3a1e20c98fc16d15d383d88da3d742753f507564 +size 2820 diff --git a/assets-source/sprites/dirt_ice_heightmap.png b/assets-source/sprites/dirt_ice_heightmap.png new file mode 100644 index 0000000..dd2763b --- /dev/null +++ b/assets-source/sprites/dirt_ice_heightmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b423d07fffb82e20c22c3d563d89ae4bc1f685e19ef3e58f254e696a014c1513 +size 7735 diff --git a/assets-source/sprites/dirt_stone_cave_height_map.png b/assets-source/sprites/dirt_stone_cave_height_map.png new file mode 100644 index 0000000..853b9b8 --- /dev/null +++ b/assets-source/sprites/dirt_stone_cave_height_map.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7c876fc6b085b8bb436af1448d4224cc4ec51d01ac0cf9bf4ef5232f4b6eeb +size 7565 diff --git a/assets-source/sprites/dirt_water_heightmap.png b/assets-source/sprites/dirt_water_heightmap.png new file mode 100644 index 0000000..8dcbd09 --- /dev/null +++ b/assets-source/sprites/dirt_water_heightmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:695794a251091c95c1aa87e4c6d58a082b885a198a811e6b69fb3d176607fab5 +size 8048 diff --git a/assets-source/sprites/firefly_light.png b/assets-source/sprites/firefly_light.png new file mode 100644 index 0000000..5f9738a --- /dev/null +++ b/assets-source/sprites/firefly_light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:047ad7bb9a7e2ad9626008d5765430d3710e2c9047ffc9d4fad3acd9b8aa6b32 +size 1006 diff --git a/assets-source/sprites/grass.png b/assets-source/sprites/grass.png new file mode 100644 index 0000000..180efe5 --- /dev/null +++ b/assets-source/sprites/grass.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94481ef11c675073e6043a8aa37a55650763254912ae91b83b3baee4cf743374 +size 1208 diff --git a/assets-source/sprites/jacko_lantern_light.png b/assets-source/sprites/jacko_lantern_light.png new file mode 100644 index 0000000..2f725a1 --- /dev/null +++ b/assets-source/sprites/jacko_lantern_light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69ab59238c7d5894d73788f72a3cdcfb7a8d553d3468548867c3c822ce25c70e +size 992 diff --git a/assets-source/sprites/nipple.png b/assets-source/sprites/nipple.png new file mode 100644 index 0000000..c06e2ee --- /dev/null +++ b/assets-source/sprites/nipple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:846de8773474c1c5741f99c5aa2cf36b474c9078a965ad9c54525bb1e6475f03 +size 2808 diff --git a/assets-source/sprites/pumpkin_light.png b/assets-source/sprites/pumpkin_light.png new file mode 100644 index 0000000..f99dc50 --- /dev/null +++ b/assets-source/sprites/pumpkin_light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e232ff45d60facde0e279cbdb2842b7aab12f6f55618aec9d221fb46f2f3c2f7 +size 1049 diff --git a/assets-source/tiles/cave-walls.png b/assets-source/tiles/cave-walls.png new file mode 100644 index 0000000..f33500d --- /dev/null +++ b/assets-source/tiles/cave-walls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25eb60ca277c1b0deb5b3c3ea0c716d9fd2c9f1188b75162174363fcf9df2bba +size 8011 diff --git a/assets-source/tiles/cliffs-grass-autumn.png b/assets-source/tiles/cliffs-grass-autumn.png new file mode 100644 index 0000000..745beac --- /dev/null +++ b/assets-source/tiles/cliffs-grass-autumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be8505483427cac5deb65f3c468cac33eed458a30f26ffdb8cefe126338e144e +size 9528 diff --git a/assets-source/tiles/cliffs-grass-winter.png b/assets-source/tiles/cliffs-grass-winter.png new file mode 100644 index 0000000..7b97806 --- /dev/null +++ b/assets-source/tiles/cliffs-grass-winter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fadd4db0e0217210d1b486c61a3a2e23c95ee8b6201c2549cc86cb7ef50a0b20 +size 9337 diff --git a/assets-source/tiles/cliffs-grass.png b/assets-source/tiles/cliffs-grass.png new file mode 100644 index 0000000..83b29e8 --- /dev/null +++ b/assets-source/tiles/cliffs-grass.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9cef947618eae8206ae8c18df0a1fb50950c8cb112203267ac00dac186c7190 +size 6991 diff --git a/assets-source/tiles/dirt-grass-autumn.png b/assets-source/tiles/dirt-grass-autumn.png new file mode 100644 index 0000000..8b49d19 --- /dev/null +++ b/assets-source/tiles/dirt-grass-autumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31a465a84b0f6bf15d8c52b65ebbffc83e22527f2f6d045466f323629f7d55b9 +size 27816 diff --git a/assets-source/tiles/dirt-grass.png b/assets-source/tiles/dirt-grass.png new file mode 100644 index 0000000..bc6500b --- /dev/null +++ b/assets-source/tiles/dirt-grass.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24a79ffcc21fd57fae0bea7bf6f104caf9bd6772e8f9be3cda5c31e0793c2218 +size 27205 diff --git a/assets-source/tiles/dirt-ice-autumn.png b/assets-source/tiles/dirt-ice-autumn.png new file mode 100644 index 0000000..7a22d68 --- /dev/null +++ b/assets-source/tiles/dirt-ice-autumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d14fa6f76a24719641064ef3764bb0541345fc616703c708d942b4f0d66ec53e +size 15553 diff --git a/assets-source/tiles/dirt-ice-winter.png b/assets-source/tiles/dirt-ice-winter.png new file mode 100644 index 0000000..65d67a8 --- /dev/null +++ b/assets-source/tiles/dirt-ice-winter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c977e0786fa92f9e0e57264e59c63706cb20a1a7f100734a009fd3c33c30fc5 +size 15573 diff --git a/assets-source/tiles/dirt-ice.png b/assets-source/tiles/dirt-ice.png new file mode 100644 index 0000000..70f3ceb --- /dev/null +++ b/assets-source/tiles/dirt-ice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee61f76ae2c8535b3fd2b2b990597a81110681ed5b5c3273c113ae615709aa83 +size 15323 diff --git a/assets-source/tiles/dirt-snow.png b/assets-source/tiles/dirt-snow.png new file mode 100644 index 0000000..dcdac97 --- /dev/null +++ b/assets-source/tiles/dirt-snow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46ac86ed6fc6b10c8935489a63809786c347288ce9b0235765ac515d621c52ee +size 14924 diff --git a/assets-source/tiles/dirt-stone-cave.png b/assets-source/tiles/dirt-stone-cave.png new file mode 100644 index 0000000..805a063 --- /dev/null +++ b/assets-source/tiles/dirt-stone-cave.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db5b4b9b13fb41f3ad75c47b28c057908890de76e6090693d1e5a1dd55f3d01e +size 23037 diff --git a/assets-source/tiles/dirt-water-1-autumn.png b/assets-source/tiles/dirt-water-1-autumn.png new file mode 100644 index 0000000..916edf7 --- /dev/null +++ b/assets-source/tiles/dirt-water-1-autumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2ee63d31a88ccd5b41a40fa5d51d54dba6eff9c9484f1c0666e440d631f337f +size 11424 diff --git a/assets-source/tiles/dirt-water-1-cave.png b/assets-source/tiles/dirt-water-1-cave.png new file mode 100644 index 0000000..0a30aae --- /dev/null +++ b/assets-source/tiles/dirt-water-1-cave.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1216778c6462eadf2d8ee9e380ad40c1cde058993550fded7847c899de0cace +size 11376 diff --git a/assets-source/tiles/dirt-water-1-winter.png b/assets-source/tiles/dirt-water-1-winter.png new file mode 100644 index 0000000..f216310 --- /dev/null +++ b/assets-source/tiles/dirt-water-1-winter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d6faa90566f6534361124da0299a6dc68043f66f052680782e9a3e8598ff4e5 +size 11247 diff --git a/assets-source/tiles/dirt-water-1.png b/assets-source/tiles/dirt-water-1.png new file mode 100644 index 0000000..ac02ab2 --- /dev/null +++ b/assets-source/tiles/dirt-water-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:948cf093abf7fabe33f5d7a14140ccb6a92f69438fc61a914faa9b11e55ce1a1 +size 13402 diff --git a/assets-source/tiles/dirt-water-2.png b/assets-source/tiles/dirt-water-2.png new file mode 100644 index 0000000..ce1d77f --- /dev/null +++ b/assets-source/tiles/dirt-water-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acc0ff7b652c8bf78cf2dd0243f7a4b533744824bc6157b427bcc3e240f66892 +size 14048 diff --git a/assets-source/tiles/dirt-water-3.png b/assets-source/tiles/dirt-water-3.png new file mode 100644 index 0000000..9b97f05 --- /dev/null +++ b/assets-source/tiles/dirt-water-3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3f70be28938d3427704d008206fe08122f148ed6ac4941135f394d7846b2bf5 +size 14251 diff --git a/assets-source/tiles/dirt-water-4.png b/assets-source/tiles/dirt-water-4.png new file mode 100644 index 0000000..c8236ef --- /dev/null +++ b/assets-source/tiles/dirt-water-4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:906ecf71577a09428d058b5404a0fefe59721598d7f0b95fb005bfdcf346c025 +size 13958 diff --git a/assets-source/tiles/grass.png b/assets-source/tiles/grass.png new file mode 100644 index 0000000..1088c7d --- /dev/null +++ b/assets-source/tiles/grass.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb5c9d3ce77b8f9c87c0cb0b87d4604bfa9080c28f147ab1354966e15209b54 +size 17022 diff --git a/assets-source/tiles/grass.psd b/assets-source/tiles/grass.psd new file mode 100644 index 0000000..6c1759c --- /dev/null +++ b/assets-source/tiles/grass.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7caf7d3f4f923cd531864bb2489bed27079c32e004f5f1f1a9189753a7b8ae9 +size 220504 diff --git a/assets-source/tiles/ice-snow.png b/assets-source/tiles/ice-snow.png new file mode 100644 index 0000000..de926db --- /dev/null +++ b/assets-source/tiles/ice-snow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d44856d1b1944bf3d91cb2f0a493f72da31a9505cc5816ef31de227956e82d3 +size 15902 diff --git a/assets-source/tiles/stone-tiles.png b/assets-source/tiles/stone-tiles.png new file mode 100644 index 0000000..5775566 --- /dev/null +++ b/assets-source/tiles/stone-tiles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf37d63643c2520089bf0bd5d2d2973542db8aba90b46e98acb49f62b68305f2 +size 13414 diff --git a/assets-source/tiles/stone2-tiles.png b/assets-source/tiles/stone2-tiles.png new file mode 100644 index 0000000..d7c42ea --- /dev/null +++ b/assets-source/tiles/stone2-tiles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6f170dfb77ef8546773065b0aca063913cedfbb6bd33f03f6e8354f67694e40 +size 9193 diff --git a/assets-source/tiles/water.png b/assets-source/tiles/water.png new file mode 100644 index 0000000..2a16423 --- /dev/null +++ b/assets-source/tiles/water.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:910d743722bfdbdd695e69c0125dd877bddcd3536a9d9aa228825710c6157dac +size 949 diff --git a/assets-source/tiles/wood-tiles-1.png b/assets-source/tiles/wood-tiles-1.png new file mode 100644 index 0000000..af786b7 --- /dev/null +++ b/assets-source/tiles/wood-tiles-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80bf5688e9a5b8b7ee8ef70f528420753d87a767b9119bd2118f79c0be6f4b5f +size 4081 diff --git a/assets-source/tiles/wood-tiles.png b/assets-source/tiles/wood-tiles.png new file mode 100644 index 0000000..c681135 --- /dev/null +++ b/assets-source/tiles/wood-tiles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba2db078ec9f5f1db04dd71561dbc11e0fad995c74f11f931b93dbc0ab6e6aa2 +size 8029 diff --git a/assets-source/tiles/wood.png b/assets-source/tiles/wood.png new file mode 100644 index 0000000..83e6236 --- /dev/null +++ b/assets-source/tiles/wood.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3403cc412725adb03a9d3dae42ec3f0d1362f6d44709dcb3fc6cd27814b65f5 +size 1193 diff --git a/assets-source/trees/pine/1.psd b/assets-source/trees/pine/1.psd new file mode 100644 index 0000000..ad4fc1e --- /dev/null +++ b/assets-source/trees/pine/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5d880d757b8a8a91617330452ec390187eb5106341c02abc1fee06f9d118a45 +size 26448 diff --git a/assets-source/trees/pine/2.psd b/assets-source/trees/pine/2.psd new file mode 100644 index 0000000..35cf942 --- /dev/null +++ b/assets-source/trees/pine/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e397b250cd6abd9ed5a1c9eab6b1a64f8d4a92975c26c6e53dae3548d091e9ec +size 24456 diff --git a/assets-source/trees/pine/3.psd b/assets-source/trees/pine/3.psd new file mode 100644 index 0000000..752a166 --- /dev/null +++ b/assets-source/trees/pine/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7703ccf7f23200ee34913595cdf44178cc9880890f2c3552fac7b337863daa67 +size 38266 diff --git a/assets-source/trees/pine/4.psd b/assets-source/trees/pine/4.psd new file mode 100644 index 0000000..0cb5ab3 --- /dev/null +++ b/assets-source/trees/pine/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cc14f7849568ddf63cfc1b4b97e4cd34187873af428666c622db8738e772562 +size 54612 diff --git a/assets-source/trees/pine/5.psd b/assets-source/trees/pine/5.psd new file mode 100644 index 0000000..51121bc --- /dev/null +++ b/assets-source/trees/pine/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c76843f575e460b4107e1f4fe14a42098cfa5fe7c91e0b7a08deb63c7d041069 +size 67035 diff --git a/assets-source/trees/pine/6.psd b/assets-source/trees/pine/6.psd new file mode 100644 index 0000000..db2479c --- /dev/null +++ b/assets-source/trees/pine/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fca3ee1f79a52f6b6c3b9cc7e8da5b5f4ec546fde78575ded55a54b192a84f31 +size 84832 diff --git a/assets-source/trees/pine/palette_1_6.psd b/assets-source/trees/pine/palette_1_6.psd new file mode 100644 index 0000000..936041e --- /dev/null +++ b/assets-source/trees/pine/palette_1_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac849f6b9fb0be65312609f781ef1a2d82df5fca6b77ab812bd87c59fb7af0f5 +size 91568 diff --git a/assets-source/trees/pine/palette_2_6.psd b/assets-source/trees/pine/palette_2_6.psd new file mode 100644 index 0000000..e5e858e --- /dev/null +++ b/assets-source/trees/pine/palette_2_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b03947a2bbeedd75f92df17f4171557f643ab8bbb4cad5241438b8e5ad3a9107 +size 87260 diff --git a/assets-source/trees/tree/1.psd b/assets-source/trees/tree/1.psd new file mode 100644 index 0000000..55c074b --- /dev/null +++ b/assets-source/trees/tree/1.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f3090dab2205063b31017f27eedb02ccecd55e163c0077c47290da74033551 +size 20954 diff --git a/assets-source/trees/tree/2.psd b/assets-source/trees/tree/2.psd new file mode 100644 index 0000000..832aea0 --- /dev/null +++ b/assets-source/trees/tree/2.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e3ad4b17af6259baeb7822c310b59fb1f0183adab364cfea2288e999835a221 +size 24046 diff --git a/assets-source/trees/tree/3.psd b/assets-source/trees/tree/3.psd new file mode 100644 index 0000000..e8d83b9 --- /dev/null +++ b/assets-source/trees/tree/3.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fb696240d5c85801181846bd89a42e6cb6880b66485db82b6bca8d239610085 +size 27992 diff --git a/assets-source/trees/tree/4.psd b/assets-source/trees/tree/4.psd new file mode 100644 index 0000000..0db9fc1 --- /dev/null +++ b/assets-source/trees/tree/4.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdef9e2b5e3cc26bcea33ef55c8883a11598c30663673f83da30cd7bbc8f6079 +size 39511 diff --git a/assets-source/trees/tree/5.psd b/assets-source/trees/tree/5.psd new file mode 100644 index 0000000..a046410 --- /dev/null +++ b/assets-source/trees/tree/5.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41ad1d4d9bcd6cd2a92eaa15836094822947406b894c0c4447c3a878baa4728f +size 49728 diff --git a/assets-source/trees/tree/6.psd b/assets-source/trees/tree/6.psd new file mode 100644 index 0000000..d970fa9 --- /dev/null +++ b/assets-source/trees/tree/6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cfddd09036786aa66b04c3371b670e1731b591ad7d6711accad946537e41f49 +size 127168 diff --git a/assets-source/trees/tree/palette_1_6.psd b/assets-source/trees/tree/palette_1_6.psd new file mode 100644 index 0000000..a6f9a59 --- /dev/null +++ b/assets-source/trees/tree/palette_1_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:921935d9889602f450089c9c397cdb5a907b4930a568db42050db8a47c7da005 +size 94430 diff --git a/assets-source/trees/tree/palette_2_6.psd b/assets-source/trees/tree/palette_2_6.psd new file mode 100644 index 0000000..2dfe329 --- /dev/null +++ b/assets-source/trees/tree/palette_2_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e0ba0caa0401b5e4edd92ca7b65de16a20efe7bc9bfc7e89e267bc68efd677e +size 124784 diff --git a/assets-source/trees/tree/palette_3_6.psd b/assets-source/trees/tree/palette_3_6.psd new file mode 100644 index 0000000..21e77e1 --- /dev/null +++ b/assets-source/trees/tree/palette_3_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f24fb644839d87b301777a4e469057def253b807282b1dde133ffb22b8d45aa +size 118186 diff --git a/assets-source/trees/tree/palette_4_6.psd b/assets-source/trees/tree/palette_4_6.psd new file mode 100644 index 0000000..6451f91 --- /dev/null +++ b/assets-source/trees/tree/palette_4_6.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27f22367af47de4805d994f0ab2c9665cc8010e55a49db43b31caf89669f5048 +size 122826 diff --git a/assets-source/walls/stone.psd b/assets-source/walls/stone.psd new file mode 100644 index 0000000..86b5c74 --- /dev/null +++ b/assets-source/walls/stone.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d085e3c69d7471d31e9337707a9045c3e63588fd21488dba9474c63c37d346f +size 104312 diff --git a/assets-source/walls/wood.psd b/assets-source/walls/wood.psd new file mode 100644 index 0000000..8b8f837 --- /dev/null +++ b/assets-source/walls/wood.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c3f24a96353b6aad7e18cab633ad35f8b94f7509b63dbe5e3d02b3ea2c159da +size 159587 diff --git a/assets/images/avatars.jpg b/assets/images/avatars.jpg new file mode 100644 index 0000000..4890514 --- /dev/null +++ b/assets/images/avatars.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc40f2b2941f341cb26e3969f2db182a7cd5798ad1a201b769876e8742fd9e8 +size 15594 diff --git a/assets/images/logo-120.png b/assets/images/logo-120.png new file mode 100644 index 0000000..f549120 --- /dev/null +++ b/assets/images/logo-120.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d497f3e9554f67c10b2bf21866722892132453f9b77d1e53799b2fb31274410a +size 3455 diff --git a/assets/images/logo-64.png b/assets/images/logo-64.png new file mode 100644 index 0000000..f7ade8e --- /dev/null +++ b/assets/images/logo-64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e188182f8ecc863920099a6c0d874e4c06fadb8a69045fbd73229377609945f4 +size 346 diff --git a/assets/images/logo-gray.png b/assets/images/logo-gray.png new file mode 100644 index 0000000..9dfd5c8 --- /dev/null +++ b/assets/images/logo-gray.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b8d07a84732ccad5f76f31f6f81a89c3490115a25f2b12d6165bae9f7b48658 +size 338 diff --git a/assets/images/logo-large.png b/assets/images/logo-large.png new file mode 100644 index 0000000..e9e5f14 --- /dev/null +++ b/assets/images/logo-large.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:186b3a9c32d4501807d3dae93a1c6d86b19d6b60acf8315b2db879a2839718d9 +size 5437 diff --git a/assets/images/logo-small.png b/assets/images/logo-small.png new file mode 100644 index 0000000..96f6148 --- /dev/null +++ b/assets/images/logo-small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38d1a38b3692eeaf1eefe0a99ae61c474ba0a228c40e468cdf43c4806b04130d +size 319 diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..6cf03b2 --- /dev/null +++ b/assets/images/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e578752564bd76c766ab90eb0d94d2691c3fdc021e26575c0d5d7f5a837259c1 +size 5695 diff --git a/assets/images/pony.png b/assets/images/pony.png new file mode 100644 index 0000000..ccacef6 --- /dev/null +++ b/assets/images/pony.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64b90e917d6a84fe39338378ad722c6e953201c02e8708a03819d91e25d95a60 +size 27217 diff --git a/assets/images/pony2.png b/assets/images/pony2.png new file mode 100644 index 0000000..85d90aa --- /dev/null +++ b/assets/images/pony2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f05c79a577cfc549b167a0345d87b0f31b71d4e7648f45b7365017469c20319f +size 558436 diff --git a/assets/images/pony2a.png b/assets/images/pony2a.png new file mode 100644 index 0000000..c0d660e --- /dev/null +++ b/assets/images/pony2a.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dff34fe8b8c89c56e82e527e539e872bb9573ac34f223c4810c1352c5bce5e58 +size 30005 diff --git a/assets/music/ambient.mp3 b/assets/music/ambient.mp3 new file mode 100644 index 0000000..db1185f --- /dev/null +++ b/assets/music/ambient.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14713956ff0987dfdce355091cf873d692168173d54e176f110a60895f29dd85 +size 1944181 diff --git a/assets/music/ambient.webm b/assets/music/ambient.webm new file mode 100644 index 0000000..8a8cd45 --- /dev/null +++ b/assets/music/ambient.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c5e2150a3c6233d30915fa0e067e3b0aec94243a33e3d7cd2624a489df7c004 +size 1670266 diff --git a/assets/music/bossanova.mp3 b/assets/music/bossanova.mp3 new file mode 100644 index 0000000..a3bc0e5 --- /dev/null +++ b/assets/music/bossanova.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac3ebfb5ce92e399ddc6a5b4502b9b13e6dc28ff7a8ba17ff42c789dd9e82eee +size 1495347 diff --git a/assets/music/bossanova.webm b/assets/music/bossanova.webm new file mode 100644 index 0000000..3e88be4 --- /dev/null +++ b/assets/music/bossanova.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:382fece55d93a534acc0a4f0be2174025f5619603545b8068cfe1c918355fc69 +size 1326880 diff --git a/assets/music/building.mp3 b/assets/music/building.mp3 new file mode 100644 index 0000000..54c1a06 --- /dev/null +++ b/assets/music/building.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69a487757b023dbd9710119208d0e1026ca7889dbb19d91b2b3494ed2fccc02f +size 3147903 diff --git a/assets/music/building.webm b/assets/music/building.webm new file mode 100644 index 0000000..25ce6fb --- /dev/null +++ b/assets/music/building.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb457182791bc1d7b786edf2fc85e6b937f630e93fe4dd8b237f06c22b08064f +size 2542388 diff --git a/assets/music/cave-crystals.mp3 b/assets/music/cave-crystals.mp3 new file mode 100644 index 0000000..131a492 --- /dev/null +++ b/assets/music/cave-crystals.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a8c18c5208cf0aec6cee75abab917a0307e0cc0440088e5bcdc1b69258cdf8c +size 2248037 diff --git a/assets/music/cave-crystals.webm b/assets/music/cave-crystals.webm new file mode 100644 index 0000000..66ff622 --- /dev/null +++ b/assets/music/cave-crystals.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:304fa16dd04afbba9b49a67ecf4315619760e83395bf8629b896743a240c7c71 +size 2010851 diff --git a/assets/music/cave-secrets.mp3 b/assets/music/cave-secrets.mp3 new file mode 100644 index 0000000..1133c54 --- /dev/null +++ b/assets/music/cave-secrets.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42dd600cb06ece8fd55f833cd0f2ab3b3d9637e427f1cbbaae9ad215ef89e27a +size 1866440 diff --git a/assets/music/cave-secrets.webm b/assets/music/cave-secrets.webm new file mode 100644 index 0000000..4d3ffc1 --- /dev/null +++ b/assets/music/cave-secrets.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c83bc964dd5b807a37b6a76c659243c096f39189f93e1121f2f97028f553093 +size 1641952 diff --git a/assets/music/clop.mp3 b/assets/music/clop.mp3 new file mode 100644 index 0000000..ffc15e1 --- /dev/null +++ b/assets/music/clop.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7337f1b122a6394d20b23e399d4b471c0c9b4ac214a68995a8d2bb92858a7001 +size 1034751 diff --git a/assets/music/clop.webm b/assets/music/clop.webm new file mode 100644 index 0000000..ca3ae80 --- /dev/null +++ b/assets/music/clop.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a9aeec98d294cdeab5772898e1909d95107cba4e2c9beb105bea656ffc6d159 +size 869236 diff --git a/assets/music/falling.mp3 b/assets/music/falling.mp3 new file mode 100644 index 0000000..6a1bebf --- /dev/null +++ b/assets/music/falling.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f3ee5e1cf48191a674e5cacf85f8a849c7f9bd7cef6b40bcf8d1e0182e3303c +size 2536847 diff --git a/assets/music/falling.webm b/assets/music/falling.webm new file mode 100644 index 0000000..a2c9015 --- /dev/null +++ b/assets/music/falling.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f2a84ab9b59dbffd525800f78ae3d65dea0c3a6ec85a16dca11925bbf684f33 +size 2222228 diff --git a/assets/music/fivefour.mp3 b/assets/music/fivefour.mp3 new file mode 100644 index 0000000..8f9f0cb --- /dev/null +++ b/assets/music/fivefour.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3e64fe898f2a1aa0e1ca3ed4eb7fe3f0b2eb1bb04395545e1fdb16fa1f3afbf +size 808224 diff --git a/assets/music/fivefour.webm b/assets/music/fivefour.webm new file mode 100644 index 0000000..7e70556 --- /dev/null +++ b/assets/music/fivefour.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9715ea2178011851b77ad8f32a8d0d8190fca50e54cbf64732d922918cfabd01 +size 665015 diff --git a/assets/music/ghost.mp3 b/assets/music/ghost.mp3 new file mode 100644 index 0000000..3148d98 --- /dev/null +++ b/assets/music/ghost.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12dfe209b5bbac6a548277c38bdf975b0c1861c00e9d45b64191e9ea45df9b18 +size 2074584 diff --git a/assets/music/ghost.webm b/assets/music/ghost.webm new file mode 100644 index 0000000..f8e33a7 --- /dev/null +++ b/assets/music/ghost.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44bab0842a46ed9c6a6289023e0961939f34c82f95d57f7f192429c41209d34f +size 1867640 diff --git a/assets/music/happy-house.mp3 b/assets/music/happy-house.mp3 new file mode 100644 index 0000000..6c9aa7c --- /dev/null +++ b/assets/music/happy-house.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b054239b3ecb6619071d2103c82abceec5bbf1df76543b6e679a25bcee4f3e5e +size 1947942 diff --git a/assets/music/happy-house.webm b/assets/music/happy-house.webm new file mode 100644 index 0000000..572cf41 --- /dev/null +++ b/assets/music/happy-house.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7b4ccaf0944801353d5609dd581ac52f8298cf5b4ce9b8ac8e226cfca2721ed +size 1596492 diff --git a/assets/music/hypnosis.mp3 b/assets/music/hypnosis.mp3 new file mode 100644 index 0000000..2e9a692 --- /dev/null +++ b/assets/music/hypnosis.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d756aea9297d90f8cb36d44b24fc524f10cf11fccde808a307190fc7a64361c +size 842071 diff --git a/assets/music/hypnosis.webm b/assets/music/hypnosis.webm new file mode 100644 index 0000000..54aa4b0 --- /dev/null +++ b/assets/music/hypnosis.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a766596bd787cecf324fa89c493c7e3c3991fdab75bd8bc8f7301098549261a6 +size 636217 diff --git a/assets/music/island.mp3 b/assets/music/island.mp3 new file mode 100644 index 0000000..d384b1c --- /dev/null +++ b/assets/music/island.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73556454345173f195e53d272daa139c8ba79be28739795a48992cc93883c437 +size 1770728 diff --git a/assets/music/island.webm b/assets/music/island.webm new file mode 100644 index 0000000..23a44bc --- /dev/null +++ b/assets/music/island.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d03a4c8dd86782878a8ca24842d47f88dba972180954134a2ae094d17d768ee4 +size 1525189 diff --git a/assets/music/largo.mp3 b/assets/music/largo.mp3 new file mode 100644 index 0000000..cbe56bc --- /dev/null +++ b/assets/music/largo.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1b8badd6cd42379d3d125e30d926a7bb7cdcf4f832d5c038ff52b5325348f7d +size 1076128 diff --git a/assets/music/largo.webm b/assets/music/largo.webm new file mode 100644 index 0000000..43618de --- /dev/null +++ b/assets/music/largo.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1768818b0bd00318f6847b9fec12fd9aa76db2ae837dbd590a7ecc14e12774b1 +size 861490 diff --git a/assets/music/musicbox.mp3 b/assets/music/musicbox.mp3 new file mode 100644 index 0000000..c3f0cab --- /dev/null +++ b/assets/music/musicbox.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a4aeb6e17e022faa15fed2909db009dab9426d1f3936a57e113ab4c2ffd2d76 +size 913960 diff --git a/assets/music/musicbox.webm b/assets/music/musicbox.webm new file mode 100644 index 0000000..8b69ff6 --- /dev/null +++ b/assets/music/musicbox.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:454981d57a4948b21982e823996b38212f3758865e89cb34b6053c89ec7cc465 +size 812985 diff --git a/assets/music/orchid.mp3 b/assets/music/orchid.mp3 new file mode 100644 index 0000000..b31e4d5 --- /dev/null +++ b/assets/music/orchid.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bd082129e4fc885f42286aca38e19a6ef7d10b2aceae3a752cc9d34fd4692b0 +size 1051420 diff --git a/assets/music/orchid.webm b/assets/music/orchid.webm new file mode 100644 index 0000000..f8a55c1 --- /dev/null +++ b/assets/music/orchid.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c50cb42c27ecb3214ffd0dc01a03b0bfca82c1b8e2b3c0ae0807a1a1bebaed6b +size 912724 diff --git a/assets/music/pumpkin.mp3 b/assets/music/pumpkin.mp3 new file mode 100644 index 0000000..00024d0 --- /dev/null +++ b/assets/music/pumpkin.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f220ea16d8f220fde53d9a7dc052f9b3373eeb3bdc0e636bcc2fd43df7790fc4 +size 3163368 diff --git a/assets/music/pumpkin.webm b/assets/music/pumpkin.webm new file mode 100644 index 0000000..306c124 --- /dev/null +++ b/assets/music/pumpkin.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cc3de3688db7cc86b89fe49f0208402fdb73962414dcfba6f34e53c71b54665 +size 2970407 diff --git a/assets/music/reindeer-winter.mp3 b/assets/music/reindeer-winter.mp3 new file mode 100644 index 0000000..a6288d3 --- /dev/null +++ b/assets/music/reindeer-winter.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94c9fa9de378cb0991e8ebd49fffc962fc2c7db4785d205519a5547514864335 +size 1345663 diff --git a/assets/music/reindeer-winter.webm b/assets/music/reindeer-winter.webm new file mode 100644 index 0000000..8be4298 --- /dev/null +++ b/assets/music/reindeer-winter.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a39b715c5a6536c43c9a48beebdb4964212ba818ac20a762a336b327832401 +size 1225776 diff --git a/assets/music/reindeer.mp3 b/assets/music/reindeer.mp3 new file mode 100644 index 0000000..84351c9 --- /dev/null +++ b/assets/music/reindeer.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10844c4cd9b1acd05aa0bc76da4e51b9464dd8d4eeed09d1cbdb3fbfbc4940ef +size 1345663 diff --git a/assets/music/reindeer.webm b/assets/music/reindeer.webm new file mode 100644 index 0000000..0b35c1e --- /dev/null +++ b/assets/music/reindeer.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff342db70ef5aaa4cb1614f634214c6197ff0ed307e2ed62cddb715d7fd42f22 +size 1084788 diff --git a/assets/music/scherzo.mp3 b/assets/music/scherzo.mp3 new file mode 100644 index 0000000..508a68d --- /dev/null +++ b/assets/music/scherzo.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76392046ae72ef0a08c466a88db73c39f8a138d4cc8fd443a18796b075d2692c +size 663608 diff --git a/assets/music/scherzo.webm b/assets/music/scherzo.webm new file mode 100644 index 0000000..745720c --- /dev/null +++ b/assets/music/scherzo.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0439ebc8e1cdc8529321e806e94e47751add27bf91a19333cea28d8b46c8ef52 +size 517344 diff --git a/assets/music/school.mp3 b/assets/music/school.mp3 new file mode 100644 index 0000000..fcf0559 --- /dev/null +++ b/assets/music/school.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d6d51ab6821affeef434114145e9182e972dbf69c97a82d2ff45fdd6c6c26f2 +size 2421072 diff --git a/assets/music/school.webm b/assets/music/school.webm new file mode 100644 index 0000000..c592d5d --- /dev/null +++ b/assets/music/school.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f83ee050d53c53591e111dc8e32b42c6ccc558199bd56617adaa1182acb14d0 +size 2075212 diff --git a/assets/music/season.mp3 b/assets/music/season.mp3 new file mode 100644 index 0000000..69f1d6e --- /dev/null +++ b/assets/music/season.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:936c61e0599cd91593f3771937f2315da8eeb57b2e4207bc831c6b436ef1606e +size 2369663 diff --git a/assets/music/season.webm b/assets/music/season.webm new file mode 100644 index 0000000..edcff25 --- /dev/null +++ b/assets/music/season.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43acac4b484cd477e3105fffe39b160d9dd197b4435249c55b2788d142d26700 +size 1988182 diff --git a/assets/music/sunny-island.mp3 b/assets/music/sunny-island.mp3 new file mode 100644 index 0000000..28e81b6 --- /dev/null +++ b/assets/music/sunny-island.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8792eb510753ab040491dff5895f363715a1414cdea23f09f5dc870a569a4eb +size 3250303 diff --git a/assets/music/sunny-island.webm b/assets/music/sunny-island.webm new file mode 100644 index 0000000..f9393a4 --- /dev/null +++ b/assets/music/sunny-island.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b264a74d7aff2bb48e1baa706a1e65822fcd1c7fcff5bc8e201f76bea91d9478 +size 2731417 diff --git a/assets/music/sweet-home.mp3 b/assets/music/sweet-home.mp3 new file mode 100644 index 0000000..6b206ff --- /dev/null +++ b/assets/music/sweet-home.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdce7e661f445a2414ac7cdda97ab521d866f2c2ab469a075d7437bb733dd9b5 +size 1897787 diff --git a/assets/music/sweet-home.webm b/assets/music/sweet-home.webm new file mode 100644 index 0000000..0463ddb --- /dev/null +++ b/assets/music/sweet-home.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:965d1cb48798f3e57edbbb29c1805fc2ab0c82f6f74f41acf4cfb6e3f1465e8e +size 1569237 diff --git a/assets/music/tio.mp3 b/assets/music/tio.mp3 new file mode 100644 index 0000000..fbf3066 --- /dev/null +++ b/assets/music/tio.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7f282af07ad05975356bf08f430786dfc27a89e9e9bfca9b4a1ce61bc0d9cfa +size 1958809 diff --git a/assets/music/tio.webm b/assets/music/tio.webm new file mode 100644 index 0000000..b67f603 --- /dev/null +++ b/assets/music/tio.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed502a1b0dea63c8f860dcd51a7236b9ad9ed516bd2f779e1ccef166b23f50f9 +size 1628227 diff --git a/assets/music/trees-winter.mp3 b/assets/music/trees-winter.mp3 new file mode 100644 index 0000000..6ecd96c --- /dev/null +++ b/assets/music/trees-winter.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ab0f493ebf399b2c7585b312276fd407b802cdc642f1df8c4ef2c39d9663d54 +size 2535593 diff --git a/assets/music/trees-winter.webm b/assets/music/trees-winter.webm new file mode 100644 index 0000000..4d042dd --- /dev/null +++ b/assets/music/trees-winter.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebdbc639f09e5c476256bb463604b9922de5576fda409cef00315703f9d4cb49 +size 2405965 diff --git a/assets/music/trees.mp3 b/assets/music/trees.mp3 new file mode 100644 index 0000000..1618e80 --- /dev/null +++ b/assets/music/trees.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20bb612d828ae38c97d1eca8a018016f535bbcd3d7a8678763a1076068425665 +size 2535593 diff --git a/assets/music/trees.webm b/assets/music/trees.webm new file mode 100644 index 0000000..a0007cd --- /dev/null +++ b/assets/music/trees.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333133a87038d26b8eb187fd7ad90db475b7d89270ec647b120e9cf2a0e1fb89 +size 2228537 diff --git a/assets/music/trills.mp3 b/assets/music/trills.mp3 new file mode 100644 index 0000000..f5a39be --- /dev/null +++ b/assets/music/trills.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f47e0220a31807efb2744df22e6cc1150fcec5bd0b4eb7cbd8064100f9f1e0c +size 1086528 diff --git a/assets/music/trills.webm b/assets/music/trills.webm new file mode 100644 index 0000000..34d4284 --- /dev/null +++ b/assets/music/trills.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90036609822961a50e1aeed28f222e09445fdc154f9bff66f4bb660b127419e5 +size 929845 diff --git a/assets/music/unrest.mp3 b/assets/music/unrest.mp3 new file mode 100644 index 0000000..03e3f37 --- /dev/null +++ b/assets/music/unrest.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:924768073bf5c095443dda335083525e47513af964ab3b9503ba4fba63f32a8b +size 1610280 diff --git a/assets/music/unrest.webm b/assets/music/unrest.webm new file mode 100644 index 0000000..155aeee --- /dev/null +++ b/assets/music/unrest.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cf1a117b725dbd863093edea7018b84095e9569332b6d3bcd1fb4cadb07c28a +size 1392246 diff --git a/assets/music/waltzalt.mp3 b/assets/music/waltzalt.mp3 new file mode 100644 index 0000000..2b0af7f --- /dev/null +++ b/assets/music/waltzalt.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:024ab0ceefd191f252257e74c35c9c62793d5ace4d1998dc5b3b2d4c1031c971 +size 1657043 diff --git a/assets/music/waltzalt.webm b/assets/music/waltzalt.webm new file mode 100644 index 0000000..466a33e --- /dev/null +++ b/assets/music/waltzalt.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc935035d8a2acefae443f2ec8e42c5ef37da502d7a2f37f5f8cf4a218cb57c3 +size 1305109 diff --git a/assets/music/xmas-air.mp3 b/assets/music/xmas-air.mp3 new file mode 100644 index 0000000..a5a3496 --- /dev/null +++ b/assets/music/xmas-air.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:415ea408999d926155a9a135dfd2a9a98c2173d29e3faab0912e3f8b09899481 +size 1708870 diff --git a/assets/music/xmas-air.webm b/assets/music/xmas-air.webm new file mode 100644 index 0000000..42b65a0 --- /dev/null +++ b/assets/music/xmas-air.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f716fdbbe3242ccfcb4528394cb0c56624f86a1dbf0765e85cf8e7002dd9bef +size 1545206 diff --git a/assets/music/xmas-horns.mp3 b/assets/music/xmas-horns.mp3 new file mode 100644 index 0000000..19798a0 --- /dev/null +++ b/assets/music/xmas-horns.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b83a72537f5113a60c027dec32187c97586decefdece26ac4647f9b6c75a64a +size 2902979 diff --git a/assets/music/xmas-horns.webm b/assets/music/xmas-horns.webm new file mode 100644 index 0000000..71cf198 --- /dev/null +++ b/assets/music/xmas-horns.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ab4a0e1fa72647ee9bf21ef374ca7910acc2ba00666bf9468519b7a2a6adf9b +size 2475388 diff --git a/assets/music/xmas-presents.mp3 b/assets/music/xmas-presents.mp3 new file mode 100644 index 0000000..afe1599 --- /dev/null +++ b/assets/music/xmas-presents.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d32950b8dfbb36078b0b6b0c93045387fed0f8a98d29403ace88690f7ec384b +size 1825062 diff --git a/assets/music/xmas-presents.webm b/assets/music/xmas-presents.webm new file mode 100644 index 0000000..ef9ed2c --- /dev/null +++ b/assets/music/xmas-presents.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8395dd64e92f5123aa45085d9cbda710d766e82d8d12facf5d88d450ffddcbe4 +size 1599833 diff --git a/cli.js b/cli.js new file mode 100644 index 0000000..0249638 --- /dev/null +++ b/cli.js @@ -0,0 +1,319 @@ +require('./src/scripts/server/boot'); +const mongoose = require('mongoose'); +const fs = require('fs'); +const _ = require('lodash'); +const { decompressPony } = require('./src/scripts/common/compressPony'); +const ponyInfo = require('./src/scripts/common/ponyInfo'); +const db = require('./src/scripts/server/db'); +const config = require('./src/scripts/server/config').config; +const argv = require('yargs').option('addrole', { type: 'array' }).option('removerole', { type: 'array' }).argv; + +mongoose.connect(config.db, { + reconnectTries: Number.MAX_VALUE, + useNewUrlParser: true, + useCreateIndex: true, + useFindAndModify: false, +}); + +function iterate(query, onData, onEnd = () => process.exit()) { + query.cursor().on('data', onData).on('end', onEnd); +} + +function generatePonyStats() { + const sets = {}; + const fields = {}; + const defaultPony = ponyInfo.createDefaultPony(); + const ignore = [ + 'cm', 'eyeshadowColor', 'eyeWhites', 'eyeColorRight', 'eyeColorLeft', 'coatFill', 'coatOutline', + 'frecklesColor', + ]; + const keys = Object.keys(defaultPony).filter(key => !_.includes(ignore, key)); + const setFields = keys.filter(key => typeof defaultPony[key] === 'object' && 'type' in defaultPony[key]); + const otherFields = keys.filter(key => typeof defaultPony[key] !== 'object' || !('type' in defaultPony[key])); + let done = 0; + + setFields.forEach(key => sets[key] = new Map()); + otherFields.forEach(key => fields[key] = new Map()); + + iterate(db.Character.find({}, '_id name info'), doc => { + try { + const pony = decompressPony(doc.info); + + for (const key of setFields) { + const stats = sets[key]; + const { type = 0, pattern = 0 } = pony[key] || {}; + const row = stats.get(type) || new Map(); + stats.set(type, row); + row.set(pattern, 1 + (row.get(pattern) || 0)); + } + + for (const key of otherFields) { + const stats = fields[key]; + const value = pony[key]; + stats.set(value, 1 + (stats.get(value) || 0)); + } + + if ((++done % 10000) === 0) { + console.log('done', done); + } + } catch (e) { + console.log(e); + console.log('id: ', doc._id); + console.log('info: ', doc._info); + } + }, () => { + const setsData = _.flatten(Object.keys(sets) + .sort() + .map(key => { + const patterns = _.range(0, 1 + _.max(entries(sets[key]).map(e => _.max(entries(e[1]).map(x => x[0]))))); + + return [ + [], + [key], + ['', '', 'pattern'], + ['', '', ...patterns], + ...entries(sets[key]) + .sort(([a], [b]) => a - b) + .map(([key, value], i) => [i === 0 ? 'type' : '', key, ...patterns.map(pat => value.get(pat) || 0)]), + ]; + })); + + const fieldsData = _.flatten(Object.keys(fields) + .sort() + .map(key => [ + [], + [key], + ...entries(fields[key]).sort(([a], [b]) => a - b).map(([key, value]) => ['', key, value]), + ])); + + fs.writeFileSync('stats.csv', `${toCsv(setsData)}\n\n${toCsv(fieldsData)}`, 'utf8'); + console.log('done'); + process.exit(); + }); + + function entries(map) { + return Array.from(map.entries()); + } + + function toCsv(data) { + return data.map(x => x.map(v => v === undefined ? 'undefined' : v).join(';')).join('\n'); + } +} + +function fixCharacterStates() { + const config = require('./config.json'); + + db.Character.countDocuments({ state: { $exists: true } }, (err, total) => { + let migrated = 0; + let updating = 0; + let done = false; + + if (err) { + console.error(err); + process.exit(); + } + + console.log('started', total); + + iterate(db.Character.find({ state: { $exists: true } }, '_id name state'), doc => { + const before = doc.state; + const after = {}; + + for (const { id, name } of config.servers) { + const state = _.clone(before[name]); + + if (state) { + if (!state.hold) { + delete state.hold; + } + + if (!state.map) { + delete state.map; + } + + const flags = (state.right ? 1 : 0) | (state.extra ? 2 : 0); + delete state.right; + delete state.extra; + + if (flags) { + state.flags = flags; + } + + after[id] = state; + } + + if (before[id]) { + after[id] = _.clone(before[id]); + } + } + + updating++; + db.Character.update({ _id: doc._id }, { state: after }, e => { + migrated++; + updating--; + + if (e) { + console.error(e); + } + + if ((migrated % 10000) === 0) { + console.log('migrated', migrated, 'of', total); + } + + if (done && updating === 0) { + console.log('ALL DONE'); + } + }); + }, () => { + done = true; + console.log('done iterating'); + }); + }); +} + +async function updateCharacterPositions() { + const servers = ['main', 'safe', 'safe-ru']; + const total = await db.Character.countDocuments({ state: { $exists: true } }).exec(); + let done = 0; + + console.log('updating', total); + + iterate(db.Character.find({ state: { $exists: true } }, '_id name state'), doc => { + const state = doc.state; + + if (state) { + let changed = false; + + for (const server of servers) { + if (state[server]) { + state[server].x += 40; + state[server].y += 40; + changed = true; + } + } + + if (changed) { + db.Character.updateOne({ _id: doc._id }, { state }, () => { + done++; + + if ((done % 10000) === 0) { + console.log(`done ${done} / ${total}`); + } + }); + } else { + done++; + + if ((done % 10000) === 0) { + console.log(`done ${done} / ${total}`); + } + } + } + }, () => console.log('done')); +} + +async function createPerfTestAccounts() { + const ponies = await db.Character.find({ account: '57ae2336a67f4dc52e123ed1' }).exec(); + + for (let i = 0; i < 1000; i++) { + const account = await db.Account.create({ name: `perf-${i}` }); + const char = ponies[i % ponies.length]; + await db.Character.create({ account: account._id, name: `perf-${i}`, info: char.info }); + } + + process.exit(); +} + +function settingsStats() { + let filterSwearWords = [0, 0]; + let filterCyrillic = [0, 0]; + let ignorePartyInvites = [0, 0]; + let ignorePublicChat = [0, 0]; + let seeThroughObjects = [0, 0]; + let chatlogOpacity = _.times(100, () => 0); + let chatlogRange = _.times(13, () => 0); + let filterWords = []; + + iterate(db.Account.find({}, 'settings'), doc => { + if (doc.settings) { + filterSwearWords[doc.settings.filterSwearWords | 0]++; + filterCyrillic[doc.settings.filterCyrillic | 0]++; + ignorePartyInvites[doc.settings.ignorePartyInvites | 0]++; + ignorePublicChat[doc.settings.ignorePublicChat | 0]++; + seeThroughObjects[doc.settings.seeThroughObjects | 0]++; + chatlogOpacity[doc.settings.chatlogOpacity | 0]++; + chatlogRange[doc.settings.chatlogRange | 0]++; + doc.settings.filterWords && filterWords.push(doc.settings.filterWords); + } + }, () => { + const pad = 20; + filterWords.forEach(x => console.log(JSON.stringify(x))); + console.log('---------------------------------------------'); + console.log(`${_.padStart(`filterSwearWords`, pad)}: ${filterSwearWords.join(', ')}`); + console.log(`${_.padStart(`filterCyrillic`, pad)}: ${filterCyrillic.join(', ')}`); + console.log(`${_.padStart(`ignorePartyInvites`, pad)}: ${ignorePartyInvites.join(', ')}`); + console.log(`${_.padStart(`ignorePublicChat`, pad)}: ${ignorePublicChat.join(', ')}`); + console.log(`${_.padStart(`seeThroughObjects`, pad)}: ${seeThroughObjects.join(', ')}`); + console.log(`${_.padStart(`chatlogRange`, pad)}: ${chatlogRange.join(', ')}`); + console.log(`${_.padStart(`chatlogOpacity`, pad)}: ${chatlogOpacity.join(', ')}`); + console.log('done'); + process.exit(); + }); +} + +async function clearOrphanedIgnores() { + const ids = new Set(); + let removed = 0; + + iterate(db.Account.find({}, '_id').lean(), doc => ids.add(doc._id.toString()), () => { + iterate(db.Account.find({ ignores: { $exists: true, $not: { $size: 0 } } }, '_id ignores').lean(), doc => { + const remove = []; + + if (doc.ignores) { + for (const ignore of doc.ignores) { + if (!ids.has(ignore) || doc._id.toString() === ignore) { + remove.push(ignore); + } + } + } + + if (remove.length) { + removed += remove.length; + db.Account.updateOne({ _id: doc._id }, { $pull: { ignores: remove } }, err => console.log(err || 'removed')); + } + }, () => { + console.log('done, removed: ', removed); + process.exit(); + }); + }); +} + +async function toggleRole(give, id, roles) { + if (typeof id === 'string' && id.length === 24 && ['superadmin', 'admin', 'mod', 'dev'].indexOf(roles) !== -1) { + if (give) { + await db.Account.updateOne({ _id: id }, { $addToSet: { roles } }).exec(); + } else { + await db.Account.updateOne({ _id: id }, { $pull: { roles } }).exec(); + } + console.log('done'); + } else { + console.log('error: usage: node cli.js --addrole '); + console.log(' roles: superadmin, admin, mod, dev'); + } + + process.exit(); +} + +if (argv.settingsstats) { + settingsStats(); +} else if (argv.shift) { + updateCharacterPositions(); +} else if (argv.clearignores) { + clearOrphanedIgnores(); +} else if (argv.addrole) { + toggleRole(true, argv.addrole[0], argv.addrole[1]); +} else if (argv.removerole) { + toggleRole(false, argv.removerole[0], argv.removerole[1]); +} else { + console.log('invalid arg'); + process.exit(); +} diff --git a/config-template.json b/config-template.json new file mode 100644 index 0000000..5d78dba --- /dev/null +++ b/config-template.json @@ -0,0 +1,33 @@ +{ + "title": "Your Town", + "twitterLink": "", + "contactEmail": "your_contact_email@example.com", + "port": 8090, + "adminPort": 8091, + "host": "http://localhost:8090/", + "local": "localhost:8090", + "adminLocal": "localhost:8091", + "secret": "gfhfdshtrdhgedryhe4t3y5uwjthr", + "token": "sdlfgihsdor8ghor8dgdrgdegrdg", + "db": "mongodb://:@localhost:27017/", + "oauth": { + "google": { + "clientID": "", + "clientSecret": "" + } + }, + "assetsPath": "assets-source", + "servers": [ + { + "id": "test", + "port": 8090, + "path": "/s00/ws", + "local": "localhost:8090", + "name": "Test server", + "desc": "Testing server", + "flags": { + "test": true + } + } + ] +} \ No newline at end of file diff --git a/favicons/android-chrome-192x192.png b/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..aeb92de --- /dev/null +++ b/favicons/android-chrome-192x192.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f1b68f39bdf2ac437a87dc0668b26666f4d8bc8b5c5677604788ea72f3a801f +size 11339 diff --git a/favicons/android-chrome-512x512.png b/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..ea90192 --- /dev/null +++ b/favicons/android-chrome-512x512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9972880151c008c82331c0c1f9f75c414d8e525547f68ad3fb640e9aa44213e +size 23052 diff --git a/favicons/apple-touch-icon-152x152.png b/favicons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..c40e7ef --- /dev/null +++ b/favicons/apple-touch-icon-152x152.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3413c24bd7fca18ec20279b83bd802aa52e3a0da1ea15f243a97744b5af6a1f +size 3361 diff --git a/favicons/apple-touch-icon-precomposed.png b/favicons/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..4305fe6 --- /dev/null +++ b/favicons/apple-touch-icon-precomposed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c84f024234375d9f0773372095acc73819fb6746fee4a773ca7be019c86fe76f +size 6838 diff --git a/favicons/apple-touch-icon.png b/favicons/apple-touch-icon.png new file mode 100644 index 0000000..da76dd2 --- /dev/null +++ b/favicons/apple-touch-icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f6b2f3fa6b3270c2e03f12e92a3a9fb5fddff57b7564b94dc2a437a429d489b +size 5727 diff --git a/favicons/browserconfig.xml b/favicons/browserconfig.xml new file mode 100644 index 0000000..8b274a2 --- /dev/null +++ b/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #e6c966 + + + diff --git a/favicons/favicon-16x16.png b/favicons/favicon-16x16.png new file mode 100644 index 0000000..3df7af7 --- /dev/null +++ b/favicons/favicon-16x16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0c39fa4757cd50125c7433e6ebba04a59fc522908df17050cffd31ca33cea77 +size 1207 diff --git a/favicons/favicon-194x194.png b/favicons/favicon-194x194.png new file mode 100644 index 0000000..e9a378a --- /dev/null +++ b/favicons/favicon-194x194.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e95aecf52de15e4afd21f34c16fed5a4892eb59059dffb8084e40972bf5b1412 +size 6315 diff --git a/favicons/favicon-32x32.png b/favicons/favicon-32x32.png new file mode 100644 index 0000000..0586664 --- /dev/null +++ b/favicons/favicon-32x32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74f1810561381c8d1698bb0f831cc62c9d5cf919ef8d4799f735564153d6e77b +size 1995 diff --git a/favicons/favicon.ico b/favicons/favicon.ico new file mode 100644 index 0000000..feee896 --- /dev/null +++ b/favicons/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36f7dacdebd4b605584d5d9a81b53c1172b4dc72d84154d6e6206c6e092ec2b3 +size 15086 diff --git a/favicons/manifest.json b/favicons/manifest.json new file mode 100644 index 0000000..9a6345b --- /dev/null +++ b/favicons/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Pony Town", + "short_name": "Pony Town", + "start_url": "/", + "scope": "/", + "theme_color": "#333333", + "background_color": "#333333", + "display": "standalone", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/favicons/mstile-150x150.png b/favicons/mstile-150x150.png new file mode 100644 index 0000000..1ac70ae --- /dev/null +++ b/favicons/mstile-150x150.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99c8272f4be3f6cbbe43fbba881e644e2a746acaad45e8e237a07ce799f5c5a8 +size 5976 diff --git a/favicons/safari-pinned-tab.svg b/favicons/safari-pinned-tab.svg new file mode 100644 index 0000000..512b8cb --- /dev/null +++ b/favicons/safari-pinned-tab.svg @@ -0,0 +1,32 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..ea1abe0 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,339 @@ +require('source-map-support').install(); + +const fs = require('fs'); +const path = require('path'); +const gulp = require('gulp'); +const _ = require('lodash'); +const gulpif = require('gulp-if'); +const rev = require('gulp-rev'); +const sass = require('gulp-sass'); +const shell = require('gulp-shell'); +const cssnano = require('gulp-cssnano'); +const imagemin = require('gulp-imagemin'); +const autoprefixer = require('gulp-autoprefixer'); +const liveServer = require('gulp-live-server'); +const sizereport = require('gulp-sizereport'); +const markdownTree = require('markdown-tree'); +const del = require('del'); +const mocha = require('gulp-spawn-mocha'); +const remapIstanbul = require('remap-istanbul/lib/gulpRemapIstanbul'); +const spawn = require('child_process').spawn; +const argv = require('yargs').argv; +const config = require('./config.json'); +const stamp = Math.floor(Math.random() * 0xffffffff); +const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; +const HASH = _.range(0, 10).map(() => chars[Math.floor(Math.random() * chars.length)]).join(''); + +let development = true; + +function swallowError(e) { + console.log(e.message); + this.emit('end'); +} + +const runAsync = (command, args) => new Promise((resolve, reject) => { + const proc = spawn(command, args) + .on('error', reject) + .on('exit', resolve); + + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); +}); + +const readFile = src => fs.readFileSync(src, { encoding: 'utf-8' }); + +const lintCode = code => (code.trim() + '\n') + .replace(/\r\n/g, '\n') + .replace(/\n/g, '\r\n') + .replace(/ /g, '\t') + .replace(/"/g, "'"); + +const npmScript = (name, args = []) => { + const func = () => runAsync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', name, ...args]); + func.displayName = `npm run ${name}`; + return func; +}; + +const clean = () => del([ + 'build/*', + 'temp/*', + '!temp/.gitignore', +]); + +const clearnAdmin = () => del([ + 'build/assets-admin', +]); + +const manifest = cb => { + const json = { + name: config.title, + short_name: config.title, + start_url: '/', + scope: '/', + theme_color: '#333333', + background_color: '#333333', + display: 'standalone', + icons: [ + { + src: '/android-chrome-192x192.png', + sizes: '192x192', + type: 'image/png' + }, + { + src: '/android-chrome-512x512.png', + sizes: '512x512', + type: 'image/png' + } + ] + }; + + fs.writeFile('src/ts/generated/changelog.ts', JSON.stringify(json, null, 2), 'utf8', cb); +}; + +const sprites = () => Promise.resolve() // del(['tools/output/images/*']) + .then(() => runAsync('node', ['src/scripts/tools/create-sprites.js'])) + .then(() => gulp.src('tools/output/images/*') + .pipe(gulpif(!argv.fast, imagemin())) + .pipe(gulp.dest('assets/images'))); + +const changelog = cb => { + const changelog = readFile('CHANGELOG.md'); + const tree = markdownTree(changelog); + const object = config.nochangelog ? [] : tree.children.map(c => ({ + version: c.text, + changes: c.tokens + .map(t => t.text) + .filter(x => x) + .map(x => x.replace(/^\[test\]/, 'test')), + })); + const type = `{ version: string; changes: string[]; }[]`; + const code = `/* tslint:disable */\n\nexport const CHANGELOG: ${type} = ${JSON.stringify(object, null, 2)};\n`; + fs.writeFile('src/ts/generated/changelog.ts', code, 'utf8', cb); +}; + +const icons = cb => { + const root1 = path.join('node_modules', '@fortawesome', 'free-solid-svg-icons'); + const root2 = path.join('node_modules', '@fortawesome', 'free-brands-svg-icons'); + + const getIconCode = src => JSON.stringify(require(`./${src}`).definition); + const iconsTs = readFile('src/ts/client/icons.ts'); + const matched = _.uniq(iconsTs.match(/\bfa[A-Z]\S*\b/g)); + const icons = matched.map(m => ({ + name: m, + code: fs.existsSync(path.join(root1, `${m}.js`)) ? getIconCode(path.join(root1, `${m}.js`)) : getIconCode(path.join(root2, `${m}.js`)), + })).sort((a, b) => a.name.localeCompare(b.name)); + const code = `/* tslint:disable */\n\n${icons.map(({ name, code }) => `export const ${name} = ${code};`).join('\n')}`; + fs.writeFile('src/ts/generated/fa-icons.ts', lintCode(code), 'utf8', cb); +}; + +const shaders = cb => { + function getShaderCode(filePath) { + return fs.readFileSync(filePath, 'utf8') + .replace(/^\s*\n/gm, '').trim(); + } + + const dir = path.join('src', 'ts', 'graphics', 'shaders'); + const code = '/* tslint:disable */\n\n' + fs.readdirSync(dir) + .map(file => [_.camelCase(file.replace(/\.glsl$/, '')), path.join(dir, file)]) + .map(([name, filePath]) => `export const ${name}Shader = \`${getShaderCode(filePath)}\`;`) + .join('\n\n'); + fs.writeFile('src/ts/generated/shaders.ts', lintCode(code), 'utf8', cb); +}; + +const hash = cb => { + const code = `export const HASH = '${HASH}';\nexport const STAMP = ${stamp};`; + fs.writeFileSync('src/ts/generated/hash.ts', lintCode(code), 'utf8'); + fs.writeFile('src/ts/generated/hash.json', JSON.stringify({ hash: HASH, stamp }), 'utf8', cb); +}; + +const rollbar = cb => { + const { environment, clientToken } = config.rollbar || {}; + const code = `export const ROLLBAR_ENV = '${environment}';\nexport const ROLLBAR_TOKEN = '${clientToken}';`; + fs.writeFile('src/ts/generated/rollbarConfig.ts', lintCode(code), 'utf8', cb); +}; + +const assetsRev = cb => { + const json = fs.readFileSync('build/rev-manifest.json', 'utf8'); + const data = _.mapValues(JSON.parse(json), value => value.replace(/^\S+-([a-f0-9]{10})\.\S+$/, '$1')); + const code = `export const REV: { [key: string]: string; } = ${JSON.stringify(data, null, 4)};`; + fs.writeFile('src/ts/generated/rev.ts', lintCode(code), 'utf8', cb); +}; + +const assetsCopy = () => gulp.src('assets/**/*') + .pipe(gulpif(!argv.fast, imagemin())) + .pipe(rev()) + .pipe(gulp.dest('build/assets')) + .pipe(rev.manifest()) + .pipe(gulp.dest('build')); + +function buildSass(name, src, dest) { + const result = () => gulp.src([src], { base: 'src' }) + .pipe(sass({ + includePaths: ['src/styles/'], + }).on('error', sass.logError)) + .pipe(autoprefixer('last 2 versions')) + .pipe(gulpif(!development, cssnano({ discardComments: { removeAll: true } }))) + .pipe(gulpif(!development, rev())) + .pipe(gulp.dest(dest)); + result.displayName = `sass (${name})`; + return result; +} + +const sassMain = buildSass('main', 'src/styles/style.scss', 'build/assets'); +const sassInline = buildSass('inline', 'src/styles/style-inline.scss', 'build/assets'); +const sassTools = buildSass('tools', 'src/styles/style-tools.scss', 'build/assets'); +const sassAdmin = buildSass('admin', 'src/styles/style-admin.scss', 'build/assets-admin'); +const sassTasks = gulp.series(sassMain, sassInline, sassTools, sassAdmin); + +const testScripts = ['src/scripts/tests/**/*.js']; +const ts = npmScript('ts'); + +const tests = () => gulp.src(testScripts, { read: false }) + .pipe(mocha({ + exit: true, + reporter: 'progress', + timeout: 10000, + })) + .on('error', swallowError); + +const coverage = () => gulp.src(testScripts, { read: false }) + .pipe(mocha({ + exit: true, + reporter: 'progress', + timeout: 10000, + istanbul: { + print: 'none', + }, + })); + +const remap = () => gulp.src('coverage/coverage.json') + .pipe(remapIstanbul({ reports: { html: 'coverage-remapped' } })); + +const size = () => gulp.src([ + 'build/assets/*.js', + 'build/assets/scripts/*.js', + 'build/assets-admin/scripts/*.js', + 'build/assets/styles/*.css', + 'build/assets-admin/styles/*.css', +]).pipe(sizereport({ gzip: true, total: true })); + +const music = () => gulp.src(path.join(config.assetsPath, 'assets/music/*.wav'), { read: false }) + .pipe(shell([ + 'ffmpeg -y -i "<%= file.path %>" -acodec libmp3lame "<%= out(file.path, ".mp3") %>"', + 'ffmpeg -y -i "<%= file.path %>" -acodec libvorbis "<%= out(file.path, ".webm") %>"', + ], { + templateData: { + out: (file, ext) => path.join('assets', 'music', path.basename(file, '.wav') + ext), + } + })); + +const serverDev = cb => { + if (!argv.noserver) { + const serverPath = path.join('src', 'scripts', 'server', 'server.js'); + const options = { env: { NODE_OPTIONS: '--inspect' } }; + const commonArgs = [serverPath, '--inspect', '--color', '--beta', '--admin']; + const server = argv.adm ? + liveServer([...commonArgs, '--adm'], options) : + liveServer([...commonArgs, '--login', '--game', '--tools'], options); + server.start(); + + const restart = cb => { + server.start(); + cb(); + }; + + gulp.watch(['build/**/*.css']).on('change', path => server.notify({ path })); + gulp.watch(['build/**/*.js']).on('change', path => server.notify({ path })); + gulp.watch([ + 'src/scripts/common/**/*.js', + 'src/scripts/generated/**/*.js', + 'src/scripts/graphics/**/*.js', + 'src/scripts/server/**/*.js', + 'views/index.pug', + ], { debounceDelay: 1000 }, restart); + } + + cb(); +}; + +let webpackScript = 'webpack-prod'; +const webpackArgs = ['--']; + +if (argv.parallel) { + webpackScript = 'webpack-prod-parallel'; +} + +if (argv.debug) { + webpackScript = 'webpack-debug'; +} + +if (argv.main) { + webpackScript = 'webpack-main'; +} + +if (argv.beta) { + webpackArgs.push('--env.beta'); +} + +if (argv.timing) { + webpackArgs.push('--env.timing'); +} + +const webpackProd = npmScript(webpackScript, webpackArgs); +const webpackAdmin = npmScript('webpack-admin'); +const sw = npmScript('sw'); + +const assets = gulp.series(assetsCopy, assetsRev); +const common = gulp.series(manifest, hash, rollbar, changelog, icons, shaders, assets, sassTasks); +const covRemap = gulp.series(coverage, remap); + +const watch = cb => { + gulp.watch(['CHANGELOG.md'], changelog); + gulp.watch(['src/ts/client/icons.ts'], icons); + gulp.watch(['src/styles/**/*.scss'], sassTasks); + gulp.watch(['src/ts/graphics/shaders/*.glsl'], shaders); + + if (argv.coverage || argv.tests) { + gulp.watch(['src/scripts/**/*.js'], { debounceDelay: 1000 }, argv.coverage ? covRemap : tests); + } + + cb(); +}; + +const watchTools = cb => { + if (argv.sprites) { + // gulp.watch(['src/scripts/tools/**/*.js'], { debounceDelay: 1000 }, sprites); + gulp.watch(['src/ts/tools/trigger.txt'], sprites); + gulp.watch(['assets/**/*'], { debounceDelay: 1000, readDelay: 1000 }, assets); + // gulp.watch([path.join(config.assetsPath, 'assets/**/*')], { debounceDelay: 1000, readDelay: 1000 }, sprites); + } + + cb(); +}; + +const watchTests = cb => { + const task = argv.coverage ? covRemap : tests; + gulp.watch(['src/scripts/**/*.js', 'src/tests/**/*.txt', 'src/tests/**/*.png'], { debounceDelay: 1000 }, task); + cb(); +}; + +const setProd = cb => { + development = false; + cb(); +}; + +const empty = cb => cb(); +const spritesTask = argv.sprites ? sprites : empty; + +const build = gulp.series(clean, setProd, common, ts, webpackProd, sw, size); +const admin = gulp.series(clearnAdmin, setProd, sassAdmin, ts, webpackAdmin); +const dev = gulp.series(clean, spritesTask, common, gulp.parallel(serverDev, watch, watchTools)); + +module.exports = { + music, + admin, + build, + dev, + default: dev, + test: watchTests, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0b012ee --- /dev/null +++ b/package-lock.json @@ -0,0 +1,23105 @@ +{ + "name": "pony-town", + "version": "0.53.0-alpha", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/core": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.1.tgz", + "integrity": "sha512-H92REifFygP4a/0fsnshiz31KsuaHRrRHwfT67tVxPnN8rwWnTWkf5gxFwG8ezcBMnR5eVLBb/Xisnc/+A/8Kw==", + "requires": { + "ajv": "6.10.2", + "fast-json-stable-stringify": "2.0.0", + "magic-string": "0.25.3", + "rxjs": "6.4.0", + "source-map": "0.7.3" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "@angular/animations": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-8.2.3.tgz", + "integrity": "sha512-kumEi9s8/cCNkndmETbAN7eSBfPkHGgX1up8ynsRgpjX7PTiGpsKMP0rxJBt281fqQPL/D6G3yZOyC5YfsP5+g==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/cdk": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.1.4.tgz", + "integrity": "sha512-U0tjhAZM4qqUHGGKIKFZZ9d8929s2U/HO0FdoT/eX+oHPOA6mm3bBep7myrSzlkP9V7O1JJfYdhEgijXhYxprQ==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^1.7.1" + } + }, + "@angular/common": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.2.3.tgz", + "integrity": "sha512-Y8ttd66bAApnJgxjndVnBP0aMsID8AzttR2TKp1KpdDKy4n6td22+1DeHZJqPMOBwoTDEdjHZqFQ7XRNYe3IVw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.3.tgz", + "integrity": "sha512-CDUiYXdy7jIwhK3aeBwz4Bik3eQUORVWDVuO8fsVot7CODE4jIkETUdeWuYuEpwPX7dkjsFvuT4btiwzZfrCUg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler-cli": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.3.tgz", + "integrity": "sha512-KdUBhno+b9hIjM3lQOm39IHgSCex6vJa5dmjfCfcNUhT3BVs2nTtJ4uLMqJMI776KCf3y3zNJ5XUd8jMpgoxdQ==", + "requires": { + "canonical-path": "1.0.0", + "chokidar": "^2.1.1", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.7.2", + "magic-string": "^0.25.0", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "source-map": "^0.6.1", + "tslib": "^1.9.0", + "yargs": "13.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.1.0.tgz", + "integrity": "sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg==", + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "@angular/core": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.2.3.tgz", + "integrity": "sha512-wNlq6r0RI8q0BCr3I16NHfteliJLVjZN3MsLfzYi4u08LcGTemGL9YwcVjCOPEH4xREo3xX0ebvmuLwObu75lQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/forms": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.2.3.tgz", + "integrity": "sha512-BDphHWx6FI3wfOd9XqhiUBKuIg4dnpr9Aw5+n1by4y9e2avBwWoefHJ15FtEZdop5qMwVWlCcHjdwZ2+q65f4g==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.3.tgz", + "integrity": "sha512-hKC5V2n0jfrFc9DXvHW+NtHDSfC417WZz8onmUhMR2Eo3fjBm5cuZi5CNgnbZt9zUoXXOfwCy6p3N7FbPDXxLQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.3.tgz", + "integrity": "sha512-rH7tpOwJ6r8+085bOmXUI9/v9MxFfsL+BYzi6bG2vCU280cSblvZR2Rx9nqx3/+Xd97ISWkUZiQhyRVJWYk6UQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-server": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-8.2.3.tgz", + "integrity": "sha512-esVJSOX6GYGzW8Rsmn/FwZl2cpsH24lWsPNmIQHe61qXCmuLrjWKrsefrWq8FMpUfyPqZNC4QuwNWZ1yeA/Hrw==", + "requires": { + "domino": "^2.1.2", + "tslib": "^1.9.0", + "xhr2": "^0.1.4" + } + }, + "@angular/router": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-8.2.3.tgz", + "integrity": "sha512-PMZ5JA0GonaNpwV3VfGWUkL6eo2KbtYUZERu6AnTYpQTTzGBiS6pakQTczE7lb9KZ9u+LIhKBaxQ8hrGLWqgKw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", + "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "requires": { + "@babel/types": "^7.5.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", + "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==" + }, + "@babel/runtime": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.4.tgz", + "integrity": "sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==", + "requires": { + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + } + } + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", + "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.5", + "@babel/types": "^7.5.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@babel/types": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", + "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + } + } + }, + "@clusterws/cws": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@clusterws/cws/-/cws-0.15.0.tgz", + "integrity": "sha512-41QpCngw86n41hIdU5Nx2QJmmxZuA9FPtDkjONrYpk27L7HjL1kj6J5oWEjbr14yXLfigZit3VY+oACDCGbiHw==" + }, + "@fortawesome/angular-fontawesome": { + "version": "0.5.0", + "resolved": "https://npm.fontawesome.com/7A23B90E-8382-46A2-96C7-9E67CA2D36A5/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.5.0.tgz", + "integrity": "sha512-5IR/jNMddiEpgApcqSMfp7l5kZqOtxQYzkSLW6iZ4MZHbQQ+Kc9tu9qi2hro6VFSk/sPxeYEzkzGZUNrMYiwOg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.22", + "resolved": "https://npm.fontawesome.com/7A23B90E-8382-46A2-96C7-9E67CA2D36A5/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.22.tgz", + "integrity": "sha512-QmEuZsipX5/cR9JOg0fsTN4Yr/9lieYWM8AQpmRa0eIfeOcl/HLYoEa366BCGRSrgNJEexuvOgbq9jnJ22IY5g==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.22", + "resolved": "https://npm.fontawesome.com/7A23B90E-8382-46A2-96C7-9E67CA2D36A5/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.22.tgz", + "integrity": "sha512-Q941E4x8UfnMH3308n0qrgoja+GoqyiV846JTLoCcCWAKokLKrixCkq6RDBs8r+TtAWaLUrBpI+JFxQNX/WNPQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.22" + } + }, + "@fortawesome/free-brands-svg-icons": { + "version": "5.10.2", + "resolved": "https://npm.fontawesome.com/7A23B90E-8382-46A2-96C7-9E67CA2D36A5/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.10.2.tgz", + "integrity": "sha512-r5Dxr2h8f9bEI7F/gj/2v1OX9S6DMif9ZKR2VFQCSXHwahojLlOWnFILYsrjhzOISESkh6WDL9IOdkdbKM7KPw==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.22" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.10.2", + "resolved": "https://npm.fontawesome.com/7A23B90E-8382-46A2-96C7-9E67CA2D36A5/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.10.2.tgz", + "integrity": "sha512-9Os/GRUcy+iVaznlg8GKcPSQFpIQpAg14jF0DWsMdnpJfIftlvfaQCWniR/ex9FoOpSEOrlXqmUCFL+JGeciuA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.22" + } + }, + "@gulp-sourcemaps/identity-map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz", + "integrity": "sha512-ciiioYMLdo16ShmfHBXJBOFm3xPC4AuwO4xeRpFeHz7WK9PYsWCmigagG2XyzZpubK4a3qNKoUBDhbzHfa50LQ==", + "requires": { + "acorn": "^5.0.3", + "css": "^2.2.1", + "normalize-path": "^2.1.1", + "source-map": "^0.6.0", + "through2": "^2.0.3" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "requires": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "@hapi/address": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", + "integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==" + }, + "@hapi/hoek": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.1.tgz", + "integrity": "sha512-+ryw4GU9pjr1uT6lBuErHJg3NYqzwJTvZ75nKuJijEzpd00Uqi6oiawTGDDf5Hl0zWmI7qHfOtaqB0kpQZJQzA==" + }, + "@hapi/joi": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.0.1.tgz", + "integrity": "sha512-6RLoFusVbSkKjo51IGy7TT72TYKaks3OLKRV4Wy4E8f0Vt2oqKBgtG+8VMnbruy+M4iLItiLYjETX7mQqklObQ==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/hoek": "6.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.0.tgz", + "integrity": "sha512-gZDI/eXOIk8kP2PkUKjWu9RW8GGVd2Hkgjxyr/S7Z+JF+0mr7bAlbw+DkTRxnD580o8Kqxlnba9wvqp5aOHBww==", + "requires": { + "@hapi/hoek": "6.x.x" + } + }, + "@ngtools/webpack": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.1.tgz", + "integrity": "sha512-VGlVnrRwCN8M5YMk8psf9qbKsOKMSuHnJnIczs5sqI9dvjx+B2oAA/+B+BJoBBnDqXccBvintPV+HSgxfnbA0A==", + "requires": { + "@angular-devkit/core": "8.3.1", + "enhanced-resolve": "4.1.0", + "rxjs": "6.4.0", + "tree-kill": "1.2.1", + "webpack-sources": "1.4.3" + }, + "dependencies": { + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz", + "integrity": "sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==", + "requires": { + "@nodelib/fs.stat": "2.0.1", + "run-parallel": "^1.1.9" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz", + "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==" + } + } + }, + "@nodelib/fs.stat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz", + "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==" + }, + "@nodelib/fs.walk": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz", + "integrity": "sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==", + "requires": { + "@nodelib/fs.scandir": "2.1.1", + "fastq": "^1.6.0" + } + }, + "@passport-next/passport-facebook": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@passport-next/passport-facebook/-/passport-facebook-3.1.2.tgz", + "integrity": "sha512-UEdKUsIuyX9oivcnd9xkl0Y1qj6DsT1VLfp1kTwsrwe2HLMGtW+KkgFw374dRXhbIG/Wzmg/g0o7c/JV3L2/6Q==", + "requires": { + "@passport-next/passport-oauth2": "^2.x.x" + }, + "dependencies": { + "@passport-next/passport-oauth2": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@passport-next/passport-oauth2/-/passport-oauth2-2.0.2.tgz", + "integrity": "sha512-fo0cq/A93zPM4AoOMOV8nZs2bFRFCRUNNZeGdbuRMe/LvKpqZYZi7oUi7xt//txULDB12mq6KM+Yi7LLlWVwmQ==", + "requires": { + "@passport-next/passport-strategy": "1.1.x", + "lodash": "^4.17.14", + "oauth": "0.9.x" + } + } + } + }, + "@passport-next/passport-google-oauth2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@passport-next/passport-google-oauth2/-/passport-google-oauth2-1.0.0.tgz", + "integrity": "sha512-rbAtK0eGgjQhT/MqMSuN7Mxy4XTr1/LINk5rYPVezP3MfkVYuK20V0XW9fg1ikFMvyxSdmfLTyYduud7SfFSqw==", + "requires": { + "@passport-next/passport-oauth2": "1.7.x" + } + }, + "@passport-next/passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@passport-next/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-0905TcHFwvqPyMsqTKDwjwddPJJcWkUs4fjbUNgnnxkfQ0/LX4FVwuMywOXiFNY12hhX92j8F5srGQC4f9UWug==", + "requires": { + "@passport-next/passport-strategy": "1.1.x", + "lodash": ">=4.17.5", + "oauth": "0.9.x" + } + }, + "@passport-next/passport-strategy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@passport-next/passport-strategy/-/passport-strategy-1.1.0.tgz", + "integrity": "sha512-2KhFjtPueJG6xVj2HnqXt9BlANOfYCVLyu+pXYjPGBDT8yk+vQwc/6tsceIj+mayKcoxMau2JimggXRPHgoc8w==" + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "optional": true + }, + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", + "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" + }, + "@types/babel-types": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", + "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" + }, + "@types/babylon": { + "version": "6.16.5", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", + "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", + "requires": { + "@types/babel-types": "*" + } + }, + "@types/base64-js": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.2.5.tgz", + "integrity": "sha1-WCskdhaabLpGCiFNR2x0REHYc9U=" + }, + "@types/bluebird": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.27.tgz", + "integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==" + }, + "@types/body-parser": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", + "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bson": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.0.tgz", + "integrity": "sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q==", + "requires": { + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, + "@types/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-zw8UvoBEImn392tLjxoavuonblX/4Yb9ha4KBU10FirCfwgzhKO0dvyJSF9ByxV1xK1r2AgnAi/tvQaLgxQqxA==" + }, + "@types/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-PO2gcfR3Oxa+u0QvECLe1xKXOqYTzCmWf0FhLhjREoW3fPAVamjihL7v1MOVLJLsnAMdLcjkfrs01yvDMwVK4Q==", + "requires": { + "@types/chai": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/cookie-parser": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz", + "integrity": "sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==", + "requires": { + "@types/express": "*" + } + }, + "@types/del": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/del/-/del-4.0.0.tgz", + "integrity": "sha512-LDE5atstX5kKnTobWknpmGHC52DH/tp8pIVsD2SSxaOFqW3AQr0EpdzYIfkZ331xe7l9Vn9NlJsBG6viU3mjBA==", + "requires": { + "del": "*" + } + }, + "@types/events": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.1.0.tgz", + "integrity": "sha512-y3bR98mzYOo0pAZuiLari+cQyiKk3UXRuT45h1RjhfeCzqkjaVsfZJNaxdgtk7/3tzOm1ozLTqEqMP3VbI48jw==" + }, + "@types/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.1.tgz", + "integrity": "sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-brute": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/express-brute/-/express-brute-0.0.37.tgz", + "integrity": "sha512-N1eDsPeRadiaLLfcCOxtQWSNN7ipdrg7IvRZNzQaUTvVvfGYwu8eZHOBbRrYb632jooc2Mmzz+TovBm+44itFA==", + "requires": { + "@types/express": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz", + "integrity": "sha512-GqpaVWR0DM8FnRUJYKlWgyARoBUAVfRIeVDZQKOttLFp5SmhhF9YFIYeTPwMd/AXfxlP7xVO2dj1fGu0Q+krKQ==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/express-session": { + "version": "1.15.14", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.15.14.tgz", + "integrity": "sha512-7kVzFTT0Jy0zmUYDt9ik76XbcqyS9NalV4gn4eLwhk1nGQn+lS/HjPODhG3Oi/GBR2w1LQHUdkz/5KICYMACiw==", + "requires": { + "@types/express": "*", + "@types/node": "*" + } + }, + "@types/file-saver": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.1.tgz", + "integrity": "sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==" + }, + "@types/fs-extra": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz", + "integrity": "sha512-bCtL5v9zdbQW86yexOlXWTEGvLNqWxMFyi7gQA7Gcthbezr2cPSOb8SkESVKA937QD5cIwOFLDFt0MQoXOEr9Q==", + "requires": { + "@types/node": "*" + } + }, + "@types/gl-matrix": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@types/gl-matrix/-/gl-matrix-2.4.5.tgz", + "integrity": "sha512-0L8Mq1+oaIW0oVzGUDbSW+HnTjCNb4CmoIQE5BkoHt/A7x20z0MJ1PnwfH3atty/vbWLGgvJwVu2Mz3SKFiEFw==" + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/google.analytics": { + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", + "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==" + }, + "@types/howler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.1.1.tgz", + "integrity": "sha512-YLwc9L853r85WqZk4xCiROGGodmlAYlKb0rQWABt4opLd6QHn2QLXIApa6vRNqjZxkzbiPs6eZskaRGOw5fDAA==" + }, + "@types/lodash": { + "version": "4.14.138", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.138.tgz", + "integrity": "sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg==" + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" + }, + "@types/mongodb": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.3.1.tgz", + "integrity": "sha512-Va7o1fN3zeabmIJSQ6yuAWkqPvrT38HSTIi4YbVOb2UL7FJ4diXrWt+OUuuEFWAVPtF9VZV5h+7LDYdzgXWgQA==", + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/mongoose": { + "version": "5.5.15", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.5.15.tgz", + "integrity": "sha512-LgUHpGl6mmcyzjZR/afUZ7mDPjDd38mA0HCf8vL11otVKiNGyu7ug8z3ePx2QAqcPrxbtfaJVgirdBeNXYG54Q==", + "requires": { + "@types/mongodb": "*", + "@types/node": "*" + } + }, + "@types/morgan": { + "version": "1.7.37", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.37.tgz", + "integrity": "sha512-tIdEA10BcHcOumMmUiiYdw8lhiVVq62r0ghih5Xpp4WETkfsMiTUZL4w9jCI502BBOrKhFrAOGml9IeELvVaBA==", + "requires": { + "@types/express": "*" + } + }, + "@types/node": { + "version": "12.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", + "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==" + }, + "@types/node-ipc": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.1.1.tgz", + "integrity": "sha512-0NoDCXvNJagYdg0SnXw7aDuxJyzy1cQevRVlCdTM4q0xAQ7Tg5VbQ9jpreKGOaLyql8r9T60hy/wA/QDIk1+Lg==", + "requires": { + "@types/node": "*" + } + }, + "@types/passport": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.1.tgz", + "integrity": "sha512-oK87JjN8i8kmqb0RN0sCUB/ZjrWf3b8U45eAzZVy1ssYYgBrMOuALmvoqp7MglsilXAjxum+LS29VQqeQx6ddA==", + "requires": { + "@types/express": "*" + } + }, + "@types/passport-local": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz", + "integrity": "sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA==", + "requires": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.33.tgz", + "integrity": "sha512-tmj//XbNqCWmD+PJ/KnxAouircAmMGLN9IHBO3utH5DXuHHHYN4ZG53DRrQBjlZMiS/1b5IP38U2ay1GfbcQrQ==", + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "@types/pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.4.tgz", + "integrity": "sha1-h3L80EGOPNLMFxVV1zAHQVBR9LI=" + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "optional": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/request": { + "version": "2.48.2", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.2.tgz", + "integrity": "sha512-gP+PSFXAXMrd5PcD7SqHeUjdGshAI8vKQ3+AvpQr3ht9iQea+59LOKvKITcQI+Lg+1EIkDP6AFSBUJPWG8GDyA==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.0.tgz", + "integrity": "sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/request-promise": { + "version": "4.1.44", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.44.tgz", + "integrity": "sha512-RId7eFsUKxfal1LirDDIcOp9u3MM3NXFDBcC3sqIMcmu7f4U6DsCEMD8RbLZtnPrQlN5Jc79di/WPsIEDO4keg==", + "requires": { + "@types/bluebird": "*", + "@types/request": "*" + } + }, + "@types/serve-favicon": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/@types/serve-favicon/-/serve-favicon-2.2.31.tgz", + "integrity": "sha512-BfGO//1n649yo2ooH0wDemWLwElgGT1tq6y1l4orwtYXjCNhHJf3LWAe6VBpxq8RGaqiM7AXYAHWIOP52y0gtA==", + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/sinon": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.13.tgz", + "integrity": "sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung==" + }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" + }, + "@types/ua-parser-js": { + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-ngUKcHnytUodUCL7C6EZ+lVXUjTMQb+9p/e1JjV5tN9TVzS98lHozWEFRPY1QcCdwFeMsmVWfZ3DPPT/udCyIw==" + }, + "@types/uws": { + "version": "0.13.2", + "resolved": "http://registry.npmjs.org/@types/uws/-/uws-0.13.2.tgz", + "integrity": "sha512-PtSuctnHZPMJKGDvpBpX+3EvL8PPcErNOv6A0HacFJV1954ggrTZusLVrZjqBrActKf/C91dbdl/piOmLMeg8A==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/ws": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", + "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz", + "integrity": "sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==" + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==" + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==" + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "dependencies": { + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + } + } + }, + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, + "ag-psd": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ag-psd/-/ag-psd-6.2.0.tgz", + "integrity": "sha512-CD8V6qlUjcdmb9Wnl3sLNALtbUwtvMBnIqHOLX4cqwW7tpY1LsxQakGkq6N0XfdvajkT7HCh0vVKLlodk0vb4A==", + "requires": { + "@types/base64-js": "^1.2.5", + "base64-js": "^1.3.0" + } + }, + "ag-sockets": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/ag-sockets/-/ag-sockets-4.7.0.tgz", + "integrity": "sha512-qM8qqyBN4X010bj/8G5/CesdGFyEgOFR1uT6WSkubP76lFYh3qFa4jm2GqHuCOAKE0MkOJkk3Mf9snBYPCI+kg==", + "requires": { + "@types/ws": "^6.0.1" + } + }, + "aggregate-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.0.tgz", + "integrity": "sha512-yKD9kEoJIR+2IFqhMwayIBgheLYbB3PS2OBhWae1L/ODTd/JF/30cW0bc9TqzRL3k4U41Dieu3BF4I29p8xesA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^3.2.0" + }, + "dependencies": { + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + }, + "dependencies": { + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + } + } + }, + "ajv-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", + "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=" + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=" + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "angular2-template-loader": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/angular2-template-loader/-/angular2-template-loader-0.6.2.tgz", + "integrity": "sha1-wNROkP/w+sleiyPwQ6zaf9HFHXw=", + "requires": { + "loader-utils": "^0.2.15" + } + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "ansi-colors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.0.1.tgz", + "integrity": "sha512-yopkAU0ZD/WQ56Tms3xLn6jRuX3SyUMAVi0FdmDIbmmnHW3jHiI1sQFdUl3gfVddjnrsP3Y6ywFKvCRopvoVIA==", + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "requires": { + "buffer-equal": "^1.0.0" + } + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "aproba": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz", + "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==" + }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==", + "optional": true + }, + "archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "optional": true, + "requires": { + "file-type": "^4.2.0" + }, + "dependencies": { + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "optional": true + } + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==" + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" + }, + "array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "requires": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + }, + "array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" + }, + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "requires": { + "lodash": "^4.17.11" + } + }, + "async-done": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.1.tgz", + "integrity": "sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^1.0.7", + "stream-exhaust": "^1.0.1" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "requires": { + "async-done": "^1.2.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + }, + "autoprefixer": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.1.tgz", + "integrity": "sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw==", + "requires": { + "browserslist": "^4.6.3", + "caniuse-lite": "^1.0.30000980", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.17", + "postcss-value-parser": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==" + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "babel-extract-comments": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz", + "integrity": "sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ==", + "requires": { + "babylon": "^6.18.0" + } + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "basic-auth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=" + }, + "bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "requires": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", + "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=" + }, + "bin-build": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", + "integrity": "sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==", + "optional": true, + "requires": { + "decompress": "^4.0.0", + "download": "^6.2.2", + "execa": "^0.7.0", + "p-map-series": "^1.0.0", + "tempfile": "^2.0.0" + } + }, + "bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "optional": true, + "requires": { + "execa": "^0.7.0", + "executable": "^4.1.0" + } + }, + "bin-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", + "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", + "optional": true, + "requires": { + "execa": "^1.0.0", + "find-versions": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "optional": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "optional": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "optional": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + } + } + }, + "bin-version-check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", + "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "optional": true, + "requires": { + "bin-version": "^3.0.0", + "semver": "^5.6.0", + "semver-truncate": "^1.1.2" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + } + } + }, + "bin-wrapper": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", + "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "optional": true, + "requires": { + "bin-check": "^4.1.0", + "bin-version-check": "^4.0.0", + "download": "^7.1.0", + "import-lazy": "^3.1.0", + "os-filter-obj": "^2.0.0", + "pify": "^4.0.1" + }, + "dependencies": { + "download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "optional": true, + "requires": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "optional": true + } + } + }, + "file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "optional": true + }, + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "optional": true, + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "optional": true + } + } + }, + "import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", + "optional": true + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "optional": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "optional": true + } + } + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "http://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "optional": true + }, + "p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "optional": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "optional": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "optional": true + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "optional": true + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "optional": true, + "requires": { + "prepend-http": "^2.0.0" + } + } + } + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=" + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "optional": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "optional": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", + "requires": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + }, + "dependencies": { + "bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" + }, + "raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", + "requires": { + "bytes": "1", + "string_decoder": "0.10" + } + } + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + }, + "dependencies": { + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "optional": true + }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", + "integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", + "requires": { + "caniuse-lite": "^1.0.30000984", + "electron-to-chromium": "^1.3.191", + "node-releases": "^1.1.25" + }, + "dependencies": { + "electron-to-chromium": { + "version": "1.3.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.235.tgz", + "integrity": "sha512-xNabEDbMIKPLQd6xgv4nyyeMaWXIKSJr6G51ZhUemHhbz6kjZAYcygA8CvfEcMF+Mt5eLmDWaLmfSOWdQxzBVQ==" + } + } + }, + "bson": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", + "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "optional": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "optional": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "optional": true + }, + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "optional": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.1", + "mississippi": "^2.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^5.2.4", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "optional": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "optional": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "optional": true + }, + "query-string": { + "version": "5.1.1", + "resolved": "http://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "optional": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "optional": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "requires": { + "browserslist": "^1.3.6", + "caniuse-db": "^1.0.30000529", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "^1.0.30000639", + "electron-to-chromium": "^1.2.7" + } + } + } + }, + "caniuse-db": { + "version": "1.0.30000820", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000820.tgz", + "integrity": "sha1-fCDiXOoXaLJhtyT4LjpqJTqqFGg=" + }, + "caniuse-lite": { + "version": "1.0.30000989", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz", + "integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==" + }, + "canonical-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", + "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==" + }, + "canvas": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz", + "integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==", + "requires": { + "nan": "^2.14.0", + "node-pre-gyp": "^0.11.0", + "simple-get": "^3.0.3" + }, + "dependencies": { + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + } + } + }, + "canvas-toBlob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canvas-toBlob/-/canvas-toBlob-1.0.0.tgz", + "integrity": "sha1-m/MrKGu04SUhiyCO7MgyH9Az5sM=" + }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "optional": true, + "requires": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "requires": { + "check-error": "^1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + }, + "check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==" + }, + "chokidar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz", + "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.0" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "requires": { + "chalk": "^1.1.3" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "requires": { + "source-map": "0.5.x" + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.1.0.tgz", + "integrity": "sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA==" + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + } + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=" + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=" + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "optional": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" + }, + "cloneable-readable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.0.0.tgz", + "integrity": "sha1-pikNQT8hemEjL5XkWP84QYz7ARc=", + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^1.0.6", + "through2": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + } + } + }, + "clusterws-uws": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/clusterws-uws/-/clusterws-uws-0.4.6.tgz", + "integrity": "sha512-r4s9ugW3pgaXF1OygRS8vRDZB7CoBfnuKE5b7hCnIIbSsge3ofinfdRBwD7J9/sR22V2ICLV4YD7hANARPiGDQ==" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "requires": { + "q": "^1.1.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "requires": { + "clone": "^1.0.2", + "color-convert": "^1.3.0", + "color-string": "^0.3.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + }, + "dependencies": { + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "requires": { + "color-name": "^1.0.0" + } + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, + "colormin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", + "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "requires": { + "color": "^0.11.0", + "css-color-names": "0.0.4", + "has": "^1.0.1" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "optional": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "common-tags": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "requires": { + "mime-db": ">= 1.40.0 < 2" + }, + "dependencies": { + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + } + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "optional": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" + }, + "connect-livereload": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.5.4.tgz", + "integrity": "sha1-gBV9E3HJ83zBQDmrGJWXDRGdw7w=" + }, + "connect-mongo": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-3.0.0.tgz", + "integrity": "sha512-Y95urWNGrAoKY2w31s7Q9Gs/W3qdMCshUIeDTgulssHi6KueYtz4XrbV3kcnQaR8EcBQvooNNX7aOaAJDgudag==", + "requires": { + "mongodb": "^3.1.0" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "console-polyfill": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/console-polyfill/-/console-polyfill-0.3.0.tgz", + "integrity": "sha512-w+JSDZS7XML43Xnwo2x5O5vxB0ID7T5BdqDtyqT6uiCAX2kZAgcWxNaGqT97tZfSHzfOcvrfsDAodKcJ3UvnXQ==" + }, + "console-stream": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", + "integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ=", + "optional": true + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "optional": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=" + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "copy-props": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", + "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", + "requires": { + "each-props": "^1.3.0", + "is-plain-object": "^2.0.1" + } + }, + "core-js": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", + "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0", + "require-from-string": "^2.0.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + }, + "css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "requires": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + }, + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "optional": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "optional": true + }, + "css-tree": { + "version": "1.0.0-alpha.33", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz", + "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==", + "optional": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.5.3" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "optional": true + }, + "cssnano": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", + "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", + "requires": { + "autoprefixer": "^6.3.1", + "decamelize": "^1.1.2", + "defined": "^1.0.0", + "has": "^1.0.1", + "object-assign": "^4.0.1", + "postcss": "^5.0.14", + "postcss-calc": "^5.2.0", + "postcss-colormin": "^2.1.8", + "postcss-convert-values": "^2.3.4", + "postcss-discard-comments": "^2.0.4", + "postcss-discard-duplicates": "^2.0.1", + "postcss-discard-empty": "^2.0.1", + "postcss-discard-overridden": "^0.1.1", + "postcss-discard-unused": "^2.2.1", + "postcss-filter-plugins": "^2.0.0", + "postcss-merge-idents": "^2.1.5", + "postcss-merge-longhand": "^2.0.1", + "postcss-merge-rules": "^2.0.3", + "postcss-minify-font-values": "^1.0.2", + "postcss-minify-gradients": "^1.0.1", + "postcss-minify-params": "^1.0.4", + "postcss-minify-selectors": "^2.0.4", + "postcss-normalize-charset": "^1.1.0", + "postcss-normalize-url": "^3.0.7", + "postcss-ordered-values": "^2.1.0", + "postcss-reduce-idents": "^2.2.2", + "postcss-reduce-initial": "^1.0.0", + "postcss-reduce-transforms": "^1.0.3", + "postcss-svgo": "^2.1.1", + "postcss-unique-selectors": "^2.0.2", + "postcss-value-parser": "^3.2.3", + "postcss-zindex": "^2.0.1" + }, + "dependencies": { + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "requires": { + "browserslist": "^1.7.6", + "caniuse-db": "^1.0.30000634", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^5.2.16", + "postcss-value-parser": "^3.2.3" + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "^1.0.30000639", + "electron-to-chromium": "^1.2.7" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "requires": { + "clap": "^1.0.9", + "source-map": "^0.5.3" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "^0.10.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "requires": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "decache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decache/-/decache-3.1.0.tgz", + "integrity": "sha1-T1A2+9ZYH8yXI3rDlUokS5U2wto=", + "optional": true, + "requires": { + "find": "^0.2.4" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "decompress": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", + "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "optional": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "optional": true + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "optional": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "optional": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "optional": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "optional": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "optional": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "optional": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "optional": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "optional": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "optional": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "optional": true + } + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepmerge": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-0.2.10.tgz", + "integrity": "sha1-iQa/nlJaT78bIDsq/LRkAkmCEhk=" + }, + "default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "requires": { + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "requires": { + "strip-bom": "^3.0.0" + } + }, + "default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "requires": { + "foreach": "^2.0.5", + "object-keys": "^1.0.8" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "del": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", + "integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==", + "requires": { + "globby": "^10.0.1", + "graceful-fs": "^4.2.2", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.1", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" + }, + "is-path-inside": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.1.tgz", + "integrity": "sha512-CKstxrctq1kUesU6WhtZDbYKzzYBuRH0UYInAVrkc/EYdB9ltbfE0gOoayG9nhohG6447sOOVGhHqsdmBvkbNg==" + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "delta-e": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/delta-e/-/delta-e-0.0.7.tgz", + "integrity": "sha1-khI4d6hvAceZdnQCqFkxOOkQwIE=" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "dependency-graph": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz", + "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + } + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, + "dom-serializer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", + "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", + "optional": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "optional": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "optional": true + }, + "domino": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.3.tgz", + "integrity": "sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg==" + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "optional": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "download": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", + "integrity": "sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==", + "optional": true, + "requires": { + "caw": "^2.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.0.0", + "ext-name": "^5.0.0", + "file-type": "5.2.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^7.0.0", + "make-dir": "^1.0.0", + "p-event": "^1.0.0", + "pify": "^3.0.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "optional": true + } + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "duplexify": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz", + "integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=", + "requires": { + "end-of-stream": "1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "end-of-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", + "integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=", + "requires": { + "once": "~1.3.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "requires": { + "wrappy": "1" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "easy-stack": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.0.tgz", + "integrity": "sha1-EskbMIWjfwuqM26UhurEv5Tj54g=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "ejs": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.2.tgz", + "integrity": "sha512-PcW2a0tyTuPHz3tWyYqtK6r1fZ3gp+3Sop8Ph+ZYN81Ob5rwmbHEzaqs10N3BEsaGTkh/ooniXK+WwszGlc2+Q==" + }, + "electron-to-chromium": { + "version": "1.3.40", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.40.tgz", + "integrity": "sha1-H71tl779crim+SHcONIkE9L2/d8=" + }, + "elliptic": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", + "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + } + } + }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "optional": true + }, + "errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "requires": { + "prr": "~0.0.0" + } + }, + "error": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", + "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", + "requires": { + "string-template": "~0.2.1", + "xtend": "~4.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-1.3.3.tgz", + "integrity": "sha1-+tpuOpzSsOCA5tb8dRQYZJc081w=", + "requires": { + "stackframe": "^0.3.1" + } + }, + "errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "requires": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + } + }, + "es-abstract": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", + "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + }, + "dependencies": { + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + } + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "requires": { + "is-callable": "^1.1.1", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.1" + } + }, + "es5-ext": { + "version": "0.10.24", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.24.tgz", + "integrity": "sha1-pVh3yZJLwMjZvTwsvhdJWsFwmxQ=", + "requires": { + "es6-iterator": "2", + "es6-symbol": "~3.1" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "es6-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-symbol": "^3.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-templates": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", + "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", + "requires": { + "recast": "~0.11.12", + "through": "~2.3.6" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "requires": { + "estraverse": "^4.1.0" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==" + }, + "event-stream": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.2.2.tgz", + "integrity": "sha1-95+ZhMB+4/2bRP+zzQQisT4kCE0=", + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-buffer": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", + "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", + "optional": true, + "requires": { + "execa": "^0.7.0", + "p-finally": "^1.0.0", + "pify": "^3.0.0", + "rimraf": "^2.5.4", + "tempfile": "^2.0.0" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "optional": true, + "requires": { + "pify": "^2.2.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "optional": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + } + } + }, + "express-brute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/express-brute/-/express-brute-1.0.1.tgz", + "integrity": "sha1-nzbRB/405ApoJZPjm//MUxArUzU=", + "requires": { + "long-timeout": "~0.1.1", + "underscore": "~1.8.3" + } + }, + "express-session": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.2.tgz", + "integrity": "sha512-oy0sRsdw6n93E9wpCNWKRnSsxYnSDX9Dnr9mhZgqUEEorzcq5nshGYSZ4ZReHFhKQ80WI5iVUUSPW7u3GaKauw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.1.2", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "optional": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "optional": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fancy-log": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", + "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "time-stamp": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz", + "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==", + "requires": { + "@nodelib/fs.stat": "^2.0.1", + "@nodelib/fs.walk": "^1.2.1", + "glob-parent": "^5.0.0", + "is-glob": "^4.0.1", + "merge2": "^1.2.3", + "micromatch": "^4.0.2" + }, + "dependencies": { + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=" + }, + "fastq": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", + "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", + "requires": { + "reusify": "^1.0.0" + } + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "optional": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "optional": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-saver": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", + "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" + }, + "file-type": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.2.0.tgz", + "integrity": "sha512-bkDBeH5doAqP69axEO69OviLlWbrZ10Ne2OPHaxBgG+fyT0w/2zfMzJz21SPwq5Iq0aN70q7RN3KRcdUY427Mg==" + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "optional": true + }, + "filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "optional": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + }, + "dependencies": { + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + } + } + }, + "find": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/find/-/find-0.2.9.tgz", + "integrity": "sha1-S3Px/55WrZG3bnFkB/5f/mVUu4w=", + "optional": true, + "requires": { + "traverse-chain": "~0.1.0" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "find-versions": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.1.0.tgz", + "integrity": "sha512-NCTfNiVzeE/xL+roNDffGuRbrWI6atI18lTJ22vKp7rs2OhYzMK3W1dIdO2TUndH/QMcacM4d1uWwgcZcHK69Q==", + "optional": true, + "requires": { + "array-uniq": "^2.1.0", + "semver-regex": "^2.0.0" + }, + "dependencies": { + "array-uniq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-2.1.0.tgz", + "integrity": "sha512-bdHxtev7FN6+MXI1YFW0Q8mQ8dTJc2S8AMfju+ZR77pbg2yAdVyDlwkaUI7Har0LyOMRFPHrJ9lYdyjZZswdlQ==", + "optional": true + } + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=" + }, + "flush-write-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.2.tgz", + "integrity": "sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "focus-visible": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/focus-visible/-/focus-visible-5.0.2.tgz", + "integrity": "sha512-zT2fj/bmOgEBjqGbURGlowTmCwsIs3bRDMr/sFZz8Ly7VkEiwuCn9swNTL3pPuf8Oua2de7CLuKdnuNajWdDsQ==" + }, + "follow-redirects": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", + "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "requires": { + "debug": "^3.2.6" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "fork-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", + "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "frameguard": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz", + "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + } + } + }, + "fs-minipass": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", + "requires": { + "minipass": "^2.2.1" + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "requires": { + "globule": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", + "integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==" + }, + "get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "optional": true, + "requires": { + "npm-conf": "^1.1.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "gif.js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/gif.js/-/gif.js-0.2.0.tgz", + "integrity": "sha1-YV5uN4iFDNOiDIX+nwlTnnhJA+g=" + }, + "gifsicle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-4.0.1.tgz", + "integrity": "sha512-A/kiCLfDdV+ERV/UB+2O41mifd+RxH8jlRG8DMxZO84Bma/Fw0htqZ+hY2iaalLRNyUu7tYZQslqUBJxBggxbg==", + "optional": true, + "requires": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "execa": "^1.0.0", + "logalot": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "optional": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "optional": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "optional": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "glob-watcher": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", + "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "requires": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "object.defaults": "^1.1.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "requires": { + "ini": "^1.3.4" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.0.tgz", + "integrity": "sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==", + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz", + "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==" + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + } + }, + "fast-glob": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.3.tgz", + "integrity": "sha512-scDJbDhN+6S4ELXzzN96Fqm5y1CMRn+Io3C4Go+n/gUKP+LW26Wma6IxLSsX2eAMBUOFmyHKDBrUSuoHsycQ5A==", + "requires": { + "@nodelib/fs.stat": "^2.0.1", + "@nodelib/fs.walk": "^1.2.1", + "glob-parent": "^5.0.0", + "is-glob": "^4.0.1", + "merge2": "^1.2.3", + "micromatch": "^4.0.2" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "ignore": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz", + "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + } + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "requires": { + "sparkles": "^1.0.0" + } + }, + "got": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", + "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "optional": true, + "requires": { + "decompress-response": "^3.2.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-plain-obj": "^1.1.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "p-cancelable": "^0.3.0", + "p-timeout": "^1.1.1", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "url-parse-lax": "^1.0.0", + "url-to-options": "^1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "optional": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, + "gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "requires": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "gulp-cli": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", + "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.1.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.0.1", + "yargs": "^7.1.0" + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + } + } + }, + "gulp-autoprefixer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gulp-autoprefixer/-/gulp-autoprefixer-7.0.0.tgz", + "integrity": "sha512-ZGMA/9iPF7kfZIVhznd3eylivBcyLCgcV32cWtvM7j2IjEQzoRg1XaKgCeO5ytutq8XW3Rip+iM4EsVbNshoZw==", + "requires": { + "autoprefixer": "^9.6.1", + "fancy-log": "^1.3.2", + "plugin-error": "^1.0.1", + "postcss": "^7.0.17", + "through2": "^3.0.1", + "vinyl-sourcemaps-apply": "^0.2.1" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "gulp-cssnano": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz", + "integrity": "sha512-r8qdX5pTXsBb/IRm9loE8Ijz8UiPW/URMC/bKJe4FPNHRaz4aEx8Bev03L0FYHd/7BSGu/ebmfumAkpGuTdenA==", + "requires": { + "buffer-from": "^1.0.0", + "cssnano": "^3.0.0", + "object-assign": "^4.0.1", + "plugin-error": "^1.0.1", + "vinyl-sourcemaps-apply": "^0.2.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + } + } + }, + "gulp-if": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", + "integrity": "sha512-fCUEngzNiEZEK2YuPm+sdMpO6ukb8+/qzbGfJBXyNOXz85bCG7yBI+pPSl+N90d7gnLvMsarthsAImx0qy7BAw==", + "requires": { + "gulp-match": "^1.1.0", + "ternary-stream": "^3.0.0", + "through2": "^3.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "gulp-imagemin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gulp-imagemin/-/gulp-imagemin-6.1.0.tgz", + "integrity": "sha512-0TPkak5BsiRfw+kfcKwIcODbOHHcTyvBM9arlRSwXdUVzrGAcq/7urZoOQD5n4uWvKhjg+l9/yn1GDZsDuWUow==", + "requires": { + "chalk": "^2.4.1", + "fancy-log": "^1.3.2", + "imagemin": "^7.0.0", + "imagemin-gifsicle": "^6.0.1", + "imagemin-jpegtran": "^6.0.0", + "imagemin-optipng": "^7.0.0", + "imagemin-svgo": "^7.0.0", + "plugin-error": "^1.0.1", + "plur": "^3.0.1", + "pretty-bytes": "^5.3.0", + "through2-concurrent": "^2.0.0" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "pretty-bytes": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", + "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==" + } + } + }, + "gulp-live-server": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/gulp-live-server/-/gulp-live-server-0.0.31.tgz", + "integrity": "sha1-O0wYb+ygcIJhcY9sis4ZPxmPLVE=", + "requires": { + "chalk": "^1.0.0", + "connect": "^3.3.4", + "connect-livereload": "^0.5.3", + "debug": "^2.1.1", + "deepmerge": "~0.2.7", + "event-stream": "~3.2.1", + "q": "^1.2.0", + "serve-static": "^1.9.1", + "tiny-lr": "^1.0.3" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "gulp-match": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.1.0.tgz", + "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==", + "requires": { + "minimatch": "^3.0.3" + } + }, + "gulp-plumber": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.1.tgz", + "integrity": "sha512-mctAi9msEAG7XzW5ytDVZ9PxWMzzi1pS2rBH7lA095DhMa6KEXjm+St0GOCc567pJKJ/oCvosVAZEpAey0q2eQ==", + "requires": { + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "through2": "^2.0.3" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-rev": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/gulp-rev/-/gulp-rev-9.0.0.tgz", + "integrity": "sha512-Ytx/uzDA2xNxHlPG8GReS1ut00msd0HlKDk9Ai/0xF2yvg+DAeGRAviCFlQzQmdZtqAoXznYspwWoGEoxDvhyA==", + "requires": { + "modify-filename": "^1.1.0", + "plugin-error": "^1.0.1", + "rev-hash": "^2.0.0", + "rev-path": "^2.0.0", + "sort-keys": "^2.0.0", + "through2": "^2.0.0", + "vinyl": "^2.1.0", + "vinyl-file": "^3.0.0" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + } + } + }, + "gulp-sass": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-4.0.2.tgz", + "integrity": "sha512-q8psj4+aDrblJMMtRxihNBdovfzGrXJp1l4JU0Sz4b/Mhsi2DPrKFYCGDwjIWRENs04ELVHxdOJQ7Vs98OFohg==", + "requires": { + "chalk": "^2.3.0", + "lodash.clonedeep": "^4.3.2", + "node-sass": "^4.8.3", + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "strip-ansi": "^4.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-shell": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/gulp-shell/-/gulp-shell-0.7.1.tgz", + "integrity": "sha512-5dKf1eJDdBiUS4LKCt4tm9IkDnWeXKGCKjQG5EJj/bVeVOisAPse5RLxccGh1OtfbzQdOWCywu936DTB8isZRw==", + "requires": { + "chalk": "^2.4.2", + "fancy-log": "^1.3.3", + "lodash.template": "^4.4.0", + "plugin-error": "^1.0.1", + "through2": "^3.0.1", + "tslib": "^1.9.3" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "gulp-sizereport": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/gulp-sizereport/-/gulp-sizereport-1.2.1.tgz", + "integrity": "sha512-thlt5LczraR7umuMLfpGo8XVUNxvU+PsSKWOrj41NjZnMeI4E9t4rLYc3eEAP20Sl1YhfxMER/SRz3udLldQVg==", + "requires": { + "ansi-colors": "^1.0.1", + "beeper": "^1.1.1", + "cli-table": "^0.3.1", + "gzip-size": "^3.0.0", + "plugin-error": "^1.0.0", + "pretty-bytes": "^3.0.1", + "through2": "^2.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "pretty-bytes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", + "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-sourcemaps": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.5.tgz", + "integrity": "sha512-SYLBRzPTew8T5Suh2U8jCSDKY+4NARua4aqjj8HOysBh2tSgT9u4jc1FYirAdPx1akUxxDeK++fqw6Jg0LkQRg==", + "requires": { + "@gulp-sourcemaps/identity-map": "1.X", + "@gulp-sourcemaps/map-sources": "1.X", + "acorn": "5.X", + "convert-source-map": "1.X", + "css": "2.X", + "debug-fabulous": "1.X", + "detect-newline": "2.X", + "graceful-fs": "4.X", + "source-map": "~0.6.0", + "strip-bom-string": "1.X", + "through2": "2.X" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-spawn-mocha": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-spawn-mocha/-/gulp-spawn-mocha-5.0.1.tgz", + "integrity": "sha512-qLWmUE3xvLYRd+tL0Hj/eAR+KPaOXVpNa3GcOSmg8PEVqXi3GS6NKOxWOdqgdXme3LEvV27W+1zLlEizX0Pq/g==", + "requires": { + "lodash": "^4.11.1", + "plugin-error": "^1.0.1", + "through": "~2.3.4" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "requires": { + "glogg": "^1.0.0" + } + }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "requires": { + "duplexer": "^0.1.1" + } + }, + "handle-thing": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==" + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "requires": { + "ajv": "^5.3.0", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "^1.0.2" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "optional": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "optional": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "requires": { + "is-stream": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==" + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "howler": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.1.2.tgz", + "integrity": "sha512-oKrTFaVXsDRoB/jik7cEpWKTj7VieoiuzMYJ7E/EU5ayvmpRhumCv3YQ3823zi9VTJkSWAhbryHnlZAionGAJg==" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "hsts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", + "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", + "requires": { + "depd": "2.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "html-comment-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", + "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=" + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" + }, + "html-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", + "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", + "requires": { + "es6-templates": "^0.2.3", + "fastparse": "^1.1.1", + "html-minifier": "^3.5.8", + "loader-utils": "^1.1.0", + "object-assign": "^4.1.1" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + } + } + }, + "html-minifier": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.16.tgz", + "integrity": "sha512-zP5EfLSpiLRp0aAgud4CQXPQZm9kXwWjR/cF0PfdOj+jjWnOaCgeZcll4kYXSvIBPeUMmyaSc7mM4IDtA+kboA==", + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.1.x", + "commander": "2.15.x", + "he": "1.1.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.3.x" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "uglify-js": { + "version": "3.3.28", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", + "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", + "requires": { + "commander": "~2.15.0", + "source-map": "~0.6.1" + } + } + } + }, + "html5-gamepad": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/html5-gamepad/-/html5-gamepad-1.1.0.tgz", + "integrity": "sha1-/vgdDzSODyEHpeD42dKymKQKrUY=" + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "optional": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } + } + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "requires": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==" + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "imagemin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-7.0.0.tgz", + "integrity": "sha512-TXvCSSIYl4KQUASur9S0+E4olVECzvxvZABU9rNqsza7vzIrUQMRTjyczGf8OmtcgvZ9jOYyinXW3epOpd/04A==", + "requires": { + "file-type": "^12.0.0", + "globby": "^10.0.0", + "junk": "^3.1.0", + "make-dir": "^3.0.0", + "p-pipe": "^3.0.0", + "replace-ext": "^1.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "imagemin-gifsicle": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-6.0.1.tgz", + "integrity": "sha512-kuu47c6iKDQ6R9J10xCwL0lgs0+sMz3LRHqRcJ2CRBWdcNmo3T5hUaM8hSZfksptZXJLGKk8heSAvwtSdB1Fng==", + "optional": true, + "requires": { + "exec-buffer": "^3.0.0", + "gifsicle": "^4.0.0", + "is-gif": "^3.0.0" + } + }, + "imagemin-jpegtran": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-6.0.0.tgz", + "integrity": "sha512-Ih+NgThzqYfEWv9t58EItncaaXIHR0u9RuhKa8CtVBlMBvY0dCIxgQJQCfwImA4AV1PMfmUKlkyIHJjb7V4z1g==", + "optional": true, + "requires": { + "exec-buffer": "^3.0.0", + "is-jpg": "^2.0.0", + "jpegtran-bin": "^4.0.0" + } + }, + "imagemin-optipng": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-7.0.0.tgz", + "integrity": "sha512-N40bmLgiyv5H8xFp/RYmWKdg6Z19MGqzcNW+IWXG7VPrLV75NbcOn8y6A7eZcSHOCNW+DqBx+b95yw+Tf6Sl/g==", + "optional": true, + "requires": { + "exec-buffer": "^3.0.0", + "is-png": "^2.0.0", + "optipng-bin": "^6.0.0" + } + }, + "imagemin-svgo": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-7.0.0.tgz", + "integrity": "sha512-+iGJFaPIMx8TjFW6zN+EkOhlqcemdL7F3N3Y0wODvV2kCUBuUtZK7DRZc1+Zfu4U2W/lTMUyx2G8YMOrZntIWg==", + "optional": true, + "requires": { + "is-svg": "^3.0.0", + "svgo": "^1.0.5" + }, + "dependencies": { + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "optional": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "optional": true, + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "optional": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "optional": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "optional": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "optional": true + }, + "svgo": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz", + "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==", + "optional": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.33", + "csso": "^3.5.1", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + } + } + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "requires": { + "import-from": "^2.1.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" + }, + "inquirer": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", + "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.11", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + } + } + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=" + }, + "into-stream": { + "version": "3.1.0", + "resolved": "http://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "optional": true, + "requires": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "ipware": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ipware/-/ipware-2.0.0.tgz", + "integrity": "sha512-xOqLoYyWIBcO6ANVZK7GgnuH9V7wbqDf6/MrRi4I+Ae1A11m6jcaNRtMSezypkupRUjiSSLq3F/DTvAM1cZfQw==" + }, + "irregular-plurals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", + "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "requires": { + "ci-info": "^1.5.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-gif": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz", + "integrity": "sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw==", + "optional": true, + "requires": { + "file-type": "^10.4.0" + }, + "dependencies": { + "file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", + "optional": true + } + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-jpg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", + "integrity": "sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc=", + "optional": true + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "optional": true + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "optional": true + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "is-path-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz", + "integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==" + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "requires": { + "is-path-inside": "^2.1.0" + }, + "dependencies": { + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "requires": { + "path-is-inside": "^1.0.2" + } + } + } + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "is-png": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-png/-/is-png-2.0.0.tgz", + "integrity": "sha512-4KPGizaVGj2LK7xwJIz8o5B2ubu1D/vcQsgOGFEDlpcvgZHto4gBnyd0ig7Ws+67ixmwKoNmu0hYnpo6AaKb5g==", + "optional": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-svg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", + "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "is_js": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/is_js/-/is_js-0.9.0.tgz", + "integrity": "sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + } + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==" + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "requires": { + "handlebars": "^4.1.2" + }, + "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "optional": true + }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + } + } + } + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "optional": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, + "jpegtran-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz", + "integrity": "sha512-2cRl1ism+wJUoYAYFt6O/rLBfpXNWG2dUWbgcEkTt5WGMnqI46eEro8T4C5zGROxKRqyKpCBSdHPvt5UYCtxaQ==", + "optional": true, + "requires": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "logalot": "^2.0.0" + } + }, + "js-base64": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz", + "integrity": "sha1-8OgK4DmkvWVLXygfyT8EqRSn/M4=" + }, + "js-message": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz", + "integrity": "sha1-IwDSSxrwjondCVvBpMnJz8uJLRU=" + }, + "js-queue": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.0.tgz", + "integrity": "sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug=", + "requires": { + "easy-stack": "^1.0.0" + } + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "optional": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonapi-datastore": { + "version": "0.4.0-beta", + "resolved": "https://registry.npmjs.org/jsonapi-datastore/-/jsonapi-datastore-0.4.0-beta.tgz", + "integrity": "sha1-tJn86STUXivDxheGgVIAY+I2HxA=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==" + }, + "just-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", + "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=" + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==" + }, + "kareem": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", + "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "optional": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "requires": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "requires": { + "flush-write-stream": "^1.0.2" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "livereload-js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", + "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=" + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "lodash.endswith": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.endswith/-/lodash.endswith-4.2.1.tgz", + "integrity": "sha1-/tWawXOO0+I27dcGTsRWRIs3vAk=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "requires": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "logalot": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz", + "integrity": "sha1-X46MkNME7fElMJUaVVSruMXj9VI=", + "optional": true, + "requires": { + "figures": "^1.3.5", + "squeak": "^1.0.0" + } + }, + "loglevel": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.3.tgz", + "integrity": "sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA==" + }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==" + }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=" + }, + "lpad-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz", + "integrity": "sha1-IfYArBwwlcPG5JfuZyce4ISB/p4=", + "optional": true, + "requires": { + "get-stdin": "^4.0.1", + "indent-string": "^2.1.0", + "longest": "^1.0.0", + "meow": "^3.3.0" + } + }, + "lru-cache": { + "version": "2.2.4", + "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", + "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=" + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "requires": { + "es5-ext": "~0.10.2" + } + }, + "magic-string": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", + "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", + "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==" + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-tree": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/markdown-tree/-/markdown-tree-0.0.0.tgz", + "integrity": "sha1-jS3zh/E1/BTtWHHut81cnckZ5EM=", + "requires": { + "marked": "~0.2.10" + }, + "dependencies": { + "marked": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.2.10.tgz", + "integrity": "sha1-1f1oJxyq5hxV0pHQe9UDTP9ec+4=" + } + } + }, + "matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "requires": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "optional": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + } + } + }, + "memoizee": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", + "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", + "requires": { + "d": "1", + "es5-ext": "^0.10.45", + "es6-weak-map": "^2.0.2", + "event-emitter": "^0.3.5", + "is-promise": "^2.1", + "lru-queue": "0.1", + "next-tick": "1", + "timers-ext": "^0.1.5" + }, + "dependencies": { + "es5-ext": { + "version": "0.10.47", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.47.tgz", + "integrity": "sha512-/1TItLfj+TTfWoeRcDn/0FbGV6SNo4R+On2GGVucPU/j3BWnXE2Co8h8CTo4Tu34gFJtnmwS9xiScKs4EjZhdw==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + } + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "merge2": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.4.tgz", + "integrity": "sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", + "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" + }, + "mime-types": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", + "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", + "requires": { + "mime-db": "~1.29.0" + } + }, + "mimic-fn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "requires": { + "minipass": "^2.2.1" + } + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^2.0.1", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mocha": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz", + "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==", + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "modify-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", + "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "mongodb": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.2.7.tgz", + "integrity": "sha512-2YdWrdf1PJgxcCrT1tWoL6nHuk6hCxhddAAaEh8QJL231ci4+P9FLyqopbTm2Z2sAU6mhCri+wd9r1hOcHdoMw==", + "requires": { + "mongodb-core": "3.2.7", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "mongodb-core": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.2.7.tgz", + "integrity": "sha512-WypKdLxFNPOH/Jy6i9z47IjG2wIldA54iDZBmHMINcgKOUcWJh8og+Wix76oGd7EyYkHJKssQ2FAOw5Su/n4XQ==", + "requires": { + "bson": "^1.1.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "mongoose": { + "version": "5.6.11", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.6.11.tgz", + "integrity": "sha512-+Mxfmu2Jcspmdqk1111BLUj5mzSLHWKVovImkjzO9GMymTwkHshsuSFmUN7ou4dWy2WR8DPSJcwU52HhnnNk8Q==", + "requires": { + "async": "2.6.2", + "bson": "~1.1.1", + "kareem": "2.3.1", + "mongodb": "3.2.7", + "mongodb-core": "3.2.7", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.6.0", + "mquery": "3.2.1", + "ms": "2.1.2", + "regexp-clone": "1.0.0", + "safe-buffer": "5.1.2", + "sift": "7.0.1", + "sliced": "1.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, + "morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "mpath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.6.0.tgz", + "integrity": "sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw==" + }, + "mquery": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.1.tgz", + "integrity": "sha512-kY/K8QToZWTTocm0U+r8rqcJCp5PRl6e8tPmoDs5OeSO3DInZE2rAL6AYH+V406JTo8305LdASOQcxRDqHojyw==", + "requires": { + "bluebird": "3.5.1", + "debug": "3.1.0", + "regexp-clone": "^1.0.0", + "safe-buffer": "5.1.2", + "sliced": "1.0.1" + }, + "dependencies": { + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" + }, + "mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "neo-async": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", + "integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==" + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "ngx-bootstrap": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.1.1.tgz", + "integrity": "sha512-++6+YHbIWTR/3sHH9C0aelIP25KMKhylWSEet6HonZfVr8quzEUjq6xEzTnDbZpnxlly2j58Gg0O9DWXu0UCxA==" + }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==" + }, + "nise": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.1.tgz", + "integrity": "sha512-edFWm0fsFG2n318rfEnKlTZTkjlbVOFF9XIA+fj+Ed+Qz1laYW2lobwavWoMzGrYDHH1EpiNJgDfvGnkZztR/g==", + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "node-forge": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + } + } + }, + "node-ipc": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", + "integrity": "sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w==", + "requires": { + "event-pubsub": "4.3.0", + "js-message": "1.0.5", + "js-queue": "2.0.0" + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + } + } + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "tar": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "node-releases": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.27.tgz", + "integrity": "sha512-9iXUqHKSGo6ph/tdXVbHFbhRVQln4ZDTIBJCzsa90HimnBYc5jw8RWYt4wBYFHehGyC3koIz5O4mb2fHrbPOuA==", + "requires": { + "semver": "^5.3.0" + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==" + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "requires": { + "once": "^1.3.2" + } + }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "optional": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + } + }, + "npm-packlist": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz", + "integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "optional": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + } + } + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "dependencies": { + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + } + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "optional": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "dependencies": { + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "optional": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "optional": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "optional": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "optional": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "optional": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "optional": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "optional": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "optional": true + } + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + } + } + }, + "optipng-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-6.0.0.tgz", + "integrity": "sha512-95bB4y8IaTsa/8x6QH4bLUuyvyOoGBCLDA7wOgDL8UFqJpSUh1Hob8JRJhit+wC1ZLN3tQ7mFt7KuBj0x8F2Wg==", + "optional": true, + "requires": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "logalot": "^2.0.0" + } + }, + "ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "requires": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "optional": true, + "requires": { + "arch": "^2.1.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "optional": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "p-event": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", + "integrity": "sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU=", + "optional": true, + "requires": { + "p-timeout": "^1.1.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "optional": true + }, + "p-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=" + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + }, + "p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", + "optional": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-pipe": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-3.0.0.tgz", + "integrity": "sha512-gwwdRFmaxsT3IU+Tl3vYKVRdjfhg8Bbdjw7B+E0y6F7Yz6l+eaQLn0BRmGMXIhcPDONPtOkMoNwx1etZh4zPJA==" + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "optional": true + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "requires": { + "retry": "^0.12.0" + } + }, + "p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", + "optional": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + }, + "dependencies": { + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + } + } + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "parallel-webpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/parallel-webpack/-/parallel-webpack-2.4.0.tgz", + "integrity": "sha512-vJ/TKfPcjayaw+ABcHHUoykxQMA1NtqqYIWnk9wi9n6/DEQfQ8oGmruPpSgz5LdpA/qBkKIfVe6nEmA6TPJazg==", + "requires": { + "ajv": "^4.9.2", + "bluebird": "^3.0.6", + "chalk": "^1.1.1", + "interpret": "^1.0.1", + "lodash.assign": "^4.0.8", + "lodash.endswith": "^4.0.1", + "lodash.flatten": "^4.2.0", + "minimist": "^1.2.0", + "node-ipc": "^9.1.0", + "pluralize": "^1.2.1", + "supports-color": "^3.1.2", + "worker-farm": "^1.3.1" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==" + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "passport": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", + "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-deviantart": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/passport-deviantart/-/passport-deviantart-0.0.5.tgz", + "integrity": "sha1-xcaSdxlIoL1dKb5NiUpZJh8fxOU=", + "requires": { + "passport-oauth": "0.1.x", + "pkginfo": "0.2.x" + } + }, + "passport-github2": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.11.tgz", + "integrity": "sha1-yStW88OKROdmqsfp58E4TF6TyZk=", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-oauth": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/passport-oauth/-/passport-oauth-0.1.15.tgz", + "integrity": "sha1-+3Tgr+hGFL+iVsX8cWzFa7/IzsA=", + "requires": { + "oauth": "0.9.x", + "passport": "~0.1.1", + "pkginfo": "0.2.x" + }, + "dependencies": { + "passport": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.1.18.tgz", + "integrity": "sha1-yCZEedy2QUytu2Z1LRKzfgtlJaE=", + "requires": { + "pause": "0.0.1", + "pkginfo": "0.2.x" + } + } + } + }, + "passport-oauth1": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.1.0.tgz", + "integrity": "sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg=", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "utils-merge": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", + "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-patreon": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/passport-patreon/-/passport-patreon-1.0.1.tgz", + "integrity": "sha1-sJ1jdQ89HNrvTokASg4mCUGsIxM=", + "requires": { + "passport-oauth2": "^1.3.0", + "pkginfo": "^0.4.0", + "request": "^2.74.0" + }, + "dependencies": { + "pkginfo": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz", + "integrity": "sha1-NJ27f/04CB/K3AhT32h/DHdEzWU=" + } + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, + "passport-tumblr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/passport-tumblr/-/passport-tumblr-0.1.2.tgz", + "integrity": "sha1-hbbFFRx54XRFIGUJWBgYBVTljKI=", + "requires": { + "passport-oauth": "0.1.x", + "pkginfo": "0.2.x" + } + }, + "passport-twitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/passport-twitter/-/passport-twitter-1.0.4.tgz", + "integrity": "sha1-AaeZ4fdgvy3knyul+6MigvGJMtc=", + "requires": { + "passport-oauth1": "1.x.x", + "xtraverse": "0.1.x" + } + }, + "passport-vkontakte": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/passport-vkontakte/-/passport-vkontakte-0.3.2.tgz", + "integrity": "sha1-oqda+WRmk2s3X8qVj9V7wQ4f6pI=", + "requires": { + "passport-oauth2": "1.3.x", + "pkginfo": "0.2.x" + }, + "dependencies": { + "passport-oauth2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.3.0.tgz", + "integrity": "sha1-1ytL1i7rgHpAif8wcaIsJsOC3Aw=", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x" + } + } + } + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" + }, + "patreon": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/patreon/-/patreon-0.4.1.tgz", + "integrity": "sha512-aLhjx4rg2BArTq0Kg61MrM4dkJnTQ9kPN8F6a2IlQoYVEtIH7kUK/dprClTx+QYQKlXMfKksN9NCux1YarQJsQ==", + "requires": { + "form-urlencoded": "^2.0.4", + "is-plain-object": "^2.0.4", + "isomorphic-fetch": "^2.2.1", + "jsonapi-datastore": "^0.4.0-beta" + }, + "dependencies": { + "form-urlencoded": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-2.0.9.tgz", + "integrity": "sha512-fWUzNiOnYa126vFAT6TFXd1mhJrvD8IqmQ9ilZPjkLYQfaRreBr5fIUoOpPlWtqaAG64nzoE7u5zSetifab9IA==" + } + } + }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "~2.3" + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "optional": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + } + } + }, + "pkginfo": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz", + "integrity": "sha1-cjnEKl72wwuPMoQ52bn/cQQkkPg=" + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "requires": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + } + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=" + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "requires": { + "kind-of": "^1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=" + } + } + }, + "plur": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", + "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==", + "requires": { + "irregular-plurals": "^2.0.0" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=" + }, + "portfinder": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.21.tgz", + "integrity": "sha512-ESabpDCzmBS3ekHbmpAIiESq3udRsCBGiBZLsC+HgBKv2ezb0R4oG+7RnYEVZ/ZCfhel5Tx3UzdNWA0Lox2QCA==", + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "requires": { + "postcss": "^5.0.2", + "postcss-message-helpers": "^2.0.0", + "reduce-css-calc": "^1.2.6" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-colormin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", + "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "requires": { + "colormin": "^1.0.5", + "postcss": "^5.0.13", + "postcss-value-parser": "^3.2.3" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-convert-values": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", + "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "requires": { + "postcss": "^5.0.11", + "postcss-value-parser": "^3.1.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-comments": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", + "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "requires": { + "postcss": "^5.0.14" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-duplicates": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", + "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-empty": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", + "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "requires": { + "postcss": "^5.0.14" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", + "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", + "requires": { + "postcss": "^5.0.16" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-discard-unused": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", + "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "requires": { + "postcss": "^5.0.14", + "uniqs": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-filter-plugins": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz", + "integrity": "sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ==", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-load-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", + "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", + "requires": { + "cosmiconfig": "^4.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz", + "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.1" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "postcss": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.2.tgz", + "integrity": "sha512-fmaUY5370keLUTx+CnwRxtGiuFTcNBLQBqr1oE3WZ/euIYmGAo0OAgOhVJ3ByDnVmOR3PK+0V9VebzfjRIUcqw==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "postcss-merge-idents": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", + "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "requires": { + "has": "^1.0.1", + "postcss": "^5.0.10", + "postcss-value-parser": "^3.1.1" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-merge-longhand": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", + "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-merge-rules": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", + "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "requires": { + "browserslist": "^1.5.2", + "caniuse-api": "^1.5.2", + "postcss": "^5.0.4", + "postcss-selector-parser": "^2.2.2", + "vendors": "^1.0.0" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "^1.0.30000639", + "electron-to-chromium": "^1.2.7" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=" + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", + "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "requires": { + "object-assign": "^4.0.1", + "postcss": "^5.0.4", + "postcss-value-parser": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-minify-gradients": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", + "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "requires": { + "postcss": "^5.0.12", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-minify-params": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", + "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "requires": { + "alphanum-sort": "^1.0.1", + "postcss": "^5.0.2", + "postcss-value-parser": "^3.0.2", + "uniqs": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-minify-selectors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", + "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "requires": { + "alphanum-sort": "^1.0.2", + "has": "^1.0.1", + "postcss": "^5.0.14", + "postcss-selector-parser": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-normalize-charset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", + "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "requires": { + "postcss": "^5.0.5" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-normalize-url": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", + "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^1.4.0", + "postcss": "^5.0.14", + "postcss-value-parser": "^3.2.3" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-ordered-values": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", + "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "requires": { + "postcss": "^5.0.4", + "postcss-value-parser": "^3.0.1" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-reduce-idents": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", + "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "requires": { + "postcss": "^5.0.4", + "postcss-value-parser": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-reduce-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", + "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "requires": { + "postcss": "^5.0.4" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-reduce-transforms": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", + "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "requires": { + "has": "^1.0.1", + "postcss": "^5.0.8", + "postcss-value-parser": "^3.0.1" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", + "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "requires": { + "is-svg": "^2.0.0", + "postcss": "^5.0.14", + "postcss-value-parser": "^3.2.3", + "svgo": "^0.7.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", + "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "requires": { + "alphanum-sort": "^1.0.1", + "postcss": "^5.0.4", + "uniqs": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" + }, + "postcss-zindex": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", + "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "requires": { + "has": "^1.0.1", + "postcss": "^5.0.4", + "uniqs": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "pretty-bytes": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.2.0.tgz", + "integrity": "sha512-ujANBhiUsl9AhREUDUEY1GPOharMGm8x8juS7qOHybcLi7XsKfrYQ88hSly1l2i0klXHTDYrlL8ihMCG55Dc3w==" + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" + }, + "private": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "optional": true + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.2.tgz", + "integrity": "sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-html-loader": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pug-html-loader/-/pug-html-loader-1.1.5.tgz", + "integrity": "sha512-EOcLK0z0vJ1fSXsDypqL8SsPXVaevS9Oy8JA1RpYYkgAOV9qJXCSedvMYw499hSXxmoXAXIV1qEF2fNFBTwyEQ==", + "requires": { + "loader-utils": "^0.2.17", + "pug": "^2.0.0-rc.2" + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-plugin-ng": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/pug-plugin-ng/-/pug-plugin-ng-0.0.2.tgz", + "integrity": "sha1-ebj6YZx7kTrSoOlNVZlD6ruJpR8=", + "requires": { + "character-parser": "^2.1.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz", + "integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==", + "requires": { + "duplexify": "^3.5.3", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "duplexify": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz", + "integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "q": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz", + "integrity": "sha1-3QG6ydBtMObyGa7LglPunr3DCPE=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } + } + }, + "raw-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-1.0.0.tgz", + "integrity": "sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==", + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "requires": { + "ast-types": "0.9.6", + "esprima": "~3.1.0", + "private": "~0.1.5", + "source-map": "~0.5.0" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + } + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "requires": { + "balanced-match": "^0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "regexp-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", + "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" + }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "requires": { + "rc": "^1.0.1" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "requires": { + "es6-error": "^4.0.1" + } + }, + "remap-istanbul": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.13.0.tgz", + "integrity": "sha512-rS5ZpVAx3fGtKZkiBe1esXg5mKYbgW9iz8kkADFt3p6lo3NsBBUX1q6SwdhwUtYCGnr7nK6gRlbYK3i8R0jbRA==", + "requires": { + "istanbul": "0.4.5", + "minimatch": "^3.0.4", + "plugin-error": "^1.0.1", + "source-map": "0.6.1", + "through2": "3.0.0" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", + "integrity": "sha512-8B+sevlqP4OiCjonI1Zw03Sf8PuV1eRsYQgLad5eonILOdyeRsY27A/2Ze8IlvlMvq31OH+3fz/styI7Ya62yQ==", + "requires": { + "readable-stream": "2 || 3", + "xtend": "~4.0.1" + } + } + } + }, + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + } + }, + "remove-trailing-separator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", + "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "requires": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + }, + "dependencies": { + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + } + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "requires": { + "mime-db": "~1.35.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "request-ip": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-2.0.2.tgz", + "integrity": "sha1-3urm1K8hdoSX24zQX6NxQ/jxJX4=", + "requires": { + "is_js": "^0.9.0" + } + }, + "request-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz", + "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==", + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "requires": { + "lodash": "^4.17.11" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resolve": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "requires": { + "value-or-function": "^3.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "optional": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + } + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rev-hash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rev-hash/-/rev-hash-2.0.0.tgz", + "integrity": "sha1-dyCiNu0MJY3z5kvsA+wEiwW5JMQ=" + }, + "rev-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rev-path/-/rev-path-2.0.0.tgz", + "integrity": "sha512-G5R2L9gYu9kEuqPfIFgO9gO+OhBWOAT83HyauOQmGHO6y9Fsa4acv+XsmNhNDrod0HDh1/VxJRmsffThzeHJlQ==", + "requires": { + "modify-filename": "^1.0.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "requires": { + "glob": "^7.0.5" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rollbar": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/rollbar/-/rollbar-2.12.2.tgz", + "integrity": "sha512-KUZ3dhR55HNhVS1fsBc3UC8Sdh53T1tVMGhRY8IXxDHWxVCQIGH8HTMZ7mFGcidS0WQkNG9ElsTiV9B8IEpyBw==", + "requires": { + "async": "~1.2.1", + "buffer-from": ">=1.1", + "console-polyfill": "0.3.0", + "debug": "2.6.9", + "decache": "^3.0.5", + "error-stack-parser": "1.3.3", + "json-stringify-safe": "~5.0.0", + "lru-cache": "~2.2.1", + "request-ip": "~2.0.1", + "source-map": ">=0.5.0", + "uuid": "3.0.x" + }, + "dependencies": { + "async": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz", + "integrity": "sha1-pIFqF81f9RbfosdpikUzabl5DeA=" + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + } + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", + "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + } + } + }, + "sass-loader": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.3.1.tgz", + "integrity": "sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==", + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.0.1", + "neo-async": "^2.5.0", + "pify": "^4.0.1", + "semver": "^6.3.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + } + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "optional": true, + "requires": { + "commander": "~2.8.1" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "selfsigned": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.4.tgz", + "integrity": "sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw==", + "requires": { + "node-forge": "0.7.5" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "requires": { + "semver": "^5.0.3" + } + }, + "semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "requires": { + "sver-compat": "^1.5.0" + } + }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "optional": true + }, + "semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", + "optional": true, + "requires": { + "semver": "^5.3.0" + } + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "serialize-javascript": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz", + "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==" + }, + "serve-favicon": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", + "integrity": "sha1-k10kDN/g9YBTB/3+ln2IlCosvPA=", + "requires": { + "etag": "~1.8.1", + "fresh": "0.5.2", + "ms": "2.1.1", + "parseurl": "~1.3.2", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + } + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "dependencies": { + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + } + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "sift": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", + "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.0.3.tgz", + "integrity": "sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==", + "requires": { + "decompress-response": "^3.3.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "sinon": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.4.1.tgz", + "integrity": "sha512-7s9buHGHN/jqoy/v4bJgmt0m1XEkCEd/tqdHXumpBp0JSujaT4Ng84JU5wDdK4E85ZMq78NuDe0I3NAqXY8TFg==", + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.2", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.1", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "sockjs-client": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.3.0.tgz", + "integrity": "sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==", + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "optional": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "sourcemap-codec": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz", + "integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==" + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==" + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "^1.0.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "spdy": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", + "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "squeak": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz", + "integrity": "sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=", + "optional": true, + "requires": { + "chalk": "^1.0.0", + "console-stream": "^0.1.1", + "lpad-align": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "optional": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "optional": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stackframe": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz", + "integrity": "sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "~0.1.1" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + } + } + }, + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==" + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-bom-buf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz", + "integrity": "sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=", + "requires": { + "is-utf8": "^0.2.1" + } + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=" + }, + "strip-comments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz", + "integrity": "sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw==", + "requires": { + "babel-extract-comments": "^1.0.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0" + } + }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "optional": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "optional": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "requires": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "requires": { + "coa": "~1.0.1", + "colors": "~1.1.2", + "csso": "~2.3.1", + "js-yaml": "~3.7.0", + "mkdirp": "~0.5.1", + "sax": "~1.2.1", + "whet.extend": "~0.9.9" + } + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==" + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "optional": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "optional": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", + "optional": true + }, + "tempfile": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", + "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", + "optional": true, + "requires": { + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "requires": { + "execa": "^0.7.0" + } + }, + "ternary-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", + "integrity": "sha512-oIzdi+UL/JdktkT+7KU5tSIQjj8pbShj3OASuvDEhm0NT5lppsm7aXWAmAq4/QMaBIyfuEcNLbAQA+HpaISobQ==", + "requires": { + "duplexify": "^4.1.1", + "fork-stream": "^0.0.4", + "merge-stream": "^2.0.0", + "through2": "^3.0.1" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "requires": { + "readable-stream": "2 || 3" + } + } + } + }, + "terser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.2.1.tgz", + "integrity": "sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", + "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "requires": { + "prr": "~1.0.1" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "serialize-javascript": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.8.0.tgz", + "integrity": "sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "requires": { + "errno": "~0.1.7" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "^3.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "through2-concurrent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-concurrent/-/through2-concurrent-2.0.0.tgz", + "integrity": "sha512-R5/jLkfMvdmDD+seLwN7vB+mhbqzWop5fAjx5IX8/yQq7VhBhzDmhXgaHAOnhnWkCpRMM7gToYHycB0CS/pd+A==", + "requires": { + "through2": "^2.0.0" + } + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + } + } + }, + "thunky": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", + "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==" + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "requires": { + "es5-ext": "~0.10.46", + "next-tick": "1" + }, + "dependencies": { + "es5-ext": { + "version": "0.10.47", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.47.tgz", + "integrity": "sha512-/1TItLfj+TTfWoeRcDn/0FbGV6SNo4R+On2GGVucPU/j3BWnXE2Co8h8CTo4Tu34gFJtnmwS9xiScKs4EjZhdw==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + } + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + }, + "tiny-lr": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.0.5.tgz", + "integrity": "sha512-YrxUSiMgOVh3PnAqtdAUQuUVEVRnqcRCxJ3BHrl/aaWV2fplKKB60oClM0GH2Gio2hcXvkxMUxsC/vXZrQePlg==", + "requires": { + "body": "^5.1.0", + "debug": "~2.6.7", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.2.2", + "object-assign": "^4.1.0", + "qs": "^6.4.0" + } + }, + "tinytim": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tinytim/-/tinytim-0.1.1.tgz", + "integrity": "sha1-yWih5VWa2VUyJO92J7qzTjyu+Kg=" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "optional": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "requires": { + "through2": "^2.0.3" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "tracer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tracer/-/tracer-1.0.1.tgz", + "integrity": "sha512-/kxtMu2KWtdjQSzFzLnA8bYpDKHlcK2BGX4e6r7n0dbD7BGTQjGmwZKNHwFPQaHjzBHtNtWva0frymfYtAWZyw==", + "requires": { + "colors": "1.3.3", + "dateformat": "3.0.3", + "mkdirp": "^0.5.1", + "nyc": "^14.1.1", + "tinytim": "0.1.1" + }, + "dependencies": { + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + } + } + }, + "transliteration": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-1.6.6.tgz", + "integrity": "sha1-in6KswRK0Z8jP1DBWJTL9p5dIF4=", + "requires": { + "yargs": "^12.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "mem": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", + "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^1.0.0", + "p-is-promise": "^2.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-is-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", + "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==" + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "traverse-chain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", + "optional": true + }, + "tree-kill": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", + "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==" + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "optional": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "requires": { + "glob": "^7.1.2" + } + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + }, + "ts-loader": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.0.4.tgz", + "integrity": "sha512-p2zJYe7OtwR+49kv4gs7v4dMrfYD1IPpOtqiSPCbe8oR+4zEBtdHwzM7A7M91F+suReqgzZrlClk4LRSSp882g==", + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "semver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==" + } + } + }, + "ts-node": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", + "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==" + } + } + }, + "tsickle": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.37.0.tgz", + "integrity": "sha512-ufUZqLUNqh+kOfr52N/hJ5JbiDO32/CO7ZCteZBX9HA2kiejwEgDaJeJe1GAj2TIu683IgTA/LPKvlns6Liw0w==", + "requires": { + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tslint": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.19.0.tgz", + "integrity": "sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "dependencies": { + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + } + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==" + }, + "ua-parser-js": { + "version": "0.7.20", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", + "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", + "requires": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "schema-utils": "^0.4.5", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "uglify-es": "^3.3.4", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "requires": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + } + } + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, + "unbzip2-stream": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", + "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "optional": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + }, + "dependencies": { + "buffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.0.tgz", + "integrity": "sha512-Xpgy0IwHK2N01ncykXTy6FpCWuM+CJSHoPVBLyNqyrWxsedpLvwsYUhf0ME3WRFNUhos0dMamz9cOS/xRDtU5g==", + "optional": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "undertaker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", + "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "requires": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + } + }, + "undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=" + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + } + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", + "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "requires": { + "json-stable-stringify": "^1.0.0", + "through2-filter": "^2.0.0" + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "optional": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "requires": { + "prepend-http": "^1.0.1" + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "optional": true + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "optional": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==" + }, + "v8flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.2.tgz", + "integrity": "sha512-MtivA7GF24yMPte9Rp/BWGCYQNaUj86zeYxV/x2RRJMKagImbbv3u8iJC57lNhWLPcGLJmHcHmFWkNsplbbLWw==", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "~1.0.0", + "spdx-expression-parse": "~1.0.0" + } + }, + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", + "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + } + } + }, + "vinyl-file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-3.0.0.tgz", + "integrity": "sha1-sQTZ5ECf+jJfqt1SBkLQo7SIs2U=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.3.0", + "strip-bom-buf": "^1.0.0", + "strip-bom-stream": "^2.0.0", + "vinyl": "^2.0.1" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" + }, + "first-chunk-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", + "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", + "requires": { + "readable-stream": "^2.0.2" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-bom-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", + "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", + "requires": { + "first-chunk-stream": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + } + } + }, + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "requires": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "requires": { + "source-map": "^0.5.1" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==" + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "webpack": { + "version": "4.39.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.3.tgz", + "integrity": "sha512-BXSI9M211JyCVc3JxHWDpze85CvjC842EvpRsVTc/d15YJGlox7GIDd38kJgWrb3ZluyvIjgenbLDMBQPDcxYQ==", + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.1", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==" + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "webpack-bundle-analyzer": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.4.1.tgz", + "integrity": "sha512-Bs8D/1zF+17lhqj2OYmzi7HEVYqEVxu7lCO9Ff8BwajenOU0vAwEoV8e4ICCPNZAcqR1PCR/7o2SkW+cnCmF0A==", + "requires": { + "acorn": "^6.0.7", + "acorn-walk": "^6.1.1", + "bfj": "^6.1.1", + "chalk": "^2.4.1", + "commander": "^2.18.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "filesize": "^3.6.1", + "gzip-size": "^5.0.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "opener": "^1.5.1", + "ws": "^6.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", + "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==" + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + } + } + }, + "webpack-cli": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.7.tgz", + "integrity": "sha512-OhTUCttAsr+IZSMVwGROGRHvT+QAs8H6/mHIl4SvhAwYywjiylYjpwybGx7WQ9Hkb45FhjtsymkwiRRbGJ1SZQ==", + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz", + "integrity": "sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA==", + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.2", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + } + } + }, + "webpack-dev-server": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.7.2.tgz", + "integrity": "sha512-mjWtrKJW2T9SsjJ4/dxDC2fkFVUw8jlpemDERqV0ZJIkjjjamR2AbQlr3oz+j4JLhYCHImHnXZK5H06P2wvUew==", + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.6", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.2.1", + "http-proxy-middleware": "^0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "killable": "^1.0.1", + "loglevel": "^1.6.3", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.20", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.4", + "semver": "^6.1.1", + "serve-index": "^1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.3.0", + "spdy": "^4.0.0", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.0", + "webpack-log": "^2.0.0", + "yargs": "12.0.5" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "requires": { + "lodash": "^4.17.15" + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "requires": { + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", + "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=" + }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=" + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "requires": { + "string-width": "^1.0.2" + } + }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "workbox-background-sync": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz", + "integrity": "sha512-1uFkvU8JXi7L7fCHVBEEnc3asPpiAL33kO495UMcD5+arew9IbKW2rV5lpzhoWcm/qhGB89YfO4PmB/0hQwPRg==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-broadcast-update": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-4.3.1.tgz", + "integrity": "sha512-MTSfgzIljpKLTBPROo4IpKjESD86pPFlZwlvVG32Kb70hW+aob4Jxpblud8EhNb1/L5m43DUM4q7C+W6eQMMbA==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-build": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-4.3.1.tgz", + "integrity": "sha512-UHdwrN3FrDvicM3AqJS/J07X0KXj67R8Cg0waq1MKEOqzo89ap6zh6LmaLnRAjpB+bDIz+7OlPye9iii9KBnxw==", + "requires": { + "@babel/runtime": "^7.3.4", + "@hapi/joi": "^15.0.0", + "common-tags": "^1.8.0", + "fs-extra": "^4.0.2", + "glob": "^7.1.3", + "lodash.template": "^4.4.0", + "pretty-bytes": "^5.1.0", + "stringify-object": "^3.3.0", + "strip-comments": "^1.0.2", + "workbox-background-sync": "^4.3.1", + "workbox-broadcast-update": "^4.3.1", + "workbox-cacheable-response": "^4.3.1", + "workbox-core": "^4.3.1", + "workbox-expiration": "^4.3.1", + "workbox-google-analytics": "^4.3.1", + "workbox-navigation-preload": "^4.3.1", + "workbox-precaching": "^4.3.1", + "workbox-range-requests": "^4.3.1", + "workbox-routing": "^4.3.1", + "workbox-strategies": "^4.3.1", + "workbox-streams": "^4.3.1", + "workbox-sw": "^4.3.1", + "workbox-window": "^4.3.1" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "workbox-cacheable-response": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz", + "integrity": "sha512-Rp5qlzm6z8IOvnQNkCdO9qrDgDpoPNguovs0H8C+wswLuPgSzSp9p2afb5maUt9R1uTIwOXrVQMmPfPypv+npw==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-cli": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-cli/-/workbox-cli-4.3.1.tgz", + "integrity": "sha512-2qkAVvqU40d40UES7jMx+vAeLMMjrzc6DO1g98z1bIjA53q1qyzF97SY8xMY/+WnGTO9l6/c/Jk8OdFCih3dsQ==", + "requires": { + "@babel/runtime": "^7.0.0", + "chalk": "^2.4.1", + "common-tags": "^1.8.0", + "fs-extra": "^7.0.0", + "glob": "^7.1.3", + "glob-watcher": "^5.0.3", + "inquirer": "^6.2.0", + "meow": "^5.0.0", + "ora": "^3.0.0", + "pretty-bytes": "^5.1.0", + "update-notifier": "^2.5.0", + "workbox-build": "^4.3.1" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=" + }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "^3.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "workbox-core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-4.3.1.tgz", + "integrity": "sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg==" + }, + "workbox-expiration": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-4.3.1.tgz", + "integrity": "sha512-vsJLhgQsQouv9m0rpbXubT5jw0jMQdjpkum0uT+d9tTwhXcEZks7qLfQ9dGSaufTD2eimxbUOJfWLbNQpIDMPw==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-google-analytics": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-4.3.1.tgz", + "integrity": "sha512-xzCjAoKuOb55CBSwQrbyWBKqp35yg1vw9ohIlU2wTy06ZrYfJ8rKochb1MSGlnoBfXGWss3UPzxR5QL5guIFdg==", + "requires": { + "workbox-background-sync": "^4.3.1", + "workbox-core": "^4.3.1", + "workbox-routing": "^4.3.1", + "workbox-strategies": "^4.3.1" + } + }, + "workbox-navigation-preload": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-4.3.1.tgz", + "integrity": "sha512-K076n3oFHYp16/C+F8CwrRqD25GitA6Rkd6+qAmLmMv1QHPI2jfDwYqrytOfKfYq42bYtW8Pr21ejZX7GvALOw==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-precaching": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-4.3.1.tgz", + "integrity": "sha512-piSg/2csPoIi/vPpp48t1q5JLYjMkmg5gsXBQkh/QYapCdVwwmKlU9mHdmy52KsDGIjVaqEUMFvEzn2LRaigqQ==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-range-requests": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-4.3.1.tgz", + "integrity": "sha512-S+HhL9+iTFypJZ/yQSl/x2Bf5pWnbXdd3j57xnb0V60FW1LVn9LRZkPtneODklzYuFZv7qK6riZ5BNyc0R0jZA==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-routing": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-4.3.1.tgz", + "integrity": "sha512-FkbtrODA4Imsi0p7TW9u9MXuQ5P4pVs1sWHK4dJMMChVROsbEltuE79fBoIk/BCztvOJ7yUpErMKa4z3uQLX+g==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-strategies": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-4.3.1.tgz", + "integrity": "sha512-F/+E57BmVG8dX6dCCopBlkDvvhg/zj6VDs0PigYwSN23L8hseSRwljrceU2WzTvk/+BSYICsWmRq5qHS2UYzhw==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-streams": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-4.3.1.tgz", + "integrity": "sha512-4Kisis1f/y0ihf4l3u/+ndMkJkIT4/6UOacU3A4BwZSAC9pQ9vSvJpIi/WFGQRH/uPXvuVjF5c2RfIPQFSS2uA==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "workbox-sw": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-4.3.1.tgz", + "integrity": "sha512-0jXdusCL2uC5gM3yYFT6QMBzKfBr2XTk0g5TPAV4y8IZDyVNDyj1a8uSXy3/XrvkVTmQvLN4O5k3JawGReXr9w==" + }, + "workbox-window": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-4.3.1.tgz", + "integrity": "sha512-C5gWKh6I58w3GeSc0wp2Ne+rqVw8qwcmZnQGpjiek8A2wpbxSJb1FdCoQVO+jDJs35bFgo/WETgl1fqgsxN0Hg==", + "requires": { + "workbox-core": "^4.3.1" + } + }, + "worker-farm": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.5.2.tgz", + "integrity": "sha512-XxiQ9kZN5n6mmnW+mFJ+wXjNNI/Nx4DIdaAKLX1Bn6LYBWlN/zaBhu34DQYPZ1AJobQuu67S2OfDdNSVULvXkQ==", + "requires": { + "errno": "^0.1.4", + "xtend": "^4.0.1" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrapper-webpack-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrapper-webpack-plugin/-/wrapper-webpack-plugin-2.1.0.tgz", + "integrity": "sha512-e+2FhSYGCxhDq3PcUw5mRhH+8vcYa+9d9AuLChJUZ9ZbUPhQOHZ/O2dnN98iTqeUuvrzSSOv13+x/NhrAm5JEg==", + "requires": { + "webpack-sources": "^1.1.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", + "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + }, + "xhr2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "xtraverse": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xtraverse/-/xtraverse-0.1.0.tgz", + "integrity": "sha1-t0G60BjveNip0ug63gB7P3lZxzI=", + "requires": { + "xmldom": "0.1.x" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.0.0.tgz", + "integrity": "sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } + }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "optional": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", + "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==" + }, + "zone.js": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.2.tgz", + "integrity": "sha512-UAYfiuvxLN4oyuqhJwd21Uxb4CNawrq6fPS/05Su5L4G+1TN+HVDJMUHNMobVQDFJRir2cLAODXwluaOKB7HFg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..585b933 --- /dev/null +++ b/package.json @@ -0,0 +1,203 @@ +{ + "name": "pony-town", + "version": "0.53.0-alpha", + "description": "A game of ponies building a town", + "main": "pony-town.js", + "scripts": { + "start": "node pony-town.js --login --admin --game", + "start-inspect": "node --inspect pony-town.js --login --admin --game", + "lint": "tslint -c tslint.json src/ts/**/*.ts", + "dev": "gulp dev", + "dev-sprites": "gulp dev --sprites", + "sw-generate": "workbox generateSW workbox-config.js", + "sw-uglify": "uglifyjs build/sw.js -o build/sw.min.js", + "sw": "npm run sw-generate && npm run sw-uglify", + "ts": "tsc --rootDir src/ts --outDir src/scripts --pretty", + "ts-watch": "npm run ts -- --watch", + "ts-cov": "npm run ts -- --target es5", + "ts-cov-watch": "npm run ts -- --target es5 --watch", + "webpack-prod": "node --max_old_space_size=8192 node_modules/webpack-cli/bin/cli --config webpack.prod.js --progress --color", + "webpack-prod-parallel": "node --max_old_space_size=8192 node_modules/parallel-webpack/bin/run --config webpack.prod.js --progress --color", + "webpack-main": "npm run webpack-prod -- --env.main", + "webpack-analyze": "npm run webpack-prod -- --env.analyze", + "webpack-debug": "npm run webpack-prod -- --env.debug", + "webpack-admin": "npm run webpack-prod -- --env.admin", + "webpack-stats": "npm run webpack-prod -- --env.debug --json > stats.json", + "webpack-verbose": "npm run webpack-prod -- --verbose", + "wds": "webpack-dev-server --config webpack.dev.js --progress --color --debug --port 8091", + "build": "gulp build", + "build-beta": "npm run build -- --beta --tools", + "build-fast": "npm run build -- --parallel", + "build-maps": "npm run build -- --maps", + "build-tools": "npm run build -- --tools", + "build-debug": "npm run build -- --debug", + "test": "npm run ts && npm run test-js", + "test-js": "mocha \"src/scripts/tests/**/*.spec.js\" -R progress --exit", + "test-watch": "npm run test-js -- --watch", + "test-ts": "node --max-old-space-size=8192 ./node_modules/mocha/bin/mocha \"src/ts/tests/**/*.spec.ts\" -r ts-node/register -R progress --exit", + "test-ts-watch": "npm run test -- --watch --watch-extensions ts", + "test-inspect": "mocha \"src/scripts/tests/**/*.spec.js\" -R progress --inspect --exit --watch", + "update-angular": "npm update @angular/common @angular/compiler @angular/core @angular/forms @angular/platform-browser @angular/platform-browser-dynamic @angular/router @angular/animations @angular/compiler-cli @angular/platform-server @angular/cdk --save", + "latest-angular": "npm i @angular/common@latest @angular/compiler@latest @angular/core@latest @angular/forms@latest @angular/platform-browser@latest @angular/platform-browser-dynamic@latest @angular/router@latest @angular/animations@latest @angular/compiler-cli@latest @angular/platform-server@latest @angular/cdk@latest --save", + "update-fontawesome": "npm update @fortawesome/fontawesome-svg-core @fortawesome/free-brands-svg-icons @fortawesome/free-solid-svg-icons @fortawesome/angular-fontawesome --save" + }, + "devDependencies": {}, + "dependencies": { + "@angular/animations": "^8.2.3", + "@angular/cdk": "^8.1.4", + "@angular/common": "^8.2.3", + "@angular/compiler": "^8.2.3", + "@angular/compiler-cli": "^8.2.3", + "@angular/core": "^8.2.3", + "@angular/forms": "^8.2.3", + "@angular/platform-browser": "^8.2.3", + "@angular/platform-browser-dynamic": "^8.2.3", + "@angular/platform-server": "^8.2.3", + "@angular/router": "^8.2.3", + "@clusterws/cws": "^0.15.0", + "@fortawesome/angular-fontawesome": "^0.5.0", + "@fortawesome/fontawesome-svg-core": "^1.2.22", + "@fortawesome/free-brands-svg-icons": "^5.10.2", + "@fortawesome/free-solid-svg-icons": "^5.10.2", + "@ngtools/webpack": "^8.3.1", + "@passport-next/passport-facebook": "^3.1.2", + "@passport-next/passport-google-oauth2": "^1.0.0", + "@types/base64-js": "^1.2.5", + "@types/bluebird": "^3.5.27", + "@types/body-parser": "^1.17.1", + "@types/chai": "^4.2.0", + "@types/chai-as-promised": "^7.1.2", + "@types/cookie-parser": "^1.4.2", + "@types/del": "^4.0.0", + "@types/express": "^4.17.1", + "@types/express-brute": "0.0.37", + "@types/express-session": "^1.15.14", + "@types/file-saver": "^2.0.1", + "@types/fs-extra": "^8.0.0", + "@types/gl-matrix": "^2.4.5", + "@types/glob": "^7.1.1", + "@types/google.analytics": "0.0.40", + "@types/howler": "^2.1.1", + "@types/lodash": "^4.14.138", + "@types/mocha": "^5.2.7", + "@types/mongodb": "^3.3.1", + "@types/mongoose": "^5.5.15", + "@types/morgan": "^1.7.37", + "@types/node": "^12.7.2", + "@types/node-ipc": "^9.1.1", + "@types/passport": "^1.0.1", + "@types/passport-local": "^1.0.33", + "@types/pug": "^2.0.4", + "@types/request": "^2.48.2", + "@types/request-promise": "^4.1.44", + "@types/serve-favicon": "^2.2.31", + "@types/sinon": "^7.0.13", + "@types/ua-parser-js": "^0.7.33", + "@types/uws": "^0.13.2", + "@types/yargs": "^13.0.2", + "ag-psd": "^6.2.0", + "ag-sockets": "^4.7.0", + "angular2-template-loader": "^0.6.2", + "base64-js": "^1.3.1", + "bluebird": "^3.5.5", + "body-parser": "^1.19.0", + "bootstrap": "^4.3.1", + "canvas": "^2.6.0", + "canvas-toBlob": "^1.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "chalk": "^2.4.2", + "clusterws-uws": "^0.4.6", + "color-convert": "^2.0.1", + "connect-mongo": "^3.0.0", + "cookie-parser": "^1.4.4", + "core-js": "^3.2.1", + "del": "^5.1.0", + "delta-e": "0.0.7", + "errorhandler": "^1.5.1", + "express": "^4.17.1", + "express-brute": "^1.0.1", + "express-session": "^1.16.2", + "file-saver": "^2.0.2", + "focus-visible": "^5.0.2", + "font-awesome": "^4.7.0", + "frameguard": "^3.1.0", + "fs-extra": "^8.1.0", + "gif.js": "^0.2.0", + "gulp": "^4.0.2", + "gulp-autoprefixer": "^7.0.0", + "gulp-cssnano": "^2.1.3", + "gulp-if": "^3.0.0", + "gulp-imagemin": "^6.1.0", + "gulp-live-server": "0.0.31", + "gulp-plumber": "^1.2.1", + "gulp-rev": "^9.0.0", + "gulp-sass": "^4.0.2", + "gulp-shell": "^0.7.1", + "gulp-sizereport": "^1.2.1", + "gulp-sourcemaps": "^2.6.5", + "gulp-spawn-mocha": "^5.0.1", + "howler": "^2.1.2", + "hsts": "^2.2.0", + "html-loader": "^0.5.5", + "html5-gamepad": "^1.1.0", + "ipware": "^2.0.0", + "lodash": "^4.17.15", + "markdown-tree": "0.0.0", + "merge2": "^1.2.4", + "mocha": "^6.2.0", + "moment": "^2.24.0", + "mongoose": "^5.6.11", + "morgan": "^1.9.1", + "ngx-bootstrap": "^5.1.1", + "node-ipc": "^9.1.1", + "node-sass": "^4.12.0", + "parallel-webpack": "^2.4.0", + "passport": "^0.4.0", + "passport-deviantart": "0.0.5", + "passport-github2": "^0.1.11", + "passport-local": "^1.0.0", + "passport-patreon": "^1.0.1", + "passport-tumblr": "^0.1.2", + "passport-twitter": "^1.0.4", + "passport-vkontakte": "^0.3.2", + "patreon": "^0.4.1", + "postcss-loader": "^3.0.0", + "pug": "^2.0.4", + "pug-html-loader": "^1.1.5", + "pug-plugin-ng": "0.0.2", + "raw-loader": "^1.0.0", + "reflect-metadata": "^0.1.13", + "remap-istanbul": "^0.13.0", + "request": "^2.88.0", + "request-promise": "^4.2.4", + "rollbar": "^2.12.2", + "rxjs": "^6.5.2", + "sass-loader": "^7.3.1", + "serve-favicon": "^2.5.0", + "sinon": "^7.4.1", + "source-list-map": "^2.0.1", + "source-map-support": "^0.5.13", + "timsort": "^0.3.0", + "tracer": "^1.0.1", + "transliteration": "1.6.6", + "ts-loader": "^6.0.4", + "ts-node": "^8.3.0", + "tsickle": "^0.37.0", + "tslib": "^1.10.0", + "tslint": "^5.19.0", + "tsutils": "^3.17.1", + "typescript": "^3.5.3", + "ua-parser-js": "^0.7.20", + "uglifyjs-webpack-plugin": "^1.3.0", + "webpack": "^4.39.3", + "webpack-bundle-analyzer": "^3.4.1", + "webpack-cli": "^3.3.7", + "webpack-dev-server": "3.7.2", + "webpack-merge": "^4.2.2", + "workbox-cli": "^4.3.1", + "wrapper-webpack-plugin": "^2.1.0", + "yargs": "^14.0.0", + "zone.js": "^0.10.2" + } +} diff --git a/pony-town-offline.js b/pony-town-offline.js new file mode 100644 index 0000000..a55c5a8 --- /dev/null +++ b/pony-town-offline.js @@ -0,0 +1,8 @@ +var express = require('express'); +var path = require('path'); +var config = require('./config.json'); +var app = express(); +app.set('port', config.port); +app.get('/images/offline-pony.png', (_, res) => res.sendFile(path.join(__dirname, 'public', 'images', 'offline-pony.png'))); +app.get('/*', (_, res) => res.sendFile(path.join(__dirname, 'public', 'offline.html'))); +app.listen(app.get('port'), () => console.log('Listening on port ' + app.get('port'))); diff --git a/pony-town.js b/pony-town.js new file mode 100644 index 0000000..2f09e4a --- /dev/null +++ b/pony-town.js @@ -0,0 +1,2 @@ +process.env.NODE_ENV = 'production'; +require('./src/scripts/server/server'); diff --git a/public/images/logo-120.png b/public/images/logo-120.png new file mode 100644 index 0000000..c5c4c00 --- /dev/null +++ b/public/images/logo-120.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb87109a87e11f76e72e962af7b0f020d42960ddb00b5084b8db05806befe1ca +size 1133 diff --git a/public/images/logo-240.png b/public/images/logo-240.png new file mode 100644 index 0000000..b0d7947 --- /dev/null +++ b/public/images/logo-240.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc94d6c812ef8fbaa611ccc92d319093454dfc8613e5ab0cae56f20716950cb2 +size 1155 diff --git a/public/images/offline-pony.png b/public/images/offline-pony.png new file mode 100644 index 0000000..39681ee --- /dev/null +++ b/public/images/offline-pony.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb5f48880bb32c4d450fa30eb1b567b36b6573739a4dd76396337271acf05663 +size 2356 diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..7c16273 --- /dev/null +++ b/public/offline.html @@ -0,0 +1,38 @@ + + + + + + Pony Town (offline) + + + +
+

+ Pony Town server is undergoing maintenance, please check back later. +

+
+ + diff --git a/public/privacypolicy.html b/public/privacypolicy.html new file mode 100644 index 0000000..aa7f95d --- /dev/null +++ b/public/privacypolicy.html @@ -0,0 +1,95 @@ + + + + + Pony Town | Privacy Policy + + + +
+

Privacy Policy

+

+ Last updated: April 27, 2018 +

+

+ We operate https://pony.town/ (the "Site"). This page informs you of our policies regarding the collection, + use and disclosure of Personal Information we receive from users of the Site. +

+

+ We use your Personal Information only for providing and improving the Site. By using the Site, you + agree to the collection and use of information in accordance with this policy. +

+

Information Collection And Use

+

+ While using our Site, we may ask you to provide us with certain personally identifiable information + that can be used to contact or identify you. Personally identifiable information may include, but is not + limited to your name or email address ("Personal Information"). +

+

Log Data

+

+ Like many site operators, we collect information that your browser sends whenever you visit our Site ("Log Data"). +

+

+ This Log Data may include information such as your computer's Internet Protocol ("IP") address, + browser type, browser version, the pages of our Site that you visit, the time and date of your visit, + the time spent on those pages, script errors, chat messages, in-game actions performed by user + and other statistics. +

+

+ In addition, we may use third party services such as Google Analytics that collect, monitor and + analyze this data. +

+

Cookies

+

+ Cookies are files with small amount of data, which may include an anonymous unique identifier. + Cookies are sent to your browser from a web site and stored on your computer's hard drive. +

+

+ Like many sites, we use "cookies" to collect information. You can instruct your browser to refuse all + cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may + not be able to use some portions of our Site. +

+

Security

+

+ The security of your Personal Information is important to us, but remember that no method of + transmission over the Internet, or method of electronic storage, is 100% secure. While we strive to + use commercially acceptable means to protect your Personal Information, we cannot guarantee its + absolute security. +

+

Changes To This Privacy Policy

+

+ This Privacy Policy is effective as of August 11, 2016 and will remain in effect except with respect to any + changes in its provisions in the future, which will be in effect immediately after being posted on this page. +

+

+ We reserve the right to update or change our Privacy Policy at any time and you should check this + Privacy Policy periodically. Your continued use of the Service after we post any modifications to the + Privacy Policy on this page will constitute your acknowledgment of the modifications and your + consent to abide and be bound by the modified Privacy Policy. +

+

+ If we make any material changes to this Privacy Policy, we will notify you either through the email + address you have provided us, or by placing a prominent notice on our website. +

+

Contact Us

+

+ If you have any questions about this Privacy Policy, please contact us. +

+
+ + diff --git a/public/reload.html b/public/reload.html new file mode 100644 index 0000000..45963a1 --- /dev/null +++ b/public/reload.html @@ -0,0 +1,39 @@ + + + + + + + Pony Town (reload) + + + + +
+

+ Page loaded correctly, you can close this tab now. +

+
+ + + \ No newline at end of file diff --git a/public/reset.html b/public/reset.html new file mode 100644 index 0000000..12878ed --- /dev/null +++ b/public/reset.html @@ -0,0 +1,64 @@ + + + + + + + Pony Town (reset) + + + + + + + + + \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..da8ba00 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: /auth/ +Disallow: /api/ +Disallow: /api2/ diff --git a/public/termsofservice.html b/public/termsofservice.html new file mode 100644 index 0000000..91d9a76 --- /dev/null +++ b/public/termsofservice.html @@ -0,0 +1,111 @@ + + + + + Pony Town | Terms of Service + + + +
+

Terms of Service ("Terms")

+

+ Last updated: August 11, 2016 +

+

+ Please read these Terms of Service ("Terms", "Terms of Service") carefully before using the + https://pony.town/ website (the "Service") operated by us. +

+

+ Your access to and use of the Service is conditioned on your acceptance of and compliance with + these Terms. These Terms apply to all visitors, users and others who access or use the Service. +

+

+ By accessing or using the Service you agree to be bound by these Terms. If you disagree with + any part of the terms then you may not access the Service. +

+

Termination

+

+ We may terminate or suspend access to our Service immediately, without prior notice or liability, + for any reason whatsoever, including without limitation if you breach the Terms. +

+

+ All provisions of the Terms which by their nature should survive termination shall survive + termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity + and limitations of liability. +

+

Links To Other Web Sites

+

+ Our Service may contain links to third­party web sites or services that are not owned or controlled + by us. +

+

+ We have no control over, and assumes no responsibility for, the + content, privacy policies, or practices of any third party web sites or services. You further + acknowledge and agree that we shall not be responsible or liable, + directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection + with use of or reliance on any such content, goods or services available on or through any such + web sites or services. +

+

+ We strongly advise you to read the terms and conditions and privacy policies of any third­party + web sites or services that you visit. +

+

User submitted content

+

+ Our Service may contain content submited by other users of the service. +

+

+ We have no control over, and assumes no responsibility for any content published by other users. You further + acknowledge and agree that we shall not be responsible or liable, + directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection + with use of or reliance on any such content. +

+

+ We strongly advise you to read the terms and conditions and privacy policies of any third­party + web sites or services that you visit. +

+

Governing Law

+

+ These Terms shall be governed and construed in accordance with the laws of EU, + without regard to its conflict of law provisions. +

+

+ Our failure to enforce any right or provision of these Terms will not be considered a waiver of + those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the + remaining provisions of these Terms will remain in effect. These Terms constitute the entire + agreement between us regarding our Service, and supersede and replace any prior agreements + we might have between us regarding the Service. +

+

Changes

+

+ We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a + revision is material we will try to provide at least 5 days notice prior to any new + terms taking effect. What constitutes a material change will be determined at our sole discretion. +

+

+ By continuing to access or use our Service after those revisions become effective, you agree to + be bound by the revised terms. If you do not agree to the new terms, please stop using the + Service. +

+

Contact Us

+

+ If you have any questions about these Terms, please contact us. +

+
+ + diff --git a/src/fixes/empty.js b/src/fixes/empty.js new file mode 100644 index 0000000..e69de29 diff --git a/src/maps/cave.json b/src/maps/cave.json new file mode 100644 index 0000000..05b7dd4 --- /dev/null +++ b/src/maps/cave.json @@ -0,0 +1,5 @@ +{ + "width": 56, + "height": 56, + "tiles": "dQACAhYABgIaAAICCQsLAAoCFwADAgkLCgAEAgQBBAIVAAMCAQEJCwkAAwIBAQMDAQEFAhUAAgICAQkLCQACAgEBBgMBAQQCFAACAgIBCQsIAAMCAQEGAwIBAwIGAAQCCgADAgEBCQsIAAICAgEGAwMBAwIDAAgCCAADAgEBCQsIAAMCAgEIAwEBAwIBAAQCAgEFAgcAAgIBAQkLAQAGCwIAAwICAQMDAwECAwEBBgIBAQIDAgEEAgcAAgIDAQMLBAAGCwIABQIEAQICAQECAwEBBAIGAwIBAgIIAAQCDQsCAAUCAgEEAgIBDAMBAQQCCQACAg0LAwADAgQBBAIHAQUDAQEFAgoAAQINCwMAAgIGAQQCAQECAgEBAgIDAwMBAgIBAQICCwADCwQABgsDAAICAwEDAgIACQICAwEBAgMDAQICCwADCw0AAgICAQQCBAAEAgEBAgIDAQMDAgECAgsAAwsMAAMCAgECAgYABAIDAQMCAwMBAQICDAADCwwAAgICAQoABAICAQMCBAECAgsAAgICAQICAQADAgUAAwIBAQICDAACAgIBAgIDAQMCCAAFAgIBCAICAAMCAgEBAg4AAgIEAQQCCAAHAgEBDQIBAQICDgAJAgkAAgIBAQQCAgECAgMBBgICAQICAwADCgkABwIJAAMCAgEEAgQBAgICAQICBAEBAgQABgoIAAMCCgADAgMBAgIFAQQCBgECAgMABwoVAAICBgECAgEBAgICAAMCAgECAgEBAgIDAAgKFAACAgIBBAMFAgMABgIBAQICAwAIChQAAwIBAQQDAQEDAgYABAIBAQQCBAAFChQAAwIBAQECAwMBAQICCQABAgMBAwIEAAUKBwADAgsAAwIEAQMCCgACAgIBAwIDAAUKBgAGAgkABAICAQQCCwADAgEBBAIBAAQKBQAEAgEBBAIIAAkCDQACAgIBAwIFCgUABAIDAQMCBwAIAg4AAwIEAQUKBAADAgMBBQIIAAMCEwACAgQBBQoDAAUCAgECAgEBAgIfAAICAQEDAgcAAgIIAQMCHgACAgEBAgIIAAICAQEDAgUBAgIYAAQCAgACAgIBAQIHAAcCAwEFAgwAAwICAAMCAgAGAgEAAgIBAQICAQEGAAcCAgEGAgsACgIBAAICAgECAgEAAgIBAQICAQECAAcCAQECAgIBBwIKAAMCAQEEAgEBBgIBAQUCAQEKAgcBAgIBAQMCCwACAgMBAwICAQUCAgEDAgMBBQIDAQIMAgECDAUBAwIMAAICCAEFAgIBAgwJAQUCAwEHAg0AAwICAQIMAgEGAgMBAgIDAQICAgEIAgMBBAIOAAQCAQECAgIBBQIDAQQCAgEGAgQABwIQAAICAgEEAgkBBQIBAQMCCQADAgwACAICAQQCAwECAgIBAQIDAQICAwECAhcABAICDAIBAgwBAQIMBAECAgEAAwIIAQICGAABAgIBBQIDAQMCAQEDAgEABAIBAQECBQECAhkACAIDAQICAQEDAgEABAIDAQICAQECAiAAAwIEAQMCAwADAgEBAQIBAQUCIgACAgIBAwIEAAMCAQEGAiMABgIGAAICAQEDAicAAwIIAAICAQECAjQAAgECAjQAAwE1AAMBGwA=" +} \ No newline at end of file diff --git a/src/maps/custom.json b/src/maps/custom.json new file mode 100644 index 0000000..ddadd9d --- /dev/null +++ b/src/maps/custom.json @@ -0,0 +1,15 @@ +{ + "width": 32, + "height": 32, + "tiles": "RAICARwCBQEbAgUBHAIEARECAgMJAgYBDgIFAwkCBAENAgYDDAICAQkCAwEGAwwCBAEFAgUBAQIEAw4CBQECAgMBBQIDAxACBwEaAgYBGgIGARoCCwEEBBICCgEEBBICBwEDAgQEEwIGAQMCBAQOAgcBAgICARICCgEDAgIBDgIDCwMBBQICAQMCBAEMAgQLBgIECgMCAwEEBQgCBAsGAgUKBQIFBQcCBQsFAgUKBAIGBQcCBQsFAgUKBAIFBRICBAoGAgQFxQI=", + "defaultTile": 2, + "spawnArea": { + "x": 16, + "y": 16, + "w": 0, + "h": 0 + }, + "spawns": {}, + "lockedTiles": [], + "entities": [] +} \ No newline at end of file diff --git a/src/maps/island.json b/src/maps/island.json new file mode 100644 index 0000000..67f357a --- /dev/null +++ b/src/maps/island.json @@ -0,0 +1,22 @@ +{ + "width": 56, + "height": 56, + "tiles": "/wP/A/8DGwMBAREDAgEjAwEBAgICAwEBDAMEAQUDAgIcAwUCAQEHAwICBAMCAgEBAgMEAgEBGwMFAgYDBQIDAwoCAQEbAwQCBQMHAgIDCwICAQgHDwMBAQYCBAMHAgEDCAIBAQMCAgEIBw8DBwIEAw8CBwEIBw4DBgIBAQQDBAICAQoCAwEDAgEBCAcOAwQCAgMBAQQDBAICAQkCAgEFAgMDBgcPAwMCAQEGAw8CAgEEAgQDBgcPAwICBwMBAQoCAgEDAgIBBQIFAwMIEQMBAQcDCwICAQICAgEHAh8DAQELAgYBBwIBAR4DCwIFAQkCAQEeAwsCBAEEAgIDBAIfAwEBCwIDAQMCAwMDAiEDDAICAQMCAwMDAiIDDAICAQICBAMDASEDAQELAgIBAwIFAwEBIQMBAQoCAgEIAiIDAQEGAgEBAwIDAQoCAQEfAwYCAQECAwICAgELAiADBwICAwMCAQELAiEDBwICAwICAQEBAgQDBgIhAwcCAQEBAwIBAgIEAwMCAQMBASQDBQICAwMBBgMBASsDAQEDAwEHNwMBBzYDAwj/A/8D9QM=", + "defaultTile": 3, + "spawnArea": { + "x": 43.4, + "y": 20, + "w": 3.3, + "h": 2.5 + }, + "spawns": { + "house": { + "x": 27, + "y": 24, + "w": 2, + "h": 2 + } + }, + "lockedTiles": [], + "entities": [] +} \ No newline at end of file diff --git a/src/maps/main.json b/src/maps/main.json new file mode 100644 index 0000000..5dfef8e --- /dev/null +++ b/src/maps/main.json @@ -0,0 +1,5 @@ +{ + "width": 160, + "height": 160, + "tiles": "cgIDBZwCBAWcAgQFiAICARICAwUBAYcCBAEQAgQFhwIGAQ8CAwWAAg0BDwIEBX4CDwEOAgQFTgIFAQkCBAEPAgUBCQIFAQMCAgEEAgIBDgIBAQQFPwIDAQgCHwECAg4BAgIFAQsCAgENAgEBBQU9AgcBBAIGAQUCHQECAg0BDAICAQwCAQEFBT4CDwELAgIBCQIDAQMCBAEMAgUBDgIBAQ0CAQEEBT8CDAFCAgEBDQIFBT8CAgEBAgMBAgICAUQCAgEMAgEFAgYBBUECBQFIAgIBDAIBBQIGAQUGAgEBBAICAQQCAgEuAgQBSQICAQwCBQUFAgcBAwIEAQICAQEsAgIBSQICAQ0CBAUGAgUBBAIIASsCAgFIAgQBDQIEBQUCBQEFAgcBIQIDAQYCAgFJAgQBDQIFBQQCAgECAgIBBAIIAR8CBAEGAgIBSQIDAQ8CBAUDAgMBAgIDAQICCQEfAgUBBAIDAUgCBAEPAgQFAwITAR8CCwEhAggBIAIDARACAwUBAQICCAECAgkBIgIBAQICBgEhAggBIAIDARACBAUEAgYBAwIIASgCAgEiAggBIQICARECAwUFAgEBAgIBAQUCBgEpAgIBIgIIASECAwEQAgMFDwIFASgCAwEiAggBIQIDAQcCAQEIAgQFDgIEASkCAwEiAggBIQIEAQICAQEDAgIBCAIDBQ4CBAEqAgIBIgIIARECAQEPAg0BBwIEBQ0CAwEOAgEBAgIBARkCAwEhAggBEAIDAQ4CAwEGAgMBCAIEBQwCAwEOAgMBGwIDASQCAgETAgIBEAICAQgCAQEIAgEBAwULAgQBDwIDARsCAwEjAgIBJQIDARECAwUJAgQBDQIBAQMCAgEcAgQBCQICAQkCAQENAgIBFAIBARACAwEQAgQFCAIFAQ4CAQECAgIBHQIPAQYCBAEMAgIBFAIBARECAwEPAgQFCAIDARMCAQEVAgEBCAIOAQYCCwEGAgIBEwIBARICAwEOAgQFAQEIAgIBEwICARUCAgEIAgMBBgICAQgCFAEUAgEBEAIDAQ4CBAUHAgMBFQIBARUCBgEEAgIBEQIFAQICDQESAgEBEgICAQ4CBQUFAgQBFgIBARQCBgEBAgEBAQIEARICAwEHAgMBAwIEARECAgERAgIBDgIEBQEBAwIEARcCAgEEAgMBDwIBAQUCBQETAgEBDgIEARACAgERAgMBCQIFAQMJCQEVAgQBAgIGARcCAwEiAgQBEAIBARECAwEHAgcBAwkLARMCDAEYAgIBEQIBARACDgEGAgEBEQIDAQYCBQECAgEBAwkHAgYBCgINAQICBAEYAgIBIgIYAQwCBwECAgQBBQIEBQsCBQEEAgsBCAICARkCAwEOAgEBEgIDAQUCAgEEAiIBBgIFBQwCDQEnAgQBEAIBARACAgEVAhcBCAIDBQEGAQUNAgoBKQIEASACAwEgAgIBAgIGAQkCBgUNAgcBLQICARACAQEQAgMBJgIDAQoCBAUPAgQBMAICAQ4CAQECAgEBDgIEAScCAgEIAgUFDwIDATICAwEfAgQBJwICAQcCBQUQAgMBMgIDAQ0CAgERAgMBJwIDAQYCBQURAgIBMwICAQwCBAEQAgMBJwIDAQQCAwUDBgMFHwIDBSICAgELAgQBEgIDAScCAgEEAgMFAwYDBR8CBAUhAgIBCwIEARICAwEnAgMBBAIDBQIGBAUFAgQFFAIGBR8CAwEMAgIBEwIDASgCAgEFAggFBQIEBRQCAgUCBgIFHwICASICBAEnAgMBBgIHBQMCBQUUAgIFAgYCBR8CAgEiAgQBKAICAQkCCwUWAgQFIAICAR0CAQEEAgMBKQIEAQcCCwUXAgEFIgICARwCCQEqAgMBCAIDBQMGBQUHAgMFDAICBSACAgEdAgkBKgIDAQkCAgUDBgcFBAIGBQMCAQUHAgEFIAICAR4CAgECAgQBKwIDAQgCDgUCAgoFBwICBR4CAwEjAgMBKwIDAQgCGwUFAgMFDQICAw8CAwEjAgMBKwIDAQoCAwUBARUFBQIEBQwCBAMNAgMBIwIDARMCAwUUAgMBDwIEBQIGEAUEAgQFDAIGAwoCAwEjAgQBEgIFBRMCAwEQAgMFBAYPBQECBwULAgkDBwIDASQCBAERAgYFEwIDARACAwUFBg0FAQIIBQoCCgMHAgIBJQIDARACAwUCBgIFFAICARACAwUFBgwFAQEJBQoCCgMHAgIBJQIEAQ8CAwUCBgIFFAICARECAgUEBhEFAgYDBQsCCgMHAgIBJQIDARACBgUVAgIBEQIBBQcGBAUBAQkFAwYDBQsCDAMFAgIBJQIDARECAwUEAgIFEAIDARECAQUHBgUFAwIGBQIGAwUMAgwDBAIDASUCAwEWAgQFEAIDARECBQUCBgYFBQIJBQwCDQMDAgMBJQIDARYCBAUQAgIBEwIKBQYCCgUMAg0DBgEkAgQBEAIDAQQCAgUPAgQBBAICAQ8CCQUCAgkFAgYCBQwCBgMDCAEHAwMGARsCAgEHAgUBDAIJARICBAEDAgMBDgIQBQIGAgUDBgIFDAIEAwkHCAEGAgYBBQICAQMCCAEEAgUBCgIGAQICAwEIAgEBAwIEAQICEQECAgEBAwIFBQIGCQUDBgIFAgYDBQwCBAMJBzoBBAIBAQICLAECBQIGAQUEBgcFAgYDBQIGAgUNAgQDCQc5AQYCCwEJAgMBBwICAQYCBwEDBQgGDwUNAgQDBgcDAw8BBAIIAQICAQEGAhUBBwIEAQQCAgEeAgQBAgUFBgIFAwYIBQIGBAUNAgQDBgcEAwEBCAIDAQYCAwECAgMBCQICAQUCBQEHAgIBBwIDASUCAwEDBQQGAwUDBggFAwYCBQ4CDgMTAgIBAgICAR0CAwEFAgQBJQIDAQQFAwYCBQUGBwUDBgIFDgIOAxMCBQEfAgoBJgIDAQkFBQYLBQ8CDQMVAgQBHwIHASoCAgEKBQMGCwUBAQ8CDAMWAgQBHwIFAS0CAgECBQIBEAUCAQIFDwIMAxYCBAEgAgQBLQIHAQ4FAwECBQEBDgIKAxgCAwEhAgMBLgIDAQMCBAEIBQMBAgIDAQ8CCQMZAgMBIQIDASwCBAEGAgIBBwUDARUCCQMYAgMBIQIEASwCBAEHAgIBAgUBAQIFBAEVAgkDGAIDASECBAErAgUBBwIKARYCCgMWAgIBJAIEASgCBAEMAggBFgIKAwEBFQICASQCBAEoAgQBEQIEARUCCgMBARQCAgElAgQBJgIEARQCAwEVAgoDAQEUAgIBJQIDASQCBwEUAgMBFQIJAwMBEwICASUCAwEiAgMBAQIDARYCAgEWAgkDBAEFAgQBCAIDASUCAwEfAgoBFgICARYCCAMFAQMCCAEEAgQBJgIDAR8CBQEDAgEBFgIDARYCBwMKAQYCCQEmAgEBAQIBAR8CBQEDAgEBFgIDARYCBwMJAQkCBgEoAgIBHgIDAQICAQEDAgIBFQIEARUCCAMGAQ4CAgEoAgMBHgICAQQCBAEWAgMBFgIIAwMCAQEQAgIBJwIEAR0CAwEEAgQBFQICARgCBgMVAgIBKgIEAQ8CDwEDAgcBEwICARgCBgMTAgQBKQIGAQMCBAEDAhwBEwICARkCBQMUAgIBLAIrARMCAgEZAgUDEwICAS4CFAEBAgIBCAIDAQICBwERAgQBGAIFAxECBAE0AgQBHQIFAQ8CBgEXAgQDEQIDATYCAwEfAg0BBQIIAQYCAQEEAgEBCgICAxICBAE2AgMBIAIOAQICAwEDAgYBAgIIAQkCAgMSAgQBNAIFASQCDgEGAgQBAwIBAQICAQEBAgEBCgICAxACAwECAgIBAgICAS0CBwEmAgwBHQIBAxECAwEDAgUBLgIFAS8CAwEvAgMBBQIFAS0CBQEvAgMBLgIEAQUCBQEtAgQBMQIDAS0CAgEHAgUBBQICBSUCBAEyAgMBKwIDAQgCBAEEAgYFIwIEATICAwErAgMBCQICAQUCBgUIAgUFCgIDBQgCBQEyAgMBKgIDARECBQUJAggFBgIEBQcCBQEyAgQBKgIDAQ8CBgULAgkFBAIDBQgCBAEwAgcBKgIDAQ8CAQUCBgIFDQIDBQMGAgUPAgQBMAIHASkCAgERAgEFAgYCBQ8CBQUQAgQBMAIHASkCAgEQAgUFJQIDATACBwEpAgMBDwIGBSQCBAExAgYBBgIBASICAgEQAgYFJAIEATMCAwEHAgEBAQIBAR8CAwERAgcFIgIDATQCAwEGAgMBIAIDARECAgUCBgMFIQIEATMCBAEGAgMBAwICARsCAwESAgEFAwYCBSECBAEzAgYBBQIIARoCAgETAgIFAgYCBSICAwEGAgQBKgIDAQcCBAECBQIBGgICARMCBQUjAgMBBQICAQECAQErAgMBBwIDAQMFAQEbAgIBFAIEBSMCAwEDAgEBAgIBAQMCAQEpAgQBBgIDAQMFGwIDATsCAwECAgMBAQIBAQECBAEoAgUBAwIIARsCAwE7AhABIAIDAQQCDwEdAgMBOQIOASICFwEdAgMBOQIJAQICBAEiAg4BAgIEAR8CBAE4AgQBAwICAQICBwEFAgEBGQIMAQYCAgEgAgMBOAIEAQQCBQEEAgEBIAIBAQQCBQEpAgQBOAIDAQQCBQEEAgIBJQIEASoCBAE3AgMBBAIEAQUCAQEmAgQBLAICATcCBAEEAgQBBAIBASYCBAEsAgQBNgIDAQUCAgEDAgEBAQIBAQQCAgEgAgQBLQIDATYCAwEFAgMBAwIDAQICBAEfAgQBLgIDASECAQETAgMBBQIEAQECBAECAgQBHwIFAS0CBQEJAgIBEwIEAQ4CAQECAgMBBgIIAQECBgEfAgQBLwIEAQYCBgEQAgcBAgIEAQMCAQECAgcBBQICAQcCBQEgAgUBLwIGAQICBwEQAgUBAgIBAQICAQECAgEBAQIBAQICDwEGAgIBJAIGAS4CAgECAgoBEQIFAQ8CDwEEAgIBIwIDAQICAwEUAgMBFQICAQQCAgEDAgQBEAIGARMCEQEdAgIBAwIDAQMCGwEUAgIBCgICARICBAEYAg4BFAIQAQMCGwEUAgIBHwICARwCTQETAgIBQgIvAQ4CAgECAggBEwICAUcCHQEIAgMBFgIFARQCAgFOAgQBAwICAQMCAgEEAgIBJQICARQCAwFOAgMBCgIBAUECAwFMAgMFAQFNAgQBRgIJBUwCBgFFAgQFAwYCBUwCBgFGAgMFAwYDBUwCBQFHAggFTAIEAUgCBAUBAgMFTQIDAU0CAgX/Av8C/wL/AgEC" +} \ No newline at end of file diff --git a/src/styles/partials/_admin.scss b/src/styles/partials/_admin.scss new file mode 100644 index 0000000..88c4b91 --- /dev/null +++ b/src/styles/partials/_admin.scss @@ -0,0 +1,398 @@ +@import './variables'; + +body { + overflow-y: scroll; +} + +a { + cursor: pointer; +} + +b, strong, th { + font-weight: 600; +} + +::-webkit-scrollbar { + width: 14px; +} + +::-webkit-scrollbar-track { + background: $body-bg; +} + +::-webkit-scrollbar-thumb { + background: lighten($body-bg, 7%); + border-radius: 3px; + + &:hover { + background: lighten($body-bg, 12%); + } + + &:active { + background: lighten($body-bg, 17%); + } +} + +.table { + .account-email { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.admin-bottom-padding { + width: 100%; + height: 1px; + clear: both; + margin-bottom: 200px; +} + +.admin-search { + width: 250px; + display: inline-block; +} + +.admin-search-inline { + @extend .admin-search; + margin-top: 20px; + margin-bottom: 20px; +} + +.icon-button { + margin-left: 1px; + opacity: 0.7; + cursor: pointer; + + &:hover { + opacity: 1; + text-shadow: 0 0 5px black; + } +} + +.text-banned { + color: #eb2c2c; +} + +.text-alert { + color: #f7ca5f; +} + +.popover-notes { + max-width: 400px; + max-height: 273px; + + .popover-inner { + width: 400px; + height: 273px; + } +} + +.note-editor { + font-size: 12px; + margin: -9px -14px; + width: 396px; + max-width: 396px; + max-height: 269px; + position: relative; + z-index: $zindex-popover + 1; +} + +.tooltip.tooltip-account { + .tooltip-inner { + max-width: 300px; + } +} + +.tooltip.tooltip-notes { + .tooltip-inner { + max-width: 400px; + white-space: pre-line; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.tooltip.tooltip-merge { + .tooltip-inner { + max-width: 400px; + white-space: pre; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.tooltip.tooltip-pony { + opacity: 1; + + .tooltip-inner { + max-width: 300px; + max-height: 300px; + background: #eee; + border-radius: 7px; + } + + &.bs-tooltip-top .arrow::before { + border-top-color: #eee; + } + + &.bs-tooltip-bottom .arrow::before { + border-bottom-color: #eee; + } +} + +.character-tooltip { + margin: -2px -6px -3px -6px; + border-radius: 7px; + overflow: hidden; + position: relative; +} + +.character-tooltip-indicator { + position: absolute; + bottom: -10px; + right: 8px; + font-size: 26px; + color: #eb2c2c; +} + +.character-tooltip-date { + position: absolute; + left: 4px; + bottom: 2px; + color: #444; + font-size: 10px; + opacity: 0.8; +} + +.pre-line { + white-space: pre-line; +} + +.pre { + white-space: pre; + overflow: auto; +} + +.deleted { + opacity: 0.5; +} + +.badge > .text-banned { + text-shadow: 0 0 3px black; +} + +.badge-admin { + margin-right: 3px; +} + +// columns + +.col-time { + width: 90px; + white-space: nowrap; +} + +.col-account { + width: 370px; + white-space: nowrap; +} + +.col-origin { + width: 235px; + white-space: nowrap; +} + +.col-auth { + width: 100px; +} + +.col-email { + width: 210px; +} + +.col-pony { + width: 100px; +} + +.bad { + color: #d25757; +} + +.chatlog { + white-space: nowrap; + + .time { + color: $text-muted; + user-select: none; + } + + .account-id { + color: $text-muted; + font-family: $font-family-monospace; + line-height: 19px; + } + + .server { + color: #ddd; + } + + .server-safe { + color: theme-color('success'); + } + + .server-test { + color: $dev; + } + + i, .server-system, .message-system, .svg-icon { + color: $text-muted; + } + + .map { + color: #aaa; + } + + .map-island, .map-house { + color: rgb(136, 181, 218); + } + + .name { + color: #bd9c60; + + $icon-color: '9c9c9c'; + + &.muted { + &::after { + content: ''; + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1em; + margin-left: 3px; + margin-right: 2px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'%3E%3Cpath fill='%23#{$icon-color}' d='M421.45 285.195L376 239.746V212c0-6.627 5.373-12 12-12h24c6.627 0 12 5.373 12 12v44c0 9.957-.881 19.71-2.55 29.195zM352 96c0-53.019-42.981-96-96-96-32.574 0-61.354 16.227-78.71 41.035L352 215.746V96zm152.971 363.716L52.284 7.029c-9.373-9.373-24.569-9.373-33.941 0L7.029 18.343c-9.372 9.373-9.372 24.568 0 33.941L160 205.254v49.577c0 53.089 43.436 97.452 96.524 97.167 14.626-.078 28.471-3.44 40.854-9.366l17.746 17.746c-17.529 9.971-37.794 15.666-59.372 15.622C189.355 375.864 136 321.053 136 254.656V212c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v44c0 84.488 62.693 154.597 144 166.278V468h-68c-6.627 0-12 5.373-12 12v20c0 6.627 5.373 12 12 12h184c6.627 0 12-5.373 12-12v-20c0-6.627-5.373-12-12-12h-68v-45.722c25.625-3.682 49.396-13.172 69.942-27.083L459.717 504.97c9.373 9.373 24.569 9.373 33.941 0l11.313-11.313c9.372-9.373 9.372-24.568 0-33.941z'/%3E%3C/svg%3E"); + background-size: cover; + background-repeat: no-repeat; + } + } + + &.ignored { + &::after { + content: ''; + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1em; + margin-left: 3px; + margin-right: 2px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'%3E%3Cpath fill='%23#{$icon-color}' d='M256 8C119.034 8 8 119.033 8 256s111.034 248 248 248 248-111.034 248-248S392.967 8 256 8zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676zM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676z'/%3E%3C/svg%3E"); + background-size: cover; + background-repeat: no-repeat; + position: relative; + top: -2px; + } + } + + &.ignorepub { + &::after { + content: ''; + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1em; + margin-left: 3px; + margin-right: 2px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath fill='%23#{$icon-color}' d='M633.8 458.1L362.3 248.3C412.1 230.7 448 183.8 448 128 448 57.3 390.7 0 320 0c-67.1 0-121.5 51.8-126.9 117.4L45.5 3.4C38.5-2 28.5-.8 23 6.2L3.4 31.4c-5.4 7-4.2 17 2.8 22.4l588.4 454.7c7 5.4 17 4.2 22.5-2.8l19.6-25.3c5.4-6.8 4.1-16.9-2.9-22.3zM96 422.4V464c0 26.5 21.5 48 48 48h350.2L207.4 290.3C144.2 301.3 96 356 96 422.4z'/%3E%3C/svg%3E"); + background-size: cover; + background-repeat: no-repeat; + position: relative; + top: -2px; + } + } + } + + .message-party { + color: #71daff; + } + + .message-command { + color: #a3a3a3; + } + + .message-supporter { + color: #ffc8c2; + } + + .message-whisper { + color: #ffa6bc; + } + + .message-muted { + text-decoration: line-through; + } +} + +.chat-translate { + display: none; + opacity: 0.5; + width: 1.5em; + height: 1em; + color: $text-muted; + opacity: 0.3; + margin-left: 2px; + + &:hover { + opacity: 0.5; + } + + .chatlog-line:hover > & { + display: inline-block; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 500'%3E%3Cpath fill='white' d='M304 416H24c-13.255 0-24-10.745-24-24V120c0-13.255 10.745-24 24-24h280v320zm-120.676-72.622A12 12 0 0 0 194.839 352h22.863c8.22 0 14.007-8.078 11.362-15.861L171.61 167.085a12 12 0 0 0-11.362-8.139h-32.489a12.001 12.001 0 0 0-11.362 8.139L58.942 336.139C56.297 343.922 62.084 352 70.304 352h22.805a12 12 0 0 0 11.535-8.693l9.118-31.807h60.211l9.351 31.878zm-39.051-140.42s4.32 21.061 7.83 33.21l10.8 37.531h-38.07l11.07-37.531c3.51-12.15 7.83-33.21 7.83-33.21h.54zM616 416H336V96h280c13.255 0 24 10.745 24 24v272c0 13.255-10.745 24-24 24zm-36-228h-64v-16c0-6.627-5.373-12-12-12h-16c-6.627 0-12 5.373-12 12v16h-64c-6.627 0-12 5.373-12 12v16c0 6.627 5.373 12 12 12h114.106c-6.263 14.299-16.518 28.972-30.023 43.206-6.56-6.898-12.397-13.91-17.365-20.933-3.639-5.144-10.585-6.675-15.995-3.446l-7.28 4.346-6.498 3.879c-5.956 3.556-7.693 11.421-3.735 17.117 6.065 8.729 13.098 17.336 20.984 25.726-8.122 6.226-16.841 12.244-26.103 17.964-5.521 3.41-7.381 10.556-4.162 16.19l7.941 13.896c3.362 5.883 10.935 7.826 16.706 4.276 12.732-7.831 24.571-16.175 35.443-24.891 10.917 8.761 22.766 17.102 35.396 24.881 5.774 3.556 13.353 1.618 16.717-4.27l7.944-13.903c3.213-5.623 1.37-12.76-4.135-16.171a312.737 312.737 0 0 1-26.06-18.019c21.024-22.425 35.768-46.289 42.713-69.85H580c6.627 0 12-5.373 12-12v-16c0-6.625-5.373-11.998-12-11.998z'/%3E%3C/svg%3E"); + background-size: cover; + background-repeat: no-repeat; + } +} + +[data-text]::after { + content: attr(data-text); + display: inline-block; + width: 60px; +} + +.help-tooltip { + font-family: $font-family-monospace; + + em { + font-family: $font-family-base; + color: $text-muted; + } +} + +.stat-field { + color: $text-muted; + + b { + color: $body-color; + font-weight: normal; + } +} + +.well { + min-height: 20px; + padding: 15px; + margin-bottom: 20px; + background-color: #222; + border: 1px solid #101010; + border-radius: 3px; +} + +.table thead th { + border-top: none; +} + +@each $color, $value in $theme-colors { + @include table-row-variant($color, fade-out(theme-color-level($color, 6), 0.8)); +} diff --git a/src/styles/partials/_animations.scss b/src/styles/partials/_animations.scss new file mode 100644 index 0000000..f713391 --- /dev/null +++ b/src/styles/partials/_animations.scss @@ -0,0 +1,19 @@ +@mixin elastic-animation($name, $fromX, $fromY) { + @at-root { + @keyframes #{$name} { + @each $percent, $mult in (0%, 1), (14%, -0.38), (28%, 0.13), (42%, -0.05), (57%, 0.02), (71%, -0.01), (85%, 0.005), (100%, 0) { + #{$percent} { + animation-timing-function: ease-in-out; + transform: translate3d($fromX * $mult, $fromY * $mult, 0); + } + } + } + } +} + +@include elastic-animation(elastic-from-right, 50px, 0); + +.elastic-from-right { + display: block; + animation: elastic-from-right 0.5s forwards linear; +} diff --git a/src/styles/partials/_base.scss b/src/styles/partials/_base.scss new file mode 100644 index 0000000..f950f16 --- /dev/null +++ b/src/styles/partials/_base.scss @@ -0,0 +1,78 @@ +@import "./variables"; + +html, body { + margin: 0; + padding: 0; + background: $body-bg; + color: $body-color; + font-family: $font-family-base; + font-size: $font-size; +} + +#loading { + position: fixed; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + font-size: 34px; + line-height: 34px; + color: $text-muted; + line-height: inherit; + + > img { + image-rendering: pixelated; + width: 74px; + height: 86px; + margin-bottom: 30px; + opacity: 0.6; + } + + > div:after { + content: ' '; + display: inline-block; + width: 30px; + } +} + +@keyframes loading { + 0%, 99% { + height: 0; + } + 100% { + height: 35px; + } +} + +.loading-error { + color: $danger; + font-size: 16px; + margin-top: 20px; + animation: loading 30s; + overflow: hidden; +} + +.loading-error-btn { + color: #d9534f; + background: none; + display: inline-block; + font-weight: 400; + text-align: center; + vertical-align: middle; + margin: 0; + padding: 0.375rem 0.75rem; + border: 1px solid #d9534f; + border-radius: 0.25rem; + font-size: 1rem; + line-height: 1.5; + appearance: none; + text-decoration: none; + + &:hover { + background: #d9534f; + color: #fff; + text-decoration: none; + } +} diff --git a/src/styles/partials/_bootstrap-admin.scss b/src/styles/partials/_bootstrap-admin.scss new file mode 100644 index 0000000..2a01353 --- /dev/null +++ b/src/styles/partials/_bootstrap-admin.scss @@ -0,0 +1,35 @@ +@import "node_modules/bootstrap/scss/functions"; +@import "node_modules/bootstrap/scss/variables"; +@import "node_modules/bootstrap/scss/mixins"; +// @import "node_modules/bootstrap/scss/root"; +// @import "node_modules/bootstrap/scss/print"; +@import "node_modules/bootstrap/scss/reboot"; +@import "node_modules/bootstrap/scss/type"; +@import "node_modules/bootstrap/scss/images"; +@import "node_modules/bootstrap/scss/code"; +@import "node_modules/bootstrap/scss/grid"; +@import "node_modules/bootstrap/scss/tables"; +@import "node_modules/bootstrap/scss/forms"; +@import "node_modules/bootstrap/scss/buttons"; +@import "node_modules/bootstrap/scss/transitions"; +@import "node_modules/bootstrap/scss/dropdown"; +@import "node_modules/bootstrap/scss/button-group"; +@import "node_modules/bootstrap/scss/input-group"; +@import "node_modules/bootstrap/scss/custom-forms"; +@import "node_modules/bootstrap/scss/nav"; +@import "node_modules/bootstrap/scss/navbar"; +// @import "node_modules/bootstrap/scss/card"; +// @import "node_modules/bootstrap/scss/breadcrumb"; +@import "node_modules/bootstrap/scss/pagination"; +@import "node_modules/bootstrap/scss/badge"; +// @import "node_modules/bootstrap/scss/jumbotron"; +@import "node_modules/bootstrap/scss/alert"; +@import "node_modules/bootstrap/scss/progress"; +// @import "node_modules/bootstrap/scss/media"; +@import "node_modules/bootstrap/scss/list-group"; +@import "node_modules/bootstrap/scss/close"; +@import "node_modules/bootstrap/scss/modal"; +@import "node_modules/bootstrap/scss/tooltip"; +@import "node_modules/bootstrap/scss/popover"; +// @import "node_modules/bootstrap/scss/carousel"; +@import "node_modules/bootstrap/scss/utilities"; diff --git a/src/styles/partials/_bootstrap.scss b/src/styles/partials/_bootstrap.scss new file mode 100644 index 0000000..3198962 --- /dev/null +++ b/src/styles/partials/_bootstrap.scss @@ -0,0 +1,35 @@ +@import "node_modules/bootstrap/scss/functions"; +@import "node_modules/bootstrap/scss/variables"; +@import "node_modules/bootstrap/scss/mixins"; +// @import "node_modules/bootstrap/scss/root"; +// @import "node_modules/bootstrap/scss/print"; +@import "node_modules/bootstrap/scss/reboot"; +@import "node_modules/bootstrap/scss/type"; +@import "node_modules/bootstrap/scss/images"; +@import "node_modules/bootstrap/scss/code"; +@import "node_modules/bootstrap/scss/grid"; +// @import "node_modules/bootstrap/scss/tables"; +@import "node_modules/bootstrap/scss/forms"; +@import "node_modules/bootstrap/scss/buttons"; +@import "node_modules/bootstrap/scss/transitions"; +@import "node_modules/bootstrap/scss/dropdown"; +@import "node_modules/bootstrap/scss/button-group"; +@import "node_modules/bootstrap/scss/input-group"; +@import "node_modules/bootstrap/scss/custom-forms"; +@import "node_modules/bootstrap/scss/nav"; +@import "node_modules/bootstrap/scss/navbar"; +// @import "node_modules/bootstrap/scss/card"; +// @import "node_modules/bootstrap/scss/breadcrumb"; +// @import "node_modules/bootstrap/scss/pagination"; +@import "node_modules/bootstrap/scss/badge"; +// @import "node_modules/bootstrap/scss/jumbotron"; +@import "node_modules/bootstrap/scss/alert"; +// @import "node_modules/bootstrap/scss/progress"; +// @import "node_modules/bootstrap/scss/media"; +@import "node_modules/bootstrap/scss/list-group"; +// @import "node_modules/bootstrap/scss/close"; +@import "node_modules/bootstrap/scss/modal"; +@import "node_modules/bootstrap/scss/tooltip"; +@import "node_modules/bootstrap/scss/popover"; +// @import "node_modules/bootstrap/scss/carousel"; +@import "node_modules/bootstrap/scss/utilities"; diff --git a/src/styles/partials/_chat.scss b/src/styles/partials/_chat.scss new file mode 100644 index 0000000..bdcdd4a --- /dev/null +++ b/src/styles/partials/_chat.scss @@ -0,0 +1,77 @@ +@import './variables'; + +$chat-line-indent: 10px; + +.chat-line { + color: $chat-color; + font-size: 14px; + padding-left: $chat-line-indent; + text-shadow: 1px 1px 1px #000; +} + +.chat-line, .chat-box-type-name { + .pixelart { + margin-top: -1px; + margin-left: 2px; + margin-right: 2px; + vertical-align: top; + height: 20px; + } +} + +@each $key, $value in $chat-colors { + &.chat-line-#{$key} { + color: $value; + + > .chat-line-name { + color: darken($value, 10%); + } + } +} + +.chat-line-party-announcement, .chat-line-whisper-announcement { + > .chat-line-message { + color: $chat-announcement; + } +} + +.chat-line-name { + color: darken($chat-color, 15%); +} + +.chat-line-name-content { + padding-right: 3px; + margin-right: -3px; +} + +.chat-line-label, .chat-line-name-content { + cursor: pointer; + + &:hover { + text-decoration: underline !important; + } +} + +.chat-line-break { + word-break: break-all; +} + +.chat-line-lead { + margin-left: -$chat-line-indent; +} + +.chat-line-name { + white-space: nowrap; +} + +.chat-line-name-content { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + vertical-align: bottom; +} + +.chat-line-name-index { + color: #aaa; +} diff --git a/src/styles/partials/_common.scss b/src/styles/partials/_common.scss new file mode 100644 index 0000000..de335bb --- /dev/null +++ b/src/styles/partials/_common.scss @@ -0,0 +1,305 @@ +@import './variables'; +@import './mixins'; + +.js-focus-visible :focus:not(.focus-visible) { + box-shadow: none !important; + outline: none; +} + +html { + touch-action: manipulation; +} + +label { + font-weight: 600; +} + +a, button:not([disabled]), .cursor-pointer { + cursor: pointer; +} + +.link-plain { + &:hover, &:active { + text-decoration: none; + } +} + +button { + margin: 0; + padding: 0; + color: inherit; + background: none; + border: none; + appearance: none; +} + +h1, h2 { + color: $headers-color; +} + +.pointer-none { + pointer-events: none; +} + +.unselectable, .sno { + user-select: none; +} + +.pixelart { + image-rendering: pixelated; +} + +.text-strike { + text-decoration: line-through; +} + +.text-dev { + color: lighten($dev, 20%); +} + +.text-mod { + color: lighten($mod, 10%); +} + +.text-safe { + color: $safe; +} + +.text-unsafe { + color: $unsafe; +} + +.text-fading { + color: mix($body-color, $text-muted, 40%); +} + +.text-faded { + opacity: 0.6; +} + +.text-wrap { + white-space: normal; +} + +.text-ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} + +.text-slashed { + text-decoration: line-through; +} + +.text-party { + color: $party-color; +} + +.text-friends { + color: $friends-color; +} + +.text-whisper { + color: $chat-whisper; +} + +.lock-box { + float: right; + margin-right: -20px; +} + +character-preview { + margin: auto; + width: 400px; + height: 400px; + border-radius: $border-radius-lg; + + @include media-breakpoint-down(sm) { + width: 280px; + height: 280px; + } + + .popover &, .tooltip & { + width: 200px; + height: 200px; + } +} + +.text-large { + font-size: 16px; +} + +.list-rules { + color: theme-color(danger) !important; + font-weight: 600; +} + +.popover.popover-wide { + max-width: none; +} + +.accessories-tabset .nav-link { + @include media-breakpoint-down(lg) { + padding-left: 10px; + padding-right: 10px; + } +} + +.supporter-1 { + color: $supporter-1 !important; +} + +.supporter-2 { + color: $supporter-2 !important; +} + +.supporter-3 { + color: $supporter-3 !important; +} + +// flags + +@mixin flag-3($a, $b, $c) { + background: linear-gradient(to bottom, $a 0%, $a 32%, $b 32%, $b 68%, $c 68%, $c 100%); +} + +@mixin flag-3h($a, $b, $c) { + background: linear-gradient(to right, $a 0%, $a 32%, $b 32%, $b 68%, $c 68%, $c 100%); +} + +.flag { + display: inline-block; + width: $flag-size * 1.05; + height: $flag-size; + background: #666; + border-radius: 100%; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.3); + margin-bottom: -3px; +} + +.flag-ru { + @include flag-3($flag-white, $flag-blue, $flag-red); +} + +.flag-mx { + position: relative; + @include flag-3h(#008158, white, #e40f27); + + &::after { + content: '•'; + color: #be915d; + position: absolute; + left: 6px; + top: -5px; + font-size: 18px; + } +} + +$argentina-blue: #74acdf; + +.flag-ar { + position: relative; + @include flag-3($argentina-blue, $flag-white, $argentina-blue); + + &::after { + content: '•'; + color: #f7b50d; + position: absolute; + left: 6px; + top: -5px; + font-size: 18px; + } +} + +.flag-sp { + position: relative; + background: linear-gradient(to bottom, + $flag-red 0%, $flag-red 27%, + $flag-yellow 27%, $flag-yellow 73%, + $flag-red 73%, $flag-red 100%); +} + +.flag-pr { + position: relative; + background: linear-gradient(to right, + $flag-green 0%, $flag-green 40%, + $flag-red 40%, $flag-red 100%); +} + +.flag-br { + position: relative; + background: $flag-green; + + &::before { + content: '◆'; + color: $flag-yellow; + position: absolute; + left: 0px; + top: -8px; + font-size: 23px; + transform: scaleY(0.8); + } + + &::after { + content: '•'; + color: #003bb1; + position: absolute; + left: 5px; + top: -11px; + font-size: 26px; + } +} + +.flag + .flag { + margin-left: -16px; +} + +.modal-frame { + width: 100%; + height: 500px; + border: 0; + border-radius: $border-radius-lg; +} + +// font size fixes + +.nav-pills, .nav-tabs { + font-size: 16px; +} + +.is-mobile action-button, action-button.draggable-dragging { + .shortcut { + display: none; + } +} + +.actions-modal-opened { + $action-bar-space: 80px; + + .modal-backdrop, .modal { + bottom: $action-bar-space; + height: auto; + } + + .modal-dialog { + @media screen and (max-height: 600px) { + height: auto; + } + + align-items: flex-end; + padding-bottom: 20px; + } +} + +.translucent-modals .modal-content { + opacity: 0.5; +} + +.d-none-for-low-height { + @media screen and (max-height: 600px) { + display: none !important; + } +} + +.tooltip.tooltip-desc .tooltip-inner { + max-width: 250px; +} diff --git a/src/styles/partials/_controls.scss b/src/styles/partials/_controls.scss new file mode 100644 index 0000000..9d275f6 --- /dev/null +++ b/src/styles/partials/_controls.scss @@ -0,0 +1,107 @@ +@import './variables'; + +kbd { + font-weight: bold; + font-family: Verdana; + padding: 2px 6px 3px; + box-shadow: inset -1px -2px 0 rgba(0, 0, 0, 0.25); + border-radius: 3px; +} + +.gamepad > kbd, kbd.gamepad { + background: $gamepad-bg; + color: $gamepad-color; + padding: 1px 5px 2px; + min-width: 25px; + text-align: center; + border-radius: 100%; + display: inline-block; +} + +@each $color, $value in $gamepad-colors { + .gamepad-#{$color} > kbd, kbd.gamepad-#{$color} { + background: $value; + } +} + +.gamepad-dpad > kbd, .gamepad-stick > kbd, kbd.gamepad-dpad, kbd.gamepad-stick { + box-shadow: inset -1px -2px 0 rgba(0, 0, 0, 0.25), 0 0 5px rgba(0, 0, 0, 0.5); + position: relative; +} + +.gamepad-dpad > kbd, kbd.gamepad-dpad { + $arrow-width: 6px; + $arrow-height: 5px; + $arrow-shift: 5px; + $arrow-color: rgba(255, 255, 255, 0.9); + + min-width: 20px; + height: 20px; + vertical-align: middle; + border-radius: 5px; + + .dpad-up, .dpad-down { + position: absolute; + width: 0; + height: 0; + border-left: $arrow-height solid transparent; + border-right: $arrow-height solid transparent; + left: $arrow-shift; + } + + .dpad-up { + border-bottom: $arrow-width solid $arrow-color; + top: 6px; + } + + .dpad-down { + border-top: $arrow-width solid $arrow-color; + bottom: 7px; + } +} + +.gamepad-stick > kbd, kbd.gamepad-stick { + $arrow-width: 2px; + $arrow-height: 3px; + $arrow-shift: 9px; + $arrow-pad: 1px; + $arrow-color: rgba(255, 255, 255, 0.7); + + .t, .l, .r, .b { + position: absolute; + width: 0; + height: 0; + } + + .t, .b { + border-left: $arrow-height solid transparent; + border-right: $arrow-height solid transparent; + left: $arrow-shift; + } + + .t { + border-bottom: $arrow-width solid $arrow-color; + top: $arrow-pad; + } + + .b { + border-top: $arrow-width solid $arrow-color; + bottom: $arrow-pad; + } + + .l, .r { + border-top: $arrow-height solid transparent; + border-bottom: $arrow-height solid transparent; + top: $arrow-shift; + } + + .l { + border-right: $arrow-width solid $arrow-color; + left: $arrow-pad; + } + + .r { + border-left: $arrow-width solid $arrow-color; + right: $arrow-pad; + } +} diff --git a/src/styles/partials/_custom.scss b/src/styles/partials/_custom.scss new file mode 100644 index 0000000..7c3ceb3 --- /dev/null +++ b/src/styles/partials/_custom.scss @@ -0,0 +1,351 @@ +@import './variables'; +@import './mixins'; + +// container + +.container { + position: relative; +} + +// links + +a.text-muted { + @include hover { + color: $link-hover-color !important; + } +} + +.alert-link { + color: white !important; +} + +// tags + +code { + padding: 2px 5px 2px; + background: $code-bg; + border-radius: $border-radius; +} + +// bg / text + +.bg-grass { + background: $grass-color; +} + +.text-smuted { + color: $text-smuted; +} + +// badge + +.badge { + padding: 4px 6px; +} + +.badge-dev, .badge-mod { + text-transform: uppercase; +} + +.badge-none { + @include badge-variant($none); +} + +.badge-test { + @include badge-variant($dev); +} + +.badge-dev { + @include badge-variant($dev); +} + +.badge-mod { + @include badge-variant($mod); +} + +.badge-forbidden { + @include badge-variant($forbidden); +} + +// dropdown / popover + +.dropdown-toggle > .fa-caret-down { + position: relative; + z-index: 100; +} + +.dropup > .dropdown-menu { + top: auto; + bottom: 100%; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +.dropdown-menu-nipple-right { + @include nipple(11px); +} + +.dropdown-menu, .popover-body { + color: $inverse-color; +} + +.dropdown-item, .dropdown-item:active { + color: inherit; + background-color: inherit; +} + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-in-outlet { + position: absolute; + left: 0; + top: 0; +} + +.no-arrow::after { + display: none; +} + +// tooltip + +.tooltip { + pointer-events: none; +} + +.tooltip.tooltip-notes .tooltip-inner { + max-width: 270px; + white-space: pre-line; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; +} + +.tooltip.tooltip-pre .tooltip-inner { + max-width: 300px; + white-space: pre-line; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; +} + +// buttons + +.btn.btn-default { + color: $btn-default-color; +} + +.btn-xs { + @include button-size($btn-padding-y-xs, $btn-padding-x-xs, $font-size-xs, $line-height-xs, $border-radius-xs); +} + +.btn-dark { + &:focus, &.focus { + box-shadow: 0 0 0 $btn-focus-width rgba(white, .5); + } +} + +.btn-link { + &:focus, &.focus { + box-shadow: 0 0 0 $btn-focus-width rgba($focus-color, .5); + } +} + +@each $color, $value in $theme-colors { + .btn-group, .input-group-append, .input-group-prepend { + > .btn-#{$color}:not(:last-child) { + border-right-color: darken($value, 10%); + } + + > .btn-#{$color}:not(:first-child) { + border-left-color: darken($value, 10%); + } + } +} + +.btn-group-shadow { + box-shadow: 0 0 5px black; + border-radius: $border-radius-sm; + margin-right: 5px; +} + +.btn-no-round-left { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.btn-no-round { + border-radius: 0 !important; +} + +// form + +.form-control[disabled] { + color: $input-disabled-color; +} + +small.form-text { + font-size: 12px; +} + +// nav + +.navbar-nav { + .navbar-link, .dropdown { + > a, > button { + line-height: 20px; + padding: 15px; + display: block; + color: #9c9c9c; + font-size: inherit; + text-decoration: none; + border: none; + + &:hover, &:focus, &:active { + text-decoration: none; + } + } + + &:hover, &.active { + > a, > button { + text-decoration: none; + color: #fff; + + &.no-highlight { + color: #9c9c9c; + } + } + } + } +} + +.nav-tabs { + .nav-link { + margin-bottom: -1px; + } +} + +.nav-link { + color: $link-color; + + &:hover { + background: $nav-link-hover-bg; + } + + .nav-pills &:focus { + box-shadow: 0 0 0 $btn-focus-width rgba($primary, .5); + outline: none; + } +} + +.modal-body { + .nav-pills { + margin-bottom: 1rem; + + .nav-link { + color: darken($body-color, 40%); + margin-right: 0.5rem; + + &:hover { + background-color: rgba($body-bg, .5); + } + + &:focus { + box-shadow: 0 0 0 $btn-focus-width rgba($body-bg, .5); + } + + &.active { + color: white; + background-color: $body-bg; + } + } + } +} + +tabset.fixed-height { + .tab-content { + display: grid; + + > .tab-pane { + grid-row: 1 / 2; + grid-column: 1 / 2; + display: block; + visibility: hidden; + + &.active { + visibility: visible; + } + } + } +} + +.is-msie, .is-yandex { + tabset.fixed-height { + .tab-content { + display: block; + + > .tab-pane { + visibility: visible; + display: none; + + &.active { + display: block; + } + } + } + } + + // actions modal + .inactive-tab { + visibility: visible; + display: none; + } +} + +// alert + +.alert-success { + @include alert-variant($alert-success-bg, $alert-success-bg, $alert-success-color); +} + +.alert-info { + @include alert-variant($alert-info-bg, $alert-info-bg, $alert-info-color); +} + +.alert-warning { + @include alert-variant($alert-warning-bg, $alert-warning-bg, $alert-warning-color); +} + +.alert-danger { + @include alert-variant($alert-danger-bg, $alert-danger-bg, $alert-danger-color); +} + +// modal + +.modal-dialog { + @include media-breakpoint-up(sm) { + display: flex; + align-items: center; + height: 100%; + margin-top: 0; + margin-bottom: 0; + } +} + +// custom controls + +.custom-control-label { + vertical-align: middle; +} + +.modal-checkboxes { + .custom-checkbox > .custom-control-label { + &::before, &::after { + margin-top: 2px; + } + } +} diff --git a/src/styles/partials/_game.scss b/src/styles/partials/_game.scss new file mode 100644 index 0000000..e601818 --- /dev/null +++ b/src/styles/partials/_game.scss @@ -0,0 +1,64 @@ +@import './variables'; + +.game-button { + margin: 0; + padding: 0; + background: none; + border: none; + appearance: none; + cursor: pointer; + border-radius: 50%; + width: 36px; + height: 36px; + text-align: center; + font-size: 24px; + + > fa-icon, > .fa-icon { + color: white; + filter: drop-shadow(0 0 1px #000); + } + + &:disabled { + pointer-events: none; + + > fa-icon, > .fa-icon { + color: #ddd; + } + } + + &:hover { + > fa-icon, > .fa-icon { + color: #ddd; + filter: drop-shadow(0 0 2px #000); + } + } + + &:focus { + outline: none; + } +} + +// notifications + +.notification-popover { + font-size: 14px; + padding: 2px; + + p { + margin-bottom: 5px; + } +} + +.notification-buttons { + .btn { + width: 50px; + } + + .btn + .btn { + margin-left: 5px; + } +} + +.popover-notification { + max-width: 250px; +} diff --git a/src/styles/partials/_loading.scss b/src/styles/partials/_loading.scss new file mode 100644 index 0000000..4bbb9d5 --- /dev/null +++ b/src/styles/partials/_loading.scss @@ -0,0 +1,13 @@ +@keyframes loading-animation { + 0%, 100% { content: ''; } + 25% { content: '.'; } + 50% { content: '..'; } + 75% { content: '...'; } +} + +#loading > div:after { + content: ''; + display: inline-block; + width: 30px; + animation: loading-animation 2s infinite; +} diff --git a/src/styles/partials/_mixins.scss b/src/styles/partials/_mixins.scss new file mode 100644 index 0000000..1217193 --- /dev/null +++ b/src/styles/partials/_mixins.scss @@ -0,0 +1,91 @@ +@mixin nipple($right) { + &:before { + content: ''; + display: inline-block; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + right: $right - 1px; + } + + &:after { + content: ''; + display: inline-block; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + right: $right; + } +} + +@mixin nippleLeft($left) { + &:before { + content: ''; + display: inline-block; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: $left - 1px; + } + + &:after { + content: ''; + display: inline-block; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: $left; + } +} + +@mixin nippleLeftFlip() { + &:before { + border-bottom: none; + border-top: 7px solid #ccc; + border-top-color: rgba(0, 0, 0, 0.2); + top: auto; + bottom: -7px; + } + + &:after { + border-bottom: none; + border-top: 6px solid #ffffff; + top: auto; + bottom: -6px; + } +} + +@mixin nippleOnLeft($top) { + &:before { + content: ''; + display: inline-block; + border-top: 9px solid transparent; + border-bottom: 9px solid transparent; + border-right: 7px solid #ccc; + border-right-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: $top - 1px; + left: -7px; + } + + &:after { + content: ''; + display: inline-block; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-right: 6px solid #ffffff; + position: absolute; + top: $top; + left: -6px; + } +} diff --git a/src/styles/partials/_shared.scss b/src/styles/partials/_shared.scss new file mode 100644 index 0000000..c92bd23 --- /dev/null +++ b/src/styles/partials/_shared.scss @@ -0,0 +1,9 @@ +@import './base'; +@import './common'; +@import './game'; +@import './chat'; +@import './animations'; +@import './controls'; +@import './custom'; +@import './tiny'; +@import './loading'; diff --git a/src/styles/partials/_tiny.scss b/src/styles/partials/_tiny.scss new file mode 100644 index 0000000..ce5bdd6 --- /dev/null +++ b/src/styles/partials/_tiny.scss @@ -0,0 +1,23 @@ +@keyframes tiny-shake { + 0%, 100% { transform: translate3d(-2px, -2px, 0); } + 50% { transform: translate3d(2px, 2px, 0); } +} + +.tinyface { + padding: 20px; + display: inline-block; + + &::after { + content: '• c •'; + white-space: nowrap; + } + + &:hover { + will-change: transform; + animation: 0.1s infinite tiny-shake; + + &::after { + content: '•` c ˊ•'; + } + } +} diff --git a/src/styles/partials/_tools.scss b/src/styles/partials/_tools.scss new file mode 100644 index 0000000..cb1c949 --- /dev/null +++ b/src/styles/partials/_tools.scss @@ -0,0 +1,47 @@ +@import './variables'; + +.popover.popover-frames { + max-width: none; + width: 530px; + + .frame-select-container { + display: flex; + flex-wrap: wrap; + } + + .frame-select { + display: block; + margin-left: -1px; + margin-top: -1px; + } + + .sprite-box { + display: block; + border: solid 1px #ddd; + + &:hover { + background: #ffd800; + } + } +} + +.tools-animation { + .sprite-box { + display: block; + margin: -6px; + + &:hover { + background: #ffd800; + } + } + + .far-cell canvas { + opacity: 0.6; + } + + .character-preview { + width: 300px; + height: 300px; + margin-right: 10px; + } +} diff --git a/src/styles/partials/_variables.scss b/src/styles/partials/_variables.scss new file mode 100644 index 0000000..4c78280 --- /dev/null +++ b/src/styles/partials/_variables.scss @@ -0,0 +1,205 @@ +// bootstrap + +$primary: #ca7e4e; +$secondary: #6c757d; +$default: #fafafa; +$success: #5cb85c; +$info: #3ab7da; +$warning: #df871c; +$danger: #d9534f; +$dark: #222; +$patreon: #f96955; + +$none: #777; +$dev: #bd3e91; +$mod: #5f2f8f; +$forbidden: #a131e2; + +$safe: #5a8f5a; +$unsafe: #d25757; + +$border-color: #555; +$focus-color: #50a5ff; +$headers-color: #ddbb74; +$grass-color: #90ee90; + +$friends-color: #40bf19; +$party-color: #50b5ff; + +$gamepad-colors: ( + yellow: #e2a800, + red: #d92f38, + blue: #1473b3, + green: #7aaa00, + pink: #e0a3c9 +); + +$theme-colors: ( + primary: $primary, + secondary: $secondary, + default: $default, + success: $success, + info: $info, + warning: $warning, + danger: $danger, + dark: $dark, + patreon: $patreon +); + +$gold: #ffcf00; + +$supporter-1: $patreon; +$supporter-2: #ffa32b; +$supporter-3: $gold; + +$chat-color: #ffffff; +$chat-party: #94e3ff; +$chat-whisper: #ffa1df; +$chat-announcement: #F0E68C; + +$chat-colors: ( + system: lighten(#bbb, 5%), + admin: lighten(#ff69b4, 7%), + mod: lighten(#b689ff, 7%), + party: $chat-party, + thinking: darken($chat-color, 15%), + party-thinking: darken($chat-party, 15%), + announcement: $chat-announcement, + party-announcement: $chat-party, + supporter-1: lighten($supporter-1, 7%), + supporter-2: lighten($supporter-2, 7%), + supporter-3: lighten($supporter-3, 7%), + whisper: $chat-whisper, + whisper-announcement: $chat-whisper +); + +$enable-print-styles: false; + +$spacer: 16px; + +// body +$body-bg: #333; +$body-color: #eee; + +$inverse-bg: #eee; +$inverse-color: #444; + +$dark-bg: darken($body-bg, 7%); + +// links +$link-color: #d88856; +$link-hover-color: lighten($link-color, 15%); + +// fonts +// $font-family-sans-serif: 'Helvetica Neue', Helvetica, Arial, sans-serif; +$font-size: 14px; + +$headings-font-weight: 600; + +$text-muted: #9C9C9C; +$text-smuted: #ccc; + +$hr-border-color: $border-color; + +// alerts +$alert-success-color: #b3e9b4; +$alert-success-bg: #519f52; +$alert-info-color: #dce5f7; +$alert-info-bg: #31708f; +$alert-warning-color: #ffffff; +$alert-warning-bg: #ca8e35; +$alert-danger-color: #f7d9d9; +$alert-danger-bg: #cf6461; + +// navs +$nav-tabs-border-color: $border-color; +$nav-tabs-link-hover-border-color: transparent; +$nav-tabs-link-active-border-color: $border-color; +$nav-tabs-link-active-color: #eee; + +// navbar +$nav-link-hover-bg: fade-out(#000, 0.7); + +// components +$border-radius-base: 3px; +$border-radius-lg: 4px; +$border-radius-sm: 2px; +$border-radius-xs: 2px; + +// buttons +$input-btn-focus-color: rgba($focus-color, .3); +$input-focus-border-color: lighten($focus-color, 10); +$input-btn-padding-y-sm: .375rem; +$input-btn-padding-x-sm: .65rem; +$btn-padding-x-xs: 5px; +$btn-padding-y-xs: 1px; +$font-size-xs: 0.858rem; +$line-height-xs: 17px; +$btn-default-color: #333; + +// dropdown +$dropdown-link-disabled-color: #a6acb1; + +// forms +$input-bg: #222; +$input-color: #ddd; +$input-disabled-bg: lighten($input-bg, 5%); +$input-disabled-color: darken($input-color, 35%); +$input-border-color: $border-color; + +// custom controls +$custom-control-indicator-focus-box-shadow: 0 0 0 3px rgba($focus-color, .5); +$custom-control-indicator-size: 1.2rem; +// $custom-control-gutter: 1.7rem; +$custom-control-indicator-checked-bg: $secondary; +$custom-control-indicator-checked-disabled-bg: rgba($secondary, .5); +$custom-control-label-disabled-color: $text-muted; +$custom-control-indicator-active-bg: rgba($secondary, .6); +$custom-control-indicator-checked-color: white; +$custom-control-indicator-bg-size: 65% 65%; +$custom-control-indicator-border-color: $border-color; + +// modals +$modal-content-bg: $dark-bg; +// $modal-content-border-color: #666; +$modal-header-border-color: #444; +$modal-footer-border-color: $modal-header-border-color; + +// tables +$table-sm-cell-padding: 0.4rem; +$table-border-color: #6c757d; +$table-hover-bg: #3a3a3a; + +// badge +$badge-padding-x: 0.4rem; +$badge-padding-y: 0.27rem; + +// code +$code-color: #d2b28f; +$code-bg: #292929; + +// kbd +$kbd-color: #444; +$kbd-bg: #fff; + +$gamepad-color: #fff; +$gamepad-bg: #7d7d7d; + +$navbar-link-border: 4px; + +// list group +$list-group-bg: $body-bg; +$list-group-disabled-color: $text-muted; + +// flags +$flag-size: 1.3rem; +$flag-white: #f9f9f9; +$flag-blue: #428bc1; +$flag-red: #ed4c5c; +$flag-black: #3e4347; +$flag-yellow: #ffe62e; +$flag-green: #00aa00; + +@import "node_modules/bootstrap/scss/functions"; +@import "node_modules/bootstrap/scss/variables"; +@import "node_modules/bootstrap/scss/mixins"; diff --git a/src/styles/style-admin.scss b/src/styles/style-admin.scss new file mode 100644 index 0000000..b6c1107 --- /dev/null +++ b/src/styles/style-admin.scss @@ -0,0 +1,4 @@ +@import 'partials/variables'; +@import 'partials/bootstrap-admin'; +@import 'partials/shared'; +@import 'partials/admin'; diff --git a/src/styles/style-inline.scss b/src/styles/style-inline.scss new file mode 100644 index 0000000..ea47bcc --- /dev/null +++ b/src/styles/style-inline.scss @@ -0,0 +1 @@ +@import "partials/base"; diff --git a/src/styles/style-tools.scss b/src/styles/style-tools.scss new file mode 100644 index 0000000..19526e8 --- /dev/null +++ b/src/styles/style-tools.scss @@ -0,0 +1,4 @@ +@import 'partials/variables'; +@import 'partials/bootstrap-admin'; +@import 'partials/shared'; +@import 'partials/tools'; diff --git a/src/styles/style.scss b/src/styles/style.scss new file mode 100644 index 0000000..82ec928 --- /dev/null +++ b/src/styles/style.scss @@ -0,0 +1,3 @@ +@import 'partials/variables'; +@import 'partials/bootstrap'; +@import 'partials/shared'; diff --git a/src/tests/font/ascii.png b/src/tests/font/ascii.png new file mode 100644 index 0000000..567bda2 --- /dev/null +++ b/src/tests/font/ascii.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84f45ed15d67c026310e1c9814b8b15aa8a822cf061b26f9a98d54130d1018bb +size 334 diff --git a/src/tests/font/chinese.png b/src/tests/font/chinese.png new file mode 100644 index 0000000..692970f --- /dev/null +++ b/src/tests/font/chinese.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f665d8d4b520798b140b2709d8f147fdaab85dc9229e21dea8570df4989ffcf +size 411 diff --git a/src/tests/font/emoji.png b/src/tests/font/emoji.png new file mode 100644 index 0000000..a494535 --- /dev/null +++ b/src/tests/font/emoji.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d45576705ab678903fec0962a70ae3b4cf7626f27adc330b8b9c66fb5e182f29 +size 591 diff --git a/src/tests/font/greek.png b/src/tests/font/greek.png new file mode 100644 index 0000000..18db806 --- /dev/null +++ b/src/tests/font/greek.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c34c27e7b2fe132b329c951751aaaa21be10a4d6d688dfb19a1f952c9d71df26 +size 295 diff --git a/src/tests/font/hiragana.png b/src/tests/font/hiragana.png new file mode 100644 index 0000000..37eea5b --- /dev/null +++ b/src/tests/font/hiragana.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6e1b4209127c30c99886c311ed0262d4084265ff48771ce0188738c29e37672 +size 341 diff --git a/src/tests/font/kanji.png b/src/tests/font/kanji.png new file mode 100644 index 0000000..d566e50 --- /dev/null +++ b/src/tests/font/kanji.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddc634775d305d7d9bc949eab100dc41187131965a84419357f2e9cbe35168b8 +size 265 diff --git a/src/tests/font/katakana.png b/src/tests/font/katakana.png new file mode 100644 index 0000000..fbe266b --- /dev/null +++ b/src/tests/font/katakana.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85c2860f24c8c73478bcc12132e9c7d96049ded077dd6a50fa210dc33e2a9132 +size 314 diff --git a/src/tests/font/newlines.png b/src/tests/font/newlines.png new file mode 100644 index 0000000..68b1512 --- /dev/null +++ b/src/tests/font/newlines.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6720bacdad8510ce6a657f9ff965dd5d2d86ce769bfd8f864206281cdcabaad +size 379 diff --git a/src/tests/font/punctuation.png b/src/tests/font/punctuation.png new file mode 100644 index 0000000..b142ca4 --- /dev/null +++ b/src/tests/font/punctuation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4efed133c3cc38bacd90aeea937a1bfb0c3d72dfe206e3c8b7dfc793c9018a8e +size 301 diff --git a/src/tests/font/romaji.png b/src/tests/font/romaji.png new file mode 100644 index 0000000..ba816fe --- /dev/null +++ b/src/tests/font/romaji.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19899ad5d08539ac7126902e725d23a7ca97d83e55c8717da27be984802d3696 +size 333 diff --git a/src/tests/font/russian.png b/src/tests/font/russian.png new file mode 100644 index 0000000..42b918c --- /dev/null +++ b/src/tests/font/russian.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3494b3c9702933dc7b9852370cd4a5a947b1c9b6fd4562f9e11c21e2f8cd32a5 +size 295 diff --git a/src/tests/font/shapes.png b/src/tests/font/shapes.png new file mode 100644 index 0000000..9c12626 --- /dev/null +++ b/src/tests/font/shapes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b15e74aec198a9740f14865d0a423bca183974de8c5138d87c747aacf6b5b16b +size 320 diff --git a/src/tests/font/special.png b/src/tests/font/special.png new file mode 100644 index 0000000..09d17bf --- /dev/null +++ b/src/tests/font/special.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4062edff8d09e5472093c8c5331149ee3c6be1c1848fdc8c03f11701b4ea0c1c +size 407 diff --git a/src/tests/font/tiny.png b/src/tests/font/tiny.png new file mode 100644 index 0000000..a4a7496 --- /dev/null +++ b/src/tests/font/tiny.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eda4bca7ff073da39353f09bab2427fd6c8745ddee7cb34f123078ff2a686b48 +size 267 diff --git a/src/tests/font/variants.png b/src/tests/font/variants.png new file mode 100644 index 0000000..b619ac2 --- /dev/null +++ b/src/tests/font/variants.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3e4b57bcd6c5a3e4999e4d4cbbd4cc7b7b0f9b41cbbc447cafdef768d86c2b5 +size 323 diff --git a/src/tests/graphics/admin.png b/src/tests/graphics/admin.png new file mode 100644 index 0000000..de81038 --- /dev/null +++ b/src/tests/graphics/admin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c65c77b49b6b4a133f99cfa163d9f37c71096d9db10ae198e7309aaf01f6b51c +size 310 diff --git a/src/tests/graphics/announcement.png b/src/tests/graphics/announcement.png new file mode 100644 index 0000000..04b5237 --- /dev/null +++ b/src/tests/graphics/announcement.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5584a05b7e0338536fe61ea642c919a85fe4b3e9544bb624b1a01ee83016d2f3 +size 349 diff --git a/src/tests/graphics/bounds-none.png b/src/tests/graphics/bounds-none.png new file mode 100644 index 0000000..345fd96 --- /dev/null +++ b/src/tests/graphics/bounds-none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:110e1327a718bf9cb1a94fbe06368942b3a8f9deca7a372f867aa230f3cf0f61 +size 117 diff --git a/src/tests/graphics/bounds.png b/src/tests/graphics/bounds.png new file mode 100644 index 0000000..e349464 --- /dev/null +++ b/src/tests/graphics/bounds.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcb040475e7d7db3b6b69f8c277974fcd56b93aff0ba0799e19074b77668cfe2 +size 178 diff --git a/src/tests/graphics/break-chat.png b/src/tests/graphics/break-chat.png new file mode 100644 index 0000000..3be9f7c --- /dev/null +++ b/src/tests/graphics/break-chat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f32ad6b109e230f763a57bcfba60f00bc1ee542216c77b9defdfd87f6e9b7bb7 +size 461 diff --git a/src/tests/graphics/break-think.png b/src/tests/graphics/break-think.png new file mode 100644 index 0000000..e2c7a02 --- /dev/null +++ b/src/tests/graphics/break-think.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3753fa88b622a11a7d9ecfb28132046b2de149c28ce90bbf3008432305d3e177 +size 493 diff --git a/src/tests/graphics/fade-in-0.png b/src/tests/graphics/fade-in-0.png new file mode 100644 index 0000000..345fd96 --- /dev/null +++ b/src/tests/graphics/fade-in-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:110e1327a718bf9cb1a94fbe06368942b3a8f9deca7a372f867aa230f3cf0f61 +size 117 diff --git a/src/tests/graphics/fade-in-1.png b/src/tests/graphics/fade-in-1.png new file mode 100644 index 0000000..05fa26d --- /dev/null +++ b/src/tests/graphics/fade-in-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dbe766123f9f8f489a08e13b38b771f5479000bf37e2bdbea792ac2d681c92b +size 313 diff --git a/src/tests/graphics/fade-in-2.png b/src/tests/graphics/fade-in-2.png new file mode 100644 index 0000000..e2bd0d6 --- /dev/null +++ b/src/tests/graphics/fade-in-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d43cf759a4087913ae7d9f2e01f554517a082b0dc25546f75ea0eea1535ff8f5 +size 302 diff --git a/src/tests/graphics/fade-out-0.png b/src/tests/graphics/fade-out-0.png new file mode 100644 index 0000000..345fd96 --- /dev/null +++ b/src/tests/graphics/fade-out-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:110e1327a718bf9cb1a94fbe06368942b3a8f9deca7a372f867aa230f3cf0f61 +size 117 diff --git a/src/tests/graphics/fade-out-1.png b/src/tests/graphics/fade-out-1.png new file mode 100644 index 0000000..7d3d84a --- /dev/null +++ b/src/tests/graphics/fade-out-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01491fa2f474a969b3bbafa09bde98894d37bb634bd5aab18a1a07bd1ec7c6ea +size 313 diff --git a/src/tests/graphics/fade-out-2.png b/src/tests/graphics/fade-out-2.png new file mode 100644 index 0000000..e2bd0d6 --- /dev/null +++ b/src/tests/graphics/fade-out-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d43cf759a4087913ae7d9f2e01f554517a082b0dc25546f75ea0eea1535ff8f5 +size 302 diff --git a/src/tests/graphics/hello-apple.png b/src/tests/graphics/hello-apple.png new file mode 100644 index 0000000..4fa880c --- /dev/null +++ b/src/tests/graphics/hello-apple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99efb3dbe24b0951dcc136bc0b412a0978d151de7f675db9cbc9837d023804fa +size 403 diff --git a/src/tests/graphics/hello.png b/src/tests/graphics/hello.png new file mode 100644 index 0000000..5b26a9b --- /dev/null +++ b/src/tests/graphics/hello.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4394869e01c05ab99a930a37c7dea010afbe8e54a3504625481e4b73fb7e6b70 +size 293 diff --git a/src/tests/graphics/mod.png b/src/tests/graphics/mod.png new file mode 100644 index 0000000..edbd178 --- /dev/null +++ b/src/tests/graphics/mod.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3f522e1e1aed9ed70b4670dc81ee0b27f25b70c77f3ed9afcb003a80653e54c +size 338 diff --git a/src/tests/graphics/move-left-chat.png b/src/tests/graphics/move-left-chat.png new file mode 100644 index 0000000..e47353b --- /dev/null +++ b/src/tests/graphics/move-left-chat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ab45b0f26b5f355275ae68c4a6ae54f0637e3efa443b9740ba031c03d4bd2fb +size 353 diff --git a/src/tests/graphics/move-left-think.png b/src/tests/graphics/move-left-think.png new file mode 100644 index 0000000..c6ffa9c --- /dev/null +++ b/src/tests/graphics/move-left-think.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:962c054e0c87e316259d31a692f3f8562cfb07093923e3c31aba053cb80345c2 +size 383 diff --git a/src/tests/graphics/move-right-chat.png b/src/tests/graphics/move-right-chat.png new file mode 100644 index 0000000..72716bd --- /dev/null +++ b/src/tests/graphics/move-right-chat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60c29964eb1ff877aca9131a0dba84429b6d001b81f600bdf6327dbabdcab7e9 +size 357 diff --git a/src/tests/graphics/move-right-nipple-chat.png b/src/tests/graphics/move-right-nipple-chat.png new file mode 100644 index 0000000..ed4774d --- /dev/null +++ b/src/tests/graphics/move-right-nipple-chat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8095415abca885a149692ace7cac516fa9ee772e364d7398f3ca1bebb3625b25 +size 348 diff --git a/src/tests/graphics/move-right-nipple-think.png b/src/tests/graphics/move-right-nipple-think.png new file mode 100644 index 0000000..df32acd --- /dev/null +++ b/src/tests/graphics/move-right-nipple-think.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d88bf0f851ca19ccb6eb29ff5c6240d52c4ea0a5b797f45f928f887867c084b2 +size 369 diff --git a/src/tests/graphics/move-right-think.png b/src/tests/graphics/move-right-think.png new file mode 100644 index 0000000..d68b104 --- /dev/null +++ b/src/tests/graphics/move-right-think.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f44f1c8eaa64ecdd2eb1b7060b7310aa8fdbbab2424c77b831f9e80bf2066a1e +size 384 diff --git a/src/tests/graphics/name-apple.png b/src/tests/graphics/name-apple.png new file mode 100644 index 0000000..a195eb9 --- /dev/null +++ b/src/tests/graphics/name-apple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197e0ae5d9dc63a1f38d0d17c5ef12778c6ff7d3cf9c08dc6ed61f8439fd8458 +size 490 diff --git a/src/tests/graphics/name-dev.png b/src/tests/graphics/name-dev.png new file mode 100644 index 0000000..ddcd6f2 --- /dev/null +++ b/src/tests/graphics/name-dev.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74285daf33e3a674beb3bac1dcac1475f0da1f549dd8df58455ecca7fdaa9cb7 +size 797 diff --git a/src/tests/graphics/name-mod.png b/src/tests/graphics/name-mod.png new file mode 100644 index 0000000..48f3d24 --- /dev/null +++ b/src/tests/graphics/name-mod.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de34b53a501554c5024f05fed6b00076ee60ae32f748a13744a132856a77e6ea +size 762 diff --git a/src/tests/graphics/name-party.png b/src/tests/graphics/name-party.png new file mode 100644 index 0000000..e1b81a7 --- /dev/null +++ b/src/tests/graphics/name-party.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1068b06e8e0bb42b21b1df33f27aaa417f9faf5404ae32e2f729a4df592064b5 +size 454 diff --git a/src/tests/graphics/name-sup1.png b/src/tests/graphics/name-sup1.png new file mode 100644 index 0000000..0d09268 --- /dev/null +++ b/src/tests/graphics/name-sup1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:063f4b606eab3eb0741d83d6e108e5c84d698e6face21b1b21674f1bdf26b4e2 +size 825 diff --git a/src/tests/graphics/name-sup2.png b/src/tests/graphics/name-sup2.png new file mode 100644 index 0000000..b276ea7 --- /dev/null +++ b/src/tests/graphics/name-sup2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88ec0b63cb3775109f9d36217be1e1e02f52c07d93160b02b03abf3e78c9c5ae +size 867 diff --git a/src/tests/graphics/name-sup3.png b/src/tests/graphics/name-sup3.png new file mode 100644 index 0000000..cfcfd48 --- /dev/null +++ b/src/tests/graphics/name-sup3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:844ba24498bf1e9226d361a20c821cd2b6a88d1a115b15b20c5031f4297bd6c6 +size 830 diff --git a/src/tests/graphics/name.png b/src/tests/graphics/name.png new file mode 100644 index 0000000..90ae62e --- /dev/null +++ b/src/tests/graphics/name.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e50db92b9253248b3bad45bf7cf7ea22f82f4605e25ec702c4e0697463d0dbae +size 389 diff --git a/src/tests/graphics/newline.png b/src/tests/graphics/newline.png new file mode 100644 index 0000000..c294a98 --- /dev/null +++ b/src/tests/graphics/newline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4890697cb28f2f435e5dd55b6651d7afecede330438183bc85b7a5ad8efad86 +size 391 diff --git a/src/tests/graphics/outline.png b/src/tests/graphics/outline.png new file mode 100644 index 0000000..34a4082 --- /dev/null +++ b/src/tests/graphics/outline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2ee907926aa39d62f64e593072c51e8b8662444897f6e7afc1fbcf5be03d723 +size 199 diff --git a/src/tests/graphics/outside-chat.png b/src/tests/graphics/outside-chat.png new file mode 100644 index 0000000..e3b5b25 --- /dev/null +++ b/src/tests/graphics/outside-chat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5ff1d4ee6f5c0ce345002ac16c15566be8ba7b36ba5f93b4ace62da953ad53e +size 321 diff --git a/src/tests/graphics/outside-think.png b/src/tests/graphics/outside-think.png new file mode 100644 index 0000000..6253951 --- /dev/null +++ b/src/tests/graphics/outside-think.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ce68c465713d15a832f44e5233dd213485ed4194688804ec49c461ba6888b1a +size 335 diff --git a/src/tests/graphics/outside.png b/src/tests/graphics/outside.png new file mode 100644 index 0000000..345fd96 --- /dev/null +++ b/src/tests/graphics/outside.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:110e1327a718bf9cb1a94fbe06368942b3a8f9deca7a372f867aa230f3cf0f61 +size 117 diff --git a/src/tests/graphics/party-announcement.png b/src/tests/graphics/party-announcement.png new file mode 100644 index 0000000..04b5237 --- /dev/null +++ b/src/tests/graphics/party-announcement.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5584a05b7e0338536fe61ea642c919a85fe4b3e9544bb624b1a01ee83016d2f3 +size 349 diff --git a/src/tests/graphics/party-thinking.png b/src/tests/graphics/party-thinking.png new file mode 100644 index 0000000..ff900cf --- /dev/null +++ b/src/tests/graphics/party-thinking.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7dbd938f26680fbf5ea3e53e4931dd4341c688d5782282ce7633a29080fa77d +size 419 diff --git a/src/tests/graphics/party.png b/src/tests/graphics/party.png new file mode 100644 index 0000000..3000d28 --- /dev/null +++ b/src/tests/graphics/party.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7711c7f569f62650ea1ecd8ef8f4bc1c4a62e3d2568d3c4d300fd1dddb2c71bd +size 333 diff --git a/src/tests/graphics/pixel-numbers.png b/src/tests/graphics/pixel-numbers.png new file mode 100644 index 0000000..79b80f6 --- /dev/null +++ b/src/tests/graphics/pixel-numbers.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfbc337e8cbb51c444fbd1602962233fd724d14cce806af7f78bacdb8fff17ee +size 246 diff --git a/src/tests/graphics/sup1.png b/src/tests/graphics/sup1.png new file mode 100644 index 0000000..acde212 --- /dev/null +++ b/src/tests/graphics/sup1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:368f3b9f2b3fc87e1dffe85661f5ec1699717750f3b0ebe85cec5b6ba741e74e +size 361 diff --git a/src/tests/graphics/sup2.png b/src/tests/graphics/sup2.png new file mode 100644 index 0000000..273155c --- /dev/null +++ b/src/tests/graphics/sup2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6eb3dcffd5353b7e5908aa73106c9a9cbfcac78df261dff64533de39b331f30d +size 434 diff --git a/src/tests/graphics/sup3.png b/src/tests/graphics/sup3.png new file mode 100644 index 0000000..53eb50b --- /dev/null +++ b/src/tests/graphics/sup3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2202d795d3a2fbb468f599ffc170cd0b63d5cd9a0424a147478e775ceb529b51 +size 443 diff --git a/src/tests/graphics/system.png b/src/tests/graphics/system.png new file mode 100644 index 0000000..2690dea --- /dev/null +++ b/src/tests/graphics/system.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d5165fab47c09705f7715438ba24fc74210cac407e017a885e7762d772439c1 +size 320 diff --git a/src/tests/graphics/thinking.png b/src/tests/graphics/thinking.png new file mode 100644 index 0000000..411c92a --- /dev/null +++ b/src/tests/graphics/thinking.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b35662d9eaf0b41c3dc9d0a868562714bcc61689e070a704e0f59cd451b84923 +size 366 diff --git a/src/tests/ponies/base.json b/src/tests/ponies/base.json new file mode 100644 index 0000000..a7959a1 --- /dev/null +++ b/src/tests/ponies/base.json @@ -0,0 +1,818 @@ +{ + "nose": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffffff", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b3b3b3", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "ears": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffffff", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b3b3b3", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "horn": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffffff", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b3b3b3", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "wings": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffffff", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b3b3b3", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontHooves": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffa500", + "ffff00", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b37400", + "b3b300", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backHooves": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffa500", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b37400", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "mane": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backMane": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "tail": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "facialHair": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "head": { + "fills": [ + "ffffff", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b3b3b3", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "headAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "earAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "808080", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "5a5a5a", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "faceAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "000000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "000000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "neckAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontLegAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backLegAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "lockBackLegAccessory": true, + "backAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "waistAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "95856f", + "674b43", + "4f4f4f", + "525252", + "c37850", + "8a3d34" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "685a46", + "482f27", + "373737", + "393939", + "884420", + "611b12" + ] + }, + "coatFill": "ffffff", + "coatOutline": "b3b3b3", + "lockCoatOutline": true, + "eyelashes": 0, + "eyeColorLeft": "daa520", + "eyeColorRight": "daa520", + "eyeWhites": "ffffff", + "eyeWhitesLeft": "ffffff", + "eyeOpennessLeft": 1, + "eyeOpennessRight": 1, + "eyeshadow": false, + "eyeshadowColor": "000000", + "lockEyes": true, + "lockEyeColor": true, + "unlockEyeWhites": false, + "fangs": 0, + "muzzle": 0, + "freckles": 0, + "frecklesColor": "8b0000", + "freeOutlines": false, + "cm": [], + "cmFlip": false, + "customOutlines": false, + "chestAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "sleeveAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "extraAccessory": { + "fills": [ + "9cc7d9", + "ffd700", + "ffd700", + "ffd700", + "daa520", + "ffd700" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "b39700", + "b39700", + "b39700", + "996d00", + "b39700" + ], + "pattern": 0, + "type": 0 + } +} \ No newline at end of file diff --git a/src/tests/ponies/default.json b/src/tests/ponies/default.json new file mode 100644 index 0000000..310ee8b --- /dev/null +++ b/src/tests/ponies/default.json @@ -0,0 +1,818 @@ +{ + "nose": { + "type": 0, + "pattern": 0, + "fills": [ + "ff0000", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b30000", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "ears": { + "type": 0, + "pattern": 0, + "fills": [ + "ff0000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b30000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "horn": { + "type": 0, + "pattern": 0, + "fills": [ + "ff0000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b30000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "wings": { + "type": 0, + "pattern": 0, + "fills": [ + "ff0000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b30000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "frontHooves": { + "type": 0, + "pattern": 0, + "fills": [ + "ffa500", + "ffff00", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b37400", + "b3b300", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "backHooves": { + "type": 0, + "pattern": 0, + "fills": [ + "ffa500", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b37400", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "mane": { + "type": 2, + "pattern": 0, + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "backMane": { + "type": 1, + "pattern": 0, + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "tail": { + "type": 1, + "pattern": 0, + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "facialHair": { + "type": 0, + "pattern": 0, + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "head": { + "fills": [ + "ff0000", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b30000", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "headAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "earAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "808080", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "5a5a5a", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "faceAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "000000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "000000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "neckAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "frontLegAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "backLegAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "lockBackLegAccessory": true, + "backAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "waistAccessory": { + "type": 0, + "pattern": 0, + "fills": [ + "95856f", + "674b43", + "4f4f4f", + "525252", + "c37850", + "8a3d34" + ], + "outlines": [ + "685a46", + "482f27", + "373737", + "393939", + "884420", + "611b12" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ] + }, + "coatFill": "ff0000", + "coatOutline": "b30000", + "lockCoatOutline": true, + "eyelashes": 0, + "eyeColorLeft": "daa520", + "eyeColorRight": "daa520", + "eyeWhites": "ffffff", + "eyeWhitesLeft": "ffffff", + "eyeOpennessLeft": 1, + "eyeOpennessRight": 1, + "eyeshadow": false, + "eyeshadowColor": "000000", + "lockEyes": true, + "lockEyeColor": true, + "unlockEyeWhites": false, + "fangs": 0, + "muzzle": 0, + "freckles": 0, + "frecklesColor": "8b0000", + "freeOutlines": false, + "cm": [], + "cmFlip": false, + "customOutlines": false, + "chestAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "sleeveAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "extraAccessory": { + "fills": [ + "9cc7d9", + "ffd700", + "ffd700", + "ffd700", + "daa520", + "ffd700" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "b39700", + "b39700", + "b39700", + "996d00", + "b39700" + ], + "pattern": 0, + "type": 0 + } +} \ No newline at end of file diff --git a/src/tests/ponies/perf.json b/src/tests/ponies/perf.json new file mode 100644 index 0000000..007de25 --- /dev/null +++ b/src/tests/ponies/perf.json @@ -0,0 +1,844 @@ +{ + "nose": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "9d906a", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + true, + true, + true, + true, + true + ], + "outlines": [ + "ffdf83", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "ears": { + "type": 3, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "9d906a", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + true, + true, + true, + true, + true + ], + "outlines": [ + "6e623f", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "horn": { + "type": 3, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "9d906a", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + true, + true, + true, + true, + true + ], + "outlines": [ + "6e623f", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "wings": { + "type": 2, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "9d906a", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + true, + true, + true, + true + ], + "outlines": [ + "6e623f", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontHooves": { + "type": 1, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffa500", + "ffff00", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b37400", + "b3b300", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backHooves": { + "type": 1, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffa500", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b37400", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "mane": { + "type": 1, + "pattern": 2, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "1e90ff", + "1e90ff", + "1e90ff", + "1e90ff" + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + false + ], + "outlines": [ + "b39700", + "005ab3", + "005ab3", + "005ab3", + "ffffff", + "005ab3" + ] + }, + "backMane": { + "type": 2, + "pattern": 1, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "tail": { + "type": 1, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "32cd32", + "32cd32", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "029002", + "029002", + "3eb38b" + ] + }, + "facialHair": { + "type": 4, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "head": { + "fills": [ + "8b0000", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "610000", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 1, + "type": 0 + }, + "headAccessory": { + "type": 2, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da9f70", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "996338", + "9a001f", + "3eb38b" + ] + }, + "earAccessory": { + "type": 6, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "808080", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + true + ], + "outlines": [ + "5a5a5a", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "faceAccessory": { + "type": 5, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "000000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + true, + true, + true, + true + ], + "outlines": [ + "000000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "neckAccessory": { + "type": 2, + "pattern": 2, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontLegAccessory": { + "type": 1, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + false + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backLegAccessory": { + "type": 1, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + false + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "lockBackLegAccessory": false, + "backAccessory": { + "type": 2, + "pattern": 2, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + false + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "waistAccessory": { + "type": 2, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "95856f", + "674b43", + "4f4f4f", + "525252", + "c37850", + "8a3d34" + ], + "lockOutlines": [ + false, + false, + false, + false, + true, + true + ], + "outlines": [ + "685a46", + "482f27", + "373737", + "393939", + "884420", + "611b12" + ] + }, + "coatFill": "9d906a", + "coatOutline": "890000", + "lockCoatOutline": false, + "eyelashes": 0, + "eyeColorLeft": "423f37", + "eyeColorRight": "daa520", + "eyeWhites": "a06f6f", + "eyeWhitesLeft": "a06f6f", + "eyeOpennessLeft": 4, + "eyeOpennessRight": 2, + "eyeshadow": true, + "eyeshadowColor": "00ff00", + "lockEyes": false, + "lockEyeColor": false, + "unlockEyeWhites": false, + "fangs": 1, + "muzzle": 5, + "freckles": 0, + "frecklesColor": "8b0000", + "freeOutlines": false, + "cm": [ + "ffa500", + "a68343", + "5a4724", + "362b17", + "8ee032", + "23211d", + "78b534", + "4a6f20", + "415927", + "627052", + "32cbe0", + "39aebd", + "206e79", + "5e8085", + "9aacaf", + "9c9aaf", + "453e95", + "7a72db", + "1d1763", + "5e5c7a", + "7a5c7a", + "db3fda", + "dd73dd", + "f3c1f3", + "7d177d" + ], + "cmFlip": true, + "customOutlines": true, + "chestAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "sleeveAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "extraAccessory": { + "fills": [ + "9cc7d9", + "ffd700", + "ffd700", + "ffd700", + "daa520", + "ffd700" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + false, + false, + false, + false, + false, + true + ], + "outlines": [ + "608898", + "ffd700", + "ffd700", + "ffd700", + "daa520", + "b39700" + ], + "pattern": 0, + "type": 0 + } +} \ No newline at end of file diff --git a/src/tests/ponies/school.json b/src/tests/ponies/school.json new file mode 100644 index 0000000..445abcf --- /dev/null +++ b/src/tests/ponies/school.json @@ -0,0 +1,818 @@ +{ + "nose": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "d2c6a8", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "93886d", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "ears": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "d2c6a8", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "93886d", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "horn": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "d2c6a8", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "93886d", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "wings": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "d2c6a8", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "93886d", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontHooves": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffa500", + "ffff00", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b37400", + "b3b300", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backHooves": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffa500", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b37400", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "mane": { + "type": 14, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "423e39", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "2e2b26", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backMane": { + "type": 1, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "423e39", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "2e2b26", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "tail": { + "type": 12, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "423e39", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "2e2b26", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "facialHair": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "423e39", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "2e2b26", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "head": { + "fills": [ + "d2c6a8", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "93886d", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "headAccessory": { + "type": 4, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "4a68a3", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "213c72", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "earAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "808080", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "5a5a5a", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "faceAccessory": { + "type": 4, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "000000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "000000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "neckAccessory": { + "type": 6, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "4a5d9d", + "ffffff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "22346e", + "b3b3b3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontLegAccessory": { + "type": 1, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffffff", + "ffffff", + "ffffff", + "ffffff", + "ffffff", + "4b78bd" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b3b3b3", + "b3b3b3", + "b3b3b3", + "b3b3b3", + "b3b3b3", + "1d4684" + ] + }, + "backLegAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "lockBackLegAccessory": true, + "backAccessory": { + "type": 2, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "224e73", + "ffffff", + "cbd9f0", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "072f51", + "b3b3b3", + "8693a8", + "993895", + "9a001f", + "3eb38b" + ] + }, + "waistAccessory": { + "type": 1, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "75552a", + "b6b6b6", + "4f4f4f", + "525252", + "c37850", + "8a3d34" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "52350e", + "7f7f7f", + "373737", + "393939", + "884420", + "611b12" + ] + }, + "coatFill": "d2c6a8", + "coatOutline": "93886d", + "lockCoatOutline": true, + "eyelashes": 1, + "eyeColorLeft": "daa520", + "eyeColorRight": "daa520", + "eyeWhites": "ffffff", + "eyeWhitesLeft": "ffffff", + "eyeOpennessLeft": 1, + "eyeOpennessRight": 1, + "eyeshadow": false, + "eyeshadowColor": "000000", + "lockEyes": true, + "lockEyeColor": true, + "unlockEyeWhites": false, + "fangs": 0, + "muzzle": 0, + "freckles": 0, + "frecklesColor": "8b0000", + "freeOutlines": false, + "cm": [], + "cmFlip": false, + "customOutlines": false, + "chestAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "sleeveAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "extraAccessory": { + "fills": [ + "9cc7d9", + "ffd700", + "ffd700", + "ffd700", + "daa520", + "ffd700" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "b39700", + "b39700", + "b39700", + "996d00", + "b39700" + ], + "pattern": 0, + "type": 0 + } +} \ No newline at end of file diff --git a/src/tests/ponies/wizard.json b/src/tests/ponies/wizard.json new file mode 100644 index 0000000..029deb7 --- /dev/null +++ b/src/tests/ponies/wizard.json @@ -0,0 +1,844 @@ +{ + "nose": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "9cc7d9", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "ears": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "9cc7d9", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "horn": { + "type": 1, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "9cc7d9", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "wings": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "9cc7d9", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontHooves": { + "type": 1, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffdb00", + "ffff00", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39900", + "b3b300", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backHooves": { + "type": 1, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffdb00", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39900", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "mane": { + "type": 2, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backMane": { + "type": 1, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "tail": { + "type": 1, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "facialHair": { + "type": 0, + "pattern": 0, + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "fills": [ + "ffd700", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "b39700", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "head": { + "fills": [ + "9cc7d9", + "800000", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "5a0000", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "headAccessory": { + "type": 2, + "pattern": 1, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "26528a", + "e9ff1e", + "cd9c32", + "3e73cd", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "062e61", + "a1b300", + "906302", + "0d3e90", + "9a001f", + "3eb38b" + ] + }, + "earAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "808080", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "5a5a5a", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "faceAccessory": { + "type": 3, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "000000", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "000000", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "neckAccessory": { + "type": 3, + "pattern": 4, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "99892b", + "ffffff", + "ffed0d", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "6b5d07", + "b3b3b3", + "b3a500", + "993895", + "9a001f", + "3eb38b" + ] + }, + "frontLegAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "backLegAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "lockBackLegAccessory": true, + "backAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ] + }, + "waistAccessory": { + "type": 0, + "pattern": 0, + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "fills": [ + "95856f", + "674b43", + "4f4f4f", + "525252", + "c37850", + "8a3d34" + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "685a46", + "482f27", + "373737", + "393939", + "884420", + "611b12" + ] + }, + "coatFill": "9cc7d9", + "coatOutline": "608898", + "lockCoatOutline": true, + "eyelashes": 0, + "eyeColorLeft": "daa520", + "eyeColorRight": "daa520", + "eyeWhites": "ffffff", + "eyeWhitesLeft": "ffffff", + "eyeOpennessLeft": 1, + "eyeOpennessRight": 1, + "eyeshadow": false, + "eyeshadowColor": "000000", + "lockEyes": true, + "lockEyeColor": true, + "unlockEyeWhites": false, + "fangs": 0, + "muzzle": 0, + "freckles": 0, + "frecklesColor": "8b0000", + "freeOutlines": false, + "cm": [ + "", + "", + "", + "ffffff", + "", + "ffffff", + "", + "", + "", + "", + "", + "", + "f6e160", + "", + "", + "", + "f6e160", + "d5c148", + "f6e160", + "", + "", + "", + "f6e160", + "", + "ffffff" + ], + "cmFlip": false, + "customOutlines": false, + "chestAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "sleeveAccessory": { + "fills": [ + "ee82ee", + "1e90ff", + "32cd32", + "da70d6", + "dc143c", + "7fffd4" + ], + "lockFills": [ + true, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "a744a7", + "005ab3", + "029002", + "993895", + "9a001f", + "3eb38b" + ], + "pattern": 0, + "type": 0 + }, + "extraAccessory": { + "fills": [ + "9cc7d9", + "ffd700", + "ffd700", + "ffd700", + "daa520", + "ffd700" + ], + "lockFills": [ + false, + false, + false, + false, + false, + false + ], + "lockOutlines": [ + true, + true, + true, + true, + true, + true + ], + "outlines": [ + "608898", + "b39700", + "b39700", + "b39700", + "996d00", + "b39700" + ], + "pattern": 0, + "type": 0 + } +} \ No newline at end of file diff --git a/src/tests/pony/all.png b/src/tests/pony/all.png new file mode 100644 index 0000000..027e687 --- /dev/null +++ b/src/tests/pony/all.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85f19d55941ae278b6e4efa8e2036d15b3bb1388493f7a45bb683de7895f67b2 +size 2917 diff --git a/src/tests/pony/base.png b/src/tests/pony/base.png new file mode 100644 index 0000000..cf28dd0 --- /dev/null +++ b/src/tests/pony/base.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09450bc47cdbf42f1dacc3e3d4aaf26e5902dd8bcc7e00653623638b9fd69287 +size 831 diff --git a/src/tests/pony/blinking-angry.png b/src/tests/pony/blinking-angry.png new file mode 100644 index 0000000..7939fe7 --- /dev/null +++ b/src/tests/pony/blinking-angry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:959f99a1cb2b2fe03af313c6ba6b50eb58d06dd6318953607e863a882a1fbf22 +size 1757 diff --git a/src/tests/pony/blinking-skip.png b/src/tests/pony/blinking-skip.png new file mode 100644 index 0000000..1d8cf6f --- /dev/null +++ b/src/tests/pony/blinking-skip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1efee489417a92a2bb3400c5c2adad6a12db95e13b2b9c75a2ad9dfdabbe641 +size 1810 diff --git a/src/tests/pony/blinking.png b/src/tests/pony/blinking.png new file mode 100644 index 0000000..fca4b84 --- /dev/null +++ b/src/tests/pony/blinking.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76be24377d5043fcde197b55406afbd8a8ee23fb00ac0e1b9ce806a459d46204 +size 1800 diff --git a/src/tests/pony/blush.png b/src/tests/pony/blush.png new file mode 100644 index 0000000..df48b03 --- /dev/null +++ b/src/tests/pony/blush.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:babe656ff85fbecedd7e077b3b312382934afa0bf04de389c87b3e712378821f +size 858 diff --git a/src/tests/pony/cape.png b/src/tests/pony/cape.png new file mode 100644 index 0000000..1aa425f --- /dev/null +++ b/src/tests/pony/cape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f409f24a249bcbcc77ff1414477e144ae3be6d4c3199a91294f5c0d64b2e3fc +size 1344 diff --git a/src/tests/pony/cm-flip.png b/src/tests/pony/cm-flip.png new file mode 100644 index 0000000..8f2dc7a --- /dev/null +++ b/src/tests/pony/cm-flip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9edbd0bff1370a7c32d6f9105657172d8e6797fa06bd1ccab0891f05bf8e747a +size 1459 diff --git a/src/tests/pony/deer.png b/src/tests/pony/deer.png new file mode 100644 index 0000000..a1f914f --- /dev/null +++ b/src/tests/pony/deer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5853a1cff511d67b138c1ff1a6d69d6011ca85e75ee8ded4c0719af48a6950ca +size 2128 diff --git a/src/tests/pony/dragon.png b/src/tests/pony/dragon.png new file mode 100644 index 0000000..a52654c --- /dev/null +++ b/src/tests/pony/dragon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae72defbd37511aa93a2ba951d1bcec0c565ced864f4b19bd261f5d7e585b6d8 +size 1450 diff --git a/src/tests/pony/extra.png b/src/tests/pony/extra.png new file mode 100644 index 0000000..35cadb8 --- /dev/null +++ b/src/tests/pony/extra.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:183bfc5ff7a6bb23fe976eb5e8e304eed23d5b6aa3c62f225ab473ef5947b5b0 +size 1289 diff --git a/src/tests/pony/eyes-0.png b/src/tests/pony/eyes-0.png new file mode 100644 index 0000000..3a408ba --- /dev/null +++ b/src/tests/pony/eyes-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:422f3de26184fb7d78aa4f66c88433a4fb32d0307c55cee0af88207f9d33ff8e +size 1070 diff --git a/src/tests/pony/face-extra-holding-letter.png b/src/tests/pony/face-extra-holding-letter.png new file mode 100644 index 0000000..c01f4ac --- /dev/null +++ b/src/tests/pony/face-extra-holding-letter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ab231254a77ef0c1eece33c9c319a34852305f85759e31bc0c1b1fe302e6e14 +size 1827 diff --git a/src/tests/pony/face-extra.png b/src/tests/pony/face-extra.png new file mode 100644 index 0000000..6d11911 --- /dev/null +++ b/src/tests/pony/face-extra.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0157bc48fc02cec6b3c6974d7ba53bc35d614d8f0022f5f3c1b04bf7cfaec42 +size 1862 diff --git a/src/tests/pony/freckles-flip.png b/src/tests/pony/freckles-flip.png new file mode 100644 index 0000000..a2ea9a4 --- /dev/null +++ b/src/tests/pony/freckles-flip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c07721178b116bea6573e52e36189b28d6c1e861382ac8ab5b0e90935e66e70 +size 1579 diff --git a/src/tests/pony/glasses.png b/src/tests/pony/glasses.png new file mode 100644 index 0000000..2421e02 --- /dev/null +++ b/src/tests/pony/glasses.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3151e502b001829c49cc5503acf379e8695732c1563659159c601ecbe154b65e +size 1307 diff --git a/src/tests/pony/griffon-lantern.png b/src/tests/pony/griffon-lantern.png new file mode 100644 index 0000000..7a71c09 --- /dev/null +++ b/src/tests/pony/griffon-lantern.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89fe488e9f2efe95d84fd777dafc5fbb79adbe8063273ee42460923ed29e4d70 +size 1836 diff --git a/src/tests/pony/griffon-socks.png b/src/tests/pony/griffon-socks.png new file mode 100644 index 0000000..1c82c25 --- /dev/null +++ b/src/tests/pony/griffon-socks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de3aef0f870de0d3402baf4ad0630be6dbd42a5dec4ad75fa2cf707b7dfcb752 +size 1905 diff --git a/src/tests/pony/griffon.png b/src/tests/pony/griffon.png new file mode 100644 index 0000000..4ab31a5 --- /dev/null +++ b/src/tests/pony/griffon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864630503d983b33b3f8510c77aa736f6ac9a21e5ca59f280205efa3235dd585 +size 1672 diff --git a/src/tests/pony/hair-behind-neck-accessory.png b/src/tests/pony/hair-behind-neck-accessory.png new file mode 100644 index 0000000..fe3f852 --- /dev/null +++ b/src/tests/pony/hair-behind-neck-accessory.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:567f96933b2e4baf8d10d13a38ba5243d0afdefedd72f696d2c934d133c5c627 +size 1494 diff --git a/src/tests/pony/hair-in-front-of-wing.png b/src/tests/pony/hair-in-front-of-wing.png new file mode 100644 index 0000000..425259b --- /dev/null +++ b/src/tests/pony/hair-in-front-of-wing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc245a88d6226fd3c604c772039c9791d4cf8660f0ade8cd84f2f24c8f380f42 +size 1337 diff --git a/src/tests/pony/hat-bald.png b/src/tests/pony/hat-bald.png new file mode 100644 index 0000000..cb03536 --- /dev/null +++ b/src/tests/pony/hat-bald.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba92424bcc8db2c4e76ee762ef2035e6afc3564bd71b4cd623a29b5927d07f11 +size 1287 diff --git a/src/tests/pony/hat-horns.png b/src/tests/pony/hat-horns.png new file mode 100644 index 0000000..ada3336 --- /dev/null +++ b/src/tests/pony/hat-horns.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5eb174d9867a43c44364a20c657c0086e117e4f37848079d5128ceaf13f5d832 +size 1509 diff --git a/src/tests/pony/holding-letter.png b/src/tests/pony/holding-letter.png new file mode 100644 index 0000000..0f7c0a8 --- /dev/null +++ b/src/tests/pony/holding-letter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34189b1a6fd25da4ef3a10d74d85656be5bce382ace49f02dec5bf8da9c323c4 +size 1310 diff --git a/src/tests/pony/holding.png b/src/tests/pony/holding.png new file mode 100644 index 0000000..de23997 --- /dev/null +++ b/src/tests/pony/holding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde8eef7de6c100c8d8bd4d8a0dbfbe4b6d9993897d10f543994c1ad1e9670e0 +size 1865 diff --git a/src/tests/pony/laughing.png b/src/tests/pony/laughing.png new file mode 100644 index 0000000..5d01123 --- /dev/null +++ b/src/tests/pony/laughing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a538a0ad30a289fda2f2a5ec0044c141f98217415a1e2c359ff539d66e3d17d +size 1783 diff --git a/src/tests/pony/lie.png b/src/tests/pony/lie.png new file mode 100644 index 0000000..9cbfcf5 --- /dev/null +++ b/src/tests/pony/lie.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6c6a5c7ee82b03dbdaad4ccf7a99b6a7512fa53e87e5e0b3c63f605f796890b +size 1255 diff --git a/src/tests/pony/mustache.png b/src/tests/pony/mustache.png new file mode 100644 index 0000000..298de82 --- /dev/null +++ b/src/tests/pony/mustache.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e73f8359c3c83343378f85cb472df2f06b2c0e157253a61f68727668f4605bcf +size 932 diff --git a/src/tests/pony/no-body.png b/src/tests/pony/no-body.png new file mode 100644 index 0000000..442627a --- /dev/null +++ b/src/tests/pony/no-body.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f56619a55df7c4fbc392c000c4d9460f0a09689f1fcef8dc4773be47c2280a4f +size 588 diff --git a/src/tests/pony/none.png b/src/tests/pony/none.png new file mode 100644 index 0000000..1fca275 --- /dev/null +++ b/src/tests/pony/none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d573d4cd0c72b4cea3f94b1a93cec4732eaf19f618ced3192be97e007bd4b3ce +size 458 diff --git a/src/tests/pony/offline.png b/src/tests/pony/offline.png new file mode 100644 index 0000000..3f86589 --- /dev/null +++ b/src/tests/pony/offline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:608c823aff37b7f5f272fc730c3989d28743a4de7637d3ebbb4419185e8b00dc +size 1136 diff --git a/src/tests/pony/paws-socks.png b/src/tests/pony/paws-socks.png new file mode 100644 index 0000000..5f426f0 --- /dev/null +++ b/src/tests/pony/paws-socks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ade62d4170df45368cd449d97dfd634687cef35476d346e9257b5d752237cba6 +size 1492 diff --git a/src/tests/pony/paws.png b/src/tests/pony/paws.png new file mode 100644 index 0000000..3ec84f0 --- /dev/null +++ b/src/tests/pony/paws.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce729b92f9305ea3ddb7ef39a8b042f1b75be8b7bd83f8e10a40e9e2cc869024 +size 1385 diff --git a/src/tests/pony/selected.png b/src/tests/pony/selected.png new file mode 100644 index 0000000..aed2bf6 --- /dev/null +++ b/src/tests/pony/selected.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e6fb86347d2391a9b116f88f6e29340ae8d754f30c39491890072e7f5d8a4e3 +size 875 diff --git a/src/tests/pony/sit-fetlocks.png b/src/tests/pony/sit-fetlocks.png new file mode 100644 index 0000000..08ad4b2 --- /dev/null +++ b/src/tests/pony/sit-fetlocks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f470a6f061b616438f4c166ce46b818fbf73bd62c18c23bf23ca7ffd9c567d5 +size 1757 diff --git a/src/tests/pony/sit-neck-accessory.png b/src/tests/pony/sit-neck-accessory.png new file mode 100644 index 0000000..40ca477 --- /dev/null +++ b/src/tests/pony/sit-neck-accessory.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9c823a9b24f3fa25acaac666cb5fd90755197a25f1f008cd45c089aa5d6771e +size 1662 diff --git a/src/tests/pony/sit-paws.png b/src/tests/pony/sit-paws.png new file mode 100644 index 0000000..fb2e547 --- /dev/null +++ b/src/tests/pony/sit-paws.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98bdc74d42bb03a95f0292c2d0332fb6c862cfe9cb9ea05902d3cd139c51227a +size 1525 diff --git a/src/tests/pony/sit-socks-cm.png b/src/tests/pony/sit-socks-cm.png new file mode 100644 index 0000000..a61fd1a --- /dev/null +++ b/src/tests/pony/sit-socks-cm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:095a1ba1159a274830c27d5eebb0c8cae276e537f707c2322d4d8e013737e6ef +size 1638 diff --git a/src/tests/pony/sit-socks.png b/src/tests/pony/sit-socks.png new file mode 100644 index 0000000..e905973 --- /dev/null +++ b/src/tests/pony/sit-socks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0f456b33e4a9eaf5bdab6dc852b723c278b021a1e09b9309ea75a410b8b2bc +size 1687 diff --git a/src/tests/pony/sit-sword.png b/src/tests/pony/sit-sword.png new file mode 100644 index 0000000..8740e30 --- /dev/null +++ b/src/tests/pony/sit-sword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74e50321d3ee22da4e40a79c329789ed35fd9422b0deffa2c950c56e8e6ed50d +size 1015 diff --git a/src/tests/pony/sit-wing.png b/src/tests/pony/sit-wing.png new file mode 100644 index 0000000..7cf8825 --- /dev/null +++ b/src/tests/pony/sit-wing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e5c2263673a50aa826a75dfd64a5d88187ca880fc9b710700e0b4e2c12efe75 +size 800 diff --git a/src/tests/pony/sit.png b/src/tests/pony/sit.png new file mode 100644 index 0000000..b8c886d --- /dev/null +++ b/src/tests/pony/sit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a895549531c5904b16280bac69d49b64c4cd057c0b526b769fd5eca18478aa65 +size 773 diff --git a/src/tests/pony/sitting-with-skirt-and-socks.png b/src/tests/pony/sitting-with-skirt-and-socks.png new file mode 100644 index 0000000..914c98e --- /dev/null +++ b/src/tests/pony/sitting-with-skirt-and-socks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7e32c7cfa1127ddd1701aeee1a623774fc49dfdb08485102ac928ae33d9b76a +size 1190 diff --git a/src/tests/pony/trot.png b/src/tests/pony/trot.png new file mode 100644 index 0000000..3687be7 --- /dev/null +++ b/src/tests/pony/trot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fbe2bcbdf6d9a381d20a38c55a8de353c8371459ec4069696ea950dc7fff296 +size 1940 diff --git a/src/tests/pony/wing-open.png b/src/tests/pony/wing-open.png new file mode 100644 index 0000000..435a213 --- /dev/null +++ b/src/tests/pony/wing-open.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e27a0585fed2b1d9b742d298a8cef744bf1721fca720056043c965812419bba +size 1651 diff --git a/src/ts/bootstrap-admin.ts b/src/ts/bootstrap-admin.ts new file mode 100644 index 0000000..19d7084 --- /dev/null +++ b/src/ts/bootstrap-admin.ts @@ -0,0 +1,5 @@ +import './bootstrap-common'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AdminAppModule } from './components/admin/admin.module'; + +platformBrowserDynamic().bootstrapModule(AdminAppModule, { preserveWhitespaces: true }); diff --git a/src/ts/bootstrap-common.ts b/src/ts/bootstrap-common.ts new file mode 100644 index 0000000..4688a80 --- /dev/null +++ b/src/ts/bootstrap-common.ts @@ -0,0 +1,17 @@ +import 'focus-visible'; +import 'core-js/es'; +import 'core-js/stable/promise/finally'; +import 'core-js/proposals/reflect-metadata'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/long-stack-trace-zone'; +import 'canvas-toBlob'; +import './client/polyfils'; +import { enableProdMode } from '@angular/core'; + +if (document.body.getAttribute('data-debug') !== 'true' || localStorage.production) { + enableProdMode(); +} + +if (typeof module !== 'undefined' && module.hot) { + module.hot.accept(); +} diff --git a/src/ts/bootstrap-es.ts b/src/ts/bootstrap-es.ts new file mode 100644 index 0000000..78f6a0d --- /dev/null +++ b/src/ts/bootstrap-es.ts @@ -0,0 +1,8 @@ +import './bootstrap-common'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './components/app/app.module'; +import { host, local } from './client/data'; + +if (DEVELOPMENT || local || host === `${location.protocol}//${location.host}/`) { + platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); +} diff --git a/src/ts/bootstrap-tools.ts b/src/ts/bootstrap-tools.ts new file mode 100644 index 0000000..f6592ad --- /dev/null +++ b/src/ts/bootstrap-tools.ts @@ -0,0 +1,5 @@ +import './bootstrap-common'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { ToolsAppModule } from './components/tools/tools.module'; + +platformBrowserDynamic().bootstrapModule(ToolsAppModule, { preserveWhitespaces: true }); diff --git a/src/ts/bootstrap.ts b/src/ts/bootstrap.ts new file mode 100644 index 0000000..2e055ab --- /dev/null +++ b/src/ts/bootstrap.ts @@ -0,0 +1,13 @@ +import './bootstrap-common'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './components/app/app.module'; +import { host, local } from './client/data'; + +if (DEVELOPMENT || local || host === `${location.protocol}//${location.host}/`) { + if (window.opener && window.opener.postMessage && window.URL) { + const path = location.href.replace(host, ''); + window.opener.postMessage({ type: 'loaded-page', path }, '*'); + } + + platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); +} diff --git a/src/ts/client/buttonActions.ts b/src/ts/client/buttonActions.ts new file mode 100644 index 0000000..1b619a5 --- /dev/null +++ b/src/ts/client/buttonActions.ts @@ -0,0 +1,676 @@ +import { compact } from 'lodash'; +import { + Expression, ExpressionButtonAction, CommandButtonAction, ActionButtonAction, ItemButtonAction, + ColorShadow, Eye, Muzzle, Iris, ButtonActionSlot, ExpressionExtra, ButtonAction, ChatType, BodyAnimation, + Action, isPartyChat, EntityButtonAction, defaultDrawOptions +} from '../common/interfaces'; +import * as sprites from '../generated/sprites'; +import { createExpression } from './clientUtils'; +import { encodeExpression, decodeExpression } from '../common/encoders/expressionEncoder'; +import { PonyTownGame } from './game'; +import { boopAction, upAction, downAction, turnHeadAction } from './playerActions'; +import { ACTIONS_LIMIT, COMMAND_ACTION_TIME_DELAY } from '../common/constants'; +import { cloneDeep, hasFlag } from '../common/utils'; +import { boop, defaultHeadFrame, stand, sneeze, yawn, lie, sit, fly, laugh } from './ponyAnimations'; +import { createDefaultPony, syncLockedPonyInfo, toPalette, mockPaletteManager } from '../common/ponyInfo'; +import { + ACTION_EXPRESSION_EYE_COLOR, ACTION_EXPRESSION_BG, ACTION_ACTION_COAT_COLOR, WHITE, HEARTS_COLOR, + ACTION_COMMAND_BG, BLACK, ACTION_ACTION_BG, ACTION_ITEM_BG, blushColor, ENTITY_ITEM_BG, TRANSPARENT +} from '../common/colors'; +import { resizeCanvasWithRatio, getPixelRatio, disableImageSmoothing } from './canvasUtils'; +import { drawCanvas, ContextSpriteBatch } from '../graphics/contextSpriteBatch'; +import { defaultPonyState, defaultDrawPonyOptions } from './ponyHelpers'; +import { drawHead, drawPony } from './ponyDraw'; +import { parseColor, toGrayscale, colorToHexRGB } from '../common/color'; +import { rect, addRects, centerPoint } from '../common/rect'; +import { drawTextAligned, HAlign, VAlign } from '../graphics/spriteFont'; +import { fontPal } from '../client/fonts'; +import { isPonyLying, isPonySitting, isPonyStanding, isPonyFlying } from '../common/entityUtils'; +import { canPonyFly } from '../common/pony'; +import { apple2, createAnEntity } from '../common/entities'; +import { fakePaletteManager } from '../common/mixins'; +import { spriteSheetsLoaded } from './spriteUtils'; +import { getEntityTypesFromName } from '../components/services/model'; +import { toWorldY, toWorldX } from '../common/positionUtils'; + +const CANVAS_SIZE = 29; +const ICON_SIZE = 16; + +const headX = -26; +const headY = -30; +const headlessBoopFrame = { ...cloneDeep(boop.frames[7]), head: 0 }; +const headlessBoop: BodyAnimation = { name: '', loop: false, fps: 1, frames: [headlessBoopFrame] }; + +function createPony(coatColor: string, wings = false, horn = false) { + const info = createDefaultPony(); + info.coatFill = coatColor; + info.mane!.type = 0; + info.backMane!.type = 0; + info.tail!.type = 0; + info.eyeColorRight = ACTION_EXPRESSION_EYE_COLOR; + + if (wings) { + info.wings!.type = 1; + } + + if (horn) { + info.horn!.type = 1; + } + + syncLockedPonyInfo(info); + return toPalette(info, mockPaletteManager); +} + +function createState() { + const state = defaultPonyState(); + state.blushColor = blushColor(parseColor(ACTION_ACTION_COAT_COLOR)); + return state; +} + +function colorToGrayscale(value: string) { + return colorToHexRGB(toGrayscale(parseColor(value))); +} + +const ACTION_ACTION_BG_DISABLED = toGrayscale(parseColor(ACTION_ACTION_BG)); +const expressionPony = createPony(ACTION_EXPRESSION_BG); +const actionPony = createPony(ACTION_ACTION_COAT_COLOR); +const actionPonyWithHorn = createPony(ACTION_ACTION_COAT_COLOR, false, true); +const actionPonyWithWings = createPony(ACTION_ACTION_COAT_COLOR, true); +const actionPonyDisabled = createPony(colorToGrayscale(ACTION_ACTION_COAT_COLOR)); +const actionPonyWithWingsDisabled = createPony(colorToGrayscale(ACTION_ACTION_COAT_COLOR), true); +const defaultPalette = mockPaletteManager.addArray(sprites.defaultPalette); +export const actionExpressionDefaultPalette = mockPaletteManager.add(Array.from(sprites.defaultPalette)); +expressionPony.defaultPalette = actionExpressionDefaultPalette; +expressionPony.defaultPalette.colors[4] = 0xe16200ff; // tongue color + +export function expressionButtonAction(expression: Expression | undefined): ExpressionButtonAction { + return { type: 'expression', expression, title: expression ? '' : 'Reset expression' }; +} + +export function commandButtonAction(command: string, icon: string): CommandButtonAction { + return { type: 'command', command, title: command, icon }; +} + +export function actionButtonAction(action: string, title: string, sendAction = Action.None): ActionButtonAction { + return { type: 'action', action, title, sendAction }; +} + +export function itemButtonAction(icon: ColorShadow, count?: number): ItemButtonAction { + return { type: 'item', icon, count }; +} + +export function entityButtonAction(entity: string): EntityButtonAction { + return { type: 'entity', entity, title: entity }; +} + +const actionActions = [ + actionButtonAction('boop', 'Boop'), + actionButtonAction('down', 'Sit down / Land'), + actionButtonAction('up', 'Stand up / Fly up'), + actionButtonAction('turn-head', 'Turn head'), + actionButtonAction('sneeze', 'Sneeze', Action.Sneeze), + actionButtonAction('sleep', 'Sleep', Action.Sleep), + actionButtonAction('yawn', 'Yawn', Action.Yawn), + actionButtonAction('love', 'Love', Action.Love), + actionButtonAction('laugh', 'Laugh', Action.Laugh), + actionButtonAction('blush', 'Blush', Action.Blush), + actionButtonAction('drop', 'Drop item', Action.Drop), + actionButtonAction('drop-toy', 'Drop toy', Action.DropToy), + actionButtonAction('magic', 'Magic', Action.Magic), + actionButtonAction('switch-tool', 'Switch tool', Action.SwitchTool), + actionButtonAction('switch-entity', 'Switch item to place'), + actionButtonAction('switch-entity-rev', 'Switch item to place (reverse)'), + actionButtonAction('switch-tile', 'Switch tile to place'), +]; + +const commandActions = [ + commandButtonAction('/roll', '🎲'), + commandButtonAction('/gifts', '🎁'), + commandButtonAction('/candies', '🍬'), + commandButtonAction('/clovers', '🍀'), + commandButtonAction('/toys', '🎅'), + commandButtonAction('/eggs', '🥚'), +]; + +const additionalActionsActions = [ + expressionButtonAction(undefined), +]; + +function getActionAction(action: string) { + return actionActions.find(a => a.action === action); +} + +function getCommandAction(command: string) { + return commandActions.find(a => a.command === command); +} + +export function createButtionActionActions() { + return [...actionActions, ...additionalActionsActions]; +} + +export function createButtonCommandActions() { + return [...commandActions]; +} + +export function createDefaultButtonActions(): ButtonActionSlot[] { + return DEVELOPMENT ? [ + { action: getActionAction('boop') }, + { action: getActionAction('down') }, + { action: getActionAction('up') }, + { action: getActionAction('turn-head') }, + { action: expressionButtonAction(createExpression(Eye.Closed, Eye.Closed, Muzzle.Smile)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral3, Muzzle.Smile)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Scrunch, Iris.Forward, Iris.Up)) }, + { action: expressionButtonAction(createExpression(Eye.Angry, Eye.Angry, Muzzle.Scrunch)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral3, Eye.Neutral3, Muzzle.Flat, Iris.Left, Iris.Left)) }, + { action: expressionButtonAction(createExpression(Eye.X, Eye.X, Muzzle.Flat)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral4, Eye.Neutral4, Muzzle.Flat)) }, + { action: undefined }, + { action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left)) }, + { action: getCommandAction('/roll') }, + { action: itemButtonAction(sprites.flower_2, 14) }, + { action: itemButtonAction(sprites.apple_1, 5) }, + { action: itemButtonAction(sprites.pumpkin_default) }, + { action: itemButtonAction(sprites.tree_1, 2) }, + { + action: expressionButtonAction( + createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked, ExpressionExtra.Tears)) + }, + { + action: expressionButtonAction( + createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Cry)) + }, + { + action: expressionButtonAction( + createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Hearts)) + }, + { + action: expressionButtonAction( + createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Zzz)) + }, + { + action: expressionButtonAction( + createExpression( + Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, + ExpressionExtra.Zzz | ExpressionExtra.Cry | ExpressionExtra.Hearts | ExpressionExtra.Blush)) + }, + ] : [ + { action: getActionAction('boop') }, + { action: getActionAction('down') }, + { action: getActionAction('up') }, + { action: getActionAction('turn-head') }, + { action: getActionAction('magic') }, + { action: expressionButtonAction(undefined) }, + { action: expressionButtonAction(createExpression(Eye.Closed, Eye.Closed, Muzzle.Smile)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral3, Muzzle.Smile)) }, + { action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Scrunch, Iris.Forward, Iris.Up)) }, + { action: expressionButtonAction(createExpression(Eye.Angry, Eye.Angry, Muzzle.Scrunch)) }, + ]; +} + +export function serializeActions(slots: ButtonActionSlot[]): string { + const serialized = slots.slice(0, ACTIONS_LIMIT).map(serializeAction); + + while (serialized.length && !serialized[serialized.length - 1]) { + serialized.pop(); + } + + return JSON.stringify(serialized); +} + +export function deserializeActions(data: string): ButtonActionSlot[] { + try { + const json = JSON.parse(data); + return json.slice(0, ACTIONS_LIMIT).map(deserializeAction); + } catch (e) { + DEVELOPMENT && console.error(e); + return []; + } +} + +function serializeAction({ action }: ButtonActionSlot): any { + if (action) { + switch (action.type) { + case 'action': + return { act: action.action }; + case 'command': + return { cmd: action.command }; + case 'expression': + return { exp: encodeExpression(action.expression) }; + case 'entity': + return { ent: action.entity }; + default: + DEVELOPMENT && console.warn(`Missing serialization for ${JSON.stringify(action)}`); + return null; + } + } else { + return null; + } +} + +function deserializeAction(data: any): ButtonActionSlot { + if (data) { + if ('act' in data || 'action' in data) { + return { action: getActionAction(data.act || data.action) }; + } else if ('cmd' in data || 'command' in data) { + return { action: getCommandAction(data.cmd || data.command) }; + } else if ('exp' in data || 'expression' in data) { + const expression = decodeExpression(data.exp || data.expression | 0); + return { action: expressionButtonAction(expression) }; + } else if ('ent' in data || 'entity' in data) { + return { action: entityButtonAction(data.ent || data.entity) }; + } else { + DEVELOPMENT && console.warn(`Missing deserialization for ${JSON.stringify(data)}`); + } + } + + return { action: undefined }; +} + +const lastCommandCalls: { [key: string]: number; } = {}; + +export function useAction(game: PonyTownGame, action: ButtonAction | undefined) { + if (action) { + switch (action.type) { + case 'expression': + game.send(server => server.expression(encodeExpression(action.expression))); + break; + case 'action': + if (action.sendAction) { + game.send(server => server.action(action.sendAction)); + } else { + switch (action.action) { + case 'boop': + boopAction(game); + break; + case 'up': + upAction(game); + break; + case 'down': + downAction(game); + break; + case 'turn-head': + turnHeadAction(game); + break; + case 'switch-entity': + game.send(server => server.action(Action.SwitchToPlaceTool)); + game.changePlaceEntity(false); + break; + case 'switch-entity-rev': + game.send(server => server.action(Action.SwitchToPlaceTool)); + game.changePlaceEntity(true); + break; + case 'switch-tile': + game.send(server => server.action(Action.SwitchToTileTool)); + game.changePlaceTile(false); + break; + default: + console.log('Action not supported: ', action.action); + } + } + break; + case 'command': + const now = performance.now(); + const lastCall = lastCommandCalls[action.command] | 0; + + if ((now - lastCall) > COMMAND_ACTION_TIME_DELAY) { + lastCommandCalls[action.command] = now; + const chatType = isPartyChat(game.lastChatMessageType) ? ChatType.Party : ChatType.Say; + game.send(server => server.say(0, action.command, chatType)); + } + break; + case 'entity': + if (BETA) { + game.editor.type = action.entity; + } + break; + default: + console.log('Action type not supported: ', action.type); + } + } +} + +function shouldRedrawAction(action: ButtonAction | undefined, state: any, game: PonyTownGame) { + if (action !== state.action) { + return true; + } else if (action) { + switch (action.type) { + case 'action': { + switch (action.action) { + case 'up': + return state.draw !== getUpDrawFunc(game); + case 'down': + return state.draw !== getDownDrawFunc(game); + // case 'turn-head': + // return state.right !== (game.player && isHeadFacingRight(game.player)); + default: + return false; + } + } + default: + return false; + } + } else { + return false; + } +} + +const canvasCache = new Map(); +const palette = mockPaletteManager.addArray(sprites.fontPalette); +const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette); + +function drawCanvasCached(key: string, action: (batch: ContextSpriteBatch) => void) { + const canvas = canvasCache.get(key) || drawCanvas(ICON_SIZE, ICON_SIZE, sprites.paletteSpriteSheet, undefined, action); + canvasCache.set(key, canvas); + return canvas; +} + +export function drawAction(canvas: HTMLCanvasElement, action: ButtonAction | undefined, state: any, game: PonyTownGame) { + if (resizeCanvasWithRatio(canvas, CANVAS_SIZE, CANVAS_SIZE)) { + state.action = 0; + } + + if (!spriteSheetsLoaded || !shouldRedrawAction(action, state, game)) + return; + + const context = canvas.getContext('2d'); + + if (!context) + return; + + state.action = action; + + context.save(); + context.clearRect(0, 0, canvas.width, canvas.height); + disableImageSmoothing(context); + + const scale = 2 * getPixelRatio(); + const bufferSize = ICON_SIZE; + + if (action) { + switch (action.type) { + case 'expression': { + const buffer = drawCanvas(bufferSize, bufferSize, sprites.paletteSpriteSheet, undefined, batch => { + const state = { ...createState(), expression: action.expression }; + const options = { ...defaultDrawPonyOptions(), noEars: true }; + drawHead(batch, expressionPony, headX, headY, undefined, defaultHeadFrame, state, options, false, 0); + + if (action.expression) { + const extra = action.expression.extra; + + if (hasFlag(extra, ExpressionExtra.Zzz)) { + batch.drawSprite(sprites.emote_sleep1.frames[13], WHITE, defaultPalette, headX + 15, headY + 3); + } + + if (hasFlag(extra, ExpressionExtra.Hearts)) { + batch.drawSprite(sprites.emote_hearts.frames[41], HEARTS_COLOR, defaultPalette, headX + 8, headY + 22); + } + + if (hasFlag(extra, ExpressionExtra.Cry)) { + batch.drawSprite(sprites.emote_cry2.frames[4], WHITE, defaultPalette, headX, headY); + } else if (hasFlag(extra, ExpressionExtra.Tears)) { + batch.drawSprite(sprites.emote_tears.frames[0], WHITE, defaultPalette, headX, headY); + } + } else { + const color = parseColor(ACTION_EXPRESSION_BG); + batch.drawRect(color, 0, 3, 15, 5); + batch.drawRect(color, 0, 8, 3, 1); + batch.drawRect(color, 8, 8, 4, 1); + } + }); + + context.fillStyle = ACTION_EXPRESSION_BG; + context.fillRect(0, 0, canvas.width, canvas.height); + context.scale(scale, scale); + context.drawImage(buffer, 0, 0); + break; + } + case 'command': { + const buffer = drawCanvasCached(`command:${action.icon}`, batch => { + const bounds = rect(0, 0, 15, 15); + const options = { palette, emojiPalette }; + drawTextAligned(batch, action.icon, fontPal, BLACK, bounds, HAlign.Center, VAlign.Middle, options); + }); + + context.fillStyle = ACTION_COMMAND_BG; + context.fillRect(0, 0, canvas.width, canvas.height); + context.scale(scale, scale); + context.drawImage(buffer, -0.5, 0.5); + break; + } + case 'action': { + let buffer: HTMLCanvasElement; + + if (action.action === 'up' || action.action === 'down') { + state.draw = action.action === 'up' ? getUpDrawFunc(game) : getDownDrawFunc(game); + + buffer = drawCanvasCached(`action:${action.action}:${state.draw}`, batch => { + state.draw && getDrawFuncByName(state.draw)(batch); + }); + } else { + buffer = drawCanvasCached(`action:${action.action}`, batch => { + switch (action.action) { + case 'boop': { + const state = { ...createState(), animation: headlessBoop, animationFrame: 0 }; + drawPony(batch, actionPony, state, 25, 32, defaultDrawPonyOptions()); + break; + } + case 'turn-head': { + // state.right = game.player && isHeadFacingRight(game.player); + const ponyState = { ...createState(), animation: stand }; + drawPony(batch, actionPony, ponyState, 15, 40, defaultDrawPonyOptions()); + + // if (!state.right) { + // context.translate(context.canvas.width, 0); + // context.scale(-1, 1); + // } + break; + } + case 'sneeze': { + const state = { ...createState(), headAnimation: sneeze, headAnimationFrame: 3 }; + drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions()); + break; + } + case 'sleep': { + const state = { ...createState(), expression: createExpression(Eye.Closed, Eye.Closed, Muzzle.Neutral) }; + drawPony(batch, actionPony, state, 18, 40, defaultDrawPonyOptions()); + batch.drawSprite(sprites.emote_sleep1.frames[13], WHITE, defaultPalette, headX + 15, headY + 3); + break; + } + case 'drop': { + const state = { ...createState(), holding: fakePaletteManager(() => apple2(0, 0)) }; + drawPony(batch, actionPony, state, 20, 40, defaultDrawPonyOptions()); + batch.drawSprite(sprites.arrow_down, BLACK, defaultPalette, 1, 3); + break; + } + case 'drop-toy': { + const state = { ...createState() }; + const options = { ...defaultDrawPonyOptions(), toy: 17 }; + drawPony(batch, actionPony, state, 18, 52, options); + batch.drawSprite(sprites.arrow_down, BLACK, defaultPalette, 1, 3); + break; + } + case 'yawn': { + const state = { ...createState(), headAnimation: yawn, headAnimationFrame: 3 }; + drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions()); + break; + } + case 'laugh': { + const state = { ...createState(), headAnimation: laugh, headAnimationFrame: 3 }; + drawPony(batch, actionPony, state, 17, 38, defaultDrawPonyOptions()); + break; + } + case 'blush': { + const state = { + ...createState(), expression: createExpression( + Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush) + }; + drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions()); + break; + } + case 'love': { + batch.drawSprite(sprites.emote_hearts.frames[10], 0xbc414fff, defaultPalette, headX + 2, headY + 18); + break; + } + case 'magic': { + const state = { ...createState(), headAnimation: laugh, headAnimationFrame: 3 }; + drawPony(batch, actionPonyWithHorn, state, 17, 48, defaultDrawPonyOptions()); + batch.drawSprite(sprites.magic_icon, WHITE, defaultPalette, 4, 2); + break; + } + case 'switch-tool': { + const palette = mockPaletteManager.addArray(sprites.tools_icon.palettes![0]); + batch.drawSprite(sprites.tools_icon.color, WHITE, palette, 0, 2); + break; + } + case 'switch-entity': { + const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]); + batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 2); + break; + } + case 'switch-entity-rev': { + const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]); + batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 3); + batch.drawSprite(sprites.arrow_left, BLACK, defaultPalette, 1, 1); + break; + } + case 'switch-tile': { + const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]); + batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 2); + break; + } + default: + throw new Error(`Invalid action: ${action.action}`); + } + }); + } + + context.fillStyle = ACTION_ACTION_BG; + context.fillRect(0, 0, canvas.width, canvas.height); + context.scale(scale, scale); + context.drawImage(buffer, 0, 0); + break; + } + case 'item': { + const buffer = drawCanvas(bufferSize, bufferSize, sprites.paletteSpriteSheet, undefined, batch => { + const palette = mockPaletteManager.addArray(action.icon.palettes![0]); + const sprite = action.icon.color; + batch.drawSprite(sprite, WHITE, palette, + Math.round((15 - sprite.w) / 2), Math.round((15 - sprite.h) / 2) + 1); + }); + + context.fillStyle = ACTION_ITEM_BG; + context.fillRect(0, 0, canvas.width, canvas.height); + context.scale(scale, scale); + context.drawImage(buffer, -0.5, -0.5); + break; + } + case 'entity': { + if (BETA) { + const types = getEntityTypesFromName(action.entity) || []; + const size = bufferSize * scale; + const entities = types.map(type => createAnEntity(type, 0, 0, 0, {}, mockPaletteManager, game)); + // createAnEntity(type, 0, toWorldX(size / 2 - 2), toWorldY(size * 0.75), {}, mockPaletteManager)); + + const bounds = compact(entities.map(e => e.bounds)).reduce(addRects, rect(0, 0, 0, 0)); + const center = centerPoint(bounds); + + const buffer = drawCanvas(size, size, sprites.paletteSpriteSheet, undefined, batch => { + for (const entity of entities) { + if (entity.draw) { + entity.x += toWorldX(size / 2 - 2 - center.x); + entity.y += toWorldY(size / 2 - center.y); + entity.draw(batch, { ...defaultDrawOptions, shadowColor: TRANSPARENT }); + } + } + }); + + context.fillStyle = ENTITY_ITEM_BG; + context.fillRect(0, 0, canvas.width, canvas.height); + context.drawImage(buffer, 0, 0); + } + break; + } + } + } + + context.restore(); +} + +function drawLie(batch: ContextSpriteBatch) { + const state = { ...createState(), animation: lie }; + drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions()); +} + +function drawLieDisabled(batch: ContextSpriteBatch) { + const state = { ...createState(), animation: lie }; + batch.drawRect(ACTION_ACTION_BG_DISABLED, 0, 0, 50, 50); + drawPony(batch, actionPonyDisabled, state, -6, 15, defaultDrawPonyOptions()); +} + +function drawSit(batch: ContextSpriteBatch) { + const state = { ...createState(), animation: sit }; + drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions()); +} + +function drawStand(batch: ContextSpriteBatch) { + const state = { ...createState(), animation: stand }; + drawPony(batch, actionPony, state, -1, 15, defaultDrawPonyOptions()); +} + +function drawFly(batch: ContextSpriteBatch) { + const state = { ...createState(), animation: fly }; + drawPony(batch, actionPonyWithWings, state, 0, 30, defaultDrawPonyOptions()); +} + +function drawFlyDisabled(batch: ContextSpriteBatch) { + const state = { ...createState(), animation: fly }; + batch.drawRect(ACTION_ACTION_BG_DISABLED, 0, 0, 50, 50); + drawPony(batch, actionPonyWithWingsDisabled, state, 0, 30, defaultDrawPonyOptions()); +} + +function getDrawFuncByName(name: string) { + switch (name) { + case 'lie': return drawLie; + case 'sit': return drawSit; + case 'stand': return drawStand; + case 'fly': return drawFly; + case 'flyDisabled': return drawFlyDisabled; + case 'lieDisabled': return drawLieDisabled; + default: + throw new Error(`Invalid name: ${name}`); + } +} + +function getUpDrawFunc(game: PonyTownGame) { + const player = game.player; + + if (player) { + if (isPonyLying(player)) { + return 'sit'; + } else if (isPonySitting(player)) { + return 'stand'; + } else if (isPonyStanding(player) && canPonyFly(player)) { + return 'fly'; + } + } + + return 'flyDisabled'; +} + +function getDownDrawFunc(game: PonyTownGame) { + const player = game.player; + + if (player) { + if (isPonySitting(player)) { + return 'lie'; + } else if (isPonyStanding(player)) { + return 'sit'; + } else if (isPonyFlying(player)) { + return 'stand'; + } + } + + return 'lieDisabled'; +} diff --git a/src/ts/client/canvasUtils.ts b/src/ts/client/canvasUtils.ts new file mode 100644 index 0000000..d2ec4a0 --- /dev/null +++ b/src/ts/client/canvasUtils.ts @@ -0,0 +1,101 @@ +import { saveAs } from 'file-saver'; + +/* istanbul ignore next */ +export let createCanvas = (width: number, height: number): HTMLCanvasElement => { + const canvas = document.createElement('canvas'); + canvas.width = width | 0; + canvas.height = height | 0; + return canvas; +}; + +/* istanbul ignore next */ +export let loadImage = (src: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', () => reject(new Error(`Error loading image (${src})`))); + img.src = src; + }); +}; + +/* istanbul ignore next */ +function canUseImageBitmap() { + return typeof fetch === 'function' && + typeof createImageBitmap === 'function' && + !/yabrowser/i.test(navigator.userAgent); // disabled due to yandex browser bug +} + +/* istanbul ignore next */ +if (canUseImageBitmap()) { + loadImage = src => fetch(src) + .then(response => response.blob()) + .then(createImageBitmap); +} + +export function setup(methods: { + createCanvas(width: number, height: number): HTMLCanvasElement; + loadImage(src: string): Promise; +}) { + createCanvas = methods.createCanvas; + loadImage = methods.loadImage; +} + +/* istanbul ignore next */ +export const getPixelRatio = SERVER ? () => 1 : () => window.devicePixelRatio; + +export function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number) { + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } +} + +export function resizeCanvasWithRatio(canvas: HTMLCanvasElement, width: number, height: number, updateStyle = true) { + const ratio = getPixelRatio(); + const w = Math.round(width * ratio); + const h = Math.round(height * ratio); + let resized = false; + + if (canvas.width !== w || canvas.height !== h) { + canvas.width = w; + canvas.height = h; + resized = true; + } + + if (updateStyle && (canvas.style.width !== width + 'px' || canvas.style.height !== height + 'px')) { + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + resized = true; + } + + return resized; +} + +/* istanbul ignore next */ +export function canvasToSource(canvas: HTMLCanvasElement) { + return new Promise((resolve, reject) => { + canvas.toBlob(blob => { + if (blob) { + resolve(URL.createObjectURL(blob)); + } else { + reject(new Error('Failed to convert canvas')); + } + }); + }); +} + +/* istanbul ignore next */ +export function saveCanvas(canvas: HTMLCanvasElement, name: string) { + canvas.toBlob(blob => blob && saveAs(blob, name)); +} + +/* istanbul ignore next */ +export function disableImageSmoothing(context: CanvasRenderingContext2D) { + if ('imageSmoothingEnabled' in context) { + context.imageSmoothingEnabled = false; + } else { + (context as any).webkitImageSmoothingEnabled = false; + (context as any).mozImageSmoothingEnabled = false; + (context as any).msImageSmoothingEnabled = false; + } +} diff --git a/src/ts/client/clientActions.ts b/src/ts/client/clientActions.ts new file mode 100644 index 0000000..4568caf --- /dev/null +++ b/src/ts/client/clientActions.ts @@ -0,0 +1,304 @@ +import { NgZone } from '@angular/core'; +import { Method, SocketClient, Bin, getMethods } from 'ag-sockets/dist/browser'; +import { + MapInfo, WorldState, PartyMember, PartyFlags, Action, NotificationFlags, Pony, LeaveReason, + SayData, MapState, defaultMapState, Apply, InfoFlags, PonyData, FriendStatusData, WorldMap +} from '../common/interfaces'; +import { hasFlag, findById } from '../common/utils'; +import { isPony } from '../common/pony'; +import { setTileAtRegion, findEntityById, createWorldMap, removeRegions, updateMapState } from '../common/worldMap'; +import { GameService } from '../components/services/gameService'; +import { PonyTownGame } from './game'; +import { supportsLetAndConst, isInIncognitoMode } from './clientUtils'; +import { savePlayerPosition, setAclCookie } from './sec'; +import { updateParty } from './partyUtils'; +import { addNotification, removeNotification, markGameAsLoaded, resetGameFields, isSelected } from './gameUtils'; +import { Model } from '../components/services/model'; +import { decodeUpdate } from '../common/encoders/updateDecoder'; +import { + updatePonyInfoWithPoof, subscribeRegion, handleUpdates, handleUpdateEntity, handleRemoveEntity, handleSays, + handleEntityInfo, handleUpdatePonies, filterEntityName, handleUpdateFriends +} from './handlers'; +import { nameToHTML } from './emoji'; + +const BinEntityId = Bin.U32; +const BinEntityPlayerState = Bin.U8; +const BinNotificationId = Bin.U16; +const BinSayDatas = [BinEntityId, Bin.Str, Bin.U8]; + +function findPonyById(map: WorldMap, id: number) { + const entity = findEntityById(map, id); + return entity && isPony(entity) ? entity : undefined; +} + +export class ClientActions implements SocketClient { + constructor(private gameService: GameService, private game: PonyTownGame, private model: Model, private zone: NgZone) { + } + private apply: Apply = func => this.zone.run(func); + connected() { + resetGameFields(this.game); + this.game.map = createWorldMap(); + this.game.player = undefined; + this.game.joined(); + this.apply(() => this.gameService.joined()); + + const supportsWasm = typeof WebAssembly !== 'undefined'; + const info = 0 | + (isInIncognitoMode ? InfoFlags.Incognito : 0) | + (supportsWasm ? InfoFlags.SupportsWASM : 0) | + (supportsLetAndConst() ? InfoFlags.SupportsLetAndConst : 0); + + this.game.send(server => server.actionParam2(Action.Info, info)); + } + disconnected() { + resetGameFields(this.game); + this.apply(() => this.gameService.disconnected()); + } + invalidVersion() { + DEVELOPMENT && !TESTS && console.error('Invalid version'); + } + @Method({ binary: [Bin.U32] }) + queue(place: number) { + this.game.placeInQueue = place; + } + @Method({ binary: [Bin.Obj, Bin.Bool] }) + worldState(state: WorldState, initial: boolean) { + this.game.placeInQueue = 0; + this.game.setWorldState(state, initial); + } + @Method({ binary: [Bin.Obj, Bin.Obj] }) + mapState(info: MapInfo, state: MapState) { + this.game.map = createWorldMap(info, state); + this.game.player = undefined; + this.game.setupMap(); + updateMapState(this.game.map, defaultMapState, this.game.map.state); + } + @Method({ binary: [Bin.Obj] }) + mapUpdate(state: MapState) { + const prevState = this.game.map.state; + this.game.map.state = state; + updateMapState(this.game.map, prevState, this.game.map.state); + } + @Method({ binary: [] }) + mapSwitching() { + this.game.loaded = false; + this.game.placeInQueue = 0; + + if (this.game.player) { + this.game.player.vx = 0; + this.game.player.vy = 0; + } + } + @Method({ binary: [Bin.I32, Bin.I32, Bin.U8Array] }) + mapTest(width: number, height: number, buffer: Uint8Array) { + const data = new Uint32Array(width * height); + (new Uint8Array(data.buffer)).set(buffer); + this.game.minimap = { width, height, data }; + } + @Method({ binary: [BinEntityId, Bin.Str, Bin.Str, Bin.Str, Bin.U16] }) + myEntity(id: number, name: string, info: string, characterId: string, crc: number) { + this.game.playerId = id; + this.game.playerName = name; + this.game.playerInfo = info; + this.game.playerCRC = crc; + + const pony = findById(this.model.ponies, characterId); + + if (pony) { + this.model.selectPony(pony); + } + + if (this.game.party) { + this.game.party.members.forEach(m => m.self = m.id === id); + this.game.onPartyUpdate.next(); + } + + const entity = findEntityById(this.game.map, id) as Pony | undefined; + + if (entity) { + entity.name = name; + updatePonyInfoWithPoof(this.game, entity, info, crc); + } + + this.game.onActionsUpdate.next(); + } + @Method({ binary: [[Bin.U8], [Bin.U8Array], Bin.U8Array, [Bin.U8Array], BinSayDatas] }) + update(unsubscribes: number[], subscribes: Uint8Array[], updates: Uint8Array | null, regions: Uint8Array[], says: SayData[]) { + removeRegions(this.game.map, unsubscribes); + + for (const subscribe of subscribes) { + subscribeRegion(this.game, subscribe); + } + + if (subscribes.length) { + markGameAsLoaded(this.game); + } + + if (updates) { + handleUpdates(this.game, updates); + } + + for (const region of regions) { + const { x, y, updates, removes, tiles } = decodeUpdate(region); + + for (const update of updates) { + handleUpdateEntity(this.game, update); + } + + for (const id of removes) { + handleRemoveEntity(this.game, id); + } + + for (const tile of tiles) { + setTileAtRegion(this.game.map, x, y, tile.x, tile.y, tile.type); + } + } + + for (const [id, message, type] of says) { + handleSays(this.game, id, message, type); + } + } + @Method({ binary: [Bin.F32, Bin.F32, Bin.Bool] }) + fixPosition(x: number, y: number, safe: boolean) { + if (DEVELOPMENT && !TESTS && !safe) { + console.error(`fix position (${x.toFixed(2)}, ${y.toFixed(2)})`); + } + + const player = this.game.player; + + if (player) { + player.x = x; + player.y = y; + savePlayerPosition(); + } + + this.game.send(server => server.fixedPosition()); + } + @Method({ binary: [BinEntityId, Bin.U8, Bin.Obj] }) + actionParam(id: number, action: Action, param: any) { + switch (action) { + case Action.ACL: + if (id === this.game.playerId && param) { + setAclCookie(param); + } + break; + case Action.FriendsCRC: + this.game.nextFriendsCRC = 0; + break; + default: + DEVELOPMENT && !TESTS && console.error(`actionParam: Invalid action: ${action}`); + } + } + @Method({ binary: [Bin.U8] }) + left(reason: LeaveReason) { + this.game.player = undefined; + this.game.map = createWorldMap(); + this.apply(() => this.gameService.left('clientActions.left', reason)); + } + @Method({ binary: [BinNotificationId, BinEntityId, Bin.Str, Bin.Str, Bin.Str, Bin.U8] }) + addNotification(id: number, entityId: number, name: string, message: string, note: string, flags: NotificationFlags) { + const defaultCharacter = hasFlag(flags, NotificationFlags.Supporter) ? this.game.supporterPony : this.game.offlinePony; + const pony = (entityId && findPonyById(this.game.map, entityId)) || defaultCharacter; + + const filteredName = filterEntityName(this.game, name, hasFlag(flags, NotificationFlags.NameBad)); + message = message.replace(/#NAME#/g, nameToHTML(filteredName || '')); + + this.apply(() => addNotification(this.game, { id, message, note, pony, flags, open: false, fresh: true })); + } + @Method({ binary: [BinNotificationId] }) + removeNotification(id: number) { + this.apply(() => removeNotification(this.game, id)); + } + @Method({ binary: [BinEntityId, BinEntityId] }) + updateSelection(currentId: number, newId: number) { + if (isSelected(this.game, currentId)) { + this.game.select(newId ? findPonyById(this.game.map, newId) : undefined); + } + } + @Method({ binary: [[BinEntityId, Bin.U8]] }) + updateParty(party: [number, PartyFlags][] | undefined) { + const members = party && party.map(([id, flags]) => ({ + id, + pony: findPonyById(this.game.map, id) || this.game.fallbackPonies.get(id), + self: id === this.game.playerId, + leader: hasFlag(flags, PartyFlags.Leader), + pending: hasFlag(flags, PartyFlags.Pending), + offline: hasFlag(flags, PartyFlags.Offline), + })); + + if (members) { + const missing = members.filter(p => !p.pony).map(p => p.id); + + if (missing.length) { + this.game.send(server => server.getPonies(missing)); + } + } + + this.apply(() => { + this.game.party = updateParty(this.game.party, members); + this.game.onPartyUpdate.next(); + }); + } + @Method({ binary: [[BinEntityId, Bin.Obj, Bin.U8Array, Bin.U8Array, BinEntityPlayerState, Bin.Bool]] }) + updatePonies(ponies: PonyData[]) { + handleUpdatePonies(this.game, ponies); + } + @Method({ binary: [Bin.Obj, Bin.Bool] }) + updateFriends(friends: FriendStatusData[], removeMissing: boolean) { + handleUpdateFriends(this.game, friends, removeMissing); + } + @Method({ binary: [BinEntityId, Bin.Str, Bin.U32, Bin.Bool] }) + entityInfo(id: number, name: string, crc: number, nameBad: boolean) { + handleEntityInfo(this.game, id, name, crc, nameBad); + } + @Method({ binary: [Bin.Obj] }) + entityList(value: { name: string; x: number; y: number; }[]) { + if (DEVELOPMENT || BETA) { + const list = value.map(({ name, x, y }) => `${name}(${x.toFixed(2)}, ${y.toFixed(2)})`).join('\n'); + console.log(`ENTITIES:\n${list}`); + } + } + @Method({ binary: [Bin.Obj] }) + testPositions(data: { frame: number; x: number | undefined; y: number | undefined; moved: boolean; }[]) { + if (DEVELOPMENT) { + const round = (x: number) => Math.round(x * 100); + const same = (ax = 0, ay = 0, bx = 0, by = 0) => round(ax) === round(bx) && round(ay) === round(by); + const fmt = (x: number | undefined) => (x === undefined ? '-' : x.toFixed(2)).padStart(5); + + for (let i = 1; i < data.length; i++) { + if (data[i - 1].frame !== (data[i].frame - 1)) { + data.splice(i, 0, { frame: data[i - 1].frame + 1, x: undefined, y: undefined, moved: false }); + } + } + + const clientIndex = this.game.positions.findIndex(p => p.moved); + const serverIndex = data.findIndex(p => p.moved); + const offset = serverIndex - clientIndex; + + const dat = data.map((p, i) => { + const pt = this.game.positions[i - offset] || { x: undefined, y: undefined }; + return { frame: p.frame, ax: p.x, ay: p.y, bx: pt.x, by: pt.y, serverMoved: p.moved, clientMoved: pt.moved }; + }); + + const log = dat.map(({ frame, ax, ay, bx, by, serverMoved, clientMoved }, i) => + `${frame.toString().padStart(7)} | ` + + `${fmt(ax)}, ${fmt(ay)} ${serverMoved ? 'M' : ' '} | ` + + `${fmt(bx)}, ${fmt(by)} ${clientMoved ? 'M' : ' '} | ` + + `${same(ax, ay, bx, by) ? '= ' : ' '} ` + + `${i > 0 && dat[i - 1].frame !== (frame - 1) ? 'I ' : ' '}`) + .join('\n'); + + console.log( + ` frame | server | client | \n` + + `-----------------------------------------------\n` + + `${log}`); + } + } +} + +/* istanbul ignore next */ +if (DEVELOPMENT) { + getMethods(ClientActions) + .filter(m => !m.options.binary) + .forEach(m => console.error(`Missing binary encoding for ClientActions.${m.name}()`)); +} diff --git a/src/ts/client/clientAdminActions.ts b/src/ts/client/clientAdminActions.ts new file mode 100644 index 0000000..ada1ea8 --- /dev/null +++ b/src/ts/client/clientAdminActions.ts @@ -0,0 +1,34 @@ +import { Method } from 'ag-sockets/dist/browser'; +import { AdminModel } from '../components/services/adminModel'; +import { ModelTypes } from '../common/adminInterfaces'; +import { ModelSubscriber } from '../components/services/modelSubscriber'; + +export interface ClientUpdate { + type: ModelTypes; + id: string; + update: any; +} + +export class ClientAdminActions { + constructor(private model: AdminModel) { + } + connected() { + this.model.initialize(true); + this.model.connectedToSocket(); + } + disconnected() { + this.model.updateTitle(); + } + @Method() + updates(updates: ClientUpdate[]) { + for (const { type, id, update } of updates) { + const model = this.model[type] as ModelSubscriber; + + if (model) { + model.update(id, update); + } else { + console.error(`Invalid model type "${type}"`); + } + } + } +} diff --git a/src/ts/client/clientUtils.ts b/src/ts/client/clientUtils.ts new file mode 100644 index 0000000..db64d99 --- /dev/null +++ b/src/ts/client/clientUtils.ts @@ -0,0 +1,502 @@ +import { clamp } from 'lodash'; +import { + SocialSite, SocialSiteInfo, Eye, Muzzle, Iris, ExpressionExtra, Expression, ServerInfo, ServerFeatureFlags, + AccountData, AccountDataFlags +} from '../common/interfaces'; +import { + PLAYER_NAME_MAX_LENGTH, SAY_MAX_LENGTH, SAYS_TIME_MIN, SAYS_TIME_MAX, isChatlogRangeUnlimited, SUPPORTER_REWARDS, + PAST_SUPPORTER_REWARDS +} from '../common/constants'; +import { matcher, isSurrogate, fromSurrogate, isLowSurrogate } from '../common/stringUtils'; +import { oauthProviders } from './data'; +import { Subject } from '../../../node_modules/rxjs'; +import { PonyTownGame } from './game'; +import { toScreenX, toScreenY } from '../common/positionUtils'; +import { hasFlag } from '../common/utils'; + +export const matchCyrillic = /[\u0400-\u04FF]/g; +export const containsCyrillic = matcher(matchCyrillic); + +const otherValid = [ + '♂♀⚲⚥⚧☿♁⚨⚩⚦⚢⚣⚤', // gender symbols + '™®♥♦♣♠❥♡♢♤♧ღஐ·´°•◦✿❀◆◇◈◉◊。¥€«»,:■□—', // other + '〈〉「」『』【】《》♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼№●○◌★☆✰✦✧▪▫・', // other 2 + '\u1160\u3000\u3164', // spaces (replaced later) +].join('').split('').reduce((set, c) => (set.add(c.charCodeAt(0)), set), new Set()); + +export function isValid(c: number): boolean { + return (c >= 0x0020 && c <= 0x007e) // latin + || (c >= 0x00a0 && c <= 0x00ff) // latin 1 supplement + || (c >= 0x0100 && c <= 0x017F) // Latin Extended-A + || (c >= 0x0180 && c <= 0x024F) // Latin Extended-B + || (c >= 0x1e00 && c <= 0x1eff) // Latin Extended Additional + || (c >= 0x0370 && c <= 0x03FF) // Greek and Coptic + || (c >= 0x0400 && c <= 0x0481) || (c >= 0x048A && c <= 0x04FF) // cyrillic + || (c >= 0x3041 && c <= 0x3096) // hiragana + || (c >= 0x30A0 && c <= 0x30FF) // hatakana + || (c >= 0x3400 && c <= 0x4DB5) || (c >= 0x4E00 && c <= 0x9FCB) || (c >= 0xF900 && c <= 0xFA6A) // kanji + || (c >= 0x2F00 && c <= 0x2FDF) // Kangxi Radicals + || (c >= 0x3000 && c <= 0x302D) // CJK Symbols and Punctuation + || (c >= 0x1D00 && c <= 0x1D7F) // Phonetic Extensions + || (c >= 0x0250 && c <= 0x02AF) // IPA Extensions + || (c >= 0xA720 && c <= 0xA7FF) // Latin Extended-D + || (c >= 0x0E00 && c <= 0x0E7F) // Thai + || (c >= 0xff01 && c <= 0xff5e) // Romaji (replaced later) + || (c >= 0x2200 && c <= 0x22FF) // Mathematical Operators + || (c >= 0x25A0 && c <= 0x25FF) // Geometric Shapes + || (c >= 0x2600 && c <= 0x26ff) || (c >= 0x2700 && c <= 0x27bf) || (c >= 0x2b00 && c <= 0x2bef) // emoji + || (c >= 0x1f600 && c <= 0x1f64f) || (c >= 0x1f680 && c <= 0x1f6f6) || (c >= 0x1f300 && c <= 0x1f5ff) // emoji + || (c >= 0x231a && c <= 0x231b) || (c >= 0x23e9 && c <= 0x23fa) // emoji + || (c >= 0x1f900 && c <= 0x1f9ff) // Supplemental Symbols and Pictographs + || otherValid.has(c) // other symbols + ; +} + +export function isValid2(c: number): boolean { + return (c >= 0x2b0 && c <= 0x2ff) // Spacing Modifier Letters + || (c >= 0x531 && c <= 0x556) || (c >= 0x559 && c <= 0x55f) || (c >= 0x561 && c <= 0x587) + || (c >= 0x589 && c <= 0x58a) || (c >= 0x58c && c <= 0x58f) // Armenian + || (c >= 0x591 && c <= 0x5c7) || (c >= 0x5d0 && c <= 0x5ea) || (c >= 0x5f0 && c <= 0x5f4) // Hebrew + || (c >= 0x600 && c <= 0x6ff) // Arabic + || (c >= 0x7c0 && c <= 0x7fa) // NKo + || (c >= 0x900 && c <= 0x97f) // Devanagari + || (c === 0xb90) || (c === 0xb9c) // Tamil + || (c >= 0xc85 && c <= 0xc8c) || (c >= 0xc8e && c <= 0xc90) || (c >= 0xc91 && c <= 0xca8) + || (c >= 0xcaa && c <= 0xcb3) || (c >= 0xcb5 && c <= 0xcb9) || (c >= 0xce6 && c <= 0xcef) // Kannada + || (c >= 0x10a0 && c <= 0x10c5) || (c === 0x10c7) || (c === 0x10cd) || (c >= 0x10d0 && c <= 0x10ff) // Georgian + || (c >= 0x1100 && c <= 0x11ff) || (c >= 0x3130 && c <= 0x318f) || (c >= 0xac00 && c <= 0xd7af) // Hangul + || (c >= 0x1400 && c <= 0x167f) // Unified Canadian Aboriginal Syllabics + || (c >= 0x2010 && c <= 0x2027) || (c >= 0x2030 && c <= 0x205e) // General Punctuation + || (c >= 0x20a0 && c <= 0x20bf) // Currency Symbols + || (c >= 0x2100 && c <= 0x214f) // Letterlike Symbols + || (c >= 0x2150 && c <= 0x218b) // Number Forms + || (c >= 0x2300 && c <= 0x239a) || (c >= 0x23b4 && c <= 0x23fa) // Miscellaneous Technical + || (c >= 0x2500 && c <= 0x257f) // Box Drawing + || (c >= 0x2800 && c <= 0x28ff) // Braille Patterns + || (c >= 0x3000 && c <= 0x303f) // CJK Symbols and Punctuation + || (c >= 0x3105 && c <= 0x312d) // Bopomofo + || (c >= 0xfe30 && c <= 0xfe4f) // CJK Compatibility Forms + || (c >= 0xff01 && c <= 0xffef) // Halfwidth and Fullwidth Forms + // || (c >= 0x1f170 && c < 0x1f189) // Enclosed Alphanumeric Supplement [a-z] + || (c >= 0x1f000 && c <= 0x1f02b) // Mahjong Tiles + || (c >= 0x1f0a0 && c <= 0x1f0ae) || (c >= 0x1f0b1 && c <= 0x1f0bf) || (c >= 0x1f0c1 && c <= 0x1f0cf) + || (c >= 0x1f0d1 && c <= 0x1f0df) || (c >= 0x1f0e0 && c <= 0x1f0f5) // Playing Cards + || (c >= 0x1f1e6 && c <= 0x1f1ff) // Enclosed Alphanumeric Supplement (regional indicators) + ; +} + +function isInvalid(c: number): boolean { + return c === 0x1f595 // middle finger emoji + || c === 0x00ad // soft hyphen + ; +} + +function isValidForName(c: number): boolean { + return isValid(c) && !isInvalid(c); +} + +function isValidForMessage(c: number): boolean { + return (isValid(c) || isValid2(c)) && !isInvalid(c); +} + +export const matchRomaji = /[\uff01-\uff5e]/g; + +const matchOtherWhitespace = /[\u1160\u2800\u3000\u3164\uffa0]+/g; + +export function replaceRomaji(match: string): string { + return String.fromCharCode(match.charCodeAt(0) - 0xfee0); +} + +export function cleanName(name: string | undefined): string { + return filterString(name, isValidForName) + .replace(matchOtherWhitespace, ' ') // whitespace characters + .replace(/\s+/g, ' ') + .replace(matchRomaji, replaceRomaji) + .trim(); +} + +export function cleanMessage(text: string | undefined): string { + return filterString(text, isValidForMessage) + .replace(matchOtherWhitespace, ' ') // whitespace characters + .replace(/[\r\n]/g, '') + .replace(matchRomaji, replaceRomaji) + .trim() + .substr(0, SAY_MAX_LENGTH); +} + +export function filterString(value: string | undefined, filter: (code: number) => boolean): string { + value = value || ''; + + for (let i = 0; i < value.length; i++) { + let code = value.charCodeAt(i); + let size = 1; + let invalidSurrogate = false; + + if (isSurrogate(code) && (i + 1) < value.length) { + const extra = value.charCodeAt(i + 1); + + if (isLowSurrogate(extra)) { + code = fromSurrogate(code, extra); + i++; + size++; + } else { + invalidSurrogate = true; + } + } + + if (invalidSurrogate || !filter(code)) { + i -= size; + value = value.substr(0, i + 1) + value.substr(i + size + 1); + } + } + + return value; +} + +export function validatePonyName(name: string | undefined): boolean { + return !!name && !!name.length && name.length <= PLAYER_NAME_MAX_LENGTH && !/^[.,_-]+$/.test(name); +} + +export function toSocialSiteInfo({ id, name, url, provider }: SocialSite): SocialSiteInfo { + const oauth = oauthProviders.find(p => p.id === provider); + + return { + id, + name, + url, + icon: oauth && oauth.id, + color: oauth && oauth.color, + }; +} + +function isMultipleMatch(message: string, last: string): boolean { + const minMessageLength = 4; + + if (message.length >= minMessageLength && last.length >= minMessageLength) { + let current = last; + + while (current.length < message.length) { + current += last; + } + + return message === current.substr(0, SAY_MAX_LENGTH); + } else { + return false; + } +} + +function checkTrailing(message: string, last: string) { + return message.indexOf(last) === 0 && (message.length - last.length) < 3; +} + +function isTrailingMatch(message: string, last: string) { + const minMessageLength = 5; + + if (message.length > last.length && last.length > minMessageLength) { + return checkTrailing(message, last); + } else if (message.length < last.length && message.length > minMessageLength) { + return checkTrailing(last, message); + } else { + return false; + } +} + +export function isSpamMessage(message: string, lastMessages: string[]): boolean { + if (!/^\//.test(message) && lastMessages.length) { + return lastMessages.some(last => message === last || isMultipleMatch(message, last) || isTrailingMatch(message, last)); + } else { + return false; + } +} + +export function getSaysTime(message: string): number { + return SAYS_TIME_MIN + clamp(message.length / SAY_MAX_LENGTH, 0, 1) * (SAYS_TIME_MAX - SAYS_TIME_MIN); +} + +export function createExpression( + right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None +): Expression { + return { right, left, muzzle, rightIris, leftIris, extra }; +} + +export const isAndroidBrowser = (() => { + const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; + + // Android browser + // Mozilla/5.0 (Linux; U; Android 4.4.2; es-ar; LG-D375AR Build/KOT49I) + // AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36 + if (/Android /.test(ua) && /AppleWebKit/.test(ua) && (!/chrome/i.test(ua) || /Chrome\/30\./.test(ua))) { + return true; + } + + return false; +})(); + +/* istanbul ignore next */ +export const isBrowserOutdated = (() => { + const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; + + // Safari <= 8 + // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) + // AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25 + const safari = /Version\/(\d+)\.[0-9.]+ Safari/.exec(ua); + + if (safari && parseInt(safari[1], 10) <= 8) { + return true; + } + + // Android browser + // Mozilla/5.0 (Linux; U; Android 4.4.2; es-ar; LG-D375AR Build/KOT49I) + // AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36 + if (isAndroidBrowser) { + return true; + } + + if (!supportsLetAndConst()) { + return true; + } + + return false; +})(); + +export function getLocale() { + return (navigator.languages ? navigator.languages[0] : navigator.language) || 'en-US'; +} + +/* istanbul ignore next */ +export function isLanguage(lang: string) { + const languages = navigator.languages || [navigator.language]; + return languages.some(l => l === lang); +} + +/* istanbul ignore next */ +export function sortServersForRussian(a: ServerInfo, b: ServerInfo) { + if (a.flag === 'ru' && a.flag !== b.flag) { + return -1; + } + + if (b.flag === 'ru' && a.flag !== b.flag) { + return 1; + } + + return a.id.localeCompare(b.id); +} + +export function readFileAsText(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e: any) => resolve(e.target && e.target.result || ''); + reader.onerror = () => reject(new Error('Failed to read file')); + reader.readAsText(file); + }); +} + +/* istanbul ignore next */ +export function isFileSaverSupported() { + try { + return !!new Blob; + } catch { + return false; + } +} + +export let isInIncognitoMode = false; + +export function setIsIncognitoMode(value: boolean) { + isInIncognitoMode = value; +} + +/* istanbul ignore next */ +function checkIncognitoMode(wnd: any) { + if (!wnd || !wnd.chrome) + return; + + const fs = wnd.RequestFileSystem || wnd.webkitRequestFileSystem; + + if (!fs) + return; + + fs(wnd.TEMPORARY, 100, () => { }, () => isInIncognitoMode = true); +} + +let focused = true; + +/* istanbul ignore next */ +export function isFocused() { + return focused; +} + +/* istanbul ignore next */ +if (typeof window !== 'undefined') { + checkIncognitoMode(window); + window.addEventListener('focus', () => focused = true); + window.addEventListener('blur', () => focused = false); +} + +/* istanbul ignore next */ +export function isStandalone() { + return !!window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone === true; // safari +} + +/* istanbul ignore next */ +export function supportsLetAndConst() { + try { + return (new Function('let x = true; return x;'))(); + } catch { + return false; + } +} + +/* istanbul ignore next */ +export function registerServiceWorker(url: string, onUpdate: () => void) { + try { + if ('serviceWorker' in navigator && typeof navigator.serviceWorker.register === 'function') { + let hadWorker = false; + + navigator.serviceWorker.register(url) + .then(worker => { + hadWorker = !!worker.active; + + worker.addEventListener('updatefound', () => { + if (hadWorker) { + onUpdate(); + } + }); + }); + + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (hadWorker) { + location.reload(); + } + }); + } + } catch (e) { + console.error(e); + } +} + +/* istanbul ignore next */ +export function unregisterServiceWorker() { + if ('serviceWorker' in navigator && typeof navigator.serviceWorker.getRegistrations === 'function') { + return navigator.serviceWorker.getRegistrations() + .then(registrations => { + for (const registration of registrations) { + registration.unregister(); + } + }); + } else { + return Promise.resolve(); + } +} + +/* istanbul ignore next */ +export function attachDebugMethod(name: string, method: any) { + if (typeof window !== 'undefined') { + (window as any)[name] = method; + } +} + +/* istanbul ignore next */ +export function updateRangeIndicator(range: number | undefined, { player, scale, camera }: PonyTownGame) { + const e = document.getElementById('range-indicator')!; + + if (player && !isChatlogRangeUnlimited(range)) { + const x = (toScreenX(player.x) - camera.x) * scale; + const y = (toScreenY(player.y) - camera.actualY) * scale; + const w = toScreenX(range!) * scale * 2; + const h = toScreenY(range!) * scale * 2; + e.style.width = `${w}px`; + e.style.height = `${h}px`; + e.style.left = `${-w / 2}px`; + e.style.top = `${-h / 2}px`; + e.style.transform = `translate3d(${x}px, ${y}px, 0)`; + e.style.display = 'block'; + } else { + e.style.display = 'none'; + } +} + +/* istanbul ignore next */ +export function checkIframeKey(iframeId: string, expectedKey: string) { + try { + const iframe = document.getElementById(iframeId) as HTMLIFrameElement; + const doc = iframe && iframe.contentWindow && iframe.contentWindow.document; + const key = doc && doc.body && doc.body.getAttribute('data-key'); + return key === expectedKey; + } catch (e) { + if (DEVELOPMENT) { + console.error(e); + } + + return false; + } +} + +let flags: ServerFeatureFlags = {}; + +export const featureFlagsChanged = new Subject(); + +export function initFeatureFlags(newFlags: ServerFeatureFlags) { + flags = newFlags; + featureFlagsChanged.next(newFlags); +} + +export function hasFeatureFlag(flag: keyof ServerFeatureFlags) { + return !!flags[flag]; +} + +export function hardReload() { + unregisterServiceWorker() + .then(() => location.reload(true)); +} + +const LOGGING = false; + +let logger = (_: string) => { }; + +export function initLogger(newLogger: (message: string) => void) { + if (LOGGING) { + logger = newLogger; + } +} + +export function log(message: string) { + if (LOGGING) { + logger(message); + } +} + +export function isSupporterOrPastSupporter(account: AccountData | undefined) { + return !!account && (!!account.supporter || hasFlag(account.flags, AccountDataFlags.PastSupporter)); +} + +export function supporterTitle(account: AccountData | undefined) { + if (account && account.supporter) { + return `Supporter Tier ${account.supporter}`; + } else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) { + return 'Past supporter'; + } else { + return ''; + } +} + +export function supporterClass(account: AccountData | undefined) { + if (account && account.supporter) { + return `supporter-${account.supporter}`; + } else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) { + return 'supporter-past'; + } else { + return 'd-none'; + } +} + +export function supporterRewards(account: AccountData | undefined) { + if (account && account.supporter) { + return SUPPORTER_REWARDS[account.supporter]; + } else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) { + return PAST_SUPPORTER_REWARDS; + } else { + return SUPPORTER_REWARDS[0]; + } +} diff --git a/src/ts/client/credits.ts b/src/ts/client/credits.ts new file mode 100644 index 0000000..312dfd7 --- /dev/null +++ b/src/ts/client/credits.ts @@ -0,0 +1,75 @@ +export interface Credit { + name: string; + title: string; + avatarIndex: number; + links: string[]; +} + +export interface Contributor { + name: string; + links?: string[]; +} + +export interface Contributors { + group: string; + contributors: Contributor[]; +} + +export const CREDITS: Credit[] = [ + // example: + // { + // name: 'Your name', + // title: 'Your role on the team', + // avatarIndex: 0, // place of the avatar in /assets/images/avatars.jpg + // links: ['https://twitter.com/your_twitter_handle'], + // }, +]; + +export const CONTRIBUTORS: Contributors[] = [ + { + group: 'Artists & Animators', + contributors: [ + { name: 'Shino', links: ['https://www.deviantart.com/shinodage'] }, + { name: 'ChiraChan', links: ['https://www.deviantart.com/chiramii-chan', 'https://chirachan-art.tumblr.com/'] }, + { name: 'Goodly', links: ['https://www.deviantart.com/goodlyay'] }, + { name: 'TioRafaJP', links: ['https://www.deviantart.com/tiorafajp', 'https://www.youtube.com/user/RafaelJP2'] }, + { name: 'ShareMyShipment', links: ['https://www.deviantart.com/sharemyshipment'] }, + { name: 'Velenor', links: ['https://www.deviantart.com/velenor'] }, + { name: 'OtakuAP', links: ['https://www.deviantart.com/otakuap'] }, + ], + }, + { + group: 'Programmers & Artists', + contributors: [ + { name: 'CyberPon3', links: ['https://www.deviantart.com/cyberpon3'] }, + ], + }, + { + group: 'Artists', + contributors: [ + { name: 'Disastral' }, + { name: 'Meno', links: ['https://www.deviantart.com/menojar'] }, + { name: 'Paulpeoples', links: ['https://www.deviantart.com/paulpeopless'] }, + { name: 'Velvet-Frost', links: ['https://www.deviantart.com/velvet-frost'] }, + { name: 'Jet7Wave', links: ['https://www.deviantart.com/jetwave'] }, + { name: 'Lalieri', links: ['https://lalieri.tumblr.com/'] }, + { name: 'Ruef-bae', links: ['https://www.deviantart.com/ruef-bae'] }, + { name: 'Alchemist3rd' }, + { name: 'Firecracker' }, + { name: 'ZippySqrl', links: ['https://www.deviantart.com/zippysqrl'] }, + { name: 'Karnel333' }, + { name: 'Wellfugzee' }, + { name: 'ScribblesHeart', links: ['https://www.deviantart.com/scribblesdesu'] }, + { name: 'dsp2003', links: ['https://dsp2003.tumblr.com/', 'http://www.deviantart.com/dsp2003'] }, + { name: 'MysticBlare', links: ['https://twitter.com/MysticBlare'] }, + { name: 'Towmacow Waffles', links: ['https://www.deviantart.com/towmacowwaffles'] }, + { name: 'OrchidPony', links: ['https://www.deviantart.com/orchidpony'] }, + { name: 'Cherry Cerise', links: ['https://www.deviantart.com/cherryceriseart'] }, + { name: 'Radio' }, + { name: 'Ultimate Fluff' }, + { name: 'SC', links: ['https://0somecunt0.tumblr.com/tagged/sfw'] }, + { name: 'SailorDolpin', links: ['https://vk.com/id324582699'] }, + { name: 'Deeraw', links: ['https://www.deviantart.com/deerdraw', 'https://twitter.com/TheOnlyDeeraw'] }, + ], + }, +]; diff --git a/src/ts/client/data.ts b/src/ts/client/data.ts new file mode 100644 index 0000000..bbc750e --- /dev/null +++ b/src/ts/client/data.ts @@ -0,0 +1,73 @@ +import { toByteArray } from 'base64-js'; +import { ClientOptions, createBinaryReader, readObject } from 'ag-sockets/dist/browser'; +import { OAuthProvider } from '../common/interfaces'; + +/* istanbul ignore next */ +function attr(name: string): string | undefined { + return typeof document !== 'undefined' ? (document.body.getAttribute(name) || undefined) : undefined; +} + +/* istanbul ignore next */ +function data(id: string): string | undefined { + const element = typeof document !== 'undefined' ? document.getElementById(id) : undefined; + return element ? element.innerHTML : undefined; +} + +function json(id: string, def: string): T { + return JSON.parse(data(id) || def); +} + +export let isMobile = false; +export const sw = attr('data-sw') === 'true'; +export const host = attr('data-host')!; +export const local = attr('data-local') === 'true'; +export const token = attr('data-token'); +export const version = attr('data-version'); +export const isPublic = attr('data-public') === 'true'; +export const supporterLink = attr('data-supporter-link'); +export const twitterLink = attr('data-twitter-link'); +export const contactEmail = attr('data-email'); +export const copyrightName = attr('data-copyright'); + +/* istanbul ignore next */ +export const oauthProviders = json('oauth-providers', '[]') + .map(a => { ...a, url: `/auth/${a.id}` }); +/* istanbul ignore next */ +export const signUpProviders = oauthProviders.filter(i => !i.connectOnly); +/* istanbul ignore next */ +export const signInProviders = oauthProviders.filter(i => i.connectOnly); + +/* istanbul ignore next */ +export function socketOptions(): ClientOptions { + const options = data('socket-options'); + + if (options) { + const buffer = toByteArray(options); + const reader = createBinaryReader(buffer); + return readObject(reader); + } else { + throw new Error('Missing socket options'); + } +} + +/* istanbul ignore next */ +function setMobile() { + isMobile = true; + window.removeEventListener('touchstart', setMobile); + document.body.classList.add('is-mobile'); +} + +/* istanbul ignore next */ +if (typeof window !== 'undefined') { + if (!/windows/i.test(navigator.userAgent)) { + window.addEventListener('touchstart', setMobile); + } + + if (/Trident/.test(navigator.userAgent)) { + document.body.classList.add('is-msie'); + } + + if (/YaBrowser/.test(navigator.userAgent)) { + document.body.classList.add('is-yandex'); + } +} diff --git a/src/ts/client/draw.ts b/src/ts/client/draw.ts new file mode 100644 index 0000000..76b17e5 --- /dev/null +++ b/src/ts/client/draw.ts @@ -0,0 +1,297 @@ +import { + Entity, DrawOptions, Camera, PaletteSpriteBatch, SpriteBatch, TileSets, Engine, Pony, WorldMap, EntityState +} from '../common/interfaces'; +import { isBoundsVisible } from '../common/camera'; +import { drawBounds, drawPixelText, drawBoundsOutline, drawOutlineRect, drawWorldBounds } from '../graphics/graphicsUtils'; +import { ORANGE, BLUE, PURPLE, BLACK, RED, WHITE, CYAN, HOTPINK, GRAY } from '../common/colors'; +import { toScreenX, toScreenY, toWorldX, toWorldY } from '../common/positionUtils'; +import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from '../common/constants'; +import { forEachRegion, getAnyBounds, getRegion, isInWaterAt } from '../common/worldMap'; +import { drawPonyEntity, drawPonyEntityLight, drawPonyEntityLightSprite } from '../common/pony'; +import { getInteractBounds, sortEntities, isHidden, getSitOnBounds } from '../common/entityUtils'; +import { drawTiles, drawTilesNew, drawTilesDebugInfo } from './tileUtils'; +import { withAlphaFloat } from '../common/color'; +import { timeStart, timeEnd } from './timing'; + +const SELECTED_ENTITY_BOUNDS = withAlphaFloat(ORANGE, 0.5); + +function drawEntities(batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) { + const drawHidden = options.drawHidden; + let entitiesDrawn = 0; + + for (const entity of entities) { + if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.bounds, entity.x, entity.y)) { + if (entity.type === PONY_TYPE) { + drawPonyEntity(batch, entity as Pony, options); + entitiesDrawn++; + } else if (entity.draw !== undefined) { + entity.draw(batch, options); + entitiesDrawn++; + } + } else { + if (entity.type === PONY_TYPE) { + const pony = entity as Pony; + + if (pony.batch !== undefined) { + batch.releaseBatch(pony.batch); + pony.batch = undefined; + } + } + } + } + + return entitiesDrawn; +} + +export function drawEntityLights(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) { + const drawHidden = options.drawHidden; + + for (const entity of entities) { + if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLight)) { + console.error('Cannot draw entity light', entity); + } + + if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.lightBounds, entity.x, entity.y)) { + if (entity.type === PONY_TYPE) { + drawPonyEntityLight(batch, entity as Pony, options); + } else { + entity.drawLight!(batch, options); + } + } + } +} + +export function drawEntityLightSprites(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) { + const drawHidden = options.drawHidden; + + for (const entity of entities) { + if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLightSprite)) { + console.error('Cannot draw entity light sprite', entity); + } + + if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.lightSpriteBounds, entity.x, entity.y)) { + if (entity.type === PONY_TYPE) { + drawPonyEntityLightSprite(batch, entity as Pony, options); + } else { + entity.drawLightSprite!(batch, options); + } + } + } +} + +export function hasDrawLight(entity: Entity) { + if (entity.type === PONY_TYPE) { + const pony = entity as Pony; + return (pony.ponyState.holding !== undefined && pony.ponyState.holding.drawLight !== undefined) || + ((pony.state & EntityState.Magic) !== 0); + } else { + return entity.drawLight !== undefined; + } +} + +export function hasLightSprite(entity: Entity) { + if (entity.type === PONY_TYPE) { + const pony = entity as Pony; + return (pony.ponyState.holding !== undefined && pony.ponyState.holding.drawLightSprite !== undefined); + } else { + return entity.drawLightSprite !== undefined; + } +} + +export function drawMap( + batch: PaletteSpriteBatch, map: WorldMap, camera: Camera, player: Pony, options: DrawOptions, + tileSets: TileSets, selectedEntities: Entity[], +) { + TIMING && timeStart('forEachRegion'); + if (BETA && options.engine === Engine.Whiteness) { + batch.drawRect(WHITE, 0, 0, toScreenX(map.width), toScreenY(map.height)); + } else if (BETA && options.engine === Engine.LayeredTiles) { + forEachRegion(map, region => drawTilesNew(batch, region, camera, map, tileSets, options)); + } else { + forEachRegion(map, region => drawTiles(batch, region, camera, map, tileSets, options)); + } + TIMING && timeEnd(); + + TIMING && timeStart('sortEntities'); + sortEntities(map.entitiesDrawable); + TIMING && timeEnd(); + + TIMING && timeStart('drawEntities'); + const entitiesDrawn = drawEntities(batch, map.entitiesDrawable, camera, options); + TIMING && timeEnd(); + + if (BETA || TOOLS) { + forEachRegion(map, region => drawTilesDebugInfo(batch, region, camera, options)); + } + + if (BETA && options.debug.showHelpers) { + drawDebugHelpers(batch, map.entities, options); + } + + if (BETA) { + for (const entity of selectedEntities) { + const bounds = getAnyBounds(entity); + drawBoundsOutline(batch, entity, bounds, SELECTED_ENTITY_BOUNDS, 2); + } + } + + if (BETA && options.debug.showHelpers) { + drawOutlineRect(batch, PURPLE, getInteractBounds(player)); + drawOutlineRect(batch, 0xff000066, getSitOnBounds(player)); + } + + if (BETA && options.showColliderMap) { + drawDebugCollider(batch, map, camera); + batch.drawRect(PURPLE, toScreenX(player.x) - 1, toScreenY(player.y), 3, 1); + batch.drawRect(PURPLE, toScreenX(player.x), toScreenY(player.y) - 1, 1, 3); + } + + if (BETA && options.showHeightmap) { + drawDebugInWater(batch, map, camera); + } + + return entitiesDrawn; +} + +// debug + +function drawDebugHelpers(batch: PaletteSpriteBatch, entities: Entity[], options: DrawOptions) { + const textColor = 0x000000b2; + const show = options.debug; + + for (const e of entities) { + batch.globalAlpha = 0.3; + show.bounds && drawBounds(batch, e, e.bounds, ORANGE); + show.cover && drawBounds(batch, e, e.coverBounds, BLUE); + show.interact && drawBounds(batch, e, e.interactBounds, PURPLE); + show.trigger && drawWorldBounds(batch, e, e.triggerBounds, CYAN); + + if (show.collider) { + batch.globalAlpha = 0.5; + + const x = Math.floor(e.x * tileWidth); + const y = Math.floor(e.y * tileHeight); + + if (e.colliders !== undefined) { + for (const collider of e.colliders) { + const x1 = x + collider.x; + const x2 = x + collider.x + collider.w; + const y1 = y + collider.y; + const y2 = y + collider.y + collider.h; + batch.drawRect(collider.tall ? RED : HOTPINK, x1, y1, x2 - x1, y2 - y1); + } + } + } + + batch.globalAlpha = 1; + batch.drawRect(BLACK, toScreenX(e.x), toScreenY(e.y), 1, 1); // anchor + + if (show.id) { + drawPixelText(batch, toScreenX(e.x) + 2, toScreenY(e.y) + 2, textColor, e.id.toFixed()); + } + } +} + +function drawDebugInWater(batch: PaletteSpriteBatch, map: WorldMap, camera: Camera) { + const color = withAlphaFloat(ORANGE, 0.4); + + forEachRegion(map, region => { + const sx = toScreenX(region.x * REGION_SIZE); + const sy = toScreenY(region.y * REGION_SIZE); + const w = REGION_WIDTH; + const h = REGION_HEIGHT; + + const cameraLeft = camera.x; + const cameraRight = camera.x + camera.w; + const cameraTop = camera.actualY; + const cameraBottom = camera.actualY + camera.h; + + if (sx > cameraRight || sy > cameraBottom || (sx + w) < cameraLeft || (sy + h) < cameraTop) + return; + + for (let y = 0; y < h; y++) { + if ((sy + y + 1) < cameraTop || (sy + y) > cameraBottom) + continue; + + for (let x = 0; x < w; x++) { + if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight) + continue; + + const tx = x; + + while (isInWaterAt(map, toWorldX(sx + x + 0.5), toWorldY(sy + y + 0.5)) && x < w) { + x++; + } + + if (x > tx) { + batch.drawRect(color, sx + tx, sy + y, x - tx, 1); + } + } + } + }); +} + +function drawDebugCollider(batch: PaletteSpriteBatch, map: WorldMap, camera: Camera) { + const color = withAlphaFloat(PURPLE, 0.4); + + forEachRegion(map, ({ x, y, collider }) => { + const sx = toScreenX(x * REGION_SIZE); + const sy = toScreenY(y * REGION_SIZE); + const w = REGION_WIDTH; + const h = REGION_HEIGHT; + + const cameraLeft = camera.x; + const cameraRight = camera.x + camera.w; + const cameraTop = camera.actualY; + const cameraBottom = camera.actualY + camera.h; + + if (sx > cameraRight || sy > cameraBottom || (sx + w) < cameraLeft || (sy + h) < cameraTop) + return; + + for (let y = 0; y < h; y++) { + if ((sy + y + 1) < cameraTop || (sy + y) > cameraBottom) + continue; + + for (let x = 0; x < w; x++) { + if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight) + continue; + + const tx = x; + + while (collider[x + y * w] !== 0 && x < w) { + x++; + } + + if (x > tx) { + batch.drawRect(color, sx + tx, sy + y, x - tx, 1); + } + } + } + }); +} + +export function drawDebugRegions(batch: SpriteBatch, map: WorldMap, player: Pony, { w, h }: Camera) { + const rw = 10; + const rh = 8; + const width = rw * map.regionsX; + const height = rh * map.regionsY; + const x = w - width - 10; + const y = h - height - 30; + + for (let i = 0; i < map.regionsY; i++) { + for (let j = 0; j < map.regionsX; j++) { + if (getRegion(map, j, i)) { + const inside = j === Math.floor(player.x / REGION_SIZE) && i === Math.floor(player.y / REGION_SIZE); + batch.drawRect(inside ? ORANGE : RED, x + rw * j, y + rh * i, rw, rh); + } + } + } + + for (let i = 0; i <= map.regionsY; i++) { + batch.drawRect(GRAY, x, y + rh * i, width + 1, 1); + } + + for (let i = 0; i <= map.regionsX; i++) { + batch.drawRect(GRAY, x + rw * i, y, 1, height); + } +} diff --git a/src/ts/client/emoji.ts b/src/ts/client/emoji.ts new file mode 100644 index 0000000..3cc9c8f --- /dev/null +++ b/src/ts/client/emoji.ts @@ -0,0 +1,194 @@ +import { escape } from 'lodash'; +import { Sprite } from '../common/interfaces'; +import { canvasToSource } from './canvasUtils'; +import { drawCanvas } from '../graphics/contextSpriteBatch'; +import { WHITE } from '../common/colors'; +import { normalSpriteSheet } from '../generated/sprites'; +import { includes } from '../common/utils'; + +export interface Emoji { + names: string[]; + symbol: string; +} + +export const emojis: Emoji[] = [ + // faces + ['🙂', 'face', 'tiny', 'tinyface', 'slight_smile'], + ['😵', 'derp', 'dizzy_face'], + ['😠', 'angry'], + ['😐', 'neutral', 'neutral_face'], + ['😑', 'expressionless'], + ['😆', 'laughing'], + ['😍', 'heart_eyes'], + ['😟', 'worried'], + ['🤔', 'thinking'], + ['🙃', 'upside_down'], + ['😈', 'evil', 'smiling_imp'], + ['👿', 'imp', 'angry_evil'], + ['👃', 'nose', 'c'], + + // cat faces + ['🐱', 'cat'], + ['😺', 'smiley_cat'], + ['😸', 'smile_cat'], + ['😹', 'joy_cat'], + ['😻', 'heart_eyes_cat'], + ['😼', 'smirk_cat'], + ['😽', 'kissing_cat'], + ['🙀', 'scream_cat'], + ['😿', 'cryingcat', 'crying_cat_face'], + ['😾', 'pouting_cat'], + + // hearts + ['❤', 'heart'], + ['💙', 'blue_heart', 'meno'], + ['💚', 'green_heart', 'chira'], + ['💛', 'yellow_heart'], + ['💜', 'purple_heart'], + ['🖤', 'black_heart', 'shino'], + ['💔', 'broken_heart'], + ['💖', 'sparkling_heart'], + ['💗', 'heartpulse'], + ['💕', 'two_hearts'], + + // food / objects + ['🥌', 'rock', 'stone'], + ['🍕', 'pizza'], + ['🍎', 'apple'], + ['🍏', 'gapple', 'green_apple'], + ['🍊', 'orange', 'tangerine'], + ['🍐', 'pear'], + ['🥭', 'mango'], + ['🥕', 'carrot'], + ['🍇', 'grapes'], + ['🍌', 'banana'], + ['⛏', 'pick'], + ['🥚', 'egg'], + ['💮', 'flower', 'white_flower'], + ['🌸', 'cherry_blossom'], + ['🍬', 'candy'], + ['🍡', 'candy_cane'], + ['🍭', 'lollipop'], + ['⭐', 'star'], + ['🌟', 'star2'], + ['🌠', 'shooting_star'], + ['⚡', 'zap'], + ['❄', 'snow', 'snowflake'], + ['⛄', 'snowpony', 'snowman'], + ['🏀', 'pumpkin'], + ['🎃', 'jacko', 'jack_o_lantern'], + ['🌲', 'evergreen_tree', 'pinetree'], + ['🎄', 'christmas_tree'], + ['🕯', 'candle'], + ['🎅', 'santa_hat', 'santa_claus'], + ['💐', 'holly'], + ['🌿', 'mistletoe'], + ['🎲', 'die', 'dice', 'game_die'], + ['✨', 'sparkles'], + ['🎁', 'gift', 'present'], + ['🔥', 'fire'], + ['🎵', 'musical_note'], + ['🎶', 'notes'], + ['🌈', 'rainbow'], + ['🐾', 'feet', 'paw', 'paws'], + ['👑', 'crown'], + ['💎', 'gem'], + ['☘', 'shamrock', 'clover'], + ['🍀', 'four_leaf_clover'], + ['🍪', 'cookie'], + + // animals + ['🦋', 'butterfly'], + ['🦇', 'bat'], + ['🕷', 'spider'], + ['👻', 'ghost'], + ['🐈', 'cat2'], + + // other + ['™', 'tm'], + ['♂', 'male'], + ['♀', 'female'], + ['⚧', 'trans', 'transgender'], +].map(createEmoji); + +export const emojiMap = new Map(); +export const emojiNames = emojis.slice().sort().map(e => `:${e.names[0]}:`); +emojis.forEach(e => e.names.forEach(name => emojiMap.set(`:${name}:`, e.symbol))); + +export function findEmoji(name: string): Emoji | undefined { + return emojis.find(e => name === e.symbol || includes(e.names, name)); +} + +export function replaceEmojis(text: string | undefined): string { + return (text || '').replace(/:[a-z0-9_]+:/ig, match => emojiMap.get(match) || match); +} + +function createEmoji([symbol, ...names]: string[]): Emoji { + return { symbol, names: [...names, ...names.filter(n => /_/.test(n)).map(n => n.replace(/_/g, ''))] }; +} + +const emojiImages = new Map(); +const emojiImagePromises = new Map>(); + +export function getEmojiImageAsync(sprite: Sprite, callback: (str: string) => void) { + const src = emojiImages.get(sprite); + + if (src) { + callback(src); + return; + } + + const promise = emojiImagePromises.get(sprite); + + if (promise) { + promise.then(callback); + return; + } + + const width = sprite.w + sprite.ox; + // const height = sprite.h + sprite.oy; + const canvas = drawCanvas(width, 10, normalSpriteSheet, undefined, batch => batch.drawSprite(sprite, WHITE, 0, 0)); + const newPromise = canvasToSource(canvas); + emojiImagePromises.set(sprite, newPromise); + + newPromise + .then(src => { + emojiImages.set(sprite, src); + emojiImagePromises.delete(sprite); + return src; + }) + .then(callback); +} + +const emojisRegex = new RegExp(`(${[ + ...emojis.map(e => e.symbol), + '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', +].join('|')})`, 'g'); + +export function splitEmojis(text: string) { + return text.split(emojisRegex); +} + +export function hasEmojis(text: string) { + return emojisRegex.test(text); +} + +export function nameToHTML(name: string) { + return escape(name); +} + +export interface AutocompleteState { + lastEmoji?: string; +} + +const names = emojiNames.slice().sort(); + +export function autocompleteMesssage(message: string, shift: boolean, state: AutocompleteState): string { + return message.replace(/:[a-z0-9_]+:?$/, match => { + state.lastEmoji = state.lastEmoji || match; + const matches = names.filter(e => e.indexOf(state.lastEmoji!) === 0); + const index = matches.indexOf(match); + const offset = index === -1 ? 0 : (index + matches.length + (shift ? -1 : 1)) % matches.length; + return matches[offset] || match; + }); +} diff --git a/src/ts/client/fonts.ts b/src/ts/client/fonts.ts new file mode 100644 index 0000000..9317025 --- /dev/null +++ b/src/ts/client/fonts.ts @@ -0,0 +1,39 @@ +import { SpriteFont, createSpriteFont } from '../graphics/spriteFont'; +import * as sprites from '../generated/sprites'; + +export let font: SpriteFont; +export let fontPal: SpriteFont; +export let fontSmall: SpriteFont; +export let fontSmallPal: SpriteFont; +export let fontMono: SpriteFont; +export let fontMonoPal: SpriteFont; + +export function createFonts() { + font = createSpriteFont(sprites.font, sprites.emoji, 3); + font.lineSpacing = 3; + font.letterShiftY = -2; + + fontPal = createSpriteFont(sprites.fontPal, sprites.emojiPal, 3); + fontPal.lineSpacing = 3; + fontPal.letterShiftY = -2; + + fontSmall = createSpriteFont(sprites.fontSmall, [], 2); + fontSmall.lineSpacing = 4; + fontSmall.letterShiftY = -2; + fontSmall.letterHeightReal += 2; + + fontSmallPal = createSpriteFont(sprites.fontSmallPal, [], 2); + fontSmallPal.lineSpacing = 4; + fontSmallPal.letterShiftY = -2; + fontSmallPal.letterHeightReal += 2; + + fontMono = createSpriteFont(sprites.fontMono, [], 4); + fontMono.lineSpacing = 4; + fontMono.letterShiftY = -2; + fontMono.letterHeightReal += 2; + + fontMonoPal = createSpriteFont(sprites.fontMonoPal, [], 4); + fontMonoPal.lineSpacing = 4; + fontMonoPal.letterShiftY = -2; + fontMonoPal.letterHeightReal += 2; +} diff --git a/src/ts/client/game.ts b/src/ts/client/game.ts new file mode 100644 index 0000000..06d8269 --- /dev/null +++ b/src/ts/client/game.ts @@ -0,0 +1,1776 @@ +import { Injectable, NgZone } from '@angular/core'; +import { Subject, BehaviorSubject } from 'rxjs'; +import { debounce } from 'lodash'; +import { + EntityState, Pony, Notification, TileType, Action, IServerActions, Season, WorldState, Holiday, + TileSets, ChatMessage, PartyInfo, Entity, DrawOptions, Engine, defaultDrawOptions, PonyStateFlags, + DoAction, ChatType, WorldStateFlags, DebugFlags, MessageType, AccountSettings, Matrix4, + FakeEntity, SelectFlags, WorldMap, MapType, MapFlags, EntityFlags, houseTiles, isValidTile +} from '../common/interfaces'; +import { + clamp, lengthOfXY, setFlag, hasFlag, boundsIntersect, point, toInt, lerpColor, distanceXY +} from '../common/utils'; +import { + OFFLINE_PONY, MAX_SCALE, SUPPORTER_PONY, HOUR, MIN_SCALE, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX, + CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX, SECOND, TILE_CHANGE_RANGE, MINUTE, PONY_TYPE, + REGION_SIZE, tileWidth, tileHeight +} from '../common/constants'; +import { + ensureAllVisiblePoniesAreDecoded, invalidatePalettes, updateMap, updateEntities, + getMapHeightAt, updateEntitiesWithNames, updateEntitiesCoverLifted, getTile, + pickEntities, updateEntitiesTriggers, getElevation, setElevation, createWorldMap +} from '../common/worldMap'; +import { updateCamera, centerCameraOn, screenToWorld, createCamera } from '../common/camera'; +import { WHITE, BLACK, SHADOW_COLOR, getTileColor, RED, CAVE_LIGHT, CAVE_SHADOW } from '../common/colors'; +import { formatHourMinutes, getLightColor, getShadowColor, createLightData } from '../common/timeUtils'; +import { toggleWalls } from '../common/mixins'; +import { getEntityTypeName, hammer, broom, createAnEntity, saw, placeableEntities, shovel } from '../common/entities'; +import { hasExtendedInfo, setHeadAnimation, createPony } from '../common/pony'; +import { PaletteManager } from '../graphics/paletteManager'; +import { getRenderTargetSize, isWebGL2 } from '../graphics/webgl/webglUtils'; +import { drawFullScreenMessage, drawNames, drawChat } from '../graphics/graphicsUtils'; +import { Key } from './input/input'; +import { loadAndInitSpriteSheets } from './spriteUtils'; +import { version, isMobile } from './data'; +import { Game } from './gameLoop'; +import { Audio } from '../components/services/audio'; +import { getPixelRatio } from './canvasUtils'; +import { colorToFloatArray, parseColor, colorToExistingFloatArray, makeTransparent } from '../common/color'; +import { nom } from './ponyAnimations'; +import { InputManager } from './input/inputManager'; +import { roundPositionX, roundPositionY, toScreenX, toScreenY } from '../common/positionUtils'; +import { StorageService } from '../components/services/storageService'; +import { SettingsService } from '../components/services/settingsService'; +import { + isPonyLying, isPonySitting, setPonyState, getInteractBounds, entityInRange, isFacingRight, + isHidden, updateEntityVelocity, releaseEntity, addChatBubble +} from '../common/entityUtils'; +import { vectorToDir, dirToVector, flagsToSpeed, encodeMovement, isMovingRight } from '../common/movementUtils'; +import { createDefaultButtonActions, useAction, deserializeActions } from './buttonActions'; +import { ClientSocketService } from '../components/services/gameService'; +import { attachDebugMethod, initFeatureFlags, updateRangeIndicator, initLogger, log, getSaysTime } from './clientUtils'; +import { restorePlayerPosition, savePlayerPosition } from './sec'; +import { drawEntityLights, drawEntityLightSprites, drawMap, drawDebugRegions } from './draw'; +import { updateTileSets, initializeTileHeightmaps } from './tileUtils'; +import { + downAction, upAction, turnHeadAction, boopAction, interact, toggleWall, editorMoveEntities, + editorSelectEntities, editorDragEntities +} from './playerActions'; +import { fontSmallPal, fontSmall, font, fontMono } from './fonts'; +import { drawText, drawOutlinedText, measureText } from '../graphics/spriteFont'; +import { initializeToys } from './ponyDraw'; +import { ErrorReporter } from '../components/services/errorReporter'; +import { mockPaletteManager } from '../common/ponyInfo'; +import { timeStart, timeEnd, timingCollate, timeReset } from './timing'; +import { bindFrameBuffer, unbindFrameBuffer, resizeFrameBuffer } from '../graphics/webgl/frameBuffer'; +import { WebGL, initWebGL, disposeWebGL, initWebGLResources } from './webgl'; +import { bindTexture } from '../graphics/webgl/texture2d'; +import { + normalSpriteSheet, paletteSpriteSheet, defaultPalette, wall_h_placeholder, wall_v_placeholder +} from '../generated/sprites'; +import { createMat4, ortho } from '../common/mat4'; +import { Model } from '../components/services/model'; +import { filterEntityName } from './handlers'; +import { isOutsideMap } from '../common/collision'; + +interface Minimap { + width: number; + height: number; + data: Uint32Array; +} + +interface IncompleteSay { + id: number; + message: string; + type: MessageType; + time: number; +} + +const LOG_POSITION = false; +const CONNECTION_ISSUE_TIMEOUT = 10 * SECOND; +const white = colorToFloatArray(WHITE); +const light = colorToFloatArray(WHITE); +const highlightColor = new Float32Array([2, 2, 2, 0.5]); +const toggleWallsTool = saw; +const removeEntitiesTool = broom; +const placeEntitiesTool = hammer; +const changeTileTool = shovel; + +const numpad = [ + Key.NUMPAD_0, + Key.NUMPAD_1, + Key.NUMPAD_2, + Key.NUMPAD_3, + Key.NUMPAD_4, + Key.NUMPAD_5, + Key.NUMPAD_6, + Key.NUMPAD_7, + Key.NUMPAD_8, + Key.NUMPAD_9, +]; + +export const engines = [ + { name: 'Default', engine: Engine.Default }, + { name: 'LayeredTiles', engine: Engine.LayeredTiles }, + { name: 'Whiteness', engine: Engine.Whiteness }, + // { name: 'NewLighting', engine: Engines.NewLighting }, +]; + +let pixelRatioEnabled = true; +let pixelRatioCache = 1; + +function pixelRatio() { + return pixelRatioEnabled ? pixelRatioCache : 1; +} + +function integerPixelRatio() { + return Math.max(1, Math.floor(pixelRatio())); +} + +function getMovementFlag(x: number, y: number, walkKey: boolean) { + const len = lengthOfXY(x, y); + const walk = len < 0.5 || walkKey; + return (x || y) ? (walk ? EntityState.PonyWalking : EntityState.PonyTrotting) : EntityState.None; +} + +export const actionButtons: { dirty: boolean; draw(): void; }[] = []; + +export function redrawActionButtons(force: boolean) { + for (const button of actionButtons) { + if (force || button.dirty) { + button.draw(); + } + } +} + +@Injectable({ providedIn: 'root' }) +export class PonyTownGame implements Game { + fallbackPonies = new Map(); + positions: { x: number; y: number; moved: boolean; }[] = []; + lastChatMessageType = ChatType.Say; + nextFriendsCRC = 0; + editingActions = false; + placeInQueue = 0; + time = performance.now(); + lightData = createLightData(Season.Summer); + season = Season.Summer; + holiday = Holiday.None; + worldFlags = WorldStateFlags.None; + showMinimap = false; + minimap: Minimap | undefined = undefined; + editor = { + type: 'stoneWall', + brushSize: 1, + tile: -1, + elevation: '', + special: '', + draggingEntities: false, + draggingStart: point(0, 0), + selectingEntities: false, + selectedEntities: [] as Entity[], + customLight: false, + lightColor: 'ffffff', + }; + incompleteSays: IncompleteSay[] = []; + shadowColor = SHADOW_COLOR; + onChat = new Subject(); + onToggleChat = new Subject(); + onCommand = new Subject(); + onCancel = () => false; + onClock = new BehaviorSubject(''); + onJoined = new Subject(); + onLeft = new Subject(); + onFrame = new Subject(); + onMessage = new Subject(); + messageQueue: ChatMessage[] = []; + lastWhisperFrom: { entityId: number; accountId?: string; } | undefined = undefined; + onPonyAddOrUpdate = new Subject(); + onActionsUpdate = new Subject(); + onPartyUpdate = new Subject(); + announcements = new Subject(); + onEntityIdUpdate = new Subject<{ old: number; new: number; }>(); + loaded = false; + fullyLoaded = false; + fps = 0; + player: Pony | undefined = undefined; + playerId: number | undefined = undefined; + playerName: string | undefined = undefined; + playerInfo: string | undefined = undefined; + playerCRC: number | undefined = undefined; + selected: Pony | undefined = undefined; + party: PartyInfo | undefined = undefined; + notifications: Notification[] = []; + map = createWorldMap(); + camera = createCamera(); + paletteManager = new PaletteManager(); + tileSets?: TileSets; + offlinePony = createPony(0, 0, OFFLINE_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); + supporterPony = createPony(0, 0, SUPPORTER_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); + scale: number; + failedFBO = false; + rightOverride?: boolean; + headTurnedOverride?: boolean; + stateOverride?: EntityState; + actions = createDefaultButtonActions(); + mod = false; + webgl?: WebGL; + actionsChanged = true; + debug: DebugFlags = {}; + whisperTo: Entity | FakeEntity | undefined = undefined; + findEntityFromChatLog: (id: number) => FakeEntity | undefined = () => undefined; + findEntityFromChatLogByName: (name: string) => FakeEntity | undefined = () => undefined; + private drawOptions: DrawOptions = { + ...defaultDrawOptions, + }; + private input = new InputManager(); + socket?: ClientSocketService; + private canvas?: HTMLCanvasElement; + private statsText?: Text; + private timeSize = 0; + private lastStats = 0; + private sent = 0; + private recv = 0; + private hideText = false; + private hidePublicChat = false; + private hover = point(0, 0); + private viewMatrix = createMat4(); + private fboMatrix = createMat4(); + private initialized = false; + private changedScale = false; + private baseTime = 0; + private targetBaseTime = 0; + private connectedTime = 0; + private lastPixelRatio = pixelRatio(); + private resized = true; + private resizedCamera = true; + private bg = colorToFloatArray(BLACK); + private deltaMultiplier = 1; + private lastDraw = 0; + private entitiesDrawn = 0; + private lastFps = performance.now(); + private frames = 0; + private drawFps = 0; + private lastCanvasRatio = 0; + private extraStats = ''; + private timingsText = ''; + private statsTextValue = ''; + private windowWidth = 0; + private windowHeight = 0; + private debugShortcuts: string[] = []; + private cameraShiftOn = false; + private cameraShiftTarget = 0; + private lastIsKeyboardOpen = false; + private element?: HTMLElement; + private showWallPlaceholder = false; + private highlightEntity?: Entity; + placeEntity = 0; + placeTile = 0; + constructor( + public audio: Audio, + private storage: StorageService, + public settings: SettingsService, + public model: Model, + private errorReporter: ErrorReporter, + private zone: NgZone, + ) { + this.scale = this.getScale(); + this.audio.initTracks(this.season, this.holiday, this.map.type); + this.audio.setVolume(this.volume); + this.debug = storage.getJSON('debug', {}); + this.drawOptions.error = message => errorReporter.reportError(message); + this.onActionsUpdate.subscribe(() => this.actionsChanged = true); + + if (DEVELOPMENT) { + attachDebugMethod('setScale', (x: number) => this.setScale(x)); + attachDebugMethod('game', this); + } + } + get volume() { + return this.settings.browser.volume || 0; + } + get disableLighting() { + return !!this.settings.browser.lowGraphicsMode || this.failedFBO; + } + get frameDelay() { + return (this.settings.browser.powerSaving || this.editingActions) ? (1000 / 45) : 0; + } + get engine() { + return BETA ? (this.debug.engine || Engine.Default) : Engine.Default; + } + set engine(value: Engine) { + if (BETA) { + this.debug.engine = value; + this.saveDebug(); + } + } + private applied(func: () => void) { + return () => this.apply(func); + } + apply = (func: () => void) => { + return this.zone.run(func); + } + applyChanges = () => this.zone.run(() => { }); + private getScale() { + const defaultScale = pixelRatio() > 1 ? 3 : 2; + const scale = toInt(this.settings.browser.scale) || defaultScale; + return clamp(scale, MIN_SCALE, MAX_SCALE); + } + private setScale(scale: number) { + if (this.scale !== scale) { + this.scale = scale; + this.settings.browser.scale = this.scale; + this.settings.saveBrowserSettings(); + this.changedScale = true; + } + } + private toggleDisableLighting() { + if (!this.failedFBO) { + this.settings.browser.lowGraphicsMode = !this.settings.browser.lowGraphicsMode; + this.settings.saveBrowserSettings(); + } + } + send(action: (server: IServerActions) => T) { + if (this.socket && this.socket.isConnected) { + return action(this.socket.server); + } else { + return undefined; + } + } + changeScale() { + this.setScale((this.scale % MAX_SCALE) + 1); + this.changedScale = true; + } + zoomIn() { + this.setScale(Math.min(MAX_SCALE, this.scale + 1)); + } + zoomOut() { + this.setScale(Math.max(1, this.scale - 1)); + } + select(pony: Pony | undefined) { + if (this.selected === pony) + return; + + if (pony && isHidden(pony) && !this.mod) + return; + + this.zone.run(() => { + if (this.selected) { + this.selected.selected = false; + } + + this.selected = pony; + + if (pony && !pony.info && !pony.palettePonyInfo) { + this.send(server => server.select(pony.id, SelectFlags.FetchEx | SelectFlags.FetchInfo)); + } else { + this.sendSelected(); + } + + if (this.selected) { + this.selected.selected = true; + } + }); + } + private sendSelected = debounce(() => { + const pony = this.selected; + const id = pony ? pony.id : 0; + const fetchEx = !!pony && !hasExtendedInfo(pony); + this.send(server => server.select(id, fetchEx ? SelectFlags.FetchEx : SelectFlags.None)); + }, 300); + load() { + return loadAndInitSpriteSheets() + .then(initializeTileHeightmaps); + } + init() { + this.canvas = document.getElementById('canvas') as HTMLCanvasElement; + this.updateTileSets(); + this.input.initialize(this.canvas); + + if (!this.initialized) { + this.canvas.addEventListener('webglcontextlost', e => { + e.preventDefault(); + DEVELOPMENT && console.warn('Context lost'); + this.errorReporter.captureEvent({ name: 'Context lost' }); + }); + + this.canvas.addEventListener('webglcontextrestored', () => { + DEVELOPMENT && console.warn('Context restored'); + this.errorReporter.captureEvent({ name: 'Context restored' }); + + if (this.webgl) { + this.webgl = initWebGLResources(this.webgl.gl, this.paletteManager, this.camera); + } + }); + + this.initialized = true; + + const stats = document.getElementById('stats') as HTMLElement; + this.statsText = document.createTextNode(''); + stats.appendChild(this.statsText); + + this.input.onReleased(Key.KEY_O, () => this.zoomOut()); + this.input.onReleased(Key.KEY_P, () => this.zoomIn()); + this.input.onReleased(Key.GAMEPAD_BUTTON_Y, () => this.changeScale()); + + this.input.onPressed(Key.ENTER, () => this.onChat.next()); + this.input.onPressed(Key.ESCAPE, () => this.escape()); + this.input.onPressed(Key.GAMEPAD_BUTTON_X, () => this.onToggleChat.next()); + // this.input.onPressed(Key.BACKSPACE, () => this.backspace()); + this.input.onPressed(Key.KEY_H, () => turnHeadAction(this)); + this.input.onPressed(Key.FORWARD_SLASH, () => this.onCommand.next()); + this.input.onPressed([Key.KEY_B, Key.GAMEPAD_BUTTON_B, Key.TOUCH_SECOND_CLICK], () => boopAction(this)); + this.input.onPressed([Key.KEY_E, Key.GAMEPAD_BUTTON_A], () => { + interact(this, this.input.isPressed(Key.SHIFT)); + }); + this.input.onPressed([Key.KEY_X, Key.GAMEPAD_BUTTON_DOWN], () => downAction(this)); + this.input.onPressed([Key.KEY_C, Key.GAMEPAD_BUTTON_UP], () => upAction(this)); + this.input.onPressed(Key.F2, () => { + if (!this.settings.browser.disableFKeys) { + this.hideText = !this.hideText; + this.hidePublicChat = false; + } + }); + this.input.onPressed(Key.F3, () => { + if (!this.settings.browser.disableFKeys) { + this.hideText = false; + this.hidePublicChat = !this.hidePublicChat; + } + }); + this.input.onPressed(Key.F4, () => { + if (!this.settings.browser.disableFKeys) { + this.settings.account.seeThroughObjects = !this.settings.account.seeThroughObjects; + this.settings.saveAccountSettings(this.settings.account); + } + }); + + [ + Key.KEY_1, Key.KEY_2, Key.KEY_3, Key.KEY_4, Key.KEY_5, Key.KEY_6, + Key.KEY_7, Key.KEY_8, Key.KEY_9, Key.KEY_0, Key.DASH, Key.EQUALS, + ].forEach((key, index) => this.input.onPressed(key, () => { + if (this.actions[index]) { + this.zone.run(() => useAction(this, this.actions[index].action)); + } + })); + + const addDebugShortcut = (num: number, name: string, action: () => void) => { + this.input.onPressed(numpad[num], () => { + if (!this.input.isPressed(Key.SHIFT)) { + this.apply(action); + } + }); + this.debugShortcuts.push(`${num} - ${name}`); + this.debugShortcuts.sort(); + }; + + // const addDebugShortcutShift = (num: number, name: string, action: () => void) => { + // this.input.onPressed(numpad[num], () => { + // if (this.input.isPressed(Key.SHIFT)) { + // this.apply(action); + // } + // }); + // this.debugShortcuts.push(`${num} (shift) - ${name}`); + // this.debugShortcuts.sort(); + // }; + + if (BETA) { + // editor + this.input.onPressed(Key.BACKSPACE, () => { + if (this.mod) { + this.send(server => server.editorAction({ type: 'undo' })); + } + }); + this.input.onPressed(Key.DELETE, this.applied(() => { + const entities = this.editor.selectedEntities.map(e => e.id); + this.send(server => server.editorAction({ type: 'remove', entities })); + this.editor.selectedEntities.length = 0; + })); + + [ + { key: Key.LEFT, dx: -1 / tileWidth, dy: 0 }, + { key: Key.RIGHT, dx: 1 / tileWidth, dy: 0 }, + { key: Key.UP, dx: 0, dy: -1 / tileHeight }, + { key: Key.DOWN, dx: 0, dy: 1 / tileHeight }, + ].forEach(({ key, dx, dy }) => this.input.onPressed(key, () => { + this.editor.selectedEntities.forEach(({ id, x, y }) => { + this.send(server => server.editorAction({ + type: 'move', + entities: [{ id, x: x + dx, y: y + dy }], + })); + }); + })); + + // debug + this.input.onReleased(Key.KEY_M, () => this.showMinimap = !this.showMinimap); + this.input.onPressed(Key.KEY_G, () => { + if (this.input.isPressed(Key.SHIFT)) { + let faceDir = 0; + let dir = 1; + this.player!.doAction = DoAction.Swing; + const state = this.player!.ponyState; + + const interval = setInterval(() => { + faceDir += dir; + + if (faceDir < 0) { + clearInterval(interval); + return; + } + + state.headTurn = faceDir; + + if (faceDir === 3) { + turnHeadAction(this); + } + + if (faceDir >= 6) { + dir = -1; + } + }, 1000 / 24); + } else { + let faceDir = 0; + const state = this.player!.ponyState; + + const interval = setInterval(() => { + faceDir++; + state.headTurn = faceDir; + + if (faceDir === 3) { + turnHeadAction(this); + } + + if (faceDir >= 7) { + clearInterval(interval); + } + }, 1000 / 24); + } + }); + addDebugShortcut(1, 'show info at cursor', () => { + this.debug.showInfo = !this.debug.showInfo; + this.saveDebug(); + }); + addDebugShortcut(2, 'show water bounds', () => { + this.drawOptions.showHeightmap = !this.drawOptions.showHeightmap; + }); + addDebugShortcut(3, 'show collision map', () => { + this.drawOptions.showColliderMap = !this.drawOptions.showColliderMap; + }); + addDebugShortcut(4, 'show helpers', () => { + this.debug.showHelpers = !this.debug.showHelpers; + this.saveDebug(); + }); + addDebugShortcut(5, 'show tile indices', () => { + this.drawOptions.tileIndices = !this.drawOptions.tileIndices; + }); + addDebugShortcut(6, 'show tile grid', () => { + this.drawOptions.tileGrid = !this.drawOptions.tileGrid; + }); + addDebugShortcut(7, 'grayscale', () => { + document.documentElement.style.filter = document.documentElement.style.filter ? null : 'grayscale(100%)'; + }); + addDebugShortcut(8, 'show regions', () => { + this.debug.showRegions = !this.debug.showRegions; + this.saveDebug(); + }); + } + + if (DEVELOPMENT) { + let showingRange = false; + addDebugShortcut(9, 'show chatlog range', () => { + showingRange = !showingRange; + updateRangeIndicator(showingRange ? this.settings.account.chatlogRange : undefined, this); + }); + this.input.onPressed(Key.F6, () => { + this.cameraShiftOn = !this.cameraShiftOn; + this.cameraShiftTarget = 400; + }); + this.input.onPressed(Key.F7, () => { + this.debug.showPalette = !this.debug.showPalette; + this.saveDebug(); + }); + this.input.onPressed(Key.F8, () => { + }); + + let loseContext: WEBGL_lose_context | null = null; + + this.input.onPressed(Key.F9, () => { + if (loseContext) { + loseContext.restoreContext(); + loseContext = null; + } else { + loseContext = this.webgl!.gl.getExtension('WEBGL_lose_context')!; + loseContext.loseContext(); + } + }); + this.input.onPressed(Key.F10, () => { + this.settings.browser.brightNight = !this.settings.browser.brightNight; + }); + this.input.onPressed(Key.KEY_R, this.applied(() => { + if (!Date.now() && this.player) { + const bounds = getInteractBounds(this.player); + const entities = this.map.entities.filter(e => + e !== this.player && boundsIntersect(e.x, e.y, e.bounds, 0, 0, bounds)); + + if (entities.length) { + const entity = entities[0]; + const typeName = getEntityTypeName(entity.type); + this.announce(`${typeName}${entities.length > 1 ? ` (1 of ${entities.length})` : ''}`); + } else { + this.announce('nothing'); + } + } + + // if (this.player) this.player.swimming = !this.player.swimming; + // this.editorElevation = ''; + // this.editorSpecial = this.editorSpecial ? '' : 'ramp-e'; + })); + this.input.onPressed(Key.KEY_J, () => { + if (this.player) { + this.player.ponyState.headTilt = (this.player.ponyState.headTilt || 0) + 0.5; + } + }); + this.input.onPressed(Key.KEY_K, () => { + if (this.player) { + this.player.ponyState.headTilt = (this.player.ponyState.headTilt || 0) - 0.5; + } + }); + this.input.onPressed(Key.KEY_L, () => this.player && setHeadAnimation(this.player, nom)); + this.input.onReleased(Key.KEY_Q, () => this.send(server => server.leave())); + this.input.onReleased(Key.KEY_T, () => this.toggleDisableLighting()); + this.input.onReleased(Key.KEY_U, () => { + if (this.player) { + console.log( + `position: ${this.player.x.toFixed(2)}, ${this.player.y.toFixed(2)} ` + + `region: ${Math.floor(this.player.x / REGION_SIZE)}, ${Math.floor(this.player.y / REGION_SIZE)}`); + } + }); + this.input.onReleased(Key.KEY_I, () => { + const state = this.player!.ponyState; + state.flags = setFlag(state.flags, PonyStateFlags.CurlTail, !hasFlag(state.flags, PonyStateFlags.CurlTail)); + }); + // this.input.onReleased(Key.KEY_N, () => this.engine = (this.engine + 1) % Engine.Total); + this.input.onPressed(Key.KEY_N, () => this.audio.playRandomTrack()); + // this.input.onPressed(Key.KEY_G, () => this.wind = Math.max(0, this.wind - 1)); + // this.input.onReleased(Key.KEY_M, () => this.send(server => server.editorAction({ type: 'party' }))); + this.input.onPressed(Key.F8, () => toggleWalls()); + this.input.onPressed(Key.COMMA, () => this.deltaMultiplier = 0.5); + this.input.onPressed(Key.PERIOD, () => this.deltaMultiplier = 2); + } + + window.addEventListener('resize', () => { + this.resized = true; + DEVELOPMENT && log(`resized ${window.innerHeight} (${window.scrollY})`); + }); + + this.canvas.addEventListener('touchstart', () => this.audio.touch()); + } + + this.resized = true; + + if (!this.webgl) { + this.initWebGL(); + } + } + leave() { + if (this.socket) { + if (this.socket.isConnected) { + this.socket.server.leave(); + } else { + this.socket.disconnect(); + } + } + } + joined() { + this.connectedTime = Math.round(performance.now()); + this.onJoined.next(); + } + togglePixelRatio() { + pixelRatioEnabled = !pixelRatioEnabled; + } + private escape() { + if (this.socket && !this.onCancel()) { + this.select(undefined); + } + } + backspace() { + if (this.player && this.player.says !== undefined) { + this.send(server => server.say(0, '.', ChatType.Dismiss)); + } + } + private initWebGL() { + this.errorReporter.captureEvent({ name: 'game.initWebGL' }); + + if (!this.canvas) { + throw new Error('Missing canvas'); + } + + try { + this.resizeCamera(); + this.webgl = initWebGL(this.canvas, this.paletteManager, this.camera); + const { failedFBO, palettes, renderer } = this.webgl; + + if (renderer) { + this.errorReporter.configureData({ renderer }); + } + + if (failedFBO) { + this.errorReporter.captureEvent({ name: 'game.initWebGL failed FBO' }); + } + + this.offlinePony = createPony(0, 0, OFFLINE_PONY, palettes.defaultPalette, this.paletteManager); + this.supporterPony = createPony(0, 0, SUPPORTER_PONY, palettes.defaultPalette, this.paletteManager); + initializeToys(this.paletteManager); + } catch (e) { + this.errorReporter.captureEvent({ name: 'failed game.initWebGL', error: e.message, stack: e.stack }); + this.releaseWebGL(); + DEVELOPMENT && console.error(e); + throw new Error(`Failed to initialize graphics device (${e.message})`); + } + } + private releaseWebGL() { + this.errorReporter.captureEvent({ name: 'game.releaseWebGL' }); + + if (this.webgl) { + try { + this.paletteManager.dispose(this.webgl.gl); + disposeWebGL(this.webgl); + } catch (e) { + DEVELOPMENT && console.error(e); + } + + this.webgl = undefined; + } + } + private resizeCamera() { + if (this.canvas) { + const actualScale = this.scale * integerPixelRatio(); + const w = clamp(Math.ceil(this.canvas.width / actualScale), CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX); + const h = clamp(Math.ceil(this.canvas.height / actualScale), CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX); + + if (this.camera.w !== w || this.camera.h !== h) { + this.camera.w = w; + this.camera.h = h; + this.resizedCamera = true; + } + } + } + release() { + this.settings.saving(() => false); + this.loaded = false; + this.fullyLoaded = false; + this.player = undefined; + this.selected = undefined; + this.party = undefined; + this.rightOverride = undefined; + this.headTurnedOverride = undefined; + this.stateOverride = undefined; + this.notifications = []; + this.map = createWorldMap(); + this.camera = createCamera(); + + if (this.socket) { + this.socket.disconnect(); + this.socket = undefined; + } + + this.audio.stop(); + this.input.release(); + this.releaseWebGL(); + } + startup(socket: ClientSocketService, mod: boolean) { + if (this.settings.account.actions) { + this.actions = deserializeActions(this.settings.account.actions); + } else { + this.actions = createDefaultButtonActions(); + } + + const saveSettings = (settings: AccountSettings) => this.send(server => server.saveSettings(settings)); + const saveSettingsDebounced = debounce(saveSettings, 1500); + + this.element = document.getElementById('app-game')!; + this.settings.saving(settings => (saveSettingsDebounced(settings), true)); + this.lastChatMessageType = ChatType.Say; + this.selected = undefined; + this.party = undefined; + this.socket = socket; + this.audio.setVolume(this.volume); + // this.audio.play(); + this.mod = mod; + this.nextFriendsCRC = performance.now() + 5 * SECOND; + + if (DEVELOPMENT) { + initLogger(message => this.onMessage.next({ + id: 1, crc: 1, name: 'log', type: MessageType.System, + message: `[${((performance.now() | 0) % 10000)}] ${message}` + })); + } + + if (DEVELOPMENT && LOG_POSITION) { + this.positions.length = 0; + } + } + private getPixelScale() { + return this.scale * (integerPixelRatio() / pixelRatio()); + } + update(delta: number, now: number, last: number) { + TIMING && timeStart('update'); + delta *= this.deltaMultiplier; + + const shiftSpeed = delta * 10; + + if (this.cameraShiftOn && this.camera.shiftRatio !== 1) { + this.camera.shiftRatio = Math.min(1, this.camera.shiftRatio + shiftSpeed); + } else if (!this.cameraShiftOn && this.camera.shiftRatio !== 0) { + this.camera.shiftRatio = Math.max(0, this.camera.shiftRatio - shiftSpeed); + } + + updateMap(this.map, delta); + + if (!this.socket || !this.socket.isConnected || !this.element) + return; + + this.updateGameTime(delta); + + if (this.lastPixelRatio !== pixelRatio()) { + this.lastPixelRatio = pixelRatio(); + this.resized = true; + } + + if (this.resized) { + this.resizeCanvas(); + } + + const player = this.player; + const camera = this.camera; + const input = this.input; + const server = this.socket.server; + + restorePlayerPosition(); + + input.disabledGamepad = !!this.settings.browser.disableGamepad; + input.update(); + + this.resizeCamera(); + this.updateCameraShift(); + + const actualScale = this.scale * integerPixelRatio(); + this.camera.offset = this.cameraShiftTarget / actualScale; + + let moved = false; + + if (player && this.loaded) { + if (BETA && this.editor.selectedEntities.length) { + input.disableArrows = true; + } + + this.showWallPlaceholder = player.hold === toggleWallsTool.type && hasFlag(this.map.flags, MapFlags.EditableWalls); + + if (this.highlightEntity) { + if (this.highlightEntity.id === 0) { + releaseEntity(this.highlightEntity); + } + + this.highlightEntity = undefined; + } + + const shift = input.isPressed(Key.SHIFT); + const x = this.fullyLoaded ? input.axisX : 0; + const y = this.fullyLoaded ? input.axisY : 0; + const dir = vectorToDir(x, y); + const vec = (x || y) ? dirToVector(dir) : { x: 0, y: 0 }; + const walk = input.isMovementFromButtons ? (this.settings.browser.walkByDefault ? !shift : shift) : false; + const flags = getMovementFlag(x, y, walk); + const speed = flagsToSpeed(flags); + const vx = vec.x * speed; + const vy = vec.y * speed; + + if (BETA) { + input.disableArrows = false; + } + + if (player.vx !== vx || player.vy !== vy) { + if (vx === 0) { + player.x = roundPositionX(player.x) + 0.5 / tileWidth; + } + + if (vy === 0) { + player.y = roundPositionY(player.y) + 0.5 / tileHeight; + } + + const time = (last - this.connectedTime) >>> 0; + const [a, b, c, d, e] = encodeMovement(player.x, player.y, dir, flags, time, camera); + server.move(a, b, c, d, e); + moved = true; + this.resizedCamera = false; + } + + if ((vx || vy) && (isPonySitting(player) || isPonyLying(player))) { + player.state = setPonyState(player.state, EntityState.PonyStanding); + } + + updateEntityVelocity(this.map, player, vx, vy); + + const facingRight = isFacingRight(player); + const right = isMovingRight(vx, facingRight); + + if (facingRight !== right) { + player.state = setFlag(player.state, EntityState.FacingRight, right); + player.state = setFlag(player.state, EntityState.HeadTurned, false); + this.rightOverride = right; + } + + updateCamera(camera, player, this.map); + + const scale = this.getPixelScale(); + const hover = screenToWorld(camera, point(input.pointerX / scale, input.pointerY / scale)); + + if (input.usingTouch && !input.wasPressed(Key.TOUCH_CLICK) && !input.isPressed(Key.TOUCH)) { + hover.x = 0; + hover.y = 0; + } + + this.hover = hover; + + if (this.fullyLoaded) { + if (BETA && this.editor.draggingEntities) { + editorDragEntities(this, hover, input.isPressed(Key.MOUSE_BUTTON2)); + } + + if (hasFlag(this.map.flags, MapFlags.EditableEntities)) { + if (player.hold === removeEntitiesTool.type) { + this.highlightEntity = pickEntities(this.map, hover, true, false, true)[0]; + } else if (player.hold === placeEntitiesTool.type) { + if (!isOutsideMap(hover.x, hover.y, this.map)) { + const { type } = placeableEntities[this.placeEntity]; + let { x, y } = hover; + + if (this.map.editableArea) { + x = clamp(x, this.map.editableArea.x, this.map.editableArea.x + this.map.editableArea.w); + y = clamp(y, this.map.editableArea.y, this.map.editableArea.y + this.map.editableArea.h); + } + + this.highlightEntity = createAnEntity(type, 0, x, y, {}, this.paletteManager, this); + } + } + } + + if (input.wasPressed(Key.MOUSE_BUTTON1) || input.wasPressed(Key.TOUCH_CLICK)) { + const pickedEntities = pickEntities(this.map, hover, shift, this.mod); + const pickedEntity = pickedEntities[(pickedEntities.indexOf(this.selected!) + 1) % pickedEntities.length]; + const holdingRemoveTool = player.hold === removeEntitiesTool.type; + const editableMap = hasFlag(this.map.flags, MapFlags.EditableEntities); + + if (BETA && this.editor.selectingEntities) { + editorSelectEntities(this, hover, shift); + } else if (pickedEntity && (!holdingRemoveTool || !editableMap || hasFlag(pickedEntity.flags, EntityFlags.IgnoreTool))) { + if (pickedEntity.type === PONY_TYPE) { + this.select(pickedEntity as Pony); + } else if (entityInRange(pickedEntity, player)) { + server.interact(pickedEntity.id); + } + } else if (BETA && this.editor.tile !== -1) { + if (this.editor.brushSize > 1) { + const x = Math.floor((hover.x - (this.editor.brushSize / 2))); + const y = Math.floor((hover.y - (this.editor.brushSize / 2))); + server.editorAction({ type: 'tile', x, y, tile: this.editor.tile, size: this.editor.brushSize }); + } else { + const x = hover.x | 0; + const y = hover.y | 0; + const type = this.editor.tile === getTile(this.map, hover.x, hover.y) ? TileType.Dirt : this.editor.tile; + server.changeTile(x, y, type); + } + } else if (player.hold === changeTileTool.type && hasFlag(this.map.flags, MapFlags.EditableTiles)) { + const x = hover.x | 0; + const y = hover.y | 0; + server.changeTile(x, y, houseTiles[this.placeTile].type); + } else if (player.hold === toggleWallsTool.type && hasFlag(this.map.flags, MapFlags.EditableWalls)) { + toggleWall(this, hover); + } else if (holdingRemoveTool && this.highlightEntity && editableMap) { + const id = this.highlightEntity.id; + this.send(server => server.actionParam(Action.RemoveEntity, id)); + } else if (player.hold === placeEntitiesTool.type && this.highlightEntity && editableMap) { + const { x, y, type } = this.highlightEntity; + this.send(server => server.actionParam(Action.PlaceEntity, { x, y, type })); + } else if (this.selected) { + this.select(undefined); + } else if (hasFlag(this.map.flags, MapFlags.EdibleGrass)) { + const tile = getTile(this.map, hover.x, hover.y); + + if (isValidTile(tile) && distanceXY(player.x, player.y, hover.x, hover.y) < TILE_CHANGE_RANGE) { + const x = hover.x | 0; + const y = hover.y | 0; + let type = tile === TileType.Grass ? TileType.Dirt : TileType.Grass; + server.changeTile(x, y, type); + } + } else if (DEVELOPMENT && this.engine === Engine.LayeredTiles && this.editor.elevation) { + const value = getElevation(this.map, hover.x, hover.y); + setElevation(this.map, hover.x, hover.y, clamp(this.editor.elevation === 'up' ? value + 1 : value - 1, 0, 10)); + } + } + + if (BETA && input.wasPressed(Key.MOUSE_BUTTON2)) { + if (this.editor.selectingEntities) { + editorMoveEntities(this, hover); + } else if (this.mod) { + toggleWall(this, hover); + } + } + + if (BETA && input.wasPressed(Key.MOUSE_BUTTON3)) { + if (this.mod) { + server.editorAction({ type: 'place', entity: this.editor.type, x: hover.x, y: hover.y }); + console.log(`${this.editor.type}(${hover.x.toFixed(2)}, ${hover.y.toFixed(2)})`); + } + } + + if (this.player && input.wheelY) { + if (input.isPressed(Key.SHIFT)) { + if (hasFlag(this.map.flags, MapFlags.EditableEntities)) { + const action = input.wheelY < 0 ? Action.SwitchTool : Action.SwitchToolRev; + this.send(server => server.action(action)); + } + } else { + if (this.player.hold === placeEntitiesTool.type) { + this.changePlaceEntity(input.wheelY < 0); + } else if (this.player.hold === changeTileTool.type) { + this.changePlaceTile(input.wheelY < 0); + } + } + } + + const isHeadTurned = hasFlag(player.state, EntityState.HeadTurned); + const isHeadFacingRight = right ? !isHeadTurned : isHeadTurned; + + if (((input.axis2X < 0 && isHeadFacingRight) || (input.axis2X > 0 && !isHeadFacingRight))) { + if (server.action(Action.TurnHead) as any) { + player.state = (player.state) ^ EntityState.HeadTurned; + } + } + } + } + + if (player) { + const safe = hasFlag(this.worldFlags, WorldStateFlags.Safe); + updateEntities(this, this.time, delta, safe); + savePlayerPosition(); + } + + if (this.changedScale) { + this.changedScale = false; + this.resizedCamera = true; + + if (player) { + centerCameraOn(camera, player); + } + } + + if (player) { + updateCamera(camera, player, this.map); + } + + if (this.resizedCamera) { + server.updateCamera(camera.x, camera.y, camera.w, camera.h); + this.resizedCamera = false; + } + + if (player) { + updateEntitiesCoverLifted(this.map, player, !!this.settings.account.seeThroughObjects, delta); + updateEntitiesWithNames(this.map, this.hover, player); + updateEntitiesTriggers(this.map, player, this); + } + + input.end(); + + if (this.nextFriendsCRC < now) { + this.send(server => server.actionParam(Action.FriendsCRC, this.model.computeFriendsCRC())); + this.nextFriendsCRC = now + 15 * MINUTE; + } + + const threshold = Date.now() - 10 * SECOND; + + for (let i = this.incompleteSays.length - 1; i >= 0; i--) { + if (this.incompleteSays[i].time < threshold) { + this.incompleteSays.splice(i, 1); + } + } + + this.updateSocketStats(delta); + TIMING && timeEnd(); + + if (DEVELOPMENT && LOG_POSITION) { + if (this.player) { + this.positions.push({ x: this.player.x, y: this.player.y, moved }); + } + } + } + private updateGameTime(delta: number) { + if (this.baseTime !== this.targetBaseTime) { + const timeDelta = Math.floor(delta * 0.2 * HOUR); + const baseTime = this.baseTime + timeDelta * (this.baseTime > this.targetBaseTime ? -1 : 1); + + if (Math.abs(this.targetBaseTime - baseTime) < timeDelta) { + this.baseTime = this.targetBaseTime; + } else { + this.baseTime = baseTime; + } + } + + this.time = this.baseTime + performance.now(); + } + private updateSocketStats(delta: number) { + this.timeSize += delta; + + if (this.timeSize > 1) { + this.sent = 8 * this.socket!.sentSize / this.timeSize / 1024; + this.recv = 8 * this.socket!.receivedSize / this.timeSize / 1024; + this.socket!.sentSize = 0; + this.socket!.receivedSize = 0; + this.timeSize = 0; + } + } + setWorldState(state: WorldState, initial: boolean) { + this.season = state.season; + this.holiday = state.holiday; + this.worldFlags = state.flags; + this.lightData = createLightData(this.season); + initFeatureFlags(state.featureFlags); + + const baseTime = state.time - performance.now(); + + if (initial) { + this.baseTime = this.targetBaseTime = baseTime; + } else { + this.targetBaseTime = baseTime; + } + + this.updateTileSets(); + + if (this.model.friends) { + for (const friend of this.model.friends) { + friend.actualName = filterEntityName(this, friend.name, friend.nameBad) || ''; + } + } + } + setPlayer(player: Pony) { + this.player = player; + centerCameraOn(this.camera, player); + this.send(server => server.loaded()); + } + setupMap() { + this.bg = colorToFloatArray(getTileColor(this.map.defaultTile, this.season)); + this.audio.initTracks(this.season, this.holiday, this.map.type); + this.audio.playOrSwitchToRandomTrack(); + this.updateTileSets(); + } + private updateTileSets() { + this.tileSets = updateTileSets(this.paletteManager, this.tileSets, this.season, this.map.type); + } + draw() { + redrawActionButtons(this.actionsChanged); + this.actionsChanged = false; + + if (!this.webgl) + return; + + if (this.webgl.gl.isContextLost()) { + DEVELOPMENT && console.warn('Context is lost'); + return; + } + + // start frame + const now = performance.now(); + + if ((now - this.lastDraw) < this.frameDelay) { + return; + } + + this.frames++; + + if ((now - this.lastFps) > 1000) { + this.drawFps = this.frames * 1000 / (now - this.lastFps); + this.frames = 0; + this.lastFps = now; + } + + this.lastDraw = now; + + // draw + const { + gl, frameBuffer, frameBufferSheet, spriteShader, spriteBatch, lightShader, paletteBatch, paletteShader, + palettes, + } = this.webgl; + + TIMING && timeStart('draw'); + + TIMING && timeStart('draw init'); + let lightColor = WHITE; + let shadowColor = 0; + + if (this.map.type === MapType.Cave) { + lightColor = CAVE_LIGHT; + shadowColor = CAVE_SHADOW; + } else { + lightColor = getLightColor(this.lightData, this.time); + shadowColor = getShadowColor(this.lightData, this.time); + } + + if (BETA && this.editor.customLight) { + lightColor = parseColor(this.editor.lightColor); + shadowColor = this.shadowColor; + } + + const camera = this.camera; + const width = camera.w; + const height = camera.h; + const ratio = integerPixelRatio(); + const actualScale = this.scale * ratio; + const bg = this.bg; + + colorToExistingFloatArray(light, lightColor); + + const drawOptions = this.drawOptions; + drawOptions.gameTime = this.time; + drawOptions.lightColor = lightColor; + drawOptions.shadowColor = shadowColor; + drawOptions.drawHidden = this.mod; + drawOptions.season = this.season; + + if (DEVELOPMENT) { + drawOptions.debug = this.debug; + } + + if (DEVELOPMENT || BETA) { + drawOptions.engine = this.engine; + } + + ortho(this.fboMatrix, 0, gl.drawingBufferWidth / actualScale, gl.drawingBufferHeight / actualScale, 0, 0, 1000); + TIMING && timeEnd(); + + TIMING && timeStart('ensureAllVisiblePon...'); + ensureAllVisiblePoniesAreDecoded(this.map, camera, this.paletteManager); + TIMING && timeEnd(); + + TIMING && timeStart('commit+invalidatePalettes'); + if (this.paletteManager.commit(gl)) { + invalidatePalettes(this.map.entitiesDrawable); + } + TIMING && timeEnd(); + + if (this.settings.browser.brightNight) { + lerpColor(light, white, 0.3); + } + + if (this.engine === Engine.NewLighting) { + // ... + } else if (this.disableLighting) { + ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.actualY + camera.h, camera.actualY, 0, 1000); + lerpColor(light, white, 0.1); // adjust lighting for missing lights + + // color -> screen + gl.clearColor(bg[0], bg[1], bg[2], bg[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.disable(gl.DEPTH_TEST); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + this.drawMap(this.webgl, this.map, this.viewMatrix, light, drawOptions); + } else { + ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.actualY, camera.actualY + camera.h, 0, 1000); + + TIMING && timeStart('initializeFrameBuffer'); + this.initializeFrameBuffer(this.webgl, width, height); + TIMING && timeEnd(); + + if (!frameBuffer) { + DEVELOPMENT && console.warn('No frame buffer'); + return; + } + + // color -> fbo + TIMING && timeStart('color -> fbo'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer.handle); + bindFrameBuffer(gl, frameBuffer); + gl.viewport(0, 0, frameBuffer.width, frameBuffer.height); + gl.clearColor(bg[0], bg[1], bg[2], bg[3]); + gl.clear(gl.COLOR_BUFFER_BIT); // | gl.DEPTH_BUFFER_BIT); + gl.viewport(0, 0, width, height); + gl.disable(gl.DEPTH_TEST); + //gl.depthFunc(gl.LEQUAL); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + this.drawMap(this.webgl, this.map, this.viewMatrix, white, drawOptions); + TIMING && timeEnd(); + + // color -> screen + TIMING && timeStart('color -> screen'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, null); + unbindFrameBuffer(gl); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + // gl.disable(gl.DEPTH_TEST); + gl.disable(gl.BLEND); + + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + gl.uniform1f(spriteShader.uniforms.textureSize, frameBufferSheet.texture!.width); + bindTexture(gl, 0, frameBufferSheet.texture); + spriteBatch.begin(); + spriteBatch.drawImage(WHITE, 0, 0, width, height, 0, 0, width, height); + spriteBatch.end(); + TIMING && timeEnd(); + + // light -> fbo + TIMING && timeStart('light -> fbo'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer.handle); + bindFrameBuffer(gl, frameBuffer); + gl.viewport(0, 0, frameBuffer.width, frameBuffer.height); + gl.clearColor(light[0], light[1], light[2], light[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.viewport(0, 0, width, height); + //gl.enable(gl.DEPTH_TEST); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE); + TIMING && timeEnd(); + + // shadows + //for (const e of map.entities) { + // if (e.drawShadow && camera.isBoundVisible(e.shadowBounds || e.bounds, e.x, e.y)) { + // this.spriteBatch.depth = camera.mapDepth(e.y); + // e.drawShadow(this.spriteBatch); + // } + //} + + // soft lights + TIMING && timeStart('drawEntityLights'); + gl.useProgram(lightShader.program); + gl.uniformMatrix4fv(lightShader.uniforms.transform, false, this.viewMatrix); + gl.uniform4fv(lightShader.uniforms.lighting, white); + spriteBatch.begin(); + drawEntityLights(spriteBatch, this.map.entitiesLight, this.camera, drawOptions); + spriteBatch.end(); + TIMING && timeEnd(); + + // light sprites + TIMING && timeStart('drawEntityLightSprites'); + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.viewMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + drawEntityLightSprites(spriteBatch, this.map.entitiesLightSprite, this.camera, drawOptions); + spriteBatch.end(); + TIMING && timeEnd(); + + // light -> screen + TIMING && timeStart('light -> screen'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, null); + unbindFrameBuffer(gl); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.DST_COLOR, gl.ZERO); + + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + gl.uniform1f(spriteShader.uniforms.textureSize, frameBufferSheet.texture!.width); + bindTexture(gl, 0, frameBufferSheet.texture); + spriteBatch.begin(); + spriteBatch.drawImage(WHITE, 0, 0, width, height, 0, 0, width, height); + spriteBatch.end(); + TIMING && timeEnd(); + } + + // ui -> screen + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + TIMING && timeStart('drawNames+drawChat'); + gl.useProgram(paletteShader.program); + gl.uniformMatrix4fv(paletteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(paletteShader.uniforms.lighting, white); + gl.uniform1f(paletteShader.uniforms.pixelSize, this.paletteManager.pixelSize); + gl.uniform1f(paletteShader.uniforms.textureSize, paletteSpriteSheet.texture!.width); + bindTexture(gl, 0, paletteSpriteSheet.texture); + bindTexture(gl, 1, this.paletteManager.texture); + paletteBatch.begin(); + + if (!this.hideText) { + drawNames( + paletteBatch, this.map.entitiesWithNames, this.player, this.party, this.camera, this.hover, this.mod, palettes); + drawChat( + paletteBatch, this.map.entitiesWithChat, this.camera, this.mod, palettes, this.hidePublicChat); + } + + if (!this.socket || !this.socket.isConnected) { + this.drawMessage(this.webgl, 'Connecting...'); + } else if (!this.loaded) { + if (this.placeInQueue) { + this.drawMessage(this.webgl, `Waiting in queue (${this.placeInQueue})`); + } else { + this.drawMessage(this.webgl, 'Loading...'); + } + } else if ((performance.now() - this.socket.lastPacket) > CONNECTION_ISSUE_TIMEOUT) { + // this.drawMessage('Connection issues...'); + } + + if (BETA && this.debug.showInfo) { + try { + const scale = this.getPixelScale(); + const x = this.input.pointerX / scale + 5; + const y = this.input.pointerY / scale; + const height = getMapHeightAt(this.map, this.hover.x, this.hover.y, this.time); + drawText(paletteBatch, `${height.toFixed(2)}`, fontSmallPal, BLACK, x, y); + } catch (e) { + console.warn(e.message); + } + } + + if (this.showWallPlaceholder) { + const x = this.hover.x | 0; + const y = this.hover.y | 0; + const dx = this.hover.x - x; + const dy = this.hover.y - y; + const palette = this.webgl.palettes.defaultPalette; + const color = makeTransparent(WHITE, 0.6); + const screenX = toScreenX(x) - this.camera.x; + const screenY = toScreenY(y) - this.camera.actualY; + + if (x >= 0 && y >= 0 && x < this.map.width && y < this.map.height) { + if (dx > dy) { + if ((dx + dy) < 1) { + paletteBatch.drawSprite(wall_h_placeholder.color, color, palette, screenX, screenY - 15); + } else { + paletteBatch.drawSprite(wall_v_placeholder.color, color, palette, screenX + tileWidth - 4, screenY - 12); + } + } else { + if ((dx + dy) < 1) { + paletteBatch.drawSprite(wall_v_placeholder.color, color, palette, screenX - 4, screenY - 12); + } else { + paletteBatch.drawSprite(wall_h_placeholder.color, color, palette, screenX, screenY + tileHeight - 15); + } + } + } + } + + paletteBatch.end(); + TIMING && timeEnd(); + + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + + if (BETA && this.showMinimap && this.minimap) { + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + spriteBatch.save(); + + const { width, height, data } = this.minimap; + const scale = 4 / this.scale; + + spriteBatch.translate(100, 100); + spriteBatch.scale(scale, scale); + + spriteBatch.drawRect(BLACK, -1, -1, width + 2, height + 2); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + spriteBatch.drawRect(data[x + y * width], x, y, 1, 1); + } + } + + if (this.player) { + spriteBatch.drawRect(RED, Math.floor(this.player.x), Math.floor(this.player.y), 1, 1); + } + + spriteBatch.restore(); + spriteBatch.end(); + } + + if (BETA && this.debug.showRegions && this.player) { + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + drawDebugRegions(spriteBatch, this.map, this.player, this.camera); + spriteBatch.end(); + } + + const showFPS = !!this.settings.browser.showFps; + const showHelp = BETA && this.input.isPressed(Key.F1); + const showPalette = DEVELOPMENT && this.debug.showPalette; + + if (showFPS || showHelp || showPalette) { + // 1 to 1 pixel scale drawing + TIMING && timeStart('showFps'); + const scale = 2; + // const height = gl.drawingBufferHeight / (ratio * scale); + ortho(this.fboMatrix, 0, gl.drawingBufferWidth / ratio, gl.drawingBufferHeight / ratio, 0, 0, 1000); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + spriteBatch.save(); + spriteBatch.scale(scale, scale); + + if (showFPS) { + drawText(spriteBatch, this.drawFps.toFixed(), fontSmall, BLACK, 2, 2); + + if (this.timingsText) { + const size = measureText(this.timingsText, fontMono); + spriteBatch.drawRect(0x000000aa, 2, 26, 220, size.h + 8); + drawText(spriteBatch, this.timingsText, fontMono, WHITE, 2, 30); + } + } + + if (BETA && showHelp) { + let y = 25; + + for (const shortcut of this.debugShortcuts) { + drawOutlinedText(spriteBatch, shortcut, font, WHITE, BLACK, 5, y); + y += 10; + } + } + + // if (DEVELOPMENT) { + // const width = gl.drawingBufferWidth / (ratio * scale); + // const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); + // const text = + // `${isCollidingCount.toString().padStart(7)} calls\n` + + // `${isCollidingObjectCount.toString().padStart(7)} total checks\n` + + // `${this.markedColliding.toString().padStart(7)} player checks`; + // const size = measureText(text, fontMono); + // const x = width - 160; + // const y = 26; + // spriteBatch.drawRect(0x000000aa, x, y, 150, size.h + 10); + // drawText(spriteBatch, text, fontMono, WHITE, x + 5, y + 5); + // } + + spriteBatch.restore(); + spriteBatch.end(); + + if (DEVELOPMENT && showPalette) { + const paletteTexture = this.paletteManager.texture!; + const { width, height } = paletteTexture; + + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + spriteBatch.drawRect(0x00000066, 20, 20, width, height); + spriteBatch.end(); + + gl.uniform1f(spriteShader.uniforms.textureSize, width); + bindTexture(gl, 0, paletteTexture); + spriteBatch.begin(); + spriteBatch.drawImage(WHITE, 0, 0, width, height, 20, 20, width, height); + spriteBatch.end(); + } + + TIMING && timeEnd(); + } + + bindTexture(gl, 0, undefined); + bindTexture(gl, 1, undefined); + gl.useProgram(null); + + TIMING && timeEnd(); + this.updateStatsText(); + + TIMING && timeStart('messageQueue'); + while (this.messageQueue.length) { + this.onMessage.next(this.messageQueue.shift()!); + } + TIMING && timeEnd(); + + TIMING && timeStart('onFrame'); + this.onFrame.next(); + TIMING && timeEnd(); + } + private drawMessage({ paletteBatch, palettes }: WebGL, message: string) { + drawFullScreenMessage(paletteBatch, this.camera, message, palettes.mainFont.white); + } + private drawMap(webgl: WebGL, map: WorldMap, viewMatrix: Matrix4, lighting: Float32Array, options: DrawOptions) { + const { gl, paletteBatch, paletteShader } = webgl; + + TIMING && timeStart('drawMap'); + if (this.tileSets && this.player) { + gl.useProgram(paletteShader.program); + gl.uniformMatrix4fv(paletteShader.uniforms.transform, false, viewMatrix); + gl.uniform4fv(paletteShader.uniforms.lighting, lighting); + gl.uniform1f(paletteShader.uniforms.pixelSize, this.paletteManager.pixelSize); + gl.uniform1f(paletteShader.uniforms.textureSize, paletteSpriteSheet.texture!.width); + bindTexture(gl, 0, paletteSpriteSheet.texture); + bindTexture(gl, 1, this.paletteManager.texture); + paletteBatch.begin(); + this.entitiesDrawn = drawMap( + paletteBatch, map, this.camera, this.player, options, this.tileSets, this.editor.selectedEntities); + paletteBatch.end(); + + if (this.highlightEntity && this.highlightEntity.draw) { + gl.uniform4fv(paletteShader.uniforms.lighting, highlightColor); + paletteBatch.begin(); + this.highlightEntity.draw(paletteBatch, this.drawOptions); + paletteBatch.end(); + } + } + TIMING && timeEnd(); + } + private updateCameraShift() { + if (isMobile) { + const isKeyboardOpen = !!document.activeElement && /input/i.test(document.activeElement.tagName); + + if (this.lastIsKeyboardOpen !== isKeyboardOpen) { + DEVELOPMENT && log(`keyboard open ${isKeyboardOpen}`); + this.lastIsKeyboardOpen = isKeyboardOpen; + } + + if (isKeyboardOpen) { + if (!this.cameraShiftOn && window.scrollY > 100) { + this.cameraShiftOn = true; + this.cameraShiftTarget = window.scrollY; + + if (DEVELOPMENT) { + log(`shift camera ${this.cameraShiftTarget} (${this.windowHeight} - ${window.innerHeight}, ${window.scrollY})`); + } + } + } else { + if (this.cameraShiftOn && window.scrollY < 100) { + this.cameraShiftOn = false; + DEVELOPMENT && log(`unshift camera`); + } + } + } + } + private resizeCanvas() { + pixelRatioCache = getPixelRatio(); + const canvas = this.canvas; + const ratio = pixelRatio(); + const rect = this.element!.getBoundingClientRect(); + this.windowWidth = rect.width; + this.windowHeight = rect.height; + let w = Math.ceil(this.windowWidth * ratio); + let h = Math.ceil(this.windowHeight * ratio); + + while ((w % 12) !== 0) { + w++; + } + + while ((h % 12) !== 0) { + h++; + } + + if (canvas && w && h && (canvas.width !== w || canvas.height !== h || this.lastCanvasRatio !== ratio)) { + canvas.width = w; + canvas.height = h; + canvas.style.width = `${w / ratio}px`; + canvas.style.height = `${h / ratio}px`; + this.lastCanvasRatio = ratio; + this.resized = false; + DEVELOPMENT && log(`scrollY: ${window.scrollY}`); + } + } + private initializeFrameBuffer({ gl, frameBuffer }: WebGL, width: number, height: number) { + const targetSize = getRenderTargetSize(width, height); + + if (frameBuffer && targetSize !== frameBuffer.width) { + const maxSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + + if (maxSize != null && targetSize > maxSize) { + this.setScale(this.scale + 1); + } else { + resizeFrameBuffer(gl, frameBuffer, targetSize, targetSize); + } + } + } + private announce(announcement: string) { + this.announcements.next(announcement); + } + changePlaceEntity(reverse: boolean) { + if (reverse) { + this.placeEntity = this.placeEntity === 0 ? (placeableEntities.length - 1) : (this.placeEntity - 1); + } else { + this.placeEntity = (this.placeEntity + 1) % placeableEntities.length; + } + + const { name } = placeableEntities[this.placeEntity]; + const total = getSaysTime(name); + addChatBubble(this.map, this.player!, { message: name, type: MessageType.System, total, timer: total, created: Date.now() }); + } + changePlaceTile(reverse: boolean) { + if (reverse) { + this.placeTile = this.placeTile === 0 ? (houseTiles.length - 1) : (this.placeTile - 1); + } else { + this.placeTile = (this.placeTile + 1) % houseTiles.length; + } + + const { name } = houseTiles[this.placeTile]; + const total = getSaysTime(name); + addChatBubble(this.map, this.player!, { message: name, type: MessageType.System, total, timer: total, created: Date.now() }); + } + saveDebug() { + this.storage.setJSON('debug', this.debug); + } + private updateStatsText() { + const { gl, spriteBatch, paletteBatch } = this.webgl!; + + if ((performance.now() - this.lastStats) > SECOND) { + TIMING && timingCollate(); + + if (TIMING) { + const timings = timingCollate(); + this.timingsText = timings + .map(({ selfTime, selfPercent, totalPercent, count, name }) => + `${selfTime.toFixed(2).padStart(6)}ms` + + `${selfPercent.toFixed(2).padStart(6)}%` + + `${totalPercent.toFixed(2).padStart(6)}%` + + `${count.toString().padStart(6)} ${name}`) + .join('\n'); + } + + if (this.statsText) { + let value = ''; + + if (this.settings.browser.showStats) { + const tris = spriteBatch.tris + paletteBatch.tris; + const flush = paletteBatch.flushes; + const sent = this.sent.toFixed(); + const recv = this.recv.toFixed(); + const drawn = this.entitiesDrawn; + const total = this.map.entities.length; + const ponies = this.map.entities.reduce((sum, e) => sum + (e.type === PONY_TYPE ? 1 : 0), 0); + const extra = DEVELOPMENT ? `(${drawn}/${total}) ${tris} tris, ${flush} flush, ${this.audio.trackName}` : version; + const gl2 = isWebGL2(gl) ? ' WebGL2' : ''; + const engine = this.engine === Engine.Default ? '' : Engine[this.engine].toUpperCase(); + const fps = this.drawFps.toFixed(0); + const low = this.disableLighting ? ' LOW' : ''; + const extraStats = this.extraStats; + const palSize = ` pal ${this.paletteManager.textureSize}`; + value = `${extraStats}${engine} ${fps} fps ${sent}/${recv} kb/s ${ponies} ` + + `ponies ${extra}${gl2}${low}${palSize}`.trim(); + } + + if (value !== this.statsTextValue) { + this.statsText.nodeValue = value; + this.statsTextValue = value; + } + } + + this.lastStats = performance.now(); + this.onClock.next(formatHourMinutes(this.time)); + } + + TIMING && timeReset(); + + spriteBatch!.tris = 0; + spriteBatch!.flushes = 0; + paletteBatch!.tris = 0; + paletteBatch!.flushes = 0; + } +} diff --git a/src/ts/client/gameLoop.ts b/src/ts/client/gameLoop.ts new file mode 100644 index 0000000..78e66eb --- /dev/null +++ b/src/ts/client/gameLoop.ts @@ -0,0 +1,89 @@ +export interface Game { + fps: number; + load(): any; + init(): void; + update(delta: number, now: number, last: number): void; + draw(): void; +} + +export interface GameLoop { + started: Promise; + cancel(): void; +} + +let gameLoop: GameLoop | undefined = undefined; + +export function startGameLoop(game: Game, onError = (e: Error) => console.error(e)): GameLoop { + let handle: any; + let backup: any; + let cancelled = false; + + let last = Math.round(performance.now()); + let lastFps = last; + let frames = 0; + let fps = 0; + + function step(now: number, draw: boolean) { + if (draw) { + handle = requestAnimationFrame(onFrame); + } + + frames++; + + if ((now - lastFps) > 1000) { + fps = frames * 1000 / (now - lastFps); + frames = 0; + lastFps = now; + } + + try { + game.fps = fps; + game.update((now - last) / 1000, now, last); + + if (draw) { + game.draw(); + } + } catch (e) { + onError(e); + } + + last = now; + } + + function onTimer() { + step(Math.round(performance.now()), false); + backup = setTimeout(onTimer, 1000 / 10); + } + + function onFrame() { + clearTimeout(backup); + step(Math.round(performance.now()), true); + backup = setTimeout(onTimer, 1000 / 10); + } + + function cancel() { + cancelAnimationFrame(handle); + clearTimeout(backup); + cancelled = true; + } + + if (gameLoop) { + gameLoop.cancel(); + } + + const started = Promise.resolve() + .then(() => game.load()) + .then(() => { + if (cancelled) { + throw new Error('Cancelled (loop)'); + } else { + game.init(); + handle = requestAnimationFrame(onFrame); + backup = setTimeout(onTimer, 1000 / 10); + } + }); + + gameLoop = { started, cancel }; + + return gameLoop; +} diff --git a/src/ts/client/gameUtils.ts b/src/ts/client/gameUtils.ts new file mode 100644 index 0000000..b7db00e --- /dev/null +++ b/src/ts/client/gameUtils.ts @@ -0,0 +1,49 @@ +import { Notification } from '../common/interfaces'; +import { PonyTownGame } from './game'; +import { removeById } from '../common/utils'; + +export function addNotification({ notifications }: PonyTownGame, notification: Notification) { + const open = notifications.length === 0; + + notifications.push(notification); + + setTimeout(() => { + notification.open = open; + notification.fresh = false; + }, 500); +} + +export function removeNotification({ notifications }: PonyTownGame, id: number) { + const notification = removeById(notifications, id); + + if (notification && notification.open && notifications.length) { + notifications[0].open = true; + } +} + +export function resetGameFields(game: PonyTownGame) { + game.loaded = false; + game.placeInQueue = 0; + game.playerId = undefined; + game.playerName = undefined; + game.playerInfo = undefined; + game.playerCRC = undefined; + game.party = undefined; + game.whisperTo = undefined; + game.messageQueue = []; + game.lastWhisperFrom = undefined; + game.onPartyUpdate.next(); + game.fallbackPonies.clear(); +} + +export function markGameAsLoaded(game: PonyTownGame) { + if (!game.loaded) { + game.loaded = true; + game.fullyLoaded = false; + setTimeout(() => game.fullyLoaded = true, 300); + } +} + +export function isSelected(game: PonyTownGame, id: number) { + return game.selected && game.selected.id === id; +} diff --git a/src/ts/client/handlers.ts b/src/ts/client/handlers.ts new file mode 100644 index 0000000..3899d07 --- /dev/null +++ b/src/ts/client/handlers.ts @@ -0,0 +1,802 @@ +import { compact, escapeRegExp, repeat } from 'lodash'; +import { createBinaryReader, readUint8, readUint32, readUint16 } from 'ag-sockets/dist/browser'; +import { decodeString } from 'ag-sockets/dist/utf8'; +import { + Entity, Region, Pony, EntityState, PonyOptions, MessageType, isNonIgnorableMessage, EntityPlayerState, + EntityOrPonyOptions, isPublicMessage, FakeEntity, Action, WorldMap, DoAction, UpdateType, DecodedUpdate, + PonyData, WorldStateFlags, FriendStatusData, FriendStatusFlags, isWhisper, isWhisperTo, +} from '../common/interfaces'; +import { bitmask, setFlag, findById, distance, hasFlag, distanceXY, invalidEnum, removeItem } from '../common/utils'; +import { isChatVisible } from '../common/camera'; +import { createRegion, worldToRegionX, worldToRegionY } from '../common/region'; +import { createAnEntity, poof, poof2 } from '../common/entities'; +import { getPonyState, setPonyState, isPonyFlying, addChatBubble, isHidden } from '../common/entityUtils'; +import { + isPony, createPony, setPonyExpression, updatePonyInfo, updatePonyHold, doPonyAction, hasHeadAnimation, + setHeadAnimation, + doBoopPonyAction +} from '../common/pony'; +import { PonyTownGame } from './game'; +import { setupPlayer, savePlayerPosition } from './sec'; +import { PONY_INFO_KEY, FLY_DELAY, isChatlogRangeUnlimited, SECOND, PONY_TYPE } from '../common/constants'; +import { getSaysTime, containsCyrillic } from './clientUtils'; +import { dismissSays } from '../graphics/graphicsUtils'; +import { decodeUpdate, readOneUpdate } from '../common/encoders/updateDecoder'; +import { updateEntityVelocity } from '../common/entityUtils'; +import { decodePonyInfo } from '../common/compressPony'; +import { mockPaletteManager } from '../common/ponyInfo'; +import { yawn, laugh, sneeze } from './ponyAnimations'; +import { + findEntityById, getRegionGlobal, setTile, removeEntity, addEntity, removeEntityDirectly, setRegion, + addEntityToMapRegion, switchEntityRegion, getRegionUnsafe, addOrRemoveFromEntityList, +} from '../common/worldMap'; +import { isSelected } from './gameUtils'; +import { compareFriends } from '../components/services/model'; +import { canCollideWith } from '../common/collision'; +import { hasDrawLight, hasLightSprite } from './draw'; + +function log(message: string) { + if (DEVELOPMENT && !TESTS) { + console.error(message); + } +} + +function handleAddEntity(game: PonyTownGame, region: Region, update: DecodedUpdate, initial: boolean) { + const { + id, type = 0, x = 0, y = 0, vx = 0, vy = 0, state = 0, playerState = 0, options = {}, + name, filterName, info, crc = 0, action // , expression + } = update; + + const filteredName = filterEntityName(game, name, filterName); + const entity = createEntityOrPony(game, type, id, x, y, options, crc, filteredName, info, state); + + entity.id = id; + entity.x = x; + entity.y = y; + entity.vx = vx; + entity.vy = vy; + entity.playerState = playerState || EntityPlayerState.None; + + addEntityToMapRegion(game.map, region, entity); + + if (isPony(entity)) { + if (id === game.playerId) { + game.apply(() => setupPlayer(game, entity)); + } + + if (isSelected(game, id)) { + game.select(entity); + } + + if (game.whisperTo && game.whisperTo.id === id) { + game.whisperTo = entity; + } + + if (!initial) { + game.onPonyAddOrUpdate.next(entity); + } + } + + if (action !== undefined) { + handleAction(game, id, action); + } +} + +export function handleUpdateEntity(game: PonyTownGame, update: DecodedUpdate) { + const { + id, x, y, vx, vy, state, playerState, expression, options, switchRegion, + name, filterName, info, crc = 0, action + } = update; + + const filteredName = filterEntityName(game, name, filterName); + const entity = findEntityByIdInGame(game, id); + + if (entity) { + const isPlayer = id === game.playerId; + + if (x !== undefined && y !== undefined) { + if (switchRegion) { + if (DEVELOPMENT && isPlayer && !getRegionGlobal(game.map, x, y)) { + console.error(`Switching player to unsubscribed region`); + } + + switchEntityRegion(game.map, entity, x, y); + } + + if (!isPlayer) { + // if (DEVELOPMENT && isPony(entity)) { + // const dx = entity.x - x; + // const dy = entity.y - y; + // console.log(`adjust x: [${num(dx)}] (${ms(dx)}) y: [${num(dy)}] (${ms(dy)})`); + // } + + entity.x = x; + entity.y = y; + updateEntityVelocity(game.map, entity, vx, vy); + + if (canCollideWith(entity)) { + const rx = worldToRegionX(entity.x, game.map); + const ry = worldToRegionY(entity.y, game.map); + + for (let y = -1; y <= 1; y++) { + for (let x = -1; x <= 1; x++) { + const region = getRegionUnsafe(game.map, rx + x, ry + y); + + if (region) { + region.colliderDirty = true; + } + } + } + } + } else if (distanceXY(entity.x, entity.y, x, y) > 8) { + log(`Fixing player position (${entity.x}, ${entity.y}) => (${x}, ${y})`); + entity.x = x; + entity.y = y; + savePlayerPosition(); + } + } + + if (state !== undefined) { + updateEntityStateInternal(game, entity, state); + } + + if (playerState !== undefined) { + updateEntityPlayerStateInternal(game, entity, playerState); + } + + if (expression !== undefined && isPony(entity)) { + setPonyExpression(entity, expression); + } + + if (options != null) { + updateEntityOptionsInternal(entity, options, game); + } + + if (filteredName !== undefined && !isPlayer) { + entity.name = filteredName; + } + + if (info !== undefined && !isPlayer) { + const ponyInfo = bitmask(info, PONY_INFO_KEY); + + if (entity.fake) { + (entity as Pony).palettePonyInfo = decodePonyInfo(ponyInfo, mockPaletteManager); + } else { + updatePonyInfoWithPoof(game, entity, ponyInfo, crc); + } + } + + if (action !== undefined) { + handleAction(game, id, action); + } + + applyIfSelected(game, id); + } else { + log(`handleUpdateEntity: missing entity: ${id}`); + } +} + +export function handleUpdatePonies(game: PonyTownGame, ponies: PonyData[]) { + for (const [id, options = {}, name, info, playerState, nameBad] of ponies) { + const decodedName = name && decodeString(name) || undefined; + const filteredName = filterEntityName(game, decodedName, nameBad); + const decodedInfo = info ? bitmask(info, PONY_INFO_KEY) : ''; + const pony = createPonyEntity(game, id, options, filteredName, decodedInfo, EntityState.None); + pony.playerState = playerState; + game.fallbackPonies.set(pony.id, pony); + } + + const missing = game.party && game.party.members.filter(p => !p.pony); + + if (missing && missing.length) { + game.apply(() => missing.forEach(p => p.pony = game.fallbackPonies.get(p.id))); + } +} + +function createPonyEntity( + game: PonyTownGame, id: number, options: PonyOptions, name: string | undefined, info: string | Uint8Array, + state: EntityState +) { + if (!game.webgl) { + throw new Error('WebGL not initialized'); + } + + const pony = createPony(id, state, info, game.webgl.palettes.defaultPalette, game.paletteManager); + + if (name) { + pony.name = name; + } + + updateEntityOptionsInternal(pony, options, game); + + // bypass name/info filtering for player pony + if (id === game.playerId) { + if (game.playerName) { + pony.name = game.playerName; + } + + if (game.playerInfo) { + pony.crc = game.playerCRC; + updatePonyInfo(pony, game.playerInfo, game.applyChanges); + } + } + + return pony; +} + +function updateEntityStateInternal(game: PonyTownGame, entity: Entity, state: EntityState) { + if (entity === game.player) { + const right = game.rightOverride; + const headTurned = game.headTurnedOverride; + const stateOverride = game.stateOverride; + + if (right !== undefined) { + state = setFlag(state, EntityState.FacingRight, right); + game.rightOverride = undefined; + } + + if (headTurned !== undefined) { + state = setFlag(state, EntityState.HeadTurned, headTurned); + game.headTurnedOverride = undefined; + } + + if (stateOverride !== undefined) { + if (stateOverride !== getPonyState(state)) { + state = setPonyState(state, stateOverride); + } + + game.stateOverride = undefined; + } + + game.onActionsUpdate.next(); + } + + const wasPonyFlying = isPonyFlying(entity); + const hadLight = hasDrawLight(entity); + const hadLightSprite = hasLightSprite(entity); + + entity.state = state; + + if (!wasPonyFlying && isPonyFlying(entity) && isPony(entity)) { + entity.inTheAirDelay = FLY_DELAY; + } + + const hasLight = hasDrawLight(entity); + const hasLightSprite1 = hasLightSprite(entity); + + addOrRemoveFromEntityList(game.map.entitiesLight, entity, hadLight, hasLight); + addOrRemoveFromEntityList(game.map.entitiesLightSprite, entity, hadLightSprite, hasLightSprite1); +} + +function updateEntityPlayerStateInternal(game: PonyTownGame, entity: Entity, playerState: EntityPlayerState) { + if (!entity.fake && !isHidden(entity) && hasFlag(playerState, EntityPlayerState.Hidden)) { + playEffect(game, entity, poof.type); + + if (isSelected(game, entity.id)) { + game.select(undefined); + } + } + + entity.playerState = playerState; +} + +function findEntityByIdInGame(game: PonyTownGame, id: number) { + let entity = findEntityById(game.map, id); + + if (!entity && isSelected(game, id)) { + entity = game.selected; + } + + return entity; +} + +function applyIfSelected(game: PonyTownGame, id: number) { + if (isSelected(game, id)) { + game.applyChanges(); + } +} + +export function handleUpdates(game: PonyTownGame, updates: Uint8Array) { + const reader = createBinaryReader(updates); + + while (reader.offset < reader.view.byteLength) { + const type = readUint8(reader) as UpdateType; + + switch (type) { + case UpdateType.None: + log(`handleUpdates (none)`); + break; + case UpdateType.AddEntity: { + const update = readOneUpdate(reader)!; + const { x = 0, y = 0 } = update; + const region = getRegionGlobal(game.map, x, y); + + if (region) { + handleAddEntity(game, region, update, false); + } else { + log(`handleUpdates (add): missing region at ${x} ${y}`); + } + break; + } + case UpdateType.UpdateEntity: { + const update = readOneUpdate(reader)!; + handleUpdateEntity(game, update); + break; + } + case UpdateType.RemoveEntity: { + const id = readUint32(reader); + handleRemoveEntity(game, id); + break; + } + case UpdateType.UpdateTile: { + const x = readUint16(reader); + const y = readUint16(reader); + const type = readUint8(reader); + setTile(game.map, x, y, type); + break; + } + default: + invalidEnum(type); + } + } +} + +export function updatePonyInfoWithPoof(game: PonyTownGame, entity: Entity, info: string | Uint8Array, crc: number) { + const update = (pony: Pony) => { + pony.crc = crc; + updatePonyInfo(pony, info, game.applyChanges); + game.onPonyAddOrUpdate.next(pony); + }; + + if (entity && isPony(entity)) { + if (isHidden(entity)) { + update(entity); + } else { + playEffect(game, entity, poof2.type); + setTimeout(() => update(entity), 100); + } + } +} + +export function handleRemoveEntity(game: PonyTownGame, id: number) { + const entity = findEntityById(game.map, id); + + if (entity) { + removeEntity(game.map, entity); + } else { + log(`handleRemoveEntity: Missing entity: ${id}`); + } + + if (id === game.playerId) { + log(`handleRemoveEntity: Removing player`); + } + + if (entity && entity.type === PONY_TYPE) { + playEffect(game, entity, poof.type); + } + + if (isSelected(game, id)) { + setTimeout(() => { + if (isSelected(game, id)) { + game.select(undefined); + } + }, 15 * SECOND); + } +} + +function findPonyById(map: WorldMap, id: number) { + const entity = findEntityById(map, id); + return entity && isPony(entity) ? entity : undefined; +} + +export function handleAction(game: PonyTownGame, id: number, action: Action) { + const pony = findPonyById(game.map, id); + + if (pony) { + switch (action) { + case Action.Boop: + doBoopPonyAction(game, pony); + break; + case Action.HoldPoof: + doPonyAction(pony, DoAction.HoldPoof); + break; + case Action.Yawn: + if (!hasHeadAnimation(pony)) { + setHeadAnimation(pony, yawn); + } + break; + case Action.Laugh: + if (!hasHeadAnimation(pony)) { + setHeadAnimation(pony, laugh); + } + break; + case Action.Sneeze: + if (!hasHeadAnimation(pony)) { + setHeadAnimation(pony, sneeze); + } + break; + default: + log(`handleAction: Invalid action: ${action}`); + } + } else { + log(`handleAction: Missing entity: ${id}`); + } +} + +export function playEffect(game: PonyTownGame, target: Entity, type: number) { + if (isHidden(target)) + return; + + try { + const entity = createAnEntity(type, 0, target.x, target.y, {}, game.paletteManager, game); + addEntity(game.map, entity); + setTimeout(() => removeEntityDirectly(game.map, entity), 1000); + } catch (e) { + DEVELOPMENT && console.error(e); + } +} + +export function findEntityOrMockByAnyMeans(game: PonyTownGame, id: number) { + if (!id) { + return undefined; + } + + let entity: Entity | FakeEntity | undefined = findEntityById(game.map, id); + + if (!entity && game.party) { + const member = findById(game.party.members, id); + entity = member && member.pony; + } + + if (!entity) { + const friend = game.model.friends && game.model.friends.find(f => f.entityId === id); + + if (friend) { + entity = { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc }; + } + } + + if (!entity) { + entity = game.findEntityFromChatLog(id); + } + + return entity; +} + +export function findBestEntityByName(game: PonyTownGame, name: string): Entity | FakeEntity | undefined { + const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i'); + + if (game.model.friends) { + for (const friend of game.model.friends) { + if (friend.online && friend.entityId && regex.test(friend.actualName)) { + return { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc }; + } + } + } + + let result: Entity | FakeEntity | undefined = undefined; + + if (game.player) { + for (const entity of game.map.entities) { + if (entity.type === PONY_TYPE && entity.id !== game.playerId && !isHidden(entity) && entity.name && regex.test(entity.name)) { + if (!result || (distance(game.player, entity) < distance(game.player, result))) { + result = entity; + } + } + } + } + + if (!result) { + result = game.findEntityFromChatLogByName(name); + } + + return result; +} + +export function findMatchingEntityNames(game: PonyTownGame, match: string): string[] { + const result: string[] = []; + const ids = new Set(); + const regex = new RegExp(`^${escapeRegExp(match)}`, 'i'); + + if (game.model.friends) { + for (const friend of game.model.friends) { + if (friend.online && friend.entityId && friend.actualName && regex.test(friend.actualName)) { + ids.add(friend.entityId); + result.push(friend.actualName); + } + } + } + + for (const entity of game.map.entities) { + if ( + entity.type === PONY_TYPE && + entity.id !== game.playerId && + entity.name && + !isHidden(entity) && + regex.test(entity.name) && + !ids.has(entity.id) + ) { + result.push(entity.name); + } + } + + return result; +} + +let cachedFilter: string | undefined = undefined; +let cachedRegex: RegExp | undefined = undefined; + +export function containsFilteredWords(message: string, filter: string | undefined) { + if (cachedFilter !== filter) { + if (filter) { + const words = compact(filter.replace(/[,]/g, ' ').split(/[\r\n\t ]+/g).map(x => x.trim())); + cachedRegex = new RegExp(`(^| )(${words.map(escapeRegExp).join('|')})($| )`, 'i'); + } else { + cachedRegex = undefined; + } + + cachedFilter = filter; + } + + return cachedRegex && cachedRegex.test(message); +} + +export function handleSays(game: PonyTownGame, id: number, message: string, type: MessageType) { + const entity = findEntityOrMockByAnyMeans(game, id); + + if (entity) { + handleSay(game, entity, message, type); + } else { + DEVELOPMENT && console.warn('incomplete say'); + game.incompleteSays.push({ id, message, type, time: Date.now() }); + game.send(server => server.actionParam2(Action.RequestEntityInfo, id)); + } +} + +function isFriendEntityId(game: PonyTownGame, id: number) { + if (game.model.friends) { + for (const friend of game.model.friends) { + if (friend.entityId === id) { + return true; + } + } + } + + return false; +} + +function shouldShowChatMessage(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType): boolean { + if (entity === game.player) + return true; + + if (isWhisperTo(type)) + return true; + + if (isWhisper(type) && isFriendEntityId(game, entity.id)) + return true; + + if (isPublicMessage(type) && !entity.fake && !isChatVisible(game.camera, entity)) + return false; + + if (isNonIgnorableMessage(type)) + return true; + + if (game.settings.account.filterCyrillic && containsCyrillic(message)) + return false; + + if (game.settings.account.ignorePublicChat && isPublicMessage(type)) + return false; + + if (isWhisper(type) && game.settings.account.ignoreNonFriendWhispers) + return false; + + if (containsFilteredWords(message, game.settings.account.filterWords)) + return false; + + return true; +} + +function isChatInRange(entity: Entity, player: Entity | undefined, range: number | undefined) { + return player === undefined || isChatlogRangeUnlimited(range) || distance(entity, player) < range!; +} + +function shouldShowChatMessageInChatlog(game: PonyTownGame, entity: Entity | FakeEntity, type: MessageType) { + if (entity.type !== PONY_TYPE) + return false; + + if (entity.fake) + return true; + + if (!isPublicMessage(type)) + return true; + + if (!isChatInRange(entity, game.player, game.settings.account.chatlogRange)) + return false; + + return true; +} + +export function handleSay(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType) { + if (!shouldShowChatMessage(game, entity, message, type)) + return; + + if (type === MessageType.Dismiss || message === '.') { + if (!entity.fake && entity.says) { + dismissSays(entity.says); + } + } else { + const bubbleEntity = isWhisperTo(type) ? game.player : entity; + + if (bubbleEntity && !bubbleEntity.fake && game.map.entitiesById.has(bubbleEntity.id)) { + const total = getSaysTime(message); + addChatBubble(game.map, bubbleEntity, { message, type, total, timer: total, created: Date.now() }); + } + + if (isWhisper(type)) { + const friend = game.model.friends && game.model.friends.find(f => f.entityId === entity.id); + game.lastWhisperFrom = { entityId: entity.id, accountId: friend && friend.accountId }; + } + + if (shouldShowChatMessageInChatlog(game, entity, type)) { + const { id, name = '', crc } = entity; + game.messageQueue.push({ id, crc, name, message, type }); + } + } +} + +export function handleEntityInfo(game: PonyTownGame, id: number, name: string, crc: number, nameBad: boolean) { + name = filterEntityName(game, name, nameBad)!; + + for (let i = 0; i < game.incompleteSays.length;) { + const say = game.incompleteSays[i]; + + if (say.id === id) { + game.incompleteSays.splice(i, 1); + const entity: FakeEntity = { fake: true, type: PONY_TYPE, id, name, crc }; + handleSay(game, entity, say.message, say.type); + } else { + i++; + } + } +} + +export function subscribeRegion(game: PonyTownGame, data: Uint8Array) { + const { x, y, updates, tileData } = decodeUpdate(data); + const region = createRegion(x, y, tileData!); + const initial = !game.loaded; + setRegion(game.map, x, y, region); + + for (const update of updates) { + handleAddEntity(game, region, update, initial); + } +} + +export function filterEntityName({ settings, worldFlags }: PonyTownGame, name: string | undefined, nameBad: boolean) { + if (name && nameBad && (settings.account.filterSwearWords || hasFlag(worldFlags, WorldStateFlags.Safe))) { + return repeat('*', name.length); + } else if (name && containsFilteredWords(name, settings.account.filterWords)) { + return repeat('?', name.length); + } else { + return name; + } +} + +function createEntityOrPony( + game: PonyTownGame, type: number, id: number, x: number, y: number, options: EntityOrPonyOptions, + crc: number, name: string | undefined, info: Uint8Array | undefined, state: EntityState +): Entity { + if (type === PONY_TYPE) { + const entity = createPonyEntity(game, id, options, name, info ? bitmask(info, PONY_INFO_KEY) : '', state); + const member = game.party && game.party.members.find(p => p.id === id); + entity.crc = crc; + + if (member) { + game.apply(() => member.pony = entity); + } + + if (isSelected(game, id)) { + game.select(entity); + } + + return entity; + } else { + const entity = createAnEntity(type, id, x, y, options, game.paletteManager, game); + + entity.state = state; + + if (name) { + entity.name = name; + } + + return entity; + } +} + +function updateEntityOptionsInternal(entity: Entity, options: Partial, game: PonyTownGame) { + Object.assign(entity, options); + + if (isPony(entity) && 'hold' in options) { + updatePonyHold(entity, game); + } +} + +export function handleUpdateFriends(game: PonyTownGame, friends: FriendStatusData[], removeMissing: boolean) { + if (!game.model.friends) + return; + + for (const { accountId, accountName, status, entityId, name, info, crc, nameBad = false } of friends) { + let friend = game.model.friends.find(f => f.accountId === accountId); + + if (hasFlag(status, FriendStatusFlags.Remove)) { + if (friend) { + removeItem(game.model.friends, friend); + } + } else { + if (!friend) { + friend = { + accountId, + accountName: '', + online: false, + name: undefined, + nameBad: false, + pony: undefined, + entityId: 0, + crc: 0, + ponyInfo: undefined, + actualName: '', + }; + + game.model.friends.push(friend); + } + + friend.online = hasFlag(status, FriendStatusFlags.Online); + + if (accountName !== undefined) { + friend.accountName = accountName; + } + + if (entityId !== undefined) { + if (game.lastWhisperFrom && game.lastWhisperFrom.accountId === friend.accountId) { + game.lastWhisperFrom.entityId = entityId; + } + + game.onEntityIdUpdate.next({ old: friend.entityId, new: entityId }); + + friend.entityId = entityId; + } + + if (name !== undefined) { + friend.name = name; + friend.nameBad = nameBad; + friend.actualName = filterEntityName(game, name, nameBad) || ''; + } + + if (crc !== undefined) { + friend.crc = crc; + } + + if (info !== undefined) { + friend.pony = info; + friend.ponyInfo = decodePonyInfo(info, mockPaletteManager); + } + + if (friend.entityId && game.whisperTo && game.whisperTo.id === friend.entityId) { + game.whisperTo.name = friend.actualName; + game.whisperTo.crc = friend.crc; + } + } + } + + if (removeMissing) { + for (let i = game.model.friends.length - 1; i >= 0; i--) { + if (!friends.find(f => f.accountId === game.model.friends![i].accountId)) { + game.model.friends.splice(i, 1); + } + } + + DEVELOPMENT && console.log('Refreshing friend list'); + } + + game.model.friends.sort(compareFriends); + game.apply(() => { }); +} diff --git a/src/ts/client/htmlUtils.ts b/src/ts/client/htmlUtils.ts new file mode 100644 index 0000000..fe9ff58 --- /dev/null +++ b/src/ts/client/htmlUtils.ts @@ -0,0 +1,174 @@ +import { hasEmojis, splitEmojis, findEmoji, getEmojiImageAsync } from './emoji'; +import { Dict } from '../common/interfaces'; +import { font } from './fonts'; +import { getCharacterSprite } from '../graphics/spriteFont'; + +export function createHtmlNodes(value: string | undefined, scale: number): Node[] { + return value ? splitEmojis(value).map(x => { + const sprite = hasEmojis(x) && font && getCharacterSprite(x, font); + + if (sprite) { + const emote = findEmoji(x); + const img = document.createElement('img'); + img.className = 'pixelart'; + img.style.display = 'inline-block'; + img.style.visibility = 'hidden'; + img.style.width = `${(sprite.w + sprite.ox) * scale}px`; + img.style.height = `${10 * scale}px`; + + if (emote) { + img.setAttribute('aria-label', emote.names[0]); + } + + getEmojiImageAsync(sprite, src => { + img.alt = x; + img.src = src; + img.style.visibility = 'visible'; + }); + + return img; + } else { + return document.createTextNode(x); + } + }) : []; +} + +export function textNode(text: string) { + return document.createTextNode(text); +} + +export function element( + tag: string, className?: string, nodes?: (Node | undefined)[], attrs?: Dict, events?: Dict<() => any> +) { + const element = document.createElement(tag); + + if (className) { + element.className = className; + } + + if (nodes !== undefined) { + appendAllNodes(element, nodes); + } + + if (attrs !== undefined) { + Object.keys(attrs).forEach(key => element.setAttribute(key, attrs[key])); + } + + if (events !== undefined) { + Object.keys(events).forEach(key => element.addEventListener(key, events[key])); + } + + return element; +} + +export function appendAllNodes(element: Element, nodes: (Node | undefined)[]) { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if (node !== undefined) { + element.appendChild(node); + } + } +} + +export function removeAllNodes(element: Element) { + let child: Node | null; + + while (child = element.lastChild) { + element.removeChild(child); + } +} + +export function removeFirstChild(element: HTMLElement) { + let child: Node | null; + + if (child = element.firstChild) { + element.removeChild(child); + } +} + +export function removeElement(element: HTMLElement) { + element.parentElement && element.parentElement.removeChild(element); +} + +export function replaceNodes(element: HTMLElement, text: string) { + while (element.lastChild && element.lastChild !== element.firstChild) { + element.removeChild(element.lastChild); + } + + let firstChild = element.firstChild; + + if (!firstChild) { + element.appendChild(firstChild = textNode('')); + } + + if (hasEmojis(text)) { + firstChild.nodeValue = ''; + appendAllNodes(element, createHtmlNodes(text, 2)); + } else { + firstChild.nodeValue = text; + } +} + +export function findParentElement(element: HTMLElement, selector: string) { + const elements = Array.from(document.querySelectorAll(selector)); + let current = element.parentElement; + + while (current && elements.indexOf(current) === -1) { + current = current.parentElement; + } + + return current; +} + +export function findFocusableElements(root: HTMLElement) { + const elements = root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + return Array.from(elements) as HTMLElement[]; +} + +export function focusFirstElement(root: HTMLElement) { + const elements = findFocusableElements(root); + + if (elements.length) { + elements[0].focus(); + return elements[0]; + } + + return undefined; +} + +export function focusElement(root: HTMLElement, selector: string) { + const target = root.querySelector(selector) as HTMLElement | null; + + if (target) { + target.focus(); + } +} + +export function focusElementAfterTimeout(root: HTMLElement, selector: string) { + setTimeout(() => focusElement(root, selector), 10); +} + +export function isParentOf(parent: Element, child: Element) { + for (let current = child.parentElement; current; current = current.parentElement) { + if (current === parent) { + return true; + } + } + + return false; +} + +export function showTextInNewTab(text: string) { + const wnd = window.open()!; + const pre = wnd.document.createElement('pre'); + pre.innerText = text; + wnd.document.body.appendChild(pre); +} + +export function addStyle(style: string) { + const styleElement = document.createElement('style'); + styleElement.appendChild(document.createTextNode(style)); + document.head.appendChild(styleElement); + return styleElement; +} diff --git a/src/ts/client/icons.ts b/src/ts/client/icons.ts new file mode 100644 index 0000000..762c805 --- /dev/null +++ b/src/ts/client/icons.ts @@ -0,0 +1,206 @@ +import { + faCrown, + faPlug, + faGamepad, + faMobile, + faTablet, + faTv, +} from '../generated/fa-icons'; + +export { + faHashtag, + faCog, + faCogs, + faMinus, + faPlus, + faCheck, + faFlag, + faStickyNote, + faCertificate, + faGlobe, + faGamepad, + faDesktop, + faQuestionCircle, + faInfo, + faSync, + faUserSecret, + faTrash, + faLock, + faApple, + faEdit, + faImage, + faLaughBeam, + faLanguage, + faCircle, + faEyeSlash, + faEnvelope, + faFont, + faCompressArrowsAlt, + faIdBadge, + faFilter, + faEraser, + faBell, + faClock, + faComment, + faComments, + faCommentSlash, + faHdd, + faMicrochip, + faUser, + faUsers, + faUserFriends, + faSpinner, + faBan, + faMicrophoneSlash, + faFileAlt, + faTimes, + faSearch, + faClipboard, + faChevronUp, + faChevronDown, + faChevronLeft, + faChevronRight, + faStar, + faAngleDoubleUp, + faAngleDoubleDown, + faAngleDoubleLeft, + faAngleDoubleRight, + faPlay, + faRedo, + faSave, + faArrowLeft, + faArrowRight, + faArrowUp, + faArrowDown, + faEyeDropper, + faPaintBrush, + faEllipsisV, + faExclamationCircle, + faUserPlus, + faUserMinus, + faUserTimes, + faSignOutAlt, + faStepForward, + faVolumeOff, + faVolumeDown, + faVolumeUp, + faHome, + faStop, + faRetweet, + faFile, + faCopy, + faShare, + faCode, + faTerminal, + faClone, + faPause, + faCrosshairs, + faFileImage, + faHeart, + faPlusCircle, + faMinusCircle, + faInfoCircle, + faCaretUp, + faCaretSquareUp, + faCaretSquareDown, + faCheckCircle, + faWrench, + faDrawPolygon, + faUserCog, + faSlidersH, + faExchangeAlt, + faDatabase, + faHorseHead, + faMapMarkerAlt, + faChartPie, + faCalendar, +} from '../generated/fa-icons'; + +import { + faPatreon, + faDeviantart, + faTwitter, + faTumblr, + faFacebook, + faGithub, + faVk, + faGoogle, + faChrome, + faInternetExplorer, + faEdge, + faAndroid, + faFirefox, + faSafari, + faOpera, + faWindows, + faApple, + faLinux, + faAmilia, + faYandexInternational, +} from '../generated/fa-icons'; + +export { + faPatreon, + faDeviantart, + faTwitter, + faTumblr, + faGithub, +} from '../generated/fa-icons'; + +export const partyLeaderIcon = faCrown; +export const offlineIcon = faPlug; +export const emptyIcon = { + prefix: 'fas', + iconName: 'empty-icon', + icon: [512, 512, [], 'ffff', ''], +}; + +export const oauthIcons: { [key: string]: any; } = { + patreon: faPatreon, + deviantart: faDeviantart, + twitter: faTwitter, + tumblr: faTumblr, + facebook: faFacebook, + github: faGithub, + vkontakte: faVk, + google: faGoogle, +}; + +export const uaIcons: { [key: string]: any; } = { + // browser + 'Chrome': faChrome, + 'Chromium': faChrome, + 'IE': faInternetExplorer, + 'Edge': faEdge, + 'Android Browser': faAndroid, + 'Firefox': faFirefox, + 'Safari': faSafari, + 'Mobile Safari': faSafari, + 'Opera': faOpera, + 'Opera Mini': faOpera, + 'Amigo': faAmilia, + 'YaBrowser': faYandexInternational, + // os + 'Windows': faWindows, + 'Windows Phone': faWindows, + 'Android': faAndroid, + 'iOS': faApple, + 'Mac OS': faApple, + 'Arch': faLinux, + 'CentOS': faLinux, + 'Fedora': faLinux, + 'FreeBSD': faLinux, + 'OpenBSD': faLinux, + 'Debian': faLinux, + 'Ubuntu': faLinux, + 'Linux': faLinux, + 'Chromium OS': faChrome, + 'Firefox OS': faFirefox, + 'Playstation': faGamepad, + 'Nintendo': faGamepad, + // device + 'console': faGamepad, + 'mobile': faMobile, + 'tablet': faTablet, + 'smarttv': faTv, +}; diff --git a/src/ts/client/input/gamepad.ts b/src/ts/client/input/gamepad.ts new file mode 100644 index 0000000..8cb0148 --- /dev/null +++ b/src/ts/client/input/gamepad.ts @@ -0,0 +1,160 @@ +import { GAMEPAD_MAPPINGS, GamepadAxes, GamepadButtons, GamepadMapping } from '../../generated/gamepad-mappings'; +import { Key, InputController } from './input'; +import { InputManager } from './inputManager'; +import { isFocused } from '../clientUtils'; + +interface GamepadInstance { + gamepad: Gamepad; + mapping: GamepadMapping; +} + +const JOYSTICK_THRESHHOLD = 0.2; + +function createGamepad(gamepad: Gamepad): GamepadInstance { + const mapping = detectMapping(gamepad.id, navigator.userAgent); + return { gamepad, mapping }; +} + +function isCompatible(mapping: any, id: string, browser: string) { + for (let i = 0; i < mapping.supported.length; i++) { + const supported = mapping.supported[i]; + + if (id.indexOf(supported.id) !== -1 && browser.indexOf(supported.os) !== -1 && browser.indexOf(browser) !== -1) { + return true; + } + } + + return false; +} + +function detectMapping(id: string, browser: string) { + for (let i = 0; i < GAMEPAD_MAPPINGS.length; i++) { + if (isCompatible(GAMEPAD_MAPPINGS[i], id, browser)) { + return GAMEPAD_MAPPINGS[i]; + } + } + + return GAMEPAD_MAPPINGS[0]; +} + +function axis({ mapping, gamepad }: GamepadInstance, name: GamepadAxes) { + const axe = mapping.axes[name] as any; + return axe ? gamepad.axes[axe.index] : 0; +} +function button({ mapping, gamepad }: GamepadInstance, name: GamepadButtons) { + const button = mapping.buttons[name] as any; + + if (!button) { + return false; + } + + if (button.index !== undefined) { + return gamepad.buttons[button.index] && gamepad.buttons[button.index].pressed; + } + + if (button.axis !== undefined) { + if (button.direction < 0) { + return gamepad.axes[button.axis] < -0.75; + } else { + return gamepad.axes[button.axis] > 0.75; + } + } + + return false; +} + +export class GamePadController implements InputController { + private initialized = false; + private gamepadIndex = -1; + private zeroed1 = false; + private zeroed2 = false; + constructor(private manager: InputManager) { + } + initialize() { + if (!this.initialized) { + this.initialized = true; + window.addEventListener('gamepadconnected', this.gamepadconnected); + window.addEventListener('gamepaddisconnected', this.gamepaddisconnected); + this.scanGamepads(); + } + } + release() { + this.initialized = false; + window.removeEventListener('gamepadconnected', this.gamepadconnected); + window.removeEventListener('gamepaddisconnected', this.gamepaddisconnected); + } + update() { + if (this.manager.disabledGamepad || !isFocused() || this.gamepadIndex === -1) + return; + + const gamepads = navigator.getGamepads(); + const gamepad = gamepads[this.gamepadIndex]; + + if (!gamepad) { + this.scanGamepads(); + return; + } + + const pad = createGamepad(gamepad); + + this.zeroed1 = readAxis( + this.manager, Key.GAMEPAD_AXIS1_X, Key.GAMEPAD_AXIS1_Y, + axis(pad, GamepadAxes.LeftStickX), axis(pad, GamepadAxes.LeftStickY), this.zeroed1); + this.zeroed2 = readAxis( + this.manager, Key.GAMEPAD_AXIS2_X, Key.GAMEPAD_AXIS2_Y, + axis(pad, GamepadAxes.RightStickX), axis(pad, GamepadAxes.RightStickY), this.zeroed2); + + this.manager.setValue(Key.GAMEPAD_BUTTON_X, button(pad, GamepadButtons.X) ? 1 : 0); + this.manager.setValue(Key.GAMEPAD_BUTTON_Y, button(pad, GamepadButtons.Y) ? 1 : 0); + this.manager.setValue(Key.GAMEPAD_BUTTON_A, button(pad, GamepadButtons.A) ? 1 : 0); + this.manager.setValue(Key.GAMEPAD_BUTTON_B, button(pad, GamepadButtons.B) ? 1 : 0); + + this.manager.setValue(Key.GAMEPAD_BUTTON_DOWN, button(pad, GamepadButtons.DpadDown) ? 1 : 0); + this.manager.setValue(Key.GAMEPAD_BUTTON_LEFT, button(pad, GamepadButtons.DpadLeft) ? 1 : 0); + this.manager.setValue(Key.GAMEPAD_BUTTON_RIGHT, button(pad, GamepadButtons.DpadRight) ? 1 : 0); + this.manager.setValue(Key.GAMEPAD_BUTTON_UP, button(pad, GamepadButtons.DpadUp) ? 1 : 0); + } + clear() { + } + private scanGamepads() { + const gamepads = navigator.getGamepads(); + + // Using regular loop because of issues with iterating over gamepads + for (let i = 0; i < gamepads.length; i++) { + const gamepad = gamepads[i]; + + if (gamepad) { + this.gamepadIndex = gamepad.index; + return; + } + } + + this.gamepadIndex = -1; + } + private gamepadconnected = (e: Event) => { + this.gamepadIndex = (e as GamepadEvent).gamepad.index; + } + private gamepaddisconnected = (e: Event) => { + if (this.gamepadIndex === (e as GamepadEvent).gamepad.index) { + this.scanGamepads(); + } + } +} + +function readAxis(manager: InputManager, keyX: Key, keyY: Key, axisX: number, axisY: number, zeroed: boolean): boolean { + const dist = Math.sqrt(axisX * axisX + axisY * axisY); + + if (dist > JOYSTICK_THRESHHOLD) { + const scaledDist = Math.min((dist - JOYSTICK_THRESHHOLD) / (1 - JOYSTICK_THRESHHOLD), 1); + const theta = Math.atan2(axisY, axisX); + manager.setValue(keyX, Math.cos(theta) * scaledDist); + manager.setValue(keyY, Math.sin(theta) * scaledDist); + return false; + } else if (!zeroed) { + manager.setValue(keyX, 0); + manager.setValue(keyY, 0); + return true; + } + + return zeroed; +} diff --git a/src/ts/client/input/input.ts b/src/ts/client/input/input.ts new file mode 100644 index 0000000..e0a0df2 --- /dev/null +++ b/src/ts/client/input/input.ts @@ -0,0 +1,144 @@ +export const enum Key { + // Keyboard + BACKSPACE = 8, + TAB = 9, + ENTER = 13, + SHIFT = 16, + CTRL = 17, + ALT = 18, + PAUSE = 19, + CAPS_LOCK = 20, + ESCAPE = 27, + SPACE = 32, + PAGE_UP = 33, + PAGE_DOWN = 34, + END = 35, + HOME = 36, + LEFT = 37, + UP = 38, + RIGHT = 39, + DOWN = 40, + INSERT = 45, + DELETE = 46, + KEY_0 = 48, + KEY_1 = 49, + KEY_2 = 50, + KEY_3 = 51, + KEY_4 = 52, + KEY_5 = 53, + KEY_6 = 54, + KEY_7 = 55, + KEY_8 = 56, + KEY_9 = 57, + KEY_A = 65, + KEY_B = 66, + KEY_C = 67, + KEY_D = 68, + KEY_E = 69, + KEY_F = 70, + KEY_G = 71, + KEY_H = 72, + KEY_I = 73, + KEY_J = 74, + KEY_K = 75, + KEY_L = 76, + KEY_M = 77, + KEY_N = 78, + KEY_O = 79, + KEY_P = 80, + KEY_Q = 81, + KEY_R = 82, + KEY_S = 83, + KEY_T = 84, + KEY_U = 85, + KEY_V = 86, + KEY_W = 87, + KEY_X = 88, + KEY_Y = 89, + KEY_Z = 90, + LEFT_META = 91, + RIGHT_META = 92, + SELECT = 93, + NUMPAD_0 = 96, + NUMPAD_1 = 97, + NUMPAD_2 = 98, + NUMPAD_3 = 99, + NUMPAD_4 = 100, + NUMPAD_5 = 101, + NUMPAD_6 = 102, + NUMPAD_7 = 103, + NUMPAD_8 = 104, + NUMPAD_9 = 105, + MULTIPLY = 106, + ADD = 107, + SUBTRACT = 109, + DECIMAL = 110, + DIVIDE = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + NUM_LOCK = 144, + SCROLL_LOCK = 145, + SEMICOLON = 186, + EQUALS = 187, + COMMA = 188, + DASH = 189, + PERIOD = 190, + FORWARD_SLASH = 191, + GRAVE_ACCENT = 192, + OPEN_BRACKET = 219, + BACK_SLASH = 220, + CLOSE_BRACKET = 221, + SINGLE_QUOTE = 222, + // Mouse + MOUSE_X = 300, + MOUSE_Y, + MOUSE_BUTTON1, + MOUSE_BUTTON2, + MOUSE_BUTTON3, + MOUSE_WHEEL_X, + MOUSE_WHEEL_Y, + // Gamepad + GAMEPAD_AXIS1_X, + GAMEPAD_AXIS1_Y, + GAMEPAD_AXIS2_X, + GAMEPAD_AXIS2_Y, + GAMEPAD_BUTTON_A, + GAMEPAD_BUTTON_B, + GAMEPAD_BUTTON_X, + GAMEPAD_BUTTON_Y, + GAMEPAD_BUTTON_L1, + GAMEPAD_BUTTON_R1, + GAMEPAD_BUTTON_L2, + GAMEPAD_BUTTON_R2, + GAMEPAD_BUTTON_START, + GAMEPAD_BUTTON_SELECT, + GAMEPAD_BUTTON_ANALOG1, + GAMEPAD_BUTTON_ANALOG2, + GAMEPAD_BUTTON_UP, + GAMEPAD_BUTTON_DOWN, + GAMEPAD_BUTTON_LEFT, + GAMEPAD_BUTTON_RIGHT, + // Touch + TOUCH, + TOUCH_CLICK, + TOUCH_SECOND_CLICK, + // Other + MAX_VALUE, +} + +export interface InputController { + initialize(element: HTMLElement): void; + release(): void; + update(): void; + clear(): void; +} diff --git a/src/ts/client/input/inputManager.ts b/src/ts/client/input/inputManager.ts new file mode 100644 index 0000000..a359a1c --- /dev/null +++ b/src/ts/client/input/inputManager.ts @@ -0,0 +1,170 @@ +import { clamp } from 'lodash'; +import { KeyboardController } from './keyboard'; +import { MouseController } from './mouse'; +import { TouchController } from './touch'; +import { GamePadController } from './gamepad'; +import { InputController, Key } from './input'; +import { array, times } from '../../common/utils'; + +type Handler = (input: Key, value: number) => boolean | void; + +const KEYS = Key.MAX_VALUE; + +export class InputManager { + disabledGamepad = false; + disabledKeyboard = false; + disableArrows = false; + usingTouch = false; + private state: number[]; + private prevState: number[]; + private actions: Handler[][]; + private controllers: InputController[] = []; + constructor() { + this.state = array(KEYS, 0); + this.prevState = array(KEYS, 0); + this.actions = times(KEYS, () => []); + } + get axisX() { + const axisX = this.getRange(Key.GAMEPAD_AXIS1_X); + const left = this.disableArrows ? this.getState(Key.KEY_A) : this.getState(Key.LEFT, Key.KEY_A); + const right = this.disableArrows ? this.getState(Key.KEY_D) : this.getState(Key.RIGHT, Key.KEY_D); + const x = axisX + (left ? -1 : (right ? 1 : 0)); + return clamp(x, -1, 1); + } + get axisY() { + const axisY = this.getRange(Key.GAMEPAD_AXIS1_Y); + const up = this.disableArrows ? this.getState(Key.KEY_W) : this.getState(Key.UP, Key.KEY_W); + const down = this.disableArrows ? this.getState(Key.KEY_S) : this.getState(Key.DOWN, Key.KEY_S); + const y = axisY + (up ? -1 : (down ? 1 : 0)); + return clamp(y, -1, 1); + } + get isMovementFromButtons() { + const up = this.getState(Key.UP, Key.KEY_W); + const down = this.getState(Key.DOWN, Key.KEY_S); + const left = this.getState(Key.LEFT, Key.KEY_A); + const right = this.getState(Key.RIGHT, Key.KEY_D); + return up || down || left || right; + } + get axis2X() { + return clamp(this.getRange(Key.GAMEPAD_AXIS2_X), -1, 1); + } + get axis2Y() { + return clamp(this.getRange(Key.GAMEPAD_AXIS2_Y), -1, 1); + } + get pointerX() { + return this.getRange(Key.MOUSE_X); + } + get pointerY() { + return this.getRange(Key.MOUSE_Y); + } + get wheelX() { + return this.getRange(Key.MOUSE_WHEEL_X); + } + get wheelY() { + return this.getRange(Key.MOUSE_WHEEL_Y); + } + initialize(element: HTMLElement) { + this.controllers = [ + new KeyboardController(this), + new MouseController(this), + new TouchController(this), + new GamePadController(this), + ]; + + this.controllers.forEach(c => c.initialize(element)); + this.clear(); + } + release() { + this.controllers.forEach(c => c.release()); + this.controllers = []; + this.clear(); + } + update() { + for (const controller of this.controllers) { + controller.update(); + } + } + end() { + for (let i = 0; i < KEYS; i++) { + this.prevState[i] = this.state[i]; + } + + this.setValue(Key.TOUCH_CLICK, 0); + this.setValue(Key.TOUCH_SECOND_CLICK, 0); + this.setValue(Key.MOUSE_WHEEL_X, 0); + this.setValue(Key.MOUSE_WHEEL_Y, 0); + } + clear() { + for (let i = 0; i < KEYS; i++) { + this.state[i] = 0; + this.prevState[i] = 0; + } + + for (const controller of this.controllers) { + controller.clear(); + } + } + onPressed(inputs: Key[] | Key, handler: () => void) { + this.onAction(inputs, (_, v) => { + if (v === 1) { + handler(); + } + }); + } + onReleased(inputs: Key[] | Key, handler: () => void) { + this.onAction(inputs, (_, v) => { + if (v === 0) { + handler(); + } + }); + } + isPressed(key: Key) { + return this.state[key] !== 0; + } + wasPressed(key: Key): boolean { + return this.state[key] === 1 && this.prevState[key] === 0; + } + private onAction(inputs: Key[] | Key, handler: Handler) { + const inputsArray = Array.isArray(inputs) ? inputs : [inputs]; + + for (const i of inputsArray) { + this.actions[i].push(handler); + } + } + private getState(...inputs: Key[]): boolean { + for (const i of inputs) { + if (this.state[i] !== 0) { + return true; + } + } + + return false; + } + private getRange(input: Key): number { + return this.state[input]; + } + setValue(input: Key, value: number): boolean { + if (input < 0 || input >= KEYS) { + console.warn(`Input out of range: ${input}`); + } else if (this.state[input] !== value) { + this.state[input] = value; + + if (this.actions[input] && this.actions[input].length) { + for (const action of this.actions[input]) { + action(input, value); + } + + return true; + } + } + + return false; + } + addValue(input: Key, value: number) { + if (input < 0 || input >= KEYS) { + console.warn(`Input out of range: ${input}`); + } else { + this.state[input] += value; + } + } +} diff --git a/src/ts/client/input/keyboard.ts b/src/ts/client/input/keyboard.ts new file mode 100644 index 0000000..4962d1b --- /dev/null +++ b/src/ts/client/input/keyboard.ts @@ -0,0 +1,96 @@ +import { InputController, Key } from './input'; +import { InputManager } from './inputManager'; +import { removeItem, includes } from '../../common/utils'; + +const firefox = !SERVER && /firefox/i.test(navigator.userAgent); + +function isKeyEventInvalid(e: KeyboardEvent) { + return e.target && /^(input|textarea|select)$/i.test((e.target as HTMLElement).tagName); +} + +function allowKey(key: number) { + return key === Key.ESCAPE || key === Key.F5 || key === Key.F12 || key === Key.F11 || key === Key.TAB; +} + +function fixKeyCode(key: number) { + if (firefox) { + if (key === 173) return Key.DASH; + if (key === 61) return Key.EQUALS; + } + + return key; +} + +const iosKeyToKeyCode: { [key: string]: number | undefined; } = { + UIKeyInputEscape: Key.ESCAPE, + UIKeyInputUpArrow: Key.UP, + UIKeyInputLeftArrow: Key.LEFT, + UIKeyInputRightArrow: Key.RIGHT, + UIKeyInputDownArrow: Key.DOWN, +}; + +const iosHandledKeyCodes = [Key.ESCAPE, Key.UP, Key.LEFT, Key.RIGHT, Key.DOWN]; + +export class KeyboardController implements InputController { + private initialized = false; + private stack: number[] = []; + constructor(private manager: InputManager) { + } + initialize() { + if (!this.initialized) { + this.initialized = true; + window.addEventListener('keydown', this.keydown); + window.addEventListener('keyup', this.keyup); + window.addEventListener('blur', this.blur); + } + } + release() { + this.initialized = false; + window.removeEventListener('keydown', this.keydown); + window.removeEventListener('keyup', this.keyup); + window.removeEventListener('blur', this.blur); + this.clear(); + } + update() { + } + clear() { + this.stack.length = 0; + } + private keydown = (e: KeyboardEvent) => { + if (!this.manager.disabledKeyboard && !isKeyEventInvalid(e)) { + const code = fixKeyCode(e.keyCode); + this.manager.setValue(code, 1); + + if (!allowKey(code)) { + e.preventDefault(); + e.stopPropagation(); + } + + if (!includes(this.stack, code) && !includes(iosHandledKeyCodes, code)) { + this.stack.push(code); + } + } + } + private keyup = (e: KeyboardEvent) => { + let code = fixKeyCode(e.keyCode); + + // fix keyCode on iOS bluetooth keyboard + if (code === 0) { + code = iosKeyToKeyCode[e.key] || 0; + + if (code === 0) { + code = this.stack.pop() || 0; + } + } + + if (this.manager.setValue(code, 0)) { + e.preventDefault(); + e.stopPropagation(); + } + + removeItem(this.stack, code); + } + private blur = () => { + this.manager.clear(); + } +} diff --git a/src/ts/client/input/mouse.ts b/src/ts/client/input/mouse.ts new file mode 100644 index 0000000..f249eb2 --- /dev/null +++ b/src/ts/client/input/mouse.ts @@ -0,0 +1,87 @@ +import { Key, InputController } from './input'; +import { InputManager } from './inputManager'; +import { clamp } from '../../common/utils'; + +const MOUSE_BUTTONS = [Key.MOUSE_BUTTON1, Key.MOUSE_BUTTON3, Key.MOUSE_BUTTON2]; + +export class MouseController implements InputController { + private initialized = false; + private element?: HTMLElement; + constructor(private manager: InputManager) { + } + initialize(element: HTMLElement) { + if (!this.initialized) { + this.initialized = true; + this.element = element; + element.addEventListener('mousemove', this.mousemove); + element.addEventListener('mousedown', this.mousedown); + element.addEventListener('mouseup', this.mouseup); + element.addEventListener('mousewheel', this.mousewheel); + element.addEventListener('contextmenu', this.contextmenu); + element.addEventListener('click', this.click); + window.addEventListener('blur', this.blur); + } + } + release() { + this.initialized = false; + + if (this.element) { + this.element.removeEventListener('mousemove', this.mousemove); + this.element.removeEventListener('mousedown', this.mousedown); + this.element.removeEventListener('mouseup', this.mouseup); + this.element.removeEventListener('mousewheel', this.mousewheel); + this.element.removeEventListener('contextmenu', this.contextmenu); + this.element.removeEventListener('click', this.click); + this.element = undefined; + } + + window.removeEventListener('blur', this.blur); + } + update() { + } + clear() { + } + private mousemove = (e: MouseEvent) => { + if (this.element) { + const rect = this.element.getBoundingClientRect(); + this.manager.setValue(Key.MOUSE_X, Math.floor(e.clientX - rect.left)); + this.manager.setValue(Key.MOUSE_Y, Math.floor(e.clientY - rect.top)); + } + } + private mousedown = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + this.manager.usingTouch = false; + + const button = MOUSE_BUTTONS[e.button]; + + if (button) { + this.manager.setValue(button, 1); + } + } + private mouseup = (e: MouseEvent) => { + const button = MOUSE_BUTTONS[e.button]; + + if (button) { + this.manager.setValue(button, 0); + } + } + private mousewheel: any = (e: MouseWheelEvent) => { + this.manager.addValue(Key.MOUSE_WHEEL_X, clamp(e.deltaX, -1, 1)); + this.manager.addValue(Key.MOUSE_WHEEL_Y, clamp(e.deltaY, -1, 1)); + } + private contextmenu = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + } + private click = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + } + private blur = () => { + for (const button of MOUSE_BUTTONS) { + this.manager.setValue(button, 0); + } + } +} diff --git a/src/ts/client/input/touch.ts b/src/ts/client/input/touch.ts new file mode 100644 index 0000000..e8123e3 --- /dev/null +++ b/src/ts/client/input/touch.ts @@ -0,0 +1,203 @@ +import { Key, InputController } from './input'; +import { Point } from '../../common/interfaces'; +import { setTransform } from '../../common/utils'; +import { InputManager } from './inputManager'; + +function getTouch(e: TouchEvent, id: number) { + if (id !== -1) { + for (let i = 0; i < e.changedTouches.length; ++i) { + const touch = e.changedTouches.item(i); + + if (touch && touch.identifier === id) { + return touch; + } + } + } + + return undefined; +} + +const TOUCH_DEADZONE = 15; +const TOUCH_MAX = 100; + +export class TouchController implements InputController { + private initialized = false; + private touchId = -1; + private touch2Id = -1; + private touchStart: Point = { x: 0, y: 0 }; + private touchCurrent: Point = { x: 0, y: 0 }; + private touchIsDrag = false; + private tapInvalidated = false; + private origin?: HTMLElement; + private position?: HTMLElement; + private originShown = false; + private originTransform?: string; + private positionShown = false; + private positionTransform?: string; + private element?: HTMLElement; + constructor(private manager: InputManager) { + } + initialize(element: HTMLElement) { + if (!this.initialized) { + this.initialized = true; + this.element = element; + this.origin = document.getElementById('touch-origin')!; + this.position = document.getElementById('touch-position')!; + + element.addEventListener('touchstart', this.touchstart); + element.addEventListener('touchmove', this.touchmove); + element.addEventListener('touchend', this.touchend); + window.addEventListener('touchend', this.blur); + window.addEventListener('blur', this.blur); + } + } + release() { + this.initialized = false; + + if (this.element) { + this.element.removeEventListener('touchstart', this.touchstart); + this.element.removeEventListener('touchmove', this.touchmove); + this.element.removeEventListener('touchend', this.touchend); + this.element = undefined; + } + + window.removeEventListener('touchend', this.blur); + window.removeEventListener('blur', this.blur); + } + update() { + const showOrigin = this.touchIsDrag && this.touchId !== -1; + const showPosition = this.touchId !== -1; + + if (this.origin && this.position) { + if (this.originShown !== showOrigin) { + this.originShown = showOrigin; + this.origin.style.display = showOrigin ? 'block' : 'none'; + } + + if (this.positionShown !== showPosition) { + this.positionShown = showPosition; + this.position.style.display = showPosition ? 'block' : 'none'; + } + + if (showOrigin) { + const transform = `translate3d(${this.touchStart.x - 50}px, ${this.touchStart.y - 50}px, 0px)`; + + if (this.originTransform !== transform) { + this.originTransform = transform; + setTransform(this.origin, transform); + } + } + + if (showPosition) { + const transform = `translate3d(${this.touchCurrent.x - 25}px, ${this.touchCurrent.y - 25}px, 0px)`; + + if (this.positionTransform !== transform) { + this.positionTransform = transform; + setTransform(this.position, transform); + } + } + } + } + clear() { + } + private reset() { + this.touch2Id = -1; + this.resetTouch(); + } + private resetTouch() { + this.touchId = -1; + this.touchStart = this.touchCurrent = { x: 0, y: 0 }; + this.touchIsDrag = false; + this.manager.setValue(Key.TOUCH, 0); + this.updateInput(); + } + private updateInput() { + const dy = this.touchStart.y - this.touchCurrent.y; + const dx = this.touchStart.x - this.touchCurrent.x; + const theta = Math.atan2(dy, dx); + const dist = Math.sqrt(dy * dy + dx * dx); + + if (dist > TOUCH_DEADZONE) { + const scaledDist = Math.min((dist - TOUCH_DEADZONE) / (TOUCH_MAX - TOUCH_DEADZONE), 1); + this.touchIsDrag = true; + this.manager.setValue(Key.GAMEPAD_AXIS1_X, -Math.cos(theta) * scaledDist); + this.manager.setValue(Key.GAMEPAD_AXIS1_Y, -Math.sin(theta) * scaledDist); + } else { + this.manager.setValue(Key.GAMEPAD_AXIS1_X, 0); + this.manager.setValue(Key.GAMEPAD_AXIS1_Y, 0); + } + } + private touchstart = (e: any) => { + e.cancellable && e.preventDefault(); + e.stopPropagation(); + + this.manager.usingTouch = true; + + if (this.touchId === -1) { + const touch = e.changedTouches.item(0); + + if (touch) { + this.tapInvalidated = false; + this.touchId = touch.identifier; + this.touchStart = this.touchCurrent = this.getTouchXY(touch); + this.manager.setValue(Key.MOUSE_X, this.touchStart.x); + this.manager.setValue(Key.MOUSE_Y, this.touchStart.y); + this.manager.setValue(Key.TOUCH, 1); + } + } else if (this.touch2Id === -1) { + const touch = e.changedTouches.item(0); + + if (touch) { + this.tapInvalidated = true; + this.touch2Id = touch.identifier; + } + } + } + private touchmove = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + + const touch = getTouch(e, this.touchId); + + if (touch) { + this.touchCurrent = this.getTouchXY(touch); + this.manager.setValue(Key.MOUSE_X, this.touchCurrent.x); + this.manager.setValue(Key.MOUSE_Y, this.touchCurrent.y); + this.updateInput(); + } + } + private touchend = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + + const touch = getTouch(e, this.touchId); + + if (touch) { + if (!this.touchIsDrag && !this.tapInvalidated) { + this.manager.setValue(Key.MOUSE_X, this.touchStart.x); + this.manager.setValue(Key.MOUSE_Y, this.touchStart.y); + this.manager.setValue(Key.TOUCH_CLICK, 1); + } + + this.resetTouch(); + } + + const touch2 = getTouch(e, this.touch2Id); + + if (touch2) { + this.manager.setValue(Key.TOUCH_SECOND_CLICK, 1); + this.touch2Id = -1; + } + } + private blur = () => { + this.reset(); + } + private getTouchXY(touch: Touch) { + const { left, top } = this.element!.getBoundingClientRect(); + + return { + x: touch.clientX - left, + y: touch.clientY - top, + }; + } +} diff --git a/src/ts/client/partyUtils.ts b/src/ts/client/partyUtils.ts new file mode 100644 index 0000000..af81fa5 --- /dev/null +++ b/src/ts/client/partyUtils.ts @@ -0,0 +1,44 @@ +import { remove } from 'lodash'; +import { PartyMember, PartyInfo, Pony } from '../common/interfaces'; +import { PonyTownGame } from './game'; + +export function updateParty(current: PartyInfo | undefined, info: PartyMember[] | undefined): PartyInfo | undefined { + if (!info || !info.length) { + return undefined; + } else { + const party = current || { + leaderId: 0, + members: [], + }; + + remove(party.members, p => !info.some(m => p.id === m.id)); + + info.forEach(m => { + const existing = party.members.find(x => m.id === x.id); + + if (existing) { + Object.assign(existing, m); + } else { + party.members.push(m); + } + + if (m.leader) { + party.leaderId = m.id; + } + }); + + return party; + } +} + +export function isPonyInParty(party: PartyInfo | undefined, pony: Pony, pending: boolean) { + return !!party && party.members.some(m => m.pony === pony && (pending || !m.pending)); +} + +export function isPartyLeader(game: PonyTownGame): boolean { + return game.party !== undefined && game.player !== undefined && game.player.id === game.party.leaderId; +} + +export function isInParty(game: PonyTownGame): boolean { + return game.party !== undefined && game.party.members.length > 0; +} diff --git a/src/ts/client/playerActions.ts b/src/ts/client/playerActions.ts new file mode 100644 index 0000000..edb6b86 --- /dev/null +++ b/src/ts/client/playerActions.ts @@ -0,0 +1,229 @@ +import { isCommand, processCommand, hasFlag, includes, point } from '../common/utils'; +import { canPonyLie, canPonyFlyUp, canPonyStand, canPonySit, doBoopPonyAction } from '../common/pony'; +import { PonyTownGame } from './game'; +import { + setPonyState, canBoop, isPonyLying, isPonyFlying, isPonyStanding, isPonySitting, getInteractBounds, + isFacingRight, closestEntity, entityInRange +} from '../common/entityUtils'; +import { EntityState, Action, Pony, ChatType, EntityFlags, Point, TileType } from '../common/interfaces'; +import { FLY_DELAY } from '../common/constants'; +import { randomString } from '../common/stringUtils'; +import { pickEntitiesByRect, pickAnyEntities } from '../common/worldMap'; +import { centerPoint } from '../common/rect'; +import { pointToWorld, roundPositionX, roundPositionY } from '../common/positionUtils'; +import { hammer, shovel } from '../common/entities'; + +export function handleActionCommand(message: string, game: PonyTownGame): boolean { + if (isCommand(message)) { + const { command = '' } = processCommand(message); + const player = game.player; + + if (DEVELOPMENT) { + if (command === 'spammessages') { + let i = 0; + setInterval(() => game.send(server => server.say(0, randomString(5) + ` #${i++}`, ChatType.Say)), 100); + return true; + } + } + + switch (command.toLowerCase()) { + case 'testerrorreporting': + throw new Error('test error'); + case 'disablepixelratio': + game.togglePixelRatio(); + return true; + case 'lie': + case 'lay': + if (player) { + if (isPonyLying(player)) { + sitAction(player, game); + } else { + lieAction(player, game); + } + } + return true; + case 'sit': + if (player) { + if (isPonyFlying(player)) { + standAction(player, game); + } else { + sitAction(player, game); + } + } + return true; + case 'stand': + if (player) { + standAction(player, game); + } + return true; + case 'fly': + if (player) { + if (isPonyFlying(player)) { + standAction(player, game); + } else { + flyAction(player, game); + } + } + return true; + } + } + + return false; +} + +export function upAction(game: PonyTownGame) { + const player = game.player; + + if (player) { + if (isPonyLying(player)) { + sitAction(player, game); + } else if (isPonySitting(player)) { + standAction(player, game); + } else if (isPonyStanding(player)) { + flyAction(player, game); + } + } +} + +export function downAction(game: PonyTownGame) { + const player = game.player; + + if (player) { + if (isPonySitting(player)) { + lieAction(player, game); + } else if (isPonyStanding(player)) { + sitAction(player, game); + } else if (isPonyFlying(player)) { + standAction(player, game); + } + } +} + +export function sitAction(player: Pony, game: PonyTownGame) { + if (canPonySit(player, game.map) && game.send(server => server.action(Action.Sit))) { + player.state = setPonyState(player.state, EntityState.PonySitting); + game.stateOverride = EntityState.PonySitting; + game.onActionsUpdate.next(); + } +} + +export function standAction(player: Pony, game: PonyTownGame) { + if (canPonyStand(player, game.map) && game.send(server => server.action(Action.Stand))) { + player.state = setPonyState(player.state, EntityState.PonyStanding); + game.stateOverride = EntityState.PonyStanding; + game.onActionsUpdate.next(); + } +} + +export function lieAction(player: Pony, game: PonyTownGame) { + if (canPonyLie(player, game.map) && game.send(server => server.action(Action.Lie))) { + player.state = setPonyState(player.state, EntityState.PonyLying); + game.stateOverride = EntityState.PonyLying; + game.onActionsUpdate.next(); + } +} + +export function flyAction(player: Pony, game: PonyTownGame) { + if (canPonyFlyUp(player) && game.send(server => server.action(Action.Fly))) { + player.state = setPonyState(player.state, EntityState.PonyFlying); + player.inTheAirDelay = FLY_DELAY; + game.stateOverride = EntityState.PonyFlying; + game.onActionsUpdate.next(); + } +} + +export function boopAction(game: PonyTownGame) { + if (game.player && canBoop(game.player) && game.send(server => server.action(Action.Boop))) { + doBoopPonyAction(game, game.player); + } +} + +export function turnHeadAction(game: PonyTownGame) { + if (game.player && game.send(server => server.action(Action.TurnHead))) { + game.player.state = game.player.state ^ EntityState.HeadTurned; + game.headTurnedOverride = hasFlag(game.player.state, EntityState.HeadTurned); + game.onActionsUpdate.next(); + } +} + +export function interact(game: PonyTownGame, shift: boolean) { + const player = game.player; + + if (player) { + const bounds = getInteractBounds(player); + const entities = pickEntitiesByRect(game.map, bounds, true, false); + const center = centerPoint(bounds); + center.x += (bounds.w / 4) * (isFacingRight(player) ? -1 : 1); + const entity = closestEntity(pointToWorld(center), entities); + + if (entity && entityInRange(entity, player)) { + game.send(server => server.interact(entity.id)); + } else if (player.hold === hammer.type) { + game.changePlaceEntity(shift); + } else if (player.hold === shovel.type) { + game.changePlaceTile(shift); + } else if (player.ponyState.holding && hasFlag(player.ponyState.holding.flags, EntityFlags.Usable)) { + game.send(server => server.use()); + } + } +} + +export function toggleWall(game: PonyTownGame, hover: Point) { + const x = hover.x | 0; + const y = hover.y | 0; + const dx = hover.x - x; + const dy = hover.y - y; + + if (dx > dy) { + if ((dx + dy) < 1) { + game.send(server => server.changeTile(x, y, TileType.WallH)); + } else { + game.send(server => server.changeTile(x + 1, y, TileType.WallV)); + } + } else { + if ((dx + dy) < 1) { + game.send(server => server.changeTile(x, y, TileType.WallV)); + } else { + game.send(server => server.changeTile(x, y + 1, TileType.WallH)); + } + } +} + +export function editorSelectEntities(game: PonyTownGame, hover: Point, shift: boolean) { + game.apply(() => { + const entities = pickAnyEntities(game.map, hover); + + if (shift) { + const entity = entities.filter(e => !includes(game.editor.selectedEntities, e))[0]; + entity && game.editor.selectedEntities.push(entity); + } else { + const index = entities.findIndex(e => includes(game.editor.selectedEntities, e)); + const entity = entities[(index + 1) % entities.length]; + game.editor.selectedEntities = entity ? [entity] : []; + } + }); +} + +export function editorDragEntities(game: PonyTownGame, hover: Point, buttonPressed: boolean) { + if (buttonPressed) { + const dx = hover.x - game.editor.draggingStart.x; + const dy = hover.y - game.editor.draggingStart.y; + + game.editor.selectedEntities.forEach(e => { + e.x = roundPositionX(e.draggingStart!.x + dx); + e.y = roundPositionY(e.draggingStart!.y + dy); + }); + } else { + game.apply(() => game.editor.draggingEntities = false); + game.send(server => server.editorAction({ + type: 'move', + entities: game.editor.selectedEntities.map(({ id, x, y }) => ({ id, x, y })), + })); + } +} + +export function editorMoveEntities(game: PonyTownGame, hover: Point) { + game.editor.draggingEntities = true; + game.editor.draggingStart = hover; + game.editor.selectedEntities.forEach(e => e.draggingStart = point(e.x, e.y)); +} diff --git a/src/ts/client/polyfils.ts b/src/ts/client/polyfils.ts new file mode 100644 index 0000000..3981639 --- /dev/null +++ b/src/ts/client/polyfils.ts @@ -0,0 +1,33 @@ +/// + +// Safari <= 8.4, Android +try { + if (!('performance' in window && 'now' in performance)) { + (window as any).performance = Date; + } +} catch { } + +try { + if (!('getGamepads' in navigator)) { + (window.navigator as any).getGamepads = () => []; + } +} catch { } + +try { + if (!('requestAnimationFrame' in window)) { + (window as any).requestAnimationFrame = (callback: any) => setTimeout(() => callback(performance.now()), 1000 / 60) as any; + } +} catch { } + +try { + if (!('cancelAnimationFrame' in window)) { + (window as any).cancelAnimationFrame = clearTimeout; + } +} catch { } + +// IE <= 10 +try { + if (!('devicePixelRatio' in window)) { + (window as any).devicePixelRatio = 1; + } +} catch { } diff --git a/src/ts/client/ponyAnimations.ts b/src/ts/client/ponyAnimations.ts new file mode 100644 index 0000000..b80d71d --- /dev/null +++ b/src/ts/client/ponyAnimations.ts @@ -0,0 +1,547 @@ +import { BodyAnimation, BodyAnimationFrame, HeadAnimation, HeadAnimationFrame, BodyShadow } from '../common/interfaces'; +import { repeat, flatten } from '../common/utils'; + +// body animations + +export function createBodyFrame([ + body = 0, head = 0, wing = 0, tail = 0, + frontLeg = 0, frontFarLeg = 0, backLeg = 0, backFarLeg = 0, + bodyX = 0, bodyY = 0, headX = 0, headY = 0, + frontLegX = 0, frontLegY = 0, frontFarLegX = 0, frontFarLegY = 0, + backLegX = 0, backLegY = 0, backFarLegX = 0, backFarLegY = 0 +]: number[]): Readonly { + return { + body, head, wing, tail, + frontLeg, frontFarLeg, backLeg, backFarLeg, + bodyX, bodyY, headX, headY, + frontLegX, frontLegY, frontFarLegX, frontFarLegY, + backLegX, backLegY, backFarLegX, backFarLegY + }; +} + +export function createBodyAnimation( + name: string, fps: number, loop: boolean, frames: number[][], shadowOffsets?: number[][] +): Readonly { + if (shadowOffsets && shadowOffsets.length !== frames.length) { + throw new Error(`Incorrect frame count for shadowOffsets for ${name}`); + } + + const shadow = shadowOffsets && shadowOffsets.map(([frame, offset]) => ({ frame, offset })); + + return { name, loop, fps, frames: frames.map(createBodyFrame), shadow }; +} + +export const stand = createBodyAnimation('stand', 24, true, [ + [1, 1, 0, 0, 1, 1, 1, 1], +]); + +export const swim = createBodyAnimation('swim', 4, true, [ + [1, 1, 0, 0, 8, 10, 6, 5, 0, 14], + [1, 1, 0, 0, 8, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 8, 10, 6, 5, 0, 12], + [1, 1, 0, 0, 8, 10, 6, 5, 0, 13] +]); + +export const trotToSwim = createBodyAnimation('trot-to-swim', 24, false, [ + [1, 1, 0, 0, 8, 10, 6, 5, 0, 2], + [1, 1, 0, 0, 8, 10, 6, 5, 0, 8], + [1, 1, 0, 0, 8, 10, 6, 5, 0, 10], + [1, 1, 0, 0, 8, 10, 6, 5, 0, 16] +]); + +export const swimToTrot = createBodyAnimation('swim-to-trot', 24, false, [ + [1, 1, 0, 0, 8, 10, 6, 5, 0, 12], + [1, 1, 0, 0, 12, 3, 4, 23, 0, 8], + [1, 1, 0, 0, 14, 26, 3, 24, 0, 4], + [1, 1, 0, 0, 18, 27, 2, 5] +]); + +export const flyToSwim = createBodyAnimation('fly-to-swim', 16, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -12], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -8], + [1, 1, 6, 0, 8, 10, 6, 5, 0, -2], + [1, 1, 7, 0, 8, 10, 6, 5, 0, 4], + [1, 1, 11, 0, 8, 10, 6, 5, 0, 10], + [1, 1, 1, 0, 8, 10, 6, 5, 0, 14] +]); + +export const flyToSwimBug = createBodyAnimation('fly-to-swim-bug', 16, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -12], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -8], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -2], + [1, 1, 3, 0, 8, 10, 6, 5, 0, 4], + [1, 1, 4, 0, 8, 10, 6, 5, 0, 10], + [1, 1, 1, 0, 8, 10, 6, 5, 0, 14] +]); + +export const swimToFly = createBodyAnimation('swim-to-fly', 16, false, [ + [1, 1, 11, 0, 8, 10, 6, 5, 0, 13], + [1, 1, 12, 0, 8, 10, 6, 5, 0, 14, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1], + [2, 1, 3, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 4, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [1, 1, 6, 0, 8, 10, 6, 1, 0, 13], + [1, 1, 7, 0, 6, 7, 5, 5, 0, -7, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [1, 1, 8, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 9, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 10, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 11, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 12, 0, 8, 10, 6, 5, 0, -17] +]); + +export const swimToFlyBug = createBodyAnimation('swim-to-fly-bug', 16, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, 13], + [1, 1, 4, 0, 8, 10, 6, 5, 0, 14, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1], + [2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 4, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [1, 1, 4, 0, 8, 10, 6, 1, 0, 13], + [1, 1, 5, 0, 6, 7, 5, 5, 0, -7, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -17] +]); + +//const trotSkew = [-1, 0, 1, 0, -1, -2, -3, -2, -1, 0, 1, 0, -1, -2, -3, -2].map(x => (x + 2) * 0.25); + +export const trot = createBodyAnimation('trot', 24, true, [ + [1, 1, 0, 0, 2, 10, 2, 10, 0, 1, 0, -1], + [1, 1, 0, 0, 3, 11, 3, 11], + [1, 1, 0, 0, 4, 12, 4, 12, 0, -1], + [1, 1, 0, 0, 5, 13, 5, 13, 0, -2], + [1, 1, 0, 0, 6, 14, 6, 14, 0, -2], + [1, 1, 0, 0, 7, 15, 7, 15, 0, -2], + [1, 1, 0, 0, 8, 16, 8, 16, 0, -1], + [1, 1, 0, 0, 9, 17, 9, 17], + [1, 1, 0, 0, 10, 2, 10, 2, 0, 1, 0, -1], + [1, 1, 0, 0, 11, 3, 11, 3], + [1, 1, 0, 0, 12, 4, 12, 4, 0, -1], + [1, 1, 0, 0, 13, 5, 13, 5, 0, -2], + [1, 1, 0, 0, 14, 6, 14, 6, 0, -2], + [1, 1, 0, 0, 15, 7, 15, 7, 0, -2], + [1, 1, 0, 0, 16, 8, 16, 8, 0, -1], + [1, 1, 0, 0, 17, 9, 17, 9], +]); + +export const boop = createBodyAnimation('boop', 24, false, [ + [1, 1, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 0, 18, 1, 1, 1], + [1, 1, 0, 0, 19, 1, 1, 1], + [1, 1, 0, 0, 20, 1, 1, 1], + [1, 1, 0, 0, 21, 1, 1, 1], + [1, 1, 0, 0, 22, 28, 18, 18, -1], + [1, 1, 0, 0, 23, 26, 19, 19, -2, -1], + ...repeat(5, [1, 1, 0, 0, 23, 27, 20, 20, -3, -1]), + [1, 1, 0, 0, 23, 26, 19, 19, -2, -1], + [1, 1, 0, 0, 22, 1, 1, 1], + [1, 1, 0, 0, 24, 1, 1, 1], + [1, 1, 0, 0, 25, 1, 1, 1], + [1, 1, 0, 0, 18, 1, 1, 1], + [1, 1, 0, 0, 1, 1, 1, 1], +]); + +export const boopSit = createBodyAnimation('boop-sit', 24, false, [ + [9, 1, 2, 2, 34, 34, 26, 26], + [9, 1, 2, 2, 13, 34, 26, 26, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 19, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 20, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 21, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 22, 34, 26, 26, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 23, 34, 26, 26, 0, -1, 0, 0, -1, -1, 0, 1, 0, 1, 0, -1], + ...repeat(5, [9, 1, 2, 2, 23, 34, 26, 26, -1, -2, 0, 0, -2, -2, 1, 2, 1, 2, 1]), + [9, 1, 2, 2, 23, 34, 26, 26, 0, -1, 0, 0, -1, -1, 0, 1, 0, 1, 0, -1], + [9, 1, 2, 2, 22, 34, 26, 26, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 24, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 25, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 12, 34, 26, 26, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -2], + [9, 1, 2, 2, 34, 34, 26, 26], +], repeat(18, [0, 6])); + +export const boopLie = createBodyAnimation('boop-lie', 24, false, [ + [15, 1, 0, 2, 38, 38, 26, 26], + ...repeat(2, [15, 1, 0, 2, 24, 38, 26, 26, 0, 0, 0, 0, 0, 1]), + [15, 1, 0, 2, 21, 38, 26, 26, 0, 0, 0, 0, 0, 1], + [15, 1, 0, 2, 22, 38, 26, 26, 0, 0, 0, 0, 0, 1], + [15, 1, 0, 2, 23, 38, 26, 26, 0, 0, -1, -1, 0, 1], + [12, 1, 0, 2, 23, 37, 26, 26, -1, 0, 0, 0, -1, 1, 0, 0, 1, 0, 1], + ...repeat(4, [12, 1, 0, 2, 23, 37, 26, 26, -1, 0, 0, 0, -2, 1, 0, 0, 1, 0, 1]), + [15, 1, 0, 2, 23, 38, 26, 26, 0, 0, 0, 0, 0, 1], + [15, 1, 0, 2, 22, 38, 26, 26, 0, 0, 0, 0, 0, 1], + [15, 1, 0, 2, 21, 38, 26, 26, 0, 0, 0, 0, 0, 1], +], repeat(14, [3, 3])); + +export const boopSwim = createBodyAnimation('boop-swim', 24, false, [ + [1, 1, 0, 0, 1, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 18, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 19, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 20, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 21, 10, 6, 5, 0, 12], + [1, 1, 0, 0, 22, 9, 6, 5, -1, 12], + [1, 1, 0, 0, 23, 8, 6, 5, -2, 11], + ...repeat(5, [1, 1, 0, 0, 23, 8, 6, 5, -3, 11]), + [1, 1, 0, 0, 23, 9, 6, 5, -2, 11], + [1, 1, 0, 0, 22, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 24, 10, 6, 5, 0, 13], + [1, 1, 0, 0, 25, 10, 6, 5, 0, 14], + [1, 1, 0, 0, 18, 10, 6, 5, 0, 14], + [1, 1, 0, 0, 1, 10, 6, 5, 0, 14] +]); + +export const sit = createBodyAnimation('sit', 24, true, [ + [9, 1, 2, 2, 34, 34, 26, 26], +], [[0, 6]]); + +const sitShadow = [0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 6].map(offset => [0, offset]); + +export const sitDown = createBodyAnimation('sit-down', 24, false, [ + [1, 1, 0, 0, 1, 1, 1, 1], + ...repeat(2, [2, 1, 0, 0, 29, 29, 1, 1]), + ...repeat(2, [3, 1, 0, 0, 30, 30, 21, 21]), + [4, 1, 0, 0, 31, 31, 22, 22], + [5, 1, 0, 1, 32, 32, 23, 23], + [6, 1, 1, 2, 33, 33, 24, 24], + [7, 1, 2, 2, 34, 34, 25, 25], + [8, 1, 2, 2, 34, 34, 25, 25], + [9, 1, 2, 2, 34, 34, 26, 26], +], sitShadow); + +export const standUp = createBodyAnimation('stand-up', 24, false, [ + [9, 1, 2, 2, 34, 34, 26, 26], + [8, 1, 2, 2, 34, 34, 25, 25], + [7, 1, 2, 2, 34, 34, 25, 25], + [6, 1, 1, 2, 33, 33, 24, 24], + [5, 1, 0, 1, 32, 32, 23, 23], + [4, 1, 0, 0, 31, 31, 22, 22], + ...repeat(2, [3, 1, 0, 0, 30, 30, 21, 21]), + [1, 1, 0, 0, 1, 1, 1, 1], +], sitShadow.slice(2).reverse()); + +export const sitToTrot = createBodyAnimation('sit-to-trot', 24, false, [ + [7, 1, 2, 2, 34, 35, 24, 25, 0, -1, 0, 0, 0, 2, 1, 1, 0, -1], + [6, 1, 1, 2, 27, 36, 23, 24, 0, -2, 0, 0, 0, -2, 0, 0, 0, 1], + [5, 1, 0, 1, 5, 13, 5, 23, 0, -2], + [4, 1, 0, 0, 6, 14, 6, 5, 0, -2], + [3, 1, 0, 0, 7, 15, 7, 15, 0, -2], + [2, 1, 0, 0, 8, 16, 8, 16, 0, -1], +], [[0, 6], [0, 5], [0, 4], [0, 3], [0, 1], [0, 0]]); + +export const lie = createBodyAnimation('lie', 24, true, [ + [15, 1, 0, 2, 38, 38, 26, 26], +], [[3, 3]]); + +const lieShadow = [[0, 6], [0, 6], [1, 5], [2, 4], [3, 3], [3, 3], [3, 3]]; + +export const lieDown = createBodyAnimation('lie-down', 24, false, [ + [9, 1, 2, 2, 34, 34, 26, 26], + [10, 1, 2, 2, 35, 34, 26, 26, 0, 0, 0, 0, 0, 0, 0, -1], + [11, 1, 1, 2, 36, 36, 26, 26, 0, 0, 0, 0, 0, 0, 1], + [12, 1, 1, 2, 37, 37, 26, 26, 0, 0, 0, 0, 0, 0, 1], + [13, 1, 0, 2, 38, 38, 26, 26, 0, 0, 0, 0, 0, 0, 1], + ...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]), +], lieShadow); + +export const sitUp = createBodyAnimation('sit-up', 24, false, [ + ...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]), + [13, 1, 0, 2, 38, 38, 26, 26], + [12, 1, 1, 2, 37, 37, 26, 26], + [11, 1, 1, 2, 36, 36, 26, 26], + [10, 1, 2, 2, 35, 34, 26, 26, 0, 0, 0, 0, 0, 0, 0, -1], + [9, 1, 2, 2, 34, 34, 26, 26], +], lieShadow.slice().reverse()); + +export const lieToTrot = createBodyAnimation('lie-to-trot', 24, false, [ + [1, 1, 0, 1, 36, 37, 24, 25, 4, 8, 0, 1, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 30, 12, 23, 24, 2, 5, 0, 0, 0, -3], + [1, 1, 0, 0, 5, 13, 5, 23, 1, 1], + [1, 1, 0, 0, 6, 14, 6, 21, 0, 0, 0, -1], + [1, 1, 0, 0, 7, 15, 7, 15, 0, -1, 0, -1], + [1, 1, 0, 0, 8, 16, 8, 16, 0, -2], +], [[2, 1], [1, 0], ...repeat(4, [0, 0])]); + +export const fly = createBodyAnimation('fly', 16, true, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -16], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 6, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 7, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 8, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 9, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 10, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 11, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 12, 0, 8, 10, 6, 5, 0, -17], +]); + +export const boopFly = createBodyAnimation('boop-fly', 16, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -16], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 5, 0, 20, 10, 6, 5, 0, -14], + [1, 1, 6, 0, 21, 10, 6, 5, 0, -14], + [1, 1, 7, 0, 22, 10, 6, 5, -1, -15], + [1, 1, 8, 0, 23, 10, 5, 5, -1, -17, -1, 0, -1, -1, 2], + [1, 1, 9, 0, 23, 10, 4, 5, -1, -18, -1, 0, -1, -1, 2], + [1, 1, 10, 0, 23, 10, 4, 4, -1, -18, -1, 0, -1, -1, 2], + [1, 1, 11, 0, 23, 10, 4, 3, -1, -18, -1, 0, -1, -1, 2], + [1, 1, 12, 0, 22, 10, 4, 3, 0, -17, -1, 0, 0, 0, 2], + [1, 1, 3, 0, 21, 10, 5, 4, 0, -16, 0, 0, 0, 0, 2], + [1, 1, 4, 0, 14, 10, 6, 5, 0, -15, 0, 0, 0, 0, 2] +]); + +export const boopFlyBug = createBodyAnimation('boop-fly-bug', 20, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -16], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 5, 0, 20, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 21, 10, 6, 5, 0, -14], + [1, 1, 3, 0, 22, 10, 6, 5, -1, -15], + [1, 1, 4, 0, 23, 10, 5, 5, -1, -17, -1, 0, -1, -1, 2], + [1, 1, 5, 0, 23, 10, 4, 5, -1, -18, -1, 0, -1, -1, 2], + [1, 1, 4, 0, 23, 10, 4, 4, -1, -18, -1, 0, -1, -1, 2], + [1, 1, 3, 0, 23, 10, 4, 3, -1, -18, -1, 0, -1, -1, 2], + [1, 1, 4, 0, 22, 10, 4, 3, 0, -17, -1, 0, 0, 0, 2], + [1, 1, 5, 0, 21, 10, 5, 4, 0, -16, 0, 0, 0, 0, 2], + [1, 1, 4, 0, 14, 10, 6, 5, 0, -15, 0, 0, 0, 0, 2] +]); + +export const flyUp = createBodyAnimation('fly-up', 16, false, [ + [1, 1, 11, 0, 1, 1, 1, 1], + [1, 1, 12, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1], + [2, 1, 3, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 4, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [1, 1, 6, 0, 1, 1, 1, 1], + [1, 1, 7, 0, 6, 7, 5, 5, 0, -10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [1, 1, 8, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 9, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 10, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 11, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 12, 0, 8, 10, 6, 5, 0, -17], +]); + +export const trotToFly = createBodyAnimation('trot-to-fly', 20, false, [ + [1, 1, 11, 0, 6, 14, 6, 14, 0, -2], + [1, 1, 12, 0, 7, 15, 7, 15, 0, -2], + [1, 1, 3, 0, 8, 16, 8, 16, 0, -1], + [1, 1, 4, 0, 9, 17, 9, 17, 0, 1, 0, 1, 0, 0, 0, -1, 0, -1], + [1, 1, 5, 0, 10, 2, 10, 2, 0, 3, 0, 1, 0, 0, 0, -2, 0, -2], + [1, 1, 6, 0, 11, 3, 11, 3], + [1, 1, 7, 0, 11, 4, 11, 4, 0, -10], + [1, 1, 8, 0, 10, 5, 9, 5, 0, -15], + [1, 1, 9, 0, 9, 10, 6, 6, 0, -17], + [1, 1, 10, 0, 8, 10, 6, 7, 0, -18], + [1, 1, 11, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 12, 0, 8, 10, 6, 5, 0, -17] +]); + +export const trotToFlyBug = createBodyAnimation('trot-to-fly-bug', 20, false, [ + [1, 1, 3, 0, 6, 14, 6, 14, 0, -2], + [1, 1, 4, 0, 7, 15, 7, 15, 0, -2], + [1, 1, 5, 0, 8, 16, 8, 16, 0, -1], + [1, 1, 3, 0, 9, 17, 9, 17, 0, 1, 0, 1, 0, 0, 0, -1, 0, -1], + [1, 1, 4, 0, 10, 2, 10, 2, 0, 3, 0, 1, 0, 0, 0, -2, 0, -2], + [1, 1, 5, 0, 11, 3, 11, 3], + [1, 1, 3, 0, 11, 4, 11, 4, 0, -10], + [1, 1, 4, 0, 10, 5, 9, 5, 0, -15], + [1, 1, 5, 0, 8, 10, 6, 6, 0, -17], + [1, 1, 3, 0, 8, 10, 6, 7, 0, -18], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -17] +]); + +export const flyToTrot = createBodyAnimation('fly-to-trot', 20, false, [ + [1, 1, 3, 0, 8, 10, 6, 10, 0, -16], + [1, 1, 4, 0, 8, 11, 5, 11, 0, -15], + [1, 1, 5, 0, 8, 12, 4, 12, 0, -12], + [1, 1, 6, 0, 7, 13, 5, 13, 0, -8], + [1, 1, 6, 0, 7, 14, 6, 14, 0, -6], + [1, 1, 6, 0, 7, 15, 7, 15, 0, -4], + [1, 1, 7, 0, 8, 16, 8, 16, 0, -1], + [1, 1, 11, 0, 9, 17, 9, 17], + [1, 1, 0, 0, 10, 2, 10, 2, 0, 3, 0, -1, 0, 0, 0, -2, 0, -2] + + // [1, 1, 4, 0, 8, 10, 6, 10, 0, -16], + // [1, 1, 5, 0, 8, 10, 6, 10, 0, -18], + // [1, 1, 7, 0, 8, 11, 5, 11, 0, -20], + // [1, 1, 8, 0, 10, 12, 4, 12, 0, -21], + // [1, 1, 9, 0, 11, 13, 4, 13, 0, -20], + // [1, 1, 10, 0, 12, 14, 5, 14, 0, -18], + // [1, 1, 11, 0, 13, 15, 7, 15, 0, -14, 0, -1], + // [1, 1, 4, 0, 14, 16, 8, 16, 0, 0, 0, -1], + // [1, 1, 5, 0, 2, 17, 9, 17, 0, 2, 0, 0, 0, -1, 0, -2, 0, -2, 0, -2], + // [1, 1, 6, 0, 3, 2, 10, 2, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 2] +]); + +export const flyToTrotBug = createBodyAnimation('fly-to-trot-bug', 20, false, [ + [1, 1, 3, 0, 8, 10, 6, 10, 0, -16], + [1, 1, 4, 0, 8, 11, 5, 11, 0, -15], + [1, 1, 5, 0, 8, 12, 4, 12, 0, -12], + [1, 1, 4, 0, 7, 13, 5, 13, 0, -8], + [1, 1, 3, 0, 7, 14, 6, 14, 0, -6], + [1, 1, 4, 0, 7, 15, 7, 15, 0, -4], + [1, 1, 5, 0, 8, 16, 8, 16, 0, -1], + [1, 1, 4, 0, 9, 17, 9, 17], + [1, 1, 3, 0, 10, 2, 10, 2, 0, 3, 0, -1, 0, 0, 0, -2, 0, -2] + + // [1, 1, 4, 0, 8, 10, 6, 10, 0, -16], + // [1, 1, 5, 0, 8, 10, 6, 10, 0, -18], + // [1, 1, 4, 0, 8, 11, 5, 11, 0, -20], + // [1, 1, 3, 0, 10, 12, 4, 12, 0, -21], + // [1, 1, 4, 0, 11, 13, 4, 13, 0, -20], + // [1, 1, 5, 0, 12, 14, 5, 14, 0, -18], + // [1, 1, 4, 0, 13, 15, 7, 15, 0, -14, 0, -1], + // [1, 1, 3, 0, 14, 16, 8, 16, 0, 0, 0, -1], + // [1, 1, 4, 0, 2, 17, 9, 17, 0, 2, 0, 0, 0, -1, 0, -2, 0, -2, 0, -2], + // [1, 1, 5, 0, 3, 2, 10, 2, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 2] +]); + +export const flyDown = createBodyAnimation('fly-down', 16, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -12], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -10], + [1, 1, 6, 0, 8, 10, 6, 5, 0, -8], + [1, 1, 7, 0, 8, 10, 6, 5, 0, -6], + [1, 1, 11, 0, 8, 10, 6, 5, 0, -4], + [1, 1, 1, 0, 8, 10, 6, 5, 0, -2] +]); + +export const flyBug = createBodyAnimation('fly-bug', 24, true, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -16], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -16], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -16], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -17] +]); + +export const flyUpBug = createBodyAnimation('fly-up-bug', 16, false, [ + [1, 1, 3, 0, 1, 1, 1, 1], + [1, 1, 4, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1], + [2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 4, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2], + [1, 1, 4, 0, 1, 1, 1, 1], + [1, 1, 5, 0, 6, 7, 5, 5, 0, -10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -15], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -17], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -18], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -17] +]); + +export const flyDownBug = createBodyAnimation('fly-down-bug', 16, false, [ + [1, 1, 3, 0, 8, 10, 6, 5, 0, -14], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -12], + [1, 1, 5, 0, 8, 10, 6, 5, 0, -10], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -8], + [1, 1, 3, 0, 8, 10, 6, 5, 0, -6], + [1, 1, 4, 0, 8, 10, 6, 5, 0, -4], + [1, 1, 1, 0, 8, 10, 6, 5, 0, -2] +]); + +export const swing = createBodyAnimation('swing', 12, false, [ + ...repeat(1, [1, 1, 0, 0, 1, 1, 1, 1]), + ...repeat(3, [2, 1, 0, 0, 12, 17, 11, 11, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]), +]); + +export const flyAnims = [undefined, fly, fly, fly, flyBug]; +export const flyUpAnims = [undefined, flyUp, flyUp, flyUp, flyUpBug]; +export const flyDownAnims = [undefined, flyDown, flyDown, flyDown, flyDownBug]; + +export const animations = [ + stand, trot, boop, boopSit, boopLie, boopSwim, boopFly, boopFlyBug, sit, sitDown, standUp, lie, lieDown, sitUp, + fly, flyBug, flyUp, flyUpBug, flyDown, flyDownBug, sitToTrot, lieToTrot, flyToTrot, flyToTrotBug, + swim, trotToSwim, swimToTrot, flyToSwim, swimToFly, +]; + +export const sitDownUp = mergeAnimations('sit', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp]); +export const lieDownUp = mergeAnimations('lie', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp]); + +export function mergeAnimations(name: string, fps: number, loop: boolean, animations: BodyAnimation[]): BodyAnimation { + return { + name, + fps, + loop, + frames: flatten(animations.map(a => a.frames)), + shadow: flatten(animations.map(a => a.shadow || a.frames.map(() => ({ frame: 0, offset: 0 })))), + }; +} + +// head animations + +export function createHeadFrame([headX = 0, headY = 0, left = 0, right = 0, mouth = 0]: number[]): HeadAnimationFrame { + return { headX, headY, left, right, mouth }; +} + +export function createHeadAnimation(name: string, fps: number, loop: boolean, frames: number[][]): HeadAnimation { + return { name, fps, loop, frames: frames.map(createHeadFrame) }; +} + +export const smile = createHeadAnimation('smile', 24, true, [ + [0, 0, 1, 1, 0], +]); + +export const nom = createHeadAnimation('nom', 12, true, [ + [0, 0, 1, 1, 0], + [0, 0, 1, 1, 25], +]); + +export const laugh = createHeadAnimation('laugh', 8, false, [ + ...repeat(4, [0, 0, 14, 14, 5], [0, 1, 14, 14, 5]), +]); + +export const yawn = createHeadAnimation('yawn', 12, false, [ + [0, 0, 3, 3, 8], + ...repeat(18, [1, -1, 12, 12, 16]), + ...repeat(8, [0, 0, 12, 12, 12]), + [0, 0, 18, 18, 2], +]); + +export const surprise = createHeadAnimation('surprise', 8, false, [ + [0, 1, 6, 6, 1], + ...repeat(10, [0, 0, 1, 1, 12]), +]); + +export const excite = createHeadAnimation('excite', 8, false, [ + [0, 1, 6, 6, 0], + ...repeat(10, [0, 0, 1, 1, 5]), +]); + +export const surpriseSad = createHeadAnimation('surpriseSad', 8, false, [ + [0, 1, 15, 15, 8], + ...repeat(8, [0, 0, 15, 15, 8]), +]); + +export const sneeze = createHeadAnimation('sneeze', 12, false, [ + [0, 0, 18, 18, 8], + ...repeat(2, [1, -1, 18, 18, 16]), + ...repeat(8, [-1, 1, 23, 23, 13]), + ...repeat(4, [0, 0, 18, 18, 7]), +]); + +export const headAnimations = [ + smile, nom, laugh, yawn, surprise, surpriseSad, sneeze, excite, +]; + +// default animations + +export const defaultBodyAnimation = createBodyAnimation('default', 24, true, [[1, 1, 0, 0, 1, 1, 1, 1]]); +export const defaultHeadAnimation = createHeadAnimation('default', 24, true, [[0, 0, -1, -1, -1]]); + +export const defaultBodyFrame = defaultBodyAnimation.frames[0]; +export const defaultHeadFrame = defaultHeadAnimation.frames[0]; diff --git a/src/ts/client/ponyDraw.ts b/src/ts/client/ponyDraw.ts new file mode 100644 index 0000000..fdd4ddc --- /dev/null +++ b/src/ts/client/ponyDraw.ts @@ -0,0 +1,790 @@ +import { + PonyEye, PonyState, PalettePonyInfo, PaletteSpriteSet, Palette, HeadAnimationFrame, + Eye, Iris, ColorExtraSets, ExpressionExtra, BodyAnimationFrame, DrawPonyOptions, Muzzle, BodyShadow, DrawOptions, + NoDraw, PaletteSpriteBatch, defaultDrawOptions, PonyStateFlags, PaletteManager, isEyeSleeping, Matrix2D, +} from '../common/interfaces'; +import { WHITE, SHINES_COLOR, FAR_COLOR, TRANSPARENT, fillToOutlineColor } from '../common/colors'; +import { toInt, hasFlag, repeat, flatten, point } from '../common/utils'; +import * as sprites from '../generated/sprites'; +import * as offsets from '../common/offsets'; +import { defaultHeadAnimation, defaultBodyFrame, defaultHeadFrame } from './ponyAnimations'; +import { toWorldX, toWorldY } from '../common/positionUtils'; +import { + frontHooves, PONY_WIDTH, PONY_HEIGHT, wings, chestBehind, tails, chest, neckAccessories, waistAccessories, + SLEEVED_ACCESSORIES, blinkFrames, flipIris, claws, Sets, backAccessories, SLEEVED_BACK_ACCESSORIES, + CHEST_ACCESSORIES_IN_FRONT, flipFaceAccessoryType, flipFaceAccessoryPattern, backLegSleeves, + NO_MANE_HEAD_ACCESSORIES, backHoovesInFront, frontHoovesInFront +} from './ponyUtils'; +import { HEAD_ACCESSORY_OFFSETS, EAR_ACCESSORY_OFFSETS, EXTRA_ACCESSORY_OFFSETS } from '../common/offsets'; +import { createMat2D, identityMat2D, translateMat2D, copyMat2D, rotateMat2D, scaleMat2D } from '../common/mat2d'; +import { darkenForOutline } from '../common/ponyInfo'; + +type Batch = PaletteSpriteBatch; +type Info = Readonly; +type State = Readonly; +type Options = Readonly; + +const holdingDrawOptions: DrawOptions = { + ...defaultDrawOptions, + shadowColor: TRANSPARENT, +}; + +function checker(parts: number[]) { + const set = new Set(parts); + return (part: number) => set.has(part); +} + +function partChecker(parts: number[]) { + const check = checker(parts); + return (set: PaletteSpriteSet | undefined) => set !== undefined && !!set.type && check(set.type); +} + +const SHADOW_OX = 20; +const SHADOW_OY = 64; +const FAR_OX = -3; +const FAR_OY = -1; +const FAR_WING_OX = -4; +const FAR_WING_OY = 0; + +const hasNoMane = checker(NO_MANE_HEAD_ACCESSORIES); +const behindBackAccessory = partChecker([1]); +const sleevedAccessory = partChecker(SLEEVED_ACCESSORIES); +const sleevedBackAccessory = partChecker(SLEEVED_BACK_ACCESSORIES); +const chestAccessoryInFront = partChecker(CHEST_ACCESSORIES_IN_FRONT); + +const pointZero = point(0, 0); + +const headFlipOffsetX = 35.5; +const headFlipOffsetY = 42; + +const headTransform = createMat2D(); + +function clamp(value: number, min: number, max: number): number { + return value > min ? (value < max ? value : max) : min; +} + +function at(items: T[], index: any): T | undefined { + // if (DEVELOPMENT) { + // if (items.length === 0) { + // console.warn(`Empty array at: ${getStackLocation(2)} / ${getStackLocation(3)}`); + // } else if (index < 0 || index >= items.length) { + // console.warn(`Index out of range ${index} of 0..${items.length} at: ${getStackLocation(2)} / ${getStackLocation(3)}`); + // } + // } + + return items[clamp(index | 0, 0, items.length - 1)]; +} + +function att(items: T[] | undefined, index: any): T | undefined { + return items && items[clamp(index | 0, 0, items.length - 1)]; +} + +function atDef(items: T[] | undefined, index: number, def: T): T { + return (items && items.length > 0 && index >= 0 && index < items.length) ? items[index | 0] : def; +} + +export function getPonyAnimationFrame({ frames }: { frames: T[] }, frame: number, defaultFrame: T): T { + return frames.length > 0 ? frames[Math.max(0, frame) % frames.length] : defaultFrame; +} + +function getHeadXY(x: number, y: number, turned: boolean, frame: BodyAnimationFrame, headFrame: HeadAnimationFrame) { + const headOffset = at(offsets.headOffsets, frame.body)!; + const headX = x + frame.headX + (headFrame.headX * (turned ? -1 : 1)) + headOffset.x; + const headY = y + frame.headY + headFrame.headY + headOffset.y; + return { headX, headY }; +} + +export function getPonyHeadPosition(state: State, ponyX: number, ponyY: number) { + const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame); + const headFrame = getPonyAnimationFrame(state.headAnimation || defaultHeadAnimation, state.headAnimationFrame, defaultHeadFrame); + const baseX = ponyX - PONY_WIDTH / 2; + const baseY = ponyY - PONY_HEIGHT; + const x = baseX + frame.bodyX; + const y = baseY + frame.bodyY; + const { headX, headY } = getHeadXY(x, y, state.headTurned, frame, headFrame); + return { x: headX, y: headY }; +} + +export function createHeadTransform( + originalTransform: Matrix2D | undefined, headX: number, headY: number, { headTilt, headTurned }: State +) { + if (originalTransform !== undefined) { + copyMat2D(headTransform, originalTransform); + } else { + identityMat2D(headTransform); + } + + translateMat2D(headTransform, headTransform, headX + headFlipOffsetX, headY + headFlipOffsetY); + + if (headTilt) { + rotateMat2D(headTransform, headTransform, headTilt * 0.1); + } + + scaleMat2D(headTransform, headTransform, headTurned ? -1 : 1, 1); + translateMat2D(headTransform, headTransform, -headFlipOffsetX, -headFlipOffsetY); + return headTransform; +} + +export function getHeadY(frame: BodyAnimationFrame, headFrame: HeadAnimationFrame): number { + const headOffset = offsets.headOffsets[frame.body]; + return frame.bodyY + frame.headY + headFrame.headY + headOffset.y; +} + +const defaultShadow: BodyShadow = { frame: 0, offset: 0 }; +const hairOffsets = [ + 0, 0, 0, 0, + -1, -1, 0, 0, + 0, 0, 0, -1, + -1, 0, 0, 0, + 0, 0, 0, 0, + ...repeat(100, 0), +]; + +function draw(options: Options, flag: NoDraw) { + if (TOOLS) { + return !hasFlag(options.no, flag); + } else { + return true; + } +} + +const headOffsetsX = [0, 1, 1, 1, 1, 1, 0]; +const headOffsetsY = [0, 0, 1, 1, 0, 0, 0]; + +let toys: (PaletteSpriteSet | undefined)[] = []; + +export function initializeToys(paletteManager: PaletteManager) { + function set(type: number, pattern: number, colors: number[]): PaletteSpriteSet { + const palette = [ + TRANSPARENT, + ...flatten(colors.map(color => [color, darkenForOutline(fillToOutlineColor(color))])) + ]; + + return { type, pattern, palette: paletteManager.add(palette) }; + } + + toys = [ + undefined, + // hat + set(2, 0, [0xffffffff, 0xff2525ff]), + set(2, 0, [0xffffffff, 0x22ac22ff]), + set(2, 0, [0xffffffff, 0x2e58f4ff]), + set(2, 0, [0xffffffff, 0xff71ffff]), + // snowpony + set(3, 0, [0xffffffff, 0x000000ff, 0xff9100ff, 0xff0000ff]), + set(4, 0, [0xffffffff, 0x000000ff, 0xff9100ff, 0xff0000ff]), + set(4, 0, [0x404040ff, 0xff0000ff, 0x000000ff, 0xff0000ff]), + // gift + set(6, 0, [0xcf1717ff, 0xecd132ff]), + set(6, 0, [0xdfc588ff, 0xe7559bff]), + set(6, 0, [0x7fc484ff, 0x4a79daff]), + set(6, 0, [0xd56a69ff, 0x62ab64ff]), + set(6, 0, [0xe586dfff, 0x9553c1ff]), + // hanging thing + set(5, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0xb9c0d8ff, 0xffd94fff, 0xeca242ff]), // bell + set(7, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0xb9c0d8ff, 0xc0ccc4ff]), // mistletoe + set(17, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0x000000ff, 0xe7b86fff, 0x3f1d0fff]), // cookie + set(10, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0x000000ff]), // spider + // teddy + set(8, 0, [0xa86230ff, 0xdfbe8bff, TRANSPARENT, TRANSPARENT]), // brown + set(8, 0, [0xa86230ff, 0xdfbe8bff, 0xffa500ff, 0xffffffff]), // brown angel + set(8, 0, [0x474444ff, 0x96623eff, TRANSPARENT, TRANSPARENT]), // black + set(8, 0, [0x474444ff, 0x96623eff, 0xffa500ff, 0xffffffff]), // black angel + set(9, 1, [0xa86230ff, 0xdfbe8bff, 0xff0000ff, TRANSPARENT, 0xf5f5f5ff]), // brown clothes + set(9, 1, [0x474444ff, 0x96623eff, 0xff0000ff, TRANSPARENT, 0xf5f5f5ff]), // black clothes + set(9, 0, [0xdce5edff, 0xffffffff, 0xff0000ff, 0xdfbe8bff, 0x645137ff]), // white santa + // xmas tree + set(11, 0, [0x1a9b2fff, 0x56c7ffff, 0xde4d68ff, 0xf1d224ff]), + set(11, 1, [0x1a9b2fff, 0xf1d224ff, 0x1a9b2fff, 0xde4d68ff]), + // deer + set(12, 0, [0x7b4b24ff, 0xcf0e0eff, 0xbfaa8cff]), + set(16, 0, [0x7b4b24ff, 0xcf0e0eff, 0xbfaa8cff, 0xffffffff, 0x56c7ffff, 0xf1d224ff]), + // candy horns + set(13, 0, [0xffffffff, 0xff1b1bff, TRANSPARENT, TRANSPARENT]), // one + set(13, 0, [0xffffffff, 0xff1b1bff, 0xffffffff, 0xff1b1bff]), // two + set(13, 0, [0x58df6aff, 0xffffffff, 0x3387e9ff, 0xffffffff]), // two (alt) + // star + set(14, 0, [0xffd94fff, 0xeca242ff]), + // halo + set(15, 0, [0xffd94fff, 0xeca242ff]), + ]; + + if (DEVELOPMENT && toys.length > 33) { + console.error('too many toys', toys.length); + } +} + +const zeroPoint = point(0, 0); +const wakes = [ + { ox: 21, oy: 60, behind: sprites.pony_wake_4, front: sprites.pony_wake_3 }, + { ox: 24, oy: 60, behind: sprites.pony_wake_6, front: sprites.pony_wake_5 }, + { ox: 18, oy: 51, behind: sprites.pony_wake_2, front: sprites.pony_wake_1 }, +]; + +const wakeIndices = [0, 2, 1, 0, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1]; + +function getWakeIndex(info: Info) { + const tail = info.tail && info.tail.type || 0; + return wakeIndices[tail]; +} + +export function drawPony(batch: Batch, info: Info, state: State, ponyX: number, ponyY: number, options: Options) { + const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame); + const headFrame = getPonyAnimationFrame(state.headAnimation || defaultHeadAnimation, state.headAnimationFrame, defaultHeadFrame); + const baseX = ponyX - PONY_WIDTH / 2; + const baseY = ponyY - PONY_HEIGHT; + const x = baseX + frame.bodyX; + const y = baseY + frame.bodyY; + const body = frame.body; + const { headX, headY } = getHeadXY(x, y, state.headTurned, frame, headFrame); + const frontLegOffset = at(offsets.frontLegOffsets, body)!; + const backLegOffset = at(offsets.backLegOffsets, body)!; + const wingOffset = at(offsets.wingOffsets, body)!; + const chestOffset = at(offsets.chestAccessoryOffsets, body)!; + const chestX = x + chestOffset.x; + const chestY = y + chestOffset.y; + const shadow = atDef(state.animation.shadow, state.animationFrame, defaultShadow); + const backOffset = at(offsets.backAccessoryOffsets, body)!; + const wing = at(wings, frame.wing); + const flipped = options.flipped; + + const headOffset = clamp(state.headTurn, 0, headOffsetsX.length - 1); + const headOffsetX = headOffsetsX[headOffset]; + const headOffsetY = headOffsetsY[headOffset]; + + const headTotalY = headY + headOffsetY; + const headTransform = createHeadTransform(undefined, headX + headOffsetX, headTotalY, state); + const headCropY = 42 - ((headTotalY + headFlipOffsetY) - baseY); + const cropW = 80; + const cropH = 65; + + const shadowX = baseX + shadow.offset + SHADOW_OX; + const shadowY = baseY + SHADOW_OY; + + const wake = wakes[getWakeIndex(info)]; + const wakeX = baseX + wake.ox; + const wakeY = baseY + wake.oy; + const wakeFrame = Math.floor(options.gameTime * 7 / 1000) % wake.behind.frames.length; + const swimming = options.swimming; + + if (swimming) { + batch.drawSprite(wake.behind.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY); + batch.crop(-40, -70, cropW, cropH); + } + + // selection + if (options.selected) { + const sprite = at(sprites.ponySelections, shadow.frame); + sprite && batch.drawSprite(sprite, WHITE, info.defaultPalette, shadowX, shadowY); + } + + // shadow + if (options.shadow) { + const sprite = at(sprites.ponyShadows, shadow.frame); + sprite && batch.drawSprite(sprite, options.shadowColor, info.defaultPalette, shadowX, shadowY); + } + + // head accessory + const bounce = BETA && options.bounce; + const maneOffsetY = bounce ? hairOffsets[state.animationFrame] : 0; + const maneBehindOffsetY = bounce ? hairOffsets[state.animationFrame] : 0; + const hatOffsetY = maneBehindOffsetY; + let hatOffset = at(HEAD_ACCESSORY_OFFSETS, info.mane ? info.mane.type : 0)!; + const noMane = !info.mane || hasNoMane(info.mane.type); + + if (info.headAccessory !== undefined && info.headAccessory.type === 20) { + hatOffset = zeroPoint; + } + + if (draw(options, NoDraw.Behind)) { + // far wing + drawSet(batch, wing, info.wings, x + FAR_WING_OX + wingOffset.x, y + FAR_WING_OY + wingOffset.y, FAR_COLOR); + + batch.save(); + batch.multiplyTransform(headTransform); + + if (swimming) { + // batch.drawRect(0xffff0066, 0, headCropY, cropW, cropH); + batch.crop(0, headCropY, cropW, cropH); + } + + if (noMane) { + drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE); + } + + if (draw(options, NoDraw.FarEar)) { + drawSet(batch, sprites.earAccessoriesBehind, info.earAccessory, 0, 0, WHITE); + } + } + + if (draw(options, NoDraw.Body) && draw(options, NoDraw.FarEar)) { + drawSet(batch, sprites.earsFar, info.ears, 0, 0, draw(options, NoDraw.FarEarShade) ? FAR_COLOR : WHITE); + } + + if (draw(options, NoDraw.Behind)) { + drawSet(batch, sprites.hornsBehind, info.horn, 0, 0, WHITE); + + if (!noMane) { + drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE); + } + + drawSet(batch, sprites.backBehindManes, info.backMane, 0, maneBehindOffsetY, WHITE); + + if (!state.headTurned) { + drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE); + } + + batch.restore(); + + // chest accessory behind + drawSet(batch, chestBehind[body], info.chestAccessory, chestX, chestY, WHITE); + } + + // legs + const behindX = x + FAR_OX; + const behindY = y + FAR_OY; + const hasTailAccessory = behindBackAccessory(info.backAccessory); + const hasSleeves = sleevedAccessory(info.chestAccessory); + const hasBackSleeves = sleevedBackAccessory(info.backAccessory); + + const frontBehindX = behindX + frontLegOffset.x + frame.frontFarLegX; + const frontBehindY = behindY + frontLegOffset.y + frame.frontFarLegY; + const backBehindX = behindX + backLegOffset.x + frame.backFarLegX; + const backBehindY = behindY + backLegOffset.y + frame.backFarLegY; + + // far leg back + if (draw(options, NoDraw.BackFarLeg)) { + drawLeg(batch, backBehindX, backBehindY, frame.backFarLeg, sprites.backLegs, + sprites.backLegHooves, sprites.backLegAccessories, info.backLegs, + flipped ? info.backLegAccessory : info.backLegAccessoryRight, info.backHooves, backHoovesInFront, FAR_COLOR, + undefined, false, 0, 0); + } + + // far leg front + if (draw(options, NoDraw.FrontFarLeg)) { + drawLeg(batch, frontBehindX, frontBehindY, frame.frontFarLeg, sprites.frontLegs, + frontHooves, sprites.frontLegAccessories, info.frontLegs, + flipped ? info.frontLegAccessory : info.frontLegAccessoryRight, info.frontHooves, frontHoovesInFront, FAR_COLOR, + undefined, false, 0, 0); + } + + // far leg back sleeve + if (draw(options, NoDraw.FarSleeves) && hasBackSleeves) { + drawSet(batch, at(sprites.backLegSleeves, frame.backFarLeg), info.backAccessory, backBehindX, backBehindY, FAR_COLOR); + } + + // far leg front sleeve + if (draw(options, NoDraw.FarSleeves) && hasSleeves) { + drawSet(batch, at(sprites.frontLegSleeves, frame.frontFarLeg), info.sleeveAccessory, frontBehindX, frontBehindY, FAR_COLOR); + } + + // tail + const tailOffset = at(offsets.tailOffsets, body)!; + const tailX = x + tailOffset.x; + const tailY = y + tailOffset.y; + const failFrame = hasFlag(state.flags, PonyStateFlags.CurlTail) ? 1 : frame.tail; + + drawSet(batch, at(tails, failFrame), info.tail, tailX, tailY, WHITE); + + // tail accessory + if (draw(options, NoDraw.BackAccessory) && hasTailAccessory) { + drawSet(batch, backAccessories[body], info.backAccessory, x + backOffset.x, y + backOffset.y, WHITE); + } + + // body + if (draw(options, NoDraw.Body) && draw(options, NoDraw.BodyOnly)) { + drawSet(batch, sprites.body[body], info.body, x, y, WHITE); + } + + // neck accessory + const frontNeckAccessory = true; // hasPart(info.neckAccessory, FRONT_NECK_ACCESSORIES); + + // if (!frontNeckAccessory) { + // drawSpriteSet(context, neckAccessories[frame.body], info.neckAccessory, x, y); + // } + + const frontX = x + frontLegOffset.x + frame.frontLegX; + const frontY = y + frontLegOffset.y + frame.frontLegY; + const backX = x + backLegOffset.x + frame.backLegX; + const backY = y + backLegOffset.y + frame.backLegY; + const cmOffset = at(offsets.cmOffsets, body)!; + const hooves = (TOOLS && options.useAllHooves) ? sprites.frontLegHooves : frontHooves; + + // close legs back + if (draw(options, NoDraw.BackLeg)) { + drawLeg( + batch, backX, backY, frame.backLeg, sprites.backLegs, sprites.backLegHooves, sprites.backLegAccessories, + info.backLegs, flipped ? info.backLegAccessoryRight : info.backLegAccessory, info.backHooves, backHoovesInFront, WHITE, + info.cmPalette, flipped && !!info.cmFlip, x + cmOffset.x, y + cmOffset.y); + } + + // back accessory + if (draw(options, NoDraw.BackAccessory) && !hasTailAccessory) { + drawSet(batch, backAccessories[body], info.backAccessory, x + backOffset.x, y + backOffset.y, WHITE); + } + + // close leg back sleeves + if (draw(options, NoDraw.CloseSleeves) && hasBackSleeves) { + drawSet(batch, at(backLegSleeves, frame.backLeg), info.backAccessory, backX, backY, WHITE); + } + + const isChestAccessoryInFront = chestAccessoryInFront(info.chestAccessory); + + // chest accessory + if (!isChestAccessoryInFront && draw(options, NoDraw.Front)) { + drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE); + } + + // close legs front + if (draw(options, NoDraw.FrontLeg)) { + drawLeg( + batch, frontX, frontY, frame.frontLeg, sprites.frontLegs, hooves, sprites.frontLegAccessories, + info.frontLegs, flipped ? info.frontLegAccessoryRight : info.frontLegAccessory, info.frontHooves, frontHoovesInFront, WHITE, + undefined, false, 0, 0); + } + + // close legs front sleeves + if (draw(options, NoDraw.CloseSleeves) && hasSleeves) { + drawSet(batch, at(sprites.frontLegSleeves, frame.frontLeg), info.sleeveAccessory, frontX, frontY, WHITE); + } + + // chest accessory + if (isChestAccessoryInFront && draw(options, NoDraw.Front)) { + drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE); + } + + // close legs back (2) + if (draw(options, NoDraw.BackLeg)) { + drawLeg( + batch, backX, backY, frame.backLeg, sprites.backLegs2, sprites.backLegHooves2, sprites.backLegAccessories2, + info.backLegs, flipped ? info.backLegAccessoryRight : info.backLegAccessory, info.backHooves, backHoovesInFront, WHITE, + undefined, false, 0, 0); + } + + // close legs back sleeves (2) + if (draw(options, NoDraw.CloseSleeves) && hasBackSleeves) { + drawSet(batch, at(sprites.backLegSleeves2, frame.backLeg), info.backAccessory, backX, backY, WHITE); + } + + // neck accessory + if (frontNeckAccessory) { + const neckOffset = at(offsets.neckAccessoryOffsets, body)!; + drawSet(batch, neckAccessories[body], info.neckAccessory, x + neckOffset.x, y + neckOffset.y, WHITE); + } + + // waist accessory + const waistFrame = frame.wing > 2 ? 16 : body; + const waistOffset = at(offsets.waistAccessoryOffsets, waistFrame)!; + drawSet(batch, waistAccessories[waistFrame], info.waistAccessory, x + waistOffset.x, y + waistOffset.y, WHITE); + + // wings + drawSet(batch, wing, info.wings, x + wingOffset.x, y + wingOffset.y, WHITE); + + // head + const headTurned = state.headTurned; + const headFlip = headTurned ? !flipped : flipped; + const headSprite = headFlip ? sprites.head0[frame.head] : sprites.head1[frame.head]; + + if (swimming) { + batch.clearCrop(); + } + + batch.save(); + batch.multiplyTransform(headTransform); + + if (swimming) { + batch.crop(0, headCropY, cropW, cropH); + } + + if (headTurned) { + drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE); + } + + drawHead(batch, info, 0, 0, headSprite, headFrame, state, options, headFlip, maneOffsetY); + drawSet(batch, sprites.headAccessories, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE); + + batch.restore(); + + if (swimming) { + batch.drawSprite(wake.front.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY); + } +} + +export function drawHead( + batch: Batch, info: Info, x: number, y: number, headSprites: ColorExtraSets, headFrame: HeadAnimationFrame, + { blinkFrame, expression, holding, blushColor, drawFaceExtra }: State, + options: Options, flip: boolean, maneOffsetY: number, +) { + const extraOffset = at(EXTRA_ACCESSORY_OFFSETS, info.mane && info.mane.type) || pointZero; + const extraX = x + extraOffset.x; + const extraY = y + extraOffset.y; + const toy = att(toys, options.toy); + + if (toy !== undefined) { + drawSet(batch, sprites.extraAccessoriesBehind, toy, extraX, extraY, WHITE); + } else if (options.extra && draw(options, NoDraw.Behind)) { + drawSet(batch, sprites.extraAccessoriesBehind, info.extraAccessory, extraX, extraY, WHITE); + } + + if (draw(options, NoDraw.Body)) { + if (draw(options, NoDraw.Head)) { + drawSet(batch, headSprites, info.head, x, y, WHITE); + } + + let eyeLeftBase = -1; + let eyeRightBase = -1; + let irisLeft = Iris.Forward; + let irisRight = Iris.Forward; + + if (expression !== undefined) { + if (hasFlag(expression.extra, ExpressionExtra.Blush)) { + batch.drawSprite(sprites.blush, blushColor, info.defaultPalette, x, y); + } + + eyeLeftBase = expression.left; + eyeRightBase = expression.right; + irisLeft = expression.leftIris; + irisRight = expression.rightIris; + + // make sure eyes are closed if sleeping + if (hasFlag(expression.extra, ExpressionExtra.Zzz)) { + if (!isEyeSleeping(eyeLeftBase)) { + eyeLeftBase = Eye.Closed; + } + + if (!isEyeSleeping(eyeRightBase)) { + eyeRightBase = Eye.Closed; + } + } + } + + const eyeRight = getEyeFrame(info.eyeOpennessRight || 1, eyeRightBase, headFrame.right, blinkFrame); + const eyeLeft = getEyeFrame(info.eyeOpennessLeft || 1, eyeLeftBase, headFrame.left, blinkFrame); + const eyeFrameLeft = flip ? eyeRight : eyeLeft; + const eyeFrameRight = flip ? eyeLeft : eyeRight; + const eyeColorLeft = flip ? info.eyeColorRight : info.eyeColorLeft; + const eyeColorRight = flip ? info.eyeColorLeft : info.eyeColorRight; + const eyePaletteLeft = flip ? info.eyePalette : info.eyePaletteLeft; + const eyePaletteRight = flip ? info.eyePaletteLeft : info.eyePalette; + const eyeIrisLeft = flip ? flipIris(irisRight) : irisLeft; + const eyeIrisRight = flip ? flipIris(irisLeft) : irisRight; + const eyeLeftSprites = sprites.eyeLeft; + const eyeRightSprites = sprites.eyeRight; + + if (draw(options, NoDraw.Eyes)) { + drawEye( + batch, att(at(eyeLeftSprites, eyeFrameLeft), info.eyelashes), + eyeIrisLeft, info, eyeColorLeft, eyePaletteLeft, x, y); + drawEye( + batch, att(at(eyeRightSprites, eyeFrameRight), info.eyelashes), + eyeIrisRight, info, eyeColorRight, eyePaletteRight, x, y); + } + } + + if (draw(options, NoDraw.Front)) { + drawSet(batch, sprites.facialHairBehind, info.facialHair, x, y, WHITE); + } + + if (drawFaceExtra !== undefined) { + drawFaceExtra(batch); + } + + const faceAccessory = info.faceAccessory; + let faceAccessoryType = 0; + let faceAccessoryPattern = 0; + + if (faceAccessory !== undefined) { + faceAccessoryType = flip ? flipFaceAccessoryType(faceAccessory.type) : faceAccessory.type; + faceAccessoryPattern = flip ? flipFaceAccessoryPattern(faceAccessoryType, faceAccessory.pattern) : faceAccessory.pattern; + + if (draw(options, NoDraw.FaceAccessory1)) { + drawTypePattern( + batch, sprites.faceAccessories, faceAccessoryType, faceAccessoryPattern, + faceAccessory.palette, faceAccessory.extraPalette, x, y, WHITE); + + // if (info.faceAccessoryExtraPalette) { + // drawTypePattern( + // batch, sprites.faceAccessoriesExtra, faceAccessoryType, faceAccessoryPattern, + // info.faceAccessoryExtraPalette, x, y, WHITE); + // } + } + } + + if (draw(options, NoDraw.Body) && draw(options, NoDraw.Nose)) { + const muzzle = holding ? + Muzzle.Smile : + headFrame.mouth === -1 ? + (expression ? expression.muzzle : info.muzzle) : + headFrame.mouth; + + const noses = at(sprites.noses, muzzle); + const nose = att(noses, info.nose && info.nose.type)![0]; + + nose.mouth && batch.drawSprite(nose.mouth, WHITE, info.defaultPalette, x, y); + + if (holding !== undefined && holding.draw !== undefined) { + holding.x = toWorldX(x + toInt(holding.pickableX)); + holding.y = toWorldY(y + toInt(holding.pickableY)); + holding.draw(batch, holdingDrawOptions); + } + + drawSet(batch, noses, info.nose, x, y, WHITE); + + if (info.fangs && nose.fangs) { + batch.drawSprite(nose.fangs, WHITE, info.defaultPalette, x, y); + } + } + + if (draw(options, NoDraw.Front2)) { + drawSet(batch, sprites.facialHair, info.facialHair, x, y, WHITE); + } + + const skipTopAndFrontMane = info.headAccessory !== undefined && info.headAccessory.type === 20; + + if (draw(options, NoDraw.FrontMane)) { + drawSet(batch, sprites.backFrontManes, info.backMane, x, y + maneOffsetY, WHITE); + } + + if (draw(options, NoDraw.TopMane) && !skipTopAndFrontMane) { + drawSet(batch, sprites.topManes, info.mane, x, y, WHITE); + } + + if (toy !== undefined) { + drawSet(batch, sprites.extraAccessories, toy, extraX, extraY, WHITE); + } else if (options.extra && draw(options, NoDraw.Front)) { + drawSet(batch, sprites.extraAccessories, info.extraAccessory, extraX, extraY, WHITE); + } + + if (draw(options, NoDraw.Front)) { + drawSet(batch, sprites.horns, info.horn, x, y, WHITE); + } + + if (draw(options, NoDraw.Body) && draw(options, NoDraw.CloseEar) && !options.noEars) { + drawSet(batch, sprites.ears, info.ears, x, y, WHITE); + } + + if (faceAccessory !== undefined && draw(options, NoDraw.FaceAccessory2)) { + drawTypePattern( + batch, sprites.faceAccessories2, faceAccessoryType, faceAccessoryPattern, + faceAccessory.palette, faceAccessory.extraPalette, x, y, WHITE); + + // if (info.faceAccessoryExtraPalette) { + // drawTypePattern( + // batch, sprites.faceAccessories2Extra, faceAccessoryType, faceAccessoryPattern, + // info.faceAccessoryExtraPalette, x, y, WHITE); + // } + } + + const earAccessoryOffset = at(EAR_ACCESSORY_OFFSETS, info.ears && info.ears.type)!; + const frontEarAccessory = false; // info.earAccessory !== undefined && info.earAccessory.type === 13; + + if (!frontEarAccessory && draw(options, NoDraw.Front) && draw(options, NoDraw.CloseEar)) { + drawSet(batch, sprites.earAccessories, info.earAccessory, x + earAccessoryOffset.x, y + earAccessoryOffset.y, WHITE); + } + + if (draw(options, NoDraw.FrontMane) && !skipTopAndFrontMane) { + drawSet(batch, sprites.frontManes, info.mane, x, y + maneOffsetY, WHITE); + } + + if (frontEarAccessory && draw(options, NoDraw.Front) && draw(options, NoDraw.CloseEar)) { + drawSet(batch, sprites.earAccessories, info.earAccessory, x + earAccessoryOffset.x, y + earAccessoryOffset.y, WHITE); + } +} + +function drawLeg( + batch: Batch, x: number, y: number, frame: number, leg: Sets, hoof: Sets, sock: Sets, + legSet: PaletteSpriteSet | undefined, sockSet: PaletteSpriteSet | undefined, hoofSet: PaletteSpriteSet | undefined, + hoovesInFront: boolean[], color: number, cmPalette: Palette | undefined, cmFlip: boolean, cmX: number, cmY: number +) { + const hoofInFront = hoofSet !== undefined && !!hoovesInFront[hoofSet.type]; + + drawSet(batch, at(leg, frame), legSet, x, y, color); + + if (!hoofInFront) { + drawSet(batch, at(hoof, frame), hoofSet, x, y, color); + } + + // CM + if (cmPalette !== undefined) { + const cm = cmFlip ? sprites.cmsFlip : sprites.cms; + batch.drawSprite(cm, WHITE, cmPalette, cmX, cmY); + } + + drawSet(batch, at(sock, frame), sockSet, x, y, color); + + if (hoofInFront) { + const hasClaws = hoofSet && hoofSet.type === 3 && hoof === frontHooves; + const hasSocks = !!(sockSet && sockSet.type > 0); + const hoofSprites = (hasClaws && hasSocks) ? claws : hoof; + drawSet(batch, at(hoofSprites, frame), hoofSet, x, y, color); + } +} + +function getEyeFrame(base: Eye, expression: Eye, anim: Eye, blinkFrame: number) { + if (anim !== -1) + return anim; + + const frame = expression === -1 ? base : expression; + const blink = blinkFrames[frame]; + + if (blinkFrame > 1 && blink) { + const frameOffset = 6 - blinkFrame; + + if (frameOffset < blink.length) { + return blink[blink.length - frameOffset - 1]; + } + } + + return frame; +} + +function drawEye( + batch: Batch, eye: PonyEye | undefined, iris: Iris, info: Info, palette: Palette | undefined, eyePalette: Palette, + x: number, y: number +) { + if (eye !== undefined) { + if (info.eyeshadow === true) { + eye.shadow && batch.drawSprite(eye.shadow, WHITE, info.eyeshadowColor, x, y); + eye.shine && batch.drawSprite(eye.shine, SHINES_COLOR, info.defaultPalette, x, y); + } + + eye.base && batch.drawSprite(eye.base, WHITE, eyePalette, x, y); + const sprite = at(eye.irises, iris); + sprite && batch.drawSprite(sprite, WHITE, palette, x, y); + } +} + +function drawSet( + batch: Batch, sprites: ColorExtraSets, set: PaletteSpriteSet | undefined, x: number, y: number, tint: number +) { + if (set !== undefined) { + const patterns = att(sprites, set.type); + + if (patterns !== undefined) { + const patternSprite = at(patterns, set.pattern); + + if (patternSprite !== undefined) { + batch.drawSprite(patternSprite.color, tint, set.palette, x, y); + } + } + } +} + +function drawTypePattern( + batch: Batch, sprites: ColorExtraSets, type: number, pattern: number, palette: Palette, extraPalette: Palette | undefined, + x: number, y: number, tint: number +) { + const patterns = att(sprites, type); + + if (patterns !== undefined) { + const patternSprite = at(patterns, pattern); + + if (patternSprite !== undefined) { + batch.drawSprite(patternSprite.color, tint, palette, x, y); + + if (patternSprite.extra !== undefined && extraPalette !== undefined) { + batch.drawSprite(patternSprite.extra, WHITE, extraPalette, x, y); + } + } + } +} diff --git a/src/ts/client/ponyHelpers.ts b/src/ts/client/ponyHelpers.ts new file mode 100644 index 0000000..2030a68 --- /dev/null +++ b/src/ts/client/ponyHelpers.ts @@ -0,0 +1,56 @@ +import { DrawPonyOptions, NoDraw, PonyState, PonyStateFlags } from '../common/interfaces'; +import { SHADOW_COLOR, blushColor } from '../common/colors'; +import { stand } from './ponyAnimations'; + +const defaultBlushColor = blushColor(0); + +export function defaultPonyState(): PonyState { + return { + animation: stand, + animationFrame: 0, + headAnimation: undefined, + headAnimationFrame: 0, + headTurned: false, + headTilt: 0, + headTurn: 0, + blinkFrame: 0, + blushColor: defaultBlushColor, + holding: undefined, + expression: undefined, + drawFaceExtra: undefined, + flags: PonyStateFlags.None, + }; +} + +export function isStateEqual(a: PonyState, b: PonyState) { + return a.animation === b.animation && + a.animationFrame === b.animationFrame && + a.headAnimation === b.headAnimation && + a.headAnimationFrame === b.headAnimationFrame && + a.headTurned === b.headTurned && + a.headTilt === b.headTilt && + a.headTurn === b.headTurn && + a.blinkFrame === b.blinkFrame && + a.blushColor === b.blushColor && + a.holding === b.holding && + a.expression === b.expression && + a.drawFaceExtra === b.drawFaceExtra && + a.flags === b.flags; +} + +export function defaultDrawPonyOptions(): DrawPonyOptions { + return { + flipped: false, + selected: false, + shadow: false, + extra: false, + toy: 0, + swimming: false, + bounce: false, + shadowColor: SHADOW_COLOR, + noEars: false, + no: NoDraw.None, + useAllHooves: false, + gameTime: 0, + }; +} diff --git a/src/ts/client/ponyStates.ts b/src/ts/client/ponyStates.ts new file mode 100644 index 0000000..7117dbd --- /dev/null +++ b/src/ts/client/ponyStates.ts @@ -0,0 +1,155 @@ +import { animatorState as state, animatorTransition as transition, anyState, AnimatorState } from '../common/animator'; +import { + stand, sit, sitDown, standUp, lie, lieDown, sitUp, flyBug, fly, flyUp, flyDown, flyUpBug, flyDownBug, + trot, boop, boopSit, swim, sitToTrot, lieToTrot, boopLie, trotToFly, trotToFlyBug, boopFly, boopFlyBug, + flyToTrot, flyToTrotBug, swing, swimToTrot, trotToSwim, swimToFly, flyToSwim, boopSwim, swimToFlyBug, flyToSwimBug +} from './ponyAnimations'; +import { BodyAnimation } from '../common/interfaces'; + +function n(value: string) { + return (DEVELOPMENT || SERVER) ? value : ''; +} + +export const standing = state(n('standing'), stand); +export const trotting = state(n('trotting'), trot); + +export const swimming = state(n('swimming'), swim); +export const swimmingToTrotting = state(n('swimming-to-trotting'), swimToTrot); +export const trottingToSwimming = state(n('trotting-to-swimming'), trotToSwim); +export const swimmingToFlying = state(n('swimming-to-flying'), swimToFly, { bug: swimToFlyBug }); +export const flyingToSwimming = state(n('flying-to-swimming'), flyToSwim, { bug: flyToSwimBug }); + +export const booping = state(n('booping'), boop); +export const boopingSitting = state(n('booping-sitting'), boopSit); +export const boopingLying = state(n('booping-lying'), boopLie); +export const boopingFlying = state(n('booping-flying'), boopFly, { bug: boopFlyBug }); +export const boopingSwimming = state(n('booping-swimming'), boopSwim); + +export const sitting = state(n('sitting'), sit); +export const sittingDown = state(n('sitting-down'), sitDown); +export const standingUp = state(n('standing-up'), standUp); +export const sittingToTrotting = state(n('sitting-to-trotting'), sitToTrot); + +export const lying = state(n('lying'), lie); +export const lyingDown = state(n('lying-down'), lieDown); +export const sittingUp = state(n('sitting-up'), sitUp); +export const lyingToTrotting = state(n('lying-to-trotting'), lieToTrot); + +export const hovering = state(n('hovering'), fly, { bug: flyBug }); +export const flying = state(n('flying'), fly, { bug: flyBug }); +export const flyingUp = state(n('flying-up'), flyUp, { bug: flyUpBug }); +export const flyingDown = state(n('flying-down'), flyDown, { bug: flyDownBug }); + +export const trottingToFlying = state(n('trotting-to-flying'), trotToFly, { bug: trotToFlyBug }); +export const flyingToTrotting = state(n('flying-to-trotting'), flyToTrot, { bug: flyToTrotBug }); + +export const swinging = state(n('swinging'), swing); + +export const ponyStates = [ + anyState, standing, trotting, swimming, swimmingToTrotting, trottingToSwimming, + booping, boopingSitting, boopingLying, boopingFlying, + sitting, sittingDown, standingUp, sittingToTrotting, + lying, lyingDown, sittingUp, lyingToTrotting, + hovering, flying, flyingUp, flyingDown, trottingToFlying, flyingToTrotting, + swinging, swimmingToFlying, flyingToSwimming, boopingSwimming, +]; + +transition(hovering, flyingDown, { exitAfter: 0 }); +transition(flyingDown, standing); +transition(standing, sittingDown, { exitAfter: 0 }); +transition(sittingDown, sitting); +transition(sitting, lyingDown, { exitAfter: 0 }); +transition(lyingDown, lying); + +transition(lying, sittingUp, { exitAfter: 0 }); +transition(sittingUp, sitting); +transition(sitting, standingUp, { exitAfter: 0 }); +transition(standingUp, standing); +transition(standing, flyingUp, { exitAfter: 0 }); +transition(flyingUp, hovering); +// transition(flyingUp, trottingToFlying, { exitAfter: 0, keepTime: true }); + +transition(sitting, sittingToTrotting, { exitAfter: 0, onlyDirectTo: trotting }); +transition(sittingToTrotting, trotting, { enterTime: 6.1 / 16 }); +transition(sittingToTrotting, standing); + +transition(lying, lyingToTrotting, { exitAfter: 0, onlyDirectTo: trotting }); +transition(lyingToTrotting, trotting, { enterTime: 6.1 / 16 }); +transition(lyingToTrotting, standing, { exitAfter: 5 / 6 }); + +transition(trotting, trottingToFlying, { exitAfter: 4 / 16 }); +transition(trottingToFlying, flying); + +transition(flying, flyingToTrotting, { exitAfter: 0 }); +transition(flyingToTrotting, trotting, { enterTime: 6 / 16 }); + +transition(swimming, swimmingToTrotting, { exitAfter: 0 }); +transition(swimming, swimmingToFlying, { exitAfter: 0 }); +transition(swimmingToTrotting, trotting); +transition(swimmingToTrotting, standing); +transition(trotting, trottingToSwimming, { exitAfter: 0 }); +transition(standing, trottingToSwimming, { exitAfter: 0 }); +transition(trottingToSwimming, swimming); +transition(swimmingToFlying, hovering); +transition(swimmingToFlying, flying); +transition(flying, flyingToSwimming, { exitAfter: 0 }); +transition(hovering, flyingToSwimming, { exitAfter: 0 }); +transition(flyingToSwimming, swimming); + +transition(trotting, standing, { exitAfter: 0 }); +transition(flying, hovering, { exitAfter: 0, keepTime: true }); + +transition(boopingSwimming, swimming); +transition(swimming, boopingSwimming, { exitAfter: 0 }); + +transition(booping, standing); +transition(standing, booping, { exitAfter: 0 }); +transition(boopingSitting, sitting); +transition(sitting, boopingSitting, { exitAfter: 0 }); +transition(boopingLying, lying); +transition(lying, boopingLying, { exitAfter: 0 }); +transition(boopingFlying, hovering, { enterTime: 1.1 / 10 }); +transition(hovering, boopingFlying, { exitAfter: 0 }); + +// transition(anyState, trottingToSwimming, { exitAfter: 0 }); + +transition(anyState, trotting, { exitAfter: 0, keepTime: true }); +transition(anyState, flying, { exitAfter: 0, keepTime: true }); + +transition(standing, swinging, { exitAfter: 0 }); +transition(swinging, standing); + +export function isFlyingUp(state: AnimatorState | undefined) { + return state === flyingUp || state === trottingToFlying || state === swimmingToFlying; +} + +export function isFlyingDown(state: AnimatorState | undefined) { + return state === flyingDown || state === flyingToTrotting || state === flyingToSwimming; +} + +export function isSwimmingState(state: AnimatorState | undefined) { + return state === swimming || state === trottingToSwimming || state === swimmingToTrotting || + state === flyingToSwimming || state === swimmingToFlying || state === boopingSwimming; +} + +export function isFlyingUpOrDown(state: AnimatorState | undefined) { + return isFlyingUp(state) || isFlyingDown(state); +} + +export function isSittingDown(state: AnimatorState | undefined) { + return state === sittingDown; +} +export function isSittingUp(state: AnimatorState | undefined) { + return state === sittingUp; +} + +export function toBoopState(state: AnimatorState) { + switch (state) { + case standing: return booping; + case sitting: return boopingSitting; + case lying: return boopingLying; + case hovering: return boopingFlying; + case swimming: return boopingSwimming; + default: return undefined; + } +} diff --git a/src/ts/client/ponyUtils.ts b/src/ts/client/ponyUtils.ts new file mode 100644 index 0000000..2a0f7b4 --- /dev/null +++ b/src/ts/client/ponyUtils.ts @@ -0,0 +1,176 @@ +/// + +import { range, dropRight, compact, max, zip } from 'lodash'; +import { + Eye, Iris, Muzzle, ExpressionExtra, Sprite, ColorExtraSets, PonyInfoBase, SpriteSetBase, ColorExtra, ColorExtraSet +} from '../common/interfaces'; +import * as sprites from '../generated/sprites'; +import { HEAD_ACCESSORY_OFFSETS, EXTRA_ACCESSORY_OFFSETS, EAR_ACCESSORY_OFFSETS } from '../common/offsets'; + +export const PONY_WIDTH = 80; +export const PONY_HEIGHT = 70; +export const BLINK_FRAMES = [2, 6, 6, 4, 2]; +export const SLEEVED_ACCESSORIES = [2, 3, 4]; +export const SLEEVED_BACK_ACCESSORIES = [5, 6]; +export const CHEST_ACCESSORIES_IN_FRONT = [1]; +export const NO_MANE_HEAD_ACCESSORIES = [0, 16]; +// export const FRONT_NECK_ACCESSORIES = [2, 10]; + +export type Sprites = (Sprite | undefined)[]; +export type Sets = ColorExtraSets[]; // [frame][type][pattern] + +export const headCenter = [ + undefined, + [[0].map(i => sprites.head[2]![0]![i])], +]; + +export const claws: Sets = sprites.frontLegHooves + .map(f => f && [undefined, undefined, undefined, f[4], undefined, undefined]); +export const frontHooves: Sets = sprites.frontLegHooves + .map(f => f && [...f.slice(0, 4), ...f.slice(5)]); + +export const frontHoovesInFront = [false, false, true, true, false, false]; +export const backHoovesInFront = [false, false, true, false, false]; + +const bodyFrames = sprites.body.length; + +export const wings = createCompleteSets(sprites.wings, 3 + 10); +export const tails = createCompleteSets(sprites.tails, 3); +export const chest = createCompleteSets(sprites.chestAccessories, bodyFrames); +export const chestBehind = createCompleteSets(sprites.chestAccessoriesBehind, bodyFrames); +export const backAccessories = createCompleteSets(sprites.backAccessories, bodyFrames); +sprites.neckAccessories.forEach(f => f && f.pop()); // TEMP: remove headphones +export const neckAccessories = createCompleteSets(sprites.neckAccessories, bodyFrames); +export const waistAccessories = createCompleteSets(sprites.waistAccessories, bodyFrames + 1); + +function frameType(sets: Sets, frame: number, type: number) { + const set = sets[frame]; + return set && set[type]; +} + +function createCompleteSets(sets: Sets, frameCount: number): Sets { + const typeCount = sets.reduce((max, s) => Math.max(max, s ? s.length : 0), 0); + const typeRange = range(0, typeCount); + const result: Sets = []; + + for (let frame = 0; frame < frameCount; frame++) { + result.push(typeRange.map(type => frameType(sets, frame, type) || frameType(result, frame - 1, type))); + } + + return result; +} + +export function canFly(info: PonyInfoBase) { + const type = info.wings && info.wings.type || 0; + return type > 0; +} + +export function canMagic(info: PonyInfoBase) { + const type = info.horn && info.horn.type || 0; + return type === 1 || type === 2 || type === 3 || type === 14; +} + +export function flipIris(iris: Iris): Iris { + if (iris === Iris.Left || iris === Iris.UpLeft) { + return iris + 1; + } else if (iris === Iris.Right || iris === Iris.UpRight) { + return iris - 1; + } else { + return iris; + } +} + +export function flipFaceAccessoryType(type: number) { + if (type === 6) return 7; + if (type === 7) return 6; + + if (type === 9) return 10; + if (type === 10) return 9; + + return type; +} + +export function flipFaceAccessoryPattern(type: number, pattern: number) { + if (type === 2) { // dark glasses + if (pattern === 1) return 2; + if (pattern === 2) return 1; + } else if (type === 11) { // large dark glasses + if (pattern === 1) return 2; + if (pattern === 2) return 1; + } + + return pattern; +} + +export const defaultExpression = { + left: Eye.Neutral, + leftIris: Iris.Forward, + right: Eye.Neutral, + rightIris: Iris.Forward, + muzzle: Muzzle.Neutral, + extra: ExpressionExtra.None, +}; + +export const blinkFrames: Eye[][] = []; + +function setupBlinkFrames(frames: Eye[]) { + dropRight(frames, 1).forEach((f, i) => blinkFrames[f] = blinkFrames[f] || frames.slice(i + 1)); +} + +setupBlinkFrames([Eye.Neutral, Eye.Neutral2, Eye.Neutral3, Eye.Neutral4, Eye.Neutral5, Eye.Closed]); +setupBlinkFrames([Eye.Frown, Eye.Frown2, Eye.Frown3, Eye.Frown4, Eye.Closed]); +setupBlinkFrames([Eye.Sad, Eye.Sad2, Eye.Sad3, Eye.Sad4, Eye.Neutral5, Eye.Closed]); +setupBlinkFrames([Eye.Angry, Eye.Angry2, Eye.Neutral4, Eye.Neutral5, Eye.Closed]); + +// sets + +function mergeColorExtras(sprites: (ColorExtra | undefined)[]): ColorExtra | undefined { + const filtered = compact(sprites); + + return { + ...filtered[0], + colors: max(filtered.map(x => x.colors || 0)), + colorMany: filtered.length > 1 ? filtered.map(x => x.color) : undefined, + }; +} + +function mergeSprites(sets: ColorExtraSet[]): ColorExtraSet { + return zip(...sets).map(mergeColorExtras); +} + +function mergeSpriteSets(...sets: ColorExtraSets[]): ColorExtraSets { + return zip(...sets).map(mergeSprites); +} + +export const backLegSleeves: Sets = sprites.backLegSleeves + .map(sets => sets && [undefined, undefined, undefined, undefined, undefined, ...sets]); + +// TEMP: remove summer hat +sprites.headAccessoriesBehind.pop(); +sprites.headAccessories.pop(); + +export const mergedManes = mergeSpriteSets(sprites.behindManes, sprites.topManes, sprites.frontManes)!; +export const mergedBackManes = mergeSpriteSets(sprites.backBehindManes, sprites.backFrontManes)!; +export const mergedFacialHair = mergeSpriteSets(sprites.facialHairBehind, sprites.facialHair)!; +export const mergedEarAccessories = mergeSpriteSets(sprites.earAccessoriesBehind, sprites.earAccessories)!; +export const mergedHeadAccessories = mergeSpriteSets(sprites.headAccessoriesBehind, sprites.headAccessories)!; +export const mergedFaceAccessories = mergeSpriteSets(sprites.faceAccessories, sprites.faceAccessories2)!; +export const mergedChestAccessories = mergeSpriteSets(sprites.chestAccessoriesBehind[1], sprites.chestAccessories[1])!; +export const mergedBackAccessories = mergeSpriteSets( + backAccessories[1], [undefined, undefined, undefined, undefined, undefined, ...sprites.backLegSleeves[1]!])!; +export const mergedExtraAccessories = mergeSpriteSets(sprites.extraAccessoriesBehind, sprites.extraAccessories)! + .slice(0, DEVELOPMENT ? 100 : 2); + +if (DEVELOPMENT) { + assertSizes('HEAD_ACCESSORY_OFFSETS', HEAD_ACCESSORY_OFFSETS, mergedManes); + assertSizes('EXTRA_ACCESSORY_OFFSETS', EXTRA_ACCESSORY_OFFSETS, mergedManes); + assertSizes('EAR_ACCESSORY_OFFSETS', EAR_ACCESSORY_OFFSETS, sprites.ears); + assertSizes('frontHoovesInFront', frontHoovesInFront, frontHooves[1]!); + assertSizes('backHoovesInFront', backHoovesInFront, sprites.backLegHooves[1]!); +} + +function assertSizes(name: string, a: any[], b: any[]) { + if (a.length !== b.length) { + throw new Error(`Invalid ${name} length (${a.length} !== ${b.length})`); + } +} diff --git a/src/ts/client/rev.ts b/src/ts/client/rev.ts new file mode 100644 index 0000000..2ecee8f --- /dev/null +++ b/src/ts/client/rev.ts @@ -0,0 +1,12 @@ +import { REV } from '../generated/rev'; + +/* istanbul ignore next */ +export function getUrl(name: string): string { + if (DEVELOPMENT) + return `/assets/${name}`; + + if (!REV[name]) + throw new Error(`Cannot find file url (${name})`); + + return `/assets/${name.replace(/(\.\S+)$/, `-${REV[name]}$1`)}`; +} diff --git a/src/ts/client/sec.ts b/src/ts/client/sec.ts new file mode 100644 index 0000000..22adcc6 --- /dev/null +++ b/src/ts/client/sec.ts @@ -0,0 +1,44 @@ +import { PonyTownGame } from './game'; +import { Pony, EntityFlags } from '../common/interfaces'; +import { setFlag, fromNow } from '../common/utils'; +import { fixCollision, isStaticCollision } from '../common/collision'; +import { WEEK } from '../common/constants'; + +let currentPlayer: Pony | undefined; +let setX = 0; +let setY = 0; + +export function setupPlayer(game: PonyTownGame, player: Pony) { + const pony = player; + pony.flags = setFlag(pony.flags, EntityFlags.Interactive, false); + + if (isStaticCollision(player, game.map, false)) { + fixCollision(player, game.map); + } + + game.setPlayer(pony); + currentPlayer = player; + savePlayerPosition(); +} + +export function savePlayerPosition() { + if (currentPlayer) { + setX = currentPlayer.x; + setY = currentPlayer.y; + } +} + +export function restorePlayerPosition() { + if (currentPlayer) { + if (currentPlayer.x !== setX || currentPlayer.y !== setY) { + currentPlayer.x = setX; + currentPlayer.y = setY; + DEVELOPMENT && console.warn('Restoring player position'); + } + } +} + +// Account creation lock +export const setAclCookie = (acl: string) => { + document.cookie = `acl=${acl}; expires=${fromNow(WEEK).toUTCString()}; path=/`; +}; diff --git a/src/ts/client/spriteAnimations.ts b/src/ts/client/spriteAnimations.ts new file mode 100644 index 0000000..cf28cd6 --- /dev/null +++ b/src/ts/client/spriteAnimations.ts @@ -0,0 +1,31 @@ +import * as sprites from '../generated/sprites'; +import { SpriteAnimation } from '../common/animationPlayer'; +import { AnimatedRenderable } from '../common/mixins'; +import { Sprite } from '../common/interfaces'; + +export const zzzAnimation1 = createSpriteAnimation( + sprites.emote_sleep1, 8, 8, 4, 7, true, sprites.emote_sleep1_flip.frames); +export const zzzAnimation2 = createSpriteAnimation( + sprites.emote_sleep2, 12, 13, 13, 12, true, sprites.emote_sleep2_flip.frames); +export const zzzAnimations = [zzzAnimation1, zzzAnimation2]; + +export const cryAnimation = createSpriteAnimation(sprites.emote_cry2, 12, 0, 13, 0); + +export const tearsAnimation = createSpriteAnimation(sprites.emote_tears, 12, 0, 1, 0); + +export const heartsAnimation = createSpriteAnimation(sprites.emote_hearts, 12, 18, 18, 9, true); + +sprites.emote_sneeze.frames.unshift(sprites.emptySprite, sprites.emptySprite); +export const sneezeAnimation = createSpriteAnimation(sprites.emote_sneeze, 8, 0, 4, 0, false); + +sprites.hold_poof.frames.push(sprites.emptySprite); +export const holdPoofAnimation = createSpriteAnimation(sprites.hold_poof, 12, 0, 4, 0, false); + +export const magicAnimation = createSpriteAnimation(sprites.magic2, 8, 2, 6, 0, true); + +function createSpriteAnimation( + { frames, palette }: AnimatedRenderable, fps: number, start: number, middle: number, end: number, loop = true, + flipFrames?: Sprite[], +): SpriteAnimation { + return { start, middle, end, fps, palette, frames, loop, flipFrames }; +} diff --git a/src/ts/client/spriteUtils.ts b/src/ts/client/spriteUtils.ts new file mode 100644 index 0000000..57c1d9c --- /dev/null +++ b/src/ts/client/spriteUtils.ts @@ -0,0 +1,81 @@ +import { once, noop } from 'lodash'; +import { ColorExtra, ColorExtraSets, PonyEye, SpriteSheet, Sprite } from '../common/interfaces'; +import { spriteSheets } from '../generated/sprites'; +import { loadImage, createCanvas } from '../client/canvasUtils'; +import { getUrl } from './rev'; +import { createFonts } from './fonts'; + +export function createSprite(x: number, y: number, w: number, h: number, ox: number, oy: number, type: number): Sprite { + return { x, y, w, h, ox, oy, type }; +} + +export function addTitles(sprites: ColorExtraSets, titles: string[]): ColorExtraSets { + return sprites && sprites.map((ns, i) => + ns && ns.map(s => s && { color: s.color, colors: s.colors, title: titles[i], label: titles[i] })); +} + +export function addLabels(sprites: ColorExtraSets, labels: string[]) { + sprites && sprites.forEach((s, i) => s && s[0] ? s[0]!.label = labels[i] : undefined); + return sprites; +} + +export function createEyeSprite(eye: PonyEye | undefined, iris: number, defaultPalette: Uint32Array): ColorExtra | undefined { + return eye && { color: eye.irises[iris]!, colors: 2, extra: eye.base, palettes: [defaultPalette] }; +} + +export function getColorCount(sprite: ColorExtra | undefined): number { + return sprite && sprite.colors ? Math.floor((sprite.colors - 1) / 2) : 0; +} + +export function createSpriteUtils() { + createFonts(); +} + +type LoadImage = (src: string) => Promise; + +function getImageData(img: HTMLImageElement | ImageBitmap) { + const canvas = createCanvas(img.width, img.height); + const context = canvas.getContext('2d')!; + context.drawImage(img, 0, 0); + return context.getImageData(0, 0, img.width, img.height); +} + +function loadSpriteSheet(sheet: SpriteSheet, loadImage: LoadImage) { + return Promise.all([ + loadImage(sheet.src!), + sheet.srcA ? loadImage(sheet.srcA) : Promise.resolve(undefined) + ]) + .then(([img, imgA]) => { + sheet.data = getImageData(img); + + if (imgA) { + const alpha = getImageData(imgA); + const alphaData = alpha.data; + const sheedData = sheet.data.data; + + for (let i = 0; i < sheedData.length; i += 4) { + sheedData[i + 3] = alphaData[i]; + } + } + }); +} + +export function loadSpriteSheets(sheets: SpriteSheet[], loadImage: LoadImage) { + return Promise.all(sheets.map(s => loadSpriteSheet(s, loadImage))).then(noop); +} + +export let spriteSheetsLoaded = false; + +export function loadAndInitSheets(sheets: SpriteSheet[], loadImage: LoadImage) { + return loadSpriteSheets(sheets, loadImage) + .then(createSpriteUtils) + .then(() => true) + .catch(e => (console.error(e), false)) + .then(loaded => spriteSheetsLoaded = loaded); +} + +export function loadImageFromUrl(url: string) { + return loadImage(getUrl(url)); +} + +export const loadAndInitSpriteSheets = once(() => loadAndInitSheets(spriteSheets, loadImageFromUrl)); diff --git a/src/ts/client/tileUtils.ts b/src/ts/client/tileUtils.ts new file mode 100644 index 0000000..16d483f --- /dev/null +++ b/src/ts/client/tileUtils.ts @@ -0,0 +1,602 @@ +import { + PaletteManager, Season, TileSets, Region, TileType, Camera, PaletteSpriteBatch, DrawOptions, WorldMap, IMap, + Sprite, MapType +} from '../common/interfaces'; +import * as sprites from '../generated/sprites'; +import { getRegionTile, getRegionElevation } from '../common/region'; +import { getRegionGlobal } from '../common/worldMap'; +import { tileWidth, tileHeight, tileElevation, WATER_FPS, REGION_SIZE, WATER_HEIGHT } from '../common/constants'; +import { clamp, toInt, at, invalidEnumReturn } from '../common/utils'; +import { isAreaVisible } from '../common/camera'; +import { WHITE } from '../common/colors'; +import { releasePalette } from '../graphics/paletteManager'; +import { drawPixelText } from '../graphics/graphicsUtils'; +import { toScreenX, toScreenY, toWorldZ } from '../common/positionUtils'; + +const TILE_COUNTS = [[0, 4], [2, 3], [4, 3], [6, 3], [8, 3], [13, 3], [14, 3], [47, 4]]; +export const TILE_COUNT_MAP: number[] = []; +export const TILE_MAP_MAP: number[] = []; + +TILE_COUNTS.forEach(([tile, count]) => { + while (TILE_COUNT_MAP.length < (tile + 1)) { + TILE_COUNT_MAP.push(1); + } + + TILE_COUNT_MAP[tile] = count; +}); + +let tileIndex = 0; + +for (let i = 0; i <= 47; i++) { + TILE_MAP_MAP.push(tileIndex); + tileIndex += TILE_COUNT_MAP[i]; +} + +// 1 | 2 | 4 +// ----+----+---- +// 8 | | 16 +// ----+----+---- +// 32 | 64 | 128 +export const TILE_MAP = [ + 46, 46, 22, 22, 46, 46, 22, 22, 21, 21, // 0-9 + 17, 11, 21, 21, 17, 11, 19, 19, 18, 18, // 10-19 + 19, 19, 12, 12, 14, 14, 24, 28, 14, 14, // 20-29 + 30, 6, 46, 46, 22, 22, 46, 46, 22, 22, // 30-39 + 21, 21, 17, 11, 21, 21, 17, 11, 19, 19, // 40-49 + 18, 18, 19, 19, 12, 12, 14, 14, 24, 28, // 50-59 + 14, 14, 30, 6, 20, 20, 13, 13, 20, 20, // 60-69 + 13, 13, 16, 16, 23, 32, 16, 16, 23, 32, // 70-79 + 15, 15, 25, 25, 15, 15, 34, 34, 26, 26, // 80-89 + 45, 41, 26, 26, 42, 36, 20, 20, 13, 13, // 90-99 + 20, 20, 13, 13, 10, 10, 31, 4, 10, 10, // 100-109 + 31, 4, 15, 15, 25, 25, 15, 15, 34, 34, // 110-119 + 27, 27, 43, 37, 27, 27, 35, 5, 46, 46, // 120-129 + 22, 22, 46, 46, 22, 22, 21, 21, 17, 11, // 130-139 + 21, 21, 17, 11, 19, 19, 18, 18, 19, 19, // 140-149 + 12, 12, 14, 14, 24, 28, 14, 14, 30, 6, // 150-159 + 46, 46, 22, 22, 46, 46, 22, 22, 21, 21, // 160-169 + 17, 11, 21, 21, 17, 11, 19, 19, 18, 18, // 170-179 + 19, 19, 12, 12, 14, 14, 24, 28, 14, 14, // 180-189 + 30, 6, 20, 20, 13, 13, 20, 20, 13, 13, // 190-199 + 16, 16, 23, 32, 16, 16, 23, 32, 9, 9, // 200-209 + 33, 33, 9, 9, 8, 8, 29, 29, 44, 39, // 210-219 + 29, 29, 38, 7, 20, 20, 13, 13, 20, 20, // 220-229 + 13, 13, 10, 10, 31, 4, 10, 10, 31, 4, // 230-239 + 9, 9, 33, 33, 9, 9, 8, 8, 2, 2, // 240-249 + 40, 3, 2, 2, 1, 0 // 250-255 +]; + +const enum TileTypeNumber { + None = 0, + Grass = 1, + Water = 2, + Wood = 3, + GrassNew = 4, + Water2 = 5, + Water3 = 6, + Water4 = 7, + Ice = 8, + SnowOnIce = 9, + Stone = 10, + Stone2 = 11, + Boat = 12, +} + +const waterFrames: number[] = [ + TileTypeNumber.Water, TileTypeNumber.Water2, TileTypeNumber.Water3, TileTypeNumber.Water4 +]; + +export function updateTileSets( + paletteManager: PaletteManager, tileSets: TileSets | undefined, season: Season, mapType: MapType +) { + if (tileSets) { + tileSets.forEach(t => releasePalette(t.palette)); + } + + return createTileSets(paletteManager, season, mapType); +} + +export function createTileSets(paletteManager: PaletteManager, season: Season, mapType: MapType): TileSets { + const isWinter = season === Season.Winter; + const isAutumn = season === Season.Autumn; + const isCave = mapType === MapType.Cave; + + const grassTiles = isCave ? sprites.caveTiles : (isWinter ? sprites.snowTiles : sprites.grassTiles); + const grassPalette = grassTiles.palettes[isCave ? 0 : (isAutumn ? 1 : 0)]; + const icePaletteIndex = isWinter ? 2 : (isAutumn ? 1 : 0); + const waterPaletteIndex = isCave ? 3 : (isWinter ? 2 : (isAutumn ? 1 : 0)); + const waterPalette = sprites.waterTiles1.palettes[waterPaletteIndex]; + + // indexes equal to TileTypeNumber values + return [ + { // 0 + sprites: [sprites.tile_none.color], + palette: paletteManager.addArray(sprites.tile_none.palettes![0]), + }, + { // 1 + sprites: grassTiles.sprites, + palette: paletteManager.addArray(grassPalette), + }, + { // 2 + sprites: sprites.waterTiles1.sprites, + palette: paletteManager.addArray(waterPalette), + }, + { // 3 + sprites: sprites.woodTiles.sprites, + palette: paletteManager.addArray(sprites.woodTiles.palettes[0]), + }, + { // 4 + sprites: sprites.grassTilesNew.sprites, + palette: paletteManager.addArray(sprites.grassTilesNew.palettes[0]), + }, + // water frames + { // 5 + sprites: sprites.waterTiles2.sprites, + palette: paletteManager.addArray(waterPalette), + }, + { // 6 + sprites: sprites.waterTiles3.sprites, + palette: paletteManager.addArray(waterPalette), + }, + { // 7 + sprites: sprites.waterTiles4.sprites, + palette: paletteManager.addArray(waterPalette), + }, + // ice + { // 8 + sprites: sprites.iceTiles.sprites, + palette: paletteManager.addArray(sprites.iceTiles.palettes[icePaletteIndex]), + }, + // snow on ice + { // 9 + sprites: sprites.snowOnIceTiles.sprites, + palette: paletteManager.addArray(sprites.snowOnIceTiles.palettes[0]), + }, + // stone + { // 10 + sprites: sprites.stoneTiles.sprites, + palette: paletteManager.addArray(sprites.stoneTiles.palettes[0]), + }, + // stone 2 + { // 11 + sprites: sprites.stone2Tiles.sprites, + palette: paletteManager.addArray(sprites.stone2Tiles.palettes[0]), + }, + ]; +} + +export function drawTiles( + batch: PaletteSpriteBatch, region: Region, camera: Camera, map: WorldMap, tileSets: TileSets, options: DrawOptions +) { + const { tileIndices } = region; + const { tileTime } = map; + const regionX = region.x * REGION_SIZE; + const regionY = region.y * REGION_SIZE; + + if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) { + const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE); + const minY = clamp(Math.floor(camera.actualY / tileHeight - regionY), 0, REGION_SIZE); + const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE); + const maxY = clamp(Math.ceil((camera.actualY + camera.h) / tileHeight - regionY), 0, REGION_SIZE); + + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const tileIndex = tileIndices[x | (y << 3)]; + + if (tileIndex === -1) { + options.error(`Uninitialized tile index at (${x}, ${y}) ` + + `region: (${region.x}, ${region.y}, ${region.tilesDirty}, ${region.lastTileUpdate}) ` + + `now: ${performance.now()}`); + region.tilesDirty = true; + continue; + } + + const tileTypeNumber = tileIndex >>> 8; + const isWater = tileTypeNumber === TileTypeNumber.Water || tileTypeNumber === TileTypeNumber.Boat; + const tileSpriteIndex = tileIndex & 0xff; + const tileSetIndex = isWater ? at(waterFrames, toInt(tileTime) % waterFrames.length)! : tileTypeNumber; + const tileSet = tileSets[tileSetIndex]; + + if (!tileSet) { + options.error(`Missing tileset: position: (${x}, ${y}) tile: (${getRegionTile(region, x, y)}) ` + + `info: (${tileIndex}, ${tileSetIndex}, ${tileTime}, ${tileSpriteIndex}, ${JSON.stringify(waterFrames)})`); + tileIndices[x | (y << 3)] = -1; + region.tilesDirty = true; + continue; + } + + const rx = (x + regionX) * tileWidth; + const ry = (y + regionY) * tileHeight; + + if (DEVELOPMENT && !tileSet.sprites[tileSpriteIndex]) { + console.error('Missing sprite', tileSetIndex, tileSpriteIndex); + } + + batch.drawSprite(tileSet.sprites[tileSpriteIndex], WHITE, tileSet.palette, rx, ry); + } + } + } +} + +export function drawTilesDebugInfo(batch: PaletteSpriteBatch, region: Region, camera: Camera, options: DrawOptions) { + const { tileIndices } = region; + const regionX = region.x * REGION_SIZE; + const regionY = region.y * REGION_SIZE; + + if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) { + const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE); + const minY = clamp(Math.floor(camera.actualY / tileHeight - regionY), 0, REGION_SIZE); + const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE); + const maxY = clamp(Math.ceil((camera.actualY + camera.h) / tileHeight - regionY), 0, REGION_SIZE); + + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const tileIndex = tileIndices[x | (y << 3)]; + + if (tileIndex === -1) { + continue; + } + + const tileTypeNumber = tileIndex >>> 8; + const tileSpriteIndex = tileIndex & 0xff; + const rx = (x + regionX) * tileWidth; + const ry = (y + regionY) * tileHeight; + + if (options.tileIndices) { + drawPixelText(batch, rx + 2, ry + 2, 0x000000ff, `${tileTypeNumber}:${tileSpriteIndex}`); + drawPixelText(batch, rx + 2, ry + 2 + 7, 0x555555ff, `${region.tiles[x + REGION_SIZE * y]}`); + } + + if (options.tileGrid) { + batch.drawRect(y !== 0 ? 0x00000011 : 0x00000022, rx, ry, tileWidth, 1); + batch.drawRect(x !== 0 ? 0x00000011 : 0x00000022, rx, ry + 1, 1, tileHeight - 1); + } + } + } + } +} + +export function drawTilesNew( + batch: PaletteSpriteBatch, region: Region, camera: Camera, map: WorldMap, tileSets: TileSets, options: DrawOptions +) { + const regionX = region.x * REGION_SIZE; + const regionY = region.y * REGION_SIZE; + const TILE_COLOR = 0x666666ff; + const TILE_FRONT_COLOR = 0x5e5e5eff; + const OUTLINE_2_COLOR = 0xffffff22; + const OUTLINE_COLOR = 0x00000022; + + if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) { + const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE); + const minY = clamp(Math.floor(camera.y / tileHeight - regionY), 0, REGION_SIZE); + const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE); + const maxY = clamp(Math.ceil((camera.y + camera.h) / tileHeight - regionY), 0, REGION_SIZE); + + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const elevation = getRegionElevation(region, x, y); + const cliffTop = getRegionElevation(region, x, y - 1) < elevation; + const cliffLeft = getRegionElevation(region, x - 1, y) < elevation; + const cliffRight = getRegionElevation(region, x + 1, y) < elevation; + const cliffBottom = getRegionElevation(region, x, y + 1) < elevation; + const elevDiff = Math.max(0, elevation - getRegionElevation(region, x, y + 1)); + const tx = (x + regionX) * tileWidth; + const ty = (y + regionY) * tileHeight - elevation * tileElevation; + + batch.drawRect(TILE_COLOR, tx, ty, tileWidth, tileHeight); + + if (elevation) { + batch.drawRect(TILE_FRONT_COLOR, tx, ty + tileHeight, tileWidth, elevDiff * tileElevation); + + if (cliffLeft) { + batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight, 1, elevation * tileElevation); + } + + if (cliffRight) { + batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty + tileHeight, 1, elevation * tileElevation); + } + + if (cliffBottom) { + batch.drawRect(OUTLINE_2_COLOR, tx, ty + tileHeight - 1, tileWidth, 1); + batch.drawRect(OUTLINE_2_COLOR, tx, (ty + tileHeight + elevDiff * tileElevation) - 1, tileWidth, 1); + } + } + + if (cliffTop) { + batch.drawRect(OUTLINE_2_COLOR, tx, ty, tileWidth, 1); + } + + if (cliffLeft) { + batch.drawRect(OUTLINE_2_COLOR, tx, ty, 1, tileHeight); + } + + if (cliffRight) { + batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty, 1, tileHeight); + } + + if (cliffBottom) { + batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight - 1, tileWidth, 1); + } + + if (options.gridLines) { + batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty, 1, tileHeight); + batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight - 1, tileWidth - 1, 1); + // drawPixelText(batch, tx + 1, ty + 1, OUTLINE_COLOR, elevation.toString(10)); + } + + const rx = x + regionX; + const ry = y + regionY; + + const tileIndex = region.tileIndices[x | (y << 3)]; + // const tileTypeNumber = tileIndex >> 8; + const tileOffset = tileIndex & 0xff; + const grass = tileSets[3]; + const baseX = (region.x * REGION_SIZE) | 0; + const baseY = (region.y * REGION_SIZE) | 0; + + if (getTileNormal(region.tiles, baseX, baseY, x, y, map, TileType.None) === TileType.Grass) { + batch.drawSprite(grass.sprites[tileOffset], WHITE, grass.palette, rx * tileWidth, ry * tileHeight); + } + } + } + } +} + +export function updateTileIndices(region: Region, map: IMap) { + for (let y = 0, i = 0; y < REGION_SIZE; y++) { + for (let x = 0; x < REGION_SIZE; x++ , i++) { + if (region.tileIndices[i] === -1) { + region.tileIndices[i] = getTileIndex(region, i, x, y, map); + } + } + } + + region.tilesDirty = false; + region.lastTileUpdate = performance.now(); +} + +function tileTypeNumber(type: TileType) { + switch (type) { + case TileType.Water: + case TileType.WalkableWater: + return TileTypeNumber.Water; + case TileType.Wood: + return TileTypeNumber.Wood; + case TileType.Ice: + case TileType.WalkableIce: + return TileTypeNumber.Ice; + case TileType.SnowOnIce: + return TileTypeNumber.SnowOnIce; + case TileType.Stone: + return TileTypeNumber.Stone; + case TileType.Stone2: + return TileTypeNumber.Stone2; + case TileType.Boat: + return TileTypeNumber.Boat; + case TileType.Grass: + case TileType.Dirt: + case TileType.ElevatedDirt: + return TileTypeNumber.Grass; + case TileType.None: + case TileType.WallH: + case TileType.WallV: + return TileTypeNumber.None; + default: + return invalidEnumReturn(type, TileTypeNumber.None); + } +} + +function normalizeTile(type: TileType, base: TileType) { + switch (type) { + case TileType.SnowOnIce: + return base === TileType.SnowOnIce ? type : TileType.Ice; + case TileType.WalkableIce: + return TileType.Ice; + case TileType.WalkableWater: + case TileType.Boat: + return TileType.Water; + case TileType.ElevatedDirt: + return TileType.Dirt; + default: + return type; + } +} + +function normalizeTileBase(type: TileType) { + switch (type) { + case TileType.WalkableIce: + return TileType.Ice; + case TileType.WalkableWater: + case TileType.Boat: + return TileType.Water; + case TileType.ElevatedDirt: + return TileType.Dirt; + default: + return type; + } +} + +function getTileNormal( + tiles: Uint8Array, baseX: number, baseY: number, x: number, y: number, map: IMap, base: TileType +) { + if (x >= 0 && y >= 0 && x < REGION_SIZE && y < REGION_SIZE) { + return normalizeTile(tiles[x | (y << 3)], base); + } else { + const mapX = clamp(x + baseX, 0, map.width - 1); + const mapY = clamp(y + baseY, 0, map.height - 1); + const region = getRegionGlobal(map, mapX, mapY); + + if (region !== undefined) { + const regionX = mapX - region.x * REGION_SIZE; + const regionY = mapY - region.y * REGION_SIZE; + return normalizeTile(region.tiles[regionX | (regionY << 3)], base); + } else { + return TileType.None; + } + } +} + +function getTileIndex(region: Region, index: number, x: number, y: number, map: IMap): number { + const tiles = region.tiles; + const type = tiles[x | (y << 3)] as TileType; + const tileType = tileTypeNumber(type); + let baseTileIndex = 0; + + if (type === TileType.Dirt || type === TileType.ElevatedDirt) { + baseTileIndex = 47; + } else if (type !== TileType.None) { + let topLeft = 0, top = 0, topRight = 0, left = 0, right = 0, bottomLeft = 0, bottom = 0, bottomRight = 0; + + if (x > 1 && y > 1 && x < (REGION_SIZE - 1) && y < (REGION_SIZE - 1)) { + topLeft = normalizeTile(tiles[(x - 1) | (y - 1) << 3], type); + top = normalizeTile(tiles[(x) | (y - 1) << 3], type); + topRight = normalizeTile(tiles[(x + 1) | (y - 1) << 3], type); + left = normalizeTile(tiles[(x - 1) | (y) << 3], type); + right = normalizeTile(tiles[(x + 1) | (y) << 3], type); + bottomLeft = normalizeTile(tiles[(x - 1) | (y + 1) << 3], type); + bottom = normalizeTile(tiles[(x) | (y + 1) << 3], type); + bottomRight = normalizeTile(tiles[(x + 1) | (y + 1) << 3], type); + } else { + const baseX = (region.x * REGION_SIZE) | 0; + const baseY = (region.y * REGION_SIZE) | 0; + topLeft = getTileNormal(tiles, baseX, baseY, x - 1, y - 1, map, type); + top = getTileNormal(tiles, baseX, baseY, x, y - 1, map, type); + topRight = getTileNormal(tiles, baseX, baseY, x + 1, y - 1, map, type); + left = getTileNormal(tiles, baseX, baseY, x - 1, y, map, type); + right = getTileNormal(tiles, baseX, baseY, x + 1, y, map, type); + bottomLeft = getTileNormal(tiles, baseX, baseY, x - 1, y + 1, map, type); + bottom = getTileNormal(tiles, baseX, baseY, x, y + 1, map, type); + bottomRight = getTileNormal(tiles, baseX, baseY, x + 1, y + 1, map, type); + } + + const normalized = normalizeTileBase(type); + const index = 0 + | ((topLeft === normalized) ? 1 : 0) + | ((top === normalized) ? 2 : 0) + | ((topRight === normalized) ? 4 : 0) + | ((left === normalized) ? 8 : 0) + | ((right === normalized) ? 16 : 0) + | ((bottomLeft === normalized) ? 32 : 0) + | ((bottom === normalized) ? 64 : 0) + | ((bottomRight === normalized) ? 128 : 0); + + baseTileIndex = TILE_MAP[index]; + } + + const tileCount = type !== TileType.None ? TILE_COUNT_MAP[baseTileIndex] : 1; + const tileIndex = TILE_MAP_MAP[baseTileIndex] + (tileCount > 1 ? region.randoms[index] % tileCount : 0); + return (tileType << 8) | tileIndex; +} + +const tileIndices = [ + 47, 47, 0, 0, 13, 19, 21, 20, 15, 16, + 47, 47, 0, 0, 13, 13, 45, 22, 18, 17, + 9, 2, 2, 2, 10, 14, 14, 14, 35, 36, + 8, 5, null, 7, 4, 27, 26, 29, 37, 38, + 8, null, 46, null, 4, 28, 24, 30, 39, 40, + 8, 3, null, 1, 4, 23, 31, 32, 41, 42, + 12, 6, 6, 6, 11, 25, 33, 34, 43, 44, +]; + +let tileHeightMaps = new Map(); +let tileHeightMapsInitialized = false; + +function valueToHeight(value: number, bottom: number, top: number) { + return bottom + ((value / 255) * (top - bottom)); +} + +export function initializeTileHeightmaps() { + if (tileHeightMapsInitialized) + return; + + function createTileHeightMaps(sprite: Sprite, tileType: TileTypeNumber, bottom: number, top: number) { + const sheetData = sprites.normalSpriteSheet.data!; + const tiles: number[][] = []; + + for (let ty = 0; ty < 7; ty++) { + for (let tx = 0; tx < 10; tx++) { + const tile: number[] = []; + const baseX = tx * tileWidth + sprite.x; + const baseY = ty * tileHeight + sprite.y; + + for (let y = 0, i = 0; y < tileHeight; y++) { + for (let x = 0; x < tileWidth; x++ , i++) { + const sx = baseX + x; + const sy = baseY + y; + const value = sheetData.data[(sx + sy * sheetData.width) * 4]; + tile.push(valueToHeight(value, bottom, top)); + } + } + + tiles.push(tile); + } + } + + tileHeightMapsInitialized = true; + + const counts = new Uint8Array(100); + + for (let i = 0; i < tileIndices.length; i++) { + const index = tileIndices[i]; + + if (index !== null) { + const key = (tileType << 8) | (TILE_MAP_MAP[index] + counts[index]); + tileHeightMaps.set(key, tiles[i]); + counts[index]++; + } + } + } + + createTileHeightMaps(sprites.dirt_water_heightmap, TileTypeNumber.Water, -0.25, 0); + createTileHeightMaps(sprites.dirt_ice_heightmap, TileTypeNumber.Ice, -0.2, 0); + createTileHeightMaps(sprites.dirt_stone_cave_height_map, TileTypeNumber.Grass, 0.2, 0); +} + +const waterHeight = WATER_HEIGHT.map(toWorldZ); + +export function isInWater(tileIndex: number, x: number, y: number) { + const tileType = (tileIndex & 0xff00) >> 8; + + if (tileType === TileTypeNumber.Water) { + const heightMaps = tileHeightMaps.get(tileIndex); + + if (heightMaps !== undefined) { + const tx = clamp(toScreenX(x - Math.floor(x)), 0, tileWidth - 1) | 0; + const ty = clamp(toScreenY(y - Math.floor(y)), 0, tileHeight - 1) | 0; + return heightMaps[tx + ty * tileWidth] === -0.25; + } + } + + return false; +} + +export function getTileHeight( + tileType: TileType, tileIndex: number, x: number, y: number, gameTime: number, mapType: MapType +) { + const typeNumber = (tileIndex & 0xff00) >> 8; + + if ( + typeNumber === TileTypeNumber.Ice || + typeNumber === TileTypeNumber.Water || + (mapType === MapType.Cave && tileType === TileType.Grass) + ) { + if (tileType !== TileType.WalkableWater && tileType !== TileType.WalkableIce) { + const heightMaps = tileHeightMaps.get(tileIndex); + + if (heightMaps !== undefined) { + const tx = clamp(toScreenX(x - Math.floor(x)), 0, tileWidth - 1) | 0; + const ty = clamp(toScreenY(y - Math.floor(y)), 0, tileHeight - 1) | 0; + return heightMaps[tx + ty * tileWidth]; + } + } + } else if (typeNumber === TileTypeNumber.SnowOnIce) { + return -0.2; + } else if (tileType === TileType.ElevatedDirt) { + return 0.5; + } else if (typeNumber === TileTypeNumber.Boat) { + const frame = ((gameTime / 1000) * WATER_FPS) | 0; + return waterHeight[frame % waterHeight.length]; + } + + return 0; +} diff --git a/src/ts/client/timing.ts b/src/ts/client/timing.ts new file mode 100644 index 0000000..9d45e65 --- /dev/null +++ b/src/ts/client/timing.ts @@ -0,0 +1,107 @@ +interface TimingEntry { + time: number; + name?: string; +} + +interface TimingResult { + name: string; + count: number; + selfTime: number; + totalTime: number; + selfPercent: number; + totalPercent: number; +} + +const ENABLED = false; +const ENTRIES_LIMIT = 8000; + +const entries: TimingEntry[] = []; +let entriesCount = 0; + +if (TIMING && ENABLED) { + for (let i = 0; i < ENTRIES_LIMIT; i++) { + entries.push({ time: 0, name: undefined }); + } +} + +export function timeStart(name: string) { + if (TIMING && ENABLED) { + if (entriesCount < ENTRIES_LIMIT) { + const entry = entries[entriesCount]; + entry.time = performance.now(); + entry.name = name; + entriesCount++; + } else { + console.warn(`exceeded timing entry limit`); + } + } +} + +export function timeEnd() { + if (TIMING && ENABLED) { + if (entriesCount < ENTRIES_LIMIT) { + const entry = entries[entriesCount]; + entry.time = performance.now(); + entry.name = undefined; + entriesCount++; + } else { + console.warn(`exceeded timing entry limit`); + } + } +} + +export function timeReset() { + if (TIMING && ENABLED) { + entriesCount = 0; + } +} + +export function timingCollate(): TimingResult[] { + if (TIMING && ENABLED && entriesCount > 0) { + interface Entry extends TimingEntry { + excludedTime: number; + } + + const listings: TimingResult[] = []; + const startStack: Entry[] = []; + + for (let i = 0; i < entriesCount; i++) { + const entry = entries[i]; + + if (entry.name !== undefined) { + startStack.push({ ...entry, excludedTime: 0 }); + } else { + const start = startStack.pop()!; + const name = start.name!; + const time = entry.time - start.time; + let listing = listings.find(l => l.name === name); + + if (!listing) { + listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 }; + listings.push(listing); + } + + listing.count++; + listing.selfTime += (time - start.excludedTime); + listing.totalTime += time; + + if (startStack.length) { + startStack[startStack.length - 1].excludedTime += time; + } + } + } + + const firstTime = entries[0].time; + const lastTime = entries[entriesCount - 1].time; + const totalTime = lastTime - firstTime; + + for (const listing of listings) { + listing.selfPercent = 100 * listing.selfTime / totalTime; + listing.totalPercent = 100 * listing.totalTime / totalTime; + } + + return listings.sort((a, b) => b.selfTime - a.selfTime); + } + + return []; +} diff --git a/src/ts/client/webgl.ts b/src/ts/client/webgl.ts new file mode 100644 index 0000000..b6300b1 --- /dev/null +++ b/src/ts/client/webgl.ts @@ -0,0 +1,136 @@ +import { spriteShader, paletteLayersShader, lightShader } from '../generated/shaders'; +import { FrameBuffer, createFrameBuffer, disposeFrameBuffer } from '../graphics/webgl/frameBuffer'; +import { SpriteSheet, CommonPalettes, PaletteManager, Camera } from '../common/interfaces'; +import { Shader, createShader, disposeShader } from '../graphics/webgl/shader'; +import { PaletteSpriteBatch, PALETTE_BATCH_BYTES_PER_VERTEX } from '../graphics/paletteSpriteBatch'; +import { getWebGLContext, getRenderTargetSize, unbindAllTexturesAndBuffers } from '../graphics/webgl/webglUtils'; +import { createCommonPalettes } from '../graphics/graphicsUtils'; +import { createTexturesForSpriteSheets, disposeTexturesForSpriteSheets } from '../graphics/spriteSheetUtils'; +import * as sprites from '../generated/sprites'; +import { SpriteBatch } from '../graphics/spriteBatch'; +import { BATCH_SIZE_MAX } from '../common/constants'; + +export interface WebGL { + gl: WebGLRenderingContext; + frameBuffer: FrameBuffer | undefined; + frameBufferSheet: SpriteSheet; + spriteShader: Shader; + lightShader: Shader; + paletteShader: Shader; + spriteBatch: SpriteBatch; + paletteBatch: PaletteSpriteBatch; + palettes: CommonPalettes; + failedFBO: boolean; + renderer: string; +} + +const spriteShaderSource = spriteShader; +const paletteShaderSource = paletteLayersShader; +const lightShaderSource = lightShader; + +function createIndices(capacity: number) { + const numIndices = (capacity * 6) | 0; + const indices = new Uint16Array(numIndices); + + for (let i = 0, j = 0; i < numIndices; j = (j + 4) | 0) { + indices[i++] = (j + 0) | 0; + indices[i++] = (j + 1) | 0; + indices[i++] = (j + 2) | 0; + indices[i++] = (j + 0) | 0; + indices[i++] = (j + 2) | 0; + indices[i++] = (j + 3) | 0; + } + + return indices; +} + +export function initWebGL(canvas: HTMLCanvasElement, paletteManager: PaletteManager, camera: Camera): WebGL { + const gl = getWebGLContext(canvas); + return initWebGLResources(gl, paletteManager, camera); +} + +export function initWebGLResources(gl: WebGLRenderingContext, paletteManager: PaletteManager, camera: Camera): WebGL { + let renderer = ''; + let failedFBO = false; + let frameBuffer: FrameBuffer | undefined; + let frameBufferSheet: SpriteSheet = { texture: undefined, sprites: [], palette: false }; + + try { + const size = getRenderTargetSize(camera.w, camera.h); + frameBuffer = createFrameBuffer(gl, size, size); + frameBufferSheet.texture = frameBuffer.texture; + } catch (e) { + DEVELOPMENT && console.warn(e); + failedFBO = true; + } + + createTexturesForSpriteSheets(gl, sprites.spriteSheets); + const palettes = createCommonPalettes(paletteManager); + + const paletteShader = createShader(gl, paletteShaderSource); + const spriteShader = createShader(gl, spriteShaderSource); + const lightShader = createShader(gl, lightShaderSource); + + const VERTICES_PER_SPRITE = 4; + const buffer = new ArrayBuffer(BATCH_SIZE_MAX * VERTICES_PER_SPRITE * PALETTE_BATCH_BYTES_PER_VERTEX); + const vertexBuffer = gl.createBuffer(); + + if (!vertexBuffer) { + throw new Error(`Failed to allocate vertex buffer`); + } + + const indexBuffer = gl.createBuffer(); + + if (!indexBuffer) { + throw new Error(`Failed to allocate index buffer`); + } + + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, createIndices(BATCH_SIZE_MAX), gl.STATIC_DRAW); + + const vertexBuffer2 = gl.createBuffer(); + + if (!vertexBuffer2) { + throw new Error(`Failed to allocate vertex buffer (2)`); + } + + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer2); + gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW); + + const spriteBatch = new SpriteBatch(gl, BATCH_SIZE_MAX, buffer, vertexBuffer2, indexBuffer); + const paletteBatch = new PaletteSpriteBatch(gl, BATCH_SIZE_MAX, buffer, vertexBuffer, indexBuffer); + spriteBatch.rectSprite = sprites.pixel; + paletteBatch.rectSprite = sprites.pixel2; + paletteBatch.defaultPalette = palettes.defaultPalette; + + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + + paletteManager.init(gl); + + const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); + + if (debugInfo) { + renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); + } + + return { + gl, paletteShader, spriteShader, lightShader, spriteBatch, paletteBatch, + frameBuffer, frameBufferSheet, palettes, failedFBO, renderer, + }; +} + +export function disposeWebGL(webgl: WebGL) { + const { gl } = webgl; + + unbindAllTexturesAndBuffers(gl); + disposeTexturesForSpriteSheets(gl, sprites.spriteSheets); + disposeFrameBuffer(gl, webgl.frameBuffer); + disposeShader(gl, webgl.lightShader); + disposeShader(gl, webgl.spriteShader); + disposeShader(gl, webgl.paletteShader); + webgl.spriteBatch.dispose(); + webgl.paletteBatch.dispose(); +} diff --git a/src/ts/common/accountUtils.ts b/src/ts/common/accountUtils.ts new file mode 100644 index 0000000..da318da --- /dev/null +++ b/src/ts/common/accountUtils.ts @@ -0,0 +1,80 @@ +import { + BASE_CHARACTER_LIMIT, ADDITIONAL_CHARACTERS_SUPPORTER1, ADDITIONAL_CHARACTERS_SUPPORTER2, + ADDITIONAL_CHARACTERS_SUPPORTER3, ADDITIONAL_CHARACTERS_PAST_SUPPORTER +} from './constants'; +import { AccountDataFlags } from './interfaces'; +import { hasFlag } from './utils'; + +export interface AccountRoles { + roles?: string[] | undefined; +} + +export interface AccountSupporter extends AccountRoles { + supporter?: number | undefined; + supporterInvited?: boolean; + flags?: AccountDataFlags; +} + +export function hasRole(account: AccountRoles | undefined, role: string): boolean { + return !!(account && account.roles && account.roles.indexOf(role) !== -1); +} + +export function isAdmin(account: AccountRoles): boolean { + return hasRole(account, 'admin') || hasRole(account, 'superadmin'); +} + +export function isMod(account: AccountRoles): boolean { + return hasRole(account, 'mod') || isAdmin(account); +} + +export function isDev(account: AccountRoles): boolean { + return hasRole(account, 'dev'); +} + +export function meetsRequirement(account: AccountSupporter, require: string | undefined): boolean { + return !require || hasRole(account, require) || meetsSupporterRequirement(account, require); +} + +function meetsSupporterRequirement(account: AccountSupporter, require: string): boolean { + const level = account.supporter || 0; + const modOrDev = isMod(account) || isDev(account); + + if (require === 'inv') { + return modOrDev || level >= 1 || !!account.supporterInvited; + } else if (require === 'sup1') { + return modOrDev || level >= 1; + } else if (require === 'sup2') { + return modOrDev || level >= 2; + } else if (require === 'sup3') { + return modOrDev || level >= 3; + } else { + return false; + } +} + +export function getCharacterLimit(account: AccountSupporter) { + switch (account.supporter || 0) { + case 1: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER1; + case 2: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER2; + case 3: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER3; + default: + if (hasFlag(account.flags, AccountDataFlags.PastSupporter)) { + return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_PAST_SUPPORTER; + } else { + return BASE_CHARACTER_LIMIT; + } + } +} + +export function getSupporterInviteLimit(account: AccountSupporter) { + if (isMod(account) || isDev(account)) { + return 100; + } else { + switch (account.supporter) { + case 1: return 1; + case 2: return 5; + case 3: return 10; + default: return 0; + } + } +} diff --git a/src/ts/common/adminInterfaces.ts b/src/ts/common/adminInterfaces.ts new file mode 100644 index 0000000..ce12b7e --- /dev/null +++ b/src/ts/common/adminInterfaces.ts @@ -0,0 +1,923 @@ +import { + PonyInfo, AccountSettings, AccountData, PonyObject, AccountCounters, ServerFeatureFlags, Dict, Subscription +} from './interfaces'; + +export const ITEM_LIMIT = 1000; + +export const ROLES = ['superadmin', 'admin', 'mod', 'dev']; + +export const SERVER_LABELS: { [key: string]: string; } = { + 'dev': 'badge-test', + 'test': 'badge-test', + 'main': 'badge-none', + 'main-ru': 'badge-none', + 'safe': 'badge-success', + 'safe-ru': 'badge-success', + 'safe-pr': 'badge-success', + 'safe-sp': 'badge-success', +}; + +export const enum Suspicious { + No, + Yes, + Very, +} + +export const enum CharacterFlags { + None = 0, + BadCM = 1, + HideSupport = 4, + RespawnAtSpawn = 8, + ForbiddenName = 16, +} + +// NOTE: also update createLoginServerStatus() (internal-login.ts) +export interface GeneralSettings { + isPageOffline?: boolean; + + canCreateAccounts?: boolean; + blockWebView?: boolean; + reportPotentialDuplicates?: boolean; + autoMergeDuplicates?: boolean; + + suspiciousNames?: string; + suspiciousPonies?: string; + suspiciousMessages?: string; + suspiciousSafeMessages?: string; + suspiciousSafeWholeMessages?: string; + suspiciousSafeInstantMessages?: string; + suspiciousSafeInstantWholeMessages?: string; + suspiciousAuths?: string; + patreonToken?: string; +} + +// NOTE: also update createLoginServerStatus() +export const LOGIN_SERVER_SETTINGS: { id: keyof GeneralSettings; label: string; }[] = [ + { id: 'canCreateAccounts', label: 'Can create accounts' }, + { id: 'blockWebView', label: 'Block web view' }, + { id: 'reportPotentialDuplicates', label: 'Report potential duplicates' }, + { id: 'autoMergeDuplicates', label: 'Auto-merge duplicates' }, +]; + +export interface ServerLiveSettings { + updating: boolean; + shutdown: boolean; +} + +export interface GameServerSettings { + isServerOffline?: boolean; + filterSwears?: boolean; + autoBanSwearing?: boolean; + autoBanSpamming?: boolean; + doubleTimeouts?: boolean; + reportSpam?: boolean; + reportSwears?: boolean; + reportTeleporting?: boolean; + logLagging?: boolean; + logTeleporting?: boolean; + logFixingPosition?: boolean; + hideSwearing?: boolean; + kickSwearing?: boolean; + kickSwearingToSpawn?: boolean; + blockJoining?: boolean; + kickTeleporting?: boolean; + fixTeleporting?: boolean; + kickLagging?: boolean; + reportSitting?: boolean; +} + +export interface Settings extends GeneralSettings { + servers: Dict; +} + +export const SERVER_SETTINGS: { id: keyof GameServerSettings; label: string; }[] = [ + { id: 'filterSwears', label: 'Swear filter' }, + { id: 'autoBanSwearing', label: 'Auto-Timeout for swearing' }, + { id: 'autoBanSpamming', label: 'Auto-Timeout for spam' }, + { id: 'doubleTimeouts', label: 'Double timeouts duration' }, + { id: 'reportSpam', label: 'Report spam' }, + { id: 'reportSwears', label: 'Report swearing' }, + { id: 'reportTeleporting', label: 'Report teleporting' }, + { id: 'logLagging', label: 'Log lagging' }, + { id: 'logTeleporting', label: 'Log teleporting' }, + { id: 'logFixingPosition', label: 'Log fixing position' }, + { id: 'hideSwearing', label: 'Hide swearing' }, + { id: 'kickSwearing', label: 'Kick for swearing' }, + { id: 'kickSwearingToSpawn', label: 'Reset swearing to spawn' }, + { id: 'blockJoining', label: 'Block joining' }, + { id: 'kickTeleporting', label: 'Kick teleporting players' }, + { id: 'fixTeleporting', label: 'Fix teleporting players' }, + { id: 'kickLagging', label: 'Kick lagging players' }, + { id: 'reportSitting', label: 'Report sitting' }, +]; + +export interface InternalCommonApi { + reloadSettings(): Promise; +} + +export interface InternalApi extends InternalCommonApi { + state(): Promise; + stats(): Promise; + statsTable(stats: Stats): Promise; + action(action: string, accountId: string): Promise; + join(accountId: string, ponyId: string): Promise; + kick(accountId: string | undefined, characterId: string | undefined): Promise; + kickAll(): Promise; + accountChanged(accountId: string): Promise; + accountMerged(accountId: string, mergeId: string): Promise; + accountStatus(accountId: string): Promise; + accountAround(accountId: string): Promise; + accountHidden(accountId: string): Promise; + notifyUpdate(): Promise; + cancelUpdate(): Promise; + shutdownServer(value: boolean): Promise; + getTimings(): Promise; + teleportTo(adminAccountId: string, targetAccountId: string): Promise; +} + +export interface InternalLoginApi extends InternalCommonApi { + state(): Promise; + loginServerStats(): Promise; + updateLiveSettings(update: Partial): Promise; + mergeAccounts(id: string, withId: string, reason: string, allowAdmin: boolean, creatingDuplicates: boolean): Promise; +} + +export interface ServerConfig { + id: string; + port: number; + path: string; + local: string; + name: string; + desc: string; + flag: string; + host?: string; + alert?: string; + require?: string; + flags: ServerFeatureFlags; + hidden?: boolean; +} + +export interface GameServerState { + id: string; + path: string; + name: string; + desc: string; + flag: string; + host?: string; + alert?: string; + require?: string; + world?: { + mapSize: number; + regionSize: number; + }; + flags: ServerFeatureFlags; + + dead: boolean; + maps: number; + online: number; + onMain: number; + queued: number; + shutdown: boolean; + settings: GameServerSettings; +} + +export interface InternalServerState { + id: string; + api: InternalCommonApi; +} + +export interface InternalLoginServerState extends InternalServerState { + api: InternalLoginApi; + state: LoginServerStatus; +} + +export interface InternalGameServerState extends InternalServerState { + api: InternalApi; + state: GameServerState; +} + +export interface ServerStatus { + diskSpace: string; + memoryUsage: string; + certificateExpiration: string; + lastPatreonUpdate: string; +} + +export interface LoginServerStatus extends GeneralSettings { + updating: boolean; + dead: boolean; +} + +export interface AdminState { + status: ServerStatus; + loginServers: LoginServerStatus[]; + gameServers: GameServerState[]; +} + +export interface MemoryStatus { + total: number; + used: number; + free: number; +} + +export interface OriginInfoBase { + ip: string; + country: string; + last?: Date; +} + +export interface MergeItemData { + id: string; + name: string; +} + +export interface MergeHideData { + id: string; + name: string; + date: string; +} + +export interface MergeAccountData { + name: string; + note: string; + flags: AccountFlags; + emails: string[]; + ignores: string[]; + counters: AccountCounters; + auths: MergeItemData[]; + characters: MergeItemData[]; + state: AccountState; + birthdate?: Date; + settings?: AccountSettings; + friends?: string[]; + hides?: MergeHideData[]; +} + +export interface MergeData { + account: MergeAccountData; + merge: MergeAccountData; +} + +export interface MergeInfo { + _id?: string; + id: string; + name: string; + //code: number; + date: Date; + reason?: string; + data?: MergeData; + split?: boolean; +} + +export interface LogEntry { + message: string; + date: Date; +} + +export interface AccountDetails { + merges: MergeInfo[]; + supporterLog: LogEntry[]; + banLog: LogEntry[]; + invitesReceived: SupporterInvite[]; + invitesSent: SupporterInvite[]; + state: AccountState; +} + +export interface AuthDetails { + id: string; + lastUsed: string | undefined; +} + +export interface BannedMuted { + mute?: number; + shadow?: number; + ban?: number; +} + +export interface Document extends Timestamps { + _id: string; + deleted?: boolean; +} + +// bases + +export interface TimestampsBase { + createdAt?: Date; + updatedAt: Date; +} + +export interface ChatMessageBase { + createdAt: Date; + message: string; +} + +export const accountCounters = [ + { name: 'spam', label: 'spam' }, + { name: 'swears', label: 'swearing' }, + { name: 'timeouts', label: 'timeouts' }, + { name: 'inviteLimit', label: 'party limits' }, + { name: 'friendLimit', label: 'friend limits' }, +]; + +export const enum AccountFlags { + None = 0, + BlockPartyInvites = 1, + CreatingDuplicates = 2, + DuplicatesNotification = 4, + BlockMerging = 16, + BlockFriendRequests = 256, +} + +export const accountFlags = [ + { value: AccountFlags.BlockPartyInvites, name: 'BlockPartyInvites', label: 'block party invites' }, + { value: AccountFlags.CreatingDuplicates, name: 'CreatingDuplicates', label: 'creating duplicates' }, + { value: AccountFlags.DuplicatesNotification, name: 'DuplicatesNotification', label: 'duplicates notification' }, + { value: AccountFlags.BlockMerging, name: 'BlockMerging', label: 'block merging' }, + { value: AccountFlags.BlockFriendRequests, name: 'BlockFriendRequests', label: 'block friend requests' }, +]; + +export const enum PatreonFlags { + None = 0, + Supporter1 = 1, + Supporter2 = 2, + Supporter3 = 3, +} + +export const enum SupporterFlags { + None = 0, + Supporter1 = 1, + Supporter2 = 2, + Supporter3 = 3, + SupporterMask = 0x0003, + IgnorePatreon = 0x0080, + PastSupporter = 0x0100, + ForcePastSupporter = 0x0200, + IgnorePastSupporter = 0x0400, +} + +export const supporterFlags = [ + { value: SupporterFlags.IgnorePatreon, label: 'ignore data from patreon' }, +]; + +// NOTE: also update mergeStates (merge.ts) +export interface AccountState { + gifts?: number; + candies?: number; + clovers?: number; + toys?: number; + eggs?: number; +} + +export interface AccountAlert { + expires: Date; + message: string; +} + +export interface AccountBase extends TimestampsBase, BannedMuted { + name: string; + birthdate?: Date; + birthyear?: number; + emails?: string[]; + roles: string[]; + origins: OriginInfoBase[]; + settings?: AccountSettings; + note: string; + noteUpdated?: Date; + lastVisit: Date; + lastUserAgent?: string; + lastBrowserId?: string; + lastOnline?: Date; + lastCharacter?: ID; + ignores?: string[]; + flags: AccountFlags; + counters?: AccountCounters; + characterCount: number; + patreon?: PatreonFlags; + supporter?: SupporterFlags; + supporterLog?: LogEntry[]; + supporterTotal?: number; + supporterDeclinedSince?: Date; + merges?: MergeInfo[]; + banLog?: LogEntry[]; + state?: AccountState; + alert?: AccountAlert; + savedMap?: string; +} + +export interface AuthBase extends TimestampsBase { + account?: ID; + openId?: string; + provider: string; + name: string; + url: string; + emails?: string[]; + disabled?: boolean; + banned?: boolean; + pledged?: number; + lastUsed?: Date; +} + +export interface OriginBase extends TimestampsBase, OriginInfoBase, BannedMuted { +} + +export const enum CharacterStateFlags { + None = 0, + Right = 1, + Extra = 2, +} + +export interface CharacterState { + x: number; + y: number; + map?: string; + toy?: number; + flags?: CharacterStateFlags; + hold?: string; +} + +export interface CharacterBase extends TimestampsBase { + account: ID; + site?: ID; + tag?: string; + name: string; + desc?: string; + info?: string; + flags: CharacterFlags; + lastUsed?: Date; + creator?: string; + state?: { [key: string]: CharacterState | undefined; }; +} + +export interface EventBase extends TimestampsBase { + account?: ID; + pony?: ID; + type: string; + server: string; + message: string; + desc: string; + origin?: OriginInfoBase; + count: number; +} + +export interface SupporterInviteBase extends TimestampsBase { + source: ID; + target: ID; + name: string; + info: string; + active: boolean; +} + +export interface FriendRequestBase { + source: ID; + target: ID; +} + +export interface HideRequestBase { + source: ID; + target: ID; + name: string; + date: Date; +} + +export const eventFields: (keyof Event)[] = [ + '_id', 'updatedAt', 'createdAt', 'type', 'server', 'message', 'desc', 'count', 'origin', 'account', 'pony' +]; + +// models + +export interface OriginInfo extends OriginInfoBase { +} + +export interface Timestamps extends TimestampsBase { +} + +export interface AccountStatus { + online: boolean; + server?: string; + map?: string; + incognito?: boolean; + character?: string; + x?: number; + y?: number; + userAgent?: string; + duration?: string; +} + +export interface DuplicatesInfo { + count: number; + name: boolean; + emails: boolean; + browserId: boolean; + generatedAt: number; + perma: boolean; +} + +export type ListListener = (items: T[]) => void; + +export interface IObservableList { + hasSubscribers(): boolean; + trigger(): void; + push(item: T): void; + pushOrdered(item: T, compare: (a: T, b: T) => number): void; + remove(item: T): boolean; + replace(list: T[]): void; + subscribe(listener: ListListener): Subscription; +} + +export interface PonyIdDateName { + id: string; + date: number; + name: string; +} + +export interface Account extends AccountBase, Document { + nameLower?: string; + auths?: Auth[]; + originsRefs?: OriginRef[]; + ignoredByLimit?: number; + ignoresLimit?: number; + ignoresCount?: number; + duplicatesLimit?: number; + totalPledged?: number; + + ponies?: Character[]; + invitesReceived?: SupporterInvite[]; + invitesSent?: SupporterInvite[]; + + authsList?: IObservableList; + poniesList?: IObservableList; + originsList?: IObservableList; +} + +export interface Auth extends AuthBase, Document { +} + +export interface Character extends CharacterBase, Document { + ponyInfo?: PonyInfo; + deleted?: boolean; +} + +export interface Origin extends OriginBase, Document { + accounts?: Account[]; + accountsCount?: number; +} + +export interface OriginRef { + origin: Origin; + last: Date; +} + +export interface Event extends EventBase, Document { + deleted?: boolean; + descHTML?: any; +} + +export interface ChatEvent { + event: Event; + account: Account | undefined; +} + +export interface SupporterInvite extends SupporterInviteBase, Document { +} + +export interface FriendRequest extends FriendRequestBase, Document { +} + +// other + +export interface UpdateOrigin extends OriginInfo, BannedMuted { +} + +export interface AccountUpdate extends BannedMuted { + age?: number; + name?: string; + note?: string; + flags?: number; + supporter?: number; +} + +export interface BaseValues { + updatedAt?: string; + createdAt?: string; + lastVisit?: string; +} + +export interface LiveResponse { + updates: any[][]; + deletes: string[]; + base: BaseValues; + more: boolean; +} + +export interface RequestStats { + path: string; + count: number; + average: string; + total: string; + order: string; + totalCount: number; +} + +export interface UserCountStats { + count: number; + date: string; +} + +export interface LoginStats { + requests: RequestStats[]; + userCounts: UserCountStats[]; +} + +export interface ItemCounts { + accounts: number; + characters: number; + auths: number; + origins: number; +} + +export interface FindPonyQuery { + search?: string; + orderBy?: string; +} + +export interface AuthUpdate { + disabled?: boolean; + banned?: boolean; + pledged?: number; +} + +export interface AccountPonies { + account: string; + count: number; + ponies: any[][]; +} + +export interface AccountPoniesResponse { + base: BaseValues; + accounts: AccountPonies[]; +} + +export interface PoniesResponse { + base: BaseValues; + ponies: any[][]; +} + +export interface PonyCreator { + _id: string; + name: string; + creator: string; +} + +export interface AccountOrigins { + accountId: string; + ips: string[]; +} + +export interface ServerStats { + actions: { + id: number; + name: string; + type: string; + countBin: number; + countStr: number; + average: string; + total: string; + }[]; +} + +export interface DuplicateInfoEntry { + account: string; + userAgent: string; + ponies: string[]; +} + +export interface AroundEntry { + account: string; + distance: number; + party: boolean; +} + +export const enum Stats { + Country, + Support, + Maps, +} + +export type StatsTable = string[][]; + +export interface OriginStats { + uniqueOrigins: number; + duplicateOrigins: number; + singleOrigins: number; + totalOrigins: number; + totalOriginsIP4: number; + totalOriginsIP6: number; + distribution: number[]; +} + +export interface OtherStats { + totalIgnores: number; + authsWithEmptyAccount: number; + authsWithMissingAccount: number; +} + +export interface Around { + account: Account; + distance: number; + party: boolean; +} + +export interface DuplicateBase { + indenticalEmail: boolean; + emails: number; + origins: number; + ponies?: string[]; + userAgent?: string; + browserId?: boolean; + birthdate: boolean; + perma: boolean; + name: number; + note: number; + lastVisit: Date; +} + +export interface DuplicateResult extends DuplicateBase { + account: string; +} + +export interface Duplicate extends DuplicateBase { + account: Account; +} + +export interface FindAccountQuery { + search: string; + showOnly: string; + not: boolean; + page: number; + itemsPerPage: number; + force?: boolean; +} + +export interface FindAccountResult { + accounts: string[]; + page: number; + totalItems: number; +} + +export interface AdminCacheEntry { + query: string; + result: T; + timestamp: Date; +} + +export interface AdminCache { + findAccounts?: AdminCacheEntry; +} + +export interface ClearOrignsOptions { + old?: boolean; + singles?: boolean; + trim?: boolean; + veryOld?: boolean; + country?: string; +} + +export interface PatreonReward { + id: string; + title: string; + description: string; +} + +export interface PatreonPledge { + user: string; + reward: string; + total: number; + declinedSince?: string; + account?: string; +} + +export interface PatreonData { + rewards: PatreonReward[]; + pledges: PatreonPledge[]; +} + +export interface HidingStats { + account: string; + hidden: string[]; + hiddenBy: string[]; + permaHidden: string[]; + permaHiddenBy: string[]; +} + +export const enum TimingEntryType { + Start, + End, +} + +export interface TimingEntry { + type: TimingEntryType; + time: number; + name?: string; +} + +export type ModelTypes = + 'accounts' | 'auths' | 'origins' | 'ponies' | 'accountAuths' | 'accountOrigins' | 'accountPonies'; + +export interface IAdminServerActions { + // subscribing + subscribe(model: ModelTypes, id: string): void; + unsubscribe(model: ModelTypes, id: string): void; + // other + getSignedAccount(): Promise; + getCounts(): Promise; + getState(): Promise; + updateSettings(update: Partial): Promise; + updateGameServerSettings(serverId: string, update: Partial): Promise; + fetchServerStats(serverId: string): Promise; + fetchServerStatsTable(serverId: string, stats: Stats): Promise; + notifyUpdate(serverId: string): Promise; + shutdownServers(serverId: string): Promise; + resetUpdating(serverId: string): Promise; + report(accountId: string): Promise; + action(action: string, accountId: string): Promise; + kick(accountId: string): Promise; + kickAll(serverId: string): Promise; + getChat(search: string, date: string, caseInsensitive: boolean): Promise; + getChatForAccounts(accountIds: string[], date: string): Promise; + getRequestStats(): Promise; + updatePatreon(): Promise; + resetSupporter(accountId: string): Promise; + getLastPatreonData(): Promise; + updatePastSupporters(): Promise; + // live + get(endPoint: 'events', id: string): Promise; + getAll(endPoint: 'events', timestamp?: string): Promise; + assignAccount(endPoint: 'events', id: string, account: string): Promise; + removeItem(endPoint: 'events', id: string): Promise; + // events + removeEvent(id: string): Promise; + // origins + updateOrigin(origin: UpdateOrigin): Promise; + getOriginStats(): Promise; + getOtherStats(): Promise; + clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions): Promise; + clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions): Promise; + // characters + getPony(id: string): Promise; + getPonyInfo(id: string): Promise; + getPoniesCreators(accountId: string): Promise; + getPoniesForAccount(accountId: string): Promise; + getDetailsForAccount(accountId: string): Promise; + findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise<{ items: string[]; totalCount: number; }>; + createPony(account: string, name: string, info: string): Promise; + assignPony(ponyId: string, accountId: string): Promise; + removePony(id: string): Promise; + removePoniesAboveLimit(accountId: string): Promise; + removeAllPonies(accountId: string): Promise; + // auths + getAuth(id: string): Promise; + getAuthsForAccount(accountId: string): Promise; + fetchAuthDetails(auths: string[]): Promise; + updateAuth(id: string, update: AuthUpdate): Promise; + assignAuth(authId: string, accountId: string): Promise; + removeAuth(id: string): Promise; + // accounts + getAccount(id: string): Promise; + findAccounts(query: FindAccountQuery): Promise; + createAccount(name: string): Promise; + getAccountsByEmails(emails: string[]): Promise>; + getAccountsByOrigin(ip: string): Promise; + setName(accountId: string, name: string): Promise; + setAge(accountId: string, age: number): Promise; + setRole(accountId: string, role: string, set: boolean): Promise; + updateAccount(accountId: string, update: AccountUpdate, message?: string): Promise; + timeoutAccount(accountId: string, timeout: number): Promise; + updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number): Promise; + removeAllOrigins(accountId: string): Promise; + removeOriginsForAccount(accountId: string, ips: string[]): Promise; + removeOriginsForAccounts(origins: AccountOrigins[]): Promise; + addOriginToAccount(accountId: string, origin: OriginInfo): Promise; + mergeAccounts(accountId: string, withId: string): Promise; + unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData): Promise; + addEmail(accountId: string, email: string): Promise; + removeEmail(accountId: string, email: string): Promise; + removeIgnore(accountId: string, ignore: string): Promise; + addIgnores(accountId: string, ignores: string[]): Promise; + removeFriend(accountId: string, friendId: string): Promise; + addFriend(accountId: string, friendId: string): Promise; + setAccountState(accountId: string, state: AccountState): Promise; + getAccountStatus(accountId: string): Promise; + getAccountAround(accountId: string): Promise; + getAccountHidden(accountId: string): Promise; + getAccountFriends(accountId: string): Promise; + removeAccount(accountId: string): Promise; + setAlert(accountId: string, message: string, expiresIn: number): Promise; + getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }>; + getAllDuplicatesQuickInfo(accountId: string): Promise; + getAllDuplicates(accountId: string): Promise; + getDuplicateEntries(force: boolean): Promise; + clearSessions(accountId: string): Promise; + // other + getTimings(serverId: string): Promise; + teleportTo(accountId: string): Promise; +} diff --git a/src/ts/common/adminUtils.ts b/src/ts/common/adminUtils.ts new file mode 100644 index 0000000..be8c3e4 --- /dev/null +++ b/src/ts/common/adminUtils.ts @@ -0,0 +1,570 @@ +import * as moment from 'moment'; +import { escape, escapeRegExp, startsWith, range, uniq, compact } from 'lodash'; +import { fromNow, toInt, hasFlag, compareDates, removeItem, includes } from './utils'; +import { DAY } from './constants'; +import { + Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase, LogEntry, + DuplicateResult, DuplicateBase, Duplicate, Auth, MergeInfo +} from './adminInterfaces'; +import { hasRole } from './accountUtils'; +import { filterBadWordsPartial } from './swears'; +import { faPlusCircle, faClock, faMinusCircle, faCaretSquareUp, faCaretSquareDown } from '../client/icons'; +import { element, textNode } from '../client/htmlUtils'; + +interface UpdatedAt { + updatedAt: Date; +} + +export const compareUpdatedAt = (a: UpdatedAt, b: UpdatedAt) => compareDates(a.updatedAt, b.updatedAt); +export const compareOrigins = (a: OriginInfo, b: OriginInfo) => a.ip.localeCompare(b.ip); +export const compareOriginRefs = (a: OriginRef, b: OriginRef) => + compareDates(b.last, a.last) || compareOrigins(a.origin, b.origin); +export const compareByName = (a: T, b: T) => (a.name || '').localeCompare(b.name || ''); +export const getId = (item: Document) => item._id; +export const tagBad = (s: string) => `${s}`; + +export function compareAccounts(a: Account, b: Account) { + return compareDates(a.createdAt, b.createdAt); +} + +export function compareAuths(a: Auth, b: Auth) { + const aDeleted = a.disabled || a.banned || false; + const bDeleted = b.disabled || b.banned || false; + + if (aDeleted && !bDeleted) { + return 1; + } else if (!aDeleted && bDeleted) { + return -1; + } else { + return compareByName(a, b); + } +} + +export function highlightWords(text?: string) { + text = text || ''; + text = filterBadWordsPartial(text, tagBad); + return text; +} + +export function getAge(birthdate: Date) { + return moment().diff(birthdate, 'years'); +} + +// chat & events + +function enc(text?: string): string { + return escape(text || ''); +} + +function encWithHighlight(text?: string): string { + return highlightWords(enc(text || '')); +} + +export function formatEventDesc(text: string): string { + return encWithHighlight(text).replace(/\[([a-z0-f]{24})\]/g, `[$1]`); +} + +function getMessageTag(message: string) { + if (/^\/p /.test(message)) { + return 'party'; + } else if (/^\/w /.test(message)) { + return 'whisper'; + } else if (/^\/s[s123] /.test(message)) { + return 'supporter'; + } else if (/^\//.test(message)) { + return 'command'; + } else { + return 'none'; + } +} + +export function replaceSwears(element: HTMLElement) { + const text = element.textContent; + + if (text) { + const replaced = encWithHighlight(text); + + if (text !== replaced) { + element.innerHTML = replaced; + } + } +} + +function formatChatLine(l: string): HTMLElement { + // 00:00:01 [system] Timed out for swearing + // 00:00:01 [patreon] fetched patreon data + // 00:00:01 [dev][Autumn Leafs] hello world + // 00:00:01 [dev][Autumn Leafs][muted] hello world + // 00:00:01 [dev][Autumn Leafs][ignored] hello world + // 00:00:01 [dev-pl][Autumn Leafs][ignored] hello world + // 00:00:01 [57a3dc6f2f0019a161cdebf6][dev][Autumn Leafs][ignored] hello world + // 00:00:01 [1][dev][Autumn Leafs][ignored] hello world + // 00:00:01 [1:merged][dev][Autumn Leafs][ignored] hello world + // 00:00:01 [merged][dev][Autumn Leafs][ignored] hello world + // 00:00:01 [merged][dev][main][Autumn Leafs][ignored] hello world + + /* tslint:disable:max-line-length */ + const regex = /^([0-9:]+) (\[(?:merged|\d+|\d+:merged|[a-z0-9]{24})\])?\[([a-z0-9_-]+)\](?:\[([a-z0-9_-]+)\])?((?:\[.*?\])?)(?:\[(muted|ignored|ignorepub)\])?\t(.*)$/; + const m = regex.exec(l); + + if (m) { + const [, time, accountId, server, map, name, mutedIgnored, message] = m; + const messageTag = server === 'system' ? 'system' : getMessageTag(message); + const modTag = mutedIgnored ? ' message-muted' : ''; + + return element('div', 'chatlog-line', [ + element('span', 'time', [], { 'data-text': time }), + accountId ? element('span', 'account-id', [textNode(accountId)]) : undefined, + element('span', `server server-${server.replace(/-.+$/g, '')}`, [textNode(`[${server}]`)]), + map ? element('span', `map map-${map}`, [textNode(`[${map}]`)]) : undefined, + element('span', mutedIgnored ? `name ${mutedIgnored}` : `name`, [textNode(name)]), + textNode(' '), + element('span', `message message-${messageTag}${modTag}`, [textNode(message)]), + textNode(' '), + element('a', 'chat-translate', [], undefined, { click: translateChat }), + ]); + } else { + return element('div', '', [textNode(highlightWords(l))]); + } +} + +function translateChat(this: HTMLElement) { + const lines: string[] = []; + let parent = this.parentElement; + + for (let i = 0; parent && i < 10; i++) { + lines.push(parent.querySelector('.message')!.textContent!); + parent = parent.nextElementSibling as HTMLElement; + } + + window.open(`https://translate.google.com/#auto/en/${encodeURIComponent(lines.join('\n'))}`); +} + +if (typeof window !== 'undefined') { + (window as any).goToAccount = (accountId: string) => { + window.dispatchEvent(new CustomEvent('go-to-account', { detail: accountId })); + }; +} + +export function formatChat(chat: string): HTMLElement[] { + return (chat || '') + .trim() + .split(/\r?\n/g) + .reverse() + .map(formatChatLine); +} + +export interface ChatDate { + value: string; + label: string; +} + +export function createChatDate(date: moment.Moment): ChatDate { + return { + value: date.toISOString(), + label: date.format('MMMM Do YYYY'), + }; +} + +export function createDateRange(startDate: string | Date, days: number): ChatDate[] { + return range(days, 0) + .map(d => moment(startDate).subtract(d, 'days')) + .map(createChatDate); +} + +// filtering + +export function filterAccounts(items: Account[], search: string, showOnly: string, not: boolean) { + if (search) { + items = items.filter(createFilter(search)); + } + + const filter = createFilter2(showOnly); + + if (filter) { + if (not) { + items = items.filter(i => !filter(i)); + } else { + items = items.filter(filter); + } + } + + return items; +} + +export function createFilter(search: string): (account: Account) => boolean { + const regex = new RegExp(escapeRegExp(search), 'i'); + + function test(value: string): boolean { + return !!value && regex.test(value); + } + + function testAuth(auth: Auth) { + return test(auth.name) || auth.provider === search || auth.url === search; + } + + function testMerge(merge: MergeInfo) { + return merge.id === search; + } + + function filter(account: Account): boolean { + if (account._id === search) + return true; + if (test(account.name)) + return true; + if (test(account.note)) + return true; + if (account.roles && account.roles.some(test)) + return true; + if (account.emails && account.emails.some(test)) + return true; + if (account.auths && account.auths.some(testAuth)) + return true; + if (account.merges && account.merges.some(testMerge)) + return true; + + return false; + } + + function prefixWith(prefix: string, action: (phrase: string) => (account: Account) => boolean) { + return startsWith(search, prefix) ? action(search.substr(prefix.length)) : undefined; + } + + function prefixWithRegex(prefix: string, action: (regex: RegExp) => (account: Account) => boolean) { + return prefixWith(prefix, phrase => action(new RegExp(escapeRegExp(phrase), 'i'))); + } + + function prefixWithNumber(prefix: string, action: (count: number) => (account: Account) => boolean) { + return prefixWith(prefix, phrase => action(+phrase)); + } + + const exactMatch = (phrase: string) => (account: Account) => account.nameLower === phrase; + const isOld = (max: number) => (account: Account) => !account.lastVisit || account.lastVisit.getTime() < max; + + return prefixWithRegex('name:', regex => account => regex.test(account.name)) + || prefixWithRegex('note:', regex => account => regex.test(account.note)) + || prefixWithRegex('email:', regex => account => !!account.emails && account.emails.some(e => regex.test(e))) + || prefixWith('role:', role => account => hasRole(account, role)) + || prefixWith('exact:', phrase => exactMatch(phrase.toLowerCase())) + || prefixWith('disabled!', () => account => !!account.auths && account.auths.some(a => !!a.disabled)) + || prefixWith('locked!', () => account => !!account.auths && account.auths.some(a => !!a.banned)) + || prefixWithNumber('ignores:', count => account => (account.ignoresCount || 0) >= count) + || prefixWithNumber('ponies:', count => account => account.characterCount >= count) + || prefixWithNumber('auths:', count => account => !!account.auths && account.auths.length >= count) + || prefixWithNumber('old:', days => isOld(fromNow(-days * DAY).getTime())) + || prefixWithNumber('spam:', count => account => !!account.counters && account.counters.spam! >= count) + || prefixWithNumber('swearing:', count => account => !!account.counters && account.counters.swears! >= count) + || prefixWithNumber('timeouts:', count => account => !!account.counters && account.counters.timeouts! >= count) + || prefixWithNumber('limits:', count => account => !!account.counters && account.counters.inviteLimit! >= count) + || filter; +} + +function hasAnyBan(account: Account) { + return isBanned(account) || isMuted(account) || isShadowed(account); +} + +export function createPotentialDuplicatesFilter(getAccountsByBrowserId: (id: string) => Account[] | undefined): (account: Account) => boolean { + return i => { + const name = i.nameLower; + + if (name === 'anonymous' || !i.lastBrowserId) + return false; + + const accounts = getAccountsByBrowserId(i.lastBrowserId); + + if (accounts !== undefined && accounts.length > 1) { + for (const a of accounts) { + if (a !== i && a.nameLower === name) { + return true; + } + } + } + + return false; + }; +} + +export function createFilter2(showOnly: string): ((account: Account) => boolean) | undefined { + const now = Date.now(); + + if (showOnly === 'banned') { + return hasAnyBan; + } else if (showOnly === 'timed out') { + return i => !!((i.mute && i.mute > now) || (i.shadow && i.shadow > now) || (i.ban && i.ban > now)); + } else if (showOnly === 'with flags') { + return i => !!i.flags; + } else if (showOnly === 'notes') { + return i => !!i.note; + } else if (showOnly === 'supporters') { + return i => !!(i.patreon || i.supporter || i.supporterDeclinedSince); + } else { + return undefined; + } +} + +export function getPotentialDuplicates(account: Account, getAccountsByBrowserId: (id: string) => Account[] | undefined) { + const accounts = account.lastBrowserId ? getAccountsByBrowserId(account.lastBrowserId) : undefined; + const name = account.nameLower; + + if (accounts !== undefined && accounts.length > 1 && name !== 'anonymous') { + return accounts.filter(a => a !== account && a.nameLower === name); + } else { + return []; + } +} + +// duplicates + +export function compareDuplicates(a: DuplicateBase, b: DuplicateBase): number { + if (a.note !== b.note) + return b.note - a.note; + if (a.emails !== b.emails) + return b.emails - a.emails; + if (a.name !== b.name) + return b.name - a.name; + if (a.browserId !== b.browserId) + return a.browserId ? -1 : 1; + if (a.origins !== b.origins) + return b.origins - a.origins; + if (a.ponies !== b.ponies) + return (b.ponies ? b.ponies.length : 0) - (a.ponies ? a.ponies.length : 0); + return b.lastVisit.getTime() - a.lastVisit.getTime(); +} + +export function emailName(email: string): string { + return email.substr(0, email.indexOf('@')).toLowerCase(); +} + +export function createEmailMatcher(emails: string[]): ((email: string) => boolean) | undefined { + if (!emails || !emails.length) { + return undefined; + } else { + const match = emails.map(emailName).map(escapeRegExp).join('|'); + const regex = new RegExp(`^(?:${match})@`, 'i'); + return email => regex.test(email); + } +} + +export function createDuplicate(account: Account, base: Account): Duplicate { + const indenticalEmail = account.emails && base.emails && account.emails.some(e => base.emails!.indexOf(e) !== -1); + + const isMatch = createEmailMatcher(base.emails || []); + const duplicateEmails = isMatch && account.emails + && account.emails.reduce((sum, e) => sum + (isMatch(e) ? 1 : 0), 0); + + const duplicateOrigins = base.originsRefs && account.originsRefs + && account.originsRefs.reduce((sum, o) => sum + (base.originsRefs!.some(r => o.origin.ip === r.origin.ip) ? 1 : 0), 0); + + const name = account.nameLower !== 'anonymous' && account.nameLower === base.nameLower; + + const note = (account.note && account.note.indexOf(base._id) !== -1) + || (base.note && base.note.indexOf(account._id) !== -1); + + const browserId = !!account.lastBrowserId && account.lastBrowserId === base.lastBrowserId; + const birthdate = !!(base.birthdate && account.birthdate && base.birthdate.getTime() === account.birthdate.getTime()); + + return { + account, + name: name ? 1 : 0, + note: note ? 1 : 0, + indenticalEmail: !!indenticalEmail, + emails: toInt(duplicateEmails), + origins: toInt(duplicateOrigins), + lastVisit: account.lastVisit || new Date(0), + browserId, + birthdate, + perma: isPermaBanned(account) || isPermaShadowed(account), + }; +} + +export function createDuplicateResult(account: Account, base: Account): DuplicateResult { + return { ...createDuplicate(account, base), account: account._id }; +} + +export function pushOrdered(items: T[], item: T, compare: (a: T, b: T) => number) { + for (let i = 0; i < items.length; i++) { + if (compare(items[i], item) >= 0) { + items.splice(i, 0, item); + return; + } + } + + items.push(item); +} + +export function duplicatesCollector(duplicates: string[]) { + const set = new Set(); + + return (item: string) => { + if (set.has(item)) { + duplicates.push(item); + } else { + set.add(item); + } + }; +} + +export function patreonSupporterLevel(account: AccountBase) { + return account.patreon! & 0xf; +} + +export function supporterLevel(account: AccountBase) { + const flags = account.supporter!; + const ignore = hasFlag(flags, SupporterFlags.IgnorePatreon); + const patreonSupporter = patreonSupporterLevel(account); + const flagsSupporter = flags & 0xf; + return Math.max(ignore ? 0 : patreonSupporter, flagsSupporter); +} + +export function isPastSupporter(account: AccountBase) { + const flags = account.supporter!; + return (hasFlag(flags, SupporterFlags.PastSupporter) || hasFlag(flags, SupporterFlags.ForcePastSupporter)) && + !hasFlag(flags, SupporterFlags.IgnorePastSupporter); +} + +const fieldToAction: { [key: string]: string | undefined; } = { + mute: 'Muted', + shadow: 'Shadowed', + ban: 'Banned', +}; + +export function banMessage(field: string, value: number) { + const action = fieldToAction[field] || 'Did'; + + if (value === 0) { + return `Un${action.toLowerCase()}`; + } else if (value === -1) { + return action; + } else { + return `${action} for (${moment.duration(value - Date.now()).humanize()})`; + } +} + +export function isActive(value: number | undefined): boolean { + return !!value && (value === -1 || value > Date.now()); +} + +export function isPerma(value: number | undefined): boolean { + return value === -1; +} + +export function isTemporarilyActive(value: number | undefined): boolean { + return !!value && value > Date.now(); +} + +export function isMuted(account: BannedMuted): boolean { + return isActive(account.mute); +} + +export function isShadowed(account: BannedMuted): boolean { + return isActive(account.shadow); +} + +export function isBanned(account: BannedMuted): boolean { + return isActive(account.ban); +} + +export function isPermaShadowed(account: BannedMuted): boolean { + return isPerma(account.shadow); +} + +export function isPermaBanned(account: BannedMuted): boolean { + return isPerma(account.ban); +} + +export function isTemporarilyBanned(account: BannedMuted): boolean { + return isTemporarilyActive(account.ban); +} + +export interface SupporterChange { + message: string; + date: Date; + icon: any; + class: string; +} + +export function createSupporterChanges(entries: LogEntry[]): SupporterChange[] { + const changes = entries.map(l => ({ + message: l.message, + level: +((/\d+/.exec(l.message) || ['0'])[0]), + added: /added/i.test(l.message), + date: new Date(l.date), + icon: /added/i.test(l.message) ? faPlusCircle : (/decline/i.test(l.message) ? faClock : faMinusCircle), + class: /added/i.test(l.message) ? 'text-success' : (/decline/i.test(l.message) ? 'text-warning' : 'text-danger'), + })); + + for (let i = 1; i < changes.length; i++) { + const prev = changes[i - 1]; + const current = changes[i]; + + if (current.date.getMonth() !== prev.date.getMonth()) { + current.class += ' border-left border-success pl-2'; + } + + if (current.added && prev.added) { + if (current.level > prev.level) { + current.icon = faCaretSquareUp; + current.class = 'text-info'; + } else if (current.level < prev.level) { + current.icon = faCaretSquareDown; + current.class = 'text-info'; + } + } + } + + return changes; +} + +export function getIdsFromNote(note: string | undefined) { + return note ? uniq(note.match(/[0-9a-f]{24}/g)) : []; +} + +export function addToMap(map: Map, key: string, item: T) { + const items = map.get(key); + + if (items) { + items.push(item); + } else { + map.set(key, [item]); + } +} + +export function removeFromMap(map: Map, key: string, item: T) { + const items = map.get(key); + + if (items) { + removeItem(items, item); + + if (items.length === 0) { + map.delete(key); + } + } +} + +export function parsePonies(ponies: string, filterIds?: string[]) { + return compact(ponies + .split(/\n\r?/g) + .map(x => /\[system\] removed pony \[([a-f0-9]{24})\] "(.+)" (\S+)/.exec(x))) + .map(([_, id, name, info]) => ({ id, name, info })) + .filter(({ id }) => !filterIds || includes(filterIds, id)); +} + +export function createIdStore() { + const idsMap = new Map(); + + return (id: string) => { + const result = idsMap.get(id); + + if (result) { + return result; + } else { + idsMap.set(id, id); + return id; + } + }; +} + +export function getTranslationUrl(text: string) { + return `https://translate.google.com/#view=home&op=translate&sl=auto&tl=en&text=${encodeURIComponent(text)}`; + // return `https://translate.google.com/#auto/en/${encodeURIComponent(text)}`; +} diff --git a/src/ts/common/animationPlayer.ts b/src/ts/common/animationPlayer.ts new file mode 100644 index 0000000..a87c7c1 --- /dev/null +++ b/src/ts/common/animationPlayer.ts @@ -0,0 +1,135 @@ +import { sample } from 'lodash'; +import { Sprite, Palette, PaletteSpriteBatch } from './interfaces'; +import { drawSpriteCropped } from '../graphics/graphicsUtils'; +import { includes } from './utils'; +import { WHITE } from './colors'; + +const enum AnimationPhase { + Starting, + Playing, + Ending, +} + +export interface SpriteAnimation { + loop: boolean; + start: number; + middle: number; + end: number; + fps: number; + palette: Uint32Array; + frames: Sprite[]; + flipFrames?: Sprite[]; +} + +export interface AnimationPlayer { + nextAnimation: SpriteAnimation | undefined; + currentAnimation: SpriteAnimation | undefined; + time: number; + frame: number; + phase: AnimationPhase; + dirty: boolean; + palette: Palette; +} + +export function createAnimationPlayer(palette: Palette): AnimationPlayer { + return { + nextAnimation: undefined, + currentAnimation: undefined, + time: 0, + frame: 0, + phase: AnimationPhase.Starting, + dirty: true, + palette, + }; +} + +export function isAnimationPlaying(player: AnimationPlayer) { + return player.currentAnimation !== undefined; +} + +export function playOneOfAnimations(player: AnimationPlayer, animations: SpriteAnimation[]) { + if (player.phase === AnimationPhase.Ending || !includes(animations, player.currentAnimation)) { + playAnimation(player, sample(animations)); + } +} + +export function playAnimation(player: AnimationPlayer, animation: SpriteAnimation | undefined) { + if (player.currentAnimation !== animation) { + if (player.currentAnimation) { + if (player.nextAnimation !== animation || player.phase !== AnimationPhase.Ending) { + player.nextAnimation = animation; + player.time = (player.frame + 1) / player.currentAnimation.fps; + player.phase = AnimationPhase.Ending; + } + } else { + player.currentAnimation = animation; + player.time = 0; + player.phase = AnimationPhase.Starting; + } + player.dirty = true; + } else if (player.phase === AnimationPhase.Ending) { + player.nextAnimation = animation; + player.dirty = true; + } +} + +export function updateAnimation(player: AnimationPlayer, delta: number) { + if (player.currentAnimation !== undefined) { + player.time += delta; + const { start, middle, end, fps, loop } = player.currentAnimation; + + let extraFrame = Math.floor(player.time * fps); + + if (player.phase === AnimationPhase.Starting && extraFrame > start) { + player.phase = loop ? AnimationPhase.Playing : AnimationPhase.Ending; + player.dirty = true; + } + + if (player.phase === AnimationPhase.Playing) { + extraFrame = start + ((extraFrame - start) % middle); + } + + if (player.phase === AnimationPhase.Ending && extraFrame > (start + middle + end)) { + player.currentAnimation = undefined; + player.dirty = true; + + if (player.nextAnimation !== undefined) { + const nextAnimation = player.nextAnimation; + player.nextAnimation = undefined; + playAnimation(player, nextAnimation); + } + } + + if (player.frame !== extraFrame) { + player.frame = extraFrame; + player.dirty = true; + } + } +} + +export function drawAnimation( + batch: PaletteSpriteBatch, player: AnimationPlayer, x: number, y: number, color = WHITE, flip = false, maxY = 0 +) { + const animation = player.currentAnimation; + + if (animation !== undefined) { + const frames = (flip && animation.flipFrames) ? animation.flipFrames : animation.frames; + + if (player.frame < frames.length) { + const frame = frames[player.frame]; + + if (DEVELOPMENT && !frame) { + throw new Error('Undefined frame in sprite animation'); + } + + if (!frame) // TEMP + return; + + if (maxY === 0) { + batch.drawSprite(frame, color, player.palette, x, y); + } else { + drawSpriteCropped(batch, frame, color, player.palette, x, y, maxY); + } + } + } +} diff --git a/src/ts/common/animator.ts b/src/ts/common/animator.ts new file mode 100644 index 0000000..c5580f7 --- /dev/null +++ b/src/ts/common/animator.ts @@ -0,0 +1,183 @@ +export interface Animation { + fps: number; + loop: boolean; + frames: any[]; +} + +export interface AnimatorTransition { + state: AnimatorState; + exitAfter?: number; + enterTime?: number; + keepTime?: boolean; + onlyDirectTo?: AnimatorState; +} + +export interface AnimatorState { + name: string; + animation: T; + variants: { [key: string]: T; }; + from: AnimatorTransition[]; +} + +export function animatorState( + name: string, animation: T, variants: { [key: string]: T; } = {} +): AnimatorState { + return { name, animation, variants, from: [] }; +} + +export function animatorTransition( + from: AnimatorState, to: AnimatorState, options: Partial> = {} +) { + to.from.push({ state: from, ...options }); +} + +export const anyState = animatorState('any', { fps: 1, loop: false, frames: [] }); + +export interface Animator { + state: AnimatorState | undefined; + target: AnimatorState | undefined; + next: AnimatorTransition | undefined; + time: number; + variant: string; +} + +export function createAnimator(): Animator { + return { + time: 0, + variant: '', + state: undefined, + target: undefined, + next: undefined, + }; +} + +export function getAnimation(animator: Animator) { + return animator.state && getAnimationForState(animator.state, animator.variant); +} + +export function getAnimationFrame(animator: Animator) { + const animation = getAnimation(animator); + return animation ? Math.floor(animator.time * animation.fps) % animation.frames.length : 0; +} + +export function resetAnimatorState(animator: Animator) { + animator.state = undefined; + animator.target = undefined; + animator.next = undefined; +} + +export function setAnimatorState(animator: Animator, state: AnimatorState) { + if (animator.target !== state) { + if (animator.state !== state) { + if (animator.state === undefined) { + animator.state = state; + } else { + animator.target = state; + } + } else { + animator.target = undefined; + } + + animator.next = undefined; + } +} + +export function updateAnimator(animator: Animator, delta: number) { + const time = animator.time; + animator.time += delta; + + if (animator.target !== undefined && animator.state !== undefined && animator.state !== animator.target) { + const animation = getAnimationForState(animator.state, animator.variant); + const animationLength = animation.frames.length / animation.fps; + const frameBefore = Math.floor(time / animationLength); + const frameAfter = Math.floor(animator.time / animationLength); + const frameTimeAfter = (animator.time % animationLength) / animationLength; + + let animationEnded = frameBefore !== frameAfter; + let switched = false; + + do { + switched = false; + const transition = animator.next = animator.next || findTransition(animator.state, animator.target); + + if (transition !== undefined) { + const exitAfter = transition.exitAfter === undefined ? 1 : transition.exitAfter; + + if (frameTimeAfter >= exitAfter || animationEnded) { + if (!transition.keepTime) { + animator.time = transition.enterTime || 0; + } else { + animator.time = animator.time % animationLength; + } + + setCurrentState(animator, transition.state); + + switched = true; + animationEnded = false; + } + } + } while (switched && animator.target); + } +} + +function setCurrentState(animator: Animator, state: AnimatorState) { + animator.next = undefined; + animator.state = state; + + if (state === animator.target) { + animator.target = undefined; + } +} + +function getAnimationForState(state: AnimatorState, variant: string) { + return state.variants[variant] || state.animation; +} + +function findTransition( + current: AnimatorState, target: AnimatorState +): AnimatorTransition | undefined { + return findTransMinMax(current, target, 0, 1) + || findTrans(anyState, target, target, 0, 0, []) + || findTransMinMax(current, target, 2, 10); +} + +function findTransMinMax( + current: AnimatorState, target: AnimatorState, min: number, max: number +): AnimatorTransition | undefined { + for (let i = min; i <= max; i++) { + const trans = findTrans(current, target, target, 0, i, [current]); + + if (trans !== undefined) { + return trans; + } + } + + return undefined; +} + +function findTrans( + current: AnimatorState, target: AnimatorState, finalTarget: AnimatorState, + depth: number, maxDepth: number, done: AnimatorState[] +): AnimatorTransition | undefined { + if (done.indexOf(target) === -1) { + done.push(target); + + for (const from of target.from) { + if (from.state === current && (from.onlyDirectTo === undefined || from.onlyDirectTo === finalTarget)) { + return { ...from, state: target }; + } + } + + if (depth < maxDepth) { + for (const from of target.from) { + const trans = findTrans(current, from.state, finalTarget, depth + 1, maxDepth, done); + + if (trans !== undefined) { + return trans; + } + } + } + } + + return undefined; +} diff --git a/src/ts/common/binaryUtils.ts b/src/ts/common/binaryUtils.ts new file mode 100644 index 0000000..ef5ff94 --- /dev/null +++ b/src/ts/common/binaryUtils.ts @@ -0,0 +1,20 @@ +import { BinaryWriter, getWriterBuffer, createBinaryWriter, resizeWriter } from 'ag-sockets/dist/browser'; + +export function writeBinary(write: (writer: BinaryWriter) => void): Uint8Array { + const writer = createBinaryWriter(); + + do { + try { + write(writer); + break; + } catch (e) { + if (e instanceof RangeError || /DataView/.test(e.message)) { + resizeWriter(writer); + } else { + throw e; + } + } + } while (true); + + return getWriterBuffer(writer); +} diff --git a/src/ts/common/bitUtils.ts b/src/ts/common/bitUtils.ts new file mode 100644 index 0000000..c810e45 --- /dev/null +++ b/src/ts/common/bitUtils.ts @@ -0,0 +1,114 @@ +export type WriteBits = (value: number, bits: number) => void; +export type ReadBits = (bits: number) => number; + +export function numberToBitCount(value: number) { + value = value >>> 0; + + for (let mask = 0xffffffff >>> 0, bits = 0; mask; mask = (mask << 1) >>> 0, bits++) { + if ((value & mask) === 0) { + return bits; + } + } + + return 32; +} + +export function countBits(value: number) { + value = value >>> 0; + + let bits = 0; + + while (value) { + bits += value & 1; + value = value >>> 1; + } + + return bits; +} + +export function bitWriter(writes: (writer: WriteBits) => void): Uint8Array { + let buffer = new Uint8Array(16); + let length = 0; + let byte = 0; + let byteBits = 0; + + function writeByte(value: number) { + if (buffer.length <= length) { + const newBuffer = new Uint8Array(buffer.length * 2); + newBuffer.set(buffer); + buffer = newBuffer; + } + + buffer[length] = value; + length++; + } + + writes((value, bits) => { + if (bits < 0 || bits > 32) { + throw new Error('Invalid bit count'); + } + + while (bits) { + const revByteBits = 8 - byteBits; + const writeBits = revByteBits < bits ? revByteBits : bits; + const write = (value >> (bits - writeBits)) & (0xff >> (8 - writeBits)); + byte |= write << (revByteBits - writeBits); + byteBits += writeBits; + bits -= writeBits; + + if (byteBits === 8) { + writeByte(byte); + byte = 0; + byteBits = 0; + } + } + }); + + if (byteBits) { + writeByte(byte); + byteBits = 0; + byte = 0; + } + + return buffer.subarray(0, length); +} + +export function bitReader(buffer: Uint8Array): ReadBits { + let offset = 0; + + return bitReaderCustom(() => { + if (buffer.length <= offset) { + throw new Error('Reading past end'); + } + + return buffer[offset++]; + }); +} + +export function bitReaderCustom(readByte: () => number): ReadBits { + let byte = 0; + let byteBits = 0; + + return bits => { + if (bits < 0 || bits > 32) { + throw new Error('Invalid bit count'); + } + + let result = 0; + + while (bits) { + if (!byteBits) { + byte = readByte(); + byteBits = 8; + } + + const readBits = byteBits < bits ? byteBits : bits; + const read = (byte >> (byteBits - readBits)) & (0xff >> (8 - readBits)); + result = (result << readBits) | read; + bits -= readBits; + byteBits -= readBits; + } + + return result >>> 0; + }; +} diff --git a/src/ts/common/camera.ts b/src/ts/common/camera.ts new file mode 100644 index 0000000..a238548 --- /dev/null +++ b/src/ts/common/camera.ts @@ -0,0 +1,132 @@ +import { Entity, Rect, Point, Size, Camera } from './interfaces'; +import { CAMERA_WIDTH_MAX, CAMERA_WIDTH_MIN, CAMERA_HEIGHT_MAX, CAMERA_HEIGHT_MIN } from './constants'; +import { clamp, intersect, pointInXYWH, pointInRect, lerp } from './utils'; +import { toScreenX, toScreenY, toWorldX, toWorldY } from './positionUtils'; +import { getChatBallonXY } from '../graphics/graphicsUtils'; + +const cameraPadding = 0.3; +export const characterHeight = 25; + +export function createCamera(): Camera { + return { + x: 0, + y: 0, + w: 100, + h: 100, + offset: 0, + shift: 0, + shiftTarget: 0, + shiftRatio: 0, + actualY: 0, + }; +} + +export function setupCamera(camera: Camera, x: number, y: number, width: number, height: number, map: Size) { + camera.w = clamp(width, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX); + camera.h = clamp(height, CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX); + camera.x = clamp(x, 0, toScreenX(map.width) - camera.w); + camera.y = clamp(y, 0, toScreenY(map.height) - camera.h); +} + +export function updateCamera(camera: Camera, player: Point, map: Size) { + const cameraWith = camera.w; + const cameraHeight = camera.h; + const cameraHeightShifted = Math.ceil(camera.h - camera.offset); + + const playerX = toScreenX(player.x); + const playerY = toScreenY(player.y); + + const mapWidth = toScreenX(map.width); + const mapHeight = toScreenY(map.height); + + const minX = Math.min(0, (mapWidth - cameraWith) / 2); + const minY = Math.min(0, (mapHeight - cameraHeight) / 2); + const minYShifted = Math.min(0, (mapHeight - cameraHeightShifted) / 2); + const maxX = Math.max(mapWidth - cameraWith, minX); + const maxY = Math.max(mapHeight - cameraHeight, minY); + const maxYShifted = Math.max(mapHeight - cameraHeightShifted, minY); + + const hSpace = Math.floor(cameraWith * cameraPadding); + const vSpace = Math.floor(cameraHeight * cameraPadding); + const vSpaceShifted = Math.floor(cameraHeightShifted * cameraPadding); + + const hPad = (cameraWith - hSpace) / 2; + const vPad = (cameraHeight - vSpace) / 2; + const vPadShifted = (cameraHeightShifted - vSpaceShifted) / 2; + + const minCamX = clamp(playerX - (hSpace + hPad), minX, maxX); + const maxCamX = clamp(playerX - hPad, minX, maxX); + const minCamY = clamp(playerY - (vSpace + vPad) - characterHeight, minY, maxY); + const maxCamY = clamp(playerY - vPad - characterHeight, minY, maxY); + const minCamYShifted = clamp(playerY - (vSpaceShifted + vPadShifted) - characterHeight, minYShifted, maxYShifted); + const maxCamYShifted = clamp(playerY - vPadShifted - characterHeight, minYShifted, maxYShifted); + + camera.x = Math.floor(clamp(camera.x, minCamX, maxCamX)); + camera.y = Math.floor(clamp(camera.y, minCamY, maxCamY)); + camera.shiftTarget = Math.floor(clamp(camera.shiftTarget, minCamYShifted, maxCamYShifted)); + camera.actualY = calculateCameraY(camera); +} + +export function centerCameraOn(camera: Camera, point: Point) { + camera.x = Math.floor(toScreenX(point.x) - camera.w / 2); + camera.y = Math.floor((toScreenY(point.y) - camera.h / 2) - characterHeight); + camera.shiftTarget = Math.floor((toScreenY(point.y) - Math.ceil(camera.h - camera.offset) / 2) - characterHeight); +} + +export function calculateCameraY(camera: Camera) { + return Math.round(lerp(camera.y, camera.shiftTarget - camera.offset, camera.shiftRatio)); +} + +export function isWorldPointVisible(camera: Camera, point: Point): boolean { + return pointInRect(toScreenX(point.x), toScreenY(point.y), camera); +} + +export function isWorldPointWithPaddingVisible(camera: Camera, point: Point, padding: number): boolean { + return pointInXYWH( + toScreenX(point.x), toScreenY(point.y), + camera.x - padding, camera.actualY - padding, camera.w + 2 * padding, camera.h + 2 * padding); +} + +export function isAreaVisible(camera: Camera, x: number, y: number, w: number, h: number): boolean { + return intersect(camera.x, camera.actualY, camera.w, camera.h, x, y, w, h); +} + +export function isRectVisible(camera: Camera, rect: Rect): boolean { + return intersect(camera.x, camera.actualY, camera.w, camera.h, rect.x, rect.y, rect.w, rect.h); +} + +export function isBoundsVisible(camera: Camera, bounds: Rect | undefined, x: number, y: number): boolean { + return bounds !== undefined && + isAreaVisible(camera, toScreenX(x) + bounds.x, toScreenY(y) + bounds.y, bounds.w, bounds.h); +} + +export function isEntityVisible(camera: Camera, entity: Entity): boolean { + return isBoundsVisible(camera, entity.bounds, entity.x, entity.y); +} + +function isChatBaloonAboveScreenTop(camera: Camera, entity: Entity) { + return getChatBallonXY(entity, camera).y <= -5; +} + +export function isChatVisible(camera: Camera, entity: Entity): boolean { + return isBoundsVisible(camera, entity.bounds, entity.x, entity.y) + && !isChatBaloonAboveScreenTop(camera, entity); +} + +export function screenToWorld(camera: Camera, point: Point): Point { + return { + x: toWorldX(point.x + camera.x), + y: toWorldY(point.y + camera.actualY), + }; +} + +export function worldToScreen(camera: Camera, point: Point): Point { + return { + x: Math.floor(toScreenX(point.x) - camera.x), + y: Math.floor(toScreenY(point.y) - camera.actualY), + }; +} + +// export function mapDepth(camera: Camera, y: number): number { +// return (toScreenY(y) - camera.actualY) - camera.maxDepth; +// } diff --git a/src/ts/common/collision.ts b/src/ts/common/collision.ts new file mode 100644 index 0000000..02cea72 --- /dev/null +++ b/src/ts/common/collision.ts @@ -0,0 +1,306 @@ +import { Entity, IMap, Region, EntityFlags } from './interfaces'; +import { clamp } from './utils'; +import { toWorldX, toWorldY } from './positionUtils'; +import { getRegionGlobal, isInWaterAt } from './worldMap'; +import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants'; +import { isInTheAir, isFlying } from './entityUtils'; + +let isCollidingCount = 0; +let isCollidingObjectCount = 0; + +export function getCollisionStats() { + const stats = { isCollidingCount, isCollidingObjectCount }; + isCollidingCount = 0; + isCollidingObjectCount = 0; + return stats; +} + +export function isOutsideMap(x: number, y: number, map: IMap): boolean { + return x < 0 || y < 0 || x >= map.width || y >= map.height; +} + +export function canCollideWith(entity: Entity): boolean { + return (entity.flags & EntityFlags.CanCollideWith) !== 0; +} + +export function isStaticCollision(entity: Entity, map: IMap, forceOnGround = false) { + if (DEVELOPMENT && entity.type !== PONY_TYPE) { + console.error(`isStaticCollision: non-pony entity`); + } + + const flying = !forceOnGround && isInTheAir(entity); + return isPonyColliding(entity.x, entity.y, map as any, flying); +} + +export function fixCollision(entity: Entity, map: IMap) { + if (DEVELOPMENT && entity.type !== PONY_TYPE) { + console.error(`fixCollision: non-pony entity`); + } + + const flying = isInTheAir(entity); + + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + const tx = entity.x + x; + const ty = entity.y + y; + + if (!isPonyColliding(tx, ty, map as any, flying)) { + entity.x += x; + entity.y += y; + return true; + } + } + } + + return false; +} + +function isPonyColliding(x: number, y: number, map: IMap, flying: boolean): boolean { + if (isOutsideMap(x, y, map)) { + return true; + } + + const region = getRegionGlobal(map, x, y); + + if (region === undefined) { + return true; + } + + const rx = clamp(Math.floor((x - region.x * REGION_SIZE) * tileWidth), 0, REGION_WIDTH); + const ry = clamp(Math.floor((y - region.y * REGION_SIZE) * tileHeight), 0, REGION_HEIGHT); + const pixel = region.collider[rx + ry * REGION_WIDTH]; + const mask = flying ? 2 : 1; + + return (pixel & mask) !== 0; +} + +function isColliding(x: number, y: number, mask: number, map: IMap) { + if (x < 0 || x >= (map.width * tileWidth) || y < 0 || y >= (map.height * tileHeight)) { + return true; + } else { + const regionX = (x / REGION_WIDTH) | 0; + const regionY = (y / REGION_HEIGHT) | 0; + const region = map.regions[regionX + regionY * map.regionsX]; + + if (region === undefined) { + return true; + } else { + const insideX = (x % REGION_WIDTH) | 0; + const insideY = (y % REGION_HEIGHT) | 0; + return (region.collider[insideX + insideY * REGION_WIDTH] & mask) !== 0; + } + } +} + +export function updatePosition(entity: Entity, delta: number, map: IMap) { + const ex = entity.x; + const ey = entity.y; + const speed = (!isFlying(entity) && isInWaterAt(map, ex, ey)) ? 0.5 : 1.0; + const destX = ex + entity.vx * speed * delta; + const destY = ey + entity.vy * speed * delta; + + if ((entity.flags & EntityFlags.CanCollide) === 0) { + entity.x = destX; + entity.y = destY; + return; + } + + if (DEVELOPMENT && entity.type !== PONY_TYPE) { + console.error(`updatePosition: non-pony entity`); + } + + const flying = isInTheAir(entity); + const mask = flying ? 2 : 1; + + const srcX = ex * tileWidth; + const srcY = ey * tileHeight; + let dstX = destX * tileWidth; + let dstY = destY * tileHeight; + + const x0 = Math.floor(srcX) | 0; + const y0 = Math.floor(srcY) | 0; + const x1 = Math.floor(dstX) | 0; + const y1 = Math.floor(dstY) | 0; + + const minX = Math.min(x0, x1) | 0; + const maxX = Math.max(x0, x1) | 0; + const minY = Math.min(y0, y1) | 0; + const maxY = Math.max(y0, y1) | 0; + + let x = x0 | 0; + let y = y0 | 0; + + let actualX = x | 0; + let actualY = y | 0; + + if (isColliding(actualX, actualY, mask, map)) { + if (!isOutsideMap(destX, destY, map)) { + entity.x = destX; + entity.y = destY; + } + + return; + } + + const a = (dstY - srcY) / (dstX - srcX); + const b = srcY - a * srcX; + const useGt = srcY < dstY; + + let stepXT = 0 | 0, stepYT = 0 | 0; + let stepXF = 0 | 0, stepYF = 0 | 0; + let ox = 0, oy = 0; + + const shiftRight = srcX <= dstX; + const shiftLeft = srcX >= dstX; + const shiftUp = srcY >= dstY; + const shiftDown = srcY <= dstY; + const horizontalOrVertical = srcX === dstX || srcY === dstY; + + if (srcX < dstX) { + if (srcY < dstY) { + ox = 1; + oy = 1; + stepYT = 1 | 0; + stepXF = 1 | 0; + } else { + ox = 1; + stepYT = -1 | 0; + stepXF = 1 | 0; + } + } else if (srcX > dstX) { + if (srcY < dstY) { + oy = 1; + stepYT = 1 | 0; + stepXF = -1 | 0; + } else { + stepYT = -1 | 0; + stepXF = -1 | 0; + } + } else { + if (srcY < dstY) { + stepYF = stepYT = 1 | 0; + } else { + stepYF = stepYT = -1 | 0; + } + } + + let steps = 1000; + + for (; steps; steps--) { + const fx = a * (x + ox) + b; + const fy = y + oy; + + let tx = 0 | 0; + let ty = 0 | 0; + + if (useGt ? (fx > fy) : (fx < fy)) { + tx = (tx + stepXT) | 0; + ty = (ty + stepYT) | 0; + } else { + tx = (tx + stepXF) | 0; + ty = (ty + stepYF) | 0; + } + + x = (x + tx) | 0; + y = (y + ty) | 0; + + if (x < minX || x > maxX || y < minY || y > maxY) { + break; + } + + let actualNX = (actualX + tx) | 0; + let actualNY = (actualY + ty) | 0; + let collides = isColliding(actualNX, actualNY, mask, map); + let canMove = false; + + if (collides) { + if (tx !== 0) { + let canShiftUp = false; + let canShiftDown = false; + + if ( + shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1, mask, map)) && + !isColliding(actualNX, actualY - 1, mask, map) + ) { + actualNX = actualX; + actualNY -= 1; + dstY -= 1; + collides = false; + } else if ( + shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1, mask, map)) && + !isColliding(actualNX, actualY + 1, mask, map) + ) { + actualNX = actualX; + actualNY += 1; + dstY += 1; + collides = false; + } else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2, mask, map)) { + actualNX = actualX; + actualNY -= 1; + dstY -= 1; + collides = false; + } else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2, mask, map)) { + actualNX = actualX; + actualNY += 1; + dstY += 1; + collides = false; + } + + canMove = canShiftUp || canShiftDown; + } else { + let canShiftLeft = false; + let canShiftRight = false; + + if ( + shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY, mask, map)) && + !isColliding(actualX - 1, actualNY, mask, map) + ) { + actualNX -= 1; + actualNY = actualY; + dstX -= 1; + collides = false; + } else if ( + shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY, mask, map)) && + !isColliding(actualX + 1, actualNY, mask, map) + ) { + actualNX += 1; + actualNY = actualY; + dstX += 1; + collides = false; + } else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY, mask, map)) { + actualNX -= 1; + actualNY = actualY; + dstX -= 1; + collides = false; + } else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY, mask, map)) { + actualNX += 1; + actualNY = actualY; + dstX += 1; + collides = false; + } + + canMove = canShiftLeft || canShiftRight; + } + } + + if (!collides) { + actualX = actualNX; + actualY = actualNY; + } else if (!canMove || horizontalOrVertical) { + break; + } + } + + const epsilon = 1 / 1024; + const left = Math.min(x0, actualX); + const right = Math.max(x0 + 1, actualX + 1) - epsilon; + const top = Math.min(y0, actualY); + const bottom = Math.max(y0 + 1, actualY + 1) - epsilon; + + entity.x = toWorldX(clamp(dstX, left, right)); + entity.y = toWorldY(clamp(dstY, top, bottom)); + + if (DEVELOPMENT && steps <= 0) { + console.error('Overflow collision steps'); + } +} diff --git a/src/ts/common/color.ts b/src/ts/common/color.ts new file mode 100644 index 0000000..f6b7040 --- /dev/null +++ b/src/ts/common/color.ts @@ -0,0 +1,495 @@ +import { isString } from 'lodash'; +import { clamp } from './utils'; + +export const colorNames: { [key: string]: string | undefined } = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' +}; + +const TRANSPARENT = 0x00000000 >>> 0; +const BLACK = 0x000000ff >>> 0; + +export interface HSVA { + h: number; + s: number; + v: number; + a: number; +} + +export interface RGB { + r: number; + g: number; + b: number; +} + +export interface RGBA extends RGB { + a: number; +} + +export function getR(color: number) { + return (color >> 24) & 0xff; +} + +export function getG(color: number) { + return (color >> 16) & 0xff; +} + +export function getB(color: number) { + return (color >> 8) & 0xff; +} + +export function getAlpha(color: number) { + return color & 0xff; +} + +export function withAlpha(color: number, alpha: number) { + return (color & 0xffffff00) | (alpha & 0xff); +} + +export function withAlphaFloat(color: number, alpha: number) { + return (color & 0xffffff00) | ((alpha * 255) & 0xff); +} + +// to + +export function colorToRGBA(color: number): RGBA { + return { + r: getR(color), + g: getG(color), + b: getB(color), + a: getAlpha(color), + }; +} + +export function colorToHSVA(color: number, h?: number): HSVA { + return rgb2hsv(getR(color), getG(color), getB(color), getAlpha(color) / 255, h); +} + +export function colorToCSS(color: number): string { + const alpha = getAlpha(color); + + if (alpha === 0xff) { + return `#${colorToHexRGB(color)}`; + } else { + return `rgba(${getR(color)},${getG(color)},${getB(color)},${alpha / 255})`; + } +} + +function toHex(value: number, length: number): string { + return value.toString(16).padStart(length, '0'); +} + +export function colorToHexRGB(color: number) { + return toHex(color >>> 8, 6); +} + +export function colorToFloatArray(color: number): Float32Array { + const result = new Float32Array(4); + colorToExistingFloatArray(result, color); + return result; +} + +export function colorToExistingFloatArray(array: Float32Array, color: number) { + array[0] = getR(color) / 255; + array[1] = getG(color) / 255; + array[2] = getB(color) / 255; + array[3] = getAlpha(color) / 255; +} + +const int8 = new Int8Array(4); +const int32 = new Int32Array(int8.buffer, 0, 1); +const float32 = new Float32Array(int8.buffer, 0, 1); + +export function colorToFloat(color: number): number { + const int = (getAlpha(color) << 24) | (getB(color) << 16) | (getG(color) << 8) | getR(color); + int32[0] = int & 0xfeffffff; + return float32[0]; +} + +export function colorToFloatAlpha(color: number, alpha: number /* 0-1 */): number { + const int = (((getAlpha(color) * alpha) & 0xff) << 24) | (getB(color) << 16) | (getG(color) << 8) | getR(color); + int32[0] = int & 0xfeffffff; + return float32[0]; +} + +// from + +export function colorFromRGBA(r: number, g: number, b: number, a: number /* 0-255 */) { + return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0; +} + +export function colorFromHSVA(h: number, s: number, v: number, a: number /* 0-1 */) { + const { r, g, b } = hsv2rgb(h, s, v); + return colorFromRGBA(r, g, b, a * 255); +} + +export function colorFromHSVAObject({ h, s, v, a }: HSVA) { + return colorFromHSVA(h, s, v, a); +} + +// parse + +export function parseColorFast(str: string): number { + if (!isString(str)) + return TRANSPARENT; + + const int = parseInt(str, 16); + + if (str.length !== 6 || isNaN(int) || int < 0) { + return parseColorWithAlpha(str, 1); + } else { + return (((int << 8) | 0xff) >>> 0); + } +} + +export function parseColor(str: string): number { + if (!isString(str)) + return TRANSPARENT; + + str = str.trim().toLowerCase(); + + if (str === '' || str === 'none' || str === 'transparent') + return TRANSPARENT; + + str = colorNames[str] || str; + + const m = /(\d+)[ ,]+(\d+)[ ,]+(\d+)(?:[ ,]+(\d*\.?\d+))?/.exec(str); + + if (m) { + return colorFromRGBA( + parseInt(m[1], 10), + parseInt(m[2], 10), + parseInt(m[3], 10), + m[4] ? parseFloat(m[4]) * 255 : 255); + } + + const n = /[0-9a-f]+/i.exec(str); + + if (n) { + const s = n[0]; + + if (s.length === 3) { + return colorFromRGBA( + parseInt(s.charAt(0), 16) * 0x11, + parseInt(s.charAt(1), 16) * 0x11, + parseInt(s.charAt(2), 16) * 0x11, 255); + } else { + return colorFromRGBA( + parseInt(s.substr(0, 2), 16), + parseInt(s.substr(2, 2), 16), + parseInt(s.substr(4, 2), 16), + s.length >= 8 ? parseInt(s.substr(6, 2), 16) : 255); + } + } + + return BLACK; +} + +export function parseColorWithAlpha(str: string, alpha: number /* 0-1 */): number { + return ((parseColor(str) & 0xffffff00) | ((alpha * 255) & 0xff)) >>> 0; +} + +// utils + +export function toGrayscale(color: number) { + const c = Math.round(clamp(getR(color) * 0.2126 + getG(color) * 0.7152 + getB(color) * 0.0722, 0, 255)) | 0; + const a = getAlpha(color); + return colorFromRGBA(c, c, c, a); +} + +export function makeTransparent(color: number, factor: number /* 0-1 */): number { + return ((color & 0xffffff00) | ((getAlpha(color) * factor) & 0xff)) >>> 0; +} + +export function multiplyColor(color: number, factor: number /* 0-1 */): number { + return colorFromRGBA( + clamp(getR(color) * factor, 0, 255), + clamp(getG(color) * factor, 0, 255), + clamp(getB(color) * factor, 0, 255), + getAlpha(color) + ); +} + +export function lerpColors(a: number, b: number, factor: number): number { + const f = factor; + const t = 1 - factor; + + return colorFromRGBA( + getR(a) * t + getR(b) * f, + getG(a) * t + getG(b) * f, + getB(a) * t + getB(b) * f, + getAlpha(a) * t + getAlpha(b) * f + ); +} + +/// r, g, b = <0, 255>, a = <0, 1> +export function rgb2hsv(r: number, g: number, b: number, a: number /* 0-1 */, h = 0): HSVA { + r = r / 255; + g = g / 255; + b = b / 255; + h = h / 360; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const v = max; + const d = max - min; + const s = max === 0 ? 0 : d / max; + + if (max !== min) { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return { h: h * 360, s, v, a }; +} + +/// h = <0, 360>; s, v = <0, 1> +export function hsv2rgb(h: number, s: number, v: number): RGB { + h = Math.max(0, Math.min(360, h === 360 ? 0 : h)); + s = Math.max(0, Math.min(1, s)); + v = Math.max(0, Math.min(1, v)); + + let r = v; + let g = v; + let b = v; + + if (s !== 0) { + h /= 60; + + const i = Math.floor(h); + const f = h - i; + const p = v * (1 - s); + const q = v * (1 - s * f); + const t = v * (1 - s * (1 - f)); + + switch (i) { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + default: + r = v; + g = p; + b = q; + } + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255), + }; +} + +export function h2rgb(h: number): RGB { + h /= 60; + let r = 0, g = 0, b = 0; + const i = Math.floor(h); + const f = h - i; + const q = (1 - f); + const t = (1 - (1 - f)); + + switch (i) { + case 0: + r = 1; + g = t; + break; + case 1: + r = q; + g = 1; + break; + case 2: + g = 1; + b = t; + break; + case 3: + g = q; + b = 1; + break; + case 4: + r = t; + b = 1; + break; + default: + r = 1; + b = q; + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +} diff --git a/src/ts/common/colors.ts b/src/ts/common/colors.ts new file mode 100644 index 0000000..1e2087d --- /dev/null +++ b/src/ts/common/colors.ts @@ -0,0 +1,173 @@ +import { MessageType, Season, TileType } from './interfaces'; +import { colorFromHSVA, colorToHexRGB, parseColorFast, colorToHSVA, withAlphaFloat } from './color'; +import { invalidEnum, invalidEnumReturn } from './utils'; +import { darkenForOutline } from './ponyInfo'; + +// basic + +export const TRANSPARENT = 0; +export const WHITE = 0xffffffff; +export const BLACK = 0x000000ff; +export const ORANGE = 0xffa500ff; +export const BLUE = 0x0000ffff; +export const GREEN = 0x00ff00ff; +export const YELLOW = 0xffff00ff; +export const MAGENTA = 0xff00ffff; +export const CYAN = 0x00ffffff; +export const GRAY = 0x444444ff; +export const RED = 0xff0000ff; +export const HOTPINK = 0xff69b4ff; +export const PURPLE = 0x800080ff; + +// messages + +export const BG_COLOR = 0x333333ff; +export const ADMIN_COLOR = 0xff69b4ff; +export const MOD_COLOR = 0xb689ffff; +export const SYSTEM_COLOR = 0xbbbbbbff; +export const MESSAGE_COLOR = 0x333333ff; +export const ANNOUNCEMENT_COLOR = 0xf0e68Cff; +export const PARTY_COLOR = 0x71daffff; +export const THINKING_COLOR = 0xafafafff; +export const PARTY_THINKING_COLOR = 0x5da9c4ff; +export const OUTLINE_COLOR = withAlphaFloat(BLACK, 0.4); +export const PATREON_COLOR = 0xf86754ff; +export const WHISPER_COLOR = 0xffa1dfff; +export const FRIENDS_COLOR = 0x71ff7fff; + +export const SUPPORTER1_COLOR = PATREON_COLOR; +export const SUPPORTER2_COLOR = 0xffa32bff; +export const SUPPORTER3_COLOR = 0xffcf00ff; + +export const SUPPORTER2_BANDS = [0xffdfc1ff, 0xffcd99ff, 0xff9f3bff, 0xd97e09ff]; +export const SUPPORTER3_BANDS = [0xffffffff, 0xfffda4ff, 0xffea3bff, 0xfdbb0bff]; + +// game + +export const SHADOW_COLOR = withAlphaFloat(BLACK, 0.3); +export const CLOUD_SHADOW_COLOR = withAlphaFloat(BLACK, 0.2); +export const SHINES_COLOR = withAlphaFloat(WHITE, 0.4); +export const FAR_COLOR = colorFromHSVA(0, 0, 0.8, 1); +export const GRASS_COLOR = 0x90ee90ff; +export const HEARTS_COLOR = 0xf15f9dff; + +export const CAVE_LIGHT = 0x090c21ff; // 0x253f76ff; +export const CAVE_SHADOW = 0x00000055; + +export let ACTION_EXPRESSION_BG = '#e7aa4e'; +export const ACTION_EXPRESSION_EYE_COLOR = '#b17a00'; +export const ACTION_ACTION_BG = '#dc9d82'; +export const ACTION_ACTION_COAT_COLOR = '#d9835e'; +export const ACTION_COMMAND_BG = '#5fb7b3'; +export const ACTION_ITEM_BG = '#cecf59'; +export const ENTITY_ITEM_BG = '#dc76bc'; + +export const MAGIC_ALPHA = 150; + +export function updateActionColor(color: string) { + if (DEVELOPMENT) { + ACTION_EXPRESSION_BG = color; + } +} + +// utils + +export function getMessageColor(type: MessageType): number { + switch (type) { + case MessageType.Chat: return WHITE; + case MessageType.System: return SYSTEM_COLOR; + case MessageType.Admin: return ADMIN_COLOR; + case MessageType.Mod: return MOD_COLOR; + case MessageType.Party: return PARTY_COLOR; + case MessageType.Thinking: return THINKING_COLOR; + case MessageType.PartyThinking: return PARTY_THINKING_COLOR; + case MessageType.Supporter1: return SUPPORTER1_COLOR; + case MessageType.Supporter2: return SUPPORTER2_COLOR; + case MessageType.Supporter3: return SUPPORTER3_COLOR; + case MessageType.Whisper: + case MessageType.WhisperTo: + return WHISPER_COLOR; + case MessageType.Announcement: + case MessageType.PartyAnnouncement: + case MessageType.WhisperAnnouncement: + case MessageType.WhisperToAnnouncement: + return ANNOUNCEMENT_COLOR; + case MessageType.Dismiss: return TRANSPARENT; + default: + return invalidEnumReturn(type, WHITE); + } +} + +export function fillToOutline(color: string | undefined): string | undefined { + return color ? colorToHexRGB(fillToOutlineColor(parseColorFast(color))) : undefined; +} + +export function fillToOutlineWithDarken(color: string | undefined): string | undefined { + return color ? colorToHexRGB(darkenForOutline(fillToOutlineColor(parseColorFast(color)))) : undefined; +} + +export function fillToOutlineColor(color: number): number { + const { h, s, v, a } = colorToHSVA(color); + return colorFromHSVA(h, Math.min(s * 1.3, 1), v * 0.7, a); +} + +const LIGHT_BLUSH = 0xff89aeff; +const DARK_BLUSH = 0xc90040ff; + +export function blushColor(coat: number): number { + const { h, s, v } = colorToHSVA(coat); + + if ( + (h < 15 && s > 0.2 && s < 0.7 && v > 0.85) || + (h > 15 && h < 50 && s > 0.2 && v > 0.85) || + (h > 280 && s > 0.2 && s < 0.7 && v > 0.85) + ) { + return DARK_BLUSH; + } else { + return LIGHT_BLUSH; + } +} + +export function getTileColor(tile: TileType, season: Season) { + switch (tile) { + case TileType.Dirt: + case TileType.ElevatedDirt: + if (season === Season.Autumn) { + return 0xedd29eff; + } else if (season === Season.Winter) { + return 0xd9c2a1ff; + } else { + return 0xf5d99bff; + } + case TileType.Water: + case TileType.WalkableWater: + case TileType.Boat: + return 0x6dbdecff; + case TileType.Grass: + if (season === Season.Autumn) { + return 0xddcf71ff; + } else if (season === Season.Winter) { + return 0xe1ebf8ff; + } else { + return 0x7cc991ff; + } + case TileType.Ice: + case TileType.WalkableIce: + return 0xc1dcecff; + case TileType.SnowOnIce: + return 0xe4eefbff; + case TileType.Wood: + return 0xd7ac7eff; + case TileType.Stone: + return 0x9da6abff; + case TileType.Stone2: + return 0xa0a691ff; + case TileType.None: + case TileType.WallH: + case TileType.WallV: + return BLACK; + default: + invalidEnum(tile); + return BLACK; + } +} diff --git a/src/ts/common/compress.ts b/src/ts/common/compress.ts new file mode 100644 index 0000000..df344fe --- /dev/null +++ b/src/ts/common/compress.ts @@ -0,0 +1,157 @@ +import { toByteArray } from 'base64-js'; +import { bitWriter, bitReader } from './bitUtils'; +import { REGION_SIZE } from './constants'; + +function getBitsForNumber(value: number) { + let bits = 0; + let max = value - 1; + + while (max > 0) { + bits++; + max >>= 1; + } + + return bits; +} + +export function compressTiles(tiles: Uint8Array): Uint8Array { + const types: number[] = []; + + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i]; + + if (types.indexOf(tile) === -1) { + types.push(tile); + } + } + + const bitsPerTile = getBitsForNumber(types.length); + const bitsPerRun = 4; + + return bitWriter(write => { + write(types.length, 8); + + for (const type of types) { + write(type, 8); + } + + if (types.length > 1) { + for (let i = 0; i < tiles.length; i++) { + const value = tiles[i]; + let count = 1; + + if (i === (tiles.length - 1)) { + write(count | 0b1000, bitsPerRun); + write(types.indexOf(value), bitsPerTile); + } else { + i++; + + if (value === tiles[i]) { + while (i < tiles.length && count < 0b111 && tiles[i] === value) { + i++; + count++; + } + + i--; + + write(count, bitsPerRun); + write(types.indexOf(value), bitsPerTile); + } else { + let last = tiles[i]; + let last2 = last; + let pushLast = true; + const values = [value]; + count++; + + for (i++; i < tiles.length; i++) { + last2 = tiles[i]; + + if (last2 === last) { + i -= 2; + count--; + pushLast = false; + break; + } else if (count === 0b111) { + i -= 1; + break; + } else { + values.push(last); + count++; + last = last2; + } + } + + write(count | 0b1000, bitsPerRun); + + for (const v of values) { + write(types.indexOf(v), bitsPerTile); + } + + if (pushLast) { + write(types.indexOf(last), bitsPerTile); + } + } + } + } + } + }); +} + +export function decompressTiles(data: Uint8Array): Uint8Array { + const size = REGION_SIZE * REGION_SIZE; + const result = new Uint8Array(size); + const read = bitReader(data); + const typesCount = read(8); + const types: number[] = []; + + for (let i = 0; i < typesCount; i++) { + types.push(read(8)); + } + + if (types.length === 1) { + result.fill(types[0]); + } else { + const bitsPerTile = getBitsForNumber(typesCount); + const bitsPerRun = 4; + + for (let i = 0; i < size;) { + const value = read(bitsPerRun); + + if ((value & 0b1000) === 0) { + const count = value; + const entry = read(bitsPerTile); + + for (let j = 0; j < count; j++) { + result[i] = types[entry]; + i++; + } + } else { + const count = value & 0b0111; + + for (let j = 0; j < count; j++) { + result[i] = types[read(bitsPerTile)]; + i++; + } + } + } + } + + return result; +} + +export function deserializeTiles(tiles: string) { + const decodedTiles = toByteArray(tiles); + const result: number[] = []; + + for (let i = 0; i < decodedTiles.length; i += 2) { + let count = decodedTiles[i]; + const tile = decodedTiles[i + 1]; + + while (count > 0) { + result.push(tile); + count--; + } + } + + return result; +} diff --git a/src/ts/common/compressPony.ts b/src/ts/common/compressPony.ts new file mode 100644 index 0000000..0c5d007 --- /dev/null +++ b/src/ts/common/compressPony.ts @@ -0,0 +1,624 @@ +import { findLastIndex, isString, isBoolean, isNumber, merge } from 'lodash'; +import { fromByteArray, toByteArray } from 'base64-js'; +import { PonyInfoNumber, SpriteSet, PonyInfo, PonyInfoBase, PaletteManager, PalettePonyInfo, ColorExtraSet } from './interfaces'; +import { syncLockedPonyInfoNumber, syncLockedPonyInfo, createBasePony, toPaletteNumber } from './ponyInfo'; +import { bitWriter, bitReader, ReadBits, WriteBits, countBits, numberToBitCount } from './bitUtils'; +import { BLACK, WHITE, TRANSPARENT } from './colors'; +import { at, toInt, pushUniq, array, clamp, includes, att } from './utils'; +import { getColorCount } from '../client/spriteUtils'; +import * as sprites from '../generated/sprites'; +import { parseColorFast, colorToHexRGB } from './color'; +import { + SLEEVED_ACCESSORIES, frontHooves, mergedFacialHair, mergedBackAccessories, mergedManes, + mergedBackManes, mergedExtraAccessories, mergedHeadAccessories +} from '../client/ponyUtils'; +import { CM_SIZE } from './constants'; + +export const VERSION = 3; + +interface FieldDefinition { + name: keyof PonyInfo; + default?: T; + omit?: (info: PonyInfoBase>) => boolean; + dontSave?: boolean; +} + +interface SetDefinition extends FieldDefinition { + preserveOnZero?: boolean; + sets: ColorExtraSet[]; + minColors?: number; + // defaultLockFills?: boolean[]; + // defaultLockOutlines?: boolean[]; +} + +export interface PrecompressedSet { + type: number; + pattern: number; + colors: number; + fillLocks: number; + fills: number[]; + outlineLocks: number; + outlines: number[]; +} + +export interface Precompressed { + version: number; + colors: number[]; + setFields: (PrecompressedSet | undefined)[]; + colorFields: number[]; + numberFields: number[]; + booleanFields: boolean[]; + cm: number[]; +} + +const identity = (x: T) => x; +const not = (x: T) => !x; + +function emptyOrUnlocked(set: SpriteSet | undefined): boolean { + return !set || !set.type || !set.lockFills || set.lockFills.every(x => !x); +} + +function emptyOrZeroLocked(set: SpriteSet | undefined, customOutlines: boolean): boolean { + return !set || ( + set.type === 0 && set.pattern === 0 && set.lockFills !== undefined && set.lockFills[0] === true && + (!customOutlines || (set.lockOutlines !== undefined && set.lockOutlines[0] === true))); +} + +function empty(set: SpriteSet | undefined): boolean { + return !set || !set.type; +} + +function omitMane(info: PonyInfoNumber) { + return empty(info.mane) && emptyOrUnlocked(info.backMane) + && emptyOrUnlocked(info.tail) && emptyOrUnlocked(info.facialHair); +} + +function omitHead(info: PonyInfoNumber): boolean { + return emptyOrZeroLocked(info.head, !!info.customOutlines); +} + +function omitSleeves(info: PonyInfoNumber) { + return !info.chestAccessory || !includes(SLEEVED_ACCESSORIES, toInt(info.chestAccessory.type)); +} + +function omitFrontHooves(info: PonyInfoNumber) { + return empty(info.frontHooves) && emptyOrUnlocked(info.backHooves); +} + +function readTimes(read: ReadBits, count: number, bitsPerItem: number): number[] { + const result: number[] = []; + + for (let i = 0; i < count; i++) { + result[i] = read(bitsPerItem); + } + + return result; +} + +// NOTE: do not reorder or remove +const setFields: SetDefinition[] = [ + { name: 'extraAccessory', sets: mergedExtraAccessories!, preserveOnZero: true }, + { name: 'nose', sets: sprites.noses[0]!, preserveOnZero: true }, + { name: 'ears', sets: sprites.ears!, preserveOnZero: true }, + { name: 'mane', sets: mergedManes!, preserveOnZero: true, minColors: 1, omit: omitMane }, + { name: 'backMane', sets: mergedBackManes! }, + { name: 'tail', sets: sprites.tails[0]! }, + { name: 'horn', sets: sprites.horns! }, + { name: 'wings', sets: sprites.wings[0]! }, + { name: 'frontHooves', sets: frontHooves[1]!, preserveOnZero: true, minColors: 1, omit: omitFrontHooves }, + { name: 'backHooves', sets: sprites.backLegHooves[1]! }, + { name: 'facialHair', sets: mergedFacialHair! }, + { name: 'headAccessory', sets: mergedHeadAccessories }, + { name: 'earAccessory', sets: sprites.earAccessories! }, + { name: 'faceAccessory', sets: sprites.faceAccessories! }, + { name: 'neckAccessory', sets: sprites.neckAccessories[1]! }, + { name: 'frontLegAccessory', sets: sprites.frontLegAccessories[1]! }, + { name: 'backLegAccessory', sets: sprites.backLegAccessories[1]!, omit: info => !!info.lockBackLegAccessory }, + { name: 'backAccessory', sets: mergedBackAccessories! }, + { name: 'waistAccessory', sets: sprites.waistAccessories[1]! }, + { name: 'chestAccessory', sets: sprites.chestAccessories[1]! }, + { name: 'sleeveAccessory', sets: sprites.frontLegSleeves[1]!, preserveOnZero: true, omit: omitSleeves }, + { name: 'head', sets: sprites.head0[1]!, preserveOnZero: true, omit: omitHead }, + { + name: 'frontLegAccessoryRight', + sets: sprites.frontLegAccessories[1]!, + omit: info => !info.unlockFrontLegAccessory, + }, + { + name: 'backLegAccessoryRight', + sets: sprites.backLegAccessories[1]!, + omit: info => !info.unlockBackLegAccessory || !!info.lockBackLegAccessory, + }, +]; + +const booleanFields: FieldDefinition[] = [ + { name: 'customOutlines' }, + { name: 'lockEyes' }, + { name: 'lockEyeColor' }, + { name: 'lockCoatOutline', omit: info => !info.customOutlines }, + { + name: 'lockBackLegAccessory', omit: info => + empty(info.frontLegAccessory) && empty(info.backLegAccessory) && + empty(info.frontLegAccessoryRight) && empty(info.backLegAccessoryRight) + }, + { name: 'eyeshadow' }, + { name: 'cmFlip', omit: info => info.cm === undefined || info.cm.every(not) }, + { name: 'unlockEyeWhites' }, + { name: 'freeOutlines' }, + { name: 'unlockFrontLegAccessory' }, + { name: 'unlockBackLegAccessory', omit: info => !!info.lockBackLegAccessory }, + { name: 'unlockEyelashColor' }, + { name: 'darkenLockedOutlines', omit: info => !info.freeOutlines }, +]; + +const numberFields: FieldDefinition[] = [ + { name: 'eyelashes' }, + { name: 'eyeOpennessRight' }, + { name: 'eyeOpennessLeft', omit: info => !!info.lockEyes }, + { name: 'fangs' }, + { name: 'muzzle' }, + { name: 'freckles', dontSave: true }, // TODO: remove +]; + +const colorFields: FieldDefinition[] = [ + { name: 'coatFill' }, + { name: 'coatOutline', omit: info => !info.customOutlines || !!info.lockCoatOutline }, + { name: 'eyeColorRight' }, + { name: 'eyeColorLeft', omit: info => !!info.lockEyeColor }, + { name: 'eyeWhites', default: WHITE }, + { name: 'eyeshadowColor', omit: info => !info.eyeshadow }, + { name: 'frecklesColor', omit: info => !info.freckles, dontSave: true }, // TODO: remove + { name: 'eyeWhitesLeft', default: WHITE, omit: info => !info.unlockEyeWhites }, + { name: 'eyelashColor' }, + { name: 'eyelashColorLeft', omit: info => !info.unlockEyelashColor }, + { name: 'magicColor', default: WHITE }, +]; + +const omittableFields: FieldDefinition[] = [ + ...setFields, + ...booleanFields, + ...numberFields, + ...colorFields, +].filter(f => !!f.omit); + +const VERSION_BITS = 6; // max 63 +const COLORS_LENGTH_BITS = 10; // max 1024 +const BOOLEAN_FIELDS_LENGTH_BITS = 4; // max 15 +const NUMBER_FIELDS_LENGTH_BITS = 4; // max 15 +const COLOR_FIELDS_LENGTH_BITS = 4; // max 15 +const SET_FIELDS_LENGTH_BITS = 5; // max 31 +const CM_LENGTH_BITS = 5; // max 31 +const NUMBERS_BITS = 6; // max 63 + +/* istanbul ignore next */ +if (DEVELOPMENT) { + (function () { + function verifyFields(obj: any, lengthBits: number, defs: FieldDefinition[], verify: (field: any) => boolean) { + const missing = Object.keys(obj) + .filter(key => verify(obj[key])) + .filter(key => defs.every(d => d.name !== key)); + + const unnecessary = defs + .filter(({ name }) => !verify(obj[name])); + + if (missing.length || unnecessary.length) { + throw new Error(`Incorrect fields (${missing} / ${unnecessary})`); + } + + if (lengthBits < countBits(defs.length)) { + throw new Error(`Incorrect field length bits (${lengthBits}/${countBits(defs.length)})`); + } + } + + const defaultPony = createBasePony(); + verifyFields(defaultPony, SET_FIELDS_LENGTH_BITS, setFields, f => f.type !== undefined); + verifyFields(defaultPony, COLOR_FIELDS_LENGTH_BITS, colorFields, isString); + verifyFields(defaultPony, NUMBER_FIELDS_LENGTH_BITS, numberFields, isNumber); + verifyFields(defaultPony, BOOLEAN_FIELDS_LENGTH_BITS, booleanFields, isBoolean); + + if (setFields.some(f => !f.sets)) { + throw new Error(`Undefined set in set field (${setFields.find(f => !f.sets)!.name})`); + } + })(); +} + +function trimRight(items: T[]) { + const index = findLastIndex(items, x => !!x); + return (index !== (items.length - 1)) ? items.slice(0, index + 1) : items; +} + +export function precompressCM(cm: (T | undefined)[] | undefined, addColor: (color: T | undefined) => number): number[] { + const result: number[] = []; + + if (cm) { + let length = CM_SIZE * CM_SIZE; + + while (length > 0 && !cm[length - 1]) { + length--; + } + + for (let i = 0; i < length; i++) { + result.push(addColor(cm[i])); + } + } + + return result; +} + +// lock sets + +export function compressLockSet(set: boolean[] | undefined, count: number): number { + const locks = set && set.slice ? set.slice(0, count) : []; + return locks.reduce((result, l, i) => result | (l ? (1 << i) : 0), 0); +} + +export function decompressLockSet(set: number, count: number, defaultValues: boolean[]): boolean[] { + const result: boolean[] = []; + + for (let i = 0; i < MAX_COLORS; i++) { + result[i] = i < count ? !!(set & (1 << i)) : defaultValues[i]; + } + + return result; +} + +// colors + +export function precompressColorSet( + set: (T | undefined)[] | undefined, count: number, locks: number, defaultColor: T, addColor: (color: T) => number +): number[] { + const result: number[] = []; + + if (set) { + for (let i = 0; i < count; i++) { + if ((locks & (1 << i)) === 0) { + const color = set[i]; + result.push(!color || color === defaultColor ? 0 : addColor(color)); + } + } + } + + return result; +} + +export function postdecompressColorSet( + colors: number[], count: number, locks: number, colorList: number[], parseColor: (color: number) => T +): T[] { + const result: T[] = []; + + for (let i = 0, j = 0; i < count; i++) { + const locked = (locks & (1 << i)) !== 0; + result.push(parseColor((locked ? 0 : colorList[colors[j++] - 1]) || BLACK)); + } + + return result; +} + +// set + +const MAX_COLORS = 6; +const ALL_UNLOCKED = array(MAX_COLORS, false); +const ALL_LOCKED = array(MAX_COLORS, true); + +export function precompressSet( + set: SpriteSet | undefined, def: SetDefinition, customOutlines: boolean, defaultColor: T, addColor: (color: T) => number +): PrecompressedSet | undefined { + if (!set) + return undefined; + + const type = clamp(toInt(set.type), 0, def.sets.length - 1); + + if (type === 0 && !def.preserveOnZero) + return undefined; + + const patterns = at(def.sets, type); + const pattern = clamp(toInt(set.pattern), 0, patterns ? patterns.length - 1 : 0); + const sprite = att(patterns, pattern); + const colors = Math.max(getColorCount(sprite), def.minColors || 0); + + /* istanbul ignore next */ + if (type === 0 && pattern === 0 && colors === 0) + return undefined; + + const fillLocks = compressLockSet(set.lockFills, colors); + const fills = precompressColorSet(set.fills, colors, fillLocks, defaultColor, addColor); + const outlineLocks = customOutlines ? compressLockSet(set.lockOutlines, colors) : 0; + const outlines = customOutlines ? precompressColorSet(set.outlines, colors, outlineLocks, defaultColor, addColor) : []; + + return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines }; +} + +export function postdecompressSet( + set: PrecompressedSet, _def: SetDefinition, customOutlines: boolean, colorList: number[], parseColor: (color: number) => T +): SpriteSet | undefined { + return { + type: set.type, + pattern: set.pattern, + lockFills: decompressLockSet(set.fillLocks, set.colors, /*def.defaultLockFills ||*/ ALL_UNLOCKED), + fills: postdecompressColorSet(set.fills, set.colors, set.fillLocks, colorList, parseColor), + lockOutlines: customOutlines ? + decompressLockSet(set.outlineLocks, set.colors, /*def.defaultLockOutlines ||*/ ALL_LOCKED) : + ALL_LOCKED, + outlines: customOutlines ? postdecompressColorSet(set.outlines, set.colors, set.outlineLocks, colorList, parseColor) : [], + }; +} + +// helpers + +function precompressFields, TValue, TResult>( + data: any, defs: TDef[], defaultValue: TResult, encode: (value: TValue | undefined, def: TDef) => TResult +): TResult[] { + return trimRight(defs.map(def => { + if (def.dontSave || (def.omit && def.omit(data))) { + return defaultValue; + } else { + return encode(data[def.name], def); + } + })); +} + +function postdecompressFields, TValue, TResult>( + result: any, defs: TDef[], values: (TValue | undefined)[], defaultValue: TValue, decode: (value: TValue, def: TDef) => TResult +) { + for (let i = 0; i < defs.length; i++) { + const def = defs[i]; + const value = i >= values.length ? undefined : values[i]; + result[def.name] = decode(value === undefined ? defaultValue : value, def); + } +} + +// pony + +type Info = PonyInfoBase>; + +export function precompressPony(info: Info, defaultColor: T, parseColor: (color: T) => number): Precompressed { + const colors: number[] = []; + const customOutlines = !!info.customOutlines; + const addColor = (color: T | undefined) => { + const c = color === undefined ? 0 : parseColor(color); + return c === 0 ? 0 : pushUniq(colors, c); + }; + + return { + version: VERSION, + colors, + booleanFields: precompressFields(info, booleanFields, false as boolean, x => !!x), + numberFields: precompressFields(info, numberFields, 0, toInt), + colorFields: precompressFields(info, colorFields, 0, + (x: T | undefined, def) => (x === undefined || parseColor(x) === (def.default || BLACK)) ? 0 : addColor(x)), + setFields: precompressFields(info, setFields, undefined, + (x: SpriteSet | undefined, def: SetDefinition) => precompressSet(x, def, customOutlines, defaultColor, addColor)), + cm: precompressCM(info.cm, addColor), + }; +} + +const frecklesToPattern = [0, 1, 1, 2, 2, 2, 1]; +const frecklesToColor: number[][] = [[], [1], [1, 2], [2], [1], [1, 2], [2]]; + +function fixVersion(result: Info, data: Precompressed, parseColor: (color: number) => T) { + if (data.version < 3) { + result.head = { + type: 0, + pattern: frecklesToPattern[result.freckles || 0] || 0, + fills: [result.coatFill], + outlines: [result.coatOutline], + lockFills: [true, true, true, true, true, true], + lockOutlines: [true, true, true, true, true, true], + }; + + frecklesToColor[result.freckles || 0].forEach(index => { + result.head!.fills![index] = result.frecklesColor || parseColor(BLACK); + result.head!.lockFills![index] = false; + }); + } +} + +export function createPostDecompressPony() { + return new Function('postdecompressSet', 'setFields', 'ommitableFields', 'fixVersion', [ + 'function identity(x) { return x; }', + 'function getColor(colors, i) { return (i >= 0 && i < colors.length) ? colors[i] : 0; }', + 'function getCM(cm, colors) {', + ' var result = [];', + ' for(var i = 0; i < cm.length; i++) { result.push(getColor(colors, cm[i] - 1) || 0); }', + ' return result;', + '}', + ...omittableFields.map((def, i) => `var omit_${def.name} = ommitableFields[${i}].omit;`), + 'return function (data) {', + ' var dataColors = data.colors;', + ' var bools = data.booleanFields;', + ' var numbers = data.numberFields;', + ' var colors = data.colorFields;', + ' var sets = data.setFields;', + ' var result = {};', + ...booleanFields.map((def, i) => ` result.${def.name} = bools.length > ${i} ? bools[${i}] : false;`), + ...numberFields.map((def, i) => ` result.${def.name} = numbers.length > ${i} ? numbers[${i}] : 0;`), + ...colorFields.map((def, i) => ` result.${def.name} = colors.length > ${i} ? ` + + `getColor(dataColors, colors[${i}] - 1) || ${def.default || BLACK} : ${def.default || BLACK};`), + ' var customOutlines = !!result.customOutlines;', + ...setFields.map((def, i) => ` result.${def.name} = sets.length > ${i} && sets[${i}] !== undefined ? ` + + `postdecompressSet(sets[${i}], setFields[${i}], customOutlines, data.colors, identity) : undefined;`), + ` result.cm = data.cm.length ? getCM(data.cm, dataColors) : undefined;`, + ...omittableFields.map(def => ` if (omit_${def.name}(result)) result.${def.name} = undefined;`), + ' fixVersion(result, data, identity);', + ' return result;', + '};', + ].join('\n')); +} + +export const fastPostdecompressPony = createPostDecompressPony()( + postdecompressSet, setFields, omittableFields, fixVersion); + +export function postdecompressPony(data: Precompressed, parseColor: (color: number) => T): Info { + // NOTE: when updating also update createPostDecompressPony() + + const result: Info = {} as any; + postdecompressFields(result, booleanFields, data.booleanFields, false as boolean, identity); + postdecompressFields(result, numberFields, data.numberFields, 0 as number, identity); + postdecompressFields(result, colorFields, data.colorFields, 0 as number, + (x, def) => parseColor(data.colors[x - 1] || def.default || BLACK)); + const customOutlines = !!result.customOutlines; + postdecompressFields(result, setFields, data.setFields, undefined, + (x, def) => x === undefined ? undefined : postdecompressSet(x, def, customOutlines, data.colors, parseColor)); + + result.cm = data.cm.length ? data.cm.map(x => parseColor(data.colors[x - 1] || TRANSPARENT)) : undefined; + + omittableFields.forEach(def => { + if (def.omit && def.omit(result)) { + result[def.name] = undefined; + } + }); + + fixVersion(result, data, parseColor); + + return result; +} + +// set + +const TYPE_BITS = 5; // max 31 +const PATTERN_BITS = 4; // max 15 +const COLORS_BITS = 3; // max 7 + +export function writeSet(write: WriteBits, colorBits: number, customOutlines: boolean, set: PrecompressedSet | undefined) { + write(set ? 1 : 0, 1); + + if (set) { + write(set.type, TYPE_BITS); + write(set.pattern, PATTERN_BITS); + write(set.colors - 1, COLORS_BITS); + write(set.fillLocks, set.colors); + set.fills.forEach(c => write(c, colorBits)); + + if (customOutlines) { + write(set.outlineLocks, set.colors); + set.outlines.forEach(c => write(c, colorBits)); + } + } +} + +export function readSet(read: ReadBits, colorBits: number, customOutlines: boolean): PrecompressedSet | undefined { + const has = read(1); + + if (has) { + const type = read(TYPE_BITS); + const pattern = read(PATTERN_BITS); + const colors = read(COLORS_BITS) + 1; + const fillLocks = read(colors); + const fills = readTimes(read, colors - countBits(fillLocks), colorBits); + const outlineLocks = customOutlines ? read(colors) : 0; + const outlines = customOutlines ? readTimes(read, colors - countBits(outlineLocks), colorBits) : []; + return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines }; + } else { + return undefined; + } +} + +// helpers + +function writeFields(write: WriteBits, lengthBits: number, fields: T[], writeField: (value: T) => void) { + write(fields.length, lengthBits); + fields.forEach(writeField); +} + +function readFields(read: ReadBits, lengthBits: number, readField: (read: ReadBits) => T): T[] { + const length = read(lengthBits); + const result: T[] = []; + + for (let i = 0; i < length; i++) { + result.push(readField(read)); + } + + return result; +} + +// pony + +export function writePony(write: WriteBits, data: Precompressed) { + const colorBits = Math.max(numberToBitCount(data.colors.length), 1); + const customOutlines = !!data.booleanFields[0]; + write(data.version, VERSION_BITS); + writeFields(write, COLORS_LENGTH_BITS, data.colors, x => write(x >> 8, 24)); + writeFields(write, BOOLEAN_FIELDS_LENGTH_BITS, data.booleanFields, x => write(x ? 1 : 0, 1)); + writeFields(write, NUMBER_FIELDS_LENGTH_BITS, data.numberFields, x => write(x, NUMBERS_BITS)); + writeFields(write, COLOR_FIELDS_LENGTH_BITS, data.colorFields, x => write(x, colorBits)); + writeFields(write, SET_FIELDS_LENGTH_BITS, data.setFields, x => writeSet(write, colorBits, customOutlines, x)); + writeFields(write, CM_LENGTH_BITS, data.cm, x => write(x, colorBits)); +} + +const readColorValue = (read: ReadBits) => ((read(24) << 8) | 0xff) >>> 0; +const readBoolean = (read: ReadBits) => !!read(1); +const readBits = (bits: number) => (read: ReadBits) => read(bits); +const readNumber = readBits(NUMBERS_BITS); + +export function readPony(read: ReadBits): Precompressed { + const version = read(VERSION_BITS); + const colors = readFields(read, COLORS_LENGTH_BITS, readColorValue); + const colorBits = Math.max(numberToBitCount(colors.length), 1); + const readColor = readBits(colorBits); + const booleanFields = readFields(read, BOOLEAN_FIELDS_LENGTH_BITS, readBoolean); + const customOutlines = !!booleanFields[0]; + const numberFields = readFields(read, NUMBER_FIELDS_LENGTH_BITS, readNumber); + const colorFields = readFields(read, COLOR_FIELDS_LENGTH_BITS, readColor); + const setFields = readFields(read, SET_FIELDS_LENGTH_BITS, read => readSet(read, colorBits, customOutlines)); + const cm = readFields(read, CM_LENGTH_BITS, readColor); + return { version, colors, booleanFields, numberFields, colorFields, setFields, cm }; +} + +function writePonyToString(data: Precompressed): string { + return fromByteArray(bitWriter(write => writePony(write, data))); +} + +function readPonyFromBuffer(info: Uint8Array): Precompressed { + return readPony(bitReader(info)); +} + +function readPonyFromString(info: string): Precompressed { + return info ? readPonyFromBuffer(toByteArray(info)) : { + version: VERSION, + colors: [], + booleanFields: [], + numberFields: [], + colorFields: [], + setFields: [], + cm: [], + }; +} + +// compress + +export function compressPony(info: PonyInfoNumber): string { + return writePonyToString(precompressPony(info, BLACK, identity)); +} + +export function decompressPony(info: string | Uint8Array): PonyInfoNumber { + const data = typeof info === 'string' ? readPonyFromString(info) : readPonyFromBuffer(info); + const pony = fastPostdecompressPony(data); // postdecompressPony(data, identity); + return syncLockedPonyInfoNumber(pony); +} + +// compress (string) + +function parseColorFastSafe(color: string): number { + return color ? parseColorFast(color) : TRANSPARENT; +} + +function colorToString(color: number): string { + return color ? colorToHexRGB(color) : ''; +} + +export function compressPonyString(info: PonyInfo): string { + return writePonyToString(precompressPony(info, '000000', parseColorFastSafe)); +} + +export function decompressPonyString(info: string, editable = false): PonyInfo { + const data = readPonyFromString(info); + const pony = postdecompressPony(data, colorToString); + const result = editable ? merge(createBasePony(), pony) : pony; + return syncLockedPonyInfo(result); +} + +// decode + +export function decodePonyInfo(info: string | Uint8Array, paletteManager: PaletteManager): PalettePonyInfo { + return toPaletteNumber(decompressPony(info), paletteManager); +} diff --git a/src/ts/common/constants.ts b/src/ts/common/constants.ts new file mode 100644 index 0000000..351edb0 --- /dev/null +++ b/src/ts/common/constants.ts @@ -0,0 +1,190 @@ +import { Season, Holiday } from './interfaces'; + +export const SEASON: Season = Season.Summer; +export const HOLIDAY: Holiday = Holiday.None; + +export const SECOND = 1000; +export const MINUTE = SECOND * 60; +export const HOUR = MINUTE * 60; +export const DAY = HOUR * 24; +export const WEEK = DAY * 7; +export const MONTH = DAY * 30; +export const YEAR = DAY * 365; + +export const BATCH_SIZE_MAX = 10000; + +export const MAX_VELOCITY = 16; // do not change + +export const PONY_TYPE = 1; +export const PONY_SPEED_TROT = 4; // tiles per sec +export const PONY_SPEED_WALK = 2; // tiles per sec + +export const SAYS_TIME_MIN = 5; // sec +export const SAYS_TIME_MAX = 8; // sec + +export const TILE_CHANGE_RANGE = 5; + +export const EXPRESSION_TIMEOUT = 7000; // ms +export const FLY_DELAY = 0.4; // sec + +export const SERVER_FPS = 10; + +export const AFK_TIMEOUT = 15 * MINUTE; +export const REMOVE_TIMEOUT = 15 * MINUTE; +export const REMOVE_INTERVAL = 1 * MINUTE; +export const MAP_DISCARD_TIMEOUT = 15 * MINUTE; +export const MAP_SWITCH_DELAY = 1 * SECOND; +export const MAP_SWITCHES_PER_UPDATE = 1; +export const JOINS_PER_UPDATE = 1; + +export const DEFAULT_CHATLOG_OPACITY = 35; +export const MAX_CHATLOG_RANGE = 11; +export const MIN_CHATLOG_RANGE = 2; + +export function isChatlogRangeUnlimited(range: number | undefined) { + return !range || range < MIN_CHATLOG_RANGE || range >= MAX_CHATLOG_RANGE; +} + +export const WATER_FPS = 6; +export const WATER_HEIGHT = [0, -1, -2, -1]; + +export const CM_SIZE = 5; +export const MIN_SCALE = 1; +export const MAX_SCALE = 4; + +export const SAY_MAX_LENGTH = 64; +export const PLAYER_NAME_MAX_LENGTH = 20; +export const PLAYER_DESC_MAX_LENGTH = 40; +export const ACCOUNT_NAME_MIN_LENGTH = 1; +export const ACCOUNT_NAME_MAX_LENGTH = 32; +export const MAX_FILTER_WORDS_LENGTH = 1000; +export const PARTY_LIMIT = 30; +export const FRIENDS_LIMIT = 100; + +export const HIDE_LIMIT = 1000; +export const UNHIDE_TIMEOUT = HOUR; +export const MIN_HIDE_TIME = HOUR; +export const MAX_HIDE_TIME = 10 * DAY; + +export const SWAP_TIMEOUT = 1000; +export const MAP_LOAD_SAVE_TIMEOUT = 5000; + +export const HIDES_PER_PAGE = 20; + +export const LATEST_CHARACTER_LIMIT = 10; +export const BASE_CHARACTER_LIMIT = 1000; +export const ADDITIONAL_CHARACTERS_SUPPORTER1 = 200; +export const ADDITIONAL_CHARACTERS_SUPPORTER2 = 300; +export const ADDITIONAL_CHARACTERS_SUPPORTER3 = 450; +export const ADDITIONAL_CHARACTERS_PAST_SUPPORTER = 100; + +export const ACTIONS_LIMIT = 50; +export const COMMAND_ACTION_TIME_DELAY = 1000; + +export const ENTITY_TYPE_LIMIT = 0xffff; + +export const HOUSE_ENTITY_LIMIT = 150; + +export const CAMERA_WIDTH_MIN = 64; +export const CAMERA_WIDTH_MAX = 0xbff; // 3071 +export const CAMERA_HEIGHT_MIN = 64; +export const CAMERA_HEIGHT_MAX = 0x7ff; // 2047 + +export const blinkFps = 24; +export const tileWidth = 32; +export const tileHeight = 24; +export const tileElevation = 20; // 24; + +export const REGION_SIZE = 8; // in tiles +export const REGION_WIDTH = REGION_SIZE * tileWidth; +export const REGION_HEIGHT = REGION_SIZE * tileHeight; +export const REGION_BORDER = 1; // in tiles +export const TILES_RESTORE_MIN_SEC = 1; // max: TILES_RESTORE_MAX_SEC - 1 +export const TILES_RESTORE_MAX_SEC = 10; // max: 255 + +export const PONY_INFO_KEY = 0x76; + +export const MIN_ADULT_AGE = 18; +export const REQUEST_DATE_OF_BIRTH = true; + +export const TIMEOUTS = [ + { value: MINUTE * 5, label: '5 minutes' }, + { value: MINUTE * 10, label: '10 minutes' }, + { value: MINUTE * 30, label: '30 minutes' }, + { value: HOUR * 1, label: '1 hour' }, + { value: HOUR * 5, label: '5 hours' }, + { value: HOUR * 10, label: '10 hours' }, + { value: HOUR * 24, label: '24 hours' }, + { value: DAY * 2, label: '2 days' }, + { value: DAY * 5, label: '5 days' }, +]; + +export const MONTH_NAMES_EN = [ + 'January', + 'February ', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + +export const OFFLINE_PONY = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='; +export const SUPPORTER_PONY = 'CAfz9PUFLUnapSD/1wD5aFT////+hHM2QIJkJ8AQLkkADAA6jXrsBT1Iw+wBMJOqoW1C2oW1AAI='; + +// patreon reward tier IDs +export const rewardLevel1 = '2255086'; +export const rewardLevel2 = '2411886'; +export const rewardLevel3 = '2411888'; + +const SUPPORTER_REWARDS_COMMON = [ + `In-game supporter tag`, + `Supporter chat color`, +]; + +const SUPPORTER_REWARDS_MORE = [ + `Access to patreon posts`, + `Early access to new and experimental features`, +]; + +export const SUPPORTER_REWARDS = [ + [], + [ + ...SUPPORTER_REWARDS_COMMON, + `${ADDITIONAL_CHARACTERS_SUPPORTER1} additional slots for saving ponies`, + ], + [ + ...SUPPORTER_REWARDS_COMMON, + ...SUPPORTER_REWARDS_MORE, + `${ADDITIONAL_CHARACTERS_SUPPORTER2} additional slots for saving ponies`, + ], + [ + ...SUPPORTER_REWARDS_COMMON, + ...SUPPORTER_REWARDS_MORE, + `${ADDITIONAL_CHARACTERS_SUPPORTER3} additional slots for saving ponies`, + ], +]; + +export const SUPPORTER_REWARDS_LIST = [ + ...SUPPORTER_REWARDS_COMMON, + ...SUPPORTER_REWARDS_MORE, + `Additional slots for saving ponies`, +]; + +export const PAST_SUPPORTER_REWARDS = [ + `${ADDITIONAL_CHARACTERS_PAST_SUPPORTER} additional slots for saving ponies`, +]; + +export const GENERAL_RULES = [ + `Be kind to others`, + `Don't spam`, + `Don't use multiple accounts`, + `Don't modify the game with hacks or scripts`, + `Don't encourage behaviour violating the rules`, + `Violation of the rules may result in temporary or permanent ban`, +]; diff --git a/src/ts/common/countries.ts b/src/ts/common/countries.ts new file mode 100644 index 0000000..b4cdb5c --- /dev/null +++ b/src/ts/common/countries.ts @@ -0,0 +1,251 @@ +export const countryCodeToName: { [key: string]: string | undefined; } = { + AD: `Andorra`, + AE: `United Arab Emirates (the)`, + AF: `Afghanistan`, + AG: `Antigua and Barbuda`, + AI: `Anguilla`, + AL: `Albania`, + AM: `Armenia`, + AO: `Angola`, + AQ: `Antarctica`, + AR: `Argentina`, + AS: `American Samoa`, + AT: `Austria`, + AU: `Australia`, + AW: `Aruba`, + AX: `Åland Islands`, + AZ: `Azerbaijan`, + BA: `Bosnia and Herzegovina`, + BB: `Barbados`, + BD: `Bangladesh`, + BE: `Belgium`, + BF: `Burkina Faso`, + BG: `Bulgaria`, + BH: `Bahrain`, + BI: `Burundi`, + BJ: `Benin`, + BL: `Saint Barthélemy`, + BM: `Bermuda`, + BN: `Brunei Darussalam`, + BO: `Bolivia (Plurinational State of)`, + BQ: `Bonaire, Sint Eustatius and Saba`, + BR: `Brazil`, + BS: `Bahamas (the)`, + BT: `Bhutan`, + BV: `Bouvet Island`, + BW: `Botswana`, + BY: `Belarus`, + BZ: `Belize`, + CA: `Canada`, + CC: `Cocos (Keeling) Islands (the)`, + CD: `Congo (the Democratic Republic of the)`, + CF: `Central African Republic (the)`, + CG: `Congo (the)`, + CH: `Switzerland`, + CI: `Côte d'Ivoire`, + CK: `Cook Islands (the)`, + CL: `Chile`, + CM: `Cameroon`, + CN: `China`, + CO: `Colombia`, + CR: `Costa Rica`, + CU: `Cuba`, + CV: `Cabo Verde`, + CW: `Curaçao`, + CX: `Christmas Island`, + CY: `Cyprus`, + CZ: `Czechia`, + DE: `Germany`, + DJ: `Djibouti`, + DK: `Denmark`, + DM: `Dominica`, + DO: `Dominican Republic (the)`, + DZ: `Algeria`, + EC: `Ecuador`, + EE: `Estonia`, + EG: `Egypt`, + EH: `Western Sahara*`, + ER: `Eritrea`, + ES: `Spain`, + ET: `Ethiopia`, + FI: `Finland`, + FJ: `Fiji`, + FK: `Falkland Islands (the) [Malvinas]`, + FM: `Micronesia (Federated States of)`, + FO: `Faroe Islands (the)`, + FR: `France`, + GA: `Gabon`, + GB: `United Kingdom of Great Britain and Northern Ireland (the)`, + GD: `Grenada`, + GE: `Georgia`, + GF: `French Guiana`, + GG: `Guernsey`, + GH: `Ghana`, + GI: `Gibraltar`, + GL: `Greenland`, + GM: `Gambia (the)`, + GN: `Guinea`, + GP: `Guadeloupe`, + GQ: `Equatorial Guinea`, + GR: `Greece`, + GS: `South Georgia and the South Sandwich Islands`, + GT: `Guatemala`, + GU: `Guam`, + GW: `Guinea-Bissau`, + GY: `Guyana`, + HK: `Hong Kong`, + HM: `Heard Island and McDonald Islands`, + HN: `Honduras`, + HR: `Croatia`, + HT: `Haiti`, + HU: `Hungary`, + ID: `Indonesia`, + IE: `Ireland`, + IL: `Israel`, + IM: `Isle of Man`, + IN: `India`, + IO: `British Indian Ocean Territory (the)`, + IQ: `Iraq`, + IR: `Iran (Islamic Republic of)`, + IS: `Iceland`, + IT: `Italy`, + JE: `Jersey`, + JM: `Jamaica`, + JO: `Jordan`, + JP: `Japan`, + KE: `Kenya`, + KG: `Kyrgyzstan`, + KH: `Cambodia`, + KI: `Kiribati`, + KM: `Comoros (the)`, + KN: `Saint Kitts and Nevis`, + KP: `Korea (the Democratic People's Republic of)`, + KR: `Korea (the Republic of)`, + KW: `Kuwait`, + KY: `Cayman Islands (the)`, + KZ: `Kazakhstan`, + LA: `Lao People's Democratic Republic (the)`, + LB: `Lebanon`, + LC: `Saint Lucia`, + LI: `Liechtenstein`, + LK: `Sri Lanka`, + LR: `Liberia`, + LS: `Lesotho`, + LT: `Lithuania`, + LU: `Luxembourg`, + LV: `Latvia`, + LY: `Libya`, + MA: `Morocco`, + MC: `Monaco`, + MD: `Moldova (the Republic of)`, + ME: `Montenegro`, + MF: `Saint Martin (French part)`, + MG: `Madagascar`, + MH: `Marshall Islands (the)`, + MK: `Macedonia (the former Yugoslav Republic of)`, + ML: `Mali`, + MM: `Myanmar`, + MN: `Mongolia`, + MO: `Macao`, + MP: `Northern Mariana Islands (the)`, + MQ: `Martinique`, + MR: `Mauritania`, + MS: `Montserrat`, + MT: `Malta`, + MU: `Mauritius`, + MV: `Maldives`, + MW: `Malawi`, + MX: `Mexico`, + MY: `Malaysia`, + MZ: `Mozambique`, + NA: `Namibia`, + NC: `New Caledonia`, + NE: `Niger (the)`, + NF: `Norfolk Island`, + NG: `Nigeria`, + NI: `Nicaragua`, + NL: `Netherlands (the)`, + NO: `Norway`, + NP: `Nepal`, + NR: `Nauru`, + NU: `Niue`, + NZ: `New Zealand`, + OM: `Oman`, + PA: `Panama`, + PE: `Peru`, + PF: `French Polynesia`, + PG: `Papua New Guinea`, + PH: `Philippines (the)`, + PK: `Pakistan`, + PL: `Poland`, + PM: `Saint Pierre and Miquelon`, + PN: `Pitcairn`, + PR: `Puerto Rico`, + PS: `Palestine, State of`, + PT: `Portugal`, + PW: `Palau`, + PY: `Paraguay`, + QA: `Qatar`, + RE: `Réunion`, + RO: `Romania`, + RS: `Serbia`, + RU: `Russian Federation (the)`, + RW: `Rwanda`, + SA: `Saudi Arabia`, + SB: `Solomon Islands`, + SC: `Seychelles`, + SD: `Sudan (the)`, + SE: `Sweden`, + SG: `Singapore`, + SH: `Saint Helena, Ascension and Tristan da Cunha`, + SI: `Slovenia`, + SJ: `Svalbard and Jan Mayen`, + SK: `Slovakia`, + SL: `Sierra Leone`, + SM: `San Marino`, + SN: `Senegal`, + SO: `Somalia`, + SR: `Suriname`, + SS: `South Sudan`, + ST: `Sao Tome and Principe`, + SV: `El Salvador`, + SX: `Sint Maarten (Dutch part)`, + SY: `Syrian Arab Republic`, + SZ: `Swaziland`, + TC: `Turks and Caicos Islands (the)`, + TD: `Chad`, + TF: `French Southern Territories (the)`, + TG: `Togo`, + TH: `Thailand`, + TJ: `Tajikistan`, + TK: `Tokelau`, + TL: `Timor-Leste`, + TM: `Turkmenistan`, + TN: `Tunisia`, + TO: `Tonga`, + TR: `Turkey`, + TT: `Trinidad and Tobago`, + TV: `Tuvalu`, + TW: `Taiwan (Province of China)`, + TZ: `Tanzania, United Republic of`, + UA: `Ukraine`, + UG: `Uganda`, + UM: `United States Minor Outlying Islands (the)`, + US: `United States of America (the)`, + UY: `Uruguay`, + UZ: `Uzbekistan`, + VA: `Holy See (the)`, + VC: `Saint Vincent and the Grenadines`, + VE: `Venezuela (Bolivarian Republic of)`, + VG: `Virgin Islands (British)`, + VI: `Virgin Islands (U.S.)`, + VN: `Viet Nam`, + VU: `Vanuatu`, + WF: `Wallis and Futuna`, + WS: `Samoa`, + YE: `Yemen`, + YT: `Mayotte`, + ZA: `South Africa`, + ZM: `Zambia`, + ZW: `Zimbabwe`, +}; diff --git a/src/ts/common/debugData.ts b/src/ts/common/debugData.ts new file mode 100644 index 0000000..1e88c47 --- /dev/null +++ b/src/ts/common/debugData.ts @@ -0,0 +1,49 @@ +import { MessageType } from './interfaces'; + +export const sampleMessages: { name: string; message: string; id?: number; type?: MessageType; }[] = []; + +if (DEVELOPMENT) { + sampleMessages.push( + { name: 'Soubi', message: 'Me lo hubieras dicho al menos.', type: MessageType.Party }, + { name: 'Doggy', message: 'Mira un menor' }, + { name: 'carry *br*', message: 'menos frama vai...nunca te falei isso' }, + { name: 'Doggy', message: 'A uste le gustan menores' }, + { name: 'Doggy', message: 'Pero no soy menor de edad' }, + { name: 'springtrap girl(:3)', message: 'pelo menos desincalho' }, + { name: 'Foxy (Menina)', message: 'que vida em a metade do povo sabe menos nois ;-;' }, + { name: 'Mangle (br)', message: 'mais pelo menos meus pais ta aqui em casa', type: MessageType.Party }, + { name: 'Experiment-z30 (ESP)', message: 'Con una menos en la clase' }, + { name: '🌙 Ň✞ĦŦΜΔŘ€ ŞΔŇŞ 🌙', message: 'aceita q doi menos' }, + { name: '⭐✨柊|Lihan|柊✨⭐', message: 'quanta frescura no rabo mano, aceita que doi menos' }, + { name: '🌙 Ň✞ĦŦΜΔŘ€ ŞΔŇŞ 🌙', message: 'aceita q doi menos' }, + { name: '⭐✨柊|Lihan|柊✨⭐', message: 'quanta frescura no rabo mano, aceita que doi menos' }, + { name: 'Ilenos Pijama', message: 'Muito menos participar' }, + { name: 'luz(br)', message: 'pelo menos fan nao vai comer todos os lanches' }, + { name: '=tord=', message: 'quien saque menos' }, + { name: 'ladybug', message: 'mais ou menos .-.' }, + { name: 'springtrap girl(:3)', message: 'pelo menos desincalho' }, + { name: 'Foxy (Menina)', message: 'que vida em a metade do povo sabe menos nois ;-;' }, + { name: 'Experiment-z30 (ESP)', message: 'Con una menos en la clase' }, + { name: 'Ilenos Pijama', message: 'Muito menos participar' }, + { name: 'Mangle (br)', message: 'mais pelo menos meus pais ta aqui em casa', type: MessageType.Party }, + { name: 'luz(br)', message: 'pelo menos fan nao vai comer todos os lanches' }, + { + message: '/help - show help\n/roll [[min-]max] - randomize a number\n/s - say\n/p - party chat\n/t - thinking baloon', + name: '', type: MessageType.System, + }, + { name: 'Molley', message: 'Some admin message here', type: MessageType.Admin }, + { name: 'Dolleyert', message: 'Some moderator message here', type: MessageType.Mod }, + { name: '', message: 'The server will restart soon', type: MessageType.Announcement }, + { name: 'Molley', message: '🎲 rolled 5 of 100', type: MessageType.Announcement }, + { name: 'Molley', message: 'Some thinki👻n👻g 🍎 mesaaasage', type: MessageType.Thinking }, + { name: 'Molley', message: 'Some party thinking message', type: MessageType.PartyThinking }, + { name: 'Molley', message: 'Some supporter 🙂 message 1', type: MessageType.Supporter1 }, + { name: 'Molley', message: 'Some supporter 🙂 message 2', type: MessageType.Supporter2, id: 1 }, + { name: 'Molley', message: 'Some supporter 🙂 message 3', type: MessageType.Supporter3, id: 2 }, + { name: 'Molley', message: 'Some whisper message', type: MessageType.Whisper, id: 2 }, + { name: 'WWWWWWWWWWWWWWWWWWWW', message: 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW' }, + { name: 'WWWWWWWWWWWWWWWWWWWW', message: 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW' }, + { name: 'tord ⚧☿♁⚨⚩⚦⚢⚣⚤', message: 'quien saque menos ⚧☿♁⚨⚩⚦⚢⚣⚤' }, + { name: 'more symbols', message: '♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎' }, + ); +} diff --git a/src/ts/common/encoders/expressionEncoder.ts b/src/ts/common/encoders/expressionEncoder.ts new file mode 100644 index 0000000..7084ac5 --- /dev/null +++ b/src/ts/common/encoders/expressionEncoder.ts @@ -0,0 +1,34 @@ +import { Expression, ExpressionExtra } from '../interfaces'; +import { hasFlag } from '../utils'; + +export const EMPTY_EXPRESSION = 0x1fffffff; + +export function encodeExpression(expression: Expression | undefined): number { + if (!expression) + return EMPTY_EXPRESSION; + + const { extra, rightIris, leftIris, right, left, muzzle } = expression; + + // bits: 5 | 4 | 4 | 5 | 5 | 5 = 28/32 + return ((extra << 23) | (rightIris << 19) | (leftIris << 15) | (right << 10) | (left << 5) | muzzle) >>> 0; +} + +export function decodeExpression(value: number): Expression | undefined { + value = value >>> 0; + + if (value === EMPTY_EXPRESSION) + return undefined; + + const muzzle = value & 0x1f; + const left = (value >> 5) & 0x1f; + const right = (value >> 10) & 0x1f; + const leftIris = (value >> 15) & 0xf; + const rightIris = (value >> 19) & 0xf; + const extra = (value >> 23) & 0x1f; + + return { muzzle, left, right, leftIris, rightIris, extra }; +} + +export function isCancellableExpression(expression: Expression) { + return hasFlag(expression.extra, ExpressionExtra.Zzz); +} diff --git a/src/ts/common/encoders/updateDecoder.ts b/src/ts/common/encoders/updateDecoder.ts new file mode 100644 index 0000000..961ddef --- /dev/null +++ b/src/ts/common/encoders/updateDecoder.ts @@ -0,0 +1,152 @@ +import { + BinaryWriter, BinaryReader, writeInt16, readInt16, createBinaryReader, readUint16, readLength, + readUint32, readUint8, readObject, readUint8Array +} from 'ag-sockets/dist/browser'; +import { decodeString } from 'ag-sockets/dist/utf8'; +import { DecodedUpdate, DecodedRegionUpdate, TileUpdate, UpdateFlags } from '../interfaces'; +import { tileWidth, tileHeight, MAX_VELOCITY } from '../constants'; + +export function writeVelocity(writer: BinaryWriter, value: number) { + if (value >= MAX_VELOCITY || value <= -MAX_VELOCITY) { + throw new Error(`Exceeded max velocity (${value})`); + } + + writeInt16(writer, (value * 0x8000) / MAX_VELOCITY); +} + +export function readVelocity(reader: BinaryReader) { + return (readInt16(reader) * MAX_VELOCITY) / 0x8000; +} + +export function writeCoordX(writer: BinaryWriter, value: number) { + writeInt16(writer, (value * tileWidth) | 0); +} + +export function writeCoordY(writer: BinaryWriter, value: number) { + writeInt16(writer, (value * tileHeight) | 0); +} + +export function readCoordX(reader: BinaryReader) { + return readInt16(reader) / tileWidth; +} + +export function readCoordY(reader: BinaryReader) { + return readInt16(reader) / tileHeight; +} + +export function emptyUpdate(id: number): DecodedUpdate { + return { + id, + x: undefined, + y: undefined, + vx: 0, + vy: 0, + state: undefined, + expression: undefined, + type: undefined, + options: undefined, + crc: undefined, + name: undefined, + filterName: false, + info: undefined, + action: undefined, + switchRegion: false, + playerState: undefined, + }; +} + +export function decodeUpdate(data: Uint8Array): DecodedRegionUpdate { + const reader = createBinaryReader(data); + const x = readUint16(reader); + const y = readUint16(reader); + const updates: DecodedUpdate[] = []; + let update: DecodedUpdate | undefined; + + while (update = readOneUpdate(reader)) { + updates.push(update); + } + + const removesLength = readLength(reader); + const removes: number[] = []; + + for (let i = 0; i < removesLength; i++) { + removes.push(readUint32(reader)); + } + + const tilesLength = readLength(reader); + const tiles: TileUpdate[] = []; + + for (let i = 0; i < tilesLength; i++) { + tiles.push({ + x: readUint8(reader), + y: readUint8(reader), + type: readUint8(reader), + }); + } + + const tileData = readUint8Array(reader); + + return { x, y, updates, removes, tiles, tileData }; +} + +export function readOneUpdate(reader: BinaryReader): DecodedUpdate | undefined { + if (reader.offset >= reader.view.byteLength) + return undefined; + + const flags = readUint16(reader); + + if (flags === 0) { + return undefined; + } + + const id = readUint32(reader); + const update = emptyUpdate(id); + + update.switchRegion = (flags & UpdateFlags.SwitchRegion) !== 0; + + if ((flags & UpdateFlags.Position) !== 0) { + update.x = readCoordX(reader); + update.y = readCoordY(reader); + } + + if ((flags & UpdateFlags.Velocity) !== 0) { + update.vx = readVelocity(reader); + update.vy = readVelocity(reader); + } + + if ((flags & UpdateFlags.State) !== 0) { + update.state = readUint8(reader); + } + + if ((flags & UpdateFlags.Expression) !== 0) { + update.expression = readUint32(reader); + } + + if ((flags & UpdateFlags.Type) !== 0) { + update.type = readUint16(reader); + } + + if ((flags & UpdateFlags.Options) !== 0) { + update.options = readObject(reader); + } + + if ((flags & UpdateFlags.Info) !== 0) { + update.crc = readUint16(reader); + update.info = readUint8Array(reader)!; + } + + if ((flags & UpdateFlags.Action) !== 0) { + update.action = readUint8(reader); + } + + if ((flags & UpdateFlags.Name) !== 0) { + update.name = decodeString(readUint8Array(reader)) || undefined; + update.filterName = (flags & UpdateFlags.NameBad) !== 0; + } + + if ((flags & UpdateFlags.PlayerState) !== 0) { + update.playerState = readUint8(reader); + } + + return update; +} diff --git a/src/ts/common/encoders/updateEncoder.ts b/src/ts/common/encoders/updateEncoder.ts new file mode 100644 index 0000000..511f8fe --- /dev/null +++ b/src/ts/common/encoders/updateEncoder.ts @@ -0,0 +1,167 @@ +import { + BinaryWriter, writeUint32, writeUint16, writeUint8, writeObject, writeUint8Array, writeLength +} from 'ag-sockets/dist/browser'; +import { UpdateFlags, EntityPlayerState, Action } from '../interfaces'; +import { writeBinary } from '../binaryUtils'; +import { ServerEntity, IClient, ServerRegion } from '../../server/serverInterfaces'; +import { isEntityShadowed } from '../../server/entityUtils'; +import { getRegionTiles } from '../../server/serverRegion'; +import { writeCoordX, writeVelocity, writeCoordY } from './updateDecoder'; +import { getPlayerState } from '../../server/playerUtils'; +import { logger } from '../../server/logger'; + +function getOptionsOrUndefined(entity: ServerEntity) { + return (entity.options !== undefined && Object.keys(entity.options).length > 0) ? entity.options : undefined; +} + +export function writeOneUpdate( + writer: BinaryWriter, entity: ServerEntity, flags: UpdateFlags, x: number, y: number, vx: number, vy: number, + options: any, action: Action, playerState: EntityPlayerState +) { + if (DEVELOPMENT && flags === 0) { + logger.error(`Writing empty update`); + } + + if ((flags & UpdateFlags.Position) !== 0) { + flags |= UpdateFlags.State; + + if (vx || vy) { + flags |= UpdateFlags.Velocity; + } + } + + if ((flags & UpdateFlags.Name) !== 0 && entity.nameBad === true) { + flags |= UpdateFlags.NameBad; + } + + writeUint16(writer, flags); + writeUint32(writer, entity.id); + + if ((flags & UpdateFlags.Position) !== 0) { + writeCoordX(writer, x); + writeCoordY(writer, y); + } + + if ((flags & UpdateFlags.Velocity) !== 0) { + writeVelocity(writer, vx); + writeVelocity(writer, vy); + } + + if ((flags & UpdateFlags.State) !== 0) { + writeUint8(writer, entity.state); + } + + if ((flags & UpdateFlags.Expression) !== 0) { + writeUint32(writer, entity.options!.expr!); + } + + if ((flags & UpdateFlags.Type) !== 0) { + writeUint16(writer, entity.type); + } + + if ((flags & UpdateFlags.Options) !== 0) { + writeObject(writer, options); + } + + if ((flags & UpdateFlags.Info) !== 0) { + writeUint16(writer, entity.crc!); + writeUint8Array(writer, entity.encryptedInfoSafe!); + } + + if ((flags & UpdateFlags.Action) !== 0) { + writeUint8(writer, action!); + } + + if ((flags & UpdateFlags.Name) !== 0) { + writeUint8Array(writer, entity.encodedName!); + } + + if ((flags & UpdateFlags.PlayerState) !== 0) { + writeUint8(writer, playerState!); + } +} + +export function writeOneEntity(writer: BinaryWriter, entity: ServerEntity, client: IClient) { + const { x, y, vx, vy } = entity; + // TODO: const expression = !!entity.options && !!entity.options.expr; // instead of in options + const options = getOptionsOrUndefined(entity); + const playerState = getPlayerState(client, entity); + + let flags = UpdateFlags.Position | UpdateFlags.State | UpdateFlags.Type; + + if (entity.encryptedInfoSafe !== undefined) { + flags |= UpdateFlags.Info; + } + + if (entity.encodedName !== undefined) { + flags |= UpdateFlags.Name; + } + + if (playerState !== 0) { + flags |= UpdateFlags.PlayerState; + } + + if (options !== undefined) { + flags |= UpdateFlags.Options; + } + + writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, Action.None, playerState); +} + +export function writeUpdate(writer: BinaryWriter, region: ServerRegion) { + const { x, y, entityUpdates, entityRemoves, tileUpdates } = region; + + writeUint16(writer, x); + writeUint16(writer, y); + + for (const { entity, flags, x, y, vx, vy, options, action, playerState } of entityUpdates) { + writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState); + } + + writeUint16(writer, 0); // end marker + + writeLength(writer, entityRemoves.length); + + for (const remove of entityRemoves) { + writeUint32(writer, remove); + } + + writeLength(writer, tileUpdates.length); + + for (const { x, y, type: tile } of tileUpdates) { + writeUint8(writer, x); + writeUint8(writer, y); + writeUint8(writer, tile); + } + + writeUint8Array(writer, null); // tile data +} + +export function writeRegion(writer: BinaryWriter, region: ServerRegion, client: IClient) { + const { x, y, entities } = region; + + writeUint16(writer, x); + writeUint16(writer, y); + + for (const entity of entities) { + if (!isEntityShadowed(entity) || entity === client.pony) { + writeOneEntity(writer, entity, client); + } + } + + writeUint16(writer, 0); // end marker + + writeLength(writer, 0); // removes + writeLength(writer, 0); // tile updates + writeUint8Array(writer, getRegionTiles(region)); // tile data +} + +// For testing +export function encodeUpdateSimple(region: ServerRegion) { + return writeBinary(writer => writeUpdate(writer, region)); +} + +// For testing +export function encodeRegionSimple(region: ServerRegion, client: IClient) { + return writeBinary(writer => writeRegion(writer, region, client)); +} diff --git a/src/ts/common/entities.ts b/src/ts/common/entities.ts new file mode 100644 index 0000000..93c1d9d --- /dev/null +++ b/src/ts/common/entities.ts @@ -0,0 +1,2393 @@ +import { compact } from 'lodash'; +import * as sprites from '../generated/sprites'; +import { + Entity, PaletteManager, Rect, ServerFlags, PaletteRenderable, ColorShadow, EntityDescriptor, EntityOptions, + CreateEntityMethod, CreateEntity, EntityFlags, MixinEntity, EntityWorldState, defaultWorldState, ColorExtra, InteractAction +} from './interfaces'; +import { tileWidth, tileHeight, ENTITY_TYPE_LIMIT, WATER_FPS, PONY_TYPE, WATER_HEIGHT } from './constants'; +import { CLOUD_SHADOW_COLOR, WHITE } from './colors'; +import { + mixDrawSpider, setPaletteManager, AnimatedRenderable, + AnimatedRenderable1, collider, taperColliderSW, taperColliderNE, taperColliderSE, + taperColliderNW, skewColliderNE, ponyColliders, ponyCollidersBounds, mixTrigger, mixDraw, mixInteract, + mixPickable, mixMinimap, mixColliderRect, mixColliderRounded, mixLight, mixLightSprite, mixColliders, + mixDrawShadow, mixAnimation, mixBobbing, mixDrawWall, mixDrawRain, mixDrawSeasonal, mixDrawWindow, + mixDrawDirectionSign, skewColliderNW, triangleColliderNE, triangleColliderNW, mixInteractAt +} from './mixins'; +import { times, repeat, flatten, hasFlag } from './utils'; +import { withAlphaFloat } from './color'; +import { rect } from './rect'; +import { mockPaletteManager } from './ponyInfo'; + +const entities: EntityDescriptor[] = []; + +export function createBaseEntity(type: number, id: number, x: number, y: number): Entity { + return { id, type, x, y, z: 0, vx: 0, vy: 0, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0 }; +} + +function createEntity( + type: number, id: number, x: number, y: number, options: EntityOptions, worldState: EntityWorldState +): Entity { + const descriptor = entities[type]; + + if (!descriptor) { + throw new Error(`Invalid entity type ${type}`); + } + + return descriptor.create(createBaseEntity(type, id, x, y), options, worldState); +} + +function register(typeName: string, create: CreateEntity): CreateEntityMethod { + if (DEVELOPMENT && entities.length >= ENTITY_TYPE_LIMIT) { + throw new Error(`Exceeded entity limit of ${ENTITY_TYPE_LIMIT} with (${typeName})`); + } + + if (DEVELOPMENT && entities.some(e => e.typeName === typeName)) { + throw new Error(`Entity name already registered (${typeName})`); + } + + const type = entities.length; + entities.push({ type, typeName, create }); + const method: any = (x: number, y: number, options: EntityOptions = {}, worldState: EntityWorldState = defaultWorldState) => + createEntity(type, 0, x, y, options, worldState); + method.type = type; + method.typeName = typeName; + + return method; +} + +function registerMix(typeName: string, ...mixins: (MixinEntity | undefined)[]) { + const mixinsCompacted = compact(mixins); + + return register(typeName, (base, options, worldState) => { + for (const mixin of mixinsCompacted) { + mixin(base, options, worldState); + } + + return base; + }); +} + +export function getEntityTypeName(type: number): string { + return entities[type].typeName; +} + +export function getEntityType(typeName: string): number { + for (let i = 1; i < entities.length; i++) { + if (entities[i].typeName === typeName) { + return i; + } + } + + return 0; +} + +export function getEntityTypesAndNames() { + return entities.map(({ type, typeName }) => ({ type, name: typeName })); +} + +function checkEntity(entity: Entity) { + if (entity.draw && !entity.bounds) { + console.error('missing bounds for', getEntityTypeName(entity.type), entity); + } + + if (entity.drawLight && !entity.lightBounds) { + console.error('missing lightBounds for', getEntityTypeName(entity.type), entity); + } + + if (entity.drawLightSprite && !entity.lightSpriteBounds) { + console.error('missing lightSpriteBounds for', getEntityTypeName(entity.type), entity); + } +} + +export function createAnEntity( + type: number, id: number, x: number, y: number, options: any, paletteManager: PaletteManager, + worldState: EntityWorldState +): Entity { + setPaletteManager(paletteManager); + + const entity = createEntity(type, id, x, y, options, worldState); + + if (DEVELOPMENT) { + checkEntity(entity); + } + + return entity; +} + +// helpers + +// strips names in release build +function n(value: string) { + return (DEVELOPMENT || SERVER) ? value : ''; +} + +function mixCover(x: number, y: number, w: number, h: number): MixinEntity { + const bounds = rect(x, y, w, h); + return base => base.coverBounds = bounds; +} + +function mixFlags(flags: EntityFlags): MixinEntity { + return base => base.flags |= flags; +} + +function mixInteractAction(action: InteractAction): MixinEntity { + return base => base.interactAction = action; +} + +function mixBounds(x: number, y: number, w: number, h: number): MixinEntity { + const bounds = rect(x, y, w, h); + return base => base.bounds = bounds; +} + +function mixServerFlags(flags: ServerFlags): MixinEntity { + if (SERVER) { + return base => base.serverFlags! |= flags; + } else { + return () => { }; + } +} + +function mixOrder(order: number): MixinEntity { + return base => base.order = order; +} + +const collectableInteractive = mixInteract(-8, -12, 16, 16, 1.5); + +function collectable(name: string, sprite: PaletteRenderable, paletteIndex = 0, ...other: MixinEntity[]) { + return doodad(name, sprite, Math.floor(sprite.color!.w / 2), sprite.color!.h - 1, paletteIndex, + collectableInteractive, + ...other); +} + +function decal(name: string, sprite: PaletteRenderable, palette = 0, ...other: MixinEntity[]) { + return registerMix(name, + mixDraw(sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, palette), + mixFlags(EntityFlags.Decal), + ...other); +} + +function decalOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, palette = 0, ...other: MixinEntity[]) { + return registerMix(name, + mixDraw(sprite, dx, dy, palette), + mixFlags(EntityFlags.Decal), + ...other); +} + +function doodad( + name: string, sprite: PaletteRenderable, ox: number, oy: number, palatte = 0, ...other: (MixinEntity | undefined)[] +) { + return registerMix(name, mixDraw(sprite, ox, oy, palatte), ...compact(other)); +} + +function doodadSet(name: string, sprite: PaletteRenderable, ox: number, oy: number, ...other: MixinEntity[]) { + return times(sprite.palettes!.length, i => registerMix(`${name}-${i}`, mixDraw(sprite, ox, oy, i), ...other)); +} + +// placeholder entity + +registerMix(n('null'), () => { throw new Error('Invalid type (0)'); }); + +// entities + +export const pony = registerMix(n('pony'), + base => { + base.flags = EntityFlags.Movable | EntityFlags.CanCollide; + base.colliders = ponyColliders; + base.collidersBounds = ponyCollidersBounds; + }, + mixServerFlags(ServerFlags.DoNotSave)); + +// triggers + +export const triggerDoor = registerMix(n('trigger-door'), + mixTrigger(-32, -6, 64, 12, true)); + +export const triggerHouseDoor = registerMix(n('trigger-house-door'), + mixTrigger(-32, -6, 64, 12, false)); + +export const triggerBoat = registerMix(n('trigger-boat'), + mixTrigger(-50, -12, 100, 24, false)); + +export const trigger3x1 = registerMix(n('trigger-3x1'), + mixTrigger(-48, 0, 96, 24, true)); + +// house + +export const house = doodad(n('house'), sprites.house, 79, 186, 0, + mixColliders( + collider(-70, -92, 137, 90, true), + collider(-70, -2, 32, 2, true), + collider(-70, 0, 30, 3, true), + collider(0, -2, 67, 2, true), + collider(2, 0, 65, 3, true), + ), + mixInteract(-35, -49, 32, 49, 3)); + +export const window1 = registerMix(n('window-1'), + mixDrawWindow(sprites.window_1, 21, 53, 0, 3, 0, 3, 1), + mixOrder(1)); + +export const picture1 = doodad(n('picture-1'), sprites.picture_1, 15, 54, 0); +export const picture2 = doodad(n('picture-2'), sprites.picture_1, 15, 54, 1); + +const cushionPickable = mixPickable(32, 39); +export const cushion1 = decal(n('cushion-1'), sprites.cushion_1, 0, cushionPickable); +export const cushion2 = decal(n('cushion-2'), sprites.cushion_1, 1, cushionPickable); +export const cushion3 = decal(n('cushion-3'), sprites.cushion_1, 2, cushionPickable); + +export const bookshelf = doodad(n('bookshelf'), sprites.bookshelf, 28, 81, 0, + mixColliderRect(-32, -14, 66, 15)); + +// boat + +const boatMinimap = mixMinimap(0x725d3fff, rect(-3, -1, 6, 2)); +const boatSailCollider = mixColliderRect(-5, -3, 11, 6); +const waterBobbing = mixBobbing(WATER_FPS, WATER_HEIGHT); + +export const boat = doodad(n('boat'), sprites.boat, 95, 4, 0, + boatMinimap, + mixOrder(-1)); + +export const boatBob = doodad(n('boat-bob'), sprites.boat, 95, 18, 0, + boatMinimap, + waterBobbing, + mixOrder(-1), + mixFlags(EntityFlags.StaticY)); + +export const boatFrontBob = doodad(n('boat-front-bob'), sprites.boat_front, 71, 16, 0, + boatMinimap, + waterBobbing, + mixFlags(EntityFlags.StaticY)); + +export const boatSail = doodad(n('boat-sail'), sprites.boat_sail, 77, 173, 0, + mixCover(-8, -130, 70, 116), + boatSailCollider); + +export const rope = doodad(n('rope'), sprites.boat_rope, 5, 19, 0, + mixPickable(31, 58)); + +export const ropeRack = doodad(n('rope-rack'), sprites.rope_rack, 11, 34, 0, + mixInteract(-10, -31, 23, 31, 5), + mixFlags(EntityFlags.StaticY)); + +export const boatRopeBob = doodad(n('boat-rope-bob'), sprites.boat_rope, 5, 19, 0, + waterBobbing, + mixFlags(EntityFlags.StaticY)); + +export const boatWake = registerMix(n('boat-wake'), + mixAnimation(sprites.boat_wake, WATER_FPS, 93, 0, { useGameTime: true }), + mixFlags(EntityFlags.StaticY)); + +export function fullBoat(x: number, y: number, sail = true) { + const sailEntities = sail ? [ + boatSail(x - (12 / tileWidth), y + (16 / tileHeight)), + boatRopeBob(x - (91 / tileWidth), y + (8 / tileHeight)), + ] : []; + + return [ + boatBob(x, y), + boatFrontBob(x, y + (29 / tileHeight)), + ...sailEntities, + boatWake(x, y + (5 / tileHeight)), + ]; +} + +// pier + +export const pierLeg = registerMix(n('pier-leg'), + mixAnimation(sprites.pier_leg, WATER_FPS, 10, -14), + mixOrder(-2), + mixFlags(EntityFlags.StaticY)); + +// planks + +const plankMinimap = mixMinimap(0x9c6141ff, rect(-1, 0, 2, 1)); +const plankFlags = mixFlags(EntityFlags.StaticY); +const plankShortMinimap = mixMinimap(0x9c6141ff, rect(0, 0, 1, 1)); +const plankPal = 1; + +export const plank1 = decalOffset(n('plank-1'), sprites.plank_1, 39, -2, plankPal, plankMinimap, plankFlags); +export const plank2 = decalOffset(n('plank-2'), sprites.plank_2, 39, -2, plankPal, plankMinimap, plankFlags); +export const plank3 = decalOffset(n('plank-3'), sprites.plank_3, 39, -2, plankPal, plankMinimap, plankFlags); +export const plank4 = decalOffset(n('plank-4'), sprites.plank_4, 39, -2, plankPal, plankMinimap, plankFlags); + +export const planks = [plank1, plank2, plank3, plank4]; + +export const plankShort1 = decalOffset( + n('plank-short-1'), sprites.plank_short_1, 21, -2, plankPal, plankShortMinimap, plankFlags); +export const plankShort2 = decalOffset( + n('plank-short-2'), sprites.plank_short_2, 21, -2, plankPal, plankShortMinimap, plankFlags); +export const plankShort3 = decalOffset( + n('plank-short-3'), sprites.plank_short_3, 21, -2, plankPal, plankShortMinimap, plankFlags); + +export const planksShort = [plankShort1, plankShort2, plankShort3]; + +export const plankShadow = registerMix(n('plank-shadow'), + mixDrawShadow(sprites.plank_shadow, 39, -12), + mixFlags(EntityFlags.Decal | EntityFlags.StaticY), + mixOrder(-1)); + +export const plankShadow2 = registerMix(n('plank-shadow-2'), + mixDrawShadow(sprites.plank_shadow2, 39, -12), + mixFlags(EntityFlags.Decal | EntityFlags.StaticY), + mixOrder(-1)); + +export const plankShadowShort = registerMix(n('plank-shadow-short'), + mixDrawShadow(sprites.plank_shadow_short, 21, -12), + mixFlags(EntityFlags.Decal | EntityFlags.StaticY), + mixOrder(-1)); + +// pickables + +const applePickable = mixPickable(29, 47); +export const apple = collectable(n('apple'), sprites.apple_1, 0, applePickable); +export const apple2 = collectable(n('apple-2'), sprites.apple_2, 0, applePickable); +export const appleGreen = collectable(n('apple-green'), sprites.apple_1, 1, applePickable); +export const appleGreen2 = collectable(n('apple-green-2'), sprites.apple_2, 1, applePickable); + +const orangePickable = mixPickable(29, 46); +export const orange = collectable(n('orange'), sprites.orange_1, 0, orangePickable); +export const orange2 = collectable(n('orange-2'), sprites.orange_2, 0, orangePickable); + +export const pear = collectable(n('pear'), sprites.pear, 0, mixPickable(30, 48)); + +export const banana = collectable(n('banana'), sprites.banana, 0, mixPickable(30, 44)); + +const lemonPickable = mixPickable(31, 45); +export const lemon = collectable(n('lemon'), sprites.lemon_1, 0, lemonPickable); +export const lime = collectable(n('lime'), sprites.lemon_1, 1, lemonPickable); + +const carrotPalette = 1; +export const carrot1 = doodad(n('carrot-1'), sprites.carrot_1, 4, 9, carrotPalette, collectableInteractive); +export const carrot1b = doodad(n('carrot-1b'), sprites.carrot_1b, 4, 9, carrotPalette, collectableInteractive); +export const carrot2 = doodad(n('carrot-2'), sprites.carrot_2, 4, 9, carrotPalette); +export const carrot2b = doodad(n('carrot-2b'), sprites.carrot_2b, 4, 9, carrotPalette); +export const carrot3 = doodad(n('carrot-3'), sprites.carrot_3, 4, 9, carrotPalette); +export const carrot4 = doodad(n('carrot-4'), sprites.carrot_4, 4, 9, carrotPalette); +export const carrotHeld = doodad(n('carrot-held'), sprites.carrot_hold, 8, 4, carrotPalette, mixPickable(32, 44)); + +const grapesPickable = mixPickable(32, 54); + +export const grapePurple = collectable(n('grape-purple'), sprites.grapes_one, 0, mixPickable(29, 43)); +export const grapeGreen = collectable(n('grape-green'), sprites.grapes_one, 1, mixPickable(29, 43)); + +function grapes(name: string, sprite: ColorShadow, palette: number) { + return doodad(name, sprite, 5, 15, palette, collectableInteractive, grapesPickable); +} + +export const grapesPurple = [ + grapes(n('grapes-purple-1'), sprites.grapes_1, 0), + grapes(n('grapes-purple-2'), sprites.grapes_2, 0), + grapes(n('grapes-purple-3'), sprites.grapes_3, 0), + grapes(n('grapes-purple-4'), sprites.grapes_4, 0), + grapes(n('grapes-purple-5'), sprites.grapes_5, 0), + grapes(n('grapes-purple-6'), sprites.grapes_6, 0), + grapes(n('grapes-purple-7'), sprites.grapes_7, 0), +]; + +export const grapesGreen = [ + grapes(n('grapes-green-1'), sprites.grapes_1, 1), + grapes(n('grapes-green-2'), sprites.grapes_2, 1), + grapes(n('grapes-green-3'), sprites.grapes_3, 1), + grapes(n('grapes-green-4'), sprites.grapes_4, 1), + grapes(n('grapes-green-5'), sprites.grapes_5, 1), + grapes(n('grapes-green-6'), sprites.grapes_6, 1), + grapes(n('grapes-green-7'), sprites.grapes_7, 1), +]; + +export const mango = collectable(n('mango'), sprites.mango, 0, mixPickable(31, 46)); + +export const candyCane1 = collectable(n('candy-cane-1'), sprites.candy_cane_1, 0, mixPickable(31, 46)); // horizontal +export const candyCane2 = collectable(n('candy-cane-2'), sprites.candy_cane_2, 0, mixPickable(31, 49)); // vertical +export const cookie = collectable(n('cookie'), sprites.cookie, 0, mixPickable(31, 46)); +export const cookiePony = collectable(n('cookie-pony'), sprites.cookie_pony, 0, mixPickable(30, 45)); + +export const cookieTable = doodad(n('cookie-table'), sprites.cookie_table_1, 13, 28, 0, + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 5, + mixInteractAction(InteractAction.GiveCookie1), + mixColliderRect(-13, -12, 26, 14)); + +export const cookieTable2 = doodad(n('cookie-table-2'), sprites.cookie_table_2, 13, 28, 0, + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 5, + mixInteractAction(InteractAction.GiveCookie2), + mixColliderRect(-13, -12, 26, 14)); + +export const letter = doodad(n('letter'), sprites.letter, 4, 10, 0, + mixPickable(30, 50)); + +export const rose = doodad(n('rose'), sprites.rose, 8, 1, 0, + mixPickable(30, 41)); + +// tools + +export const hammer = doodad(n('hammer'), sprites.hammer, 8, 10, 0, + mixPickable(25, 46), + mixFlags(EntityFlags.Usable)); + +export const shovel = doodad(n('shovel'), sprites.shovel, 16, 6, 0, + mixPickable(29, 42), + mixFlags(EntityFlags.Usable)); + +export const rake = doodad(n('rake'), sprites.rake, 16, 6, 0, + mixPickable(30, 42)); + +export const pickaxe = doodad(n('pickaxe'), sprites.pickaxe, 10, 8, 0, + mixPickable(28, 42)); + +export const broom = doodad(n('broom'), sprites.broom, 16, 6, 0, + mixPickable(27, 42)); + +export const saw = doodad(n('saw'), sprites.saw, 10, 7, 0, + mixPickable(26, 47)); + +// jacko lanterns + +const jackoLightColor = 0x80281eff; +const jackoLanternPickable = mixPickable(31, 52); +const jackLanternCollider = mixColliderRect(-5, -5, 10, 10, false); + +export const jackoLanternOff = collectable( + n('jacko-lantern-off'), sprites.jacko_lantern_off, 0, jackoLanternPickable, jackLanternCollider); + +export const jackoLanternOn = collectable( + n('jacko-lantern-on'), sprites.jacko_lantern_on, 0, jackoLanternPickable, jackLanternCollider, + mixLight(jackoLightColor, 0, 0, 192, 144), + mixLightSprite(sprites.jacko_lantern_light, WHITE, 6, 9)); + +export const jackoLantern = collectable( + n('jacko-lantern'), sprites.jacko_lantern_on, 0, jackoLanternPickable, jackLanternCollider, + mixLight(jackoLightColor, 0, 0, 192, 144), + mixLightSprite(sprites.jacko_lantern_light, WHITE, 6, 9), + mixFlags(EntityFlags.OnOff)); + +// lanterns + +const lanternLightSprite: AnimatedRenderable1 = { + frames: sprites.lantern_light.frames, +}; + +const lanternPickable = mixPickable(31, 53); +const lanternCollider = mixColliderRounded(-5, -5, 10, 10, 2, false); + +export const lanternOn = registerMix(n('lantern-on'), + mixAnimation(sprites.lantern, 12, 4, 13, { lightSprite: lanternLightSprite }), + mixLight(0x916a32ff, 0, 0, 384, 288), + lanternPickable, + lanternCollider); + +export const lanternOnWall = registerMix(n('lantern-on-wall'), + mixAnimation(sprites.lantern, 12, 4, 13 + 24, { lightSprite: lanternLightSprite }), + mixLight(0x916a32ff, 0, 0, 384, 288)); + +export const lanternOnTable = registerMix(n('lantern-on-table'), + mixAnimation(sprites.lantern, 12, 4, 13 + 14, { lightSprite: lanternLightSprite }), + mixLight(0x916a32ff, 0, 0, 384, 288)); + +export const candy = doodad(n('candy'), sprites.candy, 4, 2, 0, + mixInteract(-6, -6, 13, 13, 1.5)); + +const eggInteractive = mixInteract(-6, -6, 13, 13, 1.5); + +export const eggs = [ + // upright + collectable(n('egg-1-0'), sprites.egg_1, 0, eggInteractive), + collectable(n('egg-1-1'), sprites.egg_1, 1, eggInteractive), + collectable(n('egg-1-2'), sprites.egg_1, 2, eggInteractive), + collectable(n('egg-2-0'), sprites.egg_2, 0, eggInteractive), + collectable(n('egg-3-0'), sprites.egg_3, 0, eggInteractive), + collectable(n('egg-3-1'), sprites.egg_3, 3, eggInteractive), + collectable(n('egg-3-2'), sprites.egg_3, 4, eggInteractive), + collectable(n('egg-4-0'), sprites.egg_4, 0, eggInteractive), + collectable(n('egg-5-0'), sprites.egg_5, 0, eggInteractive), + collectable(n('egg-5-1'), sprites.egg_5, 8, eggInteractive), + collectable(n('egg-6-0'), sprites.egg_6, 0, eggInteractive), + collectable(n('egg-6-1'), sprites.egg_6, 5, eggInteractive), + collectable(n('egg-7-0'), sprites.egg_7, 0, eggInteractive), + collectable(n('egg-7-1'), sprites.egg_7, 6, eggInteractive), + collectable(n('egg-8-0'), sprites.egg_8, 0, eggInteractive), + collectable(n('egg-8-1'), sprites.egg_8, 7, eggInteractive), + collectable(n('egg-9-0'), sprites.egg_9, 0, eggInteractive), + collectable(n('egg-10-0'), sprites.egg_10, 0, eggInteractive), + collectable(n('egg-11-0'), sprites.egg_11, 0, eggInteractive), + collectable(n('egg-12-0'), sprites.egg_12, 0, eggInteractive), + collectable(n('egg-13-0'), sprites.egg_13, 0, eggInteractive), + collectable(n('egg-14-0'), sprites.egg_14, 0, eggInteractive), + // tilted + collectable(n('egg-14-1'), sprites.egg_14, 1, eggInteractive), + collectable(n('egg-14-2'), sprites.egg_14, 2, eggInteractive), + collectable(n('egg-15-0'), sprites.egg_15, 0, eggInteractive), + collectable(n('egg-15-1'), sprites.egg_15, 3, eggInteractive), + collectable(n('egg-15-2'), sprites.egg_15, 4, eggInteractive), + collectable(n('egg-16-0'), sprites.egg_16, 0, eggInteractive), + collectable(n('egg-16-1'), sprites.egg_16, 8, eggInteractive), + collectable(n('egg-17-0'), sprites.egg_17, 0, eggInteractive), + collectable(n('egg-17-1'), sprites.egg_17, 5, eggInteractive), + collectable(n('egg-18-0'), sprites.egg_18, 0, eggInteractive), + collectable(n('egg-18-1'), sprites.egg_18, 7, eggInteractive), + collectable(n('egg-19-0'), sprites.egg_19, 0, eggInteractive), + collectable(n('egg-20-0'), sprites.egg_20, 0, eggInteractive), + collectable(n('egg-21-0'), sprites.egg_21, 0, eggInteractive), + collectable(n('egg-22-0'), sprites.egg_22, 0, eggInteractive), + collectable(n('egg-23-0'), sprites.egg_23, 0, eggInteractive), +]; + +const eggBasketPickable = mixPickable(31, 53); +const eggBasketCollider = mixColliderRect(-5, -5, 11, 6); +const eggBasketParts = [eggBasketPickable, eggBasketCollider]; + +export const basket = doodad(n('egg-basket-1'), sprites.egg_basket_1, 6, 13, 0, ...eggBasketParts); +export const eggBasket2 = doodad(n('egg-basket-2'), sprites.egg_basket_2, 6, 13, 0, ...eggBasketParts); +export const eggBasket3 = doodad(n('egg-basket-3'), sprites.egg_basket_3, 6, 13, 0, ...eggBasketParts); +export const eggBasket4 = doodad(n('egg-basket-4'), sprites.egg_basket_4, 6, 13, 0, ...eggBasketParts); +export const eggBaskets = [basket, eggBasket2, eggBasket3, eggBasket4]; + +export const basketBin = doodad(n('basket-bin'), sprites.basket_bin, 21, 31, 0, + mixColliderRect(-20, -8, 46, 26), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 3); + +const signCollider = mixColliderRect(-6, -1, 16, 2); +const signInteractive = mixFlags(EntityFlags.Interactive); +const signInteractRange: MixinEntity = base => base.interactRange = 5; +const signPickable = mixPickable(32, 62); +const signParts = [signCollider, signInteractive, signInteractRange, signPickable]; + +export const sign = registerMix(n('sign'), + mixDrawSeasonal({ + summer: { sprite: sprites.sign_1, dx: 12, dy: 24, palette: 0 }, + winter: { sprite: sprites.sign_winter, dx: 12, dy: 24, palette: 0 }, + }), + ...signParts); + +export const signQuest = doodad(n('sign-quest'), sprites.sign_2, 12, 24, 0, ...signParts); +export const signQuestion = doodad(n('sign-question'), sprites.sign_4, 12, 24, 0, ...signParts); +export const signDonate = doodad(n('sign-donate'), sprites.sign_3, 12, 24, 0, ...signParts); +export const signDebug = doodad(n('sign-debug'), sprites.sign_4, 12, 24, 0, + ...signParts, + mixFlags(EntityFlags.Debug)); + +export const tile = decal(n('tile'), sprites.tile); + +// direction signs + +export const enum SignIcon { + Spawn, + Pumpkins, + TownCenter, + PineForest, + Boat, + Mountains, + GiftPile, + Forest, + Lake, + Bridge, + Mines, + Barrels, + Fields, + Carrots, +} + +export const directionSign = registerMix(n('direction-sign'), + mixDrawDirectionSign(), + mixColliderRect(-10, -8, 20, 16), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 10); + +export const directionSignLefts = times(5, i => registerMix(n(`direction-sign-left-${i}`), + mixBounds(-10, -59 + i * 11, 14, 10))); + +export const directionSignRights = times(5, i => registerMix(n(`direction-sign-right-${i}`), + mixBounds(-4, -59 + i * 11, 14, 10))); + +export const directionSignUpsLeft = times(5, i => registerMix(n(`direction-sign-up-left-${i}`), + mixBounds(-6, -70 + i * 12, 6, 11))); + +export const directionSignUpsRight = times(5, i => registerMix(n(`direction-sign-up-right-${i}`), + mixBounds(1, -70 + i * 12, 5, 11))); + +export const directionSignDownsLeft = times(5, i => registerMix(n(`direction-sign-down-left-${i}`), + mixBounds(-6, -57 + i * 13, 7, 12))); + +export const directionSignDownsRight = times(5, i => registerMix(n(`direction-sign-down-right-${i}`), + mixBounds(0, -57 + i * 13, 5, 12))); + +// box + +const boxCollider = mixColliderRect(-17, -22, 35, 22); +const boxInteractive = mixInteract(-16, -32, 32, 36, 4); +const boxInteractiveClose = mixInteract(-16, -32, 32, 36, 3); +const boxPickable = mixPickable(32, 72); +const boxParts = [boxCollider, boxInteractive, boxPickable]; + +export const box = doodad(n('box'), sprites.box_empty, 16, 32, 0, + ...boxParts); + +export const boxLanterns = doodad(n('box-lanterns'), sprites.box_lanterns, 16, 32, 0, + ...boxParts, + mixInteractAction(InteractAction.GiveLantern)); + +export const boxBaskets = doodad(n('box-baskets'), sprites.box_baskets, 16, 32, 0, + ...boxParts); + +export const boxFruits = doodad(n('box-fruits'), sprites.box_fruits, 16, 32, 0, + ...boxParts, + mixInteractAction(InteractAction.GiveFruits)); + +export const boxGifts = doodad(n('box-gifts'), sprites.box_gifts, 16, 32, 0, + boxCollider, + boxInteractiveClose, + boxPickable); + +const toolboxParts = [ + mixColliderRect(-15, -10, 30, 10), + mixInteractAt(5), + mixPickable(31, 60), +]; + +export const toolboxEmpty = doodad(n('toolbox-empty'), sprites.toolbox_empty, 16, 22, 0, + ...toolboxParts); +export const toolboxFull = doodad(n('toolbox-full'), sprites.toolbox_full, 16, 22, 0, + ...toolboxParts, + base => base.interactRange = 20, + mixInteractAction(InteractAction.Toolbox), + mixFlags(EntityFlags.IgnoreTool)); + +export const barrel = doodad(n('barrel'), sprites.barrel, 13, 27, 0, + mixColliderRounded(-12, -8, 24, 13, 5), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 3); + +export const bench1 = doodad(n('bench-1'), sprites.bench_1, 37, 20, 0, mixColliderRect(-37, -3, 75, 8)); +export const benchSeat = doodad(n('bench-seat'), sprites.bench_seat, 37, 0); +export const benchBack = doodad(n('bench-back'), sprites.bench_back, 37, 22, 0, mixColliderRect(-37, -3, 75, 8)); +export const benchSeatH = doodad(n('bench-seat-h'), sprites.bench_seath, 11, 0); +export const benchBackH = doodad(n('bench-back-h'), sprites.bench_backh, 9, 84, 0, mixColliderRect(-15, -64, 30, 65)); +export const benchBackH2 = doodad(n('bench-back-h2'), sprites.bench_backh2, 6, 84, 0, mixColliderRect(-15, -64, 30, 65)); + +export const table1 = doodad(n('table-1'), sprites.table_1, 14, 34, 0, mixColliderRect(-18, -22, 37, 23)); +export const table2 = doodad(n('table-2'), sprites.table_2, 24, 29, 0, mixColliderRect(-27, -21, 56, 49)); +export const table3 = doodad(n('table-3'), sprites.table_3, 24, 21, 0, mixColliderRect(-23, -14, 45, 14)); + +export const wallMap = doodad(n('wall-map'), sprites.wall_map, 20, 51, 0, mixFlags(EntityFlags.StaticY)); + +export const crate1A = doodad(n('crate-1a'), sprites.crate_1, 16, 45, 0, mixColliderRect(-15, -20, 30, 20)); +export const crate1B = doodad(n('crate-1b'), sprites.crate_1, 16, 45, 1, mixColliderRect(-15, -20, 30, 20)); +export const crate1AHigh = doodad(n('crate-1a-high'), sprites.crate_1, 16, 45 + 25, 0, mixColliderRect(-15, -20 - 25, 30, 20)); +export const crate1BHigh = doodad(n('crate-1b-high'), sprites.crate_1, 16, 45 + 25, 1, mixColliderRect(-15, -20 - 25, 30, 20)); +export const crate2A = doodad(n('crate-2a'), sprites.crate_2, 15, 23, 0, mixColliderRect(-14, -14, 29, 14)); +export const crate2B = doodad(n('crate-2b'), sprites.crate_2, 15, 23, 1, mixColliderRect(-14, -14, 29, 14)); +export const crate3A = doodad(n('crate-3a'), sprites.crate_3, 15, 23, 0, mixColliderRect(-14, -14, 29, 14)); +export const crate3B = doodad(n('crate-3b'), sprites.crate_3, 15, 23, 1, mixColliderRect(-14, -14, 29, 14)); + +// walls + +export type Walls = ReturnType; + +function createWalls(baseName: string, spriteFull: ColorExtra[], spritesHalf: ColorExtra[]) { + function wall(name: string, index: number, ox: number, oy: number, oy2: number, ...other: MixinEntity[]) { + return registerMix(name, + mixDrawWall(spriteFull[index], spritesHalf[index], ox, oy, oy2), + mixMinimap(0x503d45ff, rect(0, 0, 1, 1)), + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave), + ...other); + } + + function wallShort(name: string, index: number, ox: number, _oy: number, oy2: number, ...other: MixinEntity[]) { + return doodad(name, spritesHalf[index], ox, oy2, 0, + mixMinimap(0x503d45ff, rect(0, 0, 1, 1)), + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave), + ...other); + } + + const wallThickness = 8; + const wallOffsetX = wallThickness / 2; + const wallOffsetY = 18; + const wallOffsetFullY = 81; + const wallHCollider = mixColliderRect(-16, -6, 32, 6); + const wallVCollider = mixColliderRect(-10, -15, 20, 30); + + const wallH = wall( + n(`${baseName}-h`), 16, (32 - wallThickness) / 2, wallOffsetFullY, wallOffsetY, wallHCollider); + + const wallHShort = wallShort( + n(`${baseName}-h-short`), 16, (32 - wallThickness) / 2, wallOffsetFullY, wallOffsetY, wallHCollider); + + const wallV = wall( + n(`${baseName}-v`), 17, wallOffsetX, wallOffsetFullY + 3, wallOffsetY + 3, wallVCollider); + + const wallVShort = wallShort( + n(`${baseName}-v-short`), 17, wallOffsetX, wallOffsetFullY + 3, wallOffsetY + 3, wallVCollider); + + const wallCutL = doodad( + n(`${baseName}-cut-l`), spriteFull[18], (32 - wallThickness) / 2, wallOffsetFullY, 0, + wallHCollider, + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave)); + + const wallCutR = doodad( + n(`${baseName}-cut-r`), spriteFull[19], (32 - wallThickness) / 2, wallOffsetFullY, 0, + wallHCollider, + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave)); + + const wallCorners = [ + // top right bottom left + wall(n(`${baseName}-00`), 0, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 0 + wall(n(`${baseName}-01`), 1, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 1 + wall(n(`${baseName}-02`), 2, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 0 + wall(n(`${baseName}-03`), 3, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 1 + wall(n(`${baseName}-04`), 4, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 0 + wall(n(`${baseName}-05`), 5, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 1 + wall(n(`${baseName}-06`), 6, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 0 + wall(n(`${baseName}-07`), 7, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 1 + wall(n(`${baseName}-08`), 8, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 0 + wall(n(`${baseName}-09`), 9, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 1 + wall(n(`${baseName}-10`), 10, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 0 + wall(n(`${baseName}-11`), 11, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 1 + wall(n(`${baseName}-12`), 12, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 0 + wall(n(`${baseName}-13`), 13, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 1 + wall(n(`${baseName}-14`), 14, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 0 + wall(n(`${baseName}-15`), 15, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 1 + ]; + + const wallCornersShort = [ + // top right bottom left + wallShort(n(`${baseName}-00-short`), 0, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 0 + wallShort(n(`${baseName}-01-short`), 1, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 1 + wallShort(n(`${baseName}-02-short`), 2, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 0 + wallShort(n(`${baseName}-03-short`), 3, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 1 + wallShort(n(`${baseName}-04-short`), 4, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 0 + wallShort(n(`${baseName}-05-short`), 5, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 1 + wallShort(n(`${baseName}-06-short`), 6, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 0 + wallShort(n(`${baseName}-07-short`), 7, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 1 + wallShort(n(`${baseName}-08-short`), 8, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 0 + wallShort(n(`${baseName}-09-short`), 9, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 1 + wallShort(n(`${baseName}-10-short`), 10, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 0 + wallShort(n(`${baseName}-11-short`), 11, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 1 + wallShort(n(`${baseName}-12-short`), 12, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 0 + wallShort(n(`${baseName}-13-short`), 13, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 1 + wallShort(n(`${baseName}-14-short`), 14, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 0 + wallShort(n(`${baseName}-15-short`), 15, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 1 + ]; + + return { wallH, wallHShort, wallV, wallVShort, wallCutL, wallCutR, wallCorners, wallCornersShort }; +} + +export const woodenWalls = createWalls('wall', sprites.wall_wood_full, sprites.wall_wood_half); +export const stoneWalls = createWalls('wall-stone', sprites.wall_stone_full, sprites.wall_stone_half); + +const rockMinimap = mixMinimap(0x78716aff, rect(0, 0, 1, 1)); + +export const rock = doodad(n('rock'), sprites.rock_1, 15, 20, 0, + mixColliderRounded(-16, -12, 32, 12, 3, false), + rockMinimap); + +export const rock2 = doodad(n('rock-2'), sprites.rock_2, 11, 11, 0, + mixColliderRounded(-10, -4, 17, 5, 2, false), + rockMinimap); + +export const rock3 = doodad(n('rock-3'), sprites.rock_3, 10, 11, 0, + mixColliderRounded(-10, -4, 18, 5, 2, false), + rockMinimap); + +export const rockB = doodad(n('rockb'), sprites.rock_1, 15, 20, 1, + mixColliderRounded(-16, -12, 32, 12, 3, false), + rockMinimap); + +export const rock2B = doodad(n('rock-2b'), sprites.rock_2, 11, 11, 1, + mixColliderRounded(-10, -4, 17, 5, 2, false), + rockMinimap); + +export const rock3B = doodad(n('rock-3b'), sprites.rock_3, 10, 11, 1, + mixColliderRounded(-10, -4, 18, 5, 2, false), + rockMinimap); + +// other + +export const well = doodad(n('well'), sprites.well, 30, 67, 0, + mixColliderRect(-26, -20, 54, 30)); + +// water rocks + +const waterRockFPS = WATER_FPS; + +export const waterRock1 = registerMix(n('water-rock-1'), + mixAnimation(sprites.water_rock_1, waterRockFPS, 10, 12), + mixColliderRounded(-12, -6, 25, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock2 = registerMix(n('water-rock-2'), + mixAnimation(sprites.water_rock_2, waterRockFPS, 11, 8), + mixColliderRounded(-12, -5, 22, 8, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock3 = registerMix(n('water-rock-3'), + mixAnimation(sprites.water_rock_3, waterRockFPS, 12, 9), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock4 = registerMix(n('water-rock-4'), + mixAnimation(sprites.water_rock_4, waterRockFPS, 11, 12), + mixColliderRounded(-10, -4, 18, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock5 = registerMix(n('water-rock-5'), + mixAnimation(sprites.water_rock_5, waterRockFPS, 11, 11), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock6 = registerMix(n('water-rock-6'), + mixAnimation(sprites.water_rock_6, waterRockFPS, 13, 11), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock7 = registerMix(n('water-rock-7'), + mixAnimation(sprites.water_rock_7, waterRockFPS, 10, 10), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock8 = registerMix(n('water-rock-8'), + mixAnimation(sprites.water_rock_8, waterRockFPS, 11, 9), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock9 = registerMix(n('water-rock-9'), + mixAnimation(sprites.water_rock_9, waterRockFPS, 10, 15), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock10 = registerMix(n('water-rock-10'), + mixAnimation(sprites.water_rock_10, waterRockFPS, 10, 12), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterRock11 = registerMix(n('water-rock-11'), + mixAnimation(sprites.water_rock_11, waterRockFPS, 10, 13), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + +// stone wall (old) + +export const stoneWallFull = doodad(n('stone-wall-full'), sprites.stone_wall_full, 38, 22, 0, + mixColliderRect(-38, -4, 76, 6)); + +// stone wall + +const stoneWallMinimapColor = 0x9b9977ff; + +export const stoneWallPole1 = registerMix(n('stone-wall-pole-1'), + mixDrawSeasonal({ + summer: { sprite: sprites.stone_wall_pole1, dx: 7, dy: 20, palette: 0 }, + winter: { sprite: sprites.stone_wall_winter_pole1, dx: 8, dy: 21, palette: 0 }, + }), + mixColliderRect(-7, -6, 14, 12, false), + mixMinimap(stoneWallMinimapColor, rect(0, 0, 1, 1)), + mixOrder(1)); + +export const stoneWallBeamH1 = registerMix(n('stone-wall-beam-h-1'), + mixDrawSeasonal({ + summer: { sprite: sprites.stone_wall_horizontal1, dx: 25, dy: 17, palette: 0 }, + winter: { sprite: sprites.stone_wall_winter_horizontal1, dx: 25, dy: 20, palette: 0 }, + }), + mixColliderRect(-25, -6, 50, 12, false), + mixMinimap(stoneWallMinimapColor, rect(0, 0, 2, 1))); + +export const stoneWallBeamV1 = registerMix(n('stone-wall-beam-v-1'), + mixDrawSeasonal({ + summer: { sprite: sprites.stone_wall_vertical1, dx: 5, dy: 8, palette: 0 }, + winter: { sprite: sprites.stone_wall_winter_vertical1, dx: 5, dy: 8, palette: 0 }, + }), + mixColliderRect(-7, 0, 14, 48, false), + mixMinimap(stoneWallMinimapColor, rect(0, 0, 1, 2)), + mixOrder(2)); + +// wooden fence (modular) + +const woodenFenceTall = false; +const woodenFenceMinimapColor = 0xac7146ff; +const woodenFenceMinimap = mixMinimap(woodenFenceMinimapColor, rect(0, 0, 1, 1)); + +function woodenFencePole(name: string, sprite: PaletteRenderable, spriteWinter: PaletteRenderable) { + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite: sprite, dx: 4, dy: 25, palette: 0 }, + winter: { sprite: spriteWinter, dx: 4, dy: 26, palette: 0 }, + }), + mixColliderRect(-4, -3, 8, 6, woodenFenceTall), + woodenFenceMinimap, + mixOrder(1)); +} + +function woodenFenceBeamH(name: string, sprite: PaletteRenderable, spriteWinter: PaletteRenderable) { + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite: sprite, dx: 12, dy: 21, palette: 0 }, + winter: { sprite: spriteWinter, dx: 12, dy: 23, palette: 0 }, + }), + mixColliderRect(-12, -3, 24, 6, woodenFenceTall), + woodenFenceMinimap); +} + +function woodenFenceBeamV(name: string, sprite: PaletteRenderable, spriteWinter: PaletteRenderable) { + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite: sprite, dx: 2, dy: 18, palette: 0 }, + winter: { sprite: spriteWinter, dx: 2, dy: 18, palette: 0 }, + }), + mixColliderRect(-4, 0, 8, 24, woodenFenceTall), + woodenFenceMinimap, + mixOrder(2)); +} + +export const spawnPole = doodad(n('spawn-pole'), sprites.wooden_fence_pole1, 4, 25, 1, mixFlags(EntityFlags.Debug)); +export const routePole = doodad(n('route-pole'), sprites.route_pole, 2, 14, 1, mixFlags(EntityFlags.Debug)); + +export const woodenFencePole1 = woodenFencePole(n('wooden-fence-pole-1'), + sprites.wooden_fence_pole1, sprites.wooden_fence_winter_pole1); +export const woodenFencePole2 = woodenFencePole(n('wooden-fence-pole-2'), + sprites.wooden_fence_pole2, sprites.wooden_fence_winter_pole2); +export const woodenFencePole3 = woodenFencePole(n('wooden-fence-pole-3'), + sprites.wooden_fence_pole3, sprites.wooden_fence_winter_pole3); +export const woodenFencePole4 = woodenFencePole(n('wooden-fence-pole-4'), + sprites.wooden_fence_pole4, sprites.wooden_fence_winter_pole4); +export const woodenFencePole5 = woodenFencePole(n('wooden-fence-pole-5'), + sprites.wooden_fence_pole5, sprites.wooden_fence_winter_pole5); + +export const woodenFenceBeamH1 = woodenFenceBeamH(n('wooden-fence-beam-h-1'), + sprites.wooden_fence_horizontal1, sprites.wooden_fence_winter_horizontal1); +export const woodenFenceBeamH2 = woodenFenceBeamH(n('wooden-fence-beam-h-2'), + sprites.wooden_fence_horizontal2, sprites.wooden_fence_winter_horizontal2); +export const woodenFenceBeamH3 = woodenFenceBeamH(n('wooden-fence-beam-h-3'), + sprites.wooden_fence_horizontal3, sprites.wooden_fence_winter_horizontal3); +export const woodenFenceBeamH4 = woodenFenceBeamH(n('wooden-fence-beam-h-4'), + sprites.wooden_fence_horizontal4, sprites.wooden_fence_winter_horizontal4); +export const woodenFenceBeamH5 = woodenFenceBeamH(n('wooden-fence-beam-h-5'), + sprites.wooden_fence_horizontal5, sprites.wooden_fence_winter_horizontal5); +export const woodenFenceBeamH6 = woodenFenceBeamH(n('wooden-fence-beam-h-6'), + sprites.wooden_fence_horizontal6, sprites.wooden_fence_winter_horizontal6); + +export const woodenFenceBeamV1 = woodenFenceBeamV(n('wooden-fence-beam-v-1'), + sprites.wooden_fence_vertical1, sprites.wooden_fence_winter_vertical1); +export const woodenFenceBeamV2 = woodenFenceBeamV(n('wooden-fence-beam-v-2'), + sprites.wooden_fence_vertical2, sprites.wooden_fence_winter_vertical2); +export const woodenFenceBeamV3 = woodenFenceBeamV(n('wooden-fence-beam-v-3'), + sprites.wooden_fence_vertical3, sprites.wooden_fence_winter_vertical3); + +// fence + +export const fence1 = registerMix(n('fence-1'), + mixDrawSeasonal({ + summer: { sprite: sprites.fence_1, dx: 40, dy: 25, palette: 0 }, + winter: { sprite: sprites.fence_winter_1, dx: 40, dy: 25, palette: 0 }, + }), + mixColliderRect(-38, -2, 83, 4, false), + mixMinimap(woodenFenceMinimapColor, rect(0, 0, 1, 1)), + mixPickable(30, 62)); + +export const fence2 = registerMix(n('fence-2'), + mixDrawSeasonal({ + summer: { sprite: sprites.fence_2, dx: 72, dy: 25, palette: 0 }, + winter: { sprite: sprites.fence_winter_2, dx: 72, dy: 25, palette: 0 }, + }), + mixColliderRect(-70, -2, 148, 4, false), + mixMinimap(woodenFenceMinimapColor, rect(0, 0, 2, 1))); + +export const fence3 = registerMix(n('fence-3'), + mixDrawSeasonal({ + summer: { sprite: sprites.fence_3, dx: 104, dy: 25, palette: 0 }, + winter: { sprite: sprites.fence_winter_3, dx: 104, dy: 25, palette: 0 }, + }), + mixColliderRect(-102, -2, 204, 4, false), + mixMinimap(woodenFenceMinimapColor, rect(0, 0, 3, 1))); + +// rain + +const rainColor = 0xffffff77; // 48 + +export const rain = registerMix(n('rain'), + mixAnimation(sprites.rain, 12, 16, 512, { color: rainColor })); + +export const raindrop = registerMix(n('raindrop'), + mixAnimation(sprites.raindrop, 12, 4, 0, { color: rainColor })); + +export const weatherRain = registerMix(n('weather-rain'), mixDrawRain()); + +// flowers + +export const flower1 = decal(n('flower-1'), sprites.flower_1); +export const flower2 = decal(n('flower-2'), sprites.flower_2); +export const flower3 = decal(n('flower-3'), sprites.flower_3); + +export const flowerPatch1 = decal(n('flowers-1'), sprites.flower_patch1); +export const flowerPatch2 = decal(n('flowers-2'), sprites.flower_patch2); +export const flowerPatch3 = decal(n('flowers-3'), sprites.flower_patch3); +export const flowerPatch4 = decal(n('flowers-4'), sprites.flower_patch4); +export const flowerPatch5 = decal(n('flowers-5'), sprites.flower_patch5); +export const flowerPatch6 = decal(n('flowers-6'), sprites.flower_patch6); +export const flowerPatch7 = decal(n('flowers-7'), sprites.flower_patch7); + +export const flower3Pickable = decal(n('flower-3-pickable'), sprites.flower_3, 0, + mixInteract(-7, -3, 15, 15, 1.5)); + +export const flowerPick = decal(n('flower-pick'), sprites.flower_pick, 0, + mixPickable(31, 39)); + +// clovers + +export const clover1 = decal(n('clover-1'), sprites.clover_1); +export const clover2 = decal(n('clover-2'), sprites.clover_2); +export const clover3 = decal(n('clover-3'), sprites.clover_3); +export const clover4 = decal(n('clover-4'), sprites.clover_5); + +export const fourLeafClover = decal(n('four-leaf-clover'), sprites.clover_4, 0, + mixInteract(-7, -3, 15, 15, 1.5)); + +export const cloverPatch3 = decal(n('clovers-3'), sprites.clover_patch3); +export const cloverPatch4 = decal(n('clovers-4'), sprites.clover_patch4); +export const cloverPatch5 = decal(n('clovers-5'), sprites.clover_patch5); +export const cloverPatch6 = decal(n('clovers-6'), sprites.clover_patch6); +export const cloverPatch7 = decal(n('clovers-7'), sprites.clover_patch7); + +export const cloverPick = doodad(n('clover-pick'), sprites.clover_mouth, 5, 0, 0, + mixPickable(29, 39)); + +export const cloverPick2 = doodad(n('clover-pick-2'), sprites.clover_pick, 5, 0, 0, + mixPickable(31, 39)); + +// autumn + +export const leaves1 = decal(n('leaves-1'), sprites.leaves_1); +export const leaves2 = decal(n('leaves-2'), sprites.leaves_2); +export const leaves3 = decal(n('leaves-3'), sprites.leaves_3); +export const leaves4 = decal(n('leaves-4'), sprites.leaves_4); +export const leaves5 = decal(n('leaves-5'), sprites.leaves_5); + +const smallLeafPileCollider = mixColliderRect(-12, -7, 25, 12); +const mediumLeafPileCollider = mixColliderRect(-16, -8, 34, 15); +const bigLeafPileCollider = mixColliderRect(-30, -13, 60, 24); + +export const [leafpileSmallYellow, leafpileSmallOrange, leafpileSmallRed] + = doodadSet(n('leafpile-small'), sprites.leafpile_small, 18, 16, smallLeafPileCollider); + +export const [leafpileStickYellow, leafpileStickOrange, leafpileStickRed] + = doodadSet(n('leafpile-stick'), sprites.leafpile_stick, 18, 16, smallLeafPileCollider); + +export const [leafpileMediumYellow, leafpileMediumOrange, leafpileMediumRed] + = doodadSet(n('leafpile-medium'), sprites.leafpile_medium, 35, 23, mediumLeafPileCollider); + +export const [leafpileMediumAltYellow, leafpileMediumAltOrange, leafpileMediumAltRed] + = doodadSet(n('leafpile-mediumalt'), sprites.leafpile_mediumalt, 35, 23, mediumLeafPileCollider); + +export const [leafpileBigYellow, leafpileBigOrange, leafpileBigRed] + = doodadSet(n('leafpile-big'), sprites.leafpile_big, 43, 34, bigLeafPileCollider); + +export const [leafpileBigstickYellow, leafpileBigstickOrange, leafpileBigstickRed] + = doodadSet(n('leafpile-bigstick'), sprites.leafpile_bigstick, 43, 34, bigLeafPileCollider); + +// gifts + +const giftInteractive = mixInteract(-9, -15, 18, 20, 1.5); +const giftPickable = mixPickable(32, 52); +const giftOffsetX = 7; +const giftOffsetY = 15; + +export const gift1 = doodad(n('gift-1'), sprites.gift_1, giftOffsetX, giftOffsetY, 0, + giftInteractive, + giftPickable, + mixFlags(EntityFlags.Usable)); + +export const gift2 = doodad(n('gift-2'), sprites.gift_2, giftOffsetX, giftOffsetY, 0, + giftInteractive, + giftPickable, + mixFlags(EntityFlags.Usable)); + +export const gift3 = doodad(n('gift-3'), sprites.gift_2, giftOffsetX, giftOffsetY, 1, + giftInteractive, + giftPickable); + +// gift piles + +export const giftPileSign = doodad(n('giftpile-sign'), sprites.giftpile_sign, 47, 39, 0, + mixColliderRounded(-44, -21, 89, 55, 7)); +export const giftPileTree = doodad(n('giftpile-tree'), sprites.giftpile_tree, 42, 21, 0, + mixColliderRounded(-41, -12, 83, 28, 7)); +export const giftPilePine = doodad(n('giftpile-pine'), sprites.giftpile_pine, 56, 24, 0, + mixColliderRounded(-51, -19, 102, 40, 7)); + +export const giftPile1 = doodad(n('giftpile-1'), sprites.giftpile_1, 28, 26, 0, mixColliderRect(-28, -12, 57, 33)); +export const giftPile2 = doodad(n('giftpile-2'), sprites.giftpile_2, 30, 27, 0, mixColliderRect(-28, -12, 57, 31)); +export const giftPile3 = doodad(n('giftpile-3'), sprites.giftpile_3, 29, 23, 0, mixColliderRect(-28, -12, 57, 31)); +export const giftPile4 = doodad(n('giftpile-4'), sprites.giftpile_4, 26, 16, 0, mixColliderRect(-25, -12, 51, 24)); +export const giftPile5 = doodad(n('giftpile-5'), sprites.giftpile_5, 20, 20, 0, mixColliderRect(-19, -7, 42, 19)); +export const giftPile6 = doodad(n('giftpile-6'), sprites.giftpile_6, 19, 23, 0, mixColliderRect(-19, -12, 38, 28)); + +export const giftPileInteractive = doodad(n('giftpile-5-interactive'), sprites.giftpile_5, 20, 20, 0, + mixColliderRect(-19, -7, 42, 19), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 5); + +// winter + +const snowponyCollider = mixColliderRounded(-12, -4, 25, 7, 2); +export const snowpony1 = doodad(n('snowpony-1'), sprites.snowpony_1, 13, 36, 0, snowponyCollider); +export const snowpony2 = doodad(n('snowpony-2'), sprites.snowpony_2, 16, 46, 0, snowponyCollider); +export const snowpony3 = doodad(n('snowpony-3'), sprites.snowpony_3, 20, 45, 0, snowponyCollider); +export const snowpony4 = doodad(n('snowpony-4'), sprites.snowpony_4, 20, 45, 0, snowponyCollider); +export const snowpony5 = doodad(n('snowpony-5'), sprites.snowpony_5, 20, 45, 0, snowponyCollider); +export const snowpony6 = doodad(n('snowpony-6'), sprites.snowpony_6, 12, 31, 0, snowponyCollider); +export const snowpony7 = doodad(n('snowpony-7'), sprites.snowpony_7, 12, 31, 0, snowponyCollider); +export const snowpony8 = doodad(n('snowpony-8'), sprites.snowpony_8, 12, 31, 0, snowponyCollider); +export const snowpony9 = doodad(n('snowpony-9'), sprites.snowpony_9, 12, 31, 0, snowponyCollider); +export const mistletoe = doodad(n('mistletoe'), sprites.mistletoe, 5, 65); +export const holly = doodad(n('holly'), sprites.holly, 5, 25, 0, mixOrder(1)); + +export const snowponies = [ + snowpony1, + snowpony2, + snowpony3, + snowpony4, + snowpony5, + snowpony6, + snowpony7, + snowpony8, + snowpony9, +]; + +export const snowPileTinier = decal(n('snowpile-tinier'), sprites.snowpile_tinier); +export const snowPileTiny = decal(n('snowpile-tiny'), sprites.snowpile_tiny); +export const snowPileSmall = doodad(n('snowpile-small'), sprites.snowpile_small, 22, 15, 0, + mixColliderRounded(-15, -4, 31, 7, 2, false)); +export const snowPileMedium = doodad(n('snowpile-medium'), sprites.snowpile_medium, 33, 20, 0, + mixColliderRounded(-23, -4, 46, 12, 4, false)); +export const snowPileBig = doodad(n('snowpile-big'), sprites.snowpile_big, 43, 28, 0, + mixColliderRounded(-39, -2, 78, 16, 6, false)); + +export const sandPileTinier = decal(n('sandpile-tinier'), sprites.snowpile_tinier, 1); +export const sandPileTiny = decal(n('sandpile-tiny'), sprites.snowpile_tiny, 1); +export const sandPileSmall = doodad(n('sandpile-small'), sprites.snowpile_small, 22, 15, 1, + mixColliderRounded(-15, -4, 31, 7, 2, false)); +export const sandPileMedium = doodad(n('sandpile-medium'), sprites.snowpile_medium, 33, 20, 1, + mixColliderRounded(-23, -4, 46, 12, 4, false)); +export const sandPileBig = doodad(n('sandpile-big'), sprites.snowpile_big, 43, 28, 1, + mixColliderRounded(-39, -2, 78, 16, 6, false)); + +// pumpkins + +const pumpkinCollider = mixColliderRounded(-11, -6, 22, 12, 5, false); +const pumpkinPickable = mixPickable(26, 50); +const pumpkinParts = [pumpkinCollider, pumpkinPickable]; +const pumpkinDX = 11; +const pumpkinDY = 15; + +export const pumpkin = doodad(n('pumpkin'), sprites.pumpkin_default, pumpkinDX, pumpkinDY, 0, + ...pumpkinParts); + +export const jackoOff = doodad(n('jacko-off'), sprites.pumpkin_off, pumpkinDX, pumpkinDY, 0, + ...pumpkinParts); + +export const jackoOn = doodad(n('jacko-on'), sprites.pumpkin_on, pumpkinDX, pumpkinDY, 0, + ...pumpkinParts, + mixLight(jackoLightColor, 0, 0, 256, 192), + mixLightSprite(sprites.pumpkin_light, WHITE, pumpkinDX, pumpkinDY)); + +export const jacko = doodad(n('jacko'), sprites.pumpkin_on, pumpkinDX, pumpkinDY, 0, + ...pumpkinParts, + mixLight(jackoLightColor, 0, 0, 256, 192), + mixLightSprite(sprites.pumpkin_light, WHITE, pumpkinDX, pumpkinDY), + mixFlags(EntityFlags.OnOff)); + +// tombstones + +export const tombstone1 = doodad(n('tombstone-1'), sprites.tombstone_1, 14, 18, 0, + mixColliderRect(-14, -4, 29, 9)); + +export const tombstone2 = doodad(n('tombstone-2'), sprites.tombstone_2, 11, 27, 0, + mixColliderRect(-12, -3, 26, 6)); + +// torch + +const torchCollider = mixColliderRounded(-2, -2, 4, 4, 1, false); +const torchDX = 4; +const torchDY = 34; +const torchSprites = sprites.torch2; +const torchAnimOff = [0]; +const torchAnimOn = torchSprites.frames.map((_, i) => i).slice(1); + +const torchUnlitSprite: PaletteRenderable = { + color: torchSprites.frames[0], + shadow: torchSprites.shadow, + palettes: [torchSprites.palette], +}; + +const torchSpriteOn: AnimatedRenderable = { + frames: torchSprites.frames.slice(1), + shadow: torchSprites.shadow, + palette: torchSprites.palette, +}; + +const torchLightSpriteOn: AnimatedRenderable1 = { + frames: sprites.torch2_light.frames.slice(1), +}; + +export const torchOff = doodad(n('torch-off'), torchUnlitSprite, torchDX, torchDY, 0, + torchCollider); + +export const torchOn = registerMix(n('torch-on'), + mixAnimation(torchSpriteOn, 8, torchDX, torchDY, { lightSprite: torchLightSpriteOn }), + torchCollider, + mixLight(0x926923ff, 0, 0, 440, 332)); // 0x924d23ff 0x917b32ff + +export const torch = registerMix(n('torch'), + mixAnimation(torchSprites, 8, torchDX, torchDY, { + lightSprite: sprites.torch2_light, + animations: [torchAnimOff, torchAnimOn], + }), + torchCollider, + mixLight(0x926923ff, 0, 0, 440, 332), + mixFlags(EntityFlags.OnOff)); + +export const poof = registerMix(n('poof'), + mixAnimation({ + frames: [...sprites.poof.frames, sprites.emptySprite2], + palette: sprites.poof.palette + }, 12, 13, 30, { repeat: false }), + mixOrder(100)); + +export const poof2 = registerMix(n('poof-2'), + mixAnimation({ + frames: [...sprites.poof2.frames, sprites.emptySprite2], + palette: sprites.poof2.palette + }, 12, 50, 120, { repeat: false }), + mixOrder(100)); + +export const splash = registerMix(n('splash'), + mixAnimation({ + frames: [...sprites.splash.frames, sprites.emptySprite2], + palette: sprites.splash.palette + }, 20, 25, 22, { repeat: false }), + mixOrder(50)); + +const boopSplashFrames = [ + ...repeat(3, sprites.emptySprite2), + ...sprites.splash_boop.frames, + sprites.emptySprite2, +]; + +const boopSlashFps = 20; +const boopSlashDX = 11; +const boopSlashDY = 55; + +export const boopSplashRight = registerMix(n('boop-splash-right'), + mixAnimation({ + frames: boopSplashFrames, + palette: sprites.splash_boop.palette + }, boopSlashFps, boopSlashDX, boopSlashDY, { repeat: false }), + mixOrder(51)); + +export const boopSplashLeft = registerMix(n('boop-splash-left'), + mixAnimation({ + frames: boopSplashFrames, + palette: sprites.splash_boop.palette + }, boopSlashFps, boopSlashDX, boopSlashDY, { repeat: false, flipped: true }), + mixOrder(51)); + +// critters + +export const butterfly = registerMix(n('butterfly'), + mixAnimation(sprites.butterfly, 8, 5, 50), + mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); + +export const firefly = registerMix(n('firefly'), + mixAnimation(sprites.firefly, 24, 4, 44), + mixLight(0x446a27ff, 0, 37, 128, 128), // 386a27, 83842a + mixLightSprite(sprites.firefly_light, WHITE, 2, 40), + mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); + +export const bat = registerMix(n('bat'), + mixAnimation(sprites.bat, 8, 10, 65), + mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); + +export const spider = registerMix(n('spider'), + (base, options) => base.options = { height: 20, time: 0, ...options }, + mixDrawSpider(sprites.spider, 2, 2), + mixFlags(EntityFlags.Critter)); + +// cat + +sprites.cat.frames.push(sprites.emptySprite2); +sprites.cat_light.frames.push(sprites.emptySprite); + +export const enum CatAnimation { + Sit = 0, + Enter = 1, + Exit = 2, + Blink = 3, + Wag = 4, +} + +const catSit = [9]; +const catEnter = [0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 9]; +const catExit = [9, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 8, 17]; +const catBlink = [9, 10, 10, 9]; +const catWag = [9, 11, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, 16, 16, 9]; + +export const cat = registerMix(n('cat'), + mixAnimation(sprites.cat, 24, 17, 39, { + repeat: false, + animations: [catSit, catEnter, catExit, catBlink, catWag], + lightSprite: sprites.cat_light, + }), + base => base.chatY = -5); + +// bunny + +export const enum BunnyAnimation { + Sit = 0, + Walk = 1, + Blink = 2, + Clean = 3, + Look = 4, +} + +const bunnySit = [7]; +const bunnyWalk = [0, 1, 2, 3, 4, 5, 6]; +const bunnyBlink = [7, 8, ...repeat(35, 7)]; +const bunnyClean = [7, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, ...repeat(30, 7)]; +const bunnyLook = [7, 9, 25, 26, 27, 28, 29, 30, 31, 32, 33, 9, ...repeat(30, 7)]; + +export const bunny = registerMix(n('bunny'), + mixAnimation(sprites.bunny, 14, 12, 23, { + animations: [bunnySit, bunnyWalk, bunnyBlink, bunnyClean, bunnyLook], + }), + mixFlags(EntityFlags.Movable)); + +// eyes + +const spritesEyes: AnimatedRenderable = { + frames: [sprites.emptySprite2], + palette: sprites.defaultPalette, +}; + +export const eyes = registerMix(n('eyes'), + mixAnimation(spritesEyes, 24, 16, 8, { + repeat: false, + animations: [[9], [10]], + lightSprite: sprites.cat_light, + })); + +// ghosts + +const ghostSprite: AnimatedRenderable = { + frames: [ + sprites.emptySprite2, + ...sprites.ghost1.frames, + ], + palette: sprites.ghost1.palette, +}; + +const ghostHoovesSprite: AnimatedRenderable = { + frames: [ + sprites.emptySprite2, + ...sprites.ghost1_hooves.frames, + ...repeat(17, sprites.emptySprite2), + ], + palette: sprites.ghost1_hooves.palette, +}; + +const ghostLightSprite: AnimatedRenderable1 = { + frames: [ + sprites.emptySprite, + ...sprites.ghost1_light.frames, + ], +}; + +const ghostHoovesLightSprite: AnimatedRenderable1 = { + frames: [ + sprites.emptySprite, + ...sprites.ghost1_hooves_light.frames, + ], +}; + +const ghostFPS = 20; +const ghostDX = 28; +const ghostDYBase = 40; +const ghostDY = [ghostDYBase, ghostDYBase + 7]; // by tombstone +const ghostColor = withAlphaFloat(WHITE, 0.7); +const ghostLightColor = 0x777777ff; +const ghostNone = [0]; +const ghostAnim1 = [ + 0, + 1, 1, + 2, 2, + 3, 3, + 4, 4, + 5, 5, + 6, 6, + 7, 7, + 8, 8, + ...repeat(10, 9), + 10, + ...repeat(3, 11), + 12, + ...repeat(14, 13), + 14, 14, + ...repeat(10, 15), + 16, + 17, + 18, + 19, + 20, + 0, +]; + +const ghostAnim2 = [ + 0, + 21, 21, + 22, 22, + 23, 23, + 24, 24, + 25, 25, + ...repeat(10, 26), + 27, + 28, + ...repeat(4, 29), + 30, + 31, + 32, + ...repeat(10, 33), + 34, + 35, + 36, + 37, + 0, +]; + +const ghostAnim3 = [ + 0, + 38, 38, + 39, 39, + 40, 40, + 41, 41, + 42, 42, + 43, 43, + 44, 44, + 45, 45, + 46, 46, + 47, 47, + 48, 48, + ...repeat(10, 49), + 50, + 51, 51, + 52, 52, + 53, 53, + 54, 54, + 55, 55, + 56, 56, + 57, 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 0, +]; + +export const enum GhostAnimation { + None = 0, + Anim1 = 1, + Anim2 = 2, + Anim3 = 3, +} + +const createGhost = (tomb: number) => { + const anim = mixAnimation(ghostSprite, ghostFPS, ghostDX, ghostDY[tomb], { + color: ghostColor, + repeat: false, + animations: [ghostNone, ghostAnim1, ghostAnim2, ghostAnim3], + lightSprite: ghostLightSprite, + }); + + return registerMix(n(`ghost-${tomb + 1}`), + anim, + base => { + base.order = -1; + base.lightSpriteColor = ghostLightColor; + }); +}; + +const createGhostHooves = (tomb: number) => { + const anim = mixAnimation(ghostHoovesSprite, ghostFPS, ghostDX, ghostDY[tomb], { + color: ghostColor, + repeat: false, + animations: [ghostNone, ghostAnim1, ghostAnim2, ghostAnim3], + lightSprite: ghostHoovesLightSprite, + }); + + return registerMix(n(`ghost-hooves-${tomb + 1}`), + anim, + base => { + base.order = -1; + base.lightSpriteColor = ghostLightColor; + }); +}; + +export const ghost1 = createGhost(0); +export const ghost2 = createGhost(1); +export const ghostHooves1 = createGhostHooves(0); +export const ghostHooves2 = createGhostHooves(1); + +// clouds + +const cloudSprite = sprites.cloud.shadow; + +export const cloud = registerMix(n('cloud'), + mixDrawShadow(sprites.cloud, Math.floor(cloudSprite.w / 2), cloudSprite.h, CLOUD_SHADOW_COLOR), + mixFlags(EntityFlags.Decal | EntityFlags.Movable)); + +// vegetation + +const largeLeafedBushLarge = mixColliderRounded(-14, -6, 28, 12, 2, false); +const largeLeafedBushSmall = mixColliderRounded(-8, -4, 16, 8, 2, false); + +export const largeLeafedBush1 = registerMix(n('large-leafed-bush-1'), + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_1, dx: 17, dy: 23, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushLarge); + +export const largeLeafedBush2 = registerMix(n('large-leafed-bush-2'), + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_2, dx: 17, dy: 23, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushLarge); + +export const largeLeafedBush3 = registerMix(n('large-leafed-bush-3'), + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_3, dx: 12, dy: 17, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushSmall); + +export const largeLeafedBush4 = registerMix(n('large-leafed-bush-4'), + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_4, dx: 12, dy: 17, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushSmall); + +// cliffs + +const cliffTall = false; +const cliffCollider = mixColliderRect(-16, 0, 32, 24, cliffTall); +const cliffColliderTop = mixColliderRect(-16, 0, 32, 8, cliffTall); +const cliffColliderLeft = mixColliderRect(-18, 0, 6, 24, cliffTall); +const cliffColliderRight = mixColliderRect(12, 0, 6, 24, cliffTall); +const cliffColliderTrimLeft = mixColliderRect(-16, 0, 16, 24, cliffTall); +const cliffColliderTrimRight = mixColliderRect(0, 0, 16, 24, cliffTall); + +const cliffColor = 0x908d7cff; +const cliffExtra = mixMinimap(cliffColor, rect(-1, 0, 1, 1)); + +function cliffOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite, dx, dy, palette: 0 }, + autumn: { palette: 1 }, + winter: { palette: 2 }, + }), + mixFlags(EntityFlags.StaticY), + ...other); +} + +function cliff(name: string, sprite: PaletteRenderable, ...other: MixinEntity[]) { + return cliffOffset(name, sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, + mixFlags(EntityFlags.StaticY), + ...other); +} + +function cliffDecal(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { + return cliffOffset(name, sprite, dx, dy, mixFlags(EntityFlags.Decal), + ...other); +} + +export const cliffSW = cliff(n('cliff-sw'), sprites.cliffs_grass_sw, + mixColliders( + ...taperColliderSW(-16, -3, 32, 25, cliffTall), + collider(-16, 22, 35, 24 * 2 + 7, cliffTall), + ...taperColliderNE(-16, 22 + 24 * 2 + 7, 32, 24, cliffTall), + ), + mixMinimap(cliffColor, rect(-1, 0, 1, 4))); + +export const cliffSE = cliff(n('cliff-se'), sprites.cliffs_grass_se, + mixColliders( + ...taperColliderSE(-16, -3, 32, 25, cliffTall), + collider(-19, 22, 35, 24 * 2 + 7, cliffTall), + ...taperColliderNW(-16, 22 + 24 * 2 + 7, 32, 24, cliffTall), + ), + mixMinimap(cliffColor, rect(-1, 0, 1, 4))); + +const cliffSCollider = mixColliderRect(-16, -3, 32, 24 * 3 + 8, cliffTall); + +export const cliffS1 = cliff(n('cliff-s1'), sprites.cliffs_grass_s1, + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + +export const cliffS2 = cliff(n('cliff-s2'), sprites.cliffs_grass_s2, + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + +export const cliffS3 = cliff(n('cliff-s3'), sprites.cliffs_grass_s3, + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + +export const cliffSb = cliff(n('cliff-sb'), sprites.cliffs_grass_sb, + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + +export const cliffSbEntrance = cliff(n('cliff-sb-entrance'), sprites.cliffs_grass_sb, + mixColliderRect(-16, -3, 32, 24 * 3, cliffTall), + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + +const cliffNWColliders = mixColliders(...skewColliderNW(0, 0, 21, 24, cliffTall)); +const cliffNEColliders = mixColliders(...skewColliderNE(-22, 0, 21, 24, cliffTall)); + +const cliffColliderTrimLeftBot = mixColliders( + collider(-16, 0, 16, 17, cliffTall), + ...taperColliderNW(-16, 17, 16, 11, cliffTall), +); + +const cliffColliderTrimRightBot = mixColliders( + collider(0, 0, 16, 17, cliffTall), + ...taperColliderNE(0, 17, 16, 11, cliffTall), +); + +export const cliffTopNW = cliff(n('cliff-top-nw'), sprites.cliffs_grass_top_nw, cliffNWColliders, cliffExtra); +export const cliffTopN = cliff(n('cliff-top-n'), sprites.cliffs_grass_top_n, cliffColliderTop, cliffExtra); +export const cliffTopNE = cliff(n('cliff-top-ne'), sprites.cliffs_grass_top_ne, cliffNEColliders, cliffExtra); +export const cliffTopW = cliffOffset(n('cliff-top-w'), sprites.cliffs_grass_top_w, 16, 0, cliffColliderLeft, cliffExtra); +export const cliffTopE = cliff(n('cliff-top-e'), sprites.cliffs_grass_top_e, cliffColliderRight, cliffExtra); +export const cliffTopSW = cliff(n('cliff-top-sw'), sprites.cliffs_grass_top_sw, cliffCollider, cliffExtra); +export const cliffTopSE = cliff(n('cliff-top-se'), sprites.cliffs_grass_top_se, cliffCollider, cliffExtra); + +export const cliffTopS1 = cliff(n('cliff-top-s1'), sprites.cliffs_grass_top_s1, cliffCollider, cliffExtra); +export const cliffTopS2 = cliff(n('cliff-top-s2'), sprites.cliffs_grass_top_s2, cliffCollider, cliffExtra); +export const cliffTopS3 = cliff(n('cliff-top-s3'), sprites.cliffs_grass_top_s3, cliffCollider, cliffExtra); +export const cliffTopSb = cliff(n('cliff-top-sb'), sprites.cliffs_grass_top_sb, cliffCollider, cliffExtra); + +export const cliffMidS1 = cliff(n('cliff-mid-s1'), sprites.cliffs_grass_mid_s1, cliffCollider, cliffExtra); +export const cliffMidS2 = cliff(n('cliff-mid-s2'), sprites.cliffs_grass_mid_s2, cliffCollider, cliffExtra); +export const cliffMidS3 = cliff(n('cliff-mid-s3'), sprites.cliffs_grass_mid_s3, cliffCollider, cliffExtra); +export const cliffMidSb = cliff(n('cliff-mid-sb'), sprites.cliffs_grass_mid_sb, cliffCollider, cliffExtra); + +export const cliffMidSW1 = cliff(n('cliff-mid-sw1'), sprites.cliffs_grass_mid_sw1, cliffCollider, cliffExtra); +export const cliffMidSE1 = cliff(n('cliff-mid-se1'), sprites.cliffs_grass_mid_se1, cliffCollider, cliffExtra); +export const cliffMidSW2 = cliff(n('cliff-mid-sw2'), sprites.cliffs_grass_mid_sw2, cliffCollider, cliffExtra); +export const cliffMidSE2 = cliff(n('cliff-mid-se2'), sprites.cliffs_grass_mid_se2, cliffCollider, cliffExtra); + +export const cliffBotS1 = cliff(n('cliff-bot-s1'), sprites.cliffs_grass_bot_s1, cliffCollider, cliffExtra); +export const cliffBotS2 = cliff(n('cliff-bot-s2'), sprites.cliffs_grass_bot_s2, cliffCollider, cliffExtra); +export const cliffBotS3 = cliff(n('cliff-bot-s3'), sprites.cliffs_grass_bot_s3, cliffCollider, cliffExtra); +export const cliffBotSb = cliff(n('cliff-bot-sb'), sprites.cliffs_grass_bot_sb, cliffCollider, cliffExtra); + +export const cliffBotSW = cliff(n('cliff-bot-sw'), sprites.cliffs_grass_bot_sw, cliffCollider, cliffExtra); +export const cliffBotSE = cliff(n('cliff-bot-se'), sprites.cliffs_grass_bot_se, cliffCollider, cliffExtra); + +export const cliffTopTrimLeft = cliffDecal( + n('cliff-top-trim-left'), sprites.cliffs_grass_top_trim_left, 16, 0, cliffColliderTrimRight, mixOrder(1)); +export const cliffMidTrimLeft = cliffDecal( + n('cliff-mid-trim-left'), sprites.cliffs_grass_mid_trim_left, 16, 0, cliffColliderTrimRight, mixOrder(1)); +export const cliffBotTrimLeft = cliffDecal( + n('cliff-bot-trim-left'), sprites.cliffs_grass_bot_trim_left, 16, 0, cliffColliderTrimRightBot, mixOrder(1)); + +export const cliffTopTrimRight = cliffDecal( + n('cliff-top-trim-right'), sprites.cliffs_grass_top_trim_right, 16, 0, cliffColliderTrimLeft); +export const cliffMidTrimRight = cliffDecal( + n('cliff-mid-trim-right'), sprites.cliffs_grass_mid_trim_right, 16, 0, cliffColliderTrimLeft); +export const cliffBotTrimRight = cliffDecal( + n('cliff-bot-trim-right'), sprites.cliffs_grass_bot_trim_right, 16, 0, cliffColliderTrimLeftBot); + +export const cliffDecal1 = cliffDecal(n('cliff-decal-1'), sprites.cliffs_grass_decal_1, 14, 1, mixOrder(2)); +export const cliffDecal2 = cliffDecal(n('cliff-decal-2'), sprites.cliffs_grass_decal_2, 14, 1, mixOrder(2)); +export const cliffDecal3 = cliffDecal(n('cliff-decal-3'), sprites.cliffs_grass_decal_3, 16, -2, mixOrder(2)); +export const cliffDecalL = cliffDecal(n('cliff-decal-l'), sprites.cliffs_grass_decal_l, 15, 1, mixOrder(2)); +export const cliffDecalR = cliffDecal(n('cliff-decal-r'), sprites.cliffs_grass_decal_r, 16, 1, mixOrder(2)); + +// cave + +const caveTall = true; +const caveCollider = mixColliderRect(-16, 0, 32, 24, caveTall); +const caveColliderTop = mixColliderRect(-16, 0, 32, 24, caveTall); +const caveColliderLeft = mixColliderRect(-18, 0, 24, 24, caveTall); +const caveColliderRight = mixColliderRect(-16, 0, 34, 24, caveTall); +const caveColliderTrimLeft = mixColliderRect(-16, 0, 16, 24, caveTall); +const caveColliderTrimRight = mixColliderRect(0, 0, 16, 24, caveTall); + +const caveColor = 0x6a6f73ff; +const caveExtra = mixMinimap(caveColor, rect(-1, 0, 1, 1)); + +function caveOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite, dx, dy, palette: 0 }, + autumn: { palette: 1 }, + winter: { palette: 2 }, + }), + mixFlags(EntityFlags.StaticY), + ...other); +} + +function cave(name: string, sprite: PaletteRenderable, ...other: MixinEntity[]) { + return caveOffset(name, sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, + ...other); +} + +function caveDecal(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { + return caveOffset(name, sprite, dx, dy, mixFlags(EntityFlags.Decal), + ...other); +} + +export const caveSW = cave(n('cave-sw'), sprites.cave_walls_sw, + mixColliders( + collider(-16, -3, 35, 24 * 2 + 7 + 25, caveTall), + ...taperColliderNE(-16, 22 + 24 * 2 + 7, 32, 24, caveTall), + ), + mixMinimap(caveColor, rect(-1, 0, 1, 4))); + +export const caveSE = cave(n('cave-se'), sprites.cave_walls_se, + mixColliders( + collider(-19, -3, 35, 24 * 2 + 7 + 25, caveTall), + ...taperColliderNW(-16, 22 + 24 * 2 + 7, 32, 24, caveTall), + ), + mixMinimap(caveColor, rect(-1, 0, 1, 4))); + +const caveSCollider = mixColliderRect(-16, -3, 32, 24 * 3 + 8, caveTall); + +export const caveS1 = cave(n('cave-s1'), sprites.cave_walls_s1, + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); + +export const caveS2 = cave(n('cave-s2'), sprites.cave_walls_s2, + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); + +export const caveS3 = cave(n('cave-s3'), sprites.cave_walls_s3, + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); + +export const caveSb = cave(n('cave-sb'), sprites.cave_walls_sb, + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); + +const caveNWColliders = mixColliders(...triangleColliderNW(0, 0, 21, 24, caveTall)); +const caveNEColliders = mixColliders(...triangleColliderNE(-22, 0, 21, 24, caveTall)); + +const caveColliderTrimLeftBot = mixColliders( + collider(-16, 0, 16, 17, caveTall), + ...taperColliderNW(-16, 17, 16, 11, caveTall), +); + +const caveColliderTrimRightBot = mixColliders( + collider(0, 0, 16, 17, caveTall), + ...taperColliderNE(0, 17, 16, 11, caveTall), +); + +export const caveTopNW = cave(n('cave-top-nw'), sprites.cave_walls_top_nw, caveNWColliders, caveExtra); +export const caveTopN = cave(n('cave-top-n'), sprites.cave_walls_top_n, caveColliderTop, caveExtra); +export const caveTopNE = cave(n('cave-top-ne'), sprites.cave_walls_top_ne, caveNEColliders, caveExtra); +export const caveTopW = caveOffset(n('cave-top-w'), sprites.cave_walls_top_w, 16, 0, caveColliderLeft, caveExtra); +export const caveTopE = cave(n('cave-top-e'), sprites.cave_walls_top_e, caveColliderRight, caveExtra); +export const caveTopSW = cave(n('cave-top-sw'), sprites.cave_walls_top_sw, caveCollider, caveExtra); +export const caveTopSE = cave(n('cave-top-se'), sprites.cave_walls_top_se, caveCollider, caveExtra); + +export const caveTopS1 = cave(n('cave-top-s1'), sprites.cave_walls_top_s1, caveCollider, caveExtra); +export const caveTopS2 = cave(n('cave-top-s2'), sprites.cave_walls_top_s2, caveCollider, caveExtra); +export const caveTopS3 = cave(n('cave-top-s3'), sprites.cave_walls_top_s3, caveCollider, caveExtra); +export const caveTopSb = cave(n('cave-top-sb'), sprites.cave_walls_top_sb, caveCollider, caveExtra); + +export const caveMidS1 = cave(n('cave-mid-s1'), sprites.cave_walls_mid_s1, caveCollider, caveExtra); +export const caveMidS2 = cave(n('cave-mid-s2'), sprites.cave_walls_mid_s2, caveCollider, caveExtra); +export const caveMidS3 = cave(n('cave-mid-s3'), sprites.cave_walls_mid_s3, caveCollider, caveExtra); +export const caveMidSb = cave(n('cave-mid-sb'), sprites.cave_walls_mid_sb, caveCollider, caveExtra); + +export const caveMidSW1 = cave(n('cave-mid-sw1'), sprites.cave_walls_mid_sw1, caveCollider, caveExtra); +export const caveMidSE1 = cave(n('cave-mid-se1'), sprites.cave_walls_mid_se1, caveCollider, caveExtra); +export const caveMidSW2 = cave(n('cave-mid-sw2'), sprites.cave_walls_mid_sw2, caveCollider, caveExtra); +export const caveMidSE2 = cave(n('cave-mid-se2'), sprites.cave_walls_mid_se2, caveCollider, caveExtra); + +export const caveBotS1 = cave(n('cave-bot-s1'), sprites.cave_walls_bot_s1, caveCollider, caveExtra); +export const caveBotS2 = cave(n('cave-bot-s2'), sprites.cave_walls_bot_s2, caveCollider, caveExtra); +export const caveBotS3 = cave(n('cave-bot-s3'), sprites.cave_walls_bot_s3, caveCollider, caveExtra); +export const caveBotSb = cave(n('cave-bot-sb'), sprites.cave_walls_bot_sb, caveCollider, caveExtra); + +export const caveBotSW = cave(n('cave-bot-sw'), sprites.cave_walls_bot_sw, caveCollider, caveExtra); +export const caveBotSE = cave(n('cave-bot-se'), sprites.cave_walls_bot_se, caveCollider, caveExtra); + +export const caveTopTrimLeft = caveDecal( + n('cave-top-trim-left'), sprites.cave_walls_top_trim_left, 16, 0, caveColliderTrimRight, mixOrder(1)); +export const caveMidTrimLeft = caveDecal( + n('cave-mid-trim-left'), sprites.cave_walls_mid_trim_left, 16, 0, caveColliderTrimRight, mixOrder(1)); +export const caveBotTrimLeft = caveDecal( + n('cave-bot-trim-left'), sprites.cave_walls_bot_trim_left, 16, 0, caveColliderTrimRightBot, mixOrder(1)); + +export const caveTopTrimRight = caveDecal( + n('cave-top-trim-right'), sprites.cave_walls_top_trim_right, 16, 0, caveColliderTrimLeft); +export const caveMidTrimRight = caveDecal( + n('cave-mid-trim-right'), sprites.cave_walls_mid_trim_right, 16, 0, caveColliderTrimLeft); +export const caveBotTrimRight = caveDecal( + n('cave-bot-trim-right'), sprites.cave_walls_bot_trim_right, 16, 0, caveColliderTrimLeftBot); + +export const caveDecal1 = caveDecal(n('cave-decal-1'), sprites.cave_walls_decal_1, 14, 1, mixOrder(2)); +export const caveDecal2 = caveDecal(n('cave-decal-2'), sprites.cave_walls_decal_2, 14, 1, mixOrder(2)); +export const caveDecal3 = caveDecal(n('cave-decal-3'), sprites.cave_walls_decal_3, 16, -2, mixOrder(2)); +export const caveDecalL = caveDecal(n('cave-decal-l'), sprites.cave_walls_decal_l, 15, 1, mixOrder(2)); +export const caveDecalR = caveDecal(n('cave-decal-r'), sprites.cave_walls_decal_r, 16, 1, mixOrder(2)); + +export const caveFill = registerMix(n('cave-fill'), + mixDraw(sprites.tile_none, 0, 0), + mixColliderRect(0, 0, 32, 24, true, true), + mixFlags(EntityFlags.StaticY)); + +export const caveCover = registerMix(n('cave-cover'), + mixDraw(sprites.tile_none, 0, 24), + mixFlags(EntityFlags.StaticY)); + +// stalactites + +export const stalactite1 = doodad(n('stalactite-1'), sprites.stalactite_1, 4, 15, 0, + mixColliderRounded(-4, -3, 8, 5, 2), + mixFlags(EntityFlags.StaticY)); + +export const stalactite2 = doodad(n('stalactite-2'), sprites.stalactite_2, 5, 31, 0, + mixColliderRounded(-5, -4, 10, 5, 2), + mixFlags(EntityFlags.StaticY)); + +export const stalactite3 = doodad(n('stalactite-3'), sprites.stalactite_3, 6, 51, 0, + mixColliderRounded(-6, -6, 12, 7, 3), + mixFlags(EntityFlags.StaticY)); + +// crystals + +const crystalLight = 0x299ad5ff; +const mixCrystalLight = mixLight(crystalLight, 0, 0, 200, 200); +const waterCrystalFPS = WATER_FPS; + +export const crystals1 = registerMix(n('crystals-1'), + mixDraw(sprites.crystals_1, 8, 16), + mixLightSprite(sprites.light_crystals_1, WHITE, 8, 16), + mixCrystalLight, + mixColliderRounded(-7, -4, 16, 4, 1)); + +export const crystals2 = registerMix(n('crystals-2'), + mixDraw(sprites.crystals_2, 11, 19), + mixLightSprite(sprites.light_crystals_2, WHITE, 11, 19), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals3 = registerMix(n('crystals-3'), + mixDraw(sprites.crystals_3, 13, 18), + mixLightSprite(sprites.light_crystals_3, WHITE, 13, 18), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals4 = registerMix(n('crystals-4'), + mixDraw(sprites.crystals_4, 11, 15), + mixLightSprite(sprites.light_crystals_4, WHITE, 11, 15), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals5 = registerMix(n('crystals-5'), + mixDraw(sprites.crystals_5, 12, 18), + mixLightSprite(sprites.light_crystals_5, WHITE, 12, 18), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals6 = registerMix(n('crystals-6'), + mixDraw(sprites.crystals_6, 11, 13), + mixLightSprite(sprites.light_crystals_6, WHITE, 11, 13), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals7 = registerMix(n('crystals-7'), + mixDraw(sprites.crystals_7, 13, 16), + mixLightSprite(sprites.light_crystals_7, WHITE, 13, 16), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals8 = registerMix(n('crystals-8'), + mixDraw(sprites.crystals_8, 8, 17), + mixLightSprite(sprites.light_crystals_8, WHITE, 8, 17), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals9 = registerMix(n('crystals-9'), + mixDraw(sprites.crystals_9, 8, 11), + mixLightSprite(sprites.light_crystals_9, WHITE, 8, 11), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystals10 = registerMix(n('crystals-10'), + mixDraw(sprites.crystals_10, 5, 12), + mixLightSprite(sprites.light_crystals_10, WHITE, 5, 12), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); + +export const crystalsCartPile = registerMix(n('crystals-cart-pile'), + mixDraw(sprites.crystals_cart_pile, 21, 28), + mixLightSprite(sprites.light_crystals_cart_pile, WHITE, 21, 28), + mixCrystalLight, + mixInteract(-20, -28, 40, 40, 3), + mixFlags(EntityFlags.StaticY), + mixOrder(2)); + +export const crystalHeld = registerMix(n('crystal-held'), + mixDraw(sprites.crystals_held, 7, 4), + mixLightSprite(sprites.light_crystals_held, WHITE, 7, 4), + mixLight(crystalLight, 0, 0, 160, 160), + mixPickable(31, 44)); + +export const crystalLantern = registerMix(n('crystal-lantern'), + mixDraw(sprites.crystal_lantern, 4, 15), + mixLightSprite(sprites.light_crystal_lantern, WHITE, 4, 15), + mixLight(crystalLight, 0, 0, 192, 144), + mixPickable(31, 55)); + +export const waterCrystal1 = registerMix(n('water-crystal-1'), + mixAnimation(sprites.water_crystal_1, waterCrystalFPS, 4, 12, { + lightSprite: sprites.water_crystal_1_light, + }), + mixCrystalLight, + mixColliderRounded(-4, -4, 9, 8, 2, false), + mixFlags(EntityFlags.StaticY)); + +export const waterCrystal2 = registerMix(n('water-crystal-2'), + mixAnimation(sprites.water_crystal_2, waterCrystalFPS, 9, 11, { + lightSprite: sprites.water_crystal_2_light, + }), + mixCrystalLight, + mixColliderRounded(-9, -6, 16, 7, 2, false), + mixFlags(EntityFlags.StaticY)); + + +export const waterCrysta3 = registerMix(n('water-crystal-3'), + mixAnimation(sprites.water_crystal_3, waterCrystalFPS, 5, 10, { + lightSprite: sprites.water_crystal_3_light, + }), + mixCrystalLight, + mixColliderRounded(-4, -4, 7, 5, 2, false), + mixFlags(EntityFlags.StaticY)); + +// mine + +export const mineEntrance = registerMix(n('mine-entrance'), + mixDraw(sprites.mine_entrance, 49, 0), + mixInteract(-32, -48, 65, 46, 3), + mixOrder(10)); + +export const mineClosed = registerMix(n('mine-closed'), + mixDraw(sprites.mine_closed, 36, -24), + mixOrder(11)); + +export const mineCart = doodad(n('mine-cart'), sprites.mine_cart, 26, 32, 0, + mixColliderRect(-27, 0, 54, 21), + mixFlags(EntityFlags.StaticY), + mixOrder(1)); + +export const mineCartFront = doodad(n('mine-cart-front'), sprites.mine_cart_front, 26, 52, 0, + mixColliders( + collider(-30, -25, 55, 4), + collider(-30, -19, 15, 20), + collider(-30, 2, 55, 4), + ), + mixFlags(EntityFlags.StaticY)); + +export const mineCartBack = doodad(n('mine-cart-back'), sprites.mine_cart_back, 26, 30, 0, + mixFlags(EntityFlags.StaticY)); + +const railsExtra = mixFlags(EntityFlags.StaticY); + +export const mineRailsH = decal(n('mine-rails-h'), sprites.mine_rails_h, 0, railsExtra); +export const mineRailsV = decal(n('mine-rails-v'), sprites.mine_rails_v, 0, railsExtra); +export const mineRailsSE = decalOffset(n('mine-rails-se'), sprites.mine_rails_se, 16, 0, 0, railsExtra); +export const mineRailsSW = decalOffset(n('mine-rails-sw'), sprites.mine_rails_sw, 16, 0, 0, railsExtra); +export const mineRailsNE = decalOffset(n('mine-rails-ne'), sprites.mine_rails_ne, 16, 0, 0, railsExtra); +export const mineRailsNW = decalOffset(n('mine-rails-nw'), sprites.mine_rails_nw, 16, 0, 0, railsExtra); + +export const mineRailsNSW = decalOffset(n('mine-rails-nsw'), sprites.mine_rails_nsw, 16, 1, 0, railsExtra); +export const mineRailsNSE = decalOffset(n('mine-rails-nse'), sprites.mine_rails_nse, 16, 1, 0, railsExtra); +export const mineRailsNWE = decalOffset(n('mine-rails-nwe'), sprites.mine_rails_nwe, 16, 0, 0, railsExtra); +export const mineRailsSWE = decalOffset(n('mine-rails-swe'), sprites.mine_rails_swe, 16, 0, 0, railsExtra); + +export const mineRailsEndLeft = doodad(n('mine-rails-end-left'), sprites.mine_rails_end_left, 17, 30, 0, + mixColliderRect(-20, -10, 38, 23), + railsExtra); + +export const mineRailsEndRight = doodad(n('mine-rails-end-right'), sprites.mine_rails_end_right, 16, 30, 0, + mixColliderRect(-16, -10, 38, 23), + railsExtra); + +export const mineRailsEndTop = doodad(n('mine-rails-end-top'), sprites.mine_rails_end_top, 16, 32, 0, + mixColliderRect(-16, -32, 32, 32), + railsExtra); + +export const mineRailsFadeUp = decal(n('mine-rails-fade-up'), sprites.mine_rail_fade_up, 0, + railsExtra); + +// collider utils + +export const collider1x1 = registerMix(n('collider-1x1'), mixColliderRect(0, 0, 1 * tileWidth, 1 * tileHeight, false, true)); +export const collider2x1 = registerMix(n('collider-2x1'), mixColliderRect(0, 0, 2 * tileWidth, 1 * tileHeight, false, true)); +export const collider3x1 = registerMix(n('collider-3x1'), mixColliderRect(0, 0, 3 * tileWidth, 1 * tileHeight, false, true)); +export const collider1x2 = registerMix(n('collider-1x2'), mixColliderRect(0, 0, 1 * tileWidth, 2 * tileHeight, false, true)); +export const collider1x3 = registerMix(n('collider-1x3'), mixColliderRect(0, 0, 1 * tileWidth, 3 * tileHeight, false, true)); + +// trees + +const stumpsTall = false; +const treeAutumnPals = [2, 3, 4]; + +export const web = doodad(n('web'), sprites.web, -6, 39, 0, + mixCover(-50, -135, 110, 120)); + +export const xmasLights = registerMix(n('xmas-lights'), mixLightSprite(sprites.light6, WHITE, 75, 180)); +export const xmasLight = registerMix(n('xmas-light'), mixLight(0x926923ff, 0, 0, 50, 50)); + +const tree1Options = { sprite: sprites.tree_1, dx: 5, dy: 9 }; +const tree2Options = { sprite: sprites.tree_2, dx: 10, dy: 32 }; +const tree3Options = { sprite: sprites.tree_3, dx: 21, dy: 59 }; + +export const trees1 = times(3, i => registerMix(n(`tree1-${i}`), + mixDrawSeasonal({ + summer: { ...tree1Options, palette: 0 }, + autumn: { ...tree1Options, palette: treeAutumnPals[i] }, + winter: { ...tree1Options, palette: 1 }, + }))); + +export const trees2 = times(3, i => registerMix(n(`tree2-${i}`), + mixDrawSeasonal({ + summer: { ...tree2Options, palette: 0 }, + autumn: { ...tree2Options, palette: treeAutumnPals[i] }, + winter: { ...tree2Options, palette: 1 }, + }))); + +export const trees3 = times(3, i => registerMix(n(`tree3-${i}`), + mixDrawSeasonal({ + summer: { ...tree3Options, palette: 0 }, + autumn: { ...tree3Options, palette: treeAutumnPals[i] }, + winter: { ...tree3Options, palette: 1 }, + }), + mixColliderRounded(-3, -2, 6, 4, 1))); + +export const tree1 = trees1[0]; +export const tree2 = trees2[0]; +export const tree3 = trees3[0]; + +export const [tree4] = createTree(n('tree4'), 31, 92, 12, { + stumpCollider: mixColliderRounded(-5, -1, 12, 6, 1, stumpsTall), + trunkCollider: mixColliderRounded(-5, -1, 12, 6, 1), + cover: rect(-20, -77, 42, 60), + variants: times(3, i => + ({ + stump: sprites.tree_4Stump0, + trunk: sprites.tree_4Trunk0, + crown: sprites.tree_4Crown0_0, + palette: 0, + paletteAutumn: treeAutumnPals[i], + paletteWinter: 1, + })), +}); + +export const [tree5, [tree5Stump]] = createTree(n('tree5'), 43, 128, 24, { + stumpCollider: mixColliderRounded(-8, -2, 16, 8, 2, stumpsTall), + trunkCollider: mixColliderRounded(-8, -2, 16, 8, 2), + cover: rect(-30, -106, 64, 80), + variants: times(3, i => + ({ + stump: sprites.tree_5Stump0, + trunk: sprites.tree_5Trunk0, + crown: sprites.tree_5Crown0_0, + palette: 0, + paletteAutumn: treeAutumnPals[i], + paletteWinter: 1, + })), +}); + +export const [tree, [treeStump1, treeStump2]] = createTree(n('tree'), 80, 162, 30, { + stumpCollider: mixColliderRounded(-16, -1, 32, 12, 4, stumpsTall), + trunkCollider: mixColliderRounded(-16, -1, 32, 12, 4), + cover: rect(-50, -135, 110, 120), + variants: flatten(times(3, i => [ + { + stump: sprites.tree_6Stump0, + stumpWinter: sprites.tree_6StumpWinter0, + trunk: sprites.tree_6Trunk0, + crown: sprites.tree_6Crown0_0, + webX: 0, webY: 0, spiderHeight: 19, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + { + stump: sprites.tree_6Stump1, + stumpWinter: sprites.tree_6StumpWinter1, + trunk: sprites.tree_6Trunk1, + crown: sprites.tree_6Crown0_1, + webX: -2, webY: 0, spiderHeight: 19, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + { + stump: sprites.tree_6Stump0, + stumpWinter: sprites.tree_6StumpWinter0, + trunk: sprites.tree_6Trunk0, + crown: sprites.tree_6Crown1_0, + webX: 0, webY: -4, spiderHeight: 27, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + { + stump: sprites.tree_6Stump1, + stumpWinter: sprites.tree_6StumpWinter1, + trunk: sprites.tree_6Trunk1, + crown: sprites.tree_6Crown1_1, + webX: -2, webY: -4, spiderHeight: 27, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + ])) +}); + +const pine1Options = { sprite: sprites.pine_1, dx: 7, dy: 18 }; +const pine2Options = { sprite: sprites.pine_2, dx: 10, dy: 35 }; + +export const pine1 = registerMix(n('pine1'), + mixDrawSeasonal({ + summer: { ...pine1Options, palette: 0 }, + autumn: { ...pine1Options, palette: 1 }, + winter: { ...pine1Options, palette: 2 }, + })); + +export const pine2 = registerMix(n('pine2'), + mixDrawSeasonal({ + summer: { ...pine2Options, palette: 0 }, + autumn: { ...pine2Options, palette: 1 }, + winter: { ...pine2Options, palette: 2 }, + }), + mixColliderRounded(-3, -2, 6, 4, 1)); + +export const [pine3] = createTree(n('pine3'), 25, 68, 2, { + stumpCollider: mixColliderRounded(-5, -1, 12, 6, 1, stumpsTall), + trunkCollider: mixColliderRounded(-5, -1, 12, 6, 1), + crownCollider: mixColliderRounded(-14, -6, 29, 12, 4), + cover: rect(-17, -41, 35, 40), + variants: [ + { stump: sprites.pine_3Stump0, crown: sprites.pine_3Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + ] +}); + +export const [pine4] = createTree(n('pine4'), 41, 95, 8, { + stumpCollider: mixColliderRounded(-5, 4, 11, 6, 1, stumpsTall), + trunkCollider: mixColliderRounded(-5, 4, 11, 6, 1), + crownCollider: mixColliderRounded(-23, -8, 46, 20, 6), + cover: rect(-23, -68, 46, 70), + variants: [ + { stump: sprites.pine_4Stump0, crown: sprites.pine_4Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + ] +}); + +export const [pine5] = createTree(n('pine5'), 53, 136, 5, { + stumpCollider: mixColliderRounded(-8, -3, 18, 10, 4, stumpsTall), + trunkCollider: mixColliderRounded(-8, -3, 18, 10, 4), + crownCollider: mixColliderRounded(-29, -12, 60, 25, 6), + cover: rect(-38, -95, 80, 100), + variants: [ + { stump: sprites.pine_5Stump0, crown: sprites.pine_5Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + ] +}); + +const xmasCrown: ColorShadow = { + color: sprites.christmastree.color, + shadow: sprites.pine_6Crown0_0.shadow, + palettes: sprites.christmastree.palettes, +}; + +export const [pine] = createTree(n('pine'), 75, 180, 17, { + stumpCollider: mixColliderRounded(-16, -1, 32, 14, 4, stumpsTall), + trunkCollider: mixColliderRounded(-16, -1, 32, 14, 4), + crownCollider: mixColliderRounded(-38, -21, 76, 31, 7), + cover: rect(-55, -120, 110, 133), + variants: [ + { stump: sprites.pine_6Stump0, crown: sprites.pine_6Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + { stump: sprites.pine_6Stump0, crown: xmasCrown, palette: 0 }, + ] +}); + +// tree helpers + +interface TreeVariant { + stump: PaletteRenderable; + stumpWinter?: PaletteRenderable; + trunk?: PaletteRenderable; + crown: PaletteRenderable; + webX?: number; + webY?: number; + spiderHeight?: number; + palette?: number; + paletteWinter?: number; + paletteAutumn?: number; +} + +interface TreeParams { + variants: TreeVariant[]; + cover: Rect; + stumpCollider?: MixinEntity; + trunkCollider?: MixinEntity; + crownCollider?: MixinEntity; +} + +type CreateTreeMethod = (x: number, y: number, v: number, hasWeb?: boolean, hasSpider?: boolean) => Entity[]; + +function createTree( + name: string, offsetX: number, offsetY: number, crownOffset: number, + { cover, stumpCollider, trunkCollider, crownCollider, variants }: TreeParams, +): [CreateTreeMethod, CreateEntityMethod[], (CreateEntityMethod | undefined)[], CreateEntityMethod[]] { + const trunkCover: MixinEntity = base => base.coverBounds = cover; + const crownCover = mixCover(cover.x, cover.y - crownOffset, cover.w, cover.h); + const crownFlags = mixServerFlags(ServerFlags.TreeCrown); + const crownMinimap = mixMinimap(0x386c4fff, rect(-1, -1, 3, 3), 2); + + const stumps = variants.map((v, i) => v.stump && registerMix(n(`${name}-stump-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, + autumn: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.stumpWinter || v.stump, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, + }), + stumpCollider, + mixOrder(1))); + + const stumpsTall = variants.map((v, i) => v.stump && registerMix(n(`${name}-stump-tall-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, + autumn: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.stumpWinter || v.stump, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, + }), + trunkCollider, + mixOrder(1))); + + const trunks = variants.map((v, i) => v.trunk && registerMix(n(`${name}-trunk-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, + autumn: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, + }), + trunkCover, + mixOrder(2))); + + const crowns = variants.map((v, i) => v.crown && registerMix(n(`${name}-crown-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.palette || 0 }, + autumn: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.paletteWinter || v.palette || 0 }, + }), + crownCollider, crownCover, crownFlags, crownMinimap)); + + const trees = variants.map((v, i) => ({ + stump: stumps[i], + stumpTall: stumpsTall[i], + trunk: trunks[i], + crown: crowns[i], + webX: v.webX, + webY: v.webY, + spiderHeight: v.spiderHeight, + })); + + function tree(x: number, y: number, v?: number, hasWeb?: boolean, hasSpider?: boolean): Entity[] { + const { stumpTall, trunk, crown, webX, webY, spiderHeight } = trees[v || 0]; + + return compact([ + stumpTall && stumpTall(x, y), + trunk && trunk(x, y), + crown && crown(x, y + (crownOffset / tileHeight)), + hasWeb ? web(x + (webX! / tileWidth), y + (webY! / tileHeight)) : undefined, + hasSpider ? spider(x - 1, y + 0.3, { height: spiderHeight!, time: Math.random() * 100 }) : undefined, + ]); + } + + return [tree, stumps, trunks, crowns]; +} + +export const stashEntities = [ + rose, cookie, cookiePony, letter, rope, +]; + +export const placeableEntities: { type: number; name: string; }[] = [ + { type: cushion1.type, name: 'Cushion' }, + { type: barrel.type, name: 'Barrel' }, + { type: box.type, name: 'Box' }, + { type: boxLanterns.type, name: 'Box of lanterns' }, + { type: boxFruits.type, name: 'Box of fruits' }, + { type: cookieTable2.type, name: 'Cookie table' }, + { type: table1.type, name: 'Small table' }, + { type: table2.type, name: 'Large table' }, + { type: table3.type, name: 'Long table' }, + { type: lanternOn.type, name: 'Lantern' }, + { type: pumpkin.type, name: 'Pumpkin' }, + { type: jackoOn.type, name: `Jack-o'-lantern (lit)` }, + { type: jackoOff.type, name: `Jack-o'-lantern (unlit)` }, + { type: crate1A.type, name: 'Large crate' }, + { type: crate3A.type, name: 'Small crate' }, + { type: crate2A.type, name: 'Lockbox' }, + { type: largeLeafedBush1.type, name: 'Large plant' }, + { type: largeLeafedBush3.type, name: 'Small plant' }, + { type: picture1.type, name: 'Picture (1)' }, + { type: picture2.type, name: 'Picture (2)' }, + { type: window1.type, name: 'Window' }, + { type: bookshelf.type, name: 'Bookshelf' }, + { type: rock.type, name: 'Rock' }, +]; + +export const fruits = [ + apple, apple2, appleGreen, appleGreen2, orange, pear, banana, + lemon, lime, carrotHeld, mango, grapesGreen[0], grapesPurple[0], +]; + +export const tools = [ + { type: saw.type, text: 'Saw: place & remove walls' }, + { type: broom.type, text: 'Broom: remove furniture' }, + { type: hammer.type, text: 'Hammer: place furniture\nuse [mouse wheel] to switch item' }, + { type: shovel.type, text: 'Shovel: change floor\nuse [mouse wheel] to switch floor type' }, +]; + +export const candies1Types = [candyCane1, candyCane2, cookie, cookiePony].map(e => e.type); +export const candies2Types = [cookie, cookiePony].map(e => e.type); + +if (DEVELOPMENT) { + if (pony.type !== PONY_TYPE) { + throw new Error(`Invalid pony type ${pony.type} !== ${PONY_TYPE}`); + } + + for (const { type } of entities) { + if (type === 0) + continue; + + const entity = createAnEntity(type, 0, 0, 0, {}, mockPaletteManager, defaultWorldState); + const name = getEntityTypeName(type); + + if (entity.colliders) { + const maxWidth = (hasFlag(entity.flags, EntityFlags.Movable) ? 1 : 4) * tileWidth; + const maxHeight = (hasFlag(entity.flags, EntityFlags.Movable) ? 1 : 5) * tileHeight; + + for (const { x, y, w, h } of entity.colliders) { + if ((x < -maxWidth || (x + w) > maxWidth || y < -maxHeight || (y + h) > maxHeight)) { + throw new Error(`Invalid entity "${name}": Collider too large ${JSON.stringify({ x, y, w, h })}`); + } + } + } + } +} diff --git a/src/ts/common/entityUtils.ts b/src/ts/common/entityUtils.ts new file mode 100644 index 0000000..7d56fa7 --- /dev/null +++ b/src/ts/common/entityUtils.ts @@ -0,0 +1,225 @@ +import { sort } from 'timsort'; +import { + Entity, EntityState, BodyAnimation, Point, EntityFlags, IMap, Pony, Says, EntityPlayerState, WorldMap +} from './interfaces'; +import { hasFlag, distance, pushUniq, setFlag } from './utils'; +import { stand, sit, lie, fly, flyBug, swim } from '../client/ponyAnimations'; +import { releasePony, isPony } from './pony'; +import { toScreenX, toScreenY } from './positionUtils'; +import { releasePalette } from '../graphics/paletteManager'; +import { rect } from './rect'; +import { addOrRemoveFromEntityList } from './worldMap'; +import { PONY_TYPE } from './constants'; +import { isStaticCollision } from './collision'; + +export function releaseEntity(entity: Entity) { + if (isPony(entity)) { + releasePony(entity); + } + + if (entity.palettes !== undefined) { + for (const palette of entity.palettes) { + releasePalette(palette); + } + } +} + +export function addChatBubble(map: WorldMap, entity: Entity, says: Says) { + entity.says = says; + pushUniq(map.entitiesWithChat, entity); +} + +export function updateEntityVelocity(map: WorldMap, entity: Entity, vx: number, vy: number) { + const wasMoving = isMoving(entity); + entity.vx = vx; + entity.vy = vy; + const isMovingNow = isMoving(entity); + addOrRemoveFromEntityList(map.entitiesMoving, entity, wasMoving, isMovingNow); +} + +export function compareEntities(a: Entity, b: Entity) { + return (toScreenY(a.y) - toScreenY(b.y)) + || (a.order - b.order) + || (b.id - a.id) + || (toScreenX(a.x) - toScreenX(b.x)) + || (toScreenY(a.z) - toScreenY(b.z) + ); +} + +export function sortEntities(entities: Entity[]) { + sort(entities, compareEntities); +} + +export function closestEntity(point: Point, entities: Entity[]): Entity | undefined { + return entities.reduce((best, entity) => distance(point, entity) < distance(point, best) ? entity : best, entities[0]); +} + +export function getBoopRect(entity: Entity) { + const right = hasFlag(entity.state, EntityState.FacingRight); + const sitting = isPonySitting(entity); + return rect(entity.x + (right ? 0.6 : -0.9) * (sitting ? 0.6 : 1), entity.y - 0.2, 0.3, 0.4); +} + +export function isMoving(entity: Entity) { + return entity.vx !== 0 || entity.vy !== 0; +} + +export function isDrawable(entity: Entity) { + return entity.type === PONY_TYPE || entity.draw !== undefined; +} + +export function canLand(entity: Entity, map: IMap) { + return !isStaticCollision(entity, map, true); +} + +export function canStand(entity: Entity, map: IMap) { + return !isPonyStanding(entity) && isPonyLandedOrCanLand(entity, map); +} + +export function canSit(entity: Entity, map: IMap) { + return !isPonySitting(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity); +} + +export function canLie(entity: Entity, map: IMap) { + return !isPonyLying(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity); +} + +export function entityInRange(entity: Entity, player: Entity) { + return (!entity.interactRange || distance(player, entity) < entity.interactRange); +} + +export function getInteractBounds(pony: Pony) { + const boundsWidth = 1; + const boundsHeight = 1; + const boundsOffset = 0.5 + (isPonySitting(pony) ? -0.3 : (isPonyLying(pony) ? -0.2 : 0)); + + return rect( + toScreenX(isFacingRight(pony) ? (pony.x + boundsOffset) : (pony.x - boundsOffset - boundsWidth)), + toScreenY(pony.y - boundsHeight / 2), + toScreenX(boundsWidth), + toScreenY(boundsHeight)); +} + +export const SIT_ON_BOUNDS_WIDTH = 1.2; +export const SIT_ON_BOUNDS_HEIGHT = 0.5; +export const SIT_ON_BOUNDS_OFFSET = 0.4; + +export function getSitOnBounds(pony: Pony) { + const width = SIT_ON_BOUNDS_WIDTH; + const height = SIT_ON_BOUNDS_HEIGHT; + const offset = isFacingRight(pony) ? -SIT_ON_BOUNDS_OFFSET : (SIT_ON_BOUNDS_OFFSET - SIT_ON_BOUNDS_WIDTH); + return rect(toScreenX(pony.x + offset), toScreenY(pony.y - SIT_ON_BOUNDS_HEIGHT / 2), toScreenX(width), toScreenY(height)); +} + +// pony state + +export function isIdleAnimation(animation: BodyAnimation) { + return animation === stand || animation === sit || animation === lie || animation === fly || + animation === flyBug || animation === swim; +} + +export function isIdle(pony: Pony) { + return !isMoving(pony) && isIdleAnimation(pony.ponyState.animation); +} + +export function canBoop(pony: Pony) { + return isIdle(pony); +} + +export function canBoop2(entity: Entity) { + return !isMoving(entity) && (isPonyStanding(entity) || isPonySitting(entity) || isPonyLying(entity) || isPonyFlying(entity)); +} + +// entity player state + +export function isHidden(entity: Entity) { + return (entity.playerState & EntityPlayerState.Hidden) !== 0; +} + +export function isIgnored(entity: Entity) { + return (entity.playerState & EntityPlayerState.Ignored) !== 0; +} + +export function isFriend(entity: Entity) { + return (entity.playerState & EntityPlayerState.Friend) !== 0; +} + +export function isInTheAir(entity: Entity) { + return isFlying(entity) && (entity.inTheAirDelay === undefined || entity.inTheAirDelay <= 0); +} + +// entity state + +export function isFlying(entity: Entity) { + return (entity.state & EntityState.Flying) !== 0; +} + +export function isFacingRight(entity: Entity) { + return (entity.state & EntityState.FacingRight) !== 0; +} + +export function hasHeadTurned(entity: Entity) { + return (entity.state & EntityState.HeadTurned) !== 0; +} + +export function isHeadFacingRight(entity: Entity) { + const headTurned = hasHeadTurned(entity); + const facingRight = isFacingRight(entity); + return facingRight ? !headTurned : headTurned; +} + +export function getPonyState(state: EntityState): EntityState { + return state & EntityState.PonyStateMask; +} + +export function setPonyState(state: EntityState, set: EntityState) { + state = (state & ~EntityState.PonyStateMask) | set; + state = setFlag(state, EntityState.Flying, set === EntityState.PonyFlying); + return state; +} + +export function isSittingState(state: EntityState) { + return getPonyState(state) === EntityState.PonySitting; +} + +export function isLyingState(state: EntityState) { + return getPonyState(state) === EntityState.PonyLying; +} + +export function isPonyWalking(entity: Entity) { + return getPonyState(entity.state) === EntityState.PonyWalking; +} + +export function isPonyTrotting(entity: Entity) { + return getPonyState(entity.state) === EntityState.PonyTrotting; +} + +export function isPonySitting(entity: Entity) { + return getPonyState(entity.state) === EntityState.PonySitting; +} + +export function isPonyStanding(entity: Entity) { + return getPonyState(entity.state) === EntityState.PonyStanding; +} + +export function isPonyLying(entity: Entity) { + return getPonyState(entity.state) === EntityState.PonyLying; +} + +export function isPonyFlying(entity: Entity) { + return getPonyState(entity.state) === EntityState.PonyFlying; +} + +export function isPonyLandedOrCanLand(entity: Entity, map: IMap) { + return !isPonyFlying(entity) || canLand(entity, map); +} + +// entity flags + +export function isDecal(entity: Entity) { + return (entity.flags & EntityFlags.Decal) !== 0; +} + +export function isCritter(entity: Entity) { + return (entity.flags & EntityFlags.Critter) !== 0; +} diff --git a/src/ts/common/errors.ts b/src/ts/common/errors.ts new file mode 100644 index 0000000..5f72aea --- /dev/null +++ b/src/ts/common/errors.ts @@ -0,0 +1,12 @@ +export const WEBGL_CREATION_ERROR = 'Failed to create WebGL context'; +export const ACCESS_ERROR = 'Access denied'; +export const ACCOUNT_ERROR = 'Invalid account'; +export const NOT_FOUND_ERROR = 'Not found'; +export const OFFLINE_ERROR = 'Server is offline'; +export const PROTECTION_ERROR = 'DDOS protection error, reload the page to continue'; +export const VERSION_ERROR = 'Invalid version'; +export const BROWSER_NOT_SUPPORTED_ERROR = 'Your browser is not supported'; +export const NAME_ERROR = 'Invalid name'; +export const CHARACTER_SAVING_ERROR = 'Error saving character'; +export const CHARACTER_LIMIT_ERROR = 'Character limit reached'; +export const NOT_AUTHENTICATED_ERROR = 'Not authenticated'; diff --git a/src/ts/common/expressionUtils.ts b/src/ts/common/expressionUtils.ts new file mode 100644 index 0000000..4a8e3e1 --- /dev/null +++ b/src/ts/common/expressionUtils.ts @@ -0,0 +1,352 @@ +import { escapeRegExp } from 'lodash'; +import { Muzzle, Eye, Expression, Iris, ExpressionExtra, Dict } from './interfaces'; +import { createPlainMap } from './utils'; + +const double = (items: string[]) => items.map(x => x + x); +const prefix = (items: string[], fix: string) => items.map(x => fix + x); +const suffix = (items: string[], fix: string) => items.map(x => x + fix); + +export const THREE_LETTER_WORDS = [ + 'ace', 'act', 'ama', 'amp', 'amo', 'amu', 'amy', 'ana', 'ane', 'and', 'ant', 'any', 'ape', 'app', 'apo', + 'apt', 'ava', 'ave', 'avo', 'awe', 'awn', 'awp', 'axe', + 'boa', 'bob', 'bod', 'bog', 'bon', 'boo', 'bop', 'bot', 'boy', 'bub', 'bud', 'bug', 'bup', 'but', 'bun', 'buy', + 'dad', 'doe', 'dog', 'dot', 'doy', 'dna', 'dub', 'dud', 'due', 'dun', 'dug', 'duo', 'dup', 'dva', 'dvd', + 'eco', 'ecu', 'eme', 'emu', 'emo', 'eon', 'end', 'eng', 'eva', 'eve', 'exe', 'exp', + 'gnu', 'goa', 'god', 'gog', 'gon', 'goo', 'got', 'gud', 'gut', 'gun', 'guv', 'guy', + 'nnn', 'nog', 'non', 'noo', 'nop', 'not', 'nun', 'nut', 'nub', + 'oca', 'omo', 'one', 'ooo', 'oot', 'ope', 'opt', 'oud', 'out', 'ova', 'owe', 'own', 'oxo', 'oxe', 'omg', + 'pay', 'pnp', 'pod', 'pon', 'poo', 'pop', 'pot', 'pov', 'ppp', 'pub', 'pud', 'pug', 'pup', 'pun', 'put', 'pvp', + 'qqq', 'que', 'qua', + 'tnt', 'ton', 'top', 'tod', 'toe', 'tog', 'too', 'toy', 'tub', 'tug', 'tun', 'twa', 'two', + 'uuu', 'una', 'und', 'uno', 'ump', 'upo', 'uva', + 'voe', 'voy', 'vpn', 'vug', 'vvv', + 'yay', 'yob', 'yod', 'yon', 'you', 'yup', +]; + +export const TWO_LETTER_WORDS = [ + 'ox', 'ex', 'by', 'my', 'up', 'of', 'if', 'me', 'ow', 'am', 'we', 'uh', 'um', 'be', 'em', 'bi', 'oh', + 'go', 'eh', 'ah', 'ye', 'ya', 'he', 'hi', 'ho', 'ha', 'yo', 'us', 'on', 'id', 'an', 'do', 'no', + 'as', 'at', 'it', 'is', 'or', 'so', 'to', 'pc', +]; + +const threeLetterWords = new RegExp(`^(${THREE_LETTER_WORDS.join('|')})$`); +const twoLetterWords = new RegExp(`^(${TWO_LETTER_WORDS.join('|')})$`); + +// vertical :) + +const smilesRight = [')', ']', '}', '>']; +const smilesLeft = ['(', '[', '{', '<', 'C', 'c']; +const flatBoth = ['|', 'i', 'l']; +const concernedBoth = ['/', '\\', 's', 'S', '?']; +const muzzlesBoth = [ + [Muzzle.Scrunch, 't', 'T', 'I'], + [Muzzle.Blep, 'P', 'p', 'd'], + [Muzzle.FlatBlep, 'b'], + [Muzzle.Flat, ...flatBoth], + [Muzzle.Concerned, ...concernedBoth], + [Muzzle.ConcernedOpen, '0', 'v'], + [Muzzle.ConcernedOpen2, 'O'], + [Muzzle.Oh, 'o'], + [Muzzle.Kiss, '*', 'x', 'X'], + [Muzzle.NeutralPant, 'L'], + [Muzzle.SmilePant, 'Q'], + [Muzzle.FrownOpen, 'V'], + [Muzzle.NeutralOpen2, 'u', 'n'], + [Muzzle.NeutralOpen3, 'U'], + [Muzzle.NeutralTeeth, ...double(flatBoth)], + [Muzzle.ConcernedTeeth, ...double(concernedBoth)], +]; + +export const muzzlesRight = createMap([ + ...muzzlesBoth, + [Muzzle.Smile, '3', ...smilesRight], + [Muzzle.Frown, ...smilesLeft], + [Muzzle.SmileOpen, 'D'], + [Muzzle.SmileOpen2, 'DD'], + [Muzzle.SmileOpen3, 'DDD'], + [Muzzle.SmileTeeth, ...double(smilesRight)], + [Muzzle.FrownTeeth, ...double(smilesLeft)], +]); + +export const muzzlesLeft = createMap([ + ...muzzlesBoth, + [Muzzle.Smile, ...smilesLeft], + [Muzzle.Frown, ...smilesRight], + [Muzzle.ConcernedOpen2, 'D'], + [Muzzle.ConcernedOpen3, 'DD', 'DDD'], + [Muzzle.SmileTeeth, ...double(smilesLeft)], + [Muzzle.FrownTeeth, ...double(smilesRight)], +]); + +const neutralEyes = [';', ':', '=', '%', '8']; + +const verticalEyesBoth = [ + [Eye.Neutral, ...neutralEyes], + [Eye.X, 'X', 'x'], + [Eye.Neutral3, 'B'], + [Eye.Lines, '|'], +]; + +export const verticalEyesRight = createMap([ + ...verticalEyesBoth, + [Eye.Angry, ...prefix(neutralEyes, '>')], + [Eye.Angry2, '>B'], + [Eye.Sad, ...prefix(neutralEyes, '<')], + [Eye.Sad2, '([ + ...verticalEyesBoth, + [Eye.Angry, ...suffix(neutralEyes, '<')], + [Eye.Sad, ...suffix(neutralEyes, '>')], + [Eye.Frown, ...suffix(neutralEyes, '|')], +]); + +// horizontal -_- + +export const horizontalMuzzles = createMap([ + [Muzzle.Smile, 'c', 'C', 'v', 'V', 'u', 'U', 'w', 'W', '👃'], + [Muzzle.SmilePant, 'Q', 'P'], + [Muzzle.Frown, 'n', 'm', '^'], + [Muzzle.Neutral, '-', '//'], + [Muzzle.NeutralPant, 'q', 'p'], + [Muzzle.Flat, '_'], + [Muzzle.Kiss, '.', ',', '*', 'x', 'X', '3'], + [Muzzle.Concerned, '~'], + [Muzzle.ConcernedOpen, 'o'], + [Muzzle.ConcernedOpen2, 'A', 'O', '0'], +]); + +const horizontalEyes = [ + [Eye.Neutral, `'`, '.', '0', '°', 'o', 'O', 'e', 'g', '9', '6', 'd', 'b'], + [Eye.Neutral4, '='], + [Eye.Closed, '-', 'v', 'V', 'u', 'U', 'y', 'Y'], + [Eye.ClosedHappy, 'n'], + [Eye.ClosedHappy2, '^'], + [Eye.Sad, 'q', 'Q', 'p', 'P', ';', ':', ','], + [Eye.Peaceful, 't', 'T'], + [Eye.Frown, 'ô', 'Ô', 'õ', 'Õ', 'ō', 'Ō', 'ŏ', 'Ŏ'], + [Eye.Frown2, 'a'], +]; + +export const horizontalEyesLeft = createMap([ + ...horizontalEyes, + [Eye.Neutral2, '>'], + [Eye.X, '<'], + [Eye.Sad, 'ò', 'Ò'], + [Eye.Angry, 'ó', 'Ó'], +]); + +export const horizontalEyesRight = createMap([ + ...horizontalEyes, + [Eye.Neutral2, '<'], + [Eye.X, '>'], + [Eye.Sad, 'ó', 'Ó'], + [Eye.Angry, 'ò', 'Ò'], +]); + +const horizontalIrises = createMap([ + [Iris.Up, '9'], + [Iris.UpLeft, 'e'], + [Iris.UpRight, 'g'], + [Iris.Right, '<', 'd'], + [Iris.Left, '>', 'b'], +]); + +const muzzleToEye: Eye[] = []; +muzzleToEye[Muzzle.Frown] = Eye.Sad; +muzzleToEye[Muzzle.FrownOpen] = Eye.Sad; +muzzleToEye[Muzzle.ConcernedOpen2] = Eye.Sad; +muzzleToEye[Muzzle.ConcernedOpen3] = Eye.Sad; + +const neutralToSmile: Muzzle[] = []; +neutralToSmile[Muzzle.ConcernedOpen] = Muzzle.SmileOpen2; +neutralToSmile[Muzzle.ConcernedOpen2] = Muzzle.SmileOpen3; + +function any(obj: object) { + return `(${Object.keys(obj).map(escapeRegExp).join('|')})`; +} + +const bigEyes = /[O0ÒÓÔÕŌŎQ]/; +const cryingEye = /[;pqPQTyY]/; +const tears = "(['`,]?)"; +const tearsRegex = /['`,]/; +const verticalRightRegex = new RegExp(`^${any(verticalEyesRight)}${tears}-?${any(muzzlesRight)}$`); +const verticalLeftRegex = new RegExp(`^${any(muzzlesLeft)}-?${tears}${any(verticalEyesLeft)}$`); +const horizontalRegex = new RegExp(`^${any(horizontalEyesRight)}(//)?${any(horizontalMuzzles)}(//)?${any(horizontalEyesLeft)}$`); + +function matchVertical( + text: string, regex: RegExp, flip: boolean, muzzleMap: Dict, eyesMap: Dict +): Expression | undefined { + if (/^([|]{2,}|BS|8x|x8|x-?x|\d+)$/i.test(text)) + return undefined; + + const match = regex.exec(text); + + if (!match) + return undefined; + + const eyesStr = flip ? match[3] : match[1]; + const muzzleStr = flip ? match[1] : match[3]; + const muzzle = muzzleMap[muzzleStr]; + const veye = eyesMap[eyesStr]; + const eye = veye === Eye.Neutral && !/[OV]/.test(muzzleStr) ? (muzzleToEye[muzzle] || veye) : veye; + const blink = /;/.test(eyesStr); + const tear = blink && muzzleToEye[muzzle] === Eye.Sad; + const left = tear ? (/[<>]/.test(eyesStr) ? eye : Eye.Sad2) : (blink && flip ? Eye.Closed : eye); + const right = tear ? (/[<>]/.test(eyesStr) ? eye : Eye.Sad2) : (blink && !flip ? Eye.Closed : eye); + const shocked = /8/.test(eyesStr); + const rightIris = shocked ? Iris.Shocked : Iris.Forward; + const leftIris = shocked ? Iris.Shocked : (/%/.test(eyesStr) ? Iris.Up : Iris.Forward); + const extra = (tearsRegex.test(match[2]) || tear) ? ExpressionExtra.Tears : ExpressionExtra.None; + + return { right, left, muzzle, rightIris, leftIris, extra }; +} + +function matchHorizontal(text: string): Expression | undefined { + if (/\.\.|--|vv|uu|qq|pp|nn|^\d+$/i.test(text)) { + return undefined; + } + + if (/[a-zA-Z][a-z][a-z]|[A-Z]{3}/.test(text)) { + const clear = text.replace(/[^a-z]/ig, '').toLowerCase(); + + if (clear.length === 3 && threeLetterWords.test(clear)) { + return undefined; + } + } + + if (/[a-z][a-z][.,*-]/i.test(text)) { + const clear = text.replace(/[^a-z]/ig, '').toLowerCase(); + + if (clear.length === 2 && twoLetterWords.test(clear)) { + return undefined; + } + } + + const match = horizontalRegex.exec(text); + + if (!match) { + return undefined; + } + + const [, rightStr, rightBlush, muzzleStr, leftBlush, leftStr] = match; + + if ((rightBlush || leftBlush) && rightBlush !== leftBlush) { + return undefined; + } + + const leftEye = horizontalEyesLeft[leftStr]; + const rightEye = horizontalEyesRight[rightStr]; + const muzzle = horizontalMuzzles[muzzleStr]; + const same = rightStr === leftStr; + const lookingToSide = same && /[<>]/.test(rightStr); + const shocked = bigEyes.test(leftStr) && bigEyes.test(rightStr) && rightStr !== '0' && leftStr !== '0'; + const lookingDown = (same && rightStr === '6') || (rightStr === 'b' && leftStr === 'd'); + const unamused = !lookingDown && same && rightStr === '-' && /[.,_]/.test(muzzleStr); + const left = (lookingToSide || (leftStr === 'o' && bigEyes.test(rightStr))) ? Eye.Neutral2 : leftEye; + const right = (lookingToSide || (rightStr === 'o' && bigEyes.test(leftStr))) ? Eye.Neutral2 : rightEye; + const blush = /[/][/]/.test(muzzleStr) || (rightBlush && rightBlush === leftBlush); + const cry = cryingEye.test(leftStr) || cryingEye.test(rightStr); + + return { + left: unamused ? Eye.Frown2 : left, + right: unamused ? Eye.Frown2 : right, + muzzle: same && (leftEye === Eye.ClosedHappy || leftEye === Eye.ClosedHappy2) ? (neutralToSmile[muzzle] || muzzle) : muzzle, + rightIris: lookingDown ? Iris.Down : (shocked ? Iris.Shocked : (horizontalIrises[rightStr] || Iris.Forward)), + leftIris: lookingDown ? Iris.Down : (shocked ? Iris.Shocked : (horizontalIrises[leftStr] || Iris.Forward)), + extra: (blush ? ExpressionExtra.Blush : ExpressionExtra.None) | (cry ? ExpressionExtra.Cry : ExpressionExtra.None), + }; +} + +export function expression( + right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None +): Expression { + return { right, left, muzzle, rightIris, leftIris, extra }; +} + +const constants = createPlainMap<() => Expression | undefined>({ + '^^': () => expression(Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Smile), + '))': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile), + '((': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Frown), + '>>': () => expression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left), + '<<': () => expression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right), + '🙂': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile), + '😵': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Up, Iris.Forward), + '😐': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Flat), + '😑': () => expression(Eye.Lines, Eye.Lines, Muzzle.Flat), + '😆': () => expression(Eye.X, Eye.X, Muzzle.SmileOpen), + '😟': () => expression(Eye.Sad, Eye.Sad, Muzzle.Neutral), + '😠': () => expression(Eye.Angry, Eye.Angry, Muzzle.Smile), + '🤔': () => expression(Eye.Neutral, Eye.Frown2, Muzzle.Kiss), + '😈': () => expression(Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Up, Iris.Forward), + '👿': () => expression(Eye.Angry, Eye.Angry, Muzzle.SmileTeeth), +}); + +function matchOther(text: string): Expression | undefined { + if (/^A{5,}\.*$/.test(text)) { + return expression(Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3, Iris.Shocked, Iris.Shocked); + } else if (/^a{5,}\.*$/.test(text)) { + return expression(Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3); + } else if (/^z{3,}\.*$/i.test(text)) { + return expression(Eye.Closed, Eye.Closed, Muzzle.Neutral); + } else { + return constants[text] && constants[text](); + } +} + +export function matchExpression(text: string): Expression | undefined { + if (/тот/ui.test(text)) { + return undefined; + } + + text = replaceRussian(text) + .replace(/D{4,}/, 'DDD') + .replace(/\\/g, '/') + .replace(/\/{3,}/g, '//'); + + return matchVertical(text, verticalRightRegex, false, muzzlesRight, verticalEyesRight) + || matchVertical(text, verticalLeftRegex, true, muzzlesLeft, verticalEyesLeft) + || matchHorizontal(text) + || matchOther(text); +} + +export function parseExpression(text: string): Expression | undefined { + const emoteMatch = /(?:^| )(\S+)\s*$/.exec(text); + const emote = emoteMatch && emoteMatch[1].trim(); + return emote ? matchExpression(emote) : undefined; +} + +function createMap(values: any[][]): Dict { + return values.reduce((obj: Dict, [exp, ...values]) => (values.forEach(v => obj[v] = exp), obj), Object.create(null)); +} + +const charMap = createPlainMap({ + 'З': '3', 'з': '3', 'Э': '3', 'э': '3', + 'А': 'A', 'а': 'a', 'Д': 'A', 'д': 'A', + 'В': 'B', 'в': 'B', + 'Г': 'L', + 'М': 'M', 'м': 'M', + 'О': 'O', 'о': 'o', + 'П': 'n', 'п': 'n', + 'Р': 'P', 'р': 'p', + 'С': 'C', 'с': 'c', + 'Т': 'T', 'т': 'T', + 'Х': 'X', 'х': 'x', + 'Ш': 'W', 'ш': 'w', + 'Ь': 'b', 'ь': 'b', + 'е': 'e', + 'у': 'y', 'У': 'Y', +}); + +const charRegex = new RegExp(`[${Object.keys(charMap).join('')}]`, 'g'); + +function mapChar(x: string) { + return charMap[x]; +} + +function replaceRussian(text: string): string { + return text.replace(charRegex, mapChar); +} diff --git a/src/ts/common/expressions.ts b/src/ts/common/expressions.ts new file mode 100644 index 0000000..ee3c5a5 --- /dev/null +++ b/src/ts/common/expressions.ts @@ -0,0 +1,295 @@ +import { Eye, Muzzle, Iris, ExpressionExtra } from './interfaces'; +import { THREE_LETTER_WORDS, TWO_LETTER_WORDS } from './expressionUtils'; + +type Result = undefined + | [Eye, Eye, Muzzle] + | [Eye, Eye, Muzzle, Iris, Iris] + | [Eye, Eye, Muzzle, Iris, Iris, ExpressionExtra]; + +export const expressions: [string, Result][] = [ + // invalid + ['', undefined], + ['a', undefined], + ['123', undefined], + [':::', undefined], + ['XDK', undefined], + ['fooXD', undefined], + [':) hey', undefined], + ['тот', undefined], + // in text + [' :) ', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['hi :)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + // horizontal (right) + [':-)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + [':)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['=)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + [':]', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + [':>', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + [':}', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + [':3', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['rus :з', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['rus :э', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + [':(', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + [':[', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + [':C', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + [':c', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + ['rus :С', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + ['rus :с', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + [':<', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + [':{', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + [':I', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]], + [':t', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]], + [':T', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]], + ['rus :Т', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]], + ['rus :т', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]], + [':P', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]], + [':p', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]], + [':d', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]], + [':b', [Eye.Neutral, Eye.Neutral, Muzzle.FlatBlep]], + ['rus :Р', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]], + ['rus :р', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]], + [':D', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen]], + [':DD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen2]], + [':DDD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen3]], + [':DDDDD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen3]], + [':O', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen2]], + [':0', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]], + [':o', [Eye.Neutral, Eye.Neutral, Muzzle.Oh]], + [':|', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + [':l', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + [':i', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + [':v', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]], + [':V', [Eye.Neutral, Eye.Neutral, Muzzle.FrownOpen]], + [':u', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]], + [':n', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]], + [':U', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen3]], + [':*', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]], + [':x', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]], + [':X', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]], + [':/', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]], + [':\\', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]], + [':S', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]], + [':s', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]], + [':?', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]], + ['>:(', [Eye.Angry, Eye.Angry, Muzzle.Frown]], + ['>:<', [Eye.Angry, Eye.Angry, Muzzle.Frown]], + ['<:>', [Eye.Sad, Eye.Sad, Muzzle.Smile]], + ['XD', [Eye.X, Eye.X, Muzzle.SmileOpen]], + ['xD', [Eye.X, Eye.X, Muzzle.SmileOpen]], + [':L', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralPant]], + [':Q', [Eye.Neutral, Eye.Neutral, Muzzle.SmilePant]], + ['B)', [Eye.Neutral3, Eye.Neutral3, Muzzle.Smile]], + ['8)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Shocked, Iris.Shocked]], + ['>8)', [Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Shocked, Iris.Shocked]], + ['<:)', [Eye.Sad, Eye.Sad, Muzzle.Smile]], + ['>B)', [Eye.Angry2, Eye.Angry2, Muzzle.Smile]], + [';(', [Eye.Angry, Eye.Angry, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]], + ['%)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Up]], + [`c':`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]], + [`:')`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]], + [`:'(`, [Eye.Sad, Eye.Sad, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]], + [`=,)`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]], + ['=`)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]], + // TODO: :@ :y :'9 + // horizontal (left) + ['(-:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['|:', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + ['(:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['[:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['c:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['C:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['rus с:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['rus С:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['):', [Eye.Sad, Eye.Sad, Muzzle.Frown]], + ['D:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen2]], + ['DD:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen3]], + ['DDDDD:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen3]], + ['D:<', [Eye.Angry, Eye.Angry, Muzzle.ConcernedOpen2]], + ['D8<', [Eye.Angry, Eye.Angry, Muzzle.ConcernedOpen2, Iris.Shocked, Iris.Shocked]], + ['):<', [Eye.Angry, Eye.Angry, Muzzle.Frown]], + ['v:', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]], + ['/:', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]], + ['(:>', [Eye.Sad, Eye.Sad, Muzzle.Smile]], + ['(:|', [Eye.Frown, Eye.Frown, Muzzle.Smile]], + ['(|', [Eye.Lines, Eye.Lines, Muzzle.Smile]], + ['((:', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]], + ['[[:', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]], + ['||:', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralTeeth]], + [')):', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]], + ['(;', [Eye.Neutral, Eye.Closed, Muzzle.Smile]], + // horizontal (invalid) + ['||', undefined], + ['|||', undefined], + ['>||', undefined], + ['>xD', undefined], + ['(X<', undefined], + ['x-x', undefined], + // vertical + ['-_-', [Eye.Frown2, Eye.Frown2, Muzzle.Flat]], + ['-.-', [Eye.Frown2, Eye.Frown2, Muzzle.Kiss]], + ['-,-', [Eye.Frown2, Eye.Frown2, Muzzle.Kiss]], + ['^_^', [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Flat]], + ['-_^', [Eye.Closed, Eye.ClosedHappy2, Muzzle.Flat]], + ['o_O', [Eye.Neutral2, Eye.Neutral, Muzzle.Flat]], + ['0_o', [Eye.Neutral, Eye.Neutral2, Muzzle.Flat]], + ['o_o', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + ['o,o', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]], + ['ono', [Eye.Neutral, Eye.Neutral, Muzzle.Frown]], + ['O_O', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked]], + ['OoO', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen, Iris.Shocked, Iris.Shocked]], + ['0_0', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + ['°_°', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + ['0.0', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]], + ['._.', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + [',_,', [Eye.Sad, Eye.Sad, Muzzle.Flat]], + ['v_V', [Eye.Closed, Eye.Closed, Muzzle.Flat]], + ['u_U', [Eye.Closed, Eye.Closed, Muzzle.Flat]], + ['n_n', [Eye.ClosedHappy, Eye.ClosedHappy, Muzzle.Flat]], + ['>_<', [Eye.X, Eye.X, Muzzle.Flat, Iris.Left, Iris.Right]], + ['>c<', [Eye.X, Eye.X, Muzzle.Smile, Iris.Left, Iris.Right]], + [`'c'`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['-C-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['rus -с-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['-v-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['-V-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['-U-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['-u-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['-w-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + ['-W-', [Eye.Closed, Eye.Closed, Muzzle.Smile]], + [`-👃-`, [Eye.Closed, Eye.Closed, Muzzle.Smile]], + [`'_'`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + [`-*-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]], + [`-x-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]], + [`-X-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]], + [`>x<`, [Eye.X, Eye.X, Muzzle.Kiss, Iris.Left, Iris.Right]], + [`-o-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen]], + [`-O-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]], + [`-0-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]], + [`^o^`, [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.SmileOpen2]], + [`^O^`, [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.SmileOpen3]], + [`-n-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]], + [`-m-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]], + [`-^-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]], + [`-~-`, [Eye.Closed, Eye.Closed, Muzzle.Concerned]], + [`-3-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]], + [`rus -з-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]], + [`rus -э-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]], + [`-q-`, [Eye.Closed, Eye.Closed, Muzzle.NeutralPant]], + [`-p-`, [Eye.Closed, Eye.Closed, Muzzle.NeutralPant]], + [`-P-`, [Eye.Closed, Eye.Closed, Muzzle.SmilePant]], + [`-Q-`, [Eye.Closed, Eye.Closed, Muzzle.SmilePant]], + [`-A-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]], + [`q-q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`p-p`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`p-q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`rus р-р`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`;-;`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`:-:`, [Eye.Sad, Eye.Sad, Muzzle.Neutral]], + [`P-P`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`rus Р-Р`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`t-t`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Neutral]], + [`Т_Т`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Flat, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`rus Т_т`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Flat, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`Q-Q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Shocked, Iris.Shocked, ExpressionExtra.Cry]], + [`y-y`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`Y-Y`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`у-у`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`У-У`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]], + [`ò_ó`, [Eye.Angry, Eye.Angry, Muzzle.Flat]], + [`ó_ò`, [Eye.Sad, Eye.Sad, Muzzle.Flat]], + [`ô_ô`, [Eye.Frown, Eye.Frown, Muzzle.Flat]], + [`õ_õ`, [Eye.Frown, Eye.Frown, Muzzle.Flat]], + [`ō_ō`, [Eye.Frown, Eye.Frown, Muzzle.Flat]], + [`ŏ_ŏ`, [Eye.Frown, Eye.Frown, Muzzle.Flat]], + [`Ò_Ó`, [Eye.Angry, Eye.Angry, Muzzle.Flat, Iris.Shocked, Iris.Shocked]], + [`Ô_Ô`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]], + [`Õ_Õ`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]], + [`Ō_Ō`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]], + [`Ŏ_Ŏ`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]], + [`=_=`, [Eye.Neutral4, Eye.Neutral4, Muzzle.Flat]], + [`a_a`, [Eye.Frown2, Eye.Frown2, Muzzle.Flat]], + [`e_e`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpLeft, Iris.UpLeft]], + [`е_е`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpLeft, Iris.UpLeft]], + [`g_g`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpRight, Iris.UpRight]], + [`9_9`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Up, Iris.Up]], + [`6_9`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Forward, Iris.Up]], + ['>_>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left]], + ['<_<', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right]], + ['<_>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left]], + ['d_d', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Right, Iris.Right]], + ['b_b', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Left, Iris.Left]], + ['twO', [Eye.Peaceful, Eye.Neutral, Muzzle.Smile]], + ['o//o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]], + ['o/////o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]], + ['o\\\\o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]], + ['o\\\\\\o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]], + ['>//<', [Eye.X, Eye.X, Muzzle.Neutral, Iris.Left, Iris.Right, ExpressionExtra.Blush]], + ['-//v//-', [Eye.Closed, Eye.Closed, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]], + ['-///v///-', [Eye.Closed, Eye.Closed, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]], + [';//v//;', [Eye.Sad, Eye.Sad, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush | ExpressionExtra.Cry]], + [`6_6`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Down, Iris.Down]], + ['6.6', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss, Iris.Down, Iris.Down]], + ['bcd', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Down, Iris.Down]], + // TODO: o-o' o-o' ~_~ @_@ o=o oyo *_* (amazed) -_-/ -_-\ D_D + // vertical (short) + ['^^', [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Smile]], + ['))', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['((', [Eye.Neutral, Eye.Neutral, Muzzle.Frown]], + ['<<', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right]], + ['>>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left]], + // vertical (invalid) + ...[ + '---', '...', '000', 'QQQ', 'One', 'Up.', 'UP.', + ...THREE_LETTER_WORDS, + ...THREE_LETTER_WORDS.map(x => x.toUpperCase()), + ...TWO_LETTER_WORDS.map(x => x + '.'), + ...TWO_LETTER_WORDS.map(x => x + ','), + ...TWO_LETTER_WORDS.map(x => x + '-'), + ...TWO_LETTER_WORDS.map(x => x + '*'), + ].map(x => [x, undefined] as [string, any]), + ['BS', undefined], + ['x8', undefined], + ['8x', undefined], + ['xx', undefined], + ['-//c-', undefined], + ['-c//-', undefined], + ['030', undefined], + ['80', undefined], + // other + [`aaaaa`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3]], + [`AAAAAA`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3, Iris.Shocked, Iris.Shocked]], + [`aaaaa...`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3]], + [`zzz`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]], + [`ZZZZZ`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]], + [`zzz...`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]], + // emoji + ['🙂', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]], + ['😵', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Up, Iris.Forward]], + ['😐', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]], + ['😑', [Eye.Lines, Eye.Lines, Muzzle.Flat]], + ['😆', [Eye.X, Eye.X, Muzzle.SmileOpen]], + ['😟', [Eye.Sad, Eye.Sad, Muzzle.Neutral]], + ['😠', [Eye.Angry, Eye.Angry, Muzzle.Smile]], + ['🤔', [Eye.Neutral, Eye.Frown2, Muzzle.Kiss]], + ['😈', [Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Up, Iris.Forward]], + ['👿', [Eye.Angry, Eye.Angry, Muzzle.SmileTeeth]], + // unsafe faces + [':L', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralPant]], + [':Q', [Eye.Neutral, Eye.Neutral, Muzzle.SmilePant]], + // safe replacements + [':u', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]], + [':DD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen2]], +]; diff --git a/src/ts/common/filterUtils.ts b/src/ts/common/filterUtils.ts new file mode 100644 index 0000000..8288e64 --- /dev/null +++ b/src/ts/common/filterUtils.ts @@ -0,0 +1,164 @@ +import { fromPairs } from 'lodash'; +import { matchRomaji, replaceRomaji } from '../client/clientUtils'; +import { flatten } from './utils'; + +const MAX_REPEATS = 16; // needs to be even for emoji + +export const ipRegexText = '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})'; +export const ipExceptionRegex = /\d\.\d\.\d\.\d/ui; +export const urlExceptionRegex = /^(battle|paint|f(im|an)fiction)\.net$/ui; +export const urlRegexTexts = [ + 'https?:?//\\S+', + '\\bwww\\.[^. ]\\S+', + '\\S+[^. ]\\. *(c[o0]m|net)\\b', + '\\S+[^. ] *\\.(c[o0]m|net)\\b', + '(^| )[a-z][a-z0-9]{2,}[.,][a-z]{2,3}(/[a-z0-9_?=+-]+)+\\b', +]; + +export function trimRepeatedLetters(test: string): string { + if (test.length > MAX_REPEATS && (/^.?(.)\1+$/u.test(test) || /^.?(..)\1+$/u.test(test))) { + return test.substr(0, MAX_REPEATS) + '…'; + } else { + return test; + } +} + +function createCharacterMap(data: string[][]): { [key: string]: string; } { + const mappings = data.map(([to, from]) => from.split(/ /g).map(x => [x, to])); + return fromPairs(flatten(mappings)); +} + +const characters = createCharacterMap([ + [`'`, 'Ъ ъ Ь ь'], + ['a', 'á ă ắ ặ ằ ẳ ẵ ǎ â ấ ậ ầ ẩ ẫ ä ǟ ȧ ǡ ạ ȁ à ả ȃ ā ą ᶏ ẚ å ǻ ḁ ⱥ ã ɐ ₐ А а @ α'], + ['A', 'Á Ă Ắ Ặ Ằ Ẳ Ẵ Ǎ  Ấ Ậ Ầ Ẩ Ẫ Ä Ǟ Ȧ Ǡ Ạ Ȁ À Ả Ȃ Ā Ą Å Ǻ Ḁ Ⱥ à Ɐ ᴀ'], + ['aa', 'ꜳ'], + ['AA', 'Ꜳ'], + ['ae', 'æ ǽ ǣ ᴂ'], + ['AE', 'Æ Ǽ Ǣ ᴁ'], + ['ao', 'ꜵ'], + ['AO', 'Ꜵ'], + ['au', 'ꜷ'], + ['AU', 'Ꜷ'], + ['av', 'ꜹ ꜻ'], + ['AV', 'Ꜹ Ꜻ'], + ['ay', 'ꜽ'], + ['AY', 'Ꜽ'], + ['b', 'ḃ ḅ ɓ ḇ ᵬ ᶀ ƀ ƃ б'], + ['B', 'Ḃ Ḅ Ɓ Ḇ Ƀ Ƃ ʙ ᴃ Б'], + ['c', 'ć č ç ḉ ĉ ɕ ċ ƈ ȼ ↄ ꜿ'], + ['C', 'Ć Č Ç Ḉ Ĉ Ċ Ƈ Ȼ Ꜿ ᴄ'], + ['ch', 'ч'], + ['CH', 'Ч'], + ['d', 'ď ḑ ḓ ȡ ḋ ḍ ɗ ᶑ ḏ ᵭ ᶁ đ ɖ ƌ ꝺ д'], + ['D', 'Ď Ḑ Ḓ Ḋ Ḍ Ɗ Ḏ Dz Dž Đ Ƌ Ꝺ ᴅ Д'], + ['dz', 'dz dž'], + ['DZ', 'DZ DŽ'], + ['e', 'é ĕ ě ȩ ḝ ê ế ệ ề ể ễ ḙ ë ė ẹ ȅ è ẻ ȇ ē ḗ ḕ ⱸ ę ᶒ ɇ ẽ ḛ ɛ ᶓ ɘ ǝ ₑ е э ε'], + ['E', 'É Ĕ Ě Ȩ Ḝ Ê Ế Ệ Ề Ể Ễ Ḙ Ë Ė Ẹ Ȅ È Ẻ Ȇ Ē Ḗ Ḕ Ę Ɇ Ẽ Ḛ Ɛ Ǝ ᴇ ⱻ Е Э'], + ['et', 'ꝫ'], + ['ET', 'Ꝫ'], + ['f', 'ḟ ƒ ᵮ ᶂ ꝼ ф'], + ['F', 'Ḟ Ƒ Ꝼ ꜰ Ф'], + ['ff', 'ff'], + ['ffi', 'ffi'], + ['ffl', 'ffl'], + ['fi', 'fi'], + ['fl', 'fl'], + ['g', 'ǵ ğ ǧ ģ ĝ ġ ɠ ḡ ᶃ ǥ ᵹ ɡ ᵷ г'], + ['G', 'Ǵ Ğ Ǧ Ģ Ĝ Ġ Ɠ Ḡ Ǥ Ᵹ ɢ ʛ Г'], + ['h', 'ḫ ȟ ḩ ĥ ⱨ ḧ ḣ ḥ ɦ ẖ ħ ɥ ʮ ʯ х'], + ['H', 'Ḫ Ȟ Ḩ Ĥ Ⱨ Ḧ Ḣ Ḥ Ħ ʜ Х'], + ['hv', 'ƕ'], + ['i', 'ı í ĭ ǐ î ï ḯ ị ȉ ì ỉ ȋ ī į ᶖ ɨ ĩ ḭ ᴉ ᵢ й ы и ι'], + ['I', 'Í Ĭ Ǐ Î Ï Ḯ İ Ị Ȉ Ì Ỉ Ȋ Ī Į Ɨ Ĩ Ḭ ɪ Й Ы И'], + ['ij', 'ij'], + ['IJ', 'IJ'], + ['is', 'ꝭ'], + ['IS', 'Ꝭ'], + ['j', 'ȷ ɟ ʄ ǰ ĵ ʝ ɉ ⱼ'], + ['J', 'Ĵ Ɉ ᴊ'], + ['k', 'ḱ ǩ ķ ⱪ ꝃ ḳ ƙ ḵ ᶄ ꝁ ꝅ ʞ к'], + ['K', 'Ḱ Ǩ Ķ Ⱪ Ꝃ Ḳ Ƙ Ḵ Ꝁ Ꝅ ᴋ К'], + ['l', 'ĺ ƚ ɬ ľ ļ ḽ ȴ ḷ ḹ ⱡ ꝉ ḻ ŀ ɫ ᶅ ɭ ł ꞁ л'], + ['L', 'Ĺ Ƚ Ľ Ļ Ḽ Ḷ Ḹ Ⱡ Ꝉ Ḻ Ŀ Ɫ Lj Ł Ꞁ ʟ ᴌ Л'], + ['lj', 'lj'], + ['LJ', 'LJ'], + ['m', 'ḿ ṁ ṃ ɱ ᵯ ᶆ ɯ ɰ м'], + ['M', 'Ḿ Ṁ Ṃ Ɱ Ɯ ᴍ М'], + ['n', 'ń ň ņ ṋ ȵ ṅ ṇ ǹ ɲ ṉ ƞ ᵰ ᶇ ɳ ñ н η'], + ['N', 'Ń Ň Ņ Ṋ Ṅ Ṇ Ǹ Ɲ Ṉ Ƞ Nj Ñ ɴ ᴎ Н'], + ['nj', 'nj'], + ['NJ', 'NJ'], + ['o', 'ɵ ó ŏ ǒ ô ố ộ ồ ổ ỗ ö ȫ ȯ ȱ ọ ő ȍ ò ỏ ơ ớ ợ ờ ở ỡ ȏ ꝋ ꝍ ⱺ ō ṓ ṑ ǫ ǭ ø ǿ õ ṍ ṏ ȭ ɔ ᶗ ᴑ ᴓ ₒ о'], + ['O', 'Ó Ŏ Ǒ Ô Ố Ộ Ồ Ổ Ỗ Ö Ȫ Ȯ Ȱ Ọ Ő Ȍ Ò Ỏ Ơ Ớ Ợ Ờ Ở Ỡ Ȏ Ꝋ Ꝍ Ō Ṓ Ṑ Ɵ Ǫ Ǭ Ø Ǿ Õ Ṍ Ṏ Ȭ Ɔ ᴏ ᴐ О'], + ['oe', 'ᴔ œ'], + ['OE', 'Œ ɶ'], + ['oi', 'ƣ'], + ['OI', 'Ƣ'], + ['oo', 'ꝏ'], + ['OO', 'Ꝏ'], + ['ou', 'ȣ'], + ['OU', 'Ȣ ᴕ'], + ['p', 'ṕ ṗ ꝓ ƥ ᵱ ᶈ ꝕ ᵽ ꝑ п'], + ['P', 'Ṕ Ṗ Ꝓ Ƥ Ꝕ Ᵽ Ꝑ ᴘ П'], + ['q', 'ꝙ ʠ ɋ ꝗ'], + ['Q', 'Ꝙ Ꝗ'], + ['r', 'ꞃ ŕ ř ŗ ṙ ṛ ṝ ȑ ɾ ᵳ ȓ ṟ ɼ ᵲ ᶉ ɍ ɽ ɿ ɹ ɻ ɺ ⱹ ᵣ р'], + ['R', 'Ꞃ Ŕ Ř Ŗ Ṙ Ṛ Ṝ Ȑ Ȓ Ṟ Ɍ Ɽ ʁ ʀ ᴙ ᴚ Р ®'], + ['s', 'ꞅ ſ ẜ ẛ ẝ ś ṥ š ṧ ş ŝ ș ṡ ṣ ṩ ʂ ᵴ ᶊ ȿ с'], + ['S', 'Ꞅ Ś Ṥ Š Ṧ Ş Ŝ Ș Ṡ Ṣ Ṩ ꜱ С $'], + ['sch', 'щ'], + ['SCH', 'Щ'], + ['sh', 'ш'], + ['SH', 'Ш'], + ['ss', 'ß'], + ['st', 'st'], + ['t', 'ꞇ ť ţ ṱ ț ȶ ẗ ⱦ ṫ ṭ ƭ ṯ ᵵ ƫ ʈ ŧ ʇ т'], + ['T', 'Ꞇ Ť Ţ Ṱ Ț Ⱦ Ṫ Ṭ Ƭ Ṯ Ʈ Ŧ ᴛ Т'], + ['th', 'ᵺ'], + ['ts', 'ц'], + ['TS', 'Ц'], + ['tz', 'ꜩ'], + ['TZ', 'Ꜩ'], + ['u', 'ᴝ ú ŭ ǔ û ṷ ü ǘ ǚ ǜ ǖ ṳ ụ ű ȕ ù ủ ư ứ ự ừ ử ữ ȗ ū ṻ ų ᶙ ů ũ ṹ ṵ ᵤ у'], + ['U', 'Ú Ŭ Ǔ Û Ṷ Ü Ǘ Ǚ Ǜ Ǖ Ṳ Ụ Ű Ȕ Ù Ủ Ư Ứ Ự Ừ Ử Ữ Ȗ Ū Ṻ Ų Ů Ũ Ṹ Ṵ ᴜ У'], + ['ue', 'ᵫ'], + ['um', 'ꝸ'], + ['v', 'ʌ ⱴ ꝟ ṿ ʋ ᶌ ⱱ ṽ ᵥ в'], + ['V', 'Ʌ Ꝟ Ṿ Ʋ Ṽ ᴠ В'], + ['vy', 'ꝡ'], + ['VY', 'Ꝡ'], + ['w', 'ʍ ẃ ŵ ẅ ẇ ẉ ẁ ⱳ ẘ'], + ['W', 'Ẃ Ŵ Ẅ Ẇ Ẉ Ẁ Ⱳ ᴡ'], + ['x', 'ẍ ẋ ᶍ ₓ'], + ['X', 'Ẍ Ẋ'], + ['y', 'ʎ ý ŷ ÿ ẏ ỵ ỳ ƴ ỷ ỿ ȳ ẙ ɏ ỹ'], + ['Y', 'Ý Ŷ Ÿ Ẏ Ỵ Ỳ Ƴ Ỷ Ỿ Ȳ Ɏ Ỹ ʏ'], + ['ya', 'я'], + ['Ya', 'Я'], + ['yo', 'ё'], + ['YO', 'Ё'], + ['yu', 'ю'], + ['YU', 'Ю'], + ['z', 'ź ž ẑ ʑ ⱬ ż ẓ ȥ ẕ ᵶ ᶎ ʐ ƶ ɀ з'], + ['Z', 'Ź Ž Ẑ Ⱬ Ż Ẓ Ȥ Ẕ Ƶ ᴢ З'], + ['zh', 'ж'], + ['ZH', 'Ж'], +]); + +const nonAscii = /[^A-Za-z0-9]/g; + +export function latinize(text: string): string { + return text + .replace(matchRomaji, replaceRomaji) + .replace(nonAscii, x => characters[x] || x); +} + +export function latinize2(name: string): string { + return latinize(name + .replace(/[ǫ]/ui, 'q') + .replace(/[с]/ui, 'c') + .replace(/[н]|\|-\|/ui, 'h') + .replace(/[лпий]/ui, 'n')); +} diff --git a/src/ts/common/interfaces.ts b/src/ts/common/interfaces.ts new file mode 100644 index 0000000..f14bdaa --- /dev/null +++ b/src/ts/common/interfaces.ts @@ -0,0 +1,1765 @@ +import { Texture2D } from '../graphics/webgl/texture2d'; +import { WHITE, SHADOW_COLOR } from './colors'; +import { AnimationPlayer } from './animationPlayer'; +import { Animator } from './animator'; + +export type Log = (...args: any[]) => void; +export type Apply = (func: () => void) => void; +export type Matrix4 = Float32Array; +export type Matrix2D = Float32Array; + +export interface Dict { + [key: string]: T; +} + +export const enum Season { + Summer = 1, + Autumn = 2, + Winter = 4, + Spring = 8, +} + +export const enum Holiday { + None, + Christmas, + Halloween, + StPatricks, + Easter, +} + +export const enum Weather { + None, + Rain, +} + +export const enum MapType { + None, + Island, + House, + Cave, +} + +export const enum MapFlags { + None = 0, + EditableWalls = 1, + EditableEntities = 2, + EditableTiles = 4, + EdibleGrass = 8, +} + +export const enum NotificationFlags { + None = 0, + Ok = 1, + Yes = 2, + No = 4, + Accept = 8, + Reject = 16, + Supporter = 32, + Ignore = 64, + NameBad = 128, +} + +export interface Notification { + id: number; + message: string; + note?: string; + flags: NotificationFlags; + pony: Pony; + open: boolean; + fresh: boolean; +} + +export interface Palette { + x: number; + y: number; + u: number; + v: number; + refs: number; + colors: Uint32Array; +} + +export interface JoinResponse { + token?: string; + alert?: string; +} + +export interface PaletteManager { + add(colors: number[]): Palette; + addArray(colors: Uint32Array): Palette; + init(gl: WebGLRenderingContext): void; +} + +export type Batch = Float32Array; + +export interface SpriteBatchBase { + globalAlpha: number; + crop(x: number, y: number, w: number, h: number): void; + clearCrop(): void; + save(): void; + restore(): void; + translate(x: number, y: number): void; + scale(x: number, y: number): void; + rotate(angle: number): void; + multiplyTransform(mat: Matrix2D): void; + drawBatch(batch: Batch): void; + startBatch(): void; + finishBatch(): Batch | undefined; + releaseBatch(batch: Batch): void; +} + +export interface SpriteBatchCommons extends SpriteBatchBase { + palette: boolean; + depth?: number; + drawRect(color: number, x: number, y: number, w: number, h: number): void; +} + +export interface SpriteBatch extends SpriteBatchCommons { + drawImage( + color: number, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ): void; + drawSprite(sprite: Sprite | undefined, color: number, x: number, y: number): void; +} + +export interface PaletteSpriteBatch extends SpriteBatchCommons { + drawImage( + type: number, color: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ): void; + drawSprite(sprite: Sprite, color: number, palette: Palette | undefined, x: number, y: number): void; +} + +export function isPaletteSpriteBatch(batch: SpriteBatch | PaletteSpriteBatch): batch is PaletteSpriteBatch { + return batch.palette; +} + +export interface ServerFeatureFlags { + test?: boolean; // test server + objects?: boolean; // editor + // more flags here ... +} + +export const enum ServerFlags { + None = 0, + TreeCrown = 1, + DoNotSave = 2, + Seasonal = 4, +} + +export const enum EntityFlags { + None = 0, + Movable = 1, + Decal = 2, + Critter = 4, + Usable = 8, + Debug = 16, + StaticY = 32, // ignore ground Y + CanCollide = 64, // can this object collide with other objects + CanCollideWith = 128, // can other objects collide with this one + Interactive = 256, + Light = 512, + OnOff = 1024, + Bobbing = 2048, + IgnoreTool = 4096, +} + +export const enum EntityState { + None = 0, + + // general + Flying = 1, + FacingRight = 2, + + // lights + On = 4, + + // other + Editable = 8, + + // pony + HeadTurned = 4, + Magic = 8, + // CanFly ?, // Or in flags ? + + // pony state + PonyStanding = 0 << 4, + PonyWalking = 1 << 4, + PonyTrotting = 2 << 4, + PonySitting = 3 << 4, + PonyLying = 4 << 4, + PonyFlying = 5 << 4, + PonyStateMask = 0xf << 4, + + // animated entities + AnimationMask = 0xf << 4, + + // max 0xff +} + +export const enum EntityPlayerState { + None = 0, + Ignored = 1, + Hidden = 2, + Friend = 4, + + // max 0xff +} + +export function getAnimationFromEntityState(state: EntityState) { + return (state & EntityState.AnimationMask) >> 4; +} + +export function setAnimationToEntityState(state: EntityState, animation: number) { + return (state & ~EntityState.AnimationMask) | (animation << 4); +} + +export const enum MessageType { + Chat = 0, + System = 1, + Admin = 2, + Mod = 3, + Party = 4, + Thinking = 5, + PartyThinking = 6, + Announcement = 7, + PartyAnnouncement = 8, + Supporter1 = 9, + Supporter2 = 10, + Supporter3 = 11, + Dismiss = 12, + Whisper = 13, + WhisperTo = 14, + WhisperAnnouncement = 15, + WhisperToAnnouncement = 16, +} + +export function toMessageType(type: MessageType) { + if (type === MessageType.WhisperAnnouncement) { + return MessageType.WhisperToAnnouncement; + } else { + return MessageType.WhisperTo; + } +} + +export function toAnnouncementMessageType(type: ChatType) { + switch (type) { + case ChatType.Party: return MessageType.PartyAnnouncement; + case ChatType.Whisper: return MessageType.WhisperAnnouncement; + default: return MessageType.Announcement; + } +} + +export function isWhisper(type: MessageType) { + return type === MessageType.Whisper || + type === MessageType.WhisperAnnouncement; +} + +export function isWhisperTo(type: MessageType) { + return type === MessageType.WhisperTo || + type === MessageType.WhisperToAnnouncement; +} + +export function isThinking(type: MessageType) { + return type === MessageType.Thinking || + type === MessageType.PartyThinking; +} + +export function isModOrAdminMessage(type: MessageType) { + return type === MessageType.Mod || + type === MessageType.Admin; +} + +export function isPartyMessage(type: MessageType) { + return type === MessageType.Party || + type === MessageType.PartyThinking || + type === MessageType.PartyAnnouncement; +} + +export function isPublicMessage(type: MessageType) { + return type === MessageType.Chat || + type === MessageType.Thinking || + type === MessageType.Announcement || + type === MessageType.Admin || + type === MessageType.Mod || + type === MessageType.Supporter1 || + type === MessageType.Supporter2 || + type === MessageType.Supporter3; +} + +export function isNonIgnorableMessage(type: MessageType) { + return isModOrAdminMessage(type) || + isPartyMessage(type) || + type === MessageType.System || + type === MessageType.Dismiss || + type === MessageType.Announcement; +} + +export const enum ChatType { + Say = 0, + Party = 1, + Think = 2, + PartyThink = 3, + Supporter = 4, + Supporter1 = 5, + Supporter2 = 6, + Supporter3 = 7, + Dismiss = 8, + Whisper = 9, +} + +export function isPartyChat(type: ChatType | undefined) { + return type === ChatType.Party || type === ChatType.PartyThink; +} + +export function isPublicChat(type: ChatType) { + return type !== ChatType.Party && + type !== ChatType.PartyThink && + type !== ChatType.Dismiss && + type !== ChatType.Whisper; +} + +export interface Point { + x: number; + y: number; +} + +export interface Size { + width: number; + height: number; +} + +export interface Rect { + x: number; + y: number; + w: number; + h: number; +} + +export interface Camera extends Rect { + offset: number; + shift: number; + shiftTarget: number; + shiftRatio: number; + actualY: number; +} + +export interface Sprite { + x: number; + y: number; + w: number; + h: number; + ox: number; + oy: number; + type: number; +} + +export interface SpriteSheet { + src?: string; + srcA?: string; + data?: ImageData; + texture: Texture2D | undefined; + sprites: (Sprite | undefined)[]; + palette: boolean; +} + +export interface SpriteBorder { + border: number; + topLeft: Sprite; + top: Sprite; + topRight: Sprite; + left: Sprite; + bg: Sprite; + right: Sprite; + bottomLeft: Sprite; + bottom: Sprite; + bottomRight: Sprite; + palette?: number[]; +} + +export interface SpritePalette { + sprite: Sprite; + palette: number[]; +} + +export interface SpriteMap { + [key: string]: Sprite; +} + +export interface PonyEye { + base?: Sprite; + irises: (Sprite | undefined)[]; + shadow?: Sprite; + shine?: Sprite; +} + +export interface PonyNose extends ColorExtra { + mouth?: Sprite; + fangs?: Sprite; +} + +export interface FillOutline { + fill: Sprite; + outline?: Sprite; + extra?: Sprite; +} + +export interface Says { + created: number; + message: string; + type?: MessageType; + timer?: number; + total?: number; +} + +export interface PaletteRenderable { + color?: Sprite; + shadow?: Sprite; + palettes?: Uint32Array[]; +} + +export interface Collider { + x: number; + y: number; + w: number; + h: number; + tall: boolean; + exact: boolean; +} + +export const enum InteractAction { + None, + Toolbox, + GiveLantern, + GiveFruits, + GiveCookie1, + GiveCookie2, +} + +export interface EntityPart { + // info + name?: string; + crc?: number; + tag?: string; + fake?: boolean; + + // chat + says?: Says; + chatX?: number; + chatY?: number; + chatBounds?: Rect; + + // interaction + interactBounds?: Rect; + interactRange?: number; + + // collisions + colliders?: Collider[]; + collidersBounds?: Rect; + + // cover + coverBounds?: Rect; + coverLifted?: boolean; + coverLifting?: number; + + // pickable + pickableX?: number; + pickableY?: number; + + // update + update?(delta: number, gameTime: number): void | boolean; + + // draw + ox?: number; + oy?: number; + oz?: number; + bounds?: Rect; + draw?(batch: PaletteSpriteBatch, options: DrawOptions): void; + + // light + lightOn?: boolean; + lightScale?: number; + lightTarget?: number; + lightColor?: number; + lightScaleAdjust?: number; + lightBounds?: Rect; + drawLight?(batch: SpriteBatch, options: DrawOptions): void; + + // light sprite + lightSpriteOn?: boolean; + lightSpriteX?: number; + lightSpriteY?: number; + lightSpriteColor?: number; + lightSpriteBounds?: Rect; + drawLightSprite?(batch: SpriteBatch, options: DrawOptions): void; + + // trigger + triggerBounds?: Rect; + triggerTall?: boolean; + triggerOn?: boolean; + + // bobbing + bobsFps?: number; + bobs?: number[]; + + // resources to release + palettes?: (Palette | undefined)[]; + + // server + // expr?: number; + serverFlags?: ServerFlags; + + // other + text?: string; + inTheAirDelay?: number; + options?: EntityOrPonyOptions; + extraOptions?: any; + selected?: boolean; + + // for setting entity fields + order?: number; + flags?: EntityFlags; + state?: EntityState; + vx?: number; + vy?: number; + + // minimap + minimap?: { color: number; rect: Rect; order: number; }; + + // interact + interactAction?: InteractAction; +} + +export interface Entity extends EntityPart { + id: number; + type: number; + flags: EntityFlags; + state: EntityState; + playerState: EntityPlayerState; + order: number; + x: number; + y: number; + z: number; + vx: number; + vy: number; + // frame: number; // last update frame + timestamp: number; + + // editor / debug + movedOnFrame?: number; + draggingStart?: Point; +} + +export interface FakeEntity { + fake: true; + id: number; + type: number; + name?: string; + crc?: number; +} + +export const enum DoAction { + None, + Boop, + Swing, + HoldPoof, +} + +export interface Pony extends Entity { + info: string | Uint8Array | undefined; + expr: number; + currentExpression: number; + extra: boolean; + toy: number; + swimming: boolean; + inTheAirDelay: number; + hold: number; + palettePonyInfo: PalettePonyInfo | undefined; + headAnimation: HeadAnimation | undefined; + batch: Batch | undefined; + discardBatch: boolean; + headTime: number; + blinkTime: number; + nextBlink: number; + ponyState: PonyState; + drawingOptions: DrawPonyOptions; + animator: Animator; + initialized: boolean; + doAction: DoAction; + paletteManager: PaletteManager; + magicColor: number; + + // player info + name: string | undefined; + tag: string | undefined; + site: SocialSite | undefined; + modInfo: ModInfo | undefined; + ex: boolean; // extended data indicator, sent in extended options + + // effect animations + zzzEffect: AnimationPlayer; + cryEffect: AnimationPlayer; + sneezeEffect: AnimationPlayer; + holdPoofEffect: AnimationPlayer; + heartsEffect: AnimationPlayer; + magicEffect: AnimationPlayer; + + // last state + lastX: number; + lastY: number; + lastRight: boolean; + lastState: PonyState; + + lastBoopSplash: number; +} + +export interface Region { + x: number; // region number in X axis + y: number; // region number in Y axis + tiles: Uint8Array; + tileIndices: Int16Array; + tilesDirty: boolean; + lastTileUpdate?: number; + randoms: Uint8Array; + entities: Entity[]; + colliders: Entity[]; + collider: Uint8Array; + colliderDirty: boolean; + // elevation: Uint8Array; +} + +export interface CharacterTag { + id: string; + name: string; + label: string; + className: string; + tagClass: string; + color: number; +} + +export interface SocialSite { + id: string; + name: string; + provider: string; + url: string; +} + +export interface SocialSiteInfo { + id: string; + name: string; + url: string; + icon?: string; + color?: string; +} + +// NOTE: also modify fixAccountSettings +export interface AccountSettings { + defaultServer?: string; + filterSwearWords?: boolean; + filterCyrillic?: boolean; + filterWords?: string; + ignorePartyInvites?: boolean; + ignoreFriendInvites?: boolean; + ignorePublicChat?: boolean; + ignoreNonFriendWhispers?: boolean; + chatlogOpacity?: number; + seeThroughObjects?: boolean; + chatlogRange?: number; + actions?: string; + hidden?: boolean; +} + +export interface BrowserSettings { + lowGraphicsMode?: boolean; + chatlogClosed?: boolean; + chatlogTab?: string; + chatlogWidth?: number; + chatlogHeight?: number; + volume?: number; + disableGamepad?: boolean; + disableFKeys?: boolean; + showStats?: boolean; + showFps?: boolean; + powerSaving?: boolean; + scale?: number; + walkByDefault?: boolean; + brightNight?: boolean; +} + +export interface EntityTypeName { + type: number; + name: string; +} + +export interface EntityNameTypes { + name: string; + types: number[]; +} + +export interface EntitiesEditorInfo { + names: string[]; + typeToName: EntityTypeName[]; + nameToTypes: EntityNameTypes[]; +} + +export interface HiddenPlayer { + id: string; + name: string; + date: string; +} + +export interface AccountDataExtra { + sites?: SocialSite[]; + ponies?: PonyObject[]; + supporterInvited?: boolean; + editor?: EntitiesEditorInfo; + alert?: string; +} + +export const enum AccountDataFlags { + None = 0, + Duplicates = 1, + PastSupporter = 4, +} + +export interface FriendData { + accountId: string; + accountName: string; + name: string | undefined; + pony: string | undefined; + nameBad: boolean; +} + +export const enum FriendStatusFlags { + None = 0, + Online = 1, + Remove = 2, +} + +export interface FriendStatusData { + accountId: string; + accountName?: string; + status: FriendStatusFlags; + entityId?: number; + name?: string; + info?: string; + crc?: number; + nameBad?: boolean; +} + +export interface AccountData extends AccountDataExtra { + id: string; + name: string; + birthdate: string; + birthyear?: number; + settings: AccountSettings; + characterCount: number; + supporter?: number; + roles?: string[] | undefined; + check?: any; + flags: AccountDataFlags; +} + +export interface UpdateAccountData { + name: string; + birthdate: string; +} + +export interface AccountCounters { + spam?: number; + swears?: number; + timeouts?: number; + inviteLimit?: number; + friendLimit?: number; +} + +export interface ModInfo { + age?: number; + note?: string; + mute?: string; + shadow?: string; + account?: string; + country?: string; + counters?: AccountCounters; +} + +export interface PonyOptions { + expr?: number; // TODO: move to dedicated field + tag?: string; // TODO: move to flags ? + extra?: boolean; // TODO: move to flags ? + hold?: number; + toy?: number; + // mod/extra info + site?: SocialSite; + modInfo?: ModInfo; +} + +// id, options, name, info, playerState, badName +export type PonyData = [number, PonyOptions | undefined, Uint8Array | undefined, Uint8Array | undefined, number, boolean]; + +// NOTE: also update in serverActions.ts +export const enum PlayerAction { + None, + Ignore, + Unignore, + InviteToParty, + RemoveFromParty, + PromotePartyLeader, + HidePlayer, + InviteToSupporterServers, + AddFriend, + RemoveFriend, +} + +export const enum ModAction { + None, + Report, + Mute, + Shadow, + Kick, + Ban, +} + +export const enum LeaveReason { + None, + Swearing, +} + +export interface TileUpdate { + x: number; + y: number; + type: TileType; +} + +// [id, message, type] +export type SayData = [number, string, MessageType]; + +export const enum UpdateType { + None = 0, + AddEntity = 1, + UpdateEntity = 2, + RemoveEntity = 3, + UpdateTile = 4, +} + +export interface DecodedUpdate { + id: number; + type: number | undefined; + state: EntityState | undefined; + x: number | undefined; + y: number | undefined; + vx: number; + vy: number; + expression: number | undefined; + options: EntityOrPonyOptions | undefined; + crc: number | undefined; + name: string | undefined; + filterName: boolean; + info: Uint8Array | undefined; + action: Action | undefined; + switchRegion: boolean; + playerState: EntityPlayerState | undefined; +} + +export interface DecodedRegionUpdate { + x: number; + y: number; + updates: DecodedUpdate[]; + removes: number[]; + tiles: TileUpdate[]; + tileData: Uint8Array | null; +} + +export const enum WorldStateFlags { + None = 0, + Safe = 1, +} + +export interface WorldState { + time: number; + season: Season; + holiday: Holiday; + flags: WorldStateFlags; + featureFlags: ServerFeatureFlags; +} + +export interface MapState { + weather: Weather; +} + +export const defaultMapState: MapState = { + weather: Weather.None, +}; + +export const enum WallType { + None = 0, + Wood = 1, +} + +export const enum TileType { + None = 0, + Dirt = 1, + Grass = 2, + Water = 3, + Wood = 4, + Ice = 5, + SnowOnIce = 6, + WalkableWater = 7, + Boat = 8, + WalkableIce = 9, + Stone = 10, + Stone2 = 11, + ElevatedDirt = 12, + + // special + WallH = 100, + WallV = 101, +} + +export const tileTypeNames = [ + 'none', 'dirt', 'grass', 'water', 'wood', 'ice', 'snow-on-ice', 'walkable-water', 'boat', 'walkable-ice', + 'stone', 'stone-2', 'elevated-dirt', +]; + +export const houseTiles = [ + { type: TileType.Dirt, name: 'Dirt' }, + { type: TileType.Wood, name: 'Wood' }, + { type: TileType.Grass, name: 'Grass' }, + { type: TileType.Water, name: 'Water' }, + { type: TileType.Ice, name: 'Ice' }, + { type: TileType.Stone, name: 'Stone' }, + { type: TileType.Stone2, name: 'Brick' }, +]; + +export function canWalk(tile: TileType): boolean { + return tile !== TileType.None; +} + +export function isValidTile(tile: TileType): boolean { + return tile === TileType.Dirt || tile === TileType.Grass; +} + +export function isValidModTile(tile: TileType): boolean { + return tile >= TileType.None && tile < TileType.WallH; +} + +export interface MapInfo { + type: MapType; + flags: MapFlags; + regionsX: number; + regionsY: number; + defaultTile: TileType; + editableArea?: Rect; +} + +export interface WorldMap extends IMap { + type: MapType; + flags: MapFlags; + tileTime: number; + entities: Entity[]; + entitiesDrawable: Entity[]; + entitiesWithNames: Entity[]; + entitiesWithChat: Entity[]; + entitiesMoving: Entity[]; + entitiesTriggers: Entity[]; + entitiesLight: Entity[]; + entitiesLightSprite: Entity[]; + entitiesById: Map; + poniesToDecode: Pony[]; + width: number; + height: number; + regionsX: number; + regionsY: number; + defaultTile: TileType; + regions: (Region | undefined)[]; + minRegionX: number; + minRegionY: number; + maxRegionX: number; + maxRegionY: number; + state: MapState; + editableArea?: Rect; +} + +export interface TileSet { + sprites: Sprite[]; + palette: Palette; +} + +export type TileSets = TileSet[]; + +export const enum PartyFlags { + None = 0, + Leader = 1, + Pending = 2, + Offline = 4, +} + +export interface PartyMember { + id: number; + pony: Pony | undefined; + self: boolean; + leader: boolean; + pending: boolean; + offline: boolean; +} + +export interface PartyInfo { + leaderId: number; + members: PartyMember[]; +} + +export interface ChatMessage { + id: number; + crc: number | undefined; + name: string; + message: string; + type: MessageType; +} + +export interface CommonButtonAction { + title?: string; +} + +export interface ExpressionButtonAction { + type: 'expression'; + expression: Expression | undefined; + title?: string; +} + +export interface CommandButtonAction { + type: 'command'; + title: string; + icon: string; + command: string; +} + +export interface ActionButtonAction { + type: 'action'; + title: string; + action: string; + sendAction: Action; +} + +export interface ItemButtonAction { + type: 'item'; + icon: ColorExtra; + count?: number; +} + +export interface EntityButtonAction { + type: 'entity'; + entity: string; + title?: string; +} + +export type ButtonAction = CommonButtonAction & + (ExpressionButtonAction | CommandButtonAction | ActionButtonAction | ItemButtonAction | EntityButtonAction); + +export interface ButtonActionSlot { + action: ButtonAction | undefined; +} + +export const enum Action { + None = 0, + Boop, + TurnHead, + Yawn, + Laugh, + Sneeze, + Sit, + Lie, + Fly, + UnhideAllHiddenPlayers, + Stand, + SwapCharacter, + HoldPoof, + Sleep, + Drop, + DropToy, + Blush, + Cry, + Love, + CancelSupporterInvite, + Info, + KeepAlive, + RemoveFriend, + FriendsCRC, + RequestEntityInfo, + ACL, + Magic, + RemoveEntity, + PlaceEntity, + SwitchTool, + SwitchToolRev, + SwitchToPlaceTool, + SwitchToTileTool, +} + +export const enum InfoFlags { + None = 0, + Incognito = 1, + SupportsWASM = 2, + SupportsLetAndConst = 4, +} + +export function isExpressionAction(action: Action) { + return action === Action.Yawn || action === Action.Laugh || action === Action.Sneeze; +} + +export interface EditorPlaceAction { + type: 'place'; + entity: string; + x: number; + y: number; +} + +export interface EditorMoveAction { + type: 'move'; + entities: { id: number; x: number; y: number; }[]; +} + +export interface EditorRemoveAction { + type: 'remove'; + entities: number[]; +} + +export interface EditorOtherAction { + type: 'undo' | 'clear' | 'list'; +} + +export interface EditorTileAction { + type: 'tile'; + x: number; + y: number; + tile: TileType; + size: number; +} + +export interface EditorPartyAction { + type: 'party'; +} + +export type EditorAction = + EditorPlaceAction | + EditorMoveAction | + EditorRemoveAction | + EditorOtherAction | + EditorTileAction | + EditorPartyAction; + +export const enum SelectFlags { + None = 0, + FetchEx = 1, + FetchInfo = 2, +} + +export interface IServerActions { + say(entityId: number, text: string, type: ChatType): void; + select(entityId: number, flags: SelectFlags): void; + interact(entityId: number): void; + use(): void; + action(action: Action): void; + actionParam(action: Action, param: any): void; + actionParam2(action: Action, param: any): void; + getInvites(): Promise; + expression(expression: number): void; + playerAction(entityId: number, action: PlayerAction, param: any): void; + leaveParty(): void; + otherAction(entityId: number, action: ModAction, param: number): Promise; + setNote(entityId: number, text: string): Promise; + saveSettings(settings: AccountSettings): void; + acceptNotification(id: number): void; + rejectNotification(id: number): void; + getPonies(ids: number[]): void; + loaded(): void; + fixedPosition(): void; + updateCamera(x: number, y: number, w: number, h: number): void; + move(a: number, b: number, c: number, d: number, e: number): void; + changeTile(x: number, y: number, type: TileType): void; + leave(): void; + editorAction(action: EditorAction): void; +} + +// Pony + +export interface PonyObject { + id: string; + name: string; + info: string; + tag?: string; + site?: string; + desc?: string; + lastUsed?: string; + creator?: string; + ponyInfo?: PonyInfo; + hideSupport?: boolean; + respawnAtSpawn?: boolean; +} + +export interface SpriteSetBase { + type?: number; + pattern?: number; +} + +export interface SpriteSet extends SpriteSetBase { + fills?: (T | undefined)[]; + outlines?: (T | undefined)[]; + lockFills?: boolean[]; + lockOutlines?: boolean[]; +} + +export interface PonyInfoBase { + head: SET | undefined; + nose: SET | undefined; + ears: SET | undefined; + horn: SET | undefined; + wings: SET | undefined; + frontHooves: SET | undefined; + backHooves: SET | undefined; + + mane: SET | undefined; + backMane: SET | undefined; + tail: SET | undefined; + facialHair: SET | undefined; + + headAccessory: SET | undefined; + earAccessory: SET | undefined; + faceAccessory: SET | undefined; + neckAccessory: SET | undefined; + frontLegAccessory: SET | undefined; + backLegAccessory: SET | undefined; + frontLegAccessoryRight: SET | undefined; + backLegAccessoryRight: SET | undefined; + lockBackLegAccessory: boolean | undefined; + unlockFrontLegAccessory: boolean | undefined; + unlockBackLegAccessory: boolean | undefined; + backAccessory: SET | undefined; + waistAccessory: SET | undefined; + chestAccessory: SET | undefined; + sleeveAccessory: SET | undefined; + extraAccessory: SET | undefined; + + coatFill: T | undefined; + coatOutline: T | undefined; + lockCoatOutline: boolean | undefined; + + eyelashes: number | undefined; + eyeColorLeft: T | undefined; + eyeColorRight: T | undefined; + eyeWhitesLeft: T | undefined; + eyeWhites: T | undefined; + eyeOpennessLeft: number | undefined; + eyeOpennessRight: number | undefined; + eyeshadow: boolean | undefined; + eyeshadowColor: T | undefined; + lockEyes: boolean | undefined; + lockEyeColor: boolean | undefined; + unlockEyeWhites: boolean | undefined; + unlockEyelashColor: boolean | undefined; + eyelashColor: T | undefined; + eyelashColorLeft: T | undefined; + + fangs: number | undefined; + muzzle: number | undefined; + freckles: number | undefined; // TODO: remove + frecklesColor: T | undefined; // TODO: remove + magicColor: T | undefined; + + cm: T[] | undefined; + cmFlip: boolean | undefined; + + customOutlines: boolean | undefined; + freeOutlines: boolean | undefined; + darkenLockedOutlines: boolean | undefined; +} + +export interface PaletteSpriteSet extends SpriteSetBase { + type: number; + pattern: number; + palette: Palette; + extraPalette?: Palette; +} + +export interface PalettePonyInfo extends PonyInfoBase { + coatPalette: Palette; + eyePaletteLeft: Palette; + eyePalette: Palette; + cmPalette?: Palette; + defaultPalette: Palette; + waterPalette: Palette; + // faceAccessoryExtraPalette: Palette | undefined; + body: PaletteSpriteSet | undefined; + backLegs: PaletteSpriteSet | undefined; + frontLegs: PaletteSpriteSet | undefined; + magicColorValue: number; +} + +export interface PonyInfo extends PonyInfoBase> { +} + +export interface PonyInfoNumber extends PonyInfoBase> { +} + +export interface ColorExtra { + color: Sprite; + colors?: number; + extra?: Sprite; + palettes?: Uint32Array[]; + title?: string; + label?: string; + timestamp?: any; + colorMany?: Sprite[]; +} + +export interface TileSprites { + sprites: Sprite[]; + palettes: Uint32Array[]; +} + +export interface ColorShadow { + color: Sprite; + shadow: Sprite; + palettes?: Uint32Array[]; +} + +export interface Shadow { + shadow: Sprite; +} + +export type ColorExtraSet = (ColorExtra | undefined)[] | undefined; +export type ColorExtraSets = ColorExtraSet[] | undefined; + +export interface BodyAnimationFrame { + body: number; + head: number; + wing: number; + tail: number; + + frontLeg: number; + frontFarLeg: number; + backLeg: number; + backFarLeg: number; + + bodyX: number; + bodyY: number; + headX: number; + headY: number; + frontLegX: number; + frontLegY: number; + frontFarLegX: number; + frontFarLegY: number; + backLegX: number; + backLegY: number; + backFarLegX: number; + backFarLegY: number; +} + +export interface BodyShadow { + frame: number; + offset: number; +} + +export interface BodyAnimation { + name: string; + loop: boolean; + fps: number; + frames: BodyAnimationFrame[]; + shadow?: BodyShadow[]; +} + +export interface HeadAnimationFrame { + headX: number; + headY: number; + left: Eye; + right: Eye; + mouth: Muzzle; +} + +export interface HeadAnimation { + name: string; + loop: boolean; + fps: number; + frames: HeadAnimationFrame[]; +} + +export const enum PonyStateFlags { + None = 0, + CurlTail = 1, + FaceForward = 2, +} + +export interface PonyState { + animation: BodyAnimation; + animationFrame: number; + headAnimation: HeadAnimation | undefined; + headAnimationFrame: number; + headTurned: boolean; + headTilt: number; + headTurn: number; + blinkFrame: number; + expression: Expression | undefined; + holding: Entity | undefined; + blushColor: number; + drawFaceExtra?: ((batch: PaletteSpriteBatch) => void) | undefined; + flags: PonyStateFlags; +} + +export const enum NoDraw { + None = 0x0, + Front = 0x1, + Front2 = 0x2, + BehindLeg = 0x4, + BehindBody = 0x8, + Behind = 0x10, + Body = 0x20, + BodyOnly = 0x40, + FarEar = 0x80, + CloseEar = 0x100, + FaceAccessory1 = 0x200, + FaceAccessory2 = 0x400, + TopMane = 0x800, + FrontMane = 0x1000, + Nose = 0x2000, + Head = 0x4000, + Eyes = 0x8000, + BackAccessory = 0x10000, + BackLeg = 0x20000, + BackFarLeg = 0x40000, + FrontLeg = 0x80000, + FrontFarLeg = 0x100000, + FarSleeves = 0x200000, + CloseSleeves = 0x400000, + FarEarShade = 0x800000, + Ears = NoDraw.FarEar | NoDraw.CloseEar, + WholeHead = NoDraw.Head | NoDraw.Nose | NoDraw.Ears, + FarLegs = NoDraw.BackFarLeg | NoDraw.FrontFarLeg, + CloseLegs = NoDraw.BackLeg | NoDraw.FrontLeg, + AllLegs = NoDraw.FarLegs | NoDraw.CloseLegs, + Sleeves = NoDraw.FarSleeves | NoDraw.CloseSleeves, +} + +export interface DrawPonyOptions { + flipped: boolean; + selected: boolean; + shadow: boolean; + extra: boolean; + toy: number; + swimming: boolean; + bounce: boolean; + shadowColor: number; + noEars: boolean; + gameTime: number; + + // sheet generation switches + no: NoDraw; + useAllHooves: boolean; +} + +// other + +export interface OAuthProvider { + id: string; + name: string; + color: string; + url?: string; + disabled?: boolean; + connectOnly?: boolean; + faIcon?: any; +} + +export interface Profile { + id: string; + provider: string; + emails: string[]; + name: string | undefined; + username: string | undefined; + url: string | undefined; + createdAt: Date | undefined; + suspended: boolean | undefined; +} + +export interface GameStatus { + version: string; + update?: boolean; + servers: (ServerInfo | ServerInfoShort)[]; +} + +export interface ServerInfoShort { + id: string; + offline: boolean; + online: number; +} + +export interface ServerInfo extends ServerInfoShort { + name: string; + path: string; + desc: string; + host?: string; + flag?: string; + alert?: string; + dead: boolean; + filter: boolean; + require?: string; + flags?: ServerFeatureFlags; + countryFlags?: string[]; +} + +// expression + +export const enum Muzzle { + Smile = 0, + Frown = 1, + Neutral = 2, + Scrunch = 3, + Blep = 4, + SmileOpen = 5, + Flat = 6, + Concerned = 7, + ConcernedOpen = 8, + SmileOpen2 = 9, + FrownOpen = 10, + NeutralOpen2 = 11, + ConcernedOpen2 = 12, + Kiss = 13, + SmileOpen3 = 14, + NeutralOpen3 = 15, + ConcernedOpen3 = 16, + Kiss2 = 17, + SmileTeeth = 18, + FrownTeeth = 19, + NeutralTeeth = 20, + ConcernedTeeth = 21, + SmilePant = 22, + NeutralPant = 23, + Oh = 24, + FlatBlep = 25, + // max: 31 +} + +export const CLOSED_MUZZLES = [ + Muzzle.Smile, Muzzle.Frown, Muzzle.Neutral, Muzzle.Scrunch, Muzzle.Flat, Muzzle.Concerned, + Muzzle.Kiss, Muzzle.Kiss2, +]; + +export const enum Eye { + None = 0, + Neutral = 1, + Neutral2 = 2, + Neutral3 = 3, + Neutral4 = 4, + Neutral5 = 5, + Closed = 6, + Frown = 7, + Frown2 = 8, + Frown3 = 9, + Frown4 = 10, + Lines = 11, + ClosedHappy3 = 12, + ClosedHappy2 = 13, + ClosedHappy = 14, + Sad = 15, + Sad2 = 16, + Sad3 = 17, + Sad4 = 18, + Angry = 19, + Angry2 = 20, + Peaceful = 21, + Peaceful2 = 22, + X = 23, + X2 = 24, + // max: 31 +} + +export function isEyeSleeping(eye: Eye) { + return eye === Eye.Closed || + (eye >= Eye.Lines && eye <= Eye.ClosedHappy) || + (eye >= Eye.Peaceful && eye <= Eye.X2); +} + +export const enum Iris { + Forward = 0, + Up = 1, + Left = 2, + Right = 3, + UpLeft = 4, + UpRight = 5, + Shocked = 6, + Down = 7, + // max: 15 + COUNT, +} + +export const enum ExpressionExtra { + None = 0, + Blush = 1, + Zzz = 2, + Cry = 4, // overrides tears + Tears = 8, + Hearts = 16, + // max: 31 +} + +export interface Expression { + left: Eye; + leftIris: Iris; + right: Eye; + rightIris: Iris; + muzzle: Muzzle; + extra: ExpressionExtra; +} + +export interface SupporterInvite { + id: string; + active: boolean; + name: string; + info: string; +} + +export interface Subscription { + unsubscribe(): void; +} + +export enum Engine { + Default, + LayeredTiles, + Whiteness, + NewLighting, + Total, +} + +export interface DrawOptions { + gameTime: number; + lightColor: number; + shadowColor: number; + drawHidden: boolean; + showColliderMap: boolean; + showHeightmap: boolean; + debug: DebugFlags; + gridLines: boolean; + tileIndices: boolean; + tileGrid: boolean; + engine: Engine; + season: Season; + error: (message: string) => void; +} + +export const defaultDrawOptions: DrawOptions = { + gameTime: 0, + lightColor: WHITE, + shadowColor: SHADOW_COLOR, + drawHidden: false, + showColliderMap: false, + showHeightmap: false, + debug: {}, + gridLines: false, + tileIndices: false, + tileGrid: false, + engine: Engine.Default, + season: Season.Summer, + error: () => { }, +}; + +export interface PonyEntityOptions { + hold?: number; + extra?: boolean; +} + +export interface SpiderEntityOptions { + height: number; + time: number; +} + +export interface SignEntityOptions { + sign: { + r?: number; + n?: number[]; + w?: number[]; + s?: number[]; + e?: number[]; + }; +} + +export interface EntityDescriptor { + type: number; + typeName: string; + create: CreateEntity; +} + +export type EntityOptions = PonyEntityOptions | SpiderEntityOptions | SignEntityOptions | {}; +export type EntityOrPonyOptions = Partial & EntityOptions; + +export interface EntityWorldState { + season: Season; +} + +export const defaultWorldState = { + season: Season.Summer, +}; + +export type CreateEntity = (base: Entity, options: EntityOptions, worldState: EntityWorldState) => Entity; +export type MixinEntity = (base: Entity, options: EntityOptions, worldState: EntityWorldState) => void; + +export interface CreateEntityMethod { + type: number; + typeName: string; + (x: number, y: number, options?: EntityOptions): Entity; +} + +export interface IMap { + width: number; + height: number; + regionsX: number; + regionsY: number; + regions: TRegion[]; +} + +export interface FontPalettes { + emoji: Palette; + white: Palette; + supporter1: Palette; + supporter2: Palette; + supporter3: Palette; +} + +export interface CommonPalettes { + defaultPalette: Palette; + mainFont: FontPalettes; + smallFont: FontPalettes; +} + +export interface EngineInfo { + name: string; + engine: Engine; +} + +export interface DebugFlags { + showHelpers?: boolean; + showPalette?: boolean; + showRegions?: boolean; + showInfo?: boolean; + engine?: number; + debugCamera?: boolean; + // entity helpers + bounds?: boolean; + collider?: boolean; + id?: boolean; + cover?: boolean; + interact?: boolean; + trigger?: boolean; +} + +export const enum UpdateFlags { + None = 0, + Position = 1, + Velocity = 2, + State = 4, + Expression = 8, + Type = 16, + Options = 32, + Info = 64, + Action = 128, + Name = 256, + NameBad = 512, + PlayerState = 1024, + SwitchRegion = 2048, + // max 32768 +} diff --git a/src/ts/common/mat2d.ts b/src/ts/common/mat2d.ts new file mode 100644 index 0000000..4f46fd1 --- /dev/null +++ b/src/ts/common/mat2d.ts @@ -0,0 +1,122 @@ +import { Matrix2D } from './interfaces'; + +export function createMat2D(): Matrix2D { + const out = new Float32Array(6); + out[0] = 1; + out[3] = 1; + return out; +} + +export function identityMat2D(out: Matrix2D) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 1; + out[4] = 0; + out[5] = 0; + return out; +} + +export function copyMat2D(out: Matrix2D, a: Matrix2D) { + out.set(a); + return out; +} + +export function setMat2D(out: Matrix2D, a: number, b: number, c: number, d: number, tx: number, ty: number) { + out[0] = a; + out[1] = b; + out[2] = c; + out[3] = d; + out[4] = tx; + out[5] = ty; + return out; +} + +export function mulMat2D(out: Matrix2D, a: Matrix2D, b: Matrix2D) { + const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5]; + const b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5]; + out[0] = a0 * b0 + a2 * b1; + out[1] = a1 * b0 + a3 * b1; + out[2] = a0 * b2 + a2 * b3; + out[3] = a1 * b2 + a3 * b3; + out[4] = a0 * b4 + a2 * b5 + a4; + out[5] = a1 * b4 + a3 * b5 + a5; + return out; +} + +export function translateMat2D(out: Matrix2D, a: Matrix2D, x: number, y: number) { + const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5]; + out[0] = a0; + out[1] = a1; + out[2] = a2; + out[3] = a3; + out[4] = a0 * x + a2 * y + a4; + out[5] = a1 * x + a3 * y + a5; + return out; +} + +export function rotateMat2D(out: Matrix2D, a: Matrix2D, rad: number) { + const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5]; + const s = Math.sin(rad); + const c = Math.cos(rad); + out[0] = a0 * c + a2 * s; + out[1] = a1 * c + a3 * s; + out[2] = a0 * -s + a2 * c; + out[3] = a1 * -s + a3 * c; + out[4] = a4; + out[5] = a5; + return out; +} + +export function scaleMat2D(out: Matrix2D, a: Matrix2D, x: number, y: number) { + const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5]; + out[0] = a0 * x; + out[1] = a1 * x; + out[2] = a2 * y; + out[3] = a3 * y; + out[4] = a4; + out[5] = a5; + return out; +} + +const temp = createMat2D(); + +export function skewX(out: Matrix2D, a: Matrix2D, angle: number): Matrix2D { + setMat2D(temp, 1, 0, Math.tan(angle), 1, 0, 0); + mulMat2D(out, a, temp); + return out; +} + +export function skewY(out: Matrix2D, a: Matrix2D, angle: number): Matrix2D { + setMat2D(temp, 1, Math.tan(angle), 0, 1, 0, 0); + mulMat2D(out, a, temp); + return out; +} + +const tempMatrix = createMat2D(); + +export function skewTransform(base: Matrix2D | undefined, skew: number, ox: number, oy: number, x: number, y: number): Matrix2D { + identityMat2D(tempMatrix); + + if (skew) { + translateMat2D(tempMatrix, tempMatrix, ox + x, oy + y); + skewY(tempMatrix, tempMatrix, skew); + translateMat2D(tempMatrix, tempMatrix, -ox, -oy); + } else { + translateMat2D(tempMatrix, tempMatrix, x, y); + } + + if (base !== undefined) { + mulMat2D(tempMatrix, base, tempMatrix); + } + + return tempMatrix; +} + +export function isIdentity(m: Matrix2D) { + return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0; +} + +export function isTranslation(m: Matrix2D) { + return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1; +} diff --git a/src/ts/common/mat4.ts b/src/ts/common/mat4.ts new file mode 100644 index 0000000..3e830aa --- /dev/null +++ b/src/ts/common/mat4.ts @@ -0,0 +1,33 @@ +import { Matrix4 } from './interfaces'; + +export function createMat4(): Matrix4 { + const out = new Float32Array(16); + out[0] = 1; + out[5] = 1; + out[10] = 1; + out[15] = 1; + return out; +} + +export function ortho(out: Matrix4, left: number, right: number, bottom: number, top: number, near: number, far: number) { + const lr = 1 / (left - right); + const bt = 1 / (bottom - top); + const nf = 1 / (near - far); + out[0] = -2 * lr; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = -2 * bt; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 2 * nf; + out[11] = 0; + out[12] = (left + right) * lr; + out[13] = (top + bottom) * bt; + out[14] = (far + near) * nf; + out[15] = 1; + return out; +} diff --git a/src/ts/common/mixins.ts b/src/ts/common/mixins.ts new file mode 100644 index 0000000..eb88a01 --- /dev/null +++ b/src/ts/common/mixins.ts @@ -0,0 +1,850 @@ +import { clamp } from 'lodash'; +import * as sprites from '../generated/sprites'; +import { + EntityPart, Sprite, Rect, SpriteBatch, PaletteManager, Palette, PaletteRenderable, PaletteSpriteBatch, + DrawOptions, getAnimationFromEntityState, EntityState, SignEntityOptions, EntityFlags, Collider, MixinEntity, + Season, +} from './interfaces'; +import { at, att, hasFlag, invalidEnum } from './utils'; +import { WHITE, BLACK, RED } from './colors'; +import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils'; +import { rect, addRects, addRect } from './rect'; +import { SECOND } from './constants'; +import { mockPaletteManager } from './ponyInfo'; +import { releasePalette } from '../graphics/paletteManager'; + +interface Renderable { + color?: Sprite; + shadow?: Sprite; +} + +export interface AnimatedRenderable { + frames: Sprite[]; + shadow?: Sprite; + palette: Uint32Array; +} + +export interface AnimatedRenderable1 { + frames: (Sprite | undefined)[]; +} + +const predefinedSteps = [ + [], + [1], + [3, 1], + [3, 2, 1], + [4, 2, 1, 1], + [5, 3, 2, 1, 1], + [9, 5, 3, 2, 1, 1], + [14, 9, 5, 3, 2, 1, 1], +]; + +let paletteManager: PaletteManager | undefined; + +export function createPalette(palette: Uint32Array | undefined): Palette | undefined { + return palette && paletteManager && paletteManager.addArray(palette); +} + +export function setPaletteManager(manager: PaletteManager | undefined) { + paletteManager = manager; +} + +export function fakePaletteManager(action: () => T): T { + const tempPaletteManager = paletteManager; + paletteManager = mockPaletteManager; + const result = action(); + paletteManager = tempPaletteManager; + return result; +} + +function getBounds(sprite: Sprite | undefined, ox: number, oy: number): Rect { + return sprite ? rect(sprite.ox + ox, sprite.oy + oy, sprite.w, sprite.h) : rect(0, 0, 0, 0); +} + +export function getRenderableBounds({ color, shadow }: Renderable, dx: number, dy: number): Rect { + if (color && shadow) { + return addRects(getBounds(color, -dx, -dy), getBounds(shadow, -dx, -dy)); + } else if (color) { + return getBounds(color, -dx, -dy); + } else if (shadow) { + return getBounds(shadow, -dx, -dy); + } else { + return rect(0, 0, 0, 0); + } +} + +function getBoundsForFrames(frames: (Sprite | undefined)[], dx: number, dy: number) { + return frames.reduce((bounds, f) => f ? addRects(bounds, getBounds(f, dx, dy)) : bounds, rect(0, 0, 0, 0)); +} + +export function pickable(pickableX: number, pickableY: number): EntityPart { + return { pickableX, pickableY }; +} + +export function mixPickable(pickableX: number, pickableY: number): MixinEntity { + return base => { + base.pickableX = pickableX; + base.pickableY = pickableY; + }; +} + +export function mixTrigger(tileX: number, tileY: number, tileW: number, tileH: number, tall: boolean): MixinEntity { + const x = toWorldX(tileX); + const y = toWorldY(tileY); + const w = toWorldX(tileW); + const h = toWorldY(tileH); + const bounds = rect(x, y, w, h); + + return base => { + base.triggerBounds = bounds; + base.triggerTall = tall; + base.triggerOn = false; + }; +} + +export function collider(x: number, y: number, w: number, h: number, tall = true, exact = false): Collider { + return { x, y, w, h, tall, exact }; +} + +export const ponyColliders = roundedColliderList(-12, -4, 25, 7, 2); +export const ponyCollidersBounds = getColliderBounds(ponyColliders); + +function getColliderBounds(colliders: Collider[]) { + const bounds = rect(0, 0, 0, 0); + + for (const collider of colliders) { + addRect(bounds, collider); + } + + return bounds; +} + +function roundedColliderList(x: number, y: number, w: number, h: number, stepsCount: number, tall = true) { + const list: Collider[] = []; + const steps = predefinedSteps[stepsCount]; + + if (DEVELOPMENT && !steps) { + console.error('Invalid step count', steps); + } + + for (let i = 0; i < steps.length; i++) { + list.push(collider(x + steps[i], y + i, w - steps[i] * 2, 1, tall)); + } + + list.push(collider(x, y + steps.length, w, h - steps.length * 2, tall)); + + for (let i = 0; i < steps.length; i++) { + const ii = steps.length - (i + 1); + list.push(collider(x + steps[ii], y + h - steps.length + i, w - steps[ii] * 2, 1, tall)); + } + + return list; +} + +export function mixColliderRect(x: number, y: number, w: number, h: number, tall = true, exact = false): MixinEntity { + return mixColliders(collider(x, y, w, h, tall, exact)); +} + +export function mixColliderRounded(x: number, y: number, w: number, h: number, stepsCount: number, tall = true): MixinEntity { + return mixColliders(...roundedColliderList(x, y, w, h, stepsCount, tall)); +} + +export function mixColliders(...list: Collider[]): MixinEntity { + const bounds = getColliderBounds(list); + + return base => { + base.flags |= EntityFlags.CanCollideWith; + base.colliders = list; + base.collidersBounds = bounds; + }; +} + +export function taperColliderSE(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x + ix, y + iy, w - ix, 1, tall)); + } + + return colliders; +} + +export function taperColliderSW(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x, y + iy, w - ix, 1, tall)); + } + + return colliders; +} + +export function taperColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x, y + iy, w - ix, 1, tall)); + } + + return colliders; +} + +export function taperColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x + ix, y + iy, w - ix, 1, tall)); + } + + return colliders; +} + +export function skewColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x - ix, y + iy, w, 1, tall)); + } + + return colliders; +} + +export function skewColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x + ix, y + iy, w, 1, tall)); + } + + return colliders; +} + +export function triangleColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x - ix, y + iy, w + ix, 1, tall)); + } + + return colliders; +} + +export function triangleColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { + const colliders: Collider[] = []; + + for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { + colliders.push(collider(x, y + iy, w + ix, 1, tall)); + } + + return colliders; +} + +export function mixInteract(x: number, y: number, w: number, h: number, interactRange?: number): MixinEntity { + const interactBounds = rect(x, y, w, h); + + return base => { + base.flags |= EntityFlags.Interactive; + base.interactBounds = interactBounds; + base.interactRange = interactRange; + }; +} + +export function mixInteractAt(interactRange?: number): MixinEntity { + return base => { + base.flags |= EntityFlags.Interactive; + base.interactRange = interactRange; + }; +} + +export function mixMinimap(color: number, rect: Rect, order = 1): MixinEntity { + const minimap = { color, rect, order }; + return base => base.minimap = minimap; +} + +export interface AnimatedMixinOptions { + color?: number; + repeat?: boolean; + animations?: number[][]; + lightSprite?: AnimatedRenderable1; + useGameTime?: boolean; + flipped?: boolean; +} + +export function mixAnimation( + anim: AnimatedRenderable, fps: number, dx: number, dy: number, + { color = WHITE, repeat = true, animations, lightSprite, useGameTime, flipped = false }: AnimatedMixinOptions = {} +): MixinEntity { + const bounds = getBoundsForFrames(anim.frames, -dx, -dy); + const lightSpriteBounds = lightSprite ? getBoundsForFrames(lightSprite.frames, -dx, -dy) : rect(0, 0, 0, 0); + + if (SERVER && !TESTS) { + return base => base.bounds = bounds; + } + + return base => { + const defaultPalette = anim.shadow && createPalette(sprites.defaultPalette); + const palette = createPalette(anim.palette); + + let time = repeat ? Math.random() * 5 : 0; + let animation = 0; + let lastFrame = 0; + + const getFrame = (options: DrawOptions) => { + let frameNumber = Math.floor(time * fps); + + if (useGameTime) { + frameNumber = Math.floor((options.gameTime / 1000) * fps); + } + + if (animations) { + if (repeat) { + frameNumber = frameNumber % animations[animation].length; + } + + return at(animations[animation], frameNumber) || 0; + } else { + return repeat ? (frameNumber % anim.frames.length) : Math.min(frameNumber, anim.frames.length - 1); + } + }; + + base.bounds = bounds; + base.palettes = []; + defaultPalette && base.palettes.push(defaultPalette); + palette && base.palettes.push(palette); + + base.update = function (delta: number) { + time += delta; + + const anim = getAnimationFromEntityState(this.state); + + if (animations && anim !== animation) { + animation = anim; + time = 0; + } + + const frameNumber = Math.floor(time * fps); + + if (lastFrame !== frameNumber) { + lastFrame = frameNumber; + return true; + } else { + return false; + } + }; + + base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { + const frame = getFrame(options); + const frameSprite = anim.frames[frame]; + const x = toScreenX(this.x); + const y = toScreenYWithZ(this.y, this.z); + + batch.save(); + batch.translate(x, y); + + if (hasFlag(this.state, EntityState.FacingRight) || flipped) { + batch.scale(-1, 1); + } + + batch.translate(-dx, -dy); + anim.shadow && batch.drawSprite(anim.shadow, options.shadowColor, defaultPalette, 0, 0); + frameSprite && batch.drawSprite(frameSprite, color, palette, 0, 0); + batch.restore(); + }; + + if (lightSprite) { + base.lightSpriteColor = WHITE; + base.lightSpriteBounds = lightSpriteBounds; + base.drawLightSprite = function (batch, options) { + const frame = getFrame(options); + const frameSprite = lightSprite.frames[frame]; + const x = toScreenX(this.x); + const y = toScreenYWithZ(this.y, this.z); + + batch.save(); + batch.translate(x, y); + + if (hasFlag(this.state, EntityState.FacingRight) || flipped) { + batch.scale(-1, 1); + } + + batch.translate(-dx, -dy); + batch.drawSprite(frameSprite, this.lightSpriteColor!, 0, 0); + batch.restore(); + }; + } + }; +} + +export function mixDrawWindow( + sprite: PaletteRenderable, dx: number, dy: number, paletteIndex: number, + padLeft: number, padTop: number, padRight: number, padBottom: number, +): MixinEntity { + const bounds = getRenderableBounds(sprite, dx, dy); + + return base => { + base.bounds = bounds; + + if (!SERVER || TESTS) { + const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); + const palette = createPalette(att(sprite.palettes, paletteIndex)); + base.palettes = []; + defaultPalette && base.palettes.push(defaultPalette); + palette && base.palettes.push(palette); + + base.draw = function (batch, options) { + const baseX = toScreenX(this.x + (this.ox || 0)); + const baseY = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)); + const x = baseX - dx; + const y = baseY - dy; + + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } + + if (sprite.color !== undefined) { + batch.drawRect(options.lightColor, + x + padLeft, y + padTop, sprite.color.w - (padLeft + padRight), sprite.color.h - (padTop + padBottom)); + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } + }; + } + }; +} + +export function mixDraw(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): MixinEntity { + const bounds = getRenderableBounds(sprite, dx, dy); + + return base => { + base.bounds = bounds; + + if (!SERVER || TESTS) { + const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); + const palette = createPalette(att(sprite.palettes, paletteIndex)); + base.palettes = []; + defaultPalette && base.palettes.push(defaultPalette); + palette && base.palettes.push(palette); + + base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { + const x = toScreenX(this.x + (this.ox || 0)) - dx; + const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy; + const opacity = 1 - 0.6 * (this.coverLifting || 0); + + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } + + batch.globalAlpha = opacity; + + if (sprite.color !== undefined) { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } + + batch.globalAlpha = 1; + }; + } + }; +} + +export interface MixDraw { + sprite: PaletteRenderable; + dx: number; + dy: number; + palette: number; +} + +export interface MixDrawSeasonal { + summer: MixDraw; + autumn?: Partial; + winter?: Partial; + spring?: Partial; +} + +function addBounds(bounds: Rect, setup: MixDraw) { + addRect(bounds, getRenderableBounds(setup.sprite, setup.dx, setup.dy)); +} + +export function mixDrawSeasonal(setup: MixDrawSeasonal): MixinEntity { + const bounds = rect(0, 0, 0, 0); + const summer = setup.summer; + const autumn = { ...summer, ...setup.autumn }; + const winter = { ...summer, ...setup.winter }; + const spring = { ...summer, ...setup.spring }; + + addBounds(bounds, summer); + addBounds(bounds, autumn); + addBounds(bounds, winter); + addBounds(bounds, spring); + + return (base, _, worldState) => { + base.bounds = bounds; + + if (!SERVER || TESTS) { + let season = Season.Summer; + let { sprite, dx, dy, palette: paletteIndex } = setup.summer; + let defaultPalette: Palette | undefined = undefined; + let palette: Palette | undefined = undefined; + + const setupSeason = (newSeason: Season) => { + season = newSeason; + let set: MixDraw; + + switch (season) { + case Season.Summer: + set = summer; + break; + case Season.Autumn: + set = autumn; + break; + case Season.Winter: + set = winter; + break; + case Season.Spring: + set = spring; + break; + default: + invalidEnum(season); + return; + } + + sprite = set.sprite; + dx = set.dx; + dy = set.dy; + paletteIndex = set.palette; + + if (base.palettes) { + for (const palette of base.palettes) { + releasePalette(palette); + } + } + + defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); + palette = createPalette(att(sprite.palettes, paletteIndex)); + base.palettes = []; + defaultPalette && base.palettes.push(defaultPalette); + palette && base.palettes.push(palette); + }; + + setupSeason(worldState.season); + + base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { + const x = toScreenX(this.x + (this.ox || 0)) - dx; + const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy; + const opacity = 1 - 0.6 * (this.coverLifting || 0); + + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } + + batch.globalAlpha = opacity; + + if (sprite.color !== undefined) { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } + + batch.globalAlpha = 1; + + if (season !== options.season) { + setupSeason(options.season); + } + }; + } + }; +} + +function splitSprite(sprite: Sprite, x: number, w: number, h: number) { + const result: Sprite[] = []; + + for (let y = 0; y < sprite.h; y += h) { + result.push({ x: sprite.x + x, y: sprite.y + y, w, h, ox: sprite.ox, oy: sprite.oy, type: sprite.type }); + } + + return result; +} + +const poles = [ + { sprite: sprites.direction_pole_3, dy: -39 }, + { sprite: sprites.direction_pole_4, dy: -50 }, + { sprite: sprites.direction_pole_5, dy: -61 }, +]; + +const shadowLeft = sprites.direction_shadow_left.shadow; +const shadowRight = sprites.direction_shadow_right.shadow; +const leftSprites = splitSprite(sprites.direction_left_right.color, 0, 17, 10); +const rightSprites = splitSprite(sprites.direction_left_right.color, 17, 17, 10); + +const dirUpDown = [ + { + shadowUp: sprites.direction_shadow_up_left.shadow, + shadowDown: sprites.direction_shadow_down_right.shadow, + spriteUp: sprites.direction_up_left.color, + spriteDown: sprites.direction_down_right.color, + shadowUpDX: -6, shadowUpDY: -9, + shadowDownDX: -1, shadowDownDY: 3, + upDX: -6, upDY: -7, + downDX: -1, downDY: 5, + }, + { + shadowUp: sprites.direction_shadow_up_right.shadow, + shadowDown: sprites.direction_shadow_down_left.shadow, + spriteUp: sprites.direction_up_right.color, + spriteDown: sprites.direction_down_left.color, + shadowUpDX: 1, shadowUpDY: -9, + shadowDownDX: -4, shadowDownDY: 3, + upDX: 1, upDY: -7, + downDX: -5, downDY: 4, + }, +]; + +export function mixDrawDirectionSign(): MixinEntity { + const poleDX = -4; + const leftDX = -20; + const rightDX = 3; + const plateDY = 2; + const leftRightStep = 11; + const upDownStep = 11; + + return (base, options = {}) => { + const { sign: { r = 0, w = [], e = [], s = [], n = [] } = {} } = options as SignEntityOptions; + const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5); + const boundsH = 7 + max * 11; + + base.bounds = rect(-20, -boundsH, 40, boundsH); + base.options = options; + + if (SERVER && !TESTS) + return; + + const { + shadowUp, shadowDown, spriteUp, spriteDown, upDX, upDY, downDX, downDY, + shadowUpDX, shadowUpDY, shadowDownDX, shadowDownDY, + } = dirUpDown[r]; + const leftShadow = !!w.length; + const rightShadow = !!e.length; + const upShadow = !!n.length; + const downShadow = !!s.length; + const pole = poles[max - 3]; + + const defaultPalette = pole.sprite.shadow && createPalette(sprites.defaultPalette); + const palette = createPalette(att(pole.sprite.palettes, 0)); + base.palettes = []; + defaultPalette && base.palettes.push(defaultPalette); + palette && base.palettes.push(palette); + + base.draw = function (batch, options) { + const x = toScreenX(this.x); + const y = toScreenYWithZ(this.y, this.z); + + batch.drawSprite(pole.sprite.shadow, options.shadowColor, defaultPalette, x + poleDX, y + pole.dy); + leftShadow && batch.drawSprite(shadowLeft, options.shadowColor, defaultPalette, x - 18, y - 1); + rightShadow && batch.drawSprite(shadowRight, options.shadowColor, defaultPalette, x + 4, y - 1); + upShadow && batch.drawSprite(shadowUp, options.shadowColor, defaultPalette, x + shadowUpDX, y + shadowUpDY); + downShadow && batch.drawSprite(shadowDown, options.shadowColor, defaultPalette, x + shadowDownDX, y + shadowDownDY); + + for (let i = n.length - 1; i >= 0; i--) { + if (n[i] !== -1) { + batch.drawSprite(spriteUp, WHITE, palette, x + upDX, y + pole.dy + upDY + i * upDownStep); + } + } + + batch.drawSprite(pole.sprite.color, WHITE, palette, x + poleDX, y + pole.dy); + + for (let i = 0; i < w.length; i++) { + if (w[i] !== -1) { + const sprite = leftSprites[w[i]]; + sprite && batch.drawSprite(sprite, WHITE, palette, x + leftDX, y + pole.dy + plateDY + i * leftRightStep); + } + } + + for (let i = 0; i < e.length; i++) { + if (e[i] !== -1) { + const sprite = rightSprites[e[i]]; + sprite && batch.drawSprite(rightSprites[e[i]], WHITE, palette, x + rightDX, y + pole.dy + plateDY + i * leftRightStep); + } + } + + for (let i = s.length - 1; i >= 0; i--) { + if (s[i] !== -1) { + batch.drawSprite(spriteDown, WHITE, palette, x + downDX, y + pole.dy + downDY + i * upDownStep); + } + } + }; + }; +} + +export function mixLight(color: number, dx: number, dy: number, w: number, h: number): MixinEntity { + return base => { + if (!SERVER || TESTS) { + base.lightOn = true; + base.lightColor = color; + base.lightScale = 1; + base.lightTarget = 1; + base.lightScaleAdjust = 1; + base.lightBounds = rect(-(dx + w / 2), -(dy + h / 2), w, h); + base.drawLight = function (batch: SpriteBatch) { + if (!this.lightOn) + return; + + const x = toScreenX(this.x); + const y = toScreenYWithZ(this.y, this.z); + const s = this.lightScale! * this.lightScaleAdjust!; + const width = w * s; + const height = h * s; + const color = this.lightColor!; + + batch.drawImage(color, -1, -1, 2, 2, x - (dx + width / 2), y - (dy + height / 2), width, height); + }; + } + }; +} + +export function mixLightSprite(sprite: Sprite, color: number, dx: number, dy: number): MixinEntity { + return base => { + if (!SERVER || TESTS) { + base.lightSpriteOn = true; + base.lightSpriteX = dx; + base.lightSpriteY = dy; + base.lightSpriteColor = color; + base.lightSpriteBounds = getBounds(sprite, -dx, -dy); + base.drawLightSprite = function (batch: SpriteBatch) { + if (!this.lightSpriteOn) + return; + + const x = toScreenX(this.x) - this.lightSpriteX!; + const y = toScreenYWithZ(this.y, this.z) - this.lightSpriteY!; + + batch.drawSprite(sprite, this.lightSpriteColor || BLACK, x, y); + }; + } + }; +} + +export function mixDrawRain(): MixinEntity { + const sprite = sprites.rainfall.color; // 110x477 + const bounds = rect(toScreenX(-4), -sprite.h, toScreenX(8), sprite.h); + + return base => { + base.bounds = bounds; + + if (SERVER && !TESTS) + return; + + let time = 0; + const palette = createPalette(sprites.defaultPalette); + base.palettes = [palette]; + + // update(delta: number) { + // time += delta * 1000; + + // if (time > 200) { + // time -= 200; + // } + // }, + + base.draw = function (batch: PaletteSpriteBatch) { + const x = toScreenX(this.x) + bounds.x; + const y = toScreenYWithZ(this.y, this.z) - sprite.h + Math.floor(time); + batch.drawImage(sprite.type, RED, palette, sprite.x, sprite.y, sprite.w, sprite.h, x, y, sprite.w, sprite.h); + }; + }; +} + +export function mixDrawShadow(sprite: PaletteRenderable, dx: number, dy: number, shadowColor?: number): MixinEntity { + const bounds = getRenderableBounds(sprite, dx, dy); + + return base => { + base.bounds = bounds; + + if (!SERVER || TESTS) { + const defaultPalette = createPalette(sprites.defaultPalette); + base.palettes = [defaultPalette]; + base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { + const x = toScreenX(this.x + (this.ox || 0)) - dx; + const y = toScreenYWithZ(this.y + (this.oy || 0), this.z) - dy; + const color = shadowColor === undefined ? options.shadowColor : shadowColor; + sprite.shadow && batch.drawSprite(sprite.shadow, color, defaultPalette, x, y); + }; + } + }; +} + +export function mixBobbing(bobsFps: number, bobs: number[]): MixinEntity { + return base => { + base.flags |= EntityFlags.Bobbing; + base.bobsFps = bobsFps; + base.bobs = bobs; + }; +} + +let fullWalls = true; + +export function toggleWalls() { + fullWalls = !fullWalls; +} + +export function mixDrawWall( + full: PaletteRenderable, half: PaletteRenderable, dx: number, dy: number, dy2: number +): MixinEntity { + const fullBounds = getRenderableBounds(full, dx, dy); + // const halfBounds = getRenderableBounds(half, dx, dy2); + + return base => { + base.bounds = fullBounds; // fullWalls ? fullBounds : halfBounds + + if (SERVER && !TESTS) + return; + + const fullPalette = createPalette(att(full.palettes, 0)); + const halfPalette = createPalette(att(half.palettes, 0)); + + base.palettes = []; + fullPalette && base.palettes.push(fullPalette); + halfPalette && base.palettes.push(halfPalette); + + base.draw = function (batch: PaletteSpriteBatch) { + const sprite = fullWalls ? full : half; + const palette = fullWalls ? fullPalette : halfPalette; + const x = toScreenX(this.x) - dx; + const y = toScreenYWithZ(this.y, this.z) - (fullWalls ? dy : dy2); + sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y); + }; + }; +} + +export function mixDrawSpider( + sprite: PaletteRenderable, dx: number, dy: number +): MixinEntity { + const heightOffset = 30; + const spriteColor = sprite.color; + const baseBounds = getRenderableBounds(sprite, dx, dy); + + if (!spriteColor) + throw new Error('Missing sprite'); + + return base => { + const { height, time } = base.options as { height: number; time: number; }; + const bounds = { ...baseBounds }; + bounds.y -= (height + heightOffset); + bounds.h += height; + base.bounds = bounds; + + if (SERVER && !TESTS) + return; + + const palette = createPalette(sprite.palettes && sprite.palettes[0]); + base.palettes = [palette]; + + base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { + const t = options.gameTime / SECOND - time; + const h = clamp(Math.sin(t / 4) * 4, 0, 1) * height; + + if (h < height) { + const lineLength = height - h - 4; + const x = toScreenX(this.x) - dx; + const y = toScreenYWithZ(this.y, this.z) - dy - heightOffset - h; + + batch.drawRect(0x181818ff, x + 2, y - lineLength, 1, lineLength + 1); + batch.drawSprite(spriteColor, WHITE, palette, x, y); + } + }; + }; +} diff --git a/src/ts/common/movementUtils.ts b/src/ts/common/movementUtils.ts new file mode 100644 index 0000000..e31097d --- /dev/null +++ b/src/ts/common/movementUtils.ts @@ -0,0 +1,123 @@ +import { EntityState, Point, Rect, Entity } from './interfaces'; +import { PONY_SPEED_TROT, PONY_SPEED_WALK, tileWidth, tileHeight } from './constants'; +import { clamp, hasFlag } from './utils'; +import { toWorldX, toWorldY } from './positionUtils'; +import { rect } from './rect'; + +const DIRS = [ + [0, -1], // 0 + [0.5, -1], + [1, -1], + [1, -0.5], + [1, 0], // 4 + [1, 0.5], + [1, 1], + [0.5, 1], + [0, 1], // 8 + [-0.5, 1], + [-1, 1], + [-1, 0.5], + [-1, 0], // 12 + [-1, -0.5], + [-1, -1], + [-0.5, -1], +]; + +const SECA = 0xcd3003ca; +const SECB = 0x5b903a62; +const SECC = 0x1c267e56; +const SECD = 0x1921ba6f; +const SECE = 0x0000bc0e; +const PI2 = Math.PI * 2; +const DIRS_ANGLE = DIRS.length / PI2; + +export function flagsToSpeed(flags: EntityState): number { + const state = flags & EntityState.PonyStateMask; + + if (state === EntityState.PonyTrotting) { + return PONY_SPEED_TROT; + } else if (state === EntityState.PonyWalking) { + return PONY_SPEED_WALK; + } else { + return 0; + } +} + +export function dirToVector(dir: number): Point { + const [x, y] = DIRS[(dir | 0) % DIRS.length]; + return { x, y }; +} + +export function vectorToDir(x: number, y: number): number { + const angle = Math.atan2(x, -y); + return Math.round((angle < 0 ? angle + PI2 : angle) * DIRS_ANGLE) % DIRS.length; +} + +export interface Movement { + x: number; + y: number; + dir: number; + flags: EntityState; + time: number; + camera: Rect; +} + +export const POSITION_MIN = 0; +export const POSITION_MAX = 100000; + +export function encodeMovement( + x: number, y: number, dir: number, flags: EntityState, time: number, camera: Rect +): [number, number, number, number, number] { + const pixelX = Math.floor(clamp(x, POSITION_MIN, POSITION_MAX) * tileWidth); + const pixelY = Math.floor(clamp(y, POSITION_MIN, POSITION_MAX) * tileHeight); + const camX = ((pixelX - camera.x) & 0xfff) >>> 0; + const camY = ((pixelY - camera.y) & 0xfff) >>> 0; + const camW = (camera.w & 0xfff) >>> 0; + const camH = (camera.h & 0xfff) >>> 0; + + const a = pixelX | ((dir & 0xff) << 24); + const b = pixelY | ((flags & 0xff) << 24); + const c = time; + const d = (camX << 20) | (camY << 8) | (camW >>> 4); + const e = ((camW & 0xf) << 12) | camH; + + return [ + (a ^ SECA) >>> 0, + (b ^ SECB) >>> 0, + (c ^ SECC) >>> 0, + (d ^ SECD) >>> 0, + (e ^ SECE) >>> 0, + ]; +} + +export function decodeMovement(a: number, b: number, c: number, d: number, e: number): Movement { + a = (a >>> 0) ^ SECA; + b = (b >>> 0) ^ SECB; + c = (c >>> 0) ^ SECC; + d = (d >>> 0) ^ SECD; + e = (e >>> 0) ^ SECE; + + const pixelX = a & 0xffffff; + const pixelY = b & 0xffffff; + + const x = toWorldX(pixelX + 0.5); + const y = toWorldY(pixelY + 0.5); + const dir = (a >>> 24) & 0xff; + const flags = (b >>> 24) & 0xff; + const time = c; + + const camX = pixelX - ((d >>> 20) & 0xfff); + const camY = pixelY - ((d >>> 8) & 0xfff); + const camW = ((d & 0xff) << 4) | ((e >>> 12) & 0xf); + const camH = e & 0xfff; + + return { x, y, dir, flags, time, camera: rect(camX, camY, camW, camH) }; +} + +export function isMovingRight(vx: number, right: boolean): boolean { + return vx < 0 ? false : (vx > 0 ? true : right); +} + +export function shouldBeFacingRight(entity: Entity): boolean { + return isMovingRight(entity.vx, hasFlag(entity.state, EntityState.FacingRight)); +} diff --git a/src/ts/common/offsets.ts b/src/ts/common/offsets.ts new file mode 100644 index 0000000..2b0de49 --- /dev/null +++ b/src/ts/common/offsets.ts @@ -0,0 +1,138 @@ +interface Point { + x: number; + y: number; +} + +type Pt = [number, number]; + +const createPoint = ([x, y]: Pt): Point => ({ x, y }); +const createPoints = (pts: Pt[]) => pts.map(createPoint); + +export const cmOffsets: Point[] = []; +export const headOffsets: Point[] = []; +export const tailOffsets: Point[] = []; +export const wingOffsets: Point[] = []; +export const frontLegOffsets: Point[] = []; +export const backLegOffsets: Point[] = []; +export const neckAccessoryOffsets: Point[] = []; +export const backAccessoryOffsets: Point[] = []; +export const waistAccessoryOffsets: Point[] = []; +export const chestAccessoryOffsets: Point[] = []; + +function offsets( + _index: number, cm: Pt, head: Pt, tail: Pt, wing: Pt, frontLeg: Pt, backLeg: Pt, + neckAccessory: Pt, backAccessory: Pt, waistAccessory: Pt, chestAccessory: Pt +) { + cmOffsets.push(createPoint(cm)); + headOffsets.push(createPoint(head)); + tailOffsets.push(createPoint(tail)); + wingOffsets.push(createPoint(wing)); + frontLegOffsets.push(createPoint(frontLeg)); + backLegOffsets.push(createPoint(backLeg)); + neckAccessoryOffsets.push(createPoint(neckAccessory)); + backAccessoryOffsets.push(createPoint(backAccessory)); + waistAccessoryOffsets.push(createPoint(waistAccessory)); + chestAccessoryOffsets.push(createPoint(chestAccessory)); +} + +// stand: cm head tail wing frontLeg backLeg neck back waist chest +offsets(0, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]); + +// sit: cm head tail wing frontLeg backLeg neck back waist chest +offsets(1, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]); +offsets(2, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 1], [1, 1]); +offsets(3, [1, 1], [0, 1], [1, 1], [2, 1], [2, 0], [2, 0], /***/[2, 1], [1, 1], [0, 1], [2, 2]); +offsets(4, [4, 2], [2, 3], [4, 3], [5, 2], [5, 2], [5, 2], /***/[4, 3], [4, 3], [3, 3], [5, 4]); +offsets(5, [7, 6], [4, 6], [7, 7], [7, 4], [7, 4], [7, 6], /***/[6, 5], [6, 6], [5, 5], [7, 6]); +offsets(6, [8, 9], [7, 8], [9, 14], [8, 7], [9, 5], [8, 8], /***/[9, 8], [8, 7], [8, 7], [9, 8]); +offsets(7, [8, 12], [8, 8], [9, 15], [9, 9], [9, 5], [8, 11], /***/[9, 7], [8, 8], [8, 7], [9, 7]); +offsets(8, [8, 12], [9, 7], [9, 15], [9, 9], [9, 5], [8, 11], /***/[9, 7], [8, 8], [8, 7], [9, 7]); +offsets(9, [8, 11], [9, 6], [9, 14], [9, 8], [9, 5], [8, 11], /***/[9, 6], [8, 8], [8, 7], [9, 6]); + +// lie: cm head tail wing frontLeg backLeg neck back waist chest +offsets(10, [8, 11], [8, 6], [9, 14], [9, 9], [9, 6], [8, 11], /***/[9, 6], [8, 8], [8, 7], [9, 6]); +offsets(11, [8, 11], [7, 7], [9, 14], [8, 9], [8, 6], [8, 11], /***/[8, 7], [8, 8], [7, 7], [8, 6]); +offsets(12, [8, 11], [6, 9], [9, 14], [7, 10], [7, 7], [8, 11], /***/[7, 9], [8, 8], [6, 8], [7, 7]); +offsets(13, [8, 11], [6, 11], [9, 14], [7, 10], [6, 9], [8, 11], /***/[6, 11], [8, 8], [5, 10], [6, 9]); +offsets(14, [8, 11], [7, 12], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 12], [8, 8], [6, 10], [7, 10]); +offsets(15, [8, 11], [7, 11], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 11], [8, 8], [6, 10], [7, 9]); +offsets(16, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]); + +export const EAR_ACCESSORY_OFFSETS = createPoints([ + [0, 0], // 0 + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], // 5 +]); + +export const EXTRA_ACCESSORY_OFFSETS = createPoints([ + [0, 9], // 0 + [0, 0], + [0, 0], + [0, 1], + [0, 2], + [0, 2], // 5 + [0, 1], + [0, 1], + [0, 2], + [0, 3], + [0, 2], // 10 + [0, 2], + [0, 3], + [0, 1], + [0, 1], + [0, 1], // 15 + [0, 9], + [0, 3], + [0, 3], + [0, 3], + [0, 3], // 20 + [0, 3], + [0, 2], + [0, 3], + [0, 3], + [0, 3], // 25 + [0, 2], + [0, 3], + [-1, 3], + [0, 3], + [0, 3], // 30 + [0, 3], +]); + +export const HEAD_ACCESSORY_OFFSETS = createPoints([ + [0, 0], // 0 + [0, -5], + [0, -5], + [0, -4], + [0, -4], + [0, -4], // 5 + [0, -4], + [1, -4], + [0, -4], + [0, -3], + [0, -4], // 10 + [0, -4], + [0, -3], + [1, -5], + [0, -4], + [0, -4], // 15 + [0, 0], + [0, -4], + [0, -4], + [0, -4], + [0, -4], // 20 + [0, -4], + [0, -5], + [0, -5], + [0, -4], + [1, -3], // 25 + [0, -4], + [0, -4], + [0, -4], + [0, -4], + [0, -4], // 30 + [0, -3], +]); diff --git a/src/ts/common/pony.ts b/src/ts/common/pony.ts new file mode 100644 index 0000000..7bfc6b8 --- /dev/null +++ b/src/ts/common/pony.ts @@ -0,0 +1,678 @@ +import { PONY_WIDTH, PONY_HEIGHT, BLINK_FRAMES, canFly } from '../client/ponyUtils'; +import { stand, sneeze, defaultHeadAnimation, defaultBodyFrame, defaultHeadFrame } from '../client/ponyAnimations'; +import { + PaletteSpriteBatch, Pony, BodyAnimation, EntityState, SpriteBatch, ExpressionExtra, HeadAnimation, Palette, + PaletteManager, DrawOptions, Rect, EntityFlags, IMap, Entity, DoAction, Muzzle, Expression, isEyeSleeping, + Iris, EntityPlayerState, +} from './interfaces'; +import { hasFlag, setFlag } from './utils'; +import { blinkFps, PONY_TYPE } from './constants'; +import { releasePalettes } from './ponyInfo'; +import { createAnEntity, boopSplashRight, boopSplashLeft } from './entities'; +import { + createAnimationPlayer, isAnimationPlaying, drawAnimation, playAnimation, updateAnimation, playOneOfAnimations +} from './animationPlayer'; +import { blushColor, WHITE, MAGIC_ALPHA, HEARTS_COLOR } from './colors'; +import { encodeExpression, decodeExpression } from './encoders/expressionEncoder'; +import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils'; +import { getPonyAnimationFrame, getHeadY, drawPony, getPonyHeadPosition, createHeadTransform } from '../client/ponyDraw'; +import { + isPonySitting, isPonyFlying, isPonyLying, isPonyStanding, isPonyLandedOrCanLand, isIdle, isIdleAnimation, + isFacingRight, releaseEntity +} from './entityUtils'; +import { + getAnimation, getAnimationFrame, setAnimatorState, updateAnimator, createAnimator, AnimatorState, + resetAnimatorState +} from './animator'; +import { + trotting, flying, hovering, toBoopState, isFlyingUpOrDown, isFlyingDown, isSittingDown, isSittingUp, swinging, + standing, sitting, lying, swimming, isSwimmingState, swimmingToFlying, +} from '../client/ponyStates'; +import { decodePonyInfo } from './compressPony'; +import { defaultPonyState, defaultDrawPonyOptions, isStateEqual } from '../client/ponyHelpers'; +import { + sneezeAnimation, holdPoofAnimation, heartsAnimation, tearsAnimation, cryAnimation, zzzAnimations, magicAnimation +} from '../client/spriteAnimations'; +import { rect } from './rect'; +import { addOrRemoveFromEntityList } from './worldMap'; +import { hasDrawLight, hasLightSprite } from '../client/draw'; +import { ponyColliders, ponyCollidersBounds } from './mixins'; +import { PonyTownGame } from '../client/game'; +import { playEffect } from '../client/handlers'; +import * as sprites from '../generated/sprites'; +import { withAlpha } from './color'; + +const flyY = 15; +const lightExtentX = 100; +const lightExtentY = 70; +const emptyBounds = rect(0, 0, 0, 0); +const bounds = rect(-PONY_WIDTH / 2, -PONY_HEIGHT, PONY_WIDTH, PONY_HEIGHT + 5); +const boundsFly = rect(bounds.x, bounds.y - flyY, bounds.w, bounds.h + flyY); +const lightBounds = makeLightBounds(bounds); +const lightBoundsFly = makeLightBounds(boundsFly); +const interactBounds = rect(-20, -50, 40, 50); +const interactBoundsFly = rect(interactBounds.x, interactBounds.y - flyY, interactBounds.w, interactBounds.h); +const defaultExpr = encodeExpression(undefined); + +export function createPony( + id: number, state: EntityState, info: string | Uint8Array | undefined, defaultPalette: Palette, + paletteManager: PaletteManager +): Pony { + const pony: Pony = { + id, + state, + playerState: EntityPlayerState.None, + type: PONY_TYPE, + flags: EntityFlags.Movable | EntityFlags.CanCollide | EntityFlags.Interactive, + expr: defaultExpr, + ponyState: defaultPonyState(), + x: 0, + y: 0, + z: 0, + vx: 0, + vy: 0, + info, + order: 0, + timestamp: 0, + colliders: ponyColliders, + collidersBounds: ponyCollidersBounds, + selected: false, + extra: false, + toy: 0, + swimming: false, + ex: false, // extended data indicator, sent in extended option + inTheAirDelay: 0, + name: undefined, + tag: undefined, + site: undefined, + modInfo: undefined, + hold: 0, + palettePonyInfo: undefined, + headAnimation: undefined, + batch: undefined, + discardBatch: false, + headTime: Math.random() * 5, + blinkTime: 0, + nextBlink: Math.random() * 5, + currentExpression: defaultExpr, + drawingOptions: { ...defaultDrawPonyOptions(), shadow: true, bounce: BETA }, + zzzEffect: createAnimationPlayer(defaultPalette), + cryEffect: createAnimationPlayer(defaultPalette), + sneezeEffect: createAnimationPlayer(defaultPalette), + holdPoofEffect: createAnimationPlayer(defaultPalette), + heartsEffect: createAnimationPlayer(defaultPalette), + magicEffect: createAnimationPlayer(paletteManager.addArray(sprites.magic2.palette)), + animator: createAnimator(), + lastX: 0, + lastY: 0, + lastRight: false, + lastState: defaultPonyState(), + initialized: false, + doAction: DoAction.None, + bounds: bounds, + interactBounds: interactBounds, + chatBounds: interactBounds, + lightBounds: emptyBounds, + lightSpriteBounds: emptyBounds, + paletteManager, + lastBoopSplash: 0, + magicColor: 0, + }; + + pony.ponyState.drawFaceExtra = batch => drawFaceExtra(batch, pony); + + return pony; +} + +export function isPony(entity: Entity): entity is Pony { + return entity.type === PONY_TYPE; +} + +export function isPonyOnTheGround(pony: Pony) { + return !isPonyFlying(pony) && !isFlyingUpOrDown(pony.animator.state); +} + +export function getPaletteInfo(pony: Pony) { + return ensurePonyInfoDecoded(pony); +} + +export function releasePony(pony: Pony) { + if (pony.ponyState.holding) { + releaseEntity(pony.ponyState.holding); + } + + releasePalettePonyInfo(pony); +} + +export function canPonyFly(pony: Pony) { + return !!pony.palettePonyInfo && canFly(pony.palettePonyInfo); +} + +export function canPonyLie(pony: Pony, map: IMap) { + return !isPonyLying(pony) && (isIdle(pony) || isSittingDown(pony.animator.state) || isFlyingDown(pony.animator.state)) && + isPonyLandedOrCanLand(pony, map); +} +export function canPonySit(pony: Pony, map: IMap) { + return !isPonySitting(pony) && (isIdle(pony) || isFlyingDown(pony.animator.state)) && + isPonyLandedOrCanLand(pony, map); +} +export function canPonyStand(pony: Pony, map: IMap) { + return !isPonyStanding(pony) && (isIdleAnimation(pony.ponyState.animation) || isSittingUp(pony.animator.state)) && + isPonyLandedOrCanLand(pony, map); +} +export function canPonyFlyUp(pony: Pony) { + return !isPonyFlying(pony) && canPonyFly(pony) && !isFlyingUpOrDown(pony.animator.state); +} + +export function getPonyChatHeight(pony: Pony) { + const baseHeight = 2; + const state = pony.ponyState; + + if (pony.animator.state === trotting) { + return baseHeight; + } else if (pony.animator.state === flying || pony.animator.state === hovering) { + return baseHeight - 16; + } else { + const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame); + const animation = state.headAnimation || defaultHeadAnimation; + const headFrame = getPonyAnimationFrame(animation, state.headAnimationFrame, defaultHeadFrame); + return baseHeight + getHeadY(frame, headFrame); + } +} + +export function updatePonyInfo(pony: Pony, info: string | Uint8Array, apply: () => void) { + pony.info = info; + + if (pony.palettePonyInfo !== undefined) { + releasePalettePonyInfo(pony); + ensurePonyInfoDecoded(pony); + pony.discardBatch = true; + + if (isPonyFlying(pony) && !canPonyFly(pony)) { + DEVELOPMENT && console.warn('Force land'); + pony.state = setFlag(pony.state, EntityState.PonyFlying, false); + resetAnimatorState(pony.animator); + } + + apply(); + } +} + +export function ensurePonyInfoDecoded(pony: Pony) { + if (pony.info !== undefined && pony.palettePonyInfo === undefined) { + pony.palettePonyInfo = decodePonyInfo(pony.info, pony.paletteManager); + const wingType = pony.palettePonyInfo.wings && pony.palettePonyInfo.wings.type || 0; + pony.animator.variant = wingType === 4 ? 'bug' : ''; + pony.ponyState.blushColor = blushColor(pony.palettePonyInfo.coatPalette.colors[1]); + pony.magicColor = withAlpha(pony.palettePonyInfo.magicColorValue, MAGIC_ALPHA); + } + + return pony.palettePonyInfo!; +} + +export function invalidatePalettesForPony(pony: Pony) { + pony.discardBatch = true; +} + +export function doBoopPonyAction(game: PonyTownGame, pony: Pony) { + doPonyAction(pony, DoAction.Boop); + + if (pony.swimming && pony.lastBoopSplash < performance.now()) { + if (isFacingRight(pony)) { + playEffect(game, pony, boopSplashRight.type); + } else { + playEffect(game, pony, boopSplashLeft.type); + } + + pony.lastBoopSplash = performance.now() + 800; + } +} + +export function doPonyAction(pony: Pony, action: DoAction) { + pony.doAction = action; +} + +export function setPonyExpression(pony: Pony, expr: number) { + pony.expr = expr; +} + +export function hasExtendedInfo(pony: Pony) { + return pony.ex; +} + +export function hasHeadAnimation(pony: Pony) { + return pony.headAnimation !== undefined; +} + +export function setHeadAnimation(pony: Pony, headAnimation: HeadAnimation | undefined) { + if (pony.headAnimation !== headAnimation) { + pony.headTime = 0; + pony.headAnimation = headAnimation; + } +} + +export function drawPonyEntity(batch: PaletteSpriteBatch, pony: Pony, drawOptions: DrawOptions) { + if (pony.discardBatch && pony.batch !== undefined) { + batch.releaseBatch(pony.batch); + pony.batch = undefined; + pony.discardBatch = false; + } + + if (pony.batch !== undefined) { + batch.drawBatch(pony.batch); + } else if (pony.palettePonyInfo !== undefined) { + let swimming = false; + + if (isSwimmingState(pony.animator.state)) { + if (pony.animator.state === swimmingToFlying) { + swimming = pony.animator.time < 0.4; + } else { + swimming = true; + } + } + + const createBatch = pony.vx === 0 && pony.vy === 0 && !swimming; + const right = isFacingRight(pony); + + if (createBatch) { + batch.startBatch(); + } + + batch.save(); + transformBatch(batch, pony); + + const options = pony.drawingOptions; + options.flipped = right; + options.selected = pony.selected === true; + options.extra = pony.extra; + options.toy = pony.toy; + options.swimming = swimming; + options.shadow = !pony.swimming; + options.gameTime = drawOptions.gameTime + pony.id * 0.1; + + options.shadowColor = drawOptions.shadowColor; + + const ponyState = pony.ponyState; + drawPony(batch, pony.palettePonyInfo, ponyState, 0, 0, options); + + if ( + isAnimationPlaying(pony.zzzEffect) || isAnimationPlaying(pony.sneezeEffect) || + isAnimationPlaying(pony.holdPoofEffect) || isAnimationPlaying(pony.heartsEffect) || + isAnimationPlaying(pony.magicEffect) + ) { + const { x, y } = getPonyHeadPosition(pony.ponyState, 0, 0); + const right = isFacingRight(pony); + const flip = right ? !ponyState.headTurned : ponyState.headTurned; + batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState)); + drawAnimation(batch, pony.zzzEffect, 0, 0, WHITE, flip); + drawAnimation(batch, pony.sneezeEffect, 0, 0, WHITE, flip); + drawAnimation(batch, pony.holdPoofEffect, 0, 0, WHITE, flip); + drawAnimation(batch, pony.heartsEffect, 0, 0, HEARTS_COLOR, flip); + + if (pony.magicEffect.currentAnimation !== undefined) { + drawAnimation(batch, pony.magicEffect, 0, 0, pony.magicColor, flip); + const sprite = sprites.magic3.frames[pony.magicEffect.frame]; + sprite && batch.drawSprite(sprite, WHITE, pony.heartsEffect.palette, 0, 0); + } + } + + batch.restore(); + + if (createBatch) { + pony.batch = batch.finishBatch(); + pony.lastX = toScreenX(pony.x); + pony.lastY = toScreenYWithZ(pony.y, pony.z); + pony.lastRight = right; + pony.zzzEffect.dirty = false; + pony.cryEffect.dirty = false; + pony.sneezeEffect.dirty = false; + pony.holdPoofEffect.dirty = false; + pony.heartsEffect.dirty = false; + pony.magicEffect.dirty = false; + Object.assign(pony.lastState, ponyState); + } + } +} + +const magickLightSizes = [ + 0, 1.02, // fade-in + 0.97, 0.94, 0.91, 0.94, 0.97, 1.00, // loop + 0.97, 0.94, 0.91, // fade-out +]; + +export function drawPonyEntityLight(batch: SpriteBatch, pony: Pony, options: DrawOptions) { + const ponyState = pony.ponyState; + const holding = ponyState.holding; + const drawHolding = holding !== undefined && holding.drawLight !== undefined; + const drawMagic = pony.magicEffect.currentAnimation !== undefined; + const draw = drawHolding || drawMagic; + + if (draw) { + batch.save(); + transformBatch(batch, pony); + const { x, y } = getPonyHeadPosition(ponyState, 0, 0); + batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState)); + + if (drawHolding) { + holding!.x = toWorldX(holding!.pickableX!); + holding!.y = toWorldY(holding!.pickableY!); + holding!.drawLight!(batch, options); + } + + if (drawMagic) { + const size = 200 * (magickLightSizes[pony.magicEffect.frame] || 0); + batch.drawImage(WHITE, -1, -1, 2, 2, 30 - size / 2, 27 - size / 2, size, size); + } + + batch.restore(); + } +} + +export function drawPonyEntityLightSprite(batch: SpriteBatch, pony: Pony, options: DrawOptions) { + const ponyState = pony.ponyState; + const holding = ponyState.holding; + const drawHolding = holding !== undefined && holding.drawLightSprite !== undefined; + // const drawMagic = pony.magicEffect.currentAnimation !== undefined; + const draw = drawHolding; // || drawMagic; + + if (draw) { + batch.save(); + transformBatch(batch, pony); + const { x, y } = getPonyHeadPosition(ponyState, 0, 0); + batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState)); + + if (drawHolding) { + holding!.x = toWorldX(holding!.pickableX!); + holding!.y = toWorldY(holding!.pickableY!); + holding!.drawLightSprite!(batch, options); + } + + // if (drawMagic) { + // const frame = pony.magicEffect.frame; + // const sprite = sprites.magic2_light.frames[frame]; + // batch.drawSprite(sprite, WHITE, 0, 0); + // } + + batch.restore(); + } +} + +export function flagsToState(state: EntityState, moving: boolean, isSwimming: boolean): AnimatorState { + const ponyState = state & EntityState.PonyStateMask; + + if (isSwimming) { + return swimming; + } else if (moving) { + if (ponyState === EntityState.PonyFlying) { + return flying; + } else { + return trotting; + } + } else { + switch (ponyState) { + case EntityState.PonyStanding: return standing; + case EntityState.PonyWalking: return trotting; + case EntityState.PonyTrotting: return trotting; + case EntityState.PonySitting: return sitting; + case EntityState.PonyLying: return lying; + case EntityState.PonyFlying: return hovering; + default: + throw new Error(`Invalid pony state (${ponyState})`); + } + } +} + +export function updatePonyEntity(pony: Pony, delta: number, gameTime: number, safe: boolean) { + // update state + const state = pony.ponyState; + const walking = pony.vx !== 0 || pony.vy !== 0; + const animationState = flagsToState(pony.state, walking, pony.swimming); + + if (pony.inTheAirDelay > 0) { + pony.inTheAirDelay -= delta; + } + + if (pony.doAction !== DoAction.None) { + switch (pony.doAction) { + case DoAction.Boop: + setAnimatorState(pony.animator, toBoopState(animationState) || animationState); + break; + case DoAction.Swing: + setAnimatorState(pony.animator, swinging); + break; + case DoAction.HoldPoof: + playAnimation(pony.holdPoofEffect, holdPoofAnimation); + break; + default: + if (DEVELOPMENT) { + console.error(`Invalid DoAction: ${pony.doAction}`); + } + } + + pony.doAction = DoAction.None; + } else { + setAnimatorState(pony.animator, animationState); + } + + // head + pony.headTime += delta; + + if (pony.headAnimation !== undefined) { + const frame = Math.floor(pony.headTime * pony.headAnimation.fps); + + if (frame >= pony.headAnimation.frames.length && !pony.headAnimation.loop) { + pony.headAnimation = undefined; + state.headAnimationFrame = 0; + } else { + state.headAnimationFrame = frame % pony.headAnimation.frames.length; + } + } + + if (state.headAnimation !== pony.headAnimation) { + state.headAnimation = pony.headAnimation; + + if (pony.headAnimation === sneeze) { + playAnimation(pony.sneezeEffect, sneezeAnimation); + } + } + + // effects / expressions + if (pony.currentExpression !== pony.expr) { + updatePonyExpression(pony, pony.expr, safe); + } + + if ((pony.state & EntityState.Magic) !== 0) { + playAnimation(pony.magicEffect, magicAnimation); + } else { + playAnimation(pony.magicEffect, undefined); + } + + updateAnimation(pony.zzzEffect, delta); + updateAnimation(pony.cryEffect, delta); + updateAnimation(pony.sneezeEffect, delta); + updateAnimation(pony.holdPoofEffect, delta); + updateAnimation(pony.heartsEffect, delta); + updateAnimation(pony.magicEffect, delta); + + // holding + const holdingUpdated = + state.holding !== undefined && + state.holding.update !== undefined && + state.holding.update(delta, gameTime); + + // blink + pony.blinkTime += delta; + + if ((pony.blinkTime - pony.nextBlink) > 1) { + pony.nextBlink = pony.blinkTime + Math.random() * 2 + 3; + } + + // update animator + updateAnimator(pony.animator, delta); + + // update state + const blinkFrame = Math.floor((pony.blinkTime - pony.nextBlink) * blinkFps); + state.blinkFrame = blinkFrame < BLINK_FRAMES.length ? BLINK_FRAMES[blinkFrame] : 1; + state.headTurned = (pony.state & EntityState.HeadTurned) !== 0; + state.animation = getAnimation(pony.animator) || stand; + state.animationFrame = getAnimationFrame(pony.animator); + + // randomize animator time at startup + if (!pony.initialized) { + pony.initialized = true; + updateAnimator(pony.animator, Math.random() * 2); + } + + // discard batch if outdated + if (pony.batch !== undefined) { + const options = pony.drawingOptions; + const right = isFacingRight(pony); + + if ( + holdingUpdated || + toScreenX(pony.x) !== pony.lastX || toScreenYWithZ(pony.y, pony.z) !== pony.lastY || + pony.lastRight !== right || + pony.zzzEffect.dirty || pony.cryEffect.dirty || pony.sneezeEffect.dirty || pony.holdPoofEffect.dirty || + pony.heartsEffect.dirty || pony.magicEffect.dirty || + options.flipped !== right || options.selected !== pony.selected || options.extra !== pony.extra || + options.toy !== pony.toy || + !isStateEqual(pony.lastState, state) + ) { + pony.discardBatch = true; + } + } + + // update bounds + const flying = isPonyFlying(pony); + const flyingUpOrDown = isFlyingUpOrDown(pony.animator.state); + const flyingOrFlyingUpOrDown = flying || flyingUpOrDown; + + pony.bounds = flyingOrFlyingUpOrDown ? boundsFly : bounds; + pony.interactBounds = flying ? interactBoundsFly : interactBounds; + pony.lightBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds; + pony.lightSpriteBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds; +} + +export function updatePonyHold(pony: Pony, game: PonyTownGame) { + const ponyState = pony.ponyState; + const hadLight = hasDrawLight(pony); + const hadLightSprite = hasLightSprite(pony); + + if (pony.hold !== 0) { + if (ponyState.holding === undefined) { + ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game); + } else if (ponyState.holding.type !== pony.hold) { + releaseEntity(ponyState.holding); + ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game); + } + } else if (ponyState.holding !== undefined) { + releaseEntity(ponyState.holding); + ponyState.holding = undefined; + } + + const hasLight = hasDrawLight(pony); + const hasLightSprite1 = hasLightSprite(pony); + + addOrRemoveFromEntityList(game.map.entitiesLight, pony, hadLight, hasLight); + addOrRemoveFromEntityList(game.map.entitiesLightSprite, pony, hadLightSprite, hasLightSprite1); +} + +function filterExpression(expression: Expression) { + const extra = expression.extra; + const blush = hasFlag(extra, ExpressionExtra.Blush); + + if ( + blush || + hasFlag(extra, ExpressionExtra.Hearts) || + hasFlag(extra, ExpressionExtra.Cry) || + isEyeSleeping(expression.left) || + isEyeSleeping(expression.right) + ) { + if (expression.muzzle === Muzzle.SmilePant || expression.muzzle === Muzzle.NeutralPant) { + expression.muzzle = Muzzle.Neutral; + } + } + + if ( + blush || + expression.muzzle === Muzzle.SmilePant || + expression.muzzle === Muzzle.NeutralPant + ) { + if (expression.leftIris === Iris.Up || expression.rightIris === Iris.Up) { + expression.leftIris = Iris.Forward; + expression.rightIris = Iris.Forward; + } + + if (expression.muzzle === Muzzle.SmilePant) { + expression.muzzle = Muzzle.SmileOpen; + } else if (expression.muzzle === Muzzle.NeutralPant) { + expression.muzzle = Muzzle.NeutralOpen2; + } + } + + if (blush) { + if (expression.muzzle === Muzzle.SmileOpen2) { + expression.muzzle = Muzzle.SmileOpen; + } else if (expression.muzzle === Muzzle.FrownOpen) { + expression.muzzle = Muzzle.ConcernedOpen; + } else if (expression.muzzle === Muzzle.NeutralOpen2) { + expression.muzzle = Muzzle.Oh; + } + } +} + +function updatePonyExpression(pony: Pony, expr: number, safe: boolean) { + const expression = decodeExpression(expr); + pony.currentExpression = pony.expr; + pony.ponyState.expression = expression; + + if (expression && safe) { + filterExpression(expression); + } + + const extra = (expression && expression.extra) || 0; + + if (hasFlag(extra, ExpressionExtra.Cry)) { + playAnimation(pony.cryEffect, cryAnimation); + } else if (hasFlag(extra, ExpressionExtra.Tears)) { + playAnimation(pony.cryEffect, tearsAnimation); + } else { + playAnimation(pony.cryEffect, undefined); + } + + if (hasFlag(extra, ExpressionExtra.Zzz)) { + playOneOfAnimations(pony.zzzEffect, zzzAnimations); + } else { + playAnimation(pony.zzzEffect, undefined); + } + + if (hasFlag(extra, ExpressionExtra.Hearts)) { + playAnimation(pony.heartsEffect, heartsAnimation); + } else { + playAnimation(pony.heartsEffect, undefined); + } +} + +function transformBatch(batch: SpriteBatch | PaletteSpriteBatch, entity: Entity) { + batch.translate(toScreenX(entity.x), toScreenYWithZ(entity.y, entity.z)); + batch.scale(isFacingRight(entity) ? -1 : 1, 1); +} + +function releasePalettePonyInfo(pony: Pony) { + if (pony.palettePonyInfo !== undefined) { + releasePalettes(pony.palettePonyInfo); + pony.palettePonyInfo = undefined; + } +} + +function makeLightBounds({ x, y, w, h }: Rect) { + return rect(x - lightExtentX, y - lightExtentY, w + lightExtentX * 2, h + lightExtentY * 2); +} + +function drawFaceExtra(batch: PaletteSpriteBatch, pony: Pony) { + if (isAnimationPlaying(pony.cryEffect)) { + const flip = isFacingRight(pony) ? !pony.ponyState.headTurned : pony.ponyState.headTurned; + const maxY = isPonyLying(pony) ? 62 : (isPonySitting(pony) ? 65 : 0); + drawAnimation(batch, pony.cryEffect, 0, 0, WHITE, flip, maxY); + } +} diff --git a/src/ts/common/ponyInfo.ts b/src/ts/common/ponyInfo.ts new file mode 100644 index 0000000..2fdc198 --- /dev/null +++ b/src/ts/common/ponyInfo.ts @@ -0,0 +1,587 @@ +import * as sprites from '../generated/sprites'; +import { releasePalette, createPalette } from '../graphics/paletteManager'; +import { + PonyInfo, SpriteSet, PalettePonyInfo, PaletteSpriteSet, PaletteManager, Palette, ColorExtraSets, PonyInfoBase, + PonyInfoNumber, ColorExtra +} from './interfaces'; +import { toInt, array, includes, att } from './utils'; +import { CM_SIZE } from './constants'; +import { parseColorFast, getR, getAlpha, colorFromRGBA, getG, getB, colorToHexRGB } from './color'; +import { BLACK, fillToOutline, fillToOutlineColor, WHITE, TRANSPARENT, fillToOutlineWithDarken } from './colors'; +import { + mergedManes, mergedBackManes, mergedFacialHair, mergedEarAccessories, mergedChestAccessories, + SLEEVED_ACCESSORIES, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories +} from '../client/ponyUtils'; + +const MAX_COLORS = 6; +const FILLS = ['1e90ff', '32cd32', 'da70d6', 'dc143c', '7fffd4']; + +const frontHooves = sprites.frontLegHooves[1] as ColorExtraSets; +const backHooves = sprites.backLegHooves[1] as ColorExtraSets; +const frontLegAccessories = sprites.frontLegAccessories[1] as ColorExtraSets; +const backLegAccessories = sprites.backLegAccessories[1] as ColorExtraSets; +const frontLegSleeves = sprites.frontLegSleeves[1] as ColorExtraSets; + +type Arr = (T | undefined)[] | undefined; +type PonyInfoGeneric = PonyInfoBase>; + +export const mockPaletteManager: PaletteManager = { + add(colors: number[]): Palette { + return this.addArray(new Uint32Array(colors)); + }, + addArray(colors: Uint32Array): Palette { + return createPalette(colors); + }, + init() { + } +}; + +export function spriteSet(type: number, lockFirstFill = true, fill = 'ffd700', otherFills = FILLS): SpriteSet { + if (otherFills.length !== (MAX_COLORS - 1)) + throw new Error('Invalid fills count'); + + const fills = [fill, ...otherFills]; + const outlines = fills.map(fillToOutline); + + return { + type, + pattern: 0, + fills, + outlines, + lockFills: [lockFirstFill, ...array(MAX_COLORS - 1, false)], + lockOutlines: array(MAX_COLORS, true), + }; +} + +export function createDefaultPony(): PonyInfo { + const pony = createBasePony(); + pony.mane!.type = 2; + pony.backMane!.type = 1; + pony.tail!.type = 1; + return pony; +} + +export function createBasePony(): PonyInfo { + return syncLockedPonyInfo({ + head: spriteSet(0, true, 'ff0000', ['800000', '32cd32', 'da70d6', 'dc143c', '7fffd4']), + nose: spriteSet(0, true, 'ff0000', ['800000', '32cd32', 'da70d6', 'dc143c', '7fffd4']), + ears: spriteSet(0, true, 'ff0000'), + horn: spriteSet(0, true, 'ff0000'), + wings: spriteSet(0, true, 'ff0000'), + frontHooves: spriteSet(0, false, 'ffa500', ['ffff00', '32cd32', 'da70d6', 'dc143c', '7fffd4']), + backHooves: spriteSet(0, true, 'ffa500'), + + mane: spriteSet(0, false), + backMane: spriteSet(0), + tail: spriteSet(0), + facialHair: spriteSet(0), + + headAccessory: spriteSet(0, false, 'ee82ee'), + earAccessory: spriteSet(0, false, '808080'), + faceAccessory: spriteSet(0, false, '000000'), + neckAccessory: spriteSet(0, false, 'ee82ee'), + frontLegAccessory: spriteSet(0, false, 'ee82ee'), + backLegAccessory: spriteSet(0, false, 'ee82ee'), + frontLegAccessoryRight: spriteSet(0, false, 'ee82ee'), + backLegAccessoryRight: spriteSet(0, false, 'ee82ee'), + lockBackLegAccessory: true, + unlockFrontLegAccessory: false, + unlockBackLegAccessory: false, + backAccessory: spriteSet(0, false, 'ee82ee'), + waistAccessory: spriteSet(0, false, '95856f', ['674b43', '4f4f4f', '525252', 'c37850', '8a3d34']), + chestAccessory: spriteSet(0, false, 'ee82ee'), + sleeveAccessory: spriteSet(0, true, 'ee82ee'), + extraAccessory: { + ...spriteSet(0, true, 'ff0000', ['daa520', 'ffd700', 'ffd700', 'ffd700', 'ffd700']), + lockFills: array(5, true), + }, + + coatFill: 'ff0000', + coatOutline: '8b0000', + lockCoatOutline: true, + + eyelashes: 0, + eyeColorLeft: 'daa520', + eyeColorRight: 'daa520', + eyeWhitesLeft: 'ffffff', + eyeWhites: 'ffffff', + eyeOpennessLeft: 1, + eyeOpennessRight: 1, + eyeshadow: false, + eyeshadowColor: '000000', + lockEyes: true, + lockEyeColor: true, + unlockEyeWhites: false, + unlockEyelashColor: false, + eyelashColor: '000000', + eyelashColorLeft: '000000', + + fangs: 0, + muzzle: 0, + freckles: 0, + frecklesColor: '8b0000', + magicColor: 'ffffff', + + cm: [], + cmFlip: false, + + customOutlines: false, + freeOutlines: false, + darkenLockedOutlines: false, + }); +} + +// sync + +type FillToOutline = (fill: T | undefined) => T | undefined; + +export function getBaseFill(set?: SpriteSet): T | undefined { + return set && set.fills && set.fills[0]; +} + +export function getBaseOutline(set?: SpriteSet): T | undefined { + return set && set.outlines && set.outlines[0]; +} + +export function syncLockedSpriteSet( + set: SpriteSet | undefined, customOutlines: boolean, fillToOutline: FillToOutline, baseFill?: T, + baseOutline?: T +) { + if (set === undefined) + return; + + const fills = set.fills; + + if (!fills) + return; + + const lockFills = set.lockFills; + + if (lockFills) { + for (let i = 0; i < lockFills.length; i++) { + if (lockFills[i]) { + fills[i] = i === 0 ? baseFill : fills[0]; + } + } + } + + const outlines = set.outlines; + const lockOutlines = set.lockOutlines; + + if (outlines && lockOutlines) { + for (let i = 0; i < lockOutlines.length; i++) { + if (!customOutlines) { + lockOutlines[i] = true; + } + + if (lockOutlines[i]) { + if (i === 0 && baseOutline && lockFills && lockFills[i]) { + outlines[i] = baseOutline; + } else { + outlines[i] = fillToOutline(fills[i]); + } + } + } + } +} + +function syncLockedSpritesSet2( + set: SpriteSet | undefined, fillToOutline: FillToOutline, baseFills: (T | undefined)[], + baseOutlines: (T | undefined)[] +) { + if (set && set.fills && set.lockFills) { + set.lockFills.forEach((locked, i) => { + if (locked) { + set.fills![i] = baseFills[i]; + } + }); + } + + if (set && set.fills && set.outlines && set.lockOutlines) { + set.lockOutlines.forEach((locked, i) => { + if (locked) { + if (baseOutlines[i] && set.lockFills && set.lockFills[i]) { + set.outlines![i] = baseOutlines[i]; + } else { + set.outlines![i] = fillToOutline(set.fills![i]); + } + } + }); + } +} + +function getFillOf2(set: SpriteSet | undefined, defaultColor: T): T | undefined { + return set && set.type && set.fills && set.fills[0] || defaultColor; +} + +function getOutlineOf2(set: SpriteSet | undefined, defaultColor: T): T | undefined { + return set && set.type && set.outlines && set.outlines[0] || defaultColor; +} + +function syncLockedBasePonyInfo( + info: PonyInfoGeneric, fillToOutline: FillToOutline, defaultColor: T +): PonyInfoGeneric { + const customOutlines = !!info.customOutlines; + + if (!customOutlines || info.lockCoatOutline) { + info.coatOutline = fillToOutline(info.coatFill); + } + + if (info.lockEyes) { + info.eyeOpennessLeft = info.eyeOpennessRight; + } + + if (info.lockEyeColor) { + info.eyeColorLeft = info.eyeColorRight; + } + + if (!info.unlockEyeWhites) { + info.eyeWhitesLeft = info.eyeWhites; + } + + if (!info.unlockEyelashColor) { + info.eyelashColorLeft = info.eyelashColor; + } + + syncLockedSpriteSet(info.head, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.nose, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.ears, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.horn, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.wings, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.frontHooves, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet( + info.backHooves, customOutlines, fillToOutline, getBaseFill(info.frontHooves), getBaseOutline(info.frontHooves)); + + syncLockedSpriteSet(info.mane, customOutlines, fillToOutline); + + const baseManeFill = getBaseFill(info.mane); + const baseManeOutline = getBaseOutline(info.mane); + + syncLockedSpriteSet(info.backMane, customOutlines, fillToOutline, baseManeFill, baseManeOutline); + syncLockedSpriteSet(info.tail, customOutlines, fillToOutline, baseManeFill, baseManeOutline); + syncLockedSpriteSet(info.facialHair, customOutlines, fillToOutline, baseManeFill, baseManeOutline); + + syncLockedSpriteSet(info.headAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.earAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.faceAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.neckAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.frontLegAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.backLegAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.frontLegAccessoryRight, customOutlines, fillToOutline); + syncLockedSpriteSet(info.backLegAccessoryRight, customOutlines, fillToOutline); + syncLockedSpriteSet(info.backAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.waistAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.chestAccessory, customOutlines, fillToOutline); + + if (info.chestAccessory && !info.sleeveAccessory && includes(SLEEVED_ACCESSORIES, info.chestAccessory.type)) { + info.sleeveAccessory = { + type: 0, + pattern: 0, + fills: [], + outlines: [], + lockFills: array(MAX_COLORS, true), + lockOutlines: array(MAX_COLORS, true), + }; + } + + syncLockedSpriteSet( + info.sleeveAccessory, customOutlines, fillToOutline, getBaseFill(info.chestAccessory), getBaseOutline(info.chestAccessory)); + + syncLockedSpritesSet2(info.extraAccessory, fillToOutline, [ + info.coatFill, + info.eyeColorRight, + getFillOf2(info.mane, defaultColor), + getFillOf2(info.backMane, defaultColor), + getFillOf2(info.tail, defaultColor), + ], [ + info.coatOutline, + info.eyeColorRight, + getOutlineOf2(info.mane, defaultColor), + getOutlineOf2(info.backMane, defaultColor), + getOutlineOf2(info.tail, defaultColor), + ]); + + return info; +} + +export function syncLockedPonyInfo(info: PonyInfo): PonyInfo { + const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines; + const fillToOutlineFunc = darkenLocked ? fillToOutlineWithDarken : fillToOutline; + return syncLockedBasePonyInfo(info, fillToOutlineFunc, '000000'); +} + +function fillToOutlineSafe(color: number | undefined) { + return fillToOutlineColor((color === undefined || color === 0) ? BLACK : color); +} + +function fillToOutlineSafeWithDarken(color: number | undefined) { + return darkenForOutline(fillToOutlineColor((color === undefined || color === 0) ? BLACK : color)); +} + +export function syncLockedPonyInfoNumber(info: PonyInfoNumber): PonyInfoNumber { + const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines; + const fillToOutlineFunc = darkenLocked ? fillToOutlineSafeWithDarken : fillToOutlineSafe; + return syncLockedBasePonyInfo(info, fillToOutlineFunc, BLACK); +} + +// PalettePonyInfo + +function parseFast(color: string | undefined): number { + return color ? parseColorFast(color) : BLACK; +} + +function parseCMColor(color: string): number { + return color ? parseColorFast(color) : TRANSPARENT; +} + +export function toColorList(colors: (string | undefined)[]): Uint32Array { + const result = new Uint32Array(colors.length + 1); + + for (let i = 0; i < colors.length; i++) { + result[i + 1] = parseFast(colors[i]); + } + + return result; +} + +export function darkenForOutline(color: number) { + const mult = (159 / 255); + const r = (mult * getR(color)) | 0; + const g = (mult * getG(color)) | 0; + const b = (mult * getB(color)) | 0; + const a = getAlpha(color); + return colorFromRGBA(r, g, b, a); +} + +function getColorsGeneric( + fillColors: Arr, outlineColors: Arr, defaultColor: string, length: number, darken: boolean +): string[] { + const fills = fillColors || []; + const outlines = outlineColors || []; + const colors = array(length * 2, defaultColor); + + for (let i = 0; i < length; i++) { + colors[i * 2] = fills[i] || defaultColor; + + if (darken) { + colors[i * 2 + 1] = outlines[i] ? colorToHexRGB(darkenForOutline(parseColorFast(outlines[i]!))) : defaultColor; + } else { + colors[i * 2 + 1] = outlines[i] || defaultColor; + } + } + + return colors; +} + +export function getColorsFromSet({ fills, outlines }: SpriteSet, defaultColor: string, darken: boolean): string[] { + const length = Math.max(fills ? fills.length : 0, outlines ? outlines.length : 0); + return getColorsGeneric(fills, outlines, defaultColor, length, darken); +} + +export function toColorListNumber(colors: (number | undefined)[]): Uint32Array { + const result = new Uint32Array(colors.length + 1); + + for (let i = 0; i < colors.length; i++) { + result[i + 1] = colors[i] || BLACK; + } + + return result; +} + +export type GetColorsForSet = (set: SpriteSet, count: number, darken: boolean) => Uint32Array; + +export const getColorsForSet: GetColorsForSet = (set, count, darken) => { + const t = getColorsGeneric(set.fills, set.outlines, '000000', count, darken); + return toColorList(t); +}; + +const emptyArray: number[] = []; + +export const getColorsForSetNumber: GetColorsForSet = (set, length, darken) => { + const fills = set.fills || emptyArray; + const outlines = set.outlines || emptyArray; + const result = new Uint32Array(length * 2 + 1); + + for (let i = 0; i < length; i++) { + result[((i << 1) + 1) | 0] = i < fills.length ? (fills[i] || BLACK) : BLACK; + + if (darken) { + result[((i << 1) + 2) | 0] = i < outlines.length ? darkenForOutline(outlines[i] || BLACK) : BLACK; + } else { + result[((i << 1) + 2) | 0] = i < outlines.length ? (outlines[i] || BLACK) : BLACK; + } + } + + return result; +}; + +function getExtraPalette(pattern: ColorExtra | undefined, manager: PaletteManager): Palette | undefined { + const extraPalette = pattern && pattern.palettes && pattern.palettes[0]; + return extraPalette && manager.addArray(new Uint32Array(extraPalette)); +} + +export function toPaletteSet( + set: SpriteSet, sets: ColorExtraSets, manager: PaletteManager, getColorsForSet: GetColorsForSet, + hasExtra: boolean, darken: boolean +): PaletteSpriteSet | undefined { + const pattern = att(att(sets, set.type), set.pattern); + const colorCount = pattern !== undefined && pattern.colors !== undefined ? ((pattern.colors - 1) >> 1) : 0; + const colors = getColorsForSet(set, colorCount, darken); + + return { + type: toInt(set.type), + pattern: toInt(set.pattern), + palette: manager.addArray(colors), + extraPalette: hasExtra ? getExtraPalette(pattern, manager) : undefined, + }; +} + +function createCMPalette( + cm: T[] | undefined, manager: PaletteManager, parseColor: (color: T) => number +): Palette | undefined { + const size = CM_SIZE * CM_SIZE; + + if (cm === undefined || cm.length === 0 || cm.length > size) + return undefined; + + const result = new Uint32Array(size); + + for (let i = 0; i < cm.length; i++) { + result[i] = parseColor(cm[i]); + } + + return manager.addArray(result); +} + +export type ToSet = (set: SpriteSet | undefined, sets: ColorExtraSets, extra?: boolean) => PaletteSpriteSet | undefined; + +const defaultPalette = new Uint32Array(sprites.defaultPalette); + +export const createToPaletteSet = + (manager: PaletteManager, getColorsForSet: GetColorsForSet, extra: boolean, darken: boolean): ToSet => + (set, sets) => set === undefined ? undefined : toPaletteSet(set, sets, manager, getColorsForSet, extra, darken); + +export function toPaletteGeneric( + info: PonyInfoGeneric, manager: PaletteManager, toColorList: (color: (T | undefined)[]) => Uint32Array, + getColorsForSet: GetColorsForSet, blackColor: T, whiteColor: T, parseCMColor: (color: T) => number +): PalettePonyInfo { + const darken = !info.freeOutlines; + const toSet = createToPaletteSet(manager, getColorsForSet, false, darken); + const toSetExtra = createToPaletteSet(manager, getColorsForSet, true, darken); + + const defaultSet = { type: 0, pattern: 0, fills: [info.coatFill], outlines: [info.coatOutline] }; + // const defaultSet = { type: 0, pattern: 1, fills: [info.coatFill, whiteColor], outlines: [info.coatOutline, blackColor] }; + + return { + body: toSet(defaultSet, sprites.body[1]), + head: toSet(info.head || defaultSet, sprites.head0[1]), + nose: toSet(info.nose, sprites.noses[0]), + ears: toSet(info.ears || defaultSet, sprites.ears), + horn: toSet(info.horn, sprites.horns), + wings: toSet(info.wings, sprites.wings[0]), + frontLegs: toSet(defaultSet, sprites.frontLegs[1]), + backLegs: toSet(defaultSet, sprites.backLegs[1]), + frontHooves: toSet(info.frontHooves, frontHooves), + backHooves: toSet(info.backHooves, backHooves), + + mane: toSet(info.mane, mergedManes), + backMane: toSet(info.backMane, mergedBackManes), + tail: toSet(info.tail, sprites.tails[0]), + facialHair: toSet(info.facialHair, mergedFacialHair), + + headAccessory: toSet(info.headAccessory, mergedHeadAccessories), + earAccessory: toSet(info.earAccessory, mergedEarAccessories), + faceAccessory: toSetExtra(info.faceAccessory, sprites.faceAccessories), + // faceAccessoryExtraPalette: getExtraPartPalette(info.faceAccessory, sprites.faceAccessoriesExtra, manager), + neckAccessory: toSet(info.neckAccessory, sprites.neckAccessories[1]), + frontLegAccessory: toSet( + info.frontLegAccessory, frontLegAccessories), + backLegAccessory: toSet( + info.lockBackLegAccessory ? info.frontLegAccessory : info.backLegAccessory, backLegAccessories), + frontLegAccessoryRight: toSet( + info.unlockFrontLegAccessory ? info.frontLegAccessoryRight : info.frontLegAccessory, frontLegAccessories), + backLegAccessoryRight: toSet( + info.lockBackLegAccessory ? + (info.unlockFrontLegAccessory ? info.frontLegAccessoryRight : info.frontLegAccessory) : + (info.unlockBackLegAccessory ? info.backLegAccessoryRight : info.backLegAccessory), backLegAccessories), + lockBackLegAccessory: info.lockBackLegAccessory, + unlockFrontLegAccessory: info.unlockFrontLegAccessory, + unlockBackLegAccessory: info.unlockBackLegAccessory, + backAccessory: toSet(info.backAccessory, mergedBackAccessories), + waistAccessory: toSet(info.waistAccessory, sprites.waistAccessories[1]), + chestAccessory: toSet(info.chestAccessory, mergedChestAccessories), + sleeveAccessory: toSet(info.sleeveAccessory, frontLegSleeves), + extraAccessory: toSet(info.extraAccessory, mergedExtraAccessories), + + coatPalette: manager.addArray(toColorList([info.coatFill, info.coatOutline])), + coatFill: undefined, + coatOutline: undefined, + lockCoatOutline: !!info.lockCoatOutline, + + eyelashes: toInt(info.eyelashes), + eyePaletteLeft: manager.addArray(toColorList([ + info.eyeWhitesLeft || whiteColor, + info.eyelashColor || blackColor + ])), + eyePalette: manager.addArray(toColorList([ + info.eyeWhites || whiteColor, + (info.unlockEyelashColor ? info.eyelashColorLeft : info.eyelashColor) || blackColor + ])), + eyeColorLeft: manager.addArray(toColorList([info.eyeColorLeft])), + eyeColorRight: manager.addArray(toColorList([info.eyeColorRight])), + eyeWhitesLeft: undefined, + eyeWhites: undefined, + eyeOpennessLeft: toInt(info.eyeOpennessLeft), + eyeOpennessRight: toInt(info.eyeOpennessRight), + eyeshadow: info.eyeshadow, + eyeshadowColor: manager.addArray(toColorList([info.eyeshadowColor])), + lockEyes: !!info.lockEyes, + lockEyeColor: !!info.lockEyeColor, + unlockEyeWhites: !!info.unlockEyeWhites, + unlockEyelashColor: !!info.unlockEyelashColor, + eyelashColor: undefined, + eyelashColorLeft: undefined, + + fangs: toInt(info.fangs), + muzzle: toInt(info.muzzle), + freckles: 0, // remove + frecklesColor: undefined, // TODO: remove + magicColor: undefined, + magicColorValue: typeof info.magicColor === 'string' ? parseColorFast(info.magicColor) : toInt(info.magicColor), + + cm: undefined, + cmFlip: !!info.cmFlip, + cmPalette: createCMPalette(info.cm, manager, parseCMColor), + + customOutlines: !!info.customOutlines, + freeOutlines: !!info.freeOutlines, + darkenLockedOutlines: !!info.darkenLockedOutlines, + defaultPalette: manager.addArray(defaultPalette), + waterPalette: manager.addArray(sprites.pony_wake_1.palette), + }; +} + +export function toPalette(info: PonyInfo, manager = mockPaletteManager): PalettePonyInfo { + return toPaletteGeneric(info, manager, toColorList, getColorsForSet, '000000', 'ffffff', parseCMColor); +} + +export function toPaletteNumber(info: PonyInfoNumber, manager = mockPaletteManager): PalettePonyInfo { + return toPaletteGeneric(info, manager, toColorListNumber, getColorsForSetNumber, BLACK, WHITE, x => x); +} + +export function releasePalettes(info: PalettePonyInfo): void { + for (const key of Object.keys(info)) { + const value = (info as any)[key]; // undefined | number | string | PaletteSpriteSet | Palette; + + if (value && typeof value === 'object') { + if ('refs' in value) { + const palette = value as Palette; + releasePalette(palette); + } else if ('palette' in value) { + const set = value as PaletteSpriteSet; + releasePalette(set.palette); + releasePalette(set.extraPalette); + } + } + } +} diff --git a/src/ts/common/positionUtils.ts b/src/ts/common/positionUtils.ts new file mode 100644 index 0000000..c8028d9 --- /dev/null +++ b/src/ts/common/positionUtils.ts @@ -0,0 +1,70 @@ +import { tileWidth, tileHeight, tileElevation } from './constants'; +import { Point, Rect } from './interfaces'; + +export function toScreenX(x: number) { + return Math.floor(x * tileWidth) | 0; +} + +export function toScreenY(y: number) { + return Math.floor(y * tileHeight) | 0; +} + +export function toScreenYWithZ(y: number, z: number) { + return Math.floor(y * tileHeight - z * tileElevation) | 0; +} + +export function toWorldX(x: number) { + return x / tileWidth; +} + +export function toWorldY(y: number) { + return y / tileHeight; +} + +export function toWorldZ(z: number) { + return z / tileElevation; +} + +export function pointToScreen({ x, y }: Point): Point { + return { + x: toScreenX(x), + y: toScreenY(y), + }; +} + +export function pointToWorld({ x, y }: Point): Point { + return { + x: toWorldX(x), + y: toWorldY(y), + }; +} + +export function rectToScreen({ x, y, w, h }: Rect): Rect { + return { + x: toScreenX(x), + y: toScreenY(y), + w: toScreenX(w), + h: toScreenY(h), + }; +} + +export function roundPositionX(x: number) { + return Math.floor(x * tileWidth) / tileWidth; +} + +export function roundPositionY(y: number) { + return Math.floor(y * tileHeight) / tileHeight; +} + +export function roundPositionXMidPixel(x: number) { + return (Math.floor(x * tileWidth) + 0.5) / tileWidth; +} + +export function roundPositionYMidPixel(y: number) { + return (Math.floor(y * tileHeight) + 0.5) / tileHeight; +} + +export function roundPosition(point: Point) { + point.x = roundPositionX(point.x); + point.y = roundPositionY(point.y); +} diff --git a/src/ts/common/rect.ts b/src/ts/common/rect.ts new file mode 100644 index 0000000..c8c4363 --- /dev/null +++ b/src/ts/common/rect.ts @@ -0,0 +1,50 @@ +import { Rect, Point } from './interfaces'; +import { intersect } from './utils'; + +export function rect(x: number, y: number, w: number, h: number): Rect { + return { x, y, w, h }; +} + +export function centerPoint(rect: Rect): Point { + return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }; +} + +export function copyRect(dst: Rect, src: Rect) { + dst.x = src.x; + dst.y = src.y; + dst.w = src.w; + dst.h = src.h; +} + +export function withBorder({ x, y, w, h }: Rect, border: number) { + return rect(x - border, y - border, w + border * 2, h + border * 2); +} + +export function withPadding({ x, y, w, h }: Rect, top: number, right: number, bottom: number, left: number) { + return rect(x - top, y - left, w + left + right, h + top + bottom); +} + +export function rectsIntersect(a: Rect, b: Rect): boolean { + return intersect(a.x, a.y, a.w, a.h, b.x, b.y, b.w, b.h); +} + +export function addRect(a: Rect, b: Rect) { + const x = Math.min(a.x, b.x); + const y = Math.min(a.y, b.y); + + a.w = Math.max(a.x + a.w, b.x + b.w) - x; + a.h = Math.max(a.y + a.h, b.y + b.h) - y; + a.x = x; + a.y = y; +} + +export function addRects(a: Rect, b: Rect): Rect { + const x = Math.min(a.x, b.x); + const y = Math.min(a.y, b.y); + + return { + x, y, + w: Math.max(a.x + a.w, b.x + b.w) - x, + h: Math.max(a.y + a.h, b.y + b.h) - y, + }; +} diff --git a/src/ts/common/region.ts b/src/ts/common/region.ts new file mode 100644 index 0000000..6710e3f --- /dev/null +++ b/src/ts/common/region.ts @@ -0,0 +1,202 @@ +import { TileType, Region, IMap } from './interfaces'; +import { clamp } from './utils'; +import { tileWidth, tileHeight, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants'; +import { getRegion } from './worldMap'; +import { toScreenX, toScreenY } from './positionUtils'; +import { ponyColliders, ponyCollidersBounds } from './mixins'; +import { decompressTiles } from './compress'; + +const { min, max, floor } = Math; + +export function createRegion(x: number, y: number, tileData?: Uint8Array): Region { + const size = REGION_SIZE; + const tiles = tileData ? decompressTiles(tileData) : new Uint8Array(size * size); + const tileIndices = new Int16Array(size * size); + const randoms = new Uint8Array(size * size); + // const elevation = new Uint8Array(size * size); + const collider = new Uint8Array(size * size * tileWidth * tileHeight); + + if (!tileData) { + tiles.fill(TileType.Dirt); + } + + tileIndices.fill(-1); + + for (let i = 0; i < randoms.length; i++) { + randoms[i] = (Math.random() * 256) | 0; + } + + return { + x, y, tiles, tileIndices, + randoms, + // elevation, + entities: [], + colliders: [], + collider, + colliderDirty: true, + tilesDirty: true, + }; +} + +export function getRegionTile(region: Region, x: number, y: number): TileType { + return region.tiles[x | (y << 3)]; +} + +export function setRegionTile(region: Region, x: number, y: number, type: TileType) { + region.tiles[x | (y << 3)] = type; +} + +export function getRegionTileIndex(region: Region, x: number, y: number) { + return region.tileIndices[x | (y << 3)]; +} + +export function setRegionTileDirty(region: Region, x: number, y: number) { + region.tileIndices[x | (y << 3)] = -1; + region.tilesDirty = true; +} + +export function getRegionElevation(_region: Region, _x: number, _y: number) { + return 0; // region.elevation[x | (y << 3)]; +} + +export function setRegionElevation(_region: Region, _x: number, _y: number, _value: number) { + // region.elevation[x | (y << 3)] = value; +} + +export function worldToRegionX(x: number, map: IMap) { + return clamp(floor(x / REGION_SIZE), 0, map.regionsX - 1); +} + +export function worldToRegionY(y: number, map: IMap) { + return clamp(floor(y / REGION_SIZE), 0, map.regionsY - 1); +} + +export function invalidateRegionsCollider(region: Region, map: IMap) { + const minY = max(0, region.y - 1); + const maxY = min(map.regionsY - 1, region.y + 1); + const minX = max(0, region.x - 1); + const maxX = min(map.regionsX - 1, region.x + 1); + + for (let ry = minY; ry <= maxY; ry++) { + for (let rx = minX; rx <= maxX; rx++) { + const r = getRegion(map, rx, ry); + + if (r) { + r.colliderDirty = true; + } + } + } +} + +export function generateRegionCollider(region: Region, map: IMap) { + const regionCollider = region.collider; + const tileTypes = region.tiles; + + region.colliderDirty = false; + regionCollider.fill(0); + + for (let ty = 0, i = 0; ty < REGION_SIZE; ty++) { + for (let tx = 0; tx < REGION_SIZE; tx++ , i++) { + const type = tileTypes[i]; + + if (type === TileType.None) { + const x0 = (tx * tileWidth) | 0; + const y0 = (ty * tileHeight) | 0; + const x1 = (x0 + tileWidth) | 0; + const y1 = (y0 + tileHeight) | 0; + + for (let y = y0; y < y1; y++) { + for (let x = x0; x < x1; x++) { + regionCollider[(x + ((y * REGION_WIDTH) | 0)) | 0] = 3; + } + } + } + } + } + + const minY = max(0, region.y - 1); + const maxY = min(map.regionsY - 1, region.y + 1); + const minX = max(0, region.x - 1); + const maxX = min(map.regionsX - 1, region.x + 1); + + const pBounds = ponyCollidersBounds; + const pbX0 = pBounds.x | 0; + const pbY0 = pBounds.y | 0; + const pbX1 = (pbX0 + pBounds.w) | 0; + const pbY1 = (pbY0 + pBounds.h) | 0; + + const baseX = region.x * REGION_SIZE; + const baseY = region.y * REGION_SIZE; + + for (let ry = minY; ry <= maxY; ry++) { + for (let rx = minX; rx <= maxX; rx++) { + const r = getRegion(map, rx, ry); + + if (r === undefined) + continue; + + for (const entity of r.colliders) { + const entityX = toScreenX(entity.x - baseX) | 0; + const entityY = toScreenY(entity.y - baseY) | 0; + + const cBounds = entity.collidersBounds!; + const ecbX = entityX + cBounds.x; + const ecbY = entityY + cBounds.y; + + if ( + (ecbX + pbX0) > REGION_WIDTH || (ecbY + pbY0) > REGION_HEIGHT || + (ecbX + cBounds.w + pbX1) < 0 || (ecbY + cBounds.h + pbY1) < 0 + ) { + continue; + } + + for (const c of entity.colliders!) { + const value = (c.tall ? 3 : 1) | 0; + const baseX0 = (entityX + c.x) | 0; + const baseY0 = (entityY + c.y) | 0; + const baseX1 = (baseX0 + c.w) | 0; + const baseY1 = (baseY0 + c.h) | 0; + + if (c.exact) { + const x0 = (baseX0 < 0 ? 0 : baseX0) | 0; + const y0 = (baseY0 < 0 ? 0 : baseY0) | 0; + const x1 = (baseX1 > REGION_WIDTH ? REGION_WIDTH : baseX1) | 0; + const y1 = (baseY1 > REGION_HEIGHT ? REGION_HEIGHT : baseY1) | 0; + + if (x1 > x0 && y1 > y0) { + for (let y = y0 | 0; y < y1; y = (y + 1) | 0) { + const oy = (y * REGION_WIDTH) | 0; + + for (let x = x0 | 0; x < x1; x = (x + 1) | 0) { + regionCollider[(x + oy) | 0] |= value; + } + } + } + } else { + for (const pc of ponyColliders) { + const tx0 = (baseX0 + pc.x) | 0; + const ty0 = (baseY0 + pc.y) | 0; + const tx1 = (baseX1 + ((pc.x + pc.w) | 0)) | 0; + const ty1 = (baseY1 + ((pc.y + pc.h) | 0)) | 0; + + const x0 = (tx0 < 0 ? 0 : tx0) | 0; + const y0 = (ty0 < 0 ? 0 : ty0) | 0; + const x1 = (tx1 > REGION_WIDTH ? REGION_WIDTH : tx1) | 0; + const y1 = (ty1 > REGION_HEIGHT ? REGION_HEIGHT : ty1) | 0; + + if (x1 > x0 && y1 > y0) { + for (let y = y0 | 0; y < y1; y = (y + 1) | 0) { + const oy = (y * REGION_WIDTH) | 0; + + for (let x = x0 | 0; x < x1; x = (x + 1) | 0) { + regionCollider[(x + oy) | 0] |= value; + } + } + } + } + } + } + } + } + } +} diff --git a/src/ts/common/rollbar.ts b/src/ts/common/rollbar.ts new file mode 100644 index 0000000..cf9c2c8 --- /dev/null +++ b/src/ts/common/rollbar.ts @@ -0,0 +1,102 @@ +import { escapeRegExp } from 'lodash'; +import { LogArgument } from 'rollbar'; +import { CHARACTER_LIMIT_ERROR } from './errors'; + +const IGNORE = new RegExp([ + // adware / extensions + 'plantsandplay', 'anyplacetrivial', 'surfbuyermac', 'hotshoppymac', 'GM_getValue', '__gCrWeb.autofill', + '.com/affs', 'advpartners', 'tlscdn', 'yaaknaa', 'mecash', 'digitaloptout', + 'Script error', 'NS_ERROR_', 'davebestdeals', 'mflcdn', `'feedConf' of null`, 'n46gd0nenr1az.ru', + 'googst2.ru', 'downloader12.ru', 'adsafeprotected', 'gobobr.info', 'elt.parentNode', + `getElementsByTagName('video')`, 'chrome-extension', 'bestpriceninja', `'tgt' of null`, + 'jh8hrfnvs.ru', 'OperaIce', 'blueblockgames', 'adguard.com', 'kaspersky', 'igamesecrets.com', + 'Unexpected identifier', 'UnknownError', 'diableNightMode', 'Unexpected end of script', + 'Internal Server Error', 'hilitor', 'kejnoj7.ru', 'v207.info', 'inj_js', + 'object is not a function', `'Float32Array' is undefined`, 'vertamedia', '.ru/', 'v24s.net', + 'window.document.location is null', 'mediaonspot', 'ydpi.pw', 'moz-extension', 'trafficanalytics', + 'amazonaws', 'adtelligent', 'searchsens.info', 'solid-waste.top', 'cdn.immereeako.info', + 'technologiecoloniale.com', 'cloudcnfare.com', 'MyAppGet', 'rugged-r.top', `Can't find variable: webkit`, + `rgvqcsxqge.com`, 'all_small_polls', `Cannot read property 'document' of undefined`, `extAbbr is not defined`, + '__gCrWeb', 'DOMBnbPlug', + + // GPU errors + `Failed to execute 'shaderSource'`, + 'compiling shader', + 'Failed to create WebGL context', + 'CONTEXT_LOST_WEBGL', + 'Framebuffer unsupported', + 'Framebuffer failed for unspecified reason', + 'Недостаточно ресурсов памяти для завершения операции.', + 'Failed to initialize graphics device (Shader error)', + 'Failed to initialize graphics device (Failed to create WebGL context)', + 'Failed to initialize graphics device (Failed to create texture)', + 'Shader error', + + // GPU halt + 'GPU device instance has been suspended', + 'Die GPU-Geräteinstanz wurde angehalten', + 'GPU zostało zawieszone', + `GPU приостановлен`, + 'GPU se ha suspendido', + 'GPU aygıt örneği askıya alınmış', + 'GPU-enhetsinstansen har försatts', + + // other + 'androidInterface is not defined', + '/images/', + 'out of memory', + 'object is not a function', + 'Array buffer allocation failed', + 'Server is offline', + 'Failed to register a ServiceWorker', + 'Permission denied to access property', + 'Not enough storage is available', + 'Failed to initialize graphics device', + 'Not enough memory resources', + 'Ikke nok minneressurser tilgjengelig', // out of memory + 'suficientes recursos de memoria', + 'Onvoldoende geheugenbronnen', + `Cannot read property 'version' of undefined`, + + 'Maximum call stack size exceeded', // howler error on chrome mobile + + // user errors + CHARACTER_LIMIT_ERROR, + 'Too many requests', + 'Saving in progress', + 'Too many requests, please try again in', + 'Already waiting for join request', + + // server + 'Range Not Satisfiable', 'Precondition Failed', +].map(escapeRegExp).join('|'), 'i'); + +export interface Person { + id: string; + username: string; + custom?: any; +} + +function getLabel(arg: LogArgument | undefined) { + if (typeof arg === 'string') { + return arg; + } else if (arg && 'message' in arg) { + return arg.message + (arg.stack || ''); + } else { + return arg ? arg.toString() : ''; + } +} + +export function isIgnoredMessage(message: string) { + return IGNORE.test(message); +} + +export function isIgnoredError(error: Error) { + return isIgnoredMessage(error.message || `${error}` || '') || isIgnoredMessage(error.stack || ''); +} + +export function rollbarCheckIgnore(_isUncaught: boolean, args: LogArgument[], _payload: object): boolean { + return (Array.isArray(args) ? args : [args]) + .map(getLabel) + .some(isIgnoredMessage); +} diff --git a/src/ts/common/security.ts b/src/ts/common/security.ts new file mode 100644 index 0000000..58708ae --- /dev/null +++ b/src/ts/common/security.ts @@ -0,0 +1,140 @@ +import { escapeRegExp, compact, isMatchWith } from 'lodash'; +import { PonyInfoNumber, PonyInfo } from './interfaces'; +import { urlRegexTexts, ipRegexText } from './filterUtils'; +import { AuthBase, GeneralSettings, Suspicious, GameServerSettings } from './adminInterfaces'; +import { parseColorFast } from './color'; + +// suspicious + +export const urlRegex = new RegExp(urlRegexTexts.join('|'), 'ui'); +export const ipRegex = new RegExp(ipRegexText, 'ui'); + +function createRegExpFromList(list: string | undefined, wholeWords = false): RegExp | undefined { + const lines = list && compact(list.split(/\r?\n/).map(x => x.trim())); + + if (lines && lines.length) { + const combined = lines.map(escapeRegExp).join('|'); + + if (wholeWords) { + return new RegExp(`\\b(${combined})\\b`, 'ui'); + } else { + return new RegExp(combined, 'ui'); + } + } else { + return undefined; + } +} + +export const createCachedTest = (wholeWords = false) => { + let cachedList: string | undefined = undefined; + let cachedRegex: RegExp | undefined = undefined; + + return (list: string | undefined, value: string) => { + if (cachedList !== list) { + cachedList = list; + cachedRegex = createRegExpFromList(list, wholeWords); + } + + return cachedRegex ? cachedRegex.test(value) : false; + }; +}; + +export const createIsSuspiciousMessage = (general: GeneralSettings) => { + const test = createCachedTest(); + const testSafe = createCachedTest(); + const testWhole = createCachedTest(true); + const testSafeInstant = createCachedTest(); + const testWholeInstant = createCachedTest(true); + + return (text: string, { filterSwears }: GameServerSettings): Suspicious => { + if (test(general.suspiciousMessages, text)) + return Suspicious.Very; + + if (filterSwears) { + if (testSafeInstant(general.suspiciousSafeInstantMessages, text) || + testWholeInstant(general.suspiciousSafeInstantWholeMessages, text)) { + return Suspicious.Very; + } + + if (testSafe(general.suspiciousSafeMessages, text) || + testWhole(general.suspiciousSafeWholeMessages, text)) { + return Suspicious.Yes; + } + } + + return Suspicious.No; + }; +}; + +export const createIsSuspiciousName = + (settings: GeneralSettings) => { + const test = createCachedTest(); + return (name: string) => test(settings.suspiciousNames, name); + }; + +export const createIsSuspiciousAuth = + (settings: GeneralSettings) => { + const test = createCachedTest(); + return ({ name, emails = [] }: AuthBase) => + test(settings.suspiciousAuths, name) || + emails.some(email => test(settings.suspiciousAuths, email)); + }; + +// pony + +function tryParseJSON(value: string): any { + try { + return JSON.parse(value); + } catch { + return undefined; + } +} + +function createMatchesFromList(list: string | undefined): Partial[] { + return compact((list || '').split(/\n/g).map(x => x.trim()).map(tryParseJSON)); +} + +export const createIsSuspiciousPony = + (settings: GeneralSettings) => + (info: PonyInfoNumber) => { + const matches = createMatchesFromList(settings.suspiciousPonies); + return matches.some(match => matchPony(info, match)); + }; + +function matchPony(info: PonyInfoNumber, match: Partial) { + return isMatchWith(info, match, comparePonyInfoFields); +} + +function comparePonyInfoFields(a: any, b: any): boolean { + if (typeof a === 'number' && typeof b === 'string') { + return a === parseColorFast(b); + } else { + return undefined as any; + } +} + +// forbidden messages + +export function isForbiddenMessage(_message: string): boolean { + // NOTE: uncomment, to filter offensive messages + // if (/niggers$/.test(_message) || /faggots?/.test(_message)) return true; + + // NOTE: add more filters here + + return false; +} + +// forbidden name + +export function isForbiddenName(_value: string): boolean { + // NOTE: uncomment, to filter offensive names + // if (/niggers$/.test(_value) || /faggots?/.test(_value) || /hitler/.test(_value)) return true; + + // NOTE: uncomment, to filter links in names + // if (ipRegex.test(_value) && !ipExceptionRegex.test(_value)) return true; + // if (urlRegex.test(_value) && !urlExceptionRegex.test(_value)) return true; + + // NOTE: add more filters here + + return false; +} diff --git a/src/ts/common/sheets.ts b/src/ts/common/sheets.ts new file mode 100644 index 0000000..0d41a7e --- /dev/null +++ b/src/ts/common/sheets.ts @@ -0,0 +1,882 @@ +import { range, times } from 'lodash'; +import { PonyInfo, Point, PonyState, DrawPonyOptions, PonyInfoNumber, SpriteSet, PalettePonyInfo, NoDraw } from './interfaces'; +import * as offsets from './offsets'; +import { defaultPonyState } from '../client/ponyHelpers'; +import { WHITE, BLACK, ORANGE, BLUE, CYAN, RED } from './colors'; +import { createBodyFrame } from '../client/ponyAnimations'; +import { setFlag, repeat } from './utils'; + +type OnFrame = (pony: PonyInfoNumber, state: PonyState, options: DrawPonyOptions, x: number, y: number, pattern: number) => void; + +export interface SheetLayer { + name: string; + set?: string; + setOverride?: string; + options?: Partial; + patterns?: number; + drawBlack?: boolean; + shiftY?: number; + head?: boolean; + noFace?: boolean; + body?: boolean; + frontLeg?: boolean; + backLeg?: boolean; + frontFarLeg?: boolean; + backFarLeg?: boolean; + extra?: keyof PalettePonyInfo; // extra field name + fieldName?: keyof PonyInfo; + setup?: (pony: PonyInfoNumber, state: PonyState) => void; + frame?: OnFrame; + frameSet?: (set: SpriteSet, x: number, y: number, pattern: number) => void; + importMirrored?: { fieldName: string; offsetX: number }; +} + +export interface Spacer { + spacer: true; +} + +export interface Sheet { + name: string; + file?: string; + skipImport?: boolean; + alert?: string; + spacer?: boolean; + + rows?: number; + + width: number; + height: number; + offset: number; + offsetY?: number; + + padLeft?: number; + padTop?: number; + + offsets?: Point[]; + importOffsets?: Point[]; + fieldName?: keyof PonyInfo; + + groups?: string[][]; // groups for filling-in missing palette colors (used for manes) + setsWithEmpties?: string[]; // skipping slots (used for manes) + empties?: number[]; + + frame?: OnFrame; + layers: SheetLayer[]; + + masks?: { + name: string, + layerName: string, + mask: string, + reverse?: boolean, + maskFile?: string; + }[]; + + state?: PonyState; + extra?: boolean; + single?: boolean; // single frame (no animation) + duplicateFirstFrame?: number; + wrap?: number; + paletteOffsetY?: number; +} + +interface BodyFrame { + body: number; + front: number; + back: number; + wing: number; + tail: number; +} + +export const DEFAULT_COLOR = 0xdec078ff; +export const SPECIAL_COLOR = ORANGE; + +const headFrames: BodyFrame[] = [ + { body: 1, front: 1, back: 1, wing: 0, tail: 0 }, +]; + +const bodyFrames: BodyFrame[] = [ + { body: 0, front: 1, back: 1, wing: 0, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 0, tail: 0 }, + { body: 2, front: 29, back: 1, wing: 0, tail: 0 }, + { body: 3, front: 30, back: 21, wing: 0, tail: 0 }, + { body: 4, front: 31, back: 22, wing: 0, tail: 0 }, + { body: 5, front: 32, back: 23, wing: 0, tail: 1 }, + { body: 6, front: 33, back: 24, wing: 1, tail: 2 }, + { body: 7, front: 34, back: 25, wing: 2, tail: 2 }, + { body: 8, front: 34, back: 25, wing: 2, tail: 2 }, + { body: 9, front: 34, back: 26, wing: 2, tail: 2 }, + { body: 10, front: 35, back: 26, wing: 2, tail: 2 }, + { body: 11, front: 36, back: 26, wing: 1, tail: 2 }, + { body: 12, front: 37, back: 26, wing: 1, tail: 2 }, + { body: 13, front: 38, back: 26, wing: 0, tail: 2 }, + { body: 14, front: 38, back: 26, wing: 0, tail: 2 }, + { body: 15, front: 38, back: 26, wing: 0, tail: 2 }, +]; + +const waistFrames: BodyFrame[] = [ + ...bodyFrames, + { body: 1, front: 1, back: 1, wing: 3, tail: 0 }, +]; + +const wingFrames: BodyFrame[] = [ + { body: 1, front: 1, back: 1, wing: 0, tail: 0 }, + { body: 6, front: 33, back: 24, wing: 1, tail: 0 }, + { body: 9, front: 34, back: 26, wing: 2, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 3, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 4, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 5, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 6, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 7, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 8, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 9, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 10, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 11, tail: 0 }, + { body: 1, front: 1, back: 1, wing: 12, tail: 0 }, +]; + +const exampleCM = [ + BLUE, BLUE, BLUE, BLUE, BLUE, + BLUE, CYAN, CYAN, CYAN, BLUE, + BLUE, CYAN, CYAN, CYAN, BLUE, + BLUE, CYAN, CYAN, CYAN, BLUE, + BLUE, BLUE, BLUE, BLUE, BLUE, +]; + +const frontLegsCount = 39; +const backLegsCount = 27; + +const frontLegsSheet = { + width: 55, + height: 60, + offset: 50, + state: state(frontLegsCount, range(0, frontLegsCount)), +}; + +const backLegsSheet = { + width: 55, + height: 60, + offset: 55, + state: state(backLegsCount, undefined, undefined, range(0, backLegsCount)), +}; + +const bodySheet = { + width: 60, + height: 60, + offset: 50, + state: stateFromFrames(bodyFrames), +}; + +const chestSheet = { + width: 60, + height: 60, + offset: 60, + state: stateFromFrames(bodyFrames), +}; + +const waistSheet = { + ...chestSheet, + state: stateFromFrames(waistFrames), +}; + +const singleFrameSheet = { + ...bodySheet, + state: stateFromFrames(headFrames), +}; + +const headSheet = { + width: 60, + height: 75, + offset: 60, + offsetY: 20, + state: stateFromFrames(headFrames), +}; + +const bodyLayer: SheetLayer = { + name: '', body: true, head: true, frontLeg: true, backLeg: true, frontFarLeg: true, backFarLeg: true, +}; +const muzzleLayer: SheetLayer = { name: '', setup: pony => pony.nose = defaultSet() }; +const frontLegLayer: SheetLayer = { name: '', frontLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR }; +const backLegLayer: SheetLayer = { name: '', backLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR }; + +export const sheets: (Sheet | Spacer)[] = [ + // front legs + { + ...frontLegsSheet, + name: 'front legs', + file: 'front-legs', + frame: (_pony, state, _options, _x, y) => { + if (y > 0) { + state.animation.frames.forEach(f => f.frontLeg = 0); + } + }, + layers: [ + { ...bodyLayer, frontLeg: false }, + { + name: 'front', set: 'frontLegs', frontLeg: true, + frame: (pony, _state, _options, _x, _y, pattern) => { + pony.coatFill = pattern === 0 ? RED : WHITE; + pony.coatOutline = pattern === 0 ? RED : WHITE; + }, + }, + ], + }, + { + ...frontLegsSheet, + name: 'front legs - hooves', + file: 'front-legs-hooves', + fieldName: 'frontHooves', + layers: [ + { ...bodyLayer, frontLeg: false }, + frontLegLayer, + { + name: 'front', set: 'frontLegHooves', frontLeg: true, options: { useAllHooves: true }, + setup: pony => pony.coatFill = BLACK + }, + ], + }, + { + ...frontLegsSheet, + name: 'front legs - socks', + file: 'front-legs-accessories', + fieldName: 'frontLegAccessory', + layers: [ + { ...bodyLayer, frontLeg: false }, + frontLegLayer, + { name: 'front', set: 'frontLegAccessories', frontLeg: true, setup: pony => pony.coatFill = BLACK }, + ], + }, + { + ...frontLegsSheet, + name: 'front legs - sleeves', + file: 'front-legs-sleeves', + fieldName: 'sleeveAccessory', + layers: [ + { ...bodyLayer, frontLeg: false }, + frontLegLayer, + { + name: 'front', set: 'frontLegSleeves', frontLeg: true, options: { no: NoDraw.FarSleeves }, setup: pony => { + pony.chestAccessory = ignoreSet(2); + pony.coatFill = BLACK; + } + }, + ], + }, + + // back legs + { + ...backLegsSheet, + alert: 'Does not export mask layer', + name: 'back legs', + file: 'back-legs', + masks: [ + { + name: 'backLegs2', + layerName: 'front', + mask: 'mask', + }, + ], + frame: (_pony, state, _options, _x, y) => { + if (y > 0) { + state.animation.frames.forEach(f => f.backLeg = 0); + } + }, + layers: [ + // TODO: mask layer + { ...bodyLayer, backLeg: false }, + { + name: 'front', set: 'backLegs', backLeg: true, + frame: (pony, _state, _options, _x, _y, pattern) => { + pony.coatFill = pattern === 0 ? RED : WHITE; + pony.coatOutline = pattern === 0 ? RED : WHITE; + }, + }, + ], + }, + { + ...backLegsSheet, + name: 'back legs - hooves', + file: 'back-legs-hooves', + fieldName: 'backHooves', + masks: [ + { + name: 'backLegHooves2', + layerName: 'front', + mask: 'mask', + maskFile: 'back-legs', + }, + ], + layers: [ + { ...bodyLayer, backLeg: false }, + backLegLayer, + { name: 'front', set: 'backLegHooves', backLeg: true, setup: pony => pony.coatFill = BLACK }, + ], + }, + { + ...backLegsSheet, + name: 'back legs - socks', + file: 'back-legs-accessories', + fieldName: 'backLegAccessory', + masks: [ + { + name: 'backLegAccessories2', + layerName: 'front', + mask: 'mask', + maskFile: 'back-legs', + }, + ], + layers: [ + { ...bodyLayer, backLeg: false }, + backLegLayer, + { name: 'front', set: 'backLegAccessories', backLeg: true, setup: pony => pony.coatFill = BLACK }, + ], + }, + { + ...backLegsSheet, + name: 'back legs - sleeves', + file: 'back-legs-sleeves', + fieldName: 'backAccessory', + rows: 2, + masks: [ + { + name: 'backLegSleeves2', + layerName: 'front', + mask: 'mask', + maskFile: 'back-legs', + }, + ], + layers: [ + { ...bodyLayer, backLeg: false }, + backLegLayer, + { + name: 'front', set: 'backLegSleeves', setOverride: 'backAccessories', patterns: 2, + options: { no: NoDraw.BackAccessory | NoDraw.FarSleeves }, + setup: pony => pony.coatFill = BLACK, + frameSet: (set, _x, y, pattern) => { + set.type = y === 0 ? 5 : -1; + set.pattern = pattern; + }, + }, + ], + }, + + { + ...bodySheet, + name: 'body', + file: 'body', + // fieldName: 'body', // TODO: uncomment when body set is added + frame: (_pony, _state, options, _x, y) => { + // fix for missing body set + if (y > 0) { + options.no = setFlag(options.no, NoDraw.BodyOnly, true); + } + }, + layers: [ + { name: '', frontFarLeg: true, backFarLeg: true }, + { + name: 'body', body: true, set: 'body', + frame: (pony, _state, _options, _x, _y, pattern) => { + pony.coatFill = pattern === 0 ? RED : WHITE; + pony.coatOutline = pattern === 0 ? RED : WHITE; + }, + }, + { name: '', frontLeg: true }, + { name: '', backLeg: true }, + { name: '', head: true }, + ], + }, + { + name: 'body - wings', + file: 'body-wings', + fieldName: 'wings', + width: 80, + height: 70, + offset: 70, + offsetY: 10, + state: stateFromFrames(wingFrames), + layers: [ + bodyLayer, + { name: 'front', set: 'wings', options: { no: NoDraw.Behind } }, + ], + importOffsets: [1, 6, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].map(i => offsets.wingOffsets[i]), + }, + { + name: 'body - tails', + file: 'tails', + fieldName: 'tail', + width: 80, + height: 70, + offset: 70, + padLeft: 20, + state: stateFromFrames([1, 5, 9].map(i => bodyFrames[i])), + layers: [ + { name: 'behind-body', set: 'tails' }, + bodyLayer, + ], + importOffsets: [1, 5, 9].map(i => offsets.tailOffsets[i]), + }, + { + ...bodySheet, + name: 'body - neck accessory', + file: 'neck-accessory', + fieldName: 'neckAccessory', + layers: [ + { ...bodyLayer, head: false }, + { name: 'front', set: 'neckAccessories' }, + { name: '', head: true }, + ], + importOffsets: offsets.neckAccessoryOffsets, + }, + { + ...bodySheet, + name: 'body - chest accessory', + file: 'body-chest-accessory', + fieldName: 'chestAccessory', + layers: [ + { name: 'behind', set: 'chestAccessoriesBehind', options: { no: NoDraw.Front } }, + bodyLayer, + { name: 'front', set: 'chestAccessories', options: { no: NoDraw.Behind } }, + frontLegLayer, + ], + importOffsets: offsets.chestAccessoryOffsets, + }, + { + ...chestSheet, + name: 'body - back accessory', + file: 'body-back-accessory', + fieldName: 'backAccessory', + // masks: [ + // { name: 'backAccessories1', layerName: 'front', mask: 'mask' }, + // { name: 'backAccessories2', layerName: 'front', mask: 'mask', reverse: true }, + // ], + layers: [ + bodyLayer, + { name: 'front', set: 'backAccessories', options: { no: NoDraw.Sleeves } }, + ], + importOffsets: offsets.backAccessoryOffsets, + }, + { + ...waistSheet, + name: 'body - waist accessory', + file: 'body-waist-accessory', + fieldName: 'waistAccessory', + layers: [ + bodyLayer, + { name: 'front', set: 'waistAccessories' }, + { + name: '', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => { + pony.wings = x === 16 ? specialSet(1) : ignoreSet(); + }, + }, + ], + importOffsets: offsets.waistAccessoryOffsets, + }, + + // head + { + ...headSheet, + name: 'head', + file: 'head', + fieldName: 'head', + state: stateFromFrames(times(2, i => ({ body: 1, front: 1, back: 1, wing: 0, head: i, tail: 0 }))), + layers: [ + { ...bodyLayer, options: { no: NoDraw.Head | NoDraw.Eyes | NoDraw.CloseEar | NoDraw.Nose } }, + { name: 'front', set: 'head', head: true, drawBlack: false, options: { no: NoDraw.Ears | NoDraw.Nose | NoDraw.Eyes } }, + { name: '', head: true, options: { no: NoDraw.Head | NoDraw.FarEar } }, + ], + }, + { + ...singleFrameSheet, + name: 'head - ears', + file: 'ears', + fieldName: 'ears', + single: true, + wrap: 8, + paletteOffsetY: 30, + layers: [ + { + name: 'behind', set: 'earsFar', head: true, noFace: true, drawBlack: false, + options: { no: NoDraw.CloseEar | NoDraw.FarEarShade } + }, + { ...bodyLayer, options: { no: NoDraw.Ears } }, + { + name: 'front', set: 'ears', head: true, noFace: true, drawBlack: false, + options: { no: NoDraw.FarEar }, + // importMirrored: { fieldName: 'ears2', offsetX: 0 }, + }, + ], + // TODO: add layer(s) + }, + { + ...headSheet, + name: 'head - horns', + file: 'horns', + fieldName: 'horn', + single: true, + wrap: 8, + layers: [ + { name: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } }, + { name: 'behind', set: 'hornsBehind', options: { no: NoDraw.Front } }, + { ...bodyLayer, options: { no: NoDraw.Ears } }, + { + ...bodyLayer, name: '', options: { no: NoDraw.Ears | NoDraw.FrontMane }, + setup: pony => { + pony.mane = specialSet(1); + pony.backMane = specialSet(1); + } + }, + { name: 'front', set: 'horns', options: { no: NoDraw.Behind } }, + { name: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, + { + name: '', head: true, noFace: true, drawBlack: false, + options: { no: NoDraw.Ears | NoDraw.Behind | NoDraw.TopMane }, + setup: pony => pony.mane = specialSet(1), + }, + ], + }, + { + ...headSheet, + name: 'head - manes', + file: 'manes', + fieldName: 'mane', + groups: [ + ['frontManes', 'topManes', 'behindManes'], + ['backFrontManes', 'backBehindManes'], + ], + setsWithEmpties: ['backFrontManes', 'backBehindManes'], + empties: [3, 10, 13], + single: true, + wrap: 8, + layers: [ + { name: 'behind', set: 'behindManes', options: { no: NoDraw.FrontMane | NoDraw.TopMane } }, + { name: 'back-behind', set: 'backBehindManes', fieldName: 'backMane', options: { no: NoDraw.FrontMane } }, + { ...bodyLayer, options: { no: NoDraw.CloseEar } }, + { name: 'back', set: 'backFrontManes', fieldName: 'backMane', options: { no: NoDraw.Behind } }, + { name: 'top', set: 'topManes', options: { no: NoDraw.FrontMane | NoDraw.Behind } }, + { name: '', setup: pony => pony.horn = specialSet(1) }, + { name: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, + { name: 'front', set: 'frontManes', options: { no: NoDraw.TopMane | NoDraw.Behind } }, + ], + }, + { + ...singleFrameSheet, + name: 'head - facial hair', + file: 'facial-hair', + fieldName: 'facialHair', + single: true, + wrap: 8, + paletteOffsetY: 35, + layers: [ + { ...bodyLayer, options: { no: NoDraw.Nose } }, + { name: 'front', set: 'facialHairBehind' }, + muzzleLayer, + { name: 'front-2', set: 'facialHair' }, + ], + }, + { + ...singleFrameSheet, + name: 'head - ear accessory', + file: 'ear-accessory', + fieldName: 'earAccessory', + single: true, + wrap: 8, + layers: [ + { name: 'behind', set: 'earAccessoriesBehind', options: { no: NoDraw.Front } }, + { ...bodyLayer }, + { name: 'front', set: 'earAccessories', options: { no: NoDraw.Behind } }, + ], + }, + { + ...headSheet, + name: 'head - head accessory', + file: 'head-accessory', + fieldName: 'headAccessory', + single: true, + wrap: 8, + layers: [ + { name: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } }, + { name: 'behind', set: 'headAccessoriesBehind' }, + { ...bodyLayer, options: { no: NoDraw.FarEar } }, + { + ...bodyLayer, name: '', shiftY: 5, options: { no: NoDraw.FarEar }, setup: pony => { + pony.mane = specialSet(1); + pony.backMane = specialSet(1); + } + }, + { name: 'front', set: 'headAccessories' }, + ], + }, + { + ...headSheet, + name: 'head - face accessory', + file: 'face-accessory', + fieldName: 'faceAccessory', + single: true, + extra: true, + wrap: 8, + layers: [ + { ...bodyLayer, options: { no: NoDraw.CloseEar } }, + { name: 'front', set: 'faceAccessories', extra: 'faceAccessory', options: { no: NoDraw.FaceAccessory2 } }, + { name: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, + { name: 'front-2', set: 'faceAccessories2', options: { no: NoDraw.FaceAccessory1 } }, + muzzleLayer, + { name: '', setup: pony => pony.horn = specialSet(1) }, + ], + }, + { + ...headSheet, + name: 'head - extra accessory', + file: 'extra-accessory', + fieldName: 'extraAccessory', + single: true, + wrap: 8, + paletteOffsetY: 45, + layers: [ + { + name: 'behind', set: 'extraAccessoriesBehind', options: { extra: true, no: NoDraw.Front }, + setup: pony => pony.mane = ignoreSet(1) + }, + { + ...bodyLayer, setup: pony => { + pony.mane = specialSet(1); + pony.backMane = specialSet(1); + } + }, + { + name: 'front', set: 'extraAccessories', options: { extra: true, no: NoDraw.Behind }, + setup: pony => pony.mane = ignoreSet(1) + }, + ], + }, + + { + spacer: true, + }, + + // offsets + { + ...bodySheet, + name: 'offset - front legs', + offsets: offsets.frontLegOffsets, + state: stateFromFrames(bodyFrames.map(f => ({ ...f, front: 1 }))), + layers: [bodyLayer], + }, + { + ...bodySheet, + name: 'offset - back legs', + offsets: offsets.backLegOffsets, + state: stateFromFrames(bodyFrames.map(f => ({ ...f, back: 1 }))), + layers: [bodyLayer], + }, + { + ...bodySheet, + name: 'offset - wings', + fieldName: 'wings', + offsets: offsets.wingOffsets, + layers: [ + bodyLayer, + { name: 'front', set: 'wings', options: { no: NoDraw.Behind } }, + ], + duplicateFirstFrame: bodyFrames.length, + }, + { + width: 80, + height: 70, + offset: 70, + state: stateFromFrames(bodyFrames), + name: 'offset - tails', + fieldName: 'tail', + offsets: offsets.tailOffsets, + layers: [ + { name: 'behindBody', set: 'tails' }, + bodyLayer, + ], + duplicateFirstFrame: bodyFrames.length, + }, + { + ...bodySheet, + name: 'offset - head', + offsets: offsets.headOffsets, + layers: [bodyLayer], + }, + { + ...bodySheet, + name: 'offset - cm', + offsets: offsets.cmOffsets, + layers: [ + { ...bodyLayer, setup: pony => pony.cm = exampleCM }, + ], + }, + { + ...bodySheet, + name: 'offset - neck accessory', + fieldName: 'neckAccessory', + offsets: offsets.neckAccessoryOffsets, + layers: [ + { ...bodyLayer, head: false }, + { name: 'front', set: 'neckAccessories' }, + { name: '', head: true }, + ], + importOffsets: offsets.neckAccessoryOffsets, + }, + { + ...bodySheet, + name: 'offset - chest accessory', + fieldName: 'chestAccessory', + offsets: offsets.chestAccessoryOffsets, + layers: [ + { name: 'behind', set: 'chestAccessoriesBehind', options: { no: NoDraw.Front } }, + bodyLayer, + { name: 'front', set: 'chestAccessories', options: { no: NoDraw.Behind } }, + ], + }, + { + ...waistSheet, + name: 'offset - waist accessory', + fieldName: 'waistAccessory', + offsets: offsets.waistAccessoryOffsets, + layers: [ + bodyLayer, + { name: 'front', set: 'waistAccessories' }, + { + name: '', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => { + pony.wings = x === 16 ? specialSet(1) : ignoreSet(); + }, + }, + ], + }, + { + ...chestSheet, + name: 'offset - back accessory', + fieldName: 'backAccessory', + offsets: offsets.backAccessoryOffsets, + layers: [ + { name: '', setup: pony => pony.tail = specialSet(2) }, + bodyLayer, + { name: 'front', set: 'backAccessories' }, + ], + }, + { + width: 55, + height: 40, + offset: 55, + offsetY: 10, + state: stateFromFrames(repeat(offsets.HEAD_ACCESSORY_OFFSETS.length, bodyFrames[1])), + name: 'offset - hats', + rows: 19, + offsets: offsets.HEAD_ACCESSORY_OFFSETS, + layers: [ + { + ...bodyLayer, + frame: (pony, _state, _options, x, y) => { + pony.mane = specialSet(x); + // pony.backMane = specialSet(x === 0 ? 0 : 1); + pony.headAccessory = whiteSet(y + 1); + }, + }, + ], + }, + { + width: 55, + height: 40, + offset: 55, + offsetY: 10, + state: stateFromFrames(repeat(offsets.EAR_ACCESSORY_OFFSETS.length, bodyFrames[1])), + name: 'offset - earrings', + rows: 13, + offsets: offsets.EAR_ACCESSORY_OFFSETS, + layers: [ + { + ...bodyLayer, + frame: (pony, _state, _options, x, y) => { + pony.ears = defaultSet(x); + pony.earAccessory = whiteSet(y + 1); + }, + }, + ], + }, + { + width: 55, + height: 40, + offset: 55, + offsetY: 10, + state: stateFromFrames(repeat(offsets.EXTRA_ACCESSORY_OFFSETS.length, bodyFrames[1])), + name: 'offset - extra', + rows: 17, + offsets: offsets.EXTRA_ACCESSORY_OFFSETS, + layers: [ + { + ...bodyLayer, + frame: (pony, _state, options, x, y) => { + pony.mane = specialSet(x); + pony.extraAccessory = createSet(y + 1, WHITE, 7); + options.extra = true; + }, + }, + ], + }, +]; + +function stateFromFrames(frames: BodyFrame[]) { + const front = frames.map(f => f.front); + const back = frames.map(f => f.back); + const body = frames.map(f => f.body); + const wing = frames.map(f => f.wing); + const tail = frames.map(f => f.tail); + return state(frames.length, front, front, back, back, undefined, body, wing, tail); +} + +function state( + frames: number, frontLegs?: number[], frontFarLegs?: number[], backLegs?: number[], backFarLegs?: number[], + head?: number[], body?: number[], wing?: (number | undefined)[], tail?: number[] +): PonyState { + const state = defaultPonyState(); + state.blushColor = 0; + state.animation = {} as any; + const ones = times(frames, () => 1); + const zeros = times(frames, () => 0); + + state.animation = { + name: '', + loop: false, + fps: 24, + frames: times(frames, i => ({ + ...createBodyFrame([]), + head: (head || ones)[i], + body: (body || ones)[i], + wing: (wing && wing[i]) || 0, + tail: (tail || zeros)[i], + frontLeg: (frontLegs || ones)[i], + frontFarLeg: (frontFarLegs || ones)[i], + backLeg: (backLegs || ones)[i], + backFarLeg: (backFarLegs || ones)[i], + })), + }; + + return state; +} + +export function ignoreSet(type = 0): SpriteSet { + return createSet(type, BLACK); +} + +function defaultSet(type = 0): SpriteSet { + return createSet(type, DEFAULT_COLOR); +} + +function specialSet(type = 0): SpriteSet { + return createSet(type, SPECIAL_COLOR); +} + +function whiteSet(type = 0): SpriteSet { + return createSet(type, WHITE); +} + +function createSet(type: number, color: number, count = 2): SpriteSet { + return { + type, + fills: times(count, () => color), + lockFills: times(count, () => false), + outlines: times(count, () => color), + lockOutlines: times(count, () => true), + }; +} diff --git a/src/ts/common/stringUtils.ts b/src/ts/common/stringUtils.ts new file mode 100644 index 0000000..76239b7 --- /dev/null +++ b/src/ts/common/stringUtils.ts @@ -0,0 +1,90 @@ +const lowercaseCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789_'; +const uppercaseCharacters = lowercaseCharacters + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const CARRIAGERETURN = '\r'.charCodeAt(0); + +export function randomString(length: number, useUpperCase = false): string { + const characters = useUpperCase ? uppercaseCharacters : lowercaseCharacters; + let result = ''; + + for (let i = 0; i < length; i++) { + result += characters[(Math.random() * characters.length) | 0]; + } + + return result; +} + +export function isSurrogate(code: number): boolean { + return code >= 0xd800 && code <= 0xdbff; +} + +export function isLowSurrogate(code: number): boolean { + return (code & 0xfc00) === 0xdc00; +} + +export function fromSurrogate(high: number, low: number): number { + return (((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000) | 0; +} + +export function charsToCodes(text: string) { + const chars: number[] = []; + + for (let i = 0; i < text.length; i++) { + let code = text.charCodeAt(i); + + if (isSurrogate(code) && (i + 1) < text.length) { + const extra = text.charCodeAt(i + 1); + + if (isLowSurrogate(extra)) { + code = fromSurrogate(code, extra); + i++; + } + } + + chars.push(code); + } + + return chars; +} + +export function stringToCodes(buffer: Uint32Array, text: string): number { + const textLength = text.length | 0; + let length = 0 | 0; + + for (let i = 0; i < textLength; i = (i + 1) | 0) { + let code = text.charCodeAt(i) | 0; + + if (isSurrogate(code) && ((i + 1) | 0) < textLength) { + const extra = text.charCodeAt(i + 1) | 0; + + if (isLowSurrogate(extra)) { + code = fromSurrogate(code, extra) | 0; + i = (i + 1) | 0; + } + } + + if (isVisibleChar(code)) { + buffer[length] = code; + length = (length + 1) | 0; + } + } + + return length; +} + +export let codesBuffer = new Uint32Array(32); + +export function stringToCodesTemp(text: string) { + while (text.length > codesBuffer.length) { + codesBuffer = new Uint32Array(codesBuffer.length * 2); + } + + return stringToCodes(codesBuffer, text); +} + +export function matcher(regex: RegExp) { + return (text: string): boolean => !!text && regex.test(text); +} + +export function isVisibleChar(code: number) { + return code !== CARRIAGERETURN && !(code >= 0xfe00 && code <= 0xfe0f); +} diff --git a/src/ts/common/swears.ts b/src/ts/common/swears.ts new file mode 100644 index 0000000..5162e1e --- /dev/null +++ b/src/ts/common/swears.ts @@ -0,0 +1,1595 @@ +import { repeat } from 'lodash'; +import { charsToCodes } from '../common/stringUtils'; + +const otherChars = 'ąăãçćıłńęśóöøğüțţťżź'; +const wordBreakRU = `[^a-zA-Z\u0400-\u04FF${otherChars}}]`; +const wordStartRU = `(?:^|${wordBreakRU})`; +const wordEndRU = `(?=$|${wordBreakRU})`; + +function createBadWords(fast: boolean) { + const emoji = fast ? '*' : '😀-🙏☀-⛿✀-➿🚀-🛶⬀-⯯🌀-🗿'; + const separators1 = `:;!|\`"@#$%^&'*,._=+~\\-`; // \\(\\)\\{\\}\\]\\[ + const separators2 = `${separators1}\\(\\)/\\\\`; + const sep = `[ ${emoji}${separators2}]`; + const sep2 = `[${emoji}${separators2}]`; + const sep3 = `[${emoji}${separators1}]`; + const sep4 = `[${emoji}${separators1}\\(\\)\\{\\}\\]\\[]`; + const sep5 = `[ ${emoji}${separators1}\\(\\)\\{\\}\\]\\[]`; + const sep2OrNum = `[0-9${emoji}${separators2}]`; + + const onlyLetterT = fast ? 't' : 'tţțťтᴛ'; + + const letter = { + a: '[aаáα@4åα]', + b: '[bв🅱]', + c: '[cсćčçḉĉɕċƈȼ¢ς©ᴄ<]', + e: '[eе3єεęėëê€é]', + f: '[fғƒꜰ]', + g: '[gɢ]', + h: '[hн]', + i: '[ıιi!1íĭǐîïḯịȉìỉȋīįᶖɨĩḭᴉᵢ¡İ|🕯ɪ]', + k: '[kĸкḱǩķⱪꝃḳƙḵᶄꝁꝅʞκᴋ]', + n: '[nηɴñń]', + o: '[o○о0σõø🍪🥚°]', + p: '[pр]', + s: '[sş5$š]', + ss: fast ? 'ss' : '(?:ss|ß)', + t: `[${onlyLetterT}^7]`, + u: '[υuúŭǔûṷüǘǚǜǖṳụűȕùủưứựừửữȗūṻųᶙůũṹṵᴜ]', + x: '[xх]', + ь: '[ьЪ]', + }; + + if (fast) { + Object.keys(letter).forEach(key => { + (letter as any)[key] = (letter as any)[key].replace(/[^\u0020-\u007e]/g, ''); + }); + } + + const anyD = '(?:d|\\|\\))'; + const anyH = '(?:h|\\|-\\|)'; + const anyK = '(?:k|\\|<)'; + const anyL = '(?:l|\\|_)'; + const anyN = '(?:n|\\|\\\\\\|)'; + const anyO = '(?:o|0|\\(\\))'; + const anyU = '(?:u|\\|_\\||\\\\/|\\(_\\))'; + + const wordBreak = fast ? `[^a-zA-Z]` : wordBreakRU; + const wordStart = `(?:^|${wordBreak})`; + const wordEnd = `(?=$|${wordBreak})`; + + // sp + const oó = fast ? 'o' : 'oó'; + + // pr + const ã = fast ? 'a' : 'aã'; + const á = fast ? 'a' : 'aá'; + + // ro + const ă = fast ? 'a' : 'aă'; + const ț = fast ? 't' : 'tț'; + + // pl + const ą = fast ? 'a' : 'aą'; + const ę = fast ? 'e' : 'eę'; + const ć = fast ? 'c' : 'cć'; + const ń = fast ? 'n' : 'nń'; + const ł = fast ? 'l' : 'lł'; + const ó = fast ? 'u' : 'uó'; + const ś = fast ? 's' : 'sś'; + + function separeted(letters: string, separator = sep): string { + return letters.split('').map(x => `${(letter as any)[x] || x}+`).join(`${separator}*`); + } + + function baseSeparate(letters: string, sep: string, optional?: string) { + return `_*${separeted(letters, sep)}${(optional ? `(?:${separeted(optional, sep)})?` : '')}_*`; + } + + function separate(letters: string, optional?: string, separator = sep): string { + return baseSeparate(letters, separator, optional); + } + + function separate2(letters: string, optional?: string): string { + return baseSeparate(letters, sep2, optional); + } + + function alts(letters: string) { + return letters.split('').map(x => `${(letter as any)[x] || x}+`).join(''); + } + + let fuckWords: string[]; + + const badWords = [ + // english + 'aborted ?fetus', + 'all?(?:uh|ah ?u?a?|uah) ?a+c?k(?:ba+r*)?', + '(?][-~]*', // penis ascii art + '8==[|]?[oD]', // penis ascii art + '8[ =]{2,}D', // penis ascii art + '(?:^| )[.][i|][.](?: |$)', // penis ascii art + 'c[=-]{3,}3', + '(?:^| )n [i!] g g [e3] s [t^](?: |$)', + `(? new RegExp(`\\b(?:${values.join('|')})\\b`, 'ugi'); +const replaceRegexRU = (values: string[]) => new RegExp(`${wordStartRU}(?:${values.join('|')})${wordEndRU}`, 'ugi'); +const replaceRegexOther = (values: string[]) => new RegExp(`(?:${values.join('|')})`, 'ugi'); +const replaceRegexSpecific = (values: string[]) => new RegExp(`(?:${values.join('|')})`, 'ug'); + +const testRegex = (values: string[]) => new RegExp(`\\b(?:${values.join('|')})\\b`, 'ui'); +const testRegexRU = (values: string[]) => new RegExp(`${wordStartRU}(?:${values.join('|')})${wordEndRU}`, 'ui'); +const testRegexOther = (values: string[]) => new RegExp(`(?:${values.join('|')})`, 'ui'); +const testRegexSpecific = (values: string[]) => new RegExp(`(?:${values.join('|')})`, 'u'); + +const regexReplace = replaceRegex(unicode.all); +const regexReplaceRU = replaceRegexRU(unicode.foreign); +const regexReplaceOther = replaceRegexOther(unicode.other); +const regexReplaceSpecific = replaceRegexSpecific(unicode.specific); + +const regexReplaceFast = replaceRegex(ascii.all); +const regexReplaceRUFast = replaceRegexRU(ascii.foreign); +const regexReplaceOtherFast = replaceRegexOther(ascii.other); +const regexReplaceSpecificFast = replaceRegexSpecific(ascii.specific); + +const regexTest = testRegex(unicode.all); +const regexTestRU = testRegexRU(unicode.foreign); +const regexTestOther = testRegexOther(unicode.other); +const regexTestSpecific = testRegexSpecific(unicode.specific); + +const regexTestFast = testRegex(ascii.all); +const regexTestRUFast = testRegexRU(ascii.foreign); +const regexTestOtherFast = testRegexOther(ascii.other); +const regexTestSpecificFast = testRegexSpecific(ascii.specific); + +const regexTestFuck = testRegex(unicode.fuck); + +const regexReplaceRUSingle = new RegExp(`${unicode.foreign.join('|')}`, 'ugi'); +const regexReplacePartial = new RegExp(`${[ + ...unicode.all, ...unicode.foreign, ...unicode.other, ...unicode.specific +].join('|')}`, 'ugi'); + +type Replacer = (match: string) => string; + +const defaultReplacer: Replacer = match => repeat('*', match.length); +const createReplacerRU = (replacer = defaultReplacer): Replacer => match => match.replace(regexReplaceRUSingle, replacer); + +function canUseFast(text: string) { + return isAscii(text); +} + +function slowReplace(text: string, replacer: Replacer, replacerRU: Replacer) { + return text + .replace(regexReplace, replacer) + .replace(regexReplaceRU, replacerRU) + .replace(regexReplaceOther, replacer) + .replace(regexReplaceSpecific, replacer); +} + +function fastReplace(text: string, replacer: Replacer, replacerRU: Replacer) { + return text + .replace(regexReplaceFast, replacer) + .replace(regexReplaceRUFast, replacerRU) + .replace(regexReplaceOtherFast, replacer) + .replace(regexReplaceSpecificFast, replacer); +} + +export function createFilter(replacer = defaultReplacer) { + const replacerRU = createReplacerRU(replacer); + + return (text: string) => canUseFast(text) ? + fastReplace(text, replacer, replacerRU) : + slowReplace(text, replacer, replacerRU); +} + +function slowTest(text: string) { + return regexTest.test(text) || + regexTestRU.test(text) || + regexTestOther.test(text) || + regexTestSpecific.test(text); +} + +function fastTest(text: string) { + return regexTestFast.test(text) || + regexTestRUFast.test(text) || + regexTestOtherFast.test(text) || + regexTestSpecificFast.test(text); +} + +export function hasBadWords(text: string) { + return canUseFast(text) ? fastTest(text) : slowTest(text); +} + +export function hasFuck(text: string) { + return regexTestFuck.test(text); +} + +export const filterBadWords = createFilter(); + +export function filterName(name: string) { + const filtered = filterBadWords(name); + return name === filtered ? name : repeat('*', name.length); +} + +export function filterBadWordsPartial(text: string, replacer = defaultReplacer): string { + return text.replace(regexReplacePartial, replacer); +} + +export function findMatch(text: string): string | undefined { + return unicode.all.find(x => (new RegExp(`\\b(?:${x})\\b`, 'ui')).test(text)) + || unicode.foreign.find(x => (new RegExp(`${wordStartRU}(?:${x})${wordEndRU}`, 'ui')).test(text)) + || unicode.other.find(x => (new RegExp(`${x}`, 'u')).test(text)) + || unicode.specific.find(x => (new RegExp(`${x}`, 'u')).test(text)); +} + +export function createMatchEntries() { + return [ + ...unicode.all.map(line => ({ line, regex: new RegExp(`\\b(?:${line})\\b`, 'ui') })), + ...unicode.foreign.map(line => ({ line, regex: new RegExp(`${wordStartRU}(?:${line})${wordEndRU}`, 'ui') })), + ...unicode.other.map(line => ({ line, regex: new RegExp(line, 'ui') })), + ...unicode.specific.map(line => ({ line, regex: new RegExp(line, 'u') })), + ]; +} diff --git a/src/ts/common/tags.ts b/src/ts/common/tags.ts new file mode 100644 index 0000000..208627f --- /dev/null +++ b/src/ts/common/tags.ts @@ -0,0 +1,54 @@ +import { CharacterTag, FontPalettes } from './interfaces'; +import { hasRole, AccountRoles } from './accountUtils'; +import { MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, WHITE } from './colors'; + +const placeholder = { id: '', tagClass: '', label: '' }; +const tags: { [key: string]: CharacterTag; } = { + 'mod': { ...placeholder, name: 'moderator', className: 'mod', color: MOD_COLOR }, + 'dev': { ...placeholder, name: 'developer', className: 'dev', color: ADMIN_COLOR }, + 'dev:art': { ...placeholder, name: 'dev artist', className: 'dev', color: ADMIN_COLOR }, + 'dev:music': { ...placeholder, name: 'dev musician', className: 'dev', color: ADMIN_COLOR }, + 'sup1': { ...placeholder, name: 'supporter', className: 'sup1', color: PATREON_COLOR }, + 'sup2': { ...placeholder, name: 'supporter', className: 'sup2', color: WHITE }, + 'sup3': { ...placeholder, name: 'supporter', className: 'sup3', color: WHITE }, + 'hidden': { ...placeholder, name: 'hidden', className: 'hidden', color: ANNOUNCEMENT_COLOR }, +}; + +Object.keys(tags).forEach(id => { + const tag = tags[id]; + tag.id = id; + tag.label = `<${tag.name.toUpperCase()}>`; + tag.tagClass = `tag-${tag.className}`; +}); + +export const emptyTag: CharacterTag = { id: '', name: 'no tag', label: '', className: '', tagClass: '', color: 0 }; + +export function getAllTags() { + return Object.keys(tags).map(key => tags[key]); +} + +export function getTag(id: string | undefined): CharacterTag | undefined { + return id ? tags[id] : undefined; +} + +export function getTagPalette(tag: CharacterTag, palettes: FontPalettes) { + switch (tag.id) { + case 'sup2': return palettes.supporter2; + case 'sup3': return palettes.supporter3; + default: return palettes.white; + } +} + +export function canUseTag(account: AccountRoles, tag: string) { + if (tag === 'mod') { + return hasRole(account, 'mod'); + } else if (tag === 'dev' || /^dev:/.test(tag)) { + return hasRole(account, 'dev'); + } else { + return false; + } +} + +export function getAvailableTags(account: AccountRoles): CharacterTag[] { + return getAllTags().filter(tag => canUseTag(account, tag.id)); +} diff --git a/src/ts/common/timeUtils.ts b/src/ts/common/timeUtils.ts new file mode 100644 index 0000000..e93fa76 --- /dev/null +++ b/src/ts/common/timeUtils.ts @@ -0,0 +1,127 @@ +import { lerpColors, withAlphaFloat } from './color'; +import { WHITE, SHADOW_COLOR, BLACK } from './colors'; +import { MINUTE } from './constants'; +import { Season } from './interfaces'; + +const DAY_START = 4.75; // 04:45 +const DAY_END = 20.25; // 20:15 +const SUN_EASE = 1.5; // 01:30 +const SUN_HALF = SUN_EASE / 2; +const SUN_GAP = SUN_EASE / 4; + +export const HOUR_LENGTH = 2 * MINUTE; // 48 min -> 24 hours +export const DAY_LENGTH = HOUR_LENGTH * 24; + +const getTimeOfDay = (time: number) => time % DAY_LENGTH; +const getHourOfDay = (timeOfDay: number) => timeOfDay * 24 / DAY_LENGTH; + +export function getHour(time: number) { + const timeOfDay = getTimeOfDay(time); + const hourOfDay = getHourOfDay(timeOfDay); + return hourOfDay; +} + +export function formatHourMinutes(time: number): string { + const timeOfDay = getTimeOfDay(time); + const minutesInDay = 60 * 24; + const totalMinutes = Math.floor(timeOfDay * minutesInDay / DAY_LENGTH); + const minutes = totalMinutes % 60; + const hours = Math.floor(totalMinutes / 60); + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; +} + +const isHour = (test: (hour: number) => boolean) => (time: number) => { + return test(getHour(time)); +}; + +export const isDay = isHour(hour => hour > DAY_START && hour <= DAY_END); +export const isNight = (time: number) => !isDay(time); + +export const isFullDay = isHour(hour => hour > (DAY_START + SUN_HALF) && hour <= (DAY_END - SUN_HALF)); +export const isFullNight = isHour(hour => hour < (DAY_START - SUN_HALF) || hour >= (DAY_END + SUN_HALF)); + +export const isSunRaising = isHour(hour => hour > (DAY_START - SUN_HALF) && hour <= (DAY_START + SUN_HALF)); +export const isSunSetting = isHour(hour => hour > (DAY_END - SUN_HALF) && hour <= (DAY_END + SUN_HALF)); + +export const isDayTime = isHour(hour => hour > DAY_START && hour < (DAY_END - SUN_HALF)); +export const isNightTime = isHour(hour => hour < (DAY_START - SUN_HALF) || hour > DAY_END); + +// light color + +export interface LightData { + lightColors: number[]; + shadowColors: number[]; + lightStops: number[]; +} + +export function createLightData(season: Season): LightData { + const lightDay = WHITE; + const lightNight = season === Season.Winter ? 0x253f76ff : 0x2b3374ff; + + const sunrise1 = 0x853d7dff; + const sunrise2 = 0xc96161ff; + const sunrise3 = 0xeeb7a0ff; + + const sunset1 = sunrise3; + const sunset2 = sunrise2; + const sunset3 = sunrise1; + + const shadowAlphaMultiplier = season === Season.Winter ? 0.7 : 1; + const shadowDay = withAlphaFloat(BLACK, 0.3 * shadowAlphaMultiplier); + const shadowNight = withAlphaFloat(BLACK, 0.2 * shadowAlphaMultiplier); + const shadowSunset = withAlphaFloat(BLACK, 0.25 * shadowAlphaMultiplier); + const shadowSunrise = withAlphaFloat(BLACK, 0.25 * shadowAlphaMultiplier); + + const lightPoints = [ + // night + { time: 0, light: lightNight, shadow: shadowNight }, + + // transition to day + { time: DAY_START - SUN_HALF, light: lightNight, shadow: shadowNight }, + { time: DAY_START - SUN_HALF + SUN_GAP, light: sunrise1, shadow: shadowSunrise }, + { time: DAY_START - SUN_HALF + SUN_GAP * 2, light: sunrise2, shadow: shadowSunrise }, + { time: DAY_START - SUN_HALF + SUN_GAP * 3, light: sunrise3, shadow: shadowSunrise }, + { time: DAY_START + SUN_HALF, light: lightDay, shadow: shadowDay }, + + // transition to night + { time: DAY_END - SUN_HALF, light: lightDay, shadow: shadowDay }, + { time: DAY_END - SUN_HALF + SUN_GAP, light: sunset1, shadow: shadowSunset }, + { time: DAY_END - SUN_HALF + SUN_GAP * 2, light: sunset2, shadow: shadowSunset }, + { time: DAY_END - SUN_HALF + SUN_GAP * 3, light: sunset3, shadow: shadowSunset }, + { time: DAY_END + SUN_HALF, light: lightNight, shadow: shadowNight }, + + // night + { time: 24, light: lightNight, shadow: shadowNight }, + ]; + + const lightColors = lightPoints.map(l => l.light); + const shadowColors = lightPoints.map(l => l.shadow); + const lightStops = lightPoints.map(l => l.time); + + return { lightColors, shadowColors, lightStops }; +} + +export function getLightColor(data: LightData, time: number): number { + return getColorForTime(time, data.lightStops, data.lightColors, WHITE); +} + +export function getShadowColor(data: LightData, time: number): number { + return getColorForTime(time, data.lightStops, data.shadowColors, SHADOW_COLOR); +} + +function getColorForTime(time: number, stops: number[], colors: number[], defaultColor: number) { + const timeOfDay = getTimeOfDay(time); + const hourOfDay = getHourOfDay(timeOfDay); + + for (let i = 1; i < stops.length; i++) { + if (stops[i] >= hourOfDay) { + const from = stops[i - 1]; + const to = stops[i]; + const fromLight = colors[i - 1]; + const toLight = colors[i]; + return lerpColors(fromLight, toLight, (hourOfDay - from) / (to - from)); + } + } + + return defaultColor; +} diff --git a/src/ts/common/utils.ts b/src/ts/common/utils.ts new file mode 100644 index 0000000..66f0891 --- /dev/null +++ b/src/ts/common/utils.ts @@ -0,0 +1,541 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Point, Rect, Entity, Dict } from './interfaces'; +import { tileWidth, tileHeight, SECOND, MINUTE, HOUR, DAY } from './constants'; +import { ACCESS_ERROR, NOT_FOUND_ERROR, OFFLINE_ERROR, PROTECTION_ERROR } from './errors'; + +// enum + +export function invalidEnum(value: never) { + if (DEVELOPMENT) { + throw new Error(`Invalid enum value: ${value}`); + } +} + +export function invalidEnumReturn(value: never, ret: T): T { + if (DEVELOPMENT && !TESTS) { + throw new Error(`Invalid enum value: ${value}`); + } + + return ret; +} + +// date + +export function fromDate(date: Date, duration: number): Date { + date.setTime(date.getTime() + duration); + return date; +} + +export function fromNow(duration: number): Date { + return fromDate(new Date(), duration); +} + +export function compareDates(a?: Date, b?: Date) { + return a ? (b ? a.getTime() - b.getTime() : 1) : (b ? -1 : 0); +} + +export function maxDate(a?: Date, b?: Date) { + return (compareDates(a, b) > 0 ? a : b) || a || b; +} + +export function minDate(a?: Date, b?: Date) { + return (compareDates(a, b) < 0 ? a : b) || a || b; +} + +export function formatDuration(duration: number) { + const s = Math.floor(duration / SECOND) % 60; + const m = Math.floor(duration / MINUTE) % 60; + const h = Math.floor(duration / HOUR) % 24; + const d = Math.floor(duration / DAY); + + if (d > 0) { + return h ? `${d}d ${h}h` : `${d}d`; + } else if (h > 0) { + return m ? `${h}h ${m}m` : `${h}h`; + } else if (m > 0) { + return s ? `${m}m ${s}s` : `${m}m`; + } else { + return `${s}s`; + } +} + +export function formatISODate(date: Date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; +} + +export function parseISODate(value: string) { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + let day = 0; + let month = 0; + let year = 0; + + if (match) { + year = parseInt(match[1], 10); + month = parseInt(match[2], 10); + day = parseInt(match[3], 10); + } + + return { day, month, year }; +} + +export function createValidBirthDate(day: number, month: number, year: number) { + const date = new Date(0); + const currentYear = (new Date()).getFullYear(); + date.setFullYear(year, month - 1, day); + + if ( + date.getFullYear() === year && date.getMonth() === (month - 1) && date.getDate() === day && + year >= (currentYear - 120) && year < currentYear + ) { + return date; + } else { + return undefined; + } +} + +// color + +export function parseSpriteColor(str: string): number { + return str === '0' ? 0 : (str.length === 6 ? (((parseInt(str, 16) << 8) | 0xff) >>> 0) : (parseInt(str, 16) >>> 0)); +} + +// numbers + +export function clamp(value: number, min: number, max: number): number { + return value > min ? (value < max ? value : max) : min; +} + +export function lerp(a: number, b: number, t: number) { + return a + t * (b - a); +} + +export function normalize(x: number, y: number): Point { + const d = Math.sqrt(x * x + y * y); + return { x: x / d, y: y / d }; +} + +export function computeCRC(colors: Uint32Array): number { + let crc = 0; + + for (let i = 0; i < colors.length; i++) { + crc ^= colors[i]; + + for (let j = 0; j < 8; j++) { + crc = (crc & 1) ? ((crc >>> 1) ^ 0x82f63b78) : (crc >>> 1); + } + } + + return crc >>> 0; +} + +export function computeFriendsCRC(friends: string[]) { + if (!friends.length) { + return 0; + } + + friends.sort(); + const data = new Uint32Array(friends.length * 3); + + for (let i = 0; i < friends.length; i++) { + const id = friends[i]; + data[i * 3] = parseInt(id.substr(0, 8), 16); + data[i * 3 + 1] = parseInt(id.substr(8, 8), 16); + data[i * 3 + 2] = parseInt(id.substr(16, 8), 16); + } + + return computeCRC(data); +} + +export function lerpColor(a: number[] | Float32Array, b: number[] | Float32Array, t: number) { + a[0] = t * b[0] + (1 - t) * a[0]; + a[1] = t * b[1] + (1 - t) * a[1]; + a[2] = t * b[2] + (1 - t) * a[2]; + a[3] = t * b[3] + (1 - t) * a[3]; +} + +// common + +export function toInt(value: any): number { + return value | 0; +} + +export function dispose(obj: T | undefined): undefined { + obj && obj.dispose(); + return undefined; +} + +export function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +// enums + +export function hasFlag(value: number | undefined, flag: number): boolean { + return (value! & flag) === flag; +} + +export function setFlag(value: number | undefined, flag: number, on: boolean): number { + return (value! & ~flag) | (on ? flag : 0); +} + +export function flagsToString(value: number, flags: { value: number; name: string; }[], none = 'None') { + return flags + .filter(flag => hasFlag(value, flag.value)) + .map(flag => flag.name).join(' | ') || none; +} + +// collections + +export function includes(array: T[] | undefined, item: T): boolean { + return array !== undefined && array.indexOf(item) !== -1; +} + +export function array(size: number, defaultValue: T) { + const result: T[] = []; + + for (let i = 0; i < size; i++) { + result.push(defaultValue); + } + + return result; +} + +export function repeat(count: number, ...values: T[]): T[] { + const result: T[] = []; + + for (let i = 0; i < count; i++) { + result.push(...values); + } + + return result; +} + +export function times(count: number, action: (index: number) => T) { + const result: T[] = []; + + for (let i = 0; i < count; i++) { + result.push(action(i)); + } + + return result; +} + +export function last(array: T[]): T | undefined { + return array.length > 0 ? array[array.length - 1] : undefined; +} + +export function flatten(arrays: T[][]): T[] { + return ([] as T[]).concat(...arrays); +} + +export function at(items: T[], index: any): T | undefined { + return items[clamp(index | 0, 0, items.length - 1)]; +} + +export function att(items: T[] | null | undefined, index: any): T | undefined { + return items ? items[clamp(index | 0, 0, items.length - 1)] : undefined; +} + +export function findById(items: T[], id: U): T | undefined { + for (let i = 0; i < items.length; i++) { + if (items[i].id === id) { + return items[i]; + } + } + + return undefined; +} + +export function findIndexById(items: T[], id: U): number { + for (let i = 0; i < items.length; i++) { + if (items[i].id === id) { + return i; + } + } + + return -1; +} + +export function removeItem(items: T[], item: T): boolean { + const index = items.indexOf(item); + + if (index !== -1) { + items.splice(index, 1); + return true; + } else { + return false; + } +} + +export function removeItemFast(items: T[], item: T): boolean { + const index = items.indexOf(item); + + if (index !== -1) { + items[index] = items[items.length - 1]; + items.pop(); + return true; + } else { + return false; + } +} + +export function removeById(items: T[], id: U): T | undefined { + const index = findIndexById(items, id); + + if (index !== -1) { + const item = items[index]; + items.splice(index, 1); + return item; + } else { + return undefined; + } +} + +export function arraysEqual(a: T[], b: T[]): boolean { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +} + +export function pushUniq(array: T[], item: T) { + const index = array.indexOf(item); + + if (index === -1) { + array.push(item); + return array.length; + } else { + return index + 1; + } +} + +export function createPlainMap(values: Dict): Dict { + return Object.keys(values).reduce((obj: Dict, key: string) => (obj[key] = values[key], obj), Object.create(null)); +} + +// rects / points + +export function point(x: number, y: number): Point { + return { x, y }; +} + +export function contains(x: number, y: number, bounds: Rect, point: Point): boolean { + const bx = bounds.x / tileWidth + x; + const by = bounds.y / tileHeight + y; + const bw = bounds.w / tileWidth; + const bh = bounds.h / tileHeight; + return point.x > bx && point.x < bx + bw && point.y > by && point.y < by + bh; +} + +export function containsPoint(dx: number, dy: number, rect: Rect, px: number, py: number): boolean { + return pointInXYWH(px, py, rect.x + dx, rect.y + dy, rect.w, rect.h); +} + +export function containsPointWitBorder(dx: number, dy: number, rect: Rect, px: number, py: number, border: number): boolean { + return pointInXYWH(px, py, rect.x + dx - border, rect.y + dy - border, rect.w + border * 2, rect.h + border * 2); +} + +export function pointInRect(x: number, y: number, rect: Rect) { + return x > rect.x && x < rect.x + rect.w && y > rect.y && y < rect.y + rect.h; +} + +export function pointInXYWH(px: number, py: number, rx: number, ry: number, rw: number, rh: number) { + return px > rx && px < rx + rw && py > ry && py < ry + rh; +} + +export function randomPoint({ x, y, w, h }: Rect): Point { + return { + x: x + w * Math.random(), + y: y + h * Math.random(), + }; +} + +export function lengthOfXY(dx: number, dy: number): number { + return Math.sqrt(dx * dx + dy * dy); +} + +export function distanceXY(ax: number, ay: number, bx: number, by: number): number { + return lengthOfXY(ax - bx, ay - by); +} + +export function distanceSquaredXY(ax: number, ay: number, bx: number, by: number): number { + const dx = ax - bx; + const dy = ay - by; + return dx * dx + dy * dy; +} + +export function distance(a: Point, b: Point): number { + return distanceXY(a.x, a.y, b.x, b.y); +} + +export function entitiesIntersect(a: Entity, b: Entity): boolean { + const aBounds = a.bounds; + const bBounds = b.bounds; + + if (!aBounds || !bBounds) { + return false; + } + + const ax = a.x * tileWidth + aBounds.x; + const ay = a.y * tileHeight + aBounds.y; + const bx = b.x * tileWidth + bBounds.x; + const by = b.y * tileHeight + bBounds.y; + + return intersect(ax, ay, aBounds.w, aBounds.h, bx, by, bBounds.w, bBounds.h); +} + +export function collidersIntersect(ax: number, ay: number, a: Rect, bx: number, by: number, b: Rect): boolean { + const axmin = Math.floor((ax + a.x) * tileWidth) | 0; + const axmax = Math.ceil((ax + a.x + a.w) * tileWidth) | 0; + const aymin = Math.floor((ay + a.y) * tileHeight) | 0; + const aymax = Math.ceil((ay + a.y + a.h) * tileHeight) | 0; + + const bxmin = Math.floor((bx + b.x) * tileWidth) | 0; + const bxmax = Math.ceil((bx + b.x + b.w) * tileWidth) | 0; + const bymin = Math.floor((by + b.y) * tileHeight) | 0; + const bymax = Math.ceil((by + b.y + b.h) * tileHeight) | 0; + + return axmin < bxmax && axmax > bxmin && aymin < bymax && aymax > bymin; +} + +export function boundsIntersect( + ax: number, ay: number, a: Rect | undefined, bx: number, by: number, b: Rect | undefined +): boolean { + return !!(a && b && intersect( + ax * tileWidth + a.x, ay * tileHeight + a.y, a.w, a.h, + bx * tileWidth + b.x, by * tileHeight + b.y, b.w, b.h)); +} + +export function intersect( + ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number +): boolean { + return ax <= (bx + bw) && (ax + aw) >= bx && ay <= (by + bh) && (ay + ah) >= by; +} + +// requests + +export type RequestError = Error & { status?: number; text?: string; }; + +export function createError(status: number, data: string | { error: string; }): Error { + if (status > 500 && status < 600) { + return new Error(PROTECTION_ERROR); + // } else if (status === 400) { + // return new Error('Bad Request'); + } else if (status === 403) { + return new Error(ACCESS_ERROR); + } else if (status === 404) { + return new Error(NOT_FOUND_ERROR); + } else if (typeof data === 'string') { + return new Error(data || OFFLINE_ERROR); + } else { + return new Error((data && data.error) || OFFLINE_ERROR); + } +} + +export function delay(timeout: number) { + return new Promise(resolve => setTimeout(resolve, timeout)); +} + +export function observableToPromise(observable: Observable) { + return observable.toPromise() + .catch(({ status, error }: HttpErrorResponse) => { + const text = error && error.text; + + try { + error = JSON.parse(error); + } catch { } + + const e: RequestError = createError(status || 0, error); + e.status = status; + e.text = text; + throw e; + }); +} + +// other + +function setTransformDefault(element: HTMLElement | undefined, transform: string) { + if (element) { + element.style.transform = transform; + } +} + +function setTransformSafari(element: HTMLElement | undefined, transform: string) { + if (element) { + (element.style as any).webkitTransform = transform; + } +} + +export const setTransform = (typeof document !== 'undefined' && 'transform' in document.body.style) ? + setTransformDefault : setTransformSafari; + +export class ObjectCache { + private cache: T[] = []; + constructor(private limit: number, private ctor: () => T) { + } + get(): T { + return this.cache.pop() || this.ctor(); + } + put(item: T) { + if (this.cache.length < this.limit) { + this.cache.push(item); + } + } +} + +export function bitmask(data: Uint8Array, key: number) { + if (key) { + for (let i = 0; i < data.length; i++) { + data[i] = data[i] ^ key; + } + } + + return data; +} + +export function isCommand(text: string) { + return /^\//.test(text); +} + +export function processCommand(text: string) { + text = text.substr(1); + const space = text.indexOf(' '); + const command = (space === -1 ? text : text.substr(0, space)).trim() as string | undefined; + const args = space === -1 ? '' : text.substr(space + 1).trim(); + return { command, args }; +} + +// events + +export type AnyEvent = MouseEvent | PointerEvent | TouchEvent; + +export function isTouch(e: AnyEvent): e is TouchEvent { + return /^touch/i.test(e.type); +} + +export function getButton(e: AnyEvent): number { + return ('button' in e) ? (e.button || 0) : 0; +} + +export function getX(e: AnyEvent): number { + return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageX : (e as any).pageX; +} + +export function getY(e: AnyEvent): number { + return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageY : (e as any).pageY; +} + +export function isKeyEventInvalid(e: KeyboardEvent) { + return e.target && /^(input|textarea|select)$/i.test((e.target).tagName); +} diff --git a/src/ts/common/worldMap.ts b/src/ts/common/worldMap.ts new file mode 100644 index 0000000..8c70662 --- /dev/null +++ b/src/ts/common/worldMap.ts @@ -0,0 +1,741 @@ +import { + Entity, Point, TileType, Rect, MapInfo, Camera, Region, IMap, MapState, defaultMapState, Pony, + MapType, EntityFlags, WorldMap, Weather, EntityState, canWalk, MapFlags, +} from './interfaces'; +import { contains, removeItem, boundsIntersect, array, pushUniq, containsPoint, removeItemFast } from './utils'; +import { isBoundsVisible, } from './camera'; +import { + getRegionTile, setRegionTile, setRegionTileDirty, getRegionElevation, setRegionElevation, + getRegionTileIndex, worldToRegionX, worldToRegionY, generateRegionCollider, invalidateRegionsCollider +} from './region'; +import { weatherRain, splash } from './entities'; +import { releaseEntity, isMoving, isHidden, isDrawable, isPonyFlying } from './entityUtils'; +import { updatePonyEntity, invalidatePalettesForPony, ensurePonyInfoDecoded, isPony, isPonyOnTheGround } from './pony'; +import { getTileHeight, updateTileIndices, isInWater } from '../client/tileUtils'; +import { toScreenX, toScreenY, toScreenYWithZ, rectToScreen, toWorldZ } from './positionUtils'; +import { hasDrawLight, hasLightSprite } from '../client/draw'; +import { PonyTownGame } from '../client/game'; +import { WATER_FPS, PONY_TYPE, REGION_SIZE } from './constants'; +import { updatePosition, canCollideWith } from './collision'; +import { PaletteManager } from '../graphics/paletteManager'; +import { timeEnd, timeStart } from '../client/timing'; +import { playEffect } from '../client/handlers'; +import { isFlyingDown } from '../client/ponyStates'; + +const defaultMapInfo: MapInfo = { + type: MapType.None, + flags: MapFlags.None, + regionsX: 0, + regionsY: 0, + defaultTile: TileType.None, +}; + +export function createWorldMap(info = defaultMapInfo, state: MapState = { ...defaultMapState }): WorldMap { + const { type, flags, regionsX, regionsY, defaultTile, editableArea } = info; + const map: WorldMap = { + type, + flags, + tileTime: 0, + entities: [], + entitiesDrawable: [], + entitiesWithNames: [], + entitiesWithChat: [], + entitiesMoving: [], + entitiesTriggers: [], + entitiesLight: [], + entitiesLightSprite: [], + entitiesById: new Map(), + poniesToDecode: [], + regionsX, + regionsY, + regions: array(regionsX * regionsY, undefined), + defaultTile, + width: regionsX * REGION_SIZE, + height: regionsY * REGION_SIZE, + minRegionX: 0, + minRegionY: 0, + maxRegionX: 0, + maxRegionY: 0, + state, + editableArea, + }; + + updateMinMaxRegion(map); + + return map; +} + +function pickAny(entity: Entity, point: Point): boolean { + const bounds = entity.interactBounds || entity.bounds; + return !!bounds && contains(entity.x, entity.y, bounds, point); +} + +export function getAnyBounds(entity: Entity) { + return [ + entity.interactBounds, + entity.bounds, + entity.lightBounds, + entity.lightSpriteBounds, + entity.collidersBounds, + entity.triggerBounds && rectToScreen(entity.triggerBounds), + ].filter(x => x && x.w > 0 && x.h > 0)[0]; +} + +function pickAnyEvenLights(entity: Entity, point: Point): boolean { + const bounds = getAnyBounds(entity); + return !!bounds && contains(entity.x, entity.y, bounds, point); +} + +function pick(entity: Entity, point: Point, pickHidden: boolean, pickEditable: boolean): boolean { + const editableOrInteractive = pickEditable ? + (entity.type !== PONY_TYPE && ((entity.state & EntityState.Editable) !== 0)) : + ((entity.flags & EntityFlags.Interactive) !== 0); + + return editableOrInteractive && (!isHidden(entity) || pickHidden) && pickAny(entity, point); +} + +function pickEntity( + entity: Entity, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable: boolean +): boolean { + return (!ignorePonies || entity.type !== PONY_TYPE) && pick(entity, point, pickHidden, pickEditable); +} + +function pickByBounds(entity: Entity, rect: Rect, pickHidden: boolean): boolean { + if ((entity.flags & EntityFlags.Interactive) === 0 || (isHidden(entity) && !pickHidden)) { + return false; + } else { + const bounds = entity.interactBounds || entity.bounds; + return !!bounds && boundsIntersect(entity.x, entity.y, bounds, 0, 0, rect); + } +} + +function pickEntityByBounds(entity: Entity, rect: Rect, ignorePonies: boolean, pickHidden: boolean): boolean { + return (!ignorePonies || entity.type !== PONY_TYPE) && pickByBounds(entity, rect, pickHidden); +} + +export function pickAnyEntities(map: WorldMap, point: Point) { + return map.entities.filter(e => pickAnyEvenLights(e, point)).reverse(); +} + +export function pickEntities(map: WorldMap, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable = false) { + return map.entities.filter(e => pickEntity(e, point, ignorePonies, pickHidden, pickEditable)).reverse(); +} + +export function pickEntitiesByRect(map: WorldMap, rect: Rect, ignorePonies: boolean, pickHidden: boolean) { + return map.entities.filter(e => pickEntityByBounds(e, rect, ignorePonies, pickHidden)).reverse(); +} + +export function removeRegions(map: WorldMap, coords: number[]) { + if (coords.length === 0) + return; + + const entitiesToRemove = new Set(); + + for (let i = 0; i < coords.length; i += 2) { + const x = coords[i]; + const y = coords[i + 1]; + const index = x + y * map.regionsX; + const region = map.regions[index]; + + if (region) { + for (const entity of region.entities) { + entitiesToRemove.add(entity); + releaseEntity(entity); + map.entitiesById.delete(entity.id); + } + } + + map.regions[index] = undefined; + setTilesDirty(map, x * REGION_SIZE - 1, y * REGION_SIZE - 1, REGION_SIZE + 2, REGION_SIZE + 2); + } + + removeEntitiesFromEntities(map, entitiesToRemove); + updateMinMaxRegion(map); +} + +export function setRegion(map: WorldMap, x: number, y: number, region: Region) { + if (x >= 0 && y >= 0 && x < map.regionsX && y < map.regionsY) { + const index = x + y * map.regionsX; + const oldRegion = map.regions[index]; + + if (oldRegion) { + DEVELOPMENT && !TESTS && console.error(`Region already set (${x}, ${y})`); + + for (const e of oldRegion.entities.slice()) { + releaseAndRemoveEntityFromMap(map, e); + } + } + + map.regions[index] = undefined; + setTilesDirty(map, x * REGION_SIZE - 1, y * REGION_SIZE - 1, REGION_SIZE + 2, REGION_SIZE + 2); + map.regions[index] = region; + updateMinMaxRegion(map); + } else { + DEVELOPMENT && !TESTS && console.error(`Invalid region coords (${x}, ${y})`); + } +} + +export function findEntityById(map: WorldMap, id: number) { + return map.entitiesById.get(id); +} + +export function addEntity(map: WorldMap, entity: Entity) { + const region = getRegionGlobal(map, entity.x, entity.y); + + if (!region) { + throw new Error(`Missing region at ${entity.x} ${entity.y}`); + } else { + addEntityToMapRegion(map, region, entity); + } +} + +export function removeEntity(map: WorldMap, entity: Entity) { + removeEntityFromMapRegion(map, entity); + releaseAndRemoveEntityFromMap(map, entity); +} + +function releaseAndRemoveEntityFromMap(map: WorldMap, entity: Entity) { + releaseEntity(entity); + removeEntityFromEntities(map, entity); +} + +export function removeEntityDirectly(map: WorldMap, entity: Entity) { + forEachRegion(map, region => { + const removed = removeEntityFromRegion(region, entity, map); + + if (removed) { + releaseEntity(entity); + removeEntityFromEntities(map, entity); + return false; + } else { + return true; + } + }); +} + +export function setTile(map: WorldMap, worldX: number, worldY: number, type: TileType) { + const region = getRegionGlobal(map, worldX, worldY); + + if (!region) + return; + + const x = Math.floor(worldX - region.x * REGION_SIZE); + const y = Math.floor(worldY - region.y * REGION_SIZE); + + const old = getRegionTile(region, x, y); + setRegionTile(region, x, y, type); + + setTilesDirty(map, worldX - 1, worldY - 1, 3, 3); + + if (canWalk(old) !== canWalk(type)) { + setColliderDirty(map, region, x, y); + } +} + +export function setColliderDirty(map: IMap, region: Region, x: number, y: number) { + region.colliderDirty = true; + + if (x === 0) { + const r = getRegionUnsafe(map, region.x - 1, region.y); + r && (r.colliderDirty = true); + } else if (x === (REGION_SIZE - 1)) { + const r = getRegionUnsafe(map, region.x + 1, region.y); + r && (r.colliderDirty = true); + } + + if (y === 0) { + const r = getRegionUnsafe(map, region.x, region.y - 1); + r && (r.colliderDirty = true); + } else if (y === (REGION_SIZE - 1)) { + const r = getRegionUnsafe(map, region.x, region.y + 1); + r && (r.colliderDirty = true); + } +} + +export function setTileAtRegion(map: WorldMap, regionX: number, regionY: number, x: number, y: number, type: TileType) { + setTile(map, regionX * REGION_SIZE + x, regionY * REGION_SIZE + y, type); +} + +export function setTilesDirty(map: IMap, ox: number, oy: number, w: number, h: number) { + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + doRelativeToRegion(map, x + ox, y + oy, (region, x, y) => setRegionTileDirty(region, x, y)); + } + } +} + +function getTileIndex(map: IMap, x: number, y: number) { + const region = getRegionGlobal(map, x, y); + return region ? getRegionTileIndex(region, x - region.x * REGION_SIZE, y - region.y * REGION_SIZE) : 0; +} + +export function getElevation(map: WorldMap, x: number, y: number) { + const region = getRegionGlobal(map, x, y); + return region ? getRegionElevation(region, x - region.x * REGION_SIZE, y - region.y * REGION_SIZE) : 0; +} + +export function setElevation(map: WorldMap, x: number, y: number, value: number) { + doRelativeToRegion(map, x, y, (region, x, y) => setRegionElevation(region, x, y, value)); +} + +export function forEachRegion(map: WorldMap, callback: (region: Region) => boolean | void) { + for (let y = map.minRegionY; y <= map.maxRegionY; y++) { + for (let x = map.minRegionX; x <= map.maxRegionX; x++) { + const region = getRegion(map, x, y); + + if (region && callback(region) === false) { + return; + } + } + } +} + +function updateMinMaxRegion(map: WorldMap) { + map.minRegionX = map.regionsX; + map.minRegionY = map.regionsY; + map.maxRegionX = 0; + map.maxRegionY = 0; + + for (let y = 0; y < map.regionsY; y++) { + for (let x = 0; x < map.regionsX; x++) { + if (getRegion(map, x, y)) { + map.minRegionX = Math.min(x, map.minRegionX); + map.minRegionY = Math.min(y, map.minRegionY); + map.maxRegionX = Math.max(x, map.maxRegionX); + map.maxRegionY = Math.max(y, map.maxRegionY); + } + } + } + + map.maxRegionX = Math.min(map.maxRegionX, map.regionsX - 1); + map.maxRegionY = Math.min(map.maxRegionY, map.regionsY - 1); +} + +function doRelativeToRegion( + map: IMap, x: number, y: number, action: (region: Region, x: number, y: number) => void +) { + const region = getRegionGlobal(map, x, y); + + if (region) { + const regionX = Math.floor(x - region.x * REGION_SIZE); + const regionY = Math.floor(y - region.y * REGION_SIZE); + action(region, regionX, regionY); + } +} + +function addEntityToRegion(region: Region, entity: Entity, map: WorldMap) { + region.entities.push(entity); + + if (canCollideWith(entity)) { + region.colliders.push(entity); + invalidateRegionsCollider(region, map); + } +} + +function removeEntityFromRegion(region: Region, entity: Entity, map: WorldMap) { + const removed = removeItemFast(region.entities, entity); + + if (removed && canCollideWith(entity)) { + removeItemFast(region.colliders, entity); + invalidateRegionsCollider(region, map); + } + + return removed; +} + +export function addEntityToMapRegion(map: WorldMap, region: Region, entity: Entity) { + if (entity.id !== 0) { + const existing = map.entitiesById.get(entity.id); + + if (existing) { + DEVELOPMENT && !TESTS && console.error(`Adding duplicate entity ${entity.id} (` + + `${worldToRegionX(existing.x, map)}, ${worldToRegionY(existing.y, map)} => ` + + `${worldToRegionX(entity.x, map)}, ${worldToRegionY(entity.y, map)})`); + + removeEntity(map, existing); + } + + map.entitiesById.set(entity.id, entity); + } + + if (isPony(entity) && entity.palettePonyInfo === undefined) { + map.poniesToDecode.push(entity); + } + + addEntityToRegion(region, entity, map); + + map.entities.push(entity); + + if (isDrawable(entity)) { + map.entitiesDrawable.push(entity); + } + + if (isMoving(entity)) { + map.entitiesMoving.push(entity); + } + + if (hasDrawLight(entity)) { + pushUniq(map.entitiesLight, entity); + } + + if (hasLightSprite(entity)) { + pushUniq(map.entitiesLightSprite, entity); + } + + if (entity.triggerBounds !== undefined) { + pushUniq(map.entitiesTriggers, entity); + } +} + +function removeEntityFromEntities(map: WorldMap, entity: Entity) { + map.entitiesById.delete(entity.id); + removeItemFast(map.entities, entity); + removeItem(map.entitiesWithChat, entity); + removeItem(map.entitiesWithNames, entity); + + if (isDrawable(entity)) { + removeItem(map.entitiesDrawable, entity); + } + + if (isMoving(entity)) { + removeItemFast(map.entitiesMoving, entity); + } + + if (isPony(entity)) { + removeItemFast(map.poniesToDecode, entity); + } + + if (hasDrawLight(entity)) { + removeItemFast(map.entitiesLight, entity); + } + + if (hasLightSprite(entity)) { + removeItemFast(map.entitiesLightSprite, entity); + } + + if (entity.triggerBounds !== undefined) { + removeItemFast(map.entitiesTriggers, entity); + } +} + +function removeEntitiesFromEntities(map: WorldMap, set: Set) { + if (set.size > 0) { + const filter = (entity: Entity) => !set.has(entity); + map.entities = map.entities.filter(filter); + map.entitiesDrawable = map.entitiesDrawable.filter(filter); + map.entitiesWithChat = map.entitiesWithChat.filter(filter); + map.entitiesWithNames = map.entitiesWithNames.filter(filter); + map.entitiesMoving = map.entitiesMoving.filter(filter); + map.poniesToDecode = map.poniesToDecode.filter(filter); + map.entitiesLight = map.entitiesLight.filter(filter); + map.entitiesLightSprite = map.entitiesLightSprite.filter(filter); + map.entitiesTriggers = map.entitiesTriggers.filter(filter); + } +} + +function removeEntityFromMapRegion(map: WorldMap, entity: Entity) { + forEachRegion(map, region => !removeEntityFromRegion(region, entity, map)); +} + +export function getTile(map: IMap, x: number, y: number): TileType { + const region = getRegionGlobal(map, x, y) as any as Region; + + if (region) { + const regionX = Math.floor(x - region.x * REGION_SIZE); + const regionY = Math.floor(y - region.y * REGION_SIZE); + return getRegionTile(region, regionX, regionY); + } else { + return TileType.None; + } +} + +export function getRegionGlobal(map: IMap, x: number, y: number): T { + const rx = worldToRegionX(x, map); + const ry = worldToRegionY(y, map); + return getRegion(map, rx, ry); +} + +export function getRegion(map: IMap, x: number, y: number): T { + if (x < 0 || y < 0 || x >= map.regionsX || y >= map.regionsY) { + throw new Error(`Invalid region coords (${x}, ${y})`); + } else { + return map.regions[((x | 0) + (y | 0) * map.regionsX) | 0]; + } +} + +export function getRegionUnsafe(map: IMap, x: number, y: number): T | undefined { + if (x < 0 || y < 0 || x >= map.regionsX || y >= map.regionsY) { + return undefined; + } else { + return map.regions[((x | 0) + (y | 0) * map.regionsX) | 0]; + } +} + +export function addOrRemoveFromEntityList(list: Entity[], entity: Entity, had: boolean, has: boolean) { + if (had !== has) { + if (has) { + pushUniq(list, entity); + } else { + removeItemFast(list, entity); + } + } +} + +export function updateEntitiesWithNames(map: WorldMap, hover: Point, player: Entity) { + for (let i = map.entitiesWithNames.length - 1; i >= 0; i--) { + const entity = map.entitiesWithNames[i]; + + if (!pickAny(entity, hover)) { + map.entitiesWithNames.splice(i, 1); + } + } + + const regionX = worldToRegionX(hover.x, map); + const regionY = worldToRegionY(hover.y, map); + + const minX = Math.max(0, regionX - 1) | 0; + const minY = Math.max(0, regionY - 1) | 0; + const maxX = Math.min(regionX + 1, map.regionsX - 1) | 0; + const maxY = Math.min(regionY + 1, map.regionsY - 1) | 0; + + for (let ry = minY; ry <= maxY; ry++) { + for (let rx = minX; rx <= maxX; rx++) { + const region = getRegion(map, rx, ry); + + if (region !== undefined) { + for (const e of region.entities) { + if (e.name !== undefined && e !== player && pickAny(e, hover)) { + pushUniq(map.entitiesWithNames, e); + } + } + } + } + } +} + +export function updateEntitiesCoverLifted(map: WorldMap, player: Entity, hideObjects: boolean, delta: number) { + const playerX = toScreenX(player.x); + const playerY = toScreenYWithZ(player.y, player.z); + + for (const e of map.entitiesDrawable) { + if (e.coverBounds !== undefined) { + e.coverLifted = hideObjects || containsPoint(toScreenX(e.x), toScreenY(e.y), e.coverBounds, playerX, playerY); + + const lifting = e.coverLifting || 0; + + if (e.coverLifted && lifting < 1) { + e.coverLifting = Math.min(lifting + delta * 2, 1); + } else if (!e.coverLifted && lifting > 0) { + e.coverLifting = Math.max(lifting - delta * 2, 0); + } + } + } +} + +export function updateEntitiesTriggers(map: WorldMap, player: Pony, game: PonyTownGame) { + for (const e of map.entitiesTriggers) { + const on = (e.triggerTall || isPonyOnTheGround(player)) && + containsPoint(e.x, e.y, e.triggerBounds!, player.x, player.y); + + if (e.triggerOn !== on) { + if (on) { + game.send(server => server.interact(e.id)); + } + + e.triggerOn = on; + } + } +} + +export function updateMap(map: WorldMap, delta: number) { + map.tileTime += delta * WATER_FPS; + + forEachRegion(map, region => { + if (region.tilesDirty) { + updateTileIndices(region, map); + } + + if (region.colliderDirty) { + generateRegionCollider(region, map); + } + }); +} + +export function getMapHeightAt(map: WorldMap, x: number, y: number, gameTime: number) { + return getTileHeight(getTile(map, x, y), getTileIndex(map, x, y), x, y, gameTime, map.type); +} + +export function isInWaterAt(map: IMap, x: number, y: number) { + return getTile(map, x, y) === TileType.Water && isInWater(getTileIndex(map, x, y), x, y); +} + +export function updateEntities(game: PonyTownGame, gameTime: number, delta: number, safe: boolean) { + TIMING && timeStart('updateEntities'); + + const map = game.map; + + for (const entity of map.entitiesMoving) { + updatePosition(entity, delta, map); + } + + for (const entity of map.entities) { + const flags = entity.flags; + + if ((flags & EntityFlags.Bobbing) !== 0) { + const bobs = entity.bobs!; + const frame = (((gameTime / 1000) * entity.bobsFps!) | 0) % bobs.length; + entity.z = toWorldZ(bobs[frame]); + } else if ((flags & EntityFlags.StaticY) === 0) { + entity.z = getMapHeightAt(map, entity.x, entity.y, gameTime); + } + + if (entity.type === PONY_TYPE) { + const pony = entity as Pony; + updatePonyEntity(pony, delta, gameTime, safe); + + const wasSwimming = pony.swimming; + pony.swimming = !isPonyFlying(pony) && isInWaterAt(map, pony.x, pony.y); + + if (wasSwimming !== pony.swimming) { + if (isFlyingDown(pony.animator.state)) { + setTimeout(() => playEffect(game, pony, splash.type), 400); + } else { + playEffect(game, pony, splash.type); + } + } + } else if (entity.update !== undefined) { + entity.update(delta, gameTime); + } + + if ((flags & EntityFlags.OnOff) !== 0) { + const on = (entity.state & EntityState.On) !== 0; + + if (entity.lightOn !== undefined) { + entity.lightOn = on; + } + + if (entity.lightSpriteOn !== undefined) { + entity.lightSpriteOn = on; + } + } + + if ((flags & EntityFlags.Light) !== 0) { + if (entity.lightOn) { + const move = delta * 0.2; + + if (Math.abs(entity.lightScale! - entity.lightTarget!) < move) { + entity.lightScale = entity.lightTarget; + entity.lightTarget = 1 - Math.random() * 0.15; + } else { + entity.lightScale! += entity.lightScale! < entity.lightTarget! ? move : -move; + } + } + } + } + + for (let i = map.entitiesWithChat.length - 1; i >= 0; i--) { + const entity = map.entitiesWithChat[i]; + const says = entity.says!; + + if (says.timer) { + says.timer -= delta; + + if (says.timer < 0) { + says.timer = 0; + entity.says = undefined; + map.entitiesWithChat.splice(i, 1); + } + } + } + + TIMING && timeEnd(); +} + +export function invalidatePalettes(entities: Entity[]) { + for (const entity of entities) { + if (isPony(entity)) { + invalidatePalettesForPony(entity); + } + } +} + +export function ensureAllVisiblePoniesAreDecoded(map: WorldMap, camera: Camera, paletteManager: PaletteManager) { + const poniesToDecode = map.poniesToDecode; + + if (!poniesToDecode.length) + return; + + const decode = new Set(); + + for (let i = 0; i < poniesToDecode.length; i++) { + const pony = poniesToDecode[i]; + + if (isBoundsVisible(camera, pony.bounds, pony.x, pony.y)) { + decode.add(i); + } + } + + if (!decode.size) + return; + + if (decode.size > 100) { + paletteManager.deduplicate = false; + } + + map.poniesToDecode = poniesToDecode.filter((pony, i) => { + if (pony.palettePonyInfo !== undefined) { + return false; + } else if (decode.has(i)) { + ensurePonyInfoDecoded(pony); + return false; + } else { + return true; + } + }); + + if (decode.size > 100) { + paletteManager.deduplicate = true; + } +} + +export function switchEntityRegion(map: WorldMap, entity: Entity, x: number, y: number) { + removeEntityFromMapRegion(map, entity); + + const region = getRegionGlobal(map, x, y); + + if (region) { + addEntityToRegion(region, entity, map); + } else { + releaseAndRemoveEntityFromMap(map, entity); + } +} + +export function updateMapState(map: WorldMap, prevState: MapState, newState: MapState) { + if (prevState.weather !== newState.weather) { + switch (newState.weather) { + case Weather.None: + removeWeatherEffects(map); + break; + case Weather.Rain: + addRainEffects(map); + break; + } + } +} + +function removeWeatherEffects(map: WorldMap) { + const effects = map.entities.filter(e => e.id === 0 && e.type === weatherRain.type); + + for (const entity of effects) { + removeEntityDirectly(map, entity); + } +} + +function addRainEffects(map: WorldMap) { + forEachRegion(map, region => { + if (region.x === 3 && region.y === 4) { // TEMP: testing + const entity = weatherRain((region.x + 0.5) * REGION_SIZE, (region.y + 0.5) * REGION_SIZE); + addEntity(map, entity); + } + }); +} diff --git a/src/ts/components/admin/admin-account-details/admin-account-details.pug b/src/ts/components/admin/admin-account-details/admin-account-details.pug new file mode 100644 index 0000000..c53682e --- /dev/null +++ b/src/ts/components/admin/admin-account-details/admin-account-details.pug @@ -0,0 +1,447 @@ +div(*ngIf="account") + .row + .col-md-12 + h2 + | {{account.name}} + small.text-muted.ml-1 + em {{account.name | translit}} + + .row + .col-md-4 + .mb-2 + account-info([account]="account" popoverPlacement="right") + .mb-2 + code.text-muted {{account._id}} + .mb-2 + div + span.text-muted created: + from-now.ml-1([time]="account.createdAt") + span.text-muted.ml-1 ({{account.createdAt | date}}) + div + span.text-muted last visit: + from-now.ml-1([time]="account.lastVisit") + span.text-muted.ml-1 ({{account.lastVisit | date}}) + div + span.text-muted last browser: + ua-info.ml-1([userAgent]="accountObject?.lastUserAgent") + div + span.text-muted age: + span([class.text-strike]="account.birthyear") + span.ml-1 {{age}} + span.text-muted.ml-1 ({{account.birthdate | date:'yyyy-MM-dd'}}) + span(*ngIf="account.birthyear") + span.ml-1 {{forceAge}} + span.text-muted.ml-1 ({{account.birthyear}}) + .d-inline-block.dropdown(dropdown) + a.icon-button.ml-1(dropdownToggle) + fa-icon([icon]="cogIcon") + .dropdown-menu(*dropdownMenu) + button.dropdown-item((click)="setAge(-1)") + | unset + button.dropdown-item(*ngFor="let age of ages" (click)="setAge(age)") + | set to #[b {{age}}]yo + + .d-flex.justify-content-between.align-items-start + h5 Counters + div + button.btn.btn-xs.btn-default.ml-1((click)="refreshDetails()" title="Refresh account details") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + .mb-2 + div(*ngFor="let counter of counters") + span.text-muted {{counter.key}}: + span.ml-1 {{counter.value}} + + .d-flex.justify-content-between.align-items-start + h5 Status + div + button.btn.btn-xs.btn-default.ml-1((click)="clearSessions()" title="Sign out user" [disabled]="clearingSessions") + fa-icon([icon]="clearingSessions ? spinnerIcon : signOutIcon" [fixedWidth]="true" [spin]="clearingSessions") + button.btn.btn-xs.btn-default.ml-1((click)="showAccountData()" title="Fetch account data (in console)") + fa-icon([icon]="consoleIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default.ml-1((click)="accountStatus.refresh()" title="Refresh status") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .mb-2 + account-status(#accountStatus [account]="account" [verbose]="true") + + h5 Roles + .mb-2 + .btn-group.btn-group-sm + button.btn(*ngFor="let r of roles" [btnHighlight]="hasRole(r)" (click)="toggleRole(r)" [disabled]="!canToggleRole(r)") + | {{r}} + button.btn.btn-sm.btn-danger.ml-1((click)="remove()" [disabled]="!canRemove" title="Delete account") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + + .d-flex.justify-content-between.align-items-start + h5 + | Notes + a.text-muted.ml-1([href]="translateUrl(account.note)" target="_blank" title="Translate note") + fa-icon([icon]="langIcon") + .text-muted + from-now([time]="account.noteUpdated") + .well.pre-line.text-truncate + | {{account.note}} + + .d-flex.justify-content-between.align-items-start + h5 Merges #[span.text-muted ({{merges?.length || 0}})] + div + button.btn.btn-xs.btn-default.ml-1((click)="refreshDetails()" title="Refresh account details") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + table.table.table-sm(*ngIf="merges?.length") + thead + tr + th id + th name + th date + tbody + tr(*ngFor="let m of merges" [class.text-slashed]="m.split") + td + ng-template(#mergeTooltip) + div {{getMergeTooltip(m)}} + div([tooltip]="mergeTooltip" placement="right" containerClass="tooltip-merge") + | {{m.id}} + td {{m.name}} + td + .d-flex + .flex-grow-1 + time-field([time]="m.date") + .dropdown.float-right(dropdown) + button.btn.btn-xs.btn-default.dropdown-toggle( + dropdownToggle [tooltip]="(m.reason || '[null reason]') + (m.split ? ' (split)' : '')") + fa-icon([icon]="cogIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + button.dropdown-item((click)="printMerge(m)") + | Print merge info + button.dropdown-item((click)="printMerge2(m)") + | Print usable merge info + button.dropdown-item((click)="showMergeInNewTab(m)") + | Show merge info + button.dropdown-item((click)="showMergeInNewTab2(m)") + | Show usable merge info + .dropdown-divider + button.dropdown-item((click)="unmerge(m._id, m.data.merge, m.data.account)") + | Split off #[b merged] + button.dropdown-item((click)="unmerge(m._id, m.data.account, m.data.merge)") + | Split off #[b account] + + h5 Support #[span.text-muted ({{support?.length || 0}})] + table.table.table-sm(*ngIf="support?.length") + thead + tr + th message + th date + tbody + tr(*ngFor="let l of support") + td + fa-icon.mr-1([icon]="l.icon" [ngClass]="l.class") + | {{l.message}} + td + time-field.mr-2([time]="l.date") + span.text-muted ({{l.date | date:'MMM d'}}) + + h5 Ban log #[span.text-muted ({{banLog?.length || 0}})] + table.table.table-sm(*ngIf="banLog?.length") + thead + tr + th message + th date + tbody + tr(*ngFor="let l of banLog") + td {{l.message}} + td: time-field([time]="l.date") + + .col-md-5 + //- auths + .d-flex.justify-content-between.align-items-start + h5 Auths #[span.text-muted ({{auths.length}})] + .dropdown(dropdown) + button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle) + fa-icon([icon]="cogIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="fetchAuths()") + | Fetch auths data + a.dropdown-item((click)="printAuthList()") + | Print auths list + a.dropdown-item((click)="printAuthData()") + | Print auths data + + .mb-2 + auth-info-edit(*ngFor="let a of auths | slice:0:authLimit" [authId]="a" [duplicates]="duplicates") + + a.text-muted((click)="authLimit = 99999" *ngIf="authLimit < auths.length") + | more ... + + .well.mt-2.mb-1.py-2(*ngIf="authData" style="font-size: 12px;") + button.close((click)="clearAuthData()" style="color: #ddd; margin-right: -5px; margin-top: -5px;") × + button.close((click)="copyAuthData()" style="color: #ddd; margin-right: 6px; margin-top: -5px;") + fa-icon([icon]="copyIcon" size="xs") + .text-muted Auths for {{authDataAccount}} + .pre-line {{authData}} + + //- origins + .d-flex.justify-content-between + h5 Origins #[span.text-muted ({{origins.length}})] + div + button.btn.btn-xs.btn-default((click)="clearOrigins(true, true)" title="Remove all origins older than 14 days used by only one account") + fa-icon.mr-1([icon]="trashIcon") + | old single + button.btn.btn-xs.btn-default.ml-1((click)="clearOrigins(true, false)" title="Remove all origins older than 14 days") + fa-icon.mr-1([icon]="trashIcon") + | old + button.btn.btn-xs.btn-default.ml-1((click)="clearOrigins(false, true)" title="Remove all origins used by only one account") + fa-icon.mr-1([icon]="trashIcon") + | single + button.btn.btn-xs.btn-default.ml-1((click)="clearOrigins(false, false)" title="Remove all origins") + fa-icon.mr-1([icon]="trashIcon") + | all + .dropdown.d-inline-block.ml-1(dropdown) + button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle title="Remove all origins in region") + fa-icon.mr-1([icon]="trashIcon") + | region + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let region of originRegions" (click)="clearOriginsInRegion(region)") + | {{region}} + .mb-2 + .d-flex.align-items-center.mb-1(*ngFor="let o of origins") + origin-info-remote.flex-grow-1([originIP]="o.ip") + small.text-muted + from-now.mr-1([time]="o.last") + button.btn.btn-xs.btn-default((click)="removeOrigin(o.ip)" title="Remove origin") + fa-icon([icon]="trashIcon") + + hr + + //- duplicates + .d-flex.justify-content-between.align-items-start + h5 + | Duplicates #[span.text-muted ({{duplicates?.length}})] + fa-icon.text-muted.ml-1(*ngIf="loadingDuplicates" [icon]="spinnerIcon" [fixedWidth]="true" [spin]="true") + div + button.btn.btn-xs.btn-default((click)="clearOriginsFromDuplicates()" title="Remove all origins older than 14 days from all duplicates") + fa-icon.mr-1([icon]="trashIcon") + | old + button.btn.btn-xs.btn-default.ml-1((click)="refresh()" title="Refresh duplicates") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .mb-2 + .text-muted(*ngIf="!duplicates || !duplicates.length") + | - no duplicates - + + .mb-1.d-flex.align-items-center(*ngFor="let d of duplicates | slice:0:duplicatesLimit") + ng-template(#duplicatePonies) + .text-left(*ngIf="d.ponies") + div(*ngFor="let p of d.ponies") + | {{p}} + + span.badge.badge-none(title="Duplicate origins" [class.deleted]="d.origins === 0") + | {{d.origins}} + span.badge.badge-none.ml-1( + title="Duplicate ponies" [class.deleted]="!d.ponies || !d.ponies.length" + [tooltip]="duplicatePonies" [isDisabled]="!d.ponies || !d.ponies.length") + | {{d.ponies ? d.ponies.length : '-'}} + + fa-icon.ml-1.text-alert( + *ngIf="d.note" [icon]="duplicateNoteIcon" title="Duplicate in note") + fa-icon.ml-1.text-muted( + *ngIf="d.browserId" [icon]="duplicateBrowserIdIcon" title="Duplicate browser ID") + fa-icon.ml-1.text-muted( + *ngIf="d.userAgent" [icon]="duplicateBrowserIcon" [tooltip]="d.userAgent" title="Duplicate browser") + fa-icon.ml-1( + *ngIf="d.emails" [icon]="duplicateEmailIcon" title="Duplicate emails: {{d.emails}}" + [ngClass]="d.indenticalEmail ? 'text-alert' : 'text-muted'") + fa-icon.ml-1.text-alert(*ngIf="d.birthdate" [icon]="duplicateBirthdateIcon" title="Duplicate birthdate") + fa-icon.ml-1.text-muted(*ngIf="d.name" [icon]="duplicateNameIcon" title="Duplicate name") + + account-info-remote.ml-1(#info [accountId]="d.account" [extendedAuths]="true") + + button.btn.btn-xs.btn-default.ml-1((click)="merge(d.account)" title="Merge account") + fa-icon([icon]="mergeIcon" size="lg") + button.btn.btn-xs.btn-default.ml-1((click)="chatLog.add(info.account)" title="Add to chat log") + fa-icon([icon]="commentIcon" [fixedWidth]="true") + + a.text-muted(*ngIf="duplicates && duplicates.length > duplicatesLimit" (click)="duplicatesLimit = 9999999") + | more ... + a.text-muted(*ngIf="duplicates && duplicatesLimit === 9999999" (click)="duplicatesLimit = 10") + | less ... + + hr + + //- around + .d-flex.justify-content-between.align-items-start + h5 Around #[span.text-muted ({{around ? around.length : '-'}})] + button.btn.btn-xs.btn-default((click)="refreshAround()" title="Refresh around") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of around") + span.badge.badge-none(title="Distance in tiles from user") {{a.distance.toFixed(1)}} + + fa-icon.ml-1.text-alert(*ngIf="a.party" [icon]="partyIcon" title="In party with user") + + account-info-remote.ml-1(#info [accountId]="a.account" [extendedAuths]="true" [showDuplicates]="true") + + button.btn.btn-xs.btn-default.ml-1((click)="merge(a.account)" title="Merge account") + fa-icon([icon]="mergeIcon" size="lg") + button.btn.btn-xs.btn-default.ml-1((click)="chatLog.add(info.account)" title="Add to chat log") + fa-icon([icon]="commentIcon" [fixedWidth]="true") + + hr + + //- friends + .d-flex.justify-content-between.align-items-start + h5 Friends #[span.text-muted ({{friends ? friends.length : '?'}})] + button.btn.btn-xs.btn-default((click)="fetchFriends()" title="Fetch friends list") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of friends | slice:0:friendsLimit") + account-info-remote([accountId]="a") + button.btn.btn-xs.btn-default.ml-1((click)="removeFriend(a)" title="Remove friend") + fa-icon([icon]="trashIcon") + a.text-muted(*ngIf="friends && friends.length > friendsLimit" (click)="friendsLimit = 9999999") + | more ... + + //- supporter invites + //- h5 Supporter invites #[span.text-muted ({{invites?.length || 0}})] + //- div(*ngIf="invites?.length") + .mb-1.d-flex.align-items-center(*ngFor="let i of invites") + .text-muted.mr-2 {{i.type}} + fa-icon.mr-2([icon]="checkIcon" [ngClass]="i.active ? 'text-success' : 'text-danger'") + account-info-remote.ml-1([accountId]="i.target" [extendedAuths]="true") + small.text-muted.flex-grow-1.text-right + time-field([time]="i.createdAt") + + hr + + //- force hides + div(*ngIf="account.hides && account.hides.length") + .d-flex.justify-content-between.align-items-start + h5 Force hidden #[span.text-muted ({{account.hides.length}})] + + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of account.hides") + account-info-remote([accountId]="a") + + hr + + //- hides + .d-flex.justify-content-between.align-items-start + h5 Hidden by #[span.text-muted ({{hiddenBy ? hiddenBy.length : '?'}})] + button.btn.btn-xs.btn-default((click)="fetchHidden()" title="Fetch hidden list") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of hiddenBy | slice:0:hiddenByLimit") + account-info-remote([accountId]="a") + a.text-muted(*ngIf="hiddenBy && hiddenBy.length > hiddenByLimit" (click)="hiddenByLimit = 9999999") + | more ... + + h5 Hides users #[span.text-muted ({{hidden ? hidden.length : '?'}})] + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of hidden | slice:0:hiddenLimit") + account-info-remote([accountId]="a") + a.text-muted(*ngIf="hidden && hidden.length > hiddenLimit" (click)="hiddenLimit = 9999999") + | more ... + + //- hides (perma) + .d-flex.justify-content-between.align-items-start + h5 Perma hidden by #[span.text-muted ({{permaHiddenBy ? permaHiddenBy.length : '?'}})] + + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of permaHiddenBy | slice:0:permaHiddenByLimit") + account-info-remote([accountId]="a") + a.text-muted(*ngIf="permaHiddenBy && permaHiddenBy.length > permaHiddenByLimit" (click)="permaHiddenByLimit = 9999999") + | more ... + + h5 Perma hides users #[span.text-muted ({{permaHidden ? permaHidden.length : '?'}})] + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of permaHidden | slice:0:permaHiddenLimit") + account-info-remote([accountId]="a") + a.text-muted(*ngIf="permaHidden && permaHidden.length > permaHiddenLimit" (click)="permaHiddenLimit = 9999999") + | more ... + + hr + + //- ignores + .d-flex.justify-content-between.align-items-start + h5 Ignored by #[span.text-muted ({{ignoredBy ? ignoredBy.length : account.ignoresCount}})] + button.btn.btn-xs.btn-default((click)="fetchIgnores()" title="Fetch ignores list") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of ignoredBy | slice:0:ignoredByLimit") + account-info-remote([accountId]="a") + button.btn.btn-xs.btn-default.ml-1((click)="removeIgnore(a._id, account._id)" title="Remove ignore") + fa-icon([icon]="trashIcon") + a.text-muted(*ngIf="ignoredBy && ignoredBy.length > ignoredByLimit" (click)="ignoredByLimit = 9999999") + | more ... + + h5 Ignores users #[span.text-muted ({{ignores ? ignores.length : '?'}})] + .mb-2 + .mb-1.d-flex.align-items-center(*ngFor="let a of ignores | slice:0:ignoresLimit") + account-info-remote([accountId]="a") + button.btn.btn-xs.btn-default.ml-1((click)="removeIgnore(account._id, a._id)" title="Remove ignore") + fa-icon([icon]="trashIcon") + a.text-muted(*ngIf="ignores && ignores.length > ignoresLimit" (click)="ignoresLimit = 9999999") + | more ... + + .col-md-3 + //- emails + h5.d-flex.justify-content-between + div Emails #[span.text-muted ({{account.emails?.length || 0}})] + .dropdown(dropdown) + button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle) + fa-icon([icon]="cogIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="addEmail()") + | Add email + .mb-2 + .text-muted(*ngIf="!account.emails?.length") + | - no emails - + .d-flex.justify-content-between.items-align-center.mb-1(*ngFor="let e of account.emails | orderBy | slice:0:emailLimit") + div {{e}} + button.btn.btn-xs.btn-default((click)="removeEmail(e)" title="Remove email") + fa-icon([icon]="trashIcon") + + a.text-muted((click)="emailLimit = 99999" *ngIf="emailLimit < (account.emails?.length || 0)") + | more ... + + //- ponies + h5.d-flex.justify-content-between + div Ponies #[span.text-muted ({{account.characterCount}})] + .dropdown(dropdown) + button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle) + fa-icon([icon]="cogIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="getPoniesCreators()") + | Fetch creator info + a.dropdown-item((click)="restoringPonies = true") + | Restore ponies + a.dropdown-item((click)="removePoniesAboveLimit()") + | Remove ponies above limit + a.dropdown-item.text-danger((click)="removeAllPonies()") + | Remove all ponies + .mb-2(*ngIf="restoringPonies") + textarea.form-control.mb-1([(ngModel)]="poniesToRestore" placeholder="paste removed pony logs here") + textarea.form-control.mb-1([(ngModel)]="poniesToRestoreFilter" placeholder="paste pony ids to use here (optional)") + .text-right + button.btn.btn-xs.btn-default((click)="poniesToRestore = ''; restoringPonies = false") cancel + button.btn.btn-xs.btn-success.ml-1((click)="restorePonies()" [disabled]="!poniesToRestore") restore + hr + .mb-2 + pony-list-remote( + [accountId]="id" [expanded]="true" [deletable]="true" [highlight]="highlighCharacter" [duplicates]="duplicates") + + .row + .col-md-12 + h5 Events + events-table([events]="events" (showChat)="chatLog.show(account)") + + .row + .col-md-12 + admin-chat-log(#chatLog [canClose]="false" [account]="account") + button.btn.btn-sm.btn-default((click)="printJSON()") raw data + + .well.pre(*ngIf="rawData") + | {{rawData}} diff --git a/src/ts/components/admin/admin-account-details/admin-account-details.scss b/src/ts/components/admin/admin-account-details/admin-account-details.scss new file mode 100644 index 0000000..e9e8fd9 --- /dev/null +++ b/src/ts/components/admin/admin-account-details/admin-account-details.scss @@ -0,0 +1,17 @@ +@import 'partials/variables'; + +.spoiler-text { + background: $body-color; + border-radius: 3px; + cursor: default; + + &:hover { + background: none; + } +} + +.long-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src/ts/components/admin/admin-account-details/admin-account-details.ts b/src/ts/components/admin/admin-account-details/admin-account-details.ts new file mode 100644 index 0000000..02ff835 --- /dev/null +++ b/src/ts/components/admin/admin-account-details/admin-account-details.ts @@ -0,0 +1,582 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { remove, uniq } from 'lodash'; +import { + Account, Event, Auth, ROLES, Character, MergeInfo, MergeAccountData, SupporterInvite, accountFlags, + OriginInfoBase, DuplicateResult, AroundEntry, LogEntry +} from '../../../common/adminInterfaces'; +import { compareByName, createSupporterChanges, SupporterChange, getTranslationUrl, getAge } from '../../../common/adminUtils'; +import { hasRole } from '../../../common/accountUtils'; +import { AdminModel } from '../../services/adminModel'; +import { + faCog, faSync, faLanguage, faTrash, faGlobe, faTerminal, faSpinner, + faIdBadge, faEnvelope, faFont, faCompressArrowsAlt, faComment, faSignOutAlt, faUsers, faCheckCircle, + faDatabase, faCopy, faCalendar, faExclamationCircle, +} from '../../../client/icons'; +import { flagsToString, includes, flatten, removeItem } from '../../../common/utils'; +import { Subscription } from '../../../common/interfaces'; +import { showTextInNewTab } from '../../../client/htmlUtils'; + +const defaultLimit = 15; +const defaultDuplicatesLimit = 10; +const year = (new Date()).getFullYear(); + +@Component({ + selector: 'admin-account-details', + templateUrl: 'admin-account-details.pug', + styleUrls: ['admin-account-details.scss'], +}) +export class AdminAccountDetails implements OnInit, OnDestroy { + readonly cogIcon = faCog; + readonly syncIcon = faSync; + readonly langIcon = faLanguage; + readonly trashIcon = faTrash; + readonly signOutIcon = faSignOutAlt; + readonly duplicateNoteIcon = faExclamationCircle; + readonly duplicateBrowserIcon = faGlobe; + readonly duplicateBrowserIdIcon = faIdBadge; + readonly duplicateEmailIcon = faEnvelope; + readonly duplicateNameIcon = faFont; + readonly duplicateBirthdateIcon = faCalendar; + readonly mergeIcon = faCompressArrowsAlt; + readonly commentIcon = faComment; + readonly partyIcon = faUsers; + readonly checkIcon = faCheckCircle; + readonly consoleIcon = faTerminal; + readonly spinnerIcon = faSpinner; + readonly dataIcon = faDatabase; + readonly copyIcon = faCopy; + readonly roles = ROLES; + ages = [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]; + account?: Account; + duplicates?: DuplicateResult[]; + ponyNames?: string[]; + ignores?: string[]; + ignoredBy?: string[]; + hidden?: string[]; + hiddenBy?: string[]; + permaHidden?: string[]; + permaHiddenBy?: string[]; + friends?: string[]; + events?: Event[]; + merges?: MergeInfo[]; + banLog?: LogEntry[]; + support?: SupporterChange[]; + rawData?: string; + invites?: SupporterInvite[]; + auths: string[] = []; + origins: OriginInfoBase[] = []; + counters: { key: string; value: any; }[] = []; + authLimit = defaultLimit; + emailLimit = defaultLimit; + loadingDuplicates = false; + accountObject?: Account; + clearingSessions = false; + authData?: string; + authDataAccount?: string; + restoringPonies = false; + poniesToRestore = ''; + poniesToRestoreFilter = ''; + friendsLimit = defaultLimit; + hiddenByLimit = defaultLimit; + hiddenLimit = defaultLimit; + permaHiddenByLimit = defaultLimit; + permaHiddenLimit = defaultLimit; + private id?: string; + private aroundMap = new Map(); + private duplicatesLimits = new Map(); + private ignoresLimits = new Map(); + private ignoredByLimits = new Map(); + private authsSubscription?: Subscription; + private accountSubscription?: Subscription; + private originsSubscription?: Subscription; + constructor(private route: ActivatedRoute, private model: AdminModel) { + } + get canRemove() { + return hasRole(this.model.account, 'superadmin'); + } + get around() { + return this.aroundMap.get(this.id || ''); + } + get duplicatesLimit() { + return (this.id && this.duplicatesLimits.has(this.id)) ? this.duplicatesLimits.get(this.id)! : defaultDuplicatesLimit; + } + set duplicatesLimit(value) { + if (this.id) { + this.duplicatesLimits.set(this.id, value); + } + } + get ignoresLimit() { + return (this.id && this.ignoresLimits.has(this.id)) ? this.ignoresLimits.get(this.id)! : defaultLimit; + } + set ignoresLimit(value) { + if (this.id) { + this.ignoresLimits.set(this.id, value); + } + } + get ignoredByLimit() { + return (this.id && this.ignoredByLimits.has(this.id)) ? this.ignoredByLimits.get(this.id)! : defaultLimit; + } + set ignoredByLimit(value) { + if (this.id) { + this.ignoredByLimits.set(this.id, value); + } + } + get originRegions() { + return uniq(this.origins.map(o => o.country)); + } + get age() { + return (this.account && this.account.birthdate) ? getAge(this.account.birthdate) : '-'; + } + get forceAge() { + return (this.account && this.account.birthyear) ? (year - this.account.birthyear) : '-'; + } + ngOnInit() { + this.route.params.subscribe(({ id }) => { + this.id = id; + this.update(); + }); + + this.model.updated = () => this.update(); + this.update(); + } + ngOnDestroy() { + this.model.updated = () => { }; + this.authsSubscription && this.authsSubscription.unsubscribe(); + this.accountSubscription && this.accountSubscription.unsubscribe(); + this.originsSubscription && this.originsSubscription.unsubscribe(); + } + refresh() { + const account = this.account; + + this.ponyNames = []; + this.banLog = undefined; + this.merges = undefined; + this.support = undefined; + this.invites = undefined; + this.accountObject = undefined; + this.loadingDuplicates = false; + + this.authsSubscription && this.authsSubscription.unsubscribe(); + this.authsSubscription = undefined; + + this.originsSubscription && this.originsSubscription.unsubscribe(); + this.originsSubscription = undefined; + + this.auths = []; + this.origins = []; + this.counters = []; + + this.friends = undefined; + this.hidden = undefined; + this.hiddenBy = undefined; + this.permaHidden = undefined; + this.permaHiddenBy = undefined; + + if (account) { + account.ignoredByLimit = account.ignoredByLimit || 10; + account.ignoresLimit = account.ignoresLimit || 10; + account.duplicatesLimit = account.duplicatesLimit || 10; + + this.loadingDuplicates = true; + this.model.getAllDuplicates(account._id) + .then(duplicates => { + if (duplicates) { + this.ponyNames = uniq(flatten(duplicates.map(d => (d.ponies || []).map(x => x.toLowerCase())))); + this.duplicates = duplicates; + this.loadingDuplicates = false; + } + }); + + this.model.getAccount(account._id) + .then(account => this.accountObject = account); + + this.refreshDetails(); + + this.authsSubscription = this.model.accountAuths + .subscribe(account._id, auths => this.auths = auths || []); + + this.originsSubscription = this.model.accountOrigins + .subscribe(account._id, origins => this.origins = origins || []); + } else { + this.duplicates = []; + this.ignores = []; + this.ignoredBy = []; + } + } + refreshAround() { + const account = this.account; + + if (account) { + this.model.getAccountAround(account._id) + .then(accounts => { + if (accounts) { + this.aroundMap.set(account._id, accounts); + } + }); + } + } + refreshDetails() { + function createCounter(key: string, value: any) { + if (key === 'toys') { + value = (value >>> 0).toString(2).padStart(32, '0'); + } + + return { key, value }; + } + + if (this.account) { + this.model.getDetailsForAccount(this.account) + .then(details => { + if (details) { + this.banLog = details.banLog; + this.merges = details.merges; + this.support = createSupporterChanges(details.supporterLog); + this.invites = [ + ...details.invitesSent!.map(i => ({ ...i, type: 'sent' })), + ...details.invitesReceived!.map(i => ({ ...i, type: 'recv' })), + ]; + const state = details.state as any; + this.counters = Object.keys(state).map(key => createCounter(key, state[key])); + } + }); + } + } + fetchIgnores() { + if (this.id) { + this.model.getIgnoresAndIgnoredBy(this.id) + .then(result => { + if (result) { + this.ignores = result.ignores; + this.ignoredBy = result.ignoredBy; + } + }); + } + } + fetchHidden() { + if (this.id) { + this.model.getAccountHidden(this.id) + .then(result => { + if (result) { + this.hidden = result.hidden; + this.hiddenBy = result.hiddenBy; + this.permaHidden = result.permaHidden; + this.permaHiddenBy = result.permaHiddenBy; + } + }); + } + } + fetchFriends() { + if (this.id) { + this.model.getAccountFriends(this.id) + .then(result => this.friends = result); + } + } + removeFriend(friendId: string) { + if (this.id && confirm('Are you sure ?')) { + this.model.removeFriend(this.id, friendId) + .then(() => this.friends && removeItem(this.friends, friendId)); + } + } + canToggleRole(role: string) { + return role !== 'superadmin' && hasRole(this.model.account, 'superadmin'); + } + hasRole(role: string) { + return hasRole(this.account, role); + } + toggleRole(role: string) { + if (this.id) { + this.model.setRole(this.id, role, !this.hasRole(role)); + } + } + showAccountData() { + if (this.id) { + this.model.getAccount(this.id) + .then(account => { + (window as any).$data = account; + console.log(account); + console.log('accessible in $data'); + }); + } + } + printAuthList() { + this.forAuths(auths => { + const authList = auths.map((a, i) => `${i + 1}. [${a.provider}] ${a.name || ''}`).join('\n'); + this.authData = authList; + this.authDataAccount = this.account ? `${this.account.name} [${this.account._id}]` : ''; + console.log(authList); + }); + } + printAuthData() { + this.forAuths(auths => { + const authData = auths.map((a, i) => `${i + 1}. [${a._id}] [${a.provider}] ${a.name || ''} (${a.emails})`).join('\n'); + this.authData = authData; + this.authDataAccount = this.account ? `${this.account.name} [${this.account._id}]` : ''; + console.log(authData); + }); + } + copyAuthData() { + if ('clipboard' in navigator) { + (navigator as any).clipboard.writeText(this.authData); + } + } + clearAuthData() { + this.authData = undefined; + this.authDataAccount = undefined; + } + fetchAuths() { + this.forAuths(auths => { + (window as any).$auths = auths; + console.log(auths); + }); + } + private forAuths(callback: (auths: Auth[]) => void) { + if (this.account) { + this.model.getAuthsForAccount(this.account._id) + .then(auths => callback(auths || [])); + } + } + printJSON() { + if (this.id) { + this.model.getAccount(this.id) + .then(account => this.rawData = JSON.stringify(account, undefined, 2)); + } + } + clearOrigins(old: boolean, singles: boolean) { + if (this.id) { + this.model.clearOriginsForAccount(this.id, { old, singles }) + .then(() => this.refresh()); + } + } + clearOriginsInRegion(country: string) { + if (this.id) { + this.model.clearOriginsForAccount(this.id, { country }) + .then(() => this.refresh()); + } + } + clearOriginsFromDuplicates() { + if (this.account) { + const duplicates = (this.duplicates || []).slice(0, this.account.duplicatesLimit || 0); + const accounts = duplicates.map(d => d.account); + this.model.clearOriginsForAccounts(accounts, { old: true }) + .then(() => this.refresh()); + } + } + removeOrigin(ip: string) { + if (this.id) { + this.model.removeOriginsForAccount(this.id, [ip]) + .then(() => this.refresh()); + } + } + merge(accountId: string) { + if (this.account && confirm('Are you sure?')) { + this.model.mergeAccounts(this.account._id, accountId) + .then(() => remove(this.duplicates || [], d => d.account === accountId)) + .then(() => this.refresh()); + } + } + remove() { + if (this.account && confirm('Are you sure?')) { + this.model.removeAccount(this.account._id); + } + } + translateUrl(text: string) { + return getTranslationUrl(text); + } + getPoniesCreators() { + if (this.account) { + this.model.getPoniesCreators(this.account._id) + .then(items => { + if (items) { + items.sort(compareByName); + (window as any).$ponies = items; + console.log(items.map(i => `[${i._id}] "${i.name}" ${i.creator}`).join('\n')); + } + }); + } + } + removePoniesAboveLimit() { + if (this.account && confirm('Are you sure?')) { + this.model.removePoniesAboveLimit(this.account._id); + } + } + removeAllPonies() { + if (this.account && confirm('Are you sure?')) { + this.model.removeAllPonies(this.account._id); + } + } + restorePonies() { + if (this.account && this.poniesToRestore) { + let ids: string[] | undefined = undefined; + + if (this.poniesToRestoreFilter) { + const matches = Array.from(this.poniesToRestoreFilter.match(/\[[a-f0-9]{24}\]/g) || []) + .map(id => id.substr(1, 24)); + + if (matches.length) { + ids = matches; + } + } + + this.model.restorePonies(this.account._id, this.poniesToRestore, ids); + this.poniesToRestore = ''; + this.restoringPonies = false; + } + } + removeEmail(email: string) { + if (this.account && confirm('Are you sure?')) { + this.model.removeEmail(this.account._id, email); + } + } + removeIgnore(account: string, ignoredAccount: string) { + this.model.removeIgnore(account, ignoredAccount) + .then(() => this.fetchIgnores()); + } + highlighCharacter = (char: Character | undefined): boolean => { + return !!char && includes(this.ponyNames, char.name.toLowerCase()); + } + clearSessions() { + if (this.account) { + this.clearingSessions = true; + this.model.clearSessions(this.account._id) + .finally(() => this.clearingSessions = false); + } + } + getMergeTooltip(merge: MergeInfo) { + function mergeInfo(title: string, account: MergeAccountData) { + return [ + title, + `\tname: ${account.name}`, + `\tnote: ${account.note || ''}`, + `\tflags: ${flagsToString(account.flags, accountFlags)}`, + `\tage: ${account.birthdate ? getAge(account.birthdate) : '-'}`, + `\tfriends: ${account.friends ? account.friends.length : 0}`, + `\tcounters:`, + ...Object.keys(account.counters || {}).sort().map(key => `\t\t${key}: ${(account.counters as any)[key]}`), + `\tstate:`, + ...(account.state ? JSON.stringify(account.state, null, 2).split(/\n/).map(x => `\t\t${x}`) : []), + `\temails:`, + ...account.emails.map(e => `\t\t${e}`), + `\tauths:`, + ...account.auths.map(a => `\t\t[${a.id}] ${a.name}`), + `\tcharacters: ${account.characters.length}`, + ].join('\n'); + } + + if (merge.data) { + return `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`; + } else { + return ''; + } + } + printMerge(merge: MergeInfo) { + console.log(this.mergeInfo(merge)); + } + printMerge2(merge: MergeInfo) { + console.log(this.mergeInfo2(merge)); + } + mergeInfo(merge: MergeInfo) { + function mergeInfo(title: string, account: MergeAccountData) { + return [ + title, + `\tname: ${account.name}`, + `\tnote: ${account.note || ''}`, + `\tflags: ${flagsToString(account.flags, accountFlags)}`, + `\tage: ${account.birthdate ? getAge(account.birthdate) : '-'}`, + `\tcounters:`, + ...Object.keys(account.counters || {}).sort().map(key => `\t\t${key}: ${(account.counters as any)[key]}`), + `\tstate:`, + ...(account.state ? JSON.stringify(account.state, null, 2).split(/\n/).map(x => `\t\t${x}`) : []), + `\temails:`, + ...account.emails.map(e => `\t\t${e}`), + `\tauths:`, + ...account.auths.map(a => `\t\t[${a.id}] ${a.name}`), + `\tcharacters:`, + ...account.characters.map(a => `\t\t[${a.id}] ${a.name}`), + `\tfriends:`, + ...(account.friends || []).map(i => `\t\t[${i}]`), + `\tignores:`, + ...(account.ignores || []).map(i => `\t\t[${i}]`), + ].join('\n'); + } + + return merge.data && `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`; + } + private mergeInfo2(merge: MergeInfo) { + function mergeInfo(title: string, account: MergeAccountData) { + return [ + title, + `\tname: ${JSON.stringify(account.name)}`, + `\tnote: ${JSON.stringify(account.note)}`, + `\tflags: ${JSON.stringify(account.flags)}`, + `\tcounters: ${JSON.stringify(account.counters)}`, + `\tstate: ${JSON.stringify(account.state)}`, + `\temails: ${JSON.stringify(account.emails)}`, + `\tauths: ${JSON.stringify(account.auths)}`, + `\tcharacters: ${JSON.stringify(account.characters)}`, + `\tfriends: ${JSON.stringify(account.friends)}`, + `\tignores: ${JSON.stringify(account.ignores)}`, + ].join('\n'); + } + + return merge.data && `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`; + } + showMergeInNewTab(merge: MergeInfo) { + showTextInNewTab(this.mergeInfo(merge) || ''); + } + showMergeInNewTab2(merge: MergeInfo) { + showTextInNewTab(this.mergeInfo2(merge) || ''); + } + unmerge(mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { + if (this.account) { + this.model.unmergeAccounts(this.account._id, mergeId, split, keep) + .then(() => this.refresh()); + } + } + setAge(age: number) { + if (this.account) { + this.model.setAge(this.account._id, age); + } + } + addEmail() { + if (this.account) { + const email = prompt('enter email'); + + if (email && /@/.test(email)) { + this.model.addEmail(this.account._id, email); + } + } + } + private update() { + if (this.id) { + this.accountSubscription && this.accountSubscription.unsubscribe(); + this.accountSubscription = this.id ? this.model.accounts.subscribe(this.id, a => this.setAccount(a)) : undefined; + + this.events = this.model.events + .filter(e => e.account === this.id) + .slice(0, 10); + } + } + private setAccount(account: Account | undefined) { + const theSame = this.account === account || (this.account && account && this.account._id === account._id); + this.account = account; + + if (!theSame) { + this.duplicates = []; + this.ponyNames = []; + this.ignores = []; + this.ignoredBy = []; + this.rawData = undefined; + this.authLimit = defaultLimit; + this.emailLimit = defaultLimit; + this.duplicatesLimit = defaultDuplicatesLimit; + this.refresh(); + this.friendsLimit = defaultLimit; + this.hiddenByLimit = defaultLimit; + this.hiddenLimit = defaultLimit; + this.permaHiddenByLimit = defaultLimit; + this.permaHiddenLimit = defaultLimit; + (window as any).$account = account; + } + } +} diff --git a/src/ts/components/admin/admin-accounts/admin-accounts.pug b/src/ts/components/admin/admin-accounts/admin-accounts.pug new file mode 100644 index 0000000..fc890f0 --- /dev/null +++ b/src/ts/components/admin/admin-accounts/admin-accounts.pug @@ -0,0 +1,88 @@ +ng-template(#searchTooltip) + .text-left.help-tooltip + div: em search by: + div name:<account_name> #[em - account name] + div note:<note_text> #[em - note contents] + div email:<email_fragment> #[em - email fragment] + div role:<role_name> #[em - role] + div exact:<name> #[em - exact account name] + div ignores:<count> #[em - amount of ignores (at least)] + div ponies:<count> #[em - amount of ponies (at least)] + div auths:<count> #[em - amount of auths (at least)] + div old:<number_of_days> #[em - days since last visit] + div disabled! #[em - list accounts with disabled auths] + div locked! #[em - list accounts with locked auths] + div spam/swering/timeouts/limits:<number> + +ng-template(#duplicatesPopover) + a.text-secondary.d-block(*ngFor="let e of duplicateEntries" (click)="search = e") + | {{e}} + +.d-block.d-sm-flex + input.form-control.form-control-sm.admin-search(type="search" placeholder="search" [(ngModel)]="search") + .btn-group.btn-group-sm.ml-1 + button.btn.btn-default((click)="refresh(true)") + fa-icon([icon]="syncIcon" size="lg") + button.btn(btnCheckbox [(ngModel)]="autoRefresh" [btnHighlight]="autoRefresh") + | auto + .btn-group.btn-group-sm.ml-1 + button.btn.btn-sm.btn-danger( + *ngIf="duplicateEntries?.length" [popover]="duplicatesPopover" [outsideClick]="true" + placement="bottom" containerClass="popover-wide") + | Duplicates ({{duplicateEntries.length}}) + button.btn.btn-sm.btn-danger((click)="refreshDuplicates()" title="Refresh duplicates") + fa-icon([icon]="syncIcon" size="lg") + .btn-group.btn-group-sm.ml-1(btnRadioGroup [(ngModel)]="showOnly") + button.btn(*ngFor="let f of filters" [btnRadio]="f" [btnHighlight]="showOnly === f") {{f}} + button.btn.btn-sm.ml-1((click)="not = !not" [btnHighlight]="not") not + button.btn.btn-sm.btn-default.ml-2((click)="createAccount()") create account + +.d-block.d-sm-flex.justify-content-between.align-items-start + .pl-1([tooltip]="searchTooltip" placement="bottom" containerClass="tooltip-pre" style="width: 280px;") + small.text-muted + fa-icon.mr-1([icon]="filterIcon") + | filters + .d-block.d-sm-flex.mt-2 + fa-icon.text-muted.mr-3(*ngIf="loading" [icon]="spinnerIcon" [spin]="true" size="2x" style="margin-top: 3px") + strong.text-muted.mr-3(style="margin-top: 6px") + | {{totalItems === 99999999 ? '' : totalItems}} + pagination( + [totalItems]="totalItems" [itemsPerPage]="itemsPerPage" [(ngModel)]="currentPage" + [maxSize]="15" [directionLinks]="false" [boundaryLinks]="true") + +table.table.table-fixed.table-striped.table-hover.table-sm + thead + tr + th.col-time created + th.col-time last visit + th.col-account name + th.col-email emails + th.col-auth auths + th.col-origin origins + th ponies + th(style="width: 40px;") + tbody + tr(*ngFor="let a of itemsOnPageIds") + td.col-time + time-field([time]="info.account?.createdAt") + td.col-time + time-field([time]="info.account?.lastVisit") + td.col-account + account-info-remote(#info [accountId]="a") + td.col-email + email-list([emails]="info.account?.emails") + td.col-auth + auth-list-remote([accountId]="a") + td.col-origin + origin-list-remote([accountId]="a") + td + pony-list-remote([accountId]="a") + td(style="width: 40px;") + .btn-group.btn-group-xs + button.btn.btn-xs.btn-default( + (click)="$event.shiftKey ? chatLog.add(info.account) : chatLog.show(info.account)" title="Show chat") + fa-icon([icon]="commentIcon" [fixedWidth]="true") + +admin-chat-log(#chatLog) + +.admin-bottom-padding diff --git a/src/ts/components/admin/admin-accounts/admin-accounts.ts b/src/ts/components/admin/admin-accounts/admin-accounts.ts new file mode 100644 index 0000000..295f997 --- /dev/null +++ b/src/ts/components/admin/admin-accounts/admin-accounts.ts @@ -0,0 +1,123 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { debounce } from 'lodash'; +import { Account } from '../../../common/adminInterfaces'; +import { AdminModel } from '../../services/adminModel'; +import { faSync, faComment, faFilter, faEraser, faSpinner } from '../../../client/icons'; + +let autoRefresh = false; +let showOnly = 'all'; +let search = ''; +let currentPage = 0; +let not = false; + +@Component({ + selector: 'admin-accounts', + templateUrl: 'admin-accounts.pug', +}) +export class AdminAccounts implements OnInit { + readonly syncIcon = faSync; + readonly filterIcon = faFilter; + readonly commentIcon = faComment; + readonly eraserIcon = faEraser; + readonly spinnerIcon = faSpinner; + readonly filters = [ + 'all', + 'banned', + 'timed out', + 'with flags', + 'notes', + 'supporters', + ]; + totalItems = 99999999; + itemsOnPageIds: string[] = []; + itemsPerPage = 20; + loading = false; + private expanded = new Set(); + constructor(public model: AdminModel, private router: Router) { + } + get showOnly() { + return showOnly; + } + set showOnly(value) { + if (showOnly !== value) { + showOnly = value; + this.refresh(); + } + } + get not() { + return not; + } + set not(value) { + if (not !== value) { + not = value; + this.refresh(); + } + } + get autoRefresh() { + return autoRefresh; + } + set autoRefresh(value) { + autoRefresh = value; + } + get currentPage() { + return currentPage; + } + set currentPage(value) { + if (currentPage !== value) { + currentPage = value; + this.refresh(); + } + } + get search() { + return search; + } + set search(value) { + if (search !== value) { + search = value; + this.execSearch(); + } + } + private execSearch = debounce(() => this.refresh(), 500); + get duplicateEntries() { + return this.model.duplicateEntries; + } + refreshDuplicates() { + this.model.checkDuplicateEntries(true); + } + limit(account: Account) { + return this.expanded.has(account._id) ? 99999 : 2; + } + expand(account: Account) { + this.expanded.add(account._id); + } + ngOnInit() { + this.model.accountPromise + .then(() => this.refresh()); + } + refresh(force = false) { + this.loading = true; + this.model.findAccounts({ + search: this.search.trim(), + not: this.not, + showOnly: this.showOnly, + page: this.currentPage - 1, + itemsPerPage: this.itemsPerPage, + force, + }).then(result => { + if (result && result.page === (this.currentPage - 1)) { + this.totalItems = result.totalItems; + this.itemsOnPageIds = result.accounts; + this.loading = false; + } + }); + } + createAccount() { + const name = prompt('enter new account name'); + + if (name) { + this.model.createAccount(name) + .then(id => this.router.navigate(['accounts', id])); + } + } +} diff --git a/src/ts/components/admin/admin-events/admin-events.pug b/src/ts/components/admin/admin-events/admin-events.pug new file mode 100644 index 0000000..5bc14c1 --- /dev/null +++ b/src/ts/components/admin/admin-events/admin-events.pug @@ -0,0 +1,55 @@ +ng-template(#duplicatesPopover) + div(*ngFor="let e of duplicateEntries") + | {{e}} + +.d-block.d-sm-flex.justify-content-between.align-items-center + .form-inline + input.form-control.form-control-sm.admin-search-inline.d-none.d-sm-inline-block( + type="search" placeholder="search" [(ngModel)]="search" disabled) + + button.btn.btn-sm.ml-1((click)="toggleNotifications()" [btnHighlight]="notifications" title="Toggle notifications") + fa-icon([icon]="bellIcon" size="lg") + button.btn.btn-sm.btn-default.ml-1((click)="cleanupDeleted()" title="Cleanup deleted events") + fa-icon.mr-1([icon]="syncIcon" size="lg") + | clear + button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 60 * 10)" title="Delete all older than 10 minutes") + fa-icon.mr-1([icon]="clockIcon" size="lg") + | 10min + button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 3)" title="Delete all events") + fa-icon.mr-1([icon]="trashIcon" size="lg") + | all + button.btn.btn-sm.btn-default.ml-1((click)="showChat()" title="Show chatlog") + fa-icon.mr-1([icon]="commentsIcon" size="lg") + + .btn-group.ml-1 + button.btn.btn-sm.btn-danger( + *ngIf="duplicateEntries?.length" [popover]="duplicatesPopover" placement="bottom" containerClass="popover-wide") + | duplicates ({{duplicateEntries.length}}) + button.btn.btn-sm.btn-danger((click)="refreshDuplicates()" title="Refresh duplicates") + fa-icon([icon]="syncIcon" size="lg") + + span.text-muted.d-none.d-sm-inline + span.ml-3(title="Used disk space" [class.text-danger]="isLowDiskSpace") + fa-icon.mr-2([icon]="hddIcon") + | {{status.diskSpace}} + span.ml-3(title="Memory usage" [class.text-danger]="isLowMemory") + fa-icon.mr-2([icon]="ramIcon") + | {{status.memoryUsage}} + span.ml-3(title="Certificate expires in" [class.text-danger]="isOldCertificate") + fa-icon.mr-2([icon]="certificateIcon") + from-now([time]="status.certificateExpiration") + span.ml-3(title="Last Patreon update" [class.text-danger]="isOldPatreon") + fa-icon.mr-2([icon]="patreonIcon") + from-now([time]="status.lastPatreonUpdate") + + pagination( + [totalItems]="filtered.length" [itemsPerPage]="itemsPerPage" [maxSize]="15" [(ngModel)]="currentPage" + [directionLinks]="false") + +events-table( + [events]="filteredOnPage" (showChat)="showChat($event)" (addChat)="addChat($event)" + (removedEvent)="removedEvent($event)") + +admin-chat-log(#chatLog) + +.admin-bottom-padding diff --git a/src/ts/components/admin/admin-events/admin-events.ts b/src/ts/components/admin/admin-events/admin-events.ts new file mode 100644 index 0000000..1aec824 --- /dev/null +++ b/src/ts/components/admin/admin-events/admin-events.ts @@ -0,0 +1,99 @@ +import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; +import { fromNow } from '../../../common/utils'; +import { Event, ChatEvent } from '../../../common/adminInterfaces'; +import { AdminModel } from '../../services/adminModel'; +import { BaseTable, BaseTableState } from '../base-table'; +import { AdminChatLog } from '../shared/admin-chat-log/admin-chat-log'; +import { + faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon +} from '../../../client/icons'; + +let state: BaseTableState; + +@Component({ + selector: 'admin-events', + templateUrl: 'admin-events.pug', +}) +export class AdminEvents extends BaseTable implements OnInit, OnDestroy { + readonly bellIcon = faBell; + readonly syncIcon = faSync; + readonly clockIcon = faClock; + readonly trashIcon = faTrash; + readonly commentsIcon = faComments; + readonly hddIcon = faHdd; + readonly ramIcon = faMicrochip; + readonly certificateIcon = faCertificate; + readonly duplicateIcon = faClone; + readonly patreonIcon = faPatreon; + @ViewChild('chatLog', { static: true }) chatLog!: AdminChatLog; + private chatEvent?: Event; + constructor(private model: AdminModel) { + super(); + } + get status() { + return this.model.state.status; + } + get isLowDiskSpace() { + return this.model.isLowDiskSpace; + } + get isLowMemory() { + return this.model.isLowMemory; + } + get isOldCertificate() { + return this.model.isOldCertificate; + } + get isOldPatreon() { + return this.model.isOldPatreon; + } + get items() { + return this.model.events; + } + get duplicateEntries() { + return this.model.duplicateEntries; + } + get notifications() { + return this.model.notifications; + } + ngOnInit() { + this.model.updated = () => this.updateItems(); + this.setState(state); + this.updateItems(); + } + ngOnDestroy() { + this.model.updated = () => { }; + } + cleanupDeleted() { + this.model.cleanupDeletedEvents(); + } + refreshDuplicates() { + this.model.checkDuplicateEntries(true); + } + removeEvents(olderThan: number) { + const date = fromNow(-olderThan); + const oldEvents = this.items.filter(e => e.updatedAt.getTime() < date.getTime()); + return Promise.all(oldEvents.map(e => this.model.removeEvent(e._id).then(() => this.removedEvent(e)))); + } + showChat(e?: ChatEvent) { + this.chatEvent = e && e.event; + this.chatLog.show(e && e.account); + } + addChat(e: ChatEvent) { + if (e.account) { + this.chatLog.add(e.account); + } + } + removedEvent(e: Event) { + if (this.chatEvent === e) { + this.chatLog.close(); + } + } + toggleNotifications() { + this.model.toggleNotifications(); + } + protected onChange() { + state = this.getState(); + } + protected updatePage() { + super.updatePage(); + } +} diff --git a/src/ts/components/admin/admin-origin-details/admin-origin-details.pug b/src/ts/components/admin/admin-origin-details/admin-origin-details.pug new file mode 100644 index 0000000..22511b5 --- /dev/null +++ b/src/ts/components/admin/admin-origin-details/admin-origin-details.pug @@ -0,0 +1,33 @@ +.row(style="margin-top: 100px;") + .col-md-6 + h2 {{ip}} [{{info.origin?.country}}] + + div(style="margin-bottom: 10px;") + origin-info-remote(#info [originIP]="ip") + + div + a([href]="whoisHref" target="_blank" rel="noopener noreferrer") + | show whois info + + div + span.text-muted created: + from-now.ml-1([time]="info.origin?.createdAt") + + div + span.text-muted updated: + from-now.ml-1([time]="info.origin?.updatedAt") + + .col-md-6 + h3 Accounts #[span.text-muted.ml-1 ({{accounts.length}})] + + div(*ngFor="let a of accounts | slice:0:50") + account-info-remote([accountId]="a") + +.row + .col-md-12 + h3 Events + events-table([events]="events" (showChat)="chatLog.show($event.account)") + +.row + .col-md-12 + admin-chat-log(#chatLog) diff --git a/src/ts/components/admin/admin-origin-details/admin-origin-details.ts b/src/ts/components/admin/admin-origin-details/admin-origin-details.ts new file mode 100644 index 0000000..adbd2c2 --- /dev/null +++ b/src/ts/components/admin/admin-origin-details/admin-origin-details.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Event } from '../../../common/adminInterfaces'; +import { AdminModel } from '../../services/adminModel'; + +@Component({ + selector: 'admin-origin-details', + templateUrl: 'admin-origin-details.pug', +}) +export class AdminOriginDetails implements OnInit { + events?: Event[]; + accounts: string[] = []; + ip?: string; + constructor(private route: ActivatedRoute, private model: AdminModel) { + } + get whoisHref() { + return `http://whois.urih.com/record/${this.ip}/`; + } + ngOnInit() { + this.route.params.forEach(p => { + this.ip = p['ip']; + this.update(); + }); + } + private update() { + this.accounts = []; + + if (this.model.connected && this.ip) { + this.model.getAccountsByOrigin(this.ip) + .then(accounts => this.accounts = accounts || []); + this.events = this.model.events + .filter(e => e.origin && e.origin.ip === this.ip) + .slice(0, 20); + } + } +} diff --git a/src/ts/components/admin/admin-origins/admin-origins.pug b/src/ts/components/admin/admin-origins/admin-origins.pug new file mode 100644 index 0000000..3401502 --- /dev/null +++ b/src/ts/components/admin/admin-origins/admin-origins.pug @@ -0,0 +1,59 @@ +.text-center.pt-5(*ngIf="!stats") + fa-icon.text-muted([icon]="spinnerIcon" [fixedWidth]="true" [spin]="true" size="3x") + +.row(*ngIf="stats") + .col-md-6 + h3 + | Origins statistics + button.btn.btn-xs.btn-default.ml-1((click)="update()") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .stat-field Total origin count: #[b {{stats.totalOrigins}}] + .stat-field Total origin count (IP4 / IP6): #[b {{stats.totalOriginsIP4}} / {{stats.totalOriginsIP6}}] + .stat-field.mt-3 Unique origins: #[b {{stats.uniqueOrigins}}] + .stat-field Multiple account origins: #[b {{stats.duplicateOrigins}}] + .stat-field Single account origins: #[b {{stats.singleOrigins}}] + + h3.mt-3 + | Ignores statistics + button.btn.btn-xs.btn-default.ml-1((click)="update()") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .stat-field Total ignore count: #[b {{other?.totalIgnores}}] + + h3.mt-3 + | Other statistics + button.btn.btn-xs.btn-default.ml-1((click)="update()") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .stat-field Auths with empty account: #[b {{other?.authsWithEmptyAccount}}] + .stat-field Auths with missing account: #[b {{other?.authsWithMissingAccount}}] + + .col-md-6 + h3 Origins distribution + + table.table.table-sm(style="width: auto;") + thead + tr + th origins + th accounts + th + tbody + tr(*ngFor="let d of stats.distribution; let i = index") + td {{i}} + td {{d}} + td + button.btn.btn-xs.btn-default((click)="clear(i)" [disabled]="pending" title="Clear old singles in this row") + fa-icon([icon]="eraserIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default.ml-1((click)="clear(i, true)" [disabled]="pending" title="Clear old singles in or below this row") + fa-icon([icon]="eraserIcon" [fixedWidth]="true") + fa-icon([icon]="chevronDownIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default.ml-1((click)="clearSingles(i)" [disabled]="pending" title="Clear singles in or below this row") + fa-icon([icon]="userIcon" [fixedWidth]="true") + fa-icon([icon]="chevronDownIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default.ml-1((click)="clearOld(i)" [disabled]="pending" title="Clear very old (3 months) in or below this row") + fa-icon([icon]="clockIcon" [fixedWidth]="true") + fa-icon([icon]="chevronDownIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default.ml-1((click)="clearTo10(i)" [disabled]="pending" title="Trim origins to 10 most recent") + b 10 + fa-icon([icon]="chevronDownIcon" [fixedWidth]="true") diff --git a/src/ts/components/admin/admin-origins/admin-origins.ts b/src/ts/components/admin/admin-origins/admin-origins.ts new file mode 100644 index 0000000..504c844 --- /dev/null +++ b/src/ts/components/admin/admin-origins/admin-origins.ts @@ -0,0 +1,53 @@ +import { Component, OnInit } from '@angular/core'; +import { RequestStats, OriginStats, OtherStats } from '../../../common/adminInterfaces'; +import { faSync, faEraser, faClock, faUser, faChevronDown, faSpinner } from '../../../client/icons'; +import { AdminModel } from '../../services/adminModel'; + +@Component({ + selector: 'admin-origins', + templateUrl: 'admin-origins.pug', +}) +export class AdminOrigins implements OnInit { + readonly syncIcon = faSync; + readonly eraserIcon = faEraser; + readonly clockIcon = faClock; + readonly userIcon = faUser; + readonly chevronDownIcon = faChevronDown; + readonly spinnerIcon = faSpinner; + stats?: OriginStats; + other?: OtherStats; + requestStats: RequestStats[] = []; + pending = false; + constructor(public model: AdminModel) { + } + ngOnInit() { + if (this.model.connected) { + this.update(); + } + } + update() { + this.model.getOriginStats().then(stats => this.stats = stats); + this.model.getOtherStats().then(stats => this.other = stats); + } + clear(count: number, andHigher = false) { + this.clearAll(count, andHigher, true, true, false); + } + clearOld(count: number) { + this.clearAll(count, true, true, false, false); + } + clearSingles(count: number) { + this.clearAll(count, true, false, true, false); + } + clearTo10(count: number) { + this.clearAll(count, true, false, true, true); + } + clearAll(count: number, andHigher: boolean, old: boolean, singles: boolean, trim: boolean) { + this.pending = true; + + return this.model.clearOrigins(count, andHigher, { old, singles, trim }) + .finally(() => { + this.pending = false; + this.update(); + }); + } +} diff --git a/src/ts/components/admin/admin-other/admin-other.pug b/src/ts/components/admin/admin-other/admin-other.pug new file mode 100644 index 0000000..c51c212 --- /dev/null +++ b/src/ts/components/admin/admin-other/admin-other.pug @@ -0,0 +1,52 @@ +.row + .col-6 + .form + .form-group + h3 Notifications + + .form + .form-group(*ngFor="let field of fields") + label {{field.title}} + textarea.form-control([(ngModel)]="field.value" rows="4" style="width: 400px;") + .form-group + button.btn.btn-primary(type="button" (click)="saveFields()") Save + button.btn.btn-default.ml-1(type="button" (click)="resetFields()") Reset + + .col-6 + .form + .form-group + h3 Patreon + .form-group.d-flex + button.btn.btn-default((click)="updatePatreon()") + | Update Data + .input-group.ml-1(style="width: 400px") + input.form-control(#patreonToken placeholder="patreon token") + .input-group-append + button.btn.btn-warning((click)="updatePatreonToken(patreonToken.value); patreonToken.value = ''") + | Update Token + .form-group + | To get new token, got to #[a(href="https://www.patreon.com/portal/registration/register-clients") My Clients], + | refresh token and update patreon token field with "Creator's Access Token" from the site. + .form-group + button.btn.btn-default((click)="getLastPatreonData()") + | Get last patreon data + + .form + .form-group + h3 Other + .form-group + button.btn.btn-default((click)="updatePastSupporters()") + | Update past supporters + +.row + .col-12 + .form + .form-group + label Suspicious pony info to report + textarea.form-control([(ngModel)]="suspiciousPonies" rows="15") + .form-group(*ngIf="suspiciousPoniesError") + .alert.alert-danger + | {{suspiciousPoniesError}} + .form-group + button.btn.btn-primary((click)="saveSuspiciousPonies()") Save + button.btn.btn-default.ml-1((click)="resetSuspiciousPonies()") Reset diff --git a/src/ts/components/admin/admin-other/admin-other.ts b/src/ts/components/admin/admin-other/admin-other.ts new file mode 100644 index 0000000..6ce952c --- /dev/null +++ b/src/ts/components/admin/admin-other/admin-other.ts @@ -0,0 +1,90 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { compact } from 'lodash'; +import { AdminModel } from '../../services/adminModel'; +import { Account, GeneralSettings } from '../../../common/adminInterfaces'; +import { Subscription } from '../../../common/interfaces'; +import { showTextInNewTab } from '../../../client/htmlUtils'; + +interface Field { + key: keyof GeneralSettings; + title: string; + value: string | undefined; +} + +@Component({ + selector: 'admin-other', + templateUrl: 'admin-other.pug', +}) +export class AdminOther implements OnInit, OnDestroy { + fields: Field[] = [ + { key: 'suspiciousNames', title: 'Suspicious pony names & emails to report', value: undefined }, + { key: 'suspiciousAuths', title: 'Suspicious auths to report', value: undefined }, + { key: 'suspiciousMessages', title: 'Suspicious messages to report (instant)', value: undefined }, + { key: 'suspiciousSafeMessages', title: 'Suspicious messages to report (safe only) (5+)', value: undefined }, + { key: 'suspiciousSafeWholeMessages', title: 'Suspicious messages to report (safe only) (5+) (whole words)', value: undefined }, + { key: 'suspiciousSafeInstantMessages', title: 'Suspicious messages to report (safe only) (instant)', value: undefined }, + { + key: 'suspiciousSafeInstantWholeMessages', + title: 'Suspicious messages to report (safe only) (whole words) (instant)', + value: undefined + }, + ]; + max = 100; + value = 0; + error?: string; + succeeded = false; + suspiciousPonies?: string; + suspiciousPoniesError?: string; + ignoreErrors?: string; + account: Account | undefined; + private subscription?: Subscription; + constructor(public model: AdminModel) { + } + ngOnInit() { + this.model.accountPromise.then(() => { + this.resetFields(); + this.resetSuspiciousPonies(); + }); + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + saveFields() { + let settings: any = {}; + this.fields.forEach(field => settings[field.key] = field.value); + this.model.updateSettings(settings); + } + resetFields() { + this.fields.map(field => { + field.value = this.model.state.loginServers[0][field.key] as string | undefined; + }); + } + saveSuspiciousPonies() { + this.suspiciousPoniesError = undefined; + + try { + compact(this.suspiciousPonies!.split(/\n/g).map(x => x.trim())).map(x => JSON.parse(x)); + this.model.updateSettings({ suspiciousPonies: this.suspiciousPonies }); + } catch (e) { + this.suspiciousPoniesError = e.message; + } + } + resetSuspiciousPonies() { + this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies; + } + updatePatreon() { + this.model.server.updatePatreon(); + } + updatePatreonToken(patreonToken: string) { + this.model.updateSettings({ patreonToken }); + } + getLastPatreonData() { + this.model.getLastPatreonData() + .then(data => { + showTextInNewTab(JSON.stringify(data, null, 2)); + }); + } + updatePastSupporters() { + this.model.updatePastSupporters(); + } +} diff --git a/src/ts/components/admin/admin-ponies/admin-ponies.pug b/src/ts/components/admin/admin-ponies/admin-ponies.pug new file mode 100644 index 0000000..49fef44 --- /dev/null +++ b/src/ts/components/admin/admin-ponies/admin-ponies.pug @@ -0,0 +1,65 @@ +ng-template(#searchTooltip) + .text-left.help-tooltip + div: em search by: + div exact:<name> #[em - exact pony name] + div orphan #[em - list orphaned ponies] + +.form-horizontal + input.form-control.form-control-sm.admin-search(type="search" placeholder="search" [(ngModel)]="search") + .btn-group.btn-group-sm.ml-1 + button.btn.btn-default((click)="refresh()") + fa-icon([icon]="syncIcon" size="lg") + + label.mx-2 + | order by: + .btn-group.btn-group-sm(btnRadioGroup [(ngModel)]="orderBy") + button.btn(btnRadio="" [btnHighlight]="!orderBy") + | created + button.btn(btnRadio="-lastUsed" [btnHighlight]="orderBy === '-lastUsed'") + | last used + + .pl-1([tooltip]="searchTooltip" placement="bottom" containerClass="tooltip-pre" style="width: 280px;") + small.text-muted + fa-icon.mr-1([icon]="filterIcon") + | filters + +.d-block.d-sm-flex.justify-content-end + .table-loader.text-muted.flex-grow-1.text-left(*ngIf="loading") + fa-icon.mr-1([icon]="spinnerIcon" [spin]="true") + | Loading... + strong.text-muted.mr-3(style="margin-top: 6px") + | {{totalItems > 900000000 ? '---' : totalItems}} + pagination( + [totalItems]="totalItems" [itemsPerPage]="itemsPerPage" [(ngModel)]="currentPage" [maxSize]="15" + [directionLinks]="false" [boundaryLinks]="true") + +table.table.table-fixed.table-striped.table-hover.table-sm + thead + tr + th.col-time created + th.col-time last used + th.col-pony pony + th.col-account account + th + th(style="width: 80px;") + tbody + tr(*ngFor="let i of items") + td.col-time + time-field([time]="ponyInfo.pony?.createdAt") + td.col-time + time-field([time]="ponyInfo.pony?.lastUsed") + td.col-pony + pony-info-remote(#ponyInfo [ponyId]="i") + td.col-account + account-info-remote(#accountInfo [accountId]="ponyInfo.pony?.account" [showDuplicates]="true") + td + td(style="width: 80px;") + button.btn.btn-xs.btn-danger((click)="remove(i)" title="Remove pony") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default.ml-1( + (click)="$event.shiftKey ? chatLog.add(accountInfo.account) : chatLog.show(accountInfo.account)" title="Show chat") + fa-icon([icon]="commentIcon" [fixedWidth]="true") + +admin-chat-log(#chatLog) + +.admin-bottom-padding diff --git a/src/ts/components/admin/admin-ponies/admin-ponies.scss b/src/ts/components/admin/admin-ponies/admin-ponies.scss new file mode 100644 index 0000000..076e864 --- /dev/null +++ b/src/ts/components/admin/admin-ponies/admin-ponies.scss @@ -0,0 +1,4 @@ +.table-loader { + line-height: 33px; + font-size: larger; +} diff --git a/src/ts/components/admin/admin-ponies/admin-ponies.ts b/src/ts/components/admin/admin-ponies/admin-ponies.ts new file mode 100644 index 0000000..fdcfd25 --- /dev/null +++ b/src/ts/components/admin/admin-ponies/admin-ponies.ts @@ -0,0 +1,97 @@ +import { Component, OnInit } from '@angular/core'; +import { isEqual, debounce } from 'lodash'; +import { Character, FindPonyQuery } from '../../../common/adminInterfaces'; +import { AdminModel } from '../../services/adminModel'; +import { faSync, faFilter, faTrash, faComment, faSpinner } from '../../../client/icons'; +import { cloneDeep, delay } from '../../../common/utils'; + +let currentPage = 1; +let query: FindPonyQuery = {}; + +@Component({ + selector: 'admin-ponies', + templateUrl: 'admin-ponies.pug', + styleUrls: ['admin-ponies.scss'], +}) +export class AdminPonies implements OnInit { + readonly syncIcon = faSync; + readonly filterIcon = faFilter; + readonly spinnerIcon = faSpinner; + readonly trashIcon = faTrash; + readonly commentIcon = faComment; + items?: string[]; + itemsPerPage = 20; + query: FindPonyQuery = {}; + loading = false; + private lastQuery: FindPonyQuery = {}; + private totalCount?: number; + private execSearch = debounce(() => this.fetchPonies(false), 1000); + constructor(private model: AdminModel) { + } + get totalItems() { + return this.totalCount === undefined ? this.model.counts.characters : this.totalCount; + } + get search() { + return query.search; + } + set search(value: string | undefined) { + if (query.search !== value) { + query.search = value; + this.execSearch(); + } + } + get orderBy() { + return query.orderBy; + } + set orderBy(value: string | undefined) { + if (query.orderBy !== value) { + query.orderBy = value; + this.execSearch(); + } + } + get currentPage() { + return currentPage; + } + set currentPage(value) { + if (currentPage !== value) { + currentPage = value; + this.fetchPonies(isEqual(this.lastQuery, query)); + } + } + ngOnInit() { + if (this.model.connected) { + this.fetchPonies(false); + } + } + refresh() { + this.fetchPonies(false); + } + remove(pony: Character) { + if (confirm('Are you sure?')) { + this.model.removePony(pony._id) + .then(() => delay(500)) + .then(() => this.refresh()); + } + } + private fetchPonies(skipTotalCount: boolean) { + const thisQuery = cloneDeep(query); + this.lastQuery = cloneDeep(query); + this.loading = true; + + thisQuery.search = thisQuery.search && thisQuery.search.trim(); + + this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount) + .then(result => { + if (result) { + if (!skipTotalCount) { + this.totalCount = result.totalCount; + } + + this.items = result.items; + } else { + this.items = []; + } + }) + .finally(() => this.loading = false); + } +} diff --git a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug new file mode 100644 index 0000000..c1b9469 --- /dev/null +++ b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug @@ -0,0 +1,41 @@ +div(style="position: relative;") + div(style="position: absolute; top: -50px; right: 0;") + b.p-2 {{server}} + .btn-group(*ngIf="server") + button.btn.btn-default((click)="resetZoom()") + | Reset zoom + button.btn.btn-default((click)="fitZoom()") + | Fit zoom + button.btn.btn-default((click)="fullFrameZoom()") + | Full frame + button.btn.btn-default.ml-1(*ngIf="server" (click)="load(server)") + | Refresh + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | Load + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + button.dropdown-item(*ngFor="let s of servers" (click)="load(s)") + | {{s}} + + div(#container style="position: relative; overflow: hidden;") + canvas(#canvas (mousemove)="mouseMove($event)" (wheel)="wheel($event)" (agDrag)="drag($event)") + div(#tooltip style="display: none; position: absolute; left: 0; top: 0; padding: 2px 6px; background: #111; border-radius: 2px; font-size: 12px; white-space: pre; pointer-events: none;") + + .float-right.text-muted + div total samples: {{timings.length}} + + table.table.table-sm(style="font-size: small; width: 1000px;") + thead + tr + th.text-right Self Time + th.text-right Self % + th.text-right Total % + th.text-right Count + th Name + tbody.font-monospace + tr(*ngFor="let l of listing") + td.text-right {{l.selfTime.toFixed(2)}} ms + td.text-right.text-muted {{l.selfPercent.toFixed(2)}} % + td.text-right.text-muted {{l.totalPercent.toFixed(2)}} % + td.text-right.text-muted {{l.count}} + td {{l.name}} diff --git a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts new file mode 100644 index 0000000..d06c842 --- /dev/null +++ b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts @@ -0,0 +1,291 @@ +import { Component, ViewChild, ElementRef } from '@angular/core'; +import { AdminModel } from '../../../services/adminModel'; +import { TimingEntry, TimingEntryType } from '../../../../common/adminInterfaces'; +import { findById, pointInRect, clamp } from '../../../../common/utils'; +import { SERVER_FPS } from '../../../../common/constants'; +import { AgDragEvent } from '../../../shared/directives/agDrag'; + +interface Tooltip { + x: number; + y: number; + w: number; + h: number; + text: string; +} + +interface ListingEntry { + name: string; + count: number; + selfTime: number; + totalTime: number; + selfPercent: number; + totalPercent: number; +} + +const frameTime = 1000 / SERVER_FPS; +const timePadding = 10; // ms + +@Component({ + selector: 'admin-reports-perf', + templateUrl: 'admin-reports-perf.pug', +}) +export class AdminReportsPerf { + @ViewChild('container', { static: true }) container!: ElementRef; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + @ViewChild('tooltip', { static: true }) tooltip!: ElementRef; + loaded = false; + server = ''; + startTime = 0; + endTime = 0; + listing: ListingEntry[] = []; + timings: TimingEntry[] = []; + private tooltips: Tooltip[] = []; + private startTimeFrom = 0; + private endTimeFrom = 0; + private frame: any = 0; + private lastZoom = 0; + constructor(private model: AdminModel) { + if (DEVELOPMENT) { + const interval = setInterval(() => { + if (findById(this.model.state.gameServers, 'dev')) { + clearInterval(interval); + this.load('dev'); + } + }, 100); + } + } + get servers() { + return this.model.state.gameServers.map(s => s.id); + } + async load(server: string) { + this.server = server; + this.timings = []; + this.loaded = false; + const result = await this.model.getTimings(server); + + if (result) { + this.loaded = true; + this.timings = result as TimingEntry[]; + this.setupZoom(); + this.recalcListing(); + } + + this.redraw(); + } + mouseMove(e: MouseEvent) { + const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; + const x = e.pageX - rect.left; + const y = e.pageY - rect.top; + + const tooltip = this.tooltips.find(t => pointInRect(x, y, t)); + const element = this.tooltip.nativeElement as HTMLElement; + + if (tooltip) { + element.style.display = 'block'; + element.style.left = `${x + 10}px`; + element.style.top = `${y + 10}px`; + element.innerText = tooltip.text; + } else { + element.style.display = 'none'; + } + } + wheel(e: WheelEvent) { + e.preventDefault(); + + const deltaY = clamp(e.deltaY, -1, 1); + const change = ((this.endTime - this.startTime) * 0.2 * deltaY); + const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; + const ratioFromLeft = (e.pageX - rect.left) / rect.width; + + this.startTime = this.startTime - change * ratioFromLeft; + this.endTime = this.endTime + change * (1 - ratioFromLeft); + + this.redraw(); + } + setupZoom() { + switch (this.lastZoom) { + case 0: + default: + this.resetZoom(); + break; + case 1: + this.fitZoom(); + break; + case 2: + this.fullFrameZoom(); + break; + } + } + resetZoom() { + const firstTime = this.timings[0].time; + const lastTime = this.timings[this.timings.length - 1].time; + this.startTime = firstTime - timePadding; + this.endTime = lastTime + timePadding; + this.redraw(); + this.lastZoom = 0; + } + fitZoom() { + const firstTime = this.timings[0].time; + const lastTime = this.timings[this.timings.length - 1].time; + const totalTime = lastTime - firstTime; + this.startTime = firstTime - totalTime * 0.05; + this.endTime = lastTime + totalTime * 0.05; + this.redraw(); + this.lastZoom = 1; + } + fullFrameZoom() { + const firstTime = this.timings[0].time; + const lastTime = firstTime + frameTime; + this.startTime = firstTime - 2; + this.endTime = lastTime + 2; + this.redraw(); + this.lastZoom = 2; + } + drag(e: AgDragEvent) { + if (e.type === 'start') { + this.startTimeFrom = this.startTime; + this.endTimeFrom = this.endTime; + } + + const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width; + this.startTime = this.startTimeFrom - e.dx * scale; + this.endTime = this.endTimeFrom - e.dx * scale; + this.redraw(); + } + redraw() { + this.frame = this.frame || requestAnimationFrame(() => this.draw()); + } + draw() { + this.frame = 0; + + const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + canvas.width = rect.width; + canvas.height = 400; + + const context = canvas.getContext('2d')!; + context.fillStyle = '#222'; + context.fillRect(0, 0, canvas.width, canvas.height); + + this.tooltips.length = 0; + + if (!this.timings.length) + return; + + const firstTime = this.timings[0].time; + const startTime = this.startTime; + const endTime = this.endTime; + const totalTime = endTime - startTime; + const timeScale = canvas.width / totalTime; + + function timeToX(time: number) { + return (time - startTime) * timeScale; + } + + function timeToXAligned(time: number) { + return Math.floor(timeToX(time)) + 0.5; + } + + context.textBaseline = 'middle'; + context.font = 'Arial 14px normal'; + + // scale + const scaleHeight = 20; + + context.strokeStyle = '#666'; + context.beginPath(); + + for (let time = firstTime; time < endTime; time += frameTime) { + context.moveTo(timeToXAligned(time), 0); + context.lineTo(timeToXAligned(time), canvas.height); + } + + context.stroke(); + + context.strokeStyle = '#ddd'; + context.beginPath(); + context.moveTo(0, scaleHeight + 0.5); + context.lineTo(canvas.width, scaleHeight + 0.5); + context.stroke(); + + // entries + const rowHeight = 20; + const startStack: TimingEntry[] = []; + + for (const entry of this.timings) { + if (entry.type === TimingEntryType.Start) { + startStack.push(entry); + } else { + const start = startStack.pop()!; + const name = start.name!; + const startX = timeToX(start.time); + const endX = timeToX(entry.time); + const y = scaleHeight + 2 + startStack.length * rowHeight; + const w = endX - startX; + let text = name; + const time = entry.time - start.time; + + if (startStack.length === 0) { + context.fillStyle = '#efc457'; + text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`; + } else if (/\(\)$/.test(name)) { + context.fillStyle = '#d4ecc6'; + } else { + context.fillStyle = '#c6dcec'; + } + + context.fillRect(startX, y, w, rowHeight - 1); + this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` }); + + if (w > 4) { + context.fillStyle = '#222'; + context.fillText(text, startX + 4, y + rowHeight / 2); + } + } + } + } + recalcListing() { + interface Entry extends TimingEntry { + excludedTime: number; + } + + this.listing = []; + const startStack: Entry[] = []; + + for (const entry of this.timings) { + if (entry.type === TimingEntryType.Start) { + startStack.push({ ...entry, excludedTime: 0 }); + } else { + const start = startStack.pop()!; + const name = start.name!; + const time = entry.time - start.time; + + let listing = this.listing.find(l => l.name === name); + + if (!listing) { + listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 }; + this.listing.push(listing); + } + + listing.count++; + listing.selfTime += (time - start.excludedTime); + listing.totalTime += time; + + if (startStack.length) { + startStack[startStack.length - 1].excludedTime += time; + } + } + } + + const firstTime = this.timings[0].time; + const lastTime = this.timings[this.timings.length - 1].time; + const totalTime = lastTime - firstTime; + + for (const listing of this.listing) { + listing.selfPercent = 100 * listing.selfTime / totalTime; + listing.totalPercent = 100 * listing.totalTime / totalTime; + } + + this.listing.sort((a, b) => b.selfTime - a.selfTime); + } +} diff --git a/src/ts/components/admin/admin-reports/admin-reports.pug b/src/ts/components/admin/admin-reports/admin-reports.pug new file mode 100644 index 0000000..0f378c0 --- /dev/null +++ b/src/ts/components/admin/admin-reports/admin-reports.pug @@ -0,0 +1,4 @@ +nav.nav.nav-pills.mb-3 + a.nav-item.nav-link(routerLink="/reports/perf" routerLinkActive="active") Perf + +router-outlet diff --git a/src/ts/components/admin/admin-reports/admin-reports.ts b/src/ts/components/admin/admin-reports/admin-reports.ts new file mode 100644 index 0000000..4b574db --- /dev/null +++ b/src/ts/components/admin/admin-reports/admin-reports.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'admin-reports', + templateUrl: 'admin-reports.pug', +}) +export class AdminReports { +} diff --git a/src/ts/components/admin/admin-sign-in/admin-sign-in.pug b/src/ts/components/admin/admin-sign-in/admin-sign-in.pug new file mode 100644 index 0000000..cac7219 --- /dev/null +++ b/src/ts/components/admin/admin-sign-in/admin-sign-in.pug @@ -0,0 +1,2 @@ +.mx-auto.mt-4(style="max-width: 400px") + sign-in-box diff --git a/src/ts/components/admin/admin-sign-in/admin-sign-in.ts b/src/ts/components/admin/admin-sign-in/admin-sign-in.ts new file mode 100644 index 0000000..af10d93 --- /dev/null +++ b/src/ts/components/admin/admin-sign-in/admin-sign-in.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'admin-sign-in', + templateUrl: 'admin-sign-in.pug', +}) +export class AdminSignIn { +} diff --git a/src/ts/components/admin/admin-state/admin-state.pug b/src/ts/components/admin/admin-state/admin-state.pug new file mode 100644 index 0000000..a8d9037 --- /dev/null +++ b/src/ts/components/admin/admin-state/admin-state.pug @@ -0,0 +1,161 @@ +div(*ngIf="isSuperadmin") + button.btn.btn-sm.btn-default((click)="notifyOfUpdate()") + | Notify of update + button.btn.btn-sm.btn-default.ml-1((click)="shutdownServers()") + | Shutdown servers + button.btn.btn-sm.btn-default.ml-1((click)="resetUpdating()") + | Reset updating + +h3.mt-4 Servers + +.server-box(*ngFor="let s of loginServers") + .row.form-group + .col-md-4 + strong Login server + .col-md-2.pb-1 + .badge.badge-none(*ngIf="s.dead") dead + .badge.badge-success(*ngIf="!s.dead && !s.isPageOffline") online + .badge.badge-danger(*ngIf="!s.dead && s.isPageOffline") offline + .badge.badge-warning.ml-1(*ngIf="s.updating") updating + .col-md-6 + button.btn.btn-sm.btn-success((click)="updateLoginSettings({ isPageOffline: false })") + | Switch online + button.btn.btn-sm.btn-danger.ml-1((click)="updateLoginSettings({ isPageOffline: true })") + | Switch offline + button.btn.btn-sm.btn-default.ml-1((click)="fetchRequestStats()" title="Show request stats from last hour") + | Stats + .row.form-group + .col-md-3 + .col-md-9 + .text-left(style="column-width: 280px;") + div(*ngFor="let o of loginOptions") + on-off-switch([on]="s[o.id]" (toggle)="updateLoginSetting(o.id, $event)" [label]="o.label") + + .row.form-group(*ngIf="requestStats") + .col-md-12 + table.table.table-striped.table-hover.table-sm + thead + tr + th path + th all + th count + th avg + th + button.float-right.btn.btn-default.btn-xs((click)="resetRequestStats()") close + | total + tbody + tr(*ngFor="let s of requestStats") + td.limit-length {{s.path}} + td {{s.totalCount}} + td {{s.count}} + td {{s.average}} + td {{s.total}} + + .row.form-group(*ngIf="userCounts") + .col-md-12 + table.table.table-striped.table-hover.table-sm + thead + tr + th date + th unique users + tbody + tr(*ngFor="let u of userCounts") + td {{u.date | date:'MMM d'}} + td {{u.count}} + +.server-box(*ngFor="let s of servers") + .row.form-group + .col-md-2 + strong {{s.name}} + .col-md-2 + div + strong.mr-1 {{s.online}} + span.text-muted users + span.text-muted.ml-2(title="Users in queue | on main map") ({{s.queued}} | {{s.onMain}}) + strong.mr-1.ml-2 {{s.maps}} + span.text-muted maps + .col-md-2.pb-1 + .badge.badge-none(*ngIf="s.dead") dead + .badge.badge-danger(*ngIf="!s.dead && s.settings.isServerOffline") offline + .badge.badge-success(*ngIf="!s.dead && !s.settings.isServerOffline") online + .badge.badge-forbidden.ml-1(*ngIf="s.require") {{s.require}} + .badge.badge-warning.ml-1(*ngIf="s.shutdown") shutdown + .col-md-6 + .d-flex + button.btn.btn-sm.btn-success((click)="updateServerSettings(s, { isServerOffline: false })" [disabled]="s.dead") + | Switch online + button.btn.btn-sm.btn-danger.ml-1((click)="updateServerSettings(s, { isServerOffline: true })" [disabled]="s.dead") + | Switch offline + button.btn.btn-sm.btn-danger.ml-1((click)="kickAll(s)" [disabled]="s.dead") + | Kick all + .dropdown.ml-1.d-flex(dropdown) + button.dropdown-toggle.btn.btn-sm.btn-default(dropdownToggle [disabled]="s.dead") + fa-icon([icon]="cogIcon" [fixedWidth]="true") + .dropdown-menu(*dropdownMenu) + button.dropdown-item.btn.btn-sm.btn-default((click)="fetchStats(s)") + | Stats: Socket transfer rates from last hour + button.dropdown-item.btn.btn-sm.btn-default((click)="fetchCountryStats(s)") + | Stats: Online users per country + button.dropdown-item.btn.btn-sm.btn-default((click)="fetchSupportStats(s)") + | Stats: Support + button.dropdown-item.btn.btn-sm.btn-default((click)="fetchMapStats(s)") + | Stats: Maps + button.dropdown-item.btn.btn-sm.btn-default((click)="notifyOfUpdate(s.id)") + | Notify of update + button.dropdown-item.btn.btn-sm.btn-default((click)="shutdownServers(s.id)") + | Shutdown server + button.dropdown-item.btn.btn-sm.btn-default((click)="resetUpdating(s.id)") + | Reset updating + button.btn.btn-sm.btn-default.ml-1( + (click)="showSettings[s.id] = !showSettings[s.id]" [class.active]="showSettings[s.id]" + [disabled]="s.dead" title="Settings") + fa-icon([icon]="optionsIcon") + + .row.form-group(*ngIf="s.settings && !s.dead") + .col-md-3.pb-1 + .badge.badge-none.mr-1(*ngFor="let f of s.flags | keys") + | {{f}} + .col-md-9(*ngIf="showSettings[s.id]") + .text-left(style="column-width: 280px;") + div(*ngFor="let o of options") + on-off-switch( + [on]="s.settings[o.id]" (toggle)="updateServerSetting(s, o.id, $event)" [disabled]="s.dead" [label]="o.label") + + .row.form-group(*ngIf="getStats(s); let stats") + .col-12 + table.table.table-striped.table-hover.table-sm + thead + tr + th id + th name + th type + th count (bin) + th count (str) + th average + th + button.float-right.btn.btn-default.btn-xs((click)="resetStats(s)") close + | total + tbody + tr(*ngFor="let a of stats.actions") + th {{a.id}} + th {{a.name}} + td([ngClass]="a.type === 'recv' ? 'text-danger' : 'text-success'") {{a.type}} + td {{a.countBin | number}} + td {{a.countStr | number}} + td {{a.average}} + td {{a.total}} + + .row.form-group(*ngIf="getStatsTable(s); let stats") + .col-12 + table.table.table-striped.table-hover.table-sm + thead + tr + th(*ngFor="let cell of stats[0]") {{cell}} + th + button.float-right.btn.btn-default.btn-xs((click)="resetStatsTable(s)") close + | + tbody + tr(*ngFor="let row of stats | slice:1") + th {{row[0]}} + td(*ngFor="let cell of row | slice:1") {{cell}} + td diff --git a/src/ts/components/admin/admin-state/admin-state.scss b/src/ts/components/admin/admin-state/admin-state.scss new file mode 100644 index 0000000..c742be2 --- /dev/null +++ b/src/ts/components/admin/admin-state/admin-state.scss @@ -0,0 +1,14 @@ +@import 'partials/variables'; + +.limit-length { + max-width: 350px; + overflow: hidden; + text-overflow: ellipsis; +} + +// .server-box { +// background: #272727; +// border-radius: $border-radius-lg; +// padding: $spacer / 2 $spacer; +// margin-bottom: $spacer / 2; +// } diff --git a/src/ts/components/admin/admin-state/admin-state.ts b/src/ts/components/admin/admin-state/admin-state.ts new file mode 100644 index 0000000..f14b01e --- /dev/null +++ b/src/ts/components/admin/admin-state/admin-state.ts @@ -0,0 +1,111 @@ +import { Component } from '@angular/core'; +import { + GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats, + UserCountStats, Stats, StatsTable +} from '../../../common/adminInterfaces'; +import { hasRole } from '../../../common/accountUtils'; +import { AdminModel } from '../../services/adminModel'; +import { faCog, faSlidersH } from '../../../client/icons'; + +@Component({ + selector: 'admin-state', + templateUrl: 'admin-state.pug', + styleUrls: ['admin-state.scss'], +}) +export class AdminState { + readonly cogIcon = faCog; + readonly optionsIcon = faSlidersH; + readonly loginOptions = LOGIN_SERVER_SETTINGS; + readonly options = SERVER_SETTINGS; + private stats = new Map(); + private statsTables = new Map(); + requestStats?: RequestStats[]; + userCounts?: UserCountStats[]; + showSettings: any = {}; + constructor(private model: AdminModel) { + } + get state() { + return this.model.state; + } + get loginServers() { + return this.model.state.loginServers; + } + get servers() { + return this.model.state.gameServers; + } + get isSuperadmin() { + return hasRole(this.model.account, 'superadmin'); + } + kickAll(server: GameServerState) { + return this.model.kickAll(server.id); + } + updateLoginSetting(key: string, value: boolean) { + return this.model.updateSettings({ [key]: value }); + } + updateLoginSettings(state: any) { + return this.model.updateSettings(state); + } + updateServerSetting(server: GameServerState, key: string, value: boolean) { + return this.model.updateGameServerSettings(server.id, { [key]: value }); + } + updateServerSettings(server: GameServerState, state: any) { + return this.model.updateGameServerSettings(server.id, state); + } + // request stats + fetchRequestStats() { + this.model.getRequestStats() + .then(stats => { + this.requestStats = stats && stats.requests; + this.userCounts = stats && stats.userCounts; + }); + } + resetRequestStats() { + this.requestStats = undefined; + this.userCounts = undefined; + } + // socket stats + fetchStats(server: GameServerState) { + return this.model.fetchServerStats(server.id) + .then(stats => stats && this.stats.set(server.id, stats)); + } + resetStats(server: GameServerState) { + this.stats.delete(server.id); + } + getStats(server: GameServerState) { + return this.stats.get(server.id); + } + // stats tables + fetchCountryStats(server: GameServerState) { + return this.fetchStatsTable(server, Stats.Country); + } + fetchSupportStats(server: GameServerState) { + return this.fetchStatsTable(server, Stats.Support); + } + fetchMapStats(server: GameServerState) { + return this.fetchStatsTable(server, Stats.Maps); + } + resetStatsTable(server: GameServerState) { + this.statsTables.delete(server.id); + } + getStatsTable(server: GameServerState) { + return this.statsTables.get(server.id); + } + private fetchStatsTable(server: GameServerState, stats: Stats) { + return this.model.fetchServerStatsTable(server.id, stats) + .then(stats => stats && this.statsTables.set(server.id, stats)); + } + // updates + notifyOfUpdate(server = '*') { + if (confirm('Are you sure?')) { + this.model.notifyUpdate(server); + } + } + shutdownServers(server = '*') { + if (confirm('Are you sure?')) { + this.model.shutdownServers(server); + } + } + resetUpdating(server = '*') { + this.model.resetUpdating(server); + } +} diff --git a/src/ts/components/admin/admin.module.ts b/src/ts/components/admin/admin.module.ts new file mode 100644 index 0000000..074ecb2 --- /dev/null +++ b/src/ts/components/admin/admin.module.ts @@ -0,0 +1,139 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { PopoverModule } from 'ngx-bootstrap/popover'; +import { PaginationModule } from 'ngx-bootstrap/pagination'; +import { ButtonsModule } from 'ngx-bootstrap/buttons'; +import { ModalModule } from 'ngx-bootstrap/modal'; + +import { SharedModule } from '../shared/shared.module'; + +import { KeysPipe } from './pipes/keysPipe'; +import { OrderByPipe } from './pipes/orderByPipe'; +import { TranslitPipe } from './pipes/translitPipe'; + +import { EventsTable } from './shared/events-table/events-table'; +import { AccountInfo } from './shared/account-info/account-info'; +import { AccountInfoRemote } from './shared/account-info-remote/account-info-remote'; +import { AccountStatus } from './shared/account-status/account-status'; +import { AccountTooltip } from './shared/account-tooltip/account-tooltip'; +import { OriginInfo } from './shared/origin-info/origin-info'; +import { OriginInfoRemote } from './shared/origin-info-remote/origin-info-remote'; +import { OriginListRemote } from './shared/origin-list-remote/origin-list-remote'; +import { PonyInfo } from './shared/pony-info/pony-info'; +import { PonyInfoRemote } from './shared/pony-info-remote/pony-info-remote'; +import { PonyListRemote } from './shared/pony-list-remote/pony-list-remote'; +import { AuthInfo } from './shared/auth-info/auth-info'; +import { AuthInfoRemote } from './shared/auth-info-remote/auth-info-remote'; +import { AuthInfoEdit } from './shared/auth-info-edit/auth-info-edit'; +import { AuthList } from './shared/auth-list/auth-list'; +import { AuthListRemote } from './shared/auth-list-remote/auth-list-remote'; +import { OnOffSwitch } from './shared/on-off-switch/on-off-switch'; +import { AdminChatLog } from './shared/admin-chat-log/admin-chat-log'; +import { BanIcon } from './shared/ban-icon/ban-icon'; +import { EmailList } from './shared/email-list/email-list'; +import { FromNow } from './shared/from-now'; +import { TimeField } from './shared/time-field/time-field'; +import { UAInfo } from './shared/ua-info/ua-info'; + +import { AdminState } from './admin-state/admin-state'; +import { AdminEvents } from './admin-events/admin-events'; +import { AdminOther } from './admin-other/admin-other'; +import { AdminAccounts } from './admin-accounts/admin-accounts'; +import { AdminAccountDetails } from './admin-account-details/admin-account-details'; +import { AdminPonies } from './admin-ponies/admin-ponies'; +import { AdminReports } from './admin-reports/admin-reports'; +import { AdminReportsPerf } from './admin-reports/admin-reports-perf/admin-reports-perf'; +import { AdminOrigins } from './admin-origins/admin-origins'; +import { AdminOriginDetails } from './admin-origin-details/admin-origin-details'; +import { AdminSignIn } from './admin-sign-in/admin-sign-in'; +import { AdminApp } from './admin'; + +import { ErrorReporter } from '../services/errorReporter'; + +export const routes: Routes = [ + { path: '', component: AdminState }, + { path: 'sign-in', component: AdminSignIn }, + { path: 'events', component: AdminEvents }, + { path: 'accounts', component: AdminAccounts }, + { path: 'accounts/:id', component: AdminAccountDetails }, + { path: 'ponies', component: AdminPonies }, + { path: 'origins', component: AdminOrigins }, + { path: 'origins/:ip/:country', component: AdminOriginDetails }, + { + path: 'reports', + component: AdminReports, + children: [ + { path: '', redirectTo: 'perf', pathMatch: 'full' }, + { path: 'perf', component: AdminReportsPerf }, + ], + }, + { path: 'other', component: AdminOther }, +]; + +@NgModule({ + imports: [ + BrowserModule, + RouterModule, + FormsModule, + HttpClientModule, + PopoverModule.forRoot(), + PaginationModule.forRoot(), + ButtonsModule.forRoot(), + ModalModule, + TooltipModule, + SharedModule, + RouterModule.forRoot(routes), + FontAwesomeModule, + ], + declarations: [ + KeysPipe, + OrderByPipe, + TranslitPipe, + EventsTable, + AccountInfo, + AccountInfoRemote, + AccountStatus, + AccountTooltip, + OriginInfo, + OriginInfoRemote, + OriginListRemote, + PonyInfo, + PonyInfoRemote, + PonyListRemote, + AuthInfo, + AuthInfoRemote, + AuthInfoEdit, + AuthList, + AuthListRemote, + OnOffSwitch, + AdminChatLog, + BanIcon, + EmailList, + FromNow, + TimeField, + UAInfo, + AdminState, + AdminEvents, + AdminOther, + AdminAccounts, + AdminAccountDetails, + AdminPonies, + AdminReports, + AdminReportsPerf, + AdminOrigins, + AdminOriginDetails, + AdminSignIn, + AdminApp, + ], + providers: [ + ErrorReporter, + ], + bootstrap: [AdminApp], +}) +export class AdminAppModule { +} diff --git a/src/ts/components/admin/admin.pug b/src/ts/components/admin/admin.pug new file mode 100644 index 0000000..f58d250 --- /dev/null +++ b/src/ts/components/admin/admin.pug @@ -0,0 +1,24 @@ +.container-fluid + + .d-flex.justify-content-between + .admin-initializing + div(*ngIf="loading") + fa-icon.mr-1([icon]="spinnerIcon" [spin]="true") + | {{loading}}... + + menu-bar([loading]="model.loading" [account]="model.account" (signOut)="signOut()") + menu-item(route="/" name="State ({{clients}})" [icon]="stateIcon") + menu-item(route="/events" name="Events ({{events}})" [icon]="eventsIcon") + menu-item(route="/accounts" name="Accounts ({{accounts}})" [icon]="accountsIcon") + menu-item(route="/ponies" name="Ponies ({{ponies}})" [icon]="poniesIcon") + menu-item(route="/origins" name="Origins ({{origins}})" [icon]="originsIcon") + menu-item(route="/reports" name="Reports" *ngIf="isSuperadmin" [icon]="reportsIcon") + menu-item(route="/other" name="Other" *ngIf="isSuperadmin" [icon]="otherIcon") + + .admin-view.mt-5 + router-outlet + +.admin-error(*ngIf="model.error") + .alert.alert-danger + button.close.ml-2((click)="model.error = null") × + | {{model.error}} diff --git a/src/ts/components/admin/admin.scss b/src/ts/components/admin/admin.scss new file mode 100644 index 0000000..62b2a7d --- /dev/null +++ b/src/ts/components/admin/admin.scss @@ -0,0 +1,14 @@ +@import 'partials/variables'; + +.admin-initializing { + margin: 15px; + font-size: larger; + color: $text-muted; +} + +.admin-error { + position: fixed; + left: 15px; + top: 10px; + width: 400px; +} diff --git a/src/ts/components/admin/admin.ts b/src/ts/components/admin/admin.ts new file mode 100644 index 0000000..c7de660 --- /dev/null +++ b/src/ts/components/admin/admin.ts @@ -0,0 +1,75 @@ +import { Component, HostListener } from '@angular/core'; +import { Router } from '@angular/router'; +import { TooltipConfig } from 'ngx-bootstrap/tooltip'; +import { PopoverConfig } from 'ngx-bootstrap/popover'; +import { hasRole } from '../../common/accountUtils'; +import { AdminModel } from '../services/adminModel'; +import { + faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog +} from '../../client/icons'; + +export function tooltipConfig() { + return Object.assign(new TooltipConfig(), { container: 'body' }); +} + +export function popoverConfig() { + return Object.assign(new PopoverConfig(), { container: 'body' }); +} + +@Component({ + selector: 'pony-town-app', + templateUrl: 'admin.pug', + styleUrls: ['admin.scss'], + providers: [ + { provide: TooltipConfig, useFactory: tooltipConfig }, + { provide: PopoverConfig, useFactory: popoverConfig }, + ] +}) +export class AdminApp { + readonly spinnerIcon = faSpinner; + readonly stateIcon = faSlidersH; + readonly eventsIcon = faExclamationCircle; + readonly accountsIcon = faUsers; + readonly poniesIcon = faHorseHead; + readonly originsIcon = faMapMarkerAlt; + readonly reportsIcon = faChartPie; + readonly otherIcon = faCog; + constructor(public model: AdminModel, private router: Router) { + } + get loading() { + if (!this.model.initialized) { + return 'Initializing'; + } else if (!this.model.connected) { + return 'Connecting'; + } else if (!this.model.loaded) { + return 'Loading'; + } else { + return ''; + } + } + get clients() { + return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0); + } + get events() { + return this.model.events.length; + } + get accounts() { + return this.model.counts.accounts; + } + get ponies() { + return this.model.counts.characters; + } + get origins() { + return this.model.counts.origins; + } + get isSuperadmin() { + return hasRole(this.model.account, 'superadmin'); + } + @HostListener('window:go-to-account', ['$event']) + goToAccount({ detail }: CustomEvent) { + this.router.navigate(['/accounts', detail]); + } + signOut() { + window.location.href = '/'; + } +} diff --git a/src/ts/components/admin/base-table.ts b/src/ts/components/admin/base-table.ts new file mode 100644 index 0000000..9930238 --- /dev/null +++ b/src/ts/components/admin/base-table.ts @@ -0,0 +1,100 @@ +import { debounce } from 'lodash'; + +export interface BaseTableState { + search: string; + currentPage: number; + sortedBy: string; + sortedAsc: boolean; +} + +export abstract class BaseTable { + itemsPerPage = 20; + sorted: T[] = []; + filtered: T[] = []; + filteredOnPage: T[] = []; + sortedBy = 'createdAt'; + sortedAsc = true; + private _search = ''; + private _currentPage = 1; + private execSearch = debounce(() => { + this.updateFiltered(); + this.onChange(); + }, 500); + get itemsFrom() { + return (this.currentPage - 1) * this.itemsPerPage; + } + get items(): T[] { + return []; + } + get search() { + return this._search; + } + set search(value: string) { + if (this._search !== value) { + this._search = value; + this.execSearch(); + } + } + get currentPage() { + return this._currentPage; + } + set currentPage(value: number) { + if (this._currentPage !== value) { + this._currentPage = value; + this.updatePage(); + this.onChange(); + } + } + sortBy(field: string) { + if (this.sortedBy === field) { + this.sortedAsc = !this.sortedAsc; + } else { + this.sortedBy = field; + this.sortedAsc = true; + } + + this.updateSorted(); + this.onChange(); + } + sortedClass(field: string) { + return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined; + } + protected updateItems() { + this.updateSorted(); + } + protected updateSorted() { + this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc); + this.updateFiltered(); + } + protected updateFiltered() { + this.filtered = this.filterItems(this.sorted, this.search); + this.updatePage(); + } + protected updatePage() { + this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage); + } + protected sortItems(items: T[], _by: string, _asc: boolean) { + return items; + } + protected filterItems(items: T[], _search: string) { + return items; + } + protected onChange() { + } + protected getState(): BaseTableState { + return { + search: this.search, + currentPage: this.currentPage, + sortedBy: this.sortedBy, + sortedAsc: this.sortedAsc, + }; + } + protected setState(state: BaseTableState | undefined) { + if (state) { + this._search = state.search; + this._currentPage = state.currentPage || 1; + this.sortedBy = state.sortedBy; + this.sortedAsc = state.sortedAsc; + } + } +} diff --git a/src/ts/components/admin/pipes/keysPipe.ts b/src/ts/components/admin/pipes/keysPipe.ts new file mode 100644 index 0000000..44af0d1 --- /dev/null +++ b/src/ts/components/admin/pipes/keysPipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'keys', +}) +export class KeysPipe implements PipeTransform { + transform(value: any) { + return value ? Object.keys(value) : value; + } +} diff --git a/src/ts/components/admin/pipes/orderByPipe.ts b/src/ts/components/admin/pipes/orderByPipe.ts new file mode 100644 index 0000000..ad03bd9 --- /dev/null +++ b/src/ts/components/admin/pipes/orderByPipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'orderBy', +}) +export class OrderByPipe implements PipeTransform { + transform(value: any[] | undefined, compare?: any) { + return value && value.slice().sort(compare); + } +} diff --git a/src/ts/components/admin/pipes/translitPipe.ts b/src/ts/components/admin/pipes/translitPipe.ts new file mode 100644 index 0000000..dabbcaa --- /dev/null +++ b/src/ts/components/admin/pipes/translitPipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { transliterate } from 'transliteration'; + +@Pipe({ + name: 'translit', +}) +export class TranslitPipe implements PipeTransform { + transform(value: string) { + if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value)) + return undefined; + + const translit = transliterate(value); + return translit !== value ? translit : undefined; + } +} diff --git a/src/ts/components/admin/shared/account-info-remote/account-info-remote.pug b/src/ts/components/admin/shared/account-info-remote/account-info-remote.pug new file mode 100644 index 0000000..9ee0c84 --- /dev/null +++ b/src/ts/components/admin/shared/account-info-remote/account-info-remote.pug @@ -0,0 +1,5 @@ +account-info( + *ngIf="!basic" [account]="account" [extendedAuths]="extendedAuths" [popoverPlacement]="popoverPlacement" + [showDuplicates]="showDuplicates") +span(*ngIf="basic") + | {{account?.name || '[none]'}} [{{account?._id | slice:-3}}] diff --git a/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts b/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts new file mode 100644 index 0000000..2a5f4ee --- /dev/null +++ b/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Account } from '../../../../common/adminInterfaces'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; + +@Component({ + selector: 'account-info-remote', + templateUrl: 'account-info-remote.pug', +}) +export class AccountInfoRemote implements OnDestroy { + @Input() extendedAuths = false; + @Input() popoverPlacement?: string; + @Input() showDuplicates = false; + @Input() basic = false; + account?: Account; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value) { + if (this._accountId !== value) { + this._accountId = value; + this.account = undefined; + this.updateSubscription(); + } + } + private updateSubscription() { + if (this.subscription) { + this.subscription.unsubscribe(); + this.subscription = undefined; + } + + if (this.accountId) { + this.subscription = this.model.accounts + .subscribe(this.accountId, account => this.account = account); + } + } +} diff --git a/src/ts/components/admin/shared/account-info/account-info.pug b/src/ts/components/admin/shared/account-info/account-info.pug new file mode 100644 index 0000000..4cdc08c --- /dev/null +++ b/src/ts/components/admin/shared/account-info/account-info.pug @@ -0,0 +1,179 @@ +ng-template(#notePopover) + textarea.form-control.note-editor( + cols="20" rows="15" agAutoFocus [(ngModel)]="note" (keydown.escape)="blur()" (blur)="blur()") + +ng-template(#accountTooltip) + account-tooltip([account]="account" [extendedAuths]="extendedAuths") + +ng-template(#noteTooltip) + small.text-muted + from-now([time]="account.noteUpdated") + div {{account.note}} + +ng-template(#alertTooltip) + .text-left + small.text-muted + | expires in + from-now.ml-1([time]="alert.expires") + div {{alert.message}} + +ng-template(#supporterTooltip) + div {{supporterTitle}} + .text-muted(*ngIf="account.supporterDeclinedSince as declined") + | declined: #[from-now([time]="declined")] ago | {{declined | date:'MMM d'}} + +ng-template(#flagsTooltip) + .text-left + div(*ngFor="let c of counters") + span.text-muted.mr-1 {{c.label}}: + span {{getCounter(c.name)}} + br(*ngIf="account.flags || account.supporter") + div(*ngFor="let f of flags") + span(*ngIf="hasFlag(f.value)") + fa-icon.mr-1([icon]="checkIcon") + | {{f.label}} + div(*ngFor="let f of supporterFlags") + span(*ngIf="hasSupporterFlag(f.value)") + fa-icon.mr-1([icon]="checkIcon") + | {{f.label}} + +ng-template(#alertModal) + .modal-header.d-none.d-sm-block + h4.modal-title(labelledBy=".modal") + | Account Alert + .modal-body.modal-checkboxes + .form-group + label Predefined: + .btn-group.ml-2 + button.btn.btn-sm.btn-default(*ngFor="let a of predefinedAlerts" (click)="alertMessage = a.message") + | {{a.name}} + .form-group + textarea.form-control([(ngModel)]="alertMessage" autofocus rows="4") + .form-group + label Expire in + .btn-group.ml-2(btnRadioGroup [(ngModel)]="alertExpire") + button.btn.btn-sm.btn-default(*ngFor="let e of alertExpires" [btnRadio]="e" [btnHighlight]="alertExpire === e") + | {{e.name}} + .modal-footer.d-flex + .flex-grow-1 + button.btn.btn-outline-secondary((click)="cancelAlert()") + | Cancel + button.btn.btn-outline-secondary.ml-2((click)="confirmAlert()") + | Set + +//- component + +.account-info(*ngIf="account" [class.outdated]="isInactive") + span.badge.badge-admin.badge-duplicates.mr-1( + *ngIf="showDuplicates && duplicates" title="Duplicates" [class.none]="duplicates.count === 0") + | {{duplicates.count}} + fa-icon.ml-1(*ngIf="duplicates.browserId" [icon]="duplicateBrowserIdIcon") + fa-icon.ml-1(*ngIf="duplicates.emails" [icon]="duplicateEmailIcon") + fa-icon.ml-1(*ngIf="duplicates.name" [icon]="duplicateNameIcon") + fa-icon.ml-1(*ngIf="duplicates.perma" [icon]="duplicatePermaIcon") + + a.account-info-name.mr-1( + [routerLink]="['/accounts', account._id]" + [tooltip]="accountTooltip" + [placement]="popoverPlacement || 'top'" + containerClass="tooltip-pre tooltip-account") + | {{account.name || '[none]'}} [{{account._id | slice:-3}}] + + span.badge.badge-admin.badge-none(*ngFor="let r of roles" title="role") + | {{r}} + + //-span.badge.badge-admin.badge-primary([title]="account.birthdate | date:'yyyy-MM-dd'") + | {{age}} + + span.badge.badge-admin.badge-supporter( + *ngIf="isPatreonOrSupporter" + [ngClass]="supporterClass" + [class.declined]="account.supporterDeclinedSince && supporterLevel" + [tooltip]="supporterTooltip") + fa-icon([icon]="supporterIcon" [fixedWidth]="true") + | {{supporterLevelString}} + + span.badge.badge-admin.badge-supporter.badge-success(*ngIf="account.totalPledged" title="lifetime support") + | ${{account.totalPledged}} + + span.badge.badge-admin.badge-danger(*ngIf="account.ignoresCount" title="ignored by") + | {{account.ignoresCount}} + + fa-icon.text-warning(*ngIf="isNew" [icon]="newIcon" [fixedWidth]="true" title="new account") + + a.icon-button.cursor-pointer( + [class.pointer-none]="isNoteOpen" + [popover]="notePopover" + [isOpen]="isNoteOpen" + [placement]="popoverPlacement || 'right'" + (onShown)="isNoteOpen = true" + (onHidden)="isNoteOpen = false" + containerClass="popover-notes") + fa-icon.text-muted( + [icon]="noteIcon" + [fixedWidth]="true" + [class.text-present]="account.note" + [tooltip]="isNoteOpen ? null : noteTooltip" + [isDisabled]="!account.note || isNoteOpen" + [placement]="popoverPlacement || 'top'" + containerClass="tooltip-notes") + span.account-info-note-indicator.pointer-none(*ngIf="hasDuplicateNote") d + + span.dropdown(dropdown autoClose="outsideClick") + a.icon-button.cursor-pointer.dropdown-toggle.no-arrow(dropdownToggle [tooltip]="flagsTooltip") + fa-icon([icon]="flagIcon" [fixedWidth]="true" [ngClass]="flagClass") + .dropdown-menu(*dropdownMenu [class.dropdown-menu-right]="!popoverPlacement") + a.dropdown-item(*ngFor="let f of flags" (click)="toggleFlag(f.value)") + fa-icon.mr-1([icon]="checkIcon" [fixedWidth]="true" [class.invisible]="!hasFlag(f.value)") + | {{f.label}} + .dropdown-divider + a.dropdown-item(*ngFor="let f of supporterFlags" (click)="toggleSupporterFlag(f.value)") + fa-icon.mr-1([icon]="checkIcon" [fixedWidth]="true" [class.invisible]="!hasSupporterFlag(f.value)") + | {{f.label}} + .dropdown-item + .d-flex.justify-content-between + div + fa-icon.mr-1([icon]="checkIcon" [fixedWidth]="true" [class.invisible]="!hasAnySupporter") + | supporter + .btn-group.supporter-buttons + button.btn.btn-xs.btn-info([class.active]="isSupporter(0)" (click)="setSupporter(0)") none + button.btn.btn-xs.btn-info([class.active]="isSupporter(1)" (click)="setSupporter(1)") 1 + button.btn.btn-xs.btn-info([class.active]="isSupporter(2)" (click)="setSupporter(2)") 2 + button.btn.btn-xs.btn-info([class.active]="isSupporter(3)" (click)="setSupporter(3)") 3 + .dropdown-item + .d-flex.justify-content-between + div + fa-icon.mr-1([icon]="checkIcon" [fixedWidth]="true" [class.invisible]="!hasPastSupporter") + | past supporter + .btn-group.supporter-buttons + button.btn.btn-xs.btn-info([class.active]="isForcePastSupporter" (click)="toggleForcePastSupporter()") force + button.btn.btn-xs.btn-info([class.active]="isIgnorePastSupporter" (click)="toggleIgnorePastSupporter()") ignore + .dropdown-divider + .dropdown-item.counter-item(*ngFor="let c of counters") + span.text-muted {{c.label}}: + span.counter-value {{getCounter(c.name)}} + button.btn.btn-xs.btn-info((click)="decrementCounter(c.name)") + fa-icon([icon]="minusIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-info((click)="incrementCounter(c.name)") + fa-icon([icon]="plusIcon" [fixedWidth]="true") + + span.dropdown(dropdown) + a.icon-button.cursor-pointer.dropdown-toggle.no-arrow(dropdownToggle [tooltip]="alert ? alertTooltip : undefined") + fa-icon.text-muted([icon]="cogIcon" [fixedWidth]="true" [class.text-present]="alert") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="kick()") + | kick + a.dropdown-item((click)="report()") + | report + .dropdown-divider + a.dropdown-item((click)="removeAlert()") + | remove alert + a.dropdown-item((click)="setAlert()") + | set alert + + a.icon-button.cursor-pointer(title="Teleport to" (click)="teleportTo()") + fa-icon.text-muted([icon]="teleportIcon" [fixedWidth]="true") + + ban-icon((toggle)="toggleBan('mute', $event)" [value]="account.mute" type="mute") + ban-icon.ml-ban((toggle)="toggleBan('shadow', $event)" [value]="account.shadow" type="shadow") + ban-icon.ml-ban((toggle)="toggleBan('ban', $event)" [value]="account.ban" type="ban") diff --git a/src/ts/components/admin/shared/account-info/account-info.scss b/src/ts/components/admin/shared/account-info/account-info.scss new file mode 100644 index 0000000..2ee5499 --- /dev/null +++ b/src/ts/components/admin/shared/account-info/account-info.scss @@ -0,0 +1,98 @@ +@import 'partials/variables'; + +.account-info { + display: flex; + align-items: center; + white-space: nowrap; + flex-wrap: wrap; +} + +.account-info-name { + .outdated & { + opacity: 0.6; + } + + .table & { + display: inline-block; + max-width: 130px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + white-space: nowrap; + } +} + +.account-info-note-indicator { + position: absolute; + left: 0; + top: 2px; + color: $body-bg; + font-weight: bold; + text-align: center; + display: inline-block; + width: 100%; + font-size: 10px; + text-shadow: none; +} + +.badge-supporter { + padding-left: 3px; + padding-right: 3px; + + &.declined { + border-right: solid 5px $warning; + } + + > i { + font-size: 10px; + } +} + +.badge-duplicates { + @include badge-variant(#bf6060); + + &.none { + @include badge-variant(#63a367); + } +} + +.icon-button { + position: relative; + + &:hover { + filter: drop-shadow(0 0 2px #000); + } +} + +.supporter-buttons .btn { + min-width: 25px; +} + +.counter-item { + margin-bottom: 5px; + + > .text-muted { + display: inline-block; + width: 80px; + } + + > .counter-value { + display: inline-block; + font-weight: bold; + width: 60px; + padding-left: 5px; + } + + > button { + margin-right: 5px; + width: 35px; + } +} + +.text-present, .text-muted.text-present { + color: #fff !important; +} + +.ml-ban { + margin-left: 2px; +} diff --git a/src/ts/components/admin/shared/account-info/account-info.ts b/src/ts/components/admin/shared/account-info/account-info.ts new file mode 100644 index 0000000..deddb48 --- /dev/null +++ b/src/ts/components/admin/shared/account-info/account-info.ts @@ -0,0 +1,292 @@ +import { Component, Input, OnChanges, SimpleChanges, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; +import { compact } from 'lodash'; +import { DAY, MINUTE, HOUR } from '../../../../common/constants'; +import { hasFlag, fromNow, toInt, setFlag } from '../../../../common/utils'; +import { + Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo +} from '../../../../common/adminInterfaces'; +import { supporterLevel, patreonSupporterLevel, getAge, isPastSupporter } from '../../../../common/adminUtils'; +import { AdminModel } from '../../../services/adminModel'; +import { AccountCounters } from '../../../../common/interfaces'; +import { + faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont, + faClock, faBan, faMapMarkerAlt +} from '../../../../client/icons'; + +const EMPTY_ROLES: string[] = []; +const oldTime = fromNow(-14 * DAY).getTime(); +const newTime = fromNow(-DAY).getTime(); + +const accountDuplicatesIntervalTime = 10 * MINUTE; +const accountDuplicates = new Map(); + +const predefinedAlerts = [ + { + name: 'erp:flagged', + message: `Your account has been flagged for inappropriate bahavior on PG rated server. ` + + `Continuing that behavior may result in permanent ban.` + }, + { + name: 'erp:timeout', + message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. ` + + `Continuing that behavior may result in permanent ban.` + }, + { + name: 'dups', + message: `Your account has been flagged for making multiple accounts. ` + + `Continuing that behavior may result in permanent ban.` + }, + { + name: 'under', + message: `Your account has been reported for being underage, please do NOT play on 18+ server. ` + + `Continuing that may result in permanent ban.` + }, +]; + +const alertExpires = [ + { name: '1h', length: HOUR }, + { name: '5h', length: 5 * HOUR }, + { name: '12h', length: 12 * HOUR }, + { name: '1d', length: DAY }, + { name: '2d', length: 2 * DAY }, + { name: '5d', length: 5 * DAY }, + { name: '7d', length: 7 * DAY }, + { name: '2w', length: 14 * DAY }, +]; + +@Component({ + selector: 'account-info', + templateUrl: 'account-info.pug', + styleUrls: ['account-info.scss'], +}) +export class AccountInfo implements OnInit, OnChanges { + readonly counters = accountCounters; + readonly flags = accountFlags; + readonly supporterFlags = supporterFlags; + readonly cogIcon = faCog; + readonly minusIcon = faMinus; + readonly plusIcon = faPlus; + readonly checkIcon = faCheck; + readonly flagIcon = faFlag; + readonly noteIcon = faStickyNote; + readonly newIcon = faCertificate; + readonly duplicateBrowserIdIcon = faIdBadge; + readonly duplicateEmailIcon = faEnvelope; + readonly duplicateNameIcon = faFont; + readonly duplicatePermaIcon = faBan; + readonly teleportIcon = faMapMarkerAlt; + predefinedAlerts = predefinedAlerts; + alertExpires = alertExpires; + alertExpire = alertExpires[3]; + alertMessage = ''; + @Input() account!: Account; + @Input() extendedAuths = false; + @Input() popoverPlacement?: string; + @Input() showDuplicates = false; + @ViewChild('alertModal', { static: true }) alertModal!: TemplateRef; + note?: string; + duplicates?: DuplicatesInfo; + private alertModalRef?: BsModalRef; + private _isNoteOpen = false; + constructor(private model: AdminModel, private modalService: BsModalService) { + } + ngOnInit() { + this.updateDuplicates(); + } + ngOnChanges(changes: SimpleChanges) { + if (changes.account) { + this.updateDuplicates(); + } + } + get age() { + return this.account.birthdate ? getAge(this.account.birthdate) : '-'; + } + get alert() { + const alert = this.account.alert; + return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined; + } + get isNew() { + return this.account.createdAt && this.account.createdAt.getTime() > newTime; + } + get isNoteOpen() { + return this._isNoteOpen; + } + set isNoteOpen(value: boolean) { + if (this._isNoteOpen !== value) { + this._isNoteOpen = value; + + if (value) { + this.note = this.account.note; + } + } + } + get isInactive() { + return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime; + } + get roles() { + return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES; + } + get flagClass() { + const counters = this.account.counters; + + if (this.account.flags) { + return 'text-banned'; + } else if (!counters && !this.account.supporter) { + return 'text-muted'; + } else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) { + return 'text-alert'; + } else { + return 'text-present'; + } + } + get hasDuplicateNote() { + return this.account.note && /duplicate/i.test(this.account.note); + } + hasFlag(value: AccountFlags) { + return hasFlag(this.account.flags, value); + } + toggleFlag(value: AccountFlags) { + this.model.setAccountFlags(this.account._id, this.account.flags ^ value); + } + toggleBan(field: keyof BannedMuted, value: number) { + this.model.setAccountBanField(this.account._id, field, value); + } + kick() { + this.model.kick(this.account._id); + } + report() { + this.model.report(this.account._id); + } + blur() { + this.model.setNote(this.account._id, this.note || ''); + this.isNoteOpen = false; + } + decrementCounter(name: keyof AccountCounters) { + if (this.getCounter(name) > 0) { + this.setCounter(name, this.getCounter(name) - 1); + } + } + incrementCounter(name: keyof AccountCounters) { + this.setCounter(name, this.getCounter(name) + 1); + } + getCounter(name: keyof AccountCounters) { + const counters = this.account.counters; + return toInt(counters && counters[name]); + } + setCounter(name: keyof AccountCounters, value: number) { + const counters = this.account.counters || (this.account.counters = {}); + counters[name] = value; + this.model.setAccountCounter(this.account._id, name, value); + } + removeAlert() { + this.model.setAlert(this.account._id, '', 0); + } + setAlert() { + this.alertMessage = this.alert ? this.alert.message : ''; + this.alertExpire = this.alertExpires[2]; + this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true }); + } + cancelAlert() { + this.alertModalRef && this.alertModalRef.hide(); + this.alertModalRef = undefined; + } + confirmAlert() { + this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length); + this.cancelAlert(); + } + private updateDuplicates() { + const account = this.account; + + if (this.showDuplicates && account) { + const cached = accountDuplicates.get(account._id); + const threshold = fromNow(-accountDuplicatesIntervalTime); + + if (cached && cached.generatedAt > threshold.getTime()) { + this.duplicates = cached; + } else { + this.model.getAllDuplicatesQuickInfo(account._id) + .then(duplicates => { + if (duplicates) { + accountDuplicates.set(account._id, duplicates); + this.duplicates = duplicates; + } + }); + } + } + } + teleportTo() { + this.model.teleportTo(this.account._id); + } + // supporters + get isPatreonOrSupporter() { + return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince); + } + get supporterClass() { + return supporterLevel(this.account) ? 'badge-success' : 'badge-warning'; + } + get supporterTitle() { + const supporter = this.account.supporter!; + const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0; + const patreonSupporter = patreonSupporterLevel(this.account); + const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon); + const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter); + + return compact([ + flagSupporter && 'flags', + patreonSupporter && `patreon`, + ignorePatreon && 'ignore', + !patreonSupporter && this.account.supporterDeclinedSince && 'declined', + pastSupporter && 'past', + ]).join(', '); + } + get supporterIcon() { + const hasPatreon = patreonSupporterLevel(this.account); + const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon); + const hasDeclined = !!this.account.supporterDeclinedSince; + return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock); + } + get hasAnySupporter() { + return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0; + } + get hasPastSupporter() { + return hasFlag(this.account.supporter, SupporterFlags.PastSupporter); + } + get supporterLevel() { + return supporterLevel(this.account); + } + get supporterLevelString() { + const level = supporterLevel(this.account); + return level ? level : (isPastSupporter(this.account) ? 'P' : ''); + } + isSupporter(level: number) { + return (this.account.supporter! & SupporterFlags.SupporterMask) === level; + } + setSupporter(level: number) { + const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level; + this.model.setSupporterFlags(this.account._id, supporter); + } + hasSupporterFlag(value: SupporterFlags) { + return hasFlag(this.account.supporter, value); + } + toggleSupporterFlag(value: SupporterFlags) { + this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value); + } + // past supporter + get isForcePastSupporter() { + return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter); + } + get isIgnorePastSupporter() { + return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter); + } + toggleForcePastSupporter() { + const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter); + const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has); + this.model.setSupporterFlags(this.account._id, supporter); + } + toggleIgnorePastSupporter() { + const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter); + const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has); + this.model.setSupporterFlags(this.account._id, supporter); + } +} diff --git a/src/ts/components/admin/shared/account-status/account-status.pug b/src/ts/components/admin/shared/account-status/account-status.pug new file mode 100644 index 0000000..de40457 --- /dev/null +++ b/src/ts/components/admin/shared/account-status/account-status.pug @@ -0,0 +1,10 @@ +.text-muted(*ngIf="!status") + | checking... +.text-unsafe(*ngIf="status && !status.length") + | offline +.text-safe(*ngFor="let s of status") + | [{{s.server}}] [{{s.map}}] {{s.character}} ({{s.x}}, {{s.y}}) ({{s.duration}}) + ua-info.ml-1([userAgent]="s.userAgent") + fa-icon.ml-1(*ngIf="s.incognito" [icon]="incognitoIcon") + .text-muted(*ngIf="verbose") + small {{s.userAgent}} diff --git a/src/ts/components/admin/shared/account-status/account-status.ts b/src/ts/components/admin/shared/account-status/account-status.ts new file mode 100644 index 0000000..3a2b0a0 --- /dev/null +++ b/src/ts/components/admin/shared/account-status/account-status.ts @@ -0,0 +1,28 @@ +import { Component, OnInit, Input, OnChanges } from '@angular/core'; +import { AdminModel } from '../../../services/adminModel'; +import { Account, AccountStatus as IAccountStatus } from '../../../../common/adminInterfaces'; +import { faUserSecret } from '../../../../client/icons'; + +@Component({ + selector: 'account-status', + templateUrl: 'account-status.pug', +}) +export class AccountStatus implements OnInit, OnChanges { + readonly incognitoIcon = faUserSecret; + @Input() account!: Account; + @Input() verbose = false; + status: IAccountStatus[] | undefined = undefined; + constructor(private model: AdminModel) { + } + ngOnInit() { + this.refresh(); + } + ngOnChanges() { + this.status = undefined; + this.refresh(); + } + refresh() { + this.model.getAccountStatus(this.account._id) + .then(status => this.status = status); + } +} diff --git a/src/ts/components/admin/shared/account-tooltip/account-tooltip.pug b/src/ts/components/admin/shared/account-tooltip/account-tooltip.pug new file mode 100644 index 0000000..76aefe5 --- /dev/null +++ b/src/ts/components/admin/shared/account-tooltip/account-tooltip.pug @@ -0,0 +1,27 @@ +.text-left + div + | {{account.name || '[none]'}} [{{account._id | slice:-3}}] + auth-list-remote.ml-1(*ngIf="!extendedAuths" [accountId]="account._id") + .text-muted + em {{account.name | translit}} + div(*ngIf="extendedAuths") + auth-list-remote([accountId]="account._id" [extended]="true") + .text-muted(*ngFor="let e of account.emails | slice:0:3") + | {{e}} + .text-muted(*ngIf="account.emails && account.emails.length > 3") + | ({{account.emails.length - 3}} more) + div + span.text-muted created: + from-now.ml-1([time]="account.createdAt") + div + span.text-muted last visit: + from-now.ml-1([time]="account.lastVisit") + div + span.text-muted ponies: + span.ml-1 {{account.characterCount}} + div + span.text-muted age: + span.ml-1([class.text-strike]="account.birthyear") {{age}} + span.ml-1 {{forceAge}} + div + account-status([account]="account") diff --git a/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts b/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts new file mode 100644 index 0000000..7fe8d40 --- /dev/null +++ b/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; +import { Account } from '../../../../common/adminInterfaces'; +import { getAge } from '../../../../common/adminUtils'; + +const year = (new Date()).getFullYear(); + +@Component({ + selector: 'account-tooltip', + templateUrl: 'account-tooltip.pug', +}) +export class AccountTooltip { + @Input() account!: Account; + @Input() extendedAuths = false; + get age() { + return this.account.birthdate ? getAge(this.account.birthdate) : '-'; + } + get forceAge() { + return this.account.birthyear ? (year - this.account.birthyear) : ''; + } +} diff --git a/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.pug b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.pug new file mode 100644 index 0000000..c531f43 --- /dev/null +++ b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.pug @@ -0,0 +1,38 @@ +div(*ngIf="open || !canClose") + .d-flex.justify-content-between.align-items-center + h3 + | {{title}} + span.text-muted.ml-1 + | ({{date ? date.label : 'none'}}) + span.text-muted.ml-1(*ngIf="loading") + fa-icon([icon]="spinnerIcon" [spin]="true") + .form-inline + ng-content + form.d-inline-block((submit)="searchChat(searchInput.value)") + input.form-control.form-control-sm.ml-1(#searchInput type="search" placeholder="search") + button.btn.btn-sm.btn-default.ml-1(type="submit" title="Search whole chat for phrase") + fa-icon([icon]="searchIcon" [fixedWidth]="true" size="lg") + button.btn.btn-sm.btn-default.ml-1((click)="openLog()" title="Open log in plain text format") + fa-icon([icon]="fileIcon" [fixedWidth]="true" size="lg") + button.btn.btn-sm.ml-1( + [(ngModel)]="autoRefresh" btnCheckbox [btnHighlight]="autoRefresh" title="Auto-refresh every 10 seconds") + fa-icon.mr-1([icon]="syncIcon" [fixedWidth]="true" size="lg") + | auto + .btn-group.btn-group-sm.ml-1 + button.btn.btn-sm.btn-default((click)="prev()" title="Load previous day") + fa-icon([icon]="chevronLeftIcon" [fixedWidth]="true" size="lg") + button.btn.btn-sm.btn-default((click)="next()" title="Load next day") + fa-icon([icon]="chevronRightIcon" [fixedWidth]="true" size="lg") + .btn-group.btn-group-sm.dropup.ml-1(dropdown) + button.btn.btn-sm.btn-default((click)="showDate(today)") + | Load todays chat + button.btn.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item(*ngFor="let d of dates" (click)="showDate(d)") + | {{d.label}} + button.btn.btn-sm.btn-default.ml-1(*ngIf="canClose" (click)="close()") + fa-icon([icon]="closeIcon" [fixedWidth]="true") + .mb-1(*ngIf="accounts.length") + a.badge.badge-none.mr-1(*ngFor="let a of accounts; let i = index" (click)="removeAccount(i)" title="Remove") + | [{{i + 1}}] {{a.name}} + .well.pre.chatlog diff --git a/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.scss b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.scss new file mode 100644 index 0000000..87cd1fb --- /dev/null +++ b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.scss @@ -0,0 +1 @@ +// @import 'partials/variables'; diff --git a/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts new file mode 100644 index 0000000..5e179bd --- /dev/null +++ b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts @@ -0,0 +1,182 @@ +import { Component, Input, OnDestroy, ElementRef } from '@angular/core'; +import * as moment from 'moment'; +import { AdminModel } from '../../../services/adminModel'; +import { Account } from '../../../../common/adminInterfaces'; +import { ChatDate, createChatDate, createDateRange, replaceSwears } from '../../../../common/adminUtils'; +import { faSearch, faSpinner, faSync, faFileAlt, faTimes, faChevronLeft, faChevronRight } from '../../../../client/icons'; +import { removeAllNodes, appendAllNodes, showTextInNewTab } from '../../../../client/htmlUtils'; +import { includes } from '../../../../common/utils'; + +@Component({ + selector: 'admin-chat-log', + templateUrl: 'admin-chat-log.pug', + styleUrls: ['admin-chat-log.scss'], +}) +export class AdminChatLog implements OnDestroy { + readonly searchIcon = faSearch; + readonly spinnerIcon = faSpinner; + readonly syncIcon = faSync; + readonly fileIcon = faFileAlt; + readonly closeIcon = faTimes; + readonly chevronLeftIcon = faChevronLeft; + readonly chevronRightIcon = faChevronRight; + @Input() canClose = true; + accounts: Account[] = []; + search?: string; + open = false; + today: ChatDate = createChatDate(moment()); + dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)]; + date?: ChatDate; + chatRaw?: string; + loading = false; + private _account?: Account; + private refreshInterval?: any; + constructor(private model: AdminModel, private element: ElementRef) { + } + get autoRefresh() { + return !!this.refreshInterval; + } + set autoRefresh(value: boolean) { + if (value) { + this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000); + } else { + this.stopInterval(); + } + } + get title() { + return this.search || (this.account && this.account.name) || 'Chat'; + } + get account() { + return this._account; + } + @Input() set account(value) { + const theSame = this._account === value || (value && this._account && value._id === this._account._id); + this._account = value; + + if (!theSame) { + this.date = undefined; + this.setChatlogElements([]); + } + } + ngOnDestroy() { + this.close(); + } + show(account?: Account, date?: ChatDate) { + this.search = undefined; + this.account = account; + this.date = date || this.today; + this.open = true; + this.accounts = []; + this.refresh(); + } + add(account: Account) { + if (!this.account) { + this.show(account); + } else if (account !== this.account && !includes(this.accounts, account)) { + this.date = this.date || this.today; + this.accounts.push(account); + this.refresh(); + } + } + removeAccount(index: number) { + this.accounts.splice(index, 1); + this.refresh(); + } + showDate(date: ChatDate) { + this.date = date; + this.refresh(); + } + prev() { + this.switchDate(-1); + } + next() { + this.switchDate(1); + } + all() { + this.date = this.dates[0]; + this.refresh(); + } + close() { + this.account = undefined; + this.date = undefined; + this.open = false; + this.setChatlogElements([]); + this.stopInterval(); + } + searchChat(search: string | undefined) { + this.search = search; + this.refresh(); + } + refresh() { + const date = this.date && this.date.value; + + if (this.account) { + const accounts = [this.account._id, ...this.accounts.map(a => a._id)]; + this.handleChat(this.model.accountsFormattedChat(accounts, date)); + } else if (this.search) { + this.handleChat(this.model.searchFormattedChat(this.search, date)); + } + } + openLog() { + showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`); + } + private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) { + this.loading = true; + + promise + .then(({ raw, html }) => { + this.chatRaw = raw; + this.setChatlogElements(html); + }) + .finally(() => this.loading = false); + } + private switchDate(days: number) { + const validDate = this.date && this.date.value !== 'all'; + this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today; + this.refresh(); + } + private stopInterval() { + clearInterval(this.refreshInterval); + this.refreshInterval = undefined; + } + private getChatlogElement() { + return (this.element.nativeElement as HTMLElement).querySelector('.chatlog'); + } + private setChatlogElements(elements: HTMLElement[]) { + const element = this.getChatlogElement(); + + if (element) { + removeAllNodes(element); + appendAllNodes(element, elements); + this.nodesToProcess = [ + ...Array.from(element.getElementsByClassName('name')), + ...Array.from(element.getElementsByClassName('message')), + ] as HTMLElement[]; + this.atNode = 0; + this.processNodes(); + } + } + private processNodes() { + const processStep = 50; + const nodes = this.nodesToProcess; + + cancelIdleCallback(this.processIdle); + + if (nodes && this.atNode < nodes.length) { + let i = 0; + + while (i < processStep && (i + this.atNode) < nodes.length) { + replaceSwears(nodes[i + this.atNode]); + i++; + } + + this.atNode += i; + this.processIdle = requestIdleCallback(() => this.processNodes()); + } else { + this.nodesToProcess = undefined; + } + } + private nodesToProcess?: HTMLElement[]; + private atNode = 0; + private processIdle = 0; +} diff --git a/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.pug b/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.pug new file mode 100644 index 0000000..c4dd376 --- /dev/null +++ b/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.pug @@ -0,0 +1,22 @@ +.d-flex.align-items-center.mb-1 + auth-info.flex-grow-1([auth]="auth" [showName]="true") + small.text-muted + from-now.mr-1([time]="auth?.lastUsed") + .btn-group.dropdown(dropdown) + button.btn.btn-xs.btn-default(dropdownToggle title="Assign to another account") + fa-icon([icon]="assignIcon" [fixedWidth]="true") + .dropdown-menu(*dropdownMenu) + button.dropdown-item(*ngFor="let d of duplicates" (click)="assignTo(d.account)") + span.text-muted assign to + account-info-remote.ml-1([accountId]="d.account" [basic]="true") + .btn-group.ml-1 + button.btn.btn-xs.btn-default((click)="showAuthData(auth)" title="Show more data") + fa-icon([icon]="infoIcon" [fixedWidth]="true") + button.btn.btn-xs((click)="toggleAuthBanned(auth)" [btnHighlight]="auth?.banned || false" + title="Toggle auth locked (can't be restored by user)") + fa-icon([icon]="lockIcon" [fixedWidth]="true") + button.btn.btn-xs((click)="toggleAuthDisabled(auth)" [btnHighlight]="auth?.disabled || false" + title="Toggle auth disabled (can be restored by user)") + fa-icon([icon]="eyeSlashIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default((click)="removeAuth(auth)" title="Remove auth") + fa-icon([icon]="trashIcon" [fixedWidth]="true") diff --git a/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts b/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts new file mode 100644 index 0000000..152ccd1 --- /dev/null +++ b/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts @@ -0,0 +1,64 @@ +import { Component, OnDestroy, Input } from '@angular/core'; +import { Auth, DuplicateResult } from '../../../../common/adminInterfaces'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { faInfo, faLock, faTrash, faEyeSlash, faArrowRight } from '../../../../client/icons'; + +@Component({ + selector: 'auth-info-edit', + templateUrl: 'auth-info-edit.pug', +}) +export class AuthInfoEdit implements OnDestroy { + readonly assignIcon = faArrowRight; + readonly infoIcon = faInfo; + readonly lockIcon = faLock; + readonly trashIcon = faTrash; + readonly eyeSlashIcon = faEyeSlash; + @Input() duplicates?: DuplicateResult[]; + @Input() showName = false; + auth?: Auth; + private _authId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get authId() { + return this._authId; + } + @Input() set authId(value: string | undefined) { + if (this.authId !== value) { + this._authId = value; + this.auth = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + removeAuth(auth: Auth | undefined) { + if (auth && confirm('Are you sure?')) { + this.model.removeAuth(auth._id); + } + } + showAuthData(auth: Auth | undefined) { + if (auth) { + this.model.getAuth(auth._id) + .then(x => console.log(x)); + } + } + toggleAuthDisabled(auth: Auth | undefined) { + if (auth) { + this.model.updateAuth(auth._id, { disabled: !auth.disabled }); + } + } + toggleAuthBanned(auth: Auth | undefined) { + if (auth) { + this.model.updateAuth(auth._id, { banned: !auth.banned }); + } + } + assignTo(accountId: string) { + if (this.authId) { + this.model.assignAuth(this.authId, accountId); + } + } +} diff --git a/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.pug b/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.pug new file mode 100644 index 0000000..f6f7be5 --- /dev/null +++ b/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.pug @@ -0,0 +1 @@ +auth-info([auth]="auth" [showName]="showName") diff --git a/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts b/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts new file mode 100644 index 0000000..b01b6d6 --- /dev/null +++ b/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts @@ -0,0 +1,31 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { Auth } from '../../../../common/adminInterfaces'; + +@Component({ + selector: 'auth-info-remote', + templateUrl: 'auth-info-remote.pug', +}) +export class AuthInfoRemote implements OnDestroy { + @Input() showName = false; + auth?: Auth; + private _authId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get authId() { + return this._authId; + } + @Input() set authId(value: string | undefined) { + if (this.authId !== value) { + this._authId = value; + this.auth = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } +} diff --git a/src/ts/components/admin/shared/auth-info/auth-info.pug b/src/ts/components/admin/shared/auth-info/auth-info.pug new file mode 100644 index 0000000..a20b854 --- /dev/null +++ b/src/ts/components/admin/shared/auth-info/auth-info.pug @@ -0,0 +1,13 @@ +a(*ngIf="auth?.url" [href]="auth?.url" target="_blank" rel="noopener noreferrer" [title]="name") + fa-icon([icon]="icon" [fixedWidth]="true") +span(*ngIf="!auth?.url" [title]="name") + fa-icon([icon]="icon" [fixedWidth]="true") +span(*ngIf="showName") + span.ml-1(*ngIf="name") + | {{name}} + em.text-muted.ml-1 + | {{name | translit}} + span.text-muted.ml-1(*ngIf="!name") + | <no name> + span.badge.badge-success.ml-1(*ngIf="pledged") + | ${{pledged}} diff --git a/src/ts/components/admin/shared/auth-info/auth-info.scss b/src/ts/components/admin/shared/auth-info/auth-info.scss new file mode 100644 index 0000000..d71bb8f --- /dev/null +++ b/src/ts/components/admin/shared/auth-info/auth-info.scss @@ -0,0 +1,4 @@ +fa-icon { + font-size: 13px; + margin: 0 1px; +} diff --git a/src/ts/components/admin/shared/auth-info/auth-info.ts b/src/ts/components/admin/shared/auth-info/auth-info.ts new file mode 100644 index 0000000..4f1b127 --- /dev/null +++ b/src/ts/components/admin/shared/auth-info/auth-info.ts @@ -0,0 +1,28 @@ +import { Component, Input } from '@angular/core'; +import { Auth } from '../../../../common/adminInterfaces'; +import { oauthIcons, faGlobe } from '../../../../client/icons'; + +@Component({ + selector: 'auth-info', + templateUrl: 'auth-info.pug', + styleUrls: ['auth-info.scss'], + host: { + '[class.deleted]': 'deleted', + }, +}) +export class AuthInfo { + @Input() auth?: Auth; + @Input() showName = false; + get deleted(): boolean { + return !!(this.auth && (this.auth.disabled || this.auth.banned)); + } + get name(): string { + return this.auth && this.auth.name || ''; + } + get icon() { + return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe; + } + get pledged() { + return (this.auth && this.auth.pledged || 0) / 100; + } +} diff --git a/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.pug b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.pug new file mode 100644 index 0000000..dcba2bc --- /dev/null +++ b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.pug @@ -0,0 +1,5 @@ +span([class.extended]="extended") + span.text-muted(*ngIf="loading") ... + auth-info-remote(*ngFor="let a of auths | slice:0:limit" [authId]="a" [showName]="extended") + a.text-muted.ml-1.text-nowrap(*ngIf="auths.length > limit" (click)="limit = 9999") + small ({{auths.length - limit}} more) diff --git a/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.scss b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.scss new file mode 100644 index 0000000..2905285 --- /dev/null +++ b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.scss @@ -0,0 +1,3 @@ +.extended > auth-info-remote { + display: block; +} diff --git a/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts new file mode 100644 index 0000000..05f9c87 --- /dev/null +++ b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts @@ -0,0 +1,37 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; + +@Component({ + selector: 'auth-list-remote', + templateUrl: 'auth-list-remote.pug', + styleUrls: ['auth-list-remote.scss'], +}) +export class AuthListRemote implements OnDestroy { + @Input() limit = 6; + @Input() extended = false; + auths: string[] = []; + loading = false; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value: string | undefined) { + if (this.accountId !== value) { + this._accountId = value; + this.auths = []; + this.loading = true; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.accountAuths.subscribe(value, auths => { + this.auths = auths || []; + this.loading = false; + }) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } +} diff --git a/src/ts/components/admin/shared/auth-list/auth-list.pug b/src/ts/components/admin/shared/auth-list/auth-list.pug new file mode 100644 index 0000000..ca4aa70 --- /dev/null +++ b/src/ts/components/admin/shared/auth-list/auth-list.pug @@ -0,0 +1,4 @@ +span([class.extended]="extended") + auth-info(*ngFor="let a of fixedAuths | slice:0:limit" [auth]="a" [showName]="extended") + a.text-muted.ml-1.text-nowrap(*ngIf="fixedAuths.length > limit" (click)="limit = 9999") + small ({{fixedAuths.length - limit}} more) diff --git a/src/ts/components/admin/shared/auth-list/auth-list.scss b/src/ts/components/admin/shared/auth-list/auth-list.scss new file mode 100644 index 0000000..286fe48 --- /dev/null +++ b/src/ts/components/admin/shared/auth-list/auth-list.scss @@ -0,0 +1,7 @@ +.extended { + display: block; + + auth-info, span { + display: block; + } +} diff --git a/src/ts/components/admin/shared/auth-list/auth-list.ts b/src/ts/components/admin/shared/auth-list/auth-list.ts new file mode 100644 index 0000000..f7c0385 --- /dev/null +++ b/src/ts/components/admin/shared/auth-list/auth-list.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { Auth } from '../../../../common/adminInterfaces'; + +@Component({ + selector: 'auth-list', + templateUrl: 'auth-list.pug', + styleUrls: ['auth-list.scss'], + host: { + '[class.extended]': 'extended', + }, +}) +export class AuthList { + @Input() limit = 6; + @Input() extended = false; + @Input() auths?: Auth[]; + get fixedAuths() { + return this.auths || []; + } +} diff --git a/src/ts/components/admin/shared/ban-icon/ban-icon.pug b/src/ts/components/admin/shared/ban-icon/ban-icon.pug new file mode 100644 index 0000000..d7f821c --- /dev/null +++ b/src/ts/components/admin/shared/ban-icon/ban-icon.pug @@ -0,0 +1,16 @@ +ng-template(#timeoutTooltip) + from-now([time]="value") + +.dropdown(dropdown) + a.icon-button.cursor-pointer(dropdownToggle [title]="type" [tooltip]="isTimedOut ? timeoutTooltip : null") + fa-icon([icon]="icon" [fixedWidth]="true" [ngClass]="className") + fa-icon.text-alert.ban-icon-timer(*ngIf="isTimedOut" [icon]="clockIcon" [fixedWidth]="true") + + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="clear()") + | clear #[b {{type}}] + a.dropdown-item((click)="perma()") + | perma #[b {{type}}] + .dropdown-divider + a.dropdown-item(*ngFor="let t of timeouts" (click)="timeout(t.value)") + | {{t.label}} diff --git a/src/ts/components/admin/shared/ban-icon/ban-icon.scss b/src/ts/components/admin/shared/ban-icon/ban-icon.scss new file mode 100644 index 0000000..5f019f4 --- /dev/null +++ b/src/ts/components/admin/shared/ban-icon/ban-icon.scss @@ -0,0 +1,26 @@ +.icon-button { + position: relative; + + &:hover { + filter: drop-shadow(0 0 2px #000); + } +} + +.ban-icon-timer { + position: absolute; + right: -2px; + bottom: -2px; + font-size: 10px; +} + +i.ban-icon-timer { + text-shadow: 0 0 1px #000; +} + +fa-icon.ban-icon-timer { + filter: drop-shadow(0 0 1px #000); +} + +i, fa-icon { + font-size: 13px; +} diff --git a/src/ts/components/admin/shared/ban-icon/ban-icon.ts b/src/ts/components/admin/shared/ban-icon/ban-icon.ts new file mode 100644 index 0000000..6529851 --- /dev/null +++ b/src/ts/components/admin/shared/ban-icon/ban-icon.ts @@ -0,0 +1,77 @@ +import { + Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone +} from '@angular/core'; +import { TIMEOUTS } from '../../../../common/constants'; +import { BannedMuted } from '../../../../common/adminInterfaces'; +import { IntervalUpdateService } from '../../../services/intervalUpdateService'; +import { faClock, faMicrophoneSlash, faEyeSlash, faBan } from '../../../../client/icons'; + +const ICONS = { + mute: faMicrophoneSlash, + shadow: faEyeSlash, + ban: faBan, +}; + +@Component({ + selector: 'ban-icon', + templateUrl: 'ban-icon.pug', + styleUrls: ['ban-icon.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BanIcon implements OnInit, OnDestroy, OnChanges { + readonly timeouts = TIMEOUTS; + readonly clockIcon = faClock; + @Input() type: keyof BannedMuted = 'ban'; + @Input() value = 0; + @Output() toggle = new EventEmitter(); + get icon() { + return ICONS[this.type] || ICONS.ban; + } + get isPerma() { + return this.value === -1; + } + get isTimedOut() { + return this.value > Date.now(); + } + get className() { + if (this.isPerma) { + return 'text-banned'; + } else if (this.isTimedOut) { + return 'text-alert'; + } else { + return 'text-muted'; + } + } + private timedOut = false; + private toggleUpdate: (on: boolean) => void; + constructor(zone: NgZone, updateService: IntervalUpdateService) { + this.toggleUpdate = updateService.toggle(() => { + if (this.timedOut !== this.isTimedOut) { + zone.run(() => this.timedOut = this.isTimedOut); + this.toggleUpdate(this.isTimedOut); + } + }); + } + ngOnInit() { + this.toggleUpdate(this.isTimedOut); + } + ngOnChanges() { + this.toggleUpdate(this.isTimedOut); + } + ngOnDestroy() { + this.toggleUpdate(false); + } + clear() { + this.setValue(0); + } + perma() { + this.setValue(-1); + } + timeout(value: number) { + this.setValue(Date.now() + value); + } + private setValue(value: number) { + this.value = value; + this.toggle.emit(value); + } +} diff --git a/src/ts/components/admin/shared/email-list/email-list.pug b/src/ts/components/admin/shared/email-list/email-list.pug new file mode 100644 index 0000000..1e3e64b --- /dev/null +++ b/src/ts/components/admin/shared/email-list/email-list.pug @@ -0,0 +1,6 @@ +div(*ngIf="emails && emails.length") + .account-email.text-muted(*ngFor="let e of emails | slice:0:limit") + | {{e}} + div(*ngIf="hasMore") + a.text-muted.ml-1.text-nowrap((click)="showMore()") + small ({{emails.length - limit}} more) diff --git a/src/ts/components/admin/shared/email-list/email-list.ts b/src/ts/components/admin/shared/email-list/email-list.ts new file mode 100644 index 0000000..95c837d --- /dev/null +++ b/src/ts/components/admin/shared/email-list/email-list.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'email-list', + templateUrl: 'email-list.pug', +}) +export class EmailList { + @Input() emails?: string[]; + limit = 3; + get hasMore() { + return this.emails && this.emails.length > this.limit; + } + showMore() { + this.limit = 9999; + } +} diff --git a/src/ts/components/admin/shared/events-table/events-table.pug b/src/ts/components/admin/shared/events-table/events-table.pug new file mode 100644 index 0000000..f9a6056 --- /dev/null +++ b/src/ts/components/admin/shared/events-table/events-table.pug @@ -0,0 +1,51 @@ +table.table.table-fixed.table-striped.table-hover.table-sm.table-ellipsis + thead + tr + th.col-time created + th.col-time updated + th message + th.col-account account + th.col-auth auths + th.col-pony pony + th.col-origin origin + th.col-actions actions + tbody + tr(*ngFor="let e of events" [class.deleted]="e.deleted") + td.col-time + time-field([time]="e.createdAt") + td.col-time + time-field(*ngIf="e.createdAt !== e.updatedAt" [time]="e.updatedAt") + td + span.badge.badge-admin([ngClass]="serverLabel(e)") {{e.server}} + span.badge.badge-admin([ngClass]="'badge-' + e.type") {{e.type}} + span.badge.badge-admin(*ngIf="e.count > 1" [ngClass]="e.count > 9 ? 'badge-danger' : 'badge-none'") {{e.count}} + b {{e.message}} + a.text-muted(*ngIf="e.account" (click)="copyToNotes(e, accountInfo.account)" title="copy to account notes") + fa-icon([icon]="clipboardIcon") + .text-muted.event-desc.pre-line(*ngIf="e.desc") + span([innerHTML]="e.descHTML") + a.text-muted.ml-1([href]="translateUrl(e)" target="_blank" title="translate") + fa-icon([icon]="langIcon") + td.col-account + account-info-remote(#accountInfo [accountId]="e.account" [showDuplicates]="true") + td.col-auth + auth-list-remote([accountId]="e.account") + td.col-pony + pony-info-remote(*ngIf="e.pony" [ponyId]="e.pony") + td.col-origin + origin-info-remote([originIP]="e.origin?.ip") + td.col-actions.text-nowrap + button.btn.btn-xs.btn-danger.mr-1((click)="remove(e)" title="Remove event") + fa-icon([icon]="trashIcon") + .btn-group.dropdown(dropdown) + button.btn.btn-xs.btn-default( + (click)="onShowChat($event, e, accountInfo.account)" [disabled]="!e.account" title="Show chat log") + fa-icon([icon]="commentIcon") + button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="removeAll(e)") + fa-icon.mr-1([icon]="trashIcon" [fixedWidth]="true") + | Remove all #[strong {{e.message}}] + a.dropdown-item((click)="onAddChat(e, accountInfo.account)") + fa-icon.mr-1([icon]="commentIcon" [fixedWidth]="true") + | Add to chat log diff --git a/src/ts/components/admin/shared/events-table/events-table.scss b/src/ts/components/admin/shared/events-table/events-table.scss new file mode 100644 index 0000000..f6bbeb3 --- /dev/null +++ b/src/ts/components/admin/shared/events-table/events-table.scss @@ -0,0 +1,8 @@ +.event-desc { + word-wrap: break-word; + max-width: 500px; +} + +.col-actions { + width: 90px; +} diff --git a/src/ts/components/admin/shared/events-table/events-table.ts b/src/ts/components/admin/shared/events-table/events-table.ts new file mode 100644 index 0000000..115632b --- /dev/null +++ b/src/ts/components/admin/shared/events-table/events-table.ts @@ -0,0 +1,56 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Account, Event, SERVER_LABELS, ChatEvent } from '../../../../common/adminInterfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { faLanguage, faTrash, faComment, faClipboard } from '../../../../client/icons'; +import { getTranslationUrl } from '../../../../common/adminUtils'; + +@Component({ + selector: 'events-table', + templateUrl: 'events-table.pug', + styleUrls: ['events-table.scss'], +}) +export class EventsTable { + readonly clipboardIcon = faClipboard; + readonly langIcon = faLanguage; + readonly trashIcon = faTrash; + readonly commentIcon = faComment; + @Input() events!: Event[]; + @Output() showChat = new EventEmitter(); + @Output() addChat = new EventEmitter(); + @Output() removedEvent = new EventEmitter(); + constructor(private model: AdminModel) { + } + serverLabel(e: Event) { + return SERVER_LABELS[e.server] || 'badge-none'; + } + remove(e: Event) { + this.model.removeEvent(e._id); + this.removedEvent.emit(e); + } + removeAll(e: Event) { + this.model.events + .filter(x => x.message === e.message) + .forEach(x => this.model.removeEvent(x._id)); + } + copyToNotes(e: Event, account: Account | undefined) { + if (account) { + const desc = e.desc ? `: ${e.desc}` : ''; + const count = e.count > 1 ? `[${e.count}] ` : ''; + const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`; + this.model.setNote(account._id, note.trim()); + } + } + translateUrl(e: Event) { + return getTranslationUrl(e.desc); + } + onShowChat(e: MouseEvent, event: Event, account: Account | undefined) { + if (e.shiftKey) { + this.addChat.emit({ event, account }); + } else { + this.showChat.emit({ event, account }); + } + } + onAddChat(event: Event, account: Account | undefined) { + this.addChat.emit({ event, account }); + } +} diff --git a/src/ts/components/admin/shared/from-now.ts b/src/ts/components/admin/shared/from-now.ts new file mode 100644 index 0000000..2fcf0e2 --- /dev/null +++ b/src/ts/components/admin/shared/from-now.ts @@ -0,0 +1,36 @@ +import { Component, Input, OnInit, OnDestroy, OnChanges, ElementRef, ChangeDetectionStrategy } from '@angular/core'; +import * as moment from 'moment'; +import { IntervalUpdateService } from '../../services/intervalUpdateService'; + +@Component({ + selector: 'from-now', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FromNow implements OnInit, OnDestroy, OnChanges { + @Input() time?: any; + private moment?: moment.Moment; + private text?: string; + private unsubscribe?: () => void; + constructor(private element: ElementRef, private updateService: IntervalUpdateService) { + } + ngOnChanges() { + this.moment = this.time ? moment(this.time) : undefined; + this.update(); + } + ngOnInit() { + this.unsubscribe = this.updateService.subscribe(() => this.update()); + this.update(); + } + ngOnDestroy() { + this.unsubscribe && this.unsubscribe(); + } + private update() { + const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : ''; + + if (this.text !== text) { + this.text = text; + (this.element.nativeElement as HTMLElement).children[0].textContent = text; + } + } +} diff --git a/src/ts/components/admin/shared/on-off-switch/on-off-switch.pug b/src/ts/components/admin/shared/on-off-switch/on-off-switch.pug new file mode 100644 index 0000000..965421e --- /dev/null +++ b/src/ts/components/admin/shared/on-off-switch/on-off-switch.pug @@ -0,0 +1,8 @@ +.btn-group.mb-1 + button.btn.btn-xs([ngClass]="on ? 'btn-default' : 'btn-danger active'" (click)="onToggle(false)" [disabled]="disabled") + b {{offText}} + button.btn.btn-xs([ngClass]="on ? 'btn-success active' : 'btn-default'" (click)="onToggle(true)" [disabled]="disabled") + b {{onText}} + +b.ml-2.mb-1.text-muted + | {{label}} diff --git a/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts b/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts new file mode 100644 index 0000000..4c51fcf --- /dev/null +++ b/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts @@ -0,0 +1,20 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'on-off-switch', + templateUrl: 'on-off-switch.pug', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OnOffSwitch { + @Input() on = false; + @Input() disabled = false; + @Input() onText = 'ON'; + @Input() offText = 'OFF'; + @Input() label = ''; + @Output() toggle = new EventEmitter(); + onToggle(value: boolean) { + if (value !== this.on) { + this.toggle.emit(value); + } + } +} diff --git a/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.pug b/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.pug new file mode 100644 index 0000000..8400460 --- /dev/null +++ b/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.pug @@ -0,0 +1 @@ +origin-info([origin]="origin") diff --git a/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts b/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts new file mode 100644 index 0000000..55f5df8 --- /dev/null +++ b/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts @@ -0,0 +1,31 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { Origin } from '../../../../common/adminInterfaces'; + +@Component({ + selector: 'origin-info-remote', + templateUrl: 'origin-info-remote.pug', +}) +export class OriginInfoRemote implements OnDestroy { + @Input() showName = false; + origin?: Origin; + private _originIP?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get originIP() { + return this._originIP; + } + @Input() set originIP(value: string | undefined) { + if (this.originIP !== value) { + this._originIP = value; + this.origin = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } +} diff --git a/src/ts/components/admin/shared/origin-info/origin-info.pug b/src/ts/components/admin/shared/origin-info/origin-info.pug new file mode 100644 index 0000000..5633a3b --- /dev/null +++ b/src/ts/components/admin/shared/origin-info/origin-info.pug @@ -0,0 +1,9 @@ +.d-flex.align-items-center(*ngIf="origin") + a.origin-info-ip.mr-1([routerLink]="['/origins', origin.ip, origin.country]" [title]="origin.ip") + | {{origin.ip}} + span([title]="countryName") [{{origin.country}}] + span.badge.badge-none.ml-1(*ngIf="origin.accountsCount > 1" title="accounts using this IP") + | {{origin.accountsCount}} + ban-icon((toggle)="toggleBan('mute', $event)" [value]="origin.mute" type="mute") + ban-icon((toggle)="toggleBan('shadow', $event)" [value]="origin.shadow" type="shadow") + ban-icon((toggle)="toggleBan('ban', $event)" [value]="origin.ban" type="ban") diff --git a/src/ts/components/admin/shared/origin-info/origin-info.scss b/src/ts/components/admin/shared/origin-info/origin-info.scss new file mode 100644 index 0000000..c7e13ae --- /dev/null +++ b/src/ts/components/admin/shared/origin-info/origin-info.scss @@ -0,0 +1,17 @@ +.origin-info-ip { + display: inline-block; + max-width: 110px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + white-space: nowrap; +} + +i { + font-size: 13px; + margin: 0 1px; +} + +ban-icon { + margin-left: 3px; +} diff --git a/src/ts/components/admin/shared/origin-info/origin-info.ts b/src/ts/components/admin/shared/origin-info/origin-info.ts new file mode 100644 index 0000000..71681fb --- /dev/null +++ b/src/ts/components/admin/shared/origin-info/origin-info.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { Origin, BannedMuted } from '../../../../common/adminInterfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { countryCodeToName } from '../../../../common/countries'; + +@Component({ + selector: 'origin-info', + templateUrl: 'origin-info.pug', + styleUrls: ['origin-info.scss'], +}) +export class OriginInfo { + @Input() origin?: Origin; + constructor(private model: AdminModel) { + } + get countryName() { + return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown'; + } + toggleBan(field: keyof BannedMuted, value: number) { + if (this.origin) { + this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value }); + } + } +} diff --git a/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.pug b/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.pug new file mode 100644 index 0000000..d52869b --- /dev/null +++ b/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.pug @@ -0,0 +1,4 @@ +div(*ngFor="let o of origins | slice:0:limit") + origin-info-remote([originIP]="o.ip") +a.text-muted(*ngIf="origins.length > limit" (click)="limit = 999999") + small ({{origins.length - limit}} more) diff --git a/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts b/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts new file mode 100644 index 0000000..612c0b0 --- /dev/null +++ b/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts @@ -0,0 +1,32 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { OriginInfoBase } from '../../../../common/adminInterfaces'; + +@Component({ + selector: 'origin-list-remote', + templateUrl: 'origin-list-remote.pug', +}) +export class OriginListRemote implements OnDestroy { + @Input() limit = 2; + @Input() extended = false; + origins: OriginInfoBase[] = []; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value: string | undefined) { + if (this.accountId !== value) { + this._accountId = value; + this.origins = []; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } +} diff --git a/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.pug b/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.pug new file mode 100644 index 0000000..31e422f --- /dev/null +++ b/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.pug @@ -0,0 +1 @@ +pony-info([pony]="pony" [highlight]="highlight") diff --git a/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts b/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts new file mode 100644 index 0000000..1b64bb4 --- /dev/null +++ b/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts @@ -0,0 +1,32 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { Character } from '../../../../common/adminInterfaces'; + +@Component({ + selector: 'pony-info-remote', + templateUrl: 'pony-info-remote.pug', +}) +export class PonyInfoRemote implements OnDestroy { + @Input() highlight = false; + @Input() showName = false; + pony?: Character; + private _ponyId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get ponyId() { + return this._ponyId; + } + @Input() set ponyId(value: string | undefined) { + if (this.ponyId !== value) { + this._ponyId = value; + this.pony = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } +} diff --git a/src/ts/components/admin/shared/pony-info/pony-info.pug b/src/ts/components/admin/shared/pony-info/pony-info.pug new file mode 100644 index 0000000..b4fd960 --- /dev/null +++ b/src/ts/components/admin/shared/pony-info/pony-info.pug @@ -0,0 +1,12 @@ +ng-template(#tooltip) + .character-tooltip(style="position: relative") + character-preview([pony]="pony?.ponyInfo" [passive]="true") + .character-tooltip-indicator(*ngIf="isBadCM") • + .character-tooltip-date + from-now([time]="pony?.lastUsed") + span.ml-1(*ngIf="pony?.deleted") [deleted] + +span.cursor-pointer.badge( + [ngClass]="labelClass" [class.deleted]="pony?.deleted" [class.badge-highlight]="highlight" (click)="click()" + [tooltip]="tooltip" (onShown)="onShown()" containerClass="tooltip-pony") + | {{pony?.name || '...'}} diff --git a/src/ts/components/admin/shared/pony-info/pony-info.scss b/src/ts/components/admin/shared/pony-info/pony-info.scss new file mode 100644 index 0000000..2ab9afc --- /dev/null +++ b/src/ts/components/admin/shared/pony-info/pony-info.scss @@ -0,0 +1,3 @@ +.badge-highlight { + box-shadow: -3px 0 0 white; +} diff --git a/src/ts/components/admin/shared/pony-info/pony-info.ts b/src/ts/components/admin/shared/pony-info/pony-info.ts new file mode 100644 index 0000000..9e1c9b2 --- /dev/null +++ b/src/ts/components/admin/shared/pony-info/pony-info.ts @@ -0,0 +1,42 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { hasFlag } from '../../../../common/utils'; +import { Character, CharacterFlags } from '../../../../common/adminInterfaces'; +import { isForbiddenName } from '../../../../common/security'; +import { AdminModel } from '../../../services/adminModel'; + +@Component({ + selector: 'pony-info', + templateUrl: 'pony-info.pug', + styleUrls: ['pony-info.scss'], +}) +export class PonyInfo implements OnChanges { + @Input() pony?: Character; + @Input() highlight = false; + labelClass = 'badge-none'; + private promise?: Promise; + constructor(private model: AdminModel) { + } + get isBadCM() { + return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM); + } + ngOnChanges() { + if (this.pony) { + if (isForbiddenName(this.pony.name)) { + this.labelClass = 'badge-forbidden'; + } else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) { + this.labelClass = 'badge-danger'; + } else { + this.labelClass = 'badge-none'; + } + } + } + onShown() { + if (this.pony && !this.pony.ponyInfo && !this.promise) { + this.promise = this.model.getPonyInfo(this.pony) + .finally(() => this.promise = undefined); + } + } + click() { + console.log(this.pony); + } +} diff --git a/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.pug b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.pug new file mode 100644 index 0000000..b8c7903 --- /dev/null +++ b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.pug @@ -0,0 +1,17 @@ +div([class.pony-list-expanded]="expanded" [class.pony-list-deletable]="deletable") + span.text-muted(*ngIf="loading") ... + span.pony-list-item.mr-1(*ngFor="let p of ponies | slice:0:limitTo") + pony-info-remote(#info [ponyId]="p" [highlight]="highlight(info.pony)") + a.ml-1.icon-button.cursor-pointer((click)="remove(p)" title="delete character") + fa-icon.text-muted([icon]="trashIcon" [fixedWidth]="true") + .btn-group.dropdown(dropdown *ngIf="duplicates") + a.ml-1.icon-button.cursor-pointer(dropdownToggle title="Assign to another account") + fa-icon.text-muted([icon]="assignIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + button.dropdown-item(*ngFor="let d of duplicates" (click)="assignTo(p, d.account)") + span.text-muted assign to + account-info-remote.ml-1([accountId]="d.account" [basic]="true") +a.text-muted.mr-1(*ngIf="!full && ponies.length > limit" (click)="toggleFull()") + small ({{ponies.length - limit}} more) +a.text-muted(*ngIf="full && ponies.length > limit" (click)="toggleFull()") + small (less) diff --git a/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.scss b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.scss new file mode 100644 index 0000000..ae8da74 --- /dev/null +++ b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.scss @@ -0,0 +1,14 @@ +.pony-list-expanded { + .pony-list-item { + display: block; + } +} + +.icon-button { + display: none; + font-size: 12px; + + .pony-list-deletable & { + display: inline; + } +} diff --git a/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts new file mode 100644 index 0000000..3cc48e9 --- /dev/null +++ b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts @@ -0,0 +1,75 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { Subscription } from '../../../../common/interfaces'; +import { AdminModel } from '../../../services/adminModel'; +import { faTrash, faArrowRight } from '../../../../client/icons'; +import { Character, PonyIdDateName, DuplicateResult } from '../../../../common/adminInterfaces'; + +@Component({ + selector: 'pony-list-remote', + templateUrl: 'pony-list-remote.pug', + styleUrls: ['pony-list-remote.scss'], +}) +export class PonyListRemote implements OnDestroy { + readonly trashIcon = faTrash; + readonly assignIcon = faArrowRight; + @Input() limit = 10; + @Input() expanded = false; + @Input() deletable = false; + @Input() highlight: (pony: Character) => boolean = () => false; + @Input() duplicates?: DuplicateResult[]; + full = false; + ponies: string[] = []; + loading = false; + private ponyInfos: PonyIdDateName[] = []; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get limitTo() { + return this.full ? 999999 : this.limit; + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value: string | undefined) { + if (this.accountId !== value) { + this._accountId = value; + this.ponies = []; + this.ponyInfos = []; + this.loading = true; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => { + this.ponyInfos = x; + this.updatePonies(); + this.loading = false; + }) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + remove(characterId: string) { + if (confirm('Are you sure?')) { + this.model.removePony(characterId); + } + } + toggleFull() { + this.full = !this.full; + this.updatePonies(); + } + assignTo(pony: string, account: string) { + this.model.assignPony(pony, account); + } + private updatePonies() { + const compare = this.full ? compareNames : compareDates; + this.ponies = this.ponyInfos.sort(compare).map(p => p.id); + } +} + +export function compareNames(a: { name: string }, b: { name: string }) { + return a.name.localeCompare(b.name); +} + +export function compareDates(a: { date: number }, b: { date: number }) { + return b.date - a.date; +} diff --git a/src/ts/components/admin/shared/time-field/time-field.pug b/src/ts/components/admin/shared/time-field/time-field.pug new file mode 100644 index 0000000..2df48c5 --- /dev/null +++ b/src/ts/components/admin/shared/time-field/time-field.pug @@ -0,0 +1,5 @@ +ng-template(#tooltip) + div {{time | date:'mediumDate'}} + .text-muted {{time | date:'mediumTime'}} + +from-now([time]="time" [tooltip]="tooltip" [isDisabled]="!time") diff --git a/src/ts/components/admin/shared/time-field/time-field.ts b/src/ts/components/admin/shared/time-field/time-field.ts new file mode 100644 index 0000000..4ddae25 --- /dev/null +++ b/src/ts/components/admin/shared/time-field/time-field.ts @@ -0,0 +1,10 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'time-field', + templateUrl: 'time-field.pug', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimeField { + @Input() time: any; +} diff --git a/src/ts/components/admin/shared/ua-info/ua-info.pug b/src/ts/components/admin/shared/ua-info/ua-info.pug new file mode 100644 index 0000000..8f3dd4b --- /dev/null +++ b/src/ts/components/admin/shared/ua-info/ua-info.pug @@ -0,0 +1,6 @@ +div(*ngIf="userAgent" [tooltip]="userAgent") + fa-icon([icon]="browserClass" [fixedWidth]="true") + b.ml-1 {{browserVersion}} + fa-icon.ml-1([icon]="osClass" [fixedWidth]="true") + b.ml-1 {{osVersion}} + fa-icon.ml-1([icon]="deviceClass" [fixedWidth]="true") diff --git a/src/ts/components/admin/shared/ua-info/ua-info.ts b/src/ts/components/admin/shared/ua-info/ua-info.ts new file mode 100644 index 0000000..84c2794 --- /dev/null +++ b/src/ts/components/admin/shared/ua-info/ua-info.ts @@ -0,0 +1,45 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { UAParser } from 'ua-parser-js'; +import { uaIcons, faQuestionCircle, faGlobe, faDesktop } from '../../../../client/icons'; + +function icon(value: string | undefined, defaultValue: any): any { + return value && uaIcons[value] || defaultValue; +} + +const extensions = { + browser: [ + [/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION] + ], +}; + +@Component({ + selector: 'ua-info', + templateUrl: 'ua-info.pug', + styles: [`:host { display: inline-block; }`], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UAInfo { + osClass?: string; + osVersion?: string; + browserClass?: string; + browserVersion?: string; + deviceClass?: string; + private _userAgent?: string; + @Input() set userAgent(value: string | undefined) { + if (this._userAgent !== value) { + this._userAgent = value; + + const parser = new UAParser(value, extensions); + const { os, browser, device } = parser.getResult(); + + this.osVersion = os.version; + this.browserVersion = (browser.version || '').replace(/\..*$/, ''); + this.osClass = icon(os.name, faQuestionCircle); + this.browserClass = icon(browser.name, faGlobe); + this.deviceClass = icon(device.type, faDesktop); + } + } + get userAgent() { + return this._userAgent; + } +} diff --git a/src/ts/components/app/about/about.pug b/src/ts/components/app/about/about.pug new file mode 100644 index 0000000..086e399 --- /dev/null +++ b/src/ts/components/app/about/about.pug @@ -0,0 +1,102 @@ +h1(focusTitle) About + +.row.text-large + .col-lg-7 + p.lead. + A game of ponies building a town + + h2 Contact + p For all inquiries please send an email to #[b {{contactEmail}}] + + //- div + hr + + h2.text-success Custom servers + + p. + You can #[a.text-success(href="...") download the files] and start your own custom {{title}} server. + p. + The package contains server files and instruction on steps required to start your own server. + It also allows you to customize and modify the game to your liking. + p. + Feel free to use this to host a server for your friends, fandom, language group or just a general + server for everyone. + + a.d-block.mx-auto.my-4.btn.btn-lg.btn-outline-success.px-3(href="..." style="max-width: 400px;") + | Download game files + + p. + All necessary instructions required to build and run the server are included in README.md file + included in the package. + p. + Running the server requires basic knowledge of configuring and hosting web applications, + it's not intended for beginner users. + + hr + + div(*ngIf="patreonLink") + h2 Support {{title}} + + p. + If you'd like to help keep {{title}} running, and support future development of the project, + please consider #[a([href]="patreonLink" target="_blank" rel="noopener noreferrer") supporting us on Patreon]. + + support-button.d-block.mx-auto.my-4(style="max-width: 400px;") + + p. + Patreon supporter rewards include the following: + ul + li(*ngFor="let r of rewards") + | {{r}} + p. + Note that in order to receive any patreon rewards you need to connect your Patreon to your {{title}} account. + + h2 Technology + + p. + The entire game is written in #[a(href="http://www.typescriptlang.org/") TypeScript], + a typed superset of JavaScript that compiles to plain JavaScript. + Server side code is running on #[a(href="https://nodejs.org/en/") Node.js] + server with WebSockets for communication. + User interface is built using #[a(href="https://angular.io/") Angular] + framework and the game itself is using WebGL for rendering graphics. + + div(*ngIf="credits.length") + h2 The Team + + .mb-4 + .row.team-members + .col-md-6(*ngFor="let c of credits") + .team-member + .team-avatar.float-left( + [style.background-image]="c.background" + [style.background-position]="c.position" + style="background-size: 328px 246px") + b.text-truncate.sno {{c.name}} + .text-muted {{c.title}} + site-links([links]="c.links") + + //- small.text-muted + em avatars by #[a.text-muted(href="link") Name] + + h2 Contributors + + div(*ngFor="let g of contributors") + h3 {{g.group}} + p(*ngFor="let c of g.contributors") + strong {{c.name}} + span(*ngIf="c.links") + | - + site-links([links]="c.links") + + .col-lg-5 + + h2 Changelog + + div(*ngFor="let v of changelog") + h4 {{v.version}} + ul + li(*ngFor="let c of v.changes" [innerHTML]="c") + +.text-center.text-muted + .tinyface diff --git a/src/ts/components/app/about/about.scss b/src/ts/components/app/about/about.scss new file mode 100644 index 0000000..d7db6bd --- /dev/null +++ b/src/ts/components/app/about/about.scss @@ -0,0 +1,33 @@ +.team-members { + margin-top: 10px; +} + +.team-member { + b { + white-space: nowrap; + } + + .text-muted { + font-size: 14px; + } +} + +.team-avatar { + background: white; + position: relative; + margin: 0 15px 20px 0; + width: 90px; + height: 90px; + border-radius: 50%; + border: solid 4px white; + transform: scale(0.7); + margin-right: 5px; + margin-bottom: 10px; + margin-top: -10px; + + > img { + width: 100%; + height: 100%; + border-radius: 50%; + } +} diff --git a/src/ts/components/app/about/about.ts b/src/ts/components/app/about/about.ts new file mode 100644 index 0000000..e5470f8 --- /dev/null +++ b/src/ts/components/app/about/about.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { emojis } from '../../../client/emoji'; +import { getUrl } from '../../../client/rev'; +import { CREDITS, CONTRIBUTORS, Credit } from '../../../client/credits'; +import { CHANGELOG } from '../../../generated/changelog'; +import { SUPPORTER_REWARDS_LIST } from '../../../common/constants'; +import { supporterLink, contactEmail } from '../../../client/data'; + +function toCredit(credit: Credit) { + return { + ...credit, + background: `url(${getUrl('images/avatars.jpg')})`, + position: `${(credit.avatarIndex % 4) * -82}px ${Math.floor(credit.avatarIndex / 4) * -82}px`, + }; +} + +@Component({ + selector: 'about', + templateUrl: 'about.pug', + styleUrls: ['about.scss'], +}) +export class About { + readonly title = document.title; + readonly emotes = emojis; + readonly credits = CREDITS.map(toCredit); + readonly contributors = CONTRIBUTORS; + readonly changelog = CHANGELOG; + readonly rewards = SUPPORTER_REWARDS_LIST; + readonly patreonLink = supporterLink; + readonly contactEmail = contactEmail; +} diff --git a/src/ts/components/app/account/account.pug b/src/ts/components/app/account/account.pug new file mode 100644 index 0000000..408eac8 --- /dev/null +++ b/src/ts/components/app/account/account.pug @@ -0,0 +1,134 @@ +h1(focusTitle) Account settings + +.row.text-large + .col-md-6 + form(name="form" (submit)="submit()" style="max-width: 400px; margin-bottom: 100px") + .form-group + h3 Account details + .form-group + label(for="account-id") Account ID + input#account-id.form-control([value]="account?.id" readonly) + .form-group + label(for="account-name") Account Name + input#account-name.form-control( + type="text" name="name" [(ngModel)]="data.name" required [maxlength]="nameMaxLength") + small.form-text.text-warning This name will be visible to other players + .form-group + label(for="birthdate") + | Date of birth + fa-icon.ml-1.text-danger(*ngIf="showAccountAlert" [icon]="alertIcon" title="Missing field value") + date-picker([(date)]="data.birthdate") + .form-group(*ngIf="accountError") + .alert.alert-danger + | {{accountError}} + .form-group(*ngIf="accountSaved") + .alert.alert-success + | Changes saved + .form-group.text-right + button.btn.btn-default(type="submit" [disabled]="!canSubmit" style="min-width: 100px;") + | Save + + div(style="margin-bottom: 100px;") + .form-group + h3 Connected social accounts + + .form-group + p.text-muted. + If you remove social account, you won't be able to use it to sign-in to the game anymore. + + .form-group + ul.list-unstyled + li.item-connected-account.d-flex.justify-content-between(*ngFor="let s of sites") + a([href]="s.url" target="_blank" rel="noopener noreferrer") + fa-icon.mr-1([icon]="icon(s.icon)" [fixedWidth]="true") + b #[span.sr-only {{s.icon}}] {{s.name}} + button.btn.btn-sm.btn-danger(*ngIf="sites.length > 1" (click)="removeSite(s)" [disabled]="removingSite") + | remove + + .form-group + p.text-danger. + Please do NOT connect any social sites that #[b do not belong to you!] + Connecting social sites belonging to other people may result in unexpected account merge + and loss of one or both accounts. + p.text-danger. + Please contact our support email if you accidentally merge your account with someone else. + .dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | connect another social site to this account + .dropdown-menu(*dropdownMenu) + button.dropdown-item(*ngFor="let p of providers" (click)="connectSite(p)") + fa-icon.mr-1([icon]="icon(p.id)" [fixedWidth]="true") + | #[span.sr-only {{p.name}}] {{p.name}} + + .form-group(*ngIf="mergeError") + .alert.alert-danger + | {{mergeError}} + + .form-group(*ngIf="authError") + .alert.alert-danger + | {{authError}} + + .form-group(*ngIf="mergedAccount") + .alert.alert-success + | Account connected successfully + + .form-group(*ngIf="removedAccount") + .alert.alert-success + | Account removed successfully + + .col-md-6 + div(style="margin-bottom: 100px;") + .clearfix(*ngIf="showSupporter") + .form-group + h3 + fa-icon.mr-2([icon]="starIcon" [ngClass]="supporterClass") + | Thank you for your support + + supporter-pony.float-right(#pony (mouseenter)="pony.excite()" (mouseleave)="pony.reset()") + + .form-group + p Your reward tier: #[b {{supporterTitle}}] + p Your rewards include: + ul + li(*ngFor="let r of supporterRewards") + | {{r}} + + p(*ngIf="supporter > 0") + | Use /ss hello command to use supporter chat color. + + p(*ngIf="supporter > 1") + | Use /s1 hello and /s2 hello commands to use lower tier supporter chat colors. + + .clearfix(*ngIf="showSupporterInfo") + .form-group + h3 Supporter info + + supporter-pony.float-right(#pony (mouseenter)="pony.excite()" (mouseleave)="pony.reset()") + + .form-group.text-muted + p. + You've successfully connected your Patreon account to Pony Town. + If you support Pony Town on Patreon your supporter info will show up here. + p. + It should take no longer than 10 minutes to register your Patreon pledges. + + div(style="margin-bottom: 100px;") + .form-group + .float-right + span.text-muted Page: {{page + 1}} + button.btn.btn-sm.btn-outline-secondary.ml-2((click)="page = page - 1; pageChanged()" [disabled]="!hides || page === 0") + | Previous page + button.btn.btn-sm.btn-outline-secondary.ml-1((click)="page = page + 1; pageChanged()" [disabled]="!hides || hides.length < hidesPerPage") + | Next page + h3 Hidden players + + div(*ngIf="!hides") + .text-muted.text-center.p-4 loading... + + ul.list-unstyled(*ngIf="hides") + .text-muted.text-center.p-4(*ngIf="!hides.length") no hidden players + li.item-connected-account.d-flex.align-items-center(*ngFor="let h of hides") + b.flex-grow-1 {{h.name}} + span.text-muted.mr-2 hidden {{h.date}} + button.btn.btn-sm.btn-danger((click)="unhidePlayer(h)") + | unhide player diff --git a/src/ts/components/app/account/account.scss b/src/ts/components/app/account/account.scss new file mode 100644 index 0000000..8d01e46 --- /dev/null +++ b/src/ts/components/app/account/account.scss @@ -0,0 +1,17 @@ +.item-connected-account { + padding: 5px; + min-height: 40px; + + > a { + position: relative; + top: 6px; + + > i { + margin-right: 5px; + } + } + + &:hover { + background: rgba(255, 255, 255, 0.05); + } +} diff --git a/src/ts/components/app/account/account.ts b/src/ts/components/app/account/account.ts new file mode 100644 index 0000000..a6a7183 --- /dev/null +++ b/src/ts/components/app/account/account.ts @@ -0,0 +1,130 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ACCOUNT_NAME_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, HIDES_PER_PAGE } from '../../../common/constants'; +import { UpdateAccountData, SocialSiteInfo, OAuthProvider, HiddenPlayer } from '../../../common/interfaces'; +import { + toSocialSiteInfo, cleanName, supporterTitle, supporterClass, isSupporterOrPastSupporter, supporterRewards +} from '../../../client/clientUtils'; +import { oauthProviders } from '../../../client/data'; +import { Model } from '../../services/model'; +import { getProviderIcon } from '../../shared/sign-in-box/sign-in-box'; +import { faStar, faExclamationCircle, faSync } from '../../../client/icons'; + +@Component({ + selector: 'account', + templateUrl: 'account.pug', + styleUrls: ['account.scss'], +}) +export class Account implements OnInit, OnDestroy { + readonly refreshIcon = faSync; + readonly starIcon = faStar; + readonly alertIcon = faExclamationCircle; + readonly providers = oauthProviders.filter(p => !p.disabled); + readonly nameMinLength = ACCOUNT_NAME_MIN_LENGTH; + readonly nameMaxLength = ACCOUNT_NAME_MAX_LENGTH; + readonly hidesPerPage = HIDES_PER_PAGE; + data: UpdateAccountData = { + name: '', + birthdate: '', + }; + sites?: SocialSiteInfo[]; + password?: string; + removingSite?: boolean; + mergeError?: string; + removedAccount?: boolean; + accountError?: string; + accountSaved = false; + hides: HiddenPlayer[] | undefined = undefined; + page = 0; + constructor(private model: Model) { + } + ngOnInit() { + const account = this.account!; + this.sites = account.sites && account.sites.map(toSocialSiteInfo); + this.data = { + name: account.name, + birthdate: account.birthdate, + }; + + this.pageChanged(); + } + ngOnDestroy() { + this.model.mergedAccount = false; + } + pageChanged() { + this.model.getHides(this.page) + .then(result => this.hides = result); + } + get authError() { + return this.model.authError; + } + get mergedAccount() { + return this.model.mergedAccount; + } + get account() { + return this.model.account; + } + get supporter() { + return this.model.supporter; + } + get showSupporter() { + return isSupporterOrPastSupporter(this.account); + } + get canSubmit() { + return this.account && this.data.name && !!cleanName(this.data.name).length; + } + get supporterTitle() { + return supporterTitle(this.account); + } + get supporterClass() { + return supporterClass(this.account); + } + get supporterRewards() { + return supporterRewards(this.account); + } + get showSupporterInfo() { + const account = this.account; + return !!(!this.supporter && account && account.sites && account.sites.some(s => s.provider === 'patreon')); + } + get showAccountAlert() { + return this.model.missingBirthdate; + } + icon(id: string) { + return getProviderIcon(id); + } + submit() { + if (this.canSubmit) { + this.resetAllMessages(); + this.data.name = cleanName(this.data.name).substr(0, ACCOUNT_NAME_MAX_LENGTH); + this.model.updateAccount(this.data) + .catch((e: Error) => this.accountError = e.message) + .then(() => this.accountSaved = true); + } + } + removeSite(site: SocialSiteInfo) { + if (confirm('Are you sure you want to remove this social account ?')) { + this.removingSite = true; + this.resetAllMessages(); + this.model.removeSite(site.id) + .then(() => this.sites = this.account!.sites!.map(toSocialSiteInfo)) + .then(() => this.removedAccount = true) + .catch((e: Error) => this.mergeError = e.message) + .then(() => this.removingSite = false); + } + } + connectSite(provider: OAuthProvider) { + this.model.connectSite(provider); + } + private resetAllMessages() { + this.accountSaved = false; + this.mergeError = undefined; + this.accountError = undefined; + this.removedAccount = false; + this.model.authError = undefined; + this.model.mergedAccount = false; + } + unhidePlayer(player: HiddenPlayer) { + this.model.unhidePlayer(player.id) + .then(() => this.pageChanged()) + .catch((e: Error) => console.error(e)); + } +} diff --git a/src/ts/components/app/app.module.ts b/src/ts/components/app/app.module.ts new file mode 100644 index 0000000..13f49d7 --- /dev/null +++ b/src/ts/components/app/app.module.ts @@ -0,0 +1,66 @@ +import { NgModule, ErrorHandler } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { BrowserModule } from '@angular/platform-browser'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { PopoverModule } from 'ngx-bootstrap/popover'; +import { ButtonsModule } from 'ngx-bootstrap/buttons'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; + +import { SharedModule } from '../shared/shared.module'; + +import { App } from './app'; +import { Home } from './home/home'; +import { Help } from './help/help'; +import { About } from './about/about'; +import { Account } from './account/account'; +import { Character } from './character/character'; +import { EditorBox } from './editor-box/editor-box'; + +import { AuthGuard } from '../services/authGuard'; +import { RollbarService, rollbarFactory, RollbarErrorHandler } from '../services/rollbarErrorHandler'; +import { ErrorReporter } from '../services/errorReporter'; +import { RollbarErrorReporter } from '../services/rollbarErrorReporter'; + +export const routes: Routes = [ + { path: '', component: Home }, + { path: 'help', component: Help }, + { path: 'about', component: About }, + { path: 'account', component: Account, canActivate: [AuthGuard] }, + { path: 'character', component: Character, canActivate: [AuthGuard] }, + { path: '**', redirectTo: '/', pathMatch: 'full' }, +]; + +@NgModule({ + imports: [ + BrowserModule, + RouterModule, + FormsModule, + HttpClientModule, + PopoverModule.forRoot(), + ButtonsModule.forRoot(), + TooltipModule.forRoot(), + // TypeaheadModule.forRoot(), + SharedModule, + RouterModule.forRoot(routes), + FontAwesomeModule, + ], + declarations: [ + App, + Home, + Help, + About, + Account, + Character, + EditorBox, + ], + providers: [ + { provide: RollbarService, useFactory: rollbarFactory }, + { provide: ErrorHandler, useClass: RollbarErrorHandler }, + { provide: ErrorReporter, useClass: RollbarErrorReporter }, + ], + bootstrap: [App], +}) +export class AppModule { +} diff --git a/src/ts/components/app/app.pug b/src/ts/components/app/app.pug new file mode 100644 index 0000000..b1c4776 --- /dev/null +++ b/src/ts/components/app/app.pug @@ -0,0 +1,61 @@ +#app-game.app-game([hidden]="!playing") + canvas#canvas.unselectable.pixelart + span#stats.d-none.d-sm-inline + pony-box#pony-box(*ngIf="selected" [pony]="selected" (sendMessage)="messageToPony(chatBox, $event)") + editor-box#editor(*hasFeature="'editor'; also playing && isMod") + party-list#party-list + settings-box#settings-box + swap-box#swap-box + notification-list#notifications([notifications]="notifications" [notificationsLength]="notifications.length") + chat-log#chat-log(*ngIf="playing" (toggleType)="chatBox.setChatType($event)" (nameClick)="chatLogNameClick(chatBox, $event)") + action-bar#action-bar(*ngIf="showActionBar" [blurred]="chatBox.isOpen" [editable]="editingActions") + friends-box#friends-box((sendMessage)="messageToFriend(chatBox, $event)") + chat-box#chat-box(#chatBox [disabled]="editingActions") + #game-announcer(#announcer style="display: none") + div(#announcerText aria-live="assertive") + #touch-origin + #touch-position + #range-indicator + div + dropdown-outlet + +.container([hidden]="playing") + menu-bar( + *ngIf="!playing" [logo]="true" [account]="account" [loading]="loading" (signIn)="signIn($event)" (signOut)="signOut()") + menu-item(route="/" name="Home" [icon]="homeIcon") + menu-item(route="/help" name="Help" [icon]="helpIcon") + menu-item(route="/about" name="About" [icon]="aboutIcon") + menu-item(route="/character" name="Characters" [icon]="charactersIcon" *ngIf="account") + + main + router-outlet + + footer.text-muted.text-nowrap(*ngIf="!playing") + .app-version + | version #[b {{version}}] + + .text-right + .d-flex.flex-wrap + //- .text-nowrap.mr-2 + | © 2016-{{date | date:'yyyy'}} + | #[a.text-muted.mr-1([href]="twitterLink" target="_blank") {{copyright}}] | + .text-nowrap + a.text-muted.mr-2(*ngIf="twitterLink" [href]="twitterLink" target="_blank" title="Twitter") + fa-icon([icon]="twitterIcon" [fixedWidth]="true") + a.text-muted.mr-2(*ngIf="patreonLink" [href]="patreonLink" target="_blank" title="Patreon") + fa-icon([icon]="patreonIcon" [fixedWidth]="true") + a.text-muted.mr-2(*ngIf="contactEmail" href="mailto:{{contactEmail}}" target="_blank" title="Email") + fa-icon([icon]="emailIcon" [fixedWidth]="true") + div + a.text-muted.privacy-policy(href="/privacypolicy.html" target="_blank" rel="noopener") privacy policy + span.mx-2 | + a.text-muted(href="/termsofservice.html" target="_blank" rel="noopener") terms of service + +draggable-outlet + +ng-template(#reloadModal) + .modal-body + iframe.modal-frame#reload-frame(src="/reload.html" frameBorder="0") + .modal-footer + button.btn.btn-outline-secondary((click)="cancelReloadModal()") + | Cancel diff --git a/src/ts/components/app/app.scss b/src/ts/components/app/app.scss new file mode 100644 index 0000000..44e2f30 --- /dev/null +++ b/src/ts/components/app/app.scss @@ -0,0 +1,204 @@ +@import '../../../styles/partials/variables'; + +:host { + display: block; + position: relative; +} + +.container { + display: flex; + flex-direction: column; + min-height: 100vh; + position: relative; +} + +main { + padding-top: 50px; + padding-bottom: 100px; + flex-grow: 1; + + @include media-breakpoint-down(sm) { + padding-top: 20px; + padding-bottom: 50px; + } +} + +footer { + display: flex; + justify-content: space-between; + padding-bottom: 20px; + + @include media-breakpoint-down(xs) { + white-space: normal !important; + display: block; + font-size: 16px; + + .app-version { + padding-bottom: 10px; + } + + .text-right { + text-align: left !important; + } + + a { + display: inline-block; + padding: 10px 10px; + + &.privacy-policy { + padding-left: 0; + } + } + } +} + +.app-game { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + width: 100vw; + height: 100vh; + overflow: hidden; +} + +#canvas { + left: 0; + top: 0; + position: fixed; + touch-action: none; + cursor: default; + user-select: none; +} + +#pony-box { + position: fixed; + top: 10px; + left: 10px; +} + +$chat-pad: 5px; + +#chat-box { + position: fixed; + left: $chat-pad; + bottom: $chat-pad; + right: $chat-pad; + height: 0; +} + +#chat-log { + position: fixed; + left: $chat-pad; + bottom: $chat-pad + 42px; + right: $chat-pad; + height: 0; +} + +#action-bar { + position: fixed; + left: $chat-pad + 45px; + bottom: 0; +} + +#party-list { + position: fixed; + top: 105px; + left: 10px; + z-index: 2; +} + +#editor { + position: fixed; + left: 5px; + top: 5px; +} + +#settings-box { + position: fixed; + top: 5px; + right: 5px; +} + +#friends-box { + position: fixed; + top: 5px; + right: 45px; +} + +#swap-box { + position: fixed; + top: 5px; + right: 86px; +} + +#notifications { + position: fixed; + top: 45px; + right: 5px; +} + +#stats { + position: fixed; + right: 5px; + bottom: 0px; + color: #222; + font-size: 12px; + font-family: monospace; + pointer-events: none; + user-select: none; +} + +#touch-origin { + background: rgba(0, 0, 0, 0.5); + position: fixed; + left: 0; + top: 0; + height: 100px; + width: 100px; + display: none; + border: 4px solid white; + border-radius: 50%; + will-change: transform; +} + +#touch-position { + background: white; + position: fixed; + left: 0; + top: 0; + height: 50px; + width: 50px; + display: none; + border-radius: 50%; + will-change: transform; +} + +#range-indicator { + pointer-events: none; + position: fixed; + border: solid 5px white; + border-radius: 50%; + display: none; +} + +#game-announcer { + position: fixed; + left: 0; + top: 0; + right: 0; + display: flex; + justify-content: center; + text-align: center; + + > div { + font-size: 16px; + padding: 10px 15px; + background: #222; + color: $body-color; + max-width: 400px; + border-radius: 10px; + margin-top: 100px; + } +} diff --git a/src/ts/components/app/app.ts b/src/ts/components/app/app.ts new file mode 100644 index 0000000..8071de1 --- /dev/null +++ b/src/ts/components/app/app.ts @@ -0,0 +1,210 @@ +import { Component, OnInit, OnDestroy, HostListener, ViewChild, ElementRef, TemplateRef } from '@angular/core'; +import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; +import { TooltipConfig } from 'ngx-bootstrap/tooltip'; +import { PopoverConfig } from 'ngx-bootstrap/popover'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { Subscription } from 'rxjs'; +import { GameService } from '../services/gameService'; +import { Model, Friend } from '../services/model'; +import { version, host, contactEmail, supporterLink, twitterLink, copyrightName } from '../../client/data'; +import { PonyTownGame } from '../../client/game'; +import { faTwitter, faPatreon, faEnvelope, faCog, faHome, faGamepad, faInfoCircle, faHorseHead } from '../../client/icons'; +import { InstallService } from '../services/installService'; +import { OAuthProvider, Entity, FakeEntity, Pony } from '../../common/interfaces'; +import { registerServiceWorker, isBrowserOutdated, checkIframeKey } from '../../client/clientUtils'; +import { ErrorReporter } from '../services/errorReporter'; +import { SECOND, PONY_TYPE } from '../../common/constants'; +import { ChatBox } from '../shared/chat-box/chat-box'; +import { ChatLogMessage } from '../shared/chat-log/chat-log'; +import { isPony } from '../../common/pony'; +import { findEntityById } from '../../common/worldMap'; +import { isSelected } from '../../client/gameUtils'; + +export function tooltipConfig() { + return Object.assign(new TooltipConfig(), { container: 'body' }); +} + +export function popoverConfig() { + return Object.assign(new PopoverConfig(), { container: 'body' }); +} + +@Component({ + selector: 'pony-town-app', + templateUrl: 'app.pug', + styleUrls: ['app.scss'], + providers: [ + { provide: TooltipConfig, useFactory: tooltipConfig }, + { provide: PopoverConfig, useFactory: popoverConfig }, + ] +}) +export class App implements OnInit, OnDestroy { + @ViewChild('announcer', { static: true }) announcer!: ElementRef; + @ViewChild('announcerText', { static: true }) announcerText!: ElementRef; + @ViewChild('reloadModal', { static: true }) reloadModal!: TemplateRef; + @ViewChild('signInModal', { static: true }) signInModal!: TemplateRef; + readonly version = version; + readonly date = new Date(); + readonly emailIcon = faEnvelope; + readonly twitterIcon = faTwitter; + readonly patreonIcon = faPatreon; + readonly cogIcon = faCog; + readonly homeIcon = faHome; + readonly helpIcon = faGamepad; + readonly aboutIcon = faInfoCircle; + readonly charactersIcon = faHorseHead; + readonly contactEmail = contactEmail; + readonly patreonLink = supporterLink; + readonly twitterLink = twitterLink; + readonly copyright = copyrightName; + private url = location.pathname; + private reloadModalRef?: BsModalRef; + private reloadInterval?: any; + private subscriptions: Subscription[] = []; + constructor( + private modalService: BsModalService, + private gameService: GameService, + private model: Model, + private game: PonyTownGame, + private router: Router, + private activatedRoute: ActivatedRoute, + private installService: InstallService, + private errorReporter: ErrorReporter, + ) { + } + get canInstall() { + return this.installService.canInstall; + } + get loading() { + return this.model.loading; + } + get account() { + return this.model.account; + } + get isMod() { + return this.model.isMod; + } + get notifications() { + return this.game.notifications; + } + get selected() { + return this.gameService.selected; + } + get playing() { + return this.gameService.playing; + } + get showActionBar() { + return this.playing; + } + get editingActions() { + return this.game.editingActions; + } + ngOnInit() { + if (typeof ga !== 'undefined') { + this.subscriptions.push(this.router.events.subscribe(event => { + if (event instanceof NavigationEnd && this.url !== event.url) { + ga('set', 'page', this.url = event.url); + ga('send', 'pageview'); + } + })); + } + + if (isBrowserOutdated) { + this.errorReporter.disable(); + } + + if (!DEVELOPMENT) { + registerServiceWorker(`${host}sw.js`, () => { + this.model.updating = true; + setTimeout(() => { + this.model.updatingTakesLongTime = true; + }, 20 * SECOND); + }); + } + + if (DEVELOPMENT) { + this.subscriptions.push(this.game.announcements.subscribe(message => { + (this.announcer.nativeElement as HTMLElement).style.display = 'flex'; + const announcerText = this.announcerText.nativeElement as HTMLElement; + announcerText.textContent = ''; + setTimeout(() => announcerText.textContent = message, 100); + })); + } + + this.activatedRoute.queryParams.subscribe(({ error, merged, alert }) => { + this.model.authError = error; + this.model.accountAlert = alert; + this.model.mergedAccount = !!merged; + }); + + this.subscriptions.push(this.model.protectionErrors.subscribe(() => { + this.openReloadModal(); + })); + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + @HostListener('window:focus') + focus() { + this.model.verifyAccount(); + } + signIn(provider: OAuthProvider) { + this.model.signIn(provider); + } + signOut() { + this.model.signOut(); + } + openReloadModal() { + if (!this.reloadModalRef) { + this.reloadModalRef = this.modalService.show( + this.reloadModal, { class: 'modal-lg', ignoreBackdropClick: true, keyboard: false }); + + this.reloadInterval = setInterval(() => { + if (checkIframeKey('reload-frame', 'gep84r9jshge4g')) { + this.cancelReloadModal(); + } + }, 500); + } + } + cancelReloadModal() { + if (this.reloadModalRef) { + this.reloadModalRef.hide(); + this.reloadModalRef = undefined; + } + + clearInterval(this.reloadInterval); + } + chatLogNameClick(chatBox: ChatBox, message: ChatLogMessage) { + if (!message.entityId) { + return; + } + + let entity = findEntityById(this.game.map, message.entityId); + + if (entity && (!isPony(entity) || entity === this.game.player)) { + return; + } + + if (!entity) { + entity = { fake: true, type: PONY_TYPE, id: message.entityId, name: message.name } as FakeEntity as any; + } + + if (isSelected(this.game, message.entityId)) { + this.game.whisperTo = entity; + chatBox.setChatType('whisper'); + } else { + this.game.select(entity as Pony); + } + } + messageToFriend(chatBox: ChatBox, friend: Friend) { + if (friend.entityId) { + const entity: any = { id: friend.entityId, name: friend.actualName || 'unknown' }; + this.messageToPony(chatBox, entity); + } + } + messageToPony(chatBox: ChatBox, pony: Entity) { + setTimeout(() => { + this.game.whisperTo = pony; + chatBox.setChatType('whisper'); + }); + } +} diff --git a/src/ts/components/app/character/character.pug b/src/ts/components/app/character/character.pug new file mode 100644 index 0000000..14427c6 --- /dev/null +++ b/src/ts/components/app/character/character.pug @@ -0,0 +1,389 @@ +ng-template(#descriptionTooltip) + .text-left + p Description and tags help you organize and search for your ponies. + p Use # tags to be able to quickly find specific ponies like #oc #winter or #unicorn + p + | Use @ tags to sort your ponies. @top to put them at the start of the list, @end at the end, + | and @1 @2 @3 ... to order them manually. + +page-loader(*ngIf="!playing") + +.row(*ngIf="!playing && !loading") + .col-lg-6.text-center + h1.sr-only(focusTitle) Characters + h2.sr-only Character selection + .mx-auto(style="max-width: 400px;") + support-button.mb-3 + install-button.mb-3 + character-select( + [(error)]="error" [newButton]="true" [removeButton]="true" (change)="updateMuzzles()" + (preview)="previewPony = $event") + + .character-preview-box.mx-auto.my-3((fixToTop)="fixed = $event" [fixToTopOffset]="-40") + character-preview( + #characterPreview [pony]="previewInfo" [state]="state" [noBackground]="true" [noShadow]="fixed" + [name]="previewName" [tag]="previewTag" [extra]="previewExtra" style="z-index: 100") + + .form-group.text-center + button.btn.btn-lg.btn-default([disabled]="!canDuplicate" (click)="duplicate()" aria-label="Duplicate character") + | Duplicate + button.btn.btn-lg.btn-default.ml-2([disabled]="!canRevert" (click)="revert()" aria-label="Revert changes") + | Revert + button.btn.btn-lg.btn-default.ml-2([disabled]="!canSave" (click)="save()" aria-label="Save character") + | Save + + play-box(label="Save & Play" [(error)]="error") + play-notice + + .col-lg-6.character-tabs([setOutlineHidden]="!customOutlines" [class.character-tab-warning]="updateWarning") + h2.sr-only Character customization + tabset(type="pills" *ngIf="loaded" saveActiveTab="character-active-tab" label="Character customization") + tab(title="body") + .character-tab(*tabContent) + div(*ngIf="account?.supporter") + .row.form-group + label.col-sm-4.col-form-label.text-patreon + | Supporter options + .col-sm-8.text-muted.pt-2 + custom-checkbox([(checked)]="pony.hideSupport") hide supporter tag + hr.my-2 + + .row.form-group + label.col-sm-4.col-form-label Advanced options + .col-sm-8.text-muted.pt-1 + custom-checkbox([(checked)]="pony.respawnAtSpawn") + | always respawn at spawn point + custom-checkbox([(checked)]="info.customOutlines") + | allow custom outlines + custom-checkbox([(checked)]="info.freeOutlines" (checkedChange)="freeOutlinesChanged($event)") + | don't darken outlines + custom-checkbox( + [(checked)]="info.darkenLockedOutlines" [disabled]="!info.freeOutlines" + (checkedChange)="darkenLockedOutlinesChanged($event)") + | darken locked outlines + + .row.form-group(*ngIf="debug") + label.col-sm-4.col-form-label Debug options + .col-sm-8 + button.btn.btn-sm.btn-default((click)="addBlush()") Add blush + + .row.form-group + label.col-sm-4.col-form-label Export/Import options + .col-sm-8 + button.btn.btn-sm.btn-default((click)="exportPony()") Export pony + button.btn.btn-sm.btn-default.ml-1((click)="exportPonies()") Export all ponies + button.btn.btn-sm.btn-default.ml-1((click)="importFile.click()") Import ponies + input.d-none(#importFile type="file" (change)="importPonies(importFile.files[0])") + + .row.form-group + label#social-site-label.col-sm-4.col-form-label Show social site + .col-sm-8 + .dropdown.btn-group(dropdown) + button.btn.btn-default.dropdown-toggle( + dropdownToggle aria-labelledby="social-site-label" aria-describedby="social-site-help") + fa-icon.mr-1( + *ngIf="site?.icon" [icon]="icon(site.icon)" [fixedWidth]="true" [style.color]="site.color" + size="lg" [attr.aria-label]="site.icon") + strong {{site?.name}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let s of sites" (click)="site = s") + fa-icon.mr-1( + *ngIf="s.icon" [icon]="icon(s.icon)" [fixedWidth]="true" [style.color]="s.color" + size="lg" [attr.aria-label]="s.icon") + strong {{s.name}} + + .dropdown.btn-group.ml-1(dropdown *ngIf="tags.length > 1") + button.btn.btn-default.dropdown-toggle(dropdownToggle aria-label="Show tag") + span.badge(*ngIf="tag.id" [ngClass]="'badge-' + tag.className") {{tag.id}} + strong(*ngIf="!tag.id") {{tag.name}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let t of tags" (click)="tag = t") + span.badge(*ngIf="t.id" [ngClass]="'badge-' + t.className") {{t.id}} + strong(*ngIf="!t.id") {{t.name}} + + #social-site-help.text-muted.mt-1 + | Shows social site next to your name to prove to others that it's your account. + + .row.form-group + label.col-sm-4.col-form-label(for="description") + | Description + fa-icon.text-muted.ml-2([icon]="infoIcon" [tooltip]="descriptionTooltip" containerClass="tooltip-desc") + .col-sm-8 + input.form-control#description( + [(ngModel)]="pony.desc" placeholder="example: with a hat #oc #winter" [maxlength]="maxDescLength") + + .row.form-group + .col-sm-4.col-lock-box + label(for="frame-input") Animation + check-box.lock-box([icon]="playIcon" [(checked)]="playAnimation" label="Play animation") + .col-sm-8 + .input-group + input.form-control#frame-input( + type="number" [(ngModel)]="state.animationFrame" [disabled]="playAnimation" min="0" + title="Animation frame" aria-label="Animation frame") + + .input-group-append.dropdown(*ngIf="canSaveFiles" dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle *ngIf="canExport" aria-label="Export / Import") + fa-icon([icon]="codeIcon") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="export()") export all + a.dropdown-item((click)="import()") import data + a.dropdown-item((click)="testSize()") print compressed + a.dropdown-item((click)="testJSON()") print raw + + .input-group-append.dropdown(dropdown) + button.btn.btn-default( + *ngIf="canSaveFiles" (click)="export(activeAnimation)" title="Save as PNG" aria-label="Save as PNG") + fa-icon([icon]="saveIcon") + button.btn.btn-default.dropdown-toggle(dropdownToggle style="min-width: 80px") + strong {{animations[activeAnimation]().name}} + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item(*ngFor="let a of animations; let i = index" (click)="setActiveAnimation(i)") + | {{a().name}} + + hr + fill-outline( + label="Body color" [(fill)]="info.coatFill" [(outline)]="info.coatOutline" + [(outlineLocked)]="info.lockCoatOutline" [outlineHidden]="!customOutlines" (change)="changed()") + hr + set-selection( + label="Horn" [base]="coatFill" [set]="info.horn" [sets]="horns" [compact]="true" (change)="changed()" + [darken]="darken") + hr + .row.form-group + label.col-sm-4.col-form-label Magic color + .col-sm-8 + color-picker([(color)]="info.magicColor" label="Magic color") + hr + set-selection( + label="Wings" [base]="coatFill" [set]="info.wings" [sets]="wings" [compact]="true" (change)="changed()" + [darken]="darken") + hr + set-selection( + label="Ears" [base]="coatFill" [set]="info.ears" [sets]="ears" [compact]="true" (change)="changed()" + [darken]="darken") + hr + set-selection( + label="Front hooves" [base]="coatFill" [set]="info.frontHooves" [sets]="frontHooves" [compact]="true" + [darken]="darken" (change)="changed()") + hr + set-selection( + label="Back hooves" [base]="hoovesFill" [set]="info.backHooves" [sets]="backHooves" [compact]="true" + [darken]="darken" (change)="changed()") + hr + butt-mark-editor([info]="info" [state]="buttMarkState") + + tab(title="mane") + .character-tab(*tabContent) + set-selection( + label="Mane" [base]="maneFill" [set]="info.mane" [sets]="manes" [nonLockable]="true" + [darken]="darken" (change)="changed()") + hr + set-selection( + label="Back mane" [base]="maneFill" [set]="info.backMane" [sets]="backManes" + [darken]="darken" (change)="changed()") + + tab(title="tail") + .character-tab(*tabContent) + set-selection( + label="Tail" [base]="maneFill" [set]="info.tail" [sets]="tails" [darken]="darken" (change)="changed()") + .form-group.text-center + button.btn.btn-sm.btn-default((click)="copyCoatColorToTail()" [disabled]="info.tail.lockFills[0]") + | Use body color + + tab(title="face") + .character-tab(*tabContent) + + .row.form-group + .col-sm-4.col-lock-box + label#eye-color Eye color + check-box.lock-box( + [(checked)]="info.lockEyeColor" [icon]="lockIcon" (checkedChange)="eyeColorLockChanged($event)" + label="Lock color for both eyes" title="Lock color for both eyes") + .col-sm-8 + color-picker([(color)]="info.eyeColorRight" (colorChange)="changed()" labelledBy="eye-color") + .row.form-group(*ngIf="!info.lockEyeColor") + label#eye-color-left.col-sm-4.col-form-label Eye color (left) + .col-sm-8 + color-picker([(color)]="info.eyeColorLeft" labelledBy="eye-color-left") + + .row.form-group + .col-sm-4.col-lock-box + label#eye-whites-label Eye whites + check-box.lock-box( + [(checked)]="lockEyeWhites" [icon]="lockIcon" (checkedChange)="eyeWhiteLockChanged($event)" + label="Lock left eye white for both eyes" title="Lock left eye white for both eyes") + .col-sm-8 + color-picker([(color)]="info.eyeWhites" (colorChange)="changed()" labelledBy="eye-whites-label") + .row.form-group(*ngIf="info.unlockEyeWhites") + label#eye-whites-label-left.col-sm-4.col-form-label Eye whites (left) + .col-sm-8 + color-picker([(color)]="info.eyeWhitesLeft" labelledBy="eye-whites-label-left") + + .row.form-group + .col-sm-4.col-lock-box + label#eyes {{info.lockEyes ? 'Eyes' : 'Right eye'}} + check-box.lock-box( + [(checked)]="info.lockEyes" [icon]="lockIcon" (checkedChange)="eyeOpennessChanged($event)" + label="Lock both eyes" title="Lock both eyes") + .col-sm-8 + sprite-selection( + [(selected)]="info.eyeOpennessRight" [sprites]="eyesRight" [fill]="info.eyeColorRight" + [circle]="info.coatFill" [reverseExtra]="true" [skip]="1" [limit]="9" [darken]="darken" + (selectedChange)="eyeOpennessChanged(info.lockEyes)" aria-labelledby="eyes") + .row.form-group(*ngIf="!info.lockEyes") + .col-sm-4 + label#left-eye Left eye + .col-sm-8 + sprite-selection( + [(selected)]="info.eyeOpennessLeft" [sprites]="eyesLeft" [fill]="info.eyeColorLeft" [darken]="darken" + [circle]="info.coatFill" [reverseExtra]="true" [skip]="1" [limit]="5" aria-labelledby="left-eye") + + .row.form-group + .col-sm-4.col-lock-box + label Eyeshadow + check-box.lock-box( + [(checked)]="info.eyeshadow" (checkedChange)="characterPreview.blink()" + label="Use eye shadow" title="Use eye shadow") + .col-sm-8 + color-picker([(color)]="info.eyeshadowColor" [isDisabled]="!info.eyeshadow" label="Eyeshadow color") + + .row.form-group + label#eyelashes.col-sm-4.col-form-label Eyelashes + .col-sm-8 + sprite-selection( + [(selected)]="info.eyelashes" [sprites]="eyelashes" [fill]="info.eyeColorRight" [darken]="darken" + [circle]="info.coatFill" [reverseExtra]="true" aria-labelledby="eyelashes") + + .row.form-group + .col-sm-4.col-lock-box + label#eyelashes-label Eyelash color + check-box.lock-box( + [(checked)]="lockEyelashColor" [icon]="lockIcon" (checkedChange)="eyelashLockChanged($event)" + label="Lock eyelash color for both eyes" title="Lock eyelash color for both eyes") + .col-sm-8 + color-picker([(color)]="info.eyelashColor" (colorChange)="changed()" labelledBy="eyelashes-label") + .row.form-group(*ngIf="!lockEyelashColor") + label#eyelashes-label-left.col-sm-4.col-form-label Eyelash color (left) + .col-sm-8 + color-picker([(color)]="info.eyelashColorLeft" labelledBy="eyelashes-label-left") + + hr + set-selection( + label="Muzzle" base="info.coatFill" [set]="info.nose" [sets]="noses" [darken]="darken" + [compact]="true" (change)="changed(); updateMuzzles()") + hr + set-selection( + label="Markings" [base]="coatFill" [set]="info.head" [sets]="heads" [darken]="darken" + [onlyPatterns]="true" (change)="changed()") + hr + .row.form-group + label#fangs.col-sm-4.col-form-label Fangs + .col-sm-8 + sprite-selection( + [(selected)]="info.fangs" [sprites]="fangs" [fill]="info.nose.fills" [darken]="darken" + [outline]="info.nose.outlines" [circle]="info.coatFill" aria-labelledby="fangs") + .row.form-group + label#expression.col-sm-4.col-form-label(for="#expression-selection") Expression + .col-sm-8 + sprite-selection#expression-selection( + [(selected)]="info.muzzle" [sprites]="muzzles" [fill]="info.nose.fills" [darken]="darken" + [outline]="info.nose.outlines" [circle]="info.coatFill" [limit]="9" aria-labelledby="expression") + hr + set-selection( + label="Facial hair" [base]="maneFill" [set]="info.facialHair" [sets]="facialHair" [darken]="darken" + (change)="changed()") + + tab(title="other") + .character-tab(*tabContent) + tabset.accessories-tabset(saveActiveTab="character-active-accessory-tab" label="Character accessories") + tab(title="head") + .accessory-tab(*tabContent) + set-selection( + label="Head accessories" [set]="info.headAccessory" [sets]="headAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + hr + set-selection( + label="Ear accessories" [set]="info.earAccessory" [sets]="earAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + hr + set-selection( + label="Face accessories" [set]="info.faceAccessory" [sets]="faceAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + + tab(title="neck") + .accessory-tab(*tabContent) + set-selection( + label="Neck accessories" [set]="info.neckAccessory" [sets]="neckAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + + tab(title="legs") + .accessory-tab(*tabContent) + .row.form-group + label.col-sm-4.col-form-label Use the same accessory + .col-sm-8.text-muted.pt-1 + .mb-2 + custom-checkbox([(checked)]="info.lockBackLegAccessory") + | for front and back legs + .mb-2 + custom-checkbox([(checked)]="lockFrontLegAccessory") + | for both front legs + .mb-2 + custom-checkbox([(checked)]="lockBackLegAccessory" [disabled]="info.lockBackLegAccessory") + | for both back legs + + set-selection( + [label]="info.lockFrontLegAccessories ? 'Front leg accessory' : 'Front leg accessory (left)'" + [set]="info.frontLegAccessory" [sets]="frontLegAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + hr + div(*ngIf="info.unlockFrontLegAccessory") + set-selection( + label="Front leg accessory (right)" + [set]="info.frontLegAccessoryRight" [sets]="frontLegAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + hr + //- .form-group.d-flex.justify-content-center + custom-checkbox([(checked)]="info.lockBackLegAccessory") use the same accessory for back legs + div(*ngIf="!info.lockBackLegAccessory") + set-selection( + [label]="info.lockBackLegAccessories ? 'Back leg accessory' : 'Back leg accessory (left)'" + [set]="info.backLegAccessory" [sets]="backLegAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + hr + div(*ngIf="!info.lockBackLegAccessory && info.unlockBackLegAccessory") + set-selection( + label="Back leg accessory (right)" + [set]="info.backLegAccessoryRight" [sets]="backLegAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + + tab(title="chest") + .accessory-tab(*tabContent) + set-selection( + label="Chest accessories" [set]="info.chestAccessory" [sets]="chestAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + set-selection( + *ngIf="hasSleeves(info.chestAccessory.type)" label="Sleeves" [set]="info.sleeveAccessory" [darken]="darken" + [sets]="sleeveAccessories" (change)="changed()") + + tab(title="back") + .accessory-tab(*tabContent) + set-selection( + label="Back accessories" [set]="info.backAccessory" [sets]="backAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + + tab(title="waist") + .accessory-tab(*tabContent) + set-selection( + label="Waist accessories" [set]="info.waistAccessory" [sets]="waistAccessories" [darken]="darken" + [nonLockable]="true" (change)="changed()") + + tab(title="other") + .accessory-tab(*tabContent) + .form-group.d-flex.justify-content-center.pt-2 + custom-checkbox([(checked)]="previewExtra") show preview + em.text-muted.ml-2 + | (you have to enable these accessories in-game) + set-selection( + label="Extra accessories" [set]="info.extraAccessory" [sets]="extraAccessories" + [darken]="darken" (change)="changed()") diff --git a/src/ts/components/app/character/character.scss b/src/ts/components/app/character/character.scss new file mode 100644 index 0000000..0458868 --- /dev/null +++ b/src/ts/components/app/character/character.scss @@ -0,0 +1,62 @@ +@import '../../../../styles/partials/variables'; + +.character-tabs { + min-height: 500px; + margin-bottom: 80px; +} + +.character-tab { + background: $dark-bg; + margin-top: 10px; + padding: 10px 15px; + min-height: 900px; + border-radius: $border-radius-sm; + + .character-tab-warning & { + box-shadow: 0 0 0 2px $warning; + } +} + +.accessory-tab { + margin-top: 10px; +} + +.character-preview-box { + position: relative; + width: 400px; + height: 400px; + background: $grass-color; + border-radius: $border-radius-lg; + + @include media-breakpoint-down(sm) { + width: 280px; + height: 280px; + } + + &.fixed-to-top { + character-preview { + position: fixed; + top: -45px; + z-index: 100; + pointer-events: none; + } + } +} + +.col-lock-box { + display: flex; + align-items: flex-start; + justify-content: space-between; + + > .lock-box { + @include media-breakpoint-down(xs) { + margin-right: 0; + margin-bottom: 5px; + } + } + + > label { + margin: 0; + padding-top: 6px; + } +} diff --git a/src/ts/components/app/character/character.ts b/src/ts/components/app/character/character.ts new file mode 100644 index 0000000..2b88abd --- /dev/null +++ b/src/ts/components/app/character/character.ts @@ -0,0 +1,501 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { clamp } from 'lodash'; +import { PLAYER_NAME_MAX_LENGTH, PLAYER_DESC_MAX_LENGTH } from '../../../common/constants'; +import { + PonyInfo, PonyObject, PonyState, SocialSiteInfo, ColorExtraSet, ColorExtra, CharacterTag, PonyEye, Eye, Muzzle, + Iris, ExpressionExtra +} from '../../../common/interfaces'; +import { findById, toInt, cloneDeep, delay } from '../../../common/utils'; +import { + SLEEVED_ACCESSORIES, frontHooves, mergedBackManes, mergedManes, mergedFacialHair, mergedEarAccessories, + mergedChestAccessories, mergedFaceAccessories, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories +} from '../../../client/ponyUtils'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; +import { toPalette, getBaseFill, syncLockedPonyInfo } from '../../../common/ponyInfo'; +import * as sprites from '../../../generated/sprites'; +import { boop, trot, stand, sitDownUp, lieDownUp, fly, flyBug } from '../../../client/ponyAnimations'; +import { drawCanvas } from '../../../graphics/contextSpriteBatch'; +import { Model, getPonyTag } from '../../services/model'; +import { loadAndInitSpriteSheets, addTitles, createEyeSprite, addLabels } from '../../../client/spriteUtils'; +import { GameService } from '../../services/gameService'; +import { TRANSPARENT, BLACK, blushColor } from '../../../common/colors'; +import { precompressPony, compressPonyString, decompressPony, decompressPonyString } from '../../../common/compressPony'; +import { saveCanvas } from '../../../client/canvasUtils'; +import { drawPony } from '../../../client/ponyDraw'; +import { getProviderIcon } from '../../shared/sign-in-box/sign-in-box'; +import { faPlay, faLock, faSave, faCode, faInfoCircle } from '../../../client/icons'; +import { isFileSaverSupported, createExpression, readFileAsText } from '../../../client/clientUtils'; +import { emptyTag, getAvailableTags } from '../../../common/tags'; +import { ButtMarkEditorState } from '../../shared/butt-mark-editor/butt-mark-editor'; +import { parseColorWithAlpha } from '../../../common/color'; + +const frontHoofTitles = ['', 'Fetlocks', 'Paws', 'Claws', '']; +const backHoofTitles = ['', 'Fetlocks', 'Paws', '', '']; + +const horns = addLabels(sprites.horns, [ + 'None', 'Unicorn horn', 'Short unicorn horn', 'Curved unicorn horn', 'Tiny deer antlers', + 'Short deer antlers', 'Medium deer antlers', 'Large deer antlers', 'Raindeer antlers', 'Goat horns', + 'Ram horns', 'Buffalo horns', 'Moose horns', 'Bug antenna', 'Long unicorn horn', +]); + +const wings = addLabels(sprites.wings[0]!, [ + 'None', 'Pegasus wings', 'Bat wings', 'Gryphon wings', 'Bug wings' +]); + +const ears = addLabels(sprites.ears, [ + 'Regular ears', 'Fluffy ears', 'Long feathered ears', 'Bug ears', 'Short feathered ears', 'Deer ears', +]); + +const noses = addTitles(sprites.noses[0], ['Pony muzzle', 'Gryphon beak', 'Deer nose']); + +const flyAnimations = [{ ...stand, name: 'fly' }, fly, fly, fly, { ...flyBug, name: 'fly' }]; + +function eyeSprite(e: PonyEye | undefined) { + return createEyeSprite(e, 0, sprites.defaultPalette); +} + +@Component({ + selector: 'character', + templateUrl: 'character.pug', + styleUrls: ['character.scss'], +}) +export class Character implements OnInit, OnDestroy { + readonly debug = DEVELOPMENT || BETA; + readonly playIcon = faPlay; + readonly lockIcon = faLock; + readonly saveIcon = faSave; + readonly codeIcon = faCode; + readonly infoIcon = faInfoCircle; + readonly maxNameLength = PLAYER_NAME_MAX_LENGTH; + readonly maxDescLength = PLAYER_DESC_MAX_LENGTH; + readonly horns = horns; + readonly manes = mergedManes; + readonly backManes = mergedBackManes; + readonly tails = sprites.tails[0]; + readonly wings = wings; + readonly ears = ears; + readonly facialHair = mergedFacialHair; + readonly headAccessories = mergedHeadAccessories; + readonly earAccessories = mergedEarAccessories; + readonly faceAccessories = mergedFaceAccessories; + readonly neckAccessories = sprites.neckAccessories[1]; + readonly frontLegAccessories = sprites.frontLegAccessories[1]; + readonly backLegAccessories = sprites.backLegAccessories[1]; + readonly backAccessories = mergedBackAccessories; + readonly chestAccessories = mergedChestAccessories; + readonly sleeveAccessories = sprites.frontLegSleeves[1]; + readonly waistAccessories = sprites.waistAccessories[1]; + readonly extraAccessories = mergedExtraAccessories; + readonly frontHooves = addTitles(frontHooves[1], frontHoofTitles); + readonly backHooves = addTitles(sprites.backLegHooves[1], backHoofTitles); + readonly animations = [ + () => stand, + () => trot, + () => boop, + () => sitDownUp, + () => lieDownUp, + () => flyAnimations[this.previewInfo!.wings!.type || 0], + ]; + readonly eyelashes: ColorExtraSet = sprites.eyeLeft[1]!.map(eyeSprite); + readonly eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); + readonly eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); + readonly noses = noses; + readonly heads = sprites.head1[1]; + readonly buttMarkState: ButtMarkEditorState = { + brushType: 'brush', + brush: 'orange', + }; + muzzles: ColorExtraSet; + fangs: ColorExtraSet; + tags: CharacterTag[] = [ + emptyTag, + ]; + state = defaultPonyState(); + saved: PonyInfo[] = []; + activeAnimation = 0; + loaded = false; + playAnimation = true; + deleting = false; + fixed = false; + previewExtra = false; + previewPony: PonyObject | undefined = undefined; + sites: SocialSiteInfo[] = []; + error?: string; + canSaveFiles = isFileSaverSupported(); + private savingLocked = false; + private interval?: any; + private syncTimeout?: any; + private animationTime = 0; + constructor(private gameService: GameService, private model: Model) { + this.createMuzzles(); + this.updateMuzzles(); + } + private getMuzzleType() { + return clamp(toInt(this.info && this.info.nose && this.info.nose.type), 0, sprites.noses[0].length); + } + createMuzzles() { + const type = this.getMuzzleType(); + const happy = sprites.noses[0][type][0]; + + this.muzzles = sprites.noses + .slice() + .map(n => n[type][0]) + .map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palettes: [sprites.defaultPalette] } as ColorExtra)); + + this.fangs = [undefined, { color: happy.color, colors: 3, extra: happy.fangs, palettes: [sprites.defaultPalette] }]; + } + updateMuzzles() { + const type = this.getMuzzleType(); + const happy = sprites.noses[0][type][0]; + + this.muzzles!.forEach((m, i) => { + if (m) { + const { color, colors, mouth } = sprites.noses[i][type][0]; + m.color = color; + m.colors = colors; + m.extra = mouth; + m.timestamp = Date.now(); + } + }); + + const fangs = this.fangs![1]!; + fangs.color = happy.color; + fangs.extra = happy.fangs; + fangs.timestamp = Date.now(); + } + get account() { + return this.model.account; + } + get loading() { + return this.model.loading || this.model.updating; + } + get updateWarning() { + return this.gameService.updateWarning; + } + get playing() { + return this.gameService.playing; + } + get previewInfo() { + return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo; + } + get previewName() { + return this.previewPony ? this.previewPony.name : this.pony.name; + } + get previewTag() { + return getPonyTag(this.previewPony || this.pony, this.account); + } + get customOutlines() { + return this.info.customOutlines; + } + get ponies() { + return this.model.ponies; + } + get pony() { + return this.model.pony; + } + set pony(value: PonyObject) { + this.model.selectPony(value); + } + get info() { + return this.pony.ponyInfo!; + } + get maneFill() { + return getBaseFill(this.info.mane); + } + get coatFill() { + return this.info.coatFill; + } + get hoovesFill() { + return getBaseFill(this.info.frontHooves); + } + get canExport() { + return DEVELOPMENT; + } + get site() { + return findById(this.sites, this.pony.site) || this.sites[0]; + } + set site(value: SocialSiteInfo) { + this.pony.site = value.id; + } + get tag() { + return findById(this.tags, this.pony.tag) || this.tags[0]; + } + set tag(value: CharacterTag) { + this.pony.tag = value.id; + } + get lockEyeWhites() { + return !this.info.unlockEyeWhites; + } + set lockEyeWhites(value) { + this.info.unlockEyeWhites = !value; + } + get darken() { + return !this.info.freeOutlines; + } + get lockFrontLegAccessory() { + return !this.info.unlockFrontLegAccessory; + } + set lockFrontLegAccessory(value) { + this.info.unlockFrontLegAccessory = !value; + } + get lockBackLegAccessory() { + return !this.info.unlockBackLegAccessory; + } + set lockBackLegAccessory(value) { + this.info.unlockBackLegAccessory = !value; + } + get lockEyelashColor() { + return !this.info.unlockEyelashColor; + } + set lockEyelashColor(value) { + this.info.unlockEyelashColor = !value; + } + icon(id: string) { + return getProviderIcon(id); + } + hasSleeves(type: number) { + return SLEEVED_ACCESSORIES.indexOf(type) !== -1; + } + ngOnInit() { + if (this.model.account) { + this.tags.push(...getAvailableTags(this.model.account)); + } + + this.sites = this.model.sites.filter(s => !!s.name); + this.updateMuzzles(); + + let last = Date.now(); + + return loadAndInitSpriteSheets().then(() => { + this.loaded = true; + this.interval = setInterval(() => { + const now = Date.now(); + this.update((now - last) / 1000); + last = now; + }, 1000 / 24); + }); + } + ngOnDestroy() { + clearInterval(this.interval); + } + changed() { + if (!this.syncTimeout) { + this.syncTimeout = requestAnimationFrame(() => { + this.syncTimeout = undefined; + syncLockedPonyInfo(this.info); + }); + } + + if (DEVELOPMENT || BETA) { + this.state.blushColor = blushColor(parseColorWithAlpha(this.coatFill || '', 1)); + } + } + update(delta: number) { + this.animationTime += delta; + + const animation = this.animations[this.activeAnimation](); + this.state.animation = animation; + + if (this.playAnimation) { + this.state.animationFrame = Math.floor(this.animationTime * animation.fps) % animation.frames.length; + } + } + copyCoatColorToTail() { + if (this.info.tail && this.info.tail.fills) { + this.info.tail.fills[0] = this.info.coatFill; + this.changed(); + } + } + eyeColorLockChanged(locked: boolean) { + if (locked) { + this.info.eyeColorLeft = this.info.eyeColorRight; + } + } + eyeWhiteLockChanged(locked: boolean) { + if (locked) { + this.info.eyeWhitesLeft = this.info.eyeWhites; + } + } + eyeOpennessChanged(locked: boolean) { + if (locked) { + this.info.eyeOpennessLeft = this.info.eyeOpennessRight; + } + } + eyelashLockChanged(locked: boolean) { + if (locked) { + this.info.eyelashColorLeft = this.info.eyelashColor; + } + } + select(pony: PonyObject | undefined) { + if (pony) { + this.deleting = false; + this.pony = pony; + } + } + setActiveAnimation(index: number) { + this.activeAnimation = index; + this.animationTime = 0; + } + freeOutlinesChanged(_free: boolean) { + this.changed(); + } + darkenLockedOutlinesChanged(_darken: boolean) { + this.changed(); + } + get canSave() { + return !this.model.pending && !!this.pony && !!this.pony.name && !this.savingLocked; + } + save() { + if (this.canSave) { + this.error = undefined; + this.deleting = false; + this.savingLocked = true; + + this.model.savePony(this.pony) + .catch((e: Error) => this.error = e.message) + .then(() => delay(2000)) + .then(() => this.savingLocked = false); + } + } + get canRevert() { + return !!findById(this.ponies, this.pony.id); + } + revert() { + if (this.canRevert) { + this.select(findById(this.ponies, this.pony.id)); + } + } + get canDuplicate() { + return this.ponies.length < this.model.characterLimit; + } + duplicate() { + if (this.canDuplicate) { + this.deleting = false; + this.pony = cloneDeep(this.pony); + this.pony.name = ''; + this.pony.id = ''; + } + } + export(index?: number) { + const frameWidth = 80; + const frameHeight = 90; + const animations = index === undefined ? this.animations.map(a => a()) : [this.animations[index]()]; + const frames = animations.reduce((sum, a) => sum + a.frames.length, 0); + const info = toPalette(this.info); + const options = defaultDrawPonyOptions(); + + const canvas = drawCanvas(frameWidth * frames, frameHeight, sprites.paletteSpriteSheet, TRANSPARENT, batch => { + let i = 0; + + animations.forEach(a => { + for (let f = 0; f < a.frames.length; f++ , i++) { + const state: PonyState = { + ...defaultPonyState(), + animation: a, + animationFrame: f, + blinkFrame: 1, + }; + + drawPony(batch, info, state, i * frameWidth + frameWidth / 2, frameHeight - 10, options); + } + }); + }); + + const name = animations.length === 1 ? animations[0].name : 'all'; + saveCanvas(canvas, `${this.pony.name}-${name}.png`); + } + import() { + if (DEVELOPMENT) { + const data = prompt('enter data'); + + if (data) { + this.importPony(data); + } + } + } + private importPony(data: string) { + if (DEVELOPMENT) { + this.pony.ponyInfo = decompressPonyString(data, true); + const t = decompressPonyString(data, false); + console.log(JSON.stringify(t, undefined, 2)); + } + } + addBlush() { + if (DEVELOPMENT || BETA) { + this.state = { + ...this.state, + expression: createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush), + }; + this.changed(); + } + } + testSize() { + function stringifyValues(values: any[]): string { + return values.map(x => JSON.stringify(x)).join(typeof values[0] === 'object' ? ',\n\t' : ', '); + } + + if (DEVELOPMENT) { + const compressed = compressPonyString(this.info); + const regularSize = JSON.stringify(this.info).length; + const ponyInfoNumber = decompressPony(compressed); + const precomp = precompressPony(ponyInfoNumber, BLACK, x => x) as any; + const details = Object.keys(precomp) + .filter(key => key !== 'version') + .map(key => ({ key, values: precomp[key] || [] as any[] })) + .map(({ key, values }) => `${key}: [\n\t${stringifyValues(values)}\n]`) + .join(',\n'); + const serialized = compressPonyString(this.info); + + console.log(serialized); + console.log(details); + console.log(`${serialized.length} / ${regularSize}`); + } + } + testJSON() { + if (DEVELOPMENT) { + console.log(JSON.stringify(this.info, undefined, 2)); + } + } + exportPony() { + const data = ponyToExport(this.pony) + '\r\n'; + saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), `${this.pony.name}.txt`); + } + exportPonies() { + const data = this.ponies.map(ponyToExport).join('\r\n') + '\r\n'; + saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), 'ponies.txt'); + } + async importPonies(file: File | undefined) { + if (file) { + const text = await readFileAsText(file); + const lines = text.split(/\r?\n/g); + let imported = 0; + + for (const line of lines) { + try { + const [name, info, desc = ''] = line.split(/\t/g); + + if (name && info) { + const pony: PonyObject = { + name, + id: '', + info, + desc, + ponyInfo: decompressPonyString(info, true), + }; + + await this.model.savePony(pony, true); + imported++; + } + } catch (e) { + DEVELOPMENT && console.error(e); + } + } + + alert(`Imported ${imported} ponies`); + } + } +} + +function ponyToExport(pony: PonyObject) { + return `${pony.name}\t${pony.info}\t${pony.desc || ''}`.trim(); +} diff --git a/src/ts/components/app/editor-box/editor-box.pug b/src/ts/components/app/editor-box/editor-box.pug new file mode 100644 index 0000000..45d2ffd --- /dev/null +++ b/src/ts/components/app/editor-box/editor-box.pug @@ -0,0 +1,138 @@ +.d-flex + button.game-button.px-2((click)="toggleEditor()") + fa-icon([icon]="editIcon") + + .d-flex.align-items-start.pl-2.pt-1(*ngIf="hasEditor") + .dropdown-menu.p-0(*ngIf="hasSelectedEntities" style="display: block; width: 500px; position: absolute; top: 36px; left: 0;") + .px-2 + //- name + div + .row.form-group.mb-2 + label.col-12.col-form-label + .float-right + button.btn.btn-xs.btn-dark.ml-1((click)="showEntitiesInfo()" title="Show info") + | info + button.btn.btn-xs.btn-danger.ml-1((click)="deleteEntities()" title="Delete entities") + fa-icon([icon]="deleteIcon") + | {{entityName}} + + //- position + div(*ngIf="singleEntity") + .row.form-group.mb-2 + label.col-4.col-form-label + | position + label.col-1.col-form-label + | x + .col-3 + input.form-control([(ngModel)]="entityX" type="number") + label.col-1.col-form-label + | y + .col-3 + input.form-control([(ngModel)]="entityY" type="number") + + //- light + div(*ngIf="isLightEntity") + .row.form-group.mb-2 + label.col-5.col-form-label + | light color + .col-7 + color-picker([(color)]="entityLightColor") + .row.form-group.mb-2 + label.col-5.col-form-label + | light scale ({{entityLightScale | number:'0.1-1'}}) + .col-7.py-2 + slider-bar([(value)]="entityLightScale" [min]="0" [max]="3") + + //- light sprite + div(*ngIf="isLightSpriteEntity") + .row.form-group.mb-2 + label.col-5.col-form-label + | light sprite color + .col-7 + color-picker([(color)]="entityLightSpriteColor") + .row.form-group.mb-2 + label.col-4.col-form-label + | light sprite + label.col-1.col-form-label + | x + .col-3 + input.form-control([(ngModel)]="entityLightSpriteX" type="number") + label.col-1.col-form-label + | y + .col-3 + input.form-control([(ngModel)]="entityLightSpriteY" type="number") + + .input-group.input-group-sm + input.form-control([(ngModel)]="editorEntity" placeholder="entity") + .input-group-append(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu(*dropdownMenu style="max-height: 90vh; column-width: 100px; font-size: 10px;") + a.dropdown-item(*ngFor="let e of editorEntities" (click)="editorEntity = e") + | {{e}} + + .dropdown.ml-1(dropdown) + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle) + fa-icon([icon]="cogIcon") + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="clearLocalStorage()") + | Clear local storage + a.dropdown-item((click)="editorClear()" title="Remove all placed entities") + | Clear all entities + a.dropdown-item((click)="listEntities()" title="List all placed entities in console") + | List entities + div(*ngIf="dev") + h6.dropdown-header Show (numpad 4) + a.dropdown-item(*ngFor="let field of showFields" (click)="toggleShow(field); $event.stopPropagation()") + fa-icon.mr-1([icon]="isShow(field) ? checkIcon : emptyIcon" [fixedWidth]="true") + | {{field}} + + button.btn.btn-sm.btn-default.ml-1( + [btnHighlight]="selectingEntities" (click)="toggleSelecting()" title="Select entities") + fa-icon([icon]="selectIcon") + + .dropdown.ml-1(dropdown) + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle) + | tile: {{tiles[editorTile + 1]}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let t of tiles; let i = index" (click)="editorTile = i - 1") + | {{t}} + + .dropdown.ml-1(dropdown) + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle) + | brush: {{editor.brushSize}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="editor.brushSize = 1") 1 + a.dropdown-item((click)="editor.brushSize = 2") 2 + a.dropdown-item((click)="editor.brushSize = 3") 3 + a.dropdown-item((click)="editor.brushSize = 4") 4 + a.dropdown-item((click)="editor.brushSize = 5") 5 + a.dropdown-item((click)="editor.brushSize = 6") 6 + + .dropdown.ml-1(dropdown [autoClose]="false") + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle) + | global + .dropdown-menu(*dropdownMenu style="width: 400px") + button(style="display: block; width: 0; height: 0;") + .px-2 + .row.form-group + label.col-5.col-form-label.d-flex.justify-content-between + | light color + custom-checkbox([(checked)]="editor.customLight") + .col-7 + color-picker([(color)]="editor.lightColor" [isDisabled]="!editor.customLight") + .row.form-group.mb-0 + label.col-5.col-form-label shadow opacity ({{shadowOpacity}}) + .col-7.py-2 + slider-bar([(value)]="shadowOpacity" [min]="0" [max]="255") + + .dropdown.ml-1(dropdown) + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle) + | engine + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let e of engines" (click)="setEngine(e)" [class.active]="isActiveEngine(e)") + | {{e.name}} + + .btn.btn-sm.btn-default.disabled.ml-1(*ngIf="hasElevation") + | elevation: {{editorElevation}} + .btn.btn-sm.btn-default.disabled.ml-1(*ngIf="hasElevation") + | special: {{editorSpecial}} diff --git a/src/ts/components/app/editor-box/editor-box.ts b/src/ts/components/app/editor-box/editor-box.ts new file mode 100644 index 0000000..0374952 --- /dev/null +++ b/src/ts/components/app/editor-box/editor-box.ts @@ -0,0 +1,205 @@ +import { Component, NgZone } from '@angular/core'; +import { uniq } from 'lodash'; +import { faEdit, faCog, faDrawPolygon, faTrash, emptyIcon, faCheck } from '../../../client/icons'; +import { PonyTownGame, engines } from '../../../client/game'; +import { StorageService } from '../../services/storageService'; +import { Model, getEntityNameFromType, getEntityNames } from '../../services/model'; +import { getAlpha, withAlpha, colorToHexRGB, parseColor } from '../../../common/color'; +import { BLACK } from '../../../common/colors'; +import { Entity, Engine, EngineInfo, DebugFlags, tileTypeNames } from '../../../common/interfaces'; + +@Component({ + selector: 'editor-box', + templateUrl: 'editor-box.pug', +}) +export class EditorBox { + readonly dev = DEVELOPMENT; + readonly cogIcon = faCog; + readonly editIcon = faEdit; + readonly selectIcon = faDrawPolygon; + readonly deleteIcon = faTrash; + readonly checkIcon = faCheck; + readonly emptyIcon = emptyIcon; + readonly tiles = ['---', ...tileTypeNames]; + readonly engines = engines; + readonly editorEntities: string[]; + readonly showFields: (keyof DebugFlags)[] = ['id', 'bounds', 'collider', 'cover', 'interact', 'trigger']; + private showEditor = false; + constructor( + public model: Model, + private game: PonyTownGame, + private storage: StorageService, + private zone: NgZone, + ) { + this.game.editor.type = this.storage.getItem('editor-entity') || 'rock'; + this.showEditor = this.storage.getBoolean('show-editor'); + this.editorEntities = getEntityNames().slice().sort(); + } + get editor() { + return this.game.editor; + } + get hasElevation() { + return this.game.engine === Engine.LayeredTiles; + } + get editorElevation() { + return this.game.editor.elevation; + } + get editorSpecial() { + return this.game.editor.special; + } + get editorEntity() { + return this.game.editor.type; + } + set editorEntity(value: string) { + this.game.editor.type = value; + this.storage.setItem('editor-entity', value); + } + get editorTile() { + return this.game.editor.tile; + } + set editorTile(value: number) { + this.game.editor.tile = value; + } + get hasEditor() { + return this.model.isMod && this.showEditor; + } + get oneEntity() { + return this.editor.selectedEntities[0]; + } + get singleEntity() { + return this.editor.selectedEntities.length === 1; + } + get hasSelectedEntities() { + return this.editor.selectedEntities.length > 0; + } + get isLightEntity() { + return this.editor.selectedEntities.some(e => !!e.drawLight); + } + get isLightSpriteEntity() { + return this.editor.selectedEntities.some(e => !!e.drawLightSprite); + } + get selectingEntities() { + return this.game.editor.selectingEntities; + } + set selectingEntities(value) { + this.game.editor.selectingEntities = value; + } + get shadowOpacity() { + return getAlpha(this.game.shadowColor); + } + set shadowOpacity(value) { + this.game.shadowColor = withAlpha(this.game.shadowColor, value); + } + private getEntityName(type: number) { + return getEntityNameFromType(type); + } + private getEntityValue(map: (entity: Entity) => T) { + const entity = this.editor.selectedEntities[0] as any; + return map(entity); + } + get entityName() { + const entities = this.editor.selectedEntities; + const types = entities.map(e => e.type); + const names = uniq(types).map(type => this.getEntityName(type)).join(', '); + return types.length === 1 ? `${names} [${entities[0].id}]` : names; + } + get entityLightColor() { + return colorToHexRGB(this.getEntityValue(e => e && e.lightColor || BLACK)); + } + set entityLightColor(value) { + this.editor.selectedEntities.forEach(e => e.lightColor = parseColor(value)); + } + get entityLightSpriteColor() { + const entity = this.editor.selectedEntities[0]; + return colorToHexRGB(entity && entity.lightSpriteColor || BLACK); + } + set entityLightSpriteColor(value) { + this.editor.selectedEntities.forEach(e => e.lightSpriteColor = parseColor(value)); + } + get entityLightSpriteX() { + const entity = this.editor.selectedEntities[0]; + return entity && entity.lightSpriteX || 0; + } + set entityLightSpriteX(value) { + console.log('set x', value, this.editor.selectedEntities); + this.editor.selectedEntities.forEach(e => e.lightSpriteX = value); + } + get entityLightSpriteY() { + const entity = this.editor.selectedEntities[0]; + return entity && entity.lightSpriteY || 0; + } + set entityLightSpriteY(value) { + this.editor.selectedEntities.forEach(e => e.lightSpriteY = value); + } + get entityLightScale() { + return this.editor.selectedEntities.length ? this.editor.selectedEntities[0].lightScaleAdjust : 1; + } + set entityLightScale(value) { + this.editor.selectedEntities.forEach(e => e.lightScaleAdjust = value); + } + get entityX() { + return this.oneEntity.x; + } + set entityX(value) { + this.oneEntity.x = value; + const { id, x, y } = this.oneEntity; + this.game.send(server => server.editorAction({ + type: 'move', + entities: [{ id, x, y }], + })); + } + get entityY() { + return this.oneEntity.y; + } + set entityY(value) { + this.oneEntity.y = value; + const { id, x, y } = this.oneEntity; + this.game.send(server => server.editorAction({ + type: 'move', + entities: [{ id, x, y }], + })); + } + editorClear() { + this.game.send(server => server.editorAction({ type: 'clear' })); + } + clearLocalStorage() { + this.storage.clear(); + } + setEngine(engine: EngineInfo) { + this.game.engine = engine.engine; + } + isActiveEngine(engine: EngineInfo) { + return this.game.engine === engine.engine; + } + toggleEditor() { + this.zone.run(() => { + this.showEditor = !this.showEditor; + this.storage.setBoolean('show-editor', this.showEditor); + }); + } + toggleSelecting() { + this.selectingEntities = !this.selectingEntities; + + if (!this.selectingEntities) { + this.editor.selectedEntities.length = 0; + } + } + listEntities() { + this.game.send(server => server.editorAction({ type: 'list' })); + } + deleteEntities() { + const entities = this.editor.selectedEntities.map(e => e.id); + this.game.send(server => server.editorAction({ type: 'remove', entities })); + this.editor.selectedEntities.length = 0; + } + showEntitiesInfo() { + console.log(this.editor.selectedEntities); + } + toggleShow(field: keyof DebugFlags) { + (this.game.debug as any)[field] = !this.isShow(field); + this.game.saveDebug(); + } + isShow(field: keyof DebugFlags) { + return !!this.game.debug[field]; + } +} diff --git a/src/ts/components/app/help/help.pug b/src/ts/components/app/help/help.pug new file mode 100644 index 0000000..02fed9f --- /dev/null +++ b/src/ts/components/app/help/help.pug @@ -0,0 +1,288 @@ +h1(focusTitle) Help + +.row.text-large + .col-md-7 + + section + h2 Contact + p For all inquiries please send an email to #[b {{contactEmail}}] + + section + h2 Controls + + ul.spaced-list + li + b movement + .pl-2.mb-1. + Use + #[kbd-key(title="up arrow key"): fa-icon([icon]="upIcon")] + #[kbd-key(title="left arrow key"): fa-icon([icon]="leftIcon")] + #[kbd-key(title="down arrow key"): fa-icon([icon]="downIcon")] + #[kbd-key(title="right arrow key"): fa-icon([icon]="rightIcon")] + , + #[kbd-key(title="W key") W] + #[kbd-key(title="A key") A] + #[kbd-key(title="S key") S] + #[kbd-key(title="D key") D] + , + #[kbd-key.gamepad.gamepad-stick(title="Gamepad left stick") L#[.t]#[.l]#[.r]#[.b]] + or #[b touch and drag] to move. + #[br] + Hold #[kbd-key(title="shift key") shift] to walk slowly. + li + b chat + .pl-2.mb-1. + Use #[kbd-key(title="enter key") enter] + or #[kbd-key.gamepad.gamepad-blue(title="Gamepad X button") X] + to open chat box and send a message. + #[br] + Use #[kbd-key(title="escape key") esc] + or #[kbd-key.gamepad.gamepad-blue(title="Gamepad X button") X] + to cancel the message. + li. + hold #[kbd-key(title="shift key") shift] to be able to click on ground and items behind other players + li. + #[b boop] - + #[kbd-key(title="B key") B] , + #[kbd-key.gamepad.gamepad-red(title="Gamepad B button") B] , + #[b two finger tap] + or #[code /boop] command + li. + #[b interact] or #[b use item] - #[kbd-key(title="E key") E] or + #[kbd-key.gamepad.gamepad-green(title="Gamepad A button") A] + li. + #[b land] or #[b sit down] or #[b lie down] - + #[kbd-key(title="X key") X] , + #[kbd-key.gamepad.gamepad-dpad(title="Gamepad dpad down button") #[.dpad-down]] + or #[code /stand] and #[code /sit] and #[code /lie] commands + li. + #[b sit up] or #[b stand up] or #[b fly up] - + #[kbd-key(title="C key") C] , + #[kbd-key.gamepad.gamepad-dpad(title="Gamepad dpad up button") #[.dpad-up]] + or #[code /sit] and #[code /stand] and #[code /fly] commands + li. + #[b turn head] - + #[kbd-key(title="H key") H] + or #[kbd-key.gamepad.gamepad-stick(title="Gamepad right stick") R#[.t]#[.l]#[.r]#[.b]] + or #[code /turn] command + li. + #[b zoom in / out] - + #[kbd-key(title="P key") P] and #[kbd-key(title="O key") O] + or #[kbd-key.gamepad.gamepad-yellow(title="Gamepad Y button") Y] + li. + #[b hide all text] - + #[kbd-key(title="F2 key") F2] + li. + #[b hide public chat] - + #[kbd-key(title="F3 key") F3] + li. + #[b see through objects] - + #[kbd-key(title="F4 key") F4] + li + | #[b fullscreen] - + span(*ngIf="!mac") + | #[kbd-key(title="F11 key") F11] + span(*ngIf="mac") + | #[kbd-key(title="Control key") ctrl] + #[kbd-key(title="command key") ⌘] + #[kbd-key(title="F key") F] + + section + h2 Expressions + p To change your pony face expression in game: + + ul.spaced-list + li type any emote in the chat :) + li end your message with an emote Hey there :D + li change expression without saying anything, by writing the emote as a chat command /:D + li make the expression permanent using /e :( command and use /e to get your default expression back + li use both horizontal and vertical emotes: >:( D: O_o ^c^ 6.9 + li use emotes with blushies like O//O or O//v//O + + section + h2 Chat commands + p Following chat commands are available in the game chat: + + ul.spaced-list + li /help, /h or /? - list all available commands + li + | /roll, /rand or /random - roll a random number between 1 and 100 + ul.spaced-list + li use /roll 10 to roll a number between 1 and 10 + li use /roll 5-10 to roll a number between 5 and 10 + li /say or /s - say in public chat (can be used to switch from party chat) + li /party or /p - say in party chat + li /think or /t - say in thinking bubble + li /whisper <name> or /w <name> - whisper to player (you can also click on player name in chatlog) + li /reply or /r - reply to last incoming whisper + li /e - set a permanent expression like this /e :) + li /unstuck - resets position to spawn point and leaves the game + li /leave - leaves the game + li /turn - turn head around + li /magic - toggle magic light + li /drop - drop held item + li /droptoy - drop held toy + li /open - open gift + li /toys - show number of collected toys + li /boop or /) - do a boop. Can also be used with expression: /) XD + li /lie - lie down or sit up + li /sit - sit down or stand up + li /stand - stand up or land + li /fly - fly up or land + li /sleep or /zzz - fall asleep. Can also be used with some expressions: /zzz :) + li /love or /<3 - show floating hearts. Can also be used with expressions: /love :) + li /cry - start crying. Can also be used with expressions: /cry :D + li /blush - start blushing. Can also be used with expressions: /blush :D + li. + Following expression commands are available: + /smile /happy + /frown + /angry + /sad + /thinking + /yawn + /laugh /haha /хаха /jaja /lol + /sneeze /achoo + li + strong House saving + ul + li /savehouse - saves current house setup (single save state for entire account) + li /loadhouse - loads saved house setup (need to be alone in the house or a party leader) + li /resethouse - resets house setup to original state (need to be alone in the house or a party leader) + li /lockhouse - prevents other people from changing the house (need to be alone in the house or a party leader) + li /unlockhouse - enables editing by other people (need to be alone in the house or a party leader) + li /removetoolbox - removes toolbox from the house (need to be alone in the house or a party leader) + li /restoretoolbox - restores toolbox to the house (need to be alone in the house or a party leader) + + section + h4 Supporter-only commands + + ul.spaced-list + li /ss - supporter colored text + li /s1 - tier 1 supporter colored text (for use by higher tier supporters) + li /s2 - tier 2 supporter colored text (for use by higher tier supporters) + li /s3 - tier 3 supporter colored text (for use by higher tier supporters) + + section + h2 Chat emojis + p. + You can use emojis in chat by typing their name surrounded by colons #[samp :apple:] + or by using unicode characters assigned to them. Available emotes: + + .emotes + ul + li(*ngFor="let e of emotes") + span.emote-sample.mr-2 + emote-box([emote]="e") + | :{{e}}: + + .col-md-5 + section + h2 Common issues + + section + h4 Keys stuck when using iPad keyboard + p.text-fading. + Due to a bug on iOS system some keys will get stuck occasionally, + when the key gets stuck just press it again to unstuck it. + p.text-fading. + We recommend you use arrow keys for movement as those keys do not get stuck. + + section + h4 Other players can't see my messages + p.text-fading. + This happens when you get temporarily muted for breaking the rules. + This typically takes from a few hours to a few days depending on severity and frequency of the violation. + p.text-fading. + Avoid breaking the rules to prevent this from happening in the future. + + section + h4 Page not loading or stuck updating + p.text-fading. + Try reloading and clearing your browser data. + If that doesn't help you should check if any of the extensions are causing the issue, + you can check that by using incognito mode of your browser. + + section + h4 Mobile keyboard is covering the chat box when the game is installed on mobile device + p.text-fading. + Try re-installing the game. + + section + h4 Screen black or blinking after joining the game + p.text-fading. + This is usually caused by outdated graphics drivers or browser. + Check if your browser is #[a(href="https://updatemybrowser.org/") up to date] and check + if there are new updates for your graphics card or device. + p.text-fading. + If your browser is already up to date, try installing different browser. + We recommend using #[a(href="https://www.google.com/chrome/") Chrome] + or #[a(href="https://www.mozilla.org/en-US/firefox/new/") Firefox]. + p.text-fading. + Some older devices don't support necessary functions and will never work with the game. + + section + h4 Graphical glitches, disappearing objects + p.text-fading. + This can be caused by outdated drivers or not enough graphics memory, + see previous points for possible solutions. + + section + h4 Cannot connect to the game + p.text-fading. + This can be caused by outdated browser or network issues. + Try updating your browser or installing a different one, see previous points for more info. + p.text-fading. + Connection issue might also be a result of our DDOS protection service, + reloading the page and trying again should solve the issue in that case. + + section + h4 Game slow or stuttering + p.text-fading. + This can be caused by outdated browser or some browser extensions. + p.text-fading. + Adblock extension on #[b Chrome] browser is known to cause stuttering in the game, + make sure to disable it to avoid any issues. Other similar extensions may also cause + slowdowns and stuttering in the game. + p.text-fading. + Firefox is known to have issues with stuttering, we recommend + using #[b Chrome] or #[b Edge] browsers for best performance. + + section + h4 Why can't I create multiple accounts + p.text-fading. + This restriction is in-place to protect you and the game itself from other players in the game. + p.text-fading. + Allowing multiple accounts for single person invalidates ignore and hide systems that + allow you to ignore and hide other players that are harassing or bothering you in any way. + It also invalidates mute and ban system that is in-place to protect you from players breaking + the game rules to bother or harass others. + p.text-fading. + Allowing use of multiple accounts at the same time would dramatically increase load on the + server and potentially make the game unplayable and laggy for everyone else. Increased amount + of player characters in the game would also reduce rendering performance on slower computers, + especially on mobile devices to the point when a lot of players wouldn't be able to play the + game anymore. + p.text-fading. + Use of multiple accounts also invalidates any limits (like character limit) that we put on the accounts. + The limits are in-place to prevent large amount of players that we have at the moment from + exhausting our limited server resources. + + section + h4 Why can't I modify the game with hacks or scripts + p.text-fading. + This restriction is in-place to protect you and the game itself from other players in the game. + p.text-fading. + Almost all third-party scripts used in-game were used to harass other players. Even when the + creator of the script did not intend it to be used in that way, the limited knowledge of the + game and security issues can lead to unintentionally creating a script that's harmful to other + players in the game. + p.text-fading. + Even in the case where the script doesn't seem to impact other players, it often causes performance + issues for the server or other players. That forces us to work on putting more limits on player + interactions and takes development time away from adding new features to the game. + p.text-fading. + Adding more limits on player interactions also makes the game less responsive and more limiting + for everyone, not only the players using scripts. This is especially bad for players on bad + internet connections where network issues can be misinterpreted as use of scripts or spam. + +.text-center.text-muted.mt-3 + .tinyface diff --git a/src/ts/components/app/help/help.scss b/src/ts/components/app/help/help.scss new file mode 100644 index 0000000..d41660f --- /dev/null +++ b/src/ts/components/app/help/help.scss @@ -0,0 +1,21 @@ +.emote-sample { + display: inline-block; + width: 24px; + text-align: center; + margin-bottom: 3px; + + &:hover { + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + } +} + +.emotes { + column-width: 180px; +} + +.spaced-list { + > li { + padding-bottom: 2px; + } +} diff --git a/src/ts/components/app/help/help.ts b/src/ts/components/app/help/help.ts new file mode 100644 index 0000000..d9afe00 --- /dev/null +++ b/src/ts/components/app/help/help.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { emojis } from '../../../client/emoji'; +import { faArrowLeft, faArrowRight, faArrowUp, faArrowDown } from '../../../client/icons'; +import { contactEmail } from '../../../client/data'; + +@Component({ + selector: 'help', + templateUrl: 'help.pug', + styleUrls: ['help.scss'], +}) +export class Help { + readonly leftIcon = faArrowLeft; + readonly rightIcon = faArrowRight; + readonly upIcon = faArrowUp; + readonly downIcon = faArrowDown; + readonly emotes = emojis.map(e => e.names[0]); + readonly mac = /Macintosh/.test(navigator.userAgent); + readonly contactEmail = contactEmail; +} diff --git a/src/ts/components/app/home/home.pug b/src/ts/components/app/home/home.pug new file mode 100644 index 0000000..7dd7d1c --- /dev/null +++ b/src/ts/components/app/home/home.pug @@ -0,0 +1,32 @@ +div(*ngIf="!playing") + .text-center.heading + img.pixelart.home-logo(revSrc="images/logo-large.png" alt="Pony Town") + + .mx-auto.home-content + page-loader + + div(*ngIf="!loading") + h1.sr-only(focusTitle) Home + + .form-group(*ngIf="authError") + .alert.alert-danger(role="alert") + | {{authError}} + .alert.alert-warning(role="alert" *ngIf="accountAlert") + | {{accountAlert}} + + .form-group(*ngIf="!account") + sign-in-box((signIn)="signIn($event)") + + div(*ngIf="!!account") + .form-group + support-button + .form-group(*ngIf="canInstall") + install-button + .form-group + character-select([(error)]="error" [editButton]="true" (preview)="previewPony = $event") + .form-group + character-preview([pony]="previewInfo" [state]="state" [passive]="true" [name]="previewName" [tag]="previewTag") + .form-group.text-center + play-box([(error)]="error") + + play-notice diff --git a/src/ts/components/app/home/home.scss b/src/ts/components/app/home/home.scss new file mode 100644 index 0000000..5684b76 --- /dev/null +++ b/src/ts/components/app/home/home.scss @@ -0,0 +1,26 @@ +@import '../../../../styles/partials/variables'; + +.home-logo { + width: 574px; + + @include media-breakpoint-down(sm) { + width: 287px; + } +} + +.home-content { + max-width: 400px; + margin-top: 50px; + + @include media-breakpoint-down(md) { + margin-top: 15px; + } +} + +.heading { + margin-bottom: 50px; + + @include media-breakpoint-down(sm) { + margin-bottom: 20px; + } +} diff --git a/src/ts/components/app/home/home.ts b/src/ts/components/app/home/home.ts new file mode 100644 index 0000000..faa6478 --- /dev/null +++ b/src/ts/components/app/home/home.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { Model, getPonyTag } from '../../services/model'; +import { defaultPonyState } from '../../../client/ponyHelpers'; +import { GameService } from '../../services/gameService'; +import { InstallService } from '../../services/installService'; +import { OAuthProvider, PonyObject } from '../../../common/interfaces'; + +@Component({ + selector: 'home', + templateUrl: 'home.pug', + styleUrls: ['home.scss'], +}) +export class Home { + state = defaultPonyState(); + previewPony: PonyObject | undefined = undefined; + error?: string; + constructor( + private gameService: GameService, + private model: Model, + private installService: InstallService, + ) { + } + get authError() { + return this.model.authError; + } + get accountAlert() { + return this.model.accountAlert; + } + get canInstall() { + return this.installService.canInstall; + } + get playing() { + return this.gameService.playing; + } + get loading() { + return this.model.loading || this.model.updating; + } + get account() { + return this.model.account; + } + get pony() { + return this.model.pony; + } + get previewInfo() { + return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo; + } + get previewName() { + return this.previewPony ? this.previewPony.name : this.pony.name; + } + get previewTag() { + return getPonyTag(this.previewPony || this.pony, this.account); + } + signIn(provider: OAuthProvider) { + this.model.signIn(provider); + } +} diff --git a/src/ts/components/services/adminModel.ts b/src/ts/components/services/adminModel.ts new file mode 100644 index 0000000..48b01c9 --- /dev/null +++ b/src/ts/components/services/adminModel.ts @@ -0,0 +1,617 @@ +import { NgZone, Injectable } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { merge, remove, repeat, noop } from 'lodash'; +import { SocketService, createClientSocket } from 'ag-sockets/dist/browser'; +import { fromNow } from '../../common/utils'; +import { DAY, MINUTE } from '../../common/constants'; +import { AccountData, AccountCounters } from '../../common/interfaces'; +import { + Account, Auth, Character, Origin, OriginInfo, Event, AdminState, UpdateOrigin, AccountFlags, IAdminServerActions, + FindPonyQuery, AuthUpdate, ItemCounts, SupporterFlags, GameServerSettings, Settings, + AccountState, MergeAccountData, FindAccountQuery, ClearOrignsOptions, OriginInfoBase, PonyIdDateName, Stats, BaseValues, +} from '../../common/adminInterfaces'; +import { ClientAdminActions } from '../../client/clientAdminActions'; +import { LiveCollection } from './liveCollection'; +import { socketOptions, token } from '../../client/data'; +import { getUrl } from '../../client/rev'; +import { + formatChat, formatEventDesc, getId, banMessage, parsePonies +} from '../../common/adminUtils'; +import { StorageService } from './storageService'; +import { decompressPonyString } from '../../common/compressPony'; +import { ModelSubscriber } from './modelSubscriber'; + +interface FindPoniesResult { + items: string[]; + totalCount: number; +} + +const notification = window.Notification; + +function shouldNotify(e: Event) { + return !/^(Spam|Suspicious message|Invalid account|Invite limit reached|Timed out for (swearing|spamming))$/i + .test(e.message); +} + +@Injectable({ providedIn: 'root' }) +export class AdminModel { + account?: AccountData; + counts: ItemCounts = { + accounts: 0, + characters: 0, + auths: 0, + origins: 0, + }; + initialized = false; + state: AdminState = { + status: { + diskSpace: '', + memoryUsage: '', + certificateExpiration: '', + lastPatreonUpdate: '', + }, + loginServers: [ + { + updating: false, + dead: false, + }, + ], + gameServers: [], + }; + updated?: (list: string, added: boolean) => void; // TODO: remove this + duplicateEntries?: string[]; + error?: string; + log = (..._: any[]) => { }; + get loading(): boolean { + return !this.account; + } + accountPromise!: Promise; + accounts: ModelSubscriber; + origins: ModelSubscriber; + auths: ModelSubscriber; + ponies: ModelSubscriber; + accountAuths: ModelSubscriber; + accountPonies: ModelSubscriber; + accountOrigins: ModelSubscriber; + private liveEvents: LiveCollection; + private handleError = (error: Error) => { + console.error(error); + this.error = error.message; + return undefined; + } + private checkError = (promise: Promise) => promise.catch(this.handleError) as Promise; + private running = true; + private initializedLive = false; + private socket: SocketService; + private resolveAccount!: (account: AccountData) => void; + private initAccountPromise() { + this.accountPromise = new Promise(resolve => { + this.resolveAccount = resolve; + }); + } + constructor(private sanitizer: DomSanitizer, private storage: StorageService, zone: NgZone) { + this.initAccountPromise(); + this.socket = createClientSocket( + { ...socketOptions() }, token, undefined, zone.run.bind(zone)); + + (window as any).model = this; + + if (this.socket) { + this.socket.client = new ClientAdminActions(this); + this.socket.connect(); + } + + this.accounts = new ModelSubscriber('accounts', this.socket, { + fix: account => { + account.createdAt = new Date(account.createdAt!); + account.updatedAt = new Date(account.updatedAt!); + account.lastVisit = account.lastVisit && new Date(account.lastVisit); + + if (account.alert) { + account.alert.expires = new Date(account.alert.expires); + } + }, + }); + + this.auths = new ModelSubscriber('auths', this.socket, { + fix: account => { + account.updatedAt = new Date(account.updatedAt!); + account.lastUsed = account.lastUsed && new Date(account.lastUsed); + }, + }); + + this.ponies = new ModelSubscriber('ponies', this.socket, { + fix: character => { + character.createdAt = new Date(character.createdAt!); + character.updatedAt = new Date(character.updatedAt!); + character.lastUsed = character.lastUsed && new Date(character.lastUsed); + }, + }); + + this.origins = new ModelSubscriber('origins', this.socket, {}); + + this.accountAuths = new ModelSubscriber('accountAuths', this.socket, {}, []); + this.accountPonies = new ModelSubscriber('accountPonies', this.socket, {}, []); + this.accountOrigins = new ModelSubscriber('accountOrigins', this.socket, {}, []); + + this.liveEvents = new LiveCollection('events', 1000, getId, { + decode: decodeEvent, + onUpdated: (added, all) => { + if (all.length) { + this.log(`events ${all.length}`); + } + + all.forEach(e => { + e.descHTML = this.sanitizer.bypassSecurityTrustHtml(formatEventDesc(e.desc)); + }); + + this.callUpdated('events', !!added); + this.updateTitle(); + + if (this.notifications) { + added.filter(shouldNotify).forEach(e => this.notify(e.message, e.desc)); + all.filter(e => e.count === 10).forEach(e => this.notify(e.message, e.desc)); + } + }, + onDelete: () => this.updateTitle(), + }, this.socket); + + if (!this.socket) { + this.initialize(true); + } + } + get server() { + return this.socket.server; + } + get notifications() { + return this.storage.getItem('admin-notifications') === 'true'; + } + get connected() { + return this.socket.isConnected; + } + get events() { + return this.liveEvents.items; + } + get loaded() { + return this.liveEvents.finished; + } + initialize(live: boolean) { + if (this.initializedLive) + return; + + notification.requestPermission(); + + this.initializedLive = true; + this.server.getSignedAccount() + .then(account => { + this.account = account; + this.updateState(); + this.resolveAccount(account); + + if (live) { + setTimeout(() => this.liveEvents.live(), 100); + setInterval(() => this.checkDuplicateEntries(), 60 * MINUTE); + setInterval(() => { + if (this.connected) { + this.getCounts().then(counts => this.counts = counts || this.counts); + } + }, 5 * 1000); + } + }); + } + connectedToSocket() { + this.accounts.connected(); + this.origins.connected(); + this.auths.connected(); + this.ponies.connected(); + this.accountAuths.connected(); + this.accountPonies.connected(); + this.accountOrigins.connected(); + this.updateTitle(); + } + checkDuplicateEntries(force = false) { + return this.server.getDuplicateEntries(force) + .then(entries => this.duplicateEntries = entries || []) + .catch(noop); + } + stop() { + this.running = false; + this.liveEvents.stop(); + } + toggleNotifications() { + this.storage.setItem('admin-notifications', this.notifications ? 'false' : 'true'); + } + // other + getCounts() { + return this.checkError(this.server.getCounts()); + } + getRequestStats() { + return this.checkError(this.server.getRequestStats()); + } + getOtherStats() { + return this.checkError(this.server.getOtherStats()); + } + // auths + getAuth(id: string) { + return this.checkError(this.server.getAuth(id)); + } + getAuthsForAccount(accountId: string) { + return this.checkError(this.server.getAuthsForAccount(accountId)); + } + removeAuth(id: string) { + return this.checkError(this.server.removeAuth(id)); + } + assignAuth(authId: string, accountId: string) { + return this.checkError(this.server.assignAuth(authId, accountId)); + } + updateAuth(id: string, update: AuthUpdate) { + return this.checkError(this.server.updateAuth(id, update)); + } + setAuthPledged(id: string, pledged: number) { + return this.checkError(this.server.updateAuth(id, { pledged })); + } + // ponies + getPoniesCreators(accountId: string) { + return this.checkError(this.server.getPoniesCreators(accountId)); + } + getPoniesForAccount(accountId: string) { + return this.checkError(this.server.getPoniesForAccount(accountId)); + } + getPonyInfo(pony: Character) { + return this.server.getPonyInfo(pony._id) + .then(data => { + if (data) { + pony.info = data.info; + pony.ponyInfo = decompressPonyString(data.info, false); + pony.lastUsed = data.lastUsed ? new Date(data.lastUsed) : undefined; + pony.creator = data.creator; + } + }); + } + removePony(id: string) { + return this.checkError(this.server.removePony(id)); + } + assignPony(ponyId: string, accountId: string) { + return this.checkError(this.server.assignPony(ponyId, accountId)); + } + findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise { + return this.checkError(this.server.findPonies(query, page, skipTotalCount)); + } + removePoniesAboveLimit(account: string) { + return this.checkError(this.server.removePoniesAboveLimit(account)); + } + removeAllPonies(account: string) { + return this.checkError(this.server.removeAllPonies(account)); + } + createPony(accountId: string, name: string, info: string) { + return this.checkError(this.server.createPony(accountId, name, info)); + } + restorePonies(accountId: string, chatlog: string, onlyIds?: string[]) { + const ponies = parsePonies(chatlog, onlyIds); + return Promise.all(ponies.map(({ name, info }) => this.createPony(accountId, name, info))); + } + // origins + updateOrigin(origin: UpdateOrigin) { + return this.checkError(this.server.updateOrigin(origin)); + } + removeOriginsForAccount(accountId: string, ips: string[]) { + return this.checkError(this.server.removeOriginsForAccount(accountId, ips)); + } + clearOriginsForAccount(accountId: string, options: ClearOrignsOptions) { + return this.clearOriginsForAccounts([accountId], options); + } + clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions) { + return this.checkError(this.server.clearOriginsForAccounts(accounts, options)); + } + clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions) { + return this.checkError(this.server.clearOrigins(count, andHigher, options)); + } + addOriginToAccount(accountId: string, origin: OriginInfo) { + return this.checkError(this.server.addOriginToAccount(accountId, origin)); + } + getOriginStats() { + return this.checkError(this.server.getOriginStats()); + } + // accounts + getAccount(id: string) { + return this.checkError(this.server.getAccount(id)); + } + getDetailsForAccount(account: Account) { + return this.checkError(this.server.getDetailsForAccount(account._id)); + } + findAccounts(query: FindAccountQuery) { + return this.checkError(this.server.findAccounts(query)); + } + createAccount(name = '') { + return this.checkError(this.server.createAccount(name) + .then(id => (console.log(`created account: [${id}]`), id))); + } + getAccountStatus(accountId: string) { + return this.checkError(this.server.getAccountStatus(accountId)); + } + getAccountAround(accountId: string) { + return this.checkError(this.server.getAccountAround(accountId)); + } + getAccountHidden(accountId: string) { + return this.checkError(this.server.getAccountHidden(accountId)); + } + getAccountFriends(accountId: string) { + return this.checkError(this.server.getAccountFriends(accountId)); + } + getAllDuplicatesQuickInfo(accountId: string) { + return this.checkError(this.server.getAllDuplicatesQuickInfo(accountId)); + } + getAllDuplicates(accountId: string) { + return this.checkError(this.server.getAllDuplicates(accountId)); + } + getAccountsByEmails(emails: string[]) { + return this.checkError(this.server.getAccountsByEmails(emails)); + } + getAccountsByOrigin(ip: string) { + return this.checkError(this.server.getAccountsByOrigin(ip)); + } + removeAccount(accountId: string) { + return this.checkError(this.server.removeAccount(accountId)); + } + setAlert(accountId: string, message: string, expiresIn: number) { + return this.checkError(this.server.setAlert(accountId, message, expiresIn)); + } + setName(accountId: string, name: string) { + return this.checkError(this.server.setName(accountId, name)); + } + setAge(accountId: string, age: number) { + return this.checkError(this.server.setAge(accountId, age)); + } + setRole(accountId: string, role: string, set: boolean) { + return this.checkError(this.server.setRole(accountId, role, set)); + } + setNote(accountId: string, note: string) { + const account = this.accounts.get(accountId); + + if (account && account.note !== note) { + account.note = note; + account.noteUpdated = new Date(); + } + + return this.checkError(this.server.updateAccount(accountId, { note })); + } + setAccountFlags(accountId: string, flags: AccountFlags) { + return this.checkError(this.server.updateAccount(accountId, { flags })); + } + setSupporterFlags(accountId: string, supporter: SupporterFlags) { + return this.checkError(this.server.updateAccount(accountId, { supporter })); + } + setAccountBanField(accountId: string, field: string, value: number) { + return this.checkError(this.server.updateAccount(accountId, { [field]: value }, banMessage(field, value))); + } + setAccountTimeout(accountId: string, timeout: number) { + return this.checkError(this.server.timeoutAccount(accountId, timeout)); + } + setAccountCounter(accountId: string, name: keyof AccountCounters, value: number) { + return this.checkError(this.server.updateAccountCounter(accountId, name, value)); + } + updateAccount(accountId: string, update: Partial) { + return this.checkError(this.server.updateAccount(accountId, update)); + } + mergeAccounts(accountId: string, withId: string) { + return this.checkError(this.server.mergeAccounts(accountId, withId)); + } + unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { + return this.checkError(this.server.unmergeAccounts(accountId, mergeId, split, keep)); + } + addEmail(accountId: string, email: string) { + return this.checkError(this.server.addEmail(accountId, email)); + } + removeEmail(accountId: string, email: string) { + return this.checkError(this.server.removeEmail(accountId, email)); + } + removeIgnore(accountId: string, ignore: string) { + return this.checkError(this.server.removeIgnore(accountId, ignore)); + } + addIgnores(accountId: string, ignores: string[]) { + return this.checkError(this.server.addIgnores(accountId, ignores)); + } + removeFriend(accountId: string, friendId: string) { + return this.checkError(this.server.removeFriend(accountId, friendId)); + } + addFriend(accountId: string, friendId: string) { + return this.checkError(this.server.addFriend(accountId, friendId)); + } + setAccountState(accountId: string, state: AccountState) { + return this.checkError(this.server.setAccountState(accountId, state)); + } + getIgnoresAndIgnoredBy(accountId: string) { + return this.checkError(this.server.getIgnoresAndIgnoredBy(accountId)); + } + clearSessions(accountId: string) { + return this.checkError(this.server.clearSessions(accountId)); + } + // events + removeEvent(eventId: string) { + // return this.checkError(this.server.removeEvent(eventId)) + return this.liveEvents.remove(eventId) + .then(() => this.callUpdated('events', false)) + .then(() => this.updateTitle()) + .catch(this.handleError); + } + cleanupDeletedEvents() { + if (remove(this.events, e => e.deleted).length) { + this.callUpdated('events', false); + this.updateTitle(); + } + } + // state + updateSettings(settings: Partial) { + return this.server.updateSettings(settings) + .then(() => this.updateState()); + } + updateGameServerSettings(serverId: string, settings: Partial) { + return this.server.updateGameServerSettings(serverId, settings) + .then(() => this.updateState()); + } + report(accountId: string) { + return this.checkError(this.server.report(accountId)); + } + action(action: string, accountId: string) { + return this.checkError(this.server.action(action, accountId)); + } + kick(accountId: string) { + return this.checkError(this.server.kick(accountId)); + } + kickAll(serverId: string) { + return this.server.kickAll(serverId) + .then(() => this.updateState()); + } + getChat(search: string, date?: string, caseInsensitive = false) { + date = date || (new Date()).toISOString(); + return search ? this.server.getChat(search, date, caseInsensitive) : Promise.resolve(''); + } + getChatForAccounts(accountIds: string[], date?: string) { + date = date || (new Date()).toISOString(); + return accountIds.length ? this.server.getChatForAccounts(accountIds, date) : Promise.resolve(''); + } + searchFormattedChat(search: string, date?: string) { + return this.formatChat(this.getChat(search, date, true)); + } + accountsFormattedChat(accountIds: string[], date?: string) { + return this.formatChat(this.getChatForAccounts(accountIds, date)); + } + private formatChat(promise: Promise) { + return this.checkError(promise) + .then(chat => chat === undefined ? 'ERROR' : chat) + .then(raw => ({ raw, html: formatChat(raw) })); + } + fetchServerStats(serverId: string) { + return this.checkError(this.server.fetchServerStats(serverId)); + } + fetchServerStatsTable(serverId: string, stats: Stats) { + return this.checkError(this.server.fetchServerStatsTable(serverId, stats)); + } + notifyUpdate(server: string) { + return this.server.notifyUpdate(server) + .then(() => this.updateState()) + .catch(this.handleError); + } + shutdownServers(server: string) { + return this.server.shutdownServers(server) + .then(() => this.updateState()) + .catch(this.handleError); + } + resetUpdating(server: string) { + return this.server.resetUpdating(server) + .then(() => this.updateState()) + .catch(this.handleError); + } + resetSupporter(accountId: string) { + return this.server.resetSupporter(accountId) + .catch(this.handleError); + } + getLastPatreonData() { + return this.server.getLastPatreonData() + .catch(this.handleError); + } + updatePastSupporters() { + return this.server.updatePastSupporters() + .catch(this.handleError); + } + // other + getTimings(server: string) { + return this.server.getTimings(server) + .catch(this.handleError); + } + teleportTo(accountId: string) { + return this.server.teleportTo(accountId) + .catch(this.handleError); + } + // helpers + get isLowDiskSpace() { + return parseInt(this.state.status.diskSpace || '0', 10) > 95; + } + get isLowMemory() { + return parseInt(this.state.status.memoryUsage || '0', 10) > 90; + } + get isOldCertificate() { + const date = this.state.status.certificateExpiration; + return date && (new Date(date)).getTime() < fromNow(7 * DAY).getTime(); + } + get isOldPatreon() { + const date = this.state.status.lastPatreonUpdate; + return date && (new Date(date)).getTime() < fromNow(-21 * MINUTE).getTime(); + } + private requestState() { + return this.socket.isConnected ? this.server.getState().then(s => this.readState(s)) : Promise.resolve(); + } + private readState(state: AdminState) { + merge(this.state, state); + this.initialized = true; + this.updateTitle(); + } + private updateStateTimeout: any; + private updateState(): void { + if (!this.running) + return; + + clearTimeout(this.updateStateTimeout); + + this.requestState() + .catch((e: Error) => console.error(e.stack)) + .then(() => { + this.updateStateTimeout = setTimeout(() => this.updateState(), 1000); + }); + } + private callUpdated(list: string, added: boolean) { + if (this.updated) { + this.updated(list, added); + } + } + updateTitle() { + const ponies = this.state.gameServers.reduce((sum, s) => sum + s.online, 0); + const count = this.events.reduce((sum, e) => sum + (e.deleted ? 0 : 1), 0); + const inred = this.events.reduce((sum, e) => sum + ((!e.deleted && e.count > 9) ? 1 : 0), 0); + const flag = this.isLowDiskSpace || this.isLowMemory || this.isOldCertificate || this.isOldPatreon; + document.title = `${ponies} | ${count}${repeat('!', inred)}${flag ? ' 🚩' : ''}${!this.connected ? ' ⚠' : ''} | Pony Town`; + } + private notify(title: string, body: string) { + if (this.notifications && notification.permission === 'granted') { + const n = new notification(title, { + body: body || '', + icon: getUrl('images/logo-120.png'), + }); + + n.onclick = () => { + window.focus(); + n.close(); + }; + + n.onshow = () => { + setTimeout(() => n.close(), 4000); + }; + } + } +} + +function decodeDate(value: number | undefined, base: string | undefined): Date { + if (value == null || base == null) { + return new Date(0); + } else { + const d = new Date(base); + d.setTime(d.getTime() + value); + return d; + } +} + +export function decodeEvent(values: any[], base: BaseValues): Event { + return { + _id: values[0], + updatedAt: decodeDate(values[1], base.updatedAt!), + createdAt: decodeDate(values[2], base.createdAt!), + type: values[3], + server: values[4], + message: values[5], + desc: values[6], + count: values[7] | 0, + origin: values[8], + account: values[9], + pony: values[10], + }; +} diff --git a/src/ts/components/services/audio.ts b/src/ts/components/services/audio.ts new file mode 100644 index 0000000..6d5737d --- /dev/null +++ b/src/ts/components/services/audio.ts @@ -0,0 +1,251 @@ +import { Injectable } from '@angular/core'; +import { Howl } from 'howler'; +import { random, sample } from 'lodash'; +import { Season, Holiday, MapType } from '../../common/interfaces'; +import { getUrl } from '../../client/rev'; + +interface Track { + name: string; + src: string[]; + howl?: Howl; +} + +function getTracks(season: Season, holiday: Holiday, map: MapType) { + switch (map) { + case MapType.Island: + return [ + 'island', + 'sunny-island', + ]; + case MapType.House: + return [ + 'happy-house', + 'sweet-home', + ]; + case MapType.Cave: + return [ + 'cave-crystals', + 'cave-secrets', + ]; + default: + return [ + //'largo', + //'musicbox', + //'unrest', + 'bossanova', + 'clop', + 'fivefour', + 'hypnosis', + 'scherzo', + 'trills', + 'waltzalt', + ...(season === Season.Winter ? [ + 'trees-winter', + 'reindeer-winter', + ] : [ + 'trees', + 'reindeer', + ]), + 'season', + 'ambient', + 'building', + 'school', + 'falling', + 'tio', + 'orchid', + ...(season === Season.Winter ? [ + 'xmas-air', + 'xmas-horns', + 'xmas-presents', + ] : []), + ...(holiday === Holiday.Halloween ? [ + 'ghost', + 'pumpkin', + ] : []), + ]; + } +} + +const FADE_TRACKS = true; + +function fadeOut(track: Track, id: number, volume: number) { + const howl = track && track.howl; + + if (howl) { + if (FADE_TRACKS) { + howl + .fade(volume, 0, 1000, id) + .once('fade', () => howl.pause(id).stop(id), id); + } else { + howl + .volume(0, id) + .pause(id) + .stop(id); + } + } +} + +function fadeIn(track: Track, id: number, volume: number) { + if (track && track.howl) { + track.howl.fade(0, volume, 1000, id); + } +} + +interface Instance { + id: number; + track: Track; +} + +@Injectable({ providedIn: 'root' }) +export class Audio { + private tracks: Track[] = []; + private volume = 0; + private loops = 0; + private playing = false; + private stopped: Instance[] = []; + private instance?: Instance; + get trackName() { + return this.instance && this.volume ? this.instance.track.name : ''; + } + initTracks(season: Season, holiday: Holiday, map: MapType) { + const tracks = getTracks(season, holiday, map); + + // Make new tracks more frequent + // const duplicateTracks = tracks.filter(t => t === 'ghost' || t === 'pumpkin'); + // tracks.push(...duplicateTracks); + // tracks.push(...duplicateTracks); + + this.tracks = tracks.map(name => ({ name, src: [getUrl(`music/${name}.webm`), getUrl(`music/${name}.mp3`)] })); + this.loops = 0; + } + setVolume(volume: number) { + this.volume = volume / 100; + + if (this.playing) { + if (this.instance) { + this.setInstanceVolume(this.instance, this.volume); + } else if (this.volume) { + this.playRandomTrack(); + } + } + } + play() { + try { + if (!this.playing) { + this.playing = true; + + if (this.volume) { + if (this.instance) { + this.resumeInstance(this.instance); + } else { + this.playRandomTrack(); + } + } + } + } catch (e) { + console.error(e); + } + } + playOrSwitchToRandomTrack() { + if (FADE_TRACKS) { + if (this.playing && this.volume) { + this.playRandomTrack(); + } else { + this.play(); + } + } else { + this.play(); + } + } + stop() { + if (this.playing) { + this.playing = false; + this.stopInstance(this.instance); + } + } + forcePlay() { + if (!this.instance || !this.instance.track.howl!.playing(this.instance.id)) { + this.playRandomTrack(); + } + } + touch() { + this.stopInstances(); + this.setInstanceVolume(this.instance, this.volume); + } + private switchToTrack(track: Track) { + if (this.instance && this.instance.track === track) { + return false; + } else { + this.stopInstance(this.instance); + this.instance = this.playTrack(track); + return true; + } + } + playRandomTrack() { + while (!this.switchToTrack(sample(this.tracks)!)) + ; + + this.loops = random(4, 7); + } + private playTrack(track: Track): Instance { + this.prepareTrack(track); + const id = track.howl!.play(); + fadeIn(track, id, this.volume); + return { id, track }; + } + private resumeInstance({ track, id }: Instance) { + track.howl!.play(id); + fadeIn(track, id, this.volume); + } + private stopInstance(instance: Instance | undefined) { + if (instance) { + this.stopped.push(instance); + } + + this.stopInstances(); + } + private stopInstances() { + this.stopped.forEach(({ track, id }) => fadeOut(track, id, this.volume)); + this.stopped = this.stopped.filter(({ track, id }) => track.howl!.playing(id)); + } + private setInstanceVolume(instance: Instance | undefined, volume: number) { + if (instance) { + const howl = instance.track.howl!; + howl.volume(volume, instance.id); + + if (volume && !howl.playing(instance.id)) { + howl.play(instance.id); + } else if (!volume && howl.playing(instance.id)) { + howl.pause(instance.id); + } + } + } + private prepareTrack(track: Track) { + if (!track.howl) { + track.howl = new Howl({ + src: track.src, + loop: true, + html5: true, + }); + + track.howl.on('end', id => this.onEnd(id)); + } + } + private handlingOnEnd = 0; + private handlingOnEndAt = 0; + private onEnd(id: number) { + if ( + this.instance && this.instance.id === id && --this.loops < 0 && + (this.handlingOnEnd !== id || this.handlingOnEndAt < performance.now()) + ) { + this.handlingOnEnd = id; + this.handlingOnEndAt = performance.now() + 500; + + if (this.volume && this.playing) { + this.playRandomTrack(); + } else { + this.stopInstance(this.instance); + } + } + } +} diff --git a/src/ts/components/services/authGuard.ts b/src/ts/components/services/authGuard.ts new file mode 100644 index 0000000..b185f5f --- /dev/null +++ b/src/ts/components/services/authGuard.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; +import { Model } from './model'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor(private router: Router, private model: Model) { + } + canActivate() { + return this.model.accountPromise + .then(account => { + if (account) { + return true; + } else { + this.router.navigate(['/']); + return false; + } + }) as any; + } +} diff --git a/src/ts/components/services/errorReporter.ts b/src/ts/components/services/errorReporter.ts new file mode 100644 index 0000000..8c07615 --- /dev/null +++ b/src/ts/components/services/errorReporter.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { ClientErrorHandler, ClientOptions } from 'ag-sockets/dist/browser'; +import { Person } from '../../common/rollbar'; + +@Injectable() +export class ErrorReporter { + disable() { + } + configureUser(_person: Person) { + } + configureData(_data: any) { + } + captureEvent(_data: any) { + } + reportError(error: any, data?: any) { + console.error(error, data); + } + createClientErrorHandler(socketOptions: ClientOptions): ClientErrorHandler { + const handleRecvError = (error: Error, data: string | Uint8Array) => { + if (error.message) { + let method: string | undefined; + + if (data instanceof Uint8Array) { + const bytes: number[] = []; + const length = Math.min(data.length, 200); + + for (let i = 0; i < length; i++) { + bytes.push(data[i]); + } + + const trail = length < data.length ? '...' : ''; + + if (data.length > 0) { + const item = socketOptions.client[data[0]] as string | [string, any]; + method = typeof item === 'string' ? item : item[0]; + } + + data = `<${bytes.toString()}${trail}>`; + } + + this.reportError(error, { data, method }); + } + }; + + return { handleRecvError }; + } +} diff --git a/src/ts/components/services/frameService.ts b/src/ts/components/services/frameService.ts new file mode 100644 index 0000000..02ee716 --- /dev/null +++ b/src/ts/components/services/frameService.ts @@ -0,0 +1,38 @@ +import { Injectable, NgZone } from '@angular/core'; + +export interface FrameLoop { + init(): void; + destroy(): void; +} + +@Injectable({ + providedIn: 'root', +}) +export class FrameService { + constructor(private zone: NgZone) { + } + create(frame: (delta: number) => void) { + const zone = this.zone; + let ref = 0; + let last = 0; + + function tick(now: number) { + ref = requestAnimationFrame(tick); + frame((now - last) / 1000); + last = now; + } + + return { + init() { + if (!ref) { + last = performance.now(); + zone.runOutsideAngular(() => ref = requestAnimationFrame(tick)); + } + }, + destroy() { + cancelAnimationFrame(ref); + ref = 0; + } + }; + } +} diff --git a/src/ts/components/services/gameService.ts b/src/ts/components/services/gameService.ts new file mode 100644 index 0000000..7376eac --- /dev/null +++ b/src/ts/components/services/gameService.ts @@ -0,0 +1,312 @@ +import { Injectable, NgZone, ErrorHandler } from '@angular/core'; +import { merge } from 'lodash'; +import { createClientSocket, SocketService, ClientOptions, ClientErrorHandler } from 'ag-sockets/dist/browser'; +import { ServerInfo, IServerActions, GameStatus, AccountData, LeaveReason } from '../../common/interfaces'; +import { RequestError, findById, includes } from '../../common/utils'; +import { OFFLINE_ERROR, PROTECTION_ERROR, BROWSER_NOT_SUPPORTED_ERROR, VERSION_ERROR } from '../../common/errors'; +import { startGameLoop, GameLoop } from '../../client/gameLoop'; +import { ClientActions } from '../../client/clientActions'; +import { socketOptions } from '../../client/data'; +import { PonyTownGame } from '../../client/game'; +import { Model } from './model'; +import { ErrorReporter } from './errorReporter'; +import { meetsRequirement } from '../../common/accountUtils'; +import { isLanguage, isFocused, sortServersForRussian } from '../../client/clientUtils'; +import { StorageService } from './storageService'; + +export interface ClientSocketService extends SocketService { } + +function createSocket( + gameService: GameService, game: PonyTownGame, model: Model, zone: NgZone, options: ClientOptions, + token: string, errorHandler: ClientErrorHandler +): ClientSocketService { + const socket = createClientSocket(options, token, errorHandler); + socket.client = new ClientActions(gameService, game, model, zone); + + if (!socket.supportsBinary) { + throw new Error(BROWSER_NOT_SUPPORTED_ERROR); + } + + return socket; +} + +@Injectable({ + providedIn: 'root', +}) +export class GameService { + playing = false; + joining = false; + offline = false; + protectionError = false; + rateLimitError = false; + versionError = false; + version?: string; + server?: ServerInfo; + servers: ServerInfo[] = []; + error?: string; + leftMessage?: string; + private safelyLeft = false; + private gameLoop?: GameLoop; + private disconnectedTimeout?: any; + private initialized = false; + private update?: boolean; + private locked = false; + constructor( + private model: Model, + private game: PonyTownGame, + private zone: NgZone, + private errorHandler: ErrorHandler, + private errorReporter: ErrorReporter, + private storage: StorageService, + ) { + this.pollStatus(); + } + get selected() { + return this.game.selected; + } + get account() { + return this.model.account; + } + get canPlay(): boolean { + return !!this.model.pony && + !!this.model.pony.name && + !this.model.pending && + !this.joining && + !!this.server && + !this.server.offline && + !this.rateLimitError && + !this.versionError && + !this.locked; + } + get updateWarning(): boolean { + return !!this.update; + } + get filterSwearWords(): boolean { + return !!(this.server && this.server.filter) + || !!(this.account && this.account.settings && this.account.settings.filterSwearWords); + } + get wasPlaying() { + return this.storage.getBoolean('playing'); + } + join(ponyId: string) { + this.errorReporter.captureEvent({ name: 'Join' }); + const server = this.server; + + if (this.playing || this.joining || !server) { + return Promise.resolve(); + } + + if (typeof WebSocket === 'undefined' || typeof Float32Array === 'undefined') { + return Promise.reject(new Error(BROWSER_NOT_SUPPORTED_ERROR)); + } + + this.joining = true; + this.leftMessage = undefined; + this.safelyLeft = false; + + return this.model.join(server.id, ponyId) + .then(({ token, alert }) => { + if (!this.joining) { + return false; + } + + if (!token) { + this.model.accountAlert = alert; + this.joining = false; + return false; + } + + return this.zone.runOutsideAngular(() => { + const options = { ...socketOptions(), path: server.path, host: server.host }; + const errorHandler = this.errorReporter.createClientErrorHandler(options); + const socket = createSocket(this, this.game, this.model, this.zone, options, token, errorHandler); + + if (this.gameLoop) { + this.gameLoop.cancel(); + } + + this.game.startup(socket, this.model.isMod); + this.gameLoop = startGameLoop(this.game, e => this.handleGameError(e)); + + return this.gameLoop.started + .then(() => { + this.errorReporter.captureEvent({ name: 'gameLoop.started' }); + const socketConnected = this.pollUntilConnected(socket); + socket.connect(); + return socketConnected; + }) + .then(() => { + this.errorReporter.captureEvent({ name: 'socketConnected' }); + return true; + }) + .catch(e => { + this.errorReporter.captureEvent({ name: 'socket.disconnect()', error: e.message }); + socket.disconnect(); + throw e; + }); + }); + }) + .then(joined => { + this.errorReporter.captureEvent({ name: joined ? 'Joined game' : 'Not joined game' }); + }) + .catch((e: RequestError) => { + this.errorReporter.captureEvent({ name: 'Failed to join game', error: e.message }); + + // if (e.status && e.status > 500 && e.status < 500) { + // this.rateLimitError = true; + // setTimeout(() => this.rateLimitError = false, 5000); + // } + + this.zone.run(() => this.left('join.catch')); + throw e; + }); + } + leave(reason: string) { + this.errorReporter.captureEvent({ name: 'Leave', reason }); + this.game.leave(); + this.left('leave'); + } + joined() { + this.errorReporter.captureEvent({ name: 'Joined' }); + this.storage.setBoolean('playing', true); + clearTimeout(this.disconnectedTimeout); + setTimeout(() => { + this.joining = false; + this.playing = true; + }); + } + left(from: string, reason = LeaveReason.None) { + this.errorReporter.captureEvent({ name: 'Left', from, reason }); + this.storage.setBoolean('playing', false); + this.safelyLeft = true; + + if (reason === LeaveReason.Swearing) { + this.leftMessage = 'Kicked for swearing or inappropriate language'; + this.locked = true; + } else { + this.leftMessage = undefined; + } + + if (this.gameLoop) { + this.errorReporter.captureEvent({ name: 'gameLoop.cancel()' }); + this.gameLoop.cancel(); + this.gameLoop = undefined; + } + + clearTimeout(this.disconnectedTimeout); + + setTimeout(() => { + this.joining = false; + this.playing = false; + }); + + if (this.locked) { + setTimeout(() => { + this.locked = false; + }, 7000); + } + + if (this.model.friends) { + for (const friend of this.model.friends) { + friend.online = false; + friend.entityId = 0; + } + } + + this.game.release(); + this.game.onLeft.next(); + } + disconnected() { + this.errorReporter.captureEvent({ name: 'Disconnected' }); + clearTimeout(this.disconnectedTimeout); + + if (!this.safelyLeft) { + this.disconnectedTimeout = setTimeout(() => this.left('disconnected.timeout'), 10000); + } + } + private pollStatus() { + return this.getAndUpdateStatus(this.account) + .finally(() => { + setTimeout(() => this.pollStatus(), this.initialized ? 10000 : 500); + }); + } + private getAndUpdateStatus(account: AccountData | undefined) { + if (this.joining || this.playing || !account || !isFocused()) { + return Promise.resolve(); + } else { + return this.model.status(this.initialized) + .then(status => this.updateStatus(account, status)) + .catch((e: RequestError) => { + DEVELOPMENT && console.error(e); + this.offline = e.message === OFFLINE_ERROR; + this.versionError = e.message === VERSION_ERROR; + this.protectionError = e.message === PROTECTION_ERROR; + }); + } + } + private updateStatus(account: AccountData, status: GameStatus) { + this.initialized = true; + this.offline = false; + this.version = status.version; + this.update = status.update; + + for (const server of status.servers) { + const existing = findById(this.servers, server.id); + + if (existing) { + merge(existing, server); + } else if ('name' in server) { + const info = server as ServerInfo; + info.countryFlags = info.flag && /^[a-z]{2}( [a-z]{2})*$/.test(info.flag) ? info.flag.split(/ /g) : []; + + if (info.name && account && meetsRequirement(account, info.require)) { + this.servers.push(info); + } + } else { + // got new server on the list + this.initialized = false; + } + } + + for (let i = this.servers.length - 1; i >= 0; i--) { + if (!findById(status.servers, this.servers[i].id)) { + this.servers.splice(i, 1); + } + } + + if (isLanguage('ru')) { + this.servers.sort(sortServersForRussian); + } + + if (!this.server && account.settings.defaultServer) { + this.server = findById(this.servers, account.settings.defaultServer); + + if (DEVELOPMENT && /join/.test(this.model.pony.name)) { + setTimeout(() => this.join(this.model.pony.id)); + } + } + + if (!includes(this.servers, this.server)) { + this.server = undefined; + } + } + private handleGameError(error: Error) { + this.errorReporter.captureEvent({ name: 'handleGameError', error: error.message }); + this.error = error.message; + this.errorHandler.handleError(error); + this.leave('handleGameError'); + } + private pollUntilConnected(socket: ClientSocketService) { + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (socket.isConnected) { + clearInterval(interval); + this.zone.run(resolve); + } else if (!this.joining) { + clearInterval(interval); + this.zone.run(() => reject(new Error('Cancelled (poll)'))); + } + }, 10); + }); + } +} diff --git a/src/ts/components/services/installService.ts b/src/ts/components/services/installService.ts new file mode 100644 index 0000000..b53e496 --- /dev/null +++ b/src/ts/components/services/installService.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { StorageService } from './storageService'; + +interface InstallEvent extends Event { + prompt(): void; + userChoice: Promise<'accepted' | 'dismissed'>; +} + +@Injectable({ + providedIn: 'root', +}) +export class InstallService { + private installEvent?: InstallEvent; + constructor(private storage: StorageService) { + if (!this.storage.getBoolean('install-dismissed')) { + window.addEventListener('beforeinstallprompt', event => { + event.preventDefault(); + this.installEvent = event as any; + }); + } + } + get canInstall() { + return !!this.installEvent || (DEVELOPMENT && localStorage.getItem('install')); + } + install() { + if (!this.installEvent) { + return Promise.reject(new Error('Cannot install')); + } + + this.installEvent.prompt(); + + return this.installEvent.userChoice + .finally(() => { + this.installEvent = undefined; + }); + } + dismiss() { + this.installEvent = undefined; + this.storage.setBoolean('install-dismissed', true); + } +} diff --git a/src/ts/components/services/intervalUpdateService.ts b/src/ts/components/services/intervalUpdateService.ts new file mode 100644 index 0000000..1f04415 --- /dev/null +++ b/src/ts/components/services/intervalUpdateService.ts @@ -0,0 +1,44 @@ +import { Injectable, NgZone } from '@angular/core'; +import { removeItem } from '../../common/utils'; + +@Injectable({ + providedIn: 'root', +}) +export class IntervalUpdateService { + private interval: any; + private actions: (() => void)[] = []; + constructor(private zone: NgZone) { + } + subscribe(action: () => void) { + this.actions.push(action); + + if (!this.interval) { + this.zone.runOutsideAngular(() => { + this.interval = setInterval(() => { + this.actions.forEach(a => a()); + }, 1000 * 10); + }); + } + + return () => { + removeItem(this.actions, action); + + if (this.actions.length === 0) { + clearInterval(this.interval); + this.interval = undefined; + } + }; + } + toggle(action: () => void) { + let unsubscribe: (() => void) | undefined; + + return (on: boolean) => { + if (on && !unsubscribe) { + unsubscribe = this.subscribe(action); + } else if (!on && unsubscribe) { + unsubscribe(); + unsubscribe = undefined; + } + }; + } +} diff --git a/src/ts/components/services/liveCollection.ts b/src/ts/components/services/liveCollection.ts new file mode 100644 index 0000000..4c69456 --- /dev/null +++ b/src/ts/components/services/liveCollection.ts @@ -0,0 +1,145 @@ +import { SocketService } from 'ag-sockets/dist/browser'; +import { removeItem } from '../../common/utils'; +import { Document, LiveResponse, IAdminServerActions, BaseValues } from '../../common/adminInterfaces'; +import { ClientAdminActions } from '../../client/clientAdminActions'; + +export interface Options { + // collection + beforeUpdate?: (updates: T[]) => void; + onUpdated?: (added: T[], all: T[]) => void; + onFinished?: () => void; + // item + decode: (fields: any[], base: BaseValues) => T; + onUpdate?: (oldItem: T, newItem: T) => void; + onDelete?: (item: T) => void; + deleteItems?: boolean; + ignore?: (item: T) => boolean; +} + +export class LiveCollection { + items: T[] = []; + finished = false; + private running = true; + private itemsMap = new Map(); + private liveTimeout: any; + constructor( + private name: 'events', + private rate: number, + private getKey: (item: T) => string, + private options: Options, + private socket: SocketService, + private timestamp = (new Date(0)).toISOString(), + private logError = (e: Error) => console.error(e.stack), + ) { + } + get(key: string) { + return this.itemsMap.get(key); + } + push(item: T) { + this.items.push(item); + this.itemsMap.set(this.getKey(item), item); + return item; + } + remove(key: string) { + return this.server.removeItem(this.name, key) + .then(() => this.removeItem(key, true, true)); + } + removeItem(key: string, deleted = false, removeFromList = false) { + const item = this.itemsMap.get(key); + + if (item) { + if (removeFromList || this.options.deleteItems) { + removeItem(this.items, item); + this.itemsMap.delete(key); + } else if (deleted) { + item.deleted = true; + } + + if (deleted && this.options.onDelete) { + this.options.onDelete(item); + } + } + } + assignAccount(id: string, account: string) { + return this.server.assignAccount(this.name, id, account); + } + live(): Promise { + if (!this.running) + return Promise.resolve(); + + clearTimeout(this.liveTimeout); + + return this.update() + .catch(this.logError) + .then(more => { + this.liveTimeout = setTimeout(() => this.live(), more ? 100 : this.rate); + }); + } + stop() { + this.running = false; + } + read({ updates, deletes, base, more }: LiveResponse, liveFetch = true) { + const items = updates.map(i => this.options.decode(i, base)); + + if (liveFetch) { + const timestamp = items + .reduce((max, i) => max.getTime() < i.updatedAt.getTime() ? i.updatedAt : max, new Date(this.timestamp)); + this.timestamp = timestamp.toISOString(); + } + + if (this.options.beforeUpdate) { + this.options.beforeUpdate(items); + } + + const { added, all } = this.applyUpdates(items, liveFetch); + + if (this.options.onUpdated && items.length) { + this.options.onUpdated(added, all); + } + + deletes.forEach(key => this.removeItem(key, true)); + + if (liveFetch) { + const finished = this.finished || !more; + + if (!this.finished && finished) { + this.finished = true; + + if (this.options.onFinished) { + this.options.onFinished(); + } + } + } + + return more; + } + private get server() { + return this.socket.server; + } + private update() { + return this.socket.isConnected ? this.server.getAll(this.name, this.timestamp).then(r => this.read(r)) : Promise.resolve(false); + } + private applyUpdates(updates: T[], liveFetch: boolean) { + const added: T[] = []; + const all: T[] = []; + + updates.forEach(update => { + const doc = this.get(this.getKey(update)); + + if (doc) { + if (this.options.onUpdate) { + this.options.onUpdate(doc, update); + } else { + Object.assign(doc, update); + } + all.push(doc); + } else if (!liveFetch || !this.options.ignore || !this.options.ignore(update)) { + this.push(update); + added.push(update); + all.push(update); + } + }); + + return { added, all }; + } +} diff --git a/src/ts/components/services/model.ts b/src/ts/components/services/model.ts new file mode 100644 index 0000000..149eb36 --- /dev/null +++ b/src/ts/components/services/model.ts @@ -0,0 +1,488 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { merge } from 'lodash'; +import { Subject } from 'rxjs'; +import { HASH } from '../../generated/hash'; +import { + AccountData, UpdateAccountData, AccountSettings, GameStatus, SocialSiteInfo, PonyObject, JoinResponse, + OAuthProvider, EntitiesEditorInfo, FriendData, PalettePonyInfo, HiddenPlayer +} from '../../common/interfaces'; +import { createDefaultPony, syncLockedPonyInfo, mockPaletteManager } from '../../common/ponyInfo'; +import { removeById, observableToPromise, delay, computeFriendsCRC } from '../../common/utils'; +import { isMod, getSupporterInviteLimit, getCharacterLimit } from '../../common/accountUtils'; +import { + NAME_ERROR, ACCESS_ERROR, CHARACTER_SAVING_ERROR, NOT_AUTHENTICATED_ERROR, OFFLINE_ERROR, PROTECTION_ERROR +} from '../../common/errors'; +import { version, host } from '../../client/data'; +import { + toSocialSiteInfo, cleanName, validatePonyName, isStandalone, attachDebugMethod +} from '../../client/clientUtils'; +import { ErrorReporter } from './errorReporter'; +import { randomString } from '../../common/stringUtils'; +import { StorageService } from './storageService'; +import { decompressPonyString, compressPonyString, decodePonyInfo } from '../../common/compressPony'; +import { SECOND, PLAYER_DESC_MAX_LENGTH } from '../../common/constants'; +import { canUseTag } from '../../common/tags'; + +export interface Friend extends FriendData { + entityId: number; + crc: number; + online: boolean; + ponyInfo: PalettePonyInfo | undefined; + actualName: string; +} + +const LIMIT_ERROR = 'Request limit reached, please wait'; +const noneSite: SocialSiteInfo = { id: '', name: 'none', url: '', icon: '', color: '#222' }; +const modStatus = { + mod: false, + check: {} as any, + editor: { + names: [], + typeToName: [], + nameToTypes: [], + } as EntitiesEditorInfo, +}; + +function compareStrings(a: string | undefined, b: string | undefined) { + return (a || '').localeCompare(b || ''); +} + +function comparePonies(a: PonyObject, b: PonyObject) { + return compareStrings(a.name, b.name) || compareStrings(a.id, b.id); +} + +function getDefaultPony(ponies: PonyObject[]) { + let result = ponies[0]; + + for (let i = 1; i < ponies.length; i++) { + if (compareStrings(result.lastUsed, ponies[i].lastUsed) < 0) { + result = ponies[i]; + } + } + + return result || createDefaultPonyObject(); +} + +export function createDefaultPonyObject(): PonyObject { + return { + id: '', + name: '', + info: '', + ponyInfo: createDefaultPony(), + }; +} + +export function getPonyTag(pony: PonyObject, account: AccountData | undefined) { + if (account) { + const tag = canUseTag(account, pony.tag || '') ? pony.tag : undefined; + return (!tag && account.supporter && !pony.hideSupport) ? `sup${account.supporter}` : tag; + } else { + return undefined; + } +} + +const entityTypeToName = new Map(); +const entityNameToTypes = new Map(); + +export function getEntityNames() { + return modStatus.editor.names; +} + +export function getEntityTypesFromName(name: string) { + return entityNameToTypes.get(name); +} + +export function getEntityNameFromType(type: number) { + return entityTypeToName.get(type); +} + +export function compareFriends(a: Friend, b: Friend) { + return a.online !== b.online ? (a.online ? -1 : 1) : a.accountName.localeCompare(b.accountName); +} + +@Injectable({ providedIn: 'root' }) +export class Model { + loading = true; + loadingError?: string; + account?: AccountData; + ponies: PonyObject[] = []; + pending = false; + sites: SocialSiteInfo[] = [noneSite]; + accountPromise!: Promise; + accountChanged = new Subject(); + protectionErrors = new Subject(); + authError?: string; + accountAlert?: string; + mergedAccount = false; + updating = false; + updatingTakesLongTime = false; + suffix = ''; + friends: Friend[] | undefined = undefined; + private _pony: PonyObject = createDefaultPonyObject(); + constructor( + private http: HttpClient, + private router: Router, + private storage: StorageService, + private errorReporter: ErrorReporter, + ) { + this.initialize(); + + // handle completed sign-in + if (typeof window !== 'undefined') { + window.addEventListener('message', event => { + if (event.data && event.data.type === 'loaded-page') { + const path = event.data.path; + + if (event.source && 'close' in event.source) { + event.source.close(); + } + + this.initialize(); + this.accountPromise.then(() => router.navigateByUrl(path)); + } + }); + } + + if (DEVELOPMENT) { + attachDebugMethod('ddos', () => this.protectionErrors.next()); + attachDebugMethod('userModel', this); + } + } + private initialize() { + this.loading = true; + this.account = undefined; + this.loadingError = undefined; + this.accountAlert = undefined; + this.ponies = []; + this.friends = undefined; + this.sites = [noneSite]; + this._pony = createDefaultPonyObject(); + this.storage.setItem('bid', this.storage.getItem('bid') || randomString(20)); + this.accountPromise = this.initializeAccount(); + } + private initializeAccount(): Promise { + return this.getAccount() + .then(account => { + if (!account) { + throw new Error(ACCESS_ERROR); + } + + if ('limit' in account) { + throw new Error(LIMIT_ERROR); + } + + this.errorReporter.configureUser({ id: account.id, username: account.name }); + + try { + modStatus.mod = isMod(account); + modStatus.check = account.check; + modStatus.editor = account.editor || modStatus.editor; + } catch { } + + if (modStatus.editor) { + modStatus.editor.typeToName.forEach(({ type, name }) => entityTypeToName.set(type, name)); + modStatus.editor.nameToTypes.forEach(({ types, name }) => entityNameToTypes.set(name, types)); + } + + this.account = account; + this.sites = [noneSite, ...(account.sites || []).map(toSocialSiteInfo)]; + this.ponies = account.ponies ? account.ponies.sort(comparePonies) : []; + this.friends = undefined; + + this.selectPony(getDefaultPony(this.ponies)); + this.storage.setItem('vid', account.id); + this.loading = false; + this.accountAlert = account.alert; + this.accountChanged.next(); + this.fetchFriends(); + + return account; + }) + .catch((e: Error) => { + if (e.message === ACCESS_ERROR) { + this.loading = false; + this.storage.setItem('vid', '---'); + } else if (e.message === LIMIT_ERROR) { + this.loadingError = 'request-limit'; + return delay(5000).then(() => this.initializeAccount()); + } else if (e.message === OFFLINE_ERROR) { + this.loadingError = 'cannot-connect'; + return delay(5000).then(() => this.initializeAccount()); + } else if (e.message === PROTECTION_ERROR) { + this.loadingError = 'cloudflare-error'; + this.protectionErrors.next(); + // } else if (e.message === VERSION_ERROR) { + // this.updating = true; + } else { + setTimeout(() => this.loadingError = 'unexpected-error', 5 * SECOND); + console.error(e); + } + + return undefined; + }); + } + private fetchFriends() { + this.getFriends() + .then(friends => { + this.friends = friends.map(f => ({ + ...f, + online: false, + entityId: 0, + crc: 0, + ponyInfo: f.pony && decodePonyInfo(f.pony, mockPaletteManager) || undefined, + actualName: '', + })).sort(compareFriends); + }) + .catch(e => { + DEVELOPMENT && console.error(e); + setTimeout(() => this.fetchFriends(), 5000); + }); + } + get characterLimit() { + return this.account ? getCharacterLimit(this.account) : 0; + } + get supporterInviteLimit() { + return this.account ? getSupporterInviteLimit(this.account) : 0; + } + get isMod() { + return modStatus.mod; + } + get modCheck() { + return modStatus.check; + } + get editorInfo() { + return modStatus.editor; + } + get pony() { + return this._pony; + } + get supporter() { + return this.account && this.account.supporter || 0; + } + get missingBirthdate() { + return !!this.account && !this.account.birthdate; + } + computeFriendsCRC() { + return this.friends ? computeFriendsCRC(this.friends.map(f => f.accountId)) : 0; + } + parsePonyObject(pony: PonyObject): PonyObject { + try { + const ponyInfo = decompressPonyString(pony.info, true); + return { ponyInfo, ...pony }; + } catch (e) { + this.errorReporter.reportError(e, { ponyInfo: pony.info }); + this.errorReporter.reportError('Pony info reading error', { originalError: e.message, ponyInfo: pony.info }); + throw new Error('Error while reading pony info'); + } + } + selectPony(pony: PonyObject) { + const copy = this.parsePonyObject(pony); + copy.ponyInfo && syncLockedPonyInfo(copy.ponyInfo); + this._pony = copy; + } + // account + signIn(provider: OAuthProvider) { + this.authError = undefined; + this.openAuth(provider.url!); + } + connectSite(provider: OAuthProvider) { + this.authError = undefined; + this.openAuth(`${provider.url}/merge`); + } + signOut() { + this.authError = undefined; + + return this.post('/auth/sign-out', {}, false) + .catch(e => console.error(e)) + .then(() => this.initialize()) + .then(() => this.router.navigate(['/'])); + } + private openAuth(url: string) { + url = `${host.replace(/\/$/, '')}${url}`; + + if (isStandalone()) { + window.open(url); + } else { + location.href = url; + } + } + getAccount() { + return this.post('/api1/account', {}, false); + } + getAccountCharacters() { + return this.post('/api/account-characters', {}); + } + updateAccount(account: Partial) { + return this.post('/api/account-update', { account }) + .then(a => merge(this.account, a)); + } + saveSettings(settings: AccountSettings) { + return this.post('/api/account-settings', { settings }) + .then(a => merge(this.account, a)); + } + removeSite(siteId: string) { + return this.post('/api/remove-site', { siteId }) + .then(() => { + if (this.account && this.account.sites) { + removeById(this.account.sites, siteId); + } + }); + } + unhidePlayer(hideId: string) { + return this.post('/api/remove-hide', { hideId }); + } + verifyAccount() { + const verificationId = this.storage.getItem('vid'); + const accountId = this.account && this.account.id || '---'; + + if (!this.loading && verificationId && accountId !== verificationId) { + this.initialize(); + } + } + getHides(page: number) { + return this.post('/api/get-hides', { page }); + } + getFriends() { + return this.post('/api/get-friends', {}); + } + // ponies + savePony(pony: PonyObject, fast = false) { + return Promise.resolve() + .then(() => { + if (this.pending) { + throw new Error('Saving in progress'); + } + + pony.name = cleanName(pony.name); + pony.desc = pony.desc && pony.desc.substr(0, PLAYER_DESC_MAX_LENGTH) || ''; + + if (!validatePonyName(pony.name)) { + throw new Error(NAME_ERROR); + } + + if (pony.ponyInfo) { + pony.info = compressPonyString(pony.ponyInfo); + } + + const { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } = pony; + + if (!fast) { + this.pending = true; + } + + return this.post('/api/pony/save', { + pony: { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } + }); + }) + .catch((e: Error) => { + if (e.message === CHARACTER_SAVING_ERROR) { + this.errorReporter.reportError(e, { pony }); + } + + throw e; + }) + .then(newPony => { + if (!newPony) { + throw new Error('Failed to save pony'); + } + + if (pony.id) { + removeById(this.ponies, pony.id); + } else { + this.account!.characterCount++; + } + + this.ponies.push(newPony); + this.ponies.sort(comparePonies); + + if (this.pony === pony) { + this.selectPony(newPony); + } + + return newPony; + }) + .finally(() => this.pending = false); + } + removePony(pony: PonyObject) { + return this.post('/api/pony/remove', { id: pony.id }) + .then(() => { + removeById(this.ponies, pony.id); + this.account!.characterCount--; + + if (this.pony === pony) { + this.selectPony(getDefaultPony(this.ponies)); + } + }); + } + loadPonies() { + return this.getAccountCharacters() + .then(ponies => { + if (this.account) { + this.account.ponies = ponies || []; + this.ponies = this.account.ponies.sort(comparePonies); + } + }); + } + sortPonies() { + this.ponies.sort(comparePonies); + } + // game + status(short: boolean): Promise { + let age = 6; + + if (this.account) { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + if (this.account.birthyear) { + age = currentYear - this.account.birthyear; + } else if (this.account.birthdate) { + const [year, month] = this.account.birthdate.split('-'); + const before = parseInt(month, 10) > currentMonth; + age = Math.max(0, currentYear - parseInt(year, 10) - (before ? 1 : 0)); + } + } + + const params = new HttpParams() + .set('short', short.toString()) + .set('d', age.toString()) + .set('t', (Date.now() % 0x10000).toString(16)); + + return observableToPromise(this.http.get('/api2/game/status', { params })); + } + join(serverId: string, ponyId: string): Promise { + if (this.pending) + return Promise.reject(new Error('Joining in progress')); + if (!serverId) + return Promise.reject(new Error('Invalid server ID')); + if (!ponyId) + return Promise.reject(new Error('Invalid pony ID')); + + this.pending = true; + + const alert = !!this.accountAlert ? 'y' : ''; + + return this.post('/api/game/join', { version, ponyId, serverId, alert, url: location.href }) + .finally(() => this.pending = false); + } + private post(url: string, data: any, authenticate = true): Promise { + if (authenticate) { + if (!this.account) { + return Promise.reject(new Error(NOT_AUTHENTICATED_ERROR)); + } + + const accountId = this.account.id + this.suffix; + const accountName = this.account.name + this.suffix; + data = { accountId, accountName, ...data }; + } + + const params = new HttpParams() + .set('t', (Date.now() % 0x10000).toString(16)); + const headers = new HttpHeaders({ 'api-version': HASH, 'api-bid': this.storage.getItem('bid') || '-' }); + + return observableToPromise(this.http.post(url, data, { params, headers })); + } +} diff --git a/src/ts/components/services/modelSubscriber.ts b/src/ts/components/services/modelSubscriber.ts new file mode 100644 index 0000000..e047237 --- /dev/null +++ b/src/ts/components/services/modelSubscriber.ts @@ -0,0 +1,107 @@ +// import { Observable } from 'rxjs'; +import { SocketService } from 'ag-sockets/dist/browser'; +import { ClientAdminActions } from '../../client/clientAdminActions'; +import { IAdminServerActions, ModelTypes } from '../../common/adminInterfaces'; +import { removeItem } from '../../common/utils'; +import { Subscription } from '../../common/interfaces'; +import { MINUTE } from '../../common/constants'; + +type OnModel = (item: T | undefined) => void; + +interface ModelSubscriberConfig { + fix?: (item: T) => void; +} + +interface ModelSubscription { + value: T | undefined; + timeout: any; + callbacks: OnModel[]; +} + +const unsubscribeTimeout = 1 * MINUTE; + +export class ModelSubscriber { + private subscriptions = new Map>(); + // private observables = new Map>(); + constructor( + private type: ModelTypes, + private socket: SocketService, + private config: ModelSubscriberConfig = {}, + private defaultValue: T | undefined = undefined, + ) { + } + // for(id: string) { + // return this.createObservable(id); + // } + // private createObservable(id: string) { + // return new Observable(observer => { + // this.socket.server.subscribe(this.model, id); + + // return () => { + // this.socket.server.unsubscribe(this.model, id); + // }; + // }); + // } + get(id: string) { + const subscription = this.subscriptions.get(id); + return subscription && subscription.value; + } + subscribe(id: string, callback: OnModel): Subscription { + const subscription = this.subscriptions.get(id); + + if (subscription) { + if (subscription.timeout) { + clearTimeout(subscription.timeout); + subscription.timeout = 0; + } + + subscription.callbacks.push(callback); + + if (subscription.value !== undefined) { + callback(subscription.value); + } + } else { + this.socket.server.subscribe(this.type, id); + this.subscriptions.set(id, { + value: this.defaultValue, + timeout: 0, + callbacks: [callback], + }); + } + + return { + unsubscribe: () => this.unsubscribe(id, callback), + }; + } + unsubscribe(id: string, callback: OnModel) { + const subscription = this.subscriptions.get(id); + + if (subscription) { + removeItem(subscription.callbacks, callback); + + if (subscription.callbacks.length === 0) { + subscription.timeout = setTimeout(() => { + this.socket.server.unsubscribe(this.type, id); + this.subscriptions.delete(id); + }, unsubscribeTimeout); + } + } + } + update(id: string, update: T) { + const subscription = this.subscriptions.get(id); + + if (update !== undefined && this.config.fix) { + this.config.fix(update); + } + + if (subscription) { + subscription.value = update; + subscription.callbacks.forEach(c => c(update)); + } + } + connected() { + this.subscriptions.forEach((_, id) => { + this.socket.server.subscribe(this.type, id); + }); + } +} diff --git a/src/ts/components/services/rollbarErrorHandler.ts b/src/ts/components/services/rollbarErrorHandler.ts new file mode 100644 index 0000000..20b9d2d --- /dev/null +++ b/src/ts/components/services/rollbarErrorHandler.ts @@ -0,0 +1,61 @@ +import { ErrorHandler, Injectable, Injector, InjectionToken } from '@angular/core'; +import * as Rollbar from 'rollbar'; +import { version } from '../../client/data'; +import { HASH } from '../../generated/hash'; +import { ROLLBAR_ENV, ROLLBAR_TOKEN } from '../../generated/rollbarConfig'; +import { rollbarCheckIgnore, isIgnoredError } from '../../common/rollbar'; + +const host = typeof location === 'undefined' ? '' : location.host; + +const rollbarConfig = { + environment: ROLLBAR_ENV, + accessToken: ROLLBAR_TOKEN, + ignoredMessages: ['disconnected'], + hostWhiteList: [host], + captureUncaught: true, + captureUnhandleRejections: true, + // checkIgnore, + enabled: true, + payload: { + environment: ROLLBAR_ENV, + version: version, // NOTE: workaround for compilation issue + client: { + javascript: { + source_map_enabled: true, + guess_uncaught_frames: true, + code_version: HASH, + }, + }, + }, +}; + +export const RollbarService = new InjectionToken('rollbar'); + +export function rollbarFactory() { + if (DEVELOPMENT) { + return undefined; + } else { + const rollbar = Rollbar.init(rollbarConfig); + rollbar.configure({ checkIgnore: rollbarCheckIgnore }); + return rollbar; + } +} + +@Injectable() +export class RollbarErrorHandler extends ErrorHandler { + constructor(private injector: Injector) { + super(); + } + handleError(error: any) { + super.handleError(error); + + if (!DEVELOPMENT && rollbarConfig.accessToken) { + const rollbar = this.injector.get(RollbarService); + const err = error.originalError || error || {}; + + if (!isIgnoredError(err)) { + rollbar.error(err); + } + } + } +} diff --git a/src/ts/components/services/rollbarErrorReporter.ts b/src/ts/components/services/rollbarErrorReporter.ts new file mode 100644 index 0000000..823675e --- /dev/null +++ b/src/ts/components/services/rollbarErrorReporter.ts @@ -0,0 +1,39 @@ +import { Injectable, Inject } from '@angular/core'; +import * as Rollbar from 'rollbar'; +import { Person, rollbarCheckIgnore, isIgnoredError } from '../../common/rollbar'; +import { RollbarService } from './rollbarErrorHandler'; +import { ErrorReporter } from './errorReporter'; + +@Injectable() +export class RollbarErrorReporter extends ErrorReporter { + constructor(@Inject(RollbarService) private rollbar?: Rollbar) { + super(); + } + configureUser(person: Person) { + if (this.rollbar) { + this.rollbar.configure({ payload: { person }, checkIgnore: rollbarCheckIgnore }); + } + } + configureData(data: any) { + if (this.rollbar) { + this.rollbar.configure({ payload: data, checkIgnore: rollbarCheckIgnore }); + } + } + captureEvent(data: any) { + if (this.rollbar) { + this.rollbar.captureEvent(data, 'info'); + } + } + reportError(error: any, data?: any) { + DEVELOPMENT && console.error(error, data); + + if (this.rollbar && !isIgnoredError(error)) { + this.rollbar.error(error, data); + } + } + disable() { + if (this.rollbar) { + this.rollbar.configure({ enabled: false }); + } + } +} diff --git a/src/ts/components/services/settingsService.ts b/src/ts/components/services/settingsService.ts new file mode 100644 index 0000000..d472f9f --- /dev/null +++ b/src/ts/components/services/settingsService.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { AccountSettings, BrowserSettings } from '../../common/interfaces'; +import { StorageService } from './storageService'; +import { Model } from './model'; + +@Injectable({ providedIn: 'root' }) +export class SettingsService { + browser: BrowserSettings; + private save: (settings: AccountSettings) => boolean = () => false; + constructor(private storage: StorageService, private model: Model) { + this.browser = this.storage.getJSON('browser-settings', {}); + } + get account(): AccountSettings { + return this.model.account ? this.model.account.settings : {}; + } + set account(value) { + if (this.model.account) { + this.model.account.settings = value; + } + } + saving(save: (settings: AccountSettings) => boolean) { + this.save = save; + } + saveAccountSettings(settings: AccountSettings) { + if (this.model.account) { + this.model.account.settings = settings; + } + + if (settings.filterWords) { + settings.filterWords = settings.filterWords.trim(); + } + + if (this.save(settings)) { + return Promise.resolve(); + } else { + return this.model.saveSettings(settings); + } + } + saveBrowserSettings(settings?: BrowserSettings) { + this.browser = settings || this.browser; + this.storage.setJSON('browser-settings', this.browser); + } +} diff --git a/src/ts/components/services/storageService.ts b/src/ts/components/services/storageService.ts new file mode 100644 index 0000000..54e40f2 --- /dev/null +++ b/src/ts/components/services/storageService.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@angular/core'; + +/* istanbul ignore next */ +@Injectable({ providedIn: 'root' }) +export class StorageService { + private data?: Map = undefined; + constructor() { + try { + if (typeof localStorage === 'undefined') { + this.data = new Map(); + } + } catch { + this.data = new Map(); + } + } + getItem(key: string) { + if (this.data) { + return this.data.get(key); + } else { + try { + const value = localStorage.getItem(key); + return value == null ? undefined : value; + } catch { + return undefined; + } + } + } + setItem(key: string, data: string) { + try { + localStorage.setItem(key, data); + this.data = undefined; + } catch { + if (!this.data) { + this.data = new Map(); + } + + this.data.set(key, data); + } + } + removeItem(key: string) { + if (this.data) { + this.data.delete(key); + } else { + try { + localStorage.removeItem(key); + } catch { } + } + } + clear() { + if (this.data) { + this.data.clear(); + } else { + try { + localStorage.clear(); + } catch { } + } + } + getJSON(key: string, defaultValue: T): T { + try { + return JSON.parse(this.getItem(key) || ''); + } catch { + return defaultValue; + } + } + setJSON(key: string, value: any) { + this.setItem(key, JSON.stringify(value)); + } + getInt(key: string) { + return parseInt(this.getItem(key) || '0', 10) | 0; + } + setInt(key: string, value: number) { + this.setItem(key, value.toString(10)); + } + getBoolean(key: string) { + return this.getItem(key) === 'true'; + } + setBoolean(key: string, value: boolean) { + if (value) { + this.setItem(key, 'true'); + } else { + this.removeItem(key); + } + } +} diff --git a/src/ts/components/shared/action-bar/action-bar.pug b/src/ts/components/shared/action-bar/action-bar.pug new file mode 100644 index 0000000..6940065 --- /dev/null +++ b/src/ts/components/shared/action-bar/action-bar.pug @@ -0,0 +1,19 @@ +.action-bar( + [class.has-scroller]="hasScroller" [class.is-mobile]="mobile" [class.is-blurred]="blurred" + #scroller (mousewheel)="scroll($event)" (scroll)="true") + action-button( + *ngFor="let a of actions; let i = index" + [class.blur-me]="i < blurCount" + [action]="a.action" + [active]="a.action && activeAction === a" + [editable]="editable" + [shortcut]="shortcuts[i] || ''" + (use)="use(a.action)" + [draggableItem]="a.action" + [draggableDisabled]="!editable || !a.action" + [draggablePad]="10" + (draggableDrag)="drag(i)" + (draggableDrop)="drop($event, i)") + .action-button-padding +.scroller-label + | scroll using this bar diff --git a/src/ts/components/shared/action-bar/action-bar.scss b/src/ts/components/shared/action-bar/action-bar.scss new file mode 100644 index 0000000..119c0bc --- /dev/null +++ b/src/ts/components/shared/action-bar/action-bar.scss @@ -0,0 +1,72 @@ +@import '../../../../styles/partials/variables'; + +:host { + display: block; + pointer-events: none; +} + +.action-bar { + overflow: hidden; + padding-bottom: 7px; + padding-top: 7px; + padding-right: 5px; + max-width: calc(100vw - 50px); + display: flex; + + &.is-mobile { + overflow-x: scroll; + overflow-y: hidden; + pointer-events: auto; + } + + &.is-blurred > .blur-me { + filter: blur(3px); + } + + // &.is-blurred:not(.is-mobile) > :nth-child(-n+11) { + // filter: blur(3px); + // } + + // &.is-blurred.is-mobile > :nth-child(-n+9) { + // filter: blur(3px); + // } + + &.has-scroller { + padding-top: 25px; + min-width: calc(100vw - 50px); + } +} + +.action-button-padding { + min-width: 1px; + width: 1px; + height: 1px; +} + +.scroller-label { + display: none; + + .has-scroller + & { + display: block; + background: #111; + position: absolute; + left: 0; + top: 0; + right: 0; + padding: 1px; + margin-top: 5px; + text-align: center; + color: $text-muted; + } +} + +action-button { + pointer-events: auto; + margin: 3px 6px; + + .is-mobile & { + margin-left: 10px; + margin-right: 10px; + margin-top: 15px; + } +} diff --git a/src/ts/components/shared/action-bar/action-bar.ts b/src/ts/components/shared/action-bar/action-bar.ts new file mode 100644 index 0000000..04e2d98 --- /dev/null +++ b/src/ts/components/shared/action-bar/action-bar.ts @@ -0,0 +1,84 @@ +import { Component, Input, ViewChild, ElementRef } from '@angular/core'; +import { ButtonAction } from '../../../common/interfaces'; +import { PonyTownGame } from '../../../client/game'; +import { isMobile } from '../../../client/data'; +import { useAction, serializeActions } from '../../../client/buttonActions'; +import { SettingsService } from '../../services/settingsService'; +import { ACTIONS_LIMIT } from '../../../common/constants'; +import { last } from '../../../common/utils'; + +@Component({ + selector: 'action-bar', + templateUrl: 'action-bar.pug', + styleUrls: ['action-bar.scss'], +}) +export class ActionBar { + @ViewChild('scroller', { static: true }) scroller!: ElementRef; + @Input() blurred = false; + activeAction: ButtonAction | undefined = undefined; + shortcuts = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=']; + private _editable = false; + constructor(private game: PonyTownGame, private settings: SettingsService) { + } + @Input() get editable() { + return this._editable; + } + set editable(value) { + if (this._editable !== value) { + this._editable = value; + this.updateFreeSlots(); + + if (!value) { + this.save(); + } + } + } + get actions() { + return this.game.actions; + } + get mobile() { + return isMobile; + } + get hasScroller() { + return this.editable && isMobile; + } + get blurCount() { + const boxWidth = isMobile ? 50 : 40; + const width = 450 + this.scroller.nativeElement.scrollLeft; + return Math.floor(width / boxWidth); + } + use(action: ButtonAction | undefined) { + useAction(this.game, action); + } + drag(index: number) { + this.actions[index].action = undefined; + this.updateFreeSlots(); + } + drop(action: ButtonAction | undefined, index: number) { + this.actions[index].action = action; + this.updateFreeSlots(); + } + save() { + const settings = { ...this.settings.account, actions: serializeActions(this.actions) }; + this.settings.saveAccountSettings(settings); + } + scroll(e: MouseWheelEvent) { + if (e.deltaY) { + const delta = e.deltaY > 0 ? 1 : -1; + this.scroller.nativeElement.scrollLeft += delta * 20; + } + } + private updateFreeSlots() { + const actions = this.actions; + + if (this.editable) { + while (actions.length < 5 || (last(actions)!.action !== undefined && actions.length < ACTIONS_LIMIT)) { + actions.push({ action: undefined }); + } + } else { + while (actions.length > 0 && last(actions)!.action === undefined) { + actions.pop(); + } + } + } +} diff --git a/src/ts/components/shared/action-button/action-button.pug b/src/ts/components/shared/action-button/action-button.pug new file mode 100644 index 0000000..3d0f162 --- /dev/null +++ b/src/ts/components/shared/action-button/action-button.pug @@ -0,0 +1,6 @@ +button.action-button( + (click)="click()" [class.active]="active" [class.no-shadow]="!shadow" [title]="action && action.title || ''") + canvas(#canvas width="29" height="29") + .shortcut {{shortcut}} + .count(*ngIf="action && action.type === 'item'") {{action.count}} + .cover diff --git a/src/ts/components/shared/action-button/action-button.scss b/src/ts/components/shared/action-button/action-button.scss new file mode 100644 index 0000000..b6d80c2 --- /dev/null +++ b/src/ts/components/shared/action-button/action-button.scss @@ -0,0 +1,95 @@ +@import '../../../../styles/partials/variables'; + +$button-size: 29px; +$border-radius: 8px; +$default-box-shadow: 0 0 3px rgba(0, 0, 0, 0.7) !important; +$hover-box-shadow: 0 0 7px rgba(0, 0, 0, 0.8) !important; +$pressed-box-shadow: 0 0 7px rgba(0, 0, 0, 1) !important; + +:host { + display: block; + width: $button-size; + height: $button-size; + + &.draggable-hover { + box-shadow: 0 0 5px 1px white; + border-radius: 4px; + } + + &.empty { + visibility: hidden; + } +} + +.action-button { + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + box-shadow: $default-box-shadow; + position: relative; + user-select: none; + + &:hover, &:focus:hover { + box-shadow: $hover-box-shadow; + } + + &:focus { + outline: none; + box-shadow: $default-box-shadow; + } + + &:active, &:hover:active, &:focus:hover:active { + box-shadow: $pressed-box-shadow, inset 0 0 0 black; + + > canvas { + opacity: 0.8; + } + } + + &.active { + border-color: white; + } + + &.no-shadow, &:focus.no-shadow, &:focus:hover.no-shadow { + box-shadow: none !important; + } +} + +canvas { + display: block; + border-radius: 4px; +} + +.shortcut { + pointer-events: none; + position: absolute; + top: -4px; + right: -2px; + font-size: 9px; + color: white; + font-weight: bold; + text-shadow: 0px 0px 1px black, 0px 0px 1px black, 0px 0px 1px black, + 0px 0px 1px black, 0px 0px 1px black; +} + +.count { + pointer-events: none; + position: absolute; + bottom: -7px; + right: -2px; + font-size: 12px; + color: white; + font-weight: bold; + text-shadow: 0px 0px 1px black, 0px 0px 1px black, 0px 0px 1px black, + 0px 0px 1px black, 0px 0px 1px black; +} + +.cover { + position: absolute; + left: -5px; + top: -5px; + right: -5px; + bottom: -5px; +} diff --git a/src/ts/components/shared/action-button/action-button.ts b/src/ts/components/shared/action-button/action-button.ts new file mode 100644 index 0000000..843ac2a --- /dev/null +++ b/src/ts/components/shared/action-button/action-button.ts @@ -0,0 +1,46 @@ +import { Component, Input, ViewChild, ElementRef, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core'; +import { ButtonAction } from '../../../common/interfaces'; +import { PonyTownGame, actionButtons } from '../../../client/game'; +import { drawAction } from '../../../client/buttonActions'; +import { removeItem } from '../../../common/utils'; + +@Component({ + selector: 'action-button', + templateUrl: 'action-button.pug', + styleUrls: ['action-button.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.empty]': '!editable && !action', + }, +}) +export class ActionButton { + @Input() action?: ButtonAction; + @Input() editable = false; + @Input() active = false; + @Input() shadow = true; + @Input() shortcut = ''; + @Output() use = new EventEmitter(); + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + dirty = true; + private state: any = {}; + constructor(private game: PonyTownGame) { + } + ngOnInit() { + actionButtons.push(this); + } + ngOnDestroy() { + removeItem(actionButtons, this); + } + ngOnChanges() { + this.dirty = true; + } + click() { + if (this.action) { + this.use.emit(this.action); + } + } + draw() { + drawAction(this.canvas.nativeElement, this.action, this.state, this.game); + this.dirty = false; + } +} diff --git a/src/ts/components/shared/actions-modal/actions-modal.pug b/src/ts/components/shared/actions-modal/actions-modal.pug new file mode 100644 index 0000000..f0e3101 --- /dev/null +++ b/src/ts/components/shared/actions-modal/actions-modal.pug @@ -0,0 +1,97 @@ +.modal-header.d-none.d-sm-block.d-none-for-low-height + h4.modal-title(labelledBy=".modal") + | Actions + +.modal-body.modal-checkboxes + tabset.fixed-height( + type="pills" saveActiveTab="testing-save-tab" [destroyOnHide]="false" + (activeIndexChange)="tabIndex = $event") + tab(title="Actions" [icon]="actionsIcon") + .action-columns(*tabContent) + .d-flex.align-items-center.mb-2(*ngFor="let a of actions") + action-button.mb-0.mr-2([draggableItem]="a" [action]="a" [shadow]="false") + b {{a.title}} + + tab(title="Expressions" [icon]="expressionsIcon") + div(*tabContent) + .d-flex.mb-3 + action-button.mb-0.mr-sm-4( + [draggableItem]="emoteAction" [action]="emoteAction" [shadow]="false" (draggableDrop)="drop($event)") + .sub-tabset.nav.nav-pills.mb-0 + button.btn-unstyled.nav-link([class.active]="activeTab === 'right-eye'" (click)="activeTab = 'right-eye'") Right eye + button.btn-unstyled.nav-link([class.active]="activeTab === 'left-eye'" (click)="activeTab = 'left-eye'") Left eye + button.btn-unstyled.nav-link([class.active]="activeTab === 'mouth'" (click)="activeTab = 'mouth'") Mouth + button.btn-unstyled.nav-link([class.active]="activeTab === 'extra'" (click)="activeTab = 'extra'") Extra + .fixed-height-tabset + div([class.inactive-tab]="activeTab !== 'right-eye'") + sprite-selection( + [(selected)]="eyeRight" [sprites]="eyesRight" [fill]="eyeColor" + [circle]="coatFill" [reverseExtra]="true" [skip]="1" + [invisible]="tabIndex !== 1 || activeTab !== 'right-eye'" + (selectedChange)="changed(lockEyes)") + + div([class.inactive-tab]="activeTab !== 'left-eye'") + custom-checkbox.mb-2([(checked)]="lockEyes" (checkedChange)="changed($event)") + | Use the same as right one + sprite-selection( + [(selected)]="eyeLeft" [sprites]="eyesLeft" [fill]="eyeColor" [disabled]="lockEyes" + [circle]="coatFill" [reverseExtra]="true" [skip]="1" + [invisible]="tabIndex !== 1 || activeTab !== 'left-eye'" + (selectedChange)="changed(lockEyes)") + + div([class.inactive-tab]="activeTab !== 'mouth'") + sprite-selection#expression-selection( + [(selected)]="muzzle" [sprites]="muzzles" [fill]="noseFills" + [outline]="noseOutlines" [circle]="coatFill" + [invisible]="tabIndex !== 1 || activeTab !== 'mouth'" + (selectedChange)="changed(lockEyes)") + + div([class.inactive-tab]="activeTab !== 'extra'") + sprite-selection.mb-2( + [(selected)]="irisRight" [sprites]="irisesRight" [fill]="eyeColor" + [invisible]="tabIndex !== 1 || activeTab !== 'extra'" + [circle]="coatFill" [reverseExtra]="true" (selectedChange)="changed(lockEyes)") + custom-checkbox([(checked)]="lockIrises" (checkedChange)="changed(lockEyes)") + | Use the same as right one + sprite-selection.mt-2( + [(selected)]="irisLeft" [sprites]="irisesLeft" [fill]="eyeColor" [disabled]="lockIrises" + [invisible]="tabIndex !== 1 || activeTab !== 'extra'" + [circle]="coatFill" [reverseExtra]="true" (selectedChange)="changed(lockEyes)") + .d-flex.flex-wrap.mt-3 + label.text-muted.mr-3 Other options + custom-checkbox([(checked)]="blush" (checkedChange)="changed(lockEyes)") Blush + custom-checkbox.ml-4([(checked)]="sleeping" (checkedChange)="changed(lockEyes)") Sleeping + custom-checkbox.ml-4([(checked)]="tears" (checkedChange)="changed(lockEyes)") Tears + custom-checkbox.ml-4([(checked)]="crying" (checkedChange)="changed(lockEyes)") Crying + custom-checkbox.ml-4([(checked)]="hearts" (checkedChange)="changed(lockEyes)") Hearts + + tab(title="Chat" [icon]="chatIcon") + .action-columns(*tabContent) + .d-flex.align-items-center.mb-2(*ngFor="let a of commands") + action-button.mb-0.mr-2([draggableItem]="a" [action]="a" [shadow]="false") + b {{a.title}} + + tab(title="Options" [icon]="optionsIcon") + div(*tabContent) + button.btn.btn-outline-secondary.mr-2.mb-2((click)="resetToDefault()") + | Reset to default actions + button.btn.btn-outline-secondary.mr-2.mb-2((click)="clearActionBar()") + | Clear action bar + button.btn.btn-outline-secondary.mr-2.mb-2((click)="undo()") + | Undo + + tab(*ngIf="dev" title="Dev" [icon]="devIcon") + div(*tabContent) + .d-flex.mb-3 + action-button.mb-0.mr-4( + [draggableItem]="entityAction" [action]="entityAction" [shadow]="false" (draggableDrop)="drop($event)") + input.form-control( + placeholder="entity name" [(ngModel)]="entityName" (ngModelChange)="updateEntity()" style="max-width: 200px;") + .d-flex.flex-wrap(style="height: 250px; overflow-y: scroll") + action-button(*ngFor="let a of entityActions" [draggableItem]="a" [action]="a") + +.modal-footer.d-flex + .flex-grow-1 + em.text-muted Drag actions to the action bar + button.btn.btn-outline-secondary((click)="ok()") + | Close diff --git a/src/ts/components/shared/actions-modal/actions-modal.scss b/src/ts/components/shared/actions-modal/actions-modal.scss new file mode 100644 index 0000000..ecd0364 --- /dev/null +++ b/src/ts/components/shared/actions-modal/actions-modal.scss @@ -0,0 +1,40 @@ +@import '../../../../styles/partials/variables'; + +action-button { + margin-right: 6px; + margin-bottom: 6px; +} + +.fixed-height-tabset { + display: grid; + + > div { + grid-row: 1 / 2; + grid-column: 1 / 2; + } +} + +.sub-tabset { + font-size: 13px; + + @media screen and (max-width: 420px) { + .nav-link { + margin-right: 0.1rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + } + } +} + +.action-columns { + display: flex; + flex-wrap: wrap; + + > div { + width: 50%; + } +} + +.inactive-tab { + visibility: hidden; +} diff --git a/src/ts/components/shared/actions-modal/actions-modal.ts b/src/ts/components/shared/actions-modal/actions-modal.ts new file mode 100644 index 0000000..944b3cd --- /dev/null +++ b/src/ts/components/shared/actions-modal/actions-modal.ts @@ -0,0 +1,154 @@ +import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { + createButtionActionActions, expressionButtonAction, createButtonCommandActions, + createDefaultButtonActions, actionExpressionDefaultPalette, entityButtonAction +} from '../../../client/buttonActions'; +import * as sprites from '../../../generated/sprites'; +import { + Eye, Muzzle, ColorExtraSet, ColorExtra, Iris, ExpressionExtra, PonyEye, ButtonAction, Action, + ButtonActionSlot, EntityButtonAction +} from '../../../common/interfaces'; +import { createExpression } from '../../../client/clientUtils'; +import { ACTION_EXPRESSION_BG, ACTION_EXPRESSION_EYE_COLOR, fillToOutline } from '../../../common/colors'; +import { faLock, faApple, faLaughBeam, faComment, faCog, faCogs } from '../../../client/icons'; +import { createEyeSprite } from '../../../client/spriteUtils'; +import { times, hasFlag } from '../../../common/utils'; +import { PonyTownGame } from '../../../client/game'; +import { getEntityNames } from '../../services/model'; + +function eyeSprite(e: PonyEye | undefined) { + return createEyeSprite(e, 0, sprites.defaultPalette); +} + +@Component({ + selector: 'actions-modal', + templateUrl: 'actions-modal.pug', + styleUrls: ['actions-modal.scss'], +}) +export class ActionsModal implements OnInit, OnDestroy { + readonly lockIcon = faLock; + readonly actionsIcon = faApple; + readonly expressionsIcon = faLaughBeam; + readonly chatIcon = faComment; + readonly optionsIcon = faCog; + readonly devIcon = faCogs; + readonly dev = BETA; + @Output() close = new EventEmitter(); + actions = createButtionActionActions(); + commands = createButtonCommandActions(); + emoteAction = expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile)); + entityAction = entityButtonAction('apple'); + entityActions: EntityButtonAction[] = []; + entityName = 'apple'; + lockEyes = true; + lockIrises = true; + eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); + eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); + irisesLeft: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeLeft[1]![0]!, i, sprites.defaultPalette)); + irisesRight: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeRight[1]![0]!, i, sprites.defaultPalette)); + muzzles: ColorExtraSet = sprites.noses + .map(n => n[0][0]) + .map(({ color, colors, mouth }) => ({ + color, colors, extra: mouth, palettes: [actionExpressionDefaultPalette.colors] + } as ColorExtra)); + noseFills = [ACTION_EXPRESSION_BG]; + noseOutlines = [fillToOutline(ACTION_EXPRESSION_BG)]; + coatFill = ACTION_EXPRESSION_BG; + eyeColor = ACTION_EXPRESSION_EYE_COLOR; + muzzle: Muzzle = 0; + eyeLeft: Eye = 1; + eyeRight: Eye = 1; + irisLeft: Iris = 0; + irisRight: Iris = 0; + tabIndex = 0; + blush = false; + sleeping = false; + tears = false; + crying = false; + hearts = false; + activeTab = 'right-eye'; + private interval: any = 0; + private subscription?: Subscription; + private actionsToUndo: ButtonActionSlot[][] = []; + constructor(private game: PonyTownGame) { + this.updateEmoteAction(); + } + ngOnInit() { + document.body.classList.add('actions-modal-opened'); + this.game.editingActions = true; + this.interval = setInterval(() => this.game.send(server => server.action(Action.KeepAlive)), 10000); + this.subscription = this.game.onLeft.subscribe(() => this.ok()); + + if (BETA) { + this.entityActions = getEntityNames().map(name => entityButtonAction(name)); + } + } + ngOnDestroy() { + document.body.classList.remove('actions-modal-opened'); + this.game.editingActions = false; + clearInterval(this.interval); + this.subscription && this.subscription.unsubscribe(); + } + ok() { + this.close.emit(); + } + changed(locked: boolean) { + if (locked) { + this.eyeLeft = this.eyeRight; + } + + if (this.lockIrises) { + this.irisLeft = this.irisRight; + } + + this.updateEmoteAction(); + } + drop(action: ButtonAction) { + if (action.type === 'expression' && action.expression) { + const e = action.expression; + this.lockEyes = e.right === e.left; + this.lockIrises = e.rightIris === e.leftIris; + this.eyeRight = e.right; + this.eyeLeft = e.left; + this.muzzle = e.muzzle; + this.irisRight = e.rightIris; + this.irisLeft = e.leftIris; + this.blush = hasFlag(e.extra, ExpressionExtra.Blush); + this.sleeping = hasFlag(e.extra, ExpressionExtra.Zzz); + this.tears = hasFlag(e.extra, ExpressionExtra.Tears); + this.crying = hasFlag(e.extra, ExpressionExtra.Cry); + this.hearts = hasFlag(e.extra, ExpressionExtra.Hearts); + this.changed(this.lockEyes); + } + } + updateEmoteAction() { + const extra = + (this.blush ? ExpressionExtra.Blush : 0) | + (this.sleeping ? ExpressionExtra.Zzz : 0) | + (this.tears ? ExpressionExtra.Tears : 0) | + (this.crying ? ExpressionExtra.Cry : 0) | + (this.hearts ? ExpressionExtra.Hearts : 0); + + const expression = createExpression(this.eyeRight, this.eyeLeft, this.muzzle, this.irisRight, this.irisLeft, extra); + this.emoteAction = expressionButtonAction(expression); + } + resetToDefault() { + this.actionsToUndo.push(this.game.actions); + this.game.actions = [...createDefaultButtonActions(), { action: undefined }]; + } + clearActionBar() { + this.actionsToUndo.push(this.game.actions); + this.game.actions = this.game.actions.map(() => ({ action: undefined })); + } + undo() { + if (this.actionsToUndo.length) { + this.game.actions = this.actionsToUndo.pop()!; + } + } + updateEntity() { + if (BETA) { + this.entityAction = entityButtonAction(this.entityName); + } + } +} diff --git a/src/ts/components/shared/bitmap-box/bitmap-box.pug b/src/ts/components/shared/bitmap-box/bitmap-box.pug new file mode 100644 index 0000000..50dd5bf --- /dev/null +++ b/src/ts/components/shared/bitmap-box/bitmap-box.pug @@ -0,0 +1,2 @@ +.bitmap-box-row(*ngFor="let row of rows") + .bitmap-box-cell(*ngFor="let cell of row" [style.background]="colorAt(cell)" (mousedown)="draw(cell)") diff --git a/src/ts/components/shared/bitmap-box/bitmap-box.scss b/src/ts/components/shared/bitmap-box/bitmap-box.scss new file mode 100644 index 0000000..b5b5a90 --- /dev/null +++ b/src/ts/components/shared/bitmap-box/bitmap-box.scss @@ -0,0 +1,27 @@ +@import '../../../../styles/partials/variables'; + +:host { + border: solid 1px $border-color; + border-radius: $border-radius-base; + overflow: hidden; + display: inline-block; +} + +.bitmap-box-row { + display: flex; + + &:last-child > .bitmap-box-cell { + border-bottom: none; + } +} + +.bitmap-box-cell { + width: 40px; + height: 40px; + border-right: solid 1px $border-color; + border-bottom: solid 1px $border-color; + + &:last-child { + border-right: none; + } +} diff --git a/src/ts/components/shared/bitmap-box/bitmap-box.ts b/src/ts/components/shared/bitmap-box/bitmap-box.ts new file mode 100644 index 0000000..c015ef1 --- /dev/null +++ b/src/ts/components/shared/bitmap-box/bitmap-box.ts @@ -0,0 +1,45 @@ +import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +import { parseColor, colorToCSS } from '../../../common/color'; + +@Component({ + selector: 'bitmap-box', + templateUrl: 'bitmap-box.pug', + styleUrls: ['bitmap-box.scss'], +}) +export class BitmapBox implements OnChanges { + @Input() width = 5; + @Input() height = 5; + @Input() bitmap?: string[]; + @Input() tool?: string; + @Input() color = 'red'; + @Output() colorChange = new EventEmitter(); + rows?: number[][]; + ngOnChanges(changes: SimpleChanges) { + if (changes.width || changes.height) { + this.rows = []; + + for (let y = 0; y < this.height; y++) { + this.rows[y] = []; + + for (let x = 0; x < this.width; x++) { + this.rows[y][x] = x + this.width * y; + } + } + } + } + draw(index: number) { + if (this.bitmap) { + if (this.tool === 'eraser') { + this.bitmap[index] = ''; + } else if (this.tool === 'brush') { + this.bitmap[index] = parseColor(this.bitmap[index]) === parseColor(this.color) ? '' : this.color; + } else if (this.tool === 'eyedropper') { + this.color = this.bitmap[index]; + this.colorChange.emit(this.color); + } + } + } + colorAt(index: number) { + return this.bitmap && this.bitmap[index] ? colorToCSS(parseColor(this.bitmap[index])) : ''; + } +} diff --git a/src/ts/components/shared/butt-mark-editor/butt-mark-editor.pug b/src/ts/components/shared/butt-mark-editor/butt-mark-editor.pug new file mode 100644 index 0000000..3879528 --- /dev/null +++ b/src/ts/components/shared/butt-mark-editor/butt-mark-editor.pug @@ -0,0 +1,20 @@ +.form-group.text-center + label#butt-mark-label.text-muted Butt mark +.row.form-group(role="group" aria-labelledby="butt-mark-label") + .col-sm-7.mb-1 + button.btn.btn-primary((click)="clearCM()" title="Clear all") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + .btn-group.ml-1(btnRadioGroup [(ngModel)]="state.brushType") + button.btn.btn-primary(btnRadio="eraser" title="Eraser") + fa-icon([icon]="eraserIcon" [fixedWidth]="true") + button.btn.btn-primary(btnRadio="eyedropper" title="Eyedropper") + fa-icon([icon]="eyeDropperIcon" [fixedWidth]="true") + button.btn.btn-primary(btnRadio="brush" title="Brush") + fa-icon([icon]="paintBrushIcon" [fixedWidth]="true") + .col-sm-5.mb-1 + color-picker([(color)]="state.brush" label="Brush color") +.form-group.text-center + bitmap-box([bitmap]="info.cm" [tool]="state.brushType" [(color)]="state.brush" [width]="cmSize" [height]="cmSize") +.form-group.d-flex.justify-content-center.align-items-center.text-muted + custom-checkbox([(checked)]="info.cmFlip") + | don't flip mark on the right side diff --git a/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts b/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts new file mode 100644 index 0000000..1c421ac --- /dev/null +++ b/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts @@ -0,0 +1,30 @@ +import { Component, Input } from '@angular/core'; +import { fill } from 'lodash'; +import { PonyInfo } from '../../../common/interfaces'; +import { CM_SIZE } from '../../../common/constants'; +import { faTrash, faEraser, faPaintBrush, faEyeDropper } from '../../../client/icons'; + +export interface ButtMarkEditorState { + brushType: string; + brush: string; +} + +@Component({ + selector: 'butt-mark-editor', + templateUrl: 'butt-mark-editor.pug', +}) +export class ButtMarkEditor { + readonly trashIcon = faTrash; + readonly eraserIcon = faEraser; + readonly eyeDropperIcon = faEyeDropper; + readonly paintBrushIcon = faPaintBrush; + readonly cmSize = CM_SIZE; + @Input() info!: PonyInfo; + @Input() state = { + brushType: 'brush', + brush: 'orange', + }; + clearCM() { + fill(this.info.cm!, ''); + } +} diff --git a/src/ts/components/shared/character-list/character-list.pug b/src/ts/components/shared/character-list/character-list.pug new file mode 100644 index 0000000..6745a1b --- /dev/null +++ b/src/ts/components/shared/character-list/character-list.pug @@ -0,0 +1,29 @@ +.sr-only(#ariaAnnounce aria-live="assertive") + +.character-list([class.character-list-searchable]="searchable" [class.in-game]="inGame") + + .dropdown.character-select-search.input-group( + (mousedown)="$event.stopPropagation()" (mouseup)="$event.stopPropagation()" + (click)="$event.stopPropagation()" (keydown)="keydown($event)") + input.form-control( + #searchInput type="search" [placeholder]="placeholder" [(ngModel)]="search" (input)="updatePonies()" + autocomplete="nope") + .input-group-append(*ngIf="tags.length" #tagsDropdown="ag-dropdown" dropdown) + button.btn.btn-secondary.rounded-right(dropdownToggle title="Search by tags") + fa-icon([icon]="hashIcon") + .dropdown-menu.dropdown-menu-right.shadow.tag-list(*dropdownMenu) + button.dropdown-item(*ngFor="let tag of tags" (click)="search = tag; tagsDropdown.close(); updatePonies()") + | {{tag}} + + ul.character-select-list(role="listbox" [attr.aria-activedescendant]="'pony-item-' + selectedIndex") + li( + *ngFor="let p of ponies; let i = index" + [id]="'pony-item-' + i" + [class.active]="i === selectedIndex" + [class.selected]="p?.id === selectedPony?.id") + a.d-flex(role="option" (click)="select(p)" (mouseenter)="setPreview(p)" (mouseleave)="unsetPreview(p)") + span.flex-grow-1.character-name {{p.name}} + span.character-desc.text-muted {{p.desc}} + li(*ngIf="canNew") + a.text-center(role="option" (click)="createNew()") + em new pony diff --git a/src/ts/components/shared/character-list/character-list.scss b/src/ts/components/shared/character-list/character-list.scss new file mode 100644 index 0000000..2f7ea4e --- /dev/null +++ b/src/ts/components/shared/character-list/character-list.scss @@ -0,0 +1,108 @@ +@import '../../../../styles/partials/variables'; + +.character-list { + width: 300px; + max-width: calc(100vw - 70px); +} + +.character-list.in-game { + max-width: calc(100vw - 90px); +} + +.character-select-search { + height: 30px; + padding: 0 5px; + display: none; + position: relative; + + > .form-control { + background: white; + color: #222; + border-color: #eee !important; + border-radius: 3px !important; + box-shadow: none !important; + // width: 100%; + + &:active, &:focus { + border-color: #ddd !important; + } + } + + .character-list-searchable > & { + display: flex; + } +} + +.hash-button { + position: absolute; + right: 7px; + top: 5px; + font-size: 16px; + padding: 0 10px; +} + +.character-select-list { + background: #fff; + padding: 0; + margin: 0; + list-style: none; + font-size: $font-size-base; + text-align: left; + padding-top: 1px; + max-height: 400px; + overflow-x: hidden; + overflow-y: auto; + + > li { + &.active { + background: $nav-tabs-link-active-color; + } + + &.selected > a { + font-weight: bold; + } + + > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + white-space: nowrap; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + + &:hover, + &:focus { + text-decoration: none; + color: $dropdown-link-hover-color; + background-color: $dropdown-link-hover-bg; + } + } + } + + .character-list-searchable > & { + border-top: solid 1px #eee; + margin-top: 9px; + } +} + +.character-desc { + font-size: smaller; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 4px; + margin-left: 10px; +} + +.character-name { + white-space: nowrap; +} + +.tag-list { + max-height: 400px; + overflow-y: auto; +} diff --git a/src/ts/components/shared/character-list/character-list.ts b/src/ts/components/shared/character-list/character-list.ts new file mode 100644 index 0000000..3a961b1 --- /dev/null +++ b/src/ts/components/shared/character-list/character-list.ts @@ -0,0 +1,172 @@ +import { Component, Output, EventEmitter, ViewChild, ElementRef, OnInit, Input, NgZone } from '@angular/core'; +import { uniq } from 'lodash'; +import { Key } from '../../../client/input/input'; +import { PonyObject } from '../../../common/interfaces'; +import { clamp, flatten } from '../../../common/utils'; +import { isMobile } from '../../../client/data'; +import { Model } from '../../services/model'; +import { LATEST_CHARACTER_LIMIT } from '../../../common/constants'; +import { faHashtag } from '../../../client/icons'; + +function getSortTag(pony: PonyObject) { + const match = pony.desc && /(?:^| )@(top|end|\d+)(?:$| )/.exec(pony.desc); + return match && match[1]; +} + +function sortTagToNumber(tag: string) { + if (tag === 'top') { + return -1; + } else if (tag === 'end') { + return 999999999; + } else { + return +tag; + } +} + +function fallbackComparePonies(a: PonyObject, b: PonyObject) { + return a.name.localeCompare(b.name) || (a.desc || '').localeCompare(b.desc || ''); +} + +function comparePonies(a: PonyObject, b: PonyObject) { + const aTag = getSortTag(a); + const bTag = getSortTag(b); + + if (aTag && bTag) { + return (sortTagToNumber(aTag) - sortTagToNumber(bTag)) || fallbackComparePonies(a, b); + } else if (aTag) { + return aTag === 'end' ? 1 : -1; + } else if (bTag) { + return bTag === 'end' ? -1 : 1; + } else { + return fallbackComparePonies(a, b); + } +} + +@Component({ + selector: 'character-list', + templateUrl: 'character-list.pug', + styleUrls: ['character-list.scss'], +}) +export class CharacterList implements OnInit { + readonly hashIcon = faHashtag; + @Input() inGame = false; + @Input() canNew = false; + @Output() close = new EventEmitter(); + @Output() newCharacter = new EventEmitter(); + @Output() selectCharacter = new EventEmitter(); + @Output() previewCharacter = new EventEmitter(); + @ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef; + @ViewChild('searchInput', { static: true }) searchInput!: ElementRef; + search?: string; + selectedIndex = -1; + ponies: PonyObject[] = []; + tags: string[] = []; + private previewPony: PonyObject | undefined = undefined; + constructor(private model: Model, private zone: NgZone) { + } + get selectedPony() { + return this.model.pony; + } + get searchable() { + return this.model.ponies.length > LATEST_CHARACTER_LIMIT; + } + get placeholder() { + return `search (${this.model.ponies.length} / ${this.model.characterLimit} ponies)`; + } + ngOnInit() { + this.updatePonies(); + + this.tags = uniq(flatten(this.ponies.map(p => (p.desc || '').split(/ /g).map(x => x.trim()))) + .filter(x => /^#/.test(x))) + .sort(); + + if (!isMobile) { + setTimeout(() => this.searchInput.nativeElement.focus()); + } + } + keydown(e: KeyboardEvent) { + if (e.keyCode === Key.ESCAPE) { + if (this.search) { + e.preventDefault(); + e.stopPropagation(); + this.search = ''; + this.updatePonies(); + } else { + this.closed(); + } + } else if (e.keyCode === Key.ENTER) { + const pony = this.ponies[this.selectedIndex]; + + if (pony) { + this.select(pony); + } else { + this.closed(); + } + } else if (e.keyCode === Key.UP) { + this.setSelectedIndex(this.selectedIndex <= 0 ? (this.ponies.length - 1) : (this.selectedIndex - 1)); + } else if (e.keyCode === Key.DOWN) { + this.setSelectedIndex(this.selectedIndex === (this.ponies.length - 1) ? 0 : (this.selectedIndex + 1)); + } + } + setPreview(pony: PonyObject) { + this.previewPony = pony; + this.previewCharacter.emit(this.model.parsePonyObject(pony)); + } + unsetPreview(pony: PonyObject) { + if (this.previewPony && pony && this.previewPony.id === pony.id) { + this.previewPony = undefined; + this.previewCharacter.emit(undefined); + } + } + updatePonies() { + this.zone.run(() => { + const query = this.search && this.search.toLowerCase().trim(); + + function matchesWords(text: string, words: string[]) { + for (const word of words) { + if (text.indexOf(word) === -1) { + return false; + } + } + + return true; + } + + if (query) { + const words = query.split(/ /g).map(x => x.trim()); + + this.ponies = this.model.ponies.filter(pony => { + const text = `${pony.name} ${pony.desc || ''}`.toLowerCase(); + return matchesWords(text, words); + }).sort(comparePonies); + } else { + this.ponies = this.model.ponies.slice().sort(comparePonies); + } + + this.setSelectedIndex(this.selectedIndex); + this.previewCharacter.emit(undefined); + }); + } + select(pony: PonyObject) { + this.selectCharacter.emit(pony); + } + createNew() { + this.newCharacter.emit(); + } + private closed() { + this.zone.run(() => this.close.emit()); + } + private setSelectedIndex(index: number) { + this.zone.run(() => { + this.selectedIndex = clamp(index, -1, this.ponies.length - 1); + const pony = this.ponies[index]; + this.ariaAnnounce.nativeElement.textContent = pony ? pony.name : ''; + + if (pony) { + this.setPreview(pony); + } else if (this.previewPony) { + this.unsetPreview(this.previewPony); + } + }); + } +} diff --git a/src/ts/components/shared/character-preview/character-preview.ts b/src/ts/components/shared/character-preview/character-preview.ts new file mode 100644 index 0000000..42f8e6d --- /dev/null +++ b/src/ts/components/shared/character-preview/character-preview.ts @@ -0,0 +1,192 @@ +import { + Component, Input, ElementRef, AfterViewInit, OnDestroy, NgZone, ViewChild, OnChanges, HostListener +} from '@angular/core'; +import { PonyInfo, PonyState } from '../../../common/interfaces'; +import { toPalette } from '../../../common/ponyInfo'; +import { GRASS_COLOR, TRANSPARENT } from '../../../common/colors'; +import { + createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvas, resizeCanvasWithRatio +} from '../../../client/canvasUtils'; +import { BLINK_FRAMES } from '../../../client/ponyUtils'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; +import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import { colorToCSS } from '../../../common/color'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { drawNamePlate, commonPalettes, DrawNameFlags } from '../../../graphics/graphicsUtils'; +import { drawPony } from '../../../client/ponyDraw'; +import { paletteSpriteSheet } from '../../../generated/sprites'; +import { replaceEmojis } from '../../../client/emoji'; + +const DEFAULT_STATE = defaultPonyState(); +const DEFAULT_OPTIONS = defaultDrawPonyOptions(); + +@Component({ + selector: 'character-preview', + template: '', + styles: [`:host { display: block; } canvas { width: 100%; height: 100%; }`], +}) +export class CharacterPreview implements OnDestroy, OnChanges, AfterViewInit { + @Input() scale = 3; + @Input() name?: string; + @Input() tag?: string; + @Input() pony?: PonyInfo; + @Input() state?: PonyState = defaultPonyState(); + @Input() noBackground = false; + @Input() noOutline = false; + @Input() noShadow = false; + @Input() extra = false; + @Input() passive = false; + @Input() blinks = true; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + private batch?: ContextSpriteBatch; + private nameBatch?: ContextSpriteBatch; + private frame = 0; + private lastFrame = 0; + private initialized = false; + private nextBlink = performance.now() + 2000; + private blinkFrame = -1; + constructor(private zone: NgZone) { + } + ngAfterViewInit() { + return loadAndInitSpriteSheets() + .then(() => this.initialized = true) + .then(() => this.ngOnChanges()); + } + ngOnDestroy() { + cancelAnimationFrame(this.frame); + } + ngOnChanges() { + if (!this.frame) { + this.zone.runOutsideAngular(() => this.frame = requestAnimationFrame(this.onFrame)); + } + } + @HostListener('window:resize') + redraw() { + this.tryDraw(); + } + blink() { + this.nextBlink = performance.now(); + } + private onFrame = () => { + if (this.passive && this.initialized) { + this.frame = 0; + this.tryDraw(); + return; + } + + this.frame = requestAnimationFrame(this.onFrame); + + const now = performance.now(); + + if ((now - this.lastFrame) > (1000 / 24)) { + if (this.blinks) { + if (this.blinkFrame === -1) { + if (this.nextBlink < now) { + this.blinkFrame = 0; + } + } else { + this.blinkFrame++; + + if (this.blinkFrame >= BLINK_FRAMES.length) { + this.nextBlink = now + Math.random() * 2000 + 3000; + this.blinkFrame = -1; + } + } + + if (this.state) { + this.state.blinkFrame = this.blinkFrame === -1 ? 1 : BLINK_FRAMES[this.blinkFrame]; + } + } + + this.lastFrame = now; + this.tryDraw(); + } + } + private tryDraw() { + try { + this.draw(); + } catch { } + } + private draw() { + if (!this.initialized) + return; + + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + + const { width, height } = canvas.getBoundingClientRect(); + resizeCanvasWithRatio(canvas, width, height, false); + + const scale = this.scale * getPixelRatio(); + const bufferWidth = Math.round(canvas.width / scale); + const bufferHeight = Math.round(canvas.height / scale); + + if (!bufferWidth || !bufferHeight) + return; + + this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferWidth, bufferHeight)); + resizeCanvas(this.batch.canvas, bufferWidth, bufferHeight); + + const x = Math.round(bufferWidth / 2); + const y = Math.round(bufferHeight / 2 + 28); + + if (this.pony) { + this.batch.start(paletteSpriteSheet, this.noBackground ? TRANSPARENT : GRASS_COLOR); + + try { + const options = { ...DEFAULT_OPTIONS, shadow: !this.noShadow, extra: !!this.extra }; + drawPony(this.batch, toPalette(this.pony), this.state || DEFAULT_STATE, x, y, options); + } catch (e) { + console.error(e); + } + + this.batch.end(); + } + + const viewContext = canvas.getContext('2d'); + + if (!viewContext) + return; + + disableImageSmoothing(viewContext); + + if (this.noBackground) { + viewContext.clearRect(0, 0, canvas.width, canvas.height); + } + + viewContext.save(); + viewContext.scale(scale, scale); + + // draw outline + if (this.pony && this.noShadow && this.noBackground && !this.noOutline) { + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + viewContext.drawImage(this.batch.canvas, x, y); + } + } + + viewContext.globalCompositeOperation = 'source-in'; + viewContext.fillStyle = colorToCSS(GRASS_COLOR); + viewContext.fillRect(0, 0, viewContext.canvas.width, viewContext.canvas.height); + viewContext.globalCompositeOperation = 'source-over'; + } + + viewContext.drawImage(this.batch.canvas, 0, 0); + viewContext.restore(); + + // draw name plate + if (!this.noShadow && this.name) { + const name = replaceEmojis(this.name); + const scale = 2 * getPixelRatio(); + const nameBufferWidth = Math.round(canvas.width / scale); + this.nameBatch = this.nameBatch || new ContextSpriteBatch(createCanvas(nameBufferWidth, 25)); + resizeCanvas(this.nameBatch.canvas, nameBufferWidth, 25); + this.nameBatch.start(paletteSpriteSheet, TRANSPARENT); + drawNamePlate(this.nameBatch, name, nameBufferWidth / 2, 11, DrawNameFlags.None, commonPalettes, this.tag); + this.nameBatch.end(); + viewContext.save(); + viewContext.scale(scale, scale); + viewContext.drawImage(this.nameBatch.canvas, 0, 10); + viewContext.restore(); + } + } +} diff --git a/src/ts/components/shared/character-select/character-select.pug b/src/ts/components/shared/character-select/character-select.pug new file mode 100644 index 0000000..5e06d26 --- /dev/null +++ b/src/ts/components/shared/character-select/character-select.pug @@ -0,0 +1,36 @@ +.sr-only(#ariaAnnounce aria-live="assertive") + +.character-select.input-group + input.form-control.text-center( + #nameInput + type="text" + [(ngModel)]="pony.name" + [maxlength]="maxNameLength" + placeholder="Name of your character" + aria-label="Name of your character") + .input-group-append + button.btn.btn-default(*ngIf="newButton" (click)="createNew()" [disabled]="!canNew" aria-label="Create New character") + | new + button.btn.btn-default(*ngIf="editButton" (click)="edit()" [disabled]="!canEdit" aria-label="Edit character") + | edit + + .dropdown(dropdown #dropdown="ag-dropdown" (isOpenChange)="onToggle($event)" [focusOnOpen]="false") + button.btn.btn-default.dropdown-toggle.br-0( + [ngClass]="removeButton ? 'btn-no-round' : 'btn-no-round-left'" + [disabled]="!hasPonies || joining" dropdownToggle aria-label="Select character") + character-list.dropdown-menu( + *dropdownMenu (close)="dropdown.close()" (selectCharacter)="select($event)" (newCharacter)="createNew()" + (previewCharacter)="preview.emit($event)" [canNew]="!newButton && canNew") + + button.btn.btn-danger.remove-button( + *ngIf="removeButton && !removing" (click)="remove()" [disabled]="!canRemove" + tooltip="Delete pony" aria-label="Delete pony") + fa-icon([icon]="deleteIcon" [fixedWidth]="true") + button.btn.btn-danger.cancel-remove-button( + *ngIf="removing" (click)="cancelRemove()" + tooltip="Cancel delete" aria-label="Cancel delete") + fa-icon([icon]="removeIcon" [fixedWidth]="true") + button.btn.btn-success( + *ngIf="removing" (click)="confirmRemove()" [disabled]="!canRemove" + tooltip="Confirm delete" aria-label="Confirm delete") + fa-icon([icon]="confirmIcon" [fixedWidth]="true") diff --git a/src/ts/components/shared/character-select/character-select.scss b/src/ts/components/shared/character-select/character-select.scss new file mode 100644 index 0000000..3f6cb93 --- /dev/null +++ b/src/ts/components/shared/character-select/character-select.scss @@ -0,0 +1,14 @@ +@import '../../../../styles/partials/variables'; + +.character-select { + max-width: 400px; + margin: auto; + z-index: 200; +} + +character-list { + @include media-breakpoint-down(sm) { + left: auto; + right: 0; + } +} diff --git a/src/ts/components/shared/character-select/character-select.ts b/src/ts/components/shared/character-select/character-select.ts new file mode 100644 index 0000000..33f5688 --- /dev/null +++ b/src/ts/components/shared/character-select/character-select.ts @@ -0,0 +1,128 @@ +import { Component, Input, EventEmitter, Output, ViewChild, ElementRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { PonyObject } from '../../../common/interfaces'; +import { PLAYER_NAME_MAX_LENGTH } from '../../../common/constants'; +import { VERSION_ERROR } from '../../../common/errors'; +import { GameService } from '../../services/gameService'; +import { Model, createDefaultPonyObject } from '../../services/model'; +import { Dropdown } from '../directives/dropdown'; +import { faSpinner, faTrash, faTimes, faCheck } from '../../../client/icons'; +import { focusElementAfterTimeout } from '../../../client/htmlUtils'; +import { delay } from '../../../common/utils'; +import { isMobile } from '../../../client/data'; + +@Component({ + selector: 'character-select', + templateUrl: 'character-select.pug', + styleUrls: ['character-select.scss'], +}) +export class CharacterSelect { + readonly maxNameLength = PLAYER_NAME_MAX_LENGTH; + readonly spinnerIcon = faSpinner; + readonly deleteIcon = faTrash; + readonly removeIcon = faTimes; + readonly confirmIcon = faCheck; + @Input() newButton = false; + @Input() editButton = false; + @Input() removeButton = false; + @Input() error?: string; + @Output() errorChange = new EventEmitter(); + @Output() change = new EventEmitter(); + @Output() preview = new EventEmitter(); + @ViewChild('nameInput', { static: true }) nameInput!: ElementRef; + @ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef; + @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; + removing = false; + private locked = false; // TEMP: move to model + constructor( + private element: ElementRef, + private router: Router, + private model: Model, + private gameService: GameService, + ) { + } + get joining() { + return this.gameService.joining; + } + get pony() { + return this.model.pony; + } + get canNew() { + return !this.joining && this.model.account && this.model.account.characterCount < this.model.characterLimit; + } + get canEdit() { + return !this.joining; + } + get canRemove() { + return !this.joining && !this.locked && !this.model.pending && !!this.pony + && !!this.pony.id && this.error !== VERSION_ERROR; + } + get hasPonies() { + return !!this.model.ponies.length; + } + select(pony: PonyObject) { + if (pony) { + this.removing = false; + this.model.selectPony(pony); + this.change.emit(pony); + this.preview.emit(undefined); + } + + this.dropdown.close(); + this.focusName(); + } + createNew() { + if (this.canNew) { + this.removing = false; + this.model.selectPony(createDefaultPonyObject()); + this.change.emit(this.pony); + this.router.navigate(['/character']); + this.focusName(); + } + } + edit() { + if (this.canEdit) { + this.removing = false; + this.router.navigate(['/character']); + } + } + remove() { + if (this.canRemove) { + this.removing = true; + focusElementAfterTimeout(this.element.nativeElement, '.cancel-remove-button'); + } + } + cancelRemove() { + this.removing = false; + focusElementAfterTimeout(this.element.nativeElement, '.remove-button'); + } + confirmRemove() { + if (this.canRemove) { + this.setError(undefined); + this.removing = false; + this.locked = true; + + this.model.removePony(this.pony) + .then(() => this.change.emit(this.pony)) + .catch((e: Error) => this.setError(e.message)) + .then(() => this.ariaAnnounce.nativeElement.textContent = 'Character removed') + .then(() => delay(2000)) + .then(() => this.locked = false) + .then(() => this.focusName()); + } + } + onToggle(show: boolean) { + if (!show) { + this.preview.emit(undefined); + } + } + private focusName() { + if (!isMobile) { + this.nameInput.nativeElement.focus(); + } + } + private setError(error: string | undefined) { + this.error = error; + this.errorChange.emit(error); + } +} diff --git a/src/ts/components/shared/chat-box/chat-box.pug b/src/ts/components/shared/chat-box/chat-box.pug new file mode 100644 index 0000000..d14a4f4 --- /dev/null +++ b/src/ts/components/shared/chat-box/chat-box.pug @@ -0,0 +1,14 @@ +.chat-box(#chatBox) + div(#chatBoxInput) + input.chat-input#chat-input( + #inputElement [(ngModel)]="message" (keydown)="keydown($event)" [maxlength]="maxSayLength" + aria-label="Chat message") + .chat-box-type(#typeBox (click)="toggleChatType()") + | #[span(#typePrefix)][#[span.chat-box-type-name(#typeName)]]: + button.game-button.chat-send-button((click)="send($event)" title="Send message" aria-label="Send message") + fa-icon([icon]="sendIcon") + +button.game-button.chat-open-button( + (click)="toggle()" (mousedown)="$event.preventDefault()" [disabled]="disabled" title="Toggle chat" + aria-label="Toggle message") + fa-icon([icon]="commentIcon" [fixedWidth]="true") diff --git a/src/ts/components/shared/chat-box/chat-box.scss b/src/ts/components/shared/chat-box/chat-box.scss new file mode 100644 index 0000000..611fb1e --- /dev/null +++ b/src/ts/components/shared/chat-box/chat-box.scss @@ -0,0 +1,88 @@ +@import '../../../../styles/partials/variables'; + +.chat-box { + max-width: 500px; + height: 37px; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.chat-open-button { + display: block; + position: absolute; + left: 0; + bottom: 0; + z-index: 2; + width: 42px; + height: 40px; +} + +.chat-send-button { + position: absolute; + right: 0; + top: 0; + padding: 0 8px; +} + +.chat-input { + opacity: 0.6; + background: black; + color: white; + border: none; + z-index: 1; + padding: 8px; + font-size: 14px; + padding-right: 35px; + padding-left: 90px; + border-radius: $border-radius-base; + outline: none; + width: 100%; +} + +.chat-box-type { + position: absolute; + left: 42px; + top: 8px; + font-size: 14px; + user-select: none; + cursor: pointer; + + .chat-party > &, .chat-party-think > & { + color: $chat-party; + } + + .chat-say > &, .chat-think > & { + color: $chat-color; + } + + .chat-sup > & { + color: $supporter-1; + } + + .chat-sup1 > & { + color: $supporter-1; + } + + .chat-sup2 > & { + color: $supporter-2; + } + + .chat-sup3 > & { + color: $supporter-3; + } + + .chat-whisper > & { + color: $chat-whisper; + } +} + +.chat-box-type-name { + display: inline-block; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; +} diff --git a/src/ts/components/shared/chat-box/chat-box.ts b/src/ts/components/shared/chat-box/chat-box.ts new file mode 100644 index 0000000..909afc7 --- /dev/null +++ b/src/ts/components/shared/chat-box/chat-box.ts @@ -0,0 +1,388 @@ +import { Component, ElementRef, NgZone, AfterViewInit, ViewChild, OnDestroy, Input } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { ChatType, isPartyChat, Entity, FakeEntity } from '../../../common/interfaces'; +import { SAY_MAX_LENGTH } from '../../../common/constants'; +import { Key } from '../../../client/input/input'; +import { PonyTownGame } from '../../../client/game'; +import { cleanMessage, isSpamMessage } from '../../../client/clientUtils'; +import { faComment, faAngleDoubleRight } from '../../../client/icons'; +import { isInParty } from '../../../client/partyUtils'; +import { handleActionCommand } from '../../../client/playerActions'; +import { hasHeadAnimation } from '../../../common/pony'; +import { AutocompleteState, autocompleteMesssage, replaceEmojis } from '../../../client/emoji'; +import { replaceNodes } from '../../../client/htmlUtils'; +import { invalidEnumReturn } from '../../../common/utils'; +import { findMatchingEntityNames, findEntityOrMockByAnyMeans, findBestEntityByName } from '../../../client/handlers'; + +const chatTypeNames: string[] = []; +const chatTypeClasses: string[] = []; + +function setupChatType(type: ChatType, name: string) { + chatTypeNames[type] = name; + chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`; +} + +setupChatType(ChatType.Say, 'say'); +setupChatType(ChatType.Party, 'party'); +setupChatType(ChatType.Supporter, 'sup'); +setupChatType(ChatType.Supporter1, 'sup1'); +setupChatType(ChatType.Supporter2, 'sup2'); +setupChatType(ChatType.Supporter3, 'sup3'); +setupChatType(ChatType.Whisper, 'whisper'); +setupChatType(ChatType.Think, 'think'); +setupChatType(ChatType.PartyThink, 'party think'); + +function isActionCommand(message: string) { + return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message); +} + +@Component({ + selector: 'chat-box', + templateUrl: 'chat-box.pug', + styleUrls: ['chat-box.scss'], +}) +export class ChatBox implements AfterViewInit, OnDestroy { + readonly maxSayLength = SAY_MAX_LENGTH; + readonly commentIcon = faComment; + readonly sendIcon = faAngleDoubleRight; + @ViewChild('inputElement', { static: true }) inputElement!: ElementRef; + @ViewChild('typeBox', { static: true }) typeBox!: ElementRef; + @ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef; + @ViewChild('typeName', { static: true }) typeName!: ElementRef; + @ViewChild('chatBox', { static: true }) chatBox!: ElementRef; + @ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef; + isOpen = false; + message: string | undefined = ''; + chatType = ChatType.Say; + private pasted = false; + private lastMessages: string[] = []; + private state: AutocompleteState = {}; + private subscriptions: Subscription[]; + private _disabled = false; + constructor(private game: PonyTownGame, zone: NgZone) { + this.subscriptions = [ + this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))), + this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())), + this.game.onCommand.subscribe(() => zone.run(() => this.command())), + this.game.onLeft.subscribe(() => { + this.chatType = ChatType.Say; + this.close(); + }), + ]; + + this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false; + } + @Input() get disabled() { + return this._disabled; + } + set disabled(value) { + this._disabled = value; + + if (value) { + this.close(); + } + } + get input() { + return this.inputElement.nativeElement as HTMLInputElement; + } + ngAfterViewInit() { + this.chatBox.nativeElement.hidden = true; + this.input.addEventListener('paste', () => this.pasted = true); + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + send(_event: Event | undefined) { + let chatType = this.chatType; + let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH); + const handled = handleActionCommand(message, this.game); + const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages); + const empty = !this.game.player || !message; + const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player); + const whisperTo = this.game.whisperTo; + let entityId = whisperTo && whisperTo.id || 0; + + if (/^\/(w|whisper) .+$/i.test(message)) { + chatType = ChatType.Whisper; + message = message.substr(/^\/w /i.test(message) ? 3 : 9); + + let offset = 0; + let entity: Entity | FakeEntity | undefined = undefined; + + do { + offset = message.indexOf(' ', offset); + + if (offset === -1) + break; + + const name = message.substr(0, offset); + entity = findBestEntityByName(this.game, name); + offset++; + } while (!entity); + + if (entity) { + message = message.substr(offset); + entityId = entity.id; + } else { + entityId = 0; + } + } + + if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) { + if (message) { + this.lastMessages.push(message); + + while (this.lastMessages.length > 5) { + this.lastMessages.shift(); + } + } + + this.close(); + } + } + keydown(e: KeyboardEvent) { + if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) { + this.state.lastEmoji = undefined; + } + + if (e.keyCode === Key.TAB) { + if (this.message) { + if (/^\/(w|whisper) .+$/i.test(this.message)) { + const space = this.message.indexOf(' '); + const names = findMatchingEntityNames(this.game, this.message.substr(space + 1)); + + if (names.length === 1) { + this.message = `${this.message.substring(0, space)} ${names[0]}`; + } + } else { + this.message = autocompleteMesssage(this.message, e.shiftKey, this.state); + } + } + + e.preventDefault(); + } else if (e.keyCode === Key.ENTER && this.isOpen) { + this.send(e); + } else if (e.keyCode === Key.ESCAPE) { + this.close(); + e.preventDefault(); + } else if (e.keyCode === Key.SPACE) { + if (!this.message) + return; + + const isParty = /^\/(p|party)$/i.test(this.message); + const isSay = /^\/(s|say)$/i.test(this.message); + const isSup = /^\/(ss)$/i.test(this.message); + const isSup1 = /^\/(s1)$/i.test(this.message); + const isSup2 = /^\/(s2)$/i.test(this.message); + const isSup3 = /^\/(s3)$/i.test(this.message); + + const supporter = this.game.model.supporter; + const isSayOrInvalid = isSay + || (isParty && !isInParty(this.game)) + || (isSup && supporter === 0) + || (isSup1 && supporter < 1) + || (isSup2 && supporter < 2) + || (isSup3 && supporter < 3); + + if (isSayOrInvalid) { + this.changeChatType(e, ChatType.Say); + } else if (isParty) { + this.changeChatType(e, ChatType.Party); + } else if (isSup) { + this.changeChatType(e, ChatType.Supporter); + } else if (isSup1) { + this.changeChatType(e, ChatType.Supporter1); + } else if (isSup2) { + this.changeChatType(e, ChatType.Supporter2); + } else if (isSup3) { + this.changeChatType(e, ChatType.Supporter3); + } else if (/^\/(t|think)$/i.test(this.message)) { + if (isPartyChat(this.chatType)) { + this.changeChatType(e, ChatType.PartyThink); + } else { + this.changeChatType(e, ChatType.Think); + } + } else if (/^\/(r|reply)$/i.test(this.message)) { + const lastWhisperFrom = this.game.lastWhisperFrom; + const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId); + + if (entity) { + this.game.whisperTo = entity; + this.changeChatType(e, ChatType.Whisper); + } else { + this.changeChatType(e, ChatType.Say); + } + } else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) { + const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9); + const entity = findBestEntityByName(this.game, name); + + if (entity) { + this.game.whisperTo = entity; + this.changeChatType(e, ChatType.Whisper); + } + } + } + } + private say(message: string, chatType: ChatType, entityId: number): boolean { + this.game.lastChatMessageType = chatType; + return !!this.game.send(server => server.say(entityId, message, chatType)); + } + private changeChatType(e: KeyboardEvent, chatType: ChatType) { + this.chatType = chatType; + this.message = ''; + this.updateChatType(); + e.preventDefault(); + } + private chat(event: Event | undefined) { + if (this.isOpen) { + this.send(event); + } else { + this.open(); + } + } + private command() { + if (!this.isOpen) { + this.chat(undefined); + this.message = '/'; + this.input.selectionStart = this.input.selectionEnd = 10000; + } + } + private open() { + if (!this.isOpen) { + this.isOpen = true; + this.chatBox.nativeElement.hidden = false; + } + + this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say; + this.updateChatType(); + this.input.focus(); + } + private close() { + if (this.isOpen) { + this.input.blur(); + this.isOpen = false; + this.chatBox.nativeElement.hidden = true; + this.message = ''; + this.pasted = false; + } + } + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + toggleChatType() { + const chatTypes = getChatTypes(this.game); + this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length]; + this.updateChatType(); + this.input.focus(); + } + setChatType(type: 'say' | 'party' | 'whisper') { + if (type === 'say') { + this.chatType = ChatType.Say; + this.open(); + } else if (type === 'party' && isInParty(this.game)) { + this.chatType = ChatType.Party; + this.open(); + } else if (type === 'whisper') { + this.chatType = ChatType.Whisper; + this.open(); + } + } + private currentTypeClass = ''; + private currentTypePrefix = ''; + private currentTypeName = ''; + private updateChatType() { + let typeName: string; + let typePrefix: string; + let changed = false; + + const typeClass = chatTypeClass(this.chatType, this.game.model.supporter); + + if (this.currentTypeClass !== typeClass) { + this.currentTypeClass = typeClass; + (this.chatBoxInput.nativeElement as HTMLElement).className = typeClass; + } + + if (this.chatType === ChatType.Whisper) { + typePrefix = 'To '; + typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown'; + } else { + typePrefix = ''; + typeName = chatTypeNames[this.chatType]; + } + + if (this.currentTypePrefix !== typePrefix) { + changed = true; + this.currentTypePrefix = typePrefix; + (this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix; + } + + if (this.currentTypeName !== typeName) { + changed = true; + this.currentTypeName = typeName; + replaceNodes(this.typeName.nativeElement, typeName); + } + + if (changed) { + const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect(); + const padding = 35 + 13 + Math.ceil(width); + (this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`; + } + } +} + +function chatTypeClass(chatType: ChatType, supporter: number) { + if (chatType === ChatType.Supporter) { + switch (supporter) { + case 1: return 'chat-sup chat-sup1'; + case 2: return 'chat-sup chat-sup2'; + case 3: return 'chat-sup chat-sup3'; + } + } + + return chatTypeClasses[chatType]; +} + +function isValidChatType(type: ChatType, game: PonyTownGame) { + const supporter = game.model.supporter; + + switch (type) { + case ChatType.Say: + case ChatType.Think: + case ChatType.Whisper: + return true; + case ChatType.Party: + case ChatType.PartyThink: + return isInParty(game); + case ChatType.Supporter: + return supporter > 0; + case ChatType.Supporter1: + return supporter >= 1; + case ChatType.Supporter2: + return supporter >= 2; + case ChatType.Supporter3: + return supporter >= 3; + case ChatType.Dismiss: + return false; + default: + return invalidEnumReturn(type, false); + } +} + +function getChatTypes(game: PonyTownGame) { + const chatTypes = [ChatType.Say]; + const supporter = game.model.supporter; + + if (isInParty(game)) { + chatTypes.push(ChatType.Party); + } + + if (supporter) { + chatTypes.push(ChatType.Supporter); + } + + return chatTypes; +} diff --git a/src/ts/components/shared/chat-log/chat-log.pug b/src/ts/components/shared/chat-log/chat-log.pug new file mode 100644 index 0000000..3bc0af1 --- /dev/null +++ b/src/ts/components/shared/chat-log/chat-log.pug @@ -0,0 +1,27 @@ +.chat-log(#chatLog) + .d-flex.chat-log-buttons + .flex-grow-1.d-flex.flex-column-reverse + button.game-button.mb-2((click)="scrollToEnd()" title="Scroll to end") + fa-icon([icon]="toBottomIcon" [fixedWidth]="true") + + .chat-log-content(#content) + fa-icon.resize-icon([icon]="resizeIcon") + .chat-log-tabs + a.link-plain.chat-log-tab(#localTab tabindex (click)="switchTab('local')") + | Local + a.link-plain.chat-log-tab(#partyTab tabindex (click)="switchTab('party')") + | Party + a.link-plain.chat-log-tab(#whisperTab tabindex (click)="switchTab('whisper')") + | Whisper + .chat-log-scroll-outer() + .chat-log-scroll-inner(#scroll) + .chat-log-scroll-inner-inner(#lines) + .chat-log-resize-top((agDrag)="drag($event, true, false)") + .chat-log-resize-right((agDrag)="drag($event, false, true)") + .chat-log-resize-top-right((agDrag)="drag($event, true, true)") + +button.game-button.chat-log-toggle(#toggleButton (click)="toggle()" title="Toggle chatlog") + svg.fa-icon(aria-hidden="true" class="svg-inline--fa fa-comments fa-w-18 fa-fw" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512") + path.main-bubble(fill="currentColor" d="M416 192c0-88.4-93.1-160-208-160S0 103.6 0 192c0 34.3 14.1 65.9 38 92-13.4 30.2-35.5 54.2-35.8 54.5-2.2 2.3-2.8 5.7-1.5 8.7S4.8 352 8 352c36.6 0 66.9-12.3 88.7-25 32.2 15.7 70.3 25 111.3 25 114.9 0 208-71.6 208-160z") + path(fill="currentColor" d="M538 412c23.9-26 38-57.7 38-92 0-66.9-53.5-124.2-129.3-148.1.9 6.6 1.3 13.3 1.3 20.1 0 105.9-107.7 192-240 192-10.8 0-21.3-.8-31.7-1.9C207.8 439.6 281.8 480 368 480c41 0 79.1-9.2 111.3-25 21.8 12.7 52.1 25 88.7 25 3.2 0 6.1-1.9 7.3-4.8 1.3-2.9.7-6.3-1.5-8.7-.3-.3-22.4-24.2-35.8-54.5z") + .whispers-count(#count) diff --git a/src/ts/components/shared/chat-log/chat-log.scss b/src/ts/components/shared/chat-log/chat-log.scss new file mode 100644 index 0000000..62796ae --- /dev/null +++ b/src/ts/components/shared/chat-log/chat-log.scss @@ -0,0 +1,159 @@ +@import '../../../../styles/partials/variables'; + +$chat-log-bg: rgba(0, 0, 0, 0.2); +$chat-log-bg-faded: rgba(0, 0, 0, 0.1); +$chat-log-bg-hover: rgba(0, 0, 0, 0.5); +$chat-log-tabs-height: 30px; +$chat-log-buttons-width: 40px; + +$resizer-size: 12px; +$resizer-offset: $resizer-size / 2; + +.chat-log { + display: flex; + position: absolute; + bottom: 0; + left: 0; + right: 0; + min-width: 200px; + max-width: 100%; + max-height: calc(100vh - 220px); + min-height: 120px; +} + +.chat-log-buttons { + flex-direction: column-reverse; + width: $chat-log-buttons-width; + padding: 5px 3px; + padding-bottom: 30px; + z-index: 2; +} + +.chat-log-toggle { + position: absolute; + bottom: 5px; + left: 4px; + z-index: 3; +} + +.has-unread .main-bubble { + color: $chat-whisper; +} + +.whispers-count { + color: white; + font-weight: bold; + text-align: center; + position: absolute; + left: 6px; + top: 9px; + width: 16px; + font-size: 10px; + display: flex; + justify-content: center; + align-items: center; + line-height: 1rem; +} + +.whisper-icon { + position: absolute; + top: 0; + left: 0; + font-size: 14px; + color: $chat-whisper !important; +} + +.chat-log-content { + position: relative; + display: flex; + background: $chat-log-bg; + padding: 5px 0; + padding-left: 40px; + margin-left: -$chat-log-buttons-width; + border-radius: $border-radius-base; + word-break: break-word; + white-space: pre-line; + line-height: 20px; + height: 100%; + width: 100%; +} + +.chat-log-tabs { + position: absolute; + top: -$chat-log-tabs-height; + left: 30px; + display: flex; + user-select: none; +} + +.chat-log-tab { + background: $chat-log-bg-faded; + padding: 5px 10px 0; + margin-left: 10px; + height: $chat-log-tabs-height; + border-top-left-radius: $border-radius-base; + border-top-right-radius: $border-radius-base; + color: white; + overflow: hidden; + + &.active { + background: $chat-log-bg; + } +} + +.chat-log-scroll-outer { + overflow: hidden; + width: 100%; +} + +.chat-log-scroll-inner { + width: calc(100% + 50px); + height: 100%; + overflow-x: hidden; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} + +.chat-log-scroll-inner-inner { + display: flex; + flex-direction: column; + justify-content: flex-end; + min-height: 100%; + padding: 0 5px; +} + +.chat-log-resize-top { + position: absolute; + top: -$resizer-offset; + right: $resizer-offset; + height: $resizer-size; + left: 250px; + cursor: ns-resize; +} + +.chat-log-resize-top-right { + position: absolute; + top: -($resizer-offset + 1); + right: -($resizer-offset + 1); + width: $resizer-size + 4; + height: $resizer-size + 4; + cursor: nesw-resize; +} + +.chat-log-resize-right { + position: absolute; + top: $resizer-offset; + right: -$resizer-offset; + width: $resizer-size; + bottom: $resizer-offset; + cursor: ew-resize; +} + +.resize-icon { + font-size: 18px; + position: absolute; + top: -3px; + right: 1px; + transform: rotate(45deg); + color: rgba(255, 255, 255, 0.2); +} diff --git a/src/ts/components/shared/chat-log/chat-log.ts b/src/ts/components/shared/chat-log/chat-log.ts new file mode 100644 index 0000000..1ac6223 --- /dev/null +++ b/src/ts/components/shared/chat-log/chat-log.ts @@ -0,0 +1,620 @@ +import { + Component, ViewChild, ElementRef, NgZone, AfterViewInit, OnDestroy, HostListener, Output, EventEmitter, DoCheck +} from '@angular/core'; +import { Subscription } from 'rxjs'; +import { clamp, escapeRegExp } from 'lodash'; +import { PonyTownGame } from '../../../client/game'; +import { MessageType, isPartyMessage, ChatMessage, Pony, FakeEntity, isWhisper, isWhisperTo } from '../../../common/interfaces'; +import { SettingsService } from '../../services/settingsService'; +import { AgDragEvent } from '../directives/agDrag'; +import { element, textNode, removeAllNodes, replaceNodes } from '../../../client/htmlUtils'; +import { DEFAULT_CHATLOG_OPACITY, PONY_TYPE } from '../../../common/constants'; +import { faCaretUp, faArrowDown } from '../../../client/icons'; +import { sampleMessages } from '../../../common/debugData'; + +interface IndexEntryUser { + id: number; + crc: number | undefined; +} + +interface IndexEntry { + users: IndexEntryUser[]; + counter: number; +} + +interface ChatLogLineDOM { + entry: ChatLogMessage; + root: HTMLElement; + label: HTMLElement; + labelText: Text; + name: HTMLElement; + nameContent: HTMLElement; + index: HTMLElement; + indexText: Text; + prefixText: Text; + suffixText: Text; + message: HTMLElement; +} + +export interface ChatLogMessage { + message: string; + name?: string; + crc?: number; + prefix?: string; + suffix?: string; + label?: string; + index: number; + classes?: string; + entityId?: number; + dom?: ChatLogLineDOM; +} + +type Tab = 'local' | 'party' | 'whisper'; +type ClickHandler = (entry: ChatLogMessage) => void; + +const GENERAL_CHAT_LIMIT = 100; +const PARTY_CHAT_LIMIT = 100; +const WHISPER_CHAT_LIMIT = 100; +const FORGET_INDEX_AFTER = 1000; +const SCROLL_END_THRESHOLD = 60; + +const LABELS: (string | undefined)[] = []; +LABELS[MessageType.System] = 'system'; +LABELS[MessageType.Admin] = 'admin'; +LABELS[MessageType.Mod] = 'mod'; +LABELS[MessageType.Party] = 'party'; +LABELS[MessageType.PartyThinking] = 'party'; +LABELS[MessageType.PartyAnnouncement] = 'party'; + +const PREFIXES: (string | undefined)[] = []; +PREFIXES[MessageType.WhisperTo] = 'To '; +PREFIXES[MessageType.WhisperToAnnouncement] = 'To '; + +const SUFFIXES: (string | undefined)[] = []; +SUFFIXES[MessageType.Thinking] = 'thinks'; +SUFFIXES[MessageType.PartyThinking] = 'thinks'; +SUFFIXES[MessageType.Whisper] = 'whispers'; +SUFFIXES[MessageType.WhisperAnnouncement] = 'whispers'; + +const CLASSES: (string | undefined)[] = []; +CLASSES[MessageType.System] = 'chat-line-system'; +CLASSES[MessageType.Admin] = 'chat-line-admin'; +CLASSES[MessageType.Mod] = 'chat-line-mod'; +CLASSES[MessageType.Party] = 'chat-line-party'; +CLASSES[MessageType.Thinking] = 'chat-line-thinking'; +CLASSES[MessageType.PartyThinking] = 'chat-line-party-thinking'; +CLASSES[MessageType.Announcement] = 'chat-line-announcement'; +CLASSES[MessageType.PartyAnnouncement] = 'chat-line-party-announcement'; +CLASSES[MessageType.Supporter1] = 'chat-line-supporter-1'; +CLASSES[MessageType.Supporter2] = 'chat-line-supporter-2'; +CLASSES[MessageType.Supporter3] = 'chat-line-supporter-3'; +CLASSES[MessageType.Whisper] = 'chat-line-whisper'; +CLASSES[MessageType.WhisperTo] = 'chat-line-whisper'; +CLASSES[MessageType.WhisperAnnouncement] = 'chat-line-whisper-announcement'; +CLASSES[MessageType.WhisperToAnnouncement] = 'chat-line-whisper-announcement'; + +export function createChatLogLineDOM(clickLabel: ClickHandler, clickName: ClickHandler): ChatLogLineDOM { + const line: ChatLogLineDOM = {} as any; + + line.root = element('div', 'chat-line', [ + element('span', 'chat-line-lead'), + line.label = element( + 'span', 'chat-line-label mr-1', [line.labelText = textNode('')], undefined, { click: () => clickLabel(line.entry) }), + line.prefixText = textNode(''), + line.name = element('span', 'chat-line-name', [ + textNode('['), + line.nameContent = element( + 'span', 'chat-line-name-content', [textNode('')], undefined, { click: () => clickName(line.entry) }), + line.index = element('span', 'chat-line-name-index', [line.indexText = textNode('')], { title: 'duplicate name' }), + textNode(']'), + ]), + line.suffixText = textNode(''), + line.message = element('span', 'chat-line-message', [textNode('')]), + ]); + + return line; +} + +export function updateChatLogLine(line: ChatLogLineDOM, entry: ChatLogMessage) { + const { classes, label, message, prefix, suffix } = entry; + const hasSpace = message.indexOf(' ') !== -1; + + line.entry = entry; + line.root.className = `chat-line ${hasSpace ? '' : 'chat-line-break '}${classes}`.trim(); + line.label.style.display = label ? 'inline' : 'none'; + line.labelText.nodeValue = label ? `[${label}]` : ''; + + updateChatLogName(line, entry); + + line.prefixText.nodeValue = prefix || ''; + line.suffixText.nodeValue = suffix ? ` ${suffix}: ` : ': '; + replaceNodes(line.message, message); +} + +function updateChatLogName(line: ChatLogLineDOM, { name, index }: ChatLogMessage) { + if (name) { + line.name.style.display = 'inline'; + replaceNodes(line.nameContent, name); + line.index.style.display = (index > 0) ? 'inline' : 'none'; + line.indexText.nodeValue = (index > 0) ? ` #${index + 1}` : ''; + } else { + line.name.style.display = 'none'; + } +} + +function isMatch(e: ChatLogMessage, id: number, name: string, crc: number | undefined) { + return e.name === name && (e.entityId === id || (e.crc === crc && crc !== undefined)); +} + +function addOrUpdatePony(pony: Pony, list: ChatLogMessage[]) { + for (const e of list) { + if (isMatch(e, pony.id, pony.name!, pony.crc)) { + e.entityId = pony.id; + } + } +} + +function updateEntityId(list: ChatLogMessage[], oldId: number, newId: number) { + for (const e of list) { + if (e.entityId === oldId) { + e.entityId = newId; + } + } +} + +function findUserIndex(users: IndexEntryUser[], id: number, crc: number | undefined) { + for (let i = 0; i < users.length; i++) { + const user = users[i]; + + if (user.id === id || (crc !== undefined && user.crc === crc)) { + user.id = id; + return i; + } + } + + return -1; +} + +@Component({ + selector: 'chat-log', + templateUrl: 'chat-log.pug', + styleUrls: ['chat-log.scss'], +}) +export class ChatLog implements AfterViewInit, OnDestroy, DoCheck { + readonly toBottomIcon = faArrowDown; + readonly resizeIcon = faCaretUp; + @ViewChild('chatLog', { static: true }) chatLog!: ElementRef; + @ViewChild('scroll', { static: true }) scroll!: ElementRef; + @ViewChild('lines', { static: true }) lines!: ElementRef; + @ViewChild('localTab', { static: true }) localTab!: ElementRef; + @ViewChild('partyTab', { static: true }) partyTab!: ElementRef; + @ViewChild('whisperTab', { static: true }) whisperTab!: ElementRef; + @ViewChild('toggleButton', { static: true }) toggleButton!: ElementRef; + @ViewChild('count', { static: true }) countElement!: ElementRef; + @ViewChild('content', { static: true }) contentElement!: ElementRef; + @Output() toggleType = new EventEmitter(); + @Output() nameClick = new EventEmitter(); + innerWidth = 0; + // TODO: move to game ? + local: ChatLogMessage[] = []; + party: ChatLogMessage[] = []; + whisper: ChatLogMessage[] = []; + unread = 0; + private subscriptions: Subscription[] = []; + private startX = 0; + private startY = 0; + private shouldScrollToEnd = false; + private scrolledToEnd = true; + private scrollingToEnd = false; + private scrollToEndAtFrame = false; + private indexes = new Map(); + private messageCounter = 0; + private lastOpacity = 0; + constructor( + private game: PonyTownGame, + private settingsService: SettingsService, + private element: ElementRef, + private zone: NgZone, + ) { + // TODO: just put reference to chatlog on game ??? + this.subscriptions.push( + game.onFrame.subscribe(() => { + if (this.scrollToEndAtFrame) { + this.scrollToEndAtFrame = false; + this.scrollHandler(); + } + }), + game.onMessage.subscribe(message => { + this.addMessage(message); + }), + // TODO: move to game ? + game.onPonyAddOrUpdate.subscribe(pony => { + addOrUpdatePony(pony, this.local); + addOrUpdatePony(pony, this.party); + addOrUpdatePony(pony, this.whisper); + }), + game.onJoined.subscribe(() => { + if (!DEVELOPMENT) { + // TODO: move to game ? + this.local = []; + this.party = []; + this.whisper = []; + this.messageCounter = 0; + this.indexes = new Map(); + this.clearList(); + } + }), + game.onEntityIdUpdate.subscribe(update => { + updateEntityId(this.local, update.old, update.new); + updateEntityId(this.party, update.old, update.new); + updateEntityId(this.whisper, update.old, update.new); + }), + ); + } + get linesElement() { + return this.lines.nativeElement as HTMLElement; + } + private updateOpen() { + this.updateChatlog(); + + if (this.open) { + this.setUnread(0); + this.regenerateList(); + this.scrollToEnd(); + } else { + this.clearList(); + } + + this.updateInnerWidth(); + } + private updateChatlog() { + const element = this.chatLog.nativeElement as HTMLElement; + element.style.display = this.open ? 'flex' : 'none'; + + if (this.open) { + element.style.width = `${this.width}px`; + element.style.height = `${this.height}px`; + } + } + ngAfterViewInit() { + this.game.findEntityFromChatLog = this.findEntityFromMessages; + this.game.findEntityFromChatLogByName = this.findEntityFromMessagesByName; + + this.updateTabs(); + this.updateOpen(); + + this.zone.runOutsideAngular(() => { + const scroll = this.scroll.nativeElement as HTMLElement; + + scroll.addEventListener('scroll', () => { + if (this.scrollingToEnd) { + this.scrolledToEnd = true; + this.scrollingToEnd = false; + } else { + const clientHeight = scroll.getBoundingClientRect().height; + this.scrolledToEnd = scroll.scrollTop >= (scroll.scrollHeight - clientHeight - SCROLL_END_THRESHOLD); + } + }); + }); + + setTimeout(() => { + this.scrollToEnd(); + this.updateInnerWidth(); + }); + + if (DEVELOPMENT) { + sampleMessages.forEach(({ name, id, message, type }) => + this.addMessage({ id: id || 999999, crc: undefined, name, message, type: type || MessageType.Chat })); + } + } + ngOnDestroy() { + if (this.game.findEntityFromChatLog === this.findEntityFromMessages) { + this.game.findEntityFromChatLog = () => undefined; + } + + if (this.game.findEntityFromChatLogByName === this.findEntityFromMessagesByName) { + this.game.findEntityFromChatLogByName = () => undefined; + } + + this.subscriptions.forEach(s => s.unsubscribe()); + this.subscriptions = []; + } + ngDoCheck() { + if (this.lastOpacity !== this.opacity) { + this.lastOpacity = this.opacity; + this.contentElement.nativeElement.style.backgroundColor = this.bg; + this.updateTabs(); + } + } + @HostListener('window:resize') + updateInnerWidth() { + const maxWidth = (this.element.nativeElement as HTMLElement).getBoundingClientRect().width; + const innerWidth = Math.min(maxWidth || this.width, this.width) - 40; + + if (this.innerWidth !== innerWidth) { + this.innerWidth = innerWidth; + this.linesElement.style.width = `${innerWidth}px`; + } + + if (!maxWidth) { + setTimeout(() => this.updateInnerWidth(), 10); + } + } + get messages() { + return this[this.activeTab]; + } + get settings() { + return this.settingsService.browser; + } + get settings2() { + return this.settingsService.account; + } + get activeTab(): Tab { + const tab = this.settings.chatlogTab; + return (tab === 'local' || tab === 'party' || tab === 'whisper') ? tab : 'local'; + } + get open() { + return !this.settings.chatlogClosed; + } + get width() { + return this.settings.chatlogWidth || 500; + } + get height() { + return this.settings.chatlogHeight || 310; + } + get opacity() { + return this.settings2.chatlogOpacity === undefined ? DEFAULT_CHATLOG_OPACITY : this.settings2.chatlogOpacity; + } + get bg() { + return `rgba(0, 0, 0, ${this.opacity / 100})`; + } + get inactiveBg() { + return `rgba(0, 0, 0, ${(this.opacity / 200) * 0.5})`; + } + private createEntry({ id, crc, name, message, type }: ChatMessage): ChatLogMessage { + const system = type === MessageType.System; + const entry: ChatLogMessage = { + entityId: system ? 0 : id, + name: system ? '' : name, + index: 0, + crc, + message, + label: LABELS[type] || '', + prefix: PREFIXES[type] || '', + suffix: SUFFIXES[type] || '', + classes: CLASSES[type] || '', + }; + + if (!system) { + entry.index = this.findOrCreateIndex(name, id, crc); + } + + return entry; + } + private findOrCreateIndex(name: string, id: number, crc: number | undefined) { + let found = this.indexes.get(name); + + if (!found || (this.messageCounter - found.counter) > FORGET_INDEX_AFTER) { + found = { + users: [{ id, crc }], + counter: 0, + }; + + this.indexes.set(name, found); + } + + found.counter = this.messageCounter; + + let index = findUserIndex(found.users, id, crc); + + if (index === -1) { + index = found.users.length; + found.users.push({ id, crc }); + } + + return index; + } + addMessage(message: ChatMessage) { + if (message.name && message.message) { + const entry = this.createEntry(message); + const party = isPartyMessage(message.type); + const whisper = isWhisper(message.type) || isWhisperTo(message.type); + const open = this.open; + const scrolledToEnd = open ? this.scrolledToEnd : false; + const tab = this.activeTab; + + this.addEntryToList(this.local, GENERAL_CHAT_LIMIT, open && tab === 'local', entry); + + if (party || whisper) { + const partyEntry = { ...entry }; + partyEntry.dom = undefined; + partyEntry.label = whisper ? partyEntry.label : undefined; + this.addEntryToList(this.party, PARTY_CHAT_LIMIT, open && tab === 'party', partyEntry); + } + + if (whisper) { + const whisperEntry = { ...entry }; + whisperEntry.dom = undefined; + whisperEntry.label = undefined; + this.addEntryToList(this.whisper, WHISPER_CHAT_LIMIT, open && tab === 'whisper', whisperEntry); + } + + if (message.type === MessageType.Whisper && !this.open) { + this.setUnread(this.unread + 1); + } + + if (scrolledToEnd) { + this.scrollToEnd(); + } + + this.messageCounter++; + } + } + private addEntryToList(list: ChatLogMessage[], limit: number, isOpen: boolean, entry: ChatLogMessage) { + let removedDom: ChatLogLineDOM | undefined; + + while (list.length >= limit) { + const removed = list.shift(); + + if (isOpen && removed && removed.dom) { + if (removed.dom.root.parentElement) { + removed.dom.root.parentElement.removeChild(removed.dom.root); + } + + removedDom = removed.dom; + removed.dom = undefined; + } + } + + list.push(entry); + + if (isOpen) { + entry.dom = removedDom || createChatLogLineDOM(this.clickLabel, this.clickNameHandler); + updateChatLogLine(entry.dom, entry); + this.linesElement.appendChild(entry.dom.root); + } + } + toggle() { + this.settings.chatlogClosed = !this.settings.chatlogClosed; + this.settingsService.saveBrowserSettings(); + this.updateOpen(); + } + switchTab(tab: Tab) { + if (this.activeTab !== tab) { + this.settings.chatlogTab = tab; + this.settingsService.saveBrowserSettings(); + this.regenerateList(); + this.scrollToEnd(); + this.updateTabs(); + } + } + private updateTabs() { + this.setActiveTab(this.localTab.nativeElement, this.activeTab === 'local'); + this.setActiveTab(this.partyTab.nativeElement, this.activeTab === 'party'); + this.setActiveTab(this.whisperTab.nativeElement, this.activeTab === 'whisper'); + } + private setActiveTab(tab: HTMLElement, active: boolean) { + if (active) { + tab.classList.add('active'); + tab.style.backgroundColor = this.bg; + } else { + tab.classList.remove('active'); + tab.style.backgroundColor = this.inactiveBg; + } + } + scrollToEnd() { + // requestAnimationFrame(this.scrollHandler); + this.scrollToEndAtFrame = true; + } + scrollHandler = () => { + this.scrollingToEnd = true; + this.scroll.nativeElement.scrollTop = 99999; + } + clickNameHandler = (message: ChatLogMessage) => { + this.zone.run(() => this.nameClick.emit(message)); + } + clickLabel = (message: ChatLogMessage) => { + this.zone.run(() => { + if (message.label) { + this.toggleType.emit(message.label); + } + }); + } + private clearList() { + removeAllNodes(this.linesElement); + } + private regenerateList() { + this.clearList(); + + const lines = this.linesElement; + + this.messages.forEach(entry => { + if (!entry.dom) { + entry.dom = createChatLogLineDOM(this.clickLabel, this.clickNameHandler); + updateChatLogLine(entry.dom, entry); + } + + lines.appendChild(entry.dom.root); + }); + } + drag({ x, y, type, event }: AgDragEvent, resizeY: boolean, resizeX: boolean) { + event.preventDefault(); + + if (type === 'start') { + const { left, top } = (this.element.nativeElement as HTMLElement).getBoundingClientRect(); + this.startX = left; + this.startY = top; + this.shouldScrollToEnd = this.scrolledToEnd; + } + + if (resizeX) { + this.settings.chatlogWidth = clamp(x - this.startX, 200, 2000); + } + + if (resizeY) { + this.settings.chatlogHeight = clamp(this.startY - y, 120, 2000); + } + + this.updateChatlog(); + this.updateInnerWidth(); + + if (type === 'end') { + this.settingsService.saveBrowserSettings(); + } + + if (this.shouldScrollToEnd) { + this.scrollToEnd(); + } + } + private setUnread(value: number) { + if (this.unread !== value) { + this.unread = value; + const count = this.countElement.nativeElement as HTMLElement; + const toggle = this.toggleButton.nativeElement as HTMLElement; + + if (value) { + count.textContent = value > 99 ? '99+' : `${value}`; + toggle.classList.add('has-unread'); + } else { + count.textContent = ''; + toggle.classList.remove('has-unread'); + } + } + } + private findEntityFromMessages = (id: number): FakeEntity | undefined => { + return findEntityFromMessages(id, this.whisper) || + findEntityFromMessages(id, this.party) || + findEntityFromMessages(id, this.local); + } + private findEntityFromMessagesByName = (name: string): FakeEntity | undefined => { + return findEntityFromMessagesByName(name, this.game.playerId, this.whisper) || + findEntityFromMessagesByName(name, this.game.playerId, this.party) || + findEntityFromMessagesByName(name, this.game.playerId, this.local); + } +} + +function findEntityFromMessages(id: number, messages: ChatLogMessage[]): FakeEntity | undefined { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].entityId === id) { + return { fake: true, id, type: PONY_TYPE, name: messages[i].name, crc: messages[i].crc }; + } + } + + return undefined; +} + +function findEntityFromMessagesByName( + name: string, playerId: number | undefined, messages: ChatLogMessage[] +): FakeEntity | undefined { + const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i'); + + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + + if (message.name && message.entityId && message.entityId !== playerId && regex.test(message.name)) { + return { fake: true, id: message.entityId, type: PONY_TYPE, name: message.name, crc: message.crc }; + } + } + + return undefined; +} diff --git a/src/ts/components/shared/check-box/check-box.pug b/src/ts/components/shared/check-box/check-box.pug new file mode 100644 index 0000000..5357679 --- /dev/null +++ b/src/ts/components/shared/check-box/check-box.pug @@ -0,0 +1,8 @@ +button.check-box( + role="checkbox" + (click)="toggle()" + [class.disabled]="disabled" + [attr.aria-disabled]="disabled" + [attr.aria-label]="label || ''" + [attr.aria-checked]="checked") + fa-icon(*ngIf="checked" [icon]="icon" [fixedWidth]="true") diff --git a/src/ts/components/shared/check-box/check-box.scss b/src/ts/components/shared/check-box/check-box.scss new file mode 100644 index 0000000..f60f622 --- /dev/null +++ b/src/ts/components/shared/check-box/check-box.scss @@ -0,0 +1,30 @@ +@import '../../../../styles/partials/variables'; + +.check-box { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + height: $input-height; + width: $input-height; + margin: 0; + color: $input-color; + background-color: $input-bg; + border: 1px solid $border-color; + border-radius: $input-border-radius; + appearance: none; + + &:focus { + outline: none; + box-shadow: 0 0 0 $btn-focus-width rgba($focus-color, .5); + } + + &.disabled { + background: $input-disabled-bg; + color: $input-disabled-color; + } + + > fa-icon { + font-size: 17px; + } +} diff --git a/src/ts/components/shared/check-box/check-box.ts b/src/ts/components/shared/check-box/check-box.ts new file mode 100644 index 0000000..30ae192 --- /dev/null +++ b/src/ts/components/shared/check-box/check-box.ts @@ -0,0 +1,22 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { faCheck } from '../../../client/icons'; + +@Component({ + selector: 'check-box', + templateUrl: 'check-box.pug', + styleUrls: ['check-box.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckBox { + @Input() icon = faCheck; + @Input() label?: string; + @Input() disabled = false; + @Input() checked = false; + @Output() checkedChange = new EventEmitter(); + toggle() { + if (!this.disabled) { + this.checked = !this.checked; + this.checkedChange.emit(this.checked); + } + } +} diff --git a/src/ts/components/shared/color-picker/color-picker.pug b/src/ts/components/shared/color-picker/color-picker.pug new file mode 100644 index 0000000..4465cf6 --- /dev/null +++ b/src/ts/components/shared/color-picker/color-picker.pug @@ -0,0 +1,26 @@ +.color-picker.dropdown( + [class.disabled]="isDisabled" [class.open]="isOpen" [class.show]="isOpen") + //-[class.indicator]="indicatorColor" + .color-picker-box([style.background-color]="bg") + input.form-control.color-picker-input( + #input + type="text" + spellcheck="false" + (focus)="focus($event)" + (blur)="close()" + (input)="inputChanged(input.value)" + [value]="inputColor" + [disabled]="isDisabled" + [attr.aria-labelledby]="labelledBy" + [attr.aria-label]="label") + //-[style.border-left-color]="indicatorColor" + a.color-picker-chevron((mousedown)="toggleOpen()" [hidden]="isDisabled" aria-label="Pick color") + fa-icon([icon]="chevronIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right.color-picker-menu.show( + (mousedown)="stopEvent($event)" *ngIf="isOpen" aria-hidden="true") + .color-picker-sv( + [style.background-color]="hue" (agDrag)="dragSV($event)" agDragRelative="self" [agDragPrevent]="true") + .color-picker-sv-overlay + .color-wheel-circle-sv([style.left.%]="svLeft" [style.top.%]="svTop") + .color-picker-hue((agDrag)="dragHue($event)" agDragRelative="self" [agDragPrevent]="true") + .color-wheel-circle-hue([style.top.%]="hueTop") diff --git a/src/ts/components/shared/color-picker/color-picker.scss b/src/ts/components/shared/color-picker/color-picker.scss new file mode 100644 index 0000000..41eda6a --- /dev/null +++ b/src/ts/components/shared/color-picker/color-picker.scss @@ -0,0 +1,123 @@ +@import '../../../../styles/partials/variables'; +@import '../../../../styles/partials/mixins'; + +$color-picker-height: 175px; +$color-picker-hue-width: 28px; +$color-picker-sv-width: $color-picker-height; +$color-picker-indicator-size: 5px; +$color-picker-box-offset: 5px; + +.color-picker { + position: relative; + display: inline-block; + width: 100%; + font-size: 1rem; +} + +.color-picker-input { + padding-left: 35px; + padding-right: 40px; + width: 100% !important; + font-family: $font-family-monospace; + + .indicator > & { + border-left-width: $color-picker-indicator-size; + } +} + +.color-picker-box { + background: black; + position: absolute; + left: $color-picker-box-offset; + top: 5px; + bottom: 5px; + width: 25px; + border: solid 1px $border-color; + border-radius: 2px; + pointer-events: none; + z-index: 10; + + .disabled > & { + opacity: 0.8; + } + + .indicator > & { + left: $color-picker-box-offset + $color-picker-indicator-size; + } +} + +.color-picker-chevron { + position: absolute; + top: 0; + right: 0; + color: $text-muted !important; + width: 40px; + height: 100%; + padding: 7px 0; + text-align: center; +} + +.color-picker-menu { + @include nipple(10px); + padding: 5px; + display: flex; +} + +.color-picker-sv { + position: relative; + width: $color-picker-sv-width; + height: $color-picker-height; + margin-right: 5px; +} + +.color-picker-hue { + background: linear-gradient(to bottom, red, yellow, lime, cyan, blue, magenta, red); + position: relative; + width: $color-picker-hue-width; + height: $color-picker-height; +} + +.color-picker-sv-overlay { + position: absolute; + left: 0; + top: 0; + width: $color-picker-sv-width; + height: $color-picker-height; + background: + linear-gradient(to bottom, rgba(0, 0, 0, 0), black), + linear-gradient(to right, white, rgba(255, 255, 255, 0)); +} + +.color-wheel-circle-sv, .color-wheel-circle-hue { + position: absolute; + left: 0; + top: 0; + + &::after { + content: ''; + position: absolute; + border: solid 1px black; + box-shadow: 0 0 0 2px white; + } +} + +.color-wheel-circle-sv{ + &::after { + left: -4px; + top: -4px; + border-radius: 4px; + width: 9px; + height: 9px; + } +} + +.color-wheel-circle-hue { + width: 100%; + + &::after { + left: 0; + top: -2px; + right: 0; + height: 5px; + } +} diff --git a/src/ts/components/shared/color-picker/color-picker.ts b/src/ts/components/shared/color-picker/color-picker.ts new file mode 100644 index 0000000..b31d005 --- /dev/null +++ b/src/ts/components/shared/color-picker/color-picker.ts @@ -0,0 +1,125 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { clamp } from '../../../common/utils'; +import { parseColorFast, colorToCSS, colorFromHSVA, colorToHSVA, colorToHexRGB } from '../../../common/color'; +import { AgDragEvent } from '../directives/agDrag'; +import { faChevronDown } from '../../../client/icons'; + +const SIZE = 175; + +@Component({ + selector: 'color-picker', + templateUrl: 'color-picker.pug', + styleUrls: ['color-picker.scss'], +}) +export class ColorPicker { + readonly chevronIcon = faChevronDown; + @Input() isOpen = false; + @Input() isDisabled = false; + @Input() disabledColor = ''; + @Input() color = ''; + @Input() indicatorColor = ''; + @Input() label?: string = undefined; + @Input() labelledBy?: string = undefined; + @Output() colorChange = new EventEmitter(); + s = 0; + v = 0; + h = 0; + private lastColor = ''; + private closeHandler = () => this.close(); + get inputColor() { + return this.isDisabled && this.disabledColor ? this.disabledColor : this.color; + } + set inputColor(value) { + if (!this.isDisabled) { + this.color = value; + } + } + get bg() { + return colorToCSS(parseColorFast(this.inputColor)); + } + get svLeft() { + this.updateHsv(); + return this.s * 100; + } + get svTop() { + this.updateHsv(); + return (1 - this.v) * 100; + } + get hueTop() { + this.updateHsv(); + return this.h * 100 / 360; + } + get hue() { + this.updateHsv(); + return colorToCSS(colorFromHSVA(this.h, 1, 1, 1)); + } + focus(e: Event) { + this.isOpen = true; + (e.target as HTMLInputElement).select(); + } + dragSV({ event, x, y }: AgDragEvent) { + event.preventDefault(); + + this.updateHsv(); + this.s = clamp(x / SIZE, 0, 1); + this.v = 1 - clamp(y / SIZE, 0, 1); + this.updateColor(); + } + dragHue({ event, y }: AgDragEvent) { + event.preventDefault(); + + this.updateHsv(); + this.h = clamp(360 * y / SIZE, 0, 360); + this.updateColor(); + } + updateHsv() { + if (this.lastColor !== this.color) { + const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h); + this.h = h; + this.s = s; + this.v = v; + this.lastColor = this.color; + } + } + updateColor() { + const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1)); + const changed = this.color !== color; + this.lastColor = this.color = color; + + if (changed) { + this.colorChange.emit(color); + } + } + inputChanged(value: string) { + this.color = value; + this.colorChange.emit(this.color); + } + stopEvent(e: Event) { + e.stopPropagation(); + e.preventDefault(); + } + open() { + if (!this.isOpen) { + this.isOpen = true; + + setTimeout(() => { + document.addEventListener('mousedown', this.closeHandler); + document.addEventListener('touchstart', this.closeHandler); + }); + } + } + close() { + this.isOpen = false; + document.removeEventListener('mousedown', this.closeHandler); + document.removeEventListener('touchstart', this.closeHandler); + } + toggleOpen() { + if (!this.isDisabled) { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + } +} diff --git a/src/ts/components/shared/custom-checkbox/custom-checkbox.pug b/src/ts/components/shared/custom-checkbox/custom-checkbox.pug new file mode 100644 index 0000000..f6cf5d0 --- /dev/null +++ b/src/ts/components/shared/custom-checkbox/custom-checkbox.pug @@ -0,0 +1,11 @@ +.custom-control.custom-checkbox.mb-2([class.disabled]="disabled") + label + input.custom-control-input( + type="checkbox" + [disabled]="disabled" + [(ngModel)]="checked" + (ngModelChange)="checkedChange.emit(checked)" + [attr.aria-describedby]="help ? helpId : undefined") + .custom-control-label + ng-content + small.form-text.text-muted.mt-0(*ngIf="help" [id]="helpId") {{help}} diff --git a/src/ts/components/shared/custom-checkbox/custom-checkbox.scss b/src/ts/components/shared/custom-checkbox/custom-checkbox.scss new file mode 100644 index 0000000..39b6639 --- /dev/null +++ b/src/ts/components/shared/custom-checkbox/custom-checkbox.scss @@ -0,0 +1,8 @@ +.disabled { + opacity: 0.6; +} + +label { + line-height: 1.4rem; + margin-bottom: 0; +} diff --git a/src/ts/components/shared/custom-checkbox/custom-checkbox.ts b/src/ts/components/shared/custom-checkbox/custom-checkbox.ts new file mode 100644 index 0000000..0417484 --- /dev/null +++ b/src/ts/components/shared/custom-checkbox/custom-checkbox.ts @@ -0,0 +1,16 @@ +import { Component, ChangeDetectionStrategy, Output, Input, EventEmitter } from '@angular/core'; +import { uniqueId } from 'lodash'; + +@Component({ + selector: 'custom-checkbox', + templateUrl: 'custom-checkbox.pug', + styleUrls: ['custom-checkbox.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CustomCheckbox { + @Input() disabled = false; + @Input() help = ''; + @Input() checked = false; + @Output() checkedChange = new EventEmitter(); + helpId = uniqueId('custom-checkbox-help-'); +} diff --git a/src/ts/components/shared/date-picker/date-picker.pug b/src/ts/components/shared/date-picker/date-picker.pug new file mode 100644 index 0000000..99176f3 --- /dev/null +++ b/src/ts/components/shared/date-picker/date-picker.pug @@ -0,0 +1,10 @@ +.d-flex + select.form-control.w-50(placeholder="Day" [(ngModel)]="day" (ngModelChange)="change()") + option(disabled [ngValue]="0") Day + option(*ngFor="let d of days" [ngValue]="d") {{d}} + select.form-control.ml-2(placeholder="Month" [(ngModel)]="month" (ngModelChange)="change()") + option(disabled [ngValue]="0") Month + option(*ngFor="let m of months; let i = index" [ngValue]="i + 1") {{m}} + select.form-control.ml-2.w-50(placeholder="Year" [(ngModel)]="year" (ngModelChange)="change()") + option(disabled [ngValue]="0") Year + option(*ngFor="let y of years" [ngValue]="y") {{y}} diff --git a/src/ts/components/shared/date-picker/date-picker.ts b/src/ts/components/shared/date-picker/date-picker.ts new file mode 100644 index 0000000..aa78d58 --- /dev/null +++ b/src/ts/components/shared/date-picker/date-picker.ts @@ -0,0 +1,55 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { times, formatISODate, parseISODate, createValidBirthDate } from '../../../common/utils'; +import { MONTH_NAMES_EN } from '../../../common/constants'; +import { getLocale } from '../../../client/clientUtils'; + +@Component({ + selector: 'date-picker', + templateUrl: 'date-picker.pug', +}) +export class DatePicker { + readonly days = times(31, i => i + 1); + readonly years: number[] = []; + readonly months = getMonthNames(); + day = 0; + month = 0; + year = 0; + @Output() dateChange = new EventEmitter(); + constructor() { + const minYear = 1914; + const maxYear = (new Date()).getFullYear() - 6; + + for (let year = maxYear; year >= minYear; year--) { + this.years.push(year); + } + } + @Input() get date() { + const date = createValidBirthDate(this.day, this.month, this.year); + return date && formatISODate(date); + } + set date(value) { + if (value) { + const { day, month, year } = parseISODate(value); + this.day = day; + this.month = month; + this.year = year; + } + } + change() { + this.dateChange.emit(this.date); + } +} + +function getMonthNames() { + try { + const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' }); + + return times(12, i => { + const date = new Date(523456789); + date.setMonth(i); + return format.format(date); + }); + } catch { + return MONTH_NAMES_EN; + } +} diff --git a/src/ts/components/shared/directives/agAutoFocus.ts b/src/ts/components/shared/directives/agAutoFocus.ts new file mode 100644 index 0000000..e27dfab --- /dev/null +++ b/src/ts/components/shared/directives/agAutoFocus.ts @@ -0,0 +1,12 @@ +import { Directive, AfterViewInit, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[agAutoFocus]' +}) +export class AgAutoFocus implements AfterViewInit { + constructor(private element: ElementRef) { + } + ngAfterViewInit() { + setTimeout(() => this.element.nativeElement.focus(), 100); + } +} diff --git a/src/ts/components/shared/directives/agDrag.ts b/src/ts/components/shared/directives/agDrag.ts new file mode 100644 index 0000000..f567143 --- /dev/null +++ b/src/ts/components/shared/directives/agDrag.ts @@ -0,0 +1,140 @@ +import { Directive, OnInit, Input, Output, EventEmitter, ElementRef, OnDestroy } from '@angular/core'; +import { noop } from 'lodash'; +import { getButton, getX, getY, AnyEvent } from '../../../common/utils'; + +export interface AgDragEvent { + event: AnyEvent; + type: 'start' | 'drag' | 'end'; + x: number; + y: number; + dx: number; + dy: number; +} + +export interface AgDragOptions { + relative?: 'self' | 'parent'; + prevent?: boolean; +} + +export function handleDrag(element: HTMLElement, emit: (event: AgDragEvent) => void, options: AgDragOptions = {}) { + // typeof PointerEvent !== 'undefined' + const eventSets = window.navigator.pointerEnabled ? [ + { down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' }, + ] : [ + { down: 'mousedown', move: 'mousemove', up: 'mouseup' }, + { down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' }, + ]; + const emptyRect = { left: 0, top: 0 }; + let rect = emptyRect; + let scrollLeft = 0; + let scrollTop = 0; + let startX = 0; + let startY = 0; + let button = 0; + let dragging = false; + let lastEvent: any; + + function setupScrollAndRect() { + // TODO: fix issue with scroll + switch (options.relative) { + case 'self': + rect = element.getBoundingClientRect(); + scrollLeft = -(window.scrollX || window.pageXOffset || 0); + scrollTop = -(window.scrollY || window.pageYOffset || 0); + break; + case 'parent': + rect = element.parentElement!.getBoundingClientRect(); + scrollLeft = element.parentElement!.scrollLeft; + scrollTop = element.parentElement!.scrollTop; + break; + default: + rect = emptyRect; + scrollLeft = 0; + scrollTop = 0; + } + } + + function send(event: AnyEvent, type: 'start' | 'drag' | 'end') { + const x = getX(event); + const y = getY(event); + + emit({ + event, + type, + x: x - rect.left + scrollLeft, + y: y - rect.top + scrollTop, + dx: x - startX, + dy: y - startY, + }); + } + + const handlers = eventSets.map(events => { + function move(e: any) { + lastEvent = e; + e.preventDefault(); + send(e, 'drag'); + } + + function up(e: any) { + if (getButton(e) === button) { + // touchend event does not have x, y coordinates, use last touchmove event instead + if (e.type !== 'touchend' && e.type !== 'touchcancel') { + lastEvent = e; + } + end(); + } + } + + function end() { + send(lastEvent, 'end'); + window.removeEventListener(events.move, move); + window.removeEventListener(events.up, up); + events.up2 && window.removeEventListener(events.up2, up); + window.removeEventListener('blur', end); + dragging = false; + } + + function handler(e: any) { + if (!dragging) { + setupScrollAndRect(); + dragging = true; + button = getButton(e); + startX = getX(e); + startY = getY(e); + send(e, 'start'); + lastEvent = e; + + window.addEventListener(events.move, move); + window.addEventListener(events.up, up); + events.up2 && window.addEventListener(events.up2, up); + window.addEventListener('blur', end); + e.stopPropagation(); + + if (options.prevent) { + e.preventDefault(); + } + } + } + + element.addEventListener(events.down, handler); + return () => element.removeEventListener(events.down, handler); + }); + + return () => handlers.forEach(f => f()); +} + +@Directive({ selector: '[agDrag]' }) +export class AgDrag implements OnInit, OnDestroy { + @Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined; + @Input('agDragPrevent') prevent = false; + @Output('agDrag') drag = new EventEmitter(); + private unsubscribe = noop; + constructor(private element: ElementRef) { + } + ngOnInit() { + this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this); + } + ngOnDestroy() { + this.unsubscribe(); + } +} diff --git a/src/ts/components/shared/directives/anchor.ts b/src/ts/components/shared/directives/anchor.ts new file mode 100644 index 0000000..0cfbb1b --- /dev/null +++ b/src/ts/components/shared/directives/anchor.ts @@ -0,0 +1,17 @@ +import { Directive, OnInit, ElementRef } from '@angular/core'; + +@Directive({ + selector: 'a[href]' +}) +export class Anchor implements OnInit { + constructor(private element: ElementRef) { + } + ngOnInit() { + const a = this.element.nativeElement as HTMLAnchorElement; + + if (/^(https?|mailto):/.test(a.href) && !a.target) { + a.setAttribute('target', '_blank'); + a.setAttribute('rel', 'noopener noreferrer'); + } + } +} diff --git a/src/ts/components/shared/directives/btnHighlight.ts b/src/ts/components/shared/directives/btnHighlight.ts new file mode 100644 index 0000000..31ebfcf --- /dev/null +++ b/src/ts/components/shared/directives/btnHighlight.ts @@ -0,0 +1,30 @@ +import { Directive, Input, Optional } from '@angular/core'; +import { NgModel } from '@angular/forms'; + +@Directive({ + selector: '[btnHighlight]', + host: { + '[class.btn-default]': '!on', + '[class.btn-primary]': 'on', + }, +}) +export class BtnHighlight { + @Input() btnHighlight?: boolean = undefined; + constructor(@Optional() private model?: NgModel) { + } + get on() { + const value = this.btnHighlight; + return (value === true || value === false || !this.model) ? value : !!this.model.value; + } +} + +@Directive({ + selector: '[btnHighlightDanger]', + host: { + '[class.btn-default]': '!btnHighlightDanger', + '[class.btn-danger]': 'btnHighlightDanger', + }, +}) +export class BtnHighlightDanger { + @Input() btnHighlightDanger = false; +} diff --git a/src/ts/components/shared/directives/draggable.ts b/src/ts/components/shared/directives/draggable.ts new file mode 100644 index 0000000..88525d4 --- /dev/null +++ b/src/ts/components/shared/directives/draggable.ts @@ -0,0 +1,207 @@ +import { Component, Directive, Input, Output, EventEmitter, ElementRef, OnInit, Injectable, OnDestroy } from '@angular/core'; +import { noop } from 'lodash'; +import { AgDragEvent, handleDrag } from './agDrag'; +import { clamp, setTransform, removeItem, pointInRect } from '../../../common/utils'; +import { rect } from '../../../common/rect'; + +@Injectable({ providedIn: 'root' }) +export class DraggableService { + root?: ElementRef; + draggedItem?: any; + activeDropZone?: DraggableDrop; + dropZones: DraggableDrop[] = []; + get rootElement(): HTMLElement { + return this.root ? this.root.nativeElement : document.body; + } + setActiveDropZone(dropZone: DraggableDrop | undefined) { + if (this.activeDropZone !== dropZone) { + if (this.activeDropZone) { + this.activeDropZone.setActive(false); + } + + this.activeDropZone = dropZone; + + if (this.activeDropZone) { + this.activeDropZone.setActive(true); + } + } + } + startMove(element: HTMLElement, item: any) { + this.setActiveDropZone(undefined); + this.rootElement.appendChild(element); + this.draggedItem = item; + this.initRects(); + } + endMove() { + if (this.activeDropZone) { + this.activeDropZone.drop.emit(this.draggedItem); + this.setActiveDropZone(undefined); + } + + this.draggedItem = undefined; + } + addDropZone(dropZone: DraggableDrop) { + this.dropZones.push(dropZone); + + if (this.draggedItem) { + this.initRects(); + } + } + removeDropZone(dropZone: DraggableDrop) { + removeItem(this.dropZones, dropZone); + + if (this.draggedItem) { + this.initRects(); + } + + if (this.activeDropZone === dropZone) { + this.setActiveDropZone(undefined); + } + } + updateHover(x: number, y: number) { + if (this.draggedItem) { + for (const zone of this.dropZones) { + if (pointInRect(x, y, zone.rect)) { + this.setActiveDropZone(zone); + return; + } + } + + this.setActiveDropZone(undefined); + } + } + private initRects() { + this.dropZones.forEach(i => i.initRect()); + } +} + +@Component({ + selector: 'draggable-outlet', + template: `
`, + styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`], +}) +export class DraggableOutlet { + constructor(element: ElementRef, service: DraggableService) { + service.root = element; + } +} + +@Directive({ selector: '[draggableDrop]' }) +export class DraggableDrop implements OnInit, OnDestroy { + @Input('draggablePad') pad = 0; + @Output('draggableDrop') drop = new EventEmitter(); + rect = rect(0, 0, 0, 0); + constructor(private element: ElementRef, private service: DraggableService) { + } + ngOnInit() { + this.service.addDropZone(this); + } + ngOnDestroy() { + this.service.removeDropZone(this); + } + setActive(active: boolean) { + const element = this.element.nativeElement as HTMLElement; + + if (active) { + element.classList.add('draggable-hover'); + } else { + element.classList.remove('draggable-hover'); + } + } + initRect() { + const element = this.element.nativeElement as HTMLElement; + const clientBounds = element.getBoundingClientRect(); + this.rect.x = clientBounds.left - this.pad; + this.rect.y = clientBounds.top - this.pad; + this.rect.w = clientBounds.width + 2 * this.pad; + this.rect.h = clientBounds.height + 2 * this.pad; + } +} + +@Directive({ + selector: '[draggableItem]', + host: { + '[style.touch-action]': `touchAction`, + } +}) +export class DraggableItem implements OnInit, OnDestroy { + @Input('draggableItem') item: T | undefined; + @Output('draggableDrag') dragStarted = new EventEmitter(); + private startX = 0; + private startY = 0; + private draggable?: HTMLElement; + private width = 0; + private height = 0; + private unsubscribeDrag = noop; + private _disabled = false; + constructor(private element: ElementRef, private service: DraggableService) { + } + ngOnInit() { + this.setupDragEvents(); + } + ngOnDestroy() { + this.unsubscribeDrag(); + } + get touchAction() { + return this.disabled ? 'inherit' : 'none'; + } + @Input('draggableDisabled') get disabled() { + return this._disabled; + } + set disabled(value) { + if (this._disabled !== value) { + this._disabled = value; + this.setupDragEvents(); + } + } + private setupDragEvents() { + this.unsubscribeDrag(); + this.unsubscribeDrag = noop; + + if (!this.disabled) { + this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true }); + } + } + drag(e: AgDragEvent) { + if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) { + const element = this.element.nativeElement as HTMLElement; + const rect = element.getBoundingClientRect(); + this.startX = rect.left; + this.startY = rect.top; + this.draggable = element.cloneNode(true) as HTMLElement; + this.draggable.style.position = 'absolute'; + this.draggable.style.width = `${rect.width}px`; + this.draggable.style.height = `${rect.height}px`; + this.draggable.style.margin = '0'; + this.draggable.classList.add('draggable-dragging'); + this.width = rect.width; + this.height = rect.height; + + const src = element.querySelectorAll('canvas') as NodeListOf; + const dst = this.draggable.querySelectorAll('canvas') as NodeListOf; + + for (let i = 0; i < src.length; i++) { + const context = dst.item(i).getContext('2d'); + context && context.drawImage(src.item(i), 0, 0); + } + + this.service.startMove(this.draggable, this.item!); + this.dragStarted.emit(); + } + + if (this.draggable) { + if (e.type === 'end') { + this.draggable!.parentNode!.removeChild(this.draggable!); + this.draggable = undefined; + this.service.endMove(); + } else { + const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width); + const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height); + setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`); + this.service.updateHover(e.x, e.y); + } + } + } +} + +export const draggableComponents = [DraggableOutlet, DraggableItem, DraggableDrop]; diff --git a/src/ts/components/shared/directives/dropdown.ts b/src/ts/components/shared/directives/dropdown.ts new file mode 100644 index 0000000..74fb33d --- /dev/null +++ b/src/ts/components/shared/directives/dropdown.ts @@ -0,0 +1,227 @@ +import { + Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild, + Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable +} from '@angular/core'; +import { uniqueId } from 'lodash'; +import { focusFirstElement } from '../../../client/htmlUtils'; + +@Injectable({ providedIn: 'root' }) +export class DropdownOutletService { + viewContainer?: ViewContainerRef; + rootElement?: HTMLElement; +} + +@Component({ + selector: 'dropdown-outlet', + template: ``, +}) +export class DropdownOutlet { + constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) { + service.viewContainer = viewContainer; + service.rootElement = element.nativeElement.parentElement; + } +} + +@Directive({ + selector: '[dropdownMenu]', +}) +export class DropdownMenu { + ref?: EmbeddedViewRef; + id = uniqueId('dropdown-menu-'); + private onClose?: () => void; + constructor( + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef, + private renderer: Renderer2, + private service: DropdownOutletService, + ) { + } + private get root(): HTMLElement { + return this.ref && this.ref.rootNodes[0]; + } + open(useOutlet: boolean, rootElement: HTMLElement) { + if (!this.ref) { + if (useOutlet) { + this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef); + } else { + this.ref = this.viewContainer.createEmbeddedView(this.templateRef); + } + + const { renderer, root } = this; + + renderer.addClass(root, 'show'); + renderer.setAttribute(root, 'id', this.id); + + if (useOutlet) { + const positionMenu = () => { + const rect = rootElement.getBoundingClientRect(); + const menuRect = root.getBoundingClientRect(); + let transform: string; + + if ((rect.bottom + menuRect.height) > window.innerHeight) { + transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`; + renderer.addClass(root, 'dropdown-menu-up'); + } else { + transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`; + renderer.removeClass(root, 'dropdown-menu-up'); + } + + renderer.setStyle(root, 'transform', transform); + }; + + renderer.addClass(root, 'dropdown-in-outlet'); + positionMenu(); + + const closeDropdown = () => { + this.close(); + }; + + document.addEventListener('scroll', closeDropdown, true); + window.addEventListener('resize', closeDropdown, true); + + this.onClose = () => { + document.removeEventListener('scroll', closeDropdown, true); + window.removeEventListener('resize', closeDropdown, true); + }; + } + } + } + close() { + if (this.ref) { + this.ref.destroy(); + this.ref = undefined; + } + + if (this.onClose) { + this.onClose(); + this.onClose = undefined; + } + } + checkTarget(e: Event) { + return this.root && this.root.contains(e.target as any); + } + focusFirstElement() { + if (this.root) { + focusFirstElement(this.root); + } + } +} + +@Directive({ + selector: '[dropdown]', + exportAs: 'ag-dropdown', + host: { + '[class.show]': 'isOpen', + }, +}) +export class Dropdown { + dropdownToggle?: DropdownToggle; + @ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu; + @Input() autoClose: boolean | 'outsideClick' = true; + @Input() preventAutoCloseOnOutlet = false; + @Input() hookToCanvas = false; + @Input() focusOnOpen = true; + @Input() focusOnClose = true; + @Input() useOutlet = false; + @Input() isOpen = false; + @Output() isOpenChange = new EventEmitter(); + get menuId() { + return this.isOpen ? this.menu.id : ''; + } + constructor(private element: ElementRef, private service: DropdownOutletService) { + } + open() { + if (!this.isOpen) { + this.isOpen = true; + this.isOpenChange.emit(true); + this.menu.open(this.useOutlet, this.element.nativeElement); + + setTimeout(() => { + document.addEventListener('click', this.closeHandler); + document.addEventListener('keydown', this.closeHandler); + + if (this.focusOnOpen) { + this.menu.focusFirstElement(); + } + + if (this.hookToCanvas) { + const canvas = document.getElementById('canvas'); + + if (canvas) { + canvas.addEventListener('touchstart', this.canvasCloseHandler); + canvas.addEventListener('mousedown', this.canvasCloseHandler); + } + } + }); + } + } + close() { + if (this.isOpen) { + this.isOpen = false; + this.isOpenChange.emit(false); + this.menu.close(); + + if (this.focusOnClose && this.dropdownToggle) { + this.dropdownToggle.focus(); + } + + document.removeEventListener('click', this.closeHandler); + document.removeEventListener('keydown', this.closeHandler); + + if (this.hookToCanvas) { + const canvas = document.getElementById('canvas'); + + if (canvas) { + canvas.removeEventListener('touchstart', this.canvasCloseHandler); + canvas.removeEventListener('mousedown', this.canvasCloseHandler); + } + } + } + } + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + private closeHandler: any = (e: KeyboardEvent) => { + if ( + !e.keyCode + && (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e))) + && !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any)) + && !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e)) + ) { + this.close(); + } else if (this.autoClose && e.keyCode === 27) { // esc + this.close(); + } + } + private canvasCloseHandler: any = () => this.close(); +} + +@Directive({ + selector: '[dropdownToggle]', + host: { + 'aria-haspopup': 'true', + '[attr.aria-expanded]': 'dropdown.isOpen', + '[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined', + }, +}) +export class DropdownToggle { + constructor(private element: ElementRef, public dropdown: Dropdown) { + dropdown.dropdownToggle = this; + } + @HostListener('click') + click() { + this.dropdown.toggle(); + } + checkTarget(e: Event) { + return this.element.nativeElement.contains(e.target); + } + focus() { + this.element.nativeElement.focus(); + } +} + +export const dropdownDirectives = [Dropdown, DropdownToggle, DropdownMenu, DropdownOutlet]; diff --git a/src/ts/components/shared/directives/fixToTop.ts b/src/ts/components/shared/directives/fixToTop.ts new file mode 100644 index 0000000..9798c19 --- /dev/null +++ b/src/ts/components/shared/directives/fixToTop.ts @@ -0,0 +1,22 @@ +import { Directive, ElementRef, Input, HostListener, HostBinding, Output, EventEmitter } from '@angular/core'; + +@Directive({ + selector: '[fixToTop]', +}) +export class FixToTop { + @Input() fixToTopOffset = 0; + @Output() fixToTop = new EventEmitter(); + @HostBinding('class.fixed-to-top') fixed = false; + constructor(private element: ElementRef) { + } + @HostListener('window:scroll') + scroll() { + const element = this.element.nativeElement as HTMLElement; + const { top } = element.getBoundingClientRect(); + + if (this.fixed !== top < this.fixToTopOffset) { + this.fixed = top < this.fixToTopOffset; + this.fixToTop.emit(this.fixed); + } + } +} diff --git a/src/ts/components/shared/directives/focusTitle.ts b/src/ts/components/shared/directives/focusTitle.ts new file mode 100644 index 0000000..fd49244 --- /dev/null +++ b/src/ts/components/shared/directives/focusTitle.ts @@ -0,0 +1,15 @@ +import { Directive, AfterViewInit, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[focusTitle]', + host: { + 'tabindex': '-1', + }, +}) +export class FocusTitle implements AfterViewInit { + constructor(private element: ElementRef) { + } + ngAfterViewInit() { + setTimeout(() => this.element.nativeElement.focus()); + } +} diff --git a/src/ts/components/shared/directives/focusTrap.ts b/src/ts/components/shared/directives/focusTrap.ts new file mode 100644 index 0000000..7f2be05 --- /dev/null +++ b/src/ts/components/shared/directives/focusTrap.ts @@ -0,0 +1,57 @@ +import { Directive, Input, OnDestroy, ElementRef, OnInit } from '@angular/core'; +import { isParentOf, focusFirstElement, findFocusableElements } from '../../../client/htmlUtils'; +import { isMobile } from '../../../client/data'; + +@Directive({ + selector: '[focusTrap]', +}) +export class FocusTrap implements OnInit, OnDestroy { + private on = true; + private lastActiveElement?: HTMLElement; + @Input() set focusTrap(value: boolean) { + if (this.on !== value) { + this.on = value; + this.update(); + } + } + constructor(private element: ElementRef) { + } + ngOnInit() { + this.update(); + } + ngOnDestroy() { + this.focusTrap = false; + } + private update() { + if (!isMobile) { + if (this.on) { + this.lastActiveElement = document.activeElement as HTMLElement; + document.addEventListener('focusin', this.focus); + + if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) { + setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement)); + } + } else { + this.lastActiveElement = undefined; + document.removeEventListener('focusin', this.focus); + } + } + } + private focus = (e: Event) => { + if (isParentOf(this.element.nativeElement, e.target as any)) { + this.lastActiveElement = e.target as any; + } else { + const focusable = findFocusableElements(this.element.nativeElement); + + if (focusable.length) { + if (this.lastActiveElement === focusable[0]) { + this.lastActiveElement = focusable[focusable.length - 1]; + } else { + this.lastActiveElement = focusable[0]; + } + + this.lastActiveElement.focus(); + } + } + } +} diff --git a/src/ts/components/shared/directives/hasFeature.ts b/src/ts/components/shared/directives/hasFeature.ts new file mode 100644 index 0000000..cce9388 --- /dev/null +++ b/src/ts/components/shared/directives/hasFeature.ts @@ -0,0 +1,60 @@ +import { Directive, Input, TemplateRef, ViewContainerRef, OnDestroy, EmbeddedViewRef, AfterViewInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { hasFeatureFlag, featureFlagsChanged } from '../../../client/clientUtils'; +import { Model } from '../../services/model'; + +@Directive({ + selector: '[hasFeature]', +}) +export class HasFeature implements AfterViewInit, OnDestroy { + private subscriptions: Subscription[] = []; + private showing = false; + private _flag: string | undefined = undefined; + private _orMod = false; + private _alsoIf = true; + private ref?: EmbeddedViewRef; + constructor(private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private model: Model) { + } + ngAfterViewInit() { + this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update())); + this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update())); + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + @Input() + set hasFeature(value: string | undefined) { + if (this._flag !== value) { + this._flag = value; + this.update(); + } + } + @Input() + set hasFeatureOrMod(value: boolean) { + if (this._orMod !== value) { + this._orMod = value; + this.update(); + } + } + @Input() + set hasFeatureAlso(value: boolean) { + if (this._alsoIf !== value) { + this._alsoIf = value; + this.update(); + } + } + private update() { + const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod)); + + if (this.showing !== show) { + this.showing = show; + + if (show) { + this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + this.ref = undefined; + } + } + } +} diff --git a/src/ts/components/shared/directives/labelledBy.ts b/src/ts/components/shared/directives/labelledBy.ts new file mode 100644 index 0000000..e9a4a62 --- /dev/null +++ b/src/ts/components/shared/directives/labelledBy.ts @@ -0,0 +1,21 @@ +import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { uniqueId } from 'lodash'; +import { findParentElement } from '../../../client/htmlUtils'; + +@Directive({ + selector: '[labelledBy]', +}) +export class LabelledBy implements OnInit { + @Input('labelledBy') selector!: string; + constructor(private element: ElementRef) { + } + ngOnInit() { + const element = this.element.nativeElement as HTMLElement; + const target = findParentElement(element, this.selector); + const id = element.id = element.id || uniqueId('labelled-by-'); + + if (target) { + target.setAttribute('aria-labelledby', id); + } + } +} diff --git a/src/ts/components/shared/directives/linkCurrent.ts b/src/ts/components/shared/directives/linkCurrent.ts new file mode 100644 index 0000000..09bb882 --- /dev/null +++ b/src/ts/components/shared/directives/linkCurrent.ts @@ -0,0 +1,14 @@ +import { Directive, HostBinding } from '@angular/core'; +import { RouterLinkActive } from '@angular/router'; + +@Directive({ + selector: '[linkCurrent]', +}) +export class LinkCurrent { + constructor(private routerLinkActive: RouterLinkActive) { + } + @HostBinding('attr.aria-current') + get current() { + return this.routerLinkActive.isActive ? 'true' : undefined; + } +} diff --git a/src/ts/components/shared/directives/revSrc.ts b/src/ts/components/shared/directives/revSrc.ts new file mode 100644 index 0000000..fb2e487 --- /dev/null +++ b/src/ts/components/shared/directives/revSrc.ts @@ -0,0 +1,12 @@ +import { Directive, Input, HostBinding } from '@angular/core'; +import { getUrl } from '../../../client/rev'; + +@Directive({ + selector: '[revSrc]', +}) +export class RevSrc { + @HostBinding() get src() { + return this.revSrc && getUrl(this.revSrc); + } + @Input() revSrc?: string; +} diff --git a/src/ts/components/shared/directives/saveActiveTab.ts b/src/ts/components/shared/directives/saveActiveTab.ts new file mode 100644 index 0000000..11c6a72 --- /dev/null +++ b/src/ts/components/shared/directives/saveActiveTab.ts @@ -0,0 +1,26 @@ +import { Directive, Input, Host, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { Tabset } from '../tabset/tabset'; +import { StorageService } from '../../services/storageService'; + +@Directive({ + selector: '[saveActiveTab]', +}) +export class SaveActiveTab implements OnInit, OnDestroy { + @Input('saveActiveTab') key!: string; + private subscription?: Subscription; + constructor(@Host() private tabset: Tabset, private storage: StorageService) { + } + ngOnInit() { + // this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10); + this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10)); + this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => { + this.storage.setItem(this.key, i.toString()); + }); + } + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } +} diff --git a/src/ts/components/shared/emote-box/emote-box.ts b/src/ts/components/shared/emote-box/emote-box.ts new file mode 100644 index 0000000..cf9cbe9 --- /dev/null +++ b/src/ts/components/shared/emote-box/emote-box.ts @@ -0,0 +1,80 @@ +import { Component, Input, AfterViewInit, ElementRef, ChangeDetectionStrategy, ViewChild, NgZone } from '@angular/core'; +import { findEmoji, getEmojiImageAsync } from '../../../client/emoji'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { font } from '../../../client/fonts'; +import { getCharacterSprite } from '../../../graphics/spriteFont'; + +@Component({ + selector: 'emote-box', + template: '', + styles: ['.emote-box { pointer-events: none; }'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EmoteBox implements AfterViewInit { + @ViewChild('image', { static: true }) image!: ElementRef; + private emoteValue = ''; + private scaleValue = 2; + private initialized = false; + constructor(private zone: NgZone) { + } + ngAfterViewInit() { + loadAndInitSpriteSheets() + .then(() => { + this.initialized = true; + this.zone.runOutsideAngular(() => this.redraw()); + }); + } + get emote() { + return this.emoteValue; + } + @Input() + set emote(value: string) { + if (this.emoteValue !== value) { + this.emoteValue = value; + this.zone.runOutsideAngular(() => this.redraw()); + } + } + get scale() { + return this.scaleValue; + } + @Input() + set scale(value: number) { + if (this.scaleValue !== value) { + this.scaleValue = value; + this.zone.runOutsideAngular(() => this.redraw()); + } + } + redraw() { + if (this.initialized) { + const emote = findEmoji(this.emote); + const sprite = font && emote && getCharacterSprite(emote.symbol, font); + const image = this.image.nativeElement as HTMLImageElement; + + if (sprite) { + const width = sprite.w + sprite.ox; + const height = 10; // sprite.h + sprite.oy; + + image.style.width = `${width * this.scale}px`; + image.style.height = `${height * this.scale}px`; + image.style.marginTop = `${-this.scale}px`; + image.style.display = 'inline-block'; + image.style.visibility = 'hidden'; + + if (emote) { + image.setAttribute('aria-label', emote.names[0]); + } + + getEmojiImageAsync(sprite, src => { + image.src = src; + image.alt = emote ? emote.symbol : ''; + image.style.visibility = 'visible'; + }); + } else { + image.style.width = `0px`; + image.style.height = `0px`; + image.src = ''; + image.alt = ''; + } + } + } +} diff --git a/src/ts/components/shared/fill-outline/fill-outline.pug b/src/ts/components/shared/fill-outline/fill-outline.pug new file mode 100644 index 0000000..bacbb8e --- /dev/null +++ b/src/ts/components/shared/fill-outline/fill-outline.pug @@ -0,0 +1,29 @@ +.row.form-group(*ngIf="outlineHidden") + .col-4.col-lock-box + label.text-muted.color-label {{label}} + check-box.lock-box( + *ngIf="hasLock && !nonLockable" [(checked)]="locked" (checkedChange)="onLockedChange($event)" + [icon]="lockIcon" label="Automatic color") + .col-8 + color-picker( + [(color)]="fill" (colorChange)="onFillChange($event)" [isDisabled]="locked" [label]="label || 'Color'" + [indicatorColor]="indicatorColor") + +.row.form-group(*ngIf="!outlineHidden") + .col-4.col-sm-3.col-lock-box + label.text-muted.color-label {{label}} + check-box.lock-box( + *ngIf="hasLock && !nonLockable" [(checked)]="locked" (checkedChange)="onLockedChange($event)" + [icon]="lockIcon" label="Automatic color") + .col-8.col-sm-4 + color-picker( + [(color)]="fill" (colorChange)="onFillChange($event)" [isDisabled]="locked" [label]="label + ' fill'" + [indicatorColor]="indicatorColor") + .col-4.col-sm-1.col-outline + check-box.lock-box( + [(checked)]="outlineLocked" (checkedChange)="onOutlineLockedChange($event)" [icon]="lockIcon" + label="Automatic outline") + .col-8.col-sm-4.col-outline + color-picker( + [(color)]="outline" (colorChange)="onOutlineChange($event)" [isDisabled]="outlineLocked" + [label]="label + ' outline'" [indicatorColor]="indicatorColor") diff --git a/src/ts/components/shared/fill-outline/fill-outline.scss b/src/ts/components/shared/fill-outline/fill-outline.scss new file mode 100644 index 0000000..c9212ea --- /dev/null +++ b/src/ts/components/shared/fill-outline/fill-outline.scss @@ -0,0 +1,18 @@ +@import '../../../../styles/partials/variables'; +@import '../../../../styles/partials/mixins'; + +.col-lock-box { + display: flex; + align-items: center; + justify-content: space-between; + + > label { + margin: 0; + } +} + +.col-outline { + @include media-breakpoint-down(xs) { + padding-top: 0.5rem; + } +} diff --git a/src/ts/components/shared/fill-outline/fill-outline.ts b/src/ts/components/shared/fill-outline/fill-outline.ts new file mode 100644 index 0000000..aad4190 --- /dev/null +++ b/src/ts/components/shared/fill-outline/fill-outline.ts @@ -0,0 +1,47 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { faLock } from '../../../client/icons'; + +@Component({ + selector: 'fill-outline', + templateUrl: 'fill-outline.pug', + styleUrls: ['fill-outline.scss'], +}) +export class FillOutline { + readonly lockIcon = faLock; + @Input() label = 'Color'; + @Input() indicatorColor = ''; + @Input() base?: string; + @Input() fill?: string; + @Output() fillChange = new EventEmitter(); + @Input() outline?: string; + @Output() outlineChange = new EventEmitter(); + @Input() locked?: boolean; + @Output() lockedChange = new EventEmitter(); + @Input() nonLockable = false; + @Input() outlineLocked = false; + @Output() outlineLockedChange = new EventEmitter(); + @Input() outlineHidden = false; + @Output() change = new EventEmitter(); + get hasLock() { + return this.locked !== undefined; + } + onChange() { + this.change.emit(); + } + onFillChange(value: string) { + this.fillChange.emit(value); + this.onChange(); + } + onOutlineChange(value: string) { + this.outlineChange.emit(value); + this.onChange(); + } + onLockedChange(value: boolean) { + this.lockedChange.emit(value); + this.onChange(); + } + onOutlineLockedChange(value: boolean) { + this.outlineLockedChange.emit(value); + this.onChange(); + } +} diff --git a/src/ts/components/shared/friends-box/friends-box.pug b/src/ts/components/shared/friends-box/friends-box.pug new file mode 100644 index 0000000..3a60ef5 --- /dev/null +++ b/src/ts/components/shared/friends-box/friends-box.pug @@ -0,0 +1,55 @@ +.friends-box.dropdown( + dropdown autoClose="outsideClick" (isOpenChange)="toggle()" [hookToCanvas]="true" [focusOnOpen]="false" + [preventAutoCloseOnOutlet]="true") + button.game-button.dropdown-toggle.no-arrow(dropdownToggle (click)="false" title="Friends") + fa-icon([icon]="friendsIcon" [fixedWidth]="true") + .friends-dropdown-menu.dropdown-menu.dropdown-menu-right(*dropdownMenu) + .friends-list-header.list-unstyled + .dropdown-header.d-flex + .flex-grow-1 Friends + .dropdown(dropdown) + button.dropdown-toggle.no-arrow(dropdownToggle) + span.text-success(*ngIf="!hidden") online + span.text-muted(*ngIf="hidden") offline + fa-icon.ml-2([icon]="cogIcon") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + button.dropdown-item((click)="setStatus('online')") + fa-icon.mr-2.text-success([icon]="statusIcon" size="xs" [fixedWidth]="true") + | Online + button.dropdown-item((click)="setStatus('invisible')") + fa-icon.mr-2.text-muted([icon]="statusIcon" size="xs" [fixedWidth]="true") + | Show as Offline + .friends-list(*ngIf="friends && friends.length; else noFriends") + //- [itemSize]="42") + .friends-item( + dropdown + *ngFor="let f of friends" + [ngClass]="f.online ? 'online' : 'offline'" + [useOutlet]="true" + [hookToCanvas]="true" + [focusOnOpen]="false" + [focusOnClose]="false") + button.d-flex(dropdownToggle) + portrait-box.mr-2([pony]="f.ponyInfo" size="small" [noBorder]="true") + .flex-grow-1.friends-item-details + .friends-item-name {{f.actualName}}  + .friends-item-more + .text-success.float-right.ml-2(*ngIf="f.online") online + .text-muted.float-right.ml-2(*ngIf="!f.online") offline + .text-muted.friends-item-account {{f.accountName}} + .friends-item-delete.d-flex.px-2.py-1(*ngIf="removing === f") + button.btn.btn-sm.btn-outline-success.flex-grow-1((click)="cancelRemove(); $event.stopPropagation()") + | Cancel + button.btn.btn-sm.btn-outline-danger.flex-grow-1.ml-2((click)="confirmRemove(); $event.stopPropagation()") + | Confirm remove + //- span.badge.badge-none.friends-item-server PG + .options-dropdown-menu.dropdown-menu(*dropdownMenu) + button.dropdown-item((click)="sendMessageTo(f)" [disabled]="!f.online") Send whisper + button.dropdown-item((click)="inviteToParty(f)" [disabled]="!f.online") Invite to party + .dropdown-divider + button.dropdown-item((click)="remove(f)") Remove + ng-template(#noFriends) + .text-center.text-muted.p-2(*ngIf="friends") + | select a player and use #[fa-icon([icon]="userOptionsIcon")] menu to add them to your friends + .text-center.text-muted.p-2(*ngIf="!friends") + | Loading... diff --git a/src/ts/components/shared/friends-box/friends-box.scss b/src/ts/components/shared/friends-box/friends-box.scss new file mode 100644 index 0000000..f357b28 --- /dev/null +++ b/src/ts/components/shared/friends-box/friends-box.scss @@ -0,0 +1,107 @@ +@import '../../../../styles/partials/variables'; +@import '../../../../styles/partials/mixins'; + +.friends-box { + padding-bottom: 5px; +} + +.friends-dropdown-menu { + @include nipple(8px); + min-width: 205px; +} + +.options-dropdown-menu { + @include nippleLeft(11px); + min-width: 200px; + margin-left: 4px; + + &.dropdown-menu-up { + @include nippleLeftFlip(); + } +} + +.friends-list-header { + margin-bottom: 0; +} + +.friends-list { + width: 250px; + max-height: 500px; + overflow-y: auto; + margin-bottom: 0; + + @media screen and (max-height: 600px) { + max-height: 70vh; + max-height: calc(100vh - 100px); + } +} + +.friends-item { + height: 42px; + color: $dropdown-link-color; + padding: 0 8px; + cursor: pointer; + position: relative; + text-align: left; + + > button { + width: 100%; + overflow: hidden; + text-align: left; + } + + portrait-box { + margin-top: 5px; + } + + &.offline { + > button { + opacity: 0.6; + } + } + + &:hover { + background: $dropdown-link-hover-bg; + color: $dropdown-link-hover-color; + } +} + +.friends-item-delete { + position: absolute; + background: white; + left: 0; + top: 0; + right: 0; + bottom: 0; +} + +.friends-item-details { + overflow: hidden; + padding-bottom: 5px; +} + +.friends-item-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.friends-item-more { + margin-bottom: -2px; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.friends-item-account { + overflow: hidden; + text-overflow: ellipsis; +} + +.friends-item-server { + position: absolute; + left: 24px; + bottom: 0; + font-size: 8px; +} diff --git a/src/ts/components/shared/friends-box/friends-box.ts b/src/ts/components/shared/friends-box/friends-box.ts new file mode 100644 index 0000000..bbfa640 --- /dev/null +++ b/src/ts/components/shared/friends-box/friends-box.ts @@ -0,0 +1,61 @@ +import { Component, Output, EventEmitter } from '@angular/core'; +import { faCog, faUserFriends, faUserPlus, faUserCog, faCircle } from '../../../client/icons'; +import { Model, Friend } from '../../services/model'; +import { PonyTownGame } from '../../../client/game'; +import { PlayerAction, Action } from '../../../common/interfaces'; +import { removeItem } from '../../../common/utils'; +import { SettingsService } from '../../services/settingsService'; + +@Component({ + selector: 'friends-box', + templateUrl: 'friends-box.pug', + styleUrls: ['friends-box.scss'], +}) +export class FriendsBox { + readonly friendsIcon = faUserFriends; + readonly cogIcon = faCog; + readonly addToPartyIcon = faUserPlus; + readonly userOptionsIcon = faUserCog; + readonly statusIcon = faCircle; + @Output() sendMessage = new EventEmitter(); + removing?: Friend; + constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) { + } + get friends() { + return this.model.friends; + } + get hidden() { + return !!this.settings.account.hidden; + } + toggleHidden() { + this.settings.account.hidden = !this.settings.account.hidden; + this.settings.saveAccountSettings(this.settings.account); + } + toggle() { + this.removing = undefined; + } + sendMessageTo(friend: Friend) { + this.sendMessage.emit(friend); + } + inviteToParty(friend: Friend) { + this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined)); + } + remove(friend: Friend) { + this.removing = friend; + } + cancelRemove() { + this.removing = undefined; + } + confirmRemove() { + if (this.removing && this.model.friends) { + const { accountId } = this.removing; + this.game.send(server => server.actionParam(Action.RemoveFriend, accountId)); + removeItem(this.model.friends, this.removing); + this.removing = undefined; + } + } + setStatus(status: string) { + this.settings.account.hidden = status === 'invisible'; + this.settings.saveAccountSettings(this.settings.account); + } +} diff --git a/src/ts/components/shared/install-button/install-button.pug b/src/ts/components/shared/install-button/install-button.pug new file mode 100644 index 0000000..bc82d06 --- /dev/null +++ b/src/ts/components/shared/install-button/install-button.pug @@ -0,0 +1,6 @@ +.btn-group.d-flex(*ngIf="canInstall") + button.btn.btn-lg.btn-outline-success.text-wrap.flex-grow-1((click)="install()") + | Add #[b Pony Town] to {{isMobile ? 'home screen' : 'desktop'}} + button.btn.btn-lg.btn-outline-success.flex-grow-0( + (click)="dismiss()" title="Dismiss" [attr.aria-label]="'Dismiss add to ' + (isMobile ? 'home screen' : 'desktop')") + fa-icon([icon]="closeIcon") diff --git a/src/ts/components/shared/install-button/install-button.scss b/src/ts/components/shared/install-button/install-button.scss new file mode 100644 index 0000000..755b282 --- /dev/null +++ b/src/ts/components/shared/install-button/install-button.scss @@ -0,0 +1,5 @@ +@import '../../../../styles/partials/variables'; + +:host { + display: block; +} diff --git a/src/ts/components/shared/install-button/install-button.ts b/src/ts/components/shared/install-button/install-button.ts new file mode 100644 index 0000000..03f1340 --- /dev/null +++ b/src/ts/components/shared/install-button/install-button.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { faTimes } from '../../../client/icons'; +import { InstallService } from '../../services/installService'; +import { isMobile } from '../../../client/data'; + +@Component({ + selector: 'install-button', + templateUrl: 'install-button.pug', + styleUrls: ['install-button.scss'], +}) +export class InstallButton { + readonly closeIcon = faTimes; + constructor(private installService: InstallService) { + } + get canInstall() { + return this.installService.canInstall; + } + get isMobile() { + return isMobile; + } + install() { + this.installService.install(); + } + dismiss() { + this.installService.dismiss(); + } +} diff --git a/src/ts/components/shared/invites-modal/invites-modal.pug b/src/ts/components/shared/invites-modal/invites-modal.pug new file mode 100644 index 0000000..b3031fe --- /dev/null +++ b/src/ts/components/shared/invites-modal/invites-modal.pug @@ -0,0 +1,22 @@ +.modal-header.d-none.d-sm-block + h4.modal-title(labelledBy=".modal") + | Invites + +.modal-body + p + | Using #[b {{invites.length}}] out of #[b {{inviteLimit}}] invites. + .list-group + .list-group-item(*ngFor="let i of invites" [class.text-muted]="!i.active") + .d-flex.justify-content-between + .d-flex.align-items-center + portrait-box([pony]="i.pony" size="small" [noBorder]="true" style="margin: -5px 10px -5px -10px;") + b {{i.name}} + span.text-muted.ml-1(*ngIf="!i.active") (inactive) + button.btn.btn-xs.btn-outline-danger Cancel + + .alert.alert-danger.mt-3.mb-0(*ngIf="error") + | {{error}} + +.modal-footer.justify-content-end + button.btn.btn-outline-secondary((click)="close.emit()") + | Close diff --git a/src/ts/components/shared/invites-modal/invites-modal.ts b/src/ts/components/shared/invites-modal/invites-modal.ts new file mode 100644 index 0000000..0ba5236 --- /dev/null +++ b/src/ts/components/shared/invites-modal/invites-modal.ts @@ -0,0 +1,32 @@ +import { Component, Output, EventEmitter, OnInit } from '@angular/core'; +import { toPalette } from '../../../common/ponyInfo'; +import { decompressPonyString } from '../../../common/compressPony'; +import { PonyTownGame } from '../../../client/game'; +import { Action, SupporterInvite, PalettePonyInfo } from '../../../common/interfaces'; +import { removeItem } from '../../../common/utils'; +import { Model } from '../../services/model'; + +@Component({ + selector: 'invites-modal', + templateUrl: 'invites-modal.pug', +}) +export class InvitesModal implements OnInit { + @Output() close = new EventEmitter(); + invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = []; + error?: string; + constructor(private model: Model, private game: PonyTownGame) { + } + get inviteLimit() { + return this.model.supporterInviteLimit; + } + ngOnInit() { + this.game.send(server => server.getInvites())! + .then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) }))) + .then(invites => this.invites = invites); + } + remove(invite: SupporterInvite) { + this.error = undefined; + this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id)); + removeItem(this.invites, invite); + } +} diff --git a/src/ts/components/shared/kbd-key/kbd-key.pug b/src/ts/components/shared/kbd-key/kbd-key.pug new file mode 100644 index 0000000..a324f05 --- /dev/null +++ b/src/ts/components/shared/kbd-key/kbd-key.pug @@ -0,0 +1,3 @@ +kbd([title]="title || ''" [attr.aria-label]="title") + span([attr.aria-hidden]="!!title") + ng-content diff --git a/src/ts/components/shared/kbd-key/kbd-key.ts b/src/ts/components/shared/kbd-key/kbd-key.ts new file mode 100644 index 0000000..773ac0f --- /dev/null +++ b/src/ts/components/shared/kbd-key/kbd-key.ts @@ -0,0 +1,9 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'kbd-key', + templateUrl: 'kbd-key.pug', +}) +export class KbdKey { + @Input() title?: string; +} diff --git a/src/ts/components/shared/menu-bar/menu-bar.pug b/src/ts/components/shared/menu-bar/menu-bar.pug new file mode 100644 index 0000000..aff3dea --- /dev/null +++ b/src/ts/components/shared/menu-bar/menu-bar.pug @@ -0,0 +1,62 @@ +nav.navbar.navbar-expand.justify-content-end(role="navigation") + .d-none.d-md-block + a.pixelart.logo(*ngIf="logo" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }") + img.pixelart.logo-large(revSrc="images/logo-large.png" alt="Pony Town") + img.pixelart.logo-small(revSrc="images/logo-small.png" alt="Pony Town") + + .navbar-collapse.text-right + .navbar-nav.ml-auto + ng-content + .dropdown(*ngIf="account" #dropdown="ag-dropdown" dropdown autoClose="outsideClick") + button.btn.cursor-pointer.dropdown-toggle(dropdownToggle [attr.aria-label]="'Signed-in as ' + account.name") + fa-icon.mr-1( + *ngIf="hasSupporterIcon" [icon]="starIcon" [ngClass]="supporterClass" + [title]="supporterTitle" [attr.aria-label]="supporterTitle" aria-hidden="true") + .d-none.d-sm-inline(aria-hidden="true") {{account.name}} + fa-icon.d-inline-block.d-sm-none( + [icon]="userIcon" [title]="account.name" [fixedWidth]="true" size="lg" aria-hidden="true") + fa-icon.text-danger.account-alert-icon( + *ngIf="showAccountAlert" [icon]="alertIcon" [fixedWidth]="true" size="sm" aria-hidden="true") + + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + .dropdown-item.d-flex.align-items-center + .flex-grow-1.mr-3 Status: + .dropdown(dropdown style="width: 65px;") + button.online-offline.no-highlight.p-0.dropdown-toggle.no-arrow(dropdownToggle) + span.text-success(*ngIf="!hidden") online + span.text-muted(*ngIf="hidden") offline + fa-icon.ml-2([icon]="cogIcon") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + button.dropdown-item((click)="setStatus('online')") + fa-icon.mr-2.text-success([icon]="statusIcon" size="xs" [fixedWidth]="true") + | Online + button.dropdown-item((click)="setStatus('invisible')") + fa-icon.mr-2.text-muted([icon]="statusIcon" size="xs" [fixedWidth]="true") + | Show as Offline + a.dropdown-item(routerLink="/account" (click)="dropdown.close()" tabindex) + | Account settings + fa-icon.text-danger.ml-1(*ngIf="showAccountAlert" [icon]="alertIcon" aria-hidden="true") + .dropdown-divider + button.dropdown-item((click)="signOut.emit(); dropdown.close()") + | Sign out + + .text-muted(*ngIf="loading" style="font-size: 20px; padding: 10px 20px;") + fa-icon([icon]="spinnerIcon" [fixedWidth]="true" [spin]="true") + + form.navbar-form.ml-2(*ngIf="!loading && !account") + .button-group.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle [disabled]="!!loadingError") + | Sign in + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + .dropdown-header(*ngIf="signUpProviders.length") + | Sign in or sign up + button.dropdown-item( + *ngFor="let p of signUpProviders" (click)="signInTo(p)" title="Sign in using {{p.name}}") + fa-icon.mr-1([icon]="icon(p.id)" [fixedWidth]="true") + | {{p.name}} + .dropdown-header(*ngIf="signInProviders.length") + | Sign in only + button.dropdown-item.sign-in-only( + *ngFor="let p of signInProviders" (click)="signInTo(p)" title="Sign in using {{p.name}}") + fa-icon.mr-1([icon]="icon(p.id)" [fixedWidth]="true") + | {{p.name}} diff --git a/src/ts/components/shared/menu-bar/menu-bar.scss b/src/ts/components/shared/menu-bar/menu-bar.scss new file mode 100644 index 0000000..453d149 --- /dev/null +++ b/src/ts/components/shared/menu-bar/menu-bar.scss @@ -0,0 +1,59 @@ +@import '../../../../styles/partials/variables'; + +.navbar { + font-size: 16px; + margin-bottom: 0; + padding: 0px !important; + padding-top: $navbar-link-border !important; + + .navbar-toggler { + margin: 8px 0; + } +} + +.logo { + margin-top: 5px; + position: absolute; + top: 0; + left: 0; + + &.active { + display: none; + } +} + +.logo-large { + width: 287px; + display: none; + + @include media-breakpoint-up(lg) { + display: block; + } +} + +.logo-small { + width: 37px; + display: block; + + @include media-breakpoint-up(lg) { + display: none; + } +} + +.btn { + transition: none; +} + +.sign-in-only { + opacity: 0.6; +} + +.account-alert-icon { + position: absolute; + right: 20px; + top: 5px; +} + +.online-offline { + font-weight: 500; +} diff --git a/src/ts/components/shared/menu-bar/menu-bar.ts b/src/ts/components/shared/menu-bar/menu-bar.ts new file mode 100644 index 0000000..d7d978a --- /dev/null +++ b/src/ts/components/shared/menu-bar/menu-bar.ts @@ -0,0 +1,61 @@ +import { Component, Input, Output, EventEmitter, HostListener } from '@angular/core'; +import { AccountData, OAuthProvider } from '../../../common/interfaces'; +import { signUpProviders, signInProviders } from '../../../client/data'; +import { getProviderIcon } from '../sign-in-box/sign-in-box'; +import { faStar, faSpinner, faUser, faExclamationCircle, faCog, faCircle } from '../../../client/icons'; +import { Model } from '../../services/model'; +import { supporterClass, supporterTitle, isSupporterOrPastSupporter } from '../../../client/clientUtils'; +import { SettingsService } from '../../services/settingsService'; +import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants'; + +@Component({ + selector: 'menu-bar', + templateUrl: 'menu-bar.pug', + styleUrls: ['menu-bar.scss'], +}) +export class MenuBar { + readonly signUpProviders = signUpProviders; + readonly signInProviders = signInProviders; + readonly starIcon = faStar; + readonly spinnerIcon = faSpinner; + readonly userIcon = faUser; + readonly alertIcon = faExclamationCircle; + readonly cogIcon = faCog; + readonly statusIcon = faCircle; + @Input() logo = false; + @Input() loading = false; + @Input() loadingError = false; + @Input() account?: AccountData; + @Output() signOut = new EventEmitter(); + @Output() signIn = new EventEmitter(); + constructor(private model: Model, private settings: SettingsService) { + } + get hasSupporterIcon() { + return isSupporterOrPastSupporter(this.account); + } + get supporterTitle() { + return supporterTitle(this.account); + } + get supporterClass() { + return supporterClass(this.account); + } + get showAccountAlert() { + return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH; + } + get hidden() { + return !!this.settings.account.hidden; + } + icon(id: string) { + return getProviderIcon(id); + } + signInTo(provider: OAuthProvider) { + this.signIn.emit(provider); + } + @HostListener('window:resize') + resize() { + } + setStatus(status: string) { + this.settings.account.hidden = status === 'invisible'; + this.settings.saveAccountSettings(this.settings.account); + } +} diff --git a/src/ts/components/shared/menu-item/menu-item.pug b/src/ts/components/shared/menu-item/menu-item.pug new file mode 100644 index 0000000..520cdaf --- /dev/null +++ b/src/ts/components/shared/menu-item/menu-item.pug @@ -0,0 +1,5 @@ +.navbar-link(routerLinkActive="active" [routerLinkActiveOptions]="{ exact: route === '/' }") + a([routerLink]="route" tabindex linkCurrent) + .d-none.d-sm-inline {{name}} + fa-icon.d-inline-block.d-sm-none( + [icon]="icon" [title]="name" [attr.aria-label]="name" [fixedWidth]="true" size="lg") diff --git a/src/ts/components/shared/menu-item/menu-item.scss b/src/ts/components/shared/menu-item/menu-item.scss new file mode 100644 index 0000000..31315b8 --- /dev/null +++ b/src/ts/components/shared/menu-item/menu-item.scss @@ -0,0 +1,26 @@ +@import '../../../../styles/partials/variables'; + +.navbar-link { + border-top: solid $navbar-link-border transparent; + margin-top: -$navbar-link-border; + margin-right: 5px; + + &.active { + text-decoration: none; + border-top-color: $navbar-dark-active-color; + border-right-color: $navbar-dark-active-color; + } + + &:hover, &:active, &:link { + text-decoration: none; + border-top-color: $navbar-dark-active-color; + border-right-color: $navbar-dark-active-color; + } + + > a { + @media (max-width: 360px) { + padding-left: 10px; + padding-right: 10px; + } + } +} diff --git a/src/ts/components/shared/menu-item/menu-item.ts b/src/ts/components/shared/menu-item/menu-item.ts new file mode 100644 index 0000000..b422246 --- /dev/null +++ b/src/ts/components/shared/menu-item/menu-item.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { emptyIcon } from '../../../client/icons'; + +@Component({ + selector: 'menu-item', + templateUrl: 'menu-item.pug', + styleUrls: ['menu-item.scss'], +}) +export class MenuItem { + @Input() route: any; + @Input() name?: string; + @Input() icon = emptyIcon; +} diff --git a/src/ts/components/shared/mod-box/mod-box.pug b/src/ts/components/shared/mod-box/mod-box.pug new file mode 100644 index 0000000..08731d5 --- /dev/null +++ b/src/ts/components/shared/mod-box/mod-box.pug @@ -0,0 +1,72 @@ +ng-template(#notePopover) + textarea.form-control.pony-mod-note-editor( + cols="20" rows="8" agAutoFocus [(ngModel)]="note" (keydown.escape)="blur()" (blur)="blur()") + +ng-template(#tooltip) + .text-left.text-pre + div(*ngIf="hasCounters") + span.text-muted spam: + span.ml-1 {{counters?.spam || 0}} + span.text-muted.ml-1 swearing: + span.ml-1 {{counters?.swears || 0}} + span.text-muted.ml-1 timeouts: + span.ml-1 {{counters?.timeouts || 0}} + div {{note}} + +.btn-group.btn-group-xs.btn-group-shadow(*ngIf="check?.xcz?.vdw?.qwe?.mnb") + .btn-group.dropdown(dropdown) + button.btn.btn-xs.btn-default(dropdownToggle) + fa-icon([icon]="moreIcon") + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="report()") + fa-icon.mr-1([icon]="flagIcon" [fixedWidth]="true") + | Report + h6.dropdown-header Other options + a.dropdown-item(*ngFor="let a of check.actions" (click)="modAction(a.action)") + fa-icon.mr-1.text-danger([icon]="dangerIcon" [fixedWidth]="true") + | {{a.name}} + + .btn-group( + [tooltip]="isNoteOpen ? null : tooltip" [isDisabled]="!note && !hasCounters" + placement="bottom" containerClass="tooltip-notes") + button.btn.btn-xs( + [popover]="notePopover" + placement="bottom" + [isOpen]="isNoteOpen" + [btnHighlightDanger]="!!note" + [class.pointer-none]="isNoteOpen" + (onShown)="isNoteOpen = true" + (onHidden)="blur()" + container="body") + fa-icon([icon]="noteIcon") + + .btn-group.dropdown(dropdown [tooltip]="muteTooltip") + button.btn.btn-xs(dropdownToggle [ngClass]="className(mute)") + fa-icon([icon]="muteIcon") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="setMute(0)") + | clear #[b mute] + a.dropdown-item((click)="setMute(-1)") + | perma #[b mute] + .dropdown-divider + a.dropdown-item(*ngFor="let t of timeouts" (click)="setMute(t.value)") + | {{t.label}} + + .btn-group.dropdown(dropdown [tooltip]="shadowTooltip") + button.btn.btn-xs(dropdownToggle [ngClass]="className(shadow)") + fa-icon([icon]="hideIcon") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item((click)="setShadow(0)") + | clear #[b shadow] + a.dropdown-item((click)="setShadow(-1)") + | perma #[b shadow] + .dropdown-divider + a.dropdown-item(*ngFor="let t of timeouts" (click)="setShadow(t.value)") + | {{t.label}} + +.pony-mod-age([title]="ageTitle") + | {{ageLabel}} + +.pony-mod-account(*ngIf="account") + | {{account}} + span.text-smuted.ml-1 [{{country || '??'}}] diff --git a/src/ts/components/shared/mod-box/mod-box.scss b/src/ts/components/shared/mod-box/mod-box.scss new file mode 100644 index 0000000..011b0f3 --- /dev/null +++ b/src/ts/components/shared/mod-box/mod-box.scss @@ -0,0 +1,38 @@ +@import '../../../../styles/partials/variables'; + +.pony-mod-account { + position: absolute; + left: 2px; + bottom: -22px; + font-weight: bold; + white-space: nowrap; + color: white; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.8); + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + max-width: none; + } +} + +.pony-mod-age { + position: absolute; + left: -25px; + width: 20px; + text-align: center; + font-weight: bold; + color: $primary; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.8); +} + +.pony-mod-note-editor { + color: #333; + background: none; + margin: -9px -14px; + width: 250px; + max-width: 250px; + max-height: 140px; + font-size: smaller; +} diff --git a/src/ts/components/shared/mod-box/mod-box.ts b/src/ts/components/shared/mod-box/mod-box.ts new file mode 100644 index 0000000..9308213 --- /dev/null +++ b/src/ts/components/shared/mod-box/mod-box.ts @@ -0,0 +1,97 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { ModAction, Pony } from '../../../common/interfaces'; +import { TIMEOUTS } from '../../../common/constants'; +import { Model } from '../../services/model'; +import { PonyTownGame } from '../../../client/game'; +import { faFlag, faStickyNote, faMicrophoneSlash, faEyeSlash, faUserCog, faExclamationCircle } from '../../../client/icons'; + +const ageLabels = ['', 'M', 'A', '', '', '[M]', '[A]']; +const ageTitles = ['Not set', 'Minor', 'Adult', '', '', 'Minor (locked)', 'Adult (locked)']; + +@Component({ + selector: 'mod-box', + templateUrl: 'mod-box.pug', + styleUrls: ['mod-box.scss'], +}) +export class ModBox implements OnDestroy { + readonly flagIcon = faFlag; + readonly noteIcon = faStickyNote; + readonly muteIcon = faMicrophoneSlash; + readonly hideIcon = faEyeSlash; + readonly moreIcon = faUserCog; + readonly dangerIcon = faExclamationCircle; + readonly timeouts = TIMEOUTS; + @Input() pony!: Pony; + isNoteOpen = false; + constructor(private model: Model, private game: PonyTownGame) { + } + get ageLabel() { + return ageLabels[this.modInfo && this.modInfo.age || 0]; + } + get ageTitle() { + return ageTitles[this.modInfo && this.modInfo.age || 0]; + } + get modInfo() { + return this.pony.modInfo; + } + get account() { + return this.modInfo && this.modInfo.account; + } + get country() { + return this.modInfo && this.modInfo.country; + } + get mute() { + return this.modInfo && this.modInfo.mute; + } + get muteTooltip() { + return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute'; + } + get shadow() { + return this.modInfo && this.modInfo.shadow; + } + get shadowTooltip() { + return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow'; + } + get counters() { + return this.modInfo && this.modInfo.counters; + } + get hasCounters() { + const counters = this.counters; + return counters && (counters.spam || counters.swears || counters.timeouts); + } + get check() { + return this.model.modCheck; + } + get note() { + return this.modInfo && this.modInfo.note; + } + set note(value) { + if (this.modInfo) { + this.modInfo.note = value; + } + } + ngOnDestroy() { + if (this.isNoteOpen) { + this.blur(); + } + } + className(value: string) { + return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default'; + } + report() { + this.modAction(ModAction.Report); + } + setMute(value: number) { + this.modAction(ModAction.Mute, value); + } + setShadow(value: number) { + this.modAction(ModAction.Shadow, value); + } + blur() { + this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || '')); + this.isNoteOpen = false; + } + modAction(type: ModAction, param = 0) { + return this.game.send(server => server.otherAction(this.pony.id, type, param)); + } +} diff --git a/src/ts/components/shared/notification/notification-item.pug b/src/ts/components/shared/notification/notification-item.pug new file mode 100644 index 0000000..f68c785 --- /dev/null +++ b/src/ts/components/shared/notification/notification-item.pug @@ -0,0 +1,20 @@ +ng-template(#popover) + .notification-popover + p([innerHTML]="notification.message") + .small.text-muted.mb-1(*ngIf="notification.note") + | {{notification.note}} + div + button.btn.btn-xs.btn-outline-danger.float-right( + *ngIf="ignoreButton" (click)="ignore()" tooltip="Ignore this player") + fa-icon([icon]="banIcon") + .notification-buttons + button.btn.btn-xs.btn-success(*ngIf="okButton" (click)="accept()") ok + button.btn.btn-xs.btn-success(*ngIf="yesButton" (click)="accept()") yes + button.btn.btn-xs.btn-success(*ngIf="acceptButton" (click)="accept()") accept + button.btn.btn-xs.btn-danger(*ngIf="noButton" (click)="reject()") no + button.btn.btn-xs.btn-danger(*ngIf="rejectButton" (click)="reject()") reject + +.notification.cursor-pointer( + [popover]="popover" [isOpen]="isOpen" (contextmenu)="reject(); $event.preventDefault()" + placement="left" containerClass="popover-notification") + portrait-box([pony]="paletteInfo" [flip]="true" size="small") diff --git a/src/ts/components/shared/notification/notification-item.scss b/src/ts/components/shared/notification/notification-item.scss new file mode 100644 index 0000000..8b8d8b6 --- /dev/null +++ b/src/ts/components/shared/notification/notification-item.scss @@ -0,0 +1,3 @@ +.notification { + padding: 5px; +} diff --git a/src/ts/components/shared/notification/notification-item.ts b/src/ts/components/shared/notification/notification-item.ts new file mode 100644 index 0000000..af9cf95 --- /dev/null +++ b/src/ts/components/shared/notification/notification-item.ts @@ -0,0 +1,68 @@ +import { Component, Input, OnDestroy } from '@angular/core'; +import { PlayerAction, Notification, NotificationFlags, EntityPlayerState } from '../../../common/interfaces'; +import { PonyTownGame } from '../../../client/game'; +import { hasFlag, setFlag } from '../../../common/utils'; +import { faBan } from '../../../client/icons'; +import { getPaletteInfo } from '../../../common/pony'; + +@Component({ + selector: 'notification-item', + templateUrl: 'notification-item.pug', + styleUrls: ['notification-item.scss'], +}) +export class NotificationItem implements OnDestroy { + readonly banIcon = faBan; + @Input() notification!: Notification; + constructor(private game: PonyTownGame) { + } + get isOpen() { + return this.notification.open; + } + set isOpen(value: boolean) { + if (value) { + this.game.notifications.forEach(n => n.open = false); + } + + this.notification.open = value; + } + get okButton() { + return hasFlag(this.notification.flags, NotificationFlags.Ok); + } + get yesButton() { + return hasFlag(this.notification.flags, NotificationFlags.Yes); + } + get acceptButton() { + return hasFlag(this.notification.flags, NotificationFlags.Accept); + } + get noButton() { + return hasFlag(this.notification.flags, NotificationFlags.No); + } + get rejectButton() { + return hasFlag(this.notification.flags, NotificationFlags.Reject); + } + get ignoreButton() { + return hasFlag(this.notification.flags, NotificationFlags.Ignore); + } + get paletteInfo() { + return getPaletteInfo(this.notification.pony); + } + ngOnDestroy() { + this.isOpen = false; + } + accept() { + this.game.send(server => server.acceptNotification(this.notification.id)); + } + reject() { + this.game.send(server => server.rejectNotification(this.notification.id)); + } + ignore() { + this.reject(); + + const pony = this.notification.pony; + + if (pony !== this.game.player) { + this.game.send(server => server.playerAction(pony.id, PlayerAction.Ignore, undefined)); + pony.playerState = setFlag(pony.playerState, EntityPlayerState.Ignored, true); + } + } +} diff --git a/src/ts/components/shared/notification/notification-list.pug b/src/ts/components/shared/notification/notification-list.pug new file mode 100644 index 0000000..12dc7dd --- /dev/null +++ b/src/ts/components/shared/notification/notification-list.pug @@ -0,0 +1,7 @@ +.notification-list + .game-button.notification-ellipsis(*ngIf="start" (click)="prev()") + fa-icon([icon]="ellipsisIcon") + notification-item( + *ngFor="let n of notifications | slice:start:limit" [notification]="n" [class.elastic-from-right]="n.fresh") + .game-button.notification-ellipsis(*ngIf="hasMore" (click)="next()") + fa-icon([icon]="ellipsisIcon") diff --git a/src/ts/components/shared/notification/notification-list.scss b/src/ts/components/shared/notification/notification-list.scss new file mode 100644 index 0000000..3e82311 --- /dev/null +++ b/src/ts/components/shared/notification/notification-list.scss @@ -0,0 +1,4 @@ +.notification-ellipsis { + text-align: center; + margin-right: -2px; +} diff --git a/src/ts/components/shared/notification/notification-list.ts b/src/ts/components/shared/notification/notification-list.ts new file mode 100644 index 0000000..b43d3b1 --- /dev/null +++ b/src/ts/components/shared/notification/notification-list.ts @@ -0,0 +1,33 @@ +import { Component, Input } from '@angular/core'; +import { Notification } from '../../../common/interfaces'; +import { faEllipsisV } from '../../../client/icons'; + +const LIMIT = 8; + +@Component({ + selector: 'notification-list', + templateUrl: 'notification-list.pug', + styleUrls: ['notification-list.scss'], +}) +export class NotificationList { + readonly ellipsisIcon = faEllipsisV; + @Input() notifications!: Notification[]; + @Input() set notificationsLength(value: number) { + while (this.start > value) { + this.prev(); + } + } + start = 0; + get limit() { + return this.start + LIMIT; + } + get hasMore() { + return this.notifications.length > (this.start + this.limit); + } + next() { + this.start += this.limit; + } + prev() { + this.start -= this.start <= LIMIT ? LIMIT : LIMIT - 1; + } +} diff --git a/src/ts/components/shared/page-loader/page-loader.pug b/src/ts/components/shared/page-loader/page-loader.pug new file mode 100644 index 0000000..1cdd65f --- /dev/null +++ b/src/ts/components/shared/page-loader/page-loader.pug @@ -0,0 +1,18 @@ +.page-loader.text-muted.text-center.text-large(*ngIf="loading || updating") + fa-icon([icon]="spinnerIcon" [fixedWidth]="true" [spin]="true") + + div(*ngIf="updating") + p.page-updating.text-muted Updating... + p.text-unsafe(*ngIf="updatingTakesLongTime") + | Updating is taking longer than expected, #[button.btn.btn-outline-danger((click)="reload()") restart] + + div(*ngIf="loadingError && !updating") + div([ngSwitch]="loadingError") + p.text-unsafe(*ngSwitchCase="'request-limit'") + | Server is under heavy load, please wait... + p.text-unsafe(*ngSwitchCase="'cannot-connect'") + | Cannot connect to the server, retrying... + p.text-unsafe(*ngSwitchCase="'cloudflare-error'") + | Cloudflare protection error, #[button.btn.btn-outline-danger((click)="reload()") reload] to continue. + p.text-unsafe(*ngSwitchDefault) + | Unexpected error occurred, #[button.btn.btn-outline-danger((click)="reload()") reload] to continue. diff --git a/src/ts/components/shared/page-loader/page-loader.scss b/src/ts/components/shared/page-loader/page-loader.scss new file mode 100644 index 0000000..4afe629 --- /dev/null +++ b/src/ts/components/shared/page-loader/page-loader.scss @@ -0,0 +1,20 @@ +.page-loader { + padding: 150px 0; +} + +fa-icon { + font-size: 50px; +} + +p { + margin-top: 30px; +} + +.page-updating { + font-size: 20px; +} + +.btn { + vertical-align: baseline; + font-weight: bold; +} diff --git a/src/ts/components/shared/page-loader/page-loader.ts b/src/ts/components/shared/page-loader/page-loader.ts new file mode 100644 index 0000000..52c2330 --- /dev/null +++ b/src/ts/components/shared/page-loader/page-loader.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { faSpinner } from '../../../client/icons'; +import { Model } from '../../services/model'; +import { hardReload } from '../../../client/clientUtils'; + +@Component({ + selector: 'page-loader', + templateUrl: 'page-loader.pug', + styleUrls: ['page-loader.scss'], +}) +export class PageLoader { + readonly spinnerIcon = faSpinner; + constructor(private model: Model) { + } + get loading() { + return this.model.loading; + } + get updating() { + return this.model.updating; + } + get updatingTakesLongTime() { + return this.model.updatingTakesLongTime; + } + get loadingError() { + return this.model.loadingError; + } + reload() { + hardReload(); + } +} diff --git a/src/ts/components/shared/party-box/party-box.pug b/src/ts/components/shared/party-box/party-box.pug new file mode 100644 index 0000000..99c75c1 --- /dev/null +++ b/src/ts/components/shared/party-box/party-box.pug @@ -0,0 +1,6 @@ +.party-box([class.pending]="member.pending" [class.offline]="member.offline" (window:resize)="true") + portrait-box.cursor-pointer([pony]="paletteInfo" size="small" (click)="click()") + .party-box-icon.party-box-leader.pointer-none(*ngIf="member.leader" title="Party leader") + fa-icon([icon]="leaderIcon" size="xs") + .party-box-icon.party-box-offline.pointer-none(*ngIf="member.offline" title="Offline") + fa-icon([icon]="offlineIcon" size="sm") diff --git a/src/ts/components/shared/party-box/party-box.scss b/src/ts/components/shared/party-box/party-box.scss new file mode 100644 index 0000000..16b8edf --- /dev/null +++ b/src/ts/components/shared/party-box/party-box.scss @@ -0,0 +1,33 @@ +.party-box { + margin-bottom: 10px; + position: relative; + + &.pending { + opacity: 0.5; + } + + &.offline { + opacity: 0.7; + filter: grayscale(100%); + } +} + +.party-box-icon { + position: absolute; + text-shadow: 0 0 5px #000; + font-size: 18px; + + > fa-icon { + filter: drop-shadow(0 0 5px #000); + } +} + +.party-box-leader { + left: -5px; + top: -14px; +} + +.party-box-offline { + left: -2px; + bottom: -8px; +} diff --git a/src/ts/components/shared/party-box/party-box.ts b/src/ts/components/shared/party-box/party-box.ts new file mode 100644 index 0000000..eb73ea6 --- /dev/null +++ b/src/ts/components/shared/party-box/party-box.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { PartyMember } from '../../../common/interfaces'; +import { PonyTownGame } from '../../../client/game'; +import { partyLeaderIcon, offlineIcon } from '../../../client/icons'; +import { getPaletteInfo } from '../../../common/pony'; + +@Component({ + selector: 'party-box', + templateUrl: 'party-box.pug', + styleUrls: ['party-box.scss'], +}) +export class PartyBox { + readonly leaderIcon = partyLeaderIcon; + readonly offlineIcon = offlineIcon; + @Input() member!: PartyMember; + constructor(private game: PonyTownGame) { + } + get paletteInfo() { + return this.member.pony && getPaletteInfo(this.member.pony); + } + click() { + this.game.select(this.member.pony); + } +} diff --git a/src/ts/components/shared/party-list/party-list.pug b/src/ts/components/shared/party-list/party-list.pug new file mode 100644 index 0000000..cf27680 --- /dev/null +++ b/src/ts/components/shared/party-list/party-list.pug @@ -0,0 +1,20 @@ +.party-list.unselectable(*ngIf="hasParty") + .dropdown(dropdown [hookToCanvas]="true") + a.game-button.party-list-options(dropdownToggle) + fa-icon(*ngIf="!isLeader" [icon]="cogIcon" [fixedWidth]="true" title="Party options") + fa-icon(*ngIf="isLeader" [icon]="leaderIcon" [fixedWidth]="true" title="You are party leader") + .dropdown-menu(*dropdownMenu) + .dropdown-header(*ngIf="isLeader") + | You are party leader + a.dropdown-item((click)="leave()") + | Leave party + a.dropdown-item((click)="hidden = !hidden") + | {{hidden ? 'Show party' : 'Hide party'}} + + .party-list-items.pt-1(*ngIf="!hidden") + .game-button.party-list-ellipsis.mb-1(*ngIf="start" (click)="prev()") + fa-icon([icon]="ellipsisIcon") + div(*ngFor="let m of members | slice:start:limit") + party-box([member]="m") + .game-button.party-list-ellipsis(*ngIf="hasMore" (click)="next()" style="margin-top: -5px") + fa-icon([icon]="ellipsisIcon") diff --git a/src/ts/components/shared/party-list/party-list.scss b/src/ts/components/shared/party-list/party-list.scss new file mode 100644 index 0000000..1f9b59f --- /dev/null +++ b/src/ts/components/shared/party-list/party-list.scss @@ -0,0 +1,18 @@ +.party-list { + position: relative; + + @media (max-height: 180px) { + display: none; + } +} + +.party-list-ellipsis { + margin-top: -4px; + margin-left: -3px; +} + +.party-list-items { + @media (max-height: 260px) { + display: none; + } +} diff --git a/src/ts/components/shared/party-list/party-list.ts b/src/ts/components/shared/party-list/party-list.ts new file mode 100644 index 0000000..e2b7078 --- /dev/null +++ b/src/ts/components/shared/party-list/party-list.ts @@ -0,0 +1,93 @@ +import { Component, HostListener, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { PartyMember } from '../../../common/interfaces'; +import { PARTY_LIMIT } from '../../../common/constants'; +import { PonyTownGame } from '../../../client/game'; +import { partyLeaderIcon, faCog, faEllipsisV } from '../../../client/icons'; +import { clamp } from '../../../common/utils'; +import { isPartyLeader } from '../../../client/partyUtils'; + +function visibleMembers(members: PartyMember[], max: number, start: number) { + return members.length > max ? Math.max(max - (start > 0 ? 2 : 1), 1) : max; +} + +@Component({ + selector: 'party-list', + templateUrl: 'party-list.pug', + styleUrls: ['party-list.scss'], +}) +export class PartyList implements OnInit, OnDestroy { + readonly ellipsisIcon = faEllipsisV; + readonly leaderIcon = partyLeaderIcon; + readonly cogIcon = faCog; + hidden = false; + start = 0; + maxMembers = PARTY_LIMIT - 1; + members: PartyMember[] = []; + private subscription?: Subscription; + constructor(private game: PonyTownGame) { + } + get hasParty() { + return this.game.party !== undefined; + } + get isLeader() { + return isPartyLeader(this.game); + } + get hasMore() { + return this.members.length > (this.start + this.visible); + } + get visible() { + return visibleMembers(this.members, this.maxMembers, this.start); + } + get limit() { + return this.start + this.visible; + } + ngOnInit() { + this.subscription = this.game.onPartyUpdate.subscribe(() => this.update()); + this.resized(); + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + isMe(member: PartyMember) { + return this.game.player && this.game.player.id === member.id; + } + leave() { + this.game.send(server => server.leaveParty()); + } + update() { + if (this.hasParty) { + this.members = this.game.party ? this.game.party.members.filter(m => !m.self) : []; + + while (this.start > 0 && this.members.length <= this.start) { + this.start = 0; + } + } else { + this.members = []; + this.start = 0; + } + } + @HostListener('window:resize') + resized() { + const padding = 140 + 110; + const max = clamp(Math.floor((window.innerHeight - padding) / 43), 0, PARTY_LIMIT - 1); + + if (this.maxMembers !== max) { + this.start = 0; + this.maxMembers = max; + } + } + next() { + this.start += this.visible; + } + prev() { + const max = this.members.length - 1; + let start = 0; + + while (start < max && (start + visibleMembers(this.members, this.maxMembers, start)) !== this.start) { + start++; + } + + this.start = start; + } +} diff --git a/src/ts/components/shared/pipes/siteName.ts b/src/ts/components/shared/pipes/siteName.ts new file mode 100644 index 0000000..8566dd1 --- /dev/null +++ b/src/ts/components/shared/pipes/siteName.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'siteName', +}) +export class SiteNamePipe implements PipeTransform { + transform(value: string | undefined) { + const match = String(value || '').match(/(\w+)\.com/); + return match && match[1]; + } +} diff --git a/src/ts/components/shared/play-box/play-box.pug b/src/ts/components/shared/play-box/play-box.pug new file mode 100644 index 0000000..989d8f6 --- /dev/null +++ b/src/ts/components/shared/play-box/play-box.pug @@ -0,0 +1,121 @@ +.form-group(*ngIf="hasTooManyPonies") + .alert.alert-warning(role="alert") + | You have more than the allowed number of {{characterLimit}} ponies, the additional ones may get deleted. + | Remove some of your unused ponies to prevent losing the ones you want to keep. + +.form-group(*ngIf="isMarkedForMultiples") + .alert.alert-warning(role="alert") + | Your account has been flagged for creating multiple accounts. + | Creating more accounts may result in a permanent ban. + +.form-group(*ngIf="accountAlert") + .alert.alert-warning(role="alert") + | {{accountAlert}} + +.form-group(*ngIf="leftMessage") + .alert.alert-warning(role="alert") + | {{leftMessage}} + +.form-group(*ngIf="isAndroidBrowser") + .alert.alert-warning(role="alert") + | Your browser is outdated and is not able to correctly run Pony Town. + | Please install different browser to be able to play the game. + +.form-group(*ngIf="model.missingBirthdate && !birthdateSet && requestBirthdate") + .alert.alert-warning.text-left(role="alert") + label(for="birthdate") + | Set your date of birth + date-picker([(date)]="birthdate") + .mt-2.text-right + button.btn.btn-default.px-4((click)="saveBirthdate()") Save + p.mb-0.mt-2 + | Please fill-in your #[b date of birth] in order to not lose access to the game in future updates. + +.form-group.dropdown(dropdown) + .btn-group.d-flex + button.btn.btn-lg.btn-success.text-ellipsis.flex-grow-1( + #playButton (click)="joining ? cancel() : play()" [disabled]="!canPlay && !joining") + div(*ngIf="joining") + fa-icon.mr-1([icon]="spinnerIcon" [spin]="true") + | Cancel + div(*ngIf="!joining && server") + | #[strong {{label || 'Play'}}] on #[span {{server.name}}] + .text-faded(*ngIf="!joining && !server") + | select server to play + button.btn.btn-lg.btn-success.flex-grow-0.dropdown-toggle( + dropdownToggle aria-label="select server" [disabled]="joining" style="position: relative") + + .dropdown-menu.w-100(*dropdownMenu style="overflow: hidden") + button.dropdown-item(*ngFor="let s of servers" (click)="server = s; playButton.focus()") + div + .float-right(style="position: relative;") + .text-unsafe(*ngIf="s.offline") #[span.sr-only server] offline + .text-muted(*ngIf="!s.offline") online #[span.sr-only players] ({{s.online}}) + .flag.mr-2(*ngFor="let f of s.countryFlags" [ngClass]="'flag-' + f") + fa-icon.text-muted.mr-2(*ngIf="!hasFlag(s)" [icon]="getIcon(s)" size="lg") + strong {{s.name}} + .text-muted.text-wrap {{s.desc}} + +.form-group.text-left.text-muted.server-alert(*ngIf="server?.alert === '18+'") + fa-icon.float-left.p-2([icon]="warningIcon" size="2x") + | By playing on this server you confirm that you are over 18 years old and you take no issue + | with seeing adult topics. + +.form-group.text-left.text-info.server-alert(*ngIf="server?.alert === 'test'") + fa-icon.float-left.p-2([icon]="infoIcon" size="2x") + | Supporter test server: Here you can try experimental and unfinished features that we're working on. + | Keep in mind everything is subject to change. + +.form-group(*ngIf="server?.offline") + .alert.alert-info(role="alert") + | Selected server is offline, try again later + +.form-group(*ngIf="offline") + .alert.alert-info(role="alert") + | Server is offline, try again later + +.form-group(*ngIf="protectionError && !offline") + .alert.alert-info(role="alert") + | Cloudflare error, #[button.btn.btn-sm.btn-outline-default((click)="reload()") reload] to continue. + +.form-group(*ngIf="updateWarning") + .alert.alert-warning(role="alert") + | Server will restart shortly for updates and maintenance. + | Save your character to avoid losing any progress. + +.form-group(*ngIf="isBrowserOutdated") + .alert.alert-warning(role="alert") + button.close.float-right((click)="dismissOutdatedBrowser()" aria-label="Close" style="font-size: 20px;") × + | Your browser is outdated and is known to have issues running Pony Town. + | Make sure you have latest version installed. + +.form-group(*ngIf="invalidVersion && !offline") + .alert.alert-danger(role="alert") + | Your client version is outdated, #[button.btn.btn-sm.btn-outline-default((click)="reload()") reload] to be able to play. + +.form-group(*ngIf="isAccessError") + .alert.alert-danger(role="alert") + | You're no longer signed-in, #[button.btn.btn-sm.btn-outline-default((click)="reload()") reload] to be able to sign-in again. + +.form-group(*ngIf="failedToLoadImages") + .alert.alert-danger(role="alert") + | Failed to load game assets, #[button.btn.btn-sm.btn-outline-default((click)="hardReload()") reload] to retry. + +.form-group(*ngIf="isWebGLError") + .alert.alert-danger(role="alert") + | Failed to create WebGL context. Your graphics card drivers or browser are outdated or graphics + | acceleration is disabled. Go to #[a.alert-link(href="http://webglreport.com/" tabindex) WebGL Report] to + | check WebGL support in your browser. + +.form-group(*ngIf="isBrowserError") + .alert.alert-danger(role="alert") + | Your browser is outdated, make sure you have the latest version installed. + +.form-group(*ngIf="isOtherError") + .alert.alert-danger(role="alert") + | {{error}} + +.form-group.text-left.text-large(*ngIf="server") + h5 Server rules + p.text-muted.list-rules + | {{server.desc}} diff --git a/src/ts/components/shared/play-box/play-box.scss b/src/ts/components/shared/play-box/play-box.scss new file mode 100644 index 0000000..873ff44 --- /dev/null +++ b/src/ts/components/shared/play-box/play-box.scss @@ -0,0 +1,26 @@ +@import '../../../../styles/partials/variables'; + +.btn-group.d-flex { + display: flex; + + > .btn.flex-grow { + flex-grow: 1; + } +} + +.dropdown-toggle { + display: flex; + align-items: center; + justify-content: center; +} + +.server-alert { + display: flex; + align-items: center; + font-size: 13px; +} + +.alert .btn { + vertical-align: baseline; + font-weight: bold; +} diff --git a/src/ts/components/shared/play-box/play-box.ts b/src/ts/components/shared/play-box/play-box.ts new file mode 100644 index 0000000..87796a3 --- /dev/null +++ b/src/ts/components/shared/play-box/play-box.ts @@ -0,0 +1,183 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { ServerInfo, AccountDataFlags } from '../../../common/interfaces'; +import { RequestError, delay, includes, hasFlag } from '../../../common/utils'; +import { + WEBGL_CREATION_ERROR, ACCESS_ERROR, ACCOUNT_ERROR, BROWSER_NOT_SUPPORTED_ERROR, NAME_ERROR, VERSION_ERROR, + OFFLINE_ERROR, PROTECTION_ERROR, NOT_AUTHENTICATED_ERROR, CHARACTER_LIMIT_ERROR +} from '../../../common/errors'; +import { version } from '../../../client/data'; +import { GameService } from '../../services/gameService'; +import { Model } from '../../services/model'; +import { faSpinner, faExclamationCircle, faInfoCircle, faGlobe, faStar, faWrench } from '../../../client/icons'; +import { isBrowserOutdated, hardReload, isAndroidBrowser } from '../../../client/clientUtils'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { StorageService } from '../../services/storageService'; +import { ErrorReporter } from '../../services/errorReporter'; +import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants'; + +const ignoredErrors = [ + WEBGL_CREATION_ERROR, + BROWSER_NOT_SUPPORTED_ERROR, + NAME_ERROR, + OFFLINE_ERROR, + VERSION_ERROR, + ACCESS_ERROR, + PROTECTION_ERROR, + NOT_AUTHENTICATED_ERROR, + CHARACTER_LIMIT_ERROR, + 'Saving in progress', +]; + +@Component({ + selector: 'play-box', + templateUrl: 'play-box.pug', + styleUrls: ['play-box.scss'], +}) +export class PlayBox implements OnInit { + readonly spinnerIcon = faSpinner; + readonly warningIcon = faExclamationCircle; + readonly infoIcon = faInfoCircle; + readonly requestBirthdate = REQUEST_DATE_OF_BIRTH; + @Output() errorChange = new EventEmitter(); + @Input() label?: string; + joining = false; + failedToLoadImages = false; + birthdate = ''; + birthdateSet = false; + private locked = false; + constructor( + public gameService: GameService, + public model: Model, + private storage: StorageService, + private errorReporter: ErrorReporter, + ) { + } + @Input() + get error() { + return this.gameService.error; + } + set error(value: string | undefined) { + if (this.gameService) { + this.gameService.error = value; + this.errorChange.emit(value); + } + } + get server() { + return this.gameService.server; + } + set server(value: ServerInfo | undefined) { + this.gameService.server = value; + } + get servers() { + return this.gameService.servers; + } + get offline(): boolean { + return this.gameService.offline; + } + get updateWarning() { + return this.gameService.updateWarning; + } + get invalidVersion(): boolean { + return !!(this.gameService.versionError || this.error === VERSION_ERROR + || (this.gameService.version && this.gameService.version !== version)); + } + get protectionError(): boolean { + return this.gameService.protectionError || this.error === PROTECTION_ERROR; + } + get canPlay(): boolean { + return !!this.server && this.gameService.canPlay && !this.locked && !this.invalidVersion && !this.failedToLoadImages; + } + get isAccessError(): boolean { + return this.error === ACCESS_ERROR || this.error === ACCOUNT_ERROR; + } + get isWebGLError(): boolean { + return this.error === WEBGL_CREATION_ERROR; + } + get isBrowserError(): boolean { + return this.error === BROWSER_NOT_SUPPORTED_ERROR; + } + get isOtherError(): boolean { + return !!this.error && !this.invalidVersion && !this.isAccessError && !this.isWebGLError && !this.isBrowserError; + } + get ponyLimit() { + return this.model.characterLimit; + } + get hasTooManyPonies(): boolean { + return this.model.ponies.length > this.ponyLimit; + } + get isMarkedForMultiples(): boolean { + const account = this.model.account; + return !!account && hasFlag(account.flags, AccountDataFlags.Duplicates); + } + get isAndroidBrowser() { + return isAndroidBrowser; + } + get isBrowserOutdated() { + return !isAndroidBrowser && isBrowserOutdated && !this.storage.getBoolean('dismiss-outdated-browser'); + } + get leftMessage() { + return this.gameService.leftMessage; + } + get accountAlert() { + return this.model.accountAlert; + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(loaded => this.failedToLoadImages = !loaded); + } + play() { + if (this.canPlay) { + this.joining = true; + this.locked = true; + this.error = undefined; + + const delayTime = (!DEVELOPMENT && this.gameService.wasPlaying) ? 1500 : 10; + + delay(delayTime) // delay joing if user reloaded the game instead of leaving cleanly + .then(() => this.model.savePony(this.model.pony)) + .then(pony => this.joining ? this.gameService.join(pony.id) : Promise.resolve()) + .catch((e: RequestError) => { + if (!/^Cancelled/.test(e.message)) { + this.error = e.message; + + if (!includes(ignoredErrors, e.message) && !/shader/.test(e.message)) { + this.errorReporter.reportError(e, { status: e.status, text: e.text }); + } + + DEVELOPMENT && console.error(e); + } + }) + .finally(() => this.joining = false) + .then(() => delay(1500)) + .finally(() => this.locked = false); + } + } + cancel() { + this.gameService.leave('Cancelled joining'); + } + reload() { + location.reload(true); + } + hardReload() { + hardReload(); + } + hasFlag(server: ServerInfo) { + return server.countryFlags && server.countryFlags.length; + } + getIcon(server: ServerInfo) { + switch (server.flag) { + case 'star': return faStar; + case 'test': return faWrench; + default: return faGlobe; + } + } + dismissOutdatedBrowser() { + this.storage.setBoolean('dismiss-outdated-browser', true); + } + saveBirthdate() { + if (this.birthdate) { + this.model.updateAccount({ birthdate: this.birthdate }); + this.birthdateSet = true; + } + } +} diff --git a/src/ts/components/shared/play-notice/play-notice.pug b/src/ts/components/shared/play-notice/play-notice.pug new file mode 100644 index 0000000..1383d69 --- /dev/null +++ b/src/ts/components/shared/play-notice/play-notice.pug @@ -0,0 +1,4 @@ +.mx-auto.text-left.text-large(style="max-width: 400px;") + h5 General rules + ul.text-muted.list-rules + li(*ngFor="let r of rules") {{r}} diff --git a/src/ts/components/shared/play-notice/play-notice.ts b/src/ts/components/shared/play-notice/play-notice.ts new file mode 100644 index 0000000..77776cb --- /dev/null +++ b/src/ts/components/shared/play-notice/play-notice.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { supporterLink } from '../../../client/data'; +import { GENERAL_RULES } from '../../../common/constants'; + +@Component({ + selector: 'play-notice', + templateUrl: 'play-notice.pug', +}) +export class PlayNotice { + readonly patreonLink = supporterLink; + readonly rules = GENERAL_RULES; +} diff --git a/src/ts/components/shared/pony-box/pony-box.pug b/src/ts/components/shared/pony-box/pony-box.pug new file mode 100644 index 0000000..d1fdfa0 --- /dev/null +++ b/src/ts/components/shared/pony-box/pony-box.pug @@ -0,0 +1,63 @@ +.pony-box(*ngIf="pony") + .pony-box-tags + .pony-box-tag(*ngIf="special" [ngClass]="specialClass") + | {{special}} + .pony-box-tag.tag-friend(*ngIf="isFriend(pony)") + | friend + .pony-box-rect + .pony-box-name + | {{pony.name}} + .pony-box-buttons + site-info([site]="pony.site") + + .pony-box-buttons-box + .btn-group.btn-group-shadow.dropdown(dropdown (isOpenChange)="removingFriend = false") + button.btn.btn-xs.dropdown-toggle(dropdownToggle [ngClass]="ignoredOrHidden ? 'btn-danger' : 'btn-default'") + fa-icon([icon]="cogIcon") + + .dropdown-menu(*dropdownMenu) + button.dropdown-item((click)="toggleIgnore()") + fa-icon.mr-2([icon]="ignoreIcon" [fixedWidth]="true") + | {{isIgnored(pony) ? 'Unignore player' : 'Ignore player'}} + fa-icon.ml-1(*ngIf="isIgnored(pony)" [icon]="checkIcon") + button.dropdown-item((click)="hidePlayer(1)") + fa-icon.mr-2([icon]="hideIcon" [fixedWidth]="true") + | Hide player #[span.text-muted (24 hours)] + button.dropdown-item((click)="hidePlayer(0)") + fa-icon.mr-2([icon]="hideIcon" [fixedWidth]="true") + | Hide player #[span.text-muted (permanent)] + + .dropdown-divider + button.dropdown-item((click)="sendMessageTo()") + fa-icon.mr-2([icon]="messageIcon" [fixedWidth]="true") + | Send whisper + button.dropdown-item(*ngIf="!isFriend(pony)" (click)="addFriend()") + fa-icon.mr-2.text-success([icon]="addFriendIcon" [fixedWidth]="true") + | Send friend request + div(*ngIf="isFriend(pony)" style="position: relative;") + button.dropdown-item((click)="removingFriend = true; $event.stopPropagation()") + fa-icon.mr-2.text-danger([icon]="removeFriendIcon" [fixedWidth]="true") + | Remove from friends + .remove-confirmation.px-2.d-flex.justify-content-center(*ngIf="removingFriend") + button.btn.btn-sm.btn-outline-success.flex-grow-1((click)="removingFriend = false; $event.stopPropagation()") + | Cancel + button.btn.btn-sm.btn-outline-danger.flex-grow-1.ml-1((click)="removeFriend()") + | Confirm remove + + div(*ngIf="canInviteToSupporterServers") + h6.dropdown-header Supporter options + button.dropdown-item((click)="inviteToSupporterServers()") + fa-icon.mr-2.text-success([icon]="starIcon" [fixedWidth]="true") + | Invite to supporter servers + fa-icon.ml-1(*ngIf="isInvitedToSupporterServers" [icon]="checkIcon") + + .btn-group.btn-group-shadow(*ngIf="canInviteToParty || canRemoveFromParty || canPromoteToLeader") + button.btn.btn-xs.btn-default(*ngIf="canInviteToParty" (click)="inviteToParty()" tooltip="Invite to party") + fa-icon([icon]="inviteIcon") + button.btn.btn-xs.btn-default(*ngIf="canRemoveFromParty" (click)="removeFromParty()" tooltip="Remove from party") + fa-icon([icon]="removeIcon") + button.btn.btn-xs.btn-default(*ngIf="canPromoteToLeader" (click)="promoteToLeader()" tooltip="Promote to leader") + fa-icon([icon]="leaderIcon") + mod-box(*ngIf="isMod" [pony]="pony") + + portrait-box.pony-box-avatar([pony]="paletteInfo") diff --git a/src/ts/components/shared/pony-box/pony-box.scss b/src/ts/components/shared/pony-box/pony-box.scss new file mode 100644 index 0000000..8bed2db --- /dev/null +++ b/src/ts/components/shared/pony-box/pony-box.scss @@ -0,0 +1,113 @@ +@import '../../../../styles/partials/variables'; + +.pony-box { + color: #333; + position: relative; + width: 100px; + height: 100px; +} + +.pony-box-tags { + position: absolute; + top: -1px; + left: 50px; + display: flex; +} + +.pony-box-tag { + background: #777; + text-transform: uppercase; + color: white; + font-size: 11px; + font-weight: bold; + line-height: 14px; + padding: 1px 10px 2px 10px; + border-top-right-radius: 5px; + border-top-left-radius: 5px; + margin-left: 5px; + white-space: nowrap; + + &:first-child { + border-top-left-radius: 0; + padding-left: 40px; + margin-left: 0; + } + + &.tag-dev { + background: $dev; + } + + &.tag-mod { + background: $mod; + } + + &.tag-sup1 { + background: $supporter-1; + } + + &.tag-sup2 { + background: darken($supporter-2, 7%); + background: linear-gradient(to bottom, #ffcd99, #ffaa3b 10%, #c77305); + color: #3b2f1a; + } + + &.tag-sup3 { + background: darken($supporter-3, 7%); + background: linear-gradient(to bottom, #fffda4, #ffde3b 10%, #fdbb0b); + color: #49391c; + } + + &.tag-friend { + background: $friends-color; + } +} + +.pony-box-rect { + background: white; + position: absolute; + top: 15px; + left: 50px; + width: 250px; + height: 60px; + border-top-right-radius: 15px; + border-bottom-right-radius: 15px; + padding-left: 58px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.8); +} + +.pony-box-name { + font-size: 20px; + padding-top: 3px; + padding-right: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pony-box-buttons { + padding-top: 2px; + padding-right: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pony-box-buttons-box { + position: absolute; + top: 65px; + left: 48px; + white-space: nowrap; +} + +.pony-box-avatar { + position: absolute; +} + +.remove-confirmation { + background: white; + position: absolute; + left: 0; + top: 0; + right: 0; + height: 100%; +} diff --git a/src/ts/components/shared/pony-box/pony-box.ts b/src/ts/components/shared/pony-box/pony-box.ts new file mode 100644 index 0000000..87a17f6 --- /dev/null +++ b/src/ts/components/shared/pony-box/pony-box.ts @@ -0,0 +1,113 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { PlayerAction, Pony, EntityPlayerState, Entity } from '../../../common/interfaces'; +import { getPaletteInfo } from '../../../common/pony'; +import { Model } from '../../services/model'; +import { PonyTownGame } from '../../../client/game'; +import { + partyLeaderIcon, faUserPlus, faUserTimes, faCheck, faMicrophoneSlash, faEyeSlash, faStar, faUserMinus, + faUserCog, faComment +} from '../../../client/icons'; +import { DAY } from '../../../common/constants'; +import { isPonyInParty, isPartyLeader } from '../../../client/partyUtils'; +import { getTag } from '../../../common/tags'; +import { isIgnored, isHidden, isFriend } from '../../../common/entityUtils'; +import { setFlag } from '../../../common/utils'; + +@Component({ + selector: 'pony-box', + templateUrl: 'pony-box.pug', + styleUrls: ['pony-box.scss'], +}) +export class PonyBox { + readonly leaderIcon = partyLeaderIcon; + readonly inviteIcon = faUserPlus; + readonly removeIcon = faUserTimes; + readonly cogIcon = faUserCog; + readonly checkIcon = faCheck; + readonly ignoreIcon = faMicrophoneSlash; + readonly hideIcon = faEyeSlash; + readonly starIcon = faStar; + readonly addFriendIcon = faUserPlus; + readonly removeFriendIcon = faUserMinus; + readonly messageIcon = faComment; + isIgnored = isIgnored; + isFriend = isFriend; + removingFriend = false; + @Input() pony?: Pony; + @Output() sendMessage = new EventEmitter(); + constructor(private model: Model, private game: PonyTownGame) { + } + get ignoredOrHidden() { + return this.pony && (isIgnored(this.pony) || isHidden(this.pony)); + } + get isMod() { + return this.model.isMod; + } + get canInviteToParty() { + return this.pony && (!this.game.party || (isPartyLeader(this.game) && !isPonyInParty(this.game.party, this.pony, true))); + } + get canRemoveFromParty() { + return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, true); + } + get canPromoteToLeader() { + return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, false); + } + get special() { + const tag = getTag(this.pony && this.pony.tag); + return tag && tag.name; + } + get specialClass() { + const tag = getTag(this.pony && this.pony.tag); + return tag && tag.tagClass; + } + get paletteInfo() { + return this.pony && getPaletteInfo(this.pony); + } + inviteToParty() { + this.playerAction(PlayerAction.InviteToParty); + } + removeFromParty() { + this.playerAction(PlayerAction.RemoveFromParty); + } + promoteToLeader() { + this.playerAction(PlayerAction.PromotePartyLeader); + } + toggleIgnore() { + if (this.pony) { + const ignored = isIgnored(this.pony); + this.playerAction(ignored ? PlayerAction.Unignore : PlayerAction.Ignore); + this.pony.playerState = setFlag(this.pony.playerState, EntityPlayerState.Ignored, !ignored); + } + } + hidePlayer(days: number) { + this.playerAction(PlayerAction.HidePlayer, days * DAY); + } + addFriend() { + this.playerAction(PlayerAction.AddFriend); + } + removeFriend() { + this.playerAction(PlayerAction.RemoveFriend); + } + private playerAction(type: PlayerAction, param: any = undefined) { + const ponyId = this.pony && this.pony.id; + + if (ponyId) { + this.game.send(server => server.playerAction(ponyId, type, param)); + } + } + sendMessageTo() { + if (this.pony) { + this.sendMessage.emit(this.pony); + } + } + // supporter servers + get canInviteToSupporterServers() { + return false; // DEVELOPMENT; // TODO: check if ignored or hidden + } + get isInvitedToSupporterServers() { + return false; + } + inviteToSupporterServers() { + this.playerAction(PlayerAction.InviteToSupporterServers); + } +} diff --git a/src/ts/components/shared/portrait-box/portrait-box.pug b/src/ts/components/shared/portrait-box/portrait-box.pug new file mode 100644 index 0000000..53dd856 --- /dev/null +++ b/src/ts/components/shared/portrait-box/portrait-box.pug @@ -0,0 +1,3 @@ +.portrait-box([ngClass]="size") + canvas(#canvas) + .portrait-box-cover(*ngIf="!noBorder") diff --git a/src/ts/components/shared/portrait-box/portrait-box.scss b/src/ts/components/shared/portrait-box/portrait-box.scss new file mode 100644 index 0000000..9a53d9c --- /dev/null +++ b/src/ts/components/shared/portrait-box/portrait-box.scss @@ -0,0 +1,60 @@ +.portrait-box { + $small: 33px; + $medium: 66px; + $large: 100px; + background: #444; + border-radius: 50%; + position: relative; + + > canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + } + + &.small { + width: $small; + height: $small; + + > .portrait-box-cover { + width: $small + 4px; + height: $small + 4px; + border: solid 3px white; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.8); + } + } + + &.medium { + width: $medium; + height: $medium; + + > .portrait-box-cover { + width: $medium + 4px; + height: $medium + 4px; + border: solid 4px white; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.8); + } + } + + &.large { + width: $large; + height: $large; + + > .portrait-box-cover { + width: $large + 4px; + height: $large + 4px; + border: solid 6px white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.8); + } + } +} + +.portrait-box-cover { + position: absolute; + top: -2px; + left: -2px; + border-radius: 50%; +} diff --git a/src/ts/components/shared/portrait-box/portrait-box.ts b/src/ts/components/shared/portrait-box/portrait-box.ts new file mode 100644 index 0000000..327ba81 --- /dev/null +++ b/src/ts/components/shared/portrait-box/portrait-box.ts @@ -0,0 +1,86 @@ +import { + Component, Input, AfterViewInit, OnChanges, ElementRef, ChangeDetectionStrategy, ViewChild, NgZone +} from '@angular/core'; +import { PalettePonyInfo } from '../../../common/interfaces'; +import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import { createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvasWithRatio } from '../../../client/canvasUtils'; +import { defaultDrawPonyOptions, defaultPonyState } from '../../../client/ponyHelpers'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { drawPony } from '../../../client/ponyDraw'; +import { paletteSpriteSheet } from '../../../generated/sprites'; + +const scales: { [key: string]: number } = { + large: 3, + medium: 2, + small: 1, +}; + +const sizes: { [key: string]: number } = { + large: 100, + medium: 66, + small: 33, +}; + +const BUFFER_SIZE = 34; +const options = defaultDrawPonyOptions(); +const state = defaultPonyState(); + +@Component({ + selector: 'portrait-box', + templateUrl: 'portrait-box.pug', + styleUrls: ['portrait-box.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PortraitBox implements AfterViewInit, OnChanges { + @Input() noBorder = false; + @Input() flip = false; + @Input() size = 'large'; + @Input() pony: PalettePonyInfo | undefined = undefined; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + private frame = 0; + private batch?: ContextSpriteBatch; + constructor(private zone: NgZone) { + } + ngAfterViewInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + ngOnChanges() { + this.redraw(); + } + private redraw() { + this.frame = this.frame || this.zone.runOutsideAngular(() => requestAnimationFrame(() => { + this.frame = 0; + this.draw(); + })); + } + private draw() { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const size = sizes[this.size]; + resizeCanvasWithRatio(canvas, size, size); + + const context = canvas.getContext('2d'); + + if (context) { + context.save(); + context.fillStyle = '#444'; + context.fillRect(0, 0, canvas.width, canvas.height); + + if (this.pony) { + const scale = scales[this.size] * getPixelRatio(); + this.batch = this.batch || new ContextSpriteBatch(createCanvas(BUFFER_SIZE, BUFFER_SIZE)); + options.flipped = !this.flip; + + this.batch.start(paletteSpriteSheet, 0); + drawPony(this.batch, this.pony, state, 25, 54, options); + this.batch.end(); + + disableImageSmoothing(context); + context.scale(this.flip ? scale : -scale, scale); + context.drawImage(this.batch.canvas, this.flip ? 0 : -BUFFER_SIZE, 0); + } + + context.restore(); + } + } +} diff --git a/src/ts/components/shared/scale-picker/scale-picker.pug b/src/ts/components/shared/scale-picker/scale-picker.pug new file mode 100644 index 0000000..6513111 --- /dev/null +++ b/src/ts/components/shared/scale-picker/scale-picker.pug @@ -0,0 +1,5 @@ +.btn-group.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | ×{{scale}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let s of scales" (click)="setScale(s)") ×{{s}} diff --git a/src/ts/components/shared/scale-picker/scale-picker.ts b/src/ts/components/shared/scale-picker/scale-picker.ts new file mode 100644 index 0000000..8256373 --- /dev/null +++ b/src/ts/components/shared/scale-picker/scale-picker.ts @@ -0,0 +1,21 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { times } from 'lodash'; + +@Component({ + selector: 'scale-picker', + templateUrl: 'scale-picker.pug', +}) +export class ScalePicker { + @Input() scale = 1; + @Output() scaleChange = new EventEmitter(); + scales = [1, 2, 3, 4]; + @Input() set maxScale(value: number) { + this.scales = times(value, i => i + 1); + } + setScale(value: number) { + if (value !== this.scale) { + this.scale = value; + this.scaleChange.emit(value); + } + } +} diff --git a/src/ts/components/shared/set-selection/set-selection.pug b/src/ts/components/shared/set-selection/set-selection.pug new file mode 100644 index 0000000..da9debc --- /dev/null +++ b/src/ts/components/shared/set-selection/set-selection.pug @@ -0,0 +1,47 @@ +div(*ngIf="compact && !onlyPatterns") + .row.form-group + label.col-sm-4.col-form-label {{label}} + .col-sm-8 + sprite-selection( + [(selected)]="set.type" [sprites]="sprites" [fill]="set.fills" [outline]="set.outlines" [darken]="darken" + (selectedChange)="onChange()" [circle]="circle" [attr.aria-label]="'Select ' + label" emptyLabel="No {{label}}") + +div(*ngIf="!compact && !onlyPatterns") + .row.form-group + label.col-sm-12.text-center.col-form-label.text-muted {{label}} + .row.form-group + .col-sm-12 + sprite-selection( + [(selected)]="set.type" [sprites]="sprites" [fill]="set.fills" [outline]="set.outlines" [darken]="darken" + (selectedChange)="onChange()" [circle]="circle" [attr.aria-label]="'Select ' + label" emptyLabel="No {{label}}") + +div(*ngIf="showColorPatterns && !onlyPatterns") + .row.form-group + label.col-sm-12.text-center.col-form-label.text-muted Color pattern + .row.form-group + .col-sm-12 + sprite-selection( + [(selected)]="set.pattern" [sprites]="sets[set.type]" [fill]="exampleFills" [outline]="exampleOutlines" + (selectedChange)="onChange()" aria-label="Select color pattern" [darken]="darken") + +div(*ngIf="onlyPatterns") + .row.form-group + label.col-sm-4.col-form-label {{label}} + .col-sm-8 + sprite-selection( + [(selected)]="set.pattern" [sprites]="sets[set.type]" [fill]="exampleFills" [outline]="exampleOutlines" + (selectedChange)="onChange()" aria-label="Select color pattern" [darken]="darken") + +div(*ngFor="let c of exampleFills; let i = index") + fill-outline( + *ngIf="patternColors > i" + label="Color {{i + 1}}" + [base]="i === 0 ? base : set.fills[0]" + [nonLockable]="i === 0 && nonLockable" + [outlineHidden]="isOutlineHidden" + [indicatorColor]="showColorPatterns ? c : ''" + [(fill)]="set.fills[i]" + [(locked)]="set.lockFills[i]" + [(outline)]="set.outlines[i]" + [(outlineLocked)]="set.lockOutlines[i]" + (change)="onChange()") diff --git a/src/ts/components/shared/set-selection/set-selection.scss b/src/ts/components/shared/set-selection/set-selection.scss new file mode 100644 index 0000000..834e0a1 --- /dev/null +++ b/src/ts/components/shared/set-selection/set-selection.scss @@ -0,0 +1,3 @@ +.col-form-label.col-sm-4 { + padding-top: 16px; +} diff --git a/src/ts/components/shared/set-selection/set-selection.ts b/src/ts/components/shared/set-selection/set-selection.ts new file mode 100644 index 0000000..1906a0e --- /dev/null +++ b/src/ts/components/shared/set-selection/set-selection.ts @@ -0,0 +1,73 @@ +import { Component, Input, Output, EventEmitter, OnChanges, Directive, Optional } from '@angular/core'; +import { clamp } from 'lodash'; +import { SpriteSet, ColorExtraSets, ColorExtraSet } from '../../../common/interfaces'; +import { getColorCount } from '../../../client/spriteUtils'; + +const FILLS = ['Orange', 'DodgerBlue', 'LimeGreen', 'Orchid', 'crimson', 'Aquamarine']; +const OUTLINES = ['Chocolate', 'SteelBlue', 'ForestGreen', 'DarkOrchid', 'darkred', 'DarkTurquoise']; + +@Directive({ + selector: '[setOutlineHidden]', +}) +export class SetOutlineHidden { + @Input() setOutlineHidden = false; +} + +@Component({ + selector: 'set-selection', + templateUrl: 'set-selection.pug', + styleUrls: ['set-selection.scss'], +}) +export class SetSelection implements OnChanges { + readonly exampleFills = FILLS; + readonly exampleOutlines = OUTLINES; + @Input() label?: string; + @Input() base?: string; + @Input() set?: SpriteSet; + @Input() sets?: ColorExtraSets; + @Input() sprites?: ColorExtraSet; + @Input() circle?: string; + @Input() outlineHidden = false; + @Input() nonLockable = false; + @Input() compact = false; + @Input() onlyPatterns = false; + @Input() darken = true; + @Output() change = new EventEmitter(); + constructor(@Optional() private hidden: SetOutlineHidden) { + } + get isOutlineHidden() { + return this.hidden ? this.hidden.setOutlineHidden : this.outlineHidden; + } + get patternColors() { + const set = this.getSet(); + const pat = this.set && set && set[this.set.pattern || 0]; + + if (pat && !pat.colors) { + return 0; + } else if (pat) { + return getColorCount(pat); + } else { + return this.nonLockable ? 1 : 0; + } + } + get showColorPatterns(): boolean { + const type = this.set && this.set.type || 0; + const set = this.sets && this.sets[type]; + return !!set && set.length > 1; + } + ngOnChanges() { + this.sprites = this.sets ? this.sets.map(s => s ? s[0] : undefined) : undefined; + } + onChange() { + const set = this.getSet(); + + if (this.set && set) { + this.set.pattern = clamp(this.set.pattern || 0, 0, set.length - 1); + } + + this.change.emit(); + } + private getSet(): ColorExtraSet | undefined { + return this.set && this.sets && this.sets[this.set.type || 0]; + } +} diff --git a/src/ts/components/shared/settings-box/settings-box.pug b/src/ts/components/shared/settings-box/settings-box.pug new file mode 100644 index 0000000..4852f37 --- /dev/null +++ b/src/ts/components/shared/settings-box/settings-box.pug @@ -0,0 +1,49 @@ +ng-template(#settingsModal) + settings-modal((close)="modalRef.hide()" [focusTrap]="true" (keydown)="$event.stopPropagation()") + +ng-template(#invitesModal) + invites-modal((close)="modalRef.hide()" [focusTrap]="true" (keydown)="$event.stopPropagation()") + +ng-template(#actionsModal) + actions-modal((close)="modalRef.hide()" [focusTrap]="true" (keydown)="$event.stopPropagation()") + +.settings-box.unselectable + .dropdown(#dropdown="ag-dropdown" dropdown autoClose="outsideClick" [hookToCanvas]="true") + button.game-button.dropdown-toggle.no-arrow(dropdownToggle (click)="false" title="Settings") + fa-icon([icon]="cogIcon" [fixedWidth]="true") + .dropdown-menu.dropdown-menu-right.settings-box-menu(*dropdownMenu) + .dropdown-header.d-flex.justify-content-between + span {{server}} + span.clock-display {{time}} + .dropdown-item.d-flex + fa-icon.mr-2([icon]="searchIcon" [fixedWidth]="true") + .flex-grow-1 Scale (x{{scale}}) + button.btn.btn-xs.btn-outline-secondary((click)="zoomOut()" aria-label="Zoom out") + fa-icon([icon]="minusIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-outline-secondary.ml-2((click)="zoomIn()" aria-label="Zoom in") + fa-icon([icon]="plusIcon" [fixedWidth]="true") + .dropdown-item.dropdown-item-static + a((click)="toggleVolume()" title="Toggle sound") + fa-icon([icon]="volumeIcon" [fixedWidth]="true") + slider-bar([(value)]="volume" [step]="1" (started)="volumeStarted()") + .text-muted.text-right(*ngIf="track" style="margin-top: -5px;") + small + | playing: #[b {{track}}] + a.text-muted.ml-1((click)="nextTrack()" title="Next track") + fa-icon([icon]="forwardIcon" [fixedWidth]="true") + a.dropdown-item((click)="unhideAllHiddenPlayers()") + fa-icon.mr-2([icon]="emptyIcon" [fixedWidth]="true") + | Unhide players (temporary) + a.dropdown-item((click)="openSettings()") + fa-icon.mr-2([icon]="emptyIcon" [fixedWidth]="true") + | Settings + a.dropdown-item((click)="openActions()") + fa-icon.mr-2([icon]="emptyIcon" [fixedWidth]="true") + | Actions + //-a.dropdown-item((click)="openInvites()" *ngIf="hasInvites") + fa-icon.mr-2([icon]="emptyIcon" [fixedWidth]="true") + | Manage invites + .dropdown-divider + a.dropdown-item((click)="leave()") + fa-icon.mr-2([icon]="signOutIcon" [fixedWidth]="true") + | Leave diff --git a/src/ts/components/shared/settings-box/settings-box.scss b/src/ts/components/shared/settings-box/settings-box.scss new file mode 100644 index 0000000..9b110a8 --- /dev/null +++ b/src/ts/components/shared/settings-box/settings-box.scss @@ -0,0 +1,17 @@ +@import '../../../../styles/partials/variables'; +@import '../../../../styles/partials/mixins'; + +.settings-box { + > div { + display: inline-block; + } + + > .dropdown { + padding-bottom: 5px; + } +} + +.settings-box-menu { + @include nipple(9px); + min-width: 205px; +} diff --git a/src/ts/components/shared/settings-box/settings-box.ts b/src/ts/components/shared/settings-box/settings-box.ts new file mode 100644 index 0000000..d8ca7c3 --- /dev/null +++ b/src/ts/components/shared/settings-box/settings-box.ts @@ -0,0 +1,131 @@ +import { Component, OnInit, OnDestroy, NgZone, ViewChild, TemplateRef } from '@angular/core'; +import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; +import { distinctUntilChanged } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { Action } from '../../../common/interfaces'; +import { GameService } from '../../services/gameService'; +import { Model } from '../../services/model'; +import { PonyTownGame } from '../../../client/game'; +import { Dropdown } from '../directives/dropdown'; +import { + emptyIcon, faCog, faSearch, faSignOutAlt, faStepForward, faVolumeOff, faVolumeUp, faVolumeDown, faPlus, faMinus +} from '../../../client/icons'; +import { SettingsService } from '../../services/settingsService'; +import { Audio } from '../../services/audio'; + +@Component({ + selector: 'settings-box', + templateUrl: 'settings-box.pug', + styleUrls: ['settings-box.scss'], +}) +export class SettingsBox implements OnInit, OnDestroy { + readonly cogIcon = faCog; + readonly searchIcon = faSearch; + readonly signOutIcon = faSignOutAlt; + readonly forwardIcon = faStepForward; + readonly emptyIcon = emptyIcon; + readonly plusIcon = faPlus; + readonly minusIcon = faMinus; + modalRef?: BsModalRef; + time?: string; + @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; + @ViewChild('actionsModal', { static: true }) actionsModal!: TemplateRef; + @ViewChild('settingsModal', { static: true }) settingsModal!: TemplateRef; + @ViewChild('invitesModal', { static: true }) invitesModal!: TemplateRef; + private subscription?: Subscription; + constructor( + private model: Model, + private modalService: BsModalService, + private settingsService: SettingsService, + private gameService: GameService, + private game: PonyTownGame, + private audio: Audio, + private zone: NgZone, + ) { + } + get scale() { + return this.game.scale; + } + get volume() { + return this.game.volume; + } + set volume(value: number) { + this.settingsService.browser.volume = value; + this.settingsService.saveBrowserSettings(); + this.audio.setVolume(value); + } + get server() { + return this.gameService.server && this.gameService.server.name || ''; + } + get settings() { + return this.model.account && this.model.account.settings || {}; + } + get track() { + return this.game.audio.trackName; + } + get volumeIcon() { + return this.volume === 0 ? faVolumeOff : (this.volume < 50 ? faVolumeDown : faVolumeUp); + } + get isMod() { + return this.model.isMod; + } + get hasInvites() { + return this.isMod; // TEMP + } + ngOnInit() { + this.game.onClock + .pipe( + distinctUntilChanged(), + ) + .subscribe(text => { + if (this.dropdown.isOpen) { + this.zone.run(() => this.time = text); + } else { + this.time = text; + } + }); + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + toggleVolume() { + this.volume = this.volume === 0 ? 50 : 0; + } + volumeStarted() { + this.game.audio.forcePlay(); + } + nextTrack() { + this.game.audio.playRandomTrack(); + } + leave() { + this.gameService.leave('From settings dropdown'); + this.dropdown.close(); + } + zoomOut() { + this.game.zoomOut(); + } + zoomIn() { + this.game.zoomIn(); + } + unhideAllHiddenPlayers() { + this.game.send(server => server.action(Action.UnhideAllHiddenPlayers)); + this.dropdown.close(); + } + openModal(template: TemplateRef) { + this.modalRef = this.modalService.show(template, { ignoreBackdropClick: true }); + } + openSettings() { + this.openModal(this.settingsModal); + this.dropdown.close(); + } + openActions() { + this.openModal(this.actionsModal); + this.dropdown.close(); + } + openInvites() { + if (BETA) { + this.openModal(this.invitesModal); + this.dropdown.close(); + } + } +} diff --git a/src/ts/components/shared/settings-modal/settings-modal.pug b/src/ts/components/shared/settings-modal/settings-modal.pug new file mode 100644 index 0000000..73fe27c --- /dev/null +++ b/src/ts/components/shared/settings-modal/settings-modal.pug @@ -0,0 +1,132 @@ +.modal-header.d-none.d-sm-block.d-none-for-low-height + h4.modal-title(labelledBy=".modal") + | Settings + +.modal-body.modal-checkboxes + tabset(type="pills") + tab(title="Game" [icon]="gameIcon") + div(*tabContent) + .form-group + custom-checkbox( + [(checked)]="account.ignorePartyInvites" + help="Useful when other players bother you with unwanted party invites (does not apply to friends)") + | Ignore party invites + .form-group + custom-checkbox( + [(checked)]="account.ignoreFriendInvites" + help="Useful when other players bother you with unwanted friend requests") + | Ignore friend requests + .form-group + custom-checkbox( + [(checked)]="account.seeThroughObjects" + help="Makes all obstacles transparent") + | See through obstacles + .form-group + custom-checkbox( + [(checked)]="browser.walkByDefault" + help="Uses walking speed by default and run when pressing shift key") + | Prefer walking to running + + tab(title="Graphics" [icon]="graphicsIcon") + div(*tabContent) + .form-group + custom-checkbox( + *ngIf="!lockLowGraphicsMode" [(checked)]="browser.lowGraphicsMode" + help="Can improve game performance in some cases") + | Low graphics mode + custom-checkbox( + *ngIf="lockLowGraphicsMode" [checked]="true" [disabled]="true" + help="Can improve game performance in some cases") + | Low graphics mode + .form-group + custom-checkbox( + [(checked)]="browser.brightNight" + help="Increases brightness of the game at night and in dark areas") + | Brighter darkness + .form-group + custom-checkbox( + [(checked)]="browser.powerSaving" + help="Reduces frame rate to extend battery life and reduce CPU overheating") + | Power saving mode + .form-group + custom-checkbox( + [(checked)]="browser.showStats" + help="Show rendering and network stats") + | Show statistics + .form-group + custom-checkbox( + [(checked)]="browser.showFps" + help="Show FPS counter") + | Show FPS + + tab(title="Chat" [icon]="chatIcon") + div(*tabContent) + .form-group + custom-checkbox( + [(checked)]="account.ignorePublicChat" + help="Don't show any public chat messages, only party chat and whispes will be visible") + | Hide all public chat messages + .form-group + custom-checkbox( + [(checked)]="account.ignoreNonFriendWhispers" + help="Don't show any whispers from players who are not on your friends list") + | Only allow whispers from friends + .form-group + label#chatlog-opacity-label Chatlog background opacity + div + slider-bar.mr-2( + [(value)]="account.chatlogOpacity" style="max-width: 300px" labelledBy="chatlog-opacity-label") + | {{(account.chatlogOpacity / 100) | percent}} + small.form-text.text-muted.mt-0 Changes chatlog background to make it more readable or less obtrusive. + .form-group + label#chatlog-range-label Chatlog range + div + slider-bar.mr-2( + [(value)]="account.chatlogRange" [min]="minChatlogRange" [max]="maxChatlogRange" [step]="1" + (valueChange)="updateChatlogRange($event)" (changed)="finishChatlogRange()" + style="max-width: 300px" labelledBy="chatlog-range-label") + | {{chatlogRangeText}} + small.form-text.text-muted.mt-0 + | Only show messages in chatlog from players near you (doesn't affect party chat or whispers). + + tab(title="Filters" [icon]="filtersIcon") + div(*tabContent) + .form-group + custom-checkbox( + [(checked)]="account.filterCyrillic" + help="Hide all messages containing russian alphabet") + | Filter russian chat + .form-group + custom-checkbox( + [(checked)]="account.filterSwearWords" + help="Censors swear words and other harmful phrases in the chat") + | Filter bad words + .form-group + label.mb-1 Custom chat filter + small.form-text.text-muted.mt-0.mb-2 + | Space separated list of words to filter. + | All chat messages containing listed words will be hidden. + | Does not affect party chat or whispers from friends. + textarea.form-control(rows="5" [(ngModel)]="account.filterWords") + + tab(title="Controls" [icon]="controlsIcon") + div(*tabContent) + .form-group + custom-checkbox( + [(checked)]="browser.disableGamepad" + help="Useful if gamepad controls are causing issues with the game") + | Disable gamepad controls + .form-group + custom-checkbox( + [(checked)]="browser.disableFKeys" + help="Useful if you're using function keys in a way that conflicts with game controls") + | Disable function keys (F1-F12) + +.modal-footer.d-flex + button.btn.btn-outline-secondary.d-none.d-sm-block((click)="reset()") + | Reset + .flex-grow-1 + button.btn.btn-outline-secondary((click)="cancel()") + | Cancel + button.btn.btn-outline-secondary.ml-2((click)="ok()") + | OK diff --git a/src/ts/components/shared/settings-modal/settings-modal.scss b/src/ts/components/shared/settings-modal/settings-modal.scss new file mode 100644 index 0000000..17272fc --- /dev/null +++ b/src/ts/components/shared/settings-modal/settings-modal.scss @@ -0,0 +1,12 @@ +@import '../../../../styles/partials/variables'; + +.modal-body { + min-height: 450px; + font-size: 16px; +} + +.modal-footer { + .btn { + min-width: 100px; + } +} diff --git a/src/ts/components/shared/settings-modal/settings-modal.ts b/src/ts/components/shared/settings-modal/settings-modal.ts new file mode 100644 index 0000000..48ecf7f --- /dev/null +++ b/src/ts/components/shared/settings-modal/settings-modal.ts @@ -0,0 +1,122 @@ +import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { AccountSettings, BrowserSettings } from '../../../common/interfaces'; +import { SettingsService } from '../../services/settingsService'; +import { + DEFAULT_CHATLOG_OPACITY, MAX_CHATLOG_RANGE, MIN_CHATLOG_RANGE, isChatlogRangeUnlimited, MAX_FILTER_WORDS_LENGTH +} from '../../../common/constants'; +import { StorageService } from '../../services/storageService'; +import { cloneDeep } from '../../../common/utils'; +import { PonyTownGame } from '../../../client/game'; +import { updateRangeIndicator } from '../../../client/clientUtils'; +import { faSlidersH, faCommentSlash, faGamepad, faImage } from '../../../client/icons'; + +@Component({ + selector: 'settings-modal', + templateUrl: 'settings-modal.pug', + styleUrls: ['settings-modal.scss'], +}) +export class SettingsModal implements OnInit, OnDestroy { + readonly maxChatlogRange = MAX_CHATLOG_RANGE; + readonly minChatlogRange = MIN_CHATLOG_RANGE; + readonly gameIcon = faSlidersH; + readonly chatIcon = faCommentSlash; + readonly filtersIcon = faCommentSlash; + readonly controlsIcon = faGamepad; + readonly graphicsIcon = faImage; + @Output() close = new EventEmitter(); + account: AccountSettings = {}; + browser: BrowserSettings = {}; + accountBackup: AccountSettings = {}; + browserBackup: BrowserSettings = {}; + private done = false; + private subscription?: Subscription; + constructor( + private settingsService: SettingsService, + private storage: StorageService, + private game: PonyTownGame, + ) { + } + get pane() { + return this.storage.getItem('settings-modal-pane') || 'game'; + } + set pane(value: string) { + this.storage.setItem('settings-modal-pane', value); + } + get lockLowGraphicsMode() { + return this.game.failedFBO; + } + get chatlogRangeText() { + const range = this.account.chatlogRange; + return isChatlogRangeUnlimited(range) ? 'entire screen' : `${range} tiles`; + } + ngOnInit() { + this.accountBackup = cloneDeep(this.settingsService.account); + this.browserBackup = cloneDeep(this.settingsService.browser); + this.account = this.settingsService.account; + this.browser = this.settingsService.browser; + this.setupDefaults(); + this.subscription = this.game.onLeft.subscribe(() => this.cancel()); + } + ngOnDestroy() { + this.finishChatlogRange(); + + if (!this.done) { + this.cancel(); + } + + this.subscription && this.subscription.unsubscribe(); + } + reset() { + this.account = this.settingsService.account = {}; + this.browser = this.settingsService.browser = {}; + this.setupDefaults(); + } + cancel() { + this.done = true; + this.settingsService.account = this.accountBackup; + this.settingsService.browser = this.browserBackup; + this.close.emit(); + } + ok() { + if (this.account.filterWords) { + let filter = this.account.filterWords; + + while (filter.length > MAX_FILTER_WORDS_LENGTH && /\s/.test(filter)) { + filter = filter.trim().replace(/\s+\S+$/, ''); + } + + if (filter.length > MAX_FILTER_WORDS_LENGTH) { + this.account.filterWords = ''; + } else { + this.account.filterWords = filter; + } + } + + this.done = true; + this.settingsService.saveAccountSettings(this.account); + this.settingsService.saveBrowserSettings(this.browser); + this.close.emit(); + } + updateChatlogRange(range: number | undefined) { + document.body.classList.add('translucent-modals'); + updateRangeIndicator(range, this.game); + } + finishChatlogRange() { + document.body.classList.remove('translucent-modals'); + updateRangeIndicator(undefined, this.game); + } + private setupDefaults() { + if (this.account.chatlogOpacity === undefined) { + this.account.chatlogOpacity = DEFAULT_CHATLOG_OPACITY; + } + + if (this.account.chatlogRange === undefined) { + this.account.chatlogRange = MAX_CHATLOG_RANGE; + } + + if (this.account.filterWords === undefined) { + this.account.filterWords = ''; + } + } +} diff --git a/src/ts/components/shared/shared.module.ts b/src/ts/components/shared/shared.module.ts new file mode 100644 index 0000000..9bc1180 --- /dev/null +++ b/src/ts/components/shared/shared.module.ts @@ -0,0 +1,161 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +// import { ScrollingModule } from '@angular/cdk/scrolling'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { PopoverModule } from 'ngx-bootstrap/popover'; +import { ButtonsModule } from 'ngx-bootstrap/buttons'; +import { ModalModule } from 'ngx-bootstrap/modal'; + +import { ActionBar } from './action-bar/action-bar'; +import { ActionButton } from './action-button/action-button'; +import { ActionsModal } from './actions-modal/actions-modal'; +import { BitmapBox } from './bitmap-box/bitmap-box'; +import { ButtMarkEditor } from './butt-mark-editor/butt-mark-editor'; +import { MenuItem } from './menu-item/menu-item'; +import { MenuBar } from './menu-bar/menu-bar'; +import { CharacterList } from './character-list/character-list'; +import { CharacterPreview } from './character-preview/character-preview'; +import { CharacterSelect } from './character-select/character-select'; +import { EmoteBox } from './emote-box/emote-box'; +import { SliderBar } from './slider-bar/slider-bar'; +import { SpriteBox } from './sprite-box/sprite-box'; +import { SpriteSelection } from './sprite-selection/sprite-selection'; +import { SupportButton } from './support-button/support-button'; +import { SupporterPony } from './supporter-pony/supporter-pony'; +import { SetSelection, SetOutlineHidden } from './set-selection/set-selection'; +import { CheckBox } from './check-box/check-box'; +import { PortraitBox } from './portrait-box/portrait-box'; +import { ScalePicker } from './scale-picker/scale-picker'; +import { ColorPicker } from './color-picker/color-picker'; +import { CustomCheckbox } from './custom-checkbox/custom-checkbox'; +import { DatePicker } from './date-picker/date-picker'; +import { SignInBox } from './sign-in-box/sign-in-box'; +import { PonyBox } from './pony-box/pony-box'; +import { ModBox } from './mod-box/mod-box'; +import { PartyBox } from './party-box/party-box'; +import { PartyList } from './party-list/party-list'; +import { SiteInfo } from './site-info/site-info'; +import { SettingsBox } from './settings-box/settings-box'; +import { SettingsModal } from './settings-modal/settings-modal'; +import { InvitesModal } from './invites-modal/invites-modal'; +import { FillOutline } from './fill-outline/fill-outline'; +import { FriendsBox } from './friends-box/friends-box'; +import { InstallButton } from './install-button/install-button'; +import { KbdKey } from './kbd-key/kbd-key'; +import { PlayBox } from './play-box/play-box'; +import { ChatBox } from './chat-box/chat-box'; +import { ChatLog } from './chat-log/chat-log'; +import { SwapBox } from './swap-box/swap-box'; +import { SiteLinks } from './site-links/site-links'; +import { NotificationItem } from './notification/notification-item'; +import { NotificationList } from './notification/notification-list'; +import { tabsetComponents } from './tabset/tabset'; +import { PlayNotice } from './play-notice/play-notice'; +import { PageLoader } from './page-loader/page-loader'; +import { virtualListDirectives, VirtualList } from './virtual-list/virtual-list'; + +import { Anchor } from './directives/anchor'; +import { BtnHighlight, BtnHighlightDanger } from './directives/btnHighlight'; +import { draggableComponents } from './directives/draggable'; +import { AgDrag } from './directives/agDrag'; +import { AgAutoFocus } from './directives/agAutoFocus'; +import { LinkCurrent } from './directives/linkCurrent'; +import { LabelledBy } from './directives/labelledBy'; +import { RevSrc } from './directives/revSrc'; +import { FixToTop } from './directives/fixToTop'; +import { FocusTitle } from './directives/focusTitle'; +import { FocusTrap } from './directives/focusTrap'; +import { HasFeature } from './directives/hasFeature'; +import { dropdownDirectives } from './directives/dropdown'; +import { SaveActiveTab } from './directives/saveActiveTab'; + +import { SiteNamePipe } from './pipes/siteName'; + +const declarations = [ + ActionBar, + ActionButton, + ActionsModal, + BitmapBox, + ButtMarkEditor, + MenuBar, + MenuItem, + CharacterList, + CharacterPreview, + CharacterSelect, + EmoteBox, + SliderBar, + SpriteBox, + SpriteSelection, + SupportButton, + SupporterPony, + SetSelection, + SetOutlineHidden, + CheckBox, + PortraitBox, + ScalePicker, + SwapBox, + ColorPicker, + CustomCheckbox, + DatePicker, + SignInBox, + PonyBox, + ModBox, + PartyBox, + PartyList, + SiteInfo, + SettingsBox, + SettingsModal, + FillOutline, + FriendsBox, + InstallButton, + InvitesModal, + KbdKey, + PlayBox, + PlayNotice, + PageLoader, + ChatBox, + ChatLog, + SiteLinks, + NotificationItem, + NotificationList, + ...dropdownDirectives, + ...tabsetComponents, + ...draggableComponents, + ...virtualListDirectives, + VirtualList, + Anchor, + BtnHighlight, + BtnHighlightDanger, + AgDrag, + AgAutoFocus, + LinkCurrent, + LabelledBy, + RevSrc, + FixToTop, + FocusTitle, + FocusTrap, + HasFeature, + SaveActiveTab, + SiteNamePipe, +]; + +@NgModule({ + imports: [ + BrowserModule, + RouterModule, + FormsModule, + TooltipModule.forRoot(), + PopoverModule, + ButtonsModule, + ModalModule.forRoot(), + FontAwesomeModule, + // ScrollingModule, + ], + declarations: declarations, + exports: declarations, +}) +export class SharedModule { +} diff --git a/src/ts/components/shared/sign-in-box/sign-in-box.pug b/src/ts/components/shared/sign-in-box/sign-in-box.pug new file mode 100644 index 0000000..fea2342 --- /dev/null +++ b/src/ts/components/shared/sign-in-box/sign-in-box.pug @@ -0,0 +1,43 @@ +.sign-in-box.mx-auto.text-center + p.lead + | Sign in with your social site account + + div(*ngIf="signUpProviders.length") + .sign-in-heading + span(aria-hidden="true") Sign in #[i or] sign up + + .sign-in-box-providers + button.btn.btn-lg.btn-dark( + *ngFor="let p of signUpProviders" + (click)="signInTo(p)" + [class.disabled]="p.disabled" + [style.border-bottom-color]="p.color" + title="Sign in using {{p.name}}") + fa-icon([icon]="icon(p.id)" [fixedWidth]="true" size="lg") + .sr-only Sign in or sign up using {{p.name}} + + div(*ngIf="signInProviders.length") + .sign-in-heading + span(aria-hidden="true") Sign in #[i only] + + .text-muted.mb-3 (You #[b cannot] use these social sites to create new account) + + .sign-in-box-providers.sign-in-only + button.btn.btn-lg.btn-dark( + *ngFor="let p of signInProviders" + (click)="signInTo(p)" + [class.disabled]="p.disabled" + [style.border-bottom-color]="p.color" + title="Sign in using {{p.name}}") + fa-icon([icon]="icon(p.id)" [fixedWidth]="true" size="lg") + .sr-only Sign in using {{p.name}} + + div(*ngIf="local") + .btn-group.dropdown(dropdown) + button.btn.btn-lg.btn-dark.dropdown-toggle(dropdownToggle) + | Mock login + .dropdown-menu(*dropdownMenu) + //- NOTE: change usernames to ids of accounts you want to use for testing + a.dropdown-item(href="/auth/local?password=x&username=57a3dc6f2f0019a161cdebf6" target="_self") Admin + a.dropdown-item(href="/auth/local?password=x&username=57ae2332a67f4dc52e123e14" target="_self") Test + a.dropdown-item(href="/auth/local?password=x&username=57ae2332a67f4dc52e123e15" target="_self") Other diff --git a/src/ts/components/shared/sign-in-box/sign-in-box.scss b/src/ts/components/shared/sign-in-box/sign-in-box.scss new file mode 100644 index 0000000..0e87495 --- /dev/null +++ b/src/ts/components/shared/sign-in-box/sign-in-box.scss @@ -0,0 +1,55 @@ +@import '../../../../styles/partials/variables'; +@import '../../../../styles/partials/mixins'; + +.sign-in-box { + max-width: 400px; + margin-top: 50px; + + .lead { + margin-bottom: 40px; + } +} + +.sign-in-heading { + color: lighten($text-muted, 10%); + margin-bottom: 20px; + border-bottom: solid 1px darken($text-muted, 20%); + text-transform: uppercase; + text-align: center; + line-height: 0; + + > span { + background: $body-bg; + padding: 0 15px; + + > i { + color: darken($text-muted, 10%); + margin: 0 5px; + text-transform: lowercase; + } + } +} + +.sign-in-box-providers { + max-width: 350px; + margin: auto; + margin-bottom: 40px; + + > .btn-dark { + border: none; + border-bottom: solid 5px #666; + margin-right: 5px; + margin-bottom: 5px; + min-width: 80px; + + &.disabled { + border-bottom-color: #666 !important; + } + } +} + +.sign-in-only { + button { + opacity: 0.5; + } +} diff --git a/src/ts/components/shared/sign-in-box/sign-in-box.ts b/src/ts/components/shared/sign-in-box/sign-in-box.ts new file mode 100644 index 0000000..e08e79e --- /dev/null +++ b/src/ts/components/shared/sign-in-box/sign-in-box.ts @@ -0,0 +1,26 @@ +import { Component, Output, EventEmitter } from '@angular/core'; +import { signUpProviders, signInProviders, local } from '../../../client/data'; +import { emptyIcon, oauthIcons } from '../../../client/icons'; +import { OAuthProvider } from '../../../common/interfaces'; + +export function getProviderIcon(id: string) { + return oauthIcons[id] || emptyIcon; +} + +@Component({ + selector: 'sign-in-box', + templateUrl: 'sign-in-box.pug', + styleUrls: ['sign-in-box.scss'], +}) +export class SignInBox { + readonly signUpProviders = signUpProviders; + readonly signInProviders = signInProviders; + readonly local = local || DEVELOPMENT; + @Output() signIn = new EventEmitter(); + icon(id: string) { + return getProviderIcon(id); + } + signInTo(provider: OAuthProvider) { + this.signIn.emit(provider); + } +} diff --git a/src/ts/components/shared/site-info/site-info.pug b/src/ts/components/shared/site-info/site-info.pug new file mode 100644 index 0000000..cdb8969 --- /dev/null +++ b/src/ts/components/shared/site-info/site-info.pug @@ -0,0 +1,3 @@ +a.site-info(*ngIf="info" [href]="info.url" target="_blank" rel="noopener noreferrer") + fa-icon.mr-1([icon]="icon" [fixedWidth]="true" [style.color]="info.color" size="lg") + b {{info.name}} diff --git a/src/ts/components/shared/site-info/site-info.scss b/src/ts/components/shared/site-info/site-info.scss new file mode 100644 index 0000000..8631b97 --- /dev/null +++ b/src/ts/components/shared/site-info/site-info.scss @@ -0,0 +1,10 @@ +.site-info { + color: #333; + opacity: 0.7; + white-space: nowrap; + + &:hover, &:active, &:focus { + color: #555; + text-decoration: none; + } +} diff --git a/src/ts/components/shared/site-info/site-info.ts b/src/ts/components/shared/site-info/site-info.ts new file mode 100644 index 0000000..a1043a3 --- /dev/null +++ b/src/ts/components/shared/site-info/site-info.ts @@ -0,0 +1,19 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { SocialSite, SocialSiteInfo } from '../../../common/interfaces'; +import { toSocialSiteInfo } from '../../../client/clientUtils'; +import { getProviderIcon } from '../sign-in-box/sign-in-box'; + +@Component({ + selector: 'site-info', + templateUrl: 'site-info.pug', + styleUrls: ['site-info.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SiteInfo { + info?: SocialSiteInfo; + icon: any; + @Input() set site(value: SocialSite | undefined) { + this.info = value && toSocialSiteInfo(value); + this.icon = getProviderIcon(this.info && this.info.icon || ''); + } +} diff --git a/src/ts/components/shared/site-links/site-links.pug b/src/ts/components/shared/site-links/site-links.pug new file mode 100644 index 0000000..a93594f --- /dev/null +++ b/src/ts/components/shared/site-links/site-links.pug @@ -0,0 +1,4 @@ +span(*ngFor="let l of links; let i = index") + span(*ngIf="i > 0") | + a([href]="l" target="_blank" rel="noopener noreferrer") + | {{l | siteName}} diff --git a/src/ts/components/shared/site-links/site-links.ts b/src/ts/components/shared/site-links/site-links.ts new file mode 100644 index 0000000..ce87c67 --- /dev/null +++ b/src/ts/components/shared/site-links/site-links.ts @@ -0,0 +1,10 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; + +@Component({ + selector: 'site-links', + templateUrl: 'site-links.pug', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SiteLinks { + @Input() links: string[] = []; +} diff --git a/src/ts/components/shared/slider-bar/slider-bar.pug b/src/ts/components/shared/slider-bar/slider-bar.pug new file mode 100644 index 0000000..160bf64 --- /dev/null +++ b/src/ts/components/shared/slider-bar/slider-bar.pug @@ -0,0 +1,5 @@ +.slider-bar(#bar [class.disabled]="disabled" (agDrag)="drag($event)" agDragRelative="self") + .slider-bar-fill([style.width.%]="width") + .slider-bar-line + .slider-bar-base([style.left.%]="width") + .slider-bar-control diff --git a/src/ts/components/shared/slider-bar/slider-bar.scss b/src/ts/components/shared/slider-bar/slider-bar.scss new file mode 100644 index 0000000..6036e1a --- /dev/null +++ b/src/ts/components/shared/slider-bar/slider-bar.scss @@ -0,0 +1,80 @@ +@import '../../../../styles/partials/variables'; + +$slider-bg: #aaa; +$slider-fill: #eaeaea; + +$bar-height: 8px; +$bar-control-size: 20px; +$bar-border-radius: $bar-height / 2; + +:host { + display: inline-block; + padding: 2px ($bar-control-size - $bar-height + 10px) / 2; + width: 100%; + + &:focus { + outline: none; + } +} + +.slider-bar { + background: $slider-bg; + display: inline-block; + position: relative; + height: $bar-height; + width: 100%; + border-radius: $bar-border-radius; + + &.disabled { + background: darken($slider-bg, 20%); + + .slider-bar-fill { + background: darken($progress-bg, 20%); + } + + .slider-bar-control { + background: darken(theme-color('default'), 20%); + } + } +} + +.slider-bar-fill { + background: $slider-fill; + width: 0; + height: 100%; + border-radius: $bar-border-radius; +} + +.slider-bar-line { + position: absolute; + top: 0; + left: 0; + right: 0; +} + +.slider-bar-base { + position: absolute; + width: 1px; + top: 0; + left: 0; +} + +.slider-bar-control { + position: absolute; + left: -$bar-control-size / 2; + top: -($bar-control-size - $bar-height) / 2; + width: $bar-control-size; + height: $bar-control-size; + border-radius: 50%; + background: theme-color('default'); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); + + :host:focus & { + box-shadow: 0 0 0 $btn-focus-width rgba($focus-color, 0.7); + outline: none; + } + + :host:focus:not(.focus-visible) & { + box-shadow: 0 0 3px rgba(0, 0, 0, 0.5) !important; + } +} diff --git a/src/ts/components/shared/slider-bar/slider-bar.ts b/src/ts/components/shared/slider-bar/slider-bar.ts new file mode 100644 index 0000000..aab2461 --- /dev/null +++ b/src/ts/components/shared/slider-bar/slider-bar.ts @@ -0,0 +1,89 @@ +import { + Component, Input, Output, EventEmitter, ElementRef, ChangeDetectionStrategy, HostListener, ViewChild +} from '@angular/core'; +import { clamp } from 'lodash'; +import { AgDragEvent } from '../directives/agDrag'; +import { Key } from '../../../client/input/input'; + +@Component({ + selector: 'slider-bar', + templateUrl: 'slider-bar.pug', + styleUrls: ['slider-bar.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'role': 'slider', + '[tabindex]': 'disabled ? -1 : 0', + '[attr.aria-valuemin]': 'min', + '[attr.aria-valuemax]': 'max', + '[attr.aria-valuenow]': 'value', + '[attr.aria-disabled]': 'disabled', + }, +}) +export class SliderBar { + @Input() min = 0; + @Input() max = 100; + @Input() step = 0; + @Input() largeStep = 10; + @Input() disabled = false; + @Input() value = 0; + @Output() valueChange = new EventEmitter(); + @Output() changed = new EventEmitter(); + @ViewChild('bar', { static: true }) bar!: ElementRef; + private currentWidth = 0; + get width() { + return clamp(((this.value - this.min) / (this.max - this.min)) * 100, 0, 100); + } + drag({ type, x, event }: AgDragEvent) { + if (this.disabled) + return; + + event.preventDefault(); + + if (type === 'start') { + this.currentWidth = this.bar.nativeElement.getBoundingClientRect().width; + } + + let val = this.min + clamp(x / this.currentWidth, 0, 1) * (this.max - this.min); + + if (this.step) { + val = Math.round(val / this.step) * this.step; + } + + this.setValue(val, false); + + if (type === 'end') { + this.changed.emit(val); + } + } + @HostListener('keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (this.disabled) + return; + + const step = this.step || 1; + + if (e.keyCode === Key.LEFT || e.keyCode === Key.DOWN || e.keyCode === Key.PAGE_DOWN) { + e.preventDefault(); + this.setValue(this.value - step * (e.keyCode === Key.PAGE_DOWN ? this.largeStep : 1), true); + } else if (e.keyCode === Key.RIGHT || e.keyCode === Key.UP || e.keyCode === Key.PAGE_UP) { + e.preventDefault(); + this.setValue(this.value + step * (e.keyCode === Key.PAGE_UP ? this.largeStep : 1), true); + } else if (e.keyCode === Key.HOME) { + e.preventDefault(); + this.setValue(this.min, true); + } else if (e.keyCode === Key.END) { + e.preventDefault(); + this.setValue(this.max, true); + } + } + private setValue(value: number, emit: boolean) { + if (this.value !== value) { + this.value = clamp(value, this.min, this.max); + this.valueChange.emit(this.value); + + if (emit) { + this.changed.emit(value); + } + } + } +} diff --git a/src/ts/components/shared/sprite-box/sprite-box.pug b/src/ts/components/shared/sprite-box/sprite-box.pug new file mode 100644 index 0000000..6ea0f11 --- /dev/null +++ b/src/ts/components/shared/sprite-box/sprite-box.pug @@ -0,0 +1,4 @@ +canvas.pixelart.sprite-box(#canvas [tooltip]="sprite?.title") +.sprite-box-none(*ngIf="!sprite") + fa-icon([icon]="noneIcon" [fixedWidth]="true") +.sprite-box-index(*ngIf="debug" aria-hidden="true") {{index}} diff --git a/src/ts/components/shared/sprite-box/sprite-box.scss b/src/ts/components/shared/sprite-box/sprite-box.scss new file mode 100644 index 0000000..cce7e45 --- /dev/null +++ b/src/ts/components/shared/sprite-box/sprite-box.scss @@ -0,0 +1,30 @@ +@import '../../../../styles/partials/variables'; + +:host { + position: relative; + cursor: pointer; + overflow: hidden; + display: block; +} + +.sprite-box-none { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + color: darken($text-muted, 20%); + pointer-events: none; +} + +.sprite-box-index { + position: absolute; + left: 3px; + top: 0; + color: darken($text-muted, 20%); + font-size: 8px; +} diff --git a/src/ts/components/shared/sprite-box/sprite-box.ts b/src/ts/components/shared/sprite-box/sprite-box.ts new file mode 100644 index 0000000..124c188 --- /dev/null +++ b/src/ts/components/shared/sprite-box/sprite-box.ts @@ -0,0 +1,186 @@ +import { + Component, Input, AfterViewInit, ViewChild, ElementRef, NgZone, DoCheck, IterableDiffers, IterableDiffer, OnChanges +} from '@angular/core'; +import { Rect, Sprite, ColorExtra, Palette } from '../../../common/interfaces'; +import { parseColor, colorToCSS } from '../../../common/color'; +import { mockPaletteManager, toColorList, getColorsFromSet } from '../../../common/ponyInfo'; +import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import { createCanvas, disableImageSmoothing, resizeCanvas } from '../../../client/canvasUtils'; +import { WHITE } from '../../../common/colors'; +import { rect } from '../../../common/rect'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { faTimes } from '../../../client/icons'; +import { paletteSpriteSheet } from '../../../generated/sprites'; + +let redrawFrame = 0; +const forRedraw: SpriteBox[] = []; + +function drawAll() { + redrawFrame = 0; + forRedraw.forEach(box => box.draw()); + forRedraw.length = 0; +} + +@Component({ + selector: 'sprite-box', + templateUrl: 'sprite-box.pug', + styleUrls: ['sprite-box.scss'], +}) +export class SpriteBox implements AfterViewInit, OnChanges, DoCheck { + readonly debug = DEVELOPMENT; + readonly noneIcon = faTimes; + @Input() size = 52; + @Input() scale = 2; + @Input() x = 0; + @Input() y = 0; + @Input() center = true; + @Input() index?: number; + @Input() sprite?: ColorExtra; + @Input() palette?: Palette; + @Input() fill?: string[] | string; + @Input() outline?: string[] | string; + @Input() reverseExtra?: boolean; + @Input() timestamp: any; + @Input() invisible = false; + @Input() darken = true; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + private _circle?: string; + private fillDiffer: IterableDiffer; + private outlineDiffer: IterableDiffer; + private batch?: ContextSpriteBatch; + constructor(private zone: NgZone, iterableDiffers: IterableDiffers) { + this.fillDiffer = iterableDiffers.find([]).create(); + this.outlineDiffer = iterableDiffers.find([]).create(); + } + @Input() get circle() { + return this._circle; + } + set circle(value) { + this._circle = colorToCSS(parseColor(value || '')); + } + ngAfterViewInit() { + loadAndInitSpriteSheets().then(() => this.redraw()); + } + ngDoCheck() { + const fillChanges = this.fill && Array.isArray(this.fill) && this.fillDiffer.diff(this.fill); + const outlineChanges = this.outline && Array.isArray(this.outline) && this.outlineDiffer.diff(this.outline); + + if (fillChanges || outlineChanges) { + this.redraw(); + } + } + ngOnChanges() { + this.redraw(); + } + private redraw() { + if (!redrawFrame) { + this.zone.runOutsideAngular(() => redrawFrame = requestAnimationFrame(drawAll)); + } + + if (forRedraw.indexOf(this) === -1) { + forRedraw.push(this); + } + } + draw() { + const size = this.size; + const scale = this.scale; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + + if (!size || this.invisible) + return; + + if (canvas.width !== size || canvas.height !== size) { + canvas.width = size; + canvas.height = size; + } + + const context = canvas.getContext('2d'); + + if (!context) + return; + + context.save(); + context.clearRect(0, 0, canvas.width, canvas.height); + + const sprite = this.sprite; + + if (sprite) { + if (this.circle) { + context.fillStyle = this.circle; + context.beginPath(); + context.arc(canvas.width / 2, canvas.height / 2, canvas.width / 3, 0, Math.PI * 2); + context.fill(); + } + + const bufferSize = size / scale; + const batch = this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferSize, bufferSize)); + resizeCanvas(batch.canvas, bufferSize, bufferSize); + + const fills = Array.isArray(this.fill) ? this.fill : [this.fill]; + const outlines = Array.isArray(this.outline) ? this.outline : [this.outline]; + const paletteColors = toColorList(getColorsFromSet({ fills, outlines }, '000000', this.darken)); + const palette = mockPaletteManager.addArray(paletteColors); + const extraPalette = sprite.palettes && mockPaletteManager.addArray(sprite.palettes[0]); + + let x = this.x; + let y = this.y; + + if (this.center) { + const bounds = rect(0, 0, 0, 0); + addRect(bounds, sprite.color); + addRect(bounds, sprite.extra); + + if (sprite.colorMany) { + sprite.colorMany.forEach(c => addRect(bounds, c)); + } + + x = Math.round((bufferSize - bounds.w) / 2 - bounds.x); + y = Math.round((bufferSize - bounds.h) / 2 - bounds.y); + } + + batch.start(paletteSpriteSheet, 0); + + if (this.reverseExtra) { + batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y); + } + + if (sprite.colorMany) { + for (const color of sprite.colorMany) { + batch.drawSprite(color, WHITE, palette, x, y); + } + } else { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } + + if (!this.reverseExtra) { + batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y); + } + + batch.end(); + + disableImageSmoothing(context); + context.scale(scale, scale); + context.drawImage(batch.canvas, 0, 0); + } + + context.restore(); + } +} + +function addRect(rect: Rect, sprite: Sprite | undefined) { + if (sprite && sprite.w && sprite.h) { + if (rect.w === 0 || rect.h === 0) { + rect.x = sprite.ox; + rect.y = sprite.oy; + rect.w = sprite.w; + rect.h = sprite.h; + } else { + const x = Math.min(rect.x, sprite.ox); + const y = Math.min(rect.y, sprite.oy); + rect.w = Math.max(rect.x + rect.w, sprite.ox + sprite.w) - x; + rect.h = Math.max(rect.y + rect.h, sprite.oy + sprite.h) - y; + rect.x = x; + rect.y = y; + } + } +} diff --git a/src/ts/components/shared/sprite-selection/sprite-selection.pug b/src/ts/components/shared/sprite-selection/sprite-selection.pug new file mode 100644 index 0000000..be6eec6 --- /dev/null +++ b/src/ts/components/shared/sprite-selection/sprite-selection.pug @@ -0,0 +1,17 @@ +.selection-list + .selection-item( + *ngFor="let s of sprites | slice:skip:end; let i = index" + role="radio" + (click)="select(i + skip)" + [id]="id + '-' + i" + [class.active]="isSelected(i)" + [class.disabled]="disabled" + [attr.aria-label]="(s ? s.label : emptyLabel) || ''" + [attr.aria-setsize]="sprites.length - skip" + [attr.aria-checked]="isSelected(i)") + sprite-box( + [sprite]="s" [fill]="fill" [outline]="outline" [circle]="circle" [reverseExtra]="reverseExtra" + [index]="i + skip" [timestamp]="s?.timestamp" [invisible]="invisible" [darken]="darken") + button.btn-unstyled.selection-item.selection-item-more( + *ngIf="hasMore" (click)="showMore()" title="Show more..." aria-hidden="true" tabindex="-1") + | ... diff --git a/src/ts/components/shared/sprite-selection/sprite-selection.scss b/src/ts/components/shared/sprite-selection/sprite-selection.scss new file mode 100644 index 0000000..3a599a2 --- /dev/null +++ b/src/ts/components/shared/sprite-selection/sprite-selection.scss @@ -0,0 +1,56 @@ +@import '../../../../styles/partials/variables'; + +:host { + display: block; + overflow: hidden; + background: $dark-bg; + border: solid 1px $border-color; + border-radius: $border-radius-base; + + &:focus { + outline: none; + box-shadow: 0 0 0 $btn-focus-width rgba($focus-color, .5); + } +} + +.selection-list { + display: flex; + flex-wrap: wrap; + margin-bottom: -1px; + margin-right: -5px; +} + +.selection-item { + width: 52px; + height: 52px; + border-right: solid 1px $border-color; + border-bottom: solid 1px $border-color; + margin: 0; + overflow: hidden; + + > sprite-box { + width: 52px; + height: 52px; + } + + &:hover { + background: lighten($input-bg, 10%); + text-decoration: none; + } + + &.active { + background: #ccc; + } + + &.disabled { + opacity: 0.4; + pointer-events: none; + } +} + +.selection-item-more { + color: $text-muted; + font-weight: bold; + font-size: 27px; + text-align: center; +} diff --git a/src/ts/components/shared/sprite-selection/sprite-selection.ts b/src/ts/components/shared/sprite-selection/sprite-selection.ts new file mode 100644 index 0000000..5384502 --- /dev/null +++ b/src/ts/components/shared/sprite-selection/sprite-selection.ts @@ -0,0 +1,97 @@ +import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { uniqueId } from 'lodash'; +import { ColorExtra } from '../../../common/interfaces'; +import { Key } from '../../../client/input/input'; +import { focusElementAfterTimeout } from '../../../client/htmlUtils'; + +const MAX = 999999; + +@Component({ + selector: 'sprite-selection', + templateUrl: 'sprite-selection.pug', + styleUrls: ['sprite-selection.scss'], + host: { + 'role': 'radiogroup', + 'tabindex': '0', + '(keydown)': 'keydown($event)', + '[attr.aria-activedescendant]': 'activeDescendant', + }, +}) +export class SpriteSelection { + @Input() selected = 0; + @Output() selectedChange = new EventEmitter(); + @Input() sprites?: ColorExtra[]; + @Input() fill?: string | string[]; + @Input() outline?: string | string[]; + @Input() circle?: string; + @Input() reverseExtra = false; + @Input() limit = MAX; + @Input() skip = 0; + @Input() disabled = false; + @Input() emptyLabel?: string; + @Input() invisible = false; + @Input() darken = true; + id = uniqueId('sprite-selection-'); + constructor(private element: ElementRef) { + } + get hasMore() { + return this.sprites && this.sprites.length > this.limit; + } + get end() { + return this.skip + this.limit; + } + get activeDescendant() { + return `${this.id}-${this.selected - this.skip}`; + } + isSelected(index: number) { + return this.selected === (index + this.skip); + } + select(index: number, focus = false) { + if (!this.disabled && this.selected !== index) { + this.selected = index; + this.selectedChange.emit(index); + + if (this.hasMore && index >= this.end) { + this.showMore(); + } + + if (focus) { + focusElementAfterTimeout(this.element.nativeElement, '.active'); + } + } + } + showMore() { + this.limit = MAX; + } + keydown(e: KeyboardEvent) { + const select = this.handleKey(e.keyCode); + + if (select !== undefined) { + e.preventDefault(); + this.select(select, true); + } + } + private handleKey(keyCode: number): number | undefined { + if (this.sprites) { + if (keyCode === Key.RIGHT || keyCode === Key.DOWN) { + if (this.selected >= (this.sprites.length - 1)) { + return this.skip; + } else { + return this.selected + 1; + } + } else if (keyCode === Key.LEFT || keyCode === Key.UP) { + if (this.selected <= this.skip) { + return this.sprites.length - 1; + } else { + return this.selected - 1; + } + } else if (keyCode === Key.HOME) { + return this.skip; + } else if (keyCode === Key.END) { + return this.sprites.length - 1; + } + } + + return undefined; + } +} diff --git a/src/ts/components/shared/support-button/support-button.pug b/src/ts/components/shared/support-button/support-button.pug new file mode 100644 index 0000000..ab372d0 --- /dev/null +++ b/src/ts/components/shared/support-button/support-button.pug @@ -0,0 +1,10 @@ +a.btn.btn-lg.btn-outline-patreon.btn-block.mb-2( + *ngIf="patreonLink" (mouseenter)="pony.excite()" (mouseout)="pony.reset()" [href]="patreonLink" target="_blank" + rel="noopener noreferrer") + | {{supporter ? 'Thank you for supporting us!' : 'Support us on Patreon!'}} + svg.patreon-logo(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150") + circle(fill="#f9685f" cx="75" cy="75" r="75") + path(fill="#052d49" d="M31.729 27.659h17.578v95.888H31.729z") + circle(fill="#fff" cy="63.565" cx="95.783" r="35.955") + .supporter-pony + supporter-pony(#pony [scale]="2") diff --git a/src/ts/components/shared/support-button/support-button.scss b/src/ts/components/shared/support-button/support-button.scss new file mode 100644 index 0000000..03b79ab --- /dev/null +++ b/src/ts/components/shared/support-button/support-button.scss @@ -0,0 +1,45 @@ +@import '../../../../styles/partials/variables'; + +:host { + display: block; +} + +.btn { + position: relative; + + @media (max-width: 320px) { + white-space: normal; + } +} + +.patreon-logo { + position: absolute; + left: 8px; + top: 5px; + width: 30px; + height: 30px; + + @media (max-width: 320px) { + display: none; + } +} + +.supporter-pony { + position: absolute; + right: 0; + bottom: 0; + height: 70px; + width: 100px; + overflow: hidden; + border-radius: $border-radius-lg - 2px; + + > supporter-pony { + position: absolute; + bottom: -95px; + left: -20px; + } + + @media (max-width: 380px) { + display: none; + } +} diff --git a/src/ts/components/shared/support-button/support-button.ts b/src/ts/components/shared/support-button/support-button.ts new file mode 100644 index 0000000..4e8700f --- /dev/null +++ b/src/ts/components/shared/support-button/support-button.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { Model } from '../../services/model'; +import { supporterLink } from '../../../client/data'; + +@Component({ + selector: 'support-button', + templateUrl: 'support-button.pug', + styleUrls: ['support-button.scss'], +}) +export class SupportButton { + readonly patreonLink = supporterLink; + constructor(private model: Model) { + } + get supporter() { + return this.model.supporter; + } +} diff --git a/src/ts/components/shared/supporter-pony/supporter-pony.pug b/src/ts/components/shared/supporter-pony/supporter-pony.pug new file mode 100644 index 0000000..e5bc86f --- /dev/null +++ b/src/ts/components/shared/supporter-pony/supporter-pony.pug @@ -0,0 +1,3 @@ +character-preview( + #characterPreview [pony]="pony" [state]="state" [noBackground]="true" [noShadow]="true" + [noOutline]="true" [scale]="scale" style="width: 180px; height: 180px; position: relative; top: -20px;") diff --git a/src/ts/components/shared/supporter-pony/supporter-pony.ts b/src/ts/components/shared/supporter-pony/supporter-pony.ts new file mode 100644 index 0000000..69e742c --- /dev/null +++ b/src/ts/components/shared/supporter-pony/supporter-pony.ts @@ -0,0 +1,88 @@ +import { Component, OnInit, OnDestroy, Input, ViewChild } from '@angular/core'; +import { defaultExpression } from '../../../client/ponyUtils'; +import { defaultPonyState } from '../../../client/ponyHelpers'; +import { SUPPORTER_PONY } from '../../../common/constants'; +import { Expression, Muzzle, HeadAnimation, Iris } from '../../../common/interfaces'; +import { excite } from '../../../client/ponyAnimations'; +import { FrameService, FrameLoop } from '../../services/frameService'; +import { CharacterPreview } from '../character-preview/character-preview'; +import { decompressPonyString } from '../../../common/compressPony'; + +const BLEP: Expression = { + ...defaultExpression, + muzzle: Muzzle.Blep, +}; + +const EXCITED: Expression = { + ...defaultExpression, + muzzle: Muzzle.SmileOpen, +}; + +const DERP: Expression = { + ...defaultExpression, + muzzle: Muzzle.SmileOpen, + leftIris: Iris.Up, +}; + +@Component({ + selector: 'supporter-pony', + templateUrl: 'supporter-pony.pug', +}) +export class SupporterPony implements OnInit, OnDestroy { + @ViewChild('characterPreview', { static: true }) characterPreview!: CharacterPreview; + @Input() scale = 3; + pony = decompressPonyString(SUPPORTER_PONY); + state = defaultPonyState(); + private expression?: Expression; + private headAnimation?: HeadAnimation; + private headTime = 0; + private loop: FrameLoop; + constructor(frameService: FrameService) { + this.loop = frameService.create(delta => this.tick(delta)); + } + ngOnInit() { + this.loop.init(); + } + ngOnDestroy() { + this.loop.destroy(); + } + excite() { + this.headTime = 0; + this.headAnimation = excite; + this.expression = Math.random() < 0.2 ? DERP : EXCITED; + } + reset() { + this.expression = undefined; + } + private tick(delta: number) { + this.headTime += delta; + + if (this.headAnimation) { + const frame = Math.floor(this.headTime * this.headAnimation.fps); + + if (frame >= this.headAnimation.frames.length && !this.headAnimation.loop) { + this.headAnimation = undefined; + this.state.headAnimation = undefined; + this.state.headAnimationFrame = 0; + this.characterPreview.blink(); + } else { + this.state.headAnimation = this.headAnimation; + this.state.headAnimationFrame = frame % this.headAnimation.frames.length; + } + } else { + this.state.headAnimation = undefined; + + if (this.expression) { + if (Math.random() < 0.01) { + this.expression = undefined; + } + } else { + if (Math.random() < 0.005) { + this.expression = BLEP; + } + } + } + + this.state.expression = this.expression; + } +} diff --git a/src/ts/components/shared/swap-box/swap-box.pug b/src/ts/components/shared/swap-box/swap-box.pug new file mode 100644 index 0000000..ce5b372 --- /dev/null +++ b/src/ts/components/shared/swap-box/swap-box.pug @@ -0,0 +1,7 @@ +.dropdown(#dropdown="ag-dropdown" dropdown (isOpenChange)="toggleSwapDropdown()" [hookToCanvas]="true" style="padding-bottom: 5px") + button.game-button.dropdown-toggle.no-arrow(dropdownToggle title="Swap") + fa-icon([icon]="swapIcon" [fixedWidth]="true") + fa-icon.timer-icon([icon]="timerIcon" [style.display]="timeout ? 'block' : 'none'") + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + portrait-box([hidden]="!previewInfo" [pony]="previewInfo" size="medium" style="position: absolute; top: 5px; left: -80px;") + character-list((selectCharacter)="swapPony($event)" (close)="dropdown.close()" (previewCharacter)="preview($event)" [inGame]="true") diff --git a/src/ts/components/shared/swap-box/swap-box.scss b/src/ts/components/shared/swap-box/swap-box.scss new file mode 100644 index 0000000..dc27024 --- /dev/null +++ b/src/ts/components/shared/swap-box/swap-box.scss @@ -0,0 +1,7 @@ +.timer-icon { + position: absolute; + top: 15px; + right: 3px; + font-size: 14px; + color: #ffcc55; +} diff --git a/src/ts/components/shared/swap-box/swap-box.ts b/src/ts/components/shared/swap-box/swap-box.ts new file mode 100644 index 0000000..cc7a964 --- /dev/null +++ b/src/ts/components/shared/swap-box/swap-box.ts @@ -0,0 +1,43 @@ +import { Component, NgZone, ViewChild } from '@angular/core'; +import { PonyTownGame } from '../../../client/game'; +import { PonyObject, Action } from '../../../common/interfaces'; +import { Dropdown } from '../directives/dropdown'; +import { faClock, faExchangeAlt } from '../../../client/icons'; +import { toPalette, mockPaletteManager } from '../../../common/ponyInfo'; +import { Model } from '../../services/model'; +// import { SWAP_TIMEOUT, SECOND } from '../../../common/constants'; + +@Component({ + selector: 'swap-box', + templateUrl: 'swap-box.pug', + styleUrls: ['swap-box.scss'], +}) +export class SwapBox { + readonly swapIcon = faExchangeAlt; + readonly timerIcon = faClock; + previewInfo: any; + @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; + timeout = false; + constructor(private game: PonyTownGame, private zone: NgZone, private model: Model) { + } + toggleSwapDropdown() { + this.zone.run(() => setTimeout(() => { }, 10)); + } + swapPony(pony: PonyObject) { + this.game.send(server => server.actionParam(Action.SwapCharacter, pony.id)); + setTimeout(() => { + this.dropdown && this.dropdown.close(); + pony.lastUsed = (new Date()).toISOString(); + this.model.sortPonies(); + }); + + // if (!this.timeout) { + // this.timeout = true; + // setTimeout(() => this.timeout = false, SWAP_TIMEOUT + SECOND); + // } + } + preview(pony: PonyObject | undefined) { + const info = pony && pony.ponyInfo; + this.previewInfo = info && toPalette(info, mockPaletteManager); + } +} diff --git a/src/ts/components/shared/tabset/tabset.pug b/src/ts/components/shared/tabset/tabset.pug new file mode 100644 index 0000000..241f999 --- /dev/null +++ b/src/ts/components/shared/tabset/tabset.pug @@ -0,0 +1,30 @@ +.nav([ngClass]="navClass" role="tablist" [attr.aria-label]="label") + button.btn-unstyled.nav-link( + *ngFor="let tab of tabs; let i = index" + href + role="tab" + (click)="!tab.disabled && select(i); $event.preventDefault()" + (keydown)="keydown($event)" + [id]="tab.id" + [class.active]="i === activeIndex" + [class.disabled]="tab.disabled" + [tabindex]="i === activeIndex ? 0 : -1" + [attr.aria-controls]="(!destroyOnHide || i === activeIndex ? tab.id + '-panel' : null)" + [attr.aria-selected]="i === activeIndex" + [attr.aria-disabled]="tab.disabled") + span(*ngIf="tab.icon") + fa-icon.d-sm-none([icon]="tab.icon") + span.d-none.d-sm-inline {{tab.title}} + span(*ngIf="!tab.icon") + | {{tab.title}} + ng-template([ngTemplateOutlet]="tab.titleTpl?.templateRef") +.tab-content + ng-template(ngFor let-tab [ngForOf]="tabs" let-i="index") + .tab-pane( + *ngIf="!destroyOnHide || i === activeIndex" + role="tabpanel" + id="{{tab.id}}-panel" + [class.active]="i === activeIndex" + [attr.aria-labelledby]="tab.id" + [attr.aria-expanded]="i === activeIndex") + ng-template([ngTemplateOutlet]="tab.contentTpl.templateRef") diff --git a/src/ts/components/shared/tabset/tabset.ts b/src/ts/components/shared/tabset/tabset.ts new file mode 100644 index 0000000..7c7a43b --- /dev/null +++ b/src/ts/components/shared/tabset/tabset.ts @@ -0,0 +1,93 @@ +import { + Component, Directive, TemplateRef, Input, ContentChild, ContentChildren, QueryList, Output, EventEmitter +} from '@angular/core'; +import { uniqueId } from 'lodash'; +import { Key } from '../../../client/input/input'; + +@Directive({ + selector: '[tabTitle]' +}) +export class TabTitle { + constructor(public templateRef: TemplateRef) { + } +} + +@Directive({ + selector: '[tabContent]', +}) +export class TabContent { + constructor(public templateRef: TemplateRef) { + } +} + +@Directive({ + selector: 'tab', +}) +export class Tab { + @Input() id = uniqueId(`tabset-tab`); + @Input() title?: string; + @Input() icon?: any; + @Input() disabled = false; + @ContentChild(TabContent, { static: false }) contentTpl?: TabContent; + @ContentChild(TabTitle, { static: false }) titleTpl?: TabTitle; +} + +@Component({ + selector: 'tabset', + templateUrl: 'tabset.pug', +}) +export class Tabset { + justifyClass?: string; + @ContentChildren(Tab) tabs!: QueryList; + @Input() label = ''; + @Input() destroyOnHide = true; + @Input() + set justify(className: 'start' | 'center' | 'end' | 'fill' | 'justified') { + if (className === 'fill' || className === 'justified') { + this.justifyClass = `nav-${className}`; + } else { + this.justifyClass = `justify-content-${className}`; + } + } + @Input() orientation: 'horizontal' | 'vertical' = 'horizontal'; + @Input() type: 'tabs' | 'pills' = 'tabs'; + @Input() activeIndex = 0; + @Output() activeIndexChange = new EventEmitter(); + constructor() { + this.justify = 'start'; + } + get navClass() { + return `nav-${this.type}${this.orientation === 'horizontal' ? ` ${this.justifyClass}` : ' flex-column'}`; + } + select(index: number) { + if (this.activeIndex !== index) { + this.activeIndex = index; + this.activeIndexChange.emit(index); + } + } + keydown(e: KeyboardEvent) { + const index = this.handleKey(e.keyCode); + + if (index !== undefined) { + e.preventDefault(); + const element = document.getElementById(this.tabs.toArray()[index].id); + element && element.focus(); + this.select(index); + } + } + private handleKey(keyCode: number) { + if (keyCode === Key.LEFT) { + return this.activeIndex === 0 ? this.tabs.length - 1 : this.activeIndex - 1; + } else if (keyCode === Key.RIGHT) { + return this.activeIndex === this.tabs.length - 1 ? 0 : this.activeIndex + 1; + } else if (keyCode === Key.HOME) { + return 0; + } else if (keyCode === Key.END) { + return this.tabs.length - 1; + } else { + return undefined; + } + } +} + +export const tabsetComponents = [TabContent, TabTitle, Tabset, Tab]; diff --git a/src/ts/components/shared/virtual-list/virtual-list.scss b/src/ts/components/shared/virtual-list/virtual-list.scss new file mode 100644 index 0000000..ece00b5 --- /dev/null +++ b/src/ts/components/shared/virtual-list/virtual-list.scss @@ -0,0 +1,5 @@ +:host { + display: block; + overflow-x: hidden; + overflow-y: auto; +} diff --git a/src/ts/components/shared/virtual-list/virtual-list.ts b/src/ts/components/shared/virtual-list/virtual-list.ts new file mode 100644 index 0000000..b83361a --- /dev/null +++ b/src/ts/components/shared/virtual-list/virtual-list.ts @@ -0,0 +1,167 @@ +import { + Directive, DoCheck, Input, ViewContainerRef, TemplateRef, IterableDiffers, IterableDiffer, + EmbeddedViewRef, Component, ElementRef, ViewChild, NgZone, OnDestroy, ChangeDetectorRef, AfterViewInit +} from '@angular/core'; + +interface Context { + $implicit: T; + index: number; + count: number; + _currentIndex: number; +} + +@Component({ + selector: 'virtual-list', + template: '
', + styleUrls: ['virtual-list.scss'], + host: { + 'tabindex': '0', + }, +}) +export class VirtualList { + @Input() itemSize = 50; + @ViewChild('padStart', { static: true }) padStart!: ElementRef; + @ViewChild('padEnd', { static: true }) padEnd!: ElementRef; + constructor(public element: ElementRef) { + } +} + +@Directive({ + selector: '[virtualFor][virtualForOf]', +}) +export class VirtualFor implements DoCheck, OnDestroy, AfterViewInit { + @Input() + set virtualForOf(forOf: T[]) { + this.forOf = forOf; + this.forOfDirty = true; + } + private forOf!: T[]; + private forOfDirty: boolean = true; + private differ: IterableDiffer | null = null; + private first = 0; + private last = 0; + constructor( + private viewContainer: ViewContainerRef, + private template: TemplateRef>, + private differs: IterableDiffers, + private list: VirtualList, + private changeDetector: ChangeDetectorRef, + zone: NgZone, + ) { + zone.runOutsideAngular(() => { + list.element.nativeElement.addEventListener('scroll', this.detect); + window.addEventListener('resize', this.detect); + }); + } + private detect = () => this.changeDetector.detectChanges(); + @Input() + set virtualForTemplate(value: TemplateRef>) { + if (value) { + this.template = value; + } + } + ngOnDestroy() { + this.list.element.nativeElement.removeEventListener('scroll', this.detect); + window.removeEventListener('resize', this.detect); + } + ngAfterViewInit() { + setTimeout(this.detect, 0); + } + ngDoCheck(): void { + if (this.forOfDirty) { + this.forOfDirty = false; + const value = this.forOf; + + if (!this.differ && value) { + try { + this.differ = this.differs.find(value).create(); + } catch { + throw new Error(`Cannot find a differ`); + } + } + } + + const changes = this.differ && this.differ.diff(this.forOf); + const element = this.list.element.nativeElement as HTMLElement; + const itemSize = this.list.itemSize; + const { height } = element.getBoundingClientRect(); + const scroll = element.scrollTop; + const first = Math.floor(scroll / itemSize); + const last = first + Math.ceil(height / itemSize); + let scrollChanged = false; + + if (this.first !== first || this.last !== last) { + this.first = first; + this.last = last; + scrollChanged = true; + } + + if (changes || scrollChanged) { + this.applyChanges(); + } + } + private applyChanges() { + const viewContainer = this.viewContainer; + const first = this.first; + const last = this.last; + const forOf = this.forOf; + const actualLast = Math.min(last, forOf.length - 1); + + type Ref = EmbeddedViewRef>; + const insertTuples: { item: T; view: Ref; }[] = []; + const views: Ref[] = []; + + for (let i = viewContainer.length - 1; i >= 0; i--) { + const ref = viewContainer.get(i) as Ref; + + if (ref.context._currentIndex < first || ref.context._currentIndex > actualLast) { + viewContainer.detach(i); + views.push(ref); + } + } + + for (let index = first, i = 0; index <= actualLast; index++ , i++) { + if (viewContainer.length <= i || (viewContainer.get(i) as Ref).context._currentIndex !== index) { + let view = views.pop(); + + if (view) { + view.context.$implicit = null!; + view.context._currentIndex = index; + viewContainer.insert(view, i); + } else { + const context: Context = { $implicit: null!, index: -1, count: -1, _currentIndex: index }; + view = viewContainer.createEmbeddedView(this.template, context, i); + } + + insertTuples.push({ item: forOf[index], view }); + } + } + + if (DEVELOPMENT && viewContainer.length !== (actualLast - first + 1)) { + console.error('virtual-list: Invalid length', viewContainer.length, first, actualLast); + } + + for (const view of views) { + view.destroy(); + } + + for (let i = 0; i < insertTuples.length; i++) { + insertTuples[i].view.context.$implicit = insertTuples[i].item; + } + + const count = forOf.length; + + for (let i = 0, ilen = viewContainer.length; i < ilen; i++) { + const viewRef = viewContainer.get(i) as Ref; + viewRef.context.$implicit = forOf[first + i]; + viewRef.context.index = first + i; + viewRef.context.count = count; + } + + const itemSize = this.list.itemSize; + this.list.padStart.nativeElement.style.height = `${first * itemSize}px`; + this.list.padEnd.nativeElement.style.height = `${(forOf.length - actualLast - 1) * itemSize}px`; + } +} + +export const virtualListDirectives = [VirtualFor]; diff --git a/src/ts/components/tools/shared/tools-frame/tools-frame.pug b/src/ts/components/tools/shared/tools-frame/tools-frame.pug new file mode 100644 index 0000000..15c54f2 --- /dev/null +++ b/src/ts/components/tools/shared/tools-frame/tools-frame.pug @@ -0,0 +1,13 @@ +ng-template(#selectFrame) + .frame-select-container + a.frame-select(*ngFor="let s of sprites; index as i" [class.active]="frame === i" + (mousedown)="select(i)" (mouseenter)="enter(i)" (mouseleave)="leave()") + sprite-box([sprite]="s" [fill]="pony.coatFill" [outline]="pony.coatOutline" [size]="70" [x]="x" [y]="y" + [circle]="circle" [reverseExtra]="reverseExtra") + .frame-select-number {{i}} + +a.frame-select([popover]="selectFrame" [placement]="placement" [isOpen]="popoverIsOpen" triggers="" + (click)="togglePopover()" (mousedown)="$event.stopPropagation()" containerClass="popover-frames") + sprite-box([sprite]="sprite" [fill]="pony.coatFill" [outline]="pony.coatOutline" [size]="70" [x]="x" [y]="y" + [circle]="circle" [reverseExtra]="reverseExtra") + .frame-select-number {{frame}} diff --git a/src/ts/components/tools/shared/tools-frame/tools-frame.scss b/src/ts/components/tools/shared/tools-frame/tools-frame.scss new file mode 100644 index 0000000..dea0325 --- /dev/null +++ b/src/ts/components/tools/shared/tools-frame/tools-frame.scss @@ -0,0 +1,16 @@ +.frame-select { + display: block; + position: relative; + + &.active .sprite-box { + background: #ccc; + } +} + +.frame-select-number { + color: #999; + position: absolute; + right: 2px; + top: 1px; + font-size: 11px; +} diff --git a/src/ts/components/tools/shared/tools-frame/tools-frame.ts b/src/ts/components/tools/shared/tools-frame/tools-frame.ts new file mode 100644 index 0000000..ca64435 --- /dev/null +++ b/src/ts/components/tools/shared/tools-frame/tools-frame.ts @@ -0,0 +1,76 @@ +import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { Sprite, PonyInfo } from '../../../../common/interfaces'; + +let openedPopover: ToolsFrame; + +@Component({ + selector: 'tools-frame', + templateUrl: 'tools-frame.pug', + styleUrls: ['tools-frame.scss'], +}) +export class ToolsFrame { + @Input() x = 0; + @Input() y = 0; + @Input() sprites!: Sprite[]; + @Input() frame!: number; + @Input() pony!: PonyInfo; + @Input() reverseExtra = false; + @Input() circle?: string; + @Output() frameChange = new EventEmitter(); + popoverIsOpen = false; + placement = 'right'; + private savedFrame = 0; + private selected = false; + constructor(private element: ElementRef) { + } + get sprite() { + return this.sprites[this.frame]; + } + closePopover = () => { + if (this.popoverIsOpen) { + this.togglePopover(); + } + } + togglePopover() { + const rect = (this.element.nativeElement as HTMLElement).getBoundingClientRect(); + this.placement = (rect.left < (window.innerWidth / 2)) ? 'right' : 'left'; + + if (!this.popoverIsOpen) { + if (openedPopover) { + openedPopover.popoverIsOpen = false; + } + + openedPopover = this; + } + + this.popoverIsOpen = !this.popoverIsOpen; + + if (this.popoverIsOpen) { + this.selected = false; + window.addEventListener('mousedown', this.closePopover); + } else { + window.removeEventListener('mousedown', this.closePopover); + } + + setTimeout(() => { }, 10); + } + select(index: number) { + this.selected = true; + this.frame = index; + this.togglePopover(); + this.frameChange.emit(this.frame); + } + enter(index: number) { + if (!this.selected) { + this.savedFrame = this.frame; + this.frame = index; + this.frameChange.emit(this.frame); + } + } + leave() { + if (!this.selected) { + this.frame = this.savedFrame; + this.frameChange.emit(this.frame); + } + } +} diff --git a/src/ts/components/tools/shared/tools-offset/tools-offset.pug b/src/ts/components/tools/shared/tools-offset/tools-offset.pug new file mode 100644 index 0000000..54c965e --- /dev/null +++ b/src/ts/components/tools/shared/tools-offset/tools-offset.pug @@ -0,0 +1,14 @@ +.tools-offset + .text-center + button.btn.btn-xs.btn-default((click)="moveY(-1)") + fa-icon([icon]="upIcon" [fixedWidth]="true") + .text-center + button.btn.btn-xs.btn-default((click)="moveX(-1)") + fa-icon([icon]="leftIcon" [fixedWidth]="true") + small.text-center.tools-offset-value + | {{offset?.x}} / {{offset?.y}} + button.btn.btn-xs.btn-default((click)="moveX(1)") + fa-icon([icon]="rightIcon" [fixedWidth]="true") + .text-center + button.btn.btn-xs.btn-default((click)="moveY(1)") + fa-icon([icon]="downIcon" [fixedWidth]="true") diff --git a/src/ts/components/tools/shared/tools-offset/tools-offset.scss b/src/ts/components/tools/shared/tools-offset/tools-offset.scss new file mode 100644 index 0000000..25c0605 --- /dev/null +++ b/src/ts/components/tools/shared/tools-offset/tools-offset.scss @@ -0,0 +1,17 @@ +.tools-offset { + width: 110px; + margin: auto; + + .btn { + position: relative; + box-shadow: 0 0 5px black; + } +} + +.tools-offset-value { + width: 40px; + padding: 5px; + background: #222; + color: #eee; + border-radius: 5px; +} diff --git a/src/ts/components/tools/shared/tools-offset/tools-offset.ts b/src/ts/components/tools/shared/tools-offset/tools-offset.ts new file mode 100644 index 0000000..f48e04e --- /dev/null +++ b/src/ts/components/tools/shared/tools-offset/tools-offset.ts @@ -0,0 +1,29 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Point } from '../../../../common/interfaces'; +import { faChevronRight, faChevronLeft, faChevronDown, faChevronUp } from '../../../../client/icons'; + +@Component({ + selector: 'tools-offset', + templateUrl: 'tools-offset.pug', + styleUrls: ['tools-offset.scss'], +}) +export class ToolsOffset { + readonly rightIcon = faChevronRight; + readonly leftIcon = faChevronLeft; + readonly upIcon = faChevronUp; + readonly downIcon = faChevronDown; + @Input() offset?: Point; + @Output() change = new EventEmitter(); + moveX(value: number) { + if (this.offset) { + this.offset.x += value; + this.change.emit(); + } + } + moveY(value: number) { + if (this.offset) { + this.offset.y += value; + this.change.emit(); + } + } +} diff --git a/src/ts/components/tools/shared/tools-range/tools-range.pug b/src/ts/components/tools/shared/tools-range/tools-range.pug new file mode 100644 index 0000000..ab36129 --- /dev/null +++ b/src/ts/components/tools/shared/tools-range/tools-range.pug @@ -0,0 +1,8 @@ +.tools-range.input-group([class.input-group-sm]="small") + .input-group-prepend + button.btn.btn-default((click)="decrement()") + fa-icon([icon]="vertical ? upIcon : leftIcon" [fixedWidth]="true") + input.form-control([(ngModel)]="value" type="number" step=1 [min]="min" [max]="max" [placeholder]="placeholder") + .input-group-append + button.btn.btn-default((click)="increment()") + fa-icon([icon]="vertical ? downIcon : rightIcon" [fixedWidth]="true") diff --git a/src/ts/components/tools/shared/tools-range/tools-range.scss b/src/ts/components/tools/shared/tools-range/tools-range.scss new file mode 100644 index 0000000..fdf0e93 --- /dev/null +++ b/src/ts/components/tools/shared/tools-range/tools-range.scss @@ -0,0 +1,7 @@ +.tools-range { + width: 160px; + + &.input-group-sm { + width: 140px; + } +} diff --git a/src/ts/components/tools/shared/tools-range/tools-range.ts b/src/ts/components/tools/shared/tools-range/tools-range.ts new file mode 100644 index 0000000..bebcd08 --- /dev/null +++ b/src/ts/components/tools/shared/tools-range/tools-range.ts @@ -0,0 +1,55 @@ +import { Component, forwardRef, Input, ChangeDetectionStrategy, EventEmitter, Output } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { faChevronRight, faChevronLeft, faChevronUp, faChevronDown } from '../../../../client/icons'; + +@Component({ + selector: 'tools-range', + templateUrl: 'tools-range.pug', + styleUrls: ['tools-range.scss'], + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToolsRange), multi: true }, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToolsRange implements ControlValueAccessor { + readonly rightIcon = faChevronRight; + readonly leftIcon = faChevronLeft; + readonly upIcon = faChevronUp; + readonly downIcon = faChevronDown; + @Input() min = 0; + @Input() max = 100; + @Input() vertical = false; + @Input() small = false; + @Input() placeholder?: string; + @Output() change = new EventEmitter(); + private _value = 0; + private propagateChange: any = () => { }; + get value() { + return this._value; + } + set value(value: number) { + this._value = value; + this.propagateChange(value); + this.change.emit(); + } + decrement() { + if (this.value > this.min) { + this.value = this.value - 1; + } + } + increment() { + if (this.value < this.max) { + this.value = this.value + 1; + } + } + writeValue(value: number | undefined) { + if (value !== undefined) { + this.value = value; + } + } + registerOnChange(callback: any) { + this.propagateChange = callback; + } + registerOnTouched() { + } +} diff --git a/src/ts/components/tools/shared/tools-xy/tools-xy.pug b/src/ts/components/tools/shared/tools-xy/tools-xy.pug new file mode 100644 index 0000000..8a078d2 --- /dev/null +++ b/src/ts/components/tools/shared/tools-xy/tools-xy.pug @@ -0,0 +1,9 @@ +.tools-xy.btn-group + button.btn.btn-xs.btn-default((click)="changeY(y - 1)") + fa-icon([icon]="upIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default((click)="changeX(x - 1)") + fa-icon([icon]="leftIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default((click)="changeX(x + 1)") + fa-icon([icon]="rightIcon" [fixedWidth]="true") + button.btn.btn-xs.btn-default((click)="changeY(y + 1)") + fa-icon([icon]="downIcon" [fixedWidth]="true") diff --git a/src/ts/components/tools/shared/tools-xy/tools-xy.scss b/src/ts/components/tools/shared/tools-xy/tools-xy.scss new file mode 100644 index 0000000..a84872b --- /dev/null +++ b/src/ts/components/tools/shared/tools-xy/tools-xy.scss @@ -0,0 +1,9 @@ +.tools-xy { + width: 80px; + + .btn { + padding: 0; + font-size: 10px; + width: 19px; + } +} diff --git a/src/ts/components/tools/shared/tools-xy/tools-xy.ts b/src/ts/components/tools/shared/tools-xy/tools-xy.ts new file mode 100644 index 0000000..0d3863f --- /dev/null +++ b/src/ts/components/tools/shared/tools-xy/tools-xy.ts @@ -0,0 +1,32 @@ +import { Component, Input, ChangeDetectionStrategy, EventEmitter, Output } from '@angular/core'; +import { faChevronRight, faChevronLeft, faChevronUp, faChevronDown } from '../../../../client/icons'; + +@Component({ + selector: 'tools-xy', + templateUrl: 'tools-xy.pug', + styleUrls: ['tools-xy.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToolsXY { + readonly rightIcon = faChevronRight; + readonly leftIcon = faChevronLeft; + readonly upIcon = faChevronUp; + readonly downIcon = faChevronDown; + @Input() min = 0; + @Input() max = 100; + @Input() x = 0; + @Input() y = 0; + @Output() xChange = new EventEmitter(); + @Output() yChange = new EventEmitter(); + @Output() change = new EventEmitter(); + changeX(value: number) { + this.x = value; + this.xChange.emit(value); + this.change.emit(); + } + changeY(value: number) { + this.y = value; + this.yChange.emit(value); + this.change.emit(); + } +} diff --git a/src/ts/components/tools/sheetExport.ts b/src/ts/components/tools/sheetExport.ts new file mode 100644 index 0000000..563a5ed --- /dev/null +++ b/src/ts/components/tools/sheetExport.ts @@ -0,0 +1,358 @@ +import { max, compact } from 'lodash'; +import { saveAs } from 'file-saver'; +import { Psd, writePsd, Layer } from 'ag-psd'; +import { SpriteSet, PonyInfoNumber, PaletteSpriteSet, NoDraw } from '../../common/interfaces'; +import { times, cloneDeep, setFlag, includes, toInt } from '../../common/utils'; +import { createDefaultPony, syncLockedPonyInfoNumber, toPaletteNumber, mockPaletteManager } from '../../common/ponyInfo'; +import { Sets } from '../../client/ponyUtils'; +import { defaultDrawPonyOptions, defaultPonyState } from '../../client/ponyHelpers'; +import { createCanvas, disableImageSmoothing } from '../../client/canvasUtils'; +import { ContextSpriteBatch } from '../../graphics/contextSpriteBatch'; +import { BLACK, BLUE, CYAN, WHITE, RED, GREEN, YELLOW, MAGENTA, TRANSPARENT } from '../../common/colors'; +import { colorToCSS } from '../../common/color'; +import { decompressPony, compressPonyString } from '../../common/compressPony'; +import { drawPony } from '../../client/ponyDraw'; +import * as sprites from '../../generated/sprites'; +import { drawPixelTextOnCanvas, fillRect } from '../../graphics/graphicsUtils'; +import { Sheet, SheetLayer, ignoreSet, DEFAULT_COLOR } from '../../common/sheets'; +import { createHeadAnimation } from '../../client/ponyAnimations'; + +const PONY_X = 30; +const PONY_Y = 50; +const patternColors = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, BLACK]; +const whiteColors = [WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE]; + +function maxPatterns(sprites: Sets): number { + return max(sprites.map(s => s && s.length ? max(s.map(x => x ? x.length : 0)) : 0))!; +} + +const backupSprites: any = { + head: [ + undefined, + [ + [ + {} + ], + ], + ], +}; + +function getSets(sheet: Sheet, key: string, override?: string): Sets | undefined { + const setsKey = override || key || ''; + const sets = backupSprites[setsKey] || (sprites as any)[setsKey]; + + if (sheet.duplicateFirstFrame !== undefined) { + return times(sheet.duplicateFirstFrame, () => sets[0]); + } else { + return sheet.single ? [sets] : sets; + } +} + +function getSetsForFirstKey(sheet: Sheet) { + const layer = sheet.layers.find(l => !!l.set); + return layer && getSets(sheet, layer.set!); +} + +export function getCols(sheet: Sheet) { + return sheet.state!.animation.frames.length; +} + +export function getRows(sheet: Sheet) { + if (sheet.rows !== undefined) { + return sheet.rows; + } else { + const sets = getSetsForFirstKey(sheet); + const maxFrames = sets && max(sets.map(f => f ? f.length : 0)); + return (maxFrames || 0) + 1; + } +} + +export function savePsd(psd: Psd, name: string) { + saveAs(new Blob([writePsd(psd, { generateThumbnail: true })], { type: 'application/octet-stream' }), name); +} + +export function createPsd(sheet: Sheet, rows: number, cols: number): Psd { + const width = canvasWidth(sheet, rows, cols); + const height = canvasHeight(sheet, rows, cols); + + return { + width, + height, + children: compact([ + { name: '', canvas: createBackground(rows, cols, width, height, sheet), transparencyProtected: true }, + ...sheet.layers!.map(layer => createPsdLayer(sheet, rows, cols, layer)), + { name: '', canvas: createRefsCanvas(width, height, sheet.paletteOffsetY) }, + ]), + }; +} + +function canvasWidth(sheet: Sheet, _rows: number, cols: number) { + if (sheet.wrap) { + cols = sheet.wrap; + } + + return (sheet.offset * (cols - 1)) + sheet.width; +} + +function canvasHeight(sheet: Sheet, rows: number, _cols: number) { + if (sheet.wrap) { + rows = Math.ceil(rows / sheet.wrap); + } + + return sheet.height * rows; +} + +function drawPsdLayer( + sheet: Sheet, rows: number, cols: number, layer: SheetLayer, pattern = -1, extra = false +): HTMLCanvasElement { + const { width, height, offset, offsetY = 0, wrap } = sheet; + const pony = createPony(); + const baseState = { ...defaultPonyState(), ...sheet.state, blushColor: BLACK }; + const options = { ...defaultDrawPonyOptions(), ...layer.options }; + const ignoreColor = (layer.drawBlack === undefined ? !!layer.head : layer.drawBlack) ? TRANSPARENT : BLACK; + const fieldName = layer.fieldName || sheet.fieldName; + + if (!layer.head) { + baseState.headAnimation = createHeadAnimation('', 1, false, [[]]); + pony.head = ignoreSet(); + pony.nose = ignoreSet(); + pony.ears = ignoreSet(); + } else if (layer.noFace) { + baseState.headAnimation = createHeadAnimation('', 1, false, [[]]); + pony.head = ignoreSet(); + pony.nose = ignoreSet(); + } + + if (!layer.body) { + options.no = setFlag(options.no, NoDraw.BodyOnly, true); + } + + if (!layer.frontLeg) { + options.no = setFlag(options.no, NoDraw.FrontLeg, true); + } + + if (!layer.backLeg) { + options.no = setFlag(options.no, NoDraw.BackLeg, true); + } + + if (!layer.frontFarLeg) { + options.no = setFlag(options.no, NoDraw.FrontFarLeg, true); + } + + if (!layer.backFarLeg) { + options.no = setFlag(options.no, NoDraw.BackFarLeg, true); + } + + layer.setup && layer.setup(pony, baseState); + + syncLockedPonyInfoNumber(pony); + + const actualRows = wrap ? Math.ceil(rows / wrap) : rows; + const actualCols = wrap ? wrap : cols; + const empties = sheet.empties && includes(sheet.setsWithEmpties, layer.set) ? sheet.empties : []; + + return drawFrames(actualRows, actualCols, width, height, offset, (batch, x, y) => { + const xIndexBase = (wrap ? actualCols * y + x : x); + const xIndexOffset = empties.filter(i => i <= xIndexBase).length; + const xIndex = includes(empties, xIndexBase) ? 0 : (xIndexBase - xIndexOffset); + const yIndex = wrap ? 0 : y; + + const state = cloneDeep(baseState); + + sheet.frame && sheet.frame(pony, state, options, xIndex, yIndex, pattern); + layer.frame && layer.frame(pony, state, options, xIndex, yIndex, pattern); + + state.animationFrame = xIndex; + + if (layer.set && fieldName) { + const sets = getSets(sheet, layer.set, layer.setOverride); + + if (!sets) { + throw new Error(`Missing sets for (${layer.set})`); + } + + const frameIndex = sheet.single ? 0 : xIndex; + const typeIndex = sheet.single ? xIndex : yIndex; + const aframe = sets[frameIndex]; + const type = (aframe && typeIndex < aframe.length) ? typeIndex : -1; + const set: SpriteSet = { type }; + + if (pattern !== -1) { + set.fills = patternColors; + set.outlines = patternColors; + + if (aframe && typeIndex < aframe.length && aframe[typeIndex] && pattern < aframe[typeIndex]!.length) { + set.pattern = pattern; + } else { + set.type = -1; + } + } else { + set.fills = whiteColors; + set.outlines = whiteColors; + + if (!(aframe && typeIndex < aframe.length && aframe[typeIndex])) { + set.type = -1; + } + } + + layer.frameSet && layer.frameSet(set, xIndex, yIndex, pattern); + + (pony as any)[fieldName] = set.type === -1 ? ignoreSet() : set; + } + + batch.disableShading = pattern !== -1; + batch.ignoreColor = ignoreColor; + + const pal = toPaletteNumber(pony); + + if (layer.extra !== undefined) { + const set = pal[layer.extra] as PaletteSpriteSet; + + if (extra) { + set.palette = mockPaletteManager.add([0, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK]); + } else { + set.extraPalette = undefined; + } + } + + drawPony(batch, pal, state, PONY_X, PONY_Y + offsetY + toInt(layer.shiftY), options); + }); +} + +function createPsdPatternLayers(sheet: Sheet, rows: number, cols: number, layer: SheetLayer): Layer[] { + const sets = getSets(sheet, layer.set!, layer.setOverride); + const patterns = layer.patterns || (sets ? maxPatterns(sets) : 0) || 6; + + return compact([ + layer.extra && { + name: 'extra', + canvas: drawPsdLayer(sheet, rows, cols, layer, -1, true), + }, + { + name: 'color', + canvas: drawPsdLayer(sheet, rows, cols, layer), + }, + ...times(patterns, i => ({ + name: `pattern ${i}`, + canvas: drawPsdLayer(sheet, rows, cols, layer, i), + hidden: true, + clipping: true, + blendMode: 'multiply', + })), + ]); +} + +function createPsdLayer(sheet: Sheet, rows: number, cols: number, layer: SheetLayer): Layer { + const name = layer.name; + + if (layer.set) { + return { name, children: createPsdPatternLayers(sheet, rows, cols, layer) }; + } else { + return { name, canvas: drawPsdLayer(sheet, rows, cols, layer) }; + } +} + +function drawFrames( + rows: number, cols: number, w: number, h: number, offset: number, + draw: (batch: ContextSpriteBatch, x: number, y: number) => void +): HTMLCanvasElement { + const canvas = createCanvas((offset * (cols - 1)) + w, h * rows); + const buffer = createCanvas(w, h); + const batch = new ContextSpriteBatch(buffer); + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + batch.start(sprites.paletteSpriteSheet, 0); + draw(batch, x, y); + batch.end(); + viewContext.drawImage(buffer, x * offset, y * h); + } + } + + viewContext.restore(); + return canvas; +} + +function createBackground(rows: number, cols: number, width: number, height: number, sheet: Sheet) { + const canvas = createCanvas(width, height); + const context = canvas.getContext('2d')!; + + if (sheet.wrap) { + cols = sheet.wrap; + rows = Math.ceil(rows / sheet.wrap); + } + + fillRect(context, 'lightgreen', 0, 0, canvas.width, canvas.height); + + context.globalAlpha = 0.1; + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const color = (x + (y % 2)) % 2 ? 'green' : 'blue'; + fillRect(context, color, x * sheet.offset, y * sheet.height, sheet.width, sheet.height); + } + } + + context.globalAlpha = 1; + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const gap = sheet.width - sheet.offset; + const index = sheet.wrap ? (cols * y + x) : x; + drawPixelTextOnCanvas(context, x * sheet.offset + gap + 2, y * sheet.height + 2, 0x76c189ff, index.toString()); + } + } + + return canvas; +} + +function createRefsCanvas(width: number, height: number, offsetY = 0) { + const canvas = createCanvas(width, height); + const context = canvas.getContext('2d')!; + const h = 2; + + patternColors.forEach((c, i) => fillRect(context, colorToCSS(c), 5, 5 + h * i + offsetY, 10, h)); + + fillRect(context, '#888888', 25, 10 + offsetY, 8, 10); + fillRect(context, '#d9d9d9', 27, 12 + offsetY, 4, 4); + fillRect(context, '#afafaf', 27, 16 + offsetY, 4, 2); + fillRect(context, '#9f9f9f', 20, 5 + offsetY, 8, 10); + fillRect(context, '#ffffff', 22, 7 + offsetY, 4, 4); + fillRect(context, '#cdcdcd', 22, 11 + offsetY, 4, 2); + + return canvas; +} + +function createPony(): PonyInfoNumber { + const pony = decompressPony(compressPonyString(createDefaultPony())); + pony.mane!.type = 0; + pony.backMane!.type = 0; + pony.tail!.type = 0; + pony.coatFill = DEFAULT_COLOR; + pony.lockCoatOutline = true; + pony.lockBackLegAccessory = false; + return syncLockedPonyInfoNumber(pony); +} + +export function drawPsd(psd: Psd, scale: number, canvas?: HTMLCanvasElement): HTMLCanvasElement { + const buffer = canvas || createCanvas(100, 100); + buffer.width = psd.width * scale; + buffer.height = psd.height * scale; + const context = buffer.getContext('2d')!; + context.save(); + context.scale(scale, scale); + disableImageSmoothing(context); + drawLayer(psd, context); + context.restore(); + return buffer; +} + +function drawLayer(layer: Layer, context: CanvasRenderingContext2D) { + if (!layer.hidden) { + layer.canvas && context.drawImage(layer.canvas, 0, 0); + layer.children && layer.children.forEach(c => drawLayer(c, context)); + } +} diff --git a/src/ts/components/tools/tools-animation/tools-animation.pug b/src/ts/components/tools/tools-animation/tools-animation.pug new file mode 100644 index 0000000..48e063c --- /dev/null +++ b/src/ts/components/tools/tools-animation/tools-animation.pug @@ -0,0 +1,313 @@ +ng-template(#shareLinkPopover) + div + span.close.ml-2((click)="shareLinkOpen = false") × + | {{shareLink}} + +.tools-animation.mb-5 + .d-flex(*ngIf="loaded") + character-preview([pony]="info" [state]="state" [scale]="scale" style="height: 350px") + + .flex-grow-1.ml-2 + .form-inline + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon" [fixedWidth]="true") + .form-group + label.ml-2 pony: + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | {{pony.name}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let p of ponies" (click)="setPony(p)") {{p.name}} + button.btn.btn-default.ml-1((click)="reloadPonies()" title="Reload ponies") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + .form-group + label.ml-2 scale: + scale-picker.ml-1([(scale)]="scale") + //-.form-group + button.btn.ml-1(btnCheckbox [(ngModel)]="flip" (change)="update()" [btnHighlight]="flip") + fa-icon([icon]="rightIcon" [fixedWidth]="true") + .form-group + label.ml-2 mode: + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | {{mode}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="setMode('body')") body + a.dropdown-item((click)="setMode('head')") head + + hr + + .form-inline + .from-group.d-flex + button.btn.btn-default((click)="playing = false; frame = 0;" title="Stop") + fa-icon([icon]="stopIcon" [fixedWidth]="true") + button.btn.btn-default.ml-1((click)="playing = !playing" [title]="playing ? 'Pause' : 'Play'") + fa-icon([icon]="playing ? pauseIcon : playIcon" [fixedWidth]="true") + button.btn.btn-default.ml-1((click)="replay()" [disabled]="!playing" title="Replay") + fa-icon([icon]="replayIcon" [fixedWidth]="true") + button.btn.btn-default.ml-1((mousedown)="prevFrame()" [disabled]="playing" title="Prev frame") + fa-icon([icon]="prevIcon" [fixedWidth]="true") + input.form-control.ml-1( + [(ngModel)]="frame" [disabled]="playing" type="number" placeholder="frame" style="width: 100px;" + min="0" step="1") + button.btn.btn-default.ml-1((click)="nextFrame()" [disabled]="playing" title="Next frame") + fa-icon([icon]="nextIcon" [fixedWidth]="true") + + .input-group.ml-1(style="width: 250px;") + input.form-control([(ngModel)]="animation.name" (change)="update(); sortAnimations()" [disabled]="animation.builtin") + .input-group-append.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item(*ngFor="let a of animations" (click)="selectAnimation(a)") + | {{a.name}} + + button.btn.ml-1(btnCheckbox [(ngModel)]="switch" (change)="update()" [btnHighlight]="switch" + title="Switch close and far legs") + fa-icon([icon]="switchIcon" [fixedWidth]="true") + button.btn.btn-default.ml-1((click)="newAnimation()" title="New Animation") + fa-icon([icon]="fileIcon" [fixedWidth]="true") + button.btn.btn-default.ml-1((click)="duplicateAnimation()" title="Duplicate Animation") + fa-icon([icon]="copyIcon" [fixedWidth]="true") + button.btn.btn-danger.ml-1((click)="removeAnimation()" title="Delete Animation") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + + .form-inline + .from-group + button.btn.btn-default( + (click)="share()" title="Share" [popover]="shareLinkPopover" [isOpen]="shareLinkOpen" + popover-enable="shareLink" popover-class="popover-wide") + fa-icon([icon]="shareIcon" [fixedWidth]="true") + button.btn.btn-default.ml-1((click)="export()" title="Export") + fa-icon([icon]="codeIcon" [fixedWidth]="true") + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default((click)="png()" title="Save as PNG") + | PNG + button.btn.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="png(2)") ×2 + a.dropdown-item((click)="png(3)") ×3 + a.dropdown-item((click)="png(4)") ×4 + .btn-group.ml-1(dropdown) + button.btn.btn-default((click)="gif()" title="Save as GIF") + | GIF + button.btn.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="gif(2)") ×2 + a.dropdown-item((click)="gif(3)") ×3 + a.dropdown-item((click)="gif(4)") ×4 + + .btn.btn-warning.ml-1(*ngIf="animation.builtin" disabled) + | Changes to builtin animations aren't saved, duplicate it, to save the changes + + hr + + .form-inline + .form-group + label duration: + tools-range.ml-2([(ngModel)]="activeFrame.duration" (change)="update()" min="1" max="100" small="true") + .form-group + label.ml-2 fps: + tools-range.ml-2([(ngModel)]="animation.fps" (change)="update()" min="1" max="100" small="true") + .form-group + button.btn.btn-sm.ml-1([(ngModel)]="animation.loop" btnCheckbox [btnHighlight]="animation.loop") + | loop + .btn-group.ml-1(dropdown) + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle title="Before animation") + | before: {{beforeAnimation?.name || 'none'}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="selectBeforeAnimation(undefined)") + | none + a.dropdown-item(*ngFor="let a of bodyAnimations" (click)="selectBeforeAnimation(a)") + | {{a.name}} + .btn-group.ml-1(dropdown) + button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle title="After animation") + | after: {{afterAnimation?.name || 'none'}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="selectAfterAnimation(undefined)") + | none + a.dropdown-item(*ngFor="let a of bodyAnimations" (click)="selectAfterAnimation(a)") + | {{a.name}} + + .form-inline + .form-group + label head offset: + tools-range.ml-2( + [(ngModel)]="activeFrame.headX" (change)="update()" [min]="-20" [max]="20" [small]="true") + .btn-group.ml-1 + button.btn.btn-default.btn-sm((click)="moveAllHead(-1, 0)" title="All frames left") + fa-icon([icon]="doubleLeftIcon" [fixedWidth]="true") + button.btn.btn-default.btn-sm((click)="moveAllHead(1, 0)" title="All frames right") + fa-icon([icon]="doubleRightIcon" [fixedWidth]="true") + tools-range.ml-2( + [(ngModel)]="activeFrame.headY" (change)="update()" [min]="-20" [max]="20" [small]="true" [vertical]="true") + .btn-group.ml-1 + button.btn.btn-default.btn-sm((click)="moveAllHead(0, -1)" title="All frames up") + fa-icon([icon]="doubleUpIcon" [fixedWidth]="true") + button.btn.btn-default.btn-sm((click)="moveAllHead(0, 1)" title="All frames down") + fa-icon([icon]="doubleDownIcon" [fixedWidth]="true") + + .form-inline + .form-group(*ngIf="mode === 'body'") + label body offset: + tools-range.ml-2( + [(ngModel)]="activeFrame.bodyX" (change)="update()" [min]="-50" [max]="50" [small]="true") + .btn-group.ml-1 + button.btn.btn-default.btn-sm((click)="moveAllBody(-1, 0)" title="All frames left") + fa-icon([icon]="doubleLeftIcon" [fixedWidth]="true") + button.btn.btn-default.btn-sm((click)="moveAllBody(1, 0)" title="All frames right") + fa-icon([icon]="doubleRightIcon" [fixedWidth]="true") + tools-range.ml-2( + [(ngModel)]="activeFrame.bodyY" (change)="update()" [min]="-50" [max]="50" [small]="true" [vertical]="true") + .btn-group.ml-1 + button.btn.btn-default.btn-sm((click)="moveAllBody(0, -1)" title="All frames up") + fa-icon([icon]="doubleUpIcon" [fixedWidth]="true") + button.btn.btn-default.btn-sm((click)="moveAllBody(0, 1)" title="All frames down") + fa-icon([icon]="doubleDownIcon" [fixedWidth]="true") + + .form-inline + .form-group(*ngIf="mode === 'body'") + label shadow (x, size): + tools-range.ml-2( + [(ngModel)]="activeFrame.shadowOffset" (change)="update()" [min]="-50" [max]="50" [small]="true") + tools-range.ml-2( + [(ngModel)]="activeFrame.shadowFrame" (change)="update()" [min]="-50" [max]="50" [small]="true") + + hr + + .form-inline + .from-group.d-flex + label frame: + button.btn.btn-sm.btn-default.ml-1((click)="addFrame()" title="Add new frame after current one") + fa-icon([icon]="plusIcon" [fixedWidth]="true") + button.btn.btn-sm.btn-default.ml-1((click)="duplicateFrame()" title="Duplicate frame and add after current one") + fa-icon([icon]="cloneIcon" [fixedWidth]="true") + button.btn.btn-sm.btn-default.ml-1((click)="moveFrameLeft()" [disabled]="frame === 0" title="Move current frame left") + fa-icon([icon]="leftIcon" [fixedWidth]="true") + button.btn.btn-sm.btn-default.ml-1((click)="moveFrameRight()" [disabled]="frame === (totalFrames - 1)" title="Move current frame right") + fa-icon([icon]="rightIcon" [fixedWidth]="true") + button.btn.btn-sm.btn-default.ml-1((click)="removeFrame()" [disabled]="totalFrames < 2" title="Delete current frame") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + + .tools-animation-timeline(*ngIf="loaded && mode === 'body'") + .float-left + table.table.table-bordered.table-frames-header.table-sm + thead + tr: th Frame (duration) + tbody + tr: td Head offset (x/y) + tr: td Body offset (x/y) + tr: td Shadow (x/size) + tr: td.frame-cell Front close Leg + tr + td.frame-cell + check-box.float-left([(checked)]="bodyAnimation.lockFrontLegs" (checkedChange)="update()" [icon]="lockIcon") + | Front far Leg + tr: td.frame-cell Back close Leg + tr + td.frame-cell + check-box.float-left([(checked)]="bodyAnimation.lockBackLegs" (checkedChange)="update()" [icon]="lockIcon") + | Back far Leg + tr: td.frame-cell Body + tr: td.frame-cell Wing + tr: td.frame-cell Tail + .frames-view + table.table.table-bordered.table-frames.table-sm(*ngIf="mode === 'body'") + thead + tr + th(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)") + | {{i + 1}} + span.text-muted.ml-1 ({{f.duration || 1}}) + tbody + tr + td(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)") + | {{f.headX}} / {{f.headY}} + tr + td(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)") + | {{f.bodyX}} / {{f.bodyY}} + tr + td(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)") + | {{f.shadowOffset}} / {{f.shadowFrame}} + tr + td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.frontLeg" [sprites]="frontLegs" [pony]="info" [x]="-15" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + tools-xy.frame-cell-xy([(x)]="f.frontLegX" [(y)]="f.frontLegY" (change)="update()") + .frame-cell-offset {{f.frontLegX}} / {{f.frontLegY}} + tr([class.frames-locked]="bodyAnimation.lockFrontLegs") + td.frame-cell.far-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.frontFarLeg" [sprites]="frontLegs" [pony]="info" [x]="-15" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + tools-xy.frame-cell-xy([(x)]="f.frontFarLegX" [(y)]="f.frontFarLegY" (change)="update()") + .frame-cell-offset {{f.frontFarLegX}} / {{f.frontFarLegY}} + tr + td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.backLeg" [sprites]="backLegs" [pony]="info" [x]="-30" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + tools-xy.frame-cell-xy([(x)]="f.backLegX" [(y)]="f.backLegY" (change)="update()") + .frame-cell-offset {{f.backLegX}} / {{f.backLegY}} + tr([class.frames-locked]="bodyAnimation.lockBackLegs") + td.frame-cell.far-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.backFarLeg" [sprites]="backLegs" [pony]="info" [x]="-30" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + tools-xy.frame-cell-xy([(x)]="f.backFarLegX" [(y)]="f.backFarLegY" (change)="update()") + .frame-cell-offset {{f.backFarLegX}} / {{f.backFarLegY}} + tr + td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.body" [sprites]="body" [pony]="info" [x]="-30" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + tr + td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.wing" [sprites]="wing" [pony]="info" [x]="-30" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + tr + td.frame-cell(*ngFor="let f of bodyFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.tail" [sprites]="tail" [pony]="info" [x]="-30" [y]="-40" + (frameChange)="update()" (click)="selectFrame(i)") + + .tools-animation-timeline(*ngIf="loaded && mode === 'head'") + .float-left + table.table.table-bordered.table-frames-header.table-sm + thead + tr: th Frame (duration) + tbody + tr: td Head offset (x/y) + tr: td.frame-cell Right eye + tr + td.frame-cell + check-box.float-left([(checked)]="headAnimation.lockEyes" (checkedChange)="update()" [icon]="lockIcon") + | Left eye + tr: td.frame-cell Mouth + .frames-view + table.table.table-bordered.table-frames.table-sm + thead + tr + th(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)") + | {{i + 1}} + span.text-muted.ml-1 ({{f.duration || 1}}) + tbody + tr + td(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)" (click)="selectFrame(i)") + | {{f.headX}} / {{f.headY}} + tr + td.frame-cell(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.right" [sprites]="rightEyes" [pony]="info" [x]="-15" [y]="-20" [circle]="coat" + (frameChange)="update()" [reverseExtra]="true" (click)="selectFrame(i)") + tr([class.frames-locked]="headAnimation.lockEyes") + td.frame-cell(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.left" [sprites]="leftEyes" [pony]="info" [x]="-15" [y]="-20" [circle]="coat" + (frameChange)="update()" [reverseExtra]="true" (click)="selectFrame(i)") + tr + td.frame-cell(*ngFor="let f of headFrames; index as i" [class.active]="isActive(i)") + tools-frame( + [(frame)]="f.mouth" [sprites]="mouths" [pony]="info" [x]="-15" [y]="-20" [circle]="coat" + (frameChange)="update()" [reverseExtra]="true" (click)="selectFrame(i)") diff --git a/src/ts/components/tools/tools-animation/tools-animation.scss b/src/ts/components/tools/tools-animation/tools-animation.scss new file mode 100644 index 0000000..177c3c9 --- /dev/null +++ b/src/ts/components/tools/tools-animation/tools-animation.scss @@ -0,0 +1,99 @@ +@import '../../../../styles/partials/variables'; + +$frame-size: 79px; + +.tools-animation { + padding: 10px; +} + +.tools-animation-timeline { + margin-top: 10px; +} + +.frame-cell { + height: $frame-size; + position: relative; +} + +.form-inline { + margin-bottom: 5px; +} + +hr { + margin: 10px 0; +} + +td, th { + &.active { + background-color: #aef5ae !important; + } +} + +.table-frames-header { + width: auto; + margin: 0; + color: $text-muted; + + td, th { + text-align: right; + white-space: nowrap; + } +} + +.frames-view { + background: #eee; + color: #222; + overflow-x: scroll; + border: solid 1px #ddd; +} + +.frames-locked { + opacity: 0.5; + pointer-events: none; +} + +.from-group { + > .input-group, > .form-control { + margin-top: 1px; + } +} + +.table-frames { + table-layout: fixed; + width: auto; + max-width: none; + margin: 0; + border: none; + color: $inverse-color; + + td, th { + width: $frame-size; + text-align: center; + border-color: #ddd; + } + + th { + cursor: pointer; + } +} + +.frame-cell-xy { + position: absolute; + bottom: 0; + left: 0; + opacity: 0.2; + + :hover > & { + opacity: 1; + } +} + +.frame-cell-offset { + position: absolute; + top: 0; + left: 0; + right: 0; + text-align: center; + font-size: 10px; + font-weight: bold; +} diff --git a/src/ts/components/tools/tools-animation/tools-animation.ts b/src/ts/components/tools/tools-animation/tools-animation.ts new file mode 100644 index 0000000..0679a10 --- /dev/null +++ b/src/ts/components/tools/tools-animation/tools-animation.ts @@ -0,0 +1,804 @@ +import { Component, HostListener, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { HttpClient } from '@angular/common/http'; +import { flatMap, dropRightWhile, compact } from 'lodash'; +import { + BodyAnimation as IBodyAnimation, + BodyAnimationFrame as IBodyAnimationFrame, + HeadAnimation as IHeadAnimation, + HeadAnimationFrame as IHeadAnimationFrame, + ColorExtraSet, PonyInfo, PonyObject, BodyShadow, PonyEye +} from '../../../common/interfaces'; +import { removeItem, repeat, isKeyEventInvalid, cloneDeep, array } from '../../../common/utils'; +import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo'; +import { Key } from '../../../client/input/input'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; +import { + headAnimations, animations, createBodyFrame, createHeadFrame, stand, sit, mergeAnimations, + sitDown, lieDown, lie, sitUp, standUp +} from '../../../client/ponyAnimations'; +import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import * as sprites from '../../../generated/sprites'; +import { createCanvas, disableImageSmoothing, saveCanvas } from '../../../client/canvasUtils'; +import { loadAndInitSpriteSheets, createEyeSprite } from '../../../client/spriteUtils'; +import { drawPony } from '../../../client/ponyDraw'; +import { + faLock, faHome, faArrowRight, faArrowLeft, faPause, faPlay, faChevronRight, faChevronLeft, faRetweet, + faClone, faPlus, faAngleDoubleDown, faAngleDoubleUp, faAngleDoubleRight, faAngleDoubleLeft, + faCode, faShare, faTrash, faCopy, faFile, faStop, faRedo, faSync +} from '../../../client/icons'; +import { FrameService, FrameLoop } from '../../services/frameService'; +import { StorageService } from '../../services/storageService'; +import { decompressPonyString } from '../../../common/compressPony'; + +const ponyWidth = 80; +const ponyHeight = 80; + +type AnimationMode = 'body' | 'head'; + +interface BaseAnimationFrame { + duration: number; +} + +interface BodyAnimationFrame extends IBodyAnimationFrame, BaseAnimationFrame { + shadowOffset: number; + shadowFrame: number; +} + +interface HeadAnimationFrame extends IHeadAnimationFrame, BaseAnimationFrame { } + +interface BaseAnimation { + name: string; + fps: number; + loop: boolean; + builtin?: boolean; +} + +interface BodyAnimation extends BaseAnimation { + lockFrontLegs?: boolean; + lockBackLegs?: boolean; + frames: BodyAnimationFrame[]; +} + +interface HeadAnimation extends BaseAnimation { + lockEyes?: boolean; + frames: HeadAnimationFrame[]; +} + +interface AnimationsData { + active?: string; + animations?: BodyAnimation[]; + headActive?: string; + headAnimations?: HeadAnimation[]; +} + +interface PonyItem { + name: string; + info: PonyInfo; +} + +const testPony = { name: 'test pony', info: createDefaultPony() }; + +function eyeSprite(e: PonyEye | undefined) { + return createEyeSprite(e, 0, sprites.defaultPalette); +} + +@Component({ + selector: 'tools-animation', + templateUrl: 'tools-animation.pug', + styleUrls: ['tools-animation.scss'], +}) +export class ToolsAnimation implements OnInit, OnDestroy { + readonly lockIcon = faLock; + readonly homeIcon = faHome; + readonly rightIcon = faArrowRight; + readonly leftIcon = faArrowLeft; + readonly stopIcon = faStop; + readonly pauseIcon = faPause; + readonly playIcon = faPlay; + readonly replayIcon = faRedo; + readonly prevIcon = faChevronLeft; + readonly nextIcon = faChevronRight; + readonly switchIcon = faRetweet; + readonly fileIcon = faFile; + readonly copyIcon = faCopy; + readonly trashIcon = faTrash; + readonly shareIcon = faShare; + readonly codeIcon = faCode; + readonly doubleLeftIcon = faAngleDoubleLeft; + readonly doubleRightIcon = faAngleDoubleRight; + readonly doubleUpIcon = faAngleDoubleUp; + readonly doubleDownIcon = faAngleDoubleDown; + readonly plusIcon = faPlus; + readonly cloneIcon = faClone; + readonly syncIcon = faSync; + loaded = false; + pony: PonyItem; + ponies: PonyItem[] = [testPony]; + scale = 3; + shareLink?: string; + shareLinkOpen = false; + state = defaultPonyState(); + bodyAnimations: BodyAnimation[] = animations.map(fromBodyAnimation); + bodyAnimation: BodyAnimation; + headAnimations: HeadAnimation[] = headAnimations.map(fromHeadAnimation); + headAnimation: HeadAnimation; + body = sprites.body.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); + wing = sprites.wings.map(types => types![3]![0]!); + tail = sprites.tails.map(types => types![17]![0]!); + frontLegs: ColorExtraSet = sprites.frontLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); + backLegs: ColorExtraSet = sprites.backLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); + leftEyes: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); + rightEyes: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); + mouths: ColorExtraSet = sprites.noses + .map(m => m[0][0]) + .map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palette: sprites.defaultPalette })); + flip = false; + switch = false; + mode: AnimationMode; + beforeAnimation: BodyAnimation | undefined; + afterAnimation: BodyAnimation | undefined; + private _playing = false; + private _frame = 0; + private loop: FrameLoop; + constructor( + private http: HttpClient, + private route: ActivatedRoute, + private storage: StorageService, + frameService: FrameService + ) { + this.mode = storage.getItem('tools-animation-mode') as AnimationMode || 'body'; + this.loop = frameService.create(delta => this.tick(delta)); + + const data = this.loadAnimations(); + + const extraAnimations: IBodyAnimation[] = [ + mergeAnimations('sit-lie-sit', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp, sit]), + mergeAnimations('stand-sit-stand', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp, stand]), + mergeAnimations('stand-to-sit', 24, false, [stand, sitDown, sit]), + mergeAnimations('sit-to-lie', 24, false, [sit, lieDown, lie]), + { ...stand, loop: false, name: 'standing (1s)', frames: array(stand.fps, stand.frames[0]) }, + { ...sit, loop: false, name: 'sitting (1s)', frames: array(sit.fps, sit.frames[0]) }, + { ...lie, loop: false, name: 'lying (1s)', frames: array(lie.fps, lie.frames[0]) }, + ]; + + this.bodyAnimations.push(...extraAnimations.map((a, i) => fromBodyAnimation(a, 90 + i))); + this.bodyAnimations.push(...(data.animations || []).map(fixBodyAnimation)); + this.headAnimations.push(...(data.headAnimations || []).map(fixHeadAnimation)); + this.bodyAnimation = this.bodyAnimations[0]; + this.headAnimation = this.headAnimations[0]; + + this.sortAnimations(); + this.selectBodyAnimation(this.bodyAnimations[parseInt(data.active || '0', 10) | 0] || this.bodyAnimation); + this.selectHeadAnimation(this.headAnimations[parseInt(data.headActive || '0', 10) | 0] || this.headAnimation); + + this.pony = this.ponies[0]; + this.pony.info.coatFill = '#9f7e7e'; + this.pony.info.mane!.type = 9; + this.pony.info.mane!.fills![0] = '#e2cf67'; + this.pony.info.lockEyes = false; + this.pony.info.cm = [ + 'orange', 'orange', 'orange', 'orange', 'orange', + 'orange', '', '', '', 'orange', + 'orange', '', '', '', 'orange', + 'orange', '', '', '', 'orange', + 'orange', 'orange', 'orange', 'orange', 'orange', + ]; + + syncLockedPonyInfo(this.pony.info); + this.reloadPonies(); + } + get info() { + return this.pony.info; + } + get frame() { + return this._frame; + } + set frame(value: number) { + if (this._frame !== value) { + this._frame = value % this.frames.length; + + if (!this.playing) { + if (this.mode === 'body') { + this.state.animationFrame = this.frame; + } else { + this.state.headAnimationFrame = this.frame; + } + } + } + } + get activeFrame(): BaseAnimationFrame { + return this.frames[this.frame] || ({} as any); + } + get totalFrames() { + return this.frames.length; + } + get playing() { + return this._playing; + } + set playing(value: boolean) { + if (this._playing !== value) { + this._playing = value; + this.time = 0; + this.update(); + + if (!value) { + this.frame = this.mode === 'body' ? this.state.animationFrame : this.state.headAnimationFrame; + } + } + } + get bodyFrames() { + return this.bodyAnimation.frames; + } + get headFrames() { + return this.headAnimation.frames; + } + ngOnInit() { + this.route.params.subscribe(({ id }) => id && this.fetchAnimation(id)); + + return loadAndInitSpriteSheets().then(() => { + this.loaded = true; + this.update(); + this.loop.init(); + }); + } + ngOnDestroy() { + this.loop.destroy(); + } + reloadPonies() { + this.http.get('/api-tools/ponies') + .subscribe(data => { + this.ponies = [ + testPony, + ...data + .map(p => ({ name: p.name, info: decompressPonyString(p.info) })) + .sort((a, b) => a.name.localeCompare(b.name)), + ]; + + const ponyName = this.storage.getItem('tools-animation-pony'); + + if (ponyName) { + this.pony = this.ponies.find(p => p.name === ponyName) || this.pony; + } + }); + } + setPony(pony: PonyItem) { + this.pony = pony; + this.update(); + this.storage.setItem('tools-animation-pony', pony.name); + } + selectBodyAnimation(animation: BodyAnimation) { + this.bodyAnimation = animation; + this.update(); + } + selectHeadAnimation(animation: HeadAnimation) { + this.headAnimation = animation; + this.update(); + } + setMode(mode: AnimationMode) { + this.mode = mode; + this.storage.setItem('tools-animation-mode', mode); + } + replay() { + this.bodyAnimationPlaying = 0; + this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; + this.time = 0; + } + private fetchAnimation(id: string) { + return this.http.get<{ type: string, animation: any; }>(`/api-tools/animation/${id}`) + .subscribe(({ type, animation }) => { + if (type === 'body') { + this.bodyAnimations.push(animation); + } else { + this.headAnimations.push(animation); + } + + this.sortAnimations(); + this.selectAnimation(animation); + }); + } + private createAnimation(): BodyAnimation | HeadAnimation { + if (this.mode === 'body') { + return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultBodyFrame()] }; + } else { + return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultHeadFrame()] }; + } + } + newAnimation() { + const animation = this.createAnimation(); + const animations = this.animations as any[]; + animations.push(animation); + this.selectAnimation(animation); + } + duplicateAnimation() { + const animation = cloneDeep(this.animation); + const animations = this.animations as any[]; + animation.name = animation.name.replace(/# builtin \d+ #/, '').trim() + ' (clone)'; + delete animation.builtin; + animations.push(animation); + this.selectAnimation(animation); + } + removeAnimation() { + const animations = this.animations; + + if (animations.length && confirm('are you sure ?')) { + removeItem(animations, this.animation); + this.selectAnimation(animations[0]); + } + } + selectAnimation(animation: BodyAnimation | HeadAnimation) { + if (this.mode === 'body') { + this.selectBodyAnimation(animation as BodyAnimation); + } else { + this.selectHeadAnimation(animation as HeadAnimation); + } + } + selectBeforeAnimation(animation: BodyAnimation | undefined) { + this.beforeAnimation = animation; + this.update(); + } + selectAfterAnimation(animation: BodyAnimation | undefined) { + this.afterAnimation = animation; + this.update(); + } + selectFrame(index: number) { + this.frame = index; + } + prevFrame() { + this.frame = this.frame === 0 ? (this.frames.length - 1) : (this.frame - 1); + } + nextFrame() { + this.frame = this.frame + 1; + } + addFrame() { + const frames = this.frames as any[]; + const frame = this.mode === 'body' ? createDefaultBodyFrame() : createDefaultHeadFrame(); + frames.splice(this.frame + 1, 0, frame); + this.update(); + this.frame++; + } + duplicateFrame() { + const frames = this.frames as any[]; + frames.splice(this.frame + 1, 0, cloneDeep(frames[this.frame])); + this.update(); + this.frame++; + } + removeFrame() { + const frames = this.frames; + + if (frames.length && confirm('are you sure ?')) { + frames.splice(this.frame, 1); + this.update(); + this.frame = Math.min(this.frame, frames.length - 1); + } + } + moveFrameLeft() { + if (this.frame > 0) { + swap(this.frames, this.frame, this.frame - 1); + this.update(); + this.frame--; + } + } + moveFrameRight() { + const frames = this.frames; + + if (this.frame < (frames.length - 1)) { + swap(frames, this.frame, this.frame + 1); + this.update(); + this.frame++; + } + } + isActive(index: number) { + return this.frame === index; + } + get animations() { + return this.mode === 'body' ? this.bodyAnimations : this.headAnimations; + } + get animation() { + return this.mode === 'body' ? this.bodyAnimation : this.headAnimation; + } + get frames() { + return this.animation.frames; + } + @HostListener('window:keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) { + e.preventDefault(); + } + } + moveAllHead(x: number, y: number) { + (this.frames as HeadAnimationFrame[]).forEach(f => { + f.headX += x; + f.headY += y; + }); + this.update(); + } + moveAllBody(x: number, y: number) { + (this.frames as BodyAnimationFrame[]).forEach(f => { + f.bodyX += x; + f.bodyY += y; + }); + this.update(); + } + handleKey(keyCode: number) { + if (keyCode === Key.OPEN_BRACKET || keyCode === Key.LEFT || keyCode === Key.COMMA) { + this.prevFrame(); + } else if (keyCode === Key.CLOSE_BRACKET || keyCode === Key.RIGHT || keyCode === Key.PERIOD) { + this.nextFrame(); + } else if (keyCode === Key.ENTER) { + this.playing = !this.playing; + } else { + return false; + } + + return true; + } + share() { + const wasOpened = this.shareLinkOpen; + this.shareLink = undefined; + this.shareLinkOpen = false; + + if (!wasOpened) { + const animation = { type: this.mode, animation: this.animation }; + this.http.post<{ name: string; }>('/api-tools/animation', { animation }) + .subscribe(({ name }) => { + this.shareLink = `${location.protocol}//${location.host}/tools/animation/${name}`; + this.shareLinkOpen = true; + }); + } + } + export() { + if (this.mode === 'body') { + const frames = this.bodyAnimation.frames + .map(f => [f.duration, '[' + compressBodyFrame(f).join(', ') + ']']) + .map(([repeat, frame]) => repeat > 1 ? `...repeat(${repeat}, ${frame})` : frame); + console.log(`frames: [\n${frames.map(x => `\t${x}`).join(',\n')}\n]`); + + if (this.bodyAnimation.frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) { + const shadow = this.bodyAnimation.frames.map(f => [f.shadowFrame, f.shadowOffset]); + console.log(`shadow: [${shadow.map(x => `[${x.join(', ')}]`).join(', ')}]`); + } + } else { + const animation = toHeadAnimation(this.headAnimation, true); + const compressed = animation.frames.map(compressHeadFrame); + console.log(JSON.stringify(compressed)); + } + } + png(scale = 1) { + const { canvas } = this.createAnimationSprites(scale); + saveCanvas(canvas, `${this.animation.name}.png`); + } + gif(scale = 1) { + const { canvas, empty } = this.createAnimationSprites(scale); + const wnd = window.open('')!; + const width = scale * ponyWidth; + const height = scale * ponyHeight; + const fps = this.animation.fps || 24; + const image = canvas.toDataURL(); + + this.http.post<{ name: string; }>('/api-tools/animation-gif', { image, width, height, fps, remove: empty }) + .subscribe(({ name }) => wnd.location.href = `/api-tools/animation/${name}.gif`); + } + sortAnimations() { + this.bodyAnimations.sort(compareAnimations); + this.headAnimations.sort(compareAnimations); + } + private update() { + if (this.headAnimation && this.headAnimation.lockEyes) { + this.headAnimation.frames.forEach(f => f.left = f.right); + } + + if (this.bodyAnimation) { + if (this.bodyAnimation.lockFrontLegs) { + this.bodyAnimation.frames.forEach(f => { + f.frontFarLeg = f.frontLeg; + f.frontFarLegX = f.frontLegX; + f.frontFarLegY = f.frontLegY; + }); + } + + if (this.bodyAnimation.lockBackLegs) { + this.bodyAnimation.frames.forEach(f => { + f.backFarLeg = f.backLeg; + f.backFarLegX = f.backLegX; + f.backFarLegY = f.backLegY; + }); + } + } + + this.bodyAnimationsToPlay = compact([ + this.playing && this.beforeAnimation && { ...toBodyAnimation(this.beforeAnimation, true, false), loop: false }, + toBodyAnimation(this.bodyAnimation, this.playing, this.switch), + this.playing && this.afterAnimation && { ...toBodyAnimation(this.afterAnimation, true, false), loop: true }, + ]); + + this.bodyAnimationPlaying = 0; + + this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; + this.state.headAnimation = toHeadAnimation(this.headAnimation, this.playing); + + if (this.playing) { + this.state.animationFrame = 0; + this.state.headAnimationFrame = 0; + } + + this.saveAnimations(); + } + private bodyAnimationsToPlay: IBodyAnimation[] = []; + private bodyAnimationPlaying = 0; + private time = 0; + private tick(delta: number) { + if (this.playing) { + this.time += delta; + + if (this.mode === 'body') { + if (this.state.animation) { + const frame = this.time * this.state.animation.fps; + + if (frame > this.state.animation.frames.length && !this.state.animation.loop) { + this.bodyAnimationPlaying = (this.bodyAnimationPlaying + 1) % this.bodyAnimationsToPlay.length; + this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; + this.state.animationFrame = 0; + this.time = 0; + } else { + this.state.animationFrame = Math.floor(frame) % this.state.animation.frames.length; + } + } + } else { + if (this.state.headAnimation) { + const frame = Math.floor(this.time * this.state.headAnimation.fps); + this.state.headAnimationFrame = frame % this.state.headAnimation.frames.length; + } + } + } + } + private saveAnimations() { + this.storage.setJSON('tools-animations', { + active: this.bodyAnimations.indexOf(this.bodyAnimation).toString(), + animations: this.bodyAnimations.filter(a => !a.builtin), + headActive: this.headAnimations.indexOf(this.headAnimation).toString(), + headAnimations: this.headAnimations.filter(a => !a.builtin), + }); + } + private loadAnimations(): AnimationsData { + return this.storage.getJSON('tools-animations', {}); + } + private createAnimationSprites(scale: number) { + const animation = toBodyAnimation(this.bodyAnimation, true, this.switch); + const headAnimation = toHeadAnimation(this.headAnimation, true); + const frames = this.mode === 'body' ? animation.frames.length : headAnimation.frames.length; + const buffer = createCanvas(ponyWidth, ponyHeight); + const batch = new ContextSpriteBatch(buffer); + const info = toPalette(this.pony.info); + const cols = Math.ceil(Math.sqrt(frames)); + const canvas = createCanvas(ponyWidth * cols * scale, ponyHeight * Math.ceil(frames / cols) * scale); + const context = canvas.getContext('2d')!; + const empty = (cols * Math.ceil(frames / cols)) - frames; + const options = defaultDrawPonyOptions(); + disableImageSmoothing(context); + context.scale(scale, scale); + + for (let i = 0; i < frames; i++) { + const x = i % cols; + const y = Math.floor(i / cols); + + batch.start(sprites.paletteSpriteSheet, 0); + + drawPony(batch, info, { + ...defaultPonyState(), + animation, + animationFrame: this.mode === 'body' ? i : 0, + headAnimation: this.mode === 'head' ? headAnimation : undefined, + headAnimationFrame: this.mode === 'head' ? i : 0, + blinkFrame: 1, + }, ponyWidth / 2, ponyHeight - 10, options); + + batch.end(); + context.drawImage(buffer, x * ponyWidth, y * ponyHeight); + } + + return { canvas, empty }; + } +} + +// helper methods + +function compareAnimations(a: T, b: T): number { + return a.name.localeCompare(b.name); +} + +function swap(array: any[], a: number, b: number) { + const temp = array[a]; + array[a] = array[b]; + array[b] = temp; +} + +function fromBodyAnimation({ name, frames, fps, loop, shadow }: IBodyAnimation, index: number): BodyAnimation { + const fs: BodyAnimationFrame[] = []; + + frames.forEach((f, i) => { + const l = fs[fs.length - 1]; + const s = shadow && shadow[i]; + + if ( + l && l.headX === f.headX && l.headY === f.headY && l.bodyX === f.bodyX && l.bodyY === f.bodyY + && l.body === f.body && l.frontLeg === f.frontLeg && l.backLeg === f.backLeg + && l.frontFarLeg === f.frontFarLeg && l.backFarLeg === f.backFarLeg + && l.frontLegX === f.frontLegX && l.frontLegY === f.frontLegY + && l.frontFarLegX === f.frontFarLegX && l.frontFarLegY === f.frontFarLegY + && l.backLegX === f.backLegX && l.backLegY === f.backLegY + && l.backFarLegX === f.backFarLegX && l.backFarLegY === f.backFarLegY + && l.wing === f.wing + ) { + l.duration++; + } else { + fs.push({ + duration: 1, + ...f, + shadowOffset: s && s.offset || 0, + shadowFrame: s && s.frame || 0 + }); + } + }); + + return { + builtin: true, + loop, + fps, + name: `# ${index.toString().padStart(2, '0')}-${name}`, + frames: fs, + }; +} + +function toBodyAnimation({ name, loop, fps, frames }: BodyAnimation, full: boolean, switchFarClose: boolean): IBodyAnimation { + let shadow: BodyShadow[] | undefined = undefined; + + if (frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) { + shadow = flatMap(frames, f => repeat(full ? f.duration : 1, { frame: f.shadowFrame, offset: f.shadowOffset })); + } + + return { + name, + loop, + fps, + shadow, + frames: flatMap(frames, f => repeat(full ? f.duration : 1, { + body: f.body, + head: f.head, + wing: f.wing, + tail: f.tail, + frontLeg: switchFarClose ? f.frontFarLeg : f.frontLeg, + frontFarLeg: switchFarClose ? f.frontLeg : f.frontFarLeg, + backLeg: switchFarClose ? f.backFarLeg : f.backLeg, + backFarLeg: switchFarClose ? f.backLeg : f.backFarLeg, + bodyX: f.bodyX, + bodyY: f.bodyY, + headX: f.headX, + headY: f.headY, + frontLegX: switchFarClose ? f.frontFarLegX : f.frontLegX, + frontLegY: switchFarClose ? f.frontFarLegY : f.frontLegY, + frontFarLegX: switchFarClose ? f.frontLegX : f.frontFarLegX, + frontFarLegY: switchFarClose ? f.frontLegY : f.frontFarLegY, + backLegX: switchFarClose ? f.backFarLegX : f.backLegX, + backLegY: switchFarClose ? f.backFarLegY : f.backLegY, + backFarLegX: switchFarClose ? f.backLegX : f.backFarLegX, + backFarLegY: switchFarClose ? f.backLegY : f.backFarLegY, + })), + }; +} + +function compressBodyFrame(f: BodyAnimationFrame): number[] { + return dropRightWhile([ + f.body, f.head, f.wing, f.tail, f.frontLeg, f.frontFarLeg, f.backLeg, f.backFarLeg, + f.bodyX, f.bodyY, f.headX, f.headY, + f.frontLegX, f.frontLegY, f.frontFarLegX, f.frontFarLegY, + f.backLegX, f.backLegY, f.backFarLegX, f.backFarLegY, + ], x => !x); +} + +function fromHeadAnimation({ name, fps, loop, frames }: IHeadAnimation, index: number): HeadAnimation { + const fs: HeadAnimationFrame[] = []; + + frames.forEach(f => { + const l = fs[fs.length - 1]; + + if (l && l.headX === f.headX && l.headY === f.headY && l.left === f.left && l.right === f.right && l.mouth === f.mouth) { + l.duration++; + } else { + fs.push({ duration: 1, ...f }); + } + }); + + return { + builtin: true, + fps, + loop, + name: `# builtin ${index.toString().padStart(2, '0')} # ${name}`, + frames: fs, + }; +} + +function toHeadAnimation({ name, frames, fps, loop }: HeadAnimation, full: boolean): IHeadAnimation { + const fs = (full && !loop) ? repeat(fps, createDefaultHeadFrame()).concat(frames) : frames; + + return { + name, + fps, + loop, + frames: flatMap(fs, f => repeat(full ? f.duration : 1, f)), + }; +} + +function compressHeadFrame({ headX, headY, left, right, mouth }: IHeadAnimationFrame) { + return [headX, headY, left, right, mouth]; +} + +function createDefaultBodyFrame(): BodyAnimationFrame { + return { duration: 1, ...createBodyFrame([1, 1, 0, 0, 1, 1, 1, 1]), shadowFrame: 0, shadowOffset: 0 }; +} + +function createDefaultHeadFrame(): HeadAnimationFrame { + return { duration: 1, ...createHeadFrame([0, 0, 1, 1, 0]) }; +} + +// fixing helpers + +function fixBodyAnimation(a: BodyAnimation): BodyAnimation { + return { + name: a.name || '', + fps: a.fps || 24, + loop: a.loop || false, + lockFrontLegs: a.lockFrontLegs || false, + lockBackLegs: a.lockBackLegs || false, + frames: (a.frames || []).map(fixBodyFrame), + }; +} + +function fixBodyFrame(f: BodyAnimationFrame): BodyAnimationFrame { + return { + duration: f.duration || 1, + body: f.body || 0, + head: f.head || 0, + wing: f.wing || 0, + tail: f.tail || 0, + frontLeg: f.frontLeg || 0, + frontFarLeg: f.frontFarLeg || 0, + backLeg: f.backLeg || 0, + backFarLeg: f.backFarLeg || 0, + bodyX: f.bodyX || 0, + bodyY: f.bodyY || 0, + headX: f.headX || 0, + headY: f.headY || 0, + frontLegX: f.frontLegX || 0, + frontLegY: f.frontLegY || 0, + frontFarLegX: f.frontFarLegX || 0, + frontFarLegY: f.frontFarLegY || 0, + backLegX: f.backLegX || 0, + backLegY: f.backLegY || 0, + backFarLegX: f.backFarLegX || 0, + backFarLegY: f.backFarLegY || 0, + shadowFrame: f.shadowFrame || 0, + shadowOffset: f.shadowOffset || 0, + }; +} + +function fixHeadAnimation(a: HeadAnimation): HeadAnimation { + return { + name: a.name || '', + fps: a.fps || 24, + loop: a.loop || false, + lockEyes: a.lockEyes || false, + frames: (a.frames || []).map(fixHeadFrame), + }; +} + +function fixHeadFrame(f: HeadAnimationFrame): HeadAnimationFrame { + return { + duration: f.duration || 1, + headX: f.headX || 0, + headY: f.headY || 0, + left: f.left || 0, + right: f.right || 0, + mouth: f.mouth || 0, + }; +} diff --git a/src/ts/components/tools/tools-chat/tools-chat.pug b/src/ts/components/tools/tools-chat/tools-chat.pug new file mode 100644 index 0000000..a92f293 --- /dev/null +++ b/src/ts/components/tools/tools-chat/tools-chat.pug @@ -0,0 +1,11 @@ +.d-flex.m-2 + canvas.pixelart(#canvas width="1000" height="900") + .ml-2 + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + button.btn.btn-default.ml-1((click)="toggleBg()") + | Toggle BG + .mt-2 + fa-icon.supporter-1([icon]="starIcon" size="lg") + fa-icon.supporter-2.ml-1([icon]="starIcon" size="lg") + fa-icon.supporter-3.ml-1([icon]="starIcon" size="lg") diff --git a/src/ts/components/tools/tools-chat/tools-chat.ts b/src/ts/components/tools/tools-chat/tools-chat.ts new file mode 100644 index 0000000..3929340 --- /dev/null +++ b/src/ts/components/tools/tools-chat/tools-chat.ts @@ -0,0 +1,191 @@ +import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { + drawSpeechBaloon, drawNamePlate, createCommonPalettes, drawBaloon, DrawNameFlags +} from '../../../graphics/graphicsUtils'; +import { drawCanvas } from '../../../graphics/contextSpriteBatch'; +import { + GRASS_COLOR, getMessageColor, OUTLINE_COLOR, MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, + WHITE, PARTY_COLOR, RED, ORANGE, PURPLE, GREEN, YELLOW, BLUE, BLACK, CYAN, TRANSPARENT, WHISPER_COLOR +} from '../../../common/colors'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { MessageType, FontPalettes, Palette } from '../../../common/interfaces'; +import { faHome, faStar } from '../../../client/icons'; +import * as sprites from '../../../generated/sprites'; +import { disableImageSmoothing } from '../../../client/canvasUtils'; +import { mockPaletteManager } from '../../../common/ponyInfo'; +import { fontPal, fontSmallPal } from '../../../client/fonts'; +import { measureText, drawText, drawOutlinedText, lineBreak, drawTextAligned, HAlign } from '../../../graphics/spriteFont'; +import { rect } from '../../../common/rect'; +import { colorToCSS } from '../../../common/color'; + +interface Message { + label: string; + color: number; + palette?: (palettes: FontPalettes) => Palette | undefined; +} + +@Component({ + selector: 'tools-chat', + templateUrl: 'tools-chat.pug', +}) +export class ToolsChat implements AfterViewInit { + readonly homeIcon = faHome; + readonly starIcon = faStar; + @ViewChild('canvas', { static: true }) element!: ElementRef; + messages: Message[] = [ + { label: 'Chat message', color: getMessageColor(MessageType.Chat) }, + { label: 'System message', color: getMessageColor(MessageType.System) }, + { label: 'Admin message', color: getMessageColor(MessageType.Admin) }, + { label: 'Mod message', color: getMessageColor(MessageType.Mod) }, + { label: 'Announcement message', color: getMessageColor(MessageType.Announcement) }, + { label: 'Party message', color: getMessageColor(MessageType.Party) }, + { label: 'Thinking message', color: getMessageColor(MessageType.Thinking) }, + { label: 'PartyThinking message', color: getMessageColor(MessageType.PartyThinking) }, + { label: 'PartyAnnouncement message', color: getMessageColor(MessageType.PartyAnnouncement) }, + // { + // label: 'PartyAnnouncement msg ⚧', color: WHITE, + // palette: () => mockPaletteManager.add([ + // TRANSPARENT, + // ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // ]), + // }, + // { + // label: 'PartyAnnouncement msg ⚧', color: WHITE, + // palette: () => mockPaletteManager.add([ + // TRANSPARENT, + // ANNOUNCEMENT_COLOR, + // ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR, + // ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR, + // ANNOUNCEMENT_COLOR, PARTY_COLOR, + // PARTY_COLOR, PARTY_COLOR, + // PARTY_COLOR, PARTY_COLOR, + // ]), + // }, + // { label: 'Red message', color: getMessageColor(MessageType.Red) }, + // { label: 'Green message', color: getMessageColor(MessageType.Green) }, + // { label: 'Blue message', color: getMessageColor(MessageType.Blue) }, + { label: 'Supporter message 1', color: getMessageColor(MessageType.Supporter1) }, + { label: 'Supporter message 2', color: WHITE, palette: p => p.supporter2 }, + { label: 'Supporter message 3', color: WHITE, palette: p => p.supporter3 }, + { label: 'Whisper message', color: WHISPER_COLOR }, + // { label: 'Supporter message 2', color: 0xffd45aff }, + ]; + names = [ + { label: 'Regular name', color: WHITE, font: () => fontPal }, + { label: 'Party name', color: PARTY_COLOR, font: () => fontPal }, + { label: 'MODERATOR tag', color: MOD_COLOR, font: () => fontSmallPal }, + { label: 'DEVELOPER tag', color: ADMIN_COLOR, font: () => fontSmallPal }, + { label: 'SUPPORTER tag', color: PATREON_COLOR, font: () => fontSmallPal }, + { label: 'HIDDEN tag', color: ANNOUNCEMENT_COLOR, font: () => fontSmallPal }, + ]; + private bg = GRASS_COLOR; + private initialized = false; + ngAfterViewInit() { + loadAndInitSpriteSheets() + .then(() => this.initialized = true) + .then(() => this.redraw()); + } + toggleBg() { + this.bg = this.bg === GRASS_COLOR ? 0x172e14ff : GRASS_COLOR; + this.redraw(); + } + redraw() { + if (!this.initialized) + return; + + const canvas1 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { + const palettes = createCommonPalettes(mockPaletteManager); + + this.messages.forEach(({ label, color, palette }, index) => { + const size = measureText(label, fontPal); + const options = { palette: palette ? palette(palettes.mainFont) : palettes.mainFont.white }; + drawSpeechBaloon(batch, label, color, options, 10 + size.w / 2, 20 + 20 * index, size.w, size.h, 1, 5); + }); + + this.names.forEach(({ label, color, font }, index) => { + const palette = font() === fontSmallPal ? palettes.smallFont.white : palettes.mainFont.white; + drawOutlinedText(batch, label, font(), color, OUTLINE_COLOR, 190, 10 + 15 * index, { palette }); + }); + + // --- + + drawOutlinedText( + batch, '', fontSmallPal, PATREON_COLOR, OUTLINE_COLOR, 190, 120, { palette: palettes.smallFont.white }); + + // names + + drawNamePlate(batch, 'Some name 1', 220, 150, DrawNameFlags.None, palettes, 'sup1'); + drawNamePlate(batch, 'Some name 2', 220, 175, DrawNameFlags.None, palettes, 'sup2'); + drawNamePlate(batch, 'Some name 3', 220, 200, DrawNameFlags.None, palettes, 'sup3'); + + // speech baloons + + drawBaloon( + batch, { message: 'regular baloon', type: MessageType.Chat, created: 0 }, + 50, 300, rect(0, 0, 1000, 1000), palettes); + drawBaloon( + batch, { message: 'thinking baloon', type: MessageType.Thinking, created: 0 }, + 50, 330, rect(0, 0, 1000, 1000), palettes); + drawBaloon( + batch, { message: 'whisper baloon', type: MessageType.Whisper, created: 0 }, + 50, 360, rect(0, 0, 1000, 1000), palettes); + }); + + const canvas2 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { + const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette); + + for (let i = 0; i < sprites.emojiPal.length; i++) { + const x = i % 10; + const y = Math.floor(i / 10); + batch.drawSprite(sprites.emojiPal[i].sprite, WHITE, emojiPalette, 10 + x * 12, 10 + y * 12); + } + + const palette = mockPaletteManager.addArray(sprites.fontSupporter2Palette); + drawText(batch, 'Lorem 🍎 ipsum', fontPal, WHITE, 10, 150, { palette, emojiPalette }); + + const palette1 = mockPaletteManager.addArray(sprites.fontSupporter1Palette); + drawText(batch, '', fontSmallPal, WHITE, 20, 180, { palette: palette1 }); + + const palette2 = mockPaletteManager.addArray(sprites.fontSupporter2Palette); + drawText(batch, '', fontSmallPal, WHITE, 20, 195, { palette: palette2 }); + + let palette3 = mockPaletteManager.add([TRANSPARENT, RED, BLUE, ORANGE, WHITE, PURPLE, BLACK, GREEN, CYAN, YELLOW]); + palette3 = mockPaletteManager.addArray(sprites.fontSupporter3Palette); + drawText(batch, '', fontSmallPal, WHITE, 20, 210, { palette: palette3 }); + }); + + const canvas3 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { + /* tslint:disable */ + const loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce scelerisque interdum scelerisque. Suspendisse malesuada, enim in viverra ornare, dui ex laoreet ipsum, at mollis orci felis vitae ipsum. In faucibus venenatis augue, ac ornare libero. Etiam vitae aliquet neque.'; + const text = lineBreak(loremIpsum, fontPal, 200); + + batch.drawRect(WHITE, 10, 10, 200, 100); + drawText(batch, text, fontPal, BLACK, 10, 10); + + const bounds = rect(10 + 200 + 20, 10, 200, 100); + batch.drawRect(WHITE, bounds.x, bounds.y, bounds.w, bounds.h); + drawTextAligned(batch, text, fontPal, BLACK, bounds, HAlign.Right); + + const bounds2 = rect(10 + 200 + 20, 10 + 120, 200, 20); + batch.drawRect(WHITE, bounds2.x, bounds2.y, bounds2.w, bounds2.h); + drawTextAligned(batch, 'test text', fontPal, BLACK, bounds2, HAlign.Right); + }); + + const canvas = this.element.nativeElement as HTMLCanvasElement; + const context = canvas.getContext('2d')!; + context.fillStyle = colorToCSS(this.bg); + context.fillRect(0, 0, canvas.width, canvas.height); + disableImageSmoothing(context); + context.save(); + context.scale(2, 2); + context.drawImage(canvas1, 0, 0); + context.drawImage(canvas2, 300, 0); + context.drawImage(canvas3, 0, 400); + context.restore(); + } +} diff --git a/src/ts/components/tools/tools-collisions/tools-collisions.pug b/src/ts/components/tools/tools-collisions/tools-collisions.pug new file mode 100644 index 0000000..483d6b0 --- /dev/null +++ b/src/ts/components/tools/tools-collisions/tools-collisions.pug @@ -0,0 +1,5 @@ +.collision-area( + (agDrag)="drag($event)" agDragRelative="self" (contextmenu)="false" + style="position: relative; width: 1200px; height: 900px; margin: 20px; background: #222;") + canvas(#canvas width="1200" height="900") +.text-danger.ml-4(*ngIf="collided") collided diff --git a/src/ts/components/tools/tools-collisions/tools-collisions.ts b/src/ts/components/tools/tools-collisions/tools-collisions.ts new file mode 100644 index 0000000..8abcd64 --- /dev/null +++ b/src/ts/components/tools/tools-collisions/tools-collisions.ts @@ -0,0 +1,781 @@ +import { Component, ViewChild, ElementRef, OnInit } from '@angular/core'; +import { AgDragEvent } from '../../shared/directives/agDrag'; +import { Rect, Point } from '../../../common/interfaces'; +import { roundPosition } from '../../../common/positionUtils'; +import { point, distanceSquaredXY, clamp } from '../../../common/utils'; +import { createCanvas, disableImageSmoothing } from '../../../client/canvasUtils'; + +const pixelSize = 10; +const tileWidth = 32 * pixelSize; +const tileHeight = 24 * pixelSize; + +interface ExPoint extends Point { + type?: string; +} + +function toWorldX(x: number) { + return x / tileWidth; +} + +function toWorldY(y: number) { + return y / tileHeight; +} + +function toWorld(pt: Point) { + return { x: toWorldX(pt.x), y: toWorldY(pt.y) }; +} + +function toScreenX(x: number) { + return x * tileWidth; +} + +function toScreenY(y: number) { + return y * tileHeight; +} + +function toScreen(pt: Point) { + return { x: toScreenX(pt.x), y: toScreenY(pt.y) }; +} + +@Component({ + selector: 'tools-collisions', + templateUrl: 'tools-collisions.pug', +}) +export class ToolsCollisions implements OnInit { + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + start: Point = { x: 0, y: 0 }; + target: Point = { x: 0, y: 0 }; + steps: Point[] = []; + deflection: Point = { x: 0, y: 0 }; + collided = false; + rects: Rect[] = [ + { x: 1, y: 1, w: 1, h: 1 }, + { x: 3, y: 1.5, w: 0.5, h: 1 }, + { x: 3.5, y: 1.5, w: 0.5, h: 1.5 }, + { x: 2, y: 4, w: 2, h: 1 }, + { x: 3.5, y: 3.5, w: 1, h: 1 }, + ]; + collider = new Uint8Array(6 * 32 * 6 * 24); + colliderCanvas!: HTMLCanvasElement; + ngOnInit() { + const line = 6 * 32; + + for (const r of this.rects) { + const x0 = Math.floor(r.x * 32); + const y0 = Math.floor(r.y * 24); + const x1 = x0 + Math.floor(r.w * 32); + const y1 = y0 + Math.floor(r.h * 24); + + for (let y = y0; y < y1; y++) { + for (let x = x0; x < x1; x++) { + this.collider[x + y * line] = 1; + } + } + } + + for (let y = 60, x0 = 40; y < 70; y++ , x0++) { + for (let x = x0; x < 60; x++) { + this.collider[x + y * line] = 1; + } + } + + for (let y = 70, x0 = 80; y < 80; y++ , x0--) { + for (let x = x0; x < (x0 + 20); x++) { + this.collider[x + y * line] = 1; + } + } + + for (let y = 10, x0 = 80; y < 20; y++ , x0 -= 2) { + for (let x = x0; x < (x0 + 20); x++) { + this.collider[x + y * line] = 1; + } + } + + this.colliderCanvas = createCanvas(6 * 32, 6 * 24); + const context = this.colliderCanvas.getContext('2d')!; + const data = context.getImageData(0, 0, this.colliderCanvas.width, this.colliderCanvas.height); + + for (let i = 0; i < this.collider.length; i++) { + if (this.collider[i] !== 0) { + data.data[i * 4 + 0] = 255; + data.data[i * 4 + 1] = 255; + data.data[i * 4 + 2] = 0; + data.data[i * 4 + 3] = 255; + } + } + + context.putImageData(data, 0, 0); + + this.draw(); + } + drag({ type, x, y, dx, dy, event: { shiftKey } }: AgDragEvent) { + if (type === 'start') { + this.start = toWorld({ x, y }); + } + + if (shiftKey) { + if (Math.abs(dx) > Math.abs(dy)) { + this.target = toWorld({ x, y: toScreenY(this.start.y) }); + } else { + this.target = toWorld({ x: toScreenX(this.start.x), y }); + } + } else { + this.target = toWorld({ x, y }); + } + + this.draw(); + } + draw() { + if (true) { + // roundPosition(this.start); + // roundPosition(this.target); + + this.steps = []; + this.collided = false; + + let steps = 100; + const current = point(this.start.x, this.start.y); + + while (--steps > 0) { + const collision = point(0, 0); + + if (isColliding(current.x, current.y, this.target.x, this.target.y, this.rects, collision)) { + this.collided = true; + this.steps.push(collision); + current.x = collision.x; + current.y = collision.y; + } else { + this.steps.push(point(this.target.x, this.target.y)); + } + + break; + } + + if (steps <= 0) { + console.error('Failed'); + } + } else { + const collision = getClosestCollisionOld(this.start, this.target, this.rects); + + if (equal(this.target, collision)) { + this.deflection = { ...this.target }; + } else { + const coll = getClosestCollisionOld(collision, this.target, this.rects); + + if (equal(collision, coll)) { + const horizontal = getClosestCollisionOld(collision, { x: this.target.x, y: collision.y }, this.rects); + const vertical = getClosestCollisionOld(collision, { x: collision.x, y: this.target.y }, this.rects); + this.deflection = equal(collision, horizontal) ? vertical : horizontal; + } else { + console.log('not', collision, coll); + } + } + } + + const checkedTiles = new Set(); + + // plot + + { + let srcX = this.start.x; + let srcY = this.start.y; + let dstX = this.target.x; + let dstY = this.target.y; + + if (srcX > dstX) { + const tx = srcX; + const ty = srcY; + srcX = dstX; + srcY = dstY; + dstX = tx; + dstY = ty; + } + + const x0 = Math.floor(srcX); + const y0 = Math.floor(srcY); + const x1 = Math.floor(dstX); + const y1 = Math.floor(dstY); + + let steps = 100; + let x = x0; + let y = y0; + + const DYbyDX = (dstY - srcY) / (dstX - srcX); + + checkedTiles.add(`${x}-${y}`); + + if (srcY < dstY) { + while (--steps && (x !== x1 || y !== y1)) { + const dx = (x + 1) - srcX; + const dy = dx * DYbyDX; + const ay = srcY + dy; + + if (ay < (y + 1)) { + x++; + } else { + y++; + } + + checkedTiles.add(`${x}-${y}`); + } + } else { + while (--steps && (x !== x1 || y !== y1)) { + const dx = (x + 1) - srcX; + const dy = dx * DYbyDX; + const ay = srcY + dy; + + if (ay >= y) { + x++; + } else { + y--; + } + + checkedTiles.add(`${x}-${y}`); + } + } + } + + // end + + const result = checkInLine( + this.start.x * 32, this.start.y * 24, + this.target.x * 32, this.target.y * 24, + this.collider); + + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const context = canvas.getContext('2d')!; + + context.fillStyle = '#444'; + context.fillRect(0, 0, canvas.width, canvas.height); + + const xs = Math.ceil(canvas.width / tileWidth); + const ys = Math.ceil(canvas.height / tileHeight); + + context.save(); + context.fillStyle = '#533'; + context.globalAlpha = 0.5; + + for (let y = 0; y < ys; y++) { + for (let x = 0; x < xs; x++) { + if (checkedTiles.has(`${x}-${y}`)) { + context.fillRect(x * tileWidth, y * tileHeight, tileWidth - 1, tileHeight - 1); + } + } + } + + context.restore(); + + context.save(); + context.scale(pixelSize, pixelSize); + context.globalAlpha = 0.2; + disableImageSmoothing(context); + context.drawImage(this.colliderCanvas, 0, 0); + context.restore(); + + context.save(); + context.strokeStyle = 'white'; + context.globalAlpha = 0.05; + context.beginPath(); + + for (let y = 0; y < canvas.height; y += pixelSize) { + context.moveTo(0, round5(y)); + context.lineTo(canvas.width, round5(y)); + } + + for (let x = 0; x < canvas.width; x += pixelSize) { + context.moveTo(round5(x), 0); + context.lineTo(round5(x), canvas.height); + } + + context.stroke(); + context.restore(); + + context.save(); + context.strokeStyle = 'white'; + context.globalAlpha = 0.15; + context.beginPath(); + + for (let y = 0; y < canvas.height; y += tileHeight) { + context.moveTo(0, round5(y)); + context.lineTo(canvas.width, round5(y)); + } + + for (let x = 0; x < canvas.width; x += tileWidth) { + context.moveTo(round5(x), 0); + context.lineTo(round5(x), canvas.height); + } + + context.stroke(); + context.restore(); + + context.save(); + context.fillStyle = 'lime'; + context.globalAlpha = 0.2; + + let collided = false; + + for (const pt of result.checks) { + const colliding = isCollidingWithRect(pt.x, pt.y, this.rects); + context.fillStyle = pt.type === 'break' ? 'blue' : 'lime'; // colliding ? 'red' : (collided ? 'orange' : 'lime'); + collided = collided || colliding; + context.fillRect(pt.x * pixelSize, pt.y * pixelSize, pixelSize, pixelSize); + } + + context.restore(); + + // context.save(); + // context.strokeStyle = 'orange'; + // context.setLineDash([3, 3]); + + // for (const rect of this.rects) { + // context.strokeRect( + // round5(toScreenX(rect.x)), round5(toScreenY(rect.y)), + // Math.round(toScreenX(rect.w)), Math.round(toScreenY(rect.h))); + // } + + // context.restore(); + + drawLine(context, toScreen(this.start), toScreen(this.target), 'gray', true); + + let last = this.start; + + this.steps = [point(result.result.x / 32, result.result.y / 24)]; + + for (const c of this.steps) { + drawLine(context, toScreen(last), toScreen(c), 'lime', true); + last = c; + } + + drawPoint(context, toScreen(this.start), 'greenyellow'); + drawPoint(context, toScreen(this.target), 'red'); + + for (const c of this.steps) { + const colliding = isColliding(c.x, c.y, c.x, c.y, this.rects, point(0, 0)); + drawPoint(context, toScreen(c), colliding ? 'red' : 'yellow'); + } + } +} + +function drawLine(context: CanvasRenderingContext2D, a: Point, b: Point, color: string, arrow = false) { + context.save(); + context.strokeStyle = color; + // context.lineWidth = 2; + context.beginPath(); + context.moveTo(a.x, a.y); + context.lineTo(b.x, b.y); + context.stroke(); + context.restore(); + + if (arrow && (a.x !== b.x || a.y !== b.y)) { + const scale = 0.5; + context.save(); + context.fillStyle = color; + context.translate(b.x, b.y); + context.rotate(Math.atan2(b.y - a.y, b.x - a.x)); + context.beginPath(); + context.moveTo(0, 0); + context.lineTo(-12 * scale, -6 * scale); + context.lineTo(-12 * scale, 6 * scale); + context.closePath(); + context.fill(); + context.restore(); + } +} + +function drawPoint(context: CanvasRenderingContext2D, { x, y }: Point, color: string) { + context.save(); + context.shadowColor = 'black'; + context.shadowBlur = 3; + context.fillStyle = color; + context.globalAlpha = 0.5; + context.beginPath(); + context.arc(x, y, 2, 0, Math.PI * 2); + context.fill(); + context.restore(); +} + +function round5(x: number) { + return Math.ceil(x) - 0.5; +} + +function equal(a: Point, b: Point) { + return a.x === b.x && a.y === b.y; +} + +function isColliding(srcX: number, srcY: number, dstX: number, dstY: number, rects: Rect[], collision: Point): boolean { + const temp = point(0, 0); + let collided = false; + collision.x = dstX; + collision.y = dstY; + + for (const r of rects) { + if (getCollision(srcX, srcY, dstX, dstY, r.x, r.y, r.x + r.w, r.y + r.h, temp)) { + if (!collided || (distanceSquaredXY(srcX, srcY, temp.x, temp.y) < distanceSquaredXY(srcX, srcY, collision.x, collision.y))) { + collision.x = temp.x; + collision.y = temp.y; + collided = true; + } + } + } + + roundPosition(collision); + return collided; +} + +function getClosestCollisionOld(a: Point, b: Point, rects: Rect[]) { + return rects.reduce((pt, r) => getCollisionTest(a, pt, r) || pt, { ...b }); +} + +function getCollisionTest({ x, y }: Point, b: Point, r: Rect): Point | undefined { + const vx = b.x - x; + const vy = b.y - y; + const p = [-vx, vx, -vy, vy]; + const q = [x - r.x, r.x + r.w - x, y - r.y, r.y + r.h - y]; + let u1 = -999999; + let u2 = 999999; + + for (let i = 0; i < 4; i++) { + if (p[i] === 0) { + if (q[i] < 0) { + return undefined; + } + } else { + const t = q[i] / p[i]; + + if (p[i] < 0 && u1 < t) { + u1 = t; + } else if (p[i] > 0 && u2 > t) { + u2 = t; + } + } + } + + if (u1 > u2 || u1 > 1 || u1 < 0) { + return undefined; + } + + return { + x: x + u1 * vx, + y: y + u1 * vy, + }; +} + +function isCollidingWithRect(x: number, y: number, rects: Rect[]) { + for (const r of rects) { + const x0 = Math.floor(r.x * 32) | 0; + const y0 = Math.floor(r.y * 24) | 0; + const x1 = Math.ceil((r.x + r.w) * 32) | 0; + const y1 = Math.ceil((r.y + r.h) * 24) | 0; + + if (x >= x0 && x < x1 && y >= y0 && y < y1) { + return true; + } + } + + return false; +} + +function checkInLine(srcX: number, srcY: number, dstX: number, dstY: number, collider: Uint8Array) { + function isColliding(x: number, y: number) { + return x < 0 || y < 0 || x >= (6 * 32) || y >= (6 * 32) || collider[x + y * (6 * 32)] !== 0; + } + + const checks: ExPoint[] = []; + const result = point(srcX, srcY); + + const x0 = Math.floor(srcX) | 0; + const y0 = Math.floor(srcY) | 0; + const x1 = Math.floor(dstX) | 0; + const y1 = Math.floor(dstY) | 0; + + let minX = Math.min(x0, x1) | 0; + let maxX = Math.max(x0, x1) | 0; + let minY = Math.min(y0, y1) | 0; + let maxY = Math.max(y0, y1) | 0; + + let x = x0 | 0; + let y = y0 | 0; + + checks.push({ x, y }); + + let actualX = x | 0; + let actualY = y | 0; + + const a = (dstY - srcY) / (dstX - srcX); + const b = srcY - a * srcX; + const useGt = srcY < dstY; + + let stepXT = 0 | 0, stepYT = 0 | 0; + let stepXF = 0 | 0, stepYF = 0 | 0; + let ox = 0, oy = 0; + + const shiftRight = srcX <= dstX; + const shiftLeft = srcX >= dstX; + const shiftUp = srcY >= dstY; + const shiftDown = srcY <= dstY; + const horizontalOrVertical = srcX === dstX || srcY === dstY; + + if (srcX < dstX) { + if (srcY < dstY) { + ox = 1; + oy = 1; + stepYT = 1 | 0; + stepXF = 1 | 0; + } else { + ox = 1; + stepYT = -1 | 0; + stepXF = 1 | 0; + } + } else if (srcX > dstX) { + if (srcY < dstY) { + oy = 1; + stepYT = 1 | 0; + stepXF = -1 | 0; + } else { + stepYT = -1 | 0; + stepXF = -1 | 0; + } + } else { + if (srcY < dstY) { + stepYF = stepYT = 1 | 0; + } else { + stepYF = stepYT = -1 | 0; + } + } + + for (let steps = 1000; steps; steps--) { + const fx = a * (x + ox) + b; + const fy = y + oy; + + let tx = 0 | 0; + let ty = 0 | 0; + + if (useGt ? (fx > fy) : (fx < fy)) { + tx = (tx + stepXT) | 0; + ty = (ty + stepYT) | 0; + } else { + tx = (tx + stepXF) | 0; + ty = (ty + stepYF) | 0; + } + + x = (x + tx) | 0; + y = (y + ty) | 0; + + if (x < minX || x > maxX || y < minY || y > maxY) { + break; + } + + let actualNX = (actualX + tx) | 0; + let actualNY = (actualY + ty) | 0; + + let collides = isColliding(actualNX, actualNY); + let canMove = false; + + if (collides) { + if (tx !== 0) { + let canShiftUp = false; + let canShiftDown = false; + + if (shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1)) && !isColliding(actualNX, actualY - 1)) { + actualNX = actualX; + actualNY -= 1; + dstY -= 1; + collides = false; + } else if (shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1)) && !isColliding(actualNX, actualY + 1)) { + actualNX = actualX; + actualNY += 1; + dstY += 1; + collides = false; + } else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2)) { + actualNX = actualX; + actualNY -= 1; + dstY -= 1; + collides = false; + } else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2)) { + actualNX = actualX; + actualNY += 1; + dstY += 1; + collides = false; + } + + canMove = canShiftUp || canShiftDown; + } else { + let canShiftLeft = false; + let canShiftRight = false; + + if (shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY)) && !isColliding(actualX - 1, actualNY)) { + actualNX -= 1; + actualNY = actualY; + dstX -= 1; + collides = false; + } else if (shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY)) && !isColliding(actualX + 1, actualNY)) { + actualNX += 1; + actualNY = actualY; + dstX += 1; + collides = false; + } else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY)) { + actualNX -= 1; + actualNY = actualY; + dstX -= 1; + collides = false; + } else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY)) { + actualNX += 1; + actualNY = actualY; + dstX += 1; + collides = false; + } + + canMove = canShiftLeft || canShiftRight; + } + } + + if (!collides) { + actualX = actualNX; + actualY = actualNY; + checks.push({ x: actualX, y: actualY }); + } else if (!canMove || horizontalOrVertical) { + checks.push({ x: actualX, y: actualY, type: 'break' }); + break; + } + } + + const epsilon = 1 / 1024; + const left = Math.min(x0, actualX); + const right = Math.max(x0 + 1, actualX + 1) - epsilon; + const top = Math.min(y0, actualY); + const bottom = Math.max(y0 + 1, actualY + 1) - epsilon; + + result.x = clamp(dstX, left, right); + result.y = clamp(dstY, top, bottom); + + return { checks, result }; +} + +// if (srcX < dstX) { +// if (srcY < dstY) { +// if ((a * (x + 1) + b) > (y + 1)) { +// ty++; +// } else { +// tx++; +// } +// } else { +// if ((a * (x + 1) + b) < y) { +// ty--; +// } else { +// tx++; +// } +// } +// } else if (srcX > dstX) { +// if (srcY < dstY) { +// if ((a * x + b) > (y + 1)) { +// ty++; +// } else { +// tx--; +// } +// } else { +// if ((a * x + b) < y) { +// ty--; +// } else { +// tx--; +// } +// } +// } else { +// if (srcY < dstY) { +// ty++; +// } else { +// ty--; +// } +// } + +function getCollision( + srcX: number, srcY: number, dstX: number, dstY: number, x0: number, y0: number, x1: number, y1: number, out: Point +): boolean { + const vx = dstX - srcX; + const vy = dstY - srcY; + let u1 = -999999; + let u2 = 999999; + + { + const p = -vx; + const q = srcX - x0; + + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; + + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } + + { + const p = vx; + const q = x1 - srcX; + + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; + + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } + + { + const p = -vy; + const q = srcY - y0; + + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; + + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } + + { + const p = vy; + const q = y1 - srcY; + + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; + + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } + + if (u1 > u2 || u1 > 1 || u1 < 0) { + return false; + } + + out.x = srcX + u1 * vx; + out.y = srcY + u1 * vy; + return true; +} diff --git a/src/ts/components/tools/tools-entity/tools-entity.pug b/src/ts/components/tools/tools-entity/tools-entity.pug new file mode 100644 index 0000000..7fbf8cb --- /dev/null +++ b/src/ts/components/tools/tools-entity/tools-entity.pug @@ -0,0 +1,75 @@ +.container-fluid.p-1 + .form-inline + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + .form-group + scale-picker.ml-1([(scale)]="scale" (scaleChange)="redraw()") + .form-group + .input-group.ml-1(style="width: 200px;") + input.form-control([(ngModel)]="name" placeholder="name") + .input-group-append.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + .dropdown-menu.dropdown-menu-right(*dropdownMenu) + a.dropdown-item(*ngFor="let e of entities" (click)="setEntity(e)") + | {{e.name}} + button.btn.btn-default.ml-1((click)="saveEntity()" [disabled]="!name") + fa-icon([icon]="saveIcon" [fixedWidth]="true") + button.btn.btn-danger.ml-1((click)="setEntity(null)") + fa-icon([icon]="eraserIcon" [fixedWidth]="true") + button.btn.btn-danger.ml-1((click)="removeEntity()") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + + .float-left.mt-1.mr-2 + canvas(#canvas width=512 height=512 (mousedown)="mousedown($event)" (agDrag)="drag($event)" agDragRelative="self") + + .form-group.d-flex + label.control-label.text-muted draw: + custom-checkbox.ml-2([(checked)]="drawCenter" (checkedChange)="changed()") center + custom-checkbox.ml-2([(checked)]="drawSelection" (checkedChange)="changed()") selection + custom-checkbox.ml-2([(checked)]="drawHold" (checkedChange)="changed()") as held + + div + .form-group(*ngFor="let p of parts") + .form-inline(*ngIf="p.type === 'sprite'") + label.text-muted sprite: + input.form-control.ml-1( + [(ngModel)]="p.sprite" (input)="changed()" style="width: 200px;" [typeahead]="sprites" (typeaheadOnSelect)="changed()") + label.ml-1 x: + input.form-control.input-sm.ml-1(type="number" [ngModel]="-p.x" (ngModelChange)="p.x = -$event" (input)="changed()" min="-1000" max="1000") + label.ml-1 y: + input.form-control.input-sm.ml-1(type="number" [ngModel]="-p.y" (ngModelChange)="p.y = -$event" (input)="changed()" min="-1000" max="1000") + button.btn.btn-sm.btn-default.ml-1((click)="centerPart(p)" title="Center sprite") + fa-icon([icon]="crosshairsIcon" [fixedWidth]="true") + button.btn.btn-sm.btn-danger.ml-1((click)="removePart(p)" title="Remove part") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + + .form-inline(*ngIf="p.type === 'cover' || p.type === 'collider' || p.type === 'pickable'") + label.text-muted {{p.type}}: + label.ml-1 x: + input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.x" (input)="changed()" min="-1000" max="1000") + label.ml-1 y: + input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.y" (input)="changed()" min="-1000" max="1000") + .form-inline(*ngIf="p.type === 'cover' || p.type === 'collider'") + label.ml-1 w: + input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.w" (input)="changed()" min="1" max="1000") + label.ml-1 h: + input.form-control.input-sm.ml-1(type="number" [(ngModel)]="p.h" (input)="changed()" min="1" max="1000") + button.btn.btn-sm.btn-danger.ml-1((click)="removePart(p)" title="Remove part") + fa-icon([icon]="trashIcon" [fixedWidth]="true") + + hr + + .form-group.form-inline + button.btn.btn-sm.btn-default.ml-1((click)="addPart('sprite')") + fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true") + | Sprite + button.btn.btn-sm.btn-default.ml-1((click)="addPart('cover')") + fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true") + | Cover + button.btn.btn-sm.btn-default.ml-1((click)="addPart('collider')") + fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true") + | Collider + button.btn.btn-sm.btn-default.ml-1((click)="addPart('pickable')") + fa-icon.mr-1([icon]="plusIcon" [fixedWidth]="true") + | Pickable diff --git a/src/ts/components/tools/tools-entity/tools-entity.ts b/src/ts/components/tools/tools-entity/tools-entity.ts new file mode 100644 index 0000000..8cd6708 --- /dev/null +++ b/src/ts/components/tools/tools-entity/tools-entity.ts @@ -0,0 +1,423 @@ +import { Component, OnInit, HostListener, ElementRef, ViewChild } from '@angular/core'; +import { findLastIndex, compact } from 'lodash'; +import { removeItem, containsPoint, isKeyEventInvalid, cloneDeep, att } from '../../../common/utils'; +import * as sprites from '../../../generated/sprites'; +import { setPaletteManager, pickable, getRenderableBounds, createPalette } from '../../../common/mixins'; +import { parseColor, withAlphaFloat } from '../../../common/color'; +import { PaletteManager, releasePalette } from '../../../graphics/paletteManager'; +import { PaletteSpriteBatch, Rect, PaletteRenderable, Dict, Entity, EntityPart, DrawOptions } from '../../../common/interfaces'; +import { SHADOW_COLOR, WHITE, BLACK, TRANSPARENT, RED, ORANGE, PURPLE } from '../../../common/colors'; +import { Key } from '../../../client/input/input'; +import { drawOutline } from '../../../graphics/graphicsUtils'; +import { drawCanvas, ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { AgDragEvent } from '../../shared/directives/agDrag'; +import { faHome, faSave, faEraser, faTrash, faPlus, faCrosshairs } from '../../../client/icons'; +import { StorageService } from '../../services/storageService'; +import { mockPaletteManager, toPalette } from '../../../common/ponyInfo'; +import { OFFLINE_PONY } from '../../../common/constants'; +import { drawPony } from '../../../client/ponyDraw'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; +import { createBaseEntity } from '../../../common/entities'; +import { decompressPonyString } from '../../../common/compressPony'; +import { disableImageSmoothing } from '../../../client/canvasUtils'; +import { toScreenX, toScreenYWithZ } from '../../../common/positionUtils'; + +const COVER = parseColor('DeepSkyBlue'); +const COLLIDER = ORANGE; +const PICKABLE = PURPLE; +const BG = parseColor('lightgreen'); +const LINES = withAlphaFloat(BLACK, 0.1); +const SELECTION = withAlphaFloat(WHITE, 0.5); +const DEFAULT_PALETTE = [TRANSPARENT, WHITE]; +const paletteManager = new PaletteManager(); +const defaultPalette = paletteManager.add(DEFAULT_PALETTE); +const X = 128; +const Y = 190; + +const colors: Dict = { + cover: COVER, + collider: COLLIDER, + pickable: PICKABLE, +}; + +interface BasePart { + x: number; + y: number; +} + +interface BoundsPart extends BasePart { + w: number; + h: number; +} + +interface SpritePart extends BasePart { + type: 'sprite'; + sprite: string; +} + +interface CoverPart extends BoundsPart { + type: 'cover'; +} + +interface ColliderPart extends BoundsPart { + type: 'collider'; +} + +interface PickablePart extends BasePart { + type: 'pickable'; +} + +type Part = SpritePart | CoverPart | ColliderPart | PickablePart; + +interface PartEntity { + name: string; + parts: Part[]; +} + +interface EntityData { + parts?: Part[]; + entities?: PartEntity[]; +} + +@Component({ + selector: 'tools-entity', + templateUrl: 'tools-entity.pug', +}) +export class ToolsEntity implements OnInit { + readonly homeIcon = faHome; + readonly saveIcon = faSave; + readonly eraserIcon = faEraser; + readonly trashIcon = faTrash; + readonly crosshairsIcon = faCrosshairs; + readonly plusIcon = faPlus; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + scale = 2; + name = ''; + drawCenter = true; + drawSelection = true; + drawHold = false; + sprites = Object.keys(sprites).filter(key => { + const s = (sprites as any)[key] as any; + return !!(s && s.color); + }); + selectedPart = -1; + entities: PartEntity[] = []; + parts: Part[] = []; + pony = toPalette(decompressPonyString(OFFLINE_PONY), mockPaletteManager); + private startX = 0; + private startY = 0; + constructor(private storage: StorageService) { + } + ngOnInit() { + setPaletteManager(paletteManager); + loadAndInitSpriteSheets().then(() => this.changed()); + + const data = this.load(); + this.parts = compact(data.parts || [this.createSpritePart('apple')]); + this.entities = compact(data.entities || []); + } + @HostListener('window:keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) { + e.preventDefault(); + } + } + handleKey(keyCode: number) { + if (keyCode === Key.UP) { + this.movePart(0, -1); + } else if (keyCode === Key.DOWN) { + this.movePart(0, 1); + } else if (keyCode === Key.LEFT) { + this.movePart(-1, 0); + } else if (keyCode === Key.RIGHT) { + this.movePart(1, 0); + } else { + return false; + } + + return true; + } + mousedown(e: MouseEvent) { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const { left, top } = canvas.getBoundingClientRect(); + const x = (e.pageX - left) / this.scale - X; + const y = (e.pageY - top) / this.scale - Y; + + this.selectedPart = findLastIndex(this.parts, p => { + if (this.drawHold) { + return p.type === 'pickable'; + } else { + const bounds = getBounds(p); + return !!bounds && containsPoint(0, 0, bounds, x, y); + } + }); + + this.changed(); + } + drag({ dx, dy, type }: AgDragEvent) { + const part = this.parts[this.selectedPart]; + + if (part) { + if (type === 'start') { + this.startX = part.x; + this.startY = part.y; + } + + part.x = Math.round(this.startX + dx / this.scale); + part.y = Math.round(this.startY + dy / this.scale); + } + + this.changed(); + } + setEntity(entity: PartEntity | null) { + if (entity) { + this.name = entity.name; + this.parts = entity.parts; + } else { + this.name = ''; + this.parts = []; + } + + this.changed(); + } + saveEntity() { + if (this.name) { + const existing = this.entities.find(e => e.name === this.name); + + if (existing) { + existing.parts = cloneDeep(this.parts); + } else { + this.entities.push({ + name: this.name, + parts: cloneDeep(this.parts), + }); + } + + this.changed(); + } + } + removeEntity() { + removeItem(this.entities, this.entities.find(e => e.name === this.name)); + this.changed(); + } + movePart(dx: number, dy: number) { + const part = this.parts[this.selectedPart]; + + if (part) { + part.x += dx; + part.y += dy; + } + + this.changed(); + } + changed() { + requestAnimationFrame(() => this.redraw()); + } + redraw() { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const scale = this.scale; + const width = Math.ceil(canvas.width / scale); + const height = Math.ceil(canvas.height / scale); + + const draw = (batch: ContextSpriteBatch) => { + if (this.drawCenter) { + batch.drawRect(LINES, 0, Y, width, 1); + batch.drawRect(LINES, X, 0, 1, height); + } + + this.parts.forEach(p => drawPart(batch, p, X, Y)); + + const part = this.parts[this.selectedPart] as Part | undefined; + + if (part) { + const bounds = getBounds(part); + + if (this.drawSelection && bounds) { + const sx = X + bounds.x; + const sy = Y + bounds.y; + drawOutline(batch, SELECTION, sx - 1, sy - 1, bounds.w + 2, bounds.h + 2); + } + } + + if (this.drawCenter) { + batch.drawRect(RED, X, Y, 1, 1); + } + }; + + const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => { + if (this.drawHold) { + const spritePart = this.parts.find(p => p.type === 'sprite') as SpritePart | undefined; + const pickablePart = this.parts.find(p => p.type === 'pickable') as PickablePart | undefined; + + const holding: Entity | undefined = spritePart && pickablePart && getSprite(spritePart.sprite) ? { + ...createBaseEntity(0, 0, 0, 0), + ...drawMixin(getSprite(spritePart.sprite), -spritePart.x, -spritePart.y), + ...pickable(pickablePart.x, pickablePart.y), + } : undefined; + + const state = { ...defaultPonyState(), holding }; + drawPony(batch, this.pony, state, X, Y, defaultDrawPonyOptions()); + } else { + draw(batch); + } + }); + + drawBufferScaled(canvas, buffer, scale); + + this.save(); + } + createSpritePart(sprite: string): SpritePart { + return { + type: 'sprite', + sprite, + x: 0, + y: 0, + }; + } + createBoundsPart(type: any): CoverPart { + return { + type, + x: 0, + y: 0, + w: 10, + h: 10, + }; + } + createPart(type: string): Part { + switch (type) { + case 'sprite': + return this.createSpritePart('apple'); + case 'cover': + case 'collider': + return this.createBoundsPart(type); + case 'pickable': + return { type, x: 0, y: 0 }; + default: + throw new Error(`Invalid type (${type})`); + } + } + addPart(type: string) { + this.parts.push(this.createPart(type)); + this.changed(); + } + removePart(part: Part) { + removeItem(this.parts, part); + this.changed(); + } + centerPart(part: Part) { + if (part.type === 'sprite') { + const sprite = getSprite(part.sprite); + const color = sprite && sprite.color; + + if (color) { + part.x = Math.round(-color.ox - color.w / 2); + part.y = Math.round(-color.oy - color.h / 2); + } + } + + this.changed(); + } + private save() { + this.storage.setJSON('tools-entity', { + parts: this.parts, + entities: this.entities, + }); + } + private load() { + return this.storage.getJSON('tools-entity', {}); + } +} + +function getBounds(part: Part): Rect | undefined { + if (part.type === 'sprite') { + const sprite = getSprite(part.sprite); + const color = sprite && sprite.color; + + if (color) { + return { + x: part.x + color.ox, + y: part.y + color.oy, + w: color.w, + h: color.h, + }; + } + } else if (part.type === 'cover' || part.type === 'collider') { + return part; + } + + return undefined; +} + +function getSprite(name: string): PaletteRenderable { + return (sprites as any)[name]; +} + +function drawSpritePart(batch: PaletteSpriteBatch, part: SpritePart, px: number, py: number) { + const sprite = getSprite(part.sprite); + + if (!sprite) + return; + + const x = px + part.x; + const y = py + part.y; + const palette = paletteManager.addArray(sprite.palettes![0]); + + sprite.shadow && batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y); + sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y); + + releasePalette(palette); +} + +function drawPart(batch: PaletteSpriteBatch, part: Part, x: number, y: number) { + if (part.type === 'sprite') { + return drawSpritePart(batch, part, x, y); + } else if (part.type === 'cover' || part.type === 'collider') { + return drawOutline(batch, colors[part.type], part.x + x, part.y + y, part.w, part.h); + } else if (part.type === 'pickable') { + return drawOutline(batch, colors[part.type], part.x + x, part.y + y, 1, 1); + } else { + throw new Error(`Invalid part type (${(part as any).type})`); + } +} + +function drawBufferScaled(canvas: HTMLCanvasElement, buffer: HTMLCanvasElement, scale: number) { + const context = canvas.getContext('2d')!; + context.save(); + disableImageSmoothing(context); + context.scale(scale, scale); + context.drawImage(buffer, 0, 0); + context.restore(); +} + +function drawMixin(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): EntityPart { + const bounds = getRenderableBounds(sprite, dx, dy); + + if (SERVER && !TESTS) + return { bounds }; + + const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); + const palette = createPalette(att(sprite.palettes, paletteIndex)); + + return { + bounds, + draw(this: Entity, batch: PaletteSpriteBatch, options: DrawOptions) { + const x = toScreenX(this.x + (this.ox || 0)) - dx; + const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy; + const opacity = 1 - 0.6 * (this.coverLifting || 0); + + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } + + batch.globalAlpha = opacity; + + if (sprite.color !== undefined) { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } + + batch.globalAlpha = 1; + }, + palettes: compact([defaultPalette, palette]), + }; +} diff --git a/src/ts/components/tools/tools-expressions/tools-expressions.pug b/src/ts/components/tools/tools-expressions/tools-expressions.pug new file mode 100644 index 0000000..705ad82 --- /dev/null +++ b/src/ts/components/tools/tools-expressions/tools-expressions.pug @@ -0,0 +1,17 @@ +.form-inline.p-1 + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + + .form-group + label.ml-2 + scale-picker([(scale)]="scale" (scaleChange)="redraw()") + + .form-group + label.control-label.ml-2 columns: + input.form-control.ml-1(type="number" [(ngModel)]="columns" (input)="redraw()" style="width: 80px;") + + .form-group + button.btn.btn-default.ml-2((click)="png()") PNG + +canvas(#canvas) diff --git a/src/ts/components/tools/tools-expressions/tools-expressions.ts b/src/ts/components/tools/tools-expressions/tools-expressions.ts new file mode 100644 index 0000000..abd6a7e --- /dev/null +++ b/src/ts/components/tools/tools-expressions/tools-expressions.ts @@ -0,0 +1,107 @@ +import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; +import { PonyInfo, PonyState } from '../../../common/interfaces'; +import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; +import { expressions } from '../../../common/expressions'; +import { createCanvas, disableImageSmoothing, saveCanvas } from '../../../client/canvasUtils'; +import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import { RED } from '../../../common/colors'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { createBodyAnimation } from '../../../client/ponyAnimations'; +import { drawPony } from '../../../client/ponyDraw'; +import { faHome } from '../../../client/icons'; +import { paletteSpriteSheet } from '../../../generated/sprites'; + +@Component({ + selector: 'tools-expressions', + templateUrl: 'tools-expressions.pug', +}) +export class ToolsExpressions implements OnInit { + readonly homeIcon = faHome; + scale = 2; + columns = 12; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + constructor() { + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + redraw() { + this.draw(); + } + png() { + this.draw(); + saveCanvas(this.canvas.nativeElement, 'expressions.png'); + } + private draw() { + drawSheet(this.canvas.nativeElement, this.scale, this.columns); + } +} + +function drawSheet(canvas: HTMLCanvasElement, scale: number, columns: number, bg = 'lightgreen'): HTMLCanvasElement { + const frameWidth = 55; + const frameOffset = 50; + const frameHeight = 30; + const buffer = createCanvas(frameWidth, frameHeight); + const batch = new ContextSpriteBatch(buffer); + const pony = createPony(); + const state = createState(); + const info = toPalette(pony); + const filteredExpressions = expressions.filter(([, expr]) => !!expr).slice(2); + const rows = Math.ceil(filteredExpressions.length / columns); + const options = defaultDrawPonyOptions(); + + canvas.width = ((frameOffset * (columns - 1)) + frameWidth) * scale; + canvas.height = (frameHeight * rows) * scale; + + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + viewContext.scale(scale, scale); + + if (bg) { + viewContext.fillStyle = bg; + viewContext.fillRect(0, 0, canvas.width, canvas.height); + } + + viewContext.font = 'normal 6px monospace'; + viewContext.textAlign = 'right'; + viewContext.fillStyle = 'black'; + + filteredExpressions.forEach(([name, [right, left, muzzle, rightIris = 0, leftIris = 0, extra = 0]]: any, i) => { + state.expression = { right, left, muzzle, rightIris, leftIris, extra }; + + batch.start(paletteSpriteSheet, 0); + drawPony(batch, info, state, 35, 50, options); + batch.end(); + + const x = (i % columns) * frameOffset; + const y = Math.floor(i / columns) * frameHeight; + + viewContext.drawImage(buffer, x, y); + viewContext.fillText(name, x + 18, y + 20); + }); + + viewContext.restore(); + return canvas; +} + +function createState(): PonyState { + const state = defaultPonyState(); + state.blushColor = RED; + state.animation = createBodyAnimation('', 24, false, [[0, 1]]); + return state; +} + +function createPony(): PonyInfo { + const pony = createDefaultPony(); + pony.mane!.type = 0; + pony.backMane!.type = 0; + pony.tail!.type = 0; + pony.coatFill = 'dec078'; + pony.lockCoatOutline = true; + pony.lockBackLegAccessory = false; + pony.eyeColorRight = 'cornflowerblue'; + return syncLockedPonyInfo(pony); +} diff --git a/src/ts/components/tools/tools-index/tools-index.pug b/src/ts/components/tools/tools-index/tools-index.pug new file mode 100644 index 0000000..3bee40e --- /dev/null +++ b/src/ts/components/tools/tools-index/tools-index.pug @@ -0,0 +1,16 @@ +.m-4 + ul.nav.nav-pills + a.nav-link(routerLink="/variants") Variant explorer + a.nav-link(routerLink="/animation") Animator + a.nav-link(routerLink="/sheet") Sheet generator + a.nav-link(routerLink="/expressions") Expressions + a.nav-link(routerLink="/entity") Entity + a.nav-link(routerLink="/palette") Palette + a.nav-link(routerLink="/ui") UI + a.nav-link(routerLink="/chat") Chat + a.nav-link(routerLink="/webgl") WebGL + a.nav-link(routerLink="/perf") Perf + a.nav-link(routerLink="/states") States + a.nav-link(routerLink="/regions") Regions + a.nav-link(routerLink="/collisions") Collisions + a.nav-link(routerLink="/map") Map diff --git a/src/ts/components/tools/tools-index/tools-index.ts b/src/ts/components/tools/tools-index/tools-index.ts new file mode 100644 index 0000000..6a80bd7 --- /dev/null +++ b/src/ts/components/tools/tools-index/tools-index.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'tools-index', + templateUrl: 'tools-index.pug', +}) +export class ToolsIndex { +} diff --git a/src/ts/components/tools/tools-map/tools-map.pug b/src/ts/components/tools/tools-map/tools-map.pug new file mode 100644 index 0000000..2868c6a --- /dev/null +++ b/src/ts/components/tools/tools-map/tools-map.pug @@ -0,0 +1,34 @@ +.form-inline.p-1 + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + + .form-group + label.ml-2 map + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) {{selectedMap || 'main'}} + .dropdown-menu(*dropdownMenu) + button.dropdown-item(*ngFor="let m of maps" (click)="selectMap(m)") {{m || 'main'}} + + .form-group + label.ml-2 type + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) {{type}} + .dropdown-menu(*dropdownMenu) + button.dropdown-item((click)="setType('regular')") regular + button.dropdown-item((click)="setType('minimap')") minimap + + .form-group + label.ml-2 scale + scale-picker.ml-1([(scale)]="scale" (scaleChange)="redraw()" [maxScale]="8") + + .form-group + button.btn.btn-default.ml-2((click)="fetch()") Reload + + .form-group + button.btn.btn-default.ml-2((click)="png()") PNG + + .form-group + custom-checkbox.ml-2([(checked)]="grid" (checkedChange)="redraw()") Grid + +canvas(#canvas) diff --git a/src/ts/components/tools/tools-map/tools-map.ts b/src/ts/components/tools/tools-map/tools-map.ts new file mode 100644 index 0000000..75dda51 --- /dev/null +++ b/src/ts/components/tools/tools-map/tools-map.ts @@ -0,0 +1,210 @@ +import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { saveCanvas, disableImageSmoothing, createCanvas } from '../../../client/canvasUtils'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { tileHeight, tileWidth, REGION_SIZE } from '../../../common/constants'; +import { faHome } from '../../../client/icons'; +import { updateMap, getTile, createWorldMap, setRegion, setTile } from '../../../common/worldMap'; +import { + Season, DrawOptions, defaultDrawOptions, EntityFlags, Entity, WorldMap, defaultWorldState, MapType, MapFlags +} from '../../../common/interfaces'; +import { drawCanvas } from '../../../graphics/contextSpriteBatch'; +import { paletteSpriteSheet } from '../../../generated/sprites'; +import { createRegion } from '../../../common/region'; +import { deserializeTiles } from '../../../common/compress'; +import { createTileSets } from '../../../client/tileUtils'; +import { createCamera } from '../../../common/camera'; +import { mockPaletteManager } from '../../../common/ponyInfo'; +import { isCritter } from '../../../common/entityUtils'; +import { + createAnEntity, cloud, pony, apple, apple2, orange, orange2, candy, gift1, gift2, appleGreen, appleGreen2 +} from '../../../common/entities'; +import { drawMap } from '../../../client/draw'; +import { includes, observableToPromise, hasFlag } from '../../../common/utils'; +import { getShadowColor, HOUR_LENGTH, createLightData } from '../../../common/timeUtils'; +import { StorageService } from '../../services/storageService'; +import { getTileColor } from '../../../common/colors'; +import { colorToCSS } from '../../../common/color'; + +export interface ToolsMapOtherInfo { + season: Season; + entities: { type: number; x: number; y: number; order: number; id: number; }[]; +} + +export interface ToolsMapInfo { + width: number; + height: number; + defaultTile: number; + tiles?: string; + type: MapType; + info: ToolsMapOtherInfo; +} + +@Component({ + selector: 'tools-map', + templateUrl: 'tools-map.pug', +}) +export class ToolsMap implements OnInit { + readonly homeIcon = faHome; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + maps: string[] = []; + selectedMap = ''; + grid = false; + private map?: WorldMap; + private info?: ToolsMapOtherInfo; + constructor(private http: HttpClient, private storage: StorageService) { + } + get scale() { + return this.storage.getInt('tools-map-scale') || 1; + } + set scale(value) { + this.storage.setInt('tools-map-scale', value); + } + get type() { + return this.storage.getItem('tools-map-type') || 'regular'; + } + set type(value) { + this.storage.setItem('tools-map-type', value); + } + async ngOnInit() { + await loadAndInitSpriteSheets(); + await this.fetchList(); + await this.fetch(); + } + setType(type: string) { + this.type = type; + this.redraw(); + } + async fetchList() { + this.maps = await observableToPromise(this.http.get('/api-tools/maps')); + } + fetch() { + this.http.get('/api-tools/map', { params: { map: this.selectedMap } }).subscribe(map => { + this.info = map.info; + + const regionsX = map.width / REGION_SIZE; + const regionsY = map.height / REGION_SIZE; + const { type, defaultTile } = map; + + this.map = createWorldMap({ type, flags: MapFlags.None, defaultTile, regionsX, regionsY }); + const tiles = deserializeTiles(map.tiles!); + + for (let y = 0, i = 0; y < regionsX; y++) { + for (let x = 0; x < regionsY; x++ , i++) { + setRegion(this.map, x, y, createRegion(x, y)); + } + } + + for (let y = 0, i = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++ , i++) { + setTile(this.map, x, y, tiles[i]); + } + } + + this.redraw(); + }); + } + selectMap(map: string) { + this.selectedMap = map; + this.fetch(); + } + redraw() { + this.draw(); + } + png() { + saveCanvas(this.canvas.nativeElement, 'map.png'); + } + private draw() { + if (this.map && this.info) { + if (this.type === 'regular') { + drawTheMap(this.canvas.nativeElement, this.map, this.info, this.scale, this.grid); + } else if (this.type === 'minimap') { + drawMinimap(this.canvas.nativeElement, this.map, this.info, this.scale); + } + } + } +} + +function drawTheMap(canvas: HTMLCanvasElement, map: WorldMap, info: ToolsMapOtherInfo, scale: number, grid: boolean) { + const mapCanvas = drawCanvas(map.width * tileWidth, map.height * tileHeight, paletteSpriteSheet, 0x222222ff, batch => { + const camera = createCamera(); + camera.w = map.width * tileWidth; + camera.h = map.height * tileHeight; + + const tileSets = createTileSets(mockPaletteManager, info.season, map.type); + const lightData = createLightData(info.season); + + const drawOptions: DrawOptions = { + ...defaultDrawOptions, + tileGrid: grid, + shadowColor: getShadowColor(lightData, HOUR_LENGTH * 12), + }; + + const ignoreTypes = [ + cloud, pony, apple, apple2, appleGreen, appleGreen2, orange, orange2, candy, gift1, gift2 + ].map(e => e.type); + + const shouldDraw = (e: Entity) => { + return !hasFlag(e.flags, EntityFlags.Debug) && !isCritter(e) && !includes(ignoreTypes, e.type); + }; + + map.entitiesDrawable = info.entities + .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState)) + .filter(shouldDraw); + + updateMap(map, 0); + drawMap(batch, map, camera, {} as any, drawOptions, tileSets, []); + }); + + canvas.width = Math.floor(mapCanvas.width / scale); + canvas.height = Math.floor(mapCanvas.height / scale); + const context = canvas.getContext('2d')!; + // disableImageSmoothing(context); + context.scale(1 / scale, 1 / scale); + context.drawImage(mapCanvas, 0, 0); +} + +function drawMinimap(canvas: HTMLCanvasElement, map: WorldMap, info: ToolsMapOtherInfo, scale: number) { + const tileWidth = 1; + const tileHeight = 1; + + const mapCanvas = createCanvas(map.width * tileWidth, map.height * tileHeight); + const mapContext = mapCanvas.getContext('2d')!; + + updateMap(map, 0); + + for (let x = 0; x < map.width; x++) { + for (let y = 0; y < map.height; y++) { + const tile = getTile(map, x, y); + const color = getTileColor(tile, info.season); + mapContext.fillStyle = colorToCSS(color); + mapContext.fillRect(x, y, 1, 1); + } + } + + map.entities = info.entities + .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState)); + + for (let i = 1; i <= 2; i++) { + for (const e of map.entities) { + if (e.minimap && e.minimap.order === i) { + const { color, rect } = e.minimap; + mapContext.fillStyle = colorToCSS(color); + mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h); + } + } + } + + canvas.width = mapCanvas.width * scale; + canvas.height = mapCanvas.height * scale; + const context = canvas.getContext('2d')!; + context.save(); + + if (scale >= 1) { + disableImageSmoothing(context); + } + + context.scale(scale, scale); + context.drawImage(mapCanvas, 0, 0); + context.restore(); +} diff --git a/src/ts/components/tools/tools-palette/tools-palette.pug b/src/ts/components/tools/tools-palette/tools-palette.pug new file mode 100644 index 0000000..e25e377 --- /dev/null +++ b/src/ts/components/tools/tools-palette/tools-palette.pug @@ -0,0 +1,24 @@ +.container-fluid + .form-inline.py-2 + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + .form-group + scale-picker.ml-1([(scale)]="scale" (scaleChange)="redraw()") + + .float-left.mr-2 + canvas(#canvas width=512 height=512) + + .float-left + .form-group.form-inline + label.text-muted sprite: + input.form-control.ml-1([(ngModel)]="spriteName" (change)="spriteChanged()" style="width: 200px;" + [typeahead]="sprites" (typeaheadOnSelect)="spriteChanged()") + button.btn.btn-default.ml-1((click)="loadPalette()") + | load palette + + div + .form-group(*ngFor="let c of palette") + .badge([style.background]="c.original" style="border: solid 1px #666") ........ + | + color-picker([(color)]="c.current" (colorChange)="redraw()" style="display: inline-block;") diff --git a/src/ts/components/tools/tools-palette/tools-palette.ts b/src/ts/components/tools/tools-palette/tools-palette.ts new file mode 100644 index 0000000..26ea2c7 --- /dev/null +++ b/src/ts/components/tools/tools-palette/tools-palette.ts @@ -0,0 +1,78 @@ +import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; +import * as sprites from '../../../generated/sprites'; +import { setPaletteManager } from '../../../common/mixins'; +import { parseColor, colorToCSS } from '../../../common/color'; +import { PaletteManager, releasePalette } from '../../../graphics/paletteManager'; +import { drawCanvas } from '../../../graphics/contextSpriteBatch'; +import { disableImageSmoothing } from '../../../client/canvasUtils'; +import { SHADOW_COLOR, WHITE } from '../../../common/colors'; +import { PaletteRenderable } from '../../../common/interfaces'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { faHome } from '../../../client/icons'; + +const BG = parseColor('lightgreen'); +const DEFAULT_PALETTE = [0, 0xffffffff]; +const paletteManager = new PaletteManager(); +const defaultPalette = paletteManager.add(DEFAULT_PALETTE); + +@Component({ + selector: 'tools-palette', + templateUrl: 'tools-palette.pug', +}) +export class ToolsPalette implements OnInit { + readonly homeIcon = faHome; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + scale = 3; + sprites = Object.keys(sprites).filter(key => { + const s = (sprites as any)[key] as any; + return !!(s && s.color); + }); + spriteName = ''; + palette = ['red', 'blue', 'orange', 'violet'].map(x => ({ original: x, current: x })); + ngOnInit() { + setPaletteManager(paletteManager); + loadAndInitSpriteSheets().then(() => this.redraw()); + } + spriteChanged() { + this.redraw(); + } + loadPalette() { + const sprite = (sprites as any)[this.spriteName] as PaletteRenderable; + + if (sprite) { + this.palette = Array.from(sprite.palettes![0]).map(colorToCSS).map(c => ({ original: c, current: c })); + } + + this.redraw(); + } + redraw() { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const width = Math.ceil(canvas.width / this.scale); + const height = Math.ceil(canvas.height / this.scale); + + const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => { + const sprite = (sprites as any)[this.spriteName] as PaletteRenderable; + + if (sprite) { + const palette = paletteManager.add(this.palette.map(x => parseColor(x.current))); + + const x = (width - (sprite.color!.w + sprite.color!.ox)) / 2; + const y = (height - (sprite.color!.h + sprite.color!.oy)) / 2; + + console.log(x, y, width, sprite.color!.w, sprite.color!.ox); + + batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y); + batch.drawSprite(sprite.color, WHITE, palette, x, y); + + releasePalette(palette); + } + }); + + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + viewContext.scale(this.scale, this.scale); + viewContext.drawImage(buffer, 0, 0); + viewContext.restore(); + } +} diff --git a/src/ts/components/tools/tools-perf/methods.ts b/src/ts/components/tools/tools-perf/methods.ts new file mode 100644 index 0000000..cff8896 --- /dev/null +++ b/src/ts/components/tools/tools-perf/methods.ts @@ -0,0 +1,156 @@ +// import { TextEncoder } from 'util'; + +function forEachCharacter(value: string, callback: (code: number) => void) { + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); + + // high surrogate + if (code >= 0xd800 && code <= 0xdbff) { + if ((i + 1) < value.length) { + const extra = value.charCodeAt(i + 1); + + // low surrogate + if ((extra & 0xfc00) === 0xdc00) { + i++; + callback(((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000); + } + } + } else { + callback(code); + } + } +} + +function charLengthInBytes(code: number): number { + if ((code & 0xffffff80) === 0) { + return 1; + } else if ((code & 0xfffff800) === 0) { + return 2; + } else if ((code & 0xffff0000) === 0) { + return 3; + } else { + return 4; + } +} + +function stringLengthInBytes(value: string): number { + let result = 0; + forEachCharacter(value, code => result += charLengthInBytes(code)); + return result; +} + +function encodeStringTo(buffer: Uint8Array | Buffer, offset: number, value: string): number { + forEachCharacter(value, code => { + const length = charLengthInBytes(code); + + if (length === 1) { + buffer[offset++] = code; + } else { + if (length === 2) { + buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0; + } else if (length === 3) { + buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } else { + buffer[offset++] = ((code >> 18) & 0x07) | 0xf0; + buffer[offset++] = ((code >> 12) & 0x3f) | 0x80; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } + + buffer[offset++] = (code & 0x3f) | 0x80; + } + }); + + return offset; +} + +export function encodeString(value: string | null): Uint8Array | null { + if (value == null) + return null; + + const buffer = new Uint8Array(stringLengthInBytes(value)); + encodeStringTo(buffer, 0, value); + return buffer; +} + +export function encodeStringNew(value: string | null): Uint8Array | null { + if (value == null) + return null; + + const buffer = new Uint8Array(stringLengthInBytes2(value)); + encodeStringTo2(buffer, 0, value); + return buffer; +} + +// new methods + +function charLengthInBytes2(code: number): number { + if ((code & 0xffffff80) === 0) { + return 1; + } else if ((code & 0xfffff800) === 0) { + return 2; + } else if ((code & 0xffff0000) === 0) { + return 3; + } else { + return 4; + } +} + +export function stringLengthInBytes2(value: string): number { + let result = 0; + forEachCharacter2(value, code => result = (result + charLengthInBytes2(code)) | 0); + return result; +} + +export function encodeStringTo2(buffer: Uint8Array, offset: number, value: string): number { + forEachCharacter2(value, code => { + const length = charLengthInBytes2(code) | 0; + + if (length === 1) { + buffer[offset++] = code; + } else { + if (length === 2) { + buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0; + } else if (length === 3) { + buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } else { + buffer[offset++] = ((code >> 18) & 0x07) | 0xf0; + buffer[offset++] = ((code >> 12) & 0x3f) | 0x80; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } + + buffer[offset++] = (code & 0x3f) | 0x80; + } + }); + + return offset; +} + +function forEachCharacter2(value: string, callback: (code: number) => void) { + const length = value.length | 0; + const lengthMinusOne = Math.max(0, length - 1) | 0; + + for (let i = 0; i < length; i = (i + 1) | 0) { + let code = value.charCodeAt(i) | 0; + + // high surrogate + if (code >= 0xd800 && code <= 0xdbff) { + if (i < lengthMinusOne) { + const extra = value.charCodeAt(i + 1) | 0; + + // low surrogate + if ((extra & 0xfc00) === 0xdc00) { + i = (i + 1) | 0; + code = (((((code & 0x3ff) << 10) + (extra & 0x3ff)) | 0) + 0x10000) | 0; + } else { + continue; + } + } else { + continue; + } + } + + callback(code); + } +} diff --git a/src/ts/components/tools/tools-perf/tools-perf.pug b/src/ts/components/tools/tools-perf/tools-perf.pug new file mode 100644 index 0000000..adf6cfa --- /dev/null +++ b/src/ts/components/tools/tools-perf/tools-perf.pug @@ -0,0 +1,9 @@ +.m-2 + .mb-2 + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + button.btn.btn-default.ml-1((click)="run()") run test + button.btn.btn-default.ml-1((click)="stats()") run stats + button.btn.btn-default.ml-1(*ngFor="let test of tests" (click)="test.func()") {{test.name}} + pre.rounded.p-3(style="color: white; background: #222;") + | {{output}} diff --git a/src/ts/components/tools/tools-perf/tools-perf.ts b/src/ts/components/tools/tools-perf/tools-perf.ts new file mode 100644 index 0000000..50e3f01 --- /dev/null +++ b/src/ts/components/tools/tools-perf/tools-perf.ts @@ -0,0 +1,404 @@ +import { Component, NgZone } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { isEqual, range, random, times } from 'lodash'; +import { parseColorWithAlpha, parseColorFast, colorToHexRGB } from '../../../common/color'; +import { fillToOutline } from '../../../common/colors'; +import { toColorListNumber } from '../../../common/ponyInfo'; +import { decodePonyInfo, createPostDecompressPony } from '../../../common/compressPony'; +import { faHome } from '../../../client/icons'; +import { PaletteManager } from '../../../graphics/paletteManager'; +import { filterBadWords, createMatchEntries } from '../../../common/swears'; +import { bitWriter } from '../../../common/bitUtils'; +import { includes as utilsIncludes } from '../../../common/utils'; +import { encodeString, encodeStringNew } from './methods'; + +@Component({ + selector: 'tools-perf', + templateUrl: 'tools-perf.pug', +}) +export class ToolsPerf { + readonly homeIcon = faHome; + output = ''; + ponies: string[] = []; + messages: string[] = []; + tests: { name: string; func: () => void; }[] = []; + constructor(http: HttpClient, private zone: NgZone) { + http.get('/tests/ponies.json').subscribe(data => this.ponies = data); + http.get('/tests/messages.json').subscribe(data => this.messages = data); + this.output = createPostDecompressPony().toString(); + this.tests.push({ name: 'arrays', func: () => this.runTest(arrayTest) }); + this.tests.push({ name: 'compress colors', func: () => this.runTest(compressColorsTest) }); + this.tests.push({ name: 'parse color', func: () => this.runTest(parseColorTest) }); + this.tests.push({ name: 'compare arrays', func: () => this.runTest(compareArrays) }); + this.tests.push({ name: 'utf', func: () => this.runTest(() => utfTest(this.messages)) }); + this.tests.push({ name: 'includes', func: () => this.runTest(includeTest) }); + } + run() { + this.runTest(() => utfTest(this.messages)); + } + stats() { + swearEntryTest(this.messages, x => this.output = x); + } + private runTest(test: () => void) { + this.zone.runOutsideAngular(() => setTimeout(test, 20)); + } +} + +function measure(name: string, iterations: number, func: (i: number) => void) { + if (!iterations) + return; + + const start = performance.now(); + let v: any; + + for (let i = 0; i < iterations; i++) { + v = func(i); + } + + const end = performance.now(); + const diff = end - start; + + console.log(`${name}: ${diff.toFixed(0)}ms, ${(diff / iterations).toFixed(3)}ms per iteration // ${!!v}`); +} + +export function compressColorsTest() { + const colors = [ + 3553475327, 2592097791, 3662487807, 545184511, 2744818431, 1658462463, 16764927, 7864319, + 4278253055, 2492366335, 10033407, 11763711, 6730751, 3003165951, 2997456127, 1447512063, + 1936281087, 874586623, 3713423103, 2099652351, 4293220607, 512819199, 852308735, 3664828159, + 3692313855, 2147472639, 2861699071, 5944319, 42992383, 2570622463, 2583699455, 1051954175, + 3013286911, 2762969343, 1400052223, 1991223551, 1049483775, 611346431, 272724223, 1392443647, + 1589395455, 3201321215, 2679322623, 3233857791, 1280068863, 2560137471, 437918463, 3908210943, + 3602601215, 4001558271, 2806294527, 2508550143, 1732985855, 1330597887, 1381126911, 1750746879, + 1211049983, 926365695, 960051711, 2659530751, 3597364223, 777334527, 2201321727, 2120247039, + 2441106175, 2354205439, 1844342271, 3613774847, 1967148031, 4289003775, 3600494335, 3164100095, + 2845275903, 3098517247, 1463486719, 1699355647, 1834628607, 1936084991, 2307095039, 857933311, + 740297471, 807801599, 740629247, 656811007, 3587560959, 2998055679, 2425393407, 1869574143, + 4294967295, 4294238719, 4293575679, 4292051711, 4291190527 + ].map(x => x >>> 0); + + const oldMethod = bitWriter(write => colors.forEach(x => write(x >> 8, 24))); + + const truncated = colors.map(c => (c >>> 8) & 0xffffff); + truncated.sort((a, b) => a > b ? 1 : (a < b ? -1 : 0)); + + console.log(truncated); + console.log(truncated.slice(1).map((c, i) => (c - truncated[i]).toString(16))); + + const newMethod = bitWriter(write => truncated.forEach(x => write(x, 24))); + + console.log('old', oldMethod.byteLength); + console.log('new', newMethod.byteLength); +} + +export const results: any[] = []; + +export function parseColorTest() { + const iterations = 100000; + + function parseColorExperimental(value: string) { + return (parseInt(value, 16) << 8) | 0xff; + } + + measure('COLOR parseColorWithAlpha', iterations, () => { + return parseColorWithAlpha('ff4354', 1); + }); + + measure('COLOR parseColorFast', iterations, () => { + return parseColorFast('ff4354'); + }); + + measure('COLOR parseColorExperimental', iterations, () => { + return parseColorExperimental('ff4354'); + }); +} + +export function compareArrays() { + const iterations = 100000; + const a = [2423, 534534, 546124, 23412, 54364, 67756, 234234]; + const b = [564, 867867, 65645, 567567, 34534, 32453, 867867]; + const c = [2423, 534534, 546124, 23412, 54364, 67756, 234234]; + const d = [2423, 534534, 546124, 23412, 54364, 67756]; + let t: any; + + function compareArrays(a: number[], b: number[]) { + if (a.length !== b.length) + return false; + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) + return false; + } + + return true; + } + + measure('ARRAY _.isEqual', iterations, () => { + t = isEqual(a, b); + t = isEqual(a, c) || t; + t = isEqual(a, d) || t; + results.push(t); + }); + + measure('ARRAY compareArrays', iterations, () => { + t = compareArrays(a, b); + t = compareArrays(a, c) || t; + t = compareArrays(a, d) || t; + results.push(t); + }); +} + +export function decode(data: string[]) { + let manager = new PaletteManager(); + // manager = ({ addArray: (x: any) => x } as any); + + measure('DECODE 1', 10000, i => { + results.push(decodePonyInfo(data[i % data.length], manager)); + }); + + // console.log(((manager as any).palettes as any[]).map(x => x.length).join(', ')); +} + +export function utfTest(messages: string[]) { + const iterations = 300000; + + measure('old', iterations, i => { + results.push(encodeString(messages[i % messages.length])); + }); + + measure('new', iterations, i => { + results.push(encodeStringNew(messages[i % messages.length])); + }); + + const encoder = new (window as any).TextEncoder('utf8'); + + measure('native', iterations, i => { + results.push(encoder.encode(messages[i % messages.length])); + }); +} + +export function arrayTest() { + const iterations = 10000; + const length = 100; + const size = 24; + const typed = new Int32Array(length * size); + const typed2 = new Uint16Array(length * size); + + for (let i = 0; i < typed.length; i++) { + typed2[i] = typed[i] = Math.random() * 0xffff; + } + + const objects = times(length, i => ({ + a: typed[i * size + 0], + b: typed[i * size + 1], + c: typed[i * size + 2], + d: typed[i * size + 3], + e: typed[i * size + 4], + f: typed[i * size + 5], + g: typed[i * size + 6], + h: typed[i * size + 7], + i: typed[i * size + 8], + j: typed[i * size + 9], + k: typed[i * size + 10], + l: typed[i * size + 11], + m: typed[i * size + 12], + n: typed[i * size + 13], + o: typed[i * size + 14], + p: typed[i * size + 15], + q: typed[i * size + 16], + r: typed[i * size + 17], + s: typed[i * size + 18], + t: typed[i * size + 19], + u: typed[i * size + 20], + v: typed[i * size + 21], + w: typed[i * size + 22], + x: typed[i * size + 23], + })); + + const indexes = times(length, () => (Math.random() * length) | 0); + + measure('typed', iterations, index => { + let sum = 0; + for (let i = 0; i < length; i++) { + const offset = (indexes[(i + index) % length]) * 24; + + for (let j = 0; j < size; j++) { + sum += typed[offset + j]; + } + } + results.push(sum); + }); + + measure('typed2', iterations, index => { + let sum = 0; + for (let i = 0; i < length; i++) { + const offset = (indexes[(i + index) % length]) * 24; + + for (let j = 0; j < size; j++) { + sum += typed2[offset + j]; + } + } + results.push(sum); + }); + + measure('objects', iterations, index => { + let sum = 0; + for (let i = 0; i < length; i++) { + const offset = indexes[(i + index) % length]; + const o = objects[offset]; + sum += o.a + o.b + o.c + o.d + o.e + o.f + o.g + o.h + o.i + o.j + o.k + o.l + + o.m + o.n + o.o + o.p + o.q + o.r + o.s + o.t + o.u + o.v + o.w + o.x; + } + results.push(sum); + }); +} + +export function fillToOutlineTest() { + const iterations = 100000; + + function fillToOutlineFast(color: string) { + return colorToHexRGB(parseColorFast(color)); + } + + measure('FILL-TO-OUTLINE fillToOutline', iterations, () => { + fillToOutline('32cd32'); + }); + + measure('FILL-TO-OUTLINE fillToOutlineFast', iterations, () => { + fillToOutlineFast('32cd32'); + }); +} + +export function includeTest() { + const iterations = 100000; + const array = range(1000).map(() => random(0, 1000)); + let t = 0; + + // measure('INCLUDE _.includes', iterations, () => { + // t += includes(array, array[random(0, 1000)]) as any | 0; + // t += includes(array, array[random(0, 1000)]) as any | 0; + // t += includes(array, array[random(0, 1000)]) as any | 0; + // }); + + measure('INCLUDE includes', iterations, () => { + t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; + t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; + t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; + }); + + measure('INCLUDE indexOf !== -1', iterations, () => { + t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; + t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; + t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; + }); + + results.push(t); +} + +export function toColorListTest() { + function toColorList2Old(colors: number[]) { + return [0, ...colors.map(c => c || 0xff)]; + } + + const iterations = 100000; + let t: any[] = []; + + measure('TOCOLORLIST2 old', iterations, () => { + t.push(toColorList2Old([1, 2, Date.now(), Date.now(), 5, 6])); + }); + + measure('TOCOLORLIST2 new', iterations, () => { + t.push(toColorListNumber([1, 2, Date.now(), Date.now(), 5, 6])); + }); + + results.push(t); +} + +export function copyTest() { + const iterations = 10000; + let t: any[] = []; + const src = new Uint32Array(1000); + const dst = new Uint32Array(10000); + + for (let i = 0; i < src.length; i++) { + src[i] = Math.random() * 10000; + } + + for (let i = 0; i < dst.length; i++) { + dst[i] = Math.random() * 10000; + } + + measure('ONE_BY_ONE', iterations, iteration => { + for (let i = 0; i < 10; i++) { + for (let j = 0; j < src.length; j++) { + dst[1000 * i + j] = src[j]; + } + } + + t.push(dst, iteration); + }); + + measure('COPY_BUFFER', iterations, iteration => { + for (let i = 0; i < 10; i++) { + dst.set(src, 1000 * i); + } + + t.push(dst, iteration); + }); + + results.push(t); +} + +export function regexTest(testStrings: string[]) { + const iterations = 1000000; + const t: any[] = []; + + measure('WITH_GROUPS', iterations, iteration => { + const test = testStrings[iteration % testStrings.length]; + t.push(test.replace(/(a|b)(.)(.)(.)(.)(.)/, 'X')); + }); + + measure('WITHOUT_GROUPS', iterations, iteration => { + const test = testStrings[iteration % testStrings.length]; + t.push(test.replace(/(?:a|b)(?:.)(?:.)(?:.)(?:.)(?:.)/, 'X')); + }); + + results.push(t); +} + +export function swearTest(testStrings: string[]) { + const iterations = 10000; + const t: any[] = []; + + measure('TEST', iterations, iteration => { + const test = testStrings[iteration % testStrings.length]; + t.push(filterBadWords(test)); + }); + + results.push(t); +} + +export function swearEntryTest(testStrings: string[], onResult: (output: string) => void) { + const iterations = 2000; + const output: any[] = []; + const entries = createMatchEntries(); + + for (const e of entries) { + const start = performance.now(); + + for (let i = 0; i < iterations; i++) { + const test = testStrings[i % testStrings.length]; + results.push(test.replace(e.regex, '*****')); + } + + const diff = performance.now() - start; + output.push({ e, diff }); + } + + onResult(output + .sort((a, b) => b.diff - a.diff) + .map(x => `${x.diff.toFixed(2).padStart(6)} "${x.e.line}"`) + .join('\n')); +} + +(window as any).__results = results; diff --git a/src/ts/components/tools/tools-regions/tools-regions.pug b/src/ts/components/tools/tools-regions/tools-regions.pug new file mode 100644 index 0000000..857bd51 --- /dev/null +++ b/src/ts/components/tools/tools-regions/tools-regions.pug @@ -0,0 +1,26 @@ +.m-4(style="position: relative;" (agDrag)="dragRegion($event)" agDragRelative="self") + table.table-fixed.pointer-none + tbody + tr(*ngFor="let row of regions") + td(*ngFor="let cell of row" + style="border: solid 1px #666;" + [style.background]="cell" + [style.width.px]="regionSize * tileWidth * scale" + [style.height.px]="regionSize * tileHeight * scale") + div(style="position: absolute; border: dashed 1px red; top: 0; left: 0;" + [style.width.px]="currentMapSize * tileWidth * scale" + [style.height.px]="currentMapSize * tileHeight * scale") + div(style="position: absolute; border: solid 1px lime; display: flex; justify-content: center; align-items: center;" + [style.width.px]="camera.w * scale" + [style.height.px]="camera.h * scale" + [style.left.px]="camera.x * scale" + [style.top.px]="camera.y * scale") + div(style="width: 30%; height: 30%; border: dashed 1px lime;") + div(style="position: absolute; border: dashed 1px orange;" + [style.width.px]="approxCamera.w * scale" + [style.height.px]="approxCamera.h * scale" + [style.left.px]="approxCamera.x * scale" + [style.top.px]="approxCamera.y * scale") + div(style="position: absolute; width: 4px; height: 4px; border-radius: 100%; background: lime;" + [style.left.px]="player.x * tileWidth * scale - 2" + [style.top.px]="player.y * tileHeight * scale - 2") diff --git a/src/ts/components/tools/tools-regions/tools-regions.ts b/src/ts/components/tools/tools-regions/tools-regions.ts new file mode 100644 index 0000000..71a935d --- /dev/null +++ b/src/ts/components/tools/tools-regions/tools-regions.ts @@ -0,0 +1,172 @@ +import { times, fill, clamp } from 'lodash'; +import { Component, OnInit, HostListener, OnDestroy } from '@angular/core'; +import { rect } from '../../../common/rect'; +import { AgDragEvent } from '../../shared/directives/agDrag'; +import { updateCamera, centerCameraOn, isAreaVisible, createCamera } from '../../../common/camera'; +import { tileWidth, tileHeight, PONY_SPEED_TROT } from '../../../common/constants'; +import { Key } from '../../../client/input/input'; +import { Rect, Point } from '../../../common/interfaces'; +import { toWorldX, toWorldY } from '../../../common/positionUtils'; + +export function getRegionsBounds(client: any, region: any) { + const screenSize = client.screenSize; + const width = Math.ceil(((1.3 * screenSize.width) / region.size) / 2) * 2 + 2; + const height = Math.ceil(((1.3 * screenSize.height) / region.size) / 2) * 2 + 2; + return rect(region.x - Math.ceil(width / 2), region.y - Math.ceil(height / 2), width, height); +} + +export function getRegionsBoundsCameraBased(_entity: Point, camera: Rect, regionSize: number) { + const left = Math.floor(camera.x / regionSize - 0.5); + const top = Math.floor(camera.y / regionSize - 0.5); + const right = Math.floor((camera.x + camera.w) / regionSize + 0.5); + const bottom = Math.floor((camera.y + camera.h) / regionSize + 0.5); + return rect(left, top, right - left, bottom - top); +} + +@Component({ + selector: 'tools-regions', + templateUrl: 'tools-regions.pug', +}) +export class ToolsRegions implements OnInit, OnDestroy { + currentMapSize = 80; + tileWidth = tileWidth; + tileHeight = tileHeight; + screen = { width: 390, height: 580 }; + // screen = { width: 1920, height: 1080 }; + regionsX = 18; + regionsY = 16; + regionSize = 8; + scale = 0.25; + zoom = 2; + regions: string[][]; + camera = createCamera(); + approxCamera = createCamera(); + player = { x: 0, y: 0 }; + frame = 0; + lastFrame = 0; + constructor() { + this.regions = times(this.regionsY, () => times(this.regionsX, () => '')); + } + ngOnInit() { + this.update(); + this.frame = requestAnimationFrame(this.tick); + } + ngOnDestroy() { + cancelAnimationFrame(this.frame); + } + update() { + const regionWidth = this.regionSize * tileWidth; + const regionHeight = this.regionSize * tileHeight; + const map = { width: this.regionsX * this.regionSize, height: this.regionsY * this.regionSize } as any; + + this.camera.w = Math.ceil(this.screen.width / this.zoom); + this.camera.h = Math.ceil(this.screen.height / this.zoom); + updateCamera(this.camera, this.player, map); + + this.approxCamera.w = this.camera.w * 1.3; + this.approxCamera.h = this.camera.h * 1.3; + centerCameraOn(this.approxCamera, this.player); + updateCamera(this.approxCamera, this.player, map); + + this.regions.forEach(x => fill(x, '')); + + const rx = clamp(Math.floor(this.player.x / this.regionSize), 0, this.regionsX - 1); + const ry = clamp(Math.floor(this.player.y / this.regionSize), 0, this.regionsY - 1); + + const bounds1 = getRegionsBounds( + { + screenSize: { + width: Math.ceil(this.camera.w / tileWidth), + height: Math.ceil(this.camera.h / tileHeight) + } + }, + { size: this.regionSize, x: rx, y: ry }); + + const bounds2 = getRegionsBoundsCameraBased( + this.player, + rect(toWorldX(this.camera.x), toWorldY(this.camera.y), toWorldX(this.camera.w), toWorldY(this.camera.h)), + this.regionSize); + + const bounds = [bounds1, bounds2][1]; + + for (let ix = 0; ix <= bounds.w; ix++) { + for (let iy = 0; iy <= bounds.h; iy++) { + const yy = bounds.y + iy; + const xx = bounds.x + ix; + + if (xx >= 0 && xx < this.regionsX && yy >= 0 && yy < this.regionsY) { + this.regions[yy][xx] = 'Sienna'; + } + } + } + + for (let x = 0; x < this.regionsX; x++) { + for (let y = 0; y < this.regionsY; y++) { + if (isAreaVisible(this.camera, x * regionWidth, y * regionHeight, regionWidth, regionHeight)) { + if (this.regions[y][x] === 'Sienna') { + this.regions[y][x] = 'SeaGreen'; + } else { + this.regions[y][x] = 'Crimson'; + } + } + } + } + + this.regions[ry][rx] = 'MediumSeaGreen'; + } + dragRegion({ x, y }: AgDragEvent) { + this.player.x = x / (this.tileWidth * this.scale); + this.player.y = y / (this.tileHeight * this.scale); + this.update(); + } + private right = false; + private left = false; + private up = false; + private down = false; + @HostListener('window:keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (e.keyCode === Key.KEY_P) { + this.zoom = this.zoom === 4 ? 1 : (this.zoom + 1); + this.update(); + } else if (e.keyCode === Key.RIGHT) { + this.right = true; + } else if (e.keyCode === Key.LEFT) { + this.left = true; + } else if (e.keyCode === Key.UP) { + this.up = true; + } else if (e.keyCode === Key.DOWN) { + this.down = true; + } + } + @HostListener('window:keyup', ['$event']) + keyup(e: KeyboardEvent) { + if (e.keyCode === Key.RIGHT) { + this.right = false; + } else if (e.keyCode === Key.LEFT) { + this.left = false; + } else if (e.keyCode === Key.UP) { + this.up = false; + } else if (e.keyCode === Key.DOWN) { + this.down = false; + } + } + tick = (now: number) => { + this.frame = requestAnimationFrame(this.tick); + const delta = (now - this.lastFrame) / 1000; + this.lastFrame = now; + + let dx = 0; + let dy = 0; + + if (this.right) dx += 1; + if (this.left) dx -= 1; + if (this.up) dy -= 1; + if (this.down) dy += 1; + + if (dx || dy) { + this.player.x += dx * delta * PONY_SPEED_TROT; + this.player.y += dy * delta * PONY_SPEED_TROT; + this.update(); + } + } +} diff --git a/src/ts/components/tools/tools-sheet/tools-sheet.pug b/src/ts/components/tools/tools-sheet/tools-sheet.pug new file mode 100644 index 0000000..0e4d14b --- /dev/null +++ b/src/ts/components/tools/tools-sheet/tools-sheet.pug @@ -0,0 +1,50 @@ +.form-inline + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + + .form-group + label.ml-2 sheets: + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle style="width: 200px;") + | {{sheet.name}} + fa-icon.ml-1(*ngIf="sheet.file" [icon]="imageIcon") + .dropdown-menu(*dropdownMenu) + div(*ngFor="let s of sheets") + .dropdown-divider(*ngIf="s.spacer") + a.dropdown-item((click)="setSheet(s)" *ngIf="!s.spacer") + | {{s.name}} + fa-icon.ml-1(*ngIf="s.file" [icon]="imageIcon") + + .form-group + label.ml-1 + scale-picker([(scale)]="scale" (scaleChange)="redraw()") + + .form-group + button.btn.btn-default.ml-1((click)="redraw()") + fa-icon([icon]="syncIcon" [fixedWidth]="true") + + .form-group + label.ml-2 rows: + input.form-control.ml-1(type="number" [(ngModel)]="rows" (input)="redraw()" style="width: 70px;") + + .form-group + label.ml-2 cols: + input.form-control.ml-1(type="number" [(ngModel)]="cols" (input)="redraw()" style="width: 70px;") + + .form-group + label.ml-2 pattern: + input.form-control.ml-1(type="number" [(ngModel)]="pattern" (input)="redraw()" style="width: 70px;") + + .form-group + button.btn.btn-default.ml-2((click)="png()") PNG + button.btn.btn-default.ml-1((click)="psd()") PSD + button.btn.btn-default.ml-1((click)="allPSDs()") All PSDs + button.btn.btn-danger.ml-1(*ngIf="sheet.alert" disabled) {{sheet.alert}} + +.tools-sheet-offsets(#offsetsDiv [hidden]="!sheet.offsets") + tools-offset( + *ngFor="let o of sheet.offsets" [offset]="o" (change)="redraw()" [style.width.px]="sheet.offset * scale") + +.tools-sheet-scroll([class.with-offsets]="sheet.offsets" (scroll)="offsetsDiv.scrollLeft = $event.target.scrollLeft") + canvas(#canvas) diff --git a/src/ts/components/tools/tools-sheet/tools-sheet.scss b/src/ts/components/tools/tools-sheet/tools-sheet.scss new file mode 100644 index 0000000..6602540 --- /dev/null +++ b/src/ts/components/tools/tools-sheet/tools-sheet.scss @@ -0,0 +1,30 @@ +.tools-sheet-offsets { + position: relative; + height: 70px; + overflow: hidden; + padding-right: 100px; + white-space: nowrap; + + > tools-offset { + display: inline-block; + text-align: center; + } +} + +.tools-sheet-scroll { + overflow: auto; + width: 100%; + position: absolute; + left: 0; + top: 45px; + right: 0; + bottom: 0; + + &.with-offsets { + top: 45px + 70px; + } +} + +.form-inline { + padding: 5px; +} diff --git a/src/ts/components/tools/tools-sheet/tools-sheet.ts b/src/ts/components/tools/tools-sheet/tools-sheet.ts new file mode 100644 index 0000000..5414b4e --- /dev/null +++ b/src/ts/components/tools/tools-sheet/tools-sheet.ts @@ -0,0 +1,76 @@ +import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; +import { compact } from 'lodash'; +import { getCols, getRows, createPsd, savePsd, drawPsd } from '../sheetExport'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { saveCanvas } from '../../../client/canvasUtils'; +import { faHome, faSync, faFileImage } from '../../../client/icons'; +import { StorageService } from '../../services/storageService'; +import { at } from '../../../common/utils'; +import { sheets, Sheet } from '../../../common/sheets'; + +@Component({ + selector: 'tools-sheet', + templateUrl: 'tools-sheet.pug', + styleUrls: ['tools-sheet.scss'], +}) +export class ToolsSheet implements OnInit { + readonly homeIcon = faHome; + readonly syncIcon = faSync; + readonly imageIcon = faFileImage; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + sheets = sheets; + sheet = sheets[0] as Sheet; + scale = 2; + rows = 1; + cols = 1; + pattern = -1; + constructor(private storage: StorageService) { + const sheet = at(this.sheets, storage.getInt('tools-sheet-sheet'))!; + + if ('name' in sheet) { + this.setSheet(sheet); + } + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + setSheet(sheet: Sheet) { + sheet = sheet.spacer ? this.sheets[0] as Sheet : sheet; + this.sheet = sheet; + this.cols = getCols(sheet); + this.rows = getRows(sheet); + this.redraw(); + this.storage.setInt('tools-sheet-sheet', this.sheets.indexOf(sheet)); + } + png() { + this.redraw(); + saveCanvas(this.canvas.nativeElement, 'sheet.png'); + } + psd() { + const psd = createPsd(this.sheet, this.rows, this.cols); + psd.canvas = drawPsd(psd, 1); + savePsd(psd, `${this.sheet.file}.psd`); + } + allPSDs() { + this.sheets + .filter(x => 'name' in x && !!x.file) + .map(x => x as Sheet) + .forEach(sheet => { + const rows = getRows(sheet); + const cols = getCols(sheet); + const psd = createPsd(sheet, rows, cols); + psd.canvas = drawPsd(psd, 1); + savePsd(psd, `${sheet.file}.psd`); + }); + } + redraw() { + if (this.canvas) { + const psd = createPsd(this.sheet, this.rows, this.cols); + const layers = compact(psd.children!.map(c => c.children)); + const patterns = compact(layers.map(xs => xs.find(x => x.name === `pattern ${this.pattern}`))); + patterns.forEach(x => x.hidden = false); + drawPsd(psd, this.scale, this.canvas.nativeElement); + } + } +} diff --git a/src/ts/components/tools/tools-states/tools-states.pug b/src/ts/components/tools/tools-states/tools-states.pug new file mode 100644 index 0000000..e22420c --- /dev/null +++ b/src/ts/components/tools/tools-states/tools-states.pug @@ -0,0 +1,25 @@ +.p-1(style="position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow: hidden;") + .mb-3 + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + button.btn.btn-default.ml-1((click)="logPositions()") + | log positions + + .state-item-container + .state-item(*ngFor="let s of states" [style.left.px]="s.x" [style.top.px]="s.y" (agDrag)="drag(s, $event)") + div([style.background]="s.color") + div + div {{s.name}} + div(*ngIf="s.variants") (+{{s.variants}}) + + svg.pointer-none(width="1500" height="1500") + defs + marker(*ngFor="let c of arrowColors" [attr.id]="'head-' + c" orient="auto" markerWidth="5" markerHeight="6" + refX="0.1" refY="3") + path(d="M0,0 V6 L5,3 Z" [attr.fill]="c") + path(*ngFor="let a of arrows" [attr.marker-end]="'url(#head-' + a.color + ')'" stroke-width="2" + [attr.stroke]="a.color" fill="none" [attr.d]="a.path") + + .state-item-time(*ngFor="let t of times" [style.left.px]="t.x" [style.top.px]="t.y" [title]="t.title || ''") + div([style.background]="t.color") + span {{t.text}} diff --git a/src/ts/components/tools/tools-states/tools-states.scss b/src/ts/components/tools/tools-states/tools-states.scss new file mode 100644 index 0000000..34a04dc --- /dev/null +++ b/src/ts/components/tools/tools-states/tools-states.scss @@ -0,0 +1,57 @@ +$state-size: 100px; +$time-size: 20px; + +.state-item-container { + position: relative; + + > svg { + position: relative; + z-index: 1; + } +} + +.state-item { + position: absolute; + width: 1px; + height: 1px; + cursor: pointer; + + > div { + position: absolute; + left: -$state-size / 2; + top: -$state-size / 2; + width: $state-size; + height: $state-size; + background: gray; + border-radius: 100%; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 5px #000; + } +} + +.state-item-time{ + position: absolute; + width: 1px; + height: 1px; + font-size: 10px; + font-weight: bold; + color: black; + z-index: 2; + + > div { + position: absolute; + left: -$time-size / 2; + top: -$time-size / 2; + width: $time-size; + height: $time-size; + background: gray; + border-radius: 100%; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/src/ts/components/tools/tools-states/tools-states.ts b/src/ts/components/tools/tools-states/tools-states.ts new file mode 100644 index 0000000..c801e14 --- /dev/null +++ b/src/ts/components/tools/tools-states/tools-states.ts @@ -0,0 +1,139 @@ +import { Component } from '@angular/core'; +import { fromPairs } from 'lodash'; +import { faHome } from '../../../client/icons'; +import { ponyStates, } from '../../../client/ponyStates'; +import { AgDragEvent } from '../../shared/directives/agDrag'; +import { AnimatorState } from '../../../common/animator'; +import { distance, flatten } from '../../../common/utils'; + +function getPos(key: string, type: 'x' | 'y', defaultValue = 100) { + const item = localStorage.getItem(`tools-stats-${key}-${type}`); + return item ? parseInt(item, 10) : defaultValue; +} + +function setPos(key: string, type: 'x' | 'y', value: number) { + localStorage.setItem(`tools-stats-${key}-${type}`, value.toString()); +} + +const defaultPositions: any = { + 'any': { 'x': 917, 'y': 43 }, + 'standing': { 'x': 317, 'y': 300 }, + 'trotting': { 'x': 660, 'y': 304 }, + 'swimming': { 'x': 988, 'y': 435 }, + 'swimming-to-trotting': { 'x': 747, 'y': 548 }, + 'trotting-to-swimming': { 'x': 805, 'y': 413 }, + 'booping': { 'x': 106, 'y': 301 }, + 'booping-sitting': { 'x': 111, 'y': 569 }, + 'booping-lying': { 'x': 127, 'y': 821 }, + 'booping-flying': { 'x': 110, 'y': 64 }, + 'sitting': { 'x': 312, 'y': 569 }, + 'sitting-down': { 'x': 186, 'y': 437 }, + 'standing-up': { 'x': 313, 'y': 436 }, + 'sitting-to-trotting': { 'x': 517, 'y': 500 }, + 'lying': { 'x': 326, 'y': 818 }, + 'lying-down': { 'x': 254, 'y': 692 }, + 'sitting-up': { 'x': 385, 'y': 682 }, + 'lying-to-trotting': { 'x': 541, 'y': 731 }, + 'hovering': { 'x': 334, 'y': 21 }, + 'flying': { 'x': 641, 'y': 29 }, + 'flying-up': { 'x': 378, 'y': 182 }, + 'flying-down': { 'x': 247, 'y': 177 }, + 'trotting-to-flying': { 'x': 601, 'y': 188 }, + 'flying-to-trotting': { 'x': 805, 'y': 270 }, + 'swinging': { 'x': 484, 'y': 238 }, + 'swimming-to-flying': { 'x': 1063, 'y': 182 }, + 'flying-to-swimming': { 'x': 938, 'y': 232 }, + 'booping-swimming': { 'x': 1144, 'y': 432 } +}; + +@Component({ + selector: 'tools-states', + templateUrl: 'tools-states.pug', + styleUrls: ['tools-states.scss'], +}) +export class ToolsStates { + readonly homeIcon = faHome; + private startX = 0; + private startY = 0; + arrowColors = ['orange', 'red', 'lime']; + states = ponyStates.map(state => { + const def = defaultPositions[state.name] || { x: 0, y: 0 }; + + return { + color: state.name === 'any' ? 'orange' : (state.animation.loop ? 'LightSeaGreen' : 'cornflowerblue'), + name: state.name, + variants: Object.keys(state.variants || {}).join(', '), + state, + x: getPos(state.name, 'x', def.x), + y: getPos(state.name, 'y', def.y), + }; + }); + arrows: { path: string; color: string; }[] = []; + times: { x: number; y: number; color: string; text: string; title?: string; }[] = []; + constructor() { + this.updateArrows(); + } + drag(state: any, { dx, dy, type }: AgDragEvent) { + if (type === 'start') { + this.startX = state.x; + this.startY = state.y; + } + + setPos(state.name, 'x', state.x = this.startX + dx); + setPos(state.name, 'y', state.y = this.startY + dy); + this.updateArrows(); + } + logPositions() { + const positions = fromPairs(this.states.map(({ name, x, y }) => [name, { x, y }])); + console.log(JSON.stringify(positions).replace(/"/g, `'`).replace(/},/g, '},\n')); + } + private updateArrows() { + this.times = []; + this.arrows = flatten(this.states.map(s => s.state.from.map(f => ({ + to: s, + from: this.findState(f.state), + color: f.exitAfter === 0 ? (f.keepTime ? 'orange' : 'red') : 'lime', + exitAfter: f.exitAfter, + enterTime: f.enterTime, + onlyDirectTo: f.onlyDirectTo, + })))) + .filter(({ from, to }) => from && to) + .map(({ from, to, color, exitAfter, enterTime, onlyDirectTo }) => { + const length = distance(from, to) || 1; + const r1 = 50; + const nx1 = ((to.x - from.x) / length) * r1; + const ny1 = ((to.y - from.y) / length) * r1; + const r2 = 60; + const nx2 = ((to.x - from.x) / length) * r2; + const ny2 = ((to.y - from.y) / length) * r2; + const r3 = 75; + const nx3 = ((to.x - from.x) / length) * r3; + const ny3 = ((to.y - from.y) / length) * r3; + const r4 = 80; + const nx4 = ((to.x - from.x) / length) * r4; + const ny4 = ((to.y - from.y) / length) * r4; + + const fromX = from.x + nx1; + const fromY = from.y + ny1; + + if (exitAfter) { + this.times.push({ x: fromX, y: fromY, color, text: exitAfter.toFixed(1) }); + } + + if (enterTime) { + this.times.push({ x: to.x - nx3, y: to.y - ny3, color, text: enterTime.toFixed(1) }); + } + + if (onlyDirectTo) { + this.times.push({ + x: from.x + nx4, y: from.y + ny4, color, text: '?', title: `only directly to: ${onlyDirectTo.name}` + }); + } + + return { path: `M ${fromX} ${fromY} L ${to.x - nx2} ${to.y - ny2}`, color }; + }); + } + private findState(state: AnimatorState) { + return this.states.find(s => s.state === state)!; + } +} diff --git a/src/ts/components/tools/tools-ui/tools-ui.pug b/src/ts/components/tools/tools-ui/tools-ui.pug new file mode 100644 index 0000000..98fff2e --- /dev/null +++ b/src/ts/components/tools/tools-ui/tools-ui.pug @@ -0,0 +1,285 @@ +ng-template(#modal) + settings-modal((close)="modalRef.hide()") + +.container-fluid.mt-3(*ngIf="initialized") + tabset(type="pills" saveActiveTab="tools-ui-main-tab") + tab(title="General") + ng-template(tabContent) + .mt-2.d-flex + div(style="width: 300px;") + .mb-3 + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + + h3 Color picker + .mb-3 + .form-group + color-picker([(color)]="color") + .form-group + color-picker([(color)]="color") + + h3 Checkbox + .mb-3 + .form-group.mb-2.d-flex.align-items-center + check-box([(checked)]="customOutlines") + check-box.ml-2([(checked)]="checked" [icon]="heartIcon") + check-box.ml-2([(checked)]="checked" [icon]="lockIcon" [disabled]="true") + label.ml-2 + | check box + span.badge.badge-success.ml-2(*ngIf="checked") checked + .form-group + button.btn.btn-default(btnCheckbox [(ngModel)]="checked") btnCheckbox + .btn-group.ml-2(btnRadioGroup [(ngModel)]="radio") + button.btn.btn-default(btnRadio="a") a + button.btn.btn-default(btnRadio="b") b + button.btn.btn-default(btnRadio="c") c + span.ml-2 btnRadio: {{radio}} + + h3 Slider + .mb-3 + .form-group + label + span#slider-label music volume: + span.text-muted.ml-1 {{slider}} + .form-group + slider-bar([(value)]="slider" [step]="1" labelledBy="slider-label") + .form-group + slider-bar.w-75([(value)]="slider" [step]="1" [disabled]="true" label="another slider") + + h3 Emotes + .mb-3 + .form-group + p. + I #[emote-box(emote="blue_heart")] you. #[emote-box(emote="rock")] #[emote-box(emote="🍎")] + #[emote-box(emote="imp")] #[emote-box(emote="face")] + + h3 Custom checkbox + .mb-3 + custom-checkbox([(checked)]="customChecked") Label here + | checked: {{customChecked}} + + h3 Supporter stars + .mb-3 + .form-group + fa-icon.supporter-1([icon]="starIcon" size="lg") + fa-icon.supporter-2.ml-1([icon]="starIcon" size="lg") + fa-icon.supporter-3.ml-1([icon]="starIcon" size="lg") + + h3 External links + .mb-3 + .form-group + a(href="http://google.com/") should open in new tab + + .ml-5(style="width: 400px;") + h3 Character preview + .mb-3 + character-preview([pony]="pony" name="Offline Pony" [tag]="selected.tag") + + h3 Sprite box + .mb-3 + .form-group.d-flex + sprite-box([class.active]="spriteActive" (click)="spriteActive = !spriteActive" [sprite]="sprite" fill="red" outline="maroon" circle="gray") + sprite-box([class.active]="spriteActive" [sprite]="sprite" [fill]="fills" [outline]="outlines") + .form-group + color-picker([(color)]="fills[0]") + + h3 Dropdown + .mb-3.d-flex + .dropdown(dropdown [autoClose]="autoCloseDropdown") + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | toggle me + .dropdown-menu(*dropdownMenu) + .dropdown-item aaa + .dropdown-item bbb + .dropdown-item ccc + .dropdown.ml-1(dropdown [autoClose]="autoCloseDropdown") + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | 2nd one + .dropdown-menu(*dropdownMenu) + .dropdown-item 111 + .dropdown-item 222 + .dropdown-item 333 + .btn-group.ml-1(btnRadioGroup [(ngModel)]="autoCloseDropdown") + button.btn([btnRadio]="true" [btnHighlight]="autoCloseDropdown === true") true + button.btn([btnRadio]="false" [btnHighlight]="autoCloseDropdown === false") false + button.btn([btnRadio]="'outsideClick'" [btnHighlight]="autoCloseDropdown === 'outsideClick'") outsideClick + + h3 Tabset + .mb-3 + tabset(type="pills" saveActiveTab="tools-ui-tab-active-index") + tab(title="A") + ng-template(tabContent) aaa + tab(title="B") + ng-template(tabContent) bbb + tab(title="C") + ng-template(tabContent) ccc + tab(title="D" [disabled]="true") + ng-template(tabContent) ddd + + .ml-5(style="width: 400px;") + h3 Bitmap box + .mb-3 + bitmap-box([bitmap]="pony.cm" tool="brush" [color]="color" [width]="cmSize" [height]="cmSize") + + h3 Sprite set selection + .mb-3 + .form-group + set-selection( + label="Tail" [base]="baseHairColor" [set]="pony.tail" [sets]="tails" + [outlineHidden]="!customOutlines" (change)="changed()") + + tab(title="Game") + ng-template(tabContent) + .mt-3.d-flex + div(style="width: 300px") + h3 Modal + .mb-3 + .form-group + button.btn.btn-default((click)="showModal(modal)") show modal + + h3 Supporter stars + .mb-3 + .form-group + custom-checkbox([(checked)]="isPartyLeader") is party leader + .form-group + party-list(style="position: absolute;") + + .ml-5(style="width: 400px") + h3 Pony box + .mb-3 + .form-group.py-4 + pony-box([pony]="selected") + .form-group.pt-3 + .btn-group.btn-group-sm + button.btn([btnHighlight]="isIgnored(selected)" (click)="toggleIgnored(selected)") ignore + button.btn([btnHighlight]="selected.modInfo.note" (click)="selected.modInfo.note = selected.modInfo.note ? '' : 'test'") note + button.btn([btnHighlight]="isHidden(selected)" (click)="toggleHidden(selected)") hide + .btn-group.btn-group-sm.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) mute + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="selected.modInfo.mute=''") none + a.dropdown-item((click)="selected.modInfo.mute='perma'") perma + a.dropdown-item((click)="selected.modInfo.mute='5 hours'") timed + .btn-group.btn-group-sm.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) shadow + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="selected.modInfo.shadow=''") none + a.dropdown-item((click)="selected.modInfo.shadow='perma'") perma + a.dropdown-item((click)="selected.modInfo.shadow='5 hours'") timed + .btn-group.btn-group-sm.dropdown(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) tag: {{selected.tag || 'none'}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let tag of tags" (click)="selected.tag = tag") {{tag || 'none'}} + .form-group + custom-checkbox([(checked)]="isFriend") is friend + + h3 Portrait + .mb-3 + .form-group.d-flex + portrait-box([pony]="pal") + portrait-box.ml-2([pony]="pal" size="medium") + portrait-box.ml-2([pony]="pal" size="small") + portrait-box.ml-2([pony]="pal" size="small" [noBorder]="true" [flip]="true") + + .ml-5(style="width: 400px") + h3 Settings box + .mb-3 + div(style="position: relative") + settings-box + canvas#canvas(width="300" height="100" style="background: #111; width: 300px; height: 100px; position: static;") + + h3 Page loader + .mb-3 + .form-group + page-loader + + tab(title="Chatlog") + ng-template(tabContent) + .mt-2 + .bg-grass(style="width: 800px; height: 600px; position: relative;") + chat-log(#chatlog style="position: absolute; left: 5px; bottom: 5px; right: 5px;") + .mt-2 + button.btn.btn-default((click)="addMessage(chatlog, 'some message')") + | Add message + button.btn.btn-default.ml-1((click)="addMessage(chatlog, 'some 🍎 message')") + | Add emote message + button.btn.btn-default.ml-1((click)="addWhisper(chatlog, 'some whisper message')") + | Add whisper + button.btn.ml-1((click)="spamChat(chatlog)" [btnHighlight]="spamChatInterval") + | Spam chat + slider-bar([(value)]="chatlogOpacity" style="width: 300px") + span {{chatlogOpacity / 100 | percent}} + + tab(title="Settings") + ng-template(tabContent) + .modal.show.d-block(style="margin-top: 55px") + .modal-dialog + .modal-content + settings-modal([focusTrap]="focusTrap") + button.btn.btn-default((click)="focusTrap = !focusTrap" style="position: fixed; top: 10px; right: 10px;") + | Toggle focus trap + + tab(title="Invites") + ng-template(tabContent) + .modal.show.d-block(style="margin-top: 55px") + .modal-dialog + .modal-content + invites-modal([focusTrap]="true") + + tab(title="Actions") + ng-template(tabContent) + .modal.show.d-block(style="margin-top: 55px; height: calc(100% - 140px)") + .modal-dialog + .modal-content + actions-modal((close)="saveActions()") + action-bar([editable]="actionBarEditable" style="position: fixed; left: 50px; bottom: 5px;") + custom-checkbox([(checked)]="actionBarEditable" style="position: fixed; left: 10px; bottom: 5px;" title="editable") + color-picker([(color)]="expressionActionsColor" style="position: fixed; top: 60px; left: 10px; width: 250px; z-index: 10000;") + + tab(title="Camera angle") + ng-template(tabContent) + .mt-3.d-flex + div(style="width: 400px") + h3 Camera angle + .mb-3 + .form-group.d-flex.align-items-center + label.mb-0 angle: + slider-bar([(value)]="angle" [max]="90") + .text-muted(style="width: 100px") {{angle | number:'0.0-0'}} deg + .form-group.d-flex.align-items-center + label.mb-0 horizontal tile height: + .text-muted.ml-2 {{horizontalTileHeight}} px + .form-group.d-flex.align-items-center + label.mb-0 vertical tile height: + .text-muted.ml-2 {{verticalTileHeight}} px + + tab(title="Virtual list") + ng-template(tabContent) + style. + .list { + background: #eee; color: #222; width: 250px; height: 500px; + } + .list2 { + background: #eee; color: #222; width: 250px; max-height: 500px; + } + .item { + height: 42px; padding: 10px; border-bottom: solid 1px #ddd; + } + .d-flex + div(style="margin: 50px;") + virtual-list.list([itemSize]="42") + .item(*virtualFor="let item of virtualItems; index as i; count as c") + div {{item.name}} [{{i}}/{{c}}] + + div(style="margin: 50px;") + virtual-list.list2([itemSize]="42") + .item(*virtualFor="let item of virtualItems2; index as i; count as c") + div {{item.name}} [{{i}}/{{c}}] + + div(style="margin: 50px;") + button.btn.btn-default((click)="virtualItems2.push({ name: 'An item ' + virtualItems2.length })") Add + button.btn.btn-default.ml-1((click)="virtualItems2.pop()") Remove + button.btn.btn-default.ml-1((click)="virtualItems2.push(virtualItems2.shift())") Move + +#range-indicator diff --git a/src/ts/components/tools/tools-ui/tools-ui.ts b/src/ts/components/tools/tools-ui/tools-ui.ts new file mode 100644 index 0000000..3035a1a --- /dev/null +++ b/src/ts/components/tools/tools-ui/tools-ui.ts @@ -0,0 +1,243 @@ +import { Component, OnInit, NgZone, TemplateRef, OnDestroy } from '@angular/core'; +import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; +import { random } from 'lodash'; +import { OFFLINE_PONY, CM_SIZE, SUPPORTER_PONY, DEFAULT_CHATLOG_OPACITY } from '../../../common/constants'; +import { toPalette, mockPaletteManager, getBaseFill, syncLockedPonyInfo } from '../../../common/ponyInfo'; +import * as sprites from '../../../generated/sprites'; +import { PonyTownGame, redrawActionButtons } from '../../../client/game'; +import { fromNow, setFlag, times } from '../../../common/utils'; +import { ChatLog } from '../../shared/chat-log/chat-log'; +import { randomString } from '../../../common/stringUtils'; +import { MessageType, Entity, EntityPlayerState } from '../../../common/interfaces'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { SettingsService } from '../../services/settingsService'; +import { faHome, faStar, faLock, faHeart } from '../../../client/icons'; +import { decompressPonyString } from '../../../common/compressPony'; +import { getAllTags } from '../../../common/tags'; +import { Model } from '../../services/model'; +import { isPartyLeader } from '../../../client/partyUtils'; +import { createPony } from '../../../common/pony'; +import { serializeActions, deserializeActions } from '../../../client/buttonActions'; +import { initializeToys } from '../../../client/ponyDraw'; +import { ACTION_EXPRESSION_BG, updateActionColor } from '../../../common/colors'; +import { parseColor, colorToCSS, colorNames } from '../../../common/color'; +import { isHidden, isIgnored, isFriend } from '../../../common/entityUtils'; +import { initFeatureFlags } from '../../../client/clientUtils'; + +const offlinePonyInfo = decompressPonyString(OFFLINE_PONY, true); +const offlinePonyPal = toPalette(offlinePonyInfo); +const defaultPalette = mockPaletteManager.addArray(sprites.defaultPalette); + +const offlinePony = createPony(1, 0, OFFLINE_PONY, defaultPalette, mockPaletteManager); +offlinePony.name = 'Offline pony'; + +const supporterPony = createPony(2, 0, SUPPORTER_PONY, defaultPalette, mockPaletteManager); +supporterPony.name = 'Supporter pony'; + +const pendingPony = createPony(3, 0, SUPPORTER_PONY, defaultPalette, mockPaletteManager); +pendingPony.name = 'Pending pony'; + +const tails = sprites.tails[0]!.slice(); +const labels = ['none', 'Long tail', 'Short tail', 'Short smooth tail', 'Long puffy tail', 'Long wavy tail']; +tails.forEach((t, i) => t ? t[0].label = labels[i] : undefined); +const colors = Object.values(colorNames); + +@Component({ + selector: 'tools-ui', + templateUrl: 'tools-ui.pug', +}) +export class ToolsUI implements OnInit, OnDestroy { + readonly homeIcon = faHome; + readonly starIcon = faStar; + readonly heartIcon = faHeart; + readonly lockIcon = faLock; + isHidden = isHidden; + isIgnored = isIgnored; + focusTrap = true; + tails = tails; + cmSize = CM_SIZE; + pony = offlinePonyInfo; + customOutlines = false; + pal = offlinePonyPal; + color = 'cornflowerblue'; + checked = true; + radio = 'a'; + slider = 50; + sprite = sprites.tails[0]![1]![0]; + fills = ['ff0000', '00ff00']; + outlines = ['990000', '009900']; + spriteActive = false; + selected = offlinePony; + timeout = fromNow(1000 * 3600 * 10).toISOString(); + autoCloseDropdown: any = true; + spamChatInterval: any; + initialized = false; + customChecked = false; + actionBarEditable = true; + tags = ['', ...getAllTags().map(t => t.id)]; + animationFrame: any; + virtualItems = times(1000, i => ({ value: i, name: `This is item ${i}`, color: colors[i % colors.length] })); + virtualItems2 = [{ name: 'An item 0' }]; + constructor( + private game: PonyTownGame, + private zone: NgZone, + public settings: SettingsService, + private modalService: BsModalService, + private model: Model, + ) { + this.selected.name = 'Offline Pony'; + this.selected.site = { + id: '', + name: 'Offline Pony (official)', + provider: 'twitter', + url: 'https://twitter.com/offlinepony', + }; + this.selected.tag = 'dev'; + this.selected.modInfo = { + account: 'offline-pony [abc]', + country: 'PL', + counters: { swears: 5 }, + age: 12, + }; + + game.player = { + id: 123, + name: 'Player pony', + } as any; + game.party = { + leaderId: 0, + members: [ + { id: 1, leader: true, offline: false, pending: false, pony: offlinePony, self: false }, + { id: 2, leader: false, offline: true, pending: false, pony: supporterPony, self: false }, + { id: 3, leader: false, offline: false, pending: true, pony: pendingPony, self: false }, + ], + }; + game.onClock.next('00:00'); + game.failedFBO = true; + game.send = (action: (server: any) => T) => action({ + action() { }, + select() { }, + say() { }, + expression() { }, + getInvites: () => Promise.resolve([ + { id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, + { id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, + { id: 'c', info: OFFLINE_PONY, name: 'Meno', active: true }, + { id: 'd', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, + { id: 'e', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, + { id: 'f', info: OFFLINE_PONY, name: 'Meno', active: false }, + { id: 'g', info: OFFLINE_PONY, name: 'Offline Pony', active: false }, + { id: 'h', info: OFFLINE_PONY, name: 'Fuzzy', active: false }, + { id: 'i', info: OFFLINE_PONY, name: 'Meno', active: false }, + { id: 'j', info: OFFLINE_PONY, name: 'Meno', active: false }, + ]), + } as any); + } + ngOnInit() { + initFeatureFlags({}); + + return loadAndInitSpriteSheets() + .then(() => { + initializeToys(mockPaletteManager); + this.initialized = true; + this.model.loading = true; + this.zone.runOutsideAngular(() => this.update()); + }); + } + ngOnDestroy() { + cancelAnimationFrame(this.animationFrame); + } + get baseHairColor() { + return getBaseFill(this.pony.mane); + } + get isFriend() { + return isFriend(this.selected); + } + set isFriend(value) { + this.selected.playerState = setFlag(this.selected.playerState, EntityPlayerState.Friend, value); + } + update() { + this.animationFrame = requestAnimationFrame(() => this.update()); + redrawActionButtons(this.game.actionsChanged); + this.game.actionsChanged = false; + this.game.onFrame.next(); + } + changed() { + syncLockedPonyInfo(this.pony); + } + toggleIgnored(entity: Entity) { + entity.playerState = setFlag(entity.playerState, EntityPlayerState.Ignored, !isIgnored(entity)); + } + toggleHidden(entity: Entity) { + entity.playerState = setFlag(entity.playerState, EntityPlayerState.Hidden, !isHidden(entity)); + } + spamChat(chatlog: ChatLog) { + if (this.spamChatInterval) { + clearInterval(this.spamChatInterval); + this.spamChatInterval = 0; + } else { + this.spamChatInterval = 1; + this.zone.runOutsideAngular(() => this.spamChatInterval = setInterval(() => { + chatlog.addMessage({ + id: 0, + crc: undefined, + name: randomString(random(1, 20)), + message: randomString(random(1, 40)), + type: MessageType.Chat + }); + }, 50)); + } + } + get isPartyLeader() { + return isPartyLeader(this.game); + } + set isPartyLeader(value: boolean) { + if (value) { + this.game.party!.leaderId = this.game.player!.id; + } else { + this.game.party!.leaderId = 1; + } + } + get chatlogOpacity() { + return this.settings.account.chatlogOpacity || DEFAULT_CHATLOG_OPACITY; + } + set chatlogOpacity(value: number) { + this.settings.account.chatlogOpacity = value; + } + addMessage(chatlog: ChatLog, message: string) { + chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Chat }); + } + addWhisper(chatlog: ChatLog, message: string) { + chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Whisper }); + } + angle = 45; + get angleInRad() { + return (this.angle / 180) * Math.PI; + } + get horizontalTileHeight() { + return 32 * Math.sin(this.angleInRad); + } + get verticalTileHeight() { + return 32 * Math.cos(this.angleInRad); + } + // angle = Math.asin(expectedHorizontalTileHeight / 32) // 0.848062078981481 + modalRef?: BsModalRef; + showModal(template: TemplateRef) { + this.modalRef = this.modalService.show(template, {}); + } + saveActions() { + if (DEVELOPMENT) { + const serialized = serializeActions(this.game.actions); + this.game.actions = deserializeActions(serialized); + console.log(serialized); + } + } + // actions + get expressionActionsColor() { + return ACTION_EXPRESSION_BG; + } + set expressionActionsColor(value) { + updateActionColor(colorToCSS(parseColor(value))); + this.game.actionsChanged = true; + } +} diff --git a/src/ts/components/tools/tools-variants/tools-variants.pug b/src/ts/components/tools/tools-variants/tools-variants.pug new file mode 100644 index 0000000..b0c6c78 --- /dev/null +++ b/src/ts/components/tools/tools-variants/tools-variants.pug @@ -0,0 +1,44 @@ +.form-inline.p-1 + .form-group + a.btn.btn-default(routerLink="/") + fa-icon([icon]="homeIcon") + .form-group + label.control-label.ml-2 horizontal: + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle style="width: 140px;") + | {{horizontal}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let f of fields" (click)="horizontal = f; redraw();") {{f}} + + .form-group + label.control-label.ml-2 vertical: + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle style="width: 140px;") + | {{vertical}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item(*ngFor="let f of fields" (click)="vertical = f; redraw();") {{f}} + + .form-group + label.control-label.ml-2 coat: + color-picker.color-picker-inline.ml-1([(color)]="coat" (colorChange)="redraw()") + + .form-group + label.control-label.ml-2 hair: + color-picker.color-picker-inline.ml-1([(color)]="hair" (colorChange)="redraw()") + + .form-group + label.control-label.ml-2 just head + check-box.check-box-inline.ml-1([(checked)]="justHead" (checkedChange)="redraw()") + + .form-group + label.control-label.ml-2 + .btn-group.dropdown.ml-1(dropdown) + button.btn.btn-default.dropdown-toggle(dropdownToggle) + | ×{{scale}} + .dropdown-menu(*dropdownMenu) + a.dropdown-item((click)="scale = 1; redraw()") ×1 + a.dropdown-item((click)="scale = 2; redraw()") ×2 + a.dropdown-item((click)="scale = 3; redraw()") ×3 + a.dropdown-item((click)="scale = 4; redraw()") ×4 + +canvas(#canvas) diff --git a/src/ts/components/tools/tools-variants/tools-variants.scss b/src/ts/components/tools/tools-variants/tools-variants.scss new file mode 100644 index 0000000..43dc59d --- /dev/null +++ b/src/ts/components/tools/tools-variants/tools-variants.scss @@ -0,0 +1,10 @@ +.color-picker-inline { + display: inline-block; + width: 150px; +} + +.check-box-inline { + display: inline-block; + vertical-align: middle; + margin-top: 2px; +} diff --git a/src/ts/components/tools/tools-variants/tools-variants.ts b/src/ts/components/tools/tools-variants/tools-variants.ts new file mode 100644 index 0000000..d554d0b --- /dev/null +++ b/src/ts/components/tools/tools-variants/tools-variants.ts @@ -0,0 +1,97 @@ +import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; +import { PonyInfo, PonyState } from '../../../common/interfaces'; +import { createCanvas, disableImageSmoothing } from '../../../client/canvasUtils'; +import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; +import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; +import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +import { compressPonyString, decompressPony } from '../../../common/compressPony'; +import { drawPony } from '../../../client/ponyDraw'; +import { faHome } from '../../../client/icons'; +import { paletteSpriteSheet } from '../../../generated/sprites'; + +@Component({ + selector: 'tools-variants', + templateUrl: 'tools-variants.pug', + styleUrls: ['tools-variants.scss'], +}) +export class ToolsVariants implements OnInit { + readonly homeIcon = faHome; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + fields: string[]; + vertical: keyof PonyInfo = 'backMane'; + horizontal: keyof PonyInfo = 'mane'; + coat = 'red'; + hair = 'gold'; + justHead = false; + scale = 2; + private pony: PonyInfo = createDefaultPony(); + private state: PonyState = defaultPonyState(); + constructor() { + this.fields = Object.keys(this.pony) + .filter(key => { + const value = (this.pony as any)[key]; + return value && value.type !== undefined; + }); + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + redraw() { + this.draw(); + } + private draw() { + this.pony.coatFill = this.coat; + this.pony.mane!.fills![0] = this.hair; + syncLockedPonyInfo(this.pony); + + this.fields.forEach(f => (this.pony as any)[f].type = 0); + (this.pony as any)[this.vertical].type = 999; + (this.pony as any)[this.horizontal].type = 999; + + const fixed: any = decompressPony(compressPonyString(this.pony)); + const maxX = fixed[this.horizontal].type; + const maxY = fixed[this.vertical].type; + + const scale = this.scale; + const info = toPalette(this.pony); + const buffer = createCanvas(80, 80); + const batch = new ContextSpriteBatch(buffer); + const options = defaultDrawPonyOptions(); + + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + canvas.width = ((maxX + 1) * (this.justHead ? 45 : 60) + 10) * scale; + canvas.height = ((maxY + 1) * (this.justHead ? 45 : 60) + 10) * scale; + + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + viewContext.scale(scale, scale); + + viewContext.fillStyle = 'LightGreen'; + viewContext.fillRect(0, 0, canvas.width, canvas.height); + + for (let y = 0; y <= maxY; y++) { + (info as any)[this.vertical].type = y; + + for (let x = 0; x <= maxX; x++) { + batch.start(paletteSpriteSheet, 0); + + (info as any)[this.horizontal].type = x; + + drawPony(batch, info, this.state, 40, 60, options); + + batch.end(); + + if (this.justHead) { + viewContext.drawImage(buffer, 0, 0, 55, 45, x * 45 - 10, y * 45, 55, 45); + } else { + viewContext.drawImage(buffer, x * 60 - 10, y * 60); + } + } + } + + viewContext.restore(); + } +} diff --git a/src/ts/components/tools/tools-webgl/tools-webgl.pug b/src/ts/components/tools/tools-webgl/tools-webgl.pug new file mode 100644 index 0000000..dd31d5f --- /dev/null +++ b/src/ts/components/tools/tools-webgl/tools-webgl.pug @@ -0,0 +1,38 @@ +style. + .canvas-container { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + } + + #ref { + position: absolute; + top: 10px; + right: 10px; + box-shadow: 0 0 5px #000; + display: none; + } + + #canvas { + position: fixed; + left: 0; + top: 0; + } + + #fps { + position: fixed; + top: 5px; + right: 5px; + font-family: monospace; + padding: 2px 7px; + background: black; + border-radius: 3px; + } + +.canvas-container + canvas#canvas.pixelart(#canvas width="800" height="600") + canvas#ref(#canvas2) + #fps 0 diff --git a/src/ts/components/tools/tools-webgl/tools-webgl.ts b/src/ts/components/tools/tools-webgl/tools-webgl.ts new file mode 100644 index 0000000..98814c1 --- /dev/null +++ b/src/ts/components/tools/tools-webgl/tools-webgl.ts @@ -0,0 +1,928 @@ +import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; +// import { range, sample } from 'lodash'; +// import { PonyState, Palette, Sprite } from '../../../common/interfaces'; +// import { startGameLoop } from '../../../client/gameLoop'; +// import { defaultPonyState } from '../../../client/ponyHelpers'; +// import { SpriteBatch } from '../../../graphics/spriteBatch'; +// import { loadSpriteSheets, loadAndInitSpriteSheets } from '../../../client/spriteUtils'; +// import { Key } from '../../../client/input/input'; +// import { trot } from '../../../client/ponyAnimations'; +// import { createTexturesForSpriteSheets } from '../../../graphics/spriteSheetUtils'; +// import { PaletteManager, releasePalette } from '../../../graphics/paletteManager'; +// import { getWebGLContext, createViewMatrix2 } from '../../../graphics/webgl/webglUtils'; +// import * as sprites from '../../../generated/sprites'; +// import { SpriteBatch2 } from '../../../graphics/spriteBatch2'; +// import { loadImage, createCanvas } from '../../../client/canvasUtils'; +// import { WHITE, ORANGE, RED, BLUE } from '../../../common/colors'; +// import { PaletteSpriteBatch } from '../../../graphics/paletteSpriteBatch'; +// import { PaletteSpriteBatch2 } from '../../../graphics/paletteSpriteBatch2'; +// import { PaletteSpriteBatch3 } from '../../../graphics/paletteSpriteBatch3'; +// import { createFrameBuffer, bindFrameBuffer } from '../../../graphics/webgl/glFbo'; +// import { createTexture, Texture2D } from '../../../graphics/webgl/texture2d'; +// import { times } from '../../../common/utils'; +// import { createShader } from '../../../graphics/webgl/shader-new'; +// import { +// lightShader, spriteShader, sprite2Shader, paletteDepthShader, paletteLayersShader, paletteLayersInstancedShader +// } from '../../../generated/shaders'; + +@Component({ + selector: 'tools-webgl', + templateUrl: 'tools-webgl.pug', +}) +export class ToolsWebgl implements OnInit { + @ViewChild('canvas', { static: true }) canvasElement!: ElementRef; + @ViewChild('canvas2', { static: true }) canvasElement2!: ElementRef; + ngOnInit() { + // testSpriteBatch(this.canvasElement.nativeElement); + } +} + +// export function testLightsShader(canvas: HTMLCanvasElement) { +// const gl = getWebGLContext(canvas); +// const shader = createShader(gl, lightShader); +// const batch = new SpriteBatch(gl); +// const fps = document.getElementById('fps')!; + +// const handle = startGameLoop({ +// fps: 0, +// load() { +// return loadAndInitSpriteSheets(); +// }, +// init() { +// createTexturesForSpriteSheets(gl, sprites.spriteSheets); +// }, +// update(_delta: number) { +// }, +// draw() { +// const scale = 2; +// const width = Math.ceil(window.innerWidth / scale); +// const height = Math.ceil(window.innerHeight / scale); + +// gl.canvas.width = width; +// gl.canvas.height = height; +// gl.canvas.style.width = (width * scale) + 'px'; +// gl.canvas.style.height = (height * scale) + 'px'; + +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT); +// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); +// gl.disable(gl.DEPTH_TEST); +// gl.depthFunc(gl.LEQUAL); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000); + +// batch.begin(shader, {} as any); + +// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix); +// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1); + +// batch.drawImage(ORANGE, -1, -1, 2, 2, 100, 100, 300, 200); + +// batch.end(); +// }, +// }); + +// window.addEventListener('keyup', e => { +// if (e.keyCode === Key.KEY_Q) { +// handle.cancel(); +// fps.textContent = `off`; +// } +// }); +// } + +// export function testSpriteBatch(canvas: HTMLCanvasElement) { +// const gl = getWebGLContext(canvas); +// const shader = createShader(gl, paletteLayersShader); +// const batch = new PaletteSpriteBatch3(gl); +// const paletteManager = new PaletteManager(); +// const defaultPalette = paletteManager.addArray(sprites.defaultPalette); +// // const applePalette = paletteManager.addArray(sprites.apple_1.palettes![0]); +// const fps = document.getElementById('fps')!; + +// const handle = startGameLoop({ +// fps: 0, +// load() { +// return loadAndInitSpriteSheets(); +// }, +// init() { +// createTexturesForSpriteSheets(gl, sprites.spriteSheets); +// }, +// update(_delta: number) { +// }, +// draw() { +// const scale = 2; +// const width = Math.ceil(window.innerWidth / scale); +// const height = Math.ceil(window.innerHeight / scale); + +// gl.canvas.width = width; +// gl.canvas.height = height; +// gl.canvas.style.width = (width * scale) + 'px'; +// gl.canvas.style.height = (height * scale) + 'px'; + +// paletteManager.commit(gl); + +// batch.palette = paletteManager.texture; +// batch.rectSprite = sprites.pixel2; +// batch.defaultPalette = defaultPalette; + +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT); +// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); +// gl.disable(gl.DEPTH_TEST); +// gl.depthFunc(gl.LEQUAL); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000); + +// batch.begin(shader, {} as any); + +// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix); +// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize); + +// // batch.drawSprite(sprites.apple_1.color, WHITE, applePalette, 100, 100); +// batch.drawRect(ORANGE, 200, 200, 300, 150); + +// batch.end(); +// }, +// }); + +// window.addEventListener('keyup', e => { +// if (e.keyCode === Key.KEY_Q) { +// handle.cancel(); +// fps.textContent = `off`; +// } +// }); +// } + +// export function testInstancedSprites(canvas: HTMLCanvasElement) { +// const gl = getWebGLContext(canvas); +// const shader = createShader(gl, paletteLayersShader); +// const shader2 = createShader(gl, paletteLayersInstancedShader); +// const batch = new PaletteSpriteBatch3(gl); +// const batch2 = new InstancedPaletteSpriteBatch(gl); +// const paletteManager = new PaletteManager(); +// const defaultPalette = paletteManager.addArray(sprites.defaultPalette); +// const fps = document.getElementById('fps')!; +// const randomSprites = [ +// sprites.pumpkin_default, +// sprites.box_lanterns, +// sprites.flower_patch5, +// sprites.leafpile_medium, +// ]; + +// const width = 600; +// const height = 500; +// const itemCount = 100000; +// const items = times(itemCount, () => { +// const sprite = sample(randomSprites)!; + +// return { +// sprite: sprite.color, +// palette: paletteManager.addArray(sprite.palettes![0]), +// x: width * Math.random(), +// y: height * Math.random(), +// }; +// }); + +// let mode = 'instanced'; + +// const handle = startGameLoop({ +// fps: 0, +// load() { +// return loadAndInitSpriteSheets(); +// }, +// init() { +// createTexturesForSpriteSheets(gl, sprites.spriteSheets); +// }, +// update(_delta: number) { +// }, +// draw() { +// const scale = 2; +// const width = Math.ceil(window.innerWidth / scale); +// const height = Math.ceil(window.innerHeight / scale); + +// gl.canvas.width = width; +// gl.canvas.height = height; +// gl.canvas.style.width = (width * scale) + 'px'; +// gl.canvas.style.height = (height * scale) + 'px'; + +// paletteManager.commit(gl); + +// batch.palette = paletteManager.texture; +// batch.rectSprite = sprites.pixelRect2; +// batch.defaultPalette = defaultPalette; + +// batch2.palette = paletteManager.texture; +// batch2.rectSprite = sprites.pixelRect2; +// batch2.defaultPalette = defaultPalette; + +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT); +// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); +// gl.disable(gl.DEPTH_TEST); +// gl.depthFunc(gl.LEQUAL); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000); + +// if (mode === 'regular') { +// batch.begin(shader, {} as any); +// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix); +// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize); + +// for (const item of items) { +// batch.drawSprite(item.sprite, WHITE, item.palette, item.x, item.y); +// } + +// batch.end(); +// } else if (mode === 'instanced') { +// batch2.begin(shader2); +// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix); +// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize); + +// for (const item of items) { +// batch2.drawSprite(item.sprite, WHITE, item.palette, item.x, item.y); +// } + +// batch2.end(); +// } + +// fps.textContent = `${this.fps.toFixed(0)} fps (${mode})`; +// }, +// }); + +// window.addEventListener('keyup', e => { +// if (e.keyCode === Key.KEY_Q) { +// handle.cancel(); +// fps.textContent = `off`; +// } + +// if (e.keyCode === Key.KEY_M) { +// mode = mode === 'regular' ? 'instanced' : 'regular'; +// } +// }); +// } + +// export function testLayeredSprites(canvas: HTMLCanvasElement, canvas2: HTMLCanvasElement) { +// function transferPixels(src: HTMLCanvasElement, dst: HTMLCanvasElement, srcChannel: number, dstChannel: number) { +// const srcContext = src.getContext('2d')!; +// const dstContext = dst.getContext('2d')!; +// const srcData = srcContext.getImageData(0, 0, src.width, src.height); +// const dstData = dstContext.getImageData(0, 0, dst.width, dst.height); + +// for (let y = 0; y < dstData.height; y++) { +// for (let x = 0; x < dstData.width; x++) { +// const offset = (x + y * dstData.width) * 4; +// dstData.data[offset + dstChannel] = srcData.data[offset + srcChannel]; +// } +// } + +// dstContext.putImageData(dstData, 0, 0); +// } + +// function transferPixels2(src: HTMLCanvasElement, dstData: ImageData, srcChannel: number, dstChannel: number) { +// const srcContext = src.getContext('2d')!; +// const srcData = srcContext.getImageData(0, 0, src.width, src.height); + +// for (let y = 0; y < srcData.height; y++) { +// for (let x = 0; x < srcData.width; x++) { +// const offset = (x + y * srcData.width) * 4; +// dstData.data[offset + dstChannel] = srcData.data[offset + srcChannel]; +// } +// } +// } + +// const gl = getWebGLContext(canvas); +// const shader = createShader(gl, paletteLayersShader); +// const batch = new PaletteSpriteBatch3(gl); +// const paletteManager = new PaletteManager(); +// const defaultPalette = paletteManager.addArray(sprites.defaultPalette); +// const palettes = [ +// paletteManager.addArray(sprites.pumpkin_default.palettes![0]), +// paletteManager.addArray(sprites.rock.palettes![0]), +// paletteManager.addArray(sprites.sign_1.palettes![0]), +// paletteManager.addArray(sprites.box_lanterns.palettes![0]), +// ]; + +// const theSprites: Sprite[] = []; + +// const handle = startGameLoop({ +// fps: 0, +// load() { +// return loadAndInitSpriteSheets(); +// }, +// init() { +// const canvas = canvas2; +// canvas2.style.display = 'block'; +// canvas.width = 256; +// canvas.height = 256; +// const context = canvas.getContext('2d')!; +// context.fillStyle = 'white'; +// context.globalAlpha = 200 / 0xff; +// context.fillRect(0, 0, 256, 256); +// context.globalAlpha = 1; +// const sheetData = (sprites.spriteSheets[1] as any).data; +// const sheetImage = createCanvas(sheetData.width, sheetData.height); +// sheetImage.getContext('2d')!.putImageData(sheetData, 0, 0); +// const sheetCanvas = createCanvas(256, 256); +// const sheetContext = sheetCanvas.getContext('2d')!; + +// const pixels = sheetContext.createImageData(256, 256); // new Uint8Array(256 * 256 * 4); + +// for (let i = 0; i < pixels.width * pixels.height * 4; i++) { +// pixels.data[i] = 255; +// } + +// const pumpkin = sprites.pumpkin_default.color; +// sheetContext.clearRect(0, 0, 256, 256); +// sheetContext.drawImage(sheetImage, pumpkin.x, pumpkin.y, pumpkin.w, pumpkin.h, 0, 0, pumpkin.w, pumpkin.h); +// transferPixels(sheetCanvas, canvas, 0, 0); +// transferPixels2(sheetCanvas, pixels, 0, 0); + +// const rock = sprites.rock.color; +// sheetContext.clearRect(0, 0, 256, 256); +// sheetContext.drawImage(sheetImage, rock.x, rock.y, rock.w, rock.h, 0, 0, rock.w, rock.h); +// transferPixels(sheetCanvas, canvas, 0, 1); +// transferPixels2(sheetCanvas, pixels, 0, 1); + +// const sign = sprites.sign_1.color; +// sheetContext.clearRect(0, 0, 256, 256); +// sheetContext.drawImage(sheetImage, sign.x, sign.y, sign.w, sign.h, 0, 0, sign.w, sign.h); +// transferPixels(sheetCanvas, canvas, 0, 2); +// transferPixels2(sheetCanvas, pixels, 0, 2); + +// const box = sprites.box_lanterns.color; +// sheetContext.clearRect(0, 0, 256, 256); +// sheetContext.drawImage(sheetImage, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h); +// // transferPixels(sheetCanvas, canvas, 0, 3); +// transferPixels2(sheetCanvas, pixels, 0, 3); + +// const texture = createTexture(gl, pixels); +// // texture.bind(0); +// // gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, 256, 256, 0, texture.format, texture.type, pixels); + +// const spr = { +// x: 0, +// y: 0, +// w: 50, +// h: 50, +// ox: 0, +// oy: 0, +// tex: texture, +// }; + +// theSprites.push({ ...spr, type: 3 }); +// theSprites.push({ ...spr, type: 4 }); +// theSprites.push({ ...spr, type: 5 }); +// theSprites.push({ ...spr, type: 6 }); + +// createTexturesForSpriteSheets(gl, sprites.spriteSheets); +// }, +// update(_delta: number) { +// }, +// draw() { +// const scale = 4; +// const width = Math.ceil(window.innerWidth / scale); +// const height = Math.ceil(window.innerHeight / scale); + +// gl.canvas.width = width; +// gl.canvas.height = height; +// gl.canvas.style.width = (width * scale) + 'px'; +// gl.canvas.style.height = (height * scale) + 'px'; + +// paletteManager.commit(gl); +// batch.palette = paletteManager.texture; +// batch.rectSprite = sprites.pixelRect2; +// batch.defaultPalette = defaultPalette; + +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); +// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); +// gl.enable(gl.DEPTH_TEST); +// gl.depthFunc(gl.LEQUAL); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// const transformMatrix = mat4.ortho(mat4.create(), 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0, 1000); + +// batch.begin(shader, {} as any); +// gl.uniformMatrix4fv(shader.uniforms.transfrom, false, transformMatrix); +// gl.uniform4f(shader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(shader.uniforms.pixelSize, paletteManager.pixelSize); +// batch.drawSprite(sprites.pumpkin_default.color, WHITE, palettes[0], 50, 50); +// batch.drawSprite(theSprites[0], WHITE, palettes[0], 50, 120); +// batch.drawSprite(theSprites[1], WHITE, palettes[1], 100, 120); +// batch.drawSprite(theSprites[2], WHITE, palettes[2], 150, 120); +// batch.drawSprite(theSprites[3], WHITE, palettes[3], 200, 120); +// batch.end(); +// }, +// }); + +// window.addEventListener('keyup', e => { +// if (e.keyCode === Key.KEY_Q) { +// handle.cancel(); +// } +// }); +// } + +// export function test3DMap(canvas: HTMLCanvasElement) { +// const gl = getWebGLContext(canvas); +// const paletteShader = createShader(gl, paletteDepthShader); +// const paletteSpriteBatch = new PaletteSpriteBatch2(gl); +// const paletteManager = new PaletteManager(); +// // const treePalette = paletteManager.add(sprites.tree_6Crown0_0.palettes![0]); +// const grassPalette = paletteManager.addArray(sprites.grass_tile.palettes![0]); +// // const wallPalette = paletteManager.add(sprites.stone_wall_6.palettes![0]); +// const defaultPalette = paletteManager.addArray(sprites.defaultPalette); + +// const camera = { +// x: -2, +// y: -2, +// w: 100, +// h: 100, +// }; + +// let redZ = 2; +// let blueZ = -2; + +// const handle = startGameLoop({ +// fps: 0, +// load() { +// return loadAndInitSpriteSheets(); +// }, +// init() { +// createTexturesForSpriteSheets(gl, sprites.spriteSheets); +// }, +// update(_delta: number) { +// }, +// draw() { +// const scale = 4; +// const width = Math.ceil(window.innerWidth / scale); +// const height = Math.ceil(window.innerHeight / scale); + +// camera.w = width / 32; +// camera.h = height / 32; + +// gl.canvas.width = width; +// gl.canvas.height = height; +// gl.canvas.style.width = (width * scale) + 'px'; +// gl.canvas.style.height = (height * scale) + 'px'; + +// paletteManager.commit(gl); +// paletteSpriteBatch.palette = paletteManager.texture; +// paletteSpriteBatch.rectSprite = sprites.pixelRect2; +// paletteSpriteBatch.defaultPalette = defaultPalette; + +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + +// gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + +// gl.enable(gl.DEPTH_TEST); +// gl.depthFunc(gl.LEQUAL); + +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// const viewMatrix = mat4.lookAt(mat4.create(), [0, 1, 1], [0, 0, 0], [0, 0, -1]); +// const projMatrix = mat4.ortho(mat4.create(), camera.x, camera.x + camera.w, camera.y + camera.h, camera.y, -1000, 1000); +// const transformMatrix = mat4.mul(mat4.create(), projMatrix, viewMatrix); + +// paletteSpriteBatch.begin(paletteShader, sprites.paletteSpriteSheet); +// gl.uniformMatrix4fv(paletteShader.uniforms.transfrom, false, transformMatrix); +// gl.uniform4f(paletteShader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(paletteShader.uniforms.pixelSize, paletteManager.pixelSize); + +// // for (let y = 0; y < 15; y++) { +// // for (let x = 0; x < 10; x++) { +// // paletteSpriteBatch.drawSprite(sprites.grass_tile.color, WHITE, grassPalette, x * 32, y * 24); +// // } +// // } + +// // paletteSpriteBatch.drawSprite(sprites.stone_wall_6.color!, WHITE, wallPalette, 60, 40); +// // paletteSpriteBatch.drawSprite(sprites.tree_6Crown0_0.color!, WHITE, treePalette, 120, 10); + +// const sprite = sprites.grass_tile.color; + +// function drawHTileAt(x: number, y: number, z: number, color: number) { +// paletteSpriteBatch.drawQuad(color, grassPalette, +// x + 0, y + 0, z, +// x + 1, y + 0, z, +// x + 1, y + 1, z, +// x + 0, y + 1, z, +// sprite.x, sprite.y, sprite.w, sprite.h); +// } + +// drawHTileAt(0.5, 0, redZ, RED); +// drawHTileAt(0, 0, 0, WHITE); +// drawHTileAt(0.5, 0, 0, ORANGE); + +// paletteSpriteBatch.drawQuad(BLUE, grassPalette, +// 0, 0, blueZ, +// 1, 0, blueZ, +// 1, 1, blueZ - 1, +// 0, 1, blueZ - 1, +// sprite.x, sprite.y, sprite.w, sprite.h); + +// paletteSpriteBatch.end(); +// }, +// }); + +// window.addEventListener('keyup', e => { +// if (e.keyCode === Key.KEY_Q) { +// handle.cancel(); +// } +// }); + +// window.addEventListener('keydown', e => { +// if (e.keyCode === Key.LEFT) { +// camera.x -= 1; +// } else if (e.keyCode === Key.RIGHT) { +// camera.x += 1; +// } else if (e.keyCode === Key.UP) { +// camera.y -= 1; +// } else if (e.keyCode === Key.DOWN) { +// camera.y += 1; +// } + +// if (e.keyCode === Key.KEY_A) { +// camera.x -= 1; +// } else if (e.keyCode === Key.KEY_D) { +// camera.x += 1; +// } else if (e.keyCode === Key.KEY_W) { +// camera.y -= 1; +// } else if (e.keyCode === Key.KEY_S) { +// camera.y += 1; +// } + +// if (e.keyCode === Key.KEY_R) { +// redZ -= 0.1; +// } else if (e.keyCode === Key.KEY_T) { +// redZ += 0.1; +// } + +// if (e.keyCode === Key.KEY_F) { +// blueZ -= 0.1; +// } else if (e.keyCode === Key.KEY_G) { +// blueZ += 0.1; +// } +// }); +// } + +// export function oldTests(canvas: HTMLCanvasElement) { +// let skewTime = 0; +// let animationTime = 0; +// let lightTexture: Texture2D; +// // let tilesTexture: Texture2D; + +// const gl = getWebGLContext(canvas); +// const spriteShaderInstance = createShader(gl, spriteShader); +// const paletteShader = createShader(gl, paletteDepthShader); +// const lightShader = createShader(gl, sprite2Shader); +// const spriteBatch = new SpriteBatch(gl); +// const spriteBatch2 = new SpriteBatch2(gl); +// const paletteSpriteBatch2 = new PaletteSpriteBatch2(gl); +// const paletteSpriteBatch = new PaletteSpriteBatch(gl); +// const paletteManager = new PaletteManager(); +// const palettes: Palette[] = []; +// const fbo = createFrameBuffer(gl, 1024, 1024, { depth: false }); +// //const light = createFBO(gl, 1024, 1024, { depth: true }); + +// // const INFO = createDefaultPony(); + +// const STATE: PonyState = { +// ...defaultPonyState(), +// animation: trot, +// animationFrame: 0, +// headAnimation: undefined, +// headAnimationFrame: 0, +// blinkFrame: 2, +// }; + +// const scale = 4; +// // const ponyInfo = toPalette(INFO, paletteManager); +// const treePalette = paletteManager.addArray(sprites.tree_6Crown0_0.palettes![0]); +// const rockPalette = paletteManager.addArray(sprites.rock.palettes![0]); +// const grassPalette = paletteManager.addArray(sprites.grass_2.palettes![0]); + +// let mouseX = 0, mouseY = 0; + +// // font.lineSpacing = 5; + +// canvas.addEventListener('mousemove', e => { +// mouseX = e.pageX / scale; +// mouseY = e.pageY / scale; +// }); + +// const handle = startGameLoop({ +// load() { +// return Promise.all([ +// //loadImage('/assets/images/pony2.png').then(img => pony2 = createTexture(gl, img, gl.RGBA, gl.UNSIGNED_BYTE)), +// // loadImage('/assets/images/tiles.png').then(img => tilesTexture = createTexture(gl, img)), +// loadImage('/images/light2.png').then(img => lightTexture = createTexture(gl, img)), +// loadSpriteSheets(sprites.spriteSheets, loadImage), +// ]).then(() => { +// createTexturesForSpriteSheets(gl, sprites.spriteSheets); +// }); +// }, +// init() { +// }, +// update(delta: number) { +// skewTime += delta * 2; // * 0.5; + +// while (skewTime > 1) { +// skewTime -= 1; +// } + +// STATE.animationFrame = Math.floor(animationTime * 24) % STATE.animation!.frames.length; +// }, +// draw() { +// gl.canvas.width = document.body.clientWidth; +// gl.canvas.height = document.body.clientHeight; +// gl.canvas.style.width = document.body.clientWidth + 'px'; +// gl.canvas.style.height = document.body.clientHeight + 'px'; + +// paletteManager.commit(gl); +// paletteSpriteBatch.palette = paletteManager.texture; + +// const width = Math.ceil(gl.canvas.width / scale); +// const height = Math.ceil(gl.canvas.height / scale); + +// // render color + +// bindFrameBuffer(fbo); + +// gl.viewport(0, 0, width, height); +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT); + +// gl.disable(gl.DEPTH_TEST); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// const viewMatrix = mat4.ortho(mat4.create(), 0, width, height, 0, 0, 1000); + +// paletteSpriteBatch.begin(paletteShader, {} as any); + +// gl.uniformMatrix4fv(paletteShader.uniforms.transfrom, false, viewMatrix); +// gl.uniform4f(paletteShader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(paletteShader.uniforms.pixelSize, paletteManager.pixelSize); + +// for (let y = 0; y < 15; y++) { +// for (let x = 0; x < 10; x++) { +// paletteSpriteBatch.drawSprite(sprites.grass_2.color, WHITE, grassPalette, x * 32, y * 24); +// } +// } + +// paletteSpriteBatch.drawSprite(sprites.tree_6Crown0_0.color!, WHITE, treePalette, 120, 10); +// paletteSpriteBatch.end(); + +// // render frame buffer (color) + +// gl.bindFramebuffer(gl.FRAMEBUFFER, null); +// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clear(gl.COLOR_BUFFER_BIT); +// gl.disable(gl.DEPTH_TEST); +// gl.disable(gl.BLEND); + +// const fboMatrix = mat4.ortho(mat4.create(), 0, width, height, 0, 0, 1000); + +// spriteBatch.begin(spriteShaderInstance, { texture: fbo.depth } as any); + +// gl.uniformMatrix4fv(spriteShaderInstance.uniforms.transfrom, false, fboMatrix); +// gl.uniform4f(spriteShaderInstance.uniforms.lighting, 1, 1, 1, 1); +// spriteBatch.drawImage(WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// spriteBatch.end(); +// }, +// draw2() { +// gl.canvas.width = document.body.clientWidth; +// gl.canvas.height = document.body.clientHeight; +// gl.canvas.style.width = document.body.clientWidth + 'px'; +// gl.canvas.style.height = document.body.clientHeight + 'px'; + +// paletteManager.commit(gl); +// paletteSpriteBatch2.palette = paletteManager.texture; + +// const width = Math.ceil(gl.canvas.width / scale); +// const height = Math.ceil(gl.canvas.height / scale); + +// // render color + +// bindFrameBuffer(fbo); + +// gl.viewport(0, 0, width, height); +// gl.clearColor(0.13, 0.5, 0.5, 1.0); +// gl.clearDepth(1000); //gl.DEPTH_CLEAR_VALUE); +// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + +// gl.enable(gl.DEPTH_TEST); +// //gl.depthMask(true); +// gl.depthFunc(gl.LEQUAL); +// //gl.depthRange(0, 1000); + +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +// //const mat = createViewMatrix(mat4.create(), width, height, 1); + +// const tst = mat4.create(); +// mat4.identity(tst); +// mat4.translate(tst, tst, vec3.fromValues(-1, 1, 0)); +// mat4.scale(tst, tst, vec3.fromValues(2 / width, -2 / height, 1)); +// mat4.scale(tst, tst, vec3.fromValues(1, 1, 0.005)); +// //mat4.translate(tst, tst, vec3.fromValues(0, 0, 0)); + +// const tst2 = mat4.ortho(mat4.create(), 0, width, height, 0, 0, 100); + +// //const viewMatrix = mat4.ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.y, camera.y + camera.h, 0, 100); +// //const fboMatrix = mat4.ortho( +// // this.fboMatrix, 0, this.canvas.width / actualScale, this.canvas.height / actualScale, 0, 0, 100); + +// //console.log(mat4.str(tst)); +// //console.log(mat4.str(tst2)); +// //handle.cancel(); + +// //spriteBatch.begin(spriteShader); +// //spriteShader.uniforms.transform = mat; +// //spriteShader.uniforms.lighting = [1, 1, s1, 1]; +// //spriteBatch.drawRect(GRAY, 10, 140, SIZE, SIZE); +// //spriteBatch.end(); + +// paletteSpriteBatch2.begin(paletteShader, {} as any); + +// gl.uniformMatrix4fv(paletteShader.uniforms.transfrom, false, tst2); +// gl.uniform4f(paletteShader.uniforms.lighting, 1, 1, 1, 1); +// gl.uniform1f(paletteShader.uniforms.pixelSize, paletteManager.pixelSize); + +// paletteSpriteBatch2.depth = -100; + +// for (let y = 0; y < 15; y++) { +// for (let x = 0; x < 10; x++) { +// paletteSpriteBatch2.drawSprite(sprites.grass_2.color, WHITE, grassPalette, x * 32, y * 24); +// } +// } + +// paletteSpriteBatch2.depth = -90; +// paletteSpriteBatch2.drawSprite(sprites.rock.color, WHITE, rockPalette, 65, 70); + +// paletteSpriteBatch2.depth = -80; +// // drawPony(paletteSpriteBatch2, ponyInfo, STATE, 100, 100, defaultDrawPonyOptions()); + +// //const tail = mat2d.create(); +// //mat2d.identity(tail); +// //mat2d.translate(tail, tail, vec2.fromValues(10, 60)); +// //skewY(tail, tail, (skewTime > 0.5 ? (1 - skewTime) : skewTime) * 0.4); +// //mat2d.translate(tail, tail, vec2.fromValues(-46, -44)); +// // +// //paletteSpriteBatch.transform = tail; +// //paletteSpriteBatch.drawSprite(sprites.ponTails[1][1].color, null, ponyInfo.tail.palette, 0, 0); +// //paletteSpriteBatch.transform = null; + +// paletteSpriteBatch2.depth = -60; +// paletteSpriteBatch2.drawSprite(sprites.tree_6Crown0_0.color, WHITE, treePalette, 120, 10); + +// //gl.depthMask(false); + +// paletteSpriteBatch2.end(); + +// //spriteBatch.begin(spriteShader); +// //spriteShader.uniforms.transform = mat; +// //spriteShader.uniforms.lighting = [1, 1, 1, 1]; +// //spriteBatch.drawRect(GRAY, 10, 140, paletteManager.textureSize, paletteManager.textureSize); +// //spriteBatch.drawImage( +// // paletteManager.texture, null, 0, 0, paletteManager.textureSize, paletteManager.textureSize, +// // 10, 140, paletteManager.textureSize, paletteManager.textureSize); +// //spriteBatch.drawImage(lightTexture, null, 0, 0, 64, 64, 50, 50, 64, 64); +// //spriteBatch.end(); + +// // render lighting test + +// // gl.blendEquation(gl.FUNC_ADD); +// // gl.blendFunc(gl.ONE, gl.ONE); +// // +// // spriteBatch2.begin(lightShader); +// // lightShader.uniforms.transform = tst; +// // spriteBatch2.defaultDepth = 70; +// // spriteBatch2.drawImage(lightTexture, null, 0, 0, 128, 128, mouseX - 64, mouseY - 64, 128, 128); +// // spriteBatch2.end(); + +// // render lighting + +// // light.bind(); +// // +// // gl.viewport(0, 0, width, height); +// // gl.clearColor(0.2, 0, 0, 1.0); +// // gl.clear(gl.COLOR_BUFFER_BIT); +// // +// // gl.disable(gl.DEPTH_TEST); +// // gl.enable(gl.BLEND); +// // gl.blendEquation(gl.FUNC_ADD); +// // gl.blendFunc(gl.ONE, gl.ONE); +// // +// // spriteBatch.begin(spriteShader); +// // spriteShader.uniforms.transform = mat; +// // spriteShader.uniforms.lighting = [1, 1, 1, 1]; +// // spriteBatch.drawImage(lightTexture, null, 0, 0, 128, 128, mouseX - 64, mouseY - 64, 128, 128); +// // spriteBatch.end(); + +// // render frame buffer (color) + +// gl.bindFramebuffer(gl.FRAMEBUFFER, null); +// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); +// gl.clearColor(1, 1, 1, 1); +// gl.clear(gl.COLOR_BUFFER_BIT); + +// gl.disable(gl.DEPTH_TEST); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.DST_COLOR, gl.ZERO); + +// spriteBatch.begin(spriteShaderInstance, { texture: fbo.depth } as any); + +// const viewMatrix = createViewMatrix2(mat4.create(), canvas.width, canvas.height, scale); +// gl.uniformMatrix4fv(spriteShaderInstance.uniforms.transfrom, false, viewMatrix); +// gl.uniform4f(spriteShaderInstance.uniforms.lighting, 1, 1, 1, 1); +// spriteBatch.drawImage(WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// //spriteBatch.drawImage(fbo.color[0], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// //spriteBatch.drawImage(fbo.color[1], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// //spriteBatch.drawImage(light.color[0], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// spriteBatch.end(); + +// // render light + +// bindFrameBuffer(fbo); + +// gl.viewport(0, 0, width, height); +// gl.clearColor(0.13, 0.5, 0.5, 1.0); // ambient light color here +// gl.clear(gl.COLOR_BUFFER_BIT); + +// gl.enable(gl.DEPTH_TEST); +// gl.depthFunc(gl.LESS); + +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.ONE, gl.ONE); + +// spriteBatch2.begin(lightShader, { texture: lightTexture } as any); +// gl.uniformMatrix4fv(lightShader.uniforms.transfrom, false, tst); +// spriteBatch2.depth = 70; +// spriteBatch2.drawImage(WHITE, 0, 0, 256, 200, mouseX - 128, mouseY - 100, 256, 200); +// spriteBatch2.end(); + +// // render frame buffer (light) + +// gl.bindFramebuffer(gl.FRAMEBUFFER, null); +// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + +// gl.disable(gl.DEPTH_TEST); +// gl.enable(gl.BLEND); +// gl.blendEquation(gl.FUNC_ADD); +// gl.blendFunc(gl.DST_COLOR, gl.ZERO); + +// spriteBatch.begin(spriteShaderInstance, { texture: fbo.color[0] } as any); +// const mat = createViewMatrix2(mat4.create(), canvas.width, canvas.height, scale); +// gl.uniformMatrix4fv(spriteShaderInstance.uniforms.transfrom, false, mat); +// gl.uniform4f(spriteShaderInstance.uniforms.lighting, 1, 1, 1, 1); +// //spriteBatch.drawImage(fbo.depth, WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// spriteBatch.drawImage(WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// //spriteBatch.drawImage(fbo.color[1], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// //spriteBatch.drawImage(light.color[0], WHITE, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); +// spriteBatch.end(); +// }, +// } as any); + +// window.addEventListener('keyup', e => { +// if (e.keyCode === Key.KEY_Q) { +// handle.cancel(); +// } +// }); + +// window.addEventListener('keydown', e => { +// if (e.keyCode === Key.KEY_E) { +// animationTime -= 1 / 24; +// } else if (e.keyCode === Key.KEY_R) { +// animationTime += 1 / 24; +// } else if (e.keyCode === Key.KEY_T) { +// for (let i = 0; i < 5; i++) { +// palettes.push(paletteManager.add(range(0, 40 * Math.random()).map(() => Math.random() * 0xffffffff))); +// } +// } else if (e.keyCode === Key.KEY_Y) { +// palettes.forEach(releasePalette); +// palettes.length = 0; +// } +// }); +// } diff --git a/src/ts/components/tools/tools.module.ts b/src/ts/components/tools/tools.module.ts new file mode 100644 index 0000000..7e6cc89 --- /dev/null +++ b/src/ts/components/tools/tools.module.ts @@ -0,0 +1,99 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { BrowserModule } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { PopoverModule } from 'ngx-bootstrap/popover'; +import { TypeaheadModule } from 'ngx-bootstrap/typeahead'; +import { ButtonsModule } from 'ngx-bootstrap/buttons'; + +import { SharedModule } from '../shared/shared.module'; + +import { ErrorReporter } from '../services/errorReporter'; + +import { ToolsRange } from './shared/tools-range/tools-range'; +import { ToolsFrame } from './shared/tools-frame/tools-frame'; +import { ToolsOffset } from './shared/tools-offset/tools-offset'; +import { ToolsXY } from './shared/tools-xy/tools-xy'; + +import { ToolsExpressions } from './tools-expressions/tools-expressions'; +import { ToolsAnimation } from './tools-animation/tools-animation'; +import { ToolsChat } from './tools-chat/tools-chat'; +import { ToolsVariants } from './tools-variants/tools-variants'; +import { ToolsWebgl } from './tools-webgl/tools-webgl'; +import { ToolsPalette } from './tools-palette/tools-palette'; +import { ToolsPerf } from './tools-perf/tools-perf'; +import { ToolsRegions } from './tools-regions/tools-regions'; +import { ToolsEntity } from './tools-entity/tools-entity'; +import { ToolsSheet } from './tools-sheet/tools-sheet'; +import { ToolsStates } from './tools-states/tools-states'; +import { ToolsUI } from './tools-ui/tools-ui'; +import { ToolsCollisions } from './tools-collisions/tools-collisions'; +import { ToolsMap } from './tools-map/tools-map'; +import { ToolsIndex } from './tools-index/tools-index'; +import { ToolsApp } from './tools'; + +export const routes: Routes = [ + { path: '', component: ToolsIndex }, + { path: 'sheet', component: ToolsSheet }, + { path: 'states', component: ToolsStates }, + { path: 'variants', component: ToolsVariants }, + { path: 'webgl', component: ToolsWebgl }, + { path: 'animation/:id', component: ToolsAnimation }, + { path: 'animation', component: ToolsAnimation }, + { path: 'chat', component: ToolsChat }, + { path: 'expressions', component: ToolsExpressions }, + { path: 'entity', component: ToolsEntity }, + { path: 'palette', component: ToolsPalette }, + { path: 'perf', component: ToolsPerf }, + { path: 'regions', component: ToolsRegions }, + { path: 'ui', component: ToolsUI }, + { path: 'collisions', component: ToolsCollisions }, + { path: 'map', component: ToolsMap }, +]; + +@NgModule({ + imports: [ + BrowserModule, + RouterModule, + FormsModule, + HttpClientModule, + SharedModule, + PopoverModule.forRoot(), + TypeaheadModule.forRoot(), + ButtonsModule.forRoot(), + RouterModule.forRoot(routes), + FontAwesomeModule, + NoopAnimationsModule, + ], + declarations: [ + ToolsRange, + ToolsFrame, + ToolsOffset, + ToolsXY, + ToolsExpressions, + ToolsAnimation, + ToolsChat, + ToolsVariants, + ToolsWebgl, + ToolsPalette, + ToolsPerf, + ToolsRegions, + ToolsEntity, + ToolsSheet, + ToolsStates, + ToolsCollisions, + ToolsMap, + ToolsUI, + ToolsIndex, + ToolsApp, + ], + providers: [ + ErrorReporter, + ], + bootstrap: [ToolsApp], +}) +export class ToolsAppModule { +} diff --git a/src/ts/components/tools/tools.pug b/src/ts/components/tools/tools.pug new file mode 100644 index 0000000..859cdd4 --- /dev/null +++ b/src/ts/components/tools/tools.pug @@ -0,0 +1,2 @@ +router-outlet +draggable-outlet diff --git a/src/ts/components/tools/tools.ts b/src/ts/components/tools/tools.ts new file mode 100644 index 0000000..3201cab --- /dev/null +++ b/src/ts/components/tools/tools.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TooltipConfig } from 'ngx-bootstrap/tooltip'; +import { PopoverConfig } from 'ngx-bootstrap/popover'; + +export function tooltipConfig() { + return Object.assign(new TooltipConfig(), { container: 'body' }); +} + +export function popoverConfig() { + return Object.assign(new PopoverConfig(), { container: 'body' }); +} + +@Component({ + selector: 'pony-town-app', + templateUrl: 'tools.pug', + providers: [ + { provide: TooltipConfig, useFactory: tooltipConfig }, + { provide: PopoverConfig, useFactory: popoverConfig }, + ] +}) +export class ToolsApp { +} diff --git a/src/ts/generated/changelog.ts b/src/ts/generated/changelog.ts new file mode 100644 index 0000000..51628a1 --- /dev/null +++ b/src/ts/generated/changelog.ts @@ -0,0 +1,693 @@ +/* tslint:disable */ + +export const CHANGELOG: { version: string; changes: string[]; }[] = [ + { + "version": "v0.53.0", + "changes": [ + "Added custom servers (about page)", + "Added export option for ponies (character page)", + "Added editing house floor tiles", + "Added more placeable object for the house", + "Added way to cycle backwards through item list by using Shift + E while holding the hammer tool", + "Added /lockhouse and /unlockhouse commands for preventing other people from modifying the house", + "Added /removetoolbox and /restoretoolbox commands for removing and restoring toolbox object from house map", + "Added switching placeable items and tiles by using mouse wheel", + "Added switching tools using Shift + scroll wheel when you're inside the house", + "Unlocked other objects in the house for removing", + "Unlocked walls in front of windows from being placed", + "Increased house object limit", + "Fixed issues with house map saving using /savehouse command", + "Fixed being able to plain object in house behind wall" + ] + }, + { + "version": "v0.52.0", + "changes": [ + "Added cave/mines map", + "Added 2 new cave/mine sound tracks", + "Added new cave/mine objects and decorations", + "Added bunnies around the map", + "Added direction signs", + "Added pickable ropes in mines", + "Added building and removing walls in party house using saw tool", + "Added removing furniture in party house using broom tool", + "Added placing furniture in party house using hammer tool", + "Added \"Switch tool\" and \"Switch item to place\" actions", + "Added carrot field in top right corner of the map", + "Added more fruits to fruit box", + "Added sort tags to character descriptions (the same names are sorted by description)", + "Added character description help tooltip", + "Added some new emotes", + "Improved sorting of character list", + "Improved tags dropdown" + ] + }, + { + "version": "v0.51.2", + "changes": [ + "Added character descriptions and tags", + "Added song switching when changing maps", + "Fixed performance issues with login server", + "Fixed issues with rendering avatars on friend list", + "Fixed issues with displaying load error messages", + "Improved character searching on character list", + "Improved map compression" + ] + }, + { + "version": "v0.51.1", + "changes": [ + "Fixed multiple issues with party maps", + "Fixed being able to permanently hide your own character" + ] + }, + { + "version": "v0.51.0", + "changes": [ + "Added party house on party island", + "Added 2 new house sound tracks", + "Added new objects for party house", + "Added magic action and /magic command (usable by unicorns)", + "Added \"darken locked outlines\" option to character editor", + "Added permanent hides", + "Added list of permanent hides to account settings", + "Changed ignore party invites to not apply to friends", + "Updated look of cliffs", + "Fixed socks resetting if only back leg socks are set", + "Fixed issues with invisible or duplicate player character", + "Fixed issues with blurring of action buttons when action bar is scrolled", + "Fixed issues with collision on boat bridge", + "Fixed some cyrillic characters not working for expressions correctly", + "Fixed rendering issues in character editor", + "Fixed issues with water animation when holding lantern", + "Fixed some rock animations overlapping with terrain", + "Fixed apples spawning on water" + ] + }, + { + "version": "v0.50.0", + "changes": [ + "Combined portuguese and spanish safe servers into one", + "Added new 18+ russian server", + "Added swimming", + "Added new sock patterns", + "Added setting separate accessory for each leg", + "Added switch for disabling outline darkening", + "Added customizable eyelash color", + "Added character count to character selection list", + "Added pickable flower on party island", + "Added rocks in water", + "Added benches", + "Added smaller island bit", + "Added box of fruits on party island", + "Added rare pickable pear and banana", + "Increased party size to 30", + "Increased friend limit to 100", + "Fixed not updating last used pony when using swap pony option", + "Fixed game update issues after server restart" + ] + }, + { + "version": "v0.49.0", + "changes": [ + "Added new 18+ non-ERP server", + "Added new Safe Spanish server", + "Added new Safe Portuguese server", + "Added character swapping", + "Added online/offline status switch to account dropdown", + "Added scrolling of action bar on PC using mouse wheel", + "Added setting separate eye white color for each eye", + "Increased base character limit to 200 and supporter character limits to +200/+300/+450", + "Fixed issues with game joining when spawning at the edge of the map", + "Fixed gamepad X button causing pony to sit down", + "Fixed player hiding getting reset on server restart", + "Fixed connection issues in some cases", + "Improved game performance" + ] + }, + { + "version": "v0.48.2", + "changes": [ + "Added flying over cliffs, fences and some other objects", + "Increased hide limit to 1000", + "Fixed issues with collision detection", + "Fixed issues with switching maps", + "Fixed issues with grass on party island map" + ] + }, + { + "version": "v0.48.1", + "changes": [ + "Added toy stash and item stash to party island", + "Increased party size to 20", + "Fixed issues with server restart", + "Fixed issues with issues with party island map in some cases" + ] + }, + { + "version": "v0.48.0", + "changes": [ + "Added harbor", + "Added party island map", + "Added party island music tracks", + "Added bright night settings option, that makes game brighter during in-game night", + "Added past supporter status", + "Added extra pony slots for past supporters", + "Fixed issues with collision inconsistencies", + "Fixed friend tag and name color showing even when switched to \"show as offline\"", + "Fixed commands not working in whispers", + "Fixed camera issue after joining to the game", + "Fixed - and = shortcuts not working on Firefox", + "Fixed issues when selecting player from chatlog", + "Fixed shortcut for hiding public chat", + "Improved displaying party and whisper commands in chatlog", + "Improved collision handling", + "Improved game performance", + "Increased friend limit", + "Updated swear filters" + ] + }, + { + "version": "v0.47.0", + "changes": [ + "Added friend list", + "Added whispers (private messages)", + "Added /w <name> and /whisper <name> command", + "Added /r and /reply command", + "Added \"Ignore friend requests\" settings option", + "Added \"Only allow whispers from friends\" settings option", + "Fixed issues with duplicate characters in certain cases", + "Fixed issues with game not rendering after switching apps or tabs in some cases", + "Fixed game loading performance issues", + "Fixed account name not saving properly when entering name of maximum allowed length", + "Improved performance", + "Improved networking", + "Improved server performance", + "Updated swear filters" + ] + }, + { + "version": "v0.46.6", + "changes": [ + "Reverted performance improvement leading to issues with sprite rendering on some browsers" + ] + }, + { + "version": "v0.46.5", + "changes": [ + "Moved rose sign", + "Fixed some commands not working properly when using uppercase letters", + "Fixed issues with map regions not loading correctly in certain cases", + "Fixed issues with map not loading in certain cases", + "Fixed server-side performance issues", + "Fixed issues with AFK check", + "Fixed issues with colors missing on held items in certain cases", + "Updated swear filters" + ] + }, + { + "version": "v0.46.4", + "changes": [ + "Fixed issues with incorrect color for chat type indicator in certain cases", + "Fixed issues with joining after disconnect resulting in missing or duplicated characters", + "Fixed issues with error reporting", + "Improved some settings labels", + "Improved server performance" + ] + }, + { + "version": "v0.46.3", + "changes": [ + "Changed longer hide to 10 days instead of 7", + "Increased hide limit to 25", + "Updated swear filters", + "Fixed critical server-side issues" + ] + }, + { + "version": "v0.46.2", + "changes": [ + "Fixed items spawning on cliffs", + "Fixed issues with date of birth month list", + "Updated swear filters" + ] + }, + { + "version": "v0.46.1", + "changes": [ + "Fixed issues with collisions", + "Fixed issues with disappearing player character in certain cases", + "Fixed items spawning on cliffs", + "Fixed some misplaced objects", + "Fixed some issues with object sorting" + ] + }, + { + "version": "v0.46.0", + "changes": [ + "Increased map size", + "Added new locations to the map", + "Added new plants", + "Increased party size limit to 10 players", + "Increased hide limit to 20", + "Fixed multiple issues with party UI", + "Fixed issues with music playback", + "Fixed issues with disappearing player character in certain cases", + "Fixed missing range limitation for changing tiles", + "Improved server performance", + "Improved rendering reliability and performance", + "Updated swear filters", + "Updated server infrastructure" + ] + }, + { + "version": "v0.45.3", + "changes": [ + "Fixed issues with saving account settings in some cases", + "Fixed server performance issues", + "Fixed some styling issues", + "Updated swear filters", + "Improved graphics performance", + "Improved server logging and performance analysis tools" + ] + }, + { + "version": "v0.45.2", + "changes": [ + "Added 25 more pony slots for all players", + "Removed collectable eggs", + "Fixed graphics memory leak issue", + "Fixed graphics initialization issues in certain cases", + "Fixed performance issues when joining to the game", + "Improved networking performance" + ] + }, + { + "version": "v0.45.1", + "changes": [ + "Added egg donation barrel", + "Fixed issues when saving settings in certain cases", + "Fixed issues with action bar on desktop PCs with touchscreens", + "Fixed disconnect issues in some cases", + "Fixed broken link in about page" + ] + }, + { + "version": "v0.45.0", + "changes": [ + "Added collectable eggs", + "Added egg emoji", + "Added /eggs command", + "Added eggs command action", + "Added keyboard shortcut F3 for toggling public chat", + "Fixed custom filters not working for player names", + "Fixed styling issues on Yandex browser", + "Fixed tongue not visible on expression action buttons", + "Reduced memory usage", + "Improved graphics resource allocation management", + "Improved memory usage for graphics resources", + "Improved chatlog performance" + ] + }, + { + "version": "v0.44.2", + "changes": [ + "Fixed actions modal styling issues on Internet explorer", + "Fixed error when running out of memory in some cases", + "Fixed not being able to join server in some cases" + ] + }, + { + "version": "v0.44.1", + "changes": [ + "Changed command actions to use last used chat type (party/public)", + "Fixed actions modal scrolling by itself on mobile", + "Fixed some closed eyes not counted correctly as closed for sleeping emotes" + ] + }, + { + "version": "v0.44.0", + "changes": [ + "Added action bar", + "Added action bar customization via \"Actions\" option in in-game settings dropdown", + "Fixed issues with showing incorrect party state after disconnect in certain cases" + ] + }, + { + "version": "v0.43.2", + "changes": [ + "Added filters section to in-game settings", + "Added custom chat filter to in-game settings", + "Improved rendering performance", + "Fixed chatlog range indicator showing incorrectly in some cases", + "Fixed incorrect errors when failed to initialize graphics device", + "Fixed connection issues when cancelling joining to the game", + "Updated swear filters" + ] + }, + { + "version": "v0.43.1", + "changes": [ + "Added ignore public chat option to in-game settings", + "Added button shortcut for opening chat on controller", + "Added date of birth field to account settings", + "Changed eyes when using uppercase Q letters in expressions QcQ", + "Changed character name validation", + "Improved camera shifting when opening mobile keyboard", + "Improved some letters in pixel font", + "Fixed issues when mobile keyboard is opened", + "Fixed incorrect error messages for party invites in some cases", + "Updated swear filters" + ] + }, + { + "version": "v0.43.0", + "changes": [ + "Added new manes", + "Added chatlog range setting", + "Added fps counter (option in settings)", + "Changed \"see through objects\" and \"chatlog opacity\" settings to be saved on server", + "Changed night to be lighter on low graphics settings to compensate for missing lights", + "Improved party invite error messages", + "Updated map with new flower and clover patches", + "Updated swear filters", + "Removed some expression combinations from PG rated servers", + "Fixed command messages in party chat being colored white instead of yellow", + "Fixed issues with part of game screen being outside the screen bounds on mobile devices in some cases", + "Fixed issues with pixel scaling on low graphics mode", + "Fixed issues with game screen turning green or black", + "Fixed issues with game loading", + "Fixed issues with music playing before the game loads", + "Fixed issue with reloading game in some cases", + "Fixed styling issues" + ] + }, + { + "version": "v0.42.4", + "changes": [ + "Changed season to spring", + "Updated swear filters", + "Fixed some issues with account alerts" + ] + }, + { + "version": "v0.42.3", + "changes": [ + "Added :thinking: emoji", + "Added account alerts for notifying players of account related issues", + "Removed some expression combinations from PG rated servers", + "Updated swear filters", + "Fixed graphical glitch with some expressions when using beak muzzle option", + "Fixed sign-in error showing off-screen on some devices", + "Fixed some disconnect issues", + "Improved performance" + ] + }, + { + "version": "v0.42.2", + "changes": [ + "Fixed issue with game breaking in some cases when the connection is interrupted" + ] + }, + { + "version": "v0.42.1", + "changes": [ + "Fixed issues with color pickers on some mobile devices" + ] + }, + { + "version": "v0.42.0", + "changes": [ + "Added rose sign", + "Added /love, /<3 command", + "Updated common issues help section", + "Updated in-game settings window", + "Fixed some issues with game screen positioning on mobile devices", + "Fixed some issues with chatlog scrolling", + "Fixed issues with switching chat mode" + ] + }, + { + "version": "v0.41.0", + "changes": [ + "Added new facial hair styles and patterns", + "Added cookie stand", + "Fixed issue with chatlog tab switching on Firefox", + "Fixed issue with bats getting stuck when returning to trees", + "Fixed issue with characters with no wings appear to hover above ground in certain cases", + "Fixed issue with delay when joining to the game", + "Fixed errors when using remote buttons while playing the game", + "Fixed some off-screen messages appearing in chatlog", + "Fixed some styling issues" + ] + }, + { + "version": "v0.40.1", + "changes": [ + "Remove loading performance optimization due to Yandex browser bug" + ] + }, + { + "version": "v0.40.0", + "changes": [ + "Added chatlog", + "Added chatlog settings", + "Added settings for walking by default", + "Fixed graphical glitch when booping while sitting", + "Fixed issues with critter movement", + "Fixed being able to be hidden by yourself in certain situations", + "Fixed incorrect error message displayed in certain situations", + "Improved game loading performance" + ] + }, + { + "version": "v0.39.4", + "changes": [ + "Added number in queue indicator when joining to the game takes longer than a second", + "Fixed issue with game not loading when graphics initialization takes longer than connecting to the server", + "Fixed issues with gamepad dpad moving the character", + "Fixed issues with iPad keyboards where each key would only work once, the keys will still get stuck occasionally due to iOS bug, we recommend to use arrow keys for movement instead of WASD keys." + ] + }, + { + "version": "v0.39.3", + "changes": [ + "Fixed issues with sending long messages on Edge and Internet Explorer browsers" + ] + }, + { + "version": "v0.39.2", + "changes": [ + "Removed Xmas decorations", + "Changed support contact email", + "Fixed some issues with disconnects", + "Fixed party chat messages visible when party member is on another map", + "Improved networking performance", + "Improved server performance" + ] + }, + { + "version": "v0.39.1", + "changes": [ + "Added gift donation spot where you can donate gifts and reduce your gift count", + "Added additional restart buttons to avoid getting stuck in game updating state", + "Added remembering of /s1, /s2 and /s3 chat mode for next message", + "Added more logging for analyzing sign-in issues", + "Fixed issues with mobile keyboard when the game is installed to home screen", + "Fixed issues with game updating", + "Fixed issues with missing files during game update process", + "Fixed focusing pony name input field on mobile devices causing mobile keyboard to open", + "Fixed being able to see lantern lights of lanterns held by hidden players in some cases" + ] + }, + { + "version": "v0.39.0", + "changes": [ + "Added xmas decorations", + "Added gifts with collectable toys (use E or /open command or gift button on mobile to open the gift)", + "Added /droptoy command for getting rid of toy on your head", + "Added /toys for listing amount of collected toys", + "Added candy/cookie stands", + "Added picking up apples and oranges", + "Added set of most common Chinese characters", + "Added Japanese Kanji characters", + "Added cookie emoji", + "Changed plushie sign to toy stash (keeps all your collected toys)", + "Changed unicode symbol assigned for some emoji characters", + "Fixed some issues with updating the game", + "Fixed some issues with height difference when standing on ice", + "Improved rendering performance" + ] + }, + { + "version": "v0.38.0", + "changes": [ + "Added winter environment details", + "Changed season to winter", + "Fixed some performance issues" + ] + }, + { + "version": "v0.37.3", + "changes": [ + "Fixed issues with character selection list on mobile devices", + "Fixed issues with keyboard on mobile devices" + ] + }, + { + "version": "v0.37.2", + "changes": [ + "Fixed issues with character selection", + "Fixed issues when using mouse with more than 3 buttons" + ] + }, + { + "version": "v0.37.1", + "changes": [ + "Added /leave command", + "Removed Halloween decorations", + "Fixed some duplicate objects on the map", + "Fixed issues with cloudflare verification window not working properly", + "Fixed issues with some sprites", + "Fixed issues with opening mobile keyboard on some devices", + "Fixed issues with drawing lights in certain cases", + "Improved networking performance", + "Improved server performance" + ] + }, + { + "version": "v0.37.0", + "changes": [ + "Added 2 new music tracks", + "Added new head, face, neck, chest and back accessories", + "Added new sleeve options for chest accessories", + "Added new manes", + "Added new tails", + "Added new face markings", + "Added new horn and horn patterns", + "Added new eyelashes", + "Added new ears", + "Added new hooves", + "Added ear patterns", + "Added pants back accessory", + "Added Halloween decorations", + "Added spooks", + "Changed google sign-in icon", + "Updated team and contributors lists", + "Updated head accessory positions for each mane", + "Fixed issues with install button", + "Fixed issues with mobile keyboard in app mode", + "Fixed issues with map loading in certain cases", + "Fixed some sprite layering issues", + "Fixed some site links not working in certain cases", + "Fixed cutting off some pixels when exporting fly animation as PNG", + "Fixed color in butt mark editor changing when switching tabs", + "Improved rendering performance" + ] + }, + { + "version": "v0.36.1", + "changes": [ + "Updated facebook sign-in", + "Fixed some lighting issues", + "Improved lighting performance" + ] + }, + { + "version": "v0.36.0", + "changes": [ + "Changed season to autumn", + "Increased map size", + "Added new locations and road system", + "Added some rule explanations to Common issues section on Help page", + "Improved lighting engine", + "Fixed issues with character not shown as flying in some cases", + "Fixed issues with updating Patreon data", + "Fixed issues with game update process", + "Fixed issues with loosing item counters when merging accounts in certain situations", + "Fixed issues with getting kicked out of the party in certain situations", + "Fixed incorrect symbol used for :trans: emoji" + ] + }, + { + "version": "v0.35.7", + "changes": [ + "Added warning message for some outdated browsers", + "Improved networking performance", + "Fixed \"Um.\" and \"Um-\" text causing expression to change", + "Fixed some issues with character selection", + "Fixed some issues with name display", + "Fixed issues with Cloudflare reloading" + ] + }, + { + "version": "v0.35.6", + "changes": [ + "Improved server performance", + "Improved network usage" + ] + }, + { + "version": "v0.35.5", + "changes": [ + "Added option to install the game on android devices", + "Improved startup performance", + "Improved server performance", + "Fixed party list covering chat buttons in some cases on small screens", + "Fixed issues with experimental progressive map loading" + ] + }, + { + "version": "v0.35.4", + "changes": [ + "Added experimental progressive map loading", + "Added power saving option to settings", + "Improved performance of map loading", + "Reworked internal server communication", + "Fixed layering issues with some accessories", + "Fixed incorrect hitbox for flying ponies", + "Fixed issues with drawing ponies at the edge of the screen", + "Fixed some unhelpful error messages", + "Fixed some words interpreted as expressions", + "Fixed some issues with movement on slow networks", + "Fixed some issues with boop animation when lying down", + "Fixed some issues with error reporting" + ] + }, + { + "version": "v0.35.3", + "changes": [ + "Added b.d / 6.6 expression", + "Added fly animation to character editor", + "Improved visibility checking", + "Improved networking performance", + "Fixed issue with disappearing party member portraits", + "Fixed server-side performance issues", + "Fixed update notification not showing when joined during an update", + "Fixed server shutdown issues", + "Fixed incorrect hitbox for flying characters", + "Fixed server-side timing issues" + ] + }, + { + "version": "v0.35.2", + "changes": [ + "Fixed performance issues when a lot of users are joining the server at the same time" + ] + }, + { + "version": "v0.35.1", + "changes": [ + "Fixed performance issues with ignore system and hiding system", + "Fixed performance issues when a lot of users are joining the server at the same time" + ] + }, + { + "version": "v0.35.0", + "changes": [ + "Added flying - C key or
button or /fly command", + "Added booping while flying", + "Added flying above water" + ] + } +]; diff --git a/src/ts/generated/fa-icons.ts b/src/ts/generated/fa-icons.ts new file mode 100644 index 0000000..2b3833c --- /dev/null +++ b/src/ts/generated/fa-icons.ts @@ -0,0 +1,132 @@ +/* tslint:disable */ + +export const faAmilia = {'prefix':'fab','iconName':'amilia','icon':[448,512,[],'f36d','M240.1 32c-61.9 0-131.5 16.9-184.2 55.4-5.1 3.1-9.1 9.2-7.2 19.4 1.1 5.1 5.1 27.4 10.2 39.6 4.1 10.2 14.2 10.2 20.3 6.1 32.5-22.3 96.5-47.7 152.3-47.7 57.9 0 58.9 28.4 58.9 73.1v38.5C203 227.7 78.2 251 46.7 264.2 11.2 280.5 16.3 357.7 16.3 376s15.2 104 124.9 104c47.8 0 113.7-20.7 153.3-42.1v25.4c0 3 2.1 8.2 6.1 9.1 3.1 1 50.7 2 59.9 2s62.5.3 66.5-.7c4.1-1 5.1-6.1 5.1-9.1V168c-.1-80.3-57.9-136-192-136zm50.2 348c-21.4 13.2-48.7 24.4-79.1 24.4-52.8 0-58.9-33.5-59-44.7 0-12.2-3-42.7 18.3-52.9 24.3-13.2 75.1-29.4 119.8-33.5z']}; +export const faAndroid = {'prefix':'fab','iconName':'android','icon':[448,512,[],'f17b','M89.6 204.5v115.8c0 15.4-12.1 27.7-27.5 27.7-15.3 0-30.1-12.4-30.1-27.7V204.5c0-15.1 14.8-27.5 30.1-27.5 15.1 0 27.5 12.4 27.5 27.5zm10.8 157c0 16.4 13.2 29.6 29.6 29.6h19.9l.3 61.1c0 36.9 55.2 36.6 55.2 0v-61.1h37.2v61.1c0 36.7 55.5 36.8 55.5 0v-61.1h20.2c16.2 0 29.4-13.2 29.4-29.6V182.1H100.4v179.4zm248-189.1H99.3c0-42.8 25.6-80 63.6-99.4l-19.1-35.3c-2.8-4.9 4.3-8 6.7-3.8l19.4 35.6c34.9-15.5 75-14.7 108.3 0L297.5 34c2.5-4.3 9.5-1.1 6.7 3.8L285.1 73c37.7 19.4 63.3 56.6 63.3 99.4zm-170.7-55.5c0-5.7-4.6-10.5-10.5-10.5-5.7 0-10.2 4.8-10.2 10.5s4.6 10.5 10.2 10.5c5.9 0 10.5-4.8 10.5-10.5zm113.4 0c0-5.7-4.6-10.5-10.2-10.5-5.9 0-10.5 4.8-10.5 10.5s4.6 10.5 10.5 10.5c5.6 0 10.2-4.8 10.2-10.5zm94.8 60.1c-15.1 0-27.5 12.1-27.5 27.5v115.8c0 15.4 12.4 27.7 27.5 27.7 15.4 0 30.1-12.4 30.1-27.7V204.5c0-15.4-14.8-27.5-30.1-27.5z']}; +export const faAngleDoubleDown = {'prefix':'fas','iconName':'angle-double-down','icon':[320,512,[],'f103','M143 256.3L7 120.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0L313 86.3c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.4 9.5-24.6 9.5-34 .1zm34 192l136-136c9.4-9.4 9.4-24.6 0-33.9l-22.6-22.6c-9.4-9.4-24.6-9.4-33.9 0L160 352.1l-96.4-96.4c-9.4-9.4-24.6-9.4-33.9 0L7 278.3c-9.4 9.4-9.4 24.6 0 33.9l136 136c9.4 9.5 24.6 9.5 34 .1z']}; +export const faAngleDoubleLeft = {'prefix':'fas','iconName':'angle-double-left','icon':[448,512,[],'f100','M223.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L319.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L393.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34zm-192 34l136 136c9.4 9.4 24.6 9.4 33.9 0l22.6-22.6c9.4-9.4 9.4-24.6 0-33.9L127.9 256l96.4-96.4c9.4-9.4 9.4-24.6 0-33.9L201.7 103c-9.4-9.4-24.6-9.4-33.9 0l-136 136c-9.5 9.4-9.5 24.6-.1 34z']}; +export const faAngleDoubleRight = {'prefix':'fas','iconName':'angle-double-right','icon':[448,512,[],'f101','M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34zm192-34l-136-136c-9.4-9.4-24.6-9.4-33.9 0l-22.6 22.6c-9.4 9.4-9.4 24.6 0 33.9l96.4 96.4-96.4 96.4c-9.4 9.4-9.4 24.6 0 33.9l22.6 22.6c9.4 9.4 24.6 9.4 33.9 0l136-136c9.4-9.2 9.4-24.4 0-33.8z']}; +export const faAngleDoubleUp = {'prefix':'fas','iconName':'angle-double-up','icon':[320,512,[],'f102','M177 255.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 351.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 425.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1zm-34-192L7 199.7c-9.4 9.4-9.4 24.6 0 33.9l22.6 22.6c9.4 9.4 24.6 9.4 33.9 0l96.4-96.4 96.4 96.4c9.4 9.4 24.6 9.4 33.9 0l22.6-22.6c9.4-9.4 9.4-24.6 0-33.9l-136-136c-9.2-9.4-24.4-9.4-33.8 0z']}; +export const faApple = {'prefix':'fab','iconName':'apple','icon':[384,512,[],'f179','M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z']}; +export const faArrowDown = {'prefix':'fas','iconName':'arrow-down','icon':[448,512,[],'f063','M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z']}; +export const faArrowLeft = {'prefix':'fas','iconName':'arrow-left','icon':[448,512,[],'f060','M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L7 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 114.8c9.8 9.3 10 24.8.4 34.3z']}; +export const faArrowRight = {'prefix':'fas','iconName':'arrow-right','icon':[448,512,[],'f061','M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z']}; +export const faArrowUp = {'prefix':'fas','iconName':'arrow-up','icon':[448,512,[],'f062','M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z']}; +export const faBan = {'prefix':'fas','iconName':'ban','icon':[512,512,[],'f05e','M256 8C119.034 8 8 119.033 8 256s111.034 248 248 248 248-111.034 248-248S392.967 8 256 8zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676zM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676z']}; +export const faBell = {'prefix':'fas','iconName':'bell','icon':[448,512,[],'f0f3','M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z']}; +export const faCalendar = {'prefix':'fas','iconName':'calendar','icon':[448,512,[],'f133','M12 192h424c6.6 0 12 5.4 12 12v260c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V204c0-6.6 5.4-12 12-12zm436-44v-36c0-26.5-21.5-48-48-48h-48V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H160V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H48C21.5 64 0 85.5 0 112v36c0 6.6 5.4 12 12 12h424c6.6 0 12-5.4 12-12z']}; +export const faCaretSquareDown = {'prefix':'fas','iconName':'caret-square-down','icon':[448,512,[],'f150','M448 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zM92.5 220.5l123 123c4.7 4.7 12.3 4.7 17 0l123-123c7.6-7.6 2.2-20.5-8.5-20.5H101c-10.7 0-16.1 12.9-8.5 20.5z']}; +export const faCaretSquareUp = {'prefix':'fas','iconName':'caret-square-up','icon':[448,512,[],'f151','M0 432V80c0-26.51 21.49-48 48-48h352c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48zm355.515-140.485l-123.03-123.03c-4.686-4.686-12.284-4.686-16.971 0L92.485 291.515c-7.56 7.56-2.206 20.485 8.485 20.485h246.059c10.691 0 16.045-12.926 8.486-20.485z']}; +export const faCaretUp = {'prefix':'fas','iconName':'caret-up','icon':[320,512,[],'f0d8','M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z']}; +export const faCertificate = {'prefix':'fas','iconName':'certificate','icon':[512,512,[],'f0a3','M458.622 255.92l45.985-45.005c13.708-12.977 7.316-36.039-10.664-40.339l-62.65-15.99 17.661-62.015c4.991-17.838-11.829-34.663-29.661-29.671l-61.994 17.667-15.984-62.671C337.085.197 313.765-6.276 300.99 7.228L256 53.57 211.011 7.229c-12.63-13.351-36.047-7.234-40.325 10.668l-15.984 62.671-61.995-17.667C74.87 57.907 58.056 74.738 63.046 92.572l17.661 62.015-62.65 15.99C.069 174.878-6.31 197.944 7.392 210.915l45.985 45.005-45.985 45.004c-13.708 12.977-7.316 36.039 10.664 40.339l62.65 15.99-17.661 62.015c-4.991 17.838 11.829 34.663 29.661 29.671l61.994-17.667 15.984 62.671c4.439 18.575 27.696 24.018 40.325 10.668L256 458.61l44.989 46.001c12.5 13.488 35.987 7.486 40.325-10.668l15.984-62.671 61.994 17.667c17.836 4.994 34.651-11.837 29.661-29.671l-17.661-62.015 62.65-15.99c17.987-4.302 24.366-27.367 10.664-40.339l-45.984-45.004z']}; +export const faChartPie = {'prefix':'fas','iconName':'chart-pie','icon':[544,512,[],'f200','M527.79 288H290.5l158.03 158.03c6.04 6.04 15.98 6.53 22.19.68 38.7-36.46 65.32-85.61 73.13-140.86 1.34-9.46-6.51-17.85-16.06-17.85zm-15.83-64.8C503.72 103.74 408.26 8.28 288.8.04 279.68-.59 272 7.1 272 16.24V240h223.77c9.14 0 16.82-7.68 16.19-16.8zM224 288V50.71c0-9.55-8.39-17.4-17.84-16.06C86.99 51.49-4.1 155.6.14 280.37 4.5 408.51 114.83 513.59 243.03 511.98c50.4-.63 96.97-16.87 135.26-44.03 7.9-5.6 8.42-17.23 1.57-24.08L224 288z']}; +export const faCheck = {'prefix':'fas','iconName':'check','icon':[512,512,[],'f00c','M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z']}; +export const faCheckCircle = {'prefix':'fas','iconName':'check-circle','icon':[512,512,[],'f058','M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z']}; +export const faChevronDown = {'prefix':'fas','iconName':'chevron-down','icon':[448,512,[],'f078','M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z']}; +export const faChevronLeft = {'prefix':'fas','iconName':'chevron-left','icon':[320,512,[],'f053','M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z']}; +export const faChevronRight = {'prefix':'fas','iconName':'chevron-right','icon':[320,512,[],'f054','M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z']}; +export const faChevronUp = {'prefix':'fas','iconName':'chevron-up','icon':[448,512,[],'f077','M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z']}; +export const faChrome = {'prefix':'fab','iconName':'chrome','icon':[496,512,[],'f268','M131.5 217.5L55.1 100.1c47.6-59.2 119-91.8 192-92.1 42.3-.3 85.5 10.5 124.8 33.2 43.4 25.2 76.4 61.4 97.4 103L264 133.4c-58.1-3.4-113.4 29.3-132.5 84.1zm32.9 38.5c0 46.2 37.4 83.6 83.6 83.6s83.6-37.4 83.6-83.6-37.4-83.6-83.6-83.6-83.6 37.3-83.6 83.6zm314.9-89.2L339.6 174c37.9 44.3 38.5 108.2 6.6 157.2L234.1 503.6c46.5 2.5 94.4-7.7 137.8-32.9 107.4-62 150.9-192 107.4-303.9zM133.7 303.6L40.4 120.1C14.9 159.1 0 205.9 0 256c0 124 90.8 226.7 209.5 244.9l63.7-124.8c-57.6 10.8-113.2-20.8-139.5-72.5z']}; +export const faCircle = {'prefix':'fas','iconName':'circle','icon':[512,512,[],'f111','M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z']}; +export const faClipboard = {'prefix':'fas','iconName':'clipboard','icon':[384,512,[],'f328','M384 112v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h80c0-35.29 28.71-64 64-64s64 28.71 64 64h80c26.51 0 48 21.49 48 48zM192 40c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24m96 114v-20a6 6 0 0 0-6-6H102a6 6 0 0 0-6 6v20a6 6 0 0 0 6 6h180a6 6 0 0 0 6-6z']}; +export const faClock = {'prefix':'fas','iconName':'clock','icon':[512,512,[],'f017','M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm57.1 350.1L224.9 294c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12v137.7l63.5 46.2c5.4 3.9 6.5 11.4 2.6 16.8l-28.2 38.8c-3.9 5.3-11.4 6.5-16.8 2.6z']}; +export const faClone = {'prefix':'fas','iconName':'clone','icon':[512,512,[],'f24d','M464 0c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48H176c-26.51 0-48-21.49-48-48V48c0-26.51 21.49-48 48-48h288M176 416c-44.112 0-80-35.888-80-80V128H48c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h288c26.51 0 48-21.49 48-48v-48H176z']}; +export const faCode = {'prefix':'fas','iconName':'code','icon':[640,512,[],'f121','M278.9 511.5l-61-17.7c-6.4-1.8-10-8.5-8.2-14.9L346.2 8.7c1.8-6.4 8.5-10 14.9-8.2l61 17.7c6.4 1.8 10 8.5 8.2 14.9L293.8 503.3c-1.9 6.4-8.5 10.1-14.9 8.2zm-114-112.2l43.5-46.4c4.6-4.9 4.3-12.7-.8-17.2L117 256l90.6-79.7c5.1-4.5 5.5-12.3.8-17.2l-43.5-46.4c-4.5-4.8-12.1-5.1-17-.5L3.8 247.2c-5.1 4.7-5.1 12.8 0 17.5l144.1 135.1c4.9 4.6 12.5 4.4 17-.5zm327.2.6l144.1-135.1c5.1-4.7 5.1-12.8 0-17.5L492.1 112.1c-4.8-4.5-12.4-4.3-17 .5L431.6 159c-4.6 4.9-4.3 12.7.8 17.2L523 256l-90.6 79.7c-5.1 4.5-5.5 12.3-.8 17.2l43.5 46.4c4.5 4.9 12.1 5.1 17 .6z']}; +export const faCog = {'prefix':'fas','iconName':'cog','icon':[512,512,[],'f013','M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z']}; +export const faCogs = {'prefix':'fas','iconName':'cogs','icon':[640,512,[],'f085','M512.1 191l-8.2 14.3c-3 5.3-9.4 7.5-15.1 5.4-11.8-4.4-22.6-10.7-32.1-18.6-4.6-3.8-5.8-10.5-2.8-15.7l8.2-14.3c-6.9-8-12.3-17.3-15.9-27.4h-16.5c-6 0-11.2-4.3-12.2-10.3-2-12-2.1-24.6 0-37.1 1-6 6.2-10.4 12.2-10.4h16.5c3.6-10.1 9-19.4 15.9-27.4l-8.2-14.3c-3-5.2-1.9-11.9 2.8-15.7 9.5-7.9 20.4-14.2 32.1-18.6 5.7-2.1 12.1.1 15.1 5.4l8.2 14.3c10.5-1.9 21.2-1.9 31.7 0L552 6.3c3-5.3 9.4-7.5 15.1-5.4 11.8 4.4 22.6 10.7 32.1 18.6 4.6 3.8 5.8 10.5 2.8 15.7l-8.2 14.3c6.9 8 12.3 17.3 15.9 27.4h16.5c6 0 11.2 4.3 12.2 10.3 2 12 2.1 24.6 0 37.1-1 6-6.2 10.4-12.2 10.4h-16.5c-3.6 10.1-9 19.4-15.9 27.4l8.2 14.3c3 5.2 1.9 11.9-2.8 15.7-9.5 7.9-20.4 14.2-32.1 18.6-5.7 2.1-12.1-.1-15.1-5.4l-8.2-14.3c-10.4 1.9-21.2 1.9-31.7 0zm-10.5-58.8c38.5 29.6 82.4-14.3 52.8-52.8-38.5-29.7-82.4 14.3-52.8 52.8zM386.3 286.1l33.7 16.8c10.1 5.8 14.5 18.1 10.5 29.1-8.9 24.2-26.4 46.4-42.6 65.8-7.4 8.9-20.2 11.1-30.3 5.3l-29.1-16.8c-16 13.7-34.6 24.6-54.9 31.7v33.6c0 11.6-8.3 21.6-19.7 23.6-24.6 4.2-50.4 4.4-75.9 0-11.5-2-20-11.9-20-23.6V418c-20.3-7.2-38.9-18-54.9-31.7L74 403c-10 5.8-22.9 3.6-30.3-5.3-16.2-19.4-33.3-41.6-42.2-65.7-4-10.9.4-23.2 10.5-29.1l33.3-16.8c-3.9-20.9-3.9-42.4 0-63.4L12 205.8c-10.1-5.8-14.6-18.1-10.5-29 8.9-24.2 26-46.4 42.2-65.8 7.4-8.9 20.2-11.1 30.3-5.3l29.1 16.8c16-13.7 34.6-24.6 54.9-31.7V57.1c0-11.5 8.2-21.5 19.6-23.5 24.6-4.2 50.5-4.4 76-.1 11.5 2 20 11.9 20 23.6v33.6c20.3 7.2 38.9 18 54.9 31.7l29.1-16.8c10-5.8 22.9-3.6 30.3 5.3 16.2 19.4 33.2 41.6 42.1 65.8 4 10.9.1 23.2-10 29.1l-33.7 16.8c3.9 21 3.9 42.5 0 63.5zm-117.6 21.1c59.2-77-28.7-164.9-105.7-105.7-59.2 77 28.7 164.9 105.7 105.7zm243.4 182.7l-8.2 14.3c-3 5.3-9.4 7.5-15.1 5.4-11.8-4.4-22.6-10.7-32.1-18.6-4.6-3.8-5.8-10.5-2.8-15.7l8.2-14.3c-6.9-8-12.3-17.3-15.9-27.4h-16.5c-6 0-11.2-4.3-12.2-10.3-2-12-2.1-24.6 0-37.1 1-6 6.2-10.4 12.2-10.4h16.5c3.6-10.1 9-19.4 15.9-27.4l-8.2-14.3c-3-5.2-1.9-11.9 2.8-15.7 9.5-7.9 20.4-14.2 32.1-18.6 5.7-2.1 12.1.1 15.1 5.4l8.2 14.3c10.5-1.9 21.2-1.9 31.7 0l8.2-14.3c3-5.3 9.4-7.5 15.1-5.4 11.8 4.4 22.6 10.7 32.1 18.6 4.6 3.8 5.8 10.5 2.8 15.7l-8.2 14.3c6.9 8 12.3 17.3 15.9 27.4h16.5c6 0 11.2 4.3 12.2 10.3 2 12 2.1 24.6 0 37.1-1 6-6.2 10.4-12.2 10.4h-16.5c-3.6 10.1-9 19.4-15.9 27.4l8.2 14.3c3 5.2 1.9 11.9-2.8 15.7-9.5 7.9-20.4 14.2-32.1 18.6-5.7 2.1-12.1-.1-15.1-5.4l-8.2-14.3c-10.4 1.9-21.2 1.9-31.7 0zM501.6 431c38.5 29.6 82.4-14.3 52.8-52.8-38.5-29.6-82.4 14.3-52.8 52.8z']}; +export const faComment = {'prefix':'fas','iconName':'comment','icon':[512,512,[],'f075','M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32z']}; +export const faComments = {'prefix':'fas','iconName':'comments','icon':[576,512,[],'f086','M416 192c0-88.4-93.1-160-208-160S0 103.6 0 192c0 34.3 14.1 65.9 38 92-13.4 30.2-35.5 54.2-35.8 54.5-2.2 2.3-2.8 5.7-1.5 8.7S4.8 352 8 352c36.6 0 66.9-12.3 88.7-25 32.2 15.7 70.3 25 111.3 25 114.9 0 208-71.6 208-160zm122 220c23.9-26 38-57.7 38-92 0-66.9-53.5-124.2-129.3-148.1.9 6.6 1.3 13.3 1.3 20.1 0 105.9-107.7 192-240 192-10.8 0-21.3-.8-31.7-1.9C207.8 439.6 281.8 480 368 480c41 0 79.1-9.2 111.3-25 21.8 12.7 52.1 25 88.7 25 3.2 0 6.1-1.9 7.3-4.8 1.3-2.9.7-6.3-1.5-8.7-.3-.3-22.4-24.2-35.8-54.5z']}; +export const faCommentSlash = {'prefix':'fas','iconName':'comment-slash','icon':[640,512,[],'f4b3','M64 240c0 49.6 21.4 95 57 130.7-12.6 50.3-54.3 95.2-54.8 95.8-2.2 2.3-2.8 5.7-1.5 8.7 1.3 2.9 4.1 4.8 7.3 4.8 66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 27.4 0 53.7-3.6 78.4-10L72.9 186.4c-5.6 17.1-8.9 35-8.9 53.6zm569.8 218.1l-114.4-88.4C554.6 334.1 576 289.2 576 240c0-114.9-114.6-208-256-208-65.1 0-124.2 20.1-169.4 52.7L45.5 3.4C38.5-2 28.5-.8 23 6.2L3.4 31.4c-5.4 7-4.2 17 2.8 22.4l588.4 454.7c7 5.4 17 4.2 22.5-2.8l19.6-25.3c5.4-6.8 4.1-16.9-2.9-22.3z']}; +export const faCompressArrowsAlt = {'prefix':'fas','iconName':'compress-arrows-alt','icon':[512,512,[],'f78c','M200 288H88c-21.4 0-32.1 25.8-17 41l32.9 31-99.2 99.3c-6.2 6.2-6.2 16.4 0 22.6l25.4 25.4c6.2 6.2 16.4 6.2 22.6 0L152 408l31.1 33c15.1 15.1 40.9 4.4 40.9-17V312c0-13.3-10.7-24-24-24zm112-64h112c21.4 0 32.1-25.9 17-41l-33-31 99.3-99.3c6.2-6.2 6.2-16.4 0-22.6L481.9 4.7c-6.2-6.2-16.4-6.2-22.6 0L360 104l-31.1-33C313.8 55.9 288 66.6 288 88v112c0 13.3 10.7 24 24 24zm96 136l33-31.1c15.1-15.1 4.4-40.9-17-40.9H312c-13.3 0-24 10.7-24 24v112c0 21.4 25.9 32.1 41 17l31-32.9 99.3 99.3c6.2 6.2 16.4 6.2 22.6 0l25.4-25.4c6.2-6.2 6.2-16.4 0-22.6L408 360zM183 71.1L152 104 52.7 4.7c-6.2-6.2-16.4-6.2-22.6 0L4.7 30.1c-6.2 6.2-6.2 16.4 0 22.6L104 152l-33 31.1C55.9 198.2 66.6 224 88 224h112c13.3 0 24-10.7 24-24V88c0-21.3-25.9-32-41-16.9z']}; +export const faCopy = {'prefix':'fas','iconName':'copy','icon':[448,512,[],'f0c5','M320 448v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V120c0-13.255 10.745-24 24-24h72v296c0 30.879 25.121 56 56 56h168zm0-344V0H152c-13.255 0-24 10.745-24 24v368c0 13.255 10.745 24 24 24h272c13.255 0 24-10.745 24-24V128H344c-13.2 0-24-10.8-24-24zm120.971-31.029L375.029 7.029A24 24 0 0 0 358.059 0H352v96h96v-6.059a24 24 0 0 0-7.029-16.97z']}; +export const faCrosshairs = {'prefix':'fas','iconName':'crosshairs','icon':[512,512,[],'f05b','M500 224h-30.364C455.724 130.325 381.675 56.276 288 42.364V12c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v30.364C130.325 56.276 56.276 130.325 42.364 224H12c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h30.364C56.276 381.675 130.325 455.724 224 469.636V500c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12v-30.364C381.675 455.724 455.724 381.675 469.636 288H500c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12zM288 404.634V364c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40.634C165.826 392.232 119.783 346.243 107.366 288H148c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12h-40.634C119.768 165.826 165.757 119.783 224 107.366V148c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12v-40.634C346.174 119.768 392.217 165.757 404.634 224H364c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40.634C392.232 346.174 346.243 392.217 288 404.634zM288 256c0 17.673-14.327 32-32 32s-32-14.327-32-32c0-17.673 14.327-32 32-32s32 14.327 32 32z']}; +export const faCrown = {'prefix':'fas','iconName':'crown','icon':[640,512,[],'f521','M528 448H112c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h416c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm64-320c-26.5 0-48 21.5-48 48 0 7.1 1.6 13.7 4.4 19.8L476 239.2c-15.4 9.2-35.3 4-44.2-11.6L350.3 85C361 76.2 368 63 368 48c0-26.5-21.5-48-48-48s-48 21.5-48 48c0 15 7 28.2 17.7 37l-81.5 142.6c-8.9 15.6-28.9 20.8-44.2 11.6l-72.3-43.4c2.7-6 4.4-12.7 4.4-19.8 0-26.5-21.5-48-48-48S0 149.5 0 176s21.5 48 48 48c2.6 0 5.2-.4 7.7-.8L128 416h384l72.3-192.8c2.5.4 5.1.8 7.7.8 26.5 0 48-21.5 48-48s-21.5-48-48-48z']}; +export const faDatabase = {'prefix':'fas','iconName':'database','icon':[448,512,[],'f1c0','M448 73.143v45.714C448 159.143 347.667 192 224 192S0 159.143 0 118.857V73.143C0 32.857 100.333 0 224 0s224 32.857 224 73.143zM448 176v102.857C448 319.143 347.667 352 224 352S0 319.143 0 278.857V176c48.125 33.143 136.208 48.572 224 48.572S399.874 209.143 448 176zm0 160v102.857C448 479.143 347.667 512 224 512S0 479.143 0 438.857V336c48.125 33.143 136.208 48.572 224 48.572S399.874 369.143 448 336z']}; +export const faDesktop = {'prefix':'fas','iconName':'desktop','icon':[576,512,[],'f108','M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z']}; +export const faDeviantart = {'prefix':'fab','iconName':'deviantart','icon':[320,512,[],'f1bd','M320 93.2l-98.2 179.1 7.4 9.5H320v127.7H159.1l-13.5 9.2-43.7 84c-.3 0-8.6 8.6-9.2 9.2H0v-93.2l93.2-179.4-7.4-9.2H0V102.5h156l13.5-9.2 43.7-84c.3 0 8.6-8.6 9.2-9.2H320v93.1z']}; +export const faDrawPolygon = {'prefix':'fas','iconName':'draw-polygon','icon':[448,512,[],'f5ee','M384 352c-.35 0-.67.1-1.02.1l-39.2-65.32c5.07-9.17 8.22-19.56 8.22-30.78s-3.14-21.61-8.22-30.78l39.2-65.32c.35.01.67.1 1.02.1 35.35 0 64-28.65 64-64s-28.65-64-64-64c-23.63 0-44.04 12.95-55.12 32H119.12C108.04 44.95 87.63 32 64 32 28.65 32 0 60.65 0 96c0 23.63 12.95 44.04 32 55.12v209.75C12.95 371.96 0 392.37 0 416c0 35.35 28.65 64 64 64 23.63 0 44.04-12.95 55.12-32h209.75c11.09 19.05 31.49 32 55.12 32 35.35 0 64-28.65 64-64 .01-35.35-28.64-64-63.99-64zm-288 8.88V151.12A63.825 63.825 0 0 0 119.12 128h208.36l-38.46 64.1c-.35-.01-.67-.1-1.02-.1-35.35 0-64 28.65-64 64s28.65 64 64 64c.35 0 .67-.1 1.02-.1l38.46 64.1H119.12A63.748 63.748 0 0 0 96 360.88zM272 256c0-8.82 7.18-16 16-16s16 7.18 16 16-7.18 16-16 16-16-7.18-16-16zM400 96c0 8.82-7.18 16-16 16s-16-7.18-16-16 7.18-16 16-16 16 7.18 16 16zM64 80c8.82 0 16 7.18 16 16s-7.18 16-16 16-16-7.18-16-16 7.18-16 16-16zM48 416c0-8.82 7.18-16 16-16s16 7.18 16 16-7.18 16-16 16-16-7.18-16-16zm336 16c-8.82 0-16-7.18-16-16s7.18-16 16-16 16 7.18 16 16-7.18 16-16 16z']}; +export const faEdge = {'prefix':'fab','iconName':'edge','icon':[512,512,[],'f282','M25.714 228.163c.111-.162.23-.323.342-.485-.021.162-.045.323-.065.485h-.277zm460.572 15.508c0-44.032-7.754-84.465-28.801-122.405C416.498 47.879 343.912 8.001 258.893 8.001 118.962 7.724 40.617 113.214 26.056 227.679c42.429-61.312 117.073-121.376 220.375-124.966 0 0 109.666 0 99.419 104.957H169.997c6.369-37.386 18.554-58.986 34.339-78.926-75.048 34.893-121.85 96.096-120.742 188.315.83 71.448 50.124 144.836 120.743 171.976 83.357 31.847 192.776 7.2 240.132-21.324V363.307c-80.864 56.494-270.871 60.925-272.255-67.572h314.073v-52.064z']}; +export const faEdit = {'prefix':'fas','iconName':'edit','icon':[576,512,[],'f044','M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-3.5 8.5z']}; +export const faEllipsisV = {'prefix':'fas','iconName':'ellipsis-v','icon':[192,512,[],'f142','M96 184c39.8 0 72 32.2 72 72s-32.2 72-72 72-72-32.2-72-72 32.2-72 72-72zM24 80c0 39.8 32.2 72 72 72s72-32.2 72-72S135.8 8 96 8 24 40.2 24 80zm0 352c0 39.8 32.2 72 72 72s72-32.2 72-72-32.2-72-72-72-72 32.2-72 72z']}; +export const faEnvelope = {'prefix':'fas','iconName':'envelope','icon':[512,512,[],'f0e0','M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z']}; +export const faEraser = {'prefix':'fas','iconName':'eraser','icon':[512,512,[],'f12d','M497.941 273.941c18.745-18.745 18.745-49.137 0-67.882l-160-160c-18.745-18.745-49.136-18.746-67.883 0l-256 256c-18.745 18.745-18.745 49.137 0 67.882l96 96A48.004 48.004 0 0 0 144 480h356c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12H355.883l142.058-142.059zm-302.627-62.627l137.373 137.373L265.373 416H150.628l-80-80 124.686-124.686z']}; +export const faExchangeAlt = {'prefix':'fas','iconName':'exchange-alt','icon':[512,512,[],'f362','M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z']}; +export const faExclamationCircle = {'prefix':'fas','iconName':'exclamation-circle','icon':[512,512,[],'f06a','M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z']}; +export const faEyeDropper = {'prefix':'fas','iconName':'eye-dropper','icon':[512,512,[],'f1fb','M50.75 333.25c-12 12-18.75 28.28-18.75 45.26V424L0 480l32 32 56-32h45.49c16.97 0 33.25-6.74 45.25-18.74l126.64-126.62-128-128L50.75 333.25zM483.88 28.12c-37.47-37.5-98.28-37.5-135.75 0l-77.09 77.09-13.1-13.1c-9.44-9.44-24.65-9.31-33.94 0l-40.97 40.97c-9.37 9.37-9.37 24.57 0 33.94l161.94 161.94c9.44 9.44 24.65 9.31 33.94 0L419.88 288c9.37-9.37 9.37-24.57 0-33.94l-13.1-13.1 77.09-77.09c37.51-37.48 37.51-98.26.01-135.75z']}; +export const faEyeSlash = {'prefix':'fas','iconName':'eye-slash','icon':[640,512,[],'f070','M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z']}; +export const faFacebook = {'prefix':'fab','iconName':'facebook','icon':[512,512,[],'f09a','M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z']}; +export const faFile = {'prefix':'fas','iconName':'file','icon':[384,512,[],'f15b','M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm160-14.1v6.1H256V0h6.1c6.4 0 12.5 2.5 17 7l97.9 98c4.5 4.5 7 10.6 7 16.9z']}; +export const faFileAlt = {'prefix':'fas','iconName':'file-alt','icon':[384,512,[],'f15c','M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm64 236c0 6.6-5.4 12-12 12H108c-6.6 0-12-5.4-12-12v-8c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12v8zm0-64c0 6.6-5.4 12-12 12H108c-6.6 0-12-5.4-12-12v-8c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12v8zm0-72v8c0 6.6-5.4 12-12 12H108c-6.6 0-12-5.4-12-12v-8c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm96-114.1v6.1H256V0h6.1c6.4 0 12.5 2.5 17 7l97.9 98c4.5 4.5 7 10.6 7 16.9z']}; +export const faFileImage = {'prefix':'fas','iconName':'file-image','icon':[384,512,[],'f1c5','M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z']}; +export const faFilter = {'prefix':'fas','iconName':'filter','icon':[512,512,[],'f0b0','M487.976 0H24.028C2.71 0-8.047 25.866 7.058 40.971L192 225.941V432c0 7.831 3.821 15.17 10.237 19.662l80 55.98C298.02 518.69 320 507.493 320 487.98V225.941l184.947-184.97C520.021 25.896 509.338 0 487.976 0z']}; +export const faFirefox = {'prefix':'fab','iconName':'firefox','icon':[480,512,[],'f269','M478.1 235.3c-.7-4.5-1.4-7.1-1.4-7.1s-1.8 2-4.7 5.9c-.9-10.7-2.8-21.2-5.8-31.6-3.7-12.9-8.5-25.4-14.5-37.4-3.8-8-8.2-15.6-13.3-22.8-1.8-2.7-3.7-5.4-5.6-7.9-8.8-14.4-19-23.3-30.7-40-7.6-12.8-12.9-26.9-15.4-41.6-3.2 8.9-5.7 18-7.4 27.3-12.1-12.2-22.5-20.8-28.9-26.7C319.4 24.2 323 9.1 323 9.1S264.7 74.2 289.9 142c8.7 23 23.8 43.1 43.4 57.9 24.4 20.2 50.8 36 64.7 76.6-11.2-21.3-28.1-39.2-48.8-51.5 6.2 14.7 9.4 30.6 9.3 46.5 0 61-49.6 110.5-110.6 110.4-8.3 0-16.5-.9-24.5-2.8-9.5-1.8-18.7-4.9-27.4-9.3-12.9-7.8-24-18.1-32.8-30.3l-.2-.3 2 .7c4.6 1.6 9.2 2.8 14 3.7 18.7 4 38.3 1.7 55.6-6.6 17.5-9.7 28-16.9 36.6-14h.2c8.4 2.7 15-5.5 9-14-10.4-13.4-27.4-20-44.2-17-17.5 2.5-33.5 15-56.4 2.9-1.5-.8-2.9-1.6-4.3-2.5-1.6-.9 4.9 1.3 3.4.3-5-2.5-9.8-5.4-14.4-8.6-.3-.3 3.5 1.1 3.1.8-5.9-4-11-9.2-15-15.2-4.1-7.4-4.5-16.4-1-24.1 2.1-3.8 5.4-6.9 9.3-8.7 3 1.5 4.8 2.6 4.8 2.6s-1.3-2.5-2.1-3.8c.3-.1.5 0 .8-.2 2.6 1.1 8.3 4 11.4 5.8 2.1 1.1 3.8 2.7 5.2 4.7 0 0 1-.5.3-2.7-1.1-2.7-2.9-5-5.4-6.6h.2c2.3 1.2 4.5 2.6 6.6 4.1 1.9-4.4 2.8-9.2 2.6-14 .2-2.6-.2-5.3-1.1-7.8-.8-1.6.5-2.2 1.9-.5-.2-1.3-.7-2.5-1.2-3.7v-.1s.8-1.1 1.2-1.5c1-1 2.1-1.9 3.4-2.7 7.2-4.5 14.8-8.4 22.7-11.6 6.4-2.8 11.7-4.9 12.8-5.6 1.6-1 3.1-2.2 4.5-3.5 5.3-4.5 9-10.8 10.2-17.7.1-.9.2-1.8.3-2.8v-1.5c-.9-3.5-6.9-6.1-38.4-9.1-11.1-1.8-20-10.1-22.5-21.1v-.1c6-15.7 16.8-29.1 30.8-38.3.8-.7-3.2.2-2.4-.5 2.7-1.3 5.4-2.5 8.2-3.5 1.4-.6-6-3.4-12.6-2.7-4 .2-8 1.2-11.7 2.8 1.6-1.3 6.2-3.1 5.1-3.1-8.4 1.6-16.5 4.7-23.9 9 0-.8.1-1.5.5-2.2-5.9 2.5-11 6.5-15 11.5.1-.9.2-1.8.2-2.7-2.7 2-5.2 4.3-7.3 6.9l-.1.1c-17.4-6.7-36.3-8.3-54.6-4.7l-.2-.1h.2c-3.8-3.1-7.1-6.7-9.7-10.9l-.2.1-.4-.2c-1.2-1.8-2.4-3.8-3.7-6-.9-1.6-1.8-3.4-2.7-5.2 0-.1-.1-.2-.2-.2-.4 0-.6 1.7-.9 1.3v-.1c-3.2-8.3-4.7-17.2-4.4-26.2l-.2.1c-5.1 3.5-9 8.6-11.1 14.5-.9 2.1-1.6 3.3-2.2 4.5v-.5c.1-1.1.6-3.3.5-3.1s-.2.3-.3.4c-1.5 1.7-2.9 3.7-3.9 5.8-.9 1.9-1.7 3.9-2.3 5.9-.1.3 0-.3 0-1s.1-2 0-1.7l-.3.7c-6.7 14.9-10.9 30.8-12.4 47.1-.4 2.8-.6 5.6-.5 8.3v.2c-4.8 5.2-9 11-12.7 17.1-12.1 20.4-21.1 42.5-26.8 65.6 4-8.8 8.8-17.2 14.3-25.1C5.5 228.5 0 257.4 0 286.6c1.8-8.6 4.2-17 7-25.3-1.7 34.5 4.9 68.9 19.4 100.3 19.4 43.5 51.6 80 92.3 104.7 16.6 11.2 34.7 19.9 53.8 25.8 2.5.9 5.1 1.8 7.7 2.7-.8-.3-1.6-.7-2.4-1 22.6 6.8 46.2 10.3 69.8 10.3 83.7 0 111.3-31.9 113.8-35 4.1-3.7 7.5-8.2 9.9-13.3 1.6-.7 3.2-1.4 4.9-2.1l1-.5 1.9-.9c12.6-5.9 24.5-13.4 35.3-22.1 16.3-11.7 27.9-28.7 32.9-48.1 3-7.1 3.1-15 .4-22.2.9-1.4 1.7-2.8 2.7-4.3 18-28.9 28.2-61.9 29.6-95.9v-2.8c0-7.3-.6-14.5-1.9-21.6zm-299-97.6c-.4 1.1-.9 2.3-1.3 3.5.4-1.2.8-2.3 1.3-3.5z']}; +export const faFlag = {'prefix':'fas','iconName':'flag','icon':[512,512,[],'f024','M349.565 98.783C295.978 98.783 251.721 64 184.348 64c-24.955 0-47.309 4.384-68.045 12.013a55.947 55.947 0 0 0 3.586-23.562C118.117 24.015 94.806 1.206 66.338.048 34.345-1.254 8 24.296 8 56c0 19.026 9.497 35.825 24 45.945V488c0 13.255 10.745 24 24 24h16c13.255 0 24-10.745 24-24v-94.4c28.311-12.064 63.582-22.122 114.435-22.122 53.588 0 97.844 34.783 165.217 34.783 48.169 0 86.667-16.294 122.505-40.858C506.84 359.452 512 349.571 512 339.045v-243.1c0-23.393-24.269-38.87-45.485-29.016-34.338 15.948-76.454 31.854-116.95 31.854z']}; +export const faFont = {'prefix':'fas','iconName':'font','icon':[448,512,[],'f031','M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z']}; +export const faGamepad = {'prefix':'fas','iconName':'gamepad','icon':[640,512,[],'f11b','M480 96H160C71.6 96 0 167.6 0 256s71.6 160 160 160c44.8 0 85.2-18.4 114.2-48h91.5c29 29.6 69.5 48 114.2 48 88.4 0 160-71.6 160-160S568.4 96 480 96zM256 276c0 6.6-5.4 12-12 12h-52v52c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-52H76c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h52v-52c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h52c6.6 0 12 5.4 12 12v40zm184 68c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48zm80-80c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z']}; +export const faGithub = {'prefix':'fab','iconName':'github','icon':[496,512,[],'f09b','M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z']}; +export const faGlobe = {'prefix':'fas','iconName':'globe','icon':[496,512,[],'f0ac','M336.5 160C322 70.7 287.8 8 248 8s-74 62.7-88.5 152h177zM152 256c0 22.2 1.2 43.5 3.3 64h185.3c2.1-20.5 3.3-41.8 3.3-64s-1.2-43.5-3.3-64H155.3c-2.1 20.5-3.3 41.8-3.3 64zm324.7-96c-28.6-67.9-86.5-120.4-158-141.6 24.4 33.8 41.2 84.7 50 141.6h108zM177.2 18.4C105.8 39.6 47.8 92.1 19.3 160h108c8.7-56.9 25.5-107.8 49.9-141.6zM487.4 192H372.7c2.1 21 3.3 42.5 3.3 64s-1.2 43-3.3 64h114.6c5.5-20.5 8.6-41.8 8.6-64s-3.1-43.5-8.5-64zM120 256c0-21.5 1.2-43 3.3-64H8.6C3.2 212.5 0 233.8 0 256s3.2 43.5 8.6 64h114.6c-2-21-3.2-42.5-3.2-64zm39.5 96c14.5 89.3 48.7 152 88.5 152s74-62.7 88.5-152h-177zm159.3 141.6c71.4-21.2 129.4-73.7 158-141.6h-108c-8.8 56.9-25.6 107.8-50 141.6zM19.3 352c28.6 67.9 86.5 120.4 158 141.6-24.4-33.8-41.2-84.7-50-141.6h-108z']}; +export const faGoogle = {'prefix':'fab','iconName':'google','icon':[488,512,[],'f1a0','M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z']}; +export const faHashtag = {'prefix':'fas','iconName':'hashtag','icon':[448,512,[],'f292','M440.667 182.109l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l14.623-81.891C377.123 38.754 371.468 32 363.997 32h-40.632a12 12 0 0 0-11.813 9.891L296.175 128H197.54l14.623-81.891C213.477 38.754 207.822 32 200.35 32h-40.632a12 12 0 0 0-11.813 9.891L132.528 128H53.432a12 12 0 0 0-11.813 9.891l-7.143 40C33.163 185.246 38.818 192 46.289 192h74.81L98.242 320H19.146a12 12 0 0 0-11.813 9.891l-7.143 40C-1.123 377.246 4.532 384 12.003 384h74.81L72.19 465.891C70.877 473.246 76.532 480 84.003 480h40.632a12 12 0 0 0 11.813-9.891L151.826 384h98.634l-14.623 81.891C234.523 473.246 240.178 480 247.65 480h40.632a12 12 0 0 0 11.813-9.891L315.472 384h79.096a12 12 0 0 0 11.813-9.891l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l22.857-128h79.096a12 12 0 0 0 11.813-9.891zM261.889 320h-98.634l22.857-128h98.634l-22.857 128z']}; +export const faHdd = {'prefix':'fas','iconName':'hdd','icon':[576,512,[],'f0a0','M576 304v96c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48v-96c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48zm-48-80a79.557 79.557 0 0 1 30.777 6.165L462.25 85.374A48.003 48.003 0 0 0 422.311 64H153.689a48 48 0 0 0-39.938 21.374L17.223 230.165A79.557 79.557 0 0 1 48 224h480zm-48 96c-17.673 0-32 14.327-32 32s14.327 32 32 32 32-14.327 32-32-14.327-32-32-32zm-96 0c-17.673 0-32 14.327-32 32s14.327 32 32 32 32-14.327 32-32-14.327-32-32-32z']}; +export const faHeart = {'prefix':'fas','iconName':'heart','icon':[512,512,[],'f004','M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z']}; +export const faHome = {'prefix':'fas','iconName':'home','icon':[576,512,[],'f015','M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z']}; +export const faHorseHead = {'prefix':'fas','iconName':'horse-head','icon':[512,512,[],'f7ab','M509.8 332.5l-69.9-164.3c-14.9-41.2-50.4-71-93-79.2 18-10.6 46.3-35.9 34.2-82.3-1.3-5-7.1-7.9-12-6.1L166.9 76.3C35.9 123.4 0 238.9 0 398.8V480c0 17.7 14.3 32 32 32h236.2c23.8 0 39.3-25 28.6-46.3L256 384v-.7c-45.6-3.5-84.6-30.7-104.3-69.6-1.6-3.1-.9-6.9 1.6-9.3l12.1-12.1c3.9-3.9 10.6-2.7 12.9 2.4 14.8 33.7 48.2 57.4 87.4 57.4 17.2 0 33-5.1 46.8-13.2l46 63.9c6 8.4 15.7 13.3 26 13.3h50.3c8.5 0 16.6-3.4 22.6-9.4l45.3-39.8c8.9-9.1 11.7-22.6 7.1-34.4zM328 224c-13.3 0-24-10.7-24-24s10.7-24 24-24 24 10.7 24 24-10.7 24-24 24z']}; +export const faIdBadge = {'prefix':'fas','iconName':'id-badge','icon':[384,512,[],'f2c1','M336 0H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM144 32h96c8.8 0 16 7.2 16 16s-7.2 16-16 16h-96c-8.8 0-16-7.2-16-16s7.2-16 16-16zm48 128c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zm112 236.8c0 10.6-10 19.2-22.4 19.2H102.4C90 416 80 407.4 80 396.8v-19.2c0-31.8 30.1-57.6 67.2-57.6h5c12.3 5.1 25.7 8 39.8 8s27.6-2.9 39.8-8h5c37.1 0 67.2 25.8 67.2 57.6v19.2z']}; +export const faImage = {'prefix':'fas','iconName':'image','icon':[512,512,[],'f03e','M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z']}; +export const faInfo = {'prefix':'fas','iconName':'info','icon':[192,512,[],'f129','M20 424.229h20V279.771H20c-11.046 0-20-8.954-20-20V212c0-11.046 8.954-20 20-20h112c11.046 0 20 8.954 20 20v212.229h20c11.046 0 20 8.954 20 20V492c0 11.046-8.954 20-20 20H20c-11.046 0-20-8.954-20-20v-47.771c0-11.046 8.954-20 20-20zM96 0C56.235 0 24 32.235 24 72s32.235 72 72 72 72-32.235 72-72S135.764 0 96 0z']}; +export const faInfoCircle = {'prefix':'fas','iconName':'info-circle','icon':[512,512,[],'f05a','M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z']}; +export const faInternetExplorer = {'prefix':'fab','iconName':'internet-explorer','icon':[512,512,[],'f26b','M483.049 159.706c10.855-24.575 21.424-60.438 21.424-87.871 0-72.722-79.641-98.371-209.673-38.577-107.632-7.181-211.221 73.67-237.098 186.457 30.852-34.862 78.271-82.298 121.977-101.158C125.404 166.85 79.128 228.002 43.992 291.725 23.246 329.651 0 390.94 0 436.747c0 98.575 92.854 86.5 180.251 42.006 31.423 15.43 66.559 15.573 101.695 15.573 97.124 0 184.249-54.294 216.814-146.022H377.927c-52.509 88.593-196.819 52.996-196.819-47.436H509.9c6.407-43.581-1.655-95.715-26.851-141.162zM64.559 346.877c17.711 51.15 53.703 95.871 100.266 123.304-88.741 48.94-173.267 29.096-100.266-123.304zm115.977-108.873c2-55.151 50.276-94.871 103.98-94.871 53.418 0 101.981 39.72 103.981 94.871H180.536zm184.536-187.6c21.425-10.287 48.563-22.003 72.558-22.003 31.422 0 54.274 21.717 54.274 53.722 0 20.003-7.427 49.007-14.569 67.867-26.28-42.292-65.986-81.584-112.263-99.586z']}; +export const faLanguage = {'prefix':'fas','iconName':'language','icon':[640,512,[],'f1ab','M152.1 236.2c-3.5-12.1-7.8-33.2-7.8-33.2h-.5s-4.3 21.1-7.8 33.2l-11.1 37.5H163zM616 96H336v320h280c13.3 0 24-10.7 24-24V120c0-13.3-10.7-24-24-24zm-24 120c0 6.6-5.4 12-12 12h-11.4c-6.9 23.6-21.7 47.4-42.7 69.9 8.4 6.4 17.1 12.5 26.1 18 5.5 3.4 7.3 10.5 4.1 16.2l-7.9 13.9c-3.4 5.9-10.9 7.8-16.7 4.3-12.6-7.8-24.5-16.1-35.4-24.9-10.9 8.7-22.7 17.1-35.4 24.9-5.8 3.5-13.3 1.6-16.7-4.3l-7.9-13.9c-3.2-5.6-1.4-12.8 4.2-16.2 9.3-5.7 18-11.7 26.1-18-7.9-8.4-14.9-17-21-25.7-4-5.7-2.2-13.6 3.7-17.1l6.5-3.9 7.3-4.3c5.4-3.2 12.4-1.7 16 3.4 5 7 10.8 14 17.4 20.9 13.5-14.2 23.8-28.9 30-43.2H412c-6.6 0-12-5.4-12-12v-16c0-6.6 5.4-12 12-12h64v-16c0-6.6 5.4-12 12-12h16c6.6 0 12 5.4 12 12v16h64c6.6 0 12 5.4 12 12zM0 120v272c0 13.3 10.7 24 24 24h280V96H24c-13.3 0-24 10.7-24 24zm58.9 216.1L116.4 167c1.7-4.9 6.2-8.1 11.4-8.1h32.5c5.1 0 9.7 3.3 11.4 8.1l57.5 169.1c2.6 7.8-3.1 15.9-11.4 15.9h-22.9a12 12 0 0 1-11.5-8.6l-9.4-31.9h-60.2l-9.1 31.8c-1.5 5.1-6.2 8.7-11.5 8.7H70.3c-8.2 0-14-8.1-11.4-15.9z']}; +export const faLaughBeam = {'prefix':'fas','iconName':'laugh-beam','icon':[496,512,[],'f59a','M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm24 199.4c3.3-42.1 32.2-71.4 56-71.4s52.7 29.3 56 71.4c.7 8.6-10.8 11.9-14.9 4.5l-9.5-17c-7.7-13.7-19.2-21.6-31.5-21.6s-23.8 7.9-31.5 21.6l-9.5 17c-4.2 7.4-15.8 4.1-15.1-4.5zm-160 0c3.3-42.1 32.2-71.4 56-71.4s52.7 29.3 56 71.4c.7 8.6-10.8 11.9-14.9 4.5l-9.5-17c-7.7-13.7-19.2-21.6-31.5-21.6s-23.8 7.9-31.5 21.6l-9.5 17c-4.3 7.4-15.8 4-15.1-4.5zM398.9 306C390 377 329.4 432 256 432h-16c-73.4 0-134-55-142.9-126-1.2-9.5 6.3-18 15.9-18h270c9.6 0 17.1 8.4 15.9 18z']}; +export const faLinux = {'prefix':'fab','iconName':'linux','icon':[448,512,[],'f17c','M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z']}; +export const faLock = {'prefix':'fas','iconName':'lock','icon':[448,512,[],'f023','M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z']}; +export const faMapMarkerAlt = {'prefix':'fas','iconName':'map-marker-alt','icon':[384,512,[],'f3c5','M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z']}; +export const faMicrochip = {'prefix':'fas','iconName':'microchip','icon':[512,512,[],'f2db','M416 48v416c0 26.51-21.49 48-48 48H144c-26.51 0-48-21.49-48-48V48c0-26.51 21.49-48 48-48h224c26.51 0 48 21.49 48 48zm96 58v12a6 6 0 0 1-6 6h-18v6a6 6 0 0 1-6 6h-42V88h42a6 6 0 0 1 6 6v6h18a6 6 0 0 1 6 6zm0 96v12a6 6 0 0 1-6 6h-18v6a6 6 0 0 1-6 6h-42v-48h42a6 6 0 0 1 6 6v6h18a6 6 0 0 1 6 6zm0 96v12a6 6 0 0 1-6 6h-18v6a6 6 0 0 1-6 6h-42v-48h42a6 6 0 0 1 6 6v6h18a6 6 0 0 1 6 6zm0 96v12a6 6 0 0 1-6 6h-18v6a6 6 0 0 1-6 6h-42v-48h42a6 6 0 0 1 6 6v6h18a6 6 0 0 1 6 6zM30 376h42v48H30a6 6 0 0 1-6-6v-6H6a6 6 0 0 1-6-6v-12a6 6 0 0 1 6-6h18v-6a6 6 0 0 1 6-6zm0-96h42v48H30a6 6 0 0 1-6-6v-6H6a6 6 0 0 1-6-6v-12a6 6 0 0 1 6-6h18v-6a6 6 0 0 1 6-6zm0-96h42v48H30a6 6 0 0 1-6-6v-6H6a6 6 0 0 1-6-6v-12a6 6 0 0 1 6-6h18v-6a6 6 0 0 1 6-6zm0-96h42v48H30a6 6 0 0 1-6-6v-6H6a6 6 0 0 1-6-6v-12a6 6 0 0 1 6-6h18v-6a6 6 0 0 1 6-6z']}; +export const faMicrophoneSlash = {'prefix':'fas','iconName':'microphone-slash','icon':[640,512,[],'f131','M633.82 458.1l-157.8-121.96C488.61 312.13 496 285.01 496 256v-48c0-8.84-7.16-16-16-16h-16c-8.84 0-16 7.16-16 16v48c0 17.92-3.96 34.8-10.72 50.2l-26.55-20.52c3.1-9.4 5.28-19.22 5.28-29.67V96c0-53.02-42.98-96-96-96s-96 42.98-96 96v45.36L45.47 3.37C38.49-2.05 28.43-.8 23.01 6.18L3.37 31.45C-2.05 38.42-.8 48.47 6.18 53.9l588.36 454.73c6.98 5.43 17.03 4.17 22.46-2.81l19.64-25.27c5.41-6.97 4.16-17.02-2.82-22.45zM400 464h-56v-33.77c11.66-1.6 22.85-4.54 33.67-8.31l-50.11-38.73c-6.71.4-13.41.87-20.35.2-55.85-5.45-98.74-48.63-111.18-101.85L144 241.31v6.85c0 89.64 63.97 169.55 152 181.69V464h-56c-8.84 0-16 7.16-16 16v16c0 8.84 7.16 16 16 16h160c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16z']}; +export const faMinus = {'prefix':'fas','iconName':'minus','icon':[448,512,[],'f068','M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z']}; +export const faMinusCircle = {'prefix':'fas','iconName':'minus-circle','icon':[512,512,[],'f056','M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zM124 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H124z']}; +export const faMobile = {'prefix':'fas','iconName':'mobile','icon':[320,512,[],'f10b','M272 0H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h224c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM160 480c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z']}; +export const faOpera = {'prefix':'fab','iconName':'opera','icon':[496,512,[],'f26a','M313.9 32.7c-170.2 0-252.6 223.8-147.5 355.1 36.5 45.4 88.6 75.6 147.5 75.6 36.3 0 70.3-11.1 99.4-30.4-43.8 39.2-101.9 63-165.3 63-3.9 0-8 0-11.9-.3C104.6 489.6 0 381.1 0 248 0 111 111 0 248 0h.8c63.1.3 120.7 24.1 164.4 63.1-29-19.4-63.1-30.4-99.3-30.4zm101.8 397.7c-40.9 24.7-90.7 23.6-132-5.8 56.2-20.5 97.7-91.6 97.7-176.6 0-84.7-41.2-155.8-97.4-176.6 41.8-29.2 91.2-30.3 132.9-5 105.9 98.7 105.5 265.7-1.2 364z']}; +export const faPaintBrush = {'prefix':'fas','iconName':'paint-brush','icon':[512,512,[],'f1fc','M167.02 309.34c-40.12 2.58-76.53 17.86-97.19 72.3-2.35 6.21-8 9.98-14.59 9.98-11.11 0-45.46-27.67-55.25-34.35C0 439.62 37.93 512 128 512c75.86 0 128-43.77 128-120.19 0-3.11-.65-6.08-.97-9.13l-88.01-73.34zM457.89 0c-15.16 0-29.37 6.71-40.21 16.45C213.27 199.05 192 203.34 192 257.09c0 13.7 3.25 26.76 8.73 38.7l63.82 53.18c7.21 1.8 14.64 3.03 22.39 3.03 62.11 0 98.11-45.47 211.16-256.46 7.38-14.35 13.9-29.85 13.9-45.99C512 20.64 486 0 457.89 0z']}; +export const faPatreon = {'prefix':'fab','iconName':'patreon','icon':[512,512,[],'f3d9','M512 194.8c0 101.3-82.4 183.8-183.8 183.8-101.7 0-184.4-82.4-184.4-183.8 0-101.6 82.7-184.3 184.4-184.3C429.6 10.5 512 93.2 512 194.8zM0 501.5h90v-491H0v491z']}; +export const faPause = {'prefix':'fas','iconName':'pause','icon':[448,512,[],'f04c','M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z']}; +export const faPlay = {'prefix':'fas','iconName':'play','icon':[448,512,[],'f04b','M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z']}; +export const faPlug = {'prefix':'fas','iconName':'plug','icon':[384,512,[],'f1e6','M256 144V32c0-17.673 14.327-32 32-32s32 14.327 32 32v112h-64zm112 16H16c-8.837 0-16 7.163-16 16v32c0 8.837 7.163 16 16 16h16v32c0 77.406 54.969 141.971 128 156.796V512h64v-99.204c73.031-14.825 128-79.39 128-156.796v-32h16c8.837 0 16-7.163 16-16v-32c0-8.837-7.163-16-16-16zm-240-16V32c0-17.673-14.327-32-32-32S64 14.327 64 32v112h64z']}; +export const faPlus = {'prefix':'fas','iconName':'plus','icon':[448,512,[],'f067','M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z']}; +export const faPlusCircle = {'prefix':'fas','iconName':'plus-circle','icon':[512,512,[],'f055','M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z']}; +export const faQuestionCircle = {'prefix':'fas','iconName':'question-circle','icon':[512,512,[],'f059','M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zM262.655 90c-54.497 0-89.255 22.957-116.549 63.758-3.536 5.286-2.353 12.415 2.715 16.258l34.699 26.31c5.205 3.947 12.621 3.008 16.665-2.122 17.864-22.658 30.113-35.797 57.303-35.797 20.429 0 45.698 13.148 45.698 32.958 0 14.976-12.363 22.667-32.534 33.976C247.128 238.528 216 254.941 216 296v4c0 6.627 5.373 12 12 12h56c6.627 0 12-5.373 12-12v-1.333c0-28.462 83.186-29.647 83.186-106.667 0-58.002-60.165-102-116.531-102zM256 338c-25.365 0-46 20.635-46 46 0 25.364 20.635 46 46 46s46-20.636 46-46c0-25.365-20.635-46-46-46z']}; +export const faRedo = {'prefix':'fas','iconName':'redo','icon':[512,512,[],'f01e','M500.33 0h-47.41a12 12 0 0 0-12 12.57l4 82.76A247.42 247.42 0 0 0 256 8C119.34 8 7.9 119.53 8 256.19 8.1 393.07 119.1 504 256 504a247.1 247.1 0 0 0 166.18-63.91 12 12 0 0 0 .48-17.43l-34-34a12 12 0 0 0-16.38-.55A176 176 0 1 1 402.1 157.8l-101.53-4.87a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12h200.33a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12z']}; +export const faRetweet = {'prefix':'fas','iconName':'retweet','icon':[640,512,[],'f079','M629.657 343.598L528.971 444.284c-9.373 9.372-24.568 9.372-33.941 0L394.343 343.598c-9.373-9.373-9.373-24.569 0-33.941l10.823-10.823c9.562-9.562 25.133-9.34 34.419.492L480 342.118V160H292.451a24.005 24.005 0 0 1-16.971-7.029l-16-16C244.361 121.851 255.069 96 276.451 96H520c13.255 0 24 10.745 24 24v222.118l40.416-42.792c9.285-9.831 24.856-10.054 34.419-.492l10.823 10.823c9.372 9.372 9.372 24.569-.001 33.941zm-265.138 15.431A23.999 23.999 0 0 0 347.548 352H160V169.881l40.416 42.792c9.286 9.831 24.856 10.054 34.419.491l10.822-10.822c9.373-9.373 9.373-24.569 0-33.941L144.971 67.716c-9.373-9.373-24.569-9.373-33.941 0L10.343 168.402c-9.373 9.373-9.373 24.569 0 33.941l10.822 10.822c9.562 9.562 25.133 9.34 34.419-.491L96 169.881V392c0 13.255 10.745 24 24 24h243.549c21.382 0 32.09-25.851 16.971-40.971l-16.001-16z']}; +export const faSafari = {'prefix':'fab','iconName':'safari','icon':[512,512,[],'f267','M236.9 256.8c0-9.1 6.6-17.7 16.3-17.7 8.9 0 17.4 6.4 17.4 16.1 0 9.1-6.4 17.7-16.1 17.7-9 0-17.6-6.7-17.6-16.1zM504 256c0 137-111 248-248 248S8 393 8 256 119 8 256 8s248 111 248 248zm-26.6 0c0-122.3-99.1-221.4-221.4-221.4S34.6 133.7 34.6 256 133.7 477.4 256 477.4 477.4 378.3 477.4 256zm-72.5 96.6c0 3.6 13 10.2 16.3 12.2-27.4 41.5-69.8 71.4-117.9 83.3l-4.4-18.5c-.3-2.5-1.9-2.8-4.2-2.8-1.9 0-3 2.8-2.8 4.2l4.4 18.8c-13.3 2.8-26.8 4.2-40.4 4.2-36.3 0-72-10.2-103-29.1 1.7-2.8 12.2-18 12.2-20.2 0-1.9-1.7-3.6-3.6-3.6-3.9 0-12.2 16.6-14.7 19.9-41.8-27.7-72-70.6-83.6-119.6l19.1-4.2c2.2-.6 2.8-2.2 2.8-4.2 0-1.9-2.8-3-4.4-2.8L62 294.5c-2.5-12.7-3.9-25.5-3.9-38.5 0-37.1 10.5-73.6 30.2-104.9 2.8 1.7 16.1 10.8 18.3 10.8 1.9 0 3.6-1.4 3.6-3.3 0-3.9-14.7-11.3-18-13.6 28.2-41.2 71.1-70.9 119.8-81.9l4.2 18.5c.6 2.2 2.2 2.8 4.2 2.8s3-2.8 2.8-4.4L219 61.7c12.2-2.2 24.6-3.6 37.1-3.6 37.1 0 73.3 10.5 104.9 30.2-1.9 2.8-10.8 15.8-10.8 18 0 1.9 1.4 3.6 3.3 3.6 3.9 0 11.3-14.4 13.3-17.7 41 27.7 70.3 70 81.7 118.2l-15.5 3.3c-2.5.6-2.8 2.2-2.8 4.4 0 1.9 2.8 3 4.2 2.8l15.8-3.6c2.5 12.7 3.9 25.7 3.9 38.7 0 36.3-10 72-28.8 102.7-2.8-1.4-14.4-9.7-16.6-9.7-2.1 0-3.8 1.7-3.8 3.6zm-33.2-242.2c-13 12.2-134.2 123.7-137.6 129.5l-96.6 160.5c12.7-11.9 134.2-124 137.3-129.3l96.9-160.7z']}; +export const faSave = {'prefix':'fas','iconName':'save','icon':[448,512,[],'f0c7','M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z']}; +export const faSearch = {'prefix':'fas','iconName':'search','icon':[512,512,[],'f002','M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z']}; +export const faShare = {'prefix':'fas','iconName':'share','icon':[512,512,[],'f064','M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z']}; +export const faSignOutAlt = {'prefix':'fas','iconName':'sign-out-alt','icon':[512,512,[],'f2f5','M497 273L329 441c-15 15-41 4.5-41-17v-96H152c-13.3 0-24-10.7-24-24v-96c0-13.3 10.7-24 24-24h136V88c0-21.4 25.9-32 41-17l168 168c9.3 9.4 9.3 24.6 0 34zM192 436v-40c0-6.6-5.4-12-12-12H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h84c6.6 0 12-5.4 12-12V76c0-6.6-5.4-12-12-12H96c-53 0-96 43-96 96v192c0 53 43 96 96 96h84c6.6 0 12-5.4 12-12z']}; +export const faSlidersH = {'prefix':'fas','iconName':'sliders-h','icon':[512,512,[],'f1de','M496 384H160v-16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v16H16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h80v16c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16v-16h336c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm0-160h-80v-16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v16H16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h336v16c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16v-16h80c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm0-160H288V48c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v16H16C7.2 64 0 71.2 0 80v32c0 8.8 7.2 16 16 16h208v16c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16v-16h208c8.8 0 16-7.2 16-16V80c0-8.8-7.2-16-16-16z']}; +export const faSpinner = {'prefix':'fas','iconName':'spinner','icon':[512,512,[],'f110','M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z']}; +export const faStar = {'prefix':'fas','iconName':'star','icon':[576,512,[],'f005','M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z']}; +export const faStepForward = {'prefix':'fas','iconName':'step-forward','icon':[448,512,[],'f051','M384 44v424c0 6.6-5.4 12-12 12h-48c-6.6 0-12-5.4-12-12V291.6l-195.5 181C95.9 489.7 64 475.4 64 448V64c0-27.4 31.9-41.7 52.5-24.6L312 219.3V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12z']}; +export const faStickyNote = {'prefix':'fas','iconName':'sticky-note','icon':[448,512,[],'f249','M312 320h136V56c0-13.3-10.7-24-24-24H24C10.7 32 0 42.7 0 56v400c0 13.3 10.7 24 24 24h264V344c0-13.2 10.8-24 24-24zm129 55l-98 98c-4.5 4.5-10.6 7-17 7h-6V352h128v6.1c0 6.3-2.5 12.4-7 16.9z']}; +export const faStop = {'prefix':'fas','iconName':'stop','icon':[448,512,[],'f04d','M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z']}; +export const faSync = {'prefix':'fas','iconName':'sync','icon':[512,512,[],'f021','M440.65 12.57l4 82.77A247.16 247.16 0 0 0 255.83 8C134.73 8 33.91 94.92 12.29 209.82A12 12 0 0 0 24.09 224h49.05a12 12 0 0 0 11.67-9.26 175.91 175.91 0 0 1 317-56.94l-101.46-4.86a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12H500a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12h-47.37a12 12 0 0 0-11.98 12.57zM255.83 432a175.61 175.61 0 0 1-146-77.8l101.8 4.87a12 12 0 0 0 12.57-12v-47.4a12 12 0 0 0-12-12H12a12 12 0 0 0-12 12V500a12 12 0 0 0 12 12h47.35a12 12 0 0 0 12-12.6l-4.15-82.57A247.17 247.17 0 0 0 255.83 504c121.11 0 221.93-86.92 243.55-201.82a12 12 0 0 0-11.8-14.18h-49.05a12 12 0 0 0-11.67 9.26A175.86 175.86 0 0 1 255.83 432z']}; +export const faTablet = {'prefix':'fas','iconName':'tablet','icon':[448,512,[],'f10a','M400 0H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM224 480c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z']}; +export const faTerminal = {'prefix':'fas','iconName':'terminal','icon':[640,512,[],'f120','M257.981 272.971L63.638 467.314c-9.373 9.373-24.569 9.373-33.941 0L7.029 444.647c-9.357-9.357-9.375-24.522-.04-33.901L161.011 256 6.99 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L257.981 239.03c9.373 9.372 9.373 24.568 0 33.941zM640 456v-32c0-13.255-10.745-24-24-24H312c-13.255 0-24 10.745-24 24v32c0 13.255 10.745 24 24 24h304c13.255 0 24-10.745 24-24z']}; +export const faTimes = {'prefix':'fas','iconName':'times','icon':[352,512,[],'f00d','M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z']}; +export const faTrash = {'prefix':'fas','iconName':'trash','icon':[448,512,[],'f1f8','M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z']}; +export const faTumblr = {'prefix':'fab','iconName':'tumblr','icon':[320,512,[],'f173','M309.8 480.3c-13.6 14.5-50 31.7-97.4 31.7-120.8 0-147-88.8-147-140.6v-144H17.9c-5.5 0-10-4.5-10-10v-68c0-7.2 4.5-13.6 11.3-16 62-21.8 81.5-76 84.3-117.1.8-11 6.5-16.3 16.1-16.3h70.9c5.5 0 10 4.5 10 10v115.2h83c5.5 0 10 4.4 10 9.9v81.7c0 5.5-4.5 10-10 10h-83.4V360c0 34.2 23.7 53.6 68 35.8 4.8-1.9 9-3.2 12.7-2.2 3.5.9 5.8 3.4 7.4 7.9l22 64.3c1.8 5 3.3 10.6-.4 14.5z']}; +export const faTv = {'prefix':'fas','iconName':'tv','icon':[640,512,[],'f26c','M592 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h245.1v32h-160c-17.7 0-32 14.3-32 32s14.3 32 32 32h384c17.7 0 32-14.3 32-32s-14.3-32-32-32h-160v-32H592c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h512v288z']}; +export const faTwitter = {'prefix':'fab','iconName':'twitter','icon':[512,512,[],'f099','M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z']}; +export const faUser = {'prefix':'fas','iconName':'user','icon':[448,512,[],'f007','M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z']}; +export const faUserCog = {'prefix':'fas','iconName':'user-cog','icon':[640,512,[],'f4fe','M610.5 373.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 400.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm201.2 226.5c-2.3-1.2-4.6-2.6-6.8-3.9l-7.9 4.6c-6 3.4-12.8 5.3-19.6 5.3-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-5.5-17.7 1.9-36.4 17.9-45.7l7.9-4.6c-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-16-9.2-23.4-28-17.9-45.7.9-2.9 2.2-5.8 3.2-8.7-3.8-.3-7.5-1.2-11.4-1.2h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c10.1 0 19.5-3.2 27.2-8.5-1.2-3.8-2-7.7-2-11.8v-9.2z']}; +export const faUserFriends = {'prefix':'fas','iconName':'user-friends','icon':[640,512,[],'f500','M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32 80 82.1 80 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zM480 256c53 0 96-43 96-96s-43-96-96-96-96 43-96 96 43 96 96 96zm48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4 24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48 0-61.9-50.1-112-112-112z']}; +export const faUserMinus = {'prefix':'fas','iconName':'user-minus','icon':[640,512,[],'f503','M624 208H432c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h192c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm-400 48c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z']}; +export const faUserPlus = {'prefix':'fas','iconName':'user-plus','icon':[640,512,[],'f234','M624 208h-64v-64c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v64h-64c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h64v64c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16v-64h64c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm-400 48c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z']}; +export const faUsers = {'prefix':'fas','iconName':'users','icon':[640,512,[],'f0c0','M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z']}; +export const faUserSecret = {'prefix':'fas','iconName':'user-secret','icon':[448,512,[],'f21b','M383.9 308.3l23.9-62.6c4-10.5-3.7-21.7-15-21.7h-58.5c11-18.9 17.8-40.6 17.8-64v-.3c39.2-7.8 64-19.1 64-31.7 0-13.3-27.3-25.1-70.1-33-9.2-32.8-27-65.8-40.6-82.8-9.5-11.9-25.9-15.6-39.5-8.8l-27.6 13.8c-9 4.5-19.6 4.5-28.6 0L182.1 3.4c-13.6-6.8-30-3.1-39.5 8.8-13.5 17-31.4 50-40.6 82.8-42.7 7.9-70 19.7-70 33 0 12.6 24.8 23.9 64 31.7v.3c0 23.4 6.8 45.1 17.8 64H56.3c-11.5 0-19.2 11.7-14.7 22.3l25.8 60.2C27.3 329.8 0 372.7 0 422.4v44.8C0 491.9 20.1 512 44.8 512h358.4c24.7 0 44.8-20.1 44.8-44.8v-44.8c0-48.4-25.8-90.4-64.1-114.1zM176 480l-41.6-192 49.6 32 24 40-32 120zm96 0l-32-120 24-40 49.6-32L272 480zm41.7-298.5c-3.9 11.9-7 24.6-16.5 33.4-10.1 9.3-48 22.4-64-25-2.8-8.4-15.4-8.4-18.3 0-17 50.2-56 32.4-64 25-9.5-8.8-12.7-21.5-16.5-33.4-.8-2.5-6.3-5.7-6.3-5.8v-10.8c28.3 3.6 61 5.8 96 5.8s67.7-2.1 96-5.8v10.8c-.1.1-5.6 3.2-6.4 5.8z']}; +export const faUserTimes = {'prefix':'fas','iconName':'user-times','icon':[640,512,[],'f235','M589.6 240l45.6-45.6c6.3-6.3 6.3-16.5 0-22.8l-22.8-22.8c-6.3-6.3-16.5-6.3-22.8 0L544 194.4l-45.6-45.6c-6.3-6.3-16.5-6.3-22.8 0l-22.8 22.8c-6.3 6.3-6.3 16.5 0 22.8l45.6 45.6-45.6 45.6c-6.3 6.3-6.3 16.5 0 22.8l22.8 22.8c6.3 6.3 16.5 6.3 22.8 0l45.6-45.6 45.6 45.6c6.3 6.3 16.5 6.3 22.8 0l22.8-22.8c6.3-6.3 6.3-16.5 0-22.8L589.6 240zM224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z']}; +export const faVk = {'prefix':'fab','iconName':'vk','icon':[576,512,[],'f189','M545 117.7c3.7-12.5 0-21.7-17.8-21.7h-58.9c-15 0-21.9 7.9-25.6 16.7 0 0-30 73.1-72.4 120.5-13.7 13.7-20 18.1-27.5 18.1-3.7 0-9.4-4.4-9.4-16.9V117.7c0-15-4.2-21.7-16.6-21.7h-92.6c-9.4 0-15 7-15 13.5 0 14.2 21.2 17.5 23.4 57.5v86.8c0 19-3.4 22.5-10.9 22.5-20 0-68.6-73.4-97.4-157.4-5.8-16.3-11.5-22.9-26.6-22.9H38.8c-16.8 0-20.2 7.9-20.2 16.7 0 15.6 20 93.1 93.1 195.5C160.4 378.1 229 416 291.4 416c37.5 0 42.1-8.4 42.1-22.9 0-66.8-3.4-73.1 15.4-73.1 8.7 0 23.7 4.4 58.7 38.1 40 40 46.6 57.9 69 57.9h58.9c16.8 0 25.3-8.4 20.4-25-11.2-34.9-86.9-106.7-90.3-111.5-8.7-11.2-6.2-16.2 0-26.2.1-.1 72-101.3 79.4-135.6z']}; +export const faVolumeDown = {'prefix':'fas','iconName':'volume-down','icon':[384,512,[],'f027','M215.03 72.04L126.06 161H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V89.02c0-21.47-25.96-31.98-40.97-16.98zm123.2 108.08c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 229.28 336 242.62 336 257c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.87z']}; +export const faVolumeOff = {'prefix':'fas','iconName':'volume-off','icon':[256,512,[],'f026','M215 71l-89 89H24a24 24 0 0 0-24 24v144a24 24 0 0 0 24 24h102.06L215 441c15 15 41 4.47 41-17V88c0-21.47-26-32-41-17z']}; +export const faVolumeUp = {'prefix':'fas','iconName':'volume-up','icon':[576,512,[],'f028','M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zm233.32-51.08c-11.17-7.33-26.18-4.24-33.51 6.95-7.34 11.17-4.22 26.18 6.95 33.51 66.27 43.49 105.82 116.6 105.82 195.58 0 78.98-39.55 152.09-105.82 195.58-11.17 7.32-14.29 22.34-6.95 33.5 7.04 10.71 21.93 14.56 33.51 6.95C528.27 439.58 576 351.33 576 256S528.27 72.43 448.35 19.97zM480 256c0-63.53-32.06-121.94-85.77-156.24-11.19-7.14-26.03-3.82-33.12 7.46s-3.78 26.21 7.41 33.36C408.27 165.97 432 209.11 432 256s-23.73 90.03-63.48 115.42c-11.19 7.14-14.5 22.07-7.41 33.36 6.51 10.36 21.12 15.14 33.12 7.46C447.94 377.94 480 319.54 480 256zm-141.77-76.87c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 228.28 336 241.63 336 256c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.86z']}; +export const faWindows = {'prefix':'fab','iconName':'windows','icon':[448,512,[],'f17a','M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z']}; +export const faWrench = {'prefix':'fas','iconName':'wrench','icon':[512,512,[],'f0ad','M507.73 109.1c-2.24-9.03-13.54-12.09-20.12-5.51l-74.36 74.36-67.88-11.31-11.31-67.88 74.36-74.36c6.62-6.62 3.43-17.9-5.66-20.16-47.38-11.74-99.55.91-136.58 37.93-39.64 39.64-50.55 97.1-34.05 147.2L18.74 402.76c-24.99 24.99-24.99 65.51 0 90.5 24.99 24.99 65.51 24.99 90.5 0l213.21-213.21c50.12 16.71 107.47 5.68 147.37-34.22 37.07-37.07 49.7-89.32 37.91-136.73zM64 472c-13.25 0-24-10.75-24-24 0-13.26 10.75-24 24-24s24 10.74 24 24c0 13.25-10.75 24-24 24z']}; +export const faYandexInternational = {'prefix':'fab','iconName':'yandex-international','icon':[320,512,[],'f414','M129.5 512V345.9L18.5 48h55.8l81.8 229.7L250.2 0h51.3L180.8 347.8V512h-51.3z']}; diff --git a/src/ts/generated/gamepad-mappings.ts b/src/ts/generated/gamepad-mappings.ts new file mode 100644 index 0000000..b4ed477 --- /dev/null +++ b/src/ts/generated/gamepad-mappings.ts @@ -0,0 +1,274 @@ +/* tslint:disable:max-line-length */ + +export interface GamepadMapping { + axes: AxesTable; + buttons: ButtonsTable; + name: string; + supported: any[]; +} + +interface Index { + index: number; +} + +interface PositiveNegative { + buttonPositive: number; + buttonNegative: number; +} + +interface Positive { + buttonPositive: number; +} + +interface AxisDirection { + axis: number; + direction: number; +} + +export const enum GamepadAxes { + LeftStickX, + LeftStickY, + RightStickX, + RightStickY, + DpadX, + DpadY, + LeftTrigger, + RightTrigger, +} + +export interface AxesTable { + [key: number]: Index | Positive | PositiveNegative | undefined; +} + +export const enum GamepadButtons { + A, + B, + X, + Y, + Back, + Start, + DpadDown, + DpadLeft, + DpadRight, + DpadUp, + LeftShoulder, + LeftStick, + LeftStickDown, + LeftStickLeft, + LeftStickRight, + LeftStickUp, + LeftTrigger, + RightShoulder, + RightStick, + RightStickDown, + RightStickLeft, + RightStickRight, + RightStickUp, + RightTrigger, + Home, +} + +export interface GamepadBrowser { + browser: string; + id: string; + os: string; +} + +export interface ButtonsTable { + [key: number]: Index | AxisDirection | undefined; +} + +function gamepad(name: string, supported: GamepadBrowser[], axes: AxesTable, buttons: ButtonsTable): GamepadMapping { + return { name, supported, axes, buttons }; +} + +function browser(browser: string, id: string, os: string): GamepadBrowser { + return { browser, id, os }; +} + +function index(index: number): Index { + return { index }; +} + +function positive(buttonPositive: number): Positive { + return { buttonPositive }; +} + +function positiveNegative(buttonPositive: number, buttonNegative: number): PositiveNegative { + return { buttonPositive, buttonNegative }; +} + +function axisDirection(axis: number, direction: number): AxisDirection { + return { axis, direction }; +} + +function axes( + lx: number, ly: number, rx: number, ry: number, dpadX?: Index | PositiveNegative, dpadY?: Index | PositiveNegative, + lt?: Index | Positive, rt?: Index | Positive +): AxesTable { + const result: AxesTable = []; + result[GamepadAxes.LeftStickX] = index(lx); + result[GamepadAxes.LeftStickY] = index(ly); + result[GamepadAxes.RightStickX] = index(rx); + result[GamepadAxes.RightStickY] = index(ry); + result[GamepadAxes.DpadX] = dpadX; + result[GamepadAxes.DpadY] = dpadY; + result[GamepadAxes.LeftTrigger] = lt; + result[GamepadAxes.RightTrigger] = rt; + return result; +} + +function buttons( + a: number, + b: number, + x: number, + y: number, + back: number, + start: number, + dpad_down: Index | AxisDirection, + dpad_left: Index | AxisDirection, + dpad_right: Index | AxisDirection, + dpad_up: Index | AxisDirection, + left_shoulder: number, + left_stick: number, + left_stick_down: AxisDirection, + left_stick_left: AxisDirection, + left_stick_right: AxisDirection, + left_stick_up: AxisDirection, + left_trigger: Index | AxisDirection, + right_shoulder: number, + right_stick: number, + right_stick_down: AxisDirection, + right_stick_left: AxisDirection, + right_stick_right: AxisDirection | undefined, + right_stick_up: AxisDirection, + right_trigger: Index | AxisDirection, + home?: number, +): ButtonsTable { + const result: ButtonsTable = []; + result[GamepadButtons.A] = index(a); + result[GamepadButtons.B] = index(b); + result[GamepadButtons.X] = index(x); + result[GamepadButtons.Y] = index(y); + result[GamepadButtons.Back] = index(back); + result[GamepadButtons.Start] = index(start); + result[GamepadButtons.DpadDown] = dpad_down; + result[GamepadButtons.DpadLeft] = dpad_left; + result[GamepadButtons.DpadRight] = dpad_right; + result[GamepadButtons.DpadUp] = dpad_up; + result[GamepadButtons.LeftShoulder] = index(left_shoulder); + result[GamepadButtons.LeftStick] = index(left_stick); + result[GamepadButtons.LeftStickDown] = left_stick_down; + result[GamepadButtons.LeftStickLeft] = left_stick_left; + result[GamepadButtons.LeftStickRight] = left_stick_right; + result[GamepadButtons.LeftStickUp] = left_stick_up; + result[GamepadButtons.LeftTrigger] = left_trigger; + result[GamepadButtons.RightShoulder] = index(right_shoulder); + result[GamepadButtons.RightStick] = index(right_stick); + result[GamepadButtons.RightStickDown] = right_stick_down; + result[GamepadButtons.RightStickLeft] = right_stick_left; + result[GamepadButtons.RightStickRight] = right_stick_right; + result[GamepadButtons.RightStickUp] = right_stick_up; + result[GamepadButtons.RightTrigger] = right_trigger; + result[GamepadButtons.Home] = home ? index(home) : undefined; + return result; +} + +export const GAMEPAD_MAPPINGS: GamepadMapping[] = [ + gamepad('Logitech F310 (DirectInput) Chrome/Firefox Linux', [ + browser('Chrome', 'Logitech Logitech Dual Action (Vendor: 046d Product: c216)', 'Linux'), + browser('Firefox', '046d-c216-Logitech Logitech Dual Action', 'Linux'), + ], + axes(0, 1, 2, 3, index(4), index(5)), + buttons(1, 2, 0, 3, 8, 9, axisDirection(5, 1), axisDirection(4, -1), axisDirection(4, 1), axisDirection(5, -1), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Chrome Windows/OSX', [ + browser('Chrome', 'Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)', 'Mac OS X'), + browser('Chrome', 'Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Firefox OSX', [ + browser('Firefox', '46d-c216-Logitech Dual Action', 'Mac OS X'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(1, 2, 0, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(2, 1), axisDirection(1, -1), axisDirection(1, 1), axisDirection(2, -1), index(6), 5, 11, axisDirection(4, 1), axisDirection(3, -1), undefined, axisDirection(4, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Firefox Windows', [ + browser('Firefox', '046d-c216-Logitech Dual Action', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(1, 2, 0, 3, 8, 9, index(14), index(15), index(16), index(13), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (XInput) Chrome Linux', [ + browser('Chrome', 'Logitech Gamepad F310 (STANDARD GAMEPAD Vendor: 046d Product: c21d)', 'Linux'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('Logitech F310 (XInput) Firefox Linux', [ + browser('Firefox', '046d-c21d-Logitech Gamepad F310', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268) Chrome OSX Linux', [ + browser('Chrome', 'PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)', 'Mac OS X'), + browser('Chrome', 'Sony PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)', 'Linux'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('054c-0268-Sony PLAYSTATION(R)3 Controller Firefox Linux', [ + browser('Firefox', '054c-0268-Sony PLAYSTATION(R)3 Controller', 'Linux'), + ], + axes(0, 1, 2, 3, index(6), index(7), index(12), index(13)), + buttons(14, 13, 15, 12, 0, 3, index(6), index(7), index(5), index(4), 10, 1, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(8), 11, 2, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(9), 16)), + gamepad('PS4 Chrome Linux', [ + browser('Chrome', 'Sony Computer Entertainment Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Linux'), + ], + axes(0, 1, 2, 3, positiveNegative(15, 14), positiveNegative(13, 12), positive(6), positive(7)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('PS4 Chrome Windows/OSX', [ + browser('Chrome', 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Windows NT'), + browser('Chrome', 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Mac OS X'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('PS4 Firefox Linux', [ + browser('Firefox', '054c-05c4-Sony Computer Entertainment Wireless Controller', 'Linux'), + ], + axes(0, 1, 2, 5, index(6), index(7), index(3), index(4)), + buttons(1, 2, 0, 3, 8, 9, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(5, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(5, -1), index(7), 12)), + gamepad('PS4 Firefox OSX', [ + browser('Firefox', '54c-5c4-Wireless Controller', 'Mac OS X'), + ], + axes(0, 1, 2, 5, index(6), index(7)), + buttons(0, 1, 2, 3, 8, 9, index(15), index(16), index(17), index(14), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(5, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(5, -1), index(7), 12)), + gamepad('XBone Chrome Linux', [ + browser('Chrome', 'Microsoft Controller (Vendor: 045e Product: 02d1)', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('Xbox One Chrome OSX Linux', [ + browser('Chrome', '©Microsoft Corporation Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)', 'Linux'), + browser('Chrome', 'Xbox One Controller (STANDARD GAMEPAD Vendor: 02d1 Product: 045e)', 'Mac OS X'), + ], + axes(0, 1, 2, 3), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('Xbone Firefox Linux', [ + browser('Firefox', '045e-02d1-Microsoft X-Box One pad', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('Xbox 360 Chrome Windows/OSX', [ + browser('Chrome', 'Xbox 360 Controller (STANDARD GAMEPAD Vendor: 028e Product: 045e)', 'Mac OS X'), + browser('Chrome', 'Xbox 360 Controller (XInput STANDARD GAMEPAD)', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('Xbox 360 Firefox Linux', [ + browser('Firefox', '045e-028e-Microsoft X-Box 360 pad', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('Xbox 360 FF Windows', [ + browser('Firefox', 'xinput', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), +]; diff --git a/src/ts/generated/gamepad-template.ts b/src/ts/generated/gamepad-template.ts new file mode 100644 index 0000000..7a2cadb --- /dev/null +++ b/src/ts/generated/gamepad-template.ts @@ -0,0 +1,177 @@ +export interface GamepadMapping { + axes: AxesTable; + buttons: ButtonsTable; + name: string; + supported: any[]; +} + +interface Index { + index: number; +} + +interface PositiveNegative { + buttonPositive: number; + buttonNegative: number; +} + +interface Positive { + buttonPositive: number; +} + +interface AxisDirection { + axis: number; + direction: number; +} + +export const enum GamepadAxes { + LeftStickX, + LeftStickY, + RightStickX, + RightStickY, + DpadX, + DpadY, + LeftTrigger, + RightTrigger, +} + +export interface AxesTable { + [key: number]: Index | Positive | PositiveNegative | undefined; +} + +export const enum GamepadButtons { + A, + B, + X, + Y, + Back, + Start, + DpadDown, + DpadLeft, + DpadRight, + DpadUp, + LeftShoulder, + LeftStick, + LeftStickDown, + LeftStickLeft, + LeftStickRight, + LeftStickUp, + LeftTrigger, + RightShoulder, + RightStick, + RightStickDown, + RightStickLeft, + RightStickRight, + RightStickUp, + RightTrigger, + Home, +} + +export interface GamepadBrowser { + browser: string; + id: string; + os: string; +} + +export interface ButtonsTable { + [key: number]: Index | AxisDirection | undefined; +} + +export function gamepad(name: string, supported: GamepadBrowser[], axes: AxesTable, buttons: ButtonsTable): GamepadMapping { + return { name, supported, axes, buttons }; +} + +export function browser(browser: string, id: string, os: string): GamepadBrowser { + return { browser, id, os }; +} + +export function index(index: number): Index { + return { index }; +} + +export function positive(buttonPositive: number): Positive { + return { buttonPositive }; +} + +export function positiveNegative(buttonPositive: number, buttonNegative: number): PositiveNegative { + return { buttonPositive, buttonNegative }; +} + +export function axisDirection(axis: number, direction: number): AxisDirection { + return { axis, direction }; +} + +export function axes( + lx: number, ly: number, rx: number, ry: number, dpadX?: Index | PositiveNegative, dpadY?: Index | PositiveNegative, + lt?: Index | Positive, rt?: Index | Positive +): AxesTable { + const result: AxesTable = []; + result[GamepadAxes.LeftStickX] = index(lx); + result[GamepadAxes.LeftStickY] = index(ly); + result[GamepadAxes.RightStickX] = index(rx); + result[GamepadAxes.RightStickY] = index(ry); + result[GamepadAxes.DpadX] = dpadX; + result[GamepadAxes.DpadY] = dpadY; + result[GamepadAxes.LeftTrigger] = lt; + result[GamepadAxes.RightTrigger] = rt; + return result; +} + +export function buttons( + a: number, + b: number, + x: number, + y: number, + back: number, + start: number, + dpad_down: Index | AxisDirection, + dpad_left: Index | AxisDirection, + dpad_right: Index | AxisDirection, + dpad_up: Index | AxisDirection, + left_shoulder: number, + left_stick: number, + left_stick_down: AxisDirection, + left_stick_left: AxisDirection, + left_stick_right: AxisDirection, + left_stick_up: AxisDirection, + left_trigger: Index | AxisDirection, + right_shoulder: number, + right_stick: number, + right_stick_down: AxisDirection, + right_stick_left: AxisDirection, + right_stick_right: AxisDirection | undefined, + right_stick_up: AxisDirection, + right_trigger: Index | AxisDirection, + home?: number, +): ButtonsTable { + const result: ButtonsTable = []; + result[GamepadButtons.A] = index(a); + result[GamepadButtons.B] = index(b); + result[GamepadButtons.X] = index(x); + result[GamepadButtons.Y] = index(y); + result[GamepadButtons.Back] = index(back); + result[GamepadButtons.Start] = index(start); + result[GamepadButtons.DpadDown] = dpad_down; + result[GamepadButtons.DpadLeft] = dpad_left; + result[GamepadButtons.DpadRight] = dpad_right; + result[GamepadButtons.DpadUp] = dpad_up; + result[GamepadButtons.LeftShoulder] = index(left_shoulder); + result[GamepadButtons.LeftStick] = index(left_stick); + result[GamepadButtons.LeftStickDown] = left_stick_down; + result[GamepadButtons.LeftStickLeft] = left_stick_left; + result[GamepadButtons.LeftStickRight] = left_stick_right; + result[GamepadButtons.LeftStickUp] = left_stick_up; + result[GamepadButtons.LeftTrigger] = left_trigger; + result[GamepadButtons.RightShoulder] = index(right_shoulder); + result[GamepadButtons.RightStick] = index(right_stick); + result[GamepadButtons.RightStickDown] = right_stick_down; + result[GamepadButtons.RightStickLeft] = right_stick_left; + result[GamepadButtons.RightStickRight] = right_stick_right; + result[GamepadButtons.RightStickUp] = right_stick_up; + result[GamepadButtons.RightTrigger] = right_trigger; + result[GamepadButtons.Home] = home ? index(home) : undefined; + return result; +} + +export const GAMEPAD_MAPPINGS: GamepadMapping[] = [ + /*MAPPINGS*/ +]; diff --git a/src/ts/generated/hash.json b/src/ts/generated/hash.json new file mode 100644 index 0000000..bbbd8d8 --- /dev/null +++ b/src/ts/generated/hash.json @@ -0,0 +1 @@ +{"hash":"L9wF5m2jwn","stamp":395651319} \ No newline at end of file diff --git a/src/ts/generated/hash.ts b/src/ts/generated/hash.ts new file mode 100644 index 0000000..e6314f3 --- /dev/null +++ b/src/ts/generated/hash.ts @@ -0,0 +1,2 @@ +export const HASH = 'L9wF5m2jwn'; +export const STAMP = 395651319; diff --git a/src/ts/generated/pony.bin b/src/ts/generated/pony.bin new file mode 100644 index 0000000000000000000000000000000000000000..2909fced6782d23ddc5c825c94ed8f202d4a6e78 GIT binary patch literal 4194304 zcmeFaO^z+ga;3*<6sN!*@dm&Nym7Frc0nj`4AtW#)RH`aXWj}5^c37Ts-}{Gu_=ms#VgAe-oY(_@z4?0g&s)6y|NPJY z{7XOo{_p?(zm=(CL0&b=6 zzx({(GFwaM?B#Ls`;!uN-N=uMJL2~4V?4b$(Jtby?R!2m)8_SN4eyhl`^CP;_T6n% zyQKzN)!)y|^I4zKJ;3MoUFqn0J`K!yerB=*Qnk`5F{XclZ#t(k?P_VXA_vyB!xf;3aI)->ZEAi2$ zw2$`q7p0bwx=!RBujI2u4o<}1x(6rHtvFF1yp~+h8U1;hc-PJ-y4T)2*lSzssoI&x zyC(+6c@qDb^Ahs4y)*s4U;X)9zAJtAp7(oa24h}#%HQX={{8=5=6`aEl)R(d_vlR8 zUjeVD^B}q`^5{%@Tm|Oy@4L2|pEOf3TaK!Y@R-%)qqLacwoq_eX@>2#` z5~k#C8*}E&?Xx%{PPD@&%O9C^@Pj=x#Gp_{VXo`@4n#uTs(4*hq(N# zW`~~ZAOp)|_FOXFan9ub@gM*3uPr}k#x~MdR&$q)@&#rM4;|4=Z|yv@LFTp@zMo^0 z(abi}tI6D#^EI1MXPxmc?mt#~FU!f4&l()k1OEAVNB-SETiiLbmS}1I@0tD2{_p=i z5BQi5a%eo)o-?P^YSMBAyk79JzR02ReAd5Q-_a-Y`S)E><|fTi%v2-lBP^fIzCHfo z+^_p41}GR%60+Ib7nD7pT|c1-G7|v@H>)v z4)C5!4-XD>WW2A2MU9;H)ZA?xOZ;48EAXUe)e*7AYHDU;hGIV3>sL5n3f~tq8s{aS z$Rphw+gAJN-}fafbk=6lSo;!Qk6bZO<9G*tAMmAn5l)Z)Gi<1H%(a&9(Q&oR@_TfS zj(-b2y|b5Y@riEqiQK8{xLka;qE2a_*-D1~+IiGt-%3mPYw}k77QM_@i=A||7OM4C zv&qj|oY4bW-@cRVj2hqBhu<}`7V}r{?4SL&|6t-{^epGN_=0*K{_hbuZ=sIwHJkjO z3V3~DJ!k#z+a-5?Hk}b$ycwVSn5Xu}0S@!8`#i$m-grmbd;ZiQn@J^OMwiXVID`jw z=DTz!vLXJF@f?Xoyq<3<=Og0SGtcj_JtCJ@Oz)o&e5+sSm6l^fo!S|?u2bXD@1fvE zTjXmr+HKvh;kTUEe74#zdRjxDc@uBebFBwzz55PlYxQ~cbH8I|IOaFzwk`keA6;z0 zg5pE8cR5Dn@dG7}NI8!*>S>RT`|6E-gx%F~xc|sgD`-|vuPJ)aOrJCNHKL9a&$}j7I(Q~NJW5V`XY_zXZo>$5}f6b(^2HZich3rPfEv%M#nAp%I_EB-1g+tur zL0;4vIy#;$^E)xF(Jh#x^J&4OmZNElZ|Rhl+h;nQXJ^RJDN*!#sc=iH=wJ|EAFXLR{yQ_j{0CnpK;%k$JF_*z$-u7T5|iY z#5Wh`&=#CE9nCmCi{b4EUu&_PdG52F=dZluVn&r7cB5hiPuTcz|Hwp%6Pr;noQ*^L zh8oWdO8&pA{zuYNkJj82A92VJOT7D)4!N{q-&K!1 zJa5l^MBKG`9REkrn(q<0)iCIvqiHP$os|}Ld(Xis(GtGoBjSsGYyS3}ML#Y37Jq9v z!dP^)R-gS-XVz$B547~}E9;D`<(KWoMoRgF1PX2pxsLkH^Jd4(1*^^_*HtXFZWu-WfN8iXrOKVNX&ujc% z{LH?`Uxu?qR`XdpYr!wsA{WNOYrV`fXXf&ZyKL#-SJrv=KILwinM8fAV)km8)1nTx z;JFX{wo?5Yt061uMPf*XEX)hsr#?-Zqpl=W58!Vsg2WGEql%!JBuO8h*xy3 zdIje5?{|He2Q@2cwis0-Va54NzT6+@%rs6p+e#gymEY=PO`kkP*Ou3Vct_lUu!z&+ zz@N@X_-Vx$nOlob{HX@y-(nYaN^>^&HR{ju9=?XnSt-7lNAG^Ee~&t^#rseH^iN-I z#Yf$}=MgXN_u{YFytnCx9b+6)FN zxA=}avEJEUHuxdFQFkC*Vv4nwuMvLMFvuy=HMX@BZsb?qgSG9_IuCqdvu-g(@I<7S;G>>qNDZeUZ!l; zEBn@IkL7-z}DR|D`m4D-`QK|Hta+-o8e<*FM%n1_qR3Hnm)M9m-rMtYPk>m zIwht^T?dSw7_k*~O!(-xR=<&F(Qn)npC~!>lZE8}hAD?1_`PZbT1c^_M^QHNsB~gM5fZzFk(c9dYJ)#{KvRII<&DYX%9qgcfCLVb!U9;bEUh{cmeT7}~ot?GB zli9iK)9)ghv+k_HxjoR*zpt$G?)}EyGB5cIC7$o=Epr?;qTmUC^olXN-!1W$bRExU z$|G{2HsmBq?VNf{8IP{xfZamf4lJkcvsAY;vKZ*MHcI!;%HjX9yBUX&t{X5?p zKYDIU9!KTib&2`l+v5>WoKQc<1}v!iDe>rn0d@S$KXF_|Jw9S47icNxca}f>`L#EG zzk6oE(eog)h|gW_H}Z<3^b>Vk_9d(}c?`s z;4AnT5Bn?!^4mhcyY(7wORTS`dvw)%YnH{e0c%nHSi7Hd3(CZpHE8aEtZ&~*_U@k- zziVbCW+%SCx6EVMfB^;js5#8>@#&PfTB+L}6|>h~e3pa9+j0-+$M_vP`epAr*n=n9 z;y>!R{Z>9=we0ah{Gy(h__@9-FrR^L=}qoL4~g=MF~C!@7s; z4%{bqHR9eQ2G|}0zK_b;WAOaF4_&9k$Jv=&BIVh8HH~qxk9m~xt;xM#ui$I3z028A zb-${9%4hMd)jYRN-C3&dk6Mr7J$48B86n)P_yklp+C379@!K?W!onZ?FbB&@eTEZ^Lv5h`F zw^IJ^D1Y*^YeejR=geWu3nTEs^107_PX4!#>-~ZS<1v~mYYLCB2JtpU)-cfPYaEXDqsAuavppU&% zuzUQM=IxT4I7B@Lcw#A0w=em~5_Xxj^p%~o1e6p_ak8h7L^4YvhEuHery&Lry;j^4#-nBh- zyNcFgdzZ7LYJXMzl+WT@YuNYB#G18i>4Bp@OHaXVsnILz#Cu{!Vs7F)J)5;M8Tz7N zK*1KhYIzJ9e^p%gB_?X-^~~xWWv&kf6l~GbLI)eii@r*Gw*dzVJ~WGiGsh|UigLHX zClp-Kx&Bn(m7i@bxzPu4_H40j(P8fJ*$fd{6M9oC0XQ|#%j=p2Sga7UEC&xMZ zd;BX7#{nDq)ARR^bN4N*m}hBU${$?Ox&Bn(RiA6EdC><)&0owMKKsenTt6$In#4O$ z?ON}#rkCBpPw#&n=9{<_gZIFif9yukTK+ZsI46VSlxOW-smBT*^QzS#p2d4RdmrO@ z=l!VKUtL4xvBaP?pKqEwfReD zZUYad`zi70iefL?>K{F~1+(Nc%F*{2z>PKcJvy0hVMRaLKJyJ8`0mqr&e0cjyv$!F za~rV0c7KsaM-)59%6vtc>x1FA;^+EPfmeO5wdO@1#M!gezD19@l3R;T zYyR;LM#a$TpZh`oYX2>@UDNSg$WL`^SiLoV_zoyiQ|&r8;DCYIioxVtIGj z-aEKM{glHJht{xX?!=n4Z0Ui$pP#4Ox76sBb^QK(Mq+M)^8FrXtjoGS=yuTIN-Za=Q$tgKdZm{biU-DTtrLx zM!DOl29E7M-<^NuXIo2d+<%-sTWni(m^-rh>#qJ4b8CKUdae1#I~WziQ~c8(#Jg74 zmUFPcR*gr@|NeY^0#6;FbN+vw`G{xeR@(CUIYUQrFMhO!J##14tYu3NZ2io{G?$deB(RT_~&0jpj1|0C+Cp>4O=tIE~jXp|yw*dzq z_>7eES_~zBQI3Cniwd{4j_|KIhyna82F^s$hmwnE%(Jw28*&%+Tt6%D%Fniz+^7a~ zk>(_3gckc29q@#WAN)d*U*TWAnGcDzC9f?y;NnLioPi8RYE~k9Qm*%ZZjtw~AyHDpiN8c$}nV*sj8*spPpW=~=qU#i#lAlbDjVM^| zC&s|ub+8i!FY}ej&<6ut{FEqqqS)^7-;#gHC;rJL%d;fI2LJdd<^8Vm`TYC7`8bD- zstq%T=Gr)aeS81?-=Lr2-*X;4cYFL^gY0(zSsZS^mzV#{uh(C^;vg1c-s@ZTU=f3; z`$CT15kB$fIcI+2Y)ni}{rUO?-eZT4d1kdJ$-IVV@TEAW{P)aUx{uCUEoQ%c43^1< z)~U1K^f&8W`mDT*{M|nTe$UKE%w16belN{inH)QCz;~a{bB;a~9MQ~ANroLb;AcK{ zhMrR}_WA~s7@!^tJZHqz69YE#3m*7u6n*>)1H4kLqa1zmhB|iS(QyinX!KFrV<#;9 ztmXZV=kxjZee2A9n#D9DjHnH>2J@YKl-3pgiHkT#<(<{eec+c`#N0>6fp7VhuhKnm zMtmdVAO=zJp?;Rtu15wYKFE!li)OfSE(Qn6v-N3|_@Ob+tQIAi*YFI!6vvbwKEo{~ zKGw(Q=kRbpN9Qr|kL{Bq>U+ks>&`xosOgh1xf^CAQJ=rW^F3dix8fN#qTt~JpH4YD z+dqDZF;en~`K|4-i+I}OQ?#_ukv>?WYkqZ(KE7*yTKszqE&j6@h((m#ocg)*Gye!waDDao-D^%y+I8c)o`(M!Rp`HE-Q z$R~M}@_cvst3Th?^7H$TGw7P_+BtKE&tL8@@>lp@%i-vAY7lc*?MC|cyU6BX;t=Ir z_cl6?kvY|{$XC?+Pr3Pu7;z@n-m%2uhOmW&_a1B{qQmTRoX+CGbUq-@RqJU@KK zTS|P2k2CxXeWspJml^NDy*Kh%^+c?9?_s|)W+i4dzBfhVjJ3AM7Az<}MC0NNT`-RH zkFSi5@Gp!E8(FM@@4X(xvbFA6o!v)`@9|H3#JiS1b;zh=fV)Pq0dG(Kwc5PO*?j(e z-y}0AbHd)*fTQ_~neyA)Z|(~Jdvn}tPZh}hsF{1qU2s>#BHO)ro?%apPMQ5nJs9JP zGqGcd=lMEU?I^u4(@b7kjz-!g|m@sZ7CoS_RwhQB7m7Az<}@Q)n*jIQ}A zoo854=Jk&}u_{LFO1>mVhn%pFxLfwZ08jWe|M84?A_dQB)bV}9E$y)ZcSQbMYV}0j zCqA=A#>|}^G4pBGVEz(*ZC&MmWG#EJsEP7Fa^7a{E6u^gA)4KV?0fP%!-m{4>iw7X zV2m%$!x58b>Lukj;%ff4hG`Nn#*?rZV)oVEI$de_c_t#@0yCt)n-TDN`1%-CM@ zZd&{IRrTJzul-(`moz`|eJNU+w`y|inZ=;^5RHp7bit_kDV<>p78D=&N3TRn_^t91 zF7jA|xL2ISj*a_921PgK8F5?p;D9SX?thIZu3owopE!xTHP3g|o6o;LH_!aZ3_7AN znl*g>s`c1a{zv4w2ZNfBziPGzFSGTTZ|nR|KR5<_=Iaa@{?{nISIarZ8|UHZqxU%; z?YQ?=8tr@cF`t-ktjCspYc6YgTYOI4>*lhxdyMC$yOQ2=rZxQ8H!){TYdsKq^xgKg z+PuS=-#0UpW+?xDU7NwObL^SNp!jgV@SLFwMuxv8!xk(kKJba2=o;ppbNAysag-?W zDt@<%Jo$-YBRXP%=P_)pMT|Gw@5Q7B#5*eg5jDIk#ydRYYB}m&G<*8|sTrYW(|RTx zW-#HG)|LKSb2&ox6&RTnN7?m<{RtLvv18Q>h<`VHrLE|@$Opp*?q}&k!ub4-kCVFj;%e=+PAN& z*IK7n)$@C0Zeot|@6SE68F-@jaKG@Jp?`+|t$B^sC-)jvU9y@WgHk_e4RgyGc=+L7 zM%Q8s9x;d#mngAr$?u(YU-h}xnwRcA&Uvl2YkEGHWwRH3ccuTe9F9Jx{?tG<8{sp~ zdGRi4_Yq^tW@==apAqppM&<+mYgBpiL%%t7KjPy&9DVz9cMtC|wD5a;xoylR<{Rs= zcfXd~Ui@i)&8)X}uchM&(IeI1I#THTkzZmez)Kqp-UV!O5EfTDd(a`EXVgdQGJi7$r>j2DoQPg z@re9i8S_=2Ypr?dZkatbOKCo6wO!K_7V}wc-n`QPS`J5_XZ7#7gE-g4z28G^9_F)- zj^7O!8IAtf@2MXcPBY&w^SGTSuSjEjaUOQx;{DmXhmUcr?f3X{+n7(xH`ZhBpF2yb zr;fP4wO(C2k1>75xrm=z>lynq?{2MEU;X^u`&ReM>?F!O1?9VQZ6?dku>}i1v8Rw< z;XjM}Oj)c&Y40`4_>$cyIX<~d?pE~7ddU~QiG|ok<@!!I^ZEDQ^S|byt9n4bwS2~o z^V#0L&&J}JiCaG7=t3h_w%;3X%|7!-Y&BZTHQs%khhscEdq>b(Tz)pf7Z@?0m~X7d z+#f&J_+5)%YVND+`Q(@jSQkdg&ar#=1ebh=#+mq;_=9`l={4@@-&eAE`u**VUw6s; zB+5($<$JSd#={mY;b-eg|2=t)w8`pmR~h%jeR98|BWqH_!Y475dg`tRYwtl{qcbv#mXVKQ>HYGm#@%`5L zqrPJ~Uy5Z*ZmL1NuQ(6KczFJfpsjZ>!WS4ZpO|l~$J~#dowIsoeX!`Ue)Z^bP?Z(% zsy1twexBiPkvW!lt%Yh0i*aJjTDJ7SmcD&O-If}?vX1VQxlWY13d;9pZ6?dkuoD#) zKC&L->}SfddfZu--4%CzMBTu4N^cU&o$Gb?*k1YB){>j`MQhv*KOLY415T5tZ*)e`+JtKxW{aUS;j z67SF95&T+!BYZeU%qQj>>oNDo&rziwSZc4e>6ui*l6)&xNx$Z!%*9DOKQBm767+Hb)R$PcWhM1inS*+E;mK{i`+jTo=K_qx7%u4m@wb$v(v z%v#^k7kAD~#f&A&cc$pttj4+9I2QQ$iaV+rHsM|PJX;xgrJNi7<;Ys^=XI}fr zZ>^)-bC38d-Q|;Ncb8c2_)I!8ChkEq7IPW1!H77(0$YBvb(MeO5+&9Vd1n~lyH9FS zn{h{;;cqk@8RHt($k^jO#d%nJ?hWeC<|pyQ56yCo^|*3hy5}XPkAE~Sx~$Ff@fMpi zbhA8_*yq}uUxtTl!EDAW>4McuBd&0^<{B|xeebn?^<2-)&+Gb*{F$}Bqc85Nclm}gdtD`oNCmsqmZG|xsX?H90H{1jp%*X9^AoG6Pqa~|bu zd+fH-3|AP7T&p?PchmJczgzA{=IeERNB+p#J^|bBoEeLmi|_W9nGGFmz<`1+T3YB} z0~XjV6dN$0U`PMxXSBuNnofr6vQjOcCinVM^GH4KHQsIGojt8KcaHZ7&!{6~^t;iV z#%#)rFv2%j!j_NHy23xR=@EH*?Ysump;n7hji!8T1^kZS<6O+$LD!z$cReS5Xv~xR z??U~imzchm7Cyt4{UX~T2?C>#9@;`!d!&|V;W-sr{2L@&%XNk@~Jzu-gomC?^iPy z^B3RgwHY^_VFQNKmU-@M|HR~eiJ4qtjxBp^MZwExtpBU+y}ntEvzoZgtJULf`P}V! zbap&`A4kr?nmN|~-uo*5@*91Rz6bJOtAXFgfdiq;E5ZW<(SoC zuPnR4R!nRc{U1wm6Rf_!f>Wal-YU+4I=klo&wA9-`#K|sFTp2+d@GGOrTq%-a{RHb zyo2fc+Rwlx^)L17yU3SM-6eIui{E(9KBM{fddti!9k+?IUCUf|m4A=T<0hZh{7W@( zeCK2RUwQ9&uko*3hdbqVm*>z`arj-t8CbKzsJMi+_IKY``j5Uxi>85 z%l$+e=VRSp{PFL`JM%Ga?PGp1|GoRQzG}o@)P65vn{@RVwHC`K-YU-G^C+DkL%HQ9 z_J_}wTxvSP%U1Mr=3enE{`6;6_^!J1&;jP&r_d|aYoMO=sVB-iY<#d<~90$T0T*YJ{Ps$OZX;Tof!wlv+8S! zYqL&_&vFy{125`kd)Z_QtlAkmXHvzp_|y84>)em8N4)!YuW_u$-S%<*(7v>`=$0^| zd<*6l-RP&Z--5YCx8(0`@+~=S(QVCh3+5KxJNnP!kFs~4|M`2`y|nfI&(PbN%Na49 z?PF^VO8PxD{QrAKY55E^_ST~I za|!#WnKS+gOjI_C7~sKK#6ej~FdB=tNuXIcud6qt#A&+1g^CVQjHq^Pim`E!z_R(fPN; z66?`ogHE*7p0idOF{j^ zWA*Uh)MyE}W>Y(JJlAWjK}ioR$6w>IDPcxA`p`A}wR3QypVA(E=oEQ z&LW<4$(I@T-QNq9f1Lldr#?>o`{7Q!#|WS0wB@d2?`R+GGo2`lHii8n{=LBZJgNF$ zbUx0psZn&DMtwi4@zGBEl77@(^nCrYi~6Pg0@7@o>7FH1{GSQ`iubG--o=mi%Dd!T z%hT&|1n+Z&^Z0UEiDwQGE$JX{)t&zTRDil=lo}mLGrv*RdZ)5$)g`CsE8DkXKGDDG z;5mD)@VwJyv%h4u=gZyC=z}%NJQ3#v-YZ2p6Lq=suH$ou%bmxsD9dH8+rqo8c(?I5 zGv0N@M=bWlnNf705i{=56J@z9>aXqH7T#^Lyd_)H;T%k2U8C5EmV6_3UGeyewB|GF zIwpQf)OC;WAMv9P^70%qo0`mR;a#8H@BZ96E52-ArbbTrj<^f;*x<8Vw$_mzjMQ=B zS++;t&yYcNw(vhzBK!6I>zMEh2yBr`$A2+45M%fZJ!+LZR_JB_-2 zb|$Yl7JnZXBf6|K*D0TMIjRTZXJO>t=ZY4dd1H;1u-5E!Ub2RfMDdeR z_g&(nonwo~r&HI7_P#Iqbh+z`cWmcf&+WzgncLKOw~4&ldES%%{qF$0R?ho(#g)9r zzmQ~V=@d#^?{aE0e@Ym-Rovn1i{K6{Px=xMH>?`^|3b*L_`if~0 z{@^noQ5J1(w)fdzIseFS?q@xY?t%Du*}M074nEE`$f5E4=skX+NBfKT_=U#vqxblQ zX8t&Hy0{N0=_g?Y>{W*B_;bUx+RP#--5YCH~J~yd*gO1$(@eD1OD@gp1e z- z$?U+5F^tML;zU2CeY6|3FZqk|QT`%M^i$ir?K{o?%xugYyhmR3Ovk^|t7577_4wdx z`mJYRIc>GQLXSA`VGSf0T5Px9H+8QZOQI z!NjITH~M7{Mx-s6*tF>4FH$g^qT~0yrC(~gVB*95I^U8L`YpQnixiAVTQIR{(T#rD zgAr*9CN?d)_=94bQOAq?)1RNQH<{I#-FTOLdY&`>bKe>18~TX8iv2e4<=lIj_3p2C zHcLxJuV*y-J{c*?d?G8wySE48XE@vABRk7v=m)xNmjTJvhv4TQ#o5HnQdZFMCsT|% zTlT_R)Bn_2)V`O=R_zz7({$oY&pyV0Jkp>y=l>J$F4X?Cp1HGMea>1Jvs#HVONfqm z#>ZJ>?|y4cd;LZGr}_7Ix!cm+BX8{`^ssYZ@Ueb-_u#hDz5b$ot6%h@{oXyeTj-k4 zJ3rf6Vxte4&qSHoc#mwsnL6jagFU6z^bh?+kI{3YPswG*Is02ZP@1Xf^^9)$zS*Li z`9fBTb5tMuXE@{CPj>Dy*A4uS*&mxl$5-_kf=#3gK+(>1YkH&3GC zi?e*(6Hk=KJ$ehB^Z$+R*Y4#uJ$H0WeCF0Da~hO+Fr)Dq9%qPn9`{jy^gjBF`_cX) zUi26DQGfJ4`ipz_=V#nCJy4=+z2xU$xF7d_1n<5w{s^CrnfY{?>mI?oFXy-VI{Mjk z2EKZcxr{kYl=sLpVwgVe^Nu(AF6bkAi(cay*y9gOr#(2mHtrX`7i-!*ulGx{HN8-y z{(aNK*RVM&#W%Xw_zXvy`H4F0GhJlpWOUJCHO(quEJjWGXX+_7B?AroE#i$c{NDX% zeShTqDZ53!GnV^(PL8+}o(pS~IgJ^xME#k*b)J~|fBWl>HQ-K5caEGMfv)wG&hMD- zjA^|Ok9ZbcfvFzM{?1?Tl2^nteZJoJE^|rtJAjt^#=e)f_^9dh`o*q>JHEW1&5!Geuj%S+Z7n)lt2lSp`K-l9GW=n(TjaNQ-jDXW z6JMWs-y&DVHhSK0`gFM%-(5=TSzVh_r=MT# zdqF+Q{Lg#jtK~TJx&FJH-tyl8sQ2RCptnZB$>^G`&ZA%UEw<9h{32_?z~7|T_h#=M z)aLD$9vG=pi={WetL^lBsI4VFt>S0-L1#aQ;U(#h%>Mjwd46$rAGKF{y5BqHs*vW- z`_!QHeDOIBKF)gDGaEW>nU!Xb-K)oHcjkAS^+1%-FD<_VsQH~|$NvuL-9LwV{`vg> z?)8}YANO2l-qD<_p3_3MMwzX9=o)s*d5>RgTJYC&!0Mqj>?`}7xUPyp{zv$(&EDjA z6s`Flk=vCR9}kGiA4_anL+)_ml|{hA%;CBMkgD^dJKTGEel{FG?ak9+)PcXYH2 z-;o~e7ku}NY%i_N;heeOJv?|dx;LLY*!z1@#xF5yz5RFi(KTR}kJRsE%Ne>KxA*gp zd6%=FPwfHkVR&XYQRX?P%zx0eS?%c0=fofX9gy8A_g$jiBlHkGRic@nDLZHOSDoMe zv!nTe*+B24?_Q6Y|M+LA*Hk@5zj^N=i?lS~Zk0#e*8NsLCH+=^QLluzRUUEK``w(u z``k+mE;~8~_a*Ox_-I$#kIv^xywW=p@+CH{;deMOf7Xs`9zwci1FAH^Q?eF=Zc&-op| zk)N3-+@a8ITvn4jI`Hp>X4Vc$6 z${oeClHBz^F7Nvu=UpCrkMpzg`&mJsLYdqA?{dY*d2j9Ae%AVTt)E@sL`=rO1 zy-KsG%l%oohj+{^{2rffyTzyLI>sK}F`eIYpIdF?{nH0f&bQ*W>VuCz(N^1Dy(8y+ zVfV)M5W8RhyLkQ?SaQ&MTn&`N*>Ugn>2v!C{xkpizyJ5Z{yn>QkM!ry6Y!M~!->AK zeP%aG=KlPSj^;}}Ku>UfR2<;rPjxuTSHvCtdw_`lqWdMSKg)X`nJ98(zIQ&JXJ_a{ zimp@B+0;V;W0URTvrB)3&ur!gS7&j#EVKR0WzPSdciP(v)9&YI$otl3HRcz8Z*F-f zpY{D&-&d>0S`W}K^pB|bRs1~w`jNUleAJ11^q}q+nP{}j_O9=Iri;w=+_!kn&~2gE zIK_^8VDD7=?mybQ?cV+bt3*Ant$Yb{i+s%|v%vfvz-vFZ_V)Q+d(B?VU83r{xvsCk z-g|Ae&t`9)J)-mmdabyvdf?&*->vqe^|t;#V6^`y;MG1IOZ-1(M|36EVlVnf@>9k+ zXC;cB)1cctlRF0dg4Jx_ql+J>Jvg$-=6J`=_{_%q|F}bwA6k>?=h&WmQhmK=vzdYS z=azTwC*?h3?f(qadVzlNnKRC#-g8mU_gO#sdB(d<Vep|%nk8fx%QsT+3dgf`22C- z8D+Mjf1J`=-gA;=I<5=vdd_nWb$ocvpl&Z7xl?RKT`nH`D0iF4M_u+{WpOx0rk~kJ zFI(7);oV=$;~BX1p4C(I zm-=n4dsJZSJ@)Fy`}ZC=^|4(cV)EA_6yc3 zPLIw}zDo0bNnVKktM;w6EZ`hcyXotgp1f0CDES|MYY1 zGj;T*Y`IU?>g?I#djVU;>Cwr2_u$lQHqQKCc`CLA*Z&)Uud|xJd3H>%yklMC=Y#iS ze`n@BI=*Me{X5q09dnNN9{2Q%_fVX@V&BdEH5KUT-+13~&;5&f5AbY(mNMoXTkxWv z(jNUa|MJ6%@89!x0P{Px=d+R7sFzCf7yUQsH5JJ2>j>G}{m19e=om`=NBfz0_k3=S z{&X$(%vzmUTYN8It2k>q_;EdCPW}8!o|u*xSi5Vm0Nn785hC)PFQfA;=~HLLZ2&b(ik;Y8^zpS#Yk_w@I(0zLh=cK@Xw;QoI`@N5g12_qSJP?f#XI^78-wNH%-@w%A`e`^$Grj0fo~ zuubdA7>D^D^_ih!!u#Y`V{GxgV1404eLt)5nO&4=?-*<6t~-eT`uF~yu`IA(>ql&h z^wIlR+o$LAit+m$@&3&_x6`BEzpt*T=YIu%b^bm6YcrUi(KGZB{gu_3vsM~0TJ5Bl ztu6K$#uocK`9JFZxqH#A_m4i*vAn+`-;)b>!3?5zaNgH@@TSeP-&u~hKl(|%MW1DT z$NX2L*h9hTrQpMlz8nqI^&?Ni_Rb3Y|%M_%F%-R zwZi^aEf>Wpn>9GB2fXol5BGO(@w|VFmfpQ3e`2qC{#Wo%tXZuG{EWV#pXe{IGcr;1 z)~MS!zh0^QX zv%St&0_!aHxzuLWX#{FmcxPSOePcm@8t>Vfb?dK(2*Qv#l{D8TYVxQp)i}MVZ zvjSbj-z&4yU(&D1u@x=pxek0ucZ(b>(URTg+1J0JV87^V z747EP*xIP>taq+0H4tS!<6WG0Z_%UPzpt*n=YIu%b^bm6TY7=sp_ican5Y)=NU%y_`)Hf# zAd9-o{%fHlDk1b!VUr)nI-6JMm7mXa{*b+~8CdEQ{7O7ucIY~7vD>3F+Lz;rFU9tG zwjBTIGvAkNro7tM#ah?urn<9gdar&)4MdsEct7X;yXD@x%iq$Bd$r(V7oG z>&Yei%*THN(1SZ|#(!r&t)`ko`TOYX@BY<$Ps?e>`&xa?%+5^jl(R_D(fl9Jw(eW| zDSJ-4ZFcT5*Zt%d@@Ki8^Bgbq>vQVocji6jPyREVZ_*pT5M=v_^i>;)dw)EZUW_a4 zJpRwxq_y}wTSEP_!alWGG^c!U4g9{|xxQWpW;5Q;c^CJ0>>B^XI(q(B@K3B+tp_-x zcjzT3J;wZxUPi$hMKd2Y*(kr@)O=-U*o~r@kDmV95>MuHi)@Rpo?Noe{G9*i`$kVM zMVmSQ&pwJhakstZb^V)vewgijjz=acIeXXXZZ_0cuDuzyz6WzX?K zH~GT;O6oCx@}KE^lb-lwExT`&@7n0-9e=Vqmg*2{H*0xO&-k5{?&=-nUI%6~W;Nc^ zTi?N-STE223jT>TYv~7ihhBoxW7HZwr!Dw z#P{I3O;0ZLq0>44*Wcs(-&dFTp1XK8n>qj2`{>o~`-%DZ+|KU+Tn5j~&+h<`@1?P3 z{v3vvPPR(&l5X&|Yzx4j03Qm;e3IecO{Ubs&u2Q{WNXY=$_{>^(a+oMvpQDlVx{}; zzfa-Y)vi*fS&I`sTi(0OX3T2-p6>72CH{$b>G@y5Ke1+|`**qb4*eubk5Oy4nf%{= z{I?5d)T5{W&WNkWM^7&Fq0>44|9qz_&G@bIo_l!(n>qjg?4wt>(^t!(b$+k?ZlDz- z(~I@==kO>#`uN;00daJJB2b4aFtH(3(5rSykwjX4_1&Sbypvz)z^EUROs1|OyK z8XEsrb*t29*5Rbj5WUM>ChGGVb6m^&_!IAw{I8-^ zHl=fyqnpr0^u4h8mV9x)W)XDnN|(rg#r&0xF}3(j?H4OM6K1~NMpmlfQlC-{e*9w= ztE1SG&6LmD9MS{oU!TjE)u7C9E${qK?BCMo_bmAnYu3^W=%|0xOWtE%+qL|`nY4fY zU#s!f^Sj=~);y%2t#_IKTjq3hvN^w121beY+}jgulrJ?>Pi5=MJh+dmVz}D>Re9t0 z4*8d6{j=oM!fEz7LFRhmYb)x8{j$vgJ`TSB`1Rgh^Ld9erIq}AXY|R{V*RCT0@?X? z;WK}4AuH9eR1<0!>1RFPqGqMqO!@hrBlV%rX3YK2IsdCd$E_#!eCzy2@A&*5E7_WV ztIw(T{dd8l_Gpx@MpGUW@x_=j5nb zP^Z{$SL8%ok)9Px^vnJ%|5y7V-$<{B>x?*3vQiC~`jl$$rkts=w0jElW+WlGy7|1+DmJ`Jf^$WttT$>B6rm;t{&VyHpJxj@3KAEEw=89y11+bmBkx za{kwUFQD1KXa3h6XDhq=sF&$PnQRtWaoPE=_ps>iZu=#vUon>wCHB&JCb!z?IP>*Z zGHM82smVu4y@tlWRozzVG;49fXN#WYcL4moKa@GH<(>ad_t_G!bgo`4ao(w}ie)Wk zdPbB!qQ7FjdvaUz|IX(<_1tQ6*ZOYFL;Aj$ZS`KxdwLJ%oo&dA*(0OWp+>!KHNJ!q z{Sw!mYu6JSc`09N;q{Wd2e;S8uH~Iyt$g)QOPsDl{#Q|trG>w`zVcs-(|g8y ziT;Xp?#ZX-|DDfzYT09R*BbZa;{B-kjQ4Wh(|a)QYC}%sD9TJ)qF$dpd%*yanrtI^-AgX3zPVx%n&(pUvr@0G|hr4UY|b zDDoObw?x-$KhM)US91Jlo;+G>x@;IY-QHT7<_YV0phxbzEdFFel@SHE3UZ_19C& z9-A$3kxLKV;zznYxp*%!3-O-bgLhXOa*DHu=YIu%hZ*STJM-ErUthY9qx}otV~|hk z4y7;98yyq4_`xp}IeMf0UyVmz=!xxL$;iHBnxTpT8zMdQP{**m7uczL}yT?uci87-x zv+>T}`W}8oEo-q6e~o(nSMasi{H&DcU29GcIiv8<+Xjp+_I0j znSW$X-END&o?7plGJckrILx7v^sbB1!g%cAb@ zQ2SVS<-GMSd-QAfe@6YU_TN*pkv4bDKhEhR_wn}t{yPBv-N48iZ^ZhTdl#EuOV33U zp7Qxz{2l7&jAy-ScBG%uon$_buICQ?A2m`=ieGeDhxWk4pY@GB5I;Mb(RerC`VJnk zkKWg217dedUQT2FXYV~0=RI!tQocvmIpUY%kepaO7x**hf8r3u{}$@@wR}rwZolT2 zy7*^LHei%|M0vcYqxR8nwkLMcEPu~A@~$uY3_p|gGZ1r--qm}s?rfu+eD;rX zD0R4kdhPigz@2N^6C3p>SL%G`{9n3%V&eRW{wII=z#lbWf29AOI~!?p*ZfD$`#o4A zYrGL_QP1bqkAv?&UgEoe&)O3y_w;=wKj*koPK*BS?#^Q+pa1zk{#kKq-7el4(`(;} z_oH~9LHr#)d*6<-GkYca_Pxhlnm?E^V*Yp76I;wfd(YGPJJy~&pg!kO=bpGd?~M1l z;T!5Plz4OxvHeh9bqO2zu z{Avbyr}=}t??T_T{_44&zTIjQ=g)ZWN7w&q|6A)dTK}&3Z=KosJN&J7CH>KL-hj8L z{+|Coge!l<&raO0n139XY)bMPX30i!?4nNZUUy>|E{7lq(IHC)%bBd^*2um9)2 za!)_5F?uewJGe{j zbq~bsd5EXCL|Hv?`(0-|{zW|o_wO+fizqtSIacN?lerBzG5?bP7=vSFzFgK*%N`r| zneiU;-uz_`UgpbXJ-KlAN6$Y!v5mA@n?I<-RrFo!Kj;5H9rv#H&*$x`{7ZBBS@Mz3 zxQ$rp9J^pt7x%SBt02o^W-ptPyoOh^(OC&Mvs;sm##+N8Z}Rxu|Co~-V*a7OVBBV} z*?-Re_qmTd^~M>o#LrHgfBgMh@8SNw9r>PkqyM=-w*ozP7Vkaonb$;1^B-9r;a~A& zG0@xi&L}z_w`0XzOMABg3+&777Zfml4t^Zl`zj}s$-y#}UtiSrDr_VAQVXgJwk^Zxou8`fa z{!1M`R_q7lH5zT=zGll=$)+U7E~9HUI>#>RvgbUb=(N%d16eBude~(Yok*i@Jnz{H zv&Sy8A-?Pknd`tS1ph&|MqU3-{QvjQ|NNJJmYB4DC6YtC8&)&UxcjtZD z-?1a#lfV0a3QtW$pT2tTEZ%#zr(Z;=7yacmyV}3ULVP}ZDt6B$%Qcg^O)dYC{(EZK zW0U0@Wmn|yF>Li8=Z~Jc+}UPr{_;LlElNK=!<2Zpb=(^NuJu2&XZ*h6UBvp&`G5So z=N;?6^j|v9NnTsAe@gorhR!m(n!JWr!UE52GhNBhEuD{$kMvQ);!KpaH~vvLilYS! z9Z|FSF(#?xg2AF#j;GIOToX-?1a#lb8FyJMZ-<@psQ5 z#@BOa@!m^&dWKrjUmn9z{_!nZ^G__q7H5CiI#wyyD0iE){P)zd*Cys0_gCcaF>LkE z%&K>E{=TxeHg~flALT}#^nIk9`>c}jU`Fb?V2Q3RbY&0b-RjsI5BVrJ@}wTUm_2rW zH~1G-Oq>yG4}Q%CT>N-XoZ-I~+gWGiF6!%y{A>01Sm2kM6c;+Jva_r&A=@63BG*7!T;5o7DQvv}{N zJv~Fc=r51qDF65tjs6vfFX9qK*C|+~JTo~qXZr7{Wv@+^v&*i?zt=y#5Z}XlYjZa{ zz56qJ_}oR_P@TmZFgKK_=juB669pGPB^rG@_LKQq{m;(J??w0EHP;z;I65wH-9LTd zGW16KzYCt{dWZU#K8s7eHqYiItde}Bk2Nfv`zposN^<3^d^~UXJ$Zrw?Zs-bM@Q6h zN$2F2I0VCw>mzHSZU^6Dm+8#;|NB0&`rKK@-HYBi&RT4q19OniKH~jd-run!U-MHs zbN_edV@+E2cg}~{O7~Ke$9pgB=^JWAf8krS=BIR4^5^pCJH|jPPSJ4+j;Q0W@z^<* zc=s3isH;6VE&faOI!a!v0ddEiV$Qygx?A)2cv}5?FZwK`IpnCiosFwpm^+kXX~q0| zYrqT_{nmUdrs)6dIvwRN*1_w4l+PM2z2MaEtY*9C%ze0T@qV_)#}jO0?q}uiH6{k{ z#aHH+zWA|5>6AN~85B+Olu8cRl^x z|NQ&^_S$>xCcnRK^lt4voOfw|zs~rc`@sKQX*QFz%I=zz$JKLZ+1*DOJtIoJ=&>{X z+q9p_K-SMKD`oUpeyz+u+F2R*>MNBkbFHCy*3-OqjXU8jVpQ2B{E$XjT% z^?i$d&{_R=4MG1ahbjNQ&smGUTF|@xPVDc@yhn?c-nEhU+yVaYM!g1m_`Bs5D-6UCOIf4o>EyCc|#c z4}B>5j`Jt^GnHGO7?k}Cp4q3GKJvxna z&i~2%G}dm@olVU8=YaW)cV^z9MQiWcob}Y#KR0*csX>W)ZAvBYB_V)Zo#_ayrV&L}=vKbk}l~|sCtpAm9P2a!Yhu*Cw{`}j+FLnD^ zdobKqV7}kW|B3GRpQCU`;f%C9C=Hv!1&gY2&eokNL!WeIIpwA8}jv zj#1+ITxc%y`S+FQ&p3ms1~LDUeIcwk1IHMm|97eX=sHilF|)xN)$w~!;bGW8mwJ7y z4%1&Iqp_j`@SR4T);$=l`cV&ir%|VM?-=4`7iGS0#YN9;BahA&3P#N)>U{2RGs|VM zFEM_^#~J$q^?SNaUWHg^d@uI93VKwO*-Y={{=VG$zTML^?*9(F&mw4 zV(7WEmVTf%u^#l;8S}s5B#vI%l50(;*DrQ8?0D95m!oZBOyto^Bi`2inqNK_nm_qB z>aR3=a=*%n{Nn7b8pQbLQAdfWaeeQ2HhkDFQd|P9i^Z)jmZLRUtFVxt3 zm)T4+o4+@=zH@ig1krP6G52gwJz@>$F~T^?e~gKE zYBbu6+}Heaw%1=zEswCt@*u|=&Fn_WN`BRcarS-H`JefleEAG!H8`R#Zc zzx}63;Yz@nG;br5zwlDPa)t@hv)6o0j?nP&f z#`eC~mf4J%&EJ_@-otzD#{J)cCwHe_2l1s^xSrd?-yy#kL(iR+@^^Wu7W5c#9Od6* zc^AH?mPgt=q3-HK&0_rBxMqqYYjP#FawAXXPx3#b2Fz=UA^In-cdLI(KTx+`+Tx?8 z)9V+z8g|PWIK8yR#~bS`sedoNm(Ql(5&Yry_^)BbGxmG@?Xkr_bTk&Y8FgRT`4*XD zMZOgWJJ**?d+6uc%X~+NoL?p{UD>D~YV_Fy!*EB%_hcMu%UmYPyei5Z*YYmkQwRLt zfqLHX^OfPT?3!p=KTNt`;T~UUZ)z*J!Z6K$655tp7S-D`9-#7t8@HC+F~mmd}Xu+ zBl>;g{gV3k;(PgQ`W?X^Zjb+nrM>VnIkG+e_SoVdIvNYyjLKIg?~&EwuHj)P9goN4 zUoqNw@%bT`C;eESA0>EF>}SPFjXv-HQTKaRje7m4raqVXyegg>FDH|k2Cc`RG~gFETpiocKATGMydS1%qt``)F#J~yq+|I!^1 zcbvtT*P?&qqx_DDeKbZ^|5%^Wp8E9A66O|pk6&!I_$ldv)k8~|$VcDv!|;}Hzn6bE zpUHnm@Q2&uJ7Q@syiAU)*WW06a76dU0#?L9UZWM8kE4>U+m&?I7z20zS*uy2fqko*GM9-mt3lss{#Px?8%q9j{{Mbfw$9$vPPL@R zRAXQ9vtHV_`kZ>-zZWRga>~DZS-q%Pl-~io>P$dhnxDv*&mnteD|AImz2Z8VUOZ>N z<{!P1->1sSE9Q?J${nmx?yN-D?C}@rnjPmQzsS)m(dd_bNk7W*Q=(Bn?(ygJslBx` z?#29j@k}_Ien;?!+v7iCX)nA?j;zNYw$Kq6BeAl0z%S8~O_Z1HU0$*&$=x>cC7Y5w z+WWp_BRP7`FS6qI;D9sl%_IFx>|^iok@Z!dGn>u%f6o6c&$E^po7rD<&-~xwYwCQ@ z{8=qp?(|)C^!#In(2T%5@M`rSFXc$S%3X`SD033}qbRsF|2o6pyVT$7lJR~wd-x2y zMYhMM+qU@1bR1(3pJ5}T=R{wv?;7%1RKC}rC&KnWLD+8j#{zq&qp=)2@>_gG+ql0A zRw<4cukTAXncVF%zGPF9XBfyzHYGWB8Lipq3>)JQdoB>mr_aOuEcg?9*5FqU(0}|6 zfWP;KGRI|eMU?gQ82;}_V+~Y`m~-4yFKk8cyn5~|<{bCbK$JSrU$GX^cJAx5{FU|T z>6N?N(2tre;yiHo*jwXcz9B#5>dWUUbVRW$QP*wd@#X%-zia)ox>2JVjrA+-OBk77 zWF;Gym;6P!+c=M}jJjTlkA6z~l6~|`kGFoNUPy0A<$IOtSd)KS|KV!7rL%~^zNBB1 zW9wA}c=%h~;N3yzE=_TqpYUz3a8`ZX5X)d)JBfzIVOIqbE81 zLVwLo17-8?|BtQjk$3laZ2F)1jCoDe=eU-+qvtb$|GQG^K`qwmfF5?DcV0brR;vN| zi+T;{uULy3R_~d|0N;z%V~0XBvGEwUb8nN5qYOee$Yk!9GDd5`lsx$Cp)ptu&;@3p12 z-&Q$y7c7q>kMz&nTRJ;RuDC{ElV8L_eg#!5qx_bzO7fcDEjH3!ThSN#lEc?%(0lwm zx#T)?cu)=KU*mnT;*qb>XczZu_MDHh>%}3C67A`mJKE5P%m>T^uQUsgi*h4R z@^;EuoQtT5Q`gr0~6{_qXI&ZnQt)#fb3%jpx}OU1-!n4vpv89^DenY@#gMmd+)Qy05e+ z?@>9G>V=;Y&HSUw8T&|eE*Vtp{Fya4vj^x=QD!uspGxolnY<@%_kUO3>k;RvY>&R^ z-B-_@Wp|H^dUy@Q(_5mx(2w@WBSjAy&++B{qfTa<$)cV1=s`0b$=ttq*UM}(ncKjl z2Spxf^zSjO*@A^$iT3o)-E6!UH4nVvEWli&+>~Q&F%um@Ykqb1iuL!}x72^FM)Q1Z z1?clv{M@%ZYhyJRo7GjaDamVqC7Y6b3zjgXAF+`4V2#8(BCeQI=HnG)BZ>(j(g^*Y<80D$*bqi;=TKx zdD3eJ&phh&D(RyO1w#}YsGp&~M%~8wk{vqubjq1%v{$Y!b6v-9-gPqD7G1Y<-m%ws z{P_JwzNdfgWaGW~s&jzPpvv*C>lN$owV(6+vd$B%(u|L-Qt(^n4rnluh z=7^6-g=w`UL7CL@bG`rm;&OLGX45}ROx?ZvVUi&%!-?j68|FNn%_;nzoQ1*KgM;k|EHI#8_tH@~?mxps=2SMee~I^vC7+qcLu%kXE1sEP z>kOb-py%GB|G7W60zG%e+*hK^jr0(d-V$YHGR`7(-N;AXqxR9S_B|`Zaaj%1^_;KS zw46B>cg9_cmhQW!hwfrST{L^X;{2&P`wU8+KuNdT{G0Qn|15$f!LqS0KET;w#)#%^M6m=r8z`$j}_i?5r5|uV`Wb+(5wbMwd}FU ze7lSoMTuXO-V!Y>*Ku1g-A{=}_pbgu_n!L8rj)znp1;=oiEuPu_W!^cKoMtW$sDXDh@7|*R z{_XGJ*?Tv#v+f*!8TDBKKIY;%--(ZL`kq{%S?+hJKXDPKD0$FdrJ8dN#ZDAF@Yg8% zclY0WXIt{8o=~rUkN-8_I^SFW?CynG^WBpRwYcj2yC>$YHnsUj`78G=Kh$g_edqdf z2h8W(*VcQk>CgE;|NTd;W~0uyPwrQ}9@mBazx(e`$z`e6WmxhZm*PT3jCY}<;}V|d z%f2^X;)C|$#F+NR74i1Og)KRbh%Ls~iuoDCoS7dmGcR}bGWW!1K$Q8w-@V~^pCA4H z-BZh248-Y_9H5@_U3g;4sOKU+%fF|Ny*8QuOh(+~M1N&9=M0LCC^)6uGvAV72Nu|C z{?UhmbAecrc=mS18N8kTS z%$LpVEde+|4hc?ajc+bQqh@txcEXWh5^j688V^*rD`@2h!YDpAix ze9T$fA76z{-W$C z|Itsj$G2$AUv=c0~6&>6~m ztCwHm{a9P)sK0MV{>*-gzJ2fUijVpD-t)hL_gFGM=Hhw6 zpILYL!tWX-cJiXXyw1p+qAv=D=$gNHjxBijDEUXvDHuoj@4dU2YqlqL^2ze=@t^sM zvgkX;KrBwtaSBdL{d;opUhsEu-q(9D?`lI%qRb$oE*GE0?>cT9dDU?3C)&gqGXGDN zQ8#i{ZG0g|uT=j%`3n<2ck{m|uNGVCTFSo#W0VfD_V_s`yP;{Z-)O=-U*olILkIX+hk%E)?sL8Nf^WS?%HQzcr z(*K%o_aE!2J@?==)02!mT734@(tClqRqy1ykN05S*@nC_+7h?hF}w6o$8o+Th9~Ke zn`%UVdCzeUJt8+@b4PLh$1n73|3~<9O!6w#-*wO6iKj%*h#~mso}m_;U(0)Y3;zpf z5$v@Ga|=$?1*1fx{#pAh9(>0$0n=?vS~cK z#hmXsE^*%?rWwaE_rP73-o<(U7F~M>Kl_eq@p#i2+ zqKue5KkqT>Et%^?9-N53w#Ux#GXH*_@!+iai|5!mX6C>5{-VFQKhl5nmF+YCUW-;9 zoXkg*_0*C+glaxwR^lDK2lv%&s7<8QPBfln`&X_L`BIZOd#mTp$X{`kVt8f$cd7pw zHSYDnbFYUkK7Xtp3~YV?EgR&mG}`;V6(iGgoQ%(G&X8rXAd`Q1r?t3Tuf}H>HT~LI zhSeg=Fk9?OI^dPg*W}n|)NM=WCO_r-VDVS=<1ve0)1T+{dfvG{e%H(=yqojxEgIjo zv;En5W-%c1*x|{`spo$M@3FuWuT%1h^z53;UyLE!6FWIU=`m3)sKXtp_rg|w$>+z~YVZ0#fR>G)weZp2_bv7% z9mmP|lHJkr7)$gQ&$E5WMsoC>M<$A`%X)dRw$PHVEf}sVJl}8Ox5N^4<;P^FJdc%X z?I-45)17DaTHd`re%H)Kyr1*_-SX}&ohx#!#YX%!>iJ*6*J9(Wl;aikmd{c=E+=++ z2}+N}`5*m^I&Lcu4t}9`^WS@iYjs%5pI-0rzvi2BVyIE>rKSVk8tuu2Iylvwz~7f@ zhUmeaHqZWE9C3f@9r`GnJ=Jp~{H$Tr=HNBk5$9L=zf1i`)bJ<_f9`GNmzaL6t@f_} z18CX!SqmTSecxhV(s7)OFWDU_Z;d0=uWluSeT9CJNepsH|OMcHpPENYjcIpu3okH zvV4%8U1Rr03^ht#^b(XFWBy04L~A~3XW-!%dN==G*DZW4cb%=}Pv39xiSC+jKPQe} z>R4+$@ok~h!Reg;+rP`U)cW0Y?=*jwVma$huJB*->7Vtp^1rJeufG)weZnC?prXTPQ=Le(WZ3|R;xaG*hR{Dq*2fJQMa~tyNu6lrB{+?7|2RCB{_B( zE!ns{+WA?Dk2Y~%vO|u(_V5c`oy(`rvunitL$AJ1;t*vvVpigveeJ!Q^DA>9h7u*0 zNIm~6_)@$j`IR-scPTcPQ)hY!N{>-%^hDi$FApw$p?C97O}Emu`n2Z1)vxr|d}rsx zSW|Ng>1BqCoKFXp^Z69q*`x-{+EMk@PqCDF9KI*RRWxJ*m z&)r|-qwd;1+WUUZHlDj}9S*(~XN&q1+1 zwf%=-@<+`kJ4+zGM45gj`!Z-?GJ9kV=08-{>^K+wVVG=O*ZD;Cm&ow_C&!ZQwf%m~3?wfd+%S3%%V~%Kfr@uo!9z#paFK3tbGP|3izmi>=cZ2zrY=Q3ND)vbS?x?PF?Y4N>_ z`rjvIF51L>+4B3O$L8DmCl|4_ca7|P%C=R&>zVNxn)D<`A3oD{8Tx_F=h2JL|7w>z z{ir8>E@M`MGDEby-`^>()>!;Ac?I7Z=biLgdI23~Z=b)t&gdOM>0MFpara-V>)x35 z`p;t964Pk^TYO)oo7Jo)^Im|T^Z&iRqiXke=d+giQ#xnmki~PBZ~4jmADMsVkIy*t zNdIfKmG7(lFEMs6YUuP6?1-NJic|i+HaN5({LI8(teTHfdHlGBzrvd`n0 zvMsURMYp9Fq(kq}OaAxn)VLL^M-Ts^^f-Q@tu=jT|2?^1Y14YA(u+&8KlA_9@8?_c zn7a4*PUSA>AO8Du^;KNx96bl(C&ogoN7IsjknU?OYS(qXX6Je>d=FO5COfOe;rcbbEoZ{qqF>X!D^@M$?5viD zpNZGXFkFVthw}FyM|^H3<}uHsm#C|HlG#Xe@hz?$R0IztvZ!ceVd4rn6-&cN_bG*7CmYf zY0Z{gg3S@1n~8bM^XMh&N{=!lF{?qDBU;|wmtKBcP00T$`ox;8^#VO3N*{@yHUCHd z^mcZ~J@wz3f6f0IR?qpC{Cn_U$;Nwvxyawid-`MAJnwtps5@icV*ZT(1|ZIWN5tVV z5C?sKgujSc^M7>y?wijyxr-j{f2|f*`(NVgT3*A*FMz|}1EUY$lYnIKqONl%-eYXx zYq7h1O|Ru_B$oVv*W!CkXEfHDzb)sAP5Q(LEn)aM{3jj$xmj>0=Bz=n2k22|6rb08 zj%a!3e`2qZ|5fyfHLLZ4_l)<_S@VD9pWe>yxTpSG^RM}@VfCDC&A-RTE7^EYXg)am zeY~gtrp5XaA_dWE=b<^C(tmm`0dN1~zbVt@A&Ybe+F^u$;VXgT;I)BA4_bPJ|Gn?L( zc~5SkPwZ9C{|f$zHLLXk^A$bhl>YKMqo2{5pVApP?pHj%MSbbb^Pca;ZM9qTe{}we(_=Wh{?y51pbyCX$opUOznmYje!Q=GzqRs9P@6E6gs?rC zR$ps+V6N%4o`W}%w)(E=5l>Ago?%m>QGd%mVs5dGx)DqJsME9m#Qplae&+ldjjlg2 zM1J)A+|PO(-2>k9%t*`*dUr0pS7-90V?6@j^S^>WBB!Ga5?i{_5l6F+~++fp1ym>`Tywrbr)CGpI-17VjmoN|7-r2^CQ-;IVSdeZNCJx z*(|QJGtA0%nEYYavXIuG*)w<98wAf~LnXCn?q!aN9?UJ&?ZuaT(+heB z=RI8S>OFliZRYoMsRz_k%zf%JU*3EC4xogyRbHDr6~k7%t@>;J6~mGF6Yo{^kN&eB zI5Yp~e~B-hgCyUv(!SDNudT&rw=LK)ZsffbyqY~a8O6@ekVWb`Tlk2xwy$CDv5mNE zShe$BtY}x-*L>H`BA)+UKXxrLaAfnzr@gOYx4|zIT=(BofA<&p7XP)}bw-?y75$X< zZbRNsa6NyI#m~h%R>@x`cRPGTqYvMs19hCtPf6x>;=vPjf8tqs2iLpt==rOszoyOW zz3(u8GJnx`(4+cqZT>#G*Vg_qRsXyDU*b#Wf8C4DI@j=bXZoZ@iM14a&9-*t zxKWR@Ei_{IehW_2b(|U>?c#oq?G~(>ADwN*iu$#E&3DNTn|SWCzIayTT~9jT7(e`T zv-p^pjWyujpzlySa)s)h*x#4^UApwXjr@uABJZo{6KlqOmnc2seH3S}TD_UkWCNCH z$zPPajmH44&wV}hkN)C*&3`R-oe}35`Dd}5;h#IeSESsV*D}_*w09fvU_~FE`%&{( zPk&9D*L!EVYW~dTZ}mFs=w4g%PaH>o{;sM&^}V})?!oC2d+$18<|zK2LNO~)VlM^F z`6ycQvnH?kU$b31AAy}=tof~-XLyp8Y|zinIdhunt;yUTe$A$GzWLw(%O`loAMv@F zmv9>l=EZ}It`&tB~RQND%K<9}rSwOmU1kMw_a{w@BggVUvMy=z$S zE&+UCd0dfKkdhIs^T^PNwmo~t?BQ!Zd+@T{T5w_(*hmC4)&Eb!f@^PETj z9z*5}S-hu~eaXMa;JM&yjk?X={2gn}FXwL4;$QPmoPkEp04;Yhb?)(e8+i}e?A0=V zkLxlW~>+koo7$qZE=aGrJ zT`%u=d-%+MFJ2E;k4-7Qky!FE3a^IaXQjM0$Upj>`JR~{A1+sVqzJzBKKYL`bS8bq z`vCX+d5@2}Ly!H6`D6C@GkVRatNHaiYUN|?vl`%AwC3N>+=e{B#-~%xYPCPozhco0aO51&dM8uwdC$b` z&a$~r{go~6J?t{fUKyCBe*^I9{!4Y^b5C8UQ%S#94laJY55yx!uh&n>4qW`;f6*T{ zZwc#j9q|tQ0qvh1oIyEbpHXx~vvbK@$N7?eue=mPN$!5o@%Wr~-N?J1^HHy5@Ai>z zv5z_t!}n3Qws*V8V<$QMPWq$Yecp#(@PFvIKZ_&kyv*M3AgRTp^L%n{9=rI`dpPg& z{*E8{SG(7l&#!0J$LBQOdugwp(d$3QN%eB&85E&1a!t9xtid;EJZ96bm1+|jhjpF?-AOEz;c zcX`jjGY4WfGCwex|3qUf_}7{Aukuf_2h{+HOg7Cq}!K)x5> z0$#HDwEu;X;kwNE=)ZQB>2A@F_S(xnF7zaa-$LCkVzU<>d@J1&1G+VAbhgk@cr~1O z))HTjO@IV#zvg0*Z?bFfqk z=At{LHxWwQu_B}a3uU*3q+ST6ArOq(q#6q%f#jy77lB-)LTE{PWOK+xd!p4U(p1c* z$JA22D9i7ebN$wO*0aX@&Uemle&1UE_01Q)^^Eb1XN>XAcg}DB*Z9`@|F>WFTfN42 z@w)uJVCVPa{=IyDpY8s;c>Two*Z=p%?|IASnbG{F=V@f|-1+S3^Pg(A`X%rEy&gS4 z-xr+FbEtK#{@#1d^>2BW{hG18|CVdly&%tP$)-nzsg+NiHD|x3zu$k^`hMKMC%?oj zu;o4-IqUyB_0ngSGat`ZK6Ci_-0l7Bu^%CGD7}T*fA8~K+kfxtudau4@V!;KJvi?{ zU(jQF^r~uj5Bi>TtCv4BBlc2*s>V{zQoL7FvC;e`T`ykk0Zw@%`{>oD1~t#crCwFM z*R#?uaeLM4)v01z_O+Cg@v1M}z4lGc{-GAEROyk5r{Jh?IE@cp@e51unt`o}$e z9ryIo_aX0lSX;TpFHX~dVkK}@zQh9miu(%ynFv`W!_@e8kzrj z_Q>7)zEc;x_H$2PF#BKn`tv;52bl4_efqR;?|xY?`<~*fm*2BdyVs;uqnFc*wdy6` zs^PkpW`y@TJzKRD_nPSwOJ4q7iymvR*LzJJw0qU-)oI1pLxo#%vQDdp_04f3A8q&z#R{)DkZB^*DK6 z%WZnGAAF(fsPiuL-|{^BwN^IQ?`Jz=lULPT!%=6Xr)$Vt>c#zcWKPrErtg=xoa>RZ z?Y}K=<=lME8lC_33^M=n>|Of#%jcfGUb-*$+55l5^|Ox~#?z&*TY1mU@zuGNziwBL z>Q=qvA2S}U5zV8zZ)L3|->ac@PPTaG(4C83bB*2VyQW5JtD319@mqB&zQ?IoD?YLh zG_4xq?={Ew8pme?SA7P0&m=Pu-%AVqd+fCJHaT8z`wa4)r_Zy(+@)t|bn(2AORc5n zF83hcbE)S@FW(y+I&-awJUqkO&tC;6z-BI6bR6Tl}=l1&f z`D5q&f2~<)%l$iYUeAATedfnzo)+?4@tjqN<|XHU_I{0LE6?EC*ME&`eC0h(+grEt zo?Xgcx2s3>+WkCcKA-bEw$@&|59#UQxo|?);JbFK?LFSQz4eT$yftT4<-^_AIp(Kh zhP>)C!1LEa<`ll4?tRaFdWNsM_oLtS&%1a371MLVyv4KAdbW~BuY8_2;Zo05+-tC} z;6)oR@`gIuKN1}M zoUyhyeX2ZSTd`HM$J5I-&S8VD+iFuwm^D1M#eQw|azE$zYtN(VWi^iZ>6js}`V3Uh zqjSt8{=IbT`)%>3XZd)&(KD#-yFVA$nzMLr{8?JE$x%MFgjL^B&g)dR@9ACdzvaDG zor!a!=iJhHtu1z|?kK<1)9*jF&q6*A<$Sm0ypNoB|Idef4q84lypU&#XRL?UoPXKJ zHGBGdfc}mzwHM#l4cVtl-FNxDJjZ5NRSjY-Y~>{H`5BYHChpo1du>0}%stVksbC8nUUuU4LN(x|LmK+3Z0|3@LK(SFMIai@_hSVIG0PH_i5Re=u6F3Pd|Tb zX0@C{T5}%!mh)eeL!S{`Q+KWQrTUNbMYHyrM)cIK&}aR${dwXU>tQ~3BYWGb(LUIp z_RY0zt+zI>I)`gs|Jwb&LH+5H=DE`E#YArW;&t3^m2-{T(!1|=tJVnbb!3y1yvL-e z;;Et5C{{JeZN+*$GlyD9(8}?;TkTfuQGRP*@<#hqI%=GQRoyjQ)!Uze zp3QS499`F(f3>G;_RDjx&g3Qcllyz?es%tEr)%G|$OlJ@pcg8~{j^7Gwscj!~wxz_bNpE>gB(L$b? z#C*PbzH+Irp1!BOZSCWl{c``+IlSb4a({2#ug)LtbnTlK`NFjYJjd&#y|!-ZvtG+5 zuN4=MW-EH_tU(N(ejq&9;)z#Cj~-TeOWbR&SM6=-da8I;2d>gDa2Ne={i*j+|2Tsu z&j8Qr5;7m5^Unc#-~S)a+`IXn>)li4|9mERHr66_gg*OUYyQ7h|LVPRX2x^4Ua!&EIsM5U?N#Zhu?4GoYq>3btDLHDJ|mUid&a8R zwdP-4qt>zW!=Y*W!4_uGO#VQ=LEDNxx~Gn@>~fREXvr%(~PPj}4~QeKc#( z^OtO|E3JIcW9Qh^6XrU%Zq}2(wdP*6yw29WD9`Jpy`k=t*-Yh+*Kr1yd2vYpE7{U^evU^<0~pLiN$&n&cBdx6f#Pp4+vre{`RF={R4`-_Km!UVT>7 zndYLr52yT1OLaCSc9OiN+nJ&m%VY0<{&(H+f|m~4vBkSaHhn5gZOh-1TjjL4HGNfM zFTLt~=}TX_)9XL{!(X1}e|&mq_W#P4$36qdy}2)2OLakdOo96t+!B)G*vk3v$3MFJ zSN`LFt30<)zyDiHo?5DpZ7A+|9cOU0XTYEHwKj8$&xo&eE;#n}o!x({p*kP#E5DP^ z{RY=QFFidq=>@WfYJYoh>}k*bYyKO4iT-QOWzAltT~qto+O^jr{keKpAOLNUrruIJj;DbAVrAqfSx%9vEjo;AKsD>7K zv-jx3gBjP{|8IQvzdpVEGXSqiuCT?YhCVHHePwUSYvuI#HN91554~pnpZ(dNonFlS zd><#D9{LR6?xX%^0Df)c`985-hb=vEa(hT#g_+0fpYuO+e`4m#FZQOhIbO#ZTIRA5z*=0{WcTdVccRzdVVe3Bj=zZ?>SNVJFtE!=1mz)QgG3g;>UAlMc zy-9!JrT0)>e`{Zt;H&DWEe|=BKX8`;TK*p7sOvpm#~EDh8Sv+DtF757=QW?*+#a0;Jjd;&eAia?QaH5U-oxB+9llb-|jv8_x9)4BOC3OJ(Ifz zTm9G6Ski8}7S8_?Sj~=0+$Huw&86NRC+8CAebT-}KWb*LQJ?%vqxQo*s9A8!^W4i} zzNClxu{aB2XnXbch|yp864$@SHH_BaOgZD1I`i1?$^G2tm}6RV%_KT~VfJU{zx%tN z-0?yq^!w_(Y|Z=1|JtAWV4DAh^2MiCb>4gLy{Q~1M>>$4^fmMU7eDkrOtb$V|MJf~ z`3yi>tyUdk)CQ-d6<^3pTv1{dsrMf#xK+>GPd@WMe#ZG7fa>o8XrJWep2cSzZN~7c z8kv(CeChNa(dSi~@%7B>nUyV0F|U=M19%ME_kYi+#_>AN;A+nR&-Xc)vlFu;nBV)K zrE$a;=&ze#^D^`5*Ri0xx+2%)iWuTb}1u4mET3 z^y{U+H0WFP_TsDlm*`E+y#BqeW2sK7|10=Qs_dorf>2*4T zY56_HCdTZq+IlhjWBX?S%Kh7a@-ID^|K)3`9!PEXtJ$B=0?3u#m)C)ZYvrKpq1V9f z=`R&q+k2~?JfY_QFBSj(9~yoKfcgK!k3K)$Tg7z_=+ftmh>vK=aU`Gm9&=mo13jaf z_4VTUb)WZ&W`AB@KR**teUG1P`N!)xgR4CQ{*0e%Ge0>p%Lzxn@An+HNAjg_^&i=T z_p9vQ9x8ouy?l>lo_vqDZ1VgJWW$5Ke9t}inOKwWef@e{^WGAl*H~S<@6Y>t?ouD( zTmIW!_4jMnKB&JI{TgIf*S}@Yqx%@?pPKYXdr7zDTKxPE`#6D@JOSo!X2mVfb1R3M zKL6&m(*IWGe_em;y!PgM=l>Gt&h;_ZXf|Q?XSNY1o4H2wzkFtY=Ke4KuXlHPX`Sj$ zbKC4aeudY3pM6x5&jE7&f2{D-qMnc?1-9mYz4*C5D6Y52cTL54e8i>(xRiq~G4qHg z-}0}er;mQ#uYY#|ze0TmpqFO4{H55CtY29o1QYzfKHHDH5PZnX#WiB*1bnd5$H=?!Mw zb7d=z)^RB>>n!=_-v4v**_&VMni&vn?|l5caUHR$beGQ3CUT~5nrQ63y#eHKKIM! zMGfr(dkHHo*HPh8J>tAi_<4V#o?kvEBL`&ePrumf?*H9C^*m}GXV82Gs`td+95a=$ zHv_oN_uxFX*MW2NeAymzyH#f?zm=!j)$*R$UPCr~4>PBgSM9+$@8SGQ+w4o%wPgm> zZ0LD;UepKkdf~V9-}2wusy}=7>uAlq74Lb}YT=eWS3R9;_2+Cy@TJZ_@5kXECvc4u z$k}_#`?8ml^JjHUxra-94v~9gAK5!~=lZSdKez5^eyjhruD`WU<{Ez1@AEjEG}X`k zr}_Uh_y6h#pG~h*kB7x|2_MLlq4}Pl^?NUIdgc62ZmXW+*@K+l{yYAKXFvZ(@Acd! z*SVg(RbTCcTBE3arM9%>${v~jrDNt7%4hk$;9A(yI-ho{R>kvPSu^{4 zo%|d?d-3?mmVdmCGq~C_kne{}`Xk@>kM!@=d8z!7zOHFqgKN@m<%oYPi)&$Ts{`3~E>CgVi@j5Mg%bw}e!m2;#x(3%?|CVdX z^FRFK1YY6N+mf zUZYj55&h`=uX@b>pg1k{nXTooEv@erd%4QGyFtm6b;!wH<;%hS^T$2b39 zzQn*^`S|WGU%i;L#D$;y$PeG?yKB9qsdB}UUtyIao&K&e`rrKO2M;!Uo&j26#{3y`yVCaR5Yzhy8_b^Y zLXZ8-zxQ9>>E&_fTBd?|C5{_z(W@ciicPU*X+veWi^1HS}!7SXp1Z zxWtyuwP}+>Eqq7Cw34fQ*SU@Mi(h>Fb3>doEHwA1e#h%LgR4CQJcqZ6xBh}6_5;(a^%(%)FZo{KR6}+yk4vNPt2C43 z_gBultsIu~ZW9+e@AjoN)Xy`J@3D+?UYrx?yxWRv?$_+&ufOxrci-_+yy`=3_LG=7 z@?FE6$lOS5;?5PhCA zHM#HjLTRp`Y?TZ4~X%(^Pl`r|MeZOU;EEKdiX5B^HtmZsm1_TAD{GJHPzxPtKKlDdeSE@Ks&vgQ@Xc#aQCW5sG)8Jap;n{q9S? z=WF)=|9<6E2q%;(Jfj?D9G&WjxCP~UrA%ci%mT1)XN=itUqVCj69;^Upx zDSa;c@xS-qOuqvlj@SL4%Mag|>-#2N>QsF{1rMTUrs6XI`4z|PLj1LBsx#u+doj)h zt^6gM`CmF_ej&ck|6WgV`RoT|58zbjUdw#gLd}iUy-BWm#`LWDX8@TaKRNa+WS>Ik zGpDNKvp%yap9kQN)bYNYUvlZEPQEmLJzSG>pA5Sz~c{F>aRCg?x^_qj{6|EKT2KfUsE z0Ojhu9Ni{Xaf$pt}RIFE%TgC7)bjZ~m`lf3GE*S}(z8&z7$}wqnV1J-r+>Isbbt zH20`;I$p;aT1^7^U^jw6X~10`)Cf_egb}; zZd1{@w9#A=na!BTKK${I?yxnVDXu@8{re@phtj@eXB^)5EuFrn`o8b`zUk#N3Y_A4 zc`WU{HT3pAaP+$S{qvqGlt$ODXi6DBhQzkFR~pw@feYEk1KTzgr^S^|CeVfBe~x zO)vdDc=`N)@MP0VXZ)-G_dk8|I~I!h{I8$?kIw&ouJjOU_V@WqGr#t!&j6VJh5B89 zNzVbEkUB;^aKs#?b6BqP}yoUy{$g_Ue}-~7Hae=)Cv z%<;lC=S3cMsP8>5Y18jqyyPC=a_w`^o%~Cl(dl`fem7g+yJ_~sAn71M$v z?)r*D?d~baz1*#{29}82B$tFb#$%L zg0i*ng{q@_#9nwd@Y#=l29UODj@FS@i*p5?n*^r8N5lT*%k~AwJyA?{qBySAQTgus zJkR~i`Cq=CF?f*qU-P~`17Pk4bN<(7DnIe-Uw`(y|IGZO^FPH$v%v#;cpYbOwP%25_g3-N z-*4P^W-TG}I>;Qq=B&sOQimS&yadw2k^Z2^WWzhAM*gvuPoO$CKUdkQ$#|u?)cI=$ z^LfAe`9I$)arKoC*Td=itoxs(>-#HaZrPfz71!%W|L5O&nBUO&{5D#LpV#+#jcC}P z*C9`P(KU13IwNfO7N%C;7rB-=oZ_0neBMu6ap}o*#8%vU!Gnrv@iQ&vM1EhK`CoC} zE4_3!em=5^RnM3|bBd4L(|K3(_xae~T_dQSNE`19X z(^9^c5PN23UmEU%P`(znaCH7>=6Ce@pPAorWd7HF_#FT~2jF-A72~@68l97d`Cs-l zbH(4i_MiJ}&-x5t(w+`W=u7A4zU<`iK{W1X4K$8ul&6L78u&!C@SfAMU0daoC!LmS z+($<|eSY?@zXPE6vOW)xrgaa+Q)_B$U$VCE1KXbmanzIDTAst+q!q9KX6U`alC76` zpZ~x95C8Y+GXUoQhikZh{`l{u>JS5|jSpUnr;kv*(@(C7`<}6@JdbJi=T&Lh&wY;e zcD#-=SoI9Vv${Fg>)eWc>+d)2v(IEckNF(G=B&u`THb@cd!pCJ_WoOJ>VSLh_pR09 zj2yR|x#xH-*?TW$Gv+b(W$VvneUG&DrTvo2_gQ>z<%lf}^CWuEHL`jAg>V0ZcY5JV z?>Wx9?HX$I>a;YvKCZKcuHigVtFvag9!{uvMl-*b&;Iy|Q{Qn)xc`#8`?9^CZ07#7 z<&T`9KU4ah|LQZQkvi(f^6P;Uj@Du3&)@$SpLM7S>KT&V`gi<%wpXpn_cNEKx*lfc_2B2J)&;PRFRSO#&;n0A}m90gcM31@M%0<&c>Jz7q^NMM~ zp_$p4#|F_PDxP&JpTGCd&;CJuCcty5T6tforC2WZQt?YQGe^GaWqsZ%yy_omq?diy2+gByr|0_Fzs;_BjoDvuzE^7XRa`oj>#@-+eXpe&Yc@SX=yAs#T&)@%tn{=mo?k6`j zyw_lZJuNY5v4y^<11g3MqL*LU@pl05h1!?VzDLSyy6Z2ksoTF--p6zMn%nvW*`=lN& z={>IRt@7n@*%|lm$Izl-ZtrQ(UP{wD8^yHeo`!1zT?+?l4r{&8Bxb$zQ(yTxXSv4D z7u(~q{TXxHb>upF+--k;otN$T*sjrx%IptcapXFpZy{$(Jbm~I{e4}DBW>Sb<|&qX`Rqa1 zdyn&P00=#&_1{GA&)${y5`6Y-`ME!QVJ@$snETk8`#n#-mUH1NcD#-=7=8w(2kQRI zmS@NFZ_(azW^XCS?>X}o^A@w%mg^;#S|B}yYnSI%Ha&#&2e}6PwMgx%@7n9ws;_z{ zx3%_2UiM0#Gpf#i?Rlr3+SxPxk?U~H>A9GeIBX$4xRz6%R<9;;u+p~rqV-(aEYC}= zG#RhB)KDk+HMwZLR@SS0^q!9mqN(yzgAaO6a`IWOtNKaje5LPcJmxkvlS|+GdCYBU zff;N0&dX*|$9rR=5xTBryWVqJe5=k#|2=Bv_xRO*D$TF_zTbDJm-KFDK6^|)ef=&& za+Oa#A@Stgcb)6qMlTM#hn0?c#L}l;^7x8l3(-5mSGwfTlJD`~{zE@{moJZFlbc8! zR2&=B|5sA44?p_+!;4r8t2$_X4{(kiuj-r=&s==(?>2EEymQVk#a-L7Rb#2XH2S>` zVYQa>Tc54?o=$aD|9Bl|aJ6S3pW9pWZ~gt|_nTRYS&JEL%NdbNEs!4Gcf=;ok(!D0 z26O-LEp)wW=;M399=V2dH1rw4bNaM$voB{+p`V#-KTp{iPv7+(qgU=F{nj;9{Ha%S zNuR+{u9cVh=_~IXuIj6LS{_Tk)z@>dt6X9r8c;konA-H0bY7F(ilYvQHvP=4;=NpA zj#&dA^qyNd&-cES?e&Q#hYzB2%ou)!;(U3X%62Wbbl8qnzH_d{CXVk&tc9+{{=wh% zBh&BqD#z!1Y@sw>Q}h03AAdZ((srGC$fuXb+~)5wv_1p)`~TXXx#Q(|*wm*VJm~S% zMj@470t;+%iBr>&dR`}c5d(=+Cu^iG!!K&y=MU$t>2YYs@5j4%R{jj#qWv!3vpx6h z_nbM3Im>6SwLLwTdO~`5-w~TUN3Var$&>Xct`)CI|R zzv2}4dTx7+I*G1NJGD!G=DWX?n{m(W*`Am4zvg{js^RlLv$1A=Y(57NiudJva9+00 z{FTkWQK7m2|M`hunqL3-*Z=;r&jD1c>I0W?@o>5Z_Wy7_AAB(V+XM2^QOj`(4tya8 zY5MRzBnL!`pGaJ&7|85j&HvSB0A6RUt-ku%zh-~te|-kP=K$=By7F0YLUHa_HhC@W zQoO3^9QknWV>?PGKlQF@#mR9bPh8rr^Emae|Kme)C#d{^{9xSZE%Idqcc) zm9FBHvo*J>M=fHGBl}}No<~j3PrK4k!?ovd>}{`l)n3;0K#B6ntOAj zd#?Jjr$n?BrWU`__Biq??B!*w%I|UHYyRgm0L}XT?|m`*E3SE1|0bVaX*2uNPqncf z=?PcO{o?)Yo-P^Z#k)|IA)oBl)0eVN)-CG{otj zNE{@N4<_$EwxjaolMjm1{LlZ>Q}aHr_kZfsJH7IE0H~#0794deM9ciInLj@VP)s`N zkpseeeUDYV;?84}*NUTSA^F7JuWG_MlH-{5(8Gh|$WI#$NSs?Gd;;h(X3DvWnQqHo$=y1a z`BTXr;qZm3Pp`daPhB^){cPyZe)dM{J9__~?KxZbAMzN*wZP7w$fdLV~^|u-}lzy$YG!4xQ~r?3DNg(sRr?04aLx{MRI-q z=V$+*Vyf#iKc53IC+p|_*_+z%^0n~6{0tyxf6e^W4WAgO9NE+S zZ*xDN0dQT?P(M+zk(GJ)o(l(N%zfF?IeMHLAUb?L17P+SRB4x2t8p8+uc z^Sc1d|Jf&f>I$jp+=xwHBDu-qD=r&dA~EzWKl9Sh{8n5%TI`mVy)ftVbANshfX@KD zPF0&3UpnT0J`YfgKKHruSG|1yYjY3%)3d(Cv-E4eMf=v@1G?uvFXb%7?6u{b$W={x z?D>pQQ}yY!WuHqq)aGpHE4BCo6Ll6z3u|&Y{Z~{#LAdI7i*&s{KftI`T6{zUwQFyhQSn$ETKjEqwCuGd79u z^=K5W^oqa_HecT-Hg=lArnR zFXd+3bKOqe7Jp>^SKa&!Kz=p*ORGI5!ii@w|MUMc$oXHhzLw^Fz5Me4@zjy6Rrzz| z@vAiQs`;PKHWbrBJ304Xl9TT%wI0LnX)@-%Y}J6H9-R9vPM-nz{9pat-|J(8^uTu{ z2Il;qe+R(#s5Gk}(-)o7n|pEH!q<9z@4_DUK0CkWTeSB+&v&h!?m4qkHR~~J zt-UVtgqqFhvF9^JP1W~)YxmmIQgC7xYDF>PNnGt|1R}ook7Mu4~nUC#O~K933@BdZ5jI_@L{& z?i%|NwY=t?BDbF3_$37+Uzkgb*7o6bt)f@97n~pyhg^|uWa&(O$DtcbG{>= z2c#BXsCxLC_4TUe|IjMu&Am8V@U>ne`xCy_J?_0Pzvf%C_dd^etseKBdC6xe+00mL zu8SNY^;CcFm3#KSYxdjHdOe>hFL5oL4c8>(>>Pbhm5tWxPOS8W;^DZCvnZ7hy{`3CI_n-UoS%A1H`Shi#E6(HCpvT4WnFhc2-_pSg>Dxo} z-Y@N5J~8Tm@Lo%?%12ACW6u7>dO2u>RX>lrtIWK#(4*NM0 zk7a#oB)V=i-#-WNdL#LK4#3a;J^$X;`|i!XinDo@*Xa9viTm&p{k`Wc+AndRUsF%+ zIr9?plhEg?(LH;P*HZmUtlYQvcYAdI=scI}xYRZHx#acG+xH`zeS#h*hgzWbY1wG0 z0ebzaKl^wo^!>4Ct_7^F`Ly3B-;1VS<%p9`%j4;1Y)Y^ke9ueHWAyd9aFvc)Ee(9u zTjP)R7}c>S*H@h4zR%Q?3?X_uide)JtD8y3gt9fAS~3=}s^5)BmPV zzIdk>@$~=JFa6BJd80|xH7A!o8c^|;ow@S)9KdIP*QplEdD-w_#^vX^qai18G@d$q zVb)yhXAQ62v(?ArnhD+aeTYvVZPtNz{2%|p*B@$+=$ACag5wkLzh&|Ebr*_aw4eIxE?URU8d5^2Armb!g$FcMhMpV=D#^ zlC#!d%Inor%$I7VPkmzUk8-M6Y2m4fFVuqfTzJr9Jv}kc+bh>=x}7~UmVUJ#X;@j$ zeKerg5{DLq)2#pT-~4QPG56=cm*zD+U%X~Y|NqNr_UFaC>G4T#|NlA7`usb5%>BY| z{^oCQa|7#zAHochpfAxdU?tbq5Pp8iWH0#rkxnF`m< zR+Zx%woohkx422%(6@53cC|lpL2~eg)j~&2X_Bjabl|;4xyS29&p^)t>srqN{caVn zbFLP6M$vOGwdcIf>Kc5%BliJ)?%!)? zUwqe)Pd(vC|I~Q?T03iv`1D}^LccEAzD_lpOFDvXi=##>?wTss2FA9f`T# z?bNnnEk5h_bQ#Ou>5u3$$NkL5UqaQ;LRZ;}u_kfj3mTzI($60F{u%&ZtYL;?|kDxSKt$xIO z4^zVFi)Me_cYfCb4M=XH$EgJ;L=Ts~c+LO(n{cV4CV8NA86!85pZ~ia8*SFDcsL7nQUc-`n3 z@ci6AA6Iz}yyq?2xBh75#KL251`dSTXF^2yB}er*5o`XrbB+8#aqQf-gBUDeEZ<%`Qz)vCD6lV9of=CThy znEhHjx+<>uKWG1(|COhuIf2}e_q$8-~X)><<~62eYeSXBn}dj&k~3I8nCw?kIB|z&$IXF!|T5DbAM@=|Ivf! zLGf;*13e}i-cdY1``6F^M}7yuYhi<`EnAD3fA;3D-W!{^=Tz9Y4_ z^k4fvVIM-@12%hUq35sZO&)z5=~Z3-smHVLPtbWx9Dd6tF4Uq92nSNfeK=4tU(^B> z!zKqLkGN~G(YTM^kvO$IhV2^HWe;NT_|(C#>Yc+?^?UW$ADWh*`%N8j`ez)!r6Z5H z^v=OM=QZKUBj!FDYN2u8HEyE?tNCC5uXO+arIXJdypQJj>G$t{>njs7`!nA&@B93( z81dHZ@4x@g%nxez@6G?3{d@DjW`E{?&Vg$LxmJ(6?Pt)^qH)CbTI4IH<*}K0{O_4- zTRPW_)XO@apE2rH2uF@k_uXfHeHM^9d{BAVj|a0s&))$czp4qB z$nW|)X8+8g4?4vw8_xA;D}-}BwqjcN;LJYf71tuiZE`A{lI#olmA1!Oi3gKf5cpv7 z_`=i>gBNPSlhfjiSI(<@X@9KOxW8jh+n*Wk>#gFgKRbEfnUR>An5Rb13B8aSAhm@r zwR{hi{Zjj)FMHz5{5(hQf$OOOQd`(sXvpz;BmJfE9O{772djM$^E!L?_P(du)1T?$ zu!StN>7%cBkGU-^^_+J*>*0efEpZ_|D$etJI?t_YdL81K<2=2{1IZzd@40I@uj@AT zoWlm`gD+%}j|QK3VoL`veap_6``(v)a72ej$g1)ct9Upe8qj01{WE}^{n09y1t)x% z{WbsJe+Iy8kB-j(gg*Nd_s;+{_cQww*WdfEW`E-Pd;k3G-)Dc~`g{NWbN`(ErQ_N_ zIBJM@d&x)Z=(XY3a@m(UpvRp{zSq~aJzdsc%HwwdT&EiNAYA2()AHVKdn|wUU;PdM zy{Lgt9Y^fe>`x8pa(P{kk>jX1K3eiYG{hazw2)ll>5JEb_u6UC%EzxGIa!08$11ax zJU)otec3GHLO3D3b8ZuFMR@M@(^u&0RCik9 zWPmgK_~$!5no373wdwbA74zk}*cq2k3`~w3;zH-pJ63V>(NPCJv88jK8Wra;#kE=- zyig16>^;5S9_O*Nrw!N_#m#-#8+jhDIM2m)9r?~l2k$xXXgvn!Sm}v-%PK1L;55fys#72=k$IMkMxt3qm*^~2H`MUtVC+Xy)$99y>TGA=5`M>q@{H#e# zsF?dc`(t|?o0^%gc*S`hHt4mmiGfv3w2soJ7TY;PQ zmfmgh#c2_rz4>c$^PV#!`P{a426MgF zp?1z+z6ZrT2b-EB=YUp7zHqcZI?tu%NPlV2`yQz4*PAw)s<^sjnTkNVjcpE!D< z*Bj}-$MtI;>|t$x*HeS*0=Zt{X}{CL2Aywl;#tyT3q2p(HKUx@^t$8|$EOGA@yxGc zsqs3k9?4TTYq{@sug6-Q>)l@413lN4wR-W?%GaWn=gyJ&tMio>E^CpOJhjO~2WH&! zTQ<2>P3PR!X8?LRC!79Y#wX2b_ScNBR}~kx6zBHd4*xz+dp=&r8C>ld;Q6~%y!H2+-*;vspS_r$n5jn33%!sU)c2ll z6G!hzy*>J?PqpX|UaEi9TO56b-dpz8>-Rd;zh?g}_dlcCaH(Nw*&au)hah{K@9Bk0*h)W+GG54`O2b&lu9vd9xq^B-9pw~)`=eeCa z{{BC;TU*)4 z7G9|3HBPqt<8_?D)t-U*DZJgUxBhdGb1)L*qZYqhZ@xPo^BII??|mn^{2Pc z`^u)baJ0YYP>&vd&2AI#?O|y9as0%L?fK$;c`Wm!bFT8e7V)f!pIZE0 zoES*XS|s05HQaaG>pL&I%Bwif%edFaM(V50$he8%t{vHcp7W9}C( zJ5ikS|NRfY`R>nu;S1C2Z+zG9z0*sd1+Z74X8x~z%eOpviF1wH>Lnj!=I3`;gyK9W zZF1r9L3p9KYN2U;1^`bk`j!tbL@RXPZO#3^_EZ1P-EaKD58i$08^2-t9KiE3*JIQH ziM4#^-9F`MugB{+gR4CQdj4*C-TM2@?>#dTbC)AC*qZYqhdR{vo^BII??|mn^rx@T zdt#r{Te;NnK7QS9yM{dB+C8AB26eqxWup_4drtq6>u>d>Ki9J5-qW8RdCjMq58sod zEniDHv-Q+=4o#~*T1Rr=@WJF;e)5^GI2=e0C|)c5neBg74!Nn!81jdPWq z9QEKq@z}!joudYrG5Nlz)x(S_pZXv*D=m67?x)`C5nIv|cYXG1#n92WrE}hG^4SBv zYp{h{XlHN!>ReR=&e8K*w(HYw<@9*3(bFmBT5RXB6Q%8OSqp6|&-tEBUDv2~wOSf@ z&&j&P;N_3xp$GrbkNttE6nk973(oEBHj{L(tF+5fBm!5_N&qyOR;rk8BZ|N1m**vaLVxo z4=RQYx;|~q{+j#!rOyJydp>p|{Xmbyfr_g zK;|#unrk4BI@I@`ZWBijQs)x==_#b2<6ga~>HTC^%Qec|vIlaMPra&F#i19nZ|aTS zfAr*oUOR0xj?@v3_Lh!3)uK;zt>RAlhd-0uZ|Qq{)-X~67d3N#$!9HmbUj2nf?j7yFRg3vsi9b9 zw{+;eCU(|ATk)Qgx=Np%d}?IA``DG1nAgfap67aWn*XbO;^ZU}cSKWhn)8=ln*YD{ zH~-l58GvSgUj7+C&j0%R5aKk`_xfv3iu?Sp9A6L`B{uX)z5 zu@`rq8A<5#muzOKHTx$=NIiPc(~;P<`_q>_CUPBXu3cL4vYvc;3R??}^wbiL>;pZ$ zdwoXw`yNL6uepA|PWJ1_wT$*fr#$K`UH>WP;m>HDhx@dE9NUkvZwN@{-r=U%kAS=g8Kg-|Rj5o`(jG z*%>Q@4@z?J3^3MYJ9RQ#IHTyIF)5lS@-IuNTUo$^H``6r$&CmZcC+oPM zw*1zjZvLzv-Z5)bF>%s4x=jsn%>T^(`q{t#{J*NBJS}3g*Y;iKcGS>*kJoVqS9=EZ zEZ*|E_4gb1o;gg&+$3cF60SKb^1PP!pfBh#&!Ntx`sej}Z@>OE*Dfvl&U)^vcWZGT zzK78>kd8joY4vIGv-jwu13iwN=-QU89AZK(>R?Y~@Xn#5md7d% zJxD*a_|!p%?>acwRQbd+*R>wQ_WEvT9{MHp8kJ35>PQ3Udg36l)Z&A#lU?!R&^bEq zd82mLB5w_Pjh3DDDxEaqK{WV|vRR%32YL;0_~gK$cOMNoXwrxG+$~(@)1O*B-(zlP z&7A$I?byqA-I86+{HpD|YJB+7=kI>%m%snv_3Qub_fEeDpw9*r=jZ&&Wr>bpa0z^M>Vx7AAS;q#`V~a`V2rbzkcrTv%fT6mz?YY z-_nopCylP_c%69$+?)IRcARJDJpHV0(Z2Qfo8NP0CFU_^FX7sAB9~eqJ%qW4p{2$p z`_rq!J^HIgRU0nPz|^Qr*=L1bgStyd4)vC<|5Wec zd)>6+!2|Y0GClX3Um&xU}&>*EzRjd)#Zt9*s$-mHA%7G3*|CF15hSOJ2Sfy4id5;aljr(r6`zPmCVm zlyslx9C?l&r(Wi#PY#&z%BN?>l2;tg>thqE>bPdq_I}g^M{BUBO7HbEz7(tC73Xyo z*Q)k3N1ndN$R(%c!&h~k%Q_yThHJ1vG>KpPQy)D1{6Ay(StI?HPP}V1=P$jerOyC( zY3^tCS6s7x^&(e2weTy{te-E}%@yXa&dbi)JuSJJBc9*?&lvT{1)X_1O_R<~&1`t#xUyqfnkPi;A)oWpz` zqmTEN4Ud)@m+YUt=;^qnzjC}r%g!@!-_L{m-d@mfP2_vcwYF+dPxX8KGsb=rspps) z_p@%xCx^Nt*MF)tEk74HM~}-U29FQA2F|g{^Bm&NDIeZzxt;ov7&TkD#IqLqDlTn{ zC+79AsfnLjYJwgo*JB<}TfWwk-s{d8k2#);E^DIM8`%r}((m^c0Mq^$NTe;rT z{mOQ(l}Byj%F)6n?ws4i9ecLNr~?vnUmOcQG2{4&nJ2%uGG9LWk(g_-TS)B+;WF3v zpqTqRoYFFLaLRoRcm7o)aFgGtYo?zCYLFJUi#QGmG{Q8z` z_O%t))@PsWyLWAho$7z?vyVT%(<^QHzR)`-J9+n8c9rLKJ(7VZcV;HdP(H71IV19!$3S{`Uu(>ma)f&< z>Zvw;N3RD+iw2-_Wk}HjDwAdMUA8n#*s3Q$LSaGeG z_n;=|F>J2`=Qxr}jWwtq)UNV!53U)Fq4PfC+|RoBz1-AP+DZc_Y~{dL==s!euF4VT zn#{|1<%{!`HN3ub?z`4w*kCIMUb@zD4Yp$|&w1(PlQ&|c^<1ynvONzQt=A&v==(xj zapZU`ZTHc)Y7!?$n2X%hI^zz+KzZG---d3ICb)3P~ zo&lcaTg6*{zxjPOtdKetk`KDZxwKs;Tk%Ba$aka$ z96sp0+r)+N!WK^qt?Mc~^`18pt78kMe!^?GGpujw(j z(SRP4tyRUuWo#1L7tb^LnK`P<#LROa8_Zn!ih*c#Pb@-01$0lC+8S7|tEywFP zgR4CQJd3xAxBhy#KujO{- zCztw+%V)XP?J6!VHSYIpk5PN6UZtz*R2;Px^HtS^lg@3=Bj)IF@`Py8ZuwrPvXz$$ zo%7z;dD+A~56-m}N503>PM@0a!d%ov>nLr;;ga{fUX1?evtIhHBNvVPaOgoa?z`=I z*x*U0>pxz{8C>ld;Ca1Oy!H2+?mM%Ska-GZ*4lDTXS|o-LG~Mb3!dst4byAs&tQb2~K|tKzO1v8f@_p7@TfSc_L~FGlT3Q*kZ5Yh`Pp70%4#*NjzuRio00OD($O zmwfU-*QbpyY%SGM%-t3IoaZq!zTxXPs|$bV<)Oca`@Dj&w}$jIBBpmP7JK#9?Lo&OFMmXozH%i?|Gh^ zcKT6EXK=iZGq~C_py%UoXSw_>i#el4F` zAT_`q->M0px%l9cj<}EZSb?Wi3MrVH|EcK+Q zbZ+=JwNnr{2GVskxQyX)OX*Fx7~dk;9rRxR?RMdNwcuB)`e}CB7zyI-u8s16wir zg6P4Dr&h+`$pzsZv88dl@~P8`DPN1Y+sRcvoKR~j7GK~UWoyxI_U5mVKDoqQ?{>zi z;XGWG*V9#cYLO>QuF6A49yy7SKH&Z{OEFu4%w22Fi5wwys87E{;^;wY?a@Db z%3o9W6547{?*f2Tc1xXd2ggoLt4Lh4y!T`P-+S z_^mvTqe;$r^(2lB!h`TaUmj21{j|wTT+%7795C&wUUHtZR7Y`N)PloLRNOiAATfLp zUMS9eIJ6#j4*L>%_TxRgPsUS6oE-Nnj(iY3KIpvHQQUoWiPS+)Jb90&?S89gYUtC_ z6DLo|BGyA`y*9q*z&Xm!MUHs4E1h$d7S55np4;P;>so4&gOASr2yQ)RJic!mBJ$=U9m+f`3HgS-+d=~lmJ*@Jj zZ}B6w=zl`hIbO#ZTUz9ovlnWU z3t#zgRn1l%=XmnT*NbJ& zROr4IHb|}XTQywQ%0*KlIiPDQPI2e4g{-9-(xGu1&e7v7yV9ct(WEaPPUx$ZBaP>! zj`%2F)$&~GfgZ!IF!gA}YvBuL=J{7NEq#@nIiBk_xv5WoO-|?j@A%;lr_TWtXGtSlsFl3OX2qkX z74tf1Kw{LwS3FlM=DFC)t!%g!dY&}7hY_DV@8NabuC$(q?R~2puj#huV<)EG{gzD) z#kJ&v9)l+a_B6yYH~EaWYLl1o%tr_Bh)oSJdHGtz!I^#JE8n%Uwa^LEca9k7G1*$J znDevpsdj`@4Xy0c%AM6uuMe$brE77SCtq6Uv8mzlv|Z!2*L6N^dV$0}zh`@%+tdL) zhAqrEG4a@rJ)W3QOL?o4WLs-_v_Mdu6Rwoz%5z!qcb9 zOAXrOJ$Jy?y*ysW8C>ld&@*|<>(<|Ie(#yFn7Nq4)|?SJ)Bve1Y%Mh8Q0tQY=~3aD zzAbH4*SXeNylXu7PRRXB^y^--J-7GFoO@2E`}S<9<}>m{^m?I3gAXRJ*`Jvo6eqoF zWy4Wx30pbvo)0IimTR%4!**Qav%cr1UDX=pR12-6+i1kG;6UfavBbNbI{Z;otx>J( zJg;R-PYg^BUZ_Qn*_*#sImA<+zI1SF>Ue#(sR1XXzH{)&wRo?acF)gxXu($8_vPB; ztC%PBi;atmJ+W$zNG`^@Q4m;8H=-?`Ca+Ob=jKNcX6sbeL{FSz@|9Bl|aJ6S(^x1x^ zx?6w0`F&-EWA-qUU3EXOz|68!RhlyOf z(67I;(Ng1*=W@DdPnLQ;A?_XLD_fkFSQ=?{*az=Nh+rvDBf32gPgQ3w?QP z65ea2alf*ibG>-?-6odY5>H&1tEz>j73=Ye`BE+UiEz%REnmyEGxPLo6^8@KX!#ZI z95&dhCq6ZB$;;QG7CJ{_sY5drxc^dVJ??hqlM9cp9Bk0JQ5#(gsqKi?IqWJ{ankR> zdySs$G4|R**Sg)?a}`srR@G;YJjUFwaw;zCwlwGRnU4>$4}9>R*YzK-;|#9$4Cwj0 z<#p@tx4ie7xtPD0&DNX|In;0*ndh$EpFMeB_RO`cxqh_d3aM9Np8s3({d}u4uFk}B zE`1)S`}MHY>%lR@dACjS#H=(GSN#qEh0ufW!irnVdySUuJ=fOn>8Y!}TKE;tibqXL z-{QR{8b@sD+|E3Y$<{*ScGk&Q#?w!|`z_n|fd=##_POY_u|aC54=+?q%efYh4y%j;Q72&yyjWI#$I{feMa*cc69Hale68( zT=!D_eQ&;Ru4nZ6r6Wg3EwAr(p8s3+{koUzYn{XCem(p-E!!7+a85QnSjC-_&V6it z2SC2t^2q_k`Ep*iR>o(x=3SdMdDO&j>6}MPZY$<-&%yRQ**!g4a>3;BL3H3K?>gD! zgT%p#hZCX$iOKgkJe>P*Nq79(g?^Mt%3)rjIzS)X(hvl^8kcqlL#8 zwsPRnk_(@HOP4+HGr#gvix0N;lC@laAJcUouj34^_6+D5yybQ4@3*}7n#-73g+9xU zo)tM>N4C#=*%u!TxzrL~vp?ssM}Mv`D26SD;_2Sa|q6es$=Q~df4oqG?tEyFT(!*m@AKrcJ z)K>l$jvTPsgU4F?$vov~_4FQ_)z9_R06ho0hna^qdHGtzh4@q8{)_WyUDx8!c)oMw zIT8nnxi8xn8liaeWgoBO46gPJ_;Y#K#|a!KaN8%4pHZa$*8Y6b-L^k}^Uvdl(5UVq z!Hhj~Rko^EakhoYMAJg_tvGz@@WIrMk0a*D9uv_x z@4YKtIjKRH_3#}tSFz;qJ%%mR%APa(oJU7Z_p2Oq-h&+H()L>D9kZ_c)Jl$;j%bAB zfy64m;;I^{a}D(!Q!k%YSzU#A=>pj-W@tWupGq=h~4xd_CLp+*{ zxsUBgO^{gSD^~I1dU=_X+A7~U^og#)7Wz_7&sQw#l9RmGArH>A6-Uj?b8Th2zUtxe zwB7gq;h&!e&pG6(N2Dp|6UPxYx%Ua}A`US0?zvFeB!PTAt zp6yGc&kTHxa$iW7PCG8e-_-L3l@U&8EVPcqgyPdj+lr(2T(`-qxY67tUDmF8N~1+BH24|I z_!i%Ds1H_stJo5!xR%#*zv4YETdUHEo1@R;LsEFTYBnyJh{rJuE*E|F??#L z*8P@EuGey|%1h3D&rO>isfS0u6i+>Q@XBYwr3N3Ju;Spej`J;>9FNn3m?Ju&R>pdq z$7JWqzKT=d?Gq>dcpYbOwPzro)xCY6E8M1!b1j=WMA*{6k7|$p^9gWn6>>(G#Hw#{ zqxV#Kqgt*hdsY5u&3CcC&cUzQuN9m5|1JL6r0eI;VxjzMWsIEE!aGWn{wSxsO1o6g zd~sUd zx3|Ap{rI(&=QZ6%PX&DMi5_$fI*++coE*h+p>ZBPF-Kz3xA>8m*Fo=GWz$c3Eqt`@ z&x-l0^JvNM#n7xpYLe%^I4$-8&g>(9j!&+T`nd<^JqAw=e9&vQG}I%8B=pp|5nU7`$WVD+ZTHo_H<%rJPwk ze?oAgR4CQ{;ck8`&=<%GjA+mtJYGU;;mfqBl*1=Bl)kne`{}hKNnul zdybsnTR#8Sc>T=&!ngQmlb%KXj27~2I#=11E^CpOd@FZErRX<{mdp#pwhx*kXF2&Ikyi}i7UHXD8 z-KBW?I<|T-|G(8gm-H;!pT7@(hWj(>8hCQ>mk_;>dYMBEK7IH^>9m|rU$IGa|HXaJ zOP)H8UI(5U={rvzT!mG>=T$ab=E8d)G{o>-gN;UhWs?h%2a?l^du`WcuE)p$Q%nCI zdJeTg^3YU1G0&+ua)m7|d1&1C{ItI%v88tnc1t_TQ^&ED>l*Bqc9f@%V=LD+*e&e{PmQqyMe>$x^<$ww#r(l>s?onCOcSNKGF!>8}O*F=|``^t468{Sd2mNc{V_!Z9cQ;UYIB_Ca{ z&YBut$Mdj3?~6@Ls0Hse;bz6-SLd-oYB^#{^H}BiGfIqFYmxj`KX{P(_`phLv^K`t9Gq~C_pl5H7#T+Nhxq=x2%y{La z-5aZ36}NX^>Q%ie4*lL(^%~*){_gR-P=mU;hrO?xdfBJtqu(1_{gU6iFZHrd&nHLt zhW}ie{y67(6#AddQ?U4go>0s8dTx`q7U=`F^65o9{fb9JZBSb57E*6XOWgHttNxM( zy|kM5nfZmk`PC1e_{7KoKlu~iG--w2uhl;(kkxgc9pC6(xN6h zd`GX1u7~8Jsr*(=*Sf7*=`V5g@O@ANqy}1e_bZz`uk9Rl;Ff%JSr4DM*MW0f(xYwF z@qBD)wa9gJdsaQwQNwl4VS6rGdZK}M-|eafabguxmwqh`IEII%*Todd_D~ zXm#z!>o|j}Jp(+OYmk|(!kj03hHyJ|_^xr=-xtnT_K22#iB&yuBQ?n#(Pm$KY9x-- zBxgiRzeH*{qVfB^=6UI9sYws->-NYsp&!xGFOeFFBQ?nx(Pm$KY9x--BxgiRKakuC zz1|zNAHM%NUE71LXI8!zHVBvgB(^V#Yo&f>pMFK-m^Qv=iqmoqdyYJhp$DbMcC2h_ zCrZo8nEPmiE#5UtI-mJ9`+xfW`wyQ1_-6rLli6Q!*;>Ra8?Hietg1$jBPLW0ka~&4 z9n+StMZF~(o%ibH5F-y?s6`#O(GtU-67Ih^w`6;qK3)r3IxTzUqf*H&Ch+sac7 z7P<0WpFHti9y!!W^jwcsJpHJN#*tVe8hWJfJa(dT#CdLV9?N`kL2~gcJ+aifpPF8* z>LGo#QY#Id+w}LC$K6INF4suDa;VYLx8iI1(T7^;!-K>VGv4!+tA$3WHM7sp9v+<4 z^H*{B8OKM1j~?{+J*S$->o|j}Jp(+OBgm}QLS_kJHAk%F_I@uM{l9DTec|4=?mhg- zbL1NGs(W+X|9$@~sLpta+xs58wt9LNPkVY;_@DyVAYA$nMeYs7wNmdKu?n4&t(Cbm z`sC*p7*HYSr<)(UdwH2cuvN*XsAK1e693n_K~mR;JUSpNR8z2iBre16(d)u zRq33|nse6kyjHD>_Zq~V^PcdjYx(3>ap$;3xXi=v>6EMGn)E$Z)pUIoC!d(~xzNDl zlY>uONX%`x|DT<|y>et(vPC;Ft`PShC7=kD;z~hLo+M8b3PBmju*$Q}UfbIP2xI}3 zS&12AWSW1Nn@3DURl{p{cj9ht#@1`C-~F>%J)U|Q>-96Y>u1fa`K&wQ)bPXLkG%u^ ze%@;5oR*mL;4ZFbiuIjd=hyz4|Lp#{-`aonervsTzqP;SKfAx~xAuF#p37&Qci=4E zdXLt*)aZSB|5vnouU-2Udv?v;o;}vNuW0vP-G0Sh-@WGgK6d}O_y1lQ&%myqxx~&{ z+mkb6XSqF|_Qcdl%v@@%ah>lPY436}mwMgL-W^lp(Y&ndeKMAKYcBu&fBsv3|M&m& zXMXC;zXANe|F8eofB)|Va`r#lH@$w|!I#C2@?XIHP=t3+s`F=O}Yq+aLs+RX39dQG?2-09ENwbEZ>*V*M~e%8&E{;k|Ab8DUS*FAo1 zTKlG-yzG_r>7UK_`fg`k$DUu?$(!-CC$9aB?Z$U=lQ&n-eT@8jpUh93D{I#FugRJ} z%iHa-%iWD-kB+I=`Rn}jck>xrYjnSJx^8MFH*>pw=GHaI%^u0?@t#XNvFojCKhsa$ ziF2j?S}Svz-}Pr~uB=V`82x8_?Wg9NpS;ALU)N-A8J~Oc!{Cp-19?~P#hlY}20V-J zexCS=e}40so_*#S&pVQM@12_YpXlY8{;K~Oc6?m_^ZL)b`LBr{oAz8u_~@PYColPH z>>8c7w$tzR-OhZ+Y0s6skKRB19#4B>Us;nnJvZ&1PkYU|mzQ;CG4*!ybCy5*>HPfn z|HPh4`_?|mnb>*VPR?#D>ync=xoIaS>l1hLyRqbVyjQbx(_U-u`pNB>ydFzCbBP(B zIAe*KPdpo6*PP|ecwf7^Grx}an%#WoCpTjg*EPHT)pfhP?A3GKUgtBHnE8&G+r`dH zd-Bqrxa)VV1R!9^!Tl@ zbzS-$*Y&&F$?x2&d9ya_(oRf`-Ta!9{={5apZ*#%zt->mtXcc1b8G!>UGlR&Iq7G9 z-LL!I&bq|R9dYj24}(AU4&)s?i#f|B=B&4iIVVo^9{-#_$N5}$pZPkL_hjOg%X{*3 z-jl!av%K|=9?$dNYx1VE<|knt?|!fAc5*V-wbpiW*Vt=&uE%$^GM2nsYiBO`YhKo8 zK5<=>@s6F7@vfiwb?mHvRwH$Ke%f6pa~bb`#?I<=Zff?LjAi|;{yNvS*P3fTYuE9v zk$cEk*6hYRXV+fG&T8Bm&s^fv+_f{-acxh3uC+$`nVbGxzmk_tAa!-f^wl zb8EYEyS?V6pL7+*8Niv)fsdy1SU1d-Zl}d#`S%=DH?1na`fPyo_Z{ayw?ObJzC0 ze%DLx#9Y0{jHRD-yRppeV%D$q&&GFo8B5GQ)9*TIXFmPT%iKD)j<566U$4~PtzC1w zpZu)PT>85>IX%AXcb&A8xAwc%v^y_zy{6}P?W}nu-mAaX+U2h~xB6K>IcX=}TDQ(+ z&BWB|oOL|?)39e zze*F&f-VbKfjar&&WS(XKXjW8{g$+Eb-nRop;tg ztC8__-Sqo9%ggx0$zS_xjr5bBHHp1`&BXr|BPo%#;3n)XDsn7FZB{Te_fM)auc(rW9B+$zQ?9L2!v z=g!*MH!*8_zk6dn-|f^`Yp*%!PfTvF-Hmnb+P>BAT9emT_MEZK%Xr3mKI_-H?srbc zC-#+^GuG|&Q={j4Y&V|znM;0;XD(xj>v;NIJ$$YnaefC=C(@$>K+0Av{mG-*t zl{LGZvpTuQHKs-O1wbx!K_*jhXN z#N?j!d%WAdSH=@}b3N93X3ZMc{LE+VI^X@|PMj-i(oW2p>F=(r>%6oRXI-~CZD`)?s z#_Z|m4Gr%78$iED|GZuQv*rEq=jV&xf&c!`h2uW{)lUCb?76jlr9U}y&7X{q*2nL) z-*ahq&aQnn*5j!)vB%R+Oy2bS%6Qh#T*lH){+g5dbv)}c-u<+@X2y20_sD$4QX~C! ze%D`fJE!Y)Ubj2H+w0n_S!1u6cIJ2EySW*gYqvHvGMBvWXDl&u9jDz_a@V-#%=}z4 z{?YpQ-Fa&}c|D%7H6~}q*Y-NrYbUqocm0fI-Sp?$^*?4ley?@2X3bgaWPV-$NdI2V zb$(YnV;y^4+KK&qy(Z(y+s!2>&z^qguIrPVoU=9Sc>25bXJgqTF?qiepH(Y$Cg$q< znz8Puo&Wv+v-!!})yw+Kr@yOnYyF)6XKiBYYfaB} zJ9RVGac!r+u1UXRaua7R?ZmUOuG8&VH`hAW{k6U3XZ}~A|NNS<^fRBBb6fwn+-a}p zy<`2<@WbHOJJ9b(-krQliFwC5zTzF~d+GKo_S)6#J=5Ohy=p9bc6`-dKcMdM2p@mP zcD&Ngdz{?qcRTaXh*NiWrFQ0~f7Z@i*4&D_8W~IM^%+ZC_gcrN-#I z=1;$Kcm1qc^OBqSHLm&TXI=NRw#TO3xoLN9x09c-j$L=gyT5B^Y+~p4c-ontI9JxB zoxF^n^*g`You3*rKi6(9WB2BFdAl{cyo^npHM=W0Gnc%KUFjz`>(fs>+jAXH{=~Dn z9`F2Kb2gr}J>O%~?zyykeAk|_xspHeW8{DS&Rk+L(w{ijjDNKLXFGH2+VpqvtY+ul z+OPAbo!r!2=aX|5vp(z6?|K>A)!2)`tGOZXU1laTv?xf$KE63$*s3UF`f_yZ6uf)Xm%)*Sz%CxU18(GC%9)>bdlLEcxqL=k{F2C(f1iX(#rY zTREMVwYxZDeRa(q@Aj?QS--}tU)Rofa+A|z$GNjZpS({Kch_0nu9fy#ZpM?Bz1Nuea`tu<1;X_L3hP5snd`)jS_rB?FOPk#5aCNVi#oAF&gxm~Z@S-a+St!}Stck8=W^1H@o z+Ff(E{>nb7k@elbwSJxJ{;o#G5@)})ow3B8f5lkWoc3I^XX4sU|5xIFa(gW8TXE** z>OIom)y-VTY0s7X)VdWj_px%nHuX|FxwrbMyT+_Z%{89YPMuz#+^&)N#GYGg^!l_D zvo3XdJnfEae)m(O_s`lMySFCu9dFg?yx(nSpTxD^jIU$A+wZ-)eWh;ar#%Rjee~rC9?KNlZCoi%0&Dg|s-LBvH-QMM9tn+&B zX=g6ES7O$5%({FA>35v`-hW-+Ytw#J?E8AO{fv9Mw`czD>v-(@ufIdnpO`E8-JkZ1 z-x^C!)_2Uh#9ghdUt{NHE@O!qpO|ar=1PC!$H@O2%iNmVLeZU346Z_v*1Vf0vj1#N12zsgd!nv9^27S-b0`c5*Y<{p_*kCa1@D z>pL&)&RgrJpO`hfm^y2m+^kq+Ra>zRB5*ulppY@8xW)$GgszwY&Tto7!__>}t%K#95biVrr#-7CSFFx9Xkc@7APV z$Fx)DUYz_~$(?@oOMC72c;?R5PwrgFOFQ`;eD=}++=l)V7G5e-Jad#bi{<>yv*UngC=k3NazsAX(vAKHP$H>R;9`AN?W-RT* z-Pp`!e2rPR#^h!Ete^43&hN2qCns??w;SvH%umcU^V7Z+Gj}yF`4jJ5sX2Mw&boDM z`h6vDjgynHw6ku;(@tD-cl~>HvSy9FM`}z?+L=$>&1LLLPI5a=`|QenWIp}uwe}}3 zV`-6&3LYiPe1L6J)d@R)|hn}Pk)V>U*qJ>SYN%i$J1WdbpARwYck*M zTY1S(>~(2(+_i7jpZvMHe)@@7pV;HK)=l1AozpcwW_4H$Vg0Ur)7vK|1&)OiMeLowAa{UX?M=F`$|s7Y0ouzAFcCt@{-%J z+q*jJ+RjhCwE=H<|gMXFZ-==t<&qfo%$Wu z_S)aA&)Axq{>PXHJA0tO`J7zWxm@PTXQ>S*FMWn zz1^COB_}l!dp=`{XLB-iq*@wC_2d-UA2 z=gNFykFD+L&(-&^uKURNr*86-cNW*Y)X8{auiuU3J9)l?_xI={e?AXC3{KyHeqZtq zCFb3ln5)Oq&fFR^zl&?`t^RKPj9tCf^;g$*-ddykowv3-Z*8x+yZ*Z76@Q+uz60yM zto?a!QtOfX%$mf!e-qa|j=KN)J3Rff7}K6lyXShW+mkc>TxV<6@yXBH%w36DcPn?z zS^HVLj<5aB%iP4dGItg~vOa!yPS$2VbLl6}_^v%;nM=&r^mDCa)1PbRJN_E1@$Pr6 zta)TCwGy*;_cNB5eUh{5XRPm~*RN|kFLP)4YwfJP6}#q@yzG&fHM=?)OK$qTChdv4 z`E_mQq@CC`vM%*^{fs5{`mD=X;^fYi{w~hgTpyVqzduGke)sz1BtLmQp0Rte>z?JN z?wZ@ziK&%!nM*(O8SgbccGgavp6{Az@A7(0+H2me^=ocw zbX?c3{mIQ(+H2jOPwmcG=hIJ4V%JYQ^%Bo=dp_+o_S%`7e&X6*`>ECAS=+JOlbb8+ z6MKBxS(ovQCBNr0*15^g*qYz-J=f#C_Eyfie{wsfeXD-gS@Uo8Q#0!mGoI(@c&lEo z?=@*>J~h&x`5x>1buM`szt>ORS~LBt^SRfF`F`Brne#pRs5gHe{4lur4&;65cW6C( z^*g8ibpo&W^PJCrV!s3VyCw1LsWYFosdx2WlYejj+2^SF-@l)4wKF&O-0gmbb$t4B z^_sPRHn+>`y|ONOnM=Q8+R0hh?&fdhCO@%ryLQiaJ2{CxH|@FBn(1dQ@vO!=KKXN{ zPGZJ7p0%^4<5_NxCx17#&P{*v=1RNc+Ma%{teO6=0{^Lz`86izO3d0dcgEMT?*D)P z&;R)~Ke4aL>A7Ar?c}cG-A~TM9-nrPb>2GO>pHK;*Y>TvuD@HCv98hWd-I)p)}9*c z+}T+6+r_D|8&9pCTlee!n%C=hc{A2)($4r|Sg)J*Ts^<8 zS?9Yy?YXjk*IvhV{Tb_Py@y@>$+Z(cJH5ciJ42x zc;Y&KwV%9a#J-nZd&br)`_A~ZGvDodW64ciYxVlcnd@xqWA>l<&P&~1pZUzCpPbZN z`^igO=XdL}M{=`v7uTG%zgu&we(ophx_?%C9q)d(Q*({UnV73{W~}?uUcYwfZkEbUuyH#cK*P0d^% zBma!0-*IxMpV)I5PrWsEZm&-}`D@-f-~C}>wl zcxv2>yFF9)Z0(%!^E2x^FXQ=lfXsDXx7WSaxpjT!6Eoj+W^S$?Tk}3L{<-($ciwLO zx-R+m)@R+Vm|BUQyN-2#+SxyAGM4f5J5D=S)^+~Oo%NHS^&PWjm$Qy_?%96H$$V;L zJpJ55_t%`vt+8vbb&}gT8SB{d-JYEEC+5od^mpwU%eBjyxvrJmjCcQREID02V_B2A zv+>>hn%n)ecIqTf-n8dRJ8Ltxj`#eph5z}r%j+6F-{aj*?OQSV6X)9He$0IQ&V0tE zziZD}=CUUJb*|SZH#zAiuDR)V%syGO#x=L|d#>A^)9vK!_P8}R>(=?y&0J!}6EmKe z@x&gVvAJe!uC@JZvVJ9}hW^#;`Dy1$(%R4X8k0A%=XT${*YDu7 zFa9c8zmnf|)9%>qUX$^RO`Ng0*7lmy{p4n>*RONQS!1tByVuUxTs^kd$XxQ$Pt17Y zZhdmk=GO7lN?g~^_*`e}*72vDbDxwGuP8tGUk2nsw}KUFW7f zv9DWeC+}=N_mG@jf88gwGM+UVPk)_1+au#^{#m``WnJg3W9hFkYcjs;&sblRJ6Fa& zde{0{x5m_(xXyJy?c}9D@$5?OBXM%)nwlT2kKemJl9$}%JUX8Bz2C0AtDmtoW_{N_ zYv+Emru%1gliM|W-EKa)shRxrdp_$EcXJs_osMVgdQEZ?XD(OQ%Gg<4bJzZ?%h*{v z^Jj5gH~qe5-CQ%4*z>pAYkt0e=R0@D7k>Yq`~I?@TRwkc?kk`D)qCfi$$OOdYv-D| zt8uMyb=~Ccu1D7IYCN+3JMI6fcOakr#795>)2YqYh=HyNk40@#N>31?2);|&big@`{=b9Ut{M? zd#+vmkC~6T-F(+wYox#CtmE0^Ud;Yk-!bhqC;fZz-ro1-Q-95y@wwLV?tf(5&YAq= zJ-c4t%dVY$cXK^ZE|nLUGAEn`fKit=gOM3pEWby?esfNd#nGmIo4KpGxrcl89;x|A?swUL{mh=#&u2d|pGjNi`6u>xayzEhv+k|ygfYbSU5xo)jpbEdyrzsu{ftUKGQ=dY|w{?&N4@7Z|jX1?!f z#xlRg%ys^*-RpMk?3;Rtox6^u-*MWHUVXp2wLPEvNnX#*SohOT%zV}+uJhB++`Tnf zn>Fe8`fhh#k9Rvc8B2c`du-ZsWnIU0{8!?Ca?@UOvi{zD@;(#KYUf@O*VdOl-o{%)^cm;4^vwb#7#*ZONe z_qE2eywts#pS-L~obj|L&b1r+nDP5jfw=!p%${d8W`6z5&T=0a@49LC`W~D1T$%sq z{qy_E_~c|o+UvR=pWMvfim93WU0lc3dOe?Z?q&M9c4L|QQ?c(o-^2U6_q6l&@2#8p&R^SC*59geW&OHm_phvf_Wu2O zJ+r-j_Ib8`-nE^aj;Zs)zk80yb`kjb0`vFoJY{0q`TIBm=_l^Sdn|Q!F=OkV8Sgml zxz>F%_G@za)pe7Xv0d!iX`jW^yA^Xgsc|cJa@PDF%Uo(?uH&@VEBPJMUUS!e^42(c z>)7bbLe$w_|tYkvB-@@8(X)cENA^E>0Im6$a>pRqM&O=9NLPweqsd&cHUe&RZo z{=~V?@-jX#d5OEZH7~gn=gNHA>EDVocklXD+%EzI{wjg|x$f`c{r!8|`3`tgf1;gzC+72dcFgC$d!F7u z?OooCrOvbOuj(C57lHQ?$lt&H#2HJRxoLM!kM&%yNq*1wSaR>h?3KK=pYe&ATVv;3 zY2R8ubGdf4dT!d2ld)a@Y%FWCN9T3Tv-veA>(}x0&*Hi+c^RKLSH^d7#&UgRfBc@Y zv$e@j%(|?}y3C)&&P#iZXKS-p=GS%UXTI|@x5l%a$#W4PK;SC`@^@IihfnP9;`#nP z@wmSq_mk)Us`wL4=b7@}^t;sWSiF1wtesjN@8!;X^3QUwj;BuI)pgIx%f21YYIjcB zXSp-}s`u~8-nZ^6bE)&H`#kEp2oNAZfB*pk1PHv1K+a(kb3R+Yd(U_91LQ~k^z(m3 z`=`3*+?IDKG4I&KxiY@StbcY~_c`ld-S=#Lk9Vyezt!$F&up)IzUuw!dtUdv+V2{# zy3eDoivR%v1PBlyK!Cv82=tjZXR~}i&-d>G<42zT=efT6`l+^gck(VJ<{j(ve{yfd zcDj`}b60Y6KWjXzeWhmBUddheO+Pi(c%^36d^We&J)@obNz60uVsduz8TWFd#xdn0 zK!5-N0t5&UAn+aneg4haEZ@)bUHp*v*O}@k_51n1qWzOB`&shtSs;;o%1@D+&>(T?OX&1 z5FkK+009C7-a{bgu!%XF<*e4<&kv0s`ELOI{9n=jsjmI*22*w~x2%NB+CYJXhYGKL4+R zKl%AR+h6f+A0Ly8009C72oNAZfWUhQ%z4cDe?67{x&HB%{mAowpZ%Txf97ZWL*Cu@ z_>10C5+Fc;009C72oN{~a?;Je0p!2?C*~YCsXy^2&;M2NCz|G+nV9z|@7L=56QAER z{Z)Tg93Pj9009C72oNAZfWUhQQr@qX?x#J! z=lSdYo;W@-7XbnU2oNAZfB=E_5XgBf=d+19x8*E%Nc_mN|2)@MUq8(@?@ZpI#Jpdt z_n&(H+*jhC+Bf@T-^7f+OU!d8exv6;ZpB4_009C72oNAZ;5`KTJeD(B&TQZCyZXcb z4v}X#i~amx(LTGc89&~@zwRf`lXoaF@7Bb=Uj2-@pU=c++}mgN&6;QI|J60}ET4&g z=-!WQTm%RZAV7cs0RjZxL!i%MIiuywmhbF`#9wy~oqNvx--`YGU(vpGZ!`C|+2?uk z4t31?)y}VczT8Vp2=HBm}JJ#p_)BRYlvG4zQ6QAz$s+zg)^?9cM zC(r-6k9(j0?mlO%*MG*U*Prc?@woJ#y}pOf?Elqk?md4$^V*I}3|{;!M|l?k0t5&U zAV7csf%gzt&%1pF>F@A|#$R^^o%`>z#IC(Q|99x;KL1AlzUMqgVxRNk{EGML>OJ)R zbUXLkbso9TUYA;lz0a+7@>A=P{l7Xl_dGGrk+?o{-h+)D}Hij$eH6TuFw8m`hCB7mY?9CcWYvnQ6YK!5-N0t5&U zAn+aneeRfcpFQ&3y}!>NDo_14fOUWUKP&L;&vWYa`;vEPV&1KZxn8mV**#?c?7fT0 z>HF^Ixq837Cbd#~w{OOhGcoJB*46#LIzE2-8Z+rf4 zkF$1r{tfMCf1X`0?@X+6zHXP!e`Y&-PrS8%?j`rOp8aQS*SdO-z2+h;$FK=zkB=HpJ&(0I}@+G zUv1L)&unM!iR=DzAARM%CMLIIYCUp)S@Tu@ETX2oNAZfB*pk zZzHff2ld%!+WlSnfN}mmOje^^ziq#3``Mpo*UNhoo1C-TqVu2GUiV(_A@|ZTIUQ4H z;v@H$bzN)Pz2DkibEls=kM94-y}lwh_q{&T`pn&5bARXgukL?5yNdt;0t5&UAV7e? z+X&<=cI$h1zDpk{51oOo%{@*RO!SA%c+jFh=^NP=OYUaIZyMD)VAAfTH zzL%@*JjbK=nRQq9Im=6(NB95g{M`51b6p)zokyQ1>+bDy)O8UcK!5-N0t5&UcpHJ7 z!zSi@mha5@4n6SzdC5=j|DD<~IE!s_mNVnu{d(2s8lTlB@7Tn-UcLX^$9fM}`+0_m zkKSjmy}I9;*EJs9|Eu%+-q+{4+V2{VK2NW`wck3~It804y@6x_?URZ6~X}9jXJk!~IT>YFL&5O%wl6UOY=l{9i+(Y7eFWpbh zE#=Da#F z=eR@T^xXS9bJ_np#D4Cy^ZXO9JZJK=zT}m_0RjXF5FkK+0D<=qn6p^UWfOB&OMIvE|5)U? z6Z8De|Bs(LV>gz0$Hus?<4@U3{#{93t{2~d_xOw6QxYIRfB*pk1PBl~1m-N3vzhb1 z^Z&dO&-lsNf1>mMb4;J{xv23!K8u6IMSuVS0t5&UAVAB%HtHm?+Q{y#%Hs8eccuW@o0t5&U zAV7csf%g!|c`WC%oYyAyIquN7d-nZIPWJzm3{Op;v z9oPJ`ey^GKS>8Inu3P(S{#pN7>polO?7n{B`0+?C0t5&UAV7cs0RrzKkaO6?oYDIH zbSPXt`+O$1_y4Z#_E^tTyMFQ~e)sD$pV_T7@o0l-=iQpvSMpOc{j-?7#5FH__5N$l zZtZStT{r#YCGK)&toNUG=k3<+#(GWK$xF=m8rS{%9`3D8t>pdics!nq009C72oNAZ zfWUhQ^jR!tw45F09B?SS^r!d#uI={7*{EaYJAT(^S?}xFpKn~+BJI3ieGkd+nEW+% z&aR#O?30-BiMw^{Sm&p`=A8ADKQa4H%=p>5-FVg{?s9r8d5OFIXRg=WYd`z__=fe4 zTXGQ~K!5-N0t5&Ucn^U-i%olXRyibI_HO{|{@<~m^VII_G-H4B_3Y0!=gBrpJLl2% zI?GRPVropxn#7(<{=L|1x_!1TIoY#g<|eM|=UIEKbGt^jJ7>4{N^Y;~cGtew&OXn6 zN4{CT<91vG2oNAZfB*pk1l~iS&tiR6$=Tr5XMlD8@7j;Y8o%rPUcIm5Er062?c{x# zxLWl&H20UBp1ajfe#bRGwa?<3zxH?QI_K8jlXrG?jcIqCZm)S$Zymdrzq^OE##ukL z`?GxZd-9Fyy~khlo{|6o0t5&UAV7e?A+VkkawhmwXMlBo{XZ)(ufVf^a&F8!l$dvG zVy@j-=GNG`YkSRoRe#-g`k%RWb$aYp&Ccugtui23e3lcR{hoZ2ddF?J2oNAZ zfB*pk1PHu`z^E}5}`R&hPxz~xg|A~3GCZ1iL z(`!@n%JV1pyYAo5e&u-{&FSZF&$M5?|J-liLq5md{(UdgPEOWL>{^dnT*yCCTm%RZ zAV7cs0RjZxL!iHZ=ezgB{{G$H!4H*hfBu|%>@&mqEd9*islDE3_wzh|vwz;NGxN+R z{?7BqYhvo8?swY%Ri9@+f1W$BpZ`_+_n*Pf_-D<;ewG*9?@`i4fB*pk1PBlyK;S(D z`Ye$1L4Ws7JKw<%73cp(dr~{jyBPb4e~bUzXYM=Cl=o{#mbt{NnV33{K7ZDwR%+kd zKlzEN^NRgn`FZBP^BlY9|LXnoS>)a(=6)0N9FO{6e6}OaMSuVS0t5&UAVAZI<~ zn02X>`nUE^ZpYNQx^MEbF12?1C-)WcEBDXd6W8Y-hrZ%6F}WR6>lH>vO&0+I1PBly zK!5;&w-MNV5AW~Z)6Vzri3iMI@zeLbK2P`i`M;vw_p)pEbNns#xzF5to-60PcFtJG z*?^pE{2^WZk{zf3)t^{O_`V96RQ-?%4C)&U0LS?oZ?$ zOI!p95FkK+009C7-a{Z~v9s^u`Tjle5cz9<`o8--v9|O4UlrH;S^Ig8zsY{?HTT`m z)9+ZkXRLG9cIrI({8`uacKi4IuAMrs+W*nISMzh<{Y<;(zgq9nyxsnNFIU^C^XUE$ zwNcr=lQd?>wU-lU-h}(d;YxZuik&|WBm-S_VWxAUv-~HT^9iY1PBly zK!5;&w-LxWU}DZ@`F@`7-v`Dkzkk21cJBXH?C1ZA_G-Pm*T2y}&ynX#%saN8|C7Iq zpLzb&$-ar-ZU4_a+m$uy9)R0RjXF5FkK+0D<=q z$T?tQ&PMBZ@<7VKkxm$I=?%g`}l19-Tg1DKT=!-2oNAZfB*pk1l~g+=YWYh8|AE&@9c-f zUwi(~z2{lh*w6nJ?Y^Jf>raSzuDnZ$dB*xQ9_IGi3 z_I>obllN(2-mzFed*9Tv|Nrym{}0Aj`{#4`3*vE$E&>Dy5FkK+009E;A&_%G&Sew( zyq0s+q2c@=tS82w>N?MrcPH;tzhfbM<@0C%iT!U6b04|v|1-HH8<+y5VdSGfOW zHILhM5g}Jr|5wGI z=$dzCV&0>Pf9k*gulwiS-hIZe@PGIHUt#*fnn!6D0RjXF5FkK+0D<=q$eAc-q&~0p zIp)xC{twm@<4<$#cc$N^KXv}!?Voph_pv?O|K0b0w&x$HcWmJzK!5-N0t5&UAn+an zITPiKG%@G4oMjG)A9?nl=lbgFr`hJ6$vf0>-mUd|_Ve%d?|0k&{}y<*|5?3!E{SJ( zuNu$3iM7wsbP*sxfB*pk1PBm#4}m@nf0}R3S9ymz=KX5t zXSOX zZ`!;4AFY2c|Iz)Ee-`iUx6Y?dp1I@h`ICPZ*ZPn2*P6MPb^KQUk^R=(TXi#ctZ@+_ zK!5-N0t5&UcpHH}i%omZ2<1KQ)l8MMp@VU zrky?dGk&D@mE1@7&poX%bzc4a>%QrKcK;al`uIN*dwps?5_|o$&-OlQx(E;;K!5-N z0t5)WjXs9+- zt$Q`E?>Wz%nCH6n>@%0VuJz3Q&-}A*;%D}s`|Ndj_QcfQ#pEP*ovY8k?msnBZ{oV| zk?$fvfB*pk1PBly@HPVLS!BK+_xI(`eg{7Q|B8PvSfBq-_x~2BNA5H4Out9#+3S{b z&S^_gTz+{)y+`-NT>QKc7MNT4O%9w!Hco^qOg> z*1E?$`%%+HfB*pk1PBlyK;Ufz*6;uMj-2nzpFM*dfS1o}a(|XppTGJ&%DXl3?CPAX z?Rr<=m0puNsr|^_pMC#7v;W*z?r~!7yK7y2{;PSb#k2PBd(LyD_PS^KneS(Rwg2Jd zB0zuu0RjXF5Fqe20{QMe@$CC|e+NHcoa-6feFxU_U%y-Z&UL$Ua%N0Sy{qqE@+YQ7 z>i3@A&iurznb@_yV*kE}tNUNgJA3{%`0D3><^G40ivR%v1PBlyK!Cv82%LTY@AJi0X}?acStH)~Vx>bscyM+~xm_UxE? zSNFe~m*>yk9kYM-o_MuxuSuQM&Ym4JH!=B%UF+HVAN5=W2oNAZfB*pk1l~rVzk^Tv z?0a~=gC8)L&j51Y+iKpKIm6|hTHm#`pS+y=IzIaTB|rDj_c8nIuIxXt_nvm@U45sM z|C#;Qy+5=6)%zTWe(tpU9`Y<#YffI)TzUTNxpV*QvtQl+xK|ee0t5&UAV7csfwvL3 zat`Y6(g%$5KZCpPK;D@-zsRydmmpUDL|D&FZ009C72oNAZfWX@b z$+CAvsdmV{fV8II)B^# zSFQS4ckMj?)q2Uxx?Rmj#!@GBug0vqx7Sh6MSuVS0t5&UAVA=41akh_#hi)qU3%gH z~_q2{(sk6&j>*T%I z#WlD4yV_UAy2iA3`yO>%1PBlyK!5-N0tDVhpwAz>cFrXIUHZWJ^qI`spJmtYMcQ-D z%6qeWr_RQ*X4a?H#NT;my3VfM_wp0>zh}2I>9Kq3@6C6u&$RE=zc+tx&ttud009C7 z2oNAZfWX@b^!ewkopVvXOCLDDbtbd^XZYoP*!_3ayfbI-)NVX$o_YSyyOw*M*kgSk ze`^1}mp`?Co_URZFYDYh`@PRu`o+ z5262_rL^D6V%~*5_val+JbRaJjc0A@_WSm#_p9&c9ru5g>t9s&xK$Sc0t5&UAV7cs zf%g#T^Uqm3=c0*yCY$!5 zyz0HmGra5muX6p1>K?c1B0zuu0RjXF5Fqd#0y+PDHs;)vbJ|%vH2>JScy|x>f6RXj za|X|6Jn>am_Q;+S`*VEN=a=Vs*Zp7R`WMwbZq-GA009C72oNAZ;5`I#{`s9aXQ%bN z)@QeFAF7xB9iZ2A``-G_-L>zn&-`cC?$-5K>fejKChdD`W1PBlyK!5-N z0`DP^^T+SR_3V_hW8(VV|9ASwr}D3}%UP}L-RH=&+(*V=wSVf~yO-o8?(+NDyS>ZJ zSl7#%e6GLqZalI!`(^+2-g1wR#MC&8eSf`w=RdOFqq(o%zn^2*e)N7H&EM^Lb?ni- zKAC?ka}gjwfB*pk1PBm#4}m^^{BAqvr;a(xO}zTOzt?p8%KFJa+yBZv^nSbct$Wzz zuVc5?_uRT}`aOTv&bl_3cE%HLt?#wJ`%XNvzW43tJ^m#A>)(8?_Sa|JJ$v6@@84^$ z?)RCz-TwXTb043nJ8SwGroDUq&(^-O?#lja&f34S&u``&%UuKr5FkK+009C7-b0|z zAJ1y{IV$bl*(2ZeGnbe(9W!??X3acH+KIchyL(9f8h7j0v2{)N*WB!#xX$<5Y0rFe zJKkHj*16Yz)_d@oI=Q!g*5gm&!)M@IKJB&MWA^X$pV|NFn%(~W>~kMiYjs}Ve?M>9 zneR2vY^VOj?Df8r^=Ip*_U`IE)9xD6&iJjkt~pz`yN4d@J$n4B+qtiKuFswUd)?}l z`iblFX8d>dKmO*pd}iP6pL_1-c%){pORcl`NZqTsXZy$YO8is)$1}MI5FkK+009C7 z2)vEJdj5E2|H}9NEBoK7^HuAw?AtkK&(Jw*`)tjO|CB4wnDc6%C67Opm(JB$lNyPs zowzg0T-K!4#H;&1lDFQ&?4P|SerEry={jfaNAC4%?r#6MJ`z9s{8#VuKY2%R5gYwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj rFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFmM9_vgiG` literal 0 HcmV?d00001 diff --git a/src/ts/generated/rev.ts b/src/ts/generated/rev.ts new file mode 100644 index 0000000..cf9642c --- /dev/null +++ b/src/ts/generated/rev.ts @@ -0,0 +1,76 @@ +export const REV: { [key: string]: string; } = { + 'images/avatars.jpg': '0f8cf8266f', + 'images/logo-120.png': 'b051cf1915', + 'images/logo-64.png': '64a63307c9', + 'images/logo-gray.png': '7d4473f069', + 'images/logo-large.png': 'ef5e56ccc9', + 'images/logo-small.png': '770b141d9d', + 'images/logo.png': 'fedfb401e4', + 'images/pony.png': '981a26108b', + 'images/pony2.png': 'c030edb607', + 'images/pony2a.png': 'a6292b4e18', + 'music/ambient.mp3': 'd37b620d70', + 'music/ambient.webm': '4dbe61d738', + 'music/bossanova.mp3': 'd43e480500', + 'music/bossanova.webm': '50804c773f', + 'music/building.mp3': '33849b8cdf', + 'music/building.webm': '191ce0fe72', + 'music/cave-crystals.mp3': 'cdbbe63437', + 'music/cave-crystals.webm': '7a8a3f78d9', + 'music/cave-secrets.mp3': 'e0ad1b7375', + 'music/cave-secrets.webm': '427ee41405', + 'music/clop.mp3': '9f22e7790c', + 'music/clop.webm': 'f64c342651', + 'music/falling.mp3': '0af7264a52', + 'music/falling.webm': '60853db70e', + 'music/fivefour.mp3': '22bd477d78', + 'music/fivefour.webm': 'bc820e69db', + 'music/ghost.mp3': '428b1aa64f', + 'music/ghost.webm': 'aad1010bf1', + 'music/happy-house.mp3': '33124c1ada', + 'music/happy-house.webm': '2a3dbb08ba', + 'music/hypnosis.mp3': '6fdea241cf', + 'music/hypnosis.webm': '1564ab67ab', + 'music/island.mp3': '37cd6a1366', + 'music/island.webm': 'bccb8ecf08', + 'music/largo.mp3': '610c3069cc', + 'music/largo.webm': 'feebe2225a', + 'music/musicbox.mp3': '611cbd3a5b', + 'music/musicbox.webm': '16d28e09e9', + 'music/orchid.mp3': 'b55f22b71b', + 'music/orchid.webm': '49619b426a', + 'music/pumpkin.mp3': '87d399ff35', + 'music/pumpkin.webm': '24483ce5bc', + 'music/reindeer-winter.mp3': 'b694f34cd3', + 'music/reindeer-winter.webm': '723bf90a42', + 'music/reindeer.mp3': '36beea329a', + 'music/reindeer.webm': '15cb6fd996', + 'music/scherzo.mp3': '40368b2226', + 'music/scherzo.webm': '46ca4a9319', + 'music/school.mp3': '123fe87d99', + 'music/school.webm': '1382c07e55', + 'music/season.mp3': 'e344fa20b9', + 'music/season.webm': '1ae126e63e', + 'music/sunny-island.mp3': '5642308479', + 'music/sunny-island.webm': 'cf21301c77', + 'music/sweet-home.mp3': 'fec2f51460', + 'music/sweet-home.webm': 'cd615b0263', + 'music/tio.mp3': '9fc08033d2', + 'music/tio.webm': '0e04d47690', + 'music/trees-winter.mp3': '8beaf61d14', + 'music/trees-winter.webm': '61b71cb819', + 'music/trees.mp3': '26329a7c08', + 'music/trees.webm': 'e9a53a89e5', + 'music/trills.mp3': 'a9ba881602', + 'music/trills.webm': '77bf29797f', + 'music/unrest.mp3': 'b6271109eb', + 'music/unrest.webm': '60d492fe63', + 'music/waltzalt.mp3': '21c33fb0ee', + 'music/waltzalt.webm': '1456aa1916', + 'music/xmas-air.mp3': '4d1d720321', + 'music/xmas-air.webm': '4bfa7f0e7d', + 'music/xmas-horns.mp3': 'aa242e4e90', + 'music/xmas-horns.webm': '152b457225', + 'music/xmas-presents.mp3': '5efe475f4f', + 'music/xmas-presents.webm': '9eea1ab5c0' +}; diff --git a/src/ts/generated/rollbarConfig.ts b/src/ts/generated/rollbarConfig.ts new file mode 100644 index 0000000..c7933a4 --- /dev/null +++ b/src/ts/generated/rollbarConfig.ts @@ -0,0 +1,2 @@ +export const ROLLBAR_ENV = 'undefined'; +export const ROLLBAR_TOKEN = 'undefined'; diff --git a/src/ts/generated/shaders.ts b/src/ts/generated/shaders.ts new file mode 100644 index 0000000..ba25a21 --- /dev/null +++ b/src/ts/generated/shaders.ts @@ -0,0 +1,17 @@ +/* tslint:disable */ + +export const basicShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; uniform mat4 transform; varying vec2 textureCoord; void main() { textureCoord = texcoords; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; varying vec2 textureCoord; void main() { gl_FragColor = texture2D(sampler1, textureCoord); }`; + +export const lightShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; varying vec2 textureCoord; varying vec4 vColor; void main() { float d = clamp((1.0 - length(textureCoord)) + 0.1, 0.0, 1.0); float m = d * d * d; gl_FragColor = vec4(m, m, m, 1) * vColor; }`; + +export const paletteDepthShader = `// VERTEX attribute vec3 position; attribute vec4 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec4 textureCoord; varying vec4 vColor; void main() { vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); vec4 color = vec4(palette.xyz * sprite.y, palette.w) * vColor; gl_FragColor = color; if (color.a < 0.01) discard; }`; + +export const paletteLayersInstancedShader = `// VERTEX attribute vec2 position0; attribute vec4 position1; attribute vec4 texcoord0; attribute vec2 texcoord1; attribute vec4 vertexColor; attribute vec4 vertexColor1; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord0; varying vec2 textureCoord1; varying vec4 vColor; varying vec4 vColor1; void main() { textureCoord0 = vec2(texcoord0.x + position0.x * texcoord0.z, texcoord0.y + position0.y * texcoord0.w); textureCoord1 = texcoord1; vColor = vertexColor * lighting; vColor1 = vertexColor1; gl_Position = transform * vec4( position1.x + position0.x * position1.z, position1.y + position0.y * position1.w, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec2 textureCoord0; varying vec2 textureCoord1; varying vec4 vColor; varying vec4 vColor1; void main() { vec4 sprite = texture2D(sampler1, textureCoord0.xy); float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); float paletteIndex = dot(mask, sprite); vec4 palette = texture2D(sampler2, vec2(textureCoord1.x + paletteIndex * pixelSize, textureCoord1.y)); gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; }`; + +export const paletteLayersShader = `// VERTEX attribute vec2 position; attribute vec4 texcoords; attribute vec4 vertexColor; attribute vec4 vertexColor1; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; varying vec4 vColor1; void main() { textureCoord = texcoords; // float f = texcoords.z; // float fr = fract(texcoords.z); // textureCoord.z = fr; // textureCoord.w = (f - fr) / 1024.0; vColor = vertexColor * lighting; vColor1 = vertexColor1; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; uniform float textureSize; varying vec4 textureCoord; varying vec4 vColor; varying vec4 vColor1; void main() { vec4 sprite = texture2D(sampler1, textureCoord.xy / textureSize); float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); float paletteIndex = dot(mask, sprite); vec2 paletteCoord = textureCoord.zw; vec4 palette = texture2D(sampler2, vec2(paletteCoord.x + paletteIndex * pixelSize, paletteCoord.y)); gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; }`; + +export const paletteShader = `// VERTEX attribute vec2 position; attribute vec4 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec4 textureCoord; varying vec4 vColor; void main() { vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); gl_FragColor = vec4(palette.xyz * sprite.y, palette.w) * vColor; }`; + +export const spriteShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; uniform float textureSize; varying vec2 textureCoord; varying vec4 vColor; void main() { gl_FragColor = texture2D(sampler1, textureCoord / textureSize) * vColor; }`; + +export const sprite2Shader = `// VERTEX attribute vec3 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor; gl_Position = transform * vec4(position, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; varying vec2 textureCoord; varying vec4 vColor; void main() { gl_FragColor = texture2D(sampler1, textureCoord) * vColor; }`; diff --git a/src/ts/generated/sprites.ts b/src/ts/generated/sprites.ts new file mode 100644 index 0000000..d3208fd --- /dev/null +++ b/src/ts/generated/sprites.ts @@ -0,0 +1,3478 @@ +// generated file +/* tslint:disable */ + +import { + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, Shadow, ColorShadow, TileSprites, SpriteSheet +} from '../common/interfaces'; +import { parseSpriteColor } from '../common/utils'; +import { bitReaderCustom } from '../common/bitUtils'; + +// [frame][type][pattern] +export type StaticSprites = (ColorExtra[] | undefined)[]; +export type AnimatedSprites = (StaticSprites | undefined)[]; +export type StaticSpritesExtra = (ColorExtra[] | undefined)[]; +export type AnimatedSpritesExtra = (StaticSprites | undefined)[]; +export type PonyEyes = ((PonyEye | undefined)[] | undefined)[]; + +const sprites = createSprites('35407c008040404038b0f3020080000035407c00804000403fd0bd01010000000230f102010000003fd0bd010100000035407c008040400038b0f3020080000035407c00804000003c506d008040404035407a00808000003c506d00804000403a706d010040000035407c008040000034409301004000003c506d00804040003c506b00808000003c506d008040000009d0a8a02a0000001dd000a02a00000009d000a02a0000003190f30200c0000031d04810060000003ee08906828000002fa0f30280c000003e60330b058000003c506d00804442403c506d00804442403d609e01804301803a207f020043c2c03a207f02004403003a207f02004481003d609e01804541003c506d00804442403c506d00804442403a207f02004381c03c506d00804442403a207f02004381c03a207f02004381c03a207f02004381c03180a802004381c03a207f02004381c03a207f02004381c03a90f3018080c28034d0f30180c0c2403500f30180c0c2403a90f3018080c28006e0f1018100c2003a60f3018080c28034d0f30180c0c24035407800808102803c90b903020607803cf0b903020607803870b9038205c78037b0a8038245c74036109f038285c7003820920382c5c6c02810de03818785003c8091090344848037a08409838404003a00910a0343c4403440940a82c384803670930a0303c4803670930a0303c4803670930a0303c4803670930a0303c4803670930a0303c4803670930a0303c4803670930a0303c48031d0930a0343c44031d0850a838384003b40910a0343c4403ed09309030444c02280de08818504002fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504402fa0c10881c504403d609f08824503c03d609f08824503802810de03818504003850e4098144c5802b40b1098204c4803670840983c483403c808009844442003a706e09848401c03e0071098483818039406d0984834140356072098483018037c07209848301c037c07209848301c037c07209848301c037c07209848301c037c07209848301c031d07309848241c033007309848281c038f07f09844302403a2080098443c2c03da09309830443c02e70c10981c4c4c02980ef098104c5c03840280283c405c03be02503048345c03bd05905048245c034307308848145c03cd06e09848005c03ba06e09848045c03ba06e09848045c03ba06e09848045c03ba06e09848045c03ba06e09848045c03ba06e09848045c03ba06e09848045c03ba06e09848045c034307308848145c03bd05905048245c03be02503048345c03110e402018445c03720e4098144c5802c70b1098204c480331093098344c3803db083098404c2803b5080098444c18038105e098504c08036e05e09850480803430600984c480c03430600984c480c03300600984c4c0c03300600984c4c0c035b05e098504c0c035b05e098504c0c031d0600984c500c031d0600984c500c0369072098485010039704209858480003aa05909854480403970580985448080397058098544808035408409840481c038d09009838482c037b09e09828483c02da0b109820485003470f30180c486403470850402c0000038403704030000003f307d0403000000384037040300000035b04804834684c03bd0b903020705c035a09f038286c54037b0920382c6c5003590940382c6c5003f307104830685003a500004094000003bd000040940000038d00004094000003b500004094000003ad000040940000039d0000409400000395000040940000039a09e060280004036d09f06024000403a609e058280004038e09e06028000403e00a80282408000360094028280800036809f0282808000360094028280800035f0f30180c000003c30b903020000803340b2040200008033c0b2040200008035f0c80381c000803b30c80381c000803790b903820000803900a8038240004003b0f1020100010034c0b204020000802d50de03018000c03540b2040200008035c0b2040200008029b0ba02820000803ed0b903020000803710b104820000802880a804824000403770dd0201c000803930dd0201c0008027d0ba02820000801f50de0181c000802010d00301c000802bc0b904020000800920f10300c0018026e0ba02820000803ce0d60281c040803f10d60281c040803e10e403814000c03da0e403814000c021a0e503014000c03b40e404014000c03280ef038100010034709003810001003fe0160081c0008031d0f30200c000802230d70281c0008031f0d00281c0008029a0c20381c000802ee0c80381c000803720f30100c000803f70dd0181c000803fc0cf0181c000803210f30200c001002690ea02814000c0354078008080020038109d028040014035407c00804002003330d00281c000802e90d60281c000802ee0d60281c000802f80d60281c000803020d60281c000803600d60281c000803650d60281c0008036f0d60281c000803740d60281c000803790d60281c0008037e0d60281c000803fe02a00818000c03fe00f0081c000c03830dd0201c000800000f1028100010036b0dd0201c000803d30d60281c000802970d00301c000801f60d70281c0008020a0d70281c0008020f0d70281c0008021e0d70281c000802320d70281c0008024b0d70281c000802500d70281c000802690d70281c0008021f0de0181c000803470d70201c000802730d70281c0008033f0d70201c0008027d0d70281c000802820d70281c000802bb0d00301c0008028c0d70281c000802c10d00301c000802aa0d70281c000802b40d70281c000802be0d70281c000802d20d70281c000803160d70281c000803110d70281c0008030c0d70281c000803070d70281c000802d70d70281c000802af0c20181c000802cd0d70281c000801e90de0181c000802e60f30280c0008038109d028040024037a0f30100c000802000ea02814001002780d70281c000801e70ea028140010026e0d70281c000803f90e902814001003530d70201c000802ec0e4028180010025a0d70281c000803fe01d0081c000803680a902020000802dc0dd0201c000803aa0520101c000803ae0e902814001003a90e902814001003a40e9028140010024c0e402818001002510e4028180010039a0e902814001000640e702814001002160de0181c000803900e9028140010038b0e9028140010034a0ea028140010037c0e902814001002560e402818001003770e902814001002ec0dd0201c000803fe0080081c000803fb0d60201c0008037c0f303008001000960e702814001002aa0b102824000401fb0d70281c0008032e0ba02820000403290ba02820000401ec0d70281c000803240ba02820000402af0b102824000001e70d70281c000c031a0ba02820000401e20d70281c000803150ba02820000402040de0181c000802e10c1018200004039405e01020000402a50b102824000002a00b1028240000029b0b102824000000910e702814001002850d00301c000802910ba02820000403e70d60281c000802870ba02820000403e20d60281c000802730ba02820000402690ba02820000803d00a803024000002910b1028240000025b0e402818001003d80d60281c000803c90d60281c000803590f30180c001002500ba02820000802460ba02820000403c705901020000402410ba028200004023c0ba02820000402280ba028200004027d0b102824000003ba0d60281c000803b00d60281c000803a10d60281c000803040de03018000802ff0f30280c001403880d60281c000803830d60281c000803fe02400818000c00120e703014000c020e0e503014000c01f60d70281c000802000ea028140010036a0d60281c000801fb0ba028200004020a0d70281c0008038f0ee02014001003a70dd0201c000803dd0ee01814001003b70b9030200008026f0e402818001002320d70281c000803f90e9028140010026e0b102824000001e20d70281c000801f20c90381c000801e40e503814001003020d60281c000800780ec02014001002e40d60281c000800700ec020140010025f0b102824000003cb0dd0201c000802730d70281c000800580ec02014001003420d00281c000800380ec02014001002540c90381c000803180ea02814001002690d70281c000800300ec02014001002bb0d00301c000803a40e9028140010031a0d00281c0008001c0ec020140010028c0d70281c0008024c0e4028180010020f0d70281c000801e70ea02814001002be0d70281c0008028c0ea02814001002c80d70281c000802560e4028180010032d0c90381c000803a30b9038200008030c0d70281c0008037c0e902814001003db0b9030200008031c0de030180010029b0d70281c000800280ec020140010035e0cf0381c0008022d0ea02814001003d50b104020000803280de03018001002870d70281c000802280ea02814001003f90c80301c000802200e503014001003d70dd0201c000803a70ee02014001002140d70281c000803a30ee02014001002000c20381c000800360e703014001001dd0d70281c000803730ee02014001003e10b903020000803460de03018001002500b1028240000020f0ba02820000403d50b903020000802280e403018001002460b102824000002190ba028200004023c0b102824000002230ba02820000402280b1028240000022d0ba02820000402230b102824000003b50d60281c000803cc0cf0301c0008021c0de0181c000803b20a803024000002910ba028200004020f0b1028240000032e0d00281c000803ba0cf0301c0008039404201008001403720f30100c000800470f102010000802370ba028200008034c09f03828000003440b204020000803fa0c10281c000803190ef04010000802000b102824000001f60b1028240000024b0ba02820000403c40a803024000002550ba02820000401ec0b1028240000025f0ba02820000401e70b102824000003030c80381c000801e20b102824000003f30b9028200004038f07201824000003630a9028240000035e0a902824000002780ba02820000403590a902824000002820ba02820000403540a9028240000028c0ba028200004034a0a902824000002e00dd0201c000803450a902824000003f80b902820000403400a9028240000033b0a902824000403310a902824000002aa0ba0282000040356060020240000038909201824000003c40d60281c000802250de0181c000802730b1028240000031f0ba02820000402050b102824000002190b102824000403fc07a01824000003fe05800814001002640b1028240004033d0d00281c000c028c0b102824000003db0a802824000003fb09f01824000003ca0a803024000002960b102824000003d60a8028240000023f0c90381c000803e50a802824000003380d00281c000802b40d70281c0008021f0de0181c000803fb08301824000003470d70201c000803b00c10401c000802770c90381c000803960cf0301c0008034f0a902824000002690b102824000003360a902824000003100ba02820000803150ba02820000403270a902824000402c30c102020000400980ec02014001000640e702814001003fe01d0081c000802040de0181c000803680a902020000800000e703014001002140e503014001002a00ba02820000402b30c102020000402bb0c1020200004025a0b102824000402d30e402818001003340c90381c000802640ea02814001002a00d70281c000803f60d60281c0008036c0cf0301c000802460ea02814001002e00cf0381c0008038e0b903820000803290d00281c0008021e0ea02814001003f904903028000003b10b903020000803f60d60281c000802640ba02820000402280d70281c000802dd0e402818001002af0ba020200004031d0e402018000c01fb0b102824000002000ba02820000402a00b102824000003fc07101824000002320ba02820000802e20e402818001002a50ba02820000802e40c101820000803420ba02820000802e70e402818001003c40a803024000002550ba02820000403ea0a802824000003330ba02820000403f90a802824000003bf0d60281c000803180a902824000002960ba02820000403f40e902814000c03810e902814000c03350c20381c0008030b0c20381c000802a80c20381c000802a10c20381c000802930c20381c000802850c20381c0008027e0c20381c000802770c20381c000800230f102010001000330f102010001001f90e5038140010025f0d70281c000802910d70281c000803010ef04010001402f90ef04010001403210ef03810001002d70f30280c001402b90f30280c001403770dd0201c000802f00dd0201c000802010ef01814000c02280ef01814000c00300e703014000c032f0ef03810001002fa0f30280c001402f50f30280c001403930dd0201c000803cf0dd0201c000802640ef01814000c02610ef01814000c00180e703014000c03b30c80381c000802150c20381c0008020e0c20381c000803240b204020000802190d70281c000803860e902814000c01dd0c20381c000803ed0b104020000801dd0ea02814000c03dd0b104020000803cd0b104020000803c50b104020000803ad0b104020000803a50b1040200008039d0b104020000802d30c1020200008035e0ba02020000803090ef04010000803110ef04010001803440f30180c000c03380f30180c000c03350f30180c001803320f30180c001802a40f30300c000c029e0f30300c001802620c90381c0008024d0c90381c000802460c90381c000802380c90381c000800470f102010001002310c90381c0008022a0c90381c0008021c0c90381c000802150c90381c0008020e0c90381c000802070c90381c000801f90c90381c000801eb0c90381c0008035b05504824000403eb0c80381c000803dd0c80381c000803d60c80381c000803cf0c80381c000803ba0c80381c000803ac0c80381c000803a50c80381c0008039e0c80381c000803970c80381c000803900c80381c000803890c80381c000803820c80381c0008037b0c80381c000803810e902814000c03f40e902814000c03740c80381c000803ac0f301008002003ab0d60281c000802780b1028240000026e0b102824000003fb083018240000026d0d00301c000802820b102824000003410e402018000c01f60d70281c0008020a0d70281c000803a70dd0201c000803270c20381c000802320d70281c000802d70d70281c000802690d70281c0008028b0d00301c0008021f0de0181c000802730d70281c000803ec0c10381c0008027d0d70281c000802820d70281c000801f10d70281c000802bb0d00301c0008031a0d00281c0008028c0d70281c000802050d70281c000802be0d70281c000803070d70281c0008032d0c90381c0008030c0d70281c000802e00cf0381c000802a90d00301c000801dd0b1028240000022d0d70281c0008026d0d00301c000802af0d00301c000801f10b102824000002410d70281c000800060e703014001001ec0ba02820000802d80e402818001002550d70281c000803530ea02014001002c70c102020000802c90e402818001003290d00281c0008035f0c201014001000580ec02014001002b50e402818000c02b00e4028180010038b0e902814001002d70c102020000803a40e9028140010001e0e7030140010029c0e4028180010002a0e703014001002050ea02814001002a00ea02814001003a30b903820000802af0d70281c000c038e0b903820000801eb0e503814001003ef0a802824000002c30d70281c0008031d0a902824000003220a9028240004032c0a902824000003510ba02820000803470ba028200008020a0b102824000402870b102824000003380ba02820000403500c90381c0008030f0d00301c0008035606902024000003560ba020200004034f08501824000002010de0181c000803fc03301824000002de0c101820000402db0c101820000803aa04201020000802cb0c102020000802bf0c1020200008038902801828000003fc03c018240004025a0ba028200008035a0ba02020000800580ec02014001002050ba02820000803aa04a010200008023c0d70281c000802130de0181c000801e70ba02820000802ce0e402818001002a40de03818000c03be0a803024000002000d70281c000803b80a803024000001e20ba02820000402910d00301c0008000c0e703014001001f60ba02820000802ba0e402818001002550b1028240000020a0ba02820000402a50ba02820000802e40c101820000803920d60281c0008037f0dd0201c000803ea0a802824000003330ba028200004024b0b1028240000038d0d60281c000802140ba028200008030a0de03018001002410b1028240000021e0ba02820000402370b102824000002320b1028240004022d0b102824000001ec0de0181c000802fd0d60281c0008032c0ea02814000c02f30d60281c000803fe00000820000803d609e01804000803be037030180008037b09d03004000803510d00281c000801e00de0181c000801dd0de0181c000803fa0dd0181c000802bf0e402818001003e30dd0201c000802eb0ea02814001002b70c1020200008021e0b102824000003f30b9028200004037a0b1048200008038c0b104820000803010b105020000802fc0c80381c0008028c0c20381c000803d30d60281c000803d30d60281c000802a10c90381c000802c40e402818001003c80c80381c000c03af0dd0201c000c01ef0de0181c000801f20de0181c000803c10c80381c0008036d0c80381c000803950b1040200008031c0b2040200008036c0c10481c000802150c20381c000803820a803824000403e40c80381c000c03f60e403814001003bc0e404014001003f30c10381c000803c80c10401c0008030c0d70281c000803c60cf0301c000802340e403018000c03d00c10381c000803950e902814001002ce0de03818000c02880e402818000c03780cf0301c000802920e402818000c03030cf0301c000802f10e402818000c022a0c20381c000802ee0cf0381c000802f50c80381c000803a20cf0301c000801e90d00301c0008035b0d60281c000802070d00301c000803260c90381c000802380c20381c0008033a0de03018000c023d0d00301c000802490d00301c0008024f0d00301c000803560d00281c000802df0d60281c000801e40c90381c0008027e0c90381c000803d70c10381c000801e40c20381c000802730d00301c0008027f0d00301c0008034a0c20381c000803580c20381c000802b50d00301c000803090d00301c000803590ef02810001403cc0e403814000c02b90de03818000802d90d00301c000802d30d00301c000802cd0d00301c000802c70d00301c000802310c20381c000802460c20381c0008024d0c20381c000802540c20381c0008025e0de03818000c02620c20381c000802700c20381c000803200c20381c000803510c20381c000802e70c80381c0008029a0c90381c000803110c90381c0008031f0c90381c0008033b0c90381c000802ab0ef05010000c00240e703014000803ef0e403814000802e70cf0381c000803640b903820000403020b903820000403e70b9030200004025b0c20381c000802690c20381c0008032e0c20381c000802790d00301c0008032e0de03018000c03430c20381c000802a10e402818000c03660c80381c000802ab0e402818000c02670d00301c000802610d00301c0008039c0d60281c0008025b0d00301c000802700c90381c000802370d00301c0008023a0e403018000c02850c90381c0008028c0c90381c000802930c90381c000802d90c90381c000803e50c10381c000803a80c10401c0008026c0de03818000c02f60e402818000c03ec0d60281c000803dd0d60281c000803680f30180c001801e30de0181c000802070de0181c0008039a0f301808001c0394042010080010038404303804000803720f30100c000803050e402018001002910ea02814001001f50d00301c000803360ea02814001001ef0d00301c0008003c0e702814001001e30d00301c000800140f102810001402000e503814000c01fb0ea02814001002230c20381c000803f60cf0301c000803f00cf0301c000803ea0cf0301c000803e40cf0301c000803de0cf0301c000803d80cf0301c0008023f0c20381c0008033c0c20381c000802460e403018000c03a80cf0301c000802690c90381c0008030a0c90381c000803720cf0301c000802130d00301c000802730de03818000c03650cf0381c000802f50cf0381c000803570c90381c0008037e0cf0301c000803840cf0301c0008038a0cf0301c000803900cf0301c000802d20c90381c000802cb0c90381c0008035e0ef02810001402880de03818000c02c40c90381c000802bd0c90381c000802b60c90381c0008034b0d70201c000803150d00281c0008025b0c90381c000803e80e403814000c039c0cf0301c000802000c90381c000803ae0cf0301c000802c00de03818000c03190c20381c000803120c20381c000803b40cf0301c000803c00cf0301c000803d20cf0301c0008034c0de03018000c01dd0d00301c0008020d0d00301c000802070e503814000c02c70de03818000802650de03818000802070c20381c000801f90c20381c000801eb0c20381c0008027a0de03818000c02370d70281c000803490c90381c000802190d00301c0008021f0d00301c000802320ea02814001002fc0cf0381c0008001e0f102810001403d30e403814000c006c0ec02014001003220de03018000c02250d00301c000802a50d70281c000803420c90381c000802b90d70281c000803160de03018000c027d0ea02814001003100de03018000c03180c90381c000801f20e503814000c022b0d00301c000802310d00301c000802af0c90381c000802dc0ea02814001002e10ea02814001002a80c90381c000803b80c10401c0008028f0de03818000803950b90382000040394042010080010038404303804001402b40f30280c001003360ef03010000c03240d00281c0008039a0f301808001c03680f30180c0018031d0f30200c000802430d00301c000803830dd0201c0008036b0dd0201c000803940660101c000803fd0650101c000803c70dd0201c000803c30dd0201c000802740e402818000c03270ea02814000c037c0f3030080014030d0f30200c0008031d0f30200c000803a30f301808001c03fe0080081c000803fb08c0201c000803f20c80381c000802190d00301c000803820f3028080008000a0f1028100008026a0e402818000802550d00301c0008038c042040080014035b0d60281c000802070d00301c000803ae0cf0301c000803980c10401c0008039f0dd0201c0008038109d028040014038109d028040014037b09d030040014038109d0280400140384043038040014038404303804001401f80de0181c000802c80f30280c001c03720f30100c000803700f30100c000803720f30100c001c037a0f30100c0008031d0f30200c000803250f30200c0008031d0f30200c001c030d0f30200c0008039404201008001403750c10481c0008029d0d00301c000802a30d00301c000803cd09e04828000002140b1028240004033d0ba02820000403f905302828000003980e405014000c03bd0b104020000403b50b104020000802be0a804824000403c409e04828000002fb0b903820000803090b903820000803aa0b9038200008039c0b903820000802460d70281c000803300a0048240004036009e028040020004f0f102010001402c90ef05010001403ab0e404814001002390de05018000c02e70ef05010001402dd0ef05010001402d30ef05010001402430de05018000c02bf0ef05010001403fe0160081c0008031d0f30200c000802230d70281c0008031f0d00281c0008029a0c20381c000802ee0c80381c000803720f30100c000803f70dd0181c000803fc0cf0181c000803210f30200c001002690ea02814000c0354078008080020038109d028040014035407c00804002003330d00281c000802e90d60281c000802ee0d60281c000802f80d60281c000803020d60281c000803600d60281c000803650d60281c0008036f0d60281c000803740d60281c000803790d60281c0008037e0d60281c000803fe02a00818000c03fe00f0081c000c03830dd0201c000800000f1028100010036b0dd0201c000803d30d60281c000802970d00301c000801f60d70281c0008020a0d70281c0008020f0d70281c0008021e0d70281c000802320d70281c0008024b0d70281c000802500d70281c000802690d70281c0008021f0de0181c000803470d70201c000802730d70281c0008033f0d70201c0008027d0d70281c000802820d70281c000802bb0d00301c0008028c0d70281c000802c10d00301c000802aa0d70281c000802b40d70281c000802be0d70281c000802d20d70281c000803160d70281c000803110d70281c0008030c0d70281c000803070d70281c000802d70d70281c000802af0c20181c000802cd0d70281c000801e90de0181c000802e60f30280c0008038109d028040024037a0f30100c000802000ea02814001002780d70281c000801e70ea028140010026e0d70281c000803f90e902814001003530d70201c000802ec0e4028180010025a0d70281c000803fe01d0081c000803680a902020000802dc0dd0201c000803aa0520101c000803ae0e902814001003a90e902814001003a40e9028140010024c0e402818001002510e4028180010039a0e902814001000640e702814001002160de0181c000803900e9028140010038b0e9028140010034a0ea028140010037c0e902814001002560e402818001003770e902814001002ec0dd0201c000803fe0080081c000803fb0d60201c0008037c0f303008001000960e702814001002b50ef05010001403a20e404814001003c00c10401c000803620c10501c000c032c0b204020000802b40b904020000802ec0b904020000802e40b904020000802dc0b904020000802d40b904020000802cc0b904020000802c40b904020000801dd0c90381c000c02f70b1050200008029a0a804824000403f40a802824000402ed0b1050200008034409f04028000003f109f05024000402d00a804824000402a30a804824000402910a8048240004027f0a8048240004026d0a804824000402250a8048240004021c0a804824000402130a8048240004020a0a804824000402010a804824000401f80a804824000403140b204020000803f50b1040200008036b0b9038200008036d0a803824000403e50b104020000801fb0d00301c000802e10a804024000402e90a804024000403390a0048240004031d0a005024000403e709f05024000403110a803824000402f10a804024000402f90a8040240004034c0ba02820000803270a004824000403680b104820000803a00c10401c000802d90a804024000402f40b903820000803800b903820000803fd05d01020000803900c10401c000803c40e4040140010030b0b104820000402640a804824000402490a804824000403090a804024000403ac0a803024000403a50a803824000402b50a804824000402ac0a804824000402400a804824000403970a803824000403890a803824000402370a8048240004022e0a804824000401ef0a804824000401e60a804824000402520a8048240004025b0a804824000403010a8040240004037e0c10481c000803740a803824000402c70a8048240004024d0de04818000c01dd0a804824000402760a804824000403de0c10381c0008039e0a803824000403830b104820000803870c10481c000803bb09e048280000035309f03828000003b109e05028000002560de04018000800800ec02014000800590f101810000c028d0e402818000802a50ea02814000802820ea02814000802790e402818000802600e4028180008039404201008001000330f102010000c00470f102010000c03400de030180008023c0ea02814000803510e401818000801dd0ef01814000803ef0e9028140008022e0e403018000802010ef01814000802640ef01814000803470d00281c000802790ef01814000803cc0e902814000803290e4020180008038b0f302008001402960d70281c000803bd0e902814040803b80e9028140408008c0e7028140008007d0e7028140008002b0f102010000c01dd0e503814000802d70f30280c001002fa0f30280c0010035407300814000803a30f301808000803090ea02814000803270d70201c000403040ea02814000802870ea028140008035407800808000803fd0e201014000803fd0dd01014000803590f30180c0008032c0f30180c000c035407800808001803f905d018040010035407c008040018005f0e702814000800800ec020140008028a0ef010140008039f0ee020140008006c0ec02014000803bb0ee02014000803770ee020140008036f0ee02014000800940ec020140008003c0ec02014000800240ec020140008039207700810000c03fe05300814000c01e60ef018140008034a0f30180c000c03e00ee01814000803690e401818000802400e403018000800000ec020140008038f0ee02014000803830ee02014000800340ec02014000800900ec020140008002c0ec02014000800840ec02014000800300ec020140008022e0ef01814000803570ea02014000800580ec02014000800140ec02014000803db0e902814000803ab0ee02014000803af0ee02014000803cf0ee020140008025a0ea02814000803670ea02014000803630ea020140008028c0ea028140008035f0ea020140008035b0ea02014000802550ea028140008037c0e902814000802500ea028140008034f0ea020140008028c0ef010140008024b0ea028140008028e0ef010140008039d0f3018080008030b0c10200400180354078008080008035c0f30180c001002400ef01814000803760f30100c0010023d0ef01814000803fc04501810000c02900ef01014000802340ef01814001002310ef018140008039207200814000803c70610101c000802250ef018140008035f0c201014000803040f30280c0010033e0f30180c001003410f30180c001002190ef01814001002160ef018140010036e0f30100c0010036f0ea01010000c02920ef01014000803530f30180c001003560f30180c001002f00f30280c001003590f30180c001002100ef018140010005c0f101810000c020d0ef01814000803fe058008140008020a0ef01814000803870f302008000c036b0f30180c001003df0dd0201c000802040ef01814000803600e401818000401fb0ef01814000801f80ef01814000801f50ef01814000801f50ef01814000801f20ef018140010035d0e401818000403570d701818000403600e401818000401e90ef0181400080392072008140008039207200814000803ef0dd0201c0000038b0dd0201c0000036f0dd0201c000000050f102810000c02140ea02814000803f80ee01814000803f50ee01814000803f20ee01814000803ef0ee01814000803ec0ee01814000801e60de0181c0008032b0d70201c000003330d70201c000000530f1018100010008c0ec02014000800880ec02014000803590f30180c000c03da0ee01814000801f50ef018140008039207200814000803f20ee01814000803ec0ee01814000803d70ee018140008033b0d70201c0000027f0ef018140008027c0ef01814000803450e401818000801f60ea02814000803970f301808001401f10ea02814000801ec0ea028140008039207200814000802aa0f30280c000c03e006e0280c000c00000ec020140008035c0f30180c001000640ec020140008038903201814000800600ec020140008025b0ef01814000c02580ef01814000803700f30100c0010026f0e4028180008006b0f101810001000900ec02014000803fc04501810000c03430d70201c000003570d701818000401e20ea02814000800560f101810000c00780ec020140008024c0ef01814000c00700ec02014000803650f30180c001003cb0dd0201c000002490ef01814000800480ec020140008032f0f30180c001000380ec02014000803620f30180c001003db0e902814000802af0f30280c001000300ec02014000803650f30180c001003af0ee02014000803410f30180c0010001c0ec020140008038909b0180c001000440ec02014000800770f101810000c03830ee02014000803760f30100c0010028c0ea02814000803290f30180c001000400ec02014000802100ef01814001002970e402818000802640d70281c0008037c0e902814000803590f30180c001002a60e4028180008038f07b02010001000280ec02014000800620f101810000c03ea0e902814000802d20f30280c001002e00c903018000803480ef03010001000200ec02014000800370f102010000c03d60e9028140008000f0f102810000c03e90ee018140008005f0f101810000c00100ec02014000803e30ee01814000c00360e703014000802e10f30280c0010000c0ec02014000800710f101810000c03250e4020180008007a0f1018100010035f0dd0201c000002dc0d701014000803350e402018000802670ef01814000c03670dd0201c000002550ef018140008037b0dd0201c000003fd0b7010180004038f0dd0201c000003630e4018180004038f0dd0201c000003630e401818000400180ec02014000802130ef01814000803970dd0201c000003f20ee018140008039b0dd0201c000003ef0ee01814000802650e40281800080394042010080010035407800808000803410f30180c000801ec0ef01814000801f10ba02820000400460e7028140008039b0ee020140008007d0f10400c000803a30dd0201c000003ab0dd0201c0000026a0ef01814000803b70dd0201c000002430ef01814000803bb0dd0201c000002960ef01014000803bf0dd0201c0000031d0ea02814000803e70dd0201c0000035d0e401818000402820ef01814000803eb0dd0201c000003f30dd0201c000002370ef01814000803db0dd0201c000002880ef01014000803570dd0201c000003c706801018000403970d60281c0000036c0e4018180004039b0dd0201c000003ef0ee01814000803000e402818000402190de0181c0008035b0dd0201c0000036f0e401818000403150e402018000403570e401818000400080ec020140008025e0ef01814000803870dd0201c0000035b0d001014000800840ec02014000802340ef01814001001fb0de0181c000003fe0300080c001003730dd0201c0008034b0e401818000c031f0d70201c000003230d70201c000001fb0de0181c0000032f0d70201c000003370d70201c000003230d70201c000003e50e9028140008034f0d70201c0000037f0ee02014000803630ea020140008022e0ef01814000802100de0181c000003570ea02014000800000e7030140008039f0e9028140008007c0ec02014000802f40dd0201c000002f80dd0201c000002fc0dd0201c000000540ec02014000803600e401818000403000dd0201c000802730ef01814000802940ef01014000c036f0ea01010000c039207200814000801e90ef018140008020d0de0181c0008033c0ef03010000c03540ef02810000c033d0e402018000803fb0ee01814000803e60ee01814000801fe0de0181c000801ef0ef01814001000190f102810001002f00f30280c0010037b0ee02014000803390e402018000803e00e902814000803db0800200c001003d10e9028140008034c0d00281c000800880ec02014000801e30ef01814000801dd0ba028200000032d0e402018000803210e402018000803210e4020180008036b0ea020140008036b0ea02014000c031d0e402018000400680f101810000c035407800808000803d30dd0201c000001fb0ef01814000803b30dd0201c000003640b20181c000003310e402018000803fd06c01014000c02fb0e402818000803fd0b101018000803190e402018000800740f101810001002cf0c102020000003f80ee01814000c039b0dd0201c000003ef0ee018140008031b0d70201c000001fb0ef01814000803bb0dd0201c000002960ef010140008035f0f30180c001003410f30180c001003a40e902814000802190ea02814000800410e7028140008005a0e702814000800690e7028140008006e0e702814000800780e702814000800870e7028140008035f0f30180c001003410f30180c001002be0f30280c001002070ef01814000802460ef01814000802dc0f30280c001002eb0f30280c001002b90f30280c001003a90f301808001403a90f301808001402010ef01814000802280ef01814000803740f30100c001003740f30100c001000980f10200c001002f50f30280c001003a00f301808001403a00f301808001402640ef01814000802610ef01814000803780f30100c001003780f30100c001003090f30200c000c02820ea02814000803b30e902814000803c20e902814000803c70e902814000801e00ef018140008002f0f102010000c025f0ea02814000803c70e9028140008003b0f102010000c02960ea02814000802c80ea02814000802cd0ea02814000802f00ea02814000802ff0ea02814000803130ea028140008024f0ef01814000802760ef01814000802cd0f30280c000802c30f30280c001003ae0f301008000803fd0e701008000803ae0f301008001403fd0e701008001403930f3020080008038f0f302008001403220ea02814000803310ea028140008033b0ea02814000803400ea02814000803680f30180c001003450ea028140008004b0e702814000800500e702814000800550e7028140008005a0e702814000800730e702814000800820e702814000803720e902814000803340de030180008020a0ea028140008020f0ea02814000802230ea02814000802370ea02814000802410ea028140008026e0ea02814000802730ea02814000802780ea028140008029b0ea02814000802aa0ea02814000802af0ea02814000803810e902814000803f40e902814000803410f30180c000c035f0f30180c000c02b40ea02814000803ac0f301008001802b90ea02814000803e70dd0201c000003430d70201c000002100de0181c0000004c0ec020140008039b0dd0201c000002280ef01814000800000ec020140008038f0ee02014000800500ec02014000802be0ea02814000800900ec020140008034f0ea02014000800300ec02014000803c70e9028140008022e0ef01814000800480ec02014000802c30ea02814000803db0e902814000803ab0ee020140008005c0ec02014000803af0ee020140008001c0ec02014000803cf0ee02014000800680ec020140008028c0ea02814000802500ea02814000802970e4028180008037c0e902814000802d20ea02814000802d70ea028140008035f0dd0201c000002dc0d7010140008004c0ec02014000800740ec02014000803630dd0201c0000035a0e401818000401fe0ef01814000c02280ef01814000803e70dd0201c0000035d0e401818000403630ef02810000c03680ef02810000c03680ef02810000c02690ea02814000c02e60ea02814000c03540e401818000803540e401818000802f50ea02814000802fa0ea02814001002830e4028180008021c0ef018140008021f0ef01814000802220ef01814000800120e7030140008020e0e503014000803720b903820000403a60d60281c0008034e0e4018180008020a0de0181c000800850f10380c0010030e0ea028140008027e0e4028180008021c0c20381c000801f20c20381c000802b20de03818000802ab0de038180008029d0de03818000802960de038180008023a0ef01814000802230c90381c0008036009e02804001803760f30100c001002980f30300c001003420ef03010000c034e0ef03010000c03bd06b0400c001002980f30300c001002f10ef04010000c008c0f10300c0010035407300814000803a30f301808000803090ea02814000803270d70201c000403040ea02814000802870ea028140008035407800808000803fd0e201014000803fd0dd01014000803590f30180c0008032c0f30180c000c035407800808001803f905d018040010035407c008040018005f0e702814000800800ec020140008028a0ef010140008039f0ee020140008006c0ec02014000803bb0ee02014000803770ee020140008036f0ee02014000800940ec020140008003c0ec02014000800240ec020140008039207700810000c03fe05300814000c01e60ef018140008034a0f30180c000c03e00ee01814000803690e401818000802400e403018000800000ec020140008038f0ee02014000803830ee02014000800340ec02014000800900ec020140008002c0ec02014000800840ec02014000800300ec020140008022e0ef01814000803570ea02014000800580ec02014000800140ec02014000803db0e902814000803ab0ee02014000803af0ee02014000803cf0ee020140008025a0ea02814000803670ea02014000803630ea020140008028c0ea028140008035f0ea020140008035b0ea02014000802550ea028140008037c0e902814000802500ea028140008034f0ea020140008028c0ef010140008024b0ea028140008028e0ef010140008039d0f3018080008030b0c10200400180354078008080008035c0f30180c001002400ef01814000803760f30100c0010023d0ef01814000803fc04501810000c02900ef01014000802340ef01814001002310ef018140008039207200814000803c70610101c000802250ef018140008035f0c201014000803040f30280c0010033e0f30180c001003410f30180c001002190ef01814001002160ef018140010036e0f30100c0010036f0ea01010000c02920ef01014000803530f30180c001003560f30180c001002f00f30280c001003590f30180c001002100ef018140010005c0f101810000c020d0ef01814000803fe058008140008020a0ef01814000803870f302008000c00800ec02014000800270f102010000c03090e402018000800650f101810040c02520ef01814040803480e401818040803660e4018180408039404201008001000330f102010000c00470f102010000c03520de02018000803b30ee02014000803510e401818000801dd0ef01814000803b70ee020140008034f08e02018000802010ef01814000802640ef01814000802e80dd0201c000802790ef01814000803bf0ee02014000803290e4020180008038b0f302008001402e40dd0201c0008026d0ef01814040802700ef01814040803c30ee02014000803c70ee020140008002b0f102010000c00800ec02014000803500f30180c0410033b0f30180c0410035407300814000803a30f301808000803cb0ee02014000802220de0181c0004004b0f102010000c03d30ee020140008035407800808000803fd0e201014000803fd0dd01014000803590f30180c0008032c0f30180c000c035407800808001803f905d018040010035407c00804001800430f102010000c00800ec02014000802850ef018140408039f0ee020140008006c0ec02014000803bb0ee02014000803770ee020140008036f0ee02014000800940ec020140008003c0ec02014000800240ec020140008039207700810000c03fe05300814000c01e60ef018140008034a0f30180c000c03e00ee01814000803ee083020180008030d0e402018000800000ec020140008038f0ee02014000803830ee02014000800340ec02014000800900ec020140008002c0ec02014000800840ec02014000800300ec020140008022e0ef01814000803570ea02014000800580ec02014000800140ec02014000803970ee02014000803ab0ee02014000803af0ee02014000803cf0ee02014000803930ee02014000803670ea02014000803630ea020140008038b0ee020140008035f0ea020140008035b0ea02014000803870ee020140008022b0ef01814040800040ec020140008034f0ea020140008028c0ef010140008003f0f102010000c028e0ef010140008039d0f3018080008030b0c10200400180354078008080008035c0f30180c001002400ef01814000803760f30100c0010023d0ef01814000803fc04501810000c02900ef01014000802340ef01814001002310ef018140008039207200814000803c70610101c000802250ef018140008035f0c201014000803110f30200c0010033e0f30180c001003410f30180c001002190ef01814001002160ef018140010036e0f30100c0010036f0ea01010000c02920ef01014000803530f30180c001003560f30180c001003150f30200c001003590f30180c001002100ef018140010005c0f101810000c020d0ef01814000803fe058008140008020a0ef01814000803870f302008000c036b0f30180c0010032100420100000003ea01f08040000003c90040c0600000036900410080000000040044ab7c000003ce0410a04c000003ea04d05834000003910290c0540000036902c0b8500000034104c0b040000003c90240c854000003ce05c0a038000003b10410a850000003e90040884c000003ea0620782c00000369048150480000033608906818000003b102904840000003600f40080400003'); + +const sprites2 = createSprites('229217020186c84538821a020186c84500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488300c02f0080c7088304205000808748c3042050008087490308103f0081874843229217020186c8451180aa028186884500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488300c02f0080c7088304205000808748c3042050008087490308103f0081874843229217020186c845210217028186884500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488300c02f0080c7088304205000808748c3042050008087490308103f0081874843229217020186c845215217028186884500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488300c02f0080c7088304205000808748c3042050008087490308103f00818748430201e3020146c88500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f00818748432700e0028146888500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f00818748430c0113028146888500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f00818748430b811b028146888500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f008187484323c0ed020106c8c5042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f00818748431d015d02810688c5042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f00818748431c816502810688c5042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f008187484324016502810688c5042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f00818748430b01480200c6c90500d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f008187484308a2a70280c6890400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f00818748430802a70280c6890400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f008187484312028f0280c6890400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f008187484318d2bc020086c943229217020186c84508103f008187484329c0e20280868943229217020186c84508103f008187484329c0e20280868943229217020186c84508103f008187484329c0e20280868943229217020186c84508103f00818748431892bc020086c943229217020186c84508103f00818748432970e20280868943229217020186c84508103f00818748432970e20280868943229217020186c84508103f00818748432970e20280868943229217020186c84508103f00818748432e821a028186c84500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f00818748431db217030186884500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f008187484324d216030186884500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f008187484325f216030186884500c02f0080c748c304205000808748c300c02f0080c748c300c02f0080c708c304205000808748c304205000808708c304205000808748c30420500080874903229217020186c84508103f0081874843050128028146c885042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f008187484305925d0301468884042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f00818748430391e60301468884042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f008187484311f0e30301468885042050008087490300d04300804749030420500080874903042050008087090300d043008047490300d043008047090300d04300804749030420500080874903229217020186c84508103f00818748430200f1028106c8c500d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f008187484308c30603010688c400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f008187484308630603010688c400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f008187484308030603010688c400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f00818748431e52470280c6c904229217020186c84508103f008187484309222f0300c68904229217020186c84508103f008187484309222f0300c68904229217020186c84508103f008187484309222f0300c68904229217020186c84508103f00818748431381f5020106c8451330bf020046c9433fe04900810748431381f5020106c84507b04e02804689433fe04900810748431381f5020106c84507b04e02804689433fe04900810748431381f5020106c84507b04e02804689433fe04900810748431381f5020106c8450080e1020086c9033fe04900810748431381f5020106c84527e0e202808689033fe04900810748431381f5020106c84527e0e202808689033fe04900810748431381f5020106c84527e0e202808689033fe04900810748433f30f80200c6c8440601980200c6c8c500c02f0080c748433f30f80200c6c8441e02470280c688c400c02f0080c748433f30f80200c6c8441ea2470280c688c400c02f0080c748433f30f80200c6c8441e02470280c688c400c02f0080c748430e10c5020086c843210195020106c88504205000808748430e10c5020086c8431d8155028106888504205000808748430e10c5020086c84324815d028106888504205000808748430e10c5020086c8431d81550281068885042050008087484339421a020186c84500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488304205000808708c304205000808748c30420500080874903229217020186c84508103f008187484332921a028186884500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488304205000808708c304205000808748c30420500080874903229217020186c84508103f008187484331a21a028186884500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488304205000808708c304205000808748c30420500080874903229217020186c84508103f008187484330b21a028186884500c02f0080c748c300c02f0080c7488300c02f0080c748c300c02f0080c708c300c02f0080c7488304205000808708c304205000808748c30420500080874903229217020186c84508103f00818748433f421a020146c88500c02f0080c748c304205000808748c300c02f0080c748c3042050008087090304205000808748c300d043008047090304205000808748c30420500080874903229217020186c84508103f0081874843060118028146888500c02f0080c748c304205000808748c300c02f0080c748c3042050008087090304205000808748c300d043008047090304205000808748c30420500080874903229217020186c84508103f0081874843018124028146888500c02f0080c748c304205000808748c300c02f0080c748c3042050008087090304205000808748c300d043008047090304205000808748c30420500080874903229217020186c84508103f00818748430d812b028146888500c02f0080c748c304205000808748c300c02f0080c748c3042050008087090304205000808748c300d043008047090304205000808748c30420500080874903229217020186c84508103f00818748431401ed020106c8c5042050008087490300d0430080474903042050008087490300d043008047094300d043008047490300d04300804749030420500080874903229217020186c84508103f008187484320812502810688c5042050008087490300d0430080474903042050008087490300d043008047094300d043008047490300d04300804749030420500080874903229217020186c84508103f008187484320812502810688c5042050008087490300d0430080474903042050008087490300d043008047094300d043008047490300d04300804749030420500080874903229217020186c84508103f008187484320812502810688c5042050008087490300d0430080474903042050008087490300d043008047094300d043008047490300d04300804749030420500080874903229217020186c84508103f008187484338930e0200c6c90300d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f00818748432843ce0280c6890400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f00818748432843ce0280c6890400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f00818748432843ce0280c6890400d043008047494300d043008047494300d043008047094300d0430080474943229217020186c84508103f00818748433dc21a020146c885042050008087490300d0430080474903042050008087490300c02f0080c708c300d043008047490304205000808708c300d04300804749030420500080874903229217020186c84508103f00818748432e51850281468884042050008087490300d0430080474903042050008087490300c02f0080c708c300d043008047490304205000808708c300d04300804749030420500080874903229217020186c84508103f008187484326a0e80281468885042050008087490300d0430080474903042050008087490300c02f0080c708c300d043008047490304205000808708c300d04300804749030420500080874903229217020186c84508103f00818748430701080281468885042050008087490300d0430080474903042050008087490300c02f0080c708c300d043008047490304205000808708c300d04300804749030420500080874903229217020186c84508103f00818748431e01c5020106c8c500d043008047494300d0430080474943042050008087090300d043008047090300d0430080474943229217020186c84508103f00818748432aa0ed02810688c500d043008047494300d0430080474943042050008087090300d043008047090300d0430080474943229217020186c84508103f00818748431e014d02810688c500d043008047494300d0430080474943042050008087090300d043008047090300d0430080474943229217020186c84508103f008187484300810902810688c500d043008047494300d0430080474943042050008087090300d043008047090300d0430080474943229217020186c84508103f00818748433e534201808709031c81dd020106c8c500d04300804749433e5342018087090321011d02810688c500d04300804749433e5342018087090321011d02810688c500d04300804749433e5342018087090321011d02810688c500d043008047494300d043008047894329e0f70200c6c90400d04300804789431000f80280c6890500d04300804789431000f80280c6890500d04300804789431000f80280c6890500d043008046c9430b81400200c6c90500d043008046c9430602bf0280c6890400d043008046c9430602bf0280c6890400d043008046c9430602bf0280c6890400d043008046c9432352170180c6c90400d043008046c9430981600200c6890500d043008046c9430981600200c6890500d043008046c9430981600200c689052cf21a02818888452ac21a02818888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c30920a20100c908830920a20100c8c883042050008088c8c31300cd010088c90308103f008188c8432cf21a02818888451d521703018888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c30920a20100c908830920a20100c8c883042050008088c8c31300cd010088c90308103f008188c8432cf21a028188884500021703018888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c30920a20100c908830920a20100c8c883042050008088c8c31300cd010088c90308103f008188c8432cf21a02818888451cf21703018888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c30920a20100c908830920a20100c8c883042050008088c8c31300cd010088c90308103f008188c8431b81a802814888850920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430f00e303014888850920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8432880b803014888850920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430b00ec03014888850920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84325015502810888c51300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8431d925e03010888c41300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c84314a2be03010888c41300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8433dd3cc03010888c41300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8430a028f0280c8890434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c8430602470300c8890434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c8432d93ee0300c8890334803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c8430d91700300c8890434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c8432830e202808889432cf21a028188884508103f008188c8430c90e203008889432cf21a028188884508103f008188c8430c90e203008889432cf21a028188884508103f008188c8430c90e203008889432cf21a028188884508103f008188c8432920e202808889432cf21a028188884508103f008188c84301504003008889432cf21a028188884508103f008188c84301504003008889432cf21a028188884508103f008188c84301504003008889432cf21a028188884508103f008188c8431e121703018848450920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84309d21603818848450920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430b921603818848450920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84310621603818848450920a20100c8c8c31300cd010088c8c30920a20100c908c30920a20100c8c8c31300cd01008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430ed30503014848841300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8430700cc03814848851300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8430680d403814848851300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8430980e003814848851300cd010088c90334803d010048c9031300cd01008909031300cd010088c90334803d010049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8432c80bd03010848c534803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c8430272ee03810848c434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c84302e2ee03810848c434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c8430f825e03810848c434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c84308622f0300c849042cf21a028188884508103f008188c8431572170380c849042cf21a028188884508103f008188c8431572170380c849042cf21a028188884508103f008188c8431572170380c849042cf21a028188884508103f008188c84322010d028108884507b04e02804889433fe049008108c84322010d028108884511919703004889433fe049008108c84322010d028108884511919703004889433fe049008108c84322010d028108884511919703004889433fe049008108c84322010d028108884528d0e202808889033fe049008108c84322010d02810888451600e203008889033fe049008108c84322010d02810888451600e203008889033fe049008108c84322010d02810888451600e203008889033fe049008108c84316a25f0280c8884416525f0280c888c400c02f0080c8c84316a25f0280c888441942470300c888c400c02f0080c8c84316a25f0280c8884421822f0300c888c400c02f0080c8c84316a25f0280c888441942470300c888c400c02f0080c8c84334e0bf02808888430180f90281088885042050008088c84334e0bf028088884306031e0301088884042050008088c84334e0bf02808888430403360301088884042050008088c84334e0bf028088884306031e0301088884042050008088c84320b21702818888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c33b30bc0100c908830920a20100c8c883042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84301821703018888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c33b30bc0100c908830920a20100c8c883042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84313521703018888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c33b30bc0100c908830920a20100c8c883042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84314721703018888450920a20100c8c8c30920a20100c8c8830920a20100c908c30920a20100c8c8c33b30bc0100c908830920a20100c8c883042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8431a01c002814888850920a20100c8c8c31300cd010088c8c33b30bc0100c908c30920a20100c8c8c306d09601008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430800f803014888850920a20100c8c8c31300cd010088c8c33b30bc0100c908c30920a20100c8c8c306d09601008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430e80eb03014888850920a20100c8c8c31300cd010088c8c33b30bc0100c908c30920a20100c8c8c306d09601008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c8430580e403014888850920a20100c8c8c31300cd010088c8c33b30bc0100c908c30920a20100c8c8c306d09601008908c31300cd010088c8c3042050008088c8c31300cd010088c9032cf21a028188884508103f008188c84324b13e02810888c41300cd010088c90334803d010048c90306d09601008909031300cd010088c90300d043008049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c84304c33603010888c41300cd010088c90334803d010048c90306d09601008909031300cd010088c90300d043008049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c84304c33603010888c41300cd010088c90334803d010048c90306d09601008909031300cd010088c90300d043008049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c84304c33603010888c41300cd010088c90334803d010048c90306d09601008909031300cd010088c90300d043008049090334803d010048c90300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8430e81100280c8890534803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c84321222f0300c8890434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c84321222f0300c8890434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c84321222f0300c8890434803d010048c94334803d010049094334803d010048c94334803d010048c9432cf21a028188884508103f008188c84302015802814888853b30b60100c8c8c327b0c9010088c8c30920a20100c908c33b30b60100c8c8c31300cd01008908c327b0c9010088c8c300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8431180eb03014888853b30b60100c8c8c327b0c9010088c8c30920a20100c908c33b30b60100c8c8c31300cd01008908c327b0c9010088c8c300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8430480f403014888853b30b60100c8c8c327b0c9010088c8c30920a20100c908c33b30b60100c8c8c31300cd01008908c327b0c9010088c8c300d043008048c9031300cd010088c9032cf21a028188884508103f008188c8431080fb03014888853b30b60100c8c8c327b0c9010088c8c30920a20100c908c33b30b60100c8c8c31300cd01008908c327b0c9010088c8c300d043008048c9031300cd010088c9032cf21a028188884508103f008188c84327013502810888c527b0c9010088c90300d04300804909031300cd010089090327b0c9010088c90334803d010049090300d043008049090327b0c9010088c9032cf21a028188884508103f008188c84302c27603010888c427b0c9010088c90300d04300804909031300cd010089090327b0c9010088c90334803d010049090300d043008049090327b0c9010088c9032cf21a028188884508103f008188c84300730603010888c427b0c9010088c90300d04300804909031300cd010089090327b0c9010088c90334803d010049090300d043008049090327b0c9010088c9032cf21a028188884508103f008188c84300d30603010888c427b0c9010088c90300d04300804909031300cd010089090327b0c9010088c90334803d010049090300d043008049090327b0c9010088c9032cf21a028188884508103f008188c84338e0d901808889030280e902810888c500d043008048c94338e0d9018088890321811502810888c500d043008048c94338e0d9018088890321811502810888c500d043008048c94338e0d9018088890321811502810888c500d043008048c94300d043008048894316025f0280c8890400d04300804889430c02770280c8890400d04300804889430c02770280c8890400d04300804889430c02770280c889042f319b020088c9031e814502810848c500d043008048c9432f319b020088c9032580b503010848c500d043008048c9432f319b020088c9032580b503010848c500d043008048c9432f319b020088c9032580b503010848c500d043008048c9433e5342018089090314a2770280c849043e5342018089090320c22f0300c849043e5342018089090320c22f0300c849043e5342018089090320c22f0300c8490400d0430080484a432ab156028107498400d0430080484a4300f043028107498000d0430080484a430ae2ee028107498400d0430080480a830f0113028147498500d0430080480a831e2049028147498000d0430080480a83068110028147498500d0430080480a832520bd028107498500d0430080480a8300f043028107498100d0430080480a832ba0cd028107498500d0430080480a8303010c028147498500d0430080480a831e2049028147498100d0430080480a830c810b028147498500d0430080484a4300b043010047ca832ab156028107498400d0430080484a4300b043010047ca8300f043028107498000d0430080484a4300b043010047ca830ae2ee028107498400d0430080484a4316221d018087ca432ab156028107498400d0430080484a4316221d018087ca4300f043028107498000d0430080484a4316221d018087ca430ae2ee028107498400d0430080484a830400d1028107498500d0430080484a8300f043028107498200d0430080484a832480d5028107498500d0430080484a430380d9028107498500d0430080484a43013073028107498000d0430080484a432420dd028107498500d0430080484a433cd342018087ca432c00c5030107498500d0430080484a433cd342018087ca432cb0e0030107498000d0430080484a433cd342018087ca430500c1030107498500d0430080484a4313d0810180c7ca452ab156028107498400d0430080484a4313d0810180c7ca4500f043028107498000d0430080484a4313d0810180c7ca450ae2ee028107498400d0430080484a83139098018087ca83241216030187498500d0430080484a83139098018087ca832e10db030187498000d0430080484a83139098018087ca8302a217030187498500d0430080484a83139098018087ca830400d1028107498500d0430080484a83139098018087ca8300f043028107498200d0430080484a83139098018087ca832480d5028107498500d0430080484a431d93ed0200c7ca431732a5038147498400d0430080484a431d93ed0200c7ca433f8028038147498000d0430080484a431d93ed0200c7ca43008365038147498429710d02810749850130730281074981000111028107498500d0430080488a0329f105020107ca0535c3ab038147498400d0430080488a0329f105020107ca053f8028038147498100d0430080488a0329f105020107ca051400ab038147498500d0430080488a4316e21c0200c7ca430200ad038147498500d0430080488a4316e21c0200c7ca433f8028038147498200d0430080488a4316e21c0200c7ca4307f0bc038147498500d0430080484a031881a5020107ca05073216038187494500d0430080484a031881a5020107ca052110db038187494000d0430080484a031881a5020107ca0507a21603818749452801250281074985013073028107498127812d028107498500d0430080484a432c60e10200c7ca042ab156028107498400d0430080484a432c60e10200c7ca0400f043028107498000d0430080484a432c60e10200c7ca040ae2ee028107498400d0430080488a4328b0bf020087ca430202ee038107498400d0430080488a4328b0bf020087ca430c20a9038107498000d0430080488a4328b0bf020087ca430ce2d6038107498400d0430080488a431992bc020087ca430c72d6038107498400d0430080488a431992bc020087ca430c20a9038107498100d0430080488a431992bc020087ca430c02d6038107498400d0430080484a432092df020087ca432c00c5030107498500d0430080484a432092df020087ca432cb0e0030107498000d0430080484a432092df020087ca430500c1030107498500d0430080484a433fc3eb018107ca442ab156028107498400d0430080484a433fc3eb018107ca4400f043028107498000d0430080484a433fc3eb018107ca440ae2ee028107498400d0430080484a8317708f0180c7ca850400d1028107498500d0430080484a8317708f0180c7ca8500f043028107498200d0430080484a8317708f0180c7ca852480d5028107498500d0430080480a83263086010047ca830f0113028147498500d0430080480a83263086010047ca831e2049028147498000d0430080480a83263086010047ca83068110028147498500d0430080484a8300b043010047ca830400d1028107498500d0430080484a8300b043010047ca8300f043028107498200d0430080484a8300b043010047ca832480d502810749850c00d1060046c9c315e3f81381c080841363f81401c0808410d3f81481c080840e33f81501c080840b83f81581c080841963de148240404416c3de15024040441163de15824040440ea3de16024040440bd3de168240404408811402814acc4502811402814acc453200b1040447cd8015a0b0040487cd001ff0af0404884d402370ae0484c88d402ed0b10484488d403ac0c00484088d403710c10483c88d4037a0c10403c88d401270c20483884d4014c0c20403884d000f20c70403480d4010f0d00483078d400af0c20503874d003280c10583c70d001b50af0684868d401e70af060486cd4011f0b00504874d401770c10484078d8005f0d0060306cd802a80d00702864d800d20d4080245ccc01380d00882c50bc01330c00884050a401610da080205cd800460d00683068d802760b10404484dc03080b10404488dc03100b10404480d8005d0b1048447cd802da0b10504474d801620c10584070d4035e0c10503c74d003680c10483c74cc03820c10403c78cc011e0c20483874cc01a40d00582c70d800f00d40702468e003270da0781c68e000df0a90380c7d1001080a704010790c01f20800582074fc026b0dc05810750c011e0e0038107d0c01080f00280c7d1050df0a90380c7d0c01080a704010790801f20800582074f8026b0dc05810750800af081038107d0801080f00280c7d0c53050e00400c851000af0e004010810c024b0da058207cfc026b0dc058107d0c00a008104010850c01080f00280c8510508b0dc03814910c01010dd04010891002560da05820850001170dc05810851000400dc03814910c000f0470280c9114029108f020189d00021f0db03018950801f50d00402c8cf402140d00382c90f4029108f020189d0012760dc020109d08002603101818a0fc02520a60302094fc01fd0d00402c8cf0021b0d00382c90f0029108f020189cfc117708901810a104002603101818a0f802520a60302094f801ec0d00482c8cf002c40d00382894f4029108f020189cf8117708901810a10003fc00d0181c9cf402520a60302090f4035a0d00482888f0037c0d00302894f0000f04a0201c98f40177089018109d0003fc00d0181c9cf002520a60302090ec033f0d00482888ec03750d00382890ec000f04a0201c98f00177089018109cfc02e70db0281890f002520a6030208ce803da0d90482484e800240da038248ce803230c90201894f003210e00280c90fc000f04a0201c90ec12520a60302088e810220d10482480e802960da0382088ec000f04a0201c90ec01770890181094f8008c081030147cf000320dc0381478f003630d00482878e4028e0da040207cec008c081030147cf011330e00200c7cf802fa0e00301078f800320dc0381474f403480d00482870e802850da0482070f0008c0810301478f423940e00280c74fc009a0be0301074fc00320dc0381470f801750d10502468ec01e40db0501868f800a80810381070fc03940e00280c710000d80a90380c6910007e08104014650802280da060205cfc010b0dc060105d0c00bd08103810690c000f0470280c691010df0a90380c6d10107e08104014690811710da0602060fc00af0dc06010610c00af081038106d0c11080f00280c6d1050df0a90380c751000d90dd04010710c01f2080058206cfc126b0dc058106d0c00af08103810750c01080f00280c751050b70e003810790800920dc03814790803a80d9058246cf402fa0db058146d04005c0dc03814790402910950200c790c00a8081038106cf810390dc038146cf8036c0d00482868e801f80db0481868f800a8081038106cf822910950200c6cfc00c90a90381064f0006a0dc0381460ec03bd0d9050245ce001da0db050185cec00c90a90381064f012910950200c64f403e40ca028145cd000e00be0281c58cc02c10da0501c54cc01880db0401c54cc00070dc028145cd003fb0db020145cd0019e0db0201c50bc01900db0281c50bc03c70d90502448bc03db0d00402448bc03e40c30281c50bc00800b20181450bc43d12160181c50a431330b00202050a401180d004830489c015b0cf03030489c013b2830201c50a443fc0b10181450a443fa0a6028185ce000cb0db0301c58dc01430db0581c58dc01ee0db0501858e003fa0a6028185ce0102603c018145ce0038e0e00300c68fc02010db0401860f003510d00482864e803780da0481c64f402720e00301068f802910950200c68fc00df0a90380c851401080a704010811002160a6058207d00026b0dc058107d1000af08103810851001080f00280c851450df0a90380c891401080a704010851002160a6058208100026b0dc05810811000af08103810891001080f00280c891450df0a90380c811001080a7040107d0c02160a60582078fc126b0dc05810790c00af08103810810c01080f00280c811050df0a90380c7d1001080a704010790c01f20800582074fc226b0dc05810750c00af081038107d0c01080f00280c7d1050df0a90380c751001080a704010710c02400da058206cfc026b0dc058106d0c00af08103810750c01080f00280c751050df0a90380c710810f90dd040106d0401380db0581c68f8026b0dc05810690400af08103810710421080f00280c710850df0a90380c750011380a70401070fc00c00db0581c6cf0026b0dc058106cfc00af0810381074fc21080f00280c750050d80a90380c74fc11380a70401070f800c00db0581c6cec026b0dc058106cf800bd0810381074f8100f0470280c74fc00d80a90380c78fc11380a70401074f812580a60581c70ec026b0dc0581070f800bd0810381078f8100f0470280c78fc00df0a90380c74f802d30dc0401070f402580a60581c6ce8126b0dc058106cf400af0810381074f401080f00280c74f850df0a90380c70f8107e081040146cf0217d0db0581c68e8026b0dc0581068f401400a70381070f401080f00280c70f8531b0e00300c68f8004e0dc0381468f0014e0db0581c60e8026b0dc0581060f401090e00381068f401080f00280c68f853140e00380c68f003f30db0401468e8014e0db0581c60e0026b0dc0581060ec012c0e00381068ec01080f00280c68f050af05d040387ce400af05d040387ce410af05d040387ce42008099040387ce40008099040387ce4127804e0403c7cdc027804e0403c7cdc127804e0403c7cdc21160980403c7cdc01160980403c7cdc128004e0403c84e0028004e0403c84e0128004e0403c84e023560950403c84e003560950403c84e013af03e0484088e003af03e0484088e013af03e0484088e023440950484088e003440950484088e0104205d0483888e0004205d0483888e0104205d0483888e0229c0980483888e0029c0980483888e0118d06a0483488e0018d06a0483488e0118d06a0483488e022050990483488e002050990483488e0109806b0483088e0009806b0483088e0109806b0483088e0232a0a50483088e0032a0a50483088e0121606a0403488dc021606a0403488dc121606a0403488dc213909a0403488dc013909a0403488dc10d706b0483084dc00d706b0483084dc10d706b0483084dc23180a50483084dc03180a50483084dc10e006b0403084d800e006b0403084d810e006b0403084d8233c0a50403084d8033c0a50403084d810e806b0403080d800e806b0403080d810e806b0403080d823440a50403080d803440a50403080d813770760402c78d803770760402c78d813770760402c78d820920a60402c78d800920a60402c78d812720760403074d802720760403074d812720760403074d8234c0a50403074d8034c0a50403074d813f506a0483070dc03f506a0483070dc13f506a0483070dc23210a50483070dc03210a50483070dc11fa04e0583c68e001fa04e0583c68e011fa04e0583c68e022b30970583c68e002b30970583c68e0120504e0503c6ce0020504e0503c6ce0120504e0503c6ce0206d0980503c6ce0006d0980503c6ce0133e03f0483c74e0033e03f0483c74e0133e03f0483c74e020810980483c74e000810980483c74e0105d05d0483878e0005d05d0483878e0105d05d0483878e023ea0980483878e003ea0980483878e0133a0760582c6cdc033a0760582c6cdc133a0760582c6cdc20480a60582c6cdc00480a60582c6cdc11d20770682464dc01d20770682464dc11d20770682464dc21e30a60682464dc01e30a60682464dc118e077070245ccc018e077070245ccc118e077070245ccc21ab0a6070245ccc01ab0a6070245ccc107e0770702850bc007e0770702850bc107e0770702850bc211e0a60702850bc011e0a60702850bc10d104e0683c50a400d104e0683c50a410d104e0683c50a420a20970683c50a400a20970683c50a4120f0800781c5cdc020f0800781c5cdc120f0800781c5cdc22900a60781c5cdc02900a60781c5cdc10e90770602868e000e90770602868e010e90770602868e0212c0a60602868e0012c0a60602868e0109705d0403884e8009705d0403884e8109705d0403884e820000990403884e800000990403884e810b705d0403888e800b705d0403888e810b705d0403888e820200990403888e800200990403888e8107705d0403880e4007705d0403880e4107705d0403880e4225d0980403880e4025d0980403880e4104b05d048387ce4004b05d048387ce4104b05d048387ce423f3098048387ce403f3098048387ce4139a05c0503874e4039a05c0503874e4139a05c0503874e423c40980503874e403c40980503874e4138505c0583870dc038505c0583870dc138505c0583870dc22470980583870dc02470980583870dc112006a0503474d8012006a0503474d8112006a0503474d821e90990503474d801e90990503474d8117b06a0483474d4017b06a0483474d4117b06a0483474d421fc0990483474d401fc0990483474d411f606a0403478d401f606a0403478d411f606a0403478d422170990403478d402170990403478d410b306b0483074d400b306b0483074d410b306b0483074d423330a50483074d403330a50483074d411000770582870dc01000770582870dc11000770582870dc20b00a60582870dc00b00a60582870dc11800770702468e001800770702468e011800770702468e021b90a60702468e001b90a60702468e0121e0800781c68e0021e0800781c68e0121e0800781c68e0229f0a60781c68e0029f0a60781c68e0124106a0383480d8024106a0383480d8124106a0383480d8221e06a0383480d8021e06a0383480d8121e06a0383480d8226d0cf0383480d801be06a0403480d801be06a0403480d811be06a0403480d820a528f0280c88d4412528f0280c88d443fa0870281888d803fa0870281888d810f105d0383880d000f105d0383880d010f105d0383880d020150320383880d000150320383880d010150320383880d020b90c20383880d0008f05d0403880d0008f05d0403880d0108f05d0403880d020a528f0280c88cc412528f0280c88cc43fa08d0281888d003fa08d0281888d010ea05d0383888d400ea05d0383888d410ea05d0383888d420e305d0383888d400e305d0383888d410e305d0383888d4225f0c20383888d400a705d0403888d400a705d0403888d410a705d0403888d420a528f0280c88d0412528f0280c88d0437421a0281888d4536f21a0281888d4526804e0403c88d4026804e0403c88d4126804e0403c88d4229004e0403c88d4029004e0403c88d4129004e0403c88d4238a0c10403c88d4025f04e0483c88d4025f04e0483c88d4125f04e0483c88d420a528f0280c88d0412528f0280c88d041f92170301888d451ff2170301888d450dc05d0383888d400dc05d0383888d410dc05d0383888d420c705d0383888d400c705d0383888d410c705d0383888d422660c20383888d403e805c0483888d403e805c0483888d413e805c0483888d420a528f0280c88d0412528f0280c88d0436021a0281888d4535b21a0281888d450ce05d0383888d400ce05d0383888d410ce05d0383888d420d505d0383888d400d505d0383888d410d505d0383888d421540c20383888d403df05c0483888d403df05c0483888d413df05c0483888d420a528f0280c88d0412528f0280c88d0436021a0281888d4535b21a0281888d4523306a0383488d4023306a0383488d4123306a0383488d4223a06a0383488d4023a06a0383488d4123a06a0383488d422cc0cf0383488d4019e06a0403488d4019e06a0403488d4119e06a0403488d420a528f0280c88d0412528f0280c88d0400d0a70301888d4000d0a70301888d4101d0510303488d4001d0510303488d4101d0510303488d4226406a0303488d4026406a0303488d4126406a0303488d4215b0c20303488d4020e06a0403488d4020e06a0403488d4120e06a0403488d420a528f0280c88d0412528f0280c88d0400d0a70301888d4000d0a70301888d4127a0760383084d4027a0760383084d4127a0760383084d422810760383084d402810760383084d412810760383084d423200cf0383084d400a106b0483084d400a106b0483084d410a106b0483084d420a528f0280c88d0412528f0280c88d040860810301488d400860810301488d410f006b0383084d000f006b0383084d010f006b0383084d022880760383084d002880760383084d012880760383084d021310d00383084d0000b06c0403084d0000b06c0403084d0100b06c0403084d020a528f0280c88cc412528f0280c88cc40860810301488d000860810301488d013870760382c80d403870760382c80d413870760382c80d4238e0760382c80d4038e0760382c80d4138e0760382c80d4220d0d00382c80d4037f0760402c80d4037f0760402c80d4137f0760402c80d420a528f0280c88d0412528f0280c88d0422f0fd0281088d450101010281088d450080780482478d400080780482478d410080780482478d4215c0800482478d4015c0800482478d4115c0800482478d423d10d90482478d4014a0800482478d4014a0800482478d4114a0800482478d421100e80280c88d051170e00280c88d050000830281488d400000830281488d4134f0760502c74d0034f0760502c74d0134f0760502c74d023450760502c74d003450760502c74d013450760502c74d021ba0d00502c74d0036d0760502c74d0036d0760502c74d0136d0760502c74d021100e80280c88d051170e00280c88d052571080281488d051001030281488d0507906b0583070d0007906b0583070d0107906b0583070d0208406b0583070d0008406b0583070d0108406b0583070d020980d00583070d0006e06b0583070d0006e06b0583070d0106e06b0583070d021100e80280c88d051170e00280c88d0532421a0281888d0531f21a0281888d051a004e0603c6cd401a004e0603c6cd411a004e0603c6cd421ac04e0603c6cd401ac04e0603c6cd411ac04e0603c6cd422330c10603c6cd4019404e0603c6cd4019404e0603c6cd4119404e0603c6cd420662470300c84d0406c2470300c84d040000a70381880d400000a70381880d411c304e0583c70d401c304e0583c70d411c304e0583c70d421ef04e0583c70d401ef04e0583c70d411ef04e0583c70d422c10c10583c70d401ce04e0583c70d401ce04e0583c70d411ce04e0583c70d420722470300c84d040782470300c84d040dc2160381880d450ff2160381880d453f105c0483878d403f105c0483878d413f105c0483878d4205405d0483878d4005405d0483878d4105405d0483878d421150c20483878d403d605c0483878d403d605c0483878d413d605c0483878d421300b80280c88d050e52bf0280c88d040920810301484d400920810301484d4114506a0483478d8014506a0483478d8114506a0483478d8216906a0483478d8016906a0483478d8116906a0483478d820e10c70483478d8012a06a0483478d8012a06a0483478d8112a06a0483478d820a528f0280c88d4412528f0280c88d440920810301484d820a50dc0301484d80210077060246cd80210077060246cd81210077060246cd821f8077060246cd801f8077060246cd811f8077060246cd8239c0d9060246cd80234077060246cd80234077060246cd81234077060246cd821300b80280c88d450e52bf0280c88d440670a70301884d800670a70301884d812560800681c68d802560800681c68d812560800681c68d822490800681c68d802490800681c68d812490800681c68d8235f0da0681c68d801b50800682068d801b50800682068d811b50800682068d821300b80280c88d450e52bf0280c88d440790a70301884d800790a70301884d811b80770682468cc01b80770682468cc11b80770682468cc21c50770682468cc01c50770682468cc11c50770682468cc20fe0d40682468cc01df0770682468cc01df0770682468cc11df0770682468cc21300b80280c88d450e52bf0280c88d440130a70301884d800130a70301884d810dc0770682860c000dc0770682860c010dc0770682860c020c20770682860c000c20770682860c010c20770682860c0225f0d00682860c000b50770682860c000b50770682860c010b50770682860c020881700200c88d050781800200c88d0507f0a70281880d0007f0a70281880d0118105d070345cb0018105d070345cb0118105d070345cb0218f05d070345cb0018f05d070345cb0118f05d070345cb0226d0c2070345cb0000f051070385cac000f051070385cac100f051070385cac21481e50201088d052370f50201088d053b00a60301c7cc803b00a60301c7cc8123b0800701c64d8023b0800701c64d8123b0800701c64d8222d0800701c64d8022d0800701c64d8122d0800701c64d823440da0701c64d801a70800702064d801a70800702064d811a70800702064d821402770280c88d440c52770280c88d440190a70301884d800190a70301884d81228077060246cd80228077060246cd81228077060246cd821ec077060246cd801ec077060246cd811ec077060246cd823820d9060246cd802a30760682c68d802a30760682c68d812a30760682c68d820a528f0280c88d4412528f0280c88d4406d0a70301884d8006d0a70301884d8122506a0383488dc022506a0383488dc122506a0383488dc224806a0383488dc024806a0383488dc124806a0383488dc22d30cf0383488dc01ce06a0403488dc01ce06a0403488dc11ce06a0403488dc20a528f0280c88d8412528f0280c88d840860810301488dc209f0dc0301488dc01fe06a0403488dc01fe06a0403488dc11fe06a0403488dc21a606a0403488dc01a606a0403488dc11a606a0403488dc20fa0c70403488dc018406a0483488dc018406a0483488dc118406a0483488dc20a528f0280c88d8412528f0280c88d840860810301488dc209f0dc0301488dc025606a0383484d8025606a0383484d8125606a0383484d8225d06a0383484d8025d06a0383484d8125d06a0383484d822740cf0383484d8020606a0403484d8020606a0403484d8120606a0403484d821402770280c88d440c52770280c88d440d01030281488d850781000281488d851de06a0403480d801de06a0403480d811de06a0403480d8219606a0403480d8019606a0403480d8119606a0403480d823f70c20403480d801c606a0403480d801c606a0403480d811c606a0403480d820a528f0280c8cd4412528f0280c8cd440070a70301888d800070a70301888d8113306a0483478d8013306a0483478d8113306a0483478d8214e06a0483478d8014e06a0483478d8114e06a0483478d821060c60483478d8013c06a0483478d8013c06a0483478d8113c06a0483478d820a528f0280c88d4412528f0280c88d443fa0870281888d803fa0870281888d8110c06a0503474d4010c06a0503474d4110c06a0503474d4210206a0503474d4010206a0503474d4110206a0503474d420cd0c50503474d400f806a0503474d400f806a0503474d410f806a0503474d420a528f0280c88d0412528f0280c88d043960a60381c80d403960a60381c80d4108f06b0483078d0008f06b0483078d0108f06b0483078d020bc06b0483078d000bc06b0483078d010bc06b0483078d020b70d00483078d000c506b0483078d000c506b0483078d010c506b0483078d021300b80280c88d050e52bf0280c88d043aa0a60301c84d003aa0a60301c84d011ee06a0403478cc01ee06a0403478cc11ee06a0403478cc21ae06a0403478cc01ae06a0403478cc11ae06a0403478cc20ea0c70403478cc01b606a0403478cc01b606a0403478cc11b606a0403478cc21300b80280c84cc50e52bf0280c84cc417015f0301c80cc533015d0301c80cc522c06a038347ccc022c06a038347ccc122c06a038347ccc224f06a038347ccc024f06a038347ccc124f06a038347ccc22da0cf038347ccc01d606a0403478cc01d606a0403478cc11d606a0403478cc21300b80280c84cc50e52bf0280c84cc40681f80281c84cc50d81f70281c84cc526a0760403078cc026a0760403078cc126a0760403078cc200006e0403078cc000006e0403078cc100006e0403078cc20770d00403078cc00aa06b0483074cc00aa06b0483074cc10aa06b0483074cc21300b80280c84c850e52bf0280c84c842f721a0281884cc52f221a0281884cc52550770502474d802550770502474d812550770502474d8224b0770502474d8024b0770502474d8124b0770502474d823b30d90502474d802400770582470d802400770582470d812400770582470d821f42470280c88d441f92470280c88d440730a70301884d800730a70301884d810cf077068286cd800cf077068286cd810cf077068286cd820a8077068286cd800a8077068286cd810a8077068286cd823270d0068286cd8009a0770702868d8009a0770702868d8109a0770702868d820301c80200c8cd850381c00200c8cd853f30a60381884d803f30a60381884d81172077070246cd80172077070246cd81172077070246cd82164077070246cd80164077070246cd81164077070246cd823820d0070246cd8017b0800782068d8017b0800782068d8117b0800782068d821300b80280c88d850e52bf0280c88d8438f0a60381c88d8038f0a60381c88d8137d0b006044a4d800420c208838a4c8018b0b007044a4cc01f30af06048a4cc01290b005048a4cc00ad0b005048a4cc01500b005048a4cc02240ae0504ca4cc02580ad05050a4cc02c60ad05050a4c800c20ad05850a4cc00840ac06054a4cc01380ab07054a4cc02b80ad07050a4cc02080ae0784ca4cc02220c10883ca4cc00530c208838a4cc02170ae0684ca4d0038f0ad07050a4d002a90ad07850a4d000900b105044a4d8026c0b105044a0d000c10c10604098c803e40cf0703090d0029a0d00702890cc01490d00702c90c802863130181ca0f831ec0df0201c98ec51281f60281c90e451281f60281c90d851281f60281c90d853050e00400cb51000b608103810b10c00a008104010b50c11080f00280cb510529108902018d8e803890b90281cd4ec029108902018d8e810ab0dc02014d8ec01330b80201cccf400550dc03814c0fc010f0c60281cc8f402db0dc02810c90003ec0a603818b8fc03eb0db04014b50003ec0a603818b8fc100f0470280cb90822d10e003010b50403e30db04014ad0000bd08103810b104210f0cd0280cb50802d70e003010b10401080a704010a90410990dc03014b10003000e002810b10400630dc03814ad0000000dc03814a50002180db03818acfc020819d02010ad0050d10dd04010a90800470dc03814a504038e0db04014a904008d0a702014a90400d00a90400ca91000b608103810a50c009808104010a90c00481b00200ca91050d00a90400ca90c00b608103810a508009808104010a90800481b00200ca90c50d00a90400cad1000b608103810a90c009808104010ad0c10481b00200cad1051100a704010b51001170e003810b11001100a704010b51011080f00280cb51450780dc03014c50c00710dc03814bd0c01250e003810c11003e30e00280cc114028b0b80201ccd00002b0dc03814c10801950db0281cc90000bb0dc02010cd0c03230c20201cd0fc034e0b10281cccfc02ec0db02818cd00013d08101810d508019a0db0201cd8ec029d0da03020d4ec01180aa02818d4f0002603701814dcf402f60db02018d8ec01590db0301cd0ec02f10db02818d4ec013d08501810dcf403050e00400cb91000b608103810b50c00a008104010b90c10501a80200cb91050f10dd04010bd1000e90dd04010b910030e0db04014bd0c000f0470280cbd1403160db04014c10c02cb0dc04010bd1002090db04018c10802910950200cc11413050e00400cad1000b608103810a90c00a008104010ad0c10481b00200cad1053050e00400ca10800b6081038109d0400a008104010a10420481b00200ca108530d0e00380c98fc00b60810381094f811400a70381098f810901680200c98fc50d00a90400c90f410b60810381090f021100e00381090f000901680200c90f450980810401090e420b60810381090e421100e00381090e400901680200c90e850980810401090e4226b0e00381090e401100e00381090e400901680200c90e853fc0130180ca1085027094020109d0400260310180ca10850260340180ca10850a81500200c98fc52f00280281094f801581d50201098f850901680200c98fc510f2a70280c90f442f00280281090f011b81750281090f050901680200c90f4523816d0281090e452f00280281090e411b81750281090e450901680200c90e8523816d0281090e452f00280281090e421b81750281090e450901680200c90e8531f02e05844a8d8031f02e05844a8d8131f02e05844a8d8216108905844a8d8016108905844a8d812fe06a07030b0d002fe06a07030b0d012fe06a07030b0d023740a407030b0d003740a407030b0d0136105c06038acd8036105c06038acd8136105c06038acd8222f09806038acd8022f09806038acd813ae05c05038acdc03ae05c05038acdc13ae05c05038acdc239209805038acdc039209805038acdc108705d04038acdc008705d04038acdc108705d04038acdc201009904038acdc001009904038acdc107f05d04038acdc007f05d04038acdc107f05d04038acdc206f05d04038acdc006f05d04038acdc106f05d04038acdc20bf05d04038acdc00bf05d04038acdc10bf05d04038acdc21440c204038acdc03b803e04840a8d803b803e04840a8d813b803e04840a8d8234d09504840a8d8034d09504840a8d810a703e04844a8d800a703e04844a8d810a703e04844a8d8209503e04844a8d8009503e04844a8d8109503e04844a8d4208c03e04844a8d4008c03e04844a8d4108c03e04844a8d422e40b104844a8d4033e02e05044a8d8033e02e05044a8d8133e02e05044a8d8238809305044a8d8038809305044a8d8111302c05848a8d8011302c05848a8d8111302c05848a8d8206208705848a8d8006208705848a8d8119302c05848b0d8019302c05848b0d8119302c05848b0d8205708705848b0d8005708705848b0d8103b01e05848b0d4003b01e05848b0d4103b01e05848b0d4204c08705848b0d4004c08705848b0d4130702e06044b0d4030702e06044b0d4130702e06044b0d4220208806044b0d4020208806044b0d4100005307038b0d0000005307038b0d0100005307038b0d0211e09807038b0d0011e09807038b0d0119d05d07034b0d0019d05d07034b0d0119d05d07034b0d0218609907034b0d0018609907034b0d0131302e06044a8d8031302e06044a8d8131302e06044a8d8215508906044a8d8015508906044a8d813f302d06044acdc03f302d06044acdc13f302d06044acdc213d08906044acdc013d08906044acdc139e02d06844acdc039e02d06844acdc139e02d06844acdc21db08806844acdc01db08806844acdc10b003e04844a8d800b003e04844a8d810b003e04844a8d8226f09404844a8d8026f09404844a8d8121904e0503ca0d8021904e0503ca0d8121904e0503ca0d820770980503ca0d800770980503ca0d8125f05d0603498d4025f05d0603498d4125f05d0603498d421c70990603498d401c70990603498d411aa0770702490dc01aa0770702490dc11aa0770702490dc21c70a60702490dc01c70a60702490dc11990800702090d401990800702090d411990800702090d422080a60702090d402080a60702090d4119c0770702490d0019c0770702490d0119c0770702490d021d50a60702490d001d50a60702490d012863130181ca0f832863210181ca0f832862f70181ca0f833482d30181ca0f832ea2d00181ca0f831ec0df0201c98ec50bf0c50201c98ec509c0b10201c98ec507f0ad0201c98ec517b2a10201c98ec41281f60281c90e451381e60281c90e452a81e50281c90e452481e10281c90e452501d90281c90e451281f60281c90d851781d10281c90d852a81e50281c90d852481e10281c90d852501d90281c90d851281f60281c90d851781d10281c90d852a81e50281c90d852481e10281c90d852501d90281c90d851bb09906034a4d801bb09906034a4d813660a407030a4c803660a407030a4c8119409906834a4cc019409906834a4cc129109805838a4cc029109805838a4cc13ba09805038a4cc03ba09805038a4cc13ce09805038a4cc03ce09805038a4cc13a609805038a4cc03a609805038a4cc137b09405040a4cc037b09405040a4cc103403005044a4cc003403005044a4cc103403005044a4c820530b105044a4c8016c08905844a4cc016c08905844a4cc114908906044a4cc014908906044a4cc128408206848a4cc028408206848a4cc131b09507040a4cc031b09507040a4cc132909507040a4cc032909507040a4cc10e609d07830a4cc00e609d07830a4cc12810a407830a4cc02810a407830a4cc12a60970683ca4d002a60970683ca4d011f508806844a4d001f508806844a4d011cd08807044a4d001cd08807044a4d011df09905034a4d801df09905034a4d813e109804838a4d003e109804838a4d012be0970503ca0c802be0970503ca0c812df0a50603098d002df0a50603098d011640a60602898cc01640a60602898cc103c0a60602c98c8003c0a60602c98c813b30ad0100ca4f832620ad0100ca4f831150ab0100ca0ec30250aa0100ca0ec303c3960181098e443e73940181098e4403c3960181098d843e73940181098d8403c3960181098d843e73940181098d843d20870a04478a403d20870a04478a41014088098447ca80014088098447ca8118e0880904484ac018e0880904484ac102e0870904890b0002e0870904890b0117b0880984498bc017b0880984498bc108308609048a0c0008308609048a0c0133108109050a0bc033108109050a0bc134308109050a0bc034308109050a0bc130d08109050a0bc030d08109050a0bc131f08109050a0bc031f08109050a0bc135f0810984c9cc0035f0810984c9cc013be0870a04498c803be0870a04498c813a90870a84494cc03a90870a84494cc10ae0850a84894c800ae0850a84894c813940870a84494cc03940870a84494cc11fd0800482098bc01fd0800482098bc11fd0800482098bc21df3de0602490bc43950760b02888b403950760b02888b413950760b02888b420e20d40702494c0003f0810481898bc003f0810481898bc103f0810481898bc232a3f0070208cb843c00760a82890b403c00760a82890b413c00760a82890b423520da0681c98bc003f0810481898bc003f0810481898bc103f0810481898bc203c3de0602890b042eb03f0983c94a802eb03f0983c94a812eb03f0983c94a822340da0602098b802d500005860a06802d500005860a06812d500005860a06823f23d705060a068300e000070809c4c000e000070809c4c100e000070809c4c20a40c205838949402c900006060a06802c900006060a06812c900006060a06822f318205064a0643000000070849c480000000070849c481000000070849c4820000d006830949c03210170685ca06c03210170685ca06c13210170685ca06c232f3d705860a068301c0000787c9c50001c0000787c9c50101c0000787c9c5023120cf0703094a4001d01f06848a080001d01f06848a080101d01f06848a08022f60e406058a070427e000078609c6c027e000078609c6c127e000078609c6c23240760582ca0c003240760582ca0c013240760582ca0c022c23dd0582ca0c042ef06a078309cc002ef06a078309cc012ef06a078309cc021df01805058a0c001df01805058a0c011df01805058a0c022d10fc050589cc0402b0000707898c0002b0000707898c0102b0000707898c0232e0170585c9cc0032e0170585c9cc0132e0170585c9cc023111130505c98c040390000707894c000390000707894c010390000707894c022e601805054a0b802e601805054a0b812e601805054a0b823412de0505c9cc03113000060689cb40113000060689cb41113000060689cb4211606a05034a0a8011606a05034a0a8111606a05034a0a823583c405034a0ac414604e0683c9ca4014604e0683c9ca4114604e0683c9ca4217c01906850a078017c01906850a078117c01906850a07821073f105838a0903104000078689c640104000078689c641104000078689c64229c00007860bca4029c00007860bca4129c00007860bca422ba00007860bca402ba00007860bca412ba00007860bca422de03e06840bca402de03e06840bca412de03e06840bca4237c03e06840bca4037c03e06840bca4137c03e06840bca420eb04e0683cb8a800eb04e0683cb8a810eb04e0683cb8a8211204e0683cb8a8011204e0683cb8a8111204e0683cb8a823db01707858b8a403db01707858b8a413db01707858b8a423cc01707858b8a403cc01707858b8a413cc01707858b8a4208e0000a868b8a4008e0000a868b8a4108e0000a868b8a420a30000a868b8a400a30000a868b8a410a30000a868b8a421c100008064b8a401c100008064b8a411c100008064b8a421b100008064b8a401b100008064b8a411b100008064b8a420d00ac08064b8a4025f07705024bca4025f07705024bca4125f07705024bca421f00a605024bca401f00a605024bca412ab00007860bca402ab00007860bca412ab00007860bca4228d00007860bca4028d00007860bca4128d00007860bca423a901709058b8a403a901709058b8a413a901709058b8a4239701709058b8a4039701709058b8a4139701709058b8a423ea0ac09058b8a400670000786cbca400670000786cbca410670000786cbca421060ab0786cbca402da01806054bcac02da01806054bcac12da01806054bcac22ce01806054bcac02ce01806054bcac12ce01806054bcac22be01808054bca802be01808054bca812be01808054bca822fd08108054bca802fd08108054bca8104404e0703cbcb8004404e0703cbcb8104404e0703cbcb8228c0c10703cbcb8036806a06830bcac036806a06830bcac136806a06830bcac22720a506830bcac02720a506830bcac12510870a844bcb802510870a844bcb813f707604028bca403f707604028bca413f707604028bca423340000f85cb4b803340000f85cb4b813340000f85cb4b822f60000f85cb4b802f60000f85cb4b812f60000f85cb4b823150000f85cb4b803150000f85cb4b813150000f85cb4b823b60ac0f85cb4b8007008609848b4b8007008609848b4b813050170705cb8a803050170705cb8a813050170705cb8a823130170705cb8a803130170705cb8a813130170705cb8a8216405d07834b8a4016405d07834b8a4116405d07834b8a4215505d07834b8a4015505d07834b8a4115505d07834b8a423ed0000785cb8a803ed0000785cb8a813ed0000785cb8a822f60170785cb8a802f60170785cb8a812f60170785cb8a8229e01808054b8a8029e01808054b8a8129e01808054b8a822ae01808054b8a802ae01808054b8a812ae01808054b8a8207601a0a04cb4b8007601a0a04cb4b8107601a0a04cb4b821d10000d860b4b801d10000d860b4b811d10000d860b4b821ec0000d860b4b801ec0000d860b4b811ec0000d860b4b822070000d860b4b802070000d860b4b812070000d860b4b822220000d860b4b802220000d860b4b812220000d860b4b823dd0000805cb8a803dd0000805cb8a813dd0000805cb8a822960810805cb8a802960810805cb8a811690000c064b4ac01690000c064b4ac11690000c064b4ac21990000c064b4ac01990000c064b4ac11990000c064b4ac224d02d0a044b4ac024d02d0a044b4ac124d02d0a044b4ac21fd02d0a044b4ac01fd02d0a044b4ac11fd02d0a044b4ac214108004824b8b8014108004824b8b8114108004824b8b8225f07705024bca4025f07705024bca4125f07705024bca421f00a605024bca401f00a605024bca413f707604028bca003f707604028bca013f707604028bca0226102d0a044b4b4026102d0a044b4b4126102d0a044b4b423e60870a044b4b403e60870a044b4b4127502d0a044b4b4027502d0a044b4b4127502d0a044b4b420000880a044b4b400000880a044b4b4107008609848b4b4007008609848b4b4121102d0a044b4b4021102d0a044b4b4121102d0a044b4b4222502d0a044b4b4022502d0a044b4b4122502d0a044b4b420fc01a0a048b4b400fc01a0a048b4b410fc01a0a048b4b4204701c0a048b4b4004701c0a048b4b4104701c0a048b4b4224d02d0a044b4a8024d02d0a044b4a8124d02d0a044b4a821fd02d0a044b4a801fd02d0a044b4a811fd02d0a044b4a8214108004824b8b4014108004824b8b4114108004824b8b421cf03f0983ca4a401cf03f0983ca4a411cf03f0983ca4a4206e03f0983ca4a4006e03f0983ca4a4106e03f0983ca4a4221803e07840b8a0021803e07840b8a0121803e07840b8a0222703e07840b8a0022703e07840b8a0122703e07840b8a0239004e09838a0a4039004e09838a0a4139004e09838a0a4237d04e09838a0a4037d04e09838a0a4137d04e09838a0a421f803e08040b0a001f803e08040b0a011f803e08040b0a021e803e08040b0a001e803e08040b0a011e803e08040b0a022400c008040b0a0025f07705024b89c025f07705024b89c125f07705024b89c21f00a605024b89c01f00a605024b89c102405007838b8a8002405007838b8a8102405007838b8a8203305007838b8a8003305007838b8a8103305007838b8a8213f03e0a040a4a0013f03e0a040a4a0113f03e0a040a4a0212b03e0a040a4a0012b03e0a040a4a0112b03e0a040a4a023280b10a040a4a0005b03f0983ca4a4005b03f0983ca4a4105b03f0983ca4a4220f0c10983ca4a403a304e09038a4a803a304e09038a4a813a304e09038a4a8207b04f09038a4a8007b04f09038a4a8107b04f09038a4a820350410783cb8a400350410783cb8a410350410783cb8a422fa0960783cb8a402fa0960783cb8a411b905d07034bcb001b905d07034bcb011b905d07034bcb023150c207034bcb0017709907834b8ac017709907834b8ac13f707604028b8a003f707604028b8a013f707604028b8a023020cf08030acb003020cf08030acb003020cf08030acb003020cf08030acb003020cf08030acb000d609d08030acb000d609d08030acb000d609d08030acb000d609d08030acb010d609d08030acb013560a408030b4b003560a408030b4b010bf04f08038b0a800bf04f08038b0a810bf04f08038b0a820af04f08038b0a800af04f08038b0a810af04f08038b0a8209e04f08838a8a8009e04f08838a8a8109e04f08838a8a8208d04f08838a8a8008d04f08838a8a8108d04f08838a8a822fe03f0883cb8a402fe03f0883cb8a412fe03f0883cb8a4230f03f0883cb8a4030f03f0883cb8a4130f03f0883cb8a422ce06a08830b0b002ce06a08830b0b012ce06a08830b0b022df06a08030acb002df06a08030acb002df06a08030acb002df06a08030acb012df06a08030acb022f20cf08030acb0012005d09034acac012005d09034acac112005d09034acac212005d09034acac203d09909034acac003d09909034acac12d102d08044b49c02d102d08044b49c12d102d08044b49c21a008808044b49c01a008808044b49c119f03e09040b0a4019f03e09040b0a4119f03e09040b0a4218d03e09040b0a4018d03e09040b0a4118d03e09040b0a4216703e09840b0a0016703e09840b0a0116703e09840b0a0217a03e09840b0a0017a03e09840b0a0117a03e09840b0a0214108004824b4ac014108004824b4ac114108004824b4ac20840a70481478b400840a70481478b4100c0320484478b0000c0320484478b0100c0320484478b0201d0310484478b001e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4028f05d040347cb413e40a6040187cb403e40a6040187cb4126e03e0704074a8026e03e0704074a8126e03e0704074a8226003e0704074a8026003e0704074a8111f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb412610da0502078ac00e10dd040107cb400e302c0604878a800e302c0604878a810e302c0604878a821db0af0604878a801150770502874b401150770502874b411150770502874b423b80800581c74b003b80800581c74b013c70a60501874b801ba0db0581874b40070077070286cb00070077070286cb010840a70481478b800840a70481478b8100c032048447cb4000c032048447cb4100c032048447cb4201d031048447cb401e606a040347cb801e606a040347cb811e606a040347cb8228f05d040347cb8228f05d040347cb813e40a6040187cb803e40a6040187cb8126003e0704078ac225203e0704078ac025203e0704078ac125203e0704078ac20110c10704078ac011f0770482878b8011f0770482878b8111f0770482878b822c90a60401c7cb802c90a60401c7cb81206080048207cb000e10dd040107cb800ef02c0604878ac00ef02c0604878ac10ef02c0604878ac20000210604878ac01150770502874b801150770502874b811150770502874b823b80800581c78b423ad0800581c78b403c70a60501874bc01ba0db0581878b801480a6070286cb001480a6070286cb010840a70481478b800840a70481478b8100c0320484478b4000c0320484478b4100c0320484478b4201d0310484478b4017206a0483478b8017206a0483478b8117206a0483478b821f30990483478b801f30990483478b813e40a6040187cb803e40a6040187cb8126003e0704074ac225203e0704074ac025203e0704074ac125203e0704074ac20110c10704074ac03630760502c74b403630760502c74b413630760502c74b4222b0a60482078b4022b0a60482078b412060800482078b000e10dd040107cb800000210604874ac10000210604874ac23880810604874ac03880810604874ac10f50770582870b800f50770582870b810f50770582870b823ad0800581c74b413ad0800581c74b422ae0a60581c70b801c50db0581874b8008c0770702868b0008c0770702868b010840a7048147cb800840a7048147cb8100c032048447cb4000c032048447cb4100c032048447cb4201d031048447cb4017206a048347cb8017206a048347cb8117206a048347cb821f3099048347cb801f3099048347cb813e40a60401880b803e40a60401880b8126003e0704078ac225203e0704078ac025203e0704078ac125203e0704078ac20110c10704078ac03630760502c78b403630760502c78b413630760502c78b4222b0a6048207cb4022b0a6048207cb41206080048207cb000e10dd0401080b800000210604878ac10000210604878ac23880810604878ac03880810604878ac10f50770582874b800f50770582874b810f50770582874b823b80800581c78b423ad0800581c78b402ae0a60581c74b801c50db0581878b8008c077070286cb0008c077070286cb013db0a6048187cb803db0a6048187cb8100c032048447cb8000c032048447cb8100c032048447cb8201d031048447cb8006605d048387cb8006605d048387cb8106605d048387cb823d8098048387cb803d8098048387cb812c10a60401c80b802c10a60401c80b8126003e0704078b0225203e0704078b0025203e0704078b0125203e0704078b020110c10704078b003590760502c78b803590760502c78b813590760502c78b8222b0a6048207cb8022b0a6048207cb81206080048207cb4031e0db0401480b8010702c0604878b0010702c0604878b0110702c0604878b020a10b00604878b0032f0760582c74b8032f0760582c74b8132f0760582c74b823b80800581c78b823ad0800581c78b802ae0a60581c74b811c50db0581878b8008c077070286cb42054077070286cb400840a70481478b400840a70481478b4100c0320484478b0000c0320484478b0100c0320484478b0201d0310484478b0017206a0483478b4017206a0483478b4117206a0483478b421f30990483478b401f30990483478b413e40a6040187cb403e40a6040187cb4126003e0704074a8225203e0704074a8025203e0704074a8125203e0704074a820110c10704074a803630760502c74b003630760502c74b013630760502c74b0222b0a60482078b0022b0a60482078b012060800482078ac00e10dd040107cb401a902c0584874a801a902c0584874a811a902c0584874a8219e02c0584874a800f50770582870b400f50770582870b410f50770582870b423b80800581c74b023ad0800581c74b001af0db0581870b401d00db0501874b400540770702868ac10540770702868ac20840a70481478b800840a70481478b8101d031048447cb4101d031048447cb42278094048447cb40278094048447cb411e606a040347cb801e606a040347cb811e606a040347cb8228f05d040347cb8228f05d040347cb813e40a6040187cb803e40a6040187cb813d004d0703c74b003d004d0703c74b013d004d0703c74b023ec04d0703c74b003ec04d0703c74b0111f0770482878b8011f0770482878b8111f0770482878b822c90a60401c7cb802c90a60401c7cb8127b0da050207cb000e10dd040107cb8019e02c0584878ac119e02c0584878ac220f0870584878ac020f0870584878ac11150770502874b801150770502874b811150770502874b823b80800581c78b423ad0800581c78b403c70a60501874b813d10a60501878b80062077070286cb00062077070286cb010840a70481478b400840a70481478b4101d031048447cb0101d031048447cb02278094048447cb00278094048447cb011e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413d004d0703c74ac03d004d0703c74ac13d004d0703c74ac23ec04d0703c74ac03ec04d0703c74ac111f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb41206080048207cac00e10dd040107cb401dc02e0604478ac01dc02e0604478ac11dc02e0604478ac20470b10604478ac01150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b413d10a60501878b40062077070286cac0062077070286cac10840a70481478b400840a70481478b4100c032048447cb0000c032048447cb0100c032048447cb0201d031048447cb001e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413d004d0703c74ac03d004d0703c74ac13d004d0703c74ac23ec04d0703c74ac03ec04d0703c74ac111f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb41206080048207cac10e10dd040107cb403c302d0604478ac03c302d0604478ac13c302d0604478ac23cf02d0604478ac01150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b413d10a60501878b40062077070286cac228c0d0070286cac00840a70481478b400840a70481478b4100c032048447cb0000c032048447cb0100c032048447cb0201d031048447cb001e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413ec04d0703c74ac23de04d0703c74ac03de04d0703c74ac13de04d0703c74ac227e0c10703c74ac011f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb41206080048207cac20e10dd040107cb403cf02d0604478ac13cf02d0604478ac23db02d0604478ac03db02d0604478ac11150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b413d10a60501878b411480a6070286cac01480a6070286cac10840a70481478b400840a70481478b4100c032048447cb0000c032048447cb0100c032048447cb0201d031048447cb001e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413ec04d0703c74ac23de04d0703c74ac03de04d0703c74ac13de04d0703c74ac227e0c10703c74ac011f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb41206080048207cac231e0db040147cb0004602e0604478ac004602e0604478ac104602e0604478ac23650b00604478ac01150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b413d10a60501878b411480a6070286cac01480a6070286cac10840a70481478b400840a70481478b4100c032048447cb0000c032048447cb0100c032048447cb0201d031048447cb001e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413ec04d0703c78a823de04d0703c78a803de04d0703c78a813de04d0703c78a8227e0c10703c78a8011f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb41206080048207cb020e10dd040107cb403db02d0604478ac21d002e0604478ac01d002e0604478ac11d002e0604478ac21150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b413d10a60501878b411480a6070286cac01480a6070286cac10840a70481478b400840a70481478b4101d031048447cb0101d031048447cb02278094048447cb00278094048447cb011e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413d004d0703c78a803d004d0703c78a813d004d0703c78a823ec04d0703c78a803ec04d0703c78a8111f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb412210a6050207cb000e10dd040107cb403e702d0604478ac03e702d0604478ac13e702d0604478ac227f0b00604478ac01150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b411c50db0581878b401480a6070286cb001480a6070286cb010840a70481478b400840a70481478b4101d031048447cb0101d031048447cb02278094048447cb00278094048447cb011e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413d004d0703c74a803d004d0703c74a813d004d0703c74a823ec04d0703c74a803ec04d0703c74a8111f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb412210a60502078b010e10dd040107cb402fb02e0604478ac02fb02e0604478ac12fb02e0604478ac202f0b10604478ac01150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b411ba0db0581874b001560a6070286cb001560a6070286cb010840a70481478b400840a70481478b4101d031048447cb0101d031048447cb02278094048447cb00278094048447cb011e606a040347cb401e606a040347cb411e606a040347cb4228f05d040347cb4228f05d040347cb413e40a6040187cb403e40a6040187cb413d004d0703c74a803d004d0703c74a813d004d0703c74a823ec04d0703c74a803ec04d0703c74a8111f0770482878b4011f0770482878b4111f0770482878b422c90a60401c7cb402c90a60401c7cb412210a60502078b000e10dd040107cb402fb02e0604478ac02fb02e0604478ac12fb02e0604478ac202f0b10604478ac01150770502874b401150770502874b411150770502874b423b80800581c78b023ad0800581c78b003c70a60501874b411ba0db0581874b00070077070286cac22b60d0070286cac028f0760383074c4028f0760383074c4028f0760383074c4109a0a60382c74c4009a0a60382c74c4009a0a60382c74c4109a0a60382c74c4009a0a60382c74c4009a0a60382c74c4109a0a60382c74c4009a0a60382c74c4009a0a60382c74c4128f0760383074c0028f0760383074c0028f0760383074c0128f0760383074c0028f0760383074c0028f0760383074c0128f0760383074c4028f0760383074c4028f0760383074c4114109a0383474c0014109a0383474c0014109a0383474c0114109a0383474c0014109a0383474c0014109a0383474c0114109a0383474c0014109a0383474c0014109a0383474c0114109a0383474c8014109a0383474c8014109a0383474c8128f0760383074d0028f0760383074d0028f0760383074d0128f0760383078cc228f0760383078cc028f0760383078cc128f0760383074c8028f0760383074c8028f0760383074c8128f0760383074c8028f0760383074c8028f0760383074c8136702d0704474b4036702d0704474b4136702d0704474b4235b06a0683078b8035b06a0683078b8135b06a0683078b8234e06a0683078b8034e06a0683078b8134e06a0683078b8231a06a0683078b8031a06a0683078b8131a06a0683078b823c306a0683078b803c306a0683078b813c306a0683078b8202c0d00683078b8005204e0703c78ac005204e0703c78ac105204e0703c78ac23820a40683078b803820a40683078b8139102d0684474b0039102d0684474b0139102d0684474b0238206a0683078b8038206a0683078b8138206a0683078b8239c06a0683078b8039c06a0683078b8139c06a0683078b823d006a0683078b803d006a0683078b813d006a0683078b823a906a0683078b803a906a0683078b813a906a0683078b820390d00683078b8006004e0703c78ac006004e0703c78ac106004e0703c78ac22650a50683078b802650a50683078b8139102d0684474b0039102d0684474b0139102d0684474b0227705d060347cb4027705d060347cb4127705d060347cb4201d05e060347cb4001d05e060347cb4101d05e060347cb4228305d060347cb4028305d060347cb4128305d060347cb4224705d060347cb4024705d060347cb4124705d060347cb4200e05f060347cb400de04e0683c7cac00de04e0683c7cac10de04e0683c7cac223b05d060347cb4023b05d060347cb4139102d0684474b0039102d0684474b0139102d0684474b0200e05f060347cb4100e05f060347cb4203505e060347cb4003505e060347cb4103505e060347cb4226b05d060347cb4026b05d060347cb4126b05d060347cb4222f05d060347cb4022f05d060347cb4122f05d060347cb421d3099060347cb401d3099060347cb410de04e0683c7cac00de04e0683c7cac10de04e0683c7cac223b05d060347cb423c10c3060347cb4036f03e0684074b4036f03e0684074b4136f03e0684074b4204a06b060307cb8004a06b060307cb8104a06b060307cb8205606b060307cb8005606b060307cb8105606b060307cb8206206b060307cb8006206b060307cb8106206b060307cb8202606b060307cb8002606b060307cb8102606b060307cb8208c0d0060307cb800f804e0683c7cac00f804e0683c7cac10f804e0683c7cac22f70a5060307cb802f70a5060307cb8135503e0684074b4035503e0684074b4135503e0684074b4201a06b060307cb4001a06b060307cb4101a06b060307cb4203206b060307cb4003206b060307cb4103206b060307cb423dd06a060307cb403dd06a060307cb413dd06a060307cb423e906a060307cb403e906a060307cb413e906a060307cb420530d0060307cb4010504e0683c7ca8010504e0683c7ca8110504e0683c7ca822eb0a5060307cb402eb0a5060307cb4128a03e0704074b8028a03e0704074b8128a03e0704074b8220805d068347cb8020805d068347cb8120805d068347cb821fb05d068347cb801fb05d068347cb811fb05d068347cb8221505d068347cb8021505d068347cb8121505d068347cb821e105d068347cb801e105d068347cb811e105d068347cb820000c3068347cb801c203e068407cac01c203e068407cac11c203e068407cac21a1099068347cb801a1099068347cb8124403e0704074b8024403e0704074b8124403e0704074b822b205c068387cb402b205c068387cb412b205c068387cb422d905c068387cb402d905c068387cb412d905c068387cb4229805c068387cb4029805c068387cb4129805c068387cb4230005c068387cb4030005c068387cb4130005c068387cb420640c2068387cb4038903e068407cac038903e068407cac138903e068407cac2222098068387cb40222098068387cb4123603e0704074b8023603e0704074b8123603e0704074b8216004e0683c7cb4016004e0683c7cb4116004e0683c7cb4218704e0683c7cb4018704e0683c7cb4118704e0683c7cb4217a04e0683c7cb4017a04e0683c7cb4117a04e0683c7cb4216d04e0683c7cb4016d04e0683c7cb4116d04e0683c7cb422a70c10683c7cb4039603e068407cb0039603e068407cb0139603e068407cb020950970683c7cb400950970683c7cb4127c03e0704074b8027c03e0704074b8127c03e0704074b8206e04e0683c7cb4006e04e0683c7cb4106e04e0683c7cb4213904e0683c7cb4013904e0683c7cb4113904e0683c7cb4212c04e0683c7cb4012c04e0683c7cb4112c04e0683c7cb4211f04e0683c7cb4011f04e0683c7cb4111f04e0683c7cb422b40c10683c7cb4034803e068407cb0034803e068407cb0134803e068407cb020af0970683c7cb400af0970683c7cb4138302d0704474bc038302d0704474bc138302d0704474bc22e605c068387cb802e605c068387cb812e605c068387cb822cc05c068387cb802cc05c068387cb812cc05c068387cb822bf05c068387cb802bf05c068387cb812bf05c068387cb822f305c068387cb802f305c068387cb812f305c068387cb820710c2068387cb8015304e0683c7cb4015304e0683c7cb4115304e0683c7cb4212c098068387cb8012c098068387cb812e102d0704474c002e102d0704474c012e102d0704474c021c705d068347cbc01c705d068347cbc11c705d068347cbc222205d068347cbc022205d068347cbc122205d068347cbc21d405d068347cbc01d405d068347cbc11d405d068347cbc21ee05d068347cbc01ee05d068347cbc11ee05d068347cbc23ea0c2068347cbc02a505c068387cb802a505c068387cb812a505c068387cb821ae099068347cbc01ae099068347cbc137502d0704478bc037502d0704478bc137502d0704478bc22b00760682c7cc002b00760682c7cc012b00760682c7cc022e40760682c7cc002e40760682c7cc012e40760682c7cc0230b0760682c7cc0030b0760682c7cc0130b0760682c7cc022960760682c7cc002960760682c7cc012960760682c7cc022ca0760682c7cc001ab05d070347cb801ab05d070347cb811ab05d070347cb822fe0760682c7cc002fe0760682c7cc0134b02d0704474b8034b02d0704474b8134b02d0704474b822ca0760682c78bc12ca0760682c78bc22f10760682c78bc02f10760682c78bc12f10760682c78bc22d70760682c78bc02d70760682c78bc12d70760682c78bc22bd0760682c78bc02bd0760682c78bc12bd0760682c78bc202f0a60682c78bc002f0a60682c78bc130c06a0703078b8030c06a0703078b8130c06a0703078b822fe0760682c78bc21800d00682c78bc029803e0704074bc029803e0704074bc129803e0704074bc232706a0683078bc032706a0683078bc132706a0683078bc233406a0683078bc033406a0683078bc133406a0683078bc238f06a0683078bc038f06a0683078bc138f06a0683078bc23b606a0683078bc03b606a0683078bc13b606a0683078bc23f20cf0683078bc017305d0703478b8017305d0703478b8117305d0703478b822d20a50683078bc02d20a50683078bc11210d004030b8a0005202e06044a0ac005202e06044a0ac105202e06044a0ac205e02e06044a0ac032a02e05044a0ac032a02e05044a0ac132a02e05044a0ac22620b105044a0ac034905c06038a0b8034905c06038a0b8134905c06038a0b8230d05c06038a0b800ad0d005030a0b800ad0d005030a0b801210d004030b8a0005e02e06044a0ac105e02e06044a0ac22ef02e06044a0ac02ef02e06044a0ac106b01b05048a0ac006b01b05048a0ac106b01b05048a0ac21460b005048a0ac030d05c06038a0b8130d05c06038a0b8233105c06038a0b8033105c06038a0b810d70c505034a0b800d70c505034a0b801210d004030bca402ef02e06044a4ac23ab02d06044a4ac03ab02d06044a4ac13ab02d06044a4ac21b402c05048a0ac01b402c05048a0ac11b402c05048a0ac21150b005048a0ac033d05c06038a4b8033d05c06038a4b8133d05c06038a4b8208c0c206038a4b8020e09904834a4b8020e09904834a4b801210d004030b8a0005e02e06044a4ac105e02e06044a4ac22ef02e06044a4ac02ef02e06044a4ac132a02e05044a0b0032a02e05044a0b0132a02e05044a0b022620b105044a0b0030d05c06038a4b8130d05c06038a4b8233105c06038a4b8033105c06038a4b8220e09904834a4b8120e09904834a4b811210d004030bca4005202e06044a4ac005202e06044a4ac103b0b106044a4ac005e02e06044a4ac032a02e05044a4ac032a02e05044a4ac132a02e05044a4ac22620b105044a4ac025305d06034a4b8025305d06034a4b8125305d06034a4b823b50c306034a4b803920cf05030a4b803920cf05030a4b801210d004030bcb802d003e07040a0b402d003e07040a0b412d003e07040a0b422c203e07040a0b401b804e0583ca0b801b804e0583ca0b811b804e0583ca0b823490c10583ca0b8002905e060349cc0002905e060349cc0102905e060349cc023cd0c3060349cc001c40d00502ca0c001c40d00502ca0c001210d004030bcb802c203e07040a0bc12c203e07040a0bc235f09407040a0bc035f09407040a0bc13b504e05838a0bc03b504e05838a0bc13b504e05838a0bc225209805838a0bc032505c060389cc0032505c060389cc0132505c060389cc0236d05c060389cc001ce0d00502ca0c001ce0d00502ca0c001210d004030bcb802b403e07040a0bc02b403e07040a0bc12b403e07040a0bc22a603e07040a0bc03b504e05838a0bc03b504e05838a0bc125209805838a0bc125209805838a0bc036d05c060389cc0136d05c060389cc0223b098060389cc0023b098060389cc011af0d00582c9cc001af0d00582c9cc001210d004030bcb402a603e07040a0b812a603e07040a0b8236d09407040a0b8036d09407040a0b811e404e0583ca0b801e404e0583ca0b811e404e0583ca0b823330c10583ca0b8035505c060389cc0035505c060389cc0135505c060389cc0237905c060389cc000530a60502ca0c000530a60502ca0c001210d004030bcb402a603e07040a0b812a603e07040a0b8236d09407040a0b8036d09407040a0b811e404e0583ca0b801e404e0583ca0b811e404e0583ca0b823330c10583ca0b8035505c060389cc0035505c060389cc0135505c060389cc0237905c060389cc000530a60502ca0c000530a60502ca0c001210d004030bcb4036203e06840a0b8036203e06840a0b8136203e06840a0b8201f0c106840a0b8024104e0503ca0b8024104e0503ca0b8124104e0503ca0b820240b20503ca0b8035505c060389cc0037905c060389cc0137905c060389cc020980c2060389cc000530a60502ca0c010530a60502ca0c011210d004030bcb403a303e06040a0b803a303e06040a0b813a303e06040a0b820800c106040a0b803a405c05038a0bc03a405c05038a0bc13a405c05038a0bc23920c105038a0bc003e06b060309cc4003e06b060309cc4103e06b060309cc4206b0d0060309cc403340d0058289cc403340d0058289cc401210d004030bcb403b702d060449cb803b702d060449cb813b702d060449cb823710b0060449cb803b805c05038a0bc03b805c05038a0bc13b805c05038a0bc239005c05038a0bc03180760602c9cc403180760602c9cc413180760602c9cc4218d0d00602c9cc401990d00582c9cc401990d00582c9cc401210d004030bcb403b702d060449cb803b702d060449cb813b702d060449cb823710b0060449cb8039005c05038a0bc139005c05038a0bc239c09805038a0bc039c09805038a0bc13180760602c9cc403180760602c9cc413180760602c9cc4218d0d00602c9cc401d80d00502ca0c401d80d00502ca0c401210d004030bcb403b702d060449cb803b702d060449cb813b702d060449cb823710b0060449cb8039005c05038a0bc139005c05038a0bc239c09805038a0bc039c09805038a0bc13180760602c9cc403180760602c9cc413180760602c9cc4218d0d00602c9cc401e20d00502ca0c401e20d00502ca0c4005d0a60502c98b8015e09a0a83084bc022d04e0503c98b4030e0a50503098b400280990a83484b8022d04e0503c98b0130e0a5050309cb800280990a83488bc122d04e0503ca0b4205d0a60502c9cbc02ef0c20a8348cbc024b04e0503ca0b0005d0a60502ca0bc015e09a0a8308cbc13b009805038a0b4005d0a60502c98bc02280d00a82888c003b00980503898b8005d0a60502c9cc0139c0d00a82488c4020f04e0503c9cbc005d0a60502c9cc0139c0d00a82488c4020f04e0503c9cbc105d0a60502c9cbc12280d00a82888c0020f04e0503c9cb8205d0a60502c98bc02280d00a82888c0020f04e0503c9cb8205d0a60502c98c003c60d00a82488c403540c10503c9cb8005d0a60502c9cc0000d0d10a82488c403b0098050389cb8105d0a60502c9cbc03b10d00a82488c0024b04e0503ca0b0105d0a60502c98bc03b10d00a82484c0024b04e0503c9cb0205d0a60502c98bc03b10d00a82484c0024b04e0503c9cb0230e0a50503098b8115e09a0a83084bc016d0c10504098a800000440783c687800000440783c687810000440783c687820150420783c687800150420783c687810150420783c687823ef03e0783c687803ef03e0783c687813ef03e0783c687820260410783c687800260410783c687810260410783c6878232f03f0783c6878032f03f0783c6878132f03f0783c6878232003f0783c6878032003f0783c6878132003f0783c687822eb0960783c687802eb0960783c6878138c3f00783c707833a40a60301c686c03a40a60301c686c12010a603824686402010a603824686410270880303068680027088030306868109a0b1030346c60009a0b1030346c60000020b030206c6c534020c030206c6c500020b030206c6c501306c0301c686c001306c0301c686c101306c0301c686c218e0a6030289c6c018e0a6030289c6c10280a6038309c6400280a6038309c6410bc0a4030309c6800bc0a4030309c6810bc097038349c6000bc097038349c60113907704028986c013907704028986c113907704028986c216508004024986c016508004024986c116508004024986c23fc0030181c746403fc0030181c746411940a6030286c5801940a6030286c58112807704828685801280770482868581128077048286858231905c060386848031905c060386848131905c06038684821b103e08840504001b103e08840504011b103e08840504023fa05b02824745c03fa05b02824745c111001a068486c60011001a068486c60111001a068486c6021a80af068486c600153080048246c5c0153080048246c5c1153080048246c5c200d0da048246c5c010b077050286c58010b077050286c58110b077050286c5823030a505830504c03030a505830504c124c0a6030207464024c0a603020746413fa06402818786c03fa06402818786c12450a6038206c6402450a6038206c6412910820201c906402910820201c906411800a604028905801800a6040289058103507708028905800350770802890581035077080289058233e04e0a8388448033e04e0a8388448133e04e0a8388448215303e0a0407440015303e0a0407440115303e0a040744021fa0a6038248c5c01fa0a6038248c5c100e020078488860000e020078488860100e02007848886022400ae078488860014107709024885c014107709024885c114107709024885c20c00d209024885c03ab0760a8288c5803ab0760a8288c5813ab0760a8288c5823fa04d02838844803fa04d02838844811700a6040286c5c01700a6040286c5c12f0018028245c8002f0018028245c8012f0018028245c8012f0018028245c8022f0018028245c8003890b0028245c8000ce06b04830608000ce06b04830608010ce06b04830608010ce06b04830608000ce06b04830608020ce06b04830608003ea017070584c7803ea017070584c7813ea017070584c7822c5081070584c7802c5081070584c7810cb02c06048587400cb02c06048587410cb02c06048587420fb02c06048587400fb02c06048587410fb02c06048587420cb02c0604858740189019050505874018901905050587411890190505058742355081050505874035508105050587413390170485c586003390170485c586013390170485c586023420170485c586003420170485c586013420170485c586023cc0000885c487c03cc0000885c487c13cc0000885c487c23bb0000885c487c03bb0000885c487c13bb0000885c487c229c02d090445074029c02d090445074129c02d09044507422ae02d09044507402ae02d09044507412ae02d09044507421e902d0a044487801e902d0a044487811e902d0a0444878223902d0a0444878023902d0a0444878123902d0a0444878202e08108818587c002e08108818587c102e08108818587c23b60a608818587c03b60a608818587c11da08006020447c01da08006020447c11da08006020447c21e608006020447c01e608006020447c11e608006020447c22560000a0604c7c02560000a0604c7c12560000a0604c7c226a0000a0604c7c026a0000a0604c7c126a0000a0604c7c20ac01a0604c5c7800ac01a0604c5c7810ac01a0604c5c7823720810604c5c7803720810604c5c78109f05d040389484009f05d040389484109f05d04038948420180990403894840018099040389484109f05d040389484011f0000c8647054011f0000c8647054111f0000c864705421380000c864705401380000c864705411380000c8647054215706a048349c80015706a048349c80115706a048349c80216006a048349c80016006a048349c80116006a048349c8020d702c06048907000d702c06048907010d702c06048907020400870604890700040087060489070102b01e080487488002b01e080487488102b01e080487488205b01c080487488005b01c080487488105b01c080487488228804e0403c907c028804e0403c907c128804e0403c907c208a0980403c907c008a0980403c907c109e03e048449080009e03e048449080109e03e04844908022f60b104844908001be02c05048946c01be02c05048946c11be02c05048946c20b70b005048946c027004e0403c947c027004e0403c947c127004e0403c947c210e0980403c947c010e0980403c947c13c205c05038947c03c205c05038947c13c205c05038947c23cc05c05038947c03cc05c05038947c13cc05c05038947c20e000009068845400e000009068845410e000009068845420f200009068845400f200009068845410f200009068845421bf0190784c847001bf0190784c847011bf0190784c8470209d01a0784c8470009d01a0784c8470109d01a0784c847020b80000a068648000b80000a068648010b80000a068648020cc0000a068648000cc0000a068648010cc0000a068648020c103e04044688c00c103e04044688c10c103e04044688c23180b104044688c03530000a85c608403530000a85c608413530000a85c6084237d0000a85c6084037d0000a85c6084137d0000a85c6084235902d070446c80035902d070446c80135902d070446c8021bf088070446c8001bf088070446c80108303e04844947c008303e04844947c108303e04844947c22ff0b104844947c00e601a0b048648400e601a0b048648410e601a0b048648420980850b048648400980850b0486484128a0180a0547460028a0180a0547460128a0180a054746022e90810a054746002e90810a054746013920000a85c745803920000a85c745813920000a85c745823680000a85c745803680000a85c745813680000a85c745823d50ac0a85c7458028b0b0020209c80022304e0503c9878022304e0503c9878122304e0503c987822c80970503c987802c80970503c987812f00210201c988c02f00210201c988c12f00210201c988c21e203f0201c988c01e203f0201c988c11e203f0201c988c207302d040489080007302d040489080107302d04048908021c802c04048908001c802c04048908011c802c04048908020c201a0484c908800c201a0484c908810c201a0484c9088222e0ae0484c908800b801a0504c907800b801a0504c907810b801a0504c9078237e0810504c9078037e0810504c9078106b02d040489480006b02d040489480106b02d040489480221a087040489480021a087040489480103e030038449484003e030038449484103e03003844948423f801703844948403f801703844948413f801703844948420b903e04044948400b903e04044948410b903e04044948420c903e04044948400c903e04044948410c903e040449484213107704028947c013107704028947c013107704028947c113107704028947c21780a604028947c01780a604028947c12630800301894940263080030189494126308003018949412630800301894942263080030189494006a0810301894940263080030189494006a0810301894941263080030189494006a081030189494201f0a7030189494001f0a7030189494107b02d040489888007b02d040489888107b02d040489888224f0ae040489888037506a068309088037506a068309088137506a068309088234106a068309088034106a068309088134106a06830908820000330484494800000033048449480100003304844948022660940484494800266094048449480117a02d0b044504c017a02d0b044504c117a02d0b044504c216402d0b044504c016402d0b044504c116402d0b044504c217a02d0b044504c017a02d0b044504c123b0870b044504c023b0870b044504c116402d0b044504c116402d0b044504c225e0180b0545058025e0180b0545058125e0180b054505822480180b054505802480180b054505812480180b0545058235304e0a8385458035304e0a8385458135304e0a8385458236804e0a8385458036804e0a8385458136804e0a8385458213205d090346050013205d090346050113205d090346050204f099090346050004f09909034605012e304e0c8384c5002e304e0c8384c5012e304e0c8384c5022b104e0c8384c5002b104e0c8384c5012b104e0c8384c5022fc04e0b0386c4c02fc04e0b0386c4c12fc04e0b0386c4c232804e0b0386c4c032804e0b0386c4c132804e0b0386c4c21510000c064485401510000c064485411510000c064485421810000c064485401810000c064485411810000c064485420b402d0b0444c5800b402d0b0444c5810b402d0b0444c5800b402d0b0444c5821620b00b0444c5801a90190b04c505401a90190b04c505411a90190b04c505421930190b04c505401930190b04c505411930190b04c5054239d0ad0b04c5054031204e0b0385058031204e0b0385058131204e0b0385058202c0c20b0385058031204e0b0385058004507707828685c004507707828685c104507707828685c20a10a607828685c00a10a607828685c104507707828685c00cb01a0d848544c00cb01a0d848544c10cb01a0d848544c22690820d848544c02690820d848544c128006a0a8305458028006a0a8305458128006a0a830545820f609b0a830545800f609b0a8305458126a06a0b0305c58026a06a0b0305c58126a06a0b0305c58214809a0b0305c58014809a0b0305c5812340a6048205c6802340a6048205c6810d103e0b840505400d103e0b840505410d103e0b840505420e803e0b840505400e803e0b840505410e803e0b840505423c103e0c03c4c5803c103e0c03c4c5813c103e0c03c4c5822d30960c03c4c5802d30960c03c4c5813c103e0c03c4c5802a906a0a030585802a906a0a030585812a906a0a0305858229506a0a0305858029506a0a0305858129506a0a030585822ca04e0c838485802ca04e0c838485812ca04e0c8384858229804e0c8384858029804e0c8384858129804e0c8384858214d02d0b8445054014d02d0b8445054114d02d0b8445054213602d0b8445054013602d0b8445054113602d0b844505421380190c850505001380190c850505011380190c8505050211f0190c8505050011f0190c8505050111f0190c850505022900ad0c850505003650170d0584c4803650170d0584c4813650170d0584c48234b0170d0584c48034b0170d0584c48134b0170d0584c48210c05d0a0345854010c05d0a0345854110c05d0a034585420f805d0a034585400f805d0a034585410f805d0a0345854208302d0c8445858008302d0c8445858108302d0c844585822220870c844585802220870c8445858111503e0b0404c4c011503e0b0404c4c111503e0b0404c4c20ff03e0b0404c4c00ff03e0b0404c4c10ff03e0b0404c4c23d903e0b03c4c5003d903e0b03c4c5013d903e0b03c4c50204503f0b03c4c50004503f0b03c4c50104503f0b03c4c50209c02d0c0444454009c02d0c0444454109c02d0c0444454211e02d0c0444454011e02d0c0444454111e02d0c0444454221a0180b8544854021a0180b8544854121a0180b854485422310180b854485402310180b854485412310180b854485422740180b0544c5802740180b0544c5812740180b0544c5822d30810b0544c5802d30810b0544c58101307708828645c001307708828645c101307708828645c23d507608828645c03d507608828645c13d507608828645c227b0d008828645c01c208006020747001c208006020747011c208006020747021ce08006020747001ce08006020747011ce08006020747021c208006020747001c2080060207470107f0da06020747001ce08006020747001ce08006020747011ce080060207470223704e0503c8870023704e0503c8870123704e0503c8870225504e0503c8870025504e0503c8870125504e0503c8870220407706024786c020407706024786c120407706024786c221c07706024786c021c07706024786c121c07706024786c21e20460280c887401e20460280c887411e20460280c887421180a70280c887401180a70280c8874105a081040187c70005a081040187c70105a081040187c702062081040187c700062081040187c701062081040187c7023ef0800401c7c7403ef0800401c7c7413ef0800401c7c74201f0810401c7c74001f0810401c7c74101f0810401c7c7423950800601c707403950800601c707413950800601c707423a10800601c707403a10800601c707413a10800601c7074236c0da0601c707401d1018070587c6801d1018070587c6811d1018070587c6822b7081070587c6802b7081070587c6812e0000058608c5c02e0000058608c5c12e0000058608c5c22eb000058608c5c02eb000058608c5c12eb000058608c5c20270810381c806c00270810381c806c10270810381c806c239d0a60381c806c039d0a60381c806c100f0810401c7c70000f0810401c7c70100f0810401c7c7023f70800401c7c7003f70800401c7c7013f70800401c7c7023d50800481c787003d50800481c787013d50800481c787023cc0800481c787003cc0800481c787013cc0800481c787020070810401c807400070810401c807400070810401c807410070810401c807422b90a60401c807402b90a60401c807410170810401c7c7400170810401c7c7410170810401c7c7423e70800401c7c7403e70800401c7c7413e70800401c7c74233402e050448060033402e050448060133402e050448060202a030050448060002a030050448060102a03005044806022d00b105044806003c30800481c806c03c30800481c806c13c30800481c806c23de0800481c806c03de0800481c806c13de0800481c806c21d904e0583c886801d904e0583c886801d904e0583c886811d904e0583c8868233e0c10583c88680048081048187c700048081048187c701048081048187c702051081048187c700051081048187c701051081048187c70216d080038247870016d080038247870116d080038247870200007a038247870000007a038247870100007a03824787023fc0000180c887403fc0000180c887413fc0000180c887423fc0000180c887403fc0000180c887403fc00a0180c887403fc00a0180c8874119e2bb0180c7cb0319a0a6088246494019a0a6088246494115307708824649401530770882464941153077088246494219e2bb0180c7cb033fc0140180c7cb040240770882864940024077088286494102407708828649420240770882864940024077088286494123d0d0088286494018a080078206898018a080078206898118a08007820689823e607608828649403e607608828649413e6076088286494224e0d008828649402bd06a08830649402bd06a08830649412bd06a08830649422e10cf088306494014405d088346494014405d088346494114405d08834649423040c208834649403c104d0783c689803c104d0783c689813c104d0783c689822500c10783c689803bb01708858649403bb01708858649413bb01708858649420000ad08858649402c002d08844649402c002d08844649412c002d08844649423540b0088446494028902d098445c94028902d098445c94128902d098445c9421780b0098445c9401220dc05810689c00ff0a7048106c9c00ff0a7048106c9c00ff0a7048106c9c01220dc05810689c01220dc05810689c00f50a7050106c9c00f50a7050106c9c00f50a7050106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c13050db048146c9c03050db048146c9c03050db048146c9c03050db048146c9c012d0dc048106c9c012d0dc048106c9c012d0dc048106c9c012d0dc048106c9c03050db048146c9c03050db048146c9c03050db048146c9c03050db048146c9c03050db048146c9c03050db048146c9c03050db048146c9c03050db048146c9c00ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c10ff0a7048106c9c11610d10a024608800c309d09830607400c309d09830607410c309d09830607410c309d09830607410c309d09830607400c309d09830607410c309d09830607410c309d098306074134803d01004ac7c30250a70100cac7831300cd01008ac78326609001010ac7833960db02814a47c023d0a6040209474023d0a6040209474105c2d601810a488401d0da03824a08801a818502010ac745174080038249c740174080038249c741174080038249c7420160da038249c740000061058348c740000061058348c741000061058348c7423d90c3058348c74007812402814a48452a608108858985c02a608108858985c10470001007064500047000100706450104700010070645020e60ab10070645000760000c0687c3c00760000c0687c3c10760000c0687c3c20f60810c0687c3c00f60810c0687c3c11260810b85c805801260810b85c805810de0810c070685000de0810c0706850123d0000c8606458023d0000c8606458123d0000c860645821510190b850786001510190b850786011510190b8507860237f0170c058745c037f0170c058745c137f0170c058745c220803e080408858020803e080408858120803e0804088582281094080408858028109408040885811680190a050804401680190a050804411680190a0508044208a01a0984c8048008a01a0984c8048108a01a0984c8048230d095070407854030d09507040785413a70000a05c745003a70000a05c745013a70000a05c7450210e0810c05c6c50010e0810c05c6c5010c40810d070645000c40810d070645011b008807844745001b00880784474501337095068407854033709506840785411e90180c8546c5c01e90180c8546c5c11e90180c8546c5c20670ad0c8546c5c02020180c054605002020180c054605012020180c054605021f40c10d83c405c02cc0c211834446400ed197050046c98304736f0600c6894303b36f0600c689432ec0e10600c689432e00e10600c689430ff197038047098321e0e10480c6c9432bc0e10600c689030712bd07014648c41602a605010708841a20db068186c8400920a20100c8c9032220d00302c888c000c02f0080c709030e00ac0202c6c8c02b20da0781c6884037b3e7040247c7843733e7040246c78407008107014648c007008107014648c107008107014648c2199215060186884531c3f0070206480403327506014688842a30da0781c648403360da0701c648403360da0701c648403f92e2020089c9032f002c02008a08c02dd0e0018109c8c309e30f020089c90309e30f020089c90309e30f020089c90302709402010a08813fa093028109c8803fa093028109c8810110ad098506c1801cf0af060487420034e0b80281c904400e00b70281c904401880a603028904001f818c0382c6c34433c0b1090406828019a0af07048742801880a6030289040122e216038187844522e216038187844507f0d106024844001290d004030782800a30d0050307c2801bb0c10e83c382c02050d00402c803001800c10f03c342c01c20af06848741c01e808806844742001e808806844742011d80c10e03c3c2c0061099058347c2c0061099058347c2c129a0c10683c702002b93c7038309034339d0c0078406c1c019e0c10e83c382c00e02ac0784c000042202170380c104441892980a890000030e030506814108442263f00703c50f8338e2841a1340000326b39b1a05400f842263f00703c8d7c334319d289d4000030ee3972888c016832202030c850adf842910e436a780000311433e368bc01e0305a3bb11870eab030e10e4432cc247c311d307430dc2698301a3de06028000041971e70400c082042313b50b0840000302027606010147841fd2f7138f0000032243de0c82424dc43b83f00703c6548334139a0507868e0334c2d31e8f0000032562f7180c824e030a02030c85079d041fd2be0608c956032ee2842d1680000338e2d1291000d60339c3ba11870fe5c31273ba13070fe5c332825b090a11de032b619d469f4340c31302be4692435e030a32894692835dc30003d817868de6032e03d717868de6032d621a098a919dc33432123d1c8602830a32d33d12465e032102ad3d12865dc31eb33e1a0b4001c31603b91a07c005832e01ad0281c000050f01080280c001452173b50d084000031ff3f80c01c046c43c2311178c4000030413f1178380090328a2f5610f4000031370e24e0640000421321b0585000004210195532c0000032100e440ac4940032ee21a1d148000030cc21f1d00c014053263dd0f028000040c031d04814000041570e2048080010305a0e443ad0207c305a21f8c9a8000033413f80381c0000413017704020000050d62c404840000040a01eb10060000042863dd0a82c000040483de05828000043050e34c2e80000308934d3e0803a6c32cd3dd0502c000040d431a05020000043e90f80500c001840573111a0d0000031603b91a07c005832ee26c19060080032c53e8088200000418821508818000c53a4344248b40000328a2ad301200000326c3e82c820150443401eb10060000043601eb10060000041d520405050000040fd3e7070240000404025e07010001842ea38b0a83c000031a20e1377741c2031ec3dd0e02c000040c015c0c85c000043e50fb0d060000043e030b08018000030ef2bd080140008402624702038000033c234202008003432523de0a824000042083dd0e02c0000439a1f70d06c000031732840a8140c5c31b015502814000042001d40b05400004160202100600000418020210060000043d123707828000031ce3b11004c000042c138e14890000033580e114808008c333a25b04094000033482842313c000033bc39a178580000425632916820043c31c43d204830000040283950480c002830993de04828000040283950480c0020304b0fa258600000403021c24810045451911ed0713c000033d5142029102c5032d63b0248500400428b3b025850000443e22a30b9040000332821a0c10404103303371258a40000337c3832485c085841831ed0713c000030201ed029100c5433de398108840000305a0e11080c00803370399108840000305a0e11080c0080300039b108840000305a0e11080c0080313f3971088c0000305a0e11080c008832c13b2108840000305a0e11080c0080327c21505018000051f819703828000043623de09024000041202d508814000c40a23de048280000404a2d604810001c40903de048280000404a2d604810001c41a20af040200008522e2170380c042041b20af040200408522e2170380c042043333f80381c080c40f81e70380c0420410b0d3018140c1443ca342018080c2430943030481c000043f439005820000031243e705824000040be0e2058080010307d3de0502800004033216050180010505e3de058280000407d3660581c000c3224331310c0000031cf3e7048240000419328d048140010433c344340b4000030693de050280000403d21605018001052e23bb10070000030c021a10060001042743d51186c000030000e31185c0010404024b0c84c000040d03d20c830001043bf3ba0d8700000334c3d10b8300850407d3bb0d8700000334c3d10b8300850431c332100b80000318015b0f85c086043e01a30f860000042693f00f03c082832a021b10060000041583f20f834083031312d506814000042a30e10680c000c30e83c2150400008406030c0a048000041801d40b854040041402040b850000043033610b040000030201d40c8540000418b3f10a038000031c02040a850000040602ac0884c0000402d3dd0782c000040373c305834000041ee3b11004c000043b63d21002c002441b014c028240000418928403050000031670e211258000033f931c02844000031181b70281c000051901b90281c000052ab144028240000400022a03050000032100e10700c000030f80e10700c000033f934e02844000031581ec030a0040030a933504014009441521ec030cc040030a93350401400c04020231030f8040030a93350401400ec43ee3e703824000040b02170380c001c43e73e703824000040b02170380c001c43e03e703824000040b02170380c001c43d93e703824000040b02170380c001c43d23e703824000040b02170380c001c43cb3e703824000040b02170380c001c43c43e703824000040b02170380c001c43bd3e703824000040b02170380c001c43b63e703824000040b02170380c001c43af3e703824000040b02170380c001c43a83e703824000040b02170380c001c42e23b203824000030b02170380c001c439a3e703824000040b02170380c001c43933e703824000040b02170380c001c407835c03824000040b02170380c001c407835303824000040b02170380c001c403834503824000040b02170380c001c409834403824000040b02170380c001c403833c03824000040b02170380c001c409833b03824000040b02170380c001c40582cd03824000040b02170380c001c40582c403824000040b02170380c001c41d825503824000040b02170380c001c425c3c306840000040a30e10580c0438324f3c306840000040a30e10580c043833f23ef06840000030a30e10580c0438305236b06840000040a30e10580c043832ea2de2b85c000c307921f2980c085c50d039b4b05c000c407921c4900c085c500039b6805c000c419f21d6600c085c51de3d62b8680000307921f2980c085c533c3d64b0680000307921c4900c085c50433d7680680000319f21d6600c085c53e03f004820000043743de06024000042bc0e10500c0418405a1987c21c000032102456f1a00000300d3de06828000041c025e06810001843ee3dd06828000041002ed068140014408b31c348c40000318133e350b8000030213ba11078000033c80e310860041842b93d31386c000032d139b13054041840002fe0d84c000031d83c40d034001840712ac06844000041b327506014043441512ab068440000401328d06014003442ab332388bc000033570e338860006440452892f130000030402d52f0f00048334e371298a0000033493ba29870003833893111c8cc000030ee3ba1c8740060334c30f1e8d0000032b61821e86c006c30003481d8b40000322e3b11d848007042473b51a880000031ec3d11a8300054400037514098000033bc3b01404c00544057345130b4000032780e213060005c41ad3b90c87c000033813dd0c828005c41723d80c868000033813dd0c8280048414018b0b05c0000409e3f10b038002c31943b90c87c0000339a3dd0c828005c41593d80c8680000339a3dd0c8280048408018c0b05c000040b43f10b038002c31c01e502010000053c234202008000c30b42dc0503c000043693f005020002040942f40483c000043693f005020002043f73830403c040043693f0050200020437239b0403c040043693f0050200020415b3b20403c040043693f005020002042fd18e0383c080033693f005020002043343600383c080033693f005020002041b23c406838000040fd0e10580c043041bf3c406838000040fd0e10580c043043a1398110880000319f3f80d01c0c70405439811088000031b93f80d01c147043353830c0600000422f3f80881c044c434d3830c060000042403f80881c184c41281380381c000053d034201808001033563f80381c000040cf2770180c100c41d014e0381c0400510b0a30100c000831181480381c0000521f0a20100c1808310d2ed04014000043d034201808000c300036504014000043d334201808140c30003112b8dc000032ea39a2b884005831a33072b8dc000032ea39a2b8840058316039721888000030003b221844004841a33971a888280032a33ee1b044284831b23d813864000032003f01303c0428318b3d813864000032003f01303c042831013d20a830040043063f00b020000843023bb0d0700000331b3830d060000c42911821204c000033963b1128400008409a3ba2a074000030000fa25860100c43a13712989c0000309a3972a08c0008304721604018000050a82170400c001043e01eb0f860000443d23d710068000032e3361100a80400304c0e1108640048427c3b5100800000308b0e110064001c400029210060000043443b010050001443e01d30f860040041d30e110064000043e01bb0f8600404430f3d710068000032b621a100a80000302b0e110864004843e02030f860000040d10e110064000041e029310060000041ae3b11004c001843e021b0f8600400424029310060000043e02330f8600000428029310060000042a0293100600000420e3b11004c001842e029310060000043601eb10060000042213d104830000040283950480c002833863f004820000040283950480c0018329c3b50e880000032843d1278300000403f3d215830000040bd3c215840000040923b227840000040433c227840000042d33d127030000040923c215840000040e13b227840000041303b215840000040433b227840000040c02c40b048000841f23c40b034002441002db0b048000841f23c40b034002440202dc0b048000841f23c40b034002440e01440f85c000041623d30f82c003842b33c4098340000406036409818002042d93c4098340000425521009818002452a30e10c864040441732840a8140c5832dd0e10c864040441732840a8140c5833260e10c864040441732840a8140c5831f30e10c864040441732840a8140c58320c0e10c864040441732840a8140c5831302892c8d40000303b37220898000032473d51686c000033f021406018000053c83dd098280000403b3480d09c000030f431c100c4000033e02e40f89c182430002cf0f8bc18043286332128c0000031de3b90b874001433c33980d888000031fd3b50d084000430433ba0b874001430932940404400004028375050800000319323c060d4000032250e426060000040bb3f0260200044429f39b19054000043260e1190080050317322207068000030e60a907008006431932710509c000031273d6190680000314d3ba080700000302026f0509c0000320136b0e88c000031202c30e848004842242f7190e8000033643b11904800a443e036b0e860000042270e20e808005c31c63b90c07c0000308034c0b0180468437c39a1005c000041370e2100080058339c39a1005c000041370e21000800583028398160880000331c3bb040700000313a3e705024004c402033c0c044000040d10e10c00c005033e225b02848000033e229102848000030603530c044000040d10e10c00c005033243bb040700000313a3e705024004c40083bc040700000313a3e705024004c43e226d028480000331c3600c044000030d10e10c00c005030003bc040700000313a3e705024004c408033b0c044000040d10e10c00c005033343bb040700000313a3e705024004c43c02140c018000052440e20c008005431e018c0c0580000408b0e10c00c005031c024b0c04c000040d10e10c00c005833e224902848000033413b8040740000313a3e70502400504032375040740000313a3e705024005040a01740c05c000040d10e10c00c005833e223702848000030103bc040700004313a3e705024005040e024c0c04c000040d10e10c00c005833e227f02848000030183bc040700004313a3e705024005040402c40c048000040d10e10c00c005432173f80c01c000042440e20c0080058332c3bb040700004313a3e7050240050433f0e10c0640000408b0e10c00c0058300d04300804040433c6342020080000300d043008040004321f09e01010000031a018d020100000521f09e010100000300d04300804040033c6342020080000300d043008040000300d043008040404301304f008080000300d043008040004309509601004000031e60450080400003027098010040000300d0430080404003042054008080000300d04300804000030533de05828000440ab3de04828040840533de058280004412f3e7058240004418d3b11084c8598418a39b10858857443de3b90f878894c307c3970f0908928334e39911084811431d839712888790832f739b12854790441663b11384c7504406a3d213830750043e80e00bf0c1c7430000e10449c6240302b0e10d774182030450e10ae54006c334803d010040c10305f0b103010040c505821004018000452d53f80401c0000437020c030206078537c20c03020607852a720c038205c7850b8182038245c7441d824b038285c70401818c0382c5c6c42553d30982c000c437d3f00783c080033db3dd09828001040df3e7078240810402f3d8090600c0031133d709860080030a01140a8580004408021b0a850001040003540a044041c404036b09044081c41002940904c0c1442401eb090540c0c40c01eb090540c0c41e0233090500c1041b612b0905c0c0442353d608060140033eb17307860180043e90e007860180042d61730785c180441fd2e108058140831a01430805c140441201a30785c180440401bc07858180841601ec08054140c41402ab0884c101441701ec07854180c42c018b0785c1804410012c0905c0c044060233090500c104100234070501c10410e234070501c1040111ec070541c0c41ee1ec070541c0c41e01ec070541c0c41ee1ec070541c0c40111ec070541c0c410e234070501c104100234070501c1040e90e10780c1458326813d02810003453dd3d006834000840ee3e707824080843a83c1088401804408b3110b82c042031c63f109038200030bd3e7088242c0043c63f00503c4408338509901014503033e43c009040100c43963c109040100c40263c308840140c422e3c308840140c41133ef098400c0c33e43b00a840040c435c39b0b040000c43eb3940a040080c431d3f80401c000040e01800301c040053723aa0401c0000416e1ed02064042433f92c4030780010316e1cd02080040833941f203080000833f92a3030840004300020803088000030251ed0207c040c32e338b0300c008031f81350281054bc51623e70502454ac412021c09850548043653830b860547043003830d860547043e024b0f060507043c53831105c4c74416639b120584c78431f3b0128504c8040003c3130404c9040003f213834509c31fa3331182c5ca831af3f01382058b842903f00f02054b442052170301860b450cf0550080460b830420520080860bc30420520080860c430143540483c0000414f3f00483c000030f42f30483c000040143540483c0000407430c0483c000040d430b0483c0000414f3f00483c000030362dc0483c000040532d6048100030420138e6009c0c14307d36d610a80408321f362620b00000313f36d610a8040830913d210830342042453f01203c2c1831d93f01383c2418313b3c214040201841123c214840241c41263f01483c242030703f117038182830003d31682c143041413de158241c3840003f915018184841853f80d01c344840580b903010884850f7197040048454321c3c408834142842f312b0e05c001040203830e060000841601730c85c080040e93d20c0300c0040df3f10a038000832fe3f109038000831773f209834000832c63c4098340008409621603818785052fe3c409034484842d93f109838404032083c40a0343c4442713dd0a82c384843eb3a40a0303c4843eb3a40a0303c4843eb3a40a0303c4843eb3a40a0303c4843eb3a40a0303c4843eb3a40a0303c4843eb3a40a0303c48429f3c40a0343c4440ca3f10a8383840328b3c40a0343c4441283d209030444c404037c08818504042513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504442513f80881c504440ce3e708824503c40ce3e7088245038409621603818504050202630984c4c5840202630984c4c4840202630984c4834412027c0984c4420416024c0984c401c412027c0984c381840202630984c341440202630984c301840202630984c301c40a027c0984c301c40802940984c301c40a027c0984c301c40202630984c301c41602930984c241c41602930984c281c418027b0984c3024400027b0984c3c2c41402640984c443c40c02640984c4c4c41a02630984c4c5c40a02dc0a048445c40a03230a048345c40a03230a048245c40a03230a048145c40a03230a048005c40a03230a048045c40e02f30a048045c40002f40a048045c40c030b0a048045c40403240a048045c40e02f30a048045c40802f40a048045c40a03230a048045c40a03230a048145c40a03230a048245c40a03230a048345c40a02dc0a048445c41d33f80881c504c41d63f008820504441d63f008820503c41a021c098504c58422015c098584c48422015c098584c38422015c098584c28422015c098584c18422015c098584c08422015c098584808422015c09858480c426018c09858480c42a01a3098584c0c40601a4098584c0c4280174098584c0c41001bb098584c0c429615c09858500c41c01a409858500c42801bb09858501040b60fc09858480040b60fc09858480440b60fc09858480842401a409858480842401a409858481c42401a409858482c423614409858483c42a33d509864484431161140985c484c40ab21603818505850ab21603818506051f01b502010746c51001600301c706453f63ce04834684c438e20c03020705c51b81c6038286c5441f53cb0382c6c5030181970382c6c5041cd3d204830685041b81bc03828644c41b81660301c604850f81680301c5c44516e1ed02064082832092be030840408311431c04084000833f436f040840008311d2d304088000430202960408c0000315e1ec0308c040032e338b0300c0484300d043008048c70317708d0182c7054323521603018746452161e70400c686442c021a028186c685235216030187864501816d0301c7450500d04300804785432d73dd158281c28422a3d31582c1c2843213d1158301c2841073f016820182841873f014020202841343f0150201c28402a3f9150181c30415e3f014820202c41cc21112818282451f1211128182824513321112818282451a721112818282450543f912818282440793f91281828244033210128182824523021012818282453443dd0f028100443633d30f02c100440b23d20f030100442033e8100200c0442ea3f00e020100442ae3f00f0200c0442ee2140f0180c0c52cc3f00f0200c0841702150c0181c00509e3f90c0181c0043d82140c0181c0052162110c0181c0051e43f90c0181c0042162110c0181c0051582150c0181c0051172150c0181c0053023dd120280404401b3061202c040433923d112030040442033f0130200004426f3f010820080442293f0118200404428f21411818040c524c3f011820040842d02140f0181000530c2140f018100053482140f0181000532a2140f018100053a22140f018100052b22140f018100053662140f018100053842140f018100050b43230483c000040543240483c000040142f40483c000041162db0483c0000439b3f00783c0004336d3f00803c0004334c3f00883c0004333b3f00883c000431e43d1040340400438a3d004034040043f63c004838000043f80ee038340400402f3f00903c080432873f00a03c040430f33f10a038040432ec3f109038080431b33f1098380000319f3f10a0380000335d3f00803c080033103f108838040033aa3f00703c100833c53c30603c140841cc3c4060381408417b3c40603c1408438f3f004820182043983f004820182043a13f004820182043aa3f004820182043703c30683c1008437d3c30683c100843213f1068381008316f3c40603c140841a73d207830100c433c3c407034140c434a3c407034140c41b63d207030140c42353ee08044080030ef2ac080440800423f3c308040080043e738308044080042ad3f80401c1c1842b53f80401c1c1843b33f004820181843980d50381c1c1852773c506834141c42693c507034141c431f3c407834141c43103c407834141c40373d00403418104199137040301810315a3d204030181042693d204030181043e13f108038041431a43c407038041441963c4070380c1441873c407838081440a035310060000040c81300280c000050f01080280c000052f81700281400005028174028140000505817b028140000508817b02814000053533e704024000043810e2060046c9830d20d2060086c9832b00e10600c6c9831cd25e060106c9843b93c30603c6c9843ed34e060846c98317325e060106c9843d13c30603c6c9843913990607c6c9833810e2060046c9830d20d2060086c9832b00e10600c6c9830d3276060106c9841633c40603c6c98407d345060846c9830cf04f008086ca03024042008146cc03042056008046d10300d043008049074300d0430080494703042050008089868304205000808986431512bc068186c6041512bc06818685c427c3f80681c6454426f3f80681c604c42893f80601c644843ea3d0060346440438a3c305834683c41d83d1060346838432e3c407034643443d03f1088385c2c32ec3c4090345c2841163d2090305828429b3dd0a02c542443b33dd0a8285424412f1a308058542040001ec088545420420021b09850502040452760a04c4c2032521eb068546c2440d21eb068546820407223306850642041122940684c601c43643c306040642443ea3d0060346440438a3c305834683c41d83d1060346838432e3c407034643443d03f1088385c2c32ec3c4090345c2841163d2090305828429b3dd0a02c542443b33dd0a828542440003de06828542041183e706024542043513f006020502042623f80681c4c2042c01a8028146c2452c01a8028146c205228105028106c2050f821603818681c502034d04014682440980ed0381c58a450073e8038244ca043810e2060046c9833bc3f00482064a4410b3e7068245ca042713d20782c58a042af3dd0982c50a4423d3de0a8244ca840cf05300808448c3009042010083c88302603a0180c348050c721603818307c538220c030203074517a20d028203070502625e02024306830273f203834306032710e403038305c43f80e003838305840b817403838305440273f203834306030d915c03028305c42381c603828305843f53e703824305441601cd02010346050ea2bf0200c385c402603a0180c4058519003c01008445430cf05300808448c3009042010083c88302603a0180c348050c721603818307c537620c030203074517020d02820307053bd259020243068321817403834306040213aa03038305c329b3f003838305830230e3038383054421817403834306040d916603028305c40f81dd03828305840153e803824305441681c5020103460511f0d80200c385c502603a0180c4058519003c01008445430e1197060046c98307e0e2060086c9830f10e10600c6c9843643ab060186c98405237b0681c689843383f0068206898435d3f0060206c9843453f0060206c98432f3ef060406c9832973d50606c6c9833f132d060846c9833f112b0605c6c98410534d060806c9830cf0560080458a830cf0560080454a430cf0560080450a431300cb01008489c31300cb01008449c31300cb01008409831300cb010083c94302603a0180c388c502603a0180c3888502603a0180c3c8451601cd02010387c51601cd020103c7851601cd02010407453f935f0303c447030262380203c486c32e9230020404c6833f930b02844486031f53b903848445c30b327c048484058401327b058483c5441b3263060483850411d2f506848384c31b3263060483c48417324c060483844409521b050503c38413327c04048403c418a23405050403041601cd02010486c51601cd020104c6852eb16d02814486042eb16d028144c5c42eb16d02814505842eb16d02814545442eb16d02814545042eb16d02814584c42eb16d02814584842eb16d02814544440753e802824503842a221a028184c3c50b42eb0501c403040cf0560080458a830cf0560080454a430cf0560080450a431300cb01008489c31300cb01008449c31300cb01008409831300cb010083c94302603a0180c388c502603a0180c3888502603a0180c3c8451681c502010387c51681c5020103c7851681c5020104074502139b0303c447033bd2410203c486c315e20f020404c68312a2f502844486032b93b503848445c319327b0484840584153264058483c544033263060483850433c33206848384c3033263060483c4840d326406048384441f2233050503c38417329304048403c418023405050403041681c502010486c51681c5020104c6850381040281448605038104028144c5c503810402814505850381040281454545038104028145450503810402814584c5038104028145848503810402814544450753e802824503842a221a028184c3c50b42eb0501c403041990e204154000033cd1ec02154000030101ed04148000c33dd138041540000316219702154100033c51ec041540000302019804154000033a41a204154000030081ed04148000033dd0e3041540000318f19804154000033dd1e204154000033bd1ec041540000315a19704154000033d51e2041540000315219704154000033bd1970c154000031890e204184000030081430c154000031671780c154000031a01bc04058000042e921a02058000032161d40404c000c40571bc0405800004026222020581000304f1bc04058000042931bb04058000043f314204058000041572040404c000040731a4040580000427318c040580000415618b04058000040e01d304058000042301bc04058000042201bc04058000041b01bc04058000042001740c058000041252d3040880000302014304154000032fe0e402154000031661cd04148000c33d518d04154000032fe13902154100031871980415400003197198041540000339419d04154000030181ed041480000339c1a2041540000317b1cd04154000031731cd04154000033b41a204154000033ac1a204154000033dd18d041540000317f17804154000031891430c154000031910e204184000033bd1420c154000030081980c154000030e81d3040580000402520c02058000031971d40404c000c41a81bc040580000416e20602058100031131bb04058000042531a404058000041d31a404058000041942340404c0000431c0fb04058000042b31a304058000040f01d3040580000409618c04058000042281bc04058000043f315804058000043f618b040580000400018c0c058000043f130b04088000030af28f0180c000040802c210060000040602c210060000043c02ab10060000043a02ab10060000043602ab10060000043402ab10060000043002ab10060000042a02ab10060000040a02aa10060000040802aa10060000040e0294100600000404029410060000042c029310060000042602931006000004220293100600000420029310060000041c0293100600000412029210060000040a02921006000004020292100600000438027b10060000042e027b10060000042c027b10060000042a027b100600000422027b10060000041e027b10060000041c027b100600000416027b10060000041a027a100600000412026410060000040a0264100600000432026310060000042e0263100600000418026310060000041002621006000004040262100600000414024c100600000434024b100600000432024b100600000430024b10060000042e024b100600000420024b10060000041a024b100600000402024b10060000042a023310060000042802331006000004260233100600000422023310060000041c0233100600000414023210060000040a0232100600000408023210060000040e021c10060000043c021b100600000438021b100600000434021b100600000432021b100600000430021b100600000426021b100600000424021b10060000041c021a100600000416021a1006000004260203100600000404020310060000040202031006000004000203100600000424020210060000040e020210060000041401ec10060000041201ec10060000043801eb10060000042c01eb10060000040201eb10060000043401d310060000041c01d210060000042001bc10060000041601bc100600000410018b10060000040c018b100600000422018a100600000402018a10060000041c017410060000040801741006000004060174100600000400017410060000043cb17310060000043ab173100600000438b173100600000436b173100600000434b173100600000432b173100600000430b17310060000042b61731006000004120173100600000410017310060000040e017310060000040c017310060000042eb17210060000042961721006000004240172100600000422017210060000041a01721006000004180172100600000404017210060000041e015c100600000408015c100600000437115b100600000435115b100600000423615a100600000400015a100600000428b144100600000421614410060000041f614410060000040c014410060000040a01441006000004180143100600000416014310060000041401431006000004120143100600000410014310060000042f314210060000042b1142100600000426b142100600000429112c100600000422b12c100600000420b12c10060000040e012c10060000040c012c100600000406012c10060000043d112b100600000418b113100600000416b113100600000414b113100600000414b113100600000414b113100600000414b11310060000041d111210060000040b611210060000040b611210060000040b61121006000004060262100600000438024b100600000438024b100600000438024b10060000043001eb10060000040e02c210060000040e02c210060000040e02c210060000041a02c310060000042002c310060000042002c310060000042002c310060000042602c310060000043002c310060000043202c310060000043602c310060000043802c310060000043802c310060000043802c310060000040c02da10060000040c02da10060000040c02da10060000042c02db10060000043002db10060000043202db10060000043402db10060000043602db10060000043802db10060000043a02db10060000043c02db10060000040002dc10060000040202f210060000040402f210060000040a02f210060000041002f210060000041202f310060000041402f310060000041602f310060000041802f310060000041a02f310060000041c02f310060000041e02f310060000042002f310060000042a02f310060000042c02f310060000042e02f310060000043002f3100600000414030b100600000416030b100600000418030b10060000041e030b100600000420030b100600000410032310060000041203231006000004140323100600000414032310060000041403231006000004140323100600000406033b10060000040c033b10060000040e033b10060000040c033b100600000416033b100600000418033b10060000041a033b10060000041c033b10060000041e033b100600000420033b100600000422033b100600000424033b100600000426033b100600000428033b10060000042a033b10060000042c033b10060000042e033b100600000430033b100600000432033b100600000434033b100600000436033b100600000438033b10060000043a033b10060000043c033b100600000400033c1006000004020352100600000408035210060000040c0353100600000412035310060000041403531006000004160353100600000418035310060000041a035310060000041c035310060000041e0353100600000420035310060000042203531006000004240353100600000426035310060000042a035310060000042c035310060000042e03531006000004300353100600000432035310060000043403531006000004360353100600000438035310060000043a0353100600000418036b10060000041a036b10060000041c036b10060000041e036b10060000042e038310060000042c038310060000042a03831006000004280383100600000426038310060000042403831006000004220383100600000420038310060000041e038310060000041c038310060000041a038310060000041a038310060000041a038310060000041a03831006000004180383100600000416038310060000041403831006000004120383100600000410038310060000040e03831006000004000383100600000404038210060000043c036b100600000432036b100600000430036b10060000042e036b100600000424036b100600000422036b100600000422036b100600000420036b100600000414036b100600000410036b100600000410036b10060000040e036b10060000040c036b10060000040a036b100600000408036b100600000404033a100600000402032410060000043c032310060000043a03231006000004380323100600000436032310060000043403231006000004320323100600000430032310060000042e032310060000042c032310060000042a03231006000004280323100600000426032310060000042403231006000004220323100600000420032310060000041e032310060000041c032310060000041a03231006000004180323100600000416032310060000040e0323100600000408032310060000040c032210060000040603221006000004000322100600000404030c10060000043c030b10060000043a030b100600000438030b100600000436030b100600000434030b100600000432030b100600000430030b10060000042e030b10060000042c030b10060000042a030b100600000428030b100600000426030b100600000426030b100600000426030b100600000426030b100600000424030b100600000424030b100600000424030b100600000424030b100600000412030b100600000410030b100600000410030b100600000410030b10060000040a030b10060000040e030a10060000040e030a10060000040e030a100600000402030a100600000400030a100600000400030a100600000400030a10060000040602f410060000043c02f310060000043c02f310060000043c02f310060000043a02f310060000043802f310060000043602f310060000043402f310060000043202f310060000043202f310060000043202f310060000042802db10060000042802db10060000042802db10060000042602db10060000042402db10060000042002db10060000041e02db10060000041c02db10060000041a02db10060000041802db10060000041402db10060000040e02db10060000041202da10060000040602da10060000040402da10060000040a02c410060000040202c410060000043c02c310060000043a02c310060000043402c310060000042e02c310060000042c02c310060000042a02c310060000042802c310060000042402c310060000042202c310060000041e02c310060000041c02c310060000041802c310060000041602c310060000041002c310060000041402c210060000040c02ac10060000040402ac10060000040202ac10060000043802ab10060000043202ab10060000042e02ab10060000042c02ab100600000424030b100600000424030b100600000424030b100600000424030b10060000042602ab10060000042202ab10060000042202ab10060000042202ab10060000042002ab10060000041e02ab10060000041e02ab10060000041e02ab10060000041002aa100600000400030a100600000400030a100600000400030a10060000040002aa10060000040602941006000004060294100600000406029410060000043c029310060000043a029310060000043802931006000004360293100600000434029310060000043402931006000004340293100600000432029310060000043202931006000004320293100600000430029310060000041a029310060000041402931006000004180292100600000408027c100600000406027c10060000043c027b10060000043a027b100600000430027b100600000426027b100600000424027b100600000420027b100600000414027a10060000040e027a10060000040c027a100600000404027a100600000402027a100600000408026410060000043c026310060000043a026310060000043802631006000004340263100600000430026310060000042c026310060000042802631006000004240263100600000420026310060000041e0263100600000400026310060000041c0262100600000416026210060000040e026210060000043802ab10060000043202ab10060000042e02ab10060000042c02ab100600000424030b100600000424030b100600000424030b100600000424030b10060000043a024b100600000436024b100600000436024b100600000436024b10060000042c024b10060000042a024b10060000042a024b10060000042a024b100600000428024b100600000400030a100600000400030a100600000400030a100600000424024b100600000422024b100600000422024b100600000422024b100600000400024b10060000041e024a100600000418024a100600000404023310060000041a023210060000041a023210060000041a0232100600000412023210060000041202321006000004120232100600000418021c100600000410021c10060000043a021b100600000436021b10060000042e021b10060000042c021b100600000428021b10060000041e021b100600000406021b100600000404021b100600000402021b100600000400021b100600000422021a100600000414021a10060000040a021a10060000041a02041006000004120204100600000410020410060000043c020310060000043a02031006000004380203100600000436020310060000043402031006000004320203100600000430020310060000042e020310060000042c020310060000042a0203100600000428020310060000042002031006000004080203100600000406020310060000043802ab10060000043202ab10060000042e02ab10060000042c02ab100600000424030b100600000424030b100600000424030b100600000424030b10060000041e020210060000040c020210060000040c020210060000040c020210060000041c01ec10060000043c01eb10060000043c01eb10060000043c01eb10060000043a01eb100600000400030a100600000400030a100600000400030a10060000042e01eb10060000042a01eb10060000042a01eb10060000042a01eb10060000042801eb10060000042201eb10060000040801eb10060000040601eb10060000040401eb10060000040401eb10060000040401eb10060000042601ea10060000042601ea10060000042601ea10060000042001ea10060000041a01ea10060000041801ea10060000041001ea10060000040e01ea10060000041e01d410060000041601d410060000041401d410060000040001d410060000043c01d310060000043a01d310060000043801d310060000043601d310060000043201d310060000043001d310060000042e01d310060000042c01d310060000042a01d310060000042401d310060000040c01d310060000040a01d310060000040801d310060000040601d310060000040401d310060000042801d210060000042201d210060000041a01d210060000041201d210060000041001d210060000041801bc10060000040201bc10060000040e026210060000043802ab10060000043202ab10060000042e02ab10060000042c02ab10060000040001bc10060000043c01bb10060000043a01bb10060000043801bb10060000043601bb10060000043401bb10060000043401bb10060000043401bb10060000043001bb10060000042e01bb10060000042e01bb10060000042e01bb10060000042c01bb10060000042601bb10060000042601bb10060000042601bb10060000040e01bb10060000040c01bb10060000040c01bb10060000040c01bb10060000040a01bb10060000040801bb10060000040601bb10060000042401ba10060000041e01ba10060000041e01ba10060000041e01ba10060000041c01ba10060000041c01ba10060000041c01ba10060000041201ba10060000042201a410060000041a01a410060000041801a410060000040401a410060000040201a410060000043c01a310060000043a01a310060000043801a310060000043601a310060000043401a310060000043201a310060000043001a310060000042e01a310060000042801a310060000041001a310060000040e01a310060000040c01a310060000040a01a310060000040801a310060000042c01a210060000042601a210060000042001a210060000041e01a210060000041601a210060000041401a210060000040001a2100600000424018c10060000041c018c10060000041a018c100600000406018c100600000404018c10060000043d618b100600000424030b10060000043b618b100600000439618b100600000437618b100600000435618b100600000433618b100600000431618b10060000042f618b10060000042a018b100600000412018b10060000041eb39b10058000840e018b10060000043e02ab0f060000040a018b10060000042d618a100600000428018a100600000426b3b0100540000420018a100600000418018a100600000426017410060000041e0174100600000406d0e20e8600c0043e027b0f0600800408012c0f0580808420b39b10058000842b31140e858000040201740e8580c0043e02f30f060000043e03230e8600c0043e02db0f06008004140173100600000431c39b100540000424b39b10058000840601440f0580808422b39b10058000842530fc0e8580000404015c0e8580c0041cb39b10058000843e02c30f060000043e02930f060000043e033b0e8600c0043e030b0f0600000427615c100600000420015c10060000041ab39b10058000840a015c100600000433c39b100540000402015c10060000043d115b10060000043b115b100600000439115b10060000043e03530e8600c0043e02630f0600800433115b100600000431115b10060000042cb15b100600000416015b100600000414015b100600000412015b100600000410015b10060000040e015b10060000042f115a10060000042ab15a100600000425615a10060000042601d30f854040841e721703018342851e721703018342051e721703018342451e721703018342851c015a10060000041a015a100600000406015a100600000404014410060000043d314310060000043b31431006000004393143100600000437314310060000043531431006000004333143100600000431314310060000042d1143100600000424b14210060000041d614210060000041b614210060000040801421006000004020142100600000400014210060000043b112b100600000439112b100600000437112b100600000435112b100600000433112b10060000042d312b100600000419612b100600000417612b100600000415612b100600000413612b100600000411612b100600000431112a10060000042b312a100600000427112a100600000425112a10060000041eb12a10060000041cb12a10060000040a012a100600000404012a100600000402012a100600000400012a10060000042931141006000004231114100600000421111410060000040f611410060000040d6114100600000408011410060000043dc11310060000043bc113100600000439c113100600000437c113100600000435c113100600000433c11310060000042f111310060000041ab113100600000412b113100600000431c11210060000042d11121006000004273112100600000425311210060000041f1112100600000406011210060000040401121006000004020112100600000400011210060000042b10fc10060000042330fc10060000042130fc100600000410b0fc10060000040eb0fc10060000040960fc10060000043c50fb10060000043a50fb10060000043850fb10060000043650fb10060000043450fb10060000042fc0fb10060000041b10fb10060000041910fb10060000041710fb10060000041510fb10060000041310fb10060000043250fa10060000042dc0fa10060000042910fa10060000042710fa10060000041f30fa10060000041d30fa10060000040cb0fa10060000042bc0e410060000041110e410060000040f10e4100600000408030a10060000042802ab10060000041802ab10060000041202ab10060000041602aa10060000040c0292100600000410027c100600000424023310060000043050e310060000040802dc10060000040c02f310060000042202f310060000042402f310060000042602f310060000042802f310060000041a030b10060000041c030b10060000040a033a100600000410033b100600000412033b100600000414033b1006000004040353100600000428035310060000043c0353100600000400036a100600000402036b100600000416036b10060000040c038310060000040a03831006000004080383100600000406038310060000043a036b100600000438036b100600000436036b100600000434036b10060000042c036b10060000042a036b100600000428036b100600000426036b100600000410035310060000040e0353100600000422030b10060000042f23e7048243434436420c03020200c51b321c05850300041973d2080302018414a3d2080301c18428027b10060000041bf3de1002400004360263100600000432026c03860000033b41f704060600032a026310060000042602631006000004220263100600000426024b100600000412024a100600000410024a100600000408024a100600000406024a100600000416023410060000040e023410060000040c023410060000043c023310060000043a02331006000004380233100600000436023310060000043402331006000004320233100600000430023310060000042e023310060000042c023310060000040281290105c780430cd0ad01060780033020fc0105c780033e51290105c000433e50e301060000033e51120105c0000339d142101800000300028710120000033c2241101200000300023f101200000317323c10120000030080e310180000032673de048243434435e20c03020200c513321c058503000413a3d208030201841873d2080301c1842a01ba10060000040ab0e4100600000412036b100600000406036a10060000042e02db10060000040002c410060000041a02ab10060000041c02ab100600000416018a10060000041401ba10060000043201bb10060000043201eb10060000042002321006000004000233100600000402023310060000043c024b10060000040a024c10060000040c024c100600000432027b100600000434027b100600000436027b10060000042402ab10060000042a02db10060000042202db10060000041602db10060000043021130105c780430280e301060780030280fb0105c780030281120105c000433020e401060000033e50fb0105c000033bd0e210180000031dd2be10120000031bd2be10120000033c228910120000030202be101200000339d0e2101800000335220c030200008507813b040200008508013b04020000851b011f0381c000850301210381c000852df20c038200008501c3e803824000442a40fd020100010535813c04020000851bd21703018000c536813c040200008537013c04020000853a520c028200008534c20c03020000853d73f004820000841ab3e7048240004417b2930201c0008409b29b0201c000843fa104028200008518f28b0181c000833281650301c000853c013c040200008508022f0300c001843fa0e4028200008508520f0281c0408508020f0281c0408507736503814000c403034d03814000c40400fc03014000c502834d04014000c40a72ee0381000104000306038100010400c0280081c000830801780200c0008506220f0281c0008511c20e0281c0008500012d0381c000851f012e0381c0008510b0a00100c000832863050181c0008328630c0181c000830581a00200c0010534812002814000c50cf05100808002030670a6028040014304205700804002032981f50281c000852381f10281c000851581f10281c000850701f00281c000850e01ef0281c000851301ee0281c000852a01ed0281c000852401e90281c000851601e90281c000850781e80281c000850e81e70281c000851e603f00818000c32f40210081c000c317b29a0201c0008422817d028100010509b2940201c000841681e10281c000851d81710301c000850f01df0281c000852b01dd0281c000851701d90281c000850881d80281c000850f81d70281c000851481d60281c000852b81d50281c000852581d10281c000853d120f0181c0008313b27c0201c000840901d00281c0008511b1c90201c000841001cf0281c000851501ce0281c000852b81710301c000852c01cd0281c000850101750301c000852601c90281c000851801c90281c000850981c80281c000851081c70281c000851581c60281c000852c81c50281c000852681c10281c000851881c10281c000850a01c00281c000853d11f30181c000831101bf0281c0008528631a0181c000830e025f0280c000841050d3028040024338509e0100c0008307012c02814001051601be0281c0008533813002814001052d01bd0281c0008506813402814001052bb1b10201c0008428e21a02818001052701b90281c0008500c0210081c000833bd26a02020000831db1ab0201c0008400903b0101c00083090143028140010503014802814001050b814b02814001052a721a02818001052b121a02818001050281500281400105210150028140010519e2b40181c00083308160028140010501016802814001051f8168028140010506816b028140010536521a028180010502017c028140010533c3bb0201c0008311d0220081c0008321f34b0201c000832640e20300800103018184028140010509d3e802824000442e81a50281c0008516b20d02820000453d720c02820000451a81a10281c000853d220c02820000450a23e802824000040c01a00281c000c53cd20c028200004518019e0281c000853b920c02820000452ea2d70181c000831831d501820000431ce01901020000430a73e802824000040ac3e802824000040b13e802824000041d0190028140010519018e0301c000853fa3f002820000442600d90281c000850763b3028200004302e2100281c000850e020e028200004518920d02820000850b920303024000041ea3e8028240000430621a02818001052400ce0281c0008518a0c80281c000853090960180c0010517520d02820000853af20c028200004519002d01020000433a020c02820000450ef20e02820000453aa20c02820000451ef3e802824000042020af0281c0008501a2100281c0008524a0b60281c000851c321703018000850ea25f0280c0014426a0c10281c000851f80c70281c000853fe03e00818000c31100f303014000c50e00f303014000c50f01df0281c0008507012c028140010523a0d60281c0008511220e02820000452b01dd0281c000852701f802014001053893000201c000831fc1f101814001042710f2030200008431021a02818001050f81d70281c0008506813402814001051f43e8028240000418019e0281c000851201400381c000850932a503814001040e01ef0281c0008517721b02014001052881a10281c0008518b21b02014001051f93e8028240000433c3c20201c000830901d00281c0008518f21b02014001051201af0281c000852e924002014001033581350381c0008503816402814001052581d10281c0008518321b02014001052b81710301c000850b814b02814001050a81b80281c0008517b21b02014001052c01cd0281c000852a721a02818001051701d90281c0008533813002814001050981c80281c0008500817002814001050801e00281c0008536521a028180010536012d0381c0008530220c03820000852681c10281c0008506816b028140010533a20c030200008514d217030180010506c20f0281c000852881e002014001051381280381c000850e012302814001052d01490402000085012217030180010508a20f0281c0008502011c02814001052c81610301c000850d80fb030140010513b28a0201c000843fb3e2020140010409e20f0281c0008519b21b02014001051401200381c000850a80f403014001050a320f0281c000851781e80201400105283329030200008314121703018001051fe3e802824000043b420c028200004535820c030200008501e21703018001050933e802824000043be20c02820000450893e802824000043c320c02820000450843e802824000043c820c02820000450663e802824000040a820f0281c000853101190301c000851831e50181c0008317917303024000043fa3f002820000440573e802824000040ad20f0281c000851700f00301c0008538f0a4010080014310b0a00100c0008322018502010000853dc20c02820000850f81d3038280000424814d04020000850b220f0281c0008516a2a6040100008404d3e802824000040483e802824000043e120c02820000452cf18b03024000043e620c028200004503e3e802824000043eb20c02820000450393e8028240000421010e0381c000850343e802824000043f020c02820000451020c7018240000302f3e8028240000402a3e802824000043f520c02820000450263de02824000043fa20c02820000451813d3028240000412420d02820000453d63d2028240000411b1bb0201c000842843c5028240000412920d02820000453dd3c302824000043f93b00282400044059254028240000412e20d028200004502625502024000033fc09701824000030b720f0281c000852ea2c20181c0008305924b028240000416120d028200004523920c02824000040b920c028240004417309a018240000301304a008140010323920302824000440cb20f0281c000c53fa18002824000042e517302824000040800ad01824000032cf19403024000043fa16e02824000042eb16402824000041601000381c000852eb15b02824000040d020f0281c000851801c90281c000853d120f0181c0008300d0c3018240000313b27c0201c000842cd3f80401c000842270f60381c000850d81880301c000852ab14d028240000406b3e8028240000405c3e8028240000417f20d02820000853b920c02820000450523e802824000443bd27a02020000433cc21a0201400105210150028140010500c0210081c000832ea2d70181c000833bd26a02020000830b80e403014001050c00dc030140010526c20d028200004502626702020000433bd26202020000430433e802824000442fc21a02818001053d51860381c000831c01a002814001050292100281c000850242100281c000850f01700301c0008524012002814001050080d20381c0008531020c038200008501f2100281c00085218148028140010521d333030280000338820c03020000850242100281c0008528020d02820000450ca0ad0281c0008531521a028180010509e2d502020000431e70e702018000c53fa17702824000040db20e02820000450ac3e802824000041570d001824000030e520e02820000852de21a02818001050ea20e02820000851831dd01820000830f420e02820000852bb21a02818001052cf18b03024000043e620c028200004524b13502824000040fe20e028200004524b12c02824000040102100281c000851cb11c028240000410320e028200004500017802814000c50101c302814000c53db3f80381c000843d43f80381c000843cd3f80381c000843c63f80381c000843bf3f80381c000843b83f80381c000843b13f80381c000843aa3f80381c000841b01f502010001051801ad02010001050a00d8038140010500b2100281c000850062100281c0008533a28004010001431aa276040100014413328e03810001040ef25f0280c001441ef2470280c0014417b2930201c0008427b1930201c0008411213101814000c411213601814000c40500ec03014000c50a02ee038100010412a0c00280c0014508c0ae0280c0014509b29b0201c0008417f0e00201c0008519c28a01814000c419c28501814000c417918503014000c40301210381c0008532c3f80381c000843253f80381c0008409015804020000850c620f0281c0008504818b02814000c534014d0381c000850c015804020000851f017002814000c50d015804020000850d815804020000850e015804020000850e815804020000850f015804020000850f8158040200008519d289020200008319d28102020000830b328e040100008418a28e040100018412f28f0180c000c419c28f0180c000c408f2a70180c001842803da0180c0018408c22f0300c000c420022f0300c001841301300381c000851a012f0381c000853001290381c000852201290381c0008521818d02010001051a81270381c000850081250381c000850181150381c000850401110381c000850281050381c000850501010381c000851680f80381c000850380f50381c000852d73e704824000440a80dd0381c000850000da0381c000850580d50381c000850800d10381c000850680c50381c0008508f0c10381c000852700b90381c000852500ae0381c000853e93f80381c000843e23f80381c0008438e3f80381c000843793f80381c000843723f80381c000840101c302814000c500017802814000c535d3f80381c0008427b0cb01008002030bc20f0281c0008512b0fc02824000041f43e8028240000400d0c301824000032780b10301c000851310ed028240000439c21a02018000c50f01df0281c000852b01dd0281c000853893000201c000830001510381c000850f81d70281c000850a01c00281c000852581d10281c000852981910301c000853d120f0181c000830901d00281c000850780d90381c000851001cf0281c000851501ce0281c0008509920f0281c000852b81710301c000850a81b80281c000852c01cd0281c0008509420f0281c000850981c80281c000851881c10281c0008536012d0381c000852681c10281c000850080d20381c0008531017d0301c000850cb0eb028240000407b20f0281c000852780b10301c000851d01790301c000851310e4028240000407620f0281c000852cf19d030140010428520d028200008529821a028180010507120f0281c000853d021a02014001053bd272020200008329d21a028180010501f2100281c00085385094010140010318f21b02014001052d421a02818000c52d921a0281800105010168028140010519d27902020000830b814b02814001050900e803014001050cb0f402818001040a80d0030140010507818b02814001050a0163028140010530220c03820000852d81b50281c000c531020c03820000853780d003814001050cb0e202824000041981b10281c000850763aa02824000030763a10282400043076398028240000319320d028200008519820d02820000850b63e802824000440983e8028240000419d20d028200004533a3f80381c000840280b20301c000853bd25002024000030402c602020000433fc0ac01824000033ed3420181c000830800b601824000031831cd018200004310b0d301820000831ce021010200008309e2dd02020000833892db020200008330909601828000033fc0b501824000431a220d02820000853892d3020200008318f21b020140010527120d028200008500903301020000830b01b00281c000852ea2bb0181c0008327620d028200008535621a02818001050ea21603818000c50391d403024000041701ae0281c000850391dd030240000428a20d02820000451c81810301c000850b921503014001040d620e028200008536a21a028180010507a3e802824000040f920e02820000450ea20e02820000851831dd01820000832801a90281c0008533c3c90201c0008324b13502824000040fe20e02820000450703e802824000041a01a90281c0008510820e02820000851ab21703018001052e517c028240000410d20e02820000451b014302824000041cb113028240004408e3e802824000043d122b0181c000830b81a80281c0008525011002814000c51281a70281c0008511d01a00820000832eb03e0180400083253216030180008510d19703004000831781a60281c0008516220f0181c000833d12080181c000833d11fa0181c0008337921a028180010521f3520201c000830f810b02814001050402be020200008307f3e802824000043f020c02820000453c53f004820000843ce3f004820000843733f005020000843803f80381c000843873f80381c000841681e10281c000851681e10281c000853953f80381c0008438321a02818001053a33f80381c000c421f3440201c000c32ea2b40181c0008319e2980181c000833f03f80381c000843f73f80381c00084318165040200008532016504020000850362eb0481c0008432c3f80381c000840003e803824000440770b50381c000c50100bd038140010509634d04014001040200ba0381c000852a53f80401c000842681c10281c000852a81810301c0008500c21703018000c50180c20381c000850a812b028140010506521603818000c534c21a02818000c516016f0301c0008534721a02818000c532016d0301c0008534221a02818000c50600cd0381c000850b70cd0381c000850500dd0381c000851081580301c000851781570301c0008513019f0281c000853381550301c000850700e10381c000850480e50381c0008512f21703018000c51f81510301c000853681250301c000852401090301c000853820fd0281c000853880f50281c000850a00e50381c000851770e80381c000850680e90381c000850400ed0381c000850700bd0301c000853af0ad0301c000851df0ef0381c000850600f10381c000851b01990301c000850c81980301c0008529011502810001450600dc03814000c505721603818000852f81950301c000851b81910301c000850d01900301c0008514018f0301c000850580f90381c000850300fd0381c000850880fd0381c000851d00ff0381c0008506c21603818000c52471010381c000852181060381c000851c81070381c000851581080381c000850481090381c0008502010d0381c0008507810d0381c000851c010f0381c000851501100381c0008504e25e05010000c40780c403014000850af0c803814000853171110381c0008532520c038200004532c20c038200004536a20c03020000450701150381c000852081160381c000851b81170381c000853a00cd0301c0008500621703018000c51481180381c000852ed21a02818000c50381190381c000852e321a02818000c52f019d0301c000851381970301c0008532a0f10281c0008530018d0301c000852301190381c000851c01890301c0008523b21603018000c501011d0381c0008506811d0381c0008520011e0381c000852281210381c000853081210381c000852e53f80401c0008408121603818000c52ca21a02818000c525a0e10281c000853920dd0281c000851730a30180c001833d121d0181c000831622160181c000833dc34201808001c31300c90100800103106197038040008310b0a00100c0008329220502018001051a81b802814001052d81510301c000850501b302814001051d80f70301c000850a815b02814001053480c10301c000851c016d02810001452800c003814000c512b10e02814001041981370381c000851981860301c000853081850301c000850001850301c0008515017f0301c000851a017e0301c0008500817d0301c0008504813d0381c0008535013d0381c0008502421703018000c50201650301c000851e013e0381c0008519013f0381c000850301550301c000852200fe0301c000850d521603818000c52081410381c000850401450381c000853481450381c000851481870301c000851581770301c000851681670301c0008502815d0301c000851d81460381c000851881470381c0008525814d02810001450f121603818000c52001490381c000852e01490381c0008503814d0381c000853892f20201c000831900c00281c0008518014f0381c000850f70db03814000c53f93b90301c000841101500381c000851f01590301c0008510d21603818000c50081490381c000852f01390381c000852a01890301c000850b00d50301c000851c81560301c000851c921703018000c53181750301c000852d01590301c000850000cd03814000c50ce21603818000850c021603818000850601250381c0008505812d0381c000852e81410381c000850a421603818000c51401de0281c000850281290381c000851c015e0301c000852901990301c000851320c302814001050801050381c000850300e102810001450180b503814000c53e421a02014001051b121703018000c51e01690301c0008508f20f0281c000853643f80381c000841681b60281c0008524721603018000c5248118028140010513b21703018000c50900f50381c000850c70d403814000c51b016e0301c000851881960301c0008536b3f80381c000842d019802814001051cb12502814001040101410381c000853153f80401c0008405e21603818000851ce20903820000451300c901008001032500c0038040014312a28f0280c0010404633603010000c406720f0281c000853dc34201808001c31730a30180c001830801780200c000851e81610301c0008517b29a0201c0008409b2940201c000841900350101c000833480360101c000830c40bd0201c000850970b90201c0008537e21a02818000c509810402814000c52780e203008001430701880200c000850801780200c000853e234201808001c311d0220081c0008329b1bb0201c000840501350381c000851c015e0301c000852880e2028080008328811d02810000852c521a02818000852c01690301c0008525c0e2040080014313019f0281c000853381550301c000851f01590301c000852bd3f80401c000841db1a40201c000840670a602804001430670a6028040014311319703004001430670a602804001432500c003804001432500c003804001433d11ec0181c000830e02bf0280c001c410b0a00100c000833b30b00100c000833b30b90100c001c338509e0100c000830801780200c000850201d80200c000850a01580200c001c50701880200c0008538f0a4010080014307431b0481c000841a81760301c000852b01790301c000850873de048280000412b105028240004418420d028200004515821402828000030202b905014000c3060178040200004506817804020000851d83e704824000440b43de048280000409e207038200008509720703820000850902070382000085089207038200008533a0d10281c000851c63e704824000443dd0e20280400203198195020100014500028e05010001440a0335048140010427221505018000c50402d60501000144153276050100014418028e050100014426821505018000c51a027605010001442500c0038040014336f11d0381c000852e017804020000450181390381c0008502817904020000452fd3f80401c000841c017904020000451c81790402000045230179040200004523817904020000452401790402000045248179040200004525017904020000450de20603820000452601790402000045268179040200004527017904020000450d7206038200004528017904020000452881790402000045290179040200004529817904020000452a017904020000452a8179040200004503017b040200004528b205038200004504017b040200004504817b0402000045284205038200004508017b040200004527d2050382000045276205038200004526f20503820000451a4205038200004532817c040200004533017c040200004533817c040200004534017c040200004519d205038200004535017c0402000045196205038200004536017c040200004518f205038200004537017c0402000045188205038200004538017c040200004538817c0402000045181205038200004517a20503820000453a017c04020000453a817c04020000453b017c04020000453b817c04020000453c017c04020000453c817c04020000453d017c04020000453d817c04020000453e017c040200004517320503820000453f017c040200004500017d04020000451a817d040200004516c20503820000451f017d0402000045165205038200004520017d040200004520817d040200004521017d040200004521817d040200004522017d040200004512e2050382000045127205038200004530017d040200004530817d040200004504f216040180008516017e040200004516817e040200004517017e040200004517817e040200004518017e040200004518817e04020000452dd3f80401c0008419817e04020000450e817f04020000450f017f04020000450f817f040200004510017f040200004510817f040200004511017f040200004511817f04020000453e720403820000453e0204038200004513017f040200004513817f04020000453d9204038200004514817f040200004505818004020000450601800402000045068180040200004507018004020000453d22040382000045090180040200004509818004020000450a018004020000450a818004020000450b018004020000450b818004020000450c018004020000450c818004020000450d018004020000450d818004020000451d018004020000451d818004020000452b018004020000452b818004020000452c018004020000452c818004020000452d018004020000452d818004020000452e0180040200004502018104020000451b018104020000451b818104020000451c018104020000452281810402000045230181040200004523818104020000452401810402000045248181040200004525018104020000452581810402000045260181040200004526818104020000452701810402000045278181040200004528018104020000452881810402000045290181040200004529818104020000452a01810402000045028183040200004503018304020000450381830402000045040183040200004504818304020000450781830402000045008184040200004501018404020000453101840402000045318184040200004532018404020000453281840402000045330184040200004533818404020000453401840402000045348184040200004535018404020000453581840402000045360184040200004536818404020000453701840402000045378184040200004538018404020000453881840402000045390184040200004539818404020000453a018404020000453a818404020000453b018404020000453b818404020000453c018404020000453c818404020000453d018404020000453d818404020000453e018404020000453e818404020000453f018404020000451a018504020000451e018504020000451e818504020000451f018504020000451f8185040200004520018504020000452081850402000045210185040200004521818504020000452e818504020000452f018504020000452f8185040200004530018504020000451501860402000045158186040200004516018604020000451681860402000045170186040200004517818604020000451801860402000045188186040200004519018604020000450e018704020000450e818704020000450f018704020000450f81870402000045100187040200004510818704020000451101870402000045118187040200004512018704020000451281870402000045130187040200004513818704020000451401870402000045050188040200004505818804020000450601880402000045068188040200004508018804020000450881880402000045090188040200004509818804020000450a018804020000450a818804020000450b018804020000450b818804020000450c018804020000450c818804020000450d018804020000451c818804020000451d018804020000452a818804020000452b018804020000452b818804020000452c018804020000452c818804020000452d018804020000452d8188040200004501818904020000451a818904020000451b018904020000451b81890402000045220189040200004522818904020000452301890402000045238189040200004524018904020000452481890402000045250189040200004525818904020000452601890402000045268189040200004527018904020000452781890402000045280189040200004528818904020000452901890402000045298189040200004502018b040200004502818b040200004503018b040200004503818b040200004504018b040200004507018b040200004500018c040200004500818c040200004530818c040200004531018c040200004531818c040200004532018c040200004532818c040200004533018c040200004533818c040200004534018c040200004534818c040200004535018c040200004535818c040200004536018c040200004536818c040200004537018c040200004537818c040200004538018c040200004538818c040200004539018c040200004539818c04020000453a018c04020000453a818c040200004535818c04020000453b018c04020000453b818c04020000453c018c04020000453c818c04020000453d018c04020000453d818c04020000453e018c04020000453e818c04020000453f018c040200004519818d04020000451d818d04020000451e018d04020000451e818d04020000451f018d04020000451f818d040200004520018d040200004520818d040200004521018d04020000452e018d04020000452e818d04020000452f018d04020000452f818d040200004514818e040200004515018e040200004515818e040200004516018e040200004516818e040200004517018e040200004517818e040200004518018e040200004518818e04020000450d818f04020000450e018f04020000450e818f04020000450f018f04020000450f818f040200004510018f040200004510818f040200004511018f040200004511818f040200004512018f040200004512818f040200004513018f040200004513818f04020000450481900402000045050190040200004505819004020000450601900402000045078190040200004508019004020000450881900402000045090190040200004509819004020000450a019004020000450a819004020000450b019004020000450b819004020000450c019004020000450c819004020000451c019004020000451c819004020000452a019004020000453cb20403820000452b019004020000453c420403820000452c019004020000452c819004020000452d0190040200004501019104020000453bd20403820000451a819104020000451b01910402000045218191040200004522019104020000452281910402000045230191040200004523819104020000452401910402000045248191040200004525019104020000452581910402000045260191040200004526819104020000452701910402000045278191040200004528019104020000452881910402000045290191040200004501819304020000450201930402000045028193040200004503019304020000450381930402000045068193040200004500019404020000453001940402000045308194040200004531019404020000453181940402000045320194040200004532819404020000453301940402000045338194040200004534019404020000453481940402000045350194040200004535819404020000453601940402000045368194040200004537019404020000453781940402000045380194040200004538819404020000452953f80401c00084398194040200004529d3f80401c0004439a204038200004539320403820000453b819404020000453c0194040200004538c2040382000045385204038200004537e20403820000453e019404020000453e819404020000453f0194040200004519019504020000451d019504020000451d819504020000451e019504020000451e819504020000451f019504020000451f81950402000045200195040200004520819504020000452d819504020000452e019504020000452e819504020000452f019504020000451401960402000045148196040200004515019604020000451581960402000045160196040200004516819604020000451701960402000045178196040200004518019604020000450d019704020000450d819704020000450e019704020000450e819704020000450f019704020000450f81970402000045100197040200004510819704020000451101970402000045118197040200004512019704020000451281970402000045130197040200004504019804020000450481980402000045050198040200004505819804020000450701980402000045078198040200004508019804020000450881980402000045090198040200004509819804020000450a019804020000450a819804020000450b019804020000450b8198040200004537720403820000451b819804020000451c0198040200004529819804020000452a019804020000452a8198040200004537020403820000452b819804020000452c019804020000452c81980402000045008199040200004519819904020000451a019904020000451a81990402000045210199040200004521819904020000452201990402000045228199040200004523019904020000452381990402000045240199040200004536920403820000452501990402000045258199040200004526019904020000452681990402000045270199040200004527819904020000452801990402000045288199040200004501019b040200004501819b040200004502019b040200004502819b040200004503019b040200004536220403820000452f819c040200004530019c040200004535b204038200004531019c040200004531819c040200004532019c040200004532819c040200004533019c040200004539420c030200004534019c040200004534819c040200004535019c040200004534d204038200004536019c040200004536819c040200004537019c040200004537819c0402000045346204038200004538819c040200004539019c040200004539819c04020000453a019c04020000453a819c04020000453b019c04020000453b819c04020000453c019c04020000453c819c040200004533f20403820000453d819c04020000453e019c04020000453e819c04020000453f019c040200004518819d04020000451c819d040200004533820403820000451d819d040200004533120403820000451e819d04020000451f019d04020000453892e3020200004331c20403820000452d019d04020000452d819d040200004531520403820000452e819d040200004513819e040200004514019e040200004530e2040382000045307204038200004515819e040200004516019e040200004516819e040200004517019e040200004517819e04020000450c819f04020000450d019f04020000450d819f04020000450e019f04020000450e819f04020000450f019f04020000450f819f040200004510019f040200004510819f040200004511019f040200004511819f040200004512019f040200004512819f04020000450381a004020000450401a004020000450481a004020000450501a004020000450681a004020000450701a004020000450781a004020000452f920403820000450881a004020000450901a004020000450100ca0381c000852eb20403820000450a81a004020000450b01a004020000452c53f80401c000841b01a004020000451b81a004020000452901a004020000452981a004020000452a01a004020000452a81a004020000452b01a004020000452b81a004020000452c01a004020000452dd20403820000451901a104020000451981a104020000451a01a104020000453793f80381c000842cf20403820000452c820403820000452c120403820000452281a104020000452301a104020000452ba20403820000452401a104020000452ac20403820000452501a104020000452581a104020000452601a104020000452a520403820000452701a104020000452781a104020000452801a104020000450081a304020000450101a3040200004529720403820000450201a304020000450281a304020000450581a304020000452f01a404020000452f81a4040200004522d20103820000453081a404020000453101a40402000045226201038200004521f2010382000045218201038200004520320103820000453381a404020000451fc20103820000453481a404020000451f520103820000453581a404020000451ee20103820000453681a404020000453701a404020000453781a404020000453801a404020000453881a404020000453901a404020000453981a404020000452ed3f80401c000843a81a404020000453b01a404020000453b81a404020000453c01a404020000453c81a404020000453d01a404020000453d81a404020000453e01a404020000453e81a404020000453f01a404020000451801a504020000451c01a504020000451c81a504020000451d01a504020000451d81a504020000451e01a504020000451e020103820000451f01a504020000451f81a504020000451d920103820000452d01a504020000452d81a504020000452e01a504020000451301a604020000451381a604020000451cb20103820000451481a604020000451501a604020000451581a604020000451601a604020000451681a604020000451701a604020000450c01a704020000450c81a704020000450d01a704020000450d81a704020000450e01a704020000450e81a704020000450f01a704020000450f81a704020000451001a704020000451081a704020000451101a704020000451181a704020000451201a704020000450301a804020000450381a804020000450401a804020000450481a804020000450601a804020000450681a804020000450701a804020000450781a804020000450801a804020000450881a804020000450901a804020000450981a804020000450a01a804020000450a81a804020000450b01a804020000451a81a804020000451b01a804020000452881a804020000452901a804020000452981a804020000451c420103820000452a81a804020000452b01a804020000452b81a804020000451881a904020000451901a904020000451981a904020000452001a904020000452081a904020000452101a904020000452181a904020000452201a904020000452281a904020000452301a904020000452381a904020000452401a904020000452481a904020000452501a904020000452581a904020000452601a904020000452681a904020000452701a904020000452781a904020000450001ab04020000450081ab04020000450101ab04020000450181ab04020000450201ab04020000450501ab04020000452e81ac04020000452f01ac04020000452f81ac04020000453001ac04020000453081ac04020000453101ac04020000453181ac04020000453201ac04020000453281ac04020000453301ac04020000453381ac04020000453401ac04020000453481ac04020000453501ac04020000451bd20103820000453601ac04020000453681ac04020000453701ac04020000453781ac04020000453801ac04020000453881ac04020000453901ac04020000453981ac04020000453a01ac04020000453a81ac04020000453b01ac04020000453b81ac04020000453c01ac04020000453c81ac04020000453d01ac04020000451b620103820000453e01ac04020000453e81ac04020000453f01ac04020000451781ad04020000451b81ad04020000451c01ad04020000451c81ad04020000451d01ad04020000451af20103820000451e01ad04020000451e81ad04020000451f01ad04020000452c01ad04020000452c81ad04020000452d01ad04020000452d81ad04020000451281ae04020000451301ae04020000451381ae04020000451401ae04020000451481ae04020000451501ae04020000451581ae04020000451601ae04020000451681ae04020000450b81af04020000450c01af04020000450c81af04020000450d01af04020000450d81af04020000450e01af04020000450e81af04020000450f01af04020000450f81af04020000451001af04020000451081af04020000451101af04020000451181af04020000450281b004020000450301b004020000450381b004020000450401b004020000450581b004020000450601b004020000450681b004020000450701b004020000450781b004020000450801b004020000450881b004020000450901b004020000450981b004020000450a01b004020000450a81b004020000451a01b004020000451a81b004020000452801b004020000452881b004020000452901b004020000452981b004020000452a01b004020000452a81b004020000452b01b004020000451801b104020000451881b104020000451901b104020000451f81b104020000452001b104020000452081b104020000452101b104020000452181b104020000452201b104020000452281b104020000452301b104020000452381b104020000452401b1040200004514c20103820000452181310381c000852581b1040200004513e201038200004513720103820000452701b104020000452682000382000045261200038200004525a20003820000450181b304020000450481b304020000452e01b404020000452e81b404020000452f01b404020000452f81b404020000453001b404020000453081b4040200004525320003820000453181b404020000453201b404020000453281b404020000453301b404020000453381b404020000453401b404020000453481b404020000453501b404020000453581b404020000453601b404020000453681b404020000453701b404020000453781b404020000453801b404020000453881b404020000453901b404020000453981b404020000453a01b404020000453a81b404020000453b01b404020000453b81b404020000453c01b404020000453c81b404020000453d01b404020000453d81b404020000453e01b404020000453e81b404020000453f01b404020000451701b504020000451b01b504020000451b81b504020000451c01b504020000451c81b504020000451d01b504020000451d81b504020000451e01b504020000451e81b504020000452b81b504020000452c01b504020000452c81b504020000452d01b504020000451201b604020000451281b604020000451301b604020000451381b604020000451401b604020000451481b604020000451501b604020000451581b604020000451601b604020000450b01b704020000450b81b704020000450c01b704020000450c81b704020000450d01b704020000450d81b704020000450e01b704020000450e81b704020000450f01b704020000450f81b704020000451001b704020000451081b704020000451101b704020000450201b804020000450281b804020000450301b804020000450381b804020000450501b804020000450581b804020000450601b804020000450681b804020000450701b804020000450781b804020000450801b804020000450881b804020000450901b804020000450981b804020000450a01b804020000451981b804020000451a01b804020000452781b804020000452801b804020000452881b804020000452901b804020000452981b804020000452a01b804020000452a81b804020000451781b904020000451801b904020000451881b904020000451f01b904020000451f81b904020000452001b904020000452081b904020000452101b904020000452181b904020000452201b904020000452281b904020000452301b904020000452381b904020000452401b904020000452481b904020000452501b904020000452581b904020000452601b904020000452681b904020000450001bb04020000450081bb040200004524c20003820000450401bb04020000452d81bc04020000452e01bc04020000452e81bc04020000452f01bc04020000452f81bc04020000453001bc04020000453081bc04020000453101bc04020000453181bc04020000453201bc04020000453281bc04020000453301bc04020000453381bc04020000453401bc040200004524520003820000453501bc040200004523e20003820000453601bc04020000453681bc04020000453701bc04020000453781bc04020000453801bc04020000453881bc04020000453901bc04020000453981bc04020000453a01bc04020000453a81bc04020000453b01bc04020000453b81bc040200004515e20003820000453c81bc04020000453d01bc04020000453d81bc04020000453e01bc04020000453e81bc04020000453f01bc040200004515720003820000451a81bd04020000451b01bd04020000451b81bd04020000451c01bd04020000451c81bd04020000451d01bd04020000451d81bd04020000451e01bd04020000452b01bd04020000452b81bd04020000452c01bd040200004505f20003820000451181be04020000451201be04020000451281be04020000451301be04020000451381be04020000451401be04020000451481be04020000451501be04020000451581be04020000450a81bf04020000450b01bf04020000450b81bf04020000450c01bf04020000450c81bf04020000450d01bf04020000450d81bf04020000450e01bf04020000450e81bf04020000450f01bf04020000450f81bf04020000451001bf04020000451081bf04020000450181c004020000450201c004020000450281c004020000450301c004020000450481c004020000450501c004020000450581c004020000450601c004020000450681c004020000450701c004020000450781c004020000450801c004020000450881c004020000450901c004020000450981c004020000451901c004020000451981c004020000452701c0040200004505820003820000452801c004020000452881c004020000452901c004020000452981c004020000452a01c004020000451701c104020000451781c104020000451801c104020000451e81c104020000451f01c104020000451f81c104020000452001c104020000452081c104020000452101c104020000452181c104020000452201c104020000452281c104020000452301c104020000452381c104020000452401c104020000452481c104020000452501c104020000452581c104020000452601c104020000450001c304020000450081c304020000450381c304020000452d01c404020000452d81c404020000452e01c404020000452e81c404020000452f01c404020000452f81c404020000453001c404020000453081c404020000453101c404020000453181c404020000453201c404020000453281c404020000453301c404020000453381c404020000453401c404020000453481c404020000453501c404020000453581c404020000453601c404020000453681c404020000453701c404020000453781c404020000453801c404020000453881c404020000453901c404020000453981c404020000450d01ff03820000453a81c404020000453b01c404020000453b81c404020000453c01c404020000453c81c404020000451201fe03820000453d81c404020000453e01c404020000453e81c404020000452901fd03820000451601c504020000451a01c504020000451a81c504020000451b01c504020000451b81c504020000451c01c504020000451c81c504020000451d01c504020000451d81c504020000452a81c504020000452b01c504020000452b81c504020000452c01c504020000451101c604020000451181c604020000451201c604020000451281c604020000451301c604020000451381c604020000451401c604020000451481c604020000451501c604020000450a01c704020000450a81c704020000450b01c704020000450b81c704020000450c01c704020000450c81c704020000450d01c704020000450d81c704020000451a81fd03820000450e81c704020000450f01c704020000450f81c704020000451001c704020000450101c804020000451301fd03820000453f81fc03820000450281c804020000450401c804020000450481c804020000450501c804020000450581c804020000450601c804020000450681c804020000450701c804020000452301f903820000450801c804020000450881c804020000450901c804020000451501f903820000451901c804020000452681c804020000452701c804020000452781c804020000452801c804020000452881c804020000452901c804020000452981c804020000451681c904020000451701c904020000451781c904020000451e01c904020000451e81c904020000451f01c904020000453f81f403820000452001c904020000452081c904020000452101c904020000452181c904020000452201c904020000452281c904020000452301c904020000452381c904020000452401c904020000452481c904020000452501c904020000452581c904020000450001cb04020000450301cb04020000452c81cc04020000452d01cc04020000452d81cc04020000452e01cc04020000452e81cc04020000452f01cc04020000452f81cc04020000453001cc04020000453081cc04020000453f81ec03820000453181cc04020000453f81e403820000453281cc04020000453301cc04020000453381cc04020000453401cc04020000453481cc04020000453501cc04020000453581cc04020000453601cc04020000453681cc04020000453701cc04020000453781cc04020000453801cc04020000453881cc04020000453901cc04020000453981cc04020000453a01cc04020000453a81cc04020000453b01cc04020000453b81cc04020000453c01cc04020000453c81cc04020000453d01cc04020000453d81cc04020000453e01cc04020000453e81cc04020000453f01cc04020000451581cd04020000451981cd04020000451a01cd04020000451a81cd04020000451b01cd04020000451b81cd04020000453f81dc03820000451c81cd04020000451d01cd04020000452a01cd04020000452a81cd04020000452b01cd04020000452b81cd04020000451081ce04020000453f81d403820000451181ce04020000451201ce04020000451281ce04020000451301ce04020000451381ce04020000451401ce04020000451481ce04020000450981cf04020000450a01cf04020000450a81cf04020000450b01cf04020000450b81cf04020000450c01cf04020000450c81cf04020000450d01cf04020000450d81cf04020000450e01cf04020000450e81cf04020000450f01cf04020000453f81cc03820000450081d004020000450101d004020000450181d004020000450201d004020000450381d004020000450401d004020000450481d004020000450501d004020000450581d004020000450601d004020000450681d004020000450701d004020000450781d004020000450801d004020000450881d004020000451801d004020000451881d004020000452601d004020000452681d004020000452701d004020000452781d004020000452801d004020000452881d004020000452901d004020000452f81310381c000851681d104020000451701d104020000453f81bc03820000451e01d104020000451e81d104020000451f01d104020000453f81b403820000452001d104020000452081d104020000452101d104020000452181d104020000452201d104020000452281d104020000453f81ac03820000453f81a403820000452401d104020000452481d104020000452501d104020000450281d304020000452c01d404020000452c81d404020000452d01d404020000452d81d404020000452e01d404020000452e81d404020000452f01d404020000453f819403820000453001d404020000453081d404020000453101d404020000453181d404020000453201d404020000453281d404020000453301d404020000453381d404020000453401d404020000453481d404020000453501d404020000453581d404020000453601d404020000453f818c03820000453701d404020000453781d404020000453801d404020000453881d404020000453901d404020000453981d404020000453a01d404020000453a81d404020000453b01d404020000453f818403820000453c01d404020000453c81d404020000453f817c03820000453d81d404020000453e01d404020000453e81d404020000453f01d404020000451501d504020000451901d504020000451981d504020000451a01d504020000451a81d504020000451b01d504020000451b81d504020000451c01d504020000451c81d504020000452981d504020000452a01d504020000452a81d504020000452b01d504020000451001d604020000451081d604020000451101d604020000451181d604020000451201d604020000451281d604020000451301d604020000451381d604020000451401d604020000450901d704020000450981d704020000450a01d704020000450a81d704020000450b01d704020000450b81d704020000450c01d704020000450c81d704020000450d01d704020000450d81d704020000450e01d704020000450e81d704020000450f01d704020000450001d804020000450081d804020000450101d804020000450181d804020000450301d804020000450381d804020000450401d804020000450481d804020000450501d804020000453f817403820000450601d804020000450681d804020000450701d804020000453f816c03820000450801d804020000453f816403820000451801d804020000452581d804020000452601d804020000453f815c03820000452701d804020000452781d804020000452801d804020000452881d804020000451581d904020000451601d904020000451681d904020000451d01d904020000451d81d904020000451e01d904020000451e81d904020000451f01d904020000451f81d904020000452001d904020000452081d904020000452101d904020000453f815403820000452201d904020000452281d904020000452301d904020000452381d904020000452401d904020000452481d904020000450201db04020000452b81dc04020000452c01dc04020000452c81dc04020000452d01dc04020000452d81dc04020000452e01dc04020000452e81dc04020000452f01dc04020000452f81dc04020000453001dc04020000453081dc04020000453101dc04020000453181dc04020000453201dc04020000453281dc04020000453301dc04020000453381dc04020000453401dc04020000453481dc04020000453501dc04020000453581dc04020000453601dc04020000453681dc04020000453701dc04020000453781dc04020000453801dc04020000453881dc04020000453901dc04020000453981dc04020000453a01dc04020000453a81dc04020000453b01dc04020000453b81dc04020000453c01dc04020000453c81dc04020000453d01dc04020000453d81dc04020000453e01dc04020000453e81dc04020000453f01dc04020000451481dd04020000451881dd04020000451901dd04020000451981dd04020000451a01dd04020000451a81dd04020000451b01dd04020000451b81dd04020000451c01dd04020000452901dd04020000452981dd04020000452a01dd04020000452a81dd04020000450f81de04020000451001de04020000451081de04020000451101de04020000451181de04020000451201de04020000451281de04020000451301de04020000451381de04020000450881df04020000450901df04020000450981df04020000450a01df04020000450a81df04020000450b01df04020000450b81df04020000450c01df04020000450c81df04020000450d01df04020000450d81df04020000450e01df04020000450e81df04020000450001e004020000450081e004020000450101e004020000450281e004020000450301e004020000450381e004020000450401e004020000450481e004020000450501e004020000450581e004020000450601e004020000450681e004020000450701e004020000450781e004020000451701e004020000451781e004020000452501e004020000452581e004020000452601e004020000452681e004020000452701e004020000452781e004020000452801e004020000451501e104020000451581e104020000451601e104020000451c81e104020000451d01e104020000451d81e104020000451e01e104020000451e81e104020000451f01e104020000451f81e104020000452001e104020000452081e104020000452101e104020000452181e104020000452201e104020000452281e104020000452301e104020000452381e104020000452401e104020000450181e304020000452b01e404020000452b81e404020000452c01e404020000452c81e404020000452d01e404020000452d81e404020000452e01e404020000452e81e404020000452f01e404020000452f81e404020000453001e404020000453081e404020000453101e404020000453181e404020000453201e404020000453281e404020000453301e404020000453381e404020000453401e404020000453481e404020000453501e404020000453581e404020000453601e404020000453681e404020000453701e404020000453781e404020000453801e404020000453881e404020000453901e404020000453981e404020000453a01e404020000453a81e404020000453b01e404020000453b81e404020000453c01e404020000453c81e404020000453d01e404020000453d81e404020000453e01e404020000453e81e404020000453f01e404020000451401e504020000451801e504020000451881e504020000451901e504020000451981e504020000451a01e504020000451a81e504020000451b01e504020000451b81e504020000452881e504020000452901e504020000452981e504020000452a01e504020000450f01e604020000450f81e604020000451001e604020000451081e604020000451101e604020000451181e604020000451201e604020000451281e604020000451301e604020000450801e704020000450881e704020000450901e704020000450981e704020000450a01e704020000450a81e704020000450b01e704020000450b81e704020000450c01e704020000450c81e704020000450d01e704020000450d81e704020000450e01e704020000450001e804020000450081e804020000450201e804020000450281e804020000450301e804020000450381e804020000450401e804020000450481e804020000450501e804020000450581e804020000450601e804020000450681e804020000450701e804020000451681e804020000451701e804020000452481e804020000452501e804020000452581e804020000452601e804020000452681e804020000452701e804020000452781e804020000451481e904020000451501e904020000451581e904020000451c01e904020000451c81e904020000451d01e904020000451d81e904020000451e01e904020000451e81e904020000451f01e904020000451f81e904020000452001e904020000452081e904020000452101e904020000452181e904020000452201e904020000452281e904020000452301e904020000452381e904020000450101eb04020000452a81ec04020000453f814c03820000453f814403820000452c01ec04020000452c81ec04020000452d01ec04020000452d81ec04020000452e01ec04020000452e81ec04020000452f01ec04020000452f81ec04020000453001ec04020000453081ec04020000453101ec04020000453181ec04020000453201ec04020000453281ec04020000453301ec04020000453381ec04020000453401ec04020000453481ec04020000453501ec04020000453581ec04020000453601ec04020000453681ec04020000453701ec04020000453781ec04020000453801ec04020000453881ec04020000453901ec04020000453981ec04020000453a01ec04020000453a81ec04020000453b01ec04020000453b81ec04020000453c01ec04020000453c81ec04020000453d01ec04020000453d81ec04020000453e01ec04020000453e81ec04020000453f01ec04020000451381ed04020000451781ed04020000451801ed04020000451881ed04020000451901ed04020000451981ed04020000451a01ed04020000451a81ed04020000451b01ed04020000452801ed04020000452881ed04020000452901ed04020000452981ed04020000450e81ee04020000450f01ee04020000450f81ee04020000451001ee04020000451081ee04020000451101ee04020000451181ee04020000451201ee04020000451281ee04020000450781ef04020000450801ef04020000450881ef04020000450901ef04020000450981ef04020000450a01ef04020000450a81ef04020000450b01ef04020000450b81ef04020000450c01ef04020000450c81ef04020000450d01ef04020000450d81ef04020000450001f004020000450181f004020000450201f004020000450281f004020000450301f004020000450381f004020000450401f004020000450481f004020000450501f004020000450581f004020000450601f004020000450681f004020000451601f004020000451681f004020000452401f004020000452481f004020000452501f004020000452581f004020000452601f004020000452681f004020000452701f004020000451401f104020000451481f104020000451501f104020000451b81f104020000451c01f104020000451c81f104020000451d01f104020000451d81f104020000451e01f104020000451e81f104020000451f01f104020000451f81f104020000452001f104020000452081f104020000452101f104020000452181f104020000452201f104020000452281f104020000452301f104020000450081f304020000452a01f404020000452a81f404020000452b01f404020000452b81f404020000452c01f404020000452c81f404020000452d01f404020000452d81f404020000452e01f404020000452e81f404020000452f01f404020000452f81f404020000453001f404020000453081f404020000453101f404020000453181f404020000453201f404020000453281f404020000453301f404020000453381f404020000453401f404020000453481f404020000453501f404020000453581f404020000453601f404020000453681f404020000453701f404020000453781f404020000453801f404020000453881f404020000453901f404020000453981f404020000453a01f404020000453a81f404020000453b01f404020000453b81f404020000453c01f404020000453c81f404020000453d01f404020000453d81f404020000453e01f404020000453e81f404020000453f01f404020000451301f504020000451701f504020000451781f504020000451801f504020000451881f504020000451901f504020000451981f504020000451a01f504020000451a81f504020000452781f504020000452801f504020000452881f504020000452901f504020000450e01f604020000450e81f604020000450f01f604020000450f81f604020000451001f604020000451081f604020000451101f604020000451181f604020000451201f604020000450701f704020000450781f704020000450801f704020000450881f704020000450901f704020000450981f704020000450a01f704020000450a81f704020000450b01f704020000450b81f704020000450c01f704020000450c81f704020000450d01f704020000450101f804020000450181f804020000450201f804020000450281f804020000450301f804020000450381f804020000450401f804020000450481f804020000450501f804020000450581f804020000450601f804020000451581f804020000451601f804020000452381f804020000452401f804020000452481f804020000452501f804020000452581f804020000452601f804020000452681f804020000451381f904020000451401f904020000451481f904020000451b01f904020000451b81f904020000451c01f904020000451c81f904020000451d01f904020000451d81f904020000451e01f904020000451e81f904020000451f01f904020000451f81f904020000452001f904020000452081f904020000452101f904020000452181f904020000452201f904020000452281f904020000450001fb04020000452981fc04020000452a01fc04020000452a81fc04020000452b01fc04020000452b81fc04020000452c01fc04020000452c81fc04020000452d01fc04020000452d81fc04020000452e01fc04020000452e81fc04020000452f01fc04020000452f81fc04020000453001fc04020000453081fc04020000453f812c03820000453181fc04020000453201fc04020000453281fc04020000453301fc04020000453381fc04020000453f80fc03820000453481fc04020000453501fc04020000453581fc04020000453601fc04020000453681fc04020000453701fc04020000453781fc04020000453801fc04020000453881fc04020000453901fc04020000453981fc04020000453a01fc04020000453a81fc04020000453b01fc04020000453b81fc04020000453c01fc04020000453c81fc04020000453d01fc04020000453d81fc04020000453e01fc04020000453e81fc04020000453f01fc04020000451281fd04020000451681fd04020000451701fd04020000451781fd04020000451801fd04020000451881fd04020000451e81360381c000851981fd04020000451a01fd04020000452701fd04020000452781fd04020000452801fd04020000452881fd04020000450d81fe04020000450e01fe04020000450e81fe04020000450f01fe04020000450f81fe04020000451001fe04020000451081fe04020000451101fe04020000451181fe04020000450681ff04020000450701ff04020000450781ff04020000450801ff04020000450881ff04020000450901ff04020000450981ff04020000450a01ff04020000450a81ff04020000450b01ff04020000450b81ff04020000450c01ff04020000450c81ff0402000045008200040200004501020004020000450880c90301c0008502020004020000453f80d4038200004531720c03820000450382000402000045040200040200004504820004020000453f80c403820000450302000402000045028200040200004501820004020000451901fd04020000453401fc04020000453101fc04020000453f813403820000453f813c03820000452b81ec04020000452b01ec04020000452181d904020000452681d804020000451781d804020000450781d804020000450581d804020000453d01d404020000453b81d404020000453681d404020000452f81d404020000453f819c03820000452381d104020000452301d104020000451f81d104020000451d81d104020000451601d104020000450f81cf04020000451101ce04020000451c01cd04020000453201cc04020000453101cc04020000451f81c904020000451881c804020000450781c804020000450201c804020000450181c804020000450e01c704020000453f01c404020000453d01c404020000453a01c404020000452781c004020000452c81bd04020000451681bd04020000453c01bc040200004523720003820000453581bc04020000453481bc04020000450101bb04020000453101b404020000450101b304020000450081b304020000450001b304020000452681b104020000452601b104020000452501b104020000452481b104020000451d81ad04020000453d81ac04020000453581ac04020000452a01a804020000451401a604020000451d220103820000452c81a504020000451e81a504020000453a01a404020000453601a404020000453501a404020000453401a404020000453301a404020000451f81260381c0008521120103820000453281a404020000453201a404020000453181a404020000453001a4040200004500020303820000450181a3040200004529e20403820000452681a104020000452481a104020000452b320403820000452381a104020000452201a104020000452181a104020000452101a104020000452081a104020000450001a104020000450b81a004020000450a01a004020000450981a004020000450801a004020000453400c90381c0008515019e040200004514819e04020000452e019d040200004520019d04020000451f819d040200004532a20403820000451e019d04020000451d019d04020000453d019c040200004538019c040200004535819c040200004533819c040200004530819c040200004506019b040200004524819904020000452b019804020000450c019804020000453d819404020000453d019404020000453c819404020000453b019404020000453a819404020000453a120403820000453a019404020000453af204038200004539019404020000451a019104020000452b819004020000452a81900402000045088180040200004514017f040200004512817f040200004512017f040200004519017e040200004515817e04020000452f817d04020000452f017d04020000451f817d04020000451e817d04020000453e817c040200004539817c040200004539017c040200004537817c040200004536817c040200004535817c040200004534817c040200004532017c040200004531817c040200004501817c040200004501017c040200004505017b040200004503817b0402000045278179040200004525817904020000451b817904020000452e817804020000452d817804020000452c817804020000452c017804020000452b817804020000451e017804020000451d817804020000450e017804020000450d817804020000450d0178040200004507420703820000450c817804020000450b017804020000450a817804020000450a017804020000450981780402000045090178040200004515017704020000451401770402000045128177040200004512017704020000451181770402000045108177040200004510017704020000450f817704020000450f01770402000045180176040200004517817604020000452f8175040200004502a2080382000045228175040200004522017504020000452181750402000045210175040200004520017504020000451f817504020000451f0175040200004500817504020000453f017404020000453e817404020000453e017404020000453d017404020000453c817404020000453b817404020000453b017404020000453a817404020000453a017404020000453981740402000045390174040200004538817404020000453801740402000045378174040200004537017404020000453601740402000045358174040200004535017404020000453381740402000045330174040200004532817404020000453201740402000045020174040200004501817404020000450881730402000045058173040200004505017304020000450481730402000045040173040200004503817304020000452b017104020000452a817104020000452a0171040200004529817104020000452901710402000045288171040200004528017104020000452781710402000045270171040200004526817104020000452601710402000045258171040200004525017104020000452481710402000045240171040200004523817104020000451d017104020000451c817104020000451c0171040200004503017104020000452f017004020000452e817004020000452e017004020000452d817004020000452d017004020000452c817004020000452c017004020000451e817004020000451e017004020000450e817004020000450e017004020000450d817004020000450d017004020000450c817004020000450c017004020000450b817004020000450b017004020000450a817004020000450a0170040200004509817004020000450801700402000045078170040200004507017004020000450681700402000045000170040200004515816f040200004515016f040200004514816f040200004514016f040200004513816f040200004513016f040200004512816f040200004512016f040200004511816f040200004511016f040200004510816f040200004510016f04020000450f816f04020000451a816e04020000451a016e040200004519816e040200004519016e040200004518816e040200004518016e040200004517816e040200004517016e040200004516816e040200004531816d040200004531016d040200004530816d040200004530016d040200004523016d040200004522816d040200004522016d040200004521816d040200004521016d040200004520816d040200004520016d04020000451f816d04020000453e016c04020000453d816c04020000453d016c04020000453c816c04020000453c016c04020000453b816c04020000453b016c04020000453a816c04020000453a016c040200004539816c040200004539016c040200004538816c040200004538016c040200004537816c040200004537016c040200004536816c040200004536016c040200004535816c040200004535016c040200004534816c040200004534016c040200004533816c040200004533016c040200004532816c040200004502816c040200004533320c038200004502016c040200004509016b040200004506016b040200004505816b040200004505016b040200004504816b040200004504016b04020000452b816904020000452b016904020000452a816904020000452a016904020000452981690402000045290169040200004528816904020000452801690402000045278169040200004527016904020000452681690402000045260169040200004525816904020000452501690402000045248169040200004524016904020000451d816904020000451d016904020000451c8169040200004503816904020000452f816804020000452f016804020000452e816804020000452e016804020000452d816804020000452d016804020000452c816804020000451f016804020000451e816804020000450f016804020000450e816804020000450e016804020000450d816804020000450d016804020000450c816804020000450c016804020000450b816804020000450b016804020000450a816804020000450a01680402000045088168040200004508016804020000450781680402000045070168040200004500816804020000450001680402000045160167040200004515816704020000451501670402000045148167040200004514016704020000451381670402000045130167040200004531e20c03820000451281670402000045120167040200004511816704020000451101670402000045108167040200004510016704020000451b016604020000451a816604020000451a016604020000451981660402000045190166040200004518816604020000451781660402000045170166040200004531016504020000453081650402000045238165040200004523016504020000452281650402000045220165040200004521816504020000452101650402000045208165040200004520016504020000451c0165040200004501816504020000453d816404020000453c816404020000453c016404020000453b816404020000453b016404020000453a816404020000453a01640402000045398164040200004539016404020000453801640402000045370164040200004536816404020000453601640402000045358164040200004535016404020000453301640402000045028164040200004509816304020000450681630402000045058163040200004504816304020000452c016104020000452b816104020000452b016104020000452a816104020000452981610402000045290161040200004528816104020000452801610402000045278161040200004526816104020000452581610402000045250161040200004524816104020000451e016104020000451d816104020000451d01610402000045040161040200004530016004020000452f816004020000452f016004020000452d816004020000452d016004020000451f816004020000451f016004020000450f816004020000450e816004020000450d816004020000450c816004020000450c016004020000450b8160040200004509016004020000450881600402000045008160040200004516815f040200004516015f040200004515815f040200004515015f040200004514815f040200004514015f040200004513815f040200004513015f040200004512815f040200004512015f040200004511815f040200004511015f040200004510815f04020000451b815e04020000451b015e04020000451a815e04020000451a015e040200004519815e040200004519015e040200004518815e040200004518015e040200004517815e040200004532815d040200004532015d040200004531815d040200004524015d040200004523815d040200004523015d040200004522015d040200004521815d040200004521015d040200004520815d04020000451c815d040200004502015d04020000453f015c04020000453e815c04020000453e015c04020000453d815c04020000453d015c04020000453c815c04020000453c015c04020000453b815c04020000453b015c04020000453a815c04020000453a015c040200004539815c040200004539015c040200004538815c040200004538015c040200004537815c040200004537015c040200004536815c040200004536015c040200004535015c040200004503815c04020000450a015b040200004507015b040200004506815b040200004506015b040200004505815b040200004505015b04020000452c815904020000452c015904020000452b815904020000452b015904020000452a815904020000452a015904020000452981590402000045290159040200004528815904020000452801590402000045270159040200004526815904020000452601590402000045258159040200004525015904020000451e815904020000451e015904020000451d815904020000450481590402000045308158040200004530015804020000452f815804020000452f015804020000452e815804020000452e015804020000452d8158040200004520015804020000451f8158040200004510015804020000450c815804020000450b815804020000450b01580402000045098158040200004508815804020000450801580402000045018158040200004501015804020000450081580402000045000158040200004517015704020000451681570402000045160157040200004515815704020000451501570402000045148157040200004514015704020000451381570402000045130157040200004512815704020000451201570402000045118157040200004511015704020000451c015604020000451b815604020000451b015604020000451a815604020000451a01560402000045198156040200004519015604020000451881560402000045180156040200004533015504020000453281550402000045320155040200004531815504020000452481550402000045240155040200004523815504020000452301550402000045228155040200004521815504020000451d015504020000453f015404020000453e015404020000453d815404020000453d015404020000453c815404020000453c015404020000453b815404020000453a015404020000453901540402000045388154040200004538015404020000453781540402000045370154040200004536815404020000453601540402000045350154040200004534815404020000453401540402000045040154040200004503815404020000450a81530402000045078153040200004507015304020000450681530402000045060153040200004505815304020000452c815104020000452c015104020000452b815104020000452b015104020000452a815104020000452a0151040200004529815104020000452901510402000045288151040200004528015104020000452781510402000045268151040200004526015104020000451e015104020000450501510402000045310150040200004530815004020000452f815004020000452f015004020000452e8150040200004520815004020000450e015004020000450d015004020000450a0150040200004509015004020000450881500402000045010150040200004516814f040200004515814f040200004514814f040200004513814f040200004512814f04020000451c014e04020000451b014e04020000451a014e040200004519014e040200004533814d040200004532014d040200004525014d040200004523814d040200004523014d040200004521814d04020000451d814d04020000453f014c04020000453e014c04020000453d814c04020000453d014c04020000453c014c04020000453b014c04020000453a014c040200004539014c040200004538814c040200004538014c040200004537014c040200004536814c040200004536014c040200004535814c040200004535014c040200004534814c040200004504814c040200004504014c04020000450b014b040200004508014b040200004507814b040200004507014b040200004530920c038200004506814b040200004506014b04020000452d814904020000452c814904020000452c014904020000452b814904020000452b014904020000452a814904020000452a01490402000045298149040200004529014904020000452881490402000045280149040200004527814904020000452701490402000045268149040200004526014904020000451f814904020000451f014904020000451e814904020000450581490402000045000149040200004531814804020000453101480402000045308148040200004530014804020000452f814804020000452f014804020000452e81480402000045210148040200004520814804020000451101480402000045108148040200004510014804020000450f014804020000450e814804020000450e014804020000450d814804020000450d014804020000450c814804020000450c014804020000450a0148040200004509814804020000450901480402000045028148040200004502014804020000450181480402000045010148040200004518014704020000451781470402000045170147040200004516814704020000451601470402000045158147040200004515014704020000451481470402000045140147040200004513814704020000451301470402000045128147040200004512014704020000451d014604020000451c814604020000451b814604020000451b014604020000451a814604020000451a014604020000451981460402000045190146040200004534014504020000453381450402000045330145040200004525014504020000452481450402000045240145040200004523814504020000452301450402000045228145040200004522014504020000451e014504020000453e014404020000452fb20c03820000453d814404020000453d014404020000453c814404020000453c014404020000453b814404020000453b014404020000453a0144040200004539814404020000453901440402000045388144040200004538014404020000453701440402000045360144040200004535814404020000453501440402000045050144040200004504814404020000450b81430402000045088143040200004508014304020000450781430402000045070143040200004506814304020000452e014104020000452d814104020000452d014104020000452c814104020000452c014104020000452b814104020000452b014104020000452a814104020000452a01410402000045298141040200004529014104020000452881410402000045280141040200004527814104020000452701410402000045268141040200004520014104020000451f8141040200004500814104020000450001410402000045320140040200004531814004020000453101400402000045308140040200004530014004020000452f814004020000452f014004020000452181400402000045210140040200004511814004020000451101400402000045108140040200004510014004020000450f814004020000450f014004020000450e814004020000450e014004020000450d814004020000450d014004020000450c814004020000450b014004020000450a814004020000450a014004020000450981400402000045030140040200004502814004020000452f420c038200004502014004020000452ed20c0382000045018140040200004518813f040200004518013f040200004517813f040200004517013f040200004516813f040200004516013f040200004515813f040200004515013f040200004514813f040200004514013f040200004513813f040200004513013f040200004512813f04020000451d813e04020000451d013e04020000451c813e04020000451c013e04020000451b813e040200004519813e040200004534813d040200004534013d040200004533813d040200004526013d040200004525813d040200004525013d040200004524813d040200004524013d040200004523813d040200004523013d040200004522813d04020000451e813d04020000452e620c038200004504013d04020000453f013c04020000453e813c04020000453e013c04020000453d813c04020000453d013c04020000453c813c04020000453b813c04020000453b013c04020000453a813c04020000453a013c040200004539813c040200004538013c040200004537813c040200004536013c040200004505813c04020000450c013b040200004509013b040200004508813b040200004507013b04020000450d81380402000045238135040200004537012c040200004536812c040200004506812c040200004506012c04020000450980f404020000450400f404020000450d80f304020000450d00f304020000450c80f304020000450c00f304020000450b80f304020000453220f1040200004531a0f104020000453120f1040200004530a0f104020000453020f104020000450b00f104020000450580f104020000450500f104020000452381110381c000852d820c03820000452d120c03820000452ca20c03820000453480f004020000453400f004020000453300f004020000452610f004020000452590f004020000451680f004020000451600f004020000451580f004020000451500f004020000451480f004020000452c320c03820000451400f004020000451380f004020000451c70ef04020000451bf0ef04020000451b70ef04020000451ef0ee04020000451e70ee04020000453860ed04020000452a20ed040200004529a0ed04020000450300ed04020000450280ed04020000453f00ec04020000453d00ec04020000453c80ec04020000450480ec04020000451100eb04020000450e00eb04020000453000e904020000452f80e904020000452f00e904020000452e80e904020000452e00e904020000452d80e904020000452d00e9040200004513f0e804020000451370e8040200004512f0e804020000451270e8040200004511f0e804020000450800e804020000451d70e704020000451cf0e704020000451a70e7040200004518f0e704020000451870e7040200004521c0e604020000452140e6040200004520c0e604020000452040e604020000451fc0e604020000451f40e604020000452390e504020000450400e504020000450380e5040200004524a0e104020000452420e104020000450c00e104020000450680e104020000450600e104020000450280e104020000450200e104020000450180e104020000450080e104020000453400e004020000453380e004020000452680e004020000452600e004020000451770e0040200004516f0e004020000451670e0040200004515f0e0040200004514f0e004020000452bc20c03820000451070e004020000450ff0e004020000450f70e004020000450900e004020000452b520c03820000450880e004020000450800e004020000450780e004020000451e40df04020000451d40df04020000451940df040200004518c0df04020000451840df040200004503239204018000832310de04020000452290de04020000452110de04020000453720dd04020000452ae0dd040200004527e0dd040200004523a0dd04020000450a00dd04020000450480dd04020000450400dd04020000453080d9040200004514c0d804020000451340d8040200004512c0d804020000451170d8040200004510f0d804020000451070d804020000450ff0d804020000450980d804020000450900d8040200004539a20c03020000450880d804020000450800d804020000452ae20c03820000451e90d704020000451e10d704020000451d90d704020000451d10d704020000451c90d704020000451c10d704020000451b90d704020000451b10d704020000451a90d704020000451a10d704020000451990d704020000452b00d504020000450480d504020000450400d504020000453f00d404020000451240d3040200004530a0d104020000450cf0d104020000450780d104020000453700d004020000453680d004020000453600d004020000451710d004020000451310d0040200004511c0d004020000452460cd04020000450af0cd04020000450580cd04020000450500cd04020000450480cd04020000453f70cc04020000452e80c904020000452e00c904020000452d80c904020000452600c904020000450780c904020000450400c904020000450180c904020000453580c804020000451720c804020000451620c8040200004514a0c804020000451320c804020000451090c804020000451e00c704020000451c00c7040200004522e0c604020000453c00c404020000450cc0c404020000450700c404020000451010c304020000450f10c304020000450e10c304020000453400c104020000453300c104020000450400c104020000450300c104020000450a70c0040200004509f0c004020000453a00bd04020000453900bd04020000452c00bd04020000452b80bd04020000452b00bd04020000452a80bd04020000452980bd04020000452900bd040200004524a0bd04020000450680bd04020000450580bd04020000450000bd04020000453ec0bc04020000450e20bb04020000453780b804020000453680b8040200004516e0b8040200004515e0b8040200004513e0b804020000451180b804020000450b40b804020000450a40b804020000451f80b704020000451e80b704020000451d80b704020000451c80b704020000451b80b704020000451a80b704020000451980b7040200004523a0b6040200004522a0b6040200004521a0b6040200004520a0b604020000453a40b504020000453940b504020000452c70b504020000452b70b504020000452a70b504020000452970b504020000452500b5040200004506f0b5040200004505f0b504020000450080b504020000453f70b404020000453e70b404020000453d70b404020000453c70b404020000453b70b404020000450d20b4040200004507f0b404020000451080b304020000450f80b304020000450e80b304020000453480b104020000453380b104020000453200b104020000453180b104020000453000b104020000452f80b104020000453840b0040200004537c0b004020000451480b004020000451260b004020000450b90b004020000450a90b004020000451e20af04020000451d20af04020000451ca0af04020000451c20af040200004529920c03820000451ba0af04020000451aa0af040200004519a0af040200004529220c03820000452400ae04020000452300ae04020000452180ae04020000452100ae04020000453a70ad04020000453970ad04020000452c80ad04020000452b80ad04020000450100ad04020000450000ad04020000453ee0ac04020000451060ab04020000450fe0ab04020000451e73f104020000440b03f104020000440803f104020000440403f104020000440183f104020000441801660402000045229209038200004522220903820000453d0164040200004534016404020000452e016004020000450e016004020000450d016004020000451f015104020000453001500402000045018150040200004517814f04020000453c814c04020000453b814c040200004505013c04020000452d8139040200004521b20903820000452c013904020000452b01390402000045068139040200004501013904020000450081390402000045328138040200004531813804020000453101380402000045308138040200004530013804020000453e013404020000453981340402000045388134040200004536813404020000452e013104020000452d813104020000452d013104020000452c813104020000452c013104020000452b813104020000452b013104020000452a813104020000452a0131040200004529813104020000452901310402000045288131040200004528013104020000450800fd04020000452781310402000045210131040200004520813104020000452001310402000045070131040200004501813104020000453301300402000045328130040200004532013004020000453101300402000045214209038200004530813004020000453001300402000045228130040200004522013004020000451281300402000045110130040200004510013004020000450f813004020000450f0130040200004520d20903820000450e81300402000045206209038200004502012904020000451f011604020000451e011604020000451d011604020000451c011604020000450e810004020000450e010004020000450d810004020000453770c0040200004536f0c004020000453670c0040200004535f0c004020000451700c004020000451600c004020000451580c004020000450af0c004020000450970c004020000451ff20903820000451ae0bf04020000451a60bf04020000452400be04020000452380be04020000452300be04020000452280be04020000450080bd04020000453e40bc04020000453dc0bc04020000450770bc04020000451300bb040200004531f0b904020000453170b904020000452ff0b904020000452f70b904020000452ef0b904020000452e70b9040200004508f0b904020000450500b904020000450480b904020000453500b804020000452800b804020000451280b804020000451200b8040200004509c0b804020000451f00b704020000451e00b704020000451d00b704020000452020b6040200004538c0b504020000452bf0b504020000453df0b404020000453cf0b404020000453280b104020000453100b104020000452e80b104020000452e00b104020000452d80b104020000452d00b1040200004504f0b1040200004503f0b104020000450370b104020000453f23f004020000441900b004020000451880b0040200004511e0b004020000451160b004020000451ea0af04020000452480ae04020000452380ae04020000452a80ad04020000452980ad04020000452900ad04020000453e60ac04020000453de0ac04020000450f60ab04020000450ee0ab04020000450a83f104020000440a03f104020000440983f104020000440903f104020000440483f104020000440303f104020000440283f1040200004413817704020000453f016404020000452e816004020000450f01600402000045080160040200004507816004020000451f8209038200004501016004020000450001600402000045220155040200004502815504020000453e815404020000452e0150040200004520015004020000450981500402000045020150040200004514014f040200004513014f040200004533014d040200004524014d040200004503014d040200004525814504020000450381450402000045298139040200004529013904020000451f8139040200004522013804020000450f0138040200004519013704020000451881370402000045138137040200004513013704020000451d013604020000451c013604020000451b813604020000450881330402000045080133040200004507813304020000452f013104020000450d8130040200004518012f040200004517812f04020000451e012e04020000451d812e040200004525812d04020000453c812c04020000453c012c04020000451f1209038200004500812c04020000451ea20903820000451e320903820000450d012b040200004509012b04020000451dc209038200004508812b04020000452b812904020000452a812904020000452a01290402000045018129040200004533812804020000451281280402000045120128040200004533c1080402000045334108040200004532c10804020000453691050402000045361105040200004529710504020000451d5209038200004528f1050402000045287105040200004527f105040200004500010104020000453591000402000045351100040200004534910004020000453411000402000045339100040200004533110004020000453291000402000045257100040200004524f1000402000045158100040200004515010004020000451481000402000045140100040200004513810004020000451301000402000045128100040200004512010004020000451181000402000045110100040200004510810004020000450f010004020000450701000402000045068100040200004506010004020000452170f6040200004520f0f604020000451ff0f604020000451f70f604020000451ef0f604020000451e70f604020000451df0f604020000453800f504020000453780f504020000453680f504020000452a10f504020000452990f504020000452890f504020000452790f504020000452710f5040200004522f0f504020000450880f504020000450300f504020000450280f504020000453f60f404020000453d60f404020000453b60f404020000451c720903820000453a60f4040200004539e0f404020000453960f4040200004538e0f404020000450a00f404020000451080f304020000452da0f104020000452ba0f104020000452b20f104020000452aa0f1040200004524c0f104020000452170ee04020000451f70ee040200004528a0ed04020000452820ed040200004527a0ed04020000452720ed040200004526a0ed04020000452340ed04020000450900ed04020000453c00ec04020000453900ec04020000453660e8040200004534e0e804020000453460e8040200004533e0e804020000453360e804020000452620e8040200004525a0e804020000453f20e404020000453ea0e404020000450a80e404020000450500e404020000452fe0e104020000452f60e104020000450000e104020000453680e004020000453600e004020000453580e004020000451cc0df04020000451c40df04020000451bc0df04020000451b40df04020000452a60dd040200004529e0dd04020000452960dd040200004528e0dd04020000453a80dc04020000453a00dc04020000453200d904020000453180d904020000452b80d904020000452580d904020000452500d904020000452480d904020000453520d8040200004534a0d804020000451440d8040200004513c0d804020000451910d704020000451890d704020000453880d504020000453800d504020000453780d504020000452a80d504020000453c80d404020000453c00d404020000450000d204020000453320d104020000452560d1040200004524e0d104020000452680d004020000451810d004020000450980d004020000450900d004020000451920cf040200004518a0cf04020000453800cd04020000452b20cd04020000453bf0cc04020000453b70cc04020000450d40c904020000450380c9040200004517a0c804020000451c020903820000451b920903820000451b220903820000451ab209038200004516a0c804020000451f00c704020000451e80c7040200004514a209038200004514320903820000451d80c704020000451d00c7040200004539f0c504020000453970c504020000450600c504020000450580c5040200004513c20903820000450500c504020000450000c5040200004512a0c304020000450f90c304020000450e90c3040200004513520903820000450100c204020000452d00c104020000452c80c1040200004537f0c004020000453570c0040200004534f0c004020000452780c0040200004511a0c0040200004526520803820000451120c0040200004510a0c004020000451f60bf04020000452100be04020000452080be040200004525e20803820000450d10bc04020000450c90bc04020000451020bb04020000450fa0bb04020000452680b904020000452600b904020000453580b804020000452780b8040200004518e0b804020000451860b804020000451a00b704020000452420b604020000452320b604020000452220b604020000453bf0b404020000453af0b404020000450ca0b404020000451360b304020000450e00b104020000450940b1040200004508c0b104020000450570b1040200004525720803820000451680b004020000451600b004020000452c00ad04020000452b00ad04020000451b012e040200004535812d040200004535012d040200004527012d040200004526012d040200004523812d040200004505012d04020000453e012c04020000453d812c04020000453d012c04020000453b812c04020000453a012c0402000045250208038200004539812c040200004539012c04020000453f910c030200004538812c04020000450a012b040200004509812b04020000452f812904020000452f012904020000452e812904020000452d812904020000452d012904020000452c812904020000452981290402000045290129040200004528012904020000452081290402000045078129040200004533012804020000453281280402000045318128040200004531012804020000453081280402000045230128040200004524920803820000452281280402000045242208038200004511812804020000451101280402000045108128040200004510012804020000450f812804020000450e812804020000450e012804020000450c812804020000450c012804020000450b8128040200004504812804020000450401280402000045038128040200004503012804020000451a012704020000451901270402000045188127040200004518012704020000451d812604020000451d012604020000451b01260402000045360125040200004520012504020000453df124040200004537f1240402000045377124040200004536f124040200004501012404020000450d812304020000452d012104020000452b012104020000452201210402000045330120040200004532812004020000451201200402000045110120040200004510812004020000450f012004020000450e812004020000450d0120040200004505012004020000450401200402000045038120040200004519811f040200004519011f040200004518011f040200004536711d040200004535711d040200004527011d040200004526011d040200004525011d040200004524811d040200004520811d0402000045298119040200004529011904020000450281190402000045347118040200004533f118040200004523b2080382000045337118040200004532f118040200004531f118040200004523811804020000451301180402000045118118040200004510011804020000450d811804020000450c011804020000450501180402000045040118040200004523420803820000451a811704020000451a0117040200004534620c0302000045190117040200004518811704020000451781170402000045170117040200004515a2080382000045160117040200004520011604020000451d81160402000045367115040200004515320803820000452701150402000045258115040200004500011504020000453ec11404020000453e411404020000453dc11404020000453d411404020000453cc11404020000453c411404020000453bc11404020000453b411404020000453ac11404020000453a4114040200004539c1140402000045394114040200004538c1140402000045384114040200004537c1140402000045080114040200004505b208038200004502011404020000450e811304020000450b811304020000450b011304020000450a81130402000045054208038200004504d20803820000450a01130402000045098113040200004530f11104020000452ff11104020000452f7111040200004504620803820000452ef11104020000452e711104020000452df11104020000452d711104020000452cf11104020000452c711104020000452b711104020000452af11104020000452a7111040200004503f2080382000045038208038200004529f11104020000452971110402000045031208038200004523011104020000452201110402000045090111040200004503811104020000450301110402000045347110040200004533f11004020000453371100402000045023208038200004532f1100402000045327110040200004531f110040200004524811004020000452401100402000045148110040200004514011004020000451381100402000045130110040200004512811004020000451201100402000045118110040200004501c20803820000450152080382000045110110040200004510811004020000450f811004020000450e011004020000450d811004020000450d011004020000450c8110040200004506011004020000450581100402000045050110040200004504811004020000451b810f04020000451b010f04020000451a810f04020000451a010f040200004519810f040200004518810f040200004518010f040200004517810f040200004517010f040200004516810f040200004516010f040200004515810f040200004500e208038200004520010e04020000451f810e04020000451f010e04020000451e810e04020000451e010e04020000451d010e04020000451c810e040200004537410d040200004536c10d040200004536410d040200004535c10d040200004528f10d040200004528710d0402000045007208038200004527f10d040200004527710d04020000450cf20703820000450c8207038200004526f10d040200004526710d040200004525f10d040200004525710d040200004521810d040200004507010d040200004501810d040200004501010d040200004500810d04020000453f110c04020000453e910c04020000453e110c04020000453d910c04020000453c910c04020000453c110c04020000453b910c04020000453b110c04020000453a910c040200004539910c040200004539110c040200004538910c040200004538110c04020000450c1207038200004508810c040200004502810c04020000450f010b04020000450c010b04020000450b810b040200004511011004020000450a810b04020000450a010b0402000045317109040200004530f109040200004530710904020000452ff10904020000452f710904020000452ef10904020000452e710904020000452df10904020000452d710904020000452cf10904020000452c710904020000452bf10904020000452b710904020000452af10904020000452a7109040200004529f10904020000452381090402000045230109040200004522810904020000450981090402000045040109040200004503810904020000450001090402000045354108040200004534c10804020000453441080402000045324108040200004524f10804020000452471080402000045150108040200004514810804020000451401080402000045138108040200004513010804020000451281080402000045120108040200004511810804020000451101080402000045108108040200004510010804020000450e810804020000450e010804020000450d810804020000450d010804020000451b010704020000451a810704020000451a0107040200004518810704020000451601070402000045210106040200004520810604020000453791050402000045371105040200004522010504020000453da10404020000453d210404020000453ca10404020000453ba10404020000453a210404020000453821040402000045090104040200004508810404020000450b01030402000045314101040200004530c101040200004530410104020000452fc10104020000452ec10104020000452e410104020000452dc10104020000452d410104020000452cc10104020000450a520703820000452bc10104020000452b410104020000452ac101040200004505810004020000451c80ff04020000451c00ff04020000451980ff04020000451900ff04020000451880ff04020000452180fe04020000452100fe04020000452080fe04020000451f80fe04020000451e00fe04020000451d80fe040200004537a0fd04020000453720fd040200004529c0fd04020000452940fd040200004528c0fd04020000452840fd040200004526c0fd04020000450280fd04020000450200fd04020000450180fd04020000453f00fc04020000453e80fc04020000453e00fc04020000453d80fc04020000453d00fc04020000453c80fc04020000453c00fc04020000453b80fc04020000453b00fc04020000453a80fc04020000453a00fc04020000453980fc04020000453900fc04020000453880fc04020000450980fc04020000450900fc04020000450380fc04020000451000fb04020000450d00fb04020000450c80fb04020000450c00fb04020000450b80fb04020000450b00fb04020000453210f904020000453190f904020000453110f904020000453090f904020000453010f904020000452f90f904020000452f10f904020000452e90f904020000452e10f904020000452d90f904020000452d10f904020000452c90f904020000452c10f904020000452b90f904020000452b10f904020000452a90f904020000452470f9040200004523f0f904020000452370f904020000450a80f904020000450500f904020000450480f904020000450100f904020000450080f904020000450000f9040200004535a0f804020000453520f8040200004534a0f804020000453420f8040200004533a0f804020000453320f8040200004532a0f8040200004525c0f804020000452540f804020000451600f804020000451580f804020000451500f804020000451480f804020000451400f804020000451380f804020000451300f804020000451280f804020000451200f804020000451180f804020000451100f804020000450f80f804020000450f00f804020000450e80f804020000450e00f804020000450780f804020000450700f804020000450680f804020000450600f804020000451d00f704020000451c80f704020000451c00f704020000451b80f704020000451b00f7040200004508220703820000451a80f704020000451a00f704020000451980f704020000451900f704020000451880f704020000451800f704020000451780f704020000451700f7040200004521f0f604020000452070f604020000453700f504020000452910f504020000452810f504020000452690f5040200004507b20703820000450200f504020000453ee0f404020000453e60f404020000453de0f404020000453ce0f404020000453c60f404020000453be0f404020000453ae0f404020000452fa0f104020000452f20f104020000452ea0f104020000452e20f104020000452d20f104020000452ca0f104020000452c20f104020000452440f1040200004523c0f104020000453380f004020000451300f004020000451280f004020000451200f004020000451180f004020000451000f004020000450f80f004020000450f00f004020000450e80f004020000450800f004020000450780f004020000450700f004020000450680f004020000451d70ef04020000451cf0ef04020000451770ef04020000452270ee040200004521f0ee040200004520f0ee04020000452070ee04020000451ff0ee04020000452920ed04020000450380ed04020000453b80ec04020000453b00ec04020000453a80ec04020000453a00ec04020000453980ec04020000450a80ec04020000450a00ec04020000450c80eb04020000450c00eb04020000453280e904020000453200e904020000453180e904020000453080e904020000452c80e904020000452c00e904020000452b80e904020000452b00e904020000452510e904020000452490e904020000452410e904020000450b80e904020000450600e904020000450580e904020000450200e904020000450180e904020000450100e904020000450080e904020000450000e9040200004535e0e804020000453560e8040200004516f0e804020000451670e8040200004515f0e804020000451570e8040200004514f0e804020000451470e804020000450700e804020000451df0e704020000451b70e704020000451af0e7040200004519f0e704020000451970e704020000452240e604020000451ec0e604020000453880e504020000453800e504020000453780e504020000453700e504020000452a80e504020000452a00e504020000452980e504020000452900e504020000452880e504020000452800e504020000452780e504020000452700e504020000450980e504020000450300e504020000453e20e404020000453da0e404020000453d20e404020000453ca0e404020000453c20e404020000453ba0e404020000453b20e404020000453aa0e404020000453a20e4040200004539a0e404020000453920e404020000450b00e404020000451170e304020000450e80e304020000450e00e304020000450d80e304020000450d00e304020000450c80e3040200004532e0e104020000453260e1040200004531e0e104020000453160e1040200004530e0e104020000453060e104020000452ee0e104020000452e60e104020000452de0e104020000452d60e104020000452ce0e104020000452c60e104020000452be0e104020000452b60e104020000452520e104020000450100e104020000453500e004020000453480e004020000451570e004020000451470e0040200004513f0e004020000451370e0040200004512f0e004020000451270e0040200004510f0e004020000451dc0df04020000451ac0df04020000451a40df04020000452210de04020000452090de04020000452010de04020000451f90de04020000451f10de040200004538a0dd04020000453820dd040200004537a0dd04020000452860dd04020000452760dd04020000450380dd04020000453f00dc04020000453e80dc04020000453e00dc04020000453d80dc04020000453d00dc04020000453c80dc04020000453c00dc04020000453b80dc04020000453b00dc04020000453980dc04020000450b80dc04020000450b00dc04020000450580dc040200004511f0db04020000450ef0db04020000450e70db04020000450df0db04020000450d70db04020000450cf0db04020000453300d904020000453280d904020000453100d904020000453000d904020000452f80d904020000452f00d904020000452e80d904020000452e00d904020000452d80d904020000452d00d904020000452c80d904020000452c00d904020000450c70d904020000450700d904020000450680d904020000450300d904020000450280d904020000450200d904020000450180d904020000450100d904020000450080d9040200004536a0d804020000453620d8040200004535a0d804020000453420d8040200004533a0d8040200004526e0d804020000452660d8040200004517c0d804020000451740d8040200004516c0d804020000451640d8040200004515c0d804020000451540d804020000452320d6040200004522a0d604020000452220d6040200004521a0d604020000452120d6040200004520a0d604020000452020d604020000451fa0d604020000451f20d604020000453900d504020000452a00d504020000452980d504020000452900d504020000452880d504020000452800d504020000452780d504020000452400d504020000450a80d504020000453e80d404020000453e00d404020000453d80d404020000453d00d404020000453b80d404020000453b00d404020000453a80d404020000453a00d404020000450bf0d404020000450b70d404020000450600d404020000450f70d304020000450ef0d304020000450e70d304020000450df0d304020000450d70d3040200004532a0d104020000453020d104020000452fa0d104020000452f20d104020000452ea0d104020000452e20d104020000452da0d104020000452d20d104020000452ca0d104020000452c20d1040200004506d20703820000452ba0d10402000045066207038200004511d206038200004525e0d1040200004511620603820000450700d104020000450380d104020000450300d104020000450280d104020000450200d104020000450180d104020000450100d1040200004510f20603820000453580d004020000453500d004020000453480d0040200004510820603820000453400d004020000452700d004020000451790d0040200004510120603820000451690d004020000450fa20603820000451610d004020000451590d004020000451510d004020000451490d004020000450f320603820000451410d004020000451390d004020000451140d0040200004510c0d004020000451040d004020000450a00d004020000450880d004020000451ea0cf04020000451e20cf04020000451da0cf04020000451d20cf04020000451ca0cf04020000451c20cf04020000451ba0cf04020000451b20cf04020000451aa0cf04020000451a20cf040200004519a0cf04020000452380ce04020000452300ce04020000452280ce04020000452200ce04020000452180ce04020000452100ce04020000452080ce04020000452000ce04020000451f80ce04020000453980cd04020000453900cd04020000453880cd04020000452aa0cd04020000452a20cd040200004529a0cd04020000452920cd040200004528a0cd04020000452820cd040200004527a0cd04020000453e70cc04020000453df0cc04020000453d70cc04020000453cf0cc04020000453c70cc04020000453af0cc04020000453a70cc04020000450c70cc04020000450bf0cc04020000450680cc04020000451290cb04020000450fc0cb04020000450f40cb04020000450ec0cb04020000450e40cb04020000452580c904020000452500c904020000450300c904020000450280c904020000450200c904020000453780c804020000453700c804020000453680c804020000453600c804020000453500c804020000453480c804020000452720c8040200004526a0c804020000451820c8040200004515a0c804020000451420c8040200004513a0c804020000451210c804020000451190c804020000451110c804020000450a70c8040200004509f0c804020000450970c8040200004508f0c804020000451c80c704020000451b80c704020000451b00c704020000451a80c704020000451a00c704020000451900c704020000452360c604020000452260c604020000452160c6040200004520e0c604020000452060c604020000451fe0c6040200004538f0c504020000453870c504020000452b80c504020000452a80c504020000452980c504020000452900c504020000452880c504020000452800c504020000452480c504020000450b70c504020000453f00c404020000453e80c404020000453e00c404020000453d80c404020000453d00c404020000453c80c404020000453b80c404020000453b00c404020000453a80c404020000450c40c404020000453380c104020000453280c104020000453200c104020000453180c104020000453100c104020000453080c104020000453000c104020000452f80c104020000452f00c104020000452e80c104020000452e00c104020000450ec20603820000452d80c104020000452620c1040200004525a0c104020000452520c104020000450d90c104020000450870c1040200004507f0c104020000450480c104020000450380c104020000450280c104020000450e520603820000450200c104020000452700c004020000451880c004020000451800c004020000451780c004020000451680c004020000451500c004020000451480c004020000451400c004020000451380c004020000453f520403820000451220c004020000451ee0bf04020000451e60bf04020000451de0bf04020000451d60bf04020000451ce0bf04020000451c60bf04020000451be0bf04020000451b60bf040200004519e0bf04020000451960bf04020000452200be04020000452180be04020000452880bd04020000450bc0bd04020000450600bd04020000453f40bc04020000453d40bc04020000453cc0bc04020000453c40bc04020000453bc0bc04020000453b40bc04020000453ac0bc04020000450f20bb04020000450180ba04020000453470b9040200004533f0b904020000453370b9040200004532f0b904020000453270b9040200004530f0b904020000453070b904020000452df0b904020000452d70b904020000452cf0b904020000452580b904020000450da0b904020000450870b904020000450400b904020000450380b904020000450300b904020000450280b904020000453800b804020000453700b804020000453600b8040200004517e0b804020000451760b804020000451660b804020000451560b8040200004514e0b804020000451460b804020000451100b804020000450ac0b804020000451c00b704020000451b00b704020000452120b6040200004539c0b504020000452af0b5040200004529f0b5040200004528f0b504020000450c10b504020000450670b504020000450100b504020000450000b504020000453ef0b404020000451000b304020000450f00b304020000450200b204020000453400b104020000453300b104020000453080b104020000452f00b104020000452700b104020000452680b104020000452600b104020000450470b1040200004502f0b104020000453740b0040200004536c0b004020000453640b0040200004535c0b004020000453540b004020000452870b0040200004527f0b004020000451800b004020000451780b004020000451700b004020000451580b004020000451500b004020000451400b0040200004512e0b004020000450b10b004020000450a10b004020000451f20af04020000452280ae04020000452200ae04020000452080ae040200004539f0ad040200004538f0ad04020000452a00ad04020000452580ad04020000450c20ad04020000450770ad040200004506f0ad04020000450670ad04020000450180ad04020000450080ad04020000453f60ac04020000453d60ac04020000453ce0ac04020000453c60ac04020000453b60ac04020000450d80ac04020000450d00ac04020000450840ac04020000451380ab040200004510e0ab04020000450e60a904020000451ef3f104020000440883f104020000440783f104020000440703f104020000440683f104020000440603f104020000440583f104020000440503f104020000440383f104020000440203f104020000440103f104020000440083f104020000440003f104020000440b817804020000450781780402000045070178040200004514817704020000453e81640402000045348164040200004533816404020000450301640402000045060163040200004505016304020000452a01610402000045270161040200004526016104020000450b016004020000450a8160040200004531015d040200004522815d040200004535815c04020000453ee204038200004534815c040200004534015c040200004533815c0402000045278159040200004521015504020000453b015404020000453a81540402000045398154040200004535815404020000452d01510402000045270151040200004525815104020000451e81510402000045108150040200004510015004020000450f815004020000450f015004020000450e815004020000450d815004020000450c815004020000450c015004020000450b81500402000045008150040200004517014f040200004516014f040200004515014f040200004512014f040200004511814f04020000451c814e04020000451b814e04020000451a814e040200004519814e040200004518814e040200004532814d04020000453e814c04020000453a814c040200004539814c040200004537814c04020000450f814804020000450a814804020000451c0146040200004532814504020000453f014404020000453e814404020000453a814404020000452e813904020000452d013904020000452c813904020000452b813904020000452a813904020000452a0139040200004528813904020000452801390402000045278139040200004520813904020000452181380402000045120138040200004511813804020000451101380402000045108138040200004510013804020000450f813804020000450e813804020000450e013804020000450d013804020000450b813804020000450b013804020000450a813804020000450a0138040200004503813804020000450301380402000045028138040200004502013804020000451801370402000045178137040200004517013704020000451681370402000045160137040200004515813704020000451501370402000045148137040200004514013704020000451e013604020000451d813604020000451c813604020000451b013604020000451a813604020000451a01360402000045350135040200004534813504020000453401350402000045338135040200004526813504020000452601350402000045258135040200004525013504020000452481350402000045240135040200004523013504020000451f0135040200004504813504020000453f013404020000453e813404020000453d813404020000453d013404020000453c813404020000453c013404020000453b813404020000453b013404020000453a813404020000453a01340402000045390134040200004538013404020000453781340402000045370134040200004500013404020000450c81330402000045098133040200004509013304020000452e81310402000045010131040200004531813004020000451201300402000045118130040200004510813004020000450e013004020000450c013004020000450b813004020000450b013004020000450a81300402000045040130040200004503813004020000450301300402000045028130040200004519812f040200004519012f040200004518812f040200004517012f040200004516812f040200004516012f040200004515812f04020000453a8204038200004515012f040200004514812f040200004514012f040200004513812f04020000451e812e04020000451d012e04020000451c812e04020000451c012e04020000451b812e04020000451a812e040200004534812d040200004534012d040200004526812d0402000045354204038200004525012d0402000045323204038200004524812d040200004530020403820000452f2204038200004524012d04020000451f812d04020000453f012c04020000453e812c04020000453b012c04020000453a812c040200004538012c040200004537812c040200004508012b04020000452e012904020000452c012904020000452b0129040200004528812904020000452e4204038200004521812904020000452101290402000045320128040200004513012804020000450f012804020000450b012804020000451981270402000045178127040200004517012704020000451681270402000045160127040200004515812704020000451501270402000045148127040200004514012704020000451f012604020000451e812604020000451e012604020000451c812604020000451c012604020000451b8126040200004535812504020000453501250402000045348125040200004527812504020000452701250402000045268125040200004526012504020000452581250402000045250125040200004524812504020000452401250402000045058125040200004500012504020000453f712404020000453ef12404020000453e712404020000453d712404020000453cf12404020000453c712404020000453bf12404020000453b712404020000453af12404020000453a7124040200004539f1240402000045397124040200004538f12404020000453871240402000045070124040200004506812404020000450a812304020000450a0123040200004509812304020000450901230402000045088123040200004530012104020000452f812104020000452f012104020000452e812104020000452e012104020000452d812104020000452c812104020000452c012104020000452b812104020000452d620403820000452a812104020000452a012104020000452981210402000045290121040200004528812104020000452181210402000045210121040200004508012104020000450281210402000045020121040200004534012004020000453381200402000045320120040200004531812004020000453101200402000045238120040200004523012004020000451381200402000045130120040200004512812004020000451181200402000045100120040200004520a20103820000450f812004020000450c812004020000450c012004020000450b8120040200004504812004020000451a811f04020000451a011f040200004518811f040200004517811f040200004517011f040200004516811f040200004516011f040200004515811f040200004515011f040200004514811f04020000451f811e04020000451f011e04020000451e811e04020000451e720103820000451e011e04020000451d811e04020000451d011e04020000451c811e04020000451c011e04020000451b811e040200004514520103820000453f81c4038200004535f11d040200004534f11d040200004528011d040200004527811d040200004526811d040200004525811d040200004506011d040200004500811d040200004500011d04020000453f711c04020000453ef11c04020000453f80ec03820000453e711c04020000453df11c04020000453d711c04020000453cf11c04020000453c711c04020000453bf11c04020000453b711c04020000453af11c04020000453a711c040200004539f11c040200004539711c040200004538f11c040200004538711c040200004537f11c040200004537711c040200004507811c040200004507011c040200004501811c04020000450e011b04020000450b011b04020000450a811b04020000450a011b040200004509811b040200004509011b0402000045308119040200004530011904020000452f811904020000452f011904020000452e811904020000452e011904020000452d811904020000452d011904020000452c811904020000452c011904020000452b811904020000452b011904020000452a811904020000452a011904020000452281190402000045220119040200004521811904020000450881190402000045030119040200004528811504020000450681150402000045078114040200004530711104020000452bf1110402000045228111040200004534f1100402000045100110040200004519010f040200004520810e04020000451d810e04020000453d110c04020000453a110c040200004508010c04020000450b010b0402000045180107040200004517810704020000451701070402000045168107040200004520010604020000451f810604020000451f010604020000451e810604020000451e010604020000451d810604020000451d0106040200004507810504020000450201050402000045018105040200004501010504020000453f210404020000453ea10404020000453b2104040200004539a1040402000045392104040200004538a104040200004503010404020000450f810304020000450c810304020000450c010304020000450b810304020000450a8103040200004531c10104020000452f410104020000452c410104020000452a4101040200004523f1010402000045237101040200004522f10104020000450a010104020000450481010402000045040101040200004500810104020000451b80ff04020000451b00ff04020000451a80ff04020000451a00ff04020000451800ff04020000451780ff04020000451700ff04020000451680ff04020000452000fe04020000451f00fe04020000451e80fe040200004536a0fd04020000453f80dc03820000453620fd040200004527c0fd04020000452740fd04020000452640fd04020000452270fd040200004505020004020000452d017804020000450c0178040200004511017704020000451a0176040200004519817604020000451901760402000045188176040200004517017604020000451681760402000045160176040200004531017504020000453081750402000045300175040200004520817504020000451b017504020000453d817404020000453c017404020000453681740402000045348174040200004534017404020000451b816d040200004501016d04020000453f016c04020000453e816c04020000453881640402000045378164040200004503015c040200004522814d040200004522014d040200004536814404020000451f0141040200004506014104020000451b013e04020000451a813e04020000451a013e040200004533013d04020000450180f104020000450100f104020000450080f104020000450000f104020000453600f004020000453580f004020000453500f004020000451af0ef04020000451a70ef040200004519f0ef04020000451970ef040200004518f0ef04020000451870ef040200004517f0ef040200004537e0ed04020000453760ed040200004536e0ed04020000453e80ec04020000453e00ec04020000453d80ec04020000450d80eb04020000450d00eb04020000453100e904020000451080e804020000451000e804020000450f80e804020000450f00e804020000450880e804020000450780e804020000451c70e704020000451bf0e7040200004517f0e7040200004522c0e6040200004519c0df04020000452190de04020000450500d504020000453220d1040200004531a0d104020000453120d104020000453ef0cc04020000450dc0cb04020000450080ca04020000453380c904020000453300c904020000453280c904020000453200c904020000453180c904020000453100c904020000453080c904020000453000c904020000452f80c904020000452f00c904020000452d00c904020000452c80c904020000452c00c904020000450800c904020000451520c804020000451980c7040200004523e0c6040200004521e0c604020000452b00c504020000452a00c504020000452000be04020000453980bd04020000453880bd04020000452a020c03820000452a00bd04020000450ea0bb04020000451fa0af04020000451da0af04020000453be0ac04020000451f73f104020000443e01640402000045378144040200004539013c040200004538813c04020000452e013904020000452701390402000045200139040200004532013804020000452f81380402000045360134040200004506013404020000450581340402000045277105040200004526f1050402000045267105040200004525f10504020000453271180402000045317118040200004524011804020000451401180402000045138118040200004512811804020000451201180402000045110118040200004510811804020000450f811804020000450f011804020000450d011804020000450c81180402000045058118040200004504811804020000451b01170402000045198117040200004518011704020000451681170402000045158117040200004515011704020000451f811604020000451e811604020000451c8116040200004536f115040200004535f11504020000453571150402000045280115040200004527811504020000452681150402000045260115040200004525011504020000452101150402000045010115040200004500811504020000453f4114040200004500c0280081c000830801780200c0008506220f0281c0008511c20e0281c0008500012d0381c000851f012e0381c0008510b0a00100c000832863050181c0008328630c0181c000830581a00200c0010534812002814000c50cf05100808002030670a6028040014304205700804002032981f50281c000852381f10281c000851581f10281c000850701f00281c000850e01ef0281c000851301ee0281c000852a01ed0281c000852401e90281c000851601e90281c000850781e80281c000850e81e70281c000851e603f00818000c32f40210081c000c317b29a0201c0008422817d028100010509b2940201c000841681e10281c000851d81710301c000850f01df0281c000852b01dd0281c000851701d90281c000850881d80281c000850f81d70281c000851481d60281c000852b81d50281c000852581d10281c000853d120f0181c0008313b27c0201c000840901d00281c0008511b1c90201c000841001cf0281c000851501ce0281c000852b81710301c000852c01cd0281c000850101750301c000852601c90281c000851801c90281c000850981c80281c000851081c70281c000851581c60281c000852c81c50281c000852681c10281c000851881c10281c000850a01c00281c000853d11f30181c000831101bf0281c0008528631a0181c000830e025f0280c000841050d3028040024338509e0100c0008307012c02814001051601be0281c0008533813002814001052d01bd0281c0008506813402814001052bb1b10201c0008428e21a02818001052701b90281c0008500c0210081c000833bd26a02020000831db1ab0201c0008400903b0101c00083090143028140010503014802814001050b814b02814001052a721a02818001052b121a02818001050281500281400105210150028140010519e2b40181c00083308160028140010501016802814001051f8168028140010506816b028140010536521a028180010502017c028140010533c3bb0201c0008311d0220081c0008321f34b0201c000832640e2030080010301818402814001051402be05010001440c931d04814001042f53f80401c000840143030501c000c406810804020000850601080402000085058108040200008505010804020000851c010704020000851b81070402000085198107040200008519010704020000850201310381c000c50b433205020000841a23e704824000440613e802824000440f430205020000843813d204028000041443e705024000441753e7048240004416c3e7048240004433a3e704824000443313e704824000443283e7048240004431f3e704824000443163e7048240004430d3e704824000443043e704824000442fb3e704824000442e93e704824000443e210404020000853c210404020000850ba20703820000850233e803824000443aa10404020000850e81780301c0008534b3e7040240004438b3e7040240004425a3e7048240004414e3e705024000441583e705024000440f824c03824000443833e704024000443433e7040240004427b20d02820000852243e7048240004437d3f004820000843053f80401c000843633e704024000440b320703820000850ac207038200008534802e010200008330d3f80401c000843f116e04014001041162ea04820000441bd3e704824000441b43e7048240004435b3e7040240004417917c03024000440f825503824000441873e7048240004417e3e704824000442e03e7048240004421818103824000440230f103824000442513e704824000442483e7048240004423f3e704824000442363e7048240004422d3e704824000441e13e7048240004436b3e704024000440543330481c0008400e3e803824000441993e704824000440d62d404818000c41903e704824000442633e704824000442101390381c000853a13e703824000443e93f004820000840143630481c0008402d3d304828000042381bc03828000040733de050280000428621504018000850081fb02014000852803d201810000c433d21a02818000850601730281400085090173028140008533821a028180008533321a02818000851300c901008001031e81bd02010000c51d01d502010000c525921603018000851e018002814000853fc380018180008311c243018140008408018302814000851f321703018000850bc27c018140008419c28001814000843300e90281c000850bc281018140008404019302814000852540f102018000851952bc02008001431f20cf0281c00085070193028140408500819402814040851c8198028140008500019c02814000851d81cd02010000c50080c503814000851452770280c001040ca2770280c001043fe04400814000831570d901808000830301a3028140008509e2ec0201c000430281ab02814000850581ab02814000852f402c0080800083266086010140008306d08701014000832f60e10180c000840ab0e10180c000c401b040008080018306701b01804001030cf05600804001831b01b002814000850081fb020140008509508c01014000830081cb02014000852a01c802014000851981c802014000852640f802014000850d40c402014000850b70c002014000850bc0b802014000850870b4020140008502404c00810000c308104500814000c33d1232018140008313d0840180c000c503c38801814000843fc0a001818000831a521703018000851040cb02014000850cf0cc02014000850ff0d3020140008512c0d302014000851270db020140008525f100020140008535c108020140008535711002014000850bc28601814000841901d002014000852981d002014000850001d302014000852b81b002814000850301d302014000851881d802014000852901d802014000851d818802814000852801e802014000850181eb020140008505018302814000852781f002014000850101f302014000852f017802814000851e81780281400085070163028140008515c2100201400085092098010140008304015c028140008521f09901014000830800bf018080008311f19702004001832f402c00808000831770890180c0010511c239018140008427b0c20100c0010319c234018140008410b0d801810000c400b06701014000831fc1fb01814001041fc1f60181400084024047008140008301a05f0101c000831fc1ec018140008426608b01014000830f81000280c0010514f2770180c001040f425f0180c0010411212c01814001043fc12201814001043b30b30100c0010300807401010000c309209d01014000833fc0140180c001033fc0970180c001050e01180280c001053090960180c001050800bc01814001041020d001810000c33fc0bb018140008408104a00814000830800b701814000841912bc02008000c31121400180c0010409e3010201c000830800ad01814000842ff38b01818000433fc0a1018140008417309f018140008430909b018140008430909b01814000843fc3e6018140010318f29201818000432ff39101818000432ff38b01818000433fc3d70181400083024047008140008302404700814000833893070201c0000309e3080201c0000321f33d0201c000032b00e502810000c534f11802814000853da3ba018140008339d3b501814000831fd3af018140008339d3ab01814000833ed34901814000833d12240181c000831d93d80201c000031d93df0201c0000300d0cd018100010419321b020140008519721b020140008505336f0180c000c300d0cc018140008330909b018140008402404700814000831fd3af01814000833ed34901814000830240ad018140008329e0e20201c000041020c701814000843090a001814000831fd397018180008320815802814000853d6342018080014304815402814000850b0123028140008502404700814000831052a70280c000c41002a70280c000c41040cb02014000851770890180c0010518721b020140008539d3b0018140008317f21b02014000853da3bf01814000c33da3c401814000831300c20100c0010330121a02818000853fc12701810001041270db020140008510b0d801810000c43fb1420201c000042ff39101818000430401c302814000853fc0be01810000c317321b02014000853da3ce01814000c316f21b02014000851770920180c0010507b1b20201c0000417708d018140008416721b02014000850260370180c0010516321b02014000853fc0100180c001052b81b002814000850852a70280c0010435711002014000851770920180c001051881d802014000850f425f0180c0010412421b02014000852ff3970180c0010311421b02014000853482da01810000c30ff0d3020140008527b0c20100c00103050183028140008502603d0180c001053e021a02014000850800bc018140010429321a028180008511720e0281c000851e817802814000853090960180c0010522421702818000852600ad02010001053d421a020140008503c39201810000c403016c02814000850aa28f0280c001041b721703018000850480c903010001053b421a02014000852001a502010000c51310f6028140008426014502810000c51fc23801814000842803d601810000c43a821a02014000851fc23d01814000c40880f003014000850e525f0280c001041701f002014000851ff3f101810000c422f0ee02018000853da3d3018100010308d0a70201c00005095091010140008324f0f902018000850bc28b01814000c43fb3db0201c0000403c383018140008409b2a20201c00004095086010180004329b1c90201c000043fc374018180004329b1c90201c000043fc37401818000430381cb020140008519c27b018140008429b1c20201c000041fd3af018140008311b1c20201c0000439d3ab018140008321a21702818000851300c901008001032f402c008080008316f25f0180c000841fc233018140008416620d028200004509813b02814000853c021a02014000850a02170400c0008421f3590201c0000309e2f30201c000031570d001814000840402ce0201c0000317309a018140008419d2910201c0000302b094010140008309e2e50201c000030e00ac02814000853892eb0201c0000318f29201818000433da3c901814000833892f90201c000031d93e60201c000033fc097018140008429e0e90201c0000401a066010140008329e0f00201c0000400b06101018000430152100281c000051fd39d018180004311b1c20201c0000439d3ab01814000832b621a028180004518f2840181c000833fb1500201c0000439d3a501818000432340e602018000451fd3a901818000433d821a02014000853fc11301814000843fb1570201c0000410b09b010140008335c10802014000851fc1fb018140010419e2a60181c0000311d0290080c001033fb15e0201c0008439d39901818000c33fb1650201c0000427b18c0201c0000419e2a60181c0000327b19a0201c000042bb1a30201c0000427b18c0201c00004058120028140008507b1a40201c000043c421a02014000850181eb02014000850bc28601814000843d12010181c000031901d002014000850a00fc03014000850601a3028140008500f365020140008425b1a40201c000042bb1aa0201c0000407b1ab0201c000041801e002014000852ff38b018180004325b1ab0201c000841fc242018140008406d09101014000c300807401010000c302404700814000833fc3d7018140008319e29f0181c0008303734e03010000c41f013d02810000c53a021a020180008519c23e018140008411c23e01814000842ea2ad0181c0008319c239018140010420012d02810001050e01180280c001053ac21a020140008539821a02018000850a013302814000850281d00200c0010501012c02814000850c120f0281c0008519721b020140008511c234018140008418e20d028200000533c3d00201800083073364020180008407336402018000843b821a02014000853bc21a02014000c538c21a02018000453fc3eb01810000c32f402c00808000831db1b20201c000043fc0a1018140008425b1b20201c0000419e2ad0181c000033a421a020180008506d08c01014000c321f217028180008500806e010180008331f10902018000853fc38c018100010319d271020200000311213b01814000c411b1c20201c0000439d3ab01814000831840d80201c000053fc0a1018140008419d2910201c0000302b09401014000831ff3f50180c001040f425f0180c001043620f802814000853680f002814000853720d8028140008527a0c8028140008510a0bb02814000850da0b402814000851100b302814000851380bb02814000851ff3f50180c001040f425f0180c0010410a2a70280c001043fc11d01814000843fc11801814000841142a70280c001041192a70280c001040652bf0280c001043df34201808001433df34201808001430bc27c01814000841570d501814000841ce0290100c001031ce0290100c001030c01380200c0010506a2bf0280c001043d934201808001433d9342018080014319c28001814000841020cc01814000843850a10100c001033850a10100c001031240d00200c000c509017302814000850481bb02814000850181bb02814000850201b3028140008500d0c801814000841501dd02010000c52c81a002814000850201b302814000851b81ed02010000c506819b028140008503819b02814000852d8190028140008501018c02814000852e018802814000852e8180028140008503c38d018140008400d0c301814000840d81200280c000850d01280280c0010527b0c701008000831300c7010080008327b0c501008001431300c501008001433bd28202008000832f719b020080014309816b02814000853001680281400085200160028140008501816002814000853fc00d0180c0010507815b02814000850b015302814000850801530281400085318150028140008527a0c8028140008505014c028140008508814b028140008532014802814000851ed21703018000850c0143028140008532814002814000852201400281400085038140028140008506013c028140008500013c028140008533013802814000852281380281400085040138028140008500813402814000850d01330281400085230130028140008504813002814000851020d10180c000c407b0e10180c000c334012802814000851150ae010080018323812802814000853892eb0201c000033fb1420201c000043d12010181c000033e821a020140008511b1c20201c000041570d501814000841040cb02014000850cf0cc02014000853ec21a02014000850e811b02814000851270db020140008515c210020140008535711002014000850201b302814000850bc286018140008416721b020140008509010c02814000852b81b002814000850301d302014000853f021a02014000851881d8020140008512421b02014000852901d802014000853f821a02014000850501830281400085070163028140008529321a02818000851e817802814000852a81c002814000852b01b8028140008508d0a70201c0000509509101014000833e821a020140008512021b02014000853fb1490201c000043fc38601818000430240ad01814000c41570d501814000843892eb0201c0000318f292018180004323017502810000c51b017d02810000c51b017d02810000c534812002814000c531015802814000c53fc37a01818000833fc37a018180008305814402814000850c813b028140010532e21a02818000853fc0ac0181400084309096018140008417709201814000843800c8030140008523921503014000843b620403820000452781b10281c000853fc36e01818000832862fe0181c000832272170380c0010408011c028140008535121a02818000853483f80381c0008434f3f80381c000840e321603818000850b2216038180008508f216038180008508821603818000853fc3dc018140008339c3f80381c000843890c0028040018327b0c20100c0010320622f0300c0010406c31e03010000c406631e03010000c43b420f0400c0010320622f0300c0010400a28e04010000c409822f0300c001043fe04400814000831570d901808000830301a3028140008509e2ec0201c000430281ab02814000850581ab02814000852f402c0080800083266086010140008306d08701014000832f60e10180c000840ab0e10180c000c401b040008080018306701b01804001030cf05600804001831b01b002814000850081fb020140008509508c01014000830081cb02014000852a01c802014000851981c802014000852640f802014000850d40c402014000850b70c002014000850bc0b802014000850870b4020140008502404c00810000c308104500814000c33d1232018140008313d0840180c000c503c38801814000843fc0a001818000831a521703018000851040cb02014000850cf0cc02014000850ff0d3020140008512c0d302014000851270db020140008525f100020140008535c108020140008535711002014000850bc28601814000841901d002014000852981d002014000850001d302014000852b81b002814000850301d302014000851881d802014000852901d802014000851d818802814000852801e802014000850181eb020140008505018302814000852781f002014000850101f302014000852f017802814000851e81780281400085070163028140008515c2100201400085092098010140008304015c028140008521f09901014000830800bf018080008311f19702004001832f402c00808000831770890180c0010511c239018140008427b0c20100c0010319c234018140008410b0d801810000c400b06701014000831fc1fb01814001041fc1f60181400084024047008140008301a05f0101c000831fc1ec018140008426608b01014000830f81000280c0010514f2770180c001040f425f0180c0010411212c01814001043fc12201814001043b30b30100c0010300807401010000c309209d01014000833fc0140180c001033fc0970180c001050e01180280c001053090960180c001050800bc01814001041020d001810000c33fc0bb018140008408104a00814000830800b701814000841912bc02008000c30081fb02014000851701bd02010000c539021a02018000853fc3e701810040c43fc3e1018140408339d39f01818040831fd3a301818040831300c901008001031e81bd02010000c51d01d502010000c5153217020180008516b21b02014000853fc380018180008311c243018140008415f21b020140008532410102018000850bc27c018140008419c280018140008401b2fe0201c000830bc281018140008415b21b02014000852540f102018000851952bc020080014309e2fa0201c000833fc09c01814040843090a0018140408415721b020140008512821b02014000851d81cd02010000c50081fb02014000851770950180c0410517708c0180c041053fe04400814000831570d9018080008311c21b02014000852ea2c90181c000431781b502010000c511821b02014000852f402c0080800083266086010140008306d08701014000832f60e10180c000840ab0e10180c000c401b040008080018306701b01804001030cf05600804001831f81ad02010000c50081fb02014000853fc0b601814040840081cb02014000852a01c802014000851981c802014000852640f802014000850d40c402014000850b70c002014000850bc0b802014000850870b4020140008502404c00810000c308104500814000c33d1232018140008313d0840180c000c503c3880181400084377115020180008537c10d02018000851040cb02014000850cf0cc02014000850ff0d3020140008512c0d302014000851270db020140008525f100020140008535c108020140008535711002014000850bc28601814000841901d002014000852981d002014000850001d302014000853c821a02014000850301d302014000851881d802014000852901d802014000853b021a02014000852801e802014000850181eb020140008515821002014000852781f002014000850101f302014000851681f8020140008519c24301814040840281db020140008515c2100201400085092098010140008319019d02010000c521f09901014000830800bf018080008311f19702004001832f402c00808000831770890180c0010511c239018140008427b0c20100c0010319c234018140008410b0d801810000c400b06701014000831fc1fb01814001041fc1f60181400084024047008140008301a05f0101c000831fc1ec018140008426608b01014000830681900200c0010514f2770180c001040f425f0180c0010411212c01814001043fc12201814001043b30b30100c0010300807401010000c309209d01014000833fc0140180c001033fc0970180c001050401b80200c001053090960180c001050800bc01814001041020d001810000c33fc0bb018140008408104a00814000830800b701814000841912bc02008000c31121400180c001040b02900080400004'); + +export const font = createFont(sprites, [ + [0, [186]], + [32, [1180, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311]], + [161, [358, 776, 341, 777, 778, 779, 207, 780, 781, 351, 359, 355, 0, 354, 782, 450, 783, 785, 786, 449, 787, 206, 448, 654, 784, 352, 360, 357, 356, 655, 353, 495, 456, 550, 457, 327, 328, 331, 313, 496, 329, 656, 657, 497, 467, 551, 658, 659, 350, 498, 443, 444, 459, 338, 343, 342, 499, 468, 660, 339, 479, 661, 454, 318, 345, 316, 458, 317, 319, 330, 320, 323, 315, 321, 322, 326, 346, 325, 324, 788, 349, 334, 347, 332, 460, 333, 789, 340, 336, 348, 335, 314, 480, 790, 337, 562, 563, 548, 549, 427, 428, 429, 430, 564, 565, 686, 687, 461, 462, 463, 464, 688, 689, 690, 691, 791, 792, 716, 717, 431, 432, 465, 466, 718, 719, 489, 490, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 491, 492, 736, 737, 738, 739, 740, 741, 742, 483, 484, 743, 744, 485, 486, 745, 746, 441, 442, 433, 434, 747, 748, 469, 470, 749, 556, 557, 750, 751, 752, 753, 558, 559, 754, 755, 487, 488, 756, 757, 471, 472, 435, 436, 758, 759, 493, 494, 473, 474, 760, 761, 475, 476, 762, 763, 764, 765, 445, 446, 766, 767, 477, 478, 560, 561, 768, 769, 770, 771, 772, 773, 774, 437, 438, 439, 440, 481, 482, 775]], + [402, [344]], + [536, [552, 553, 554, 555]], + [714, [914]], + [913, [662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 0, 679, 680, 681, 682, 683, 684, 685]], + [945, [692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 915, 709, 710, 711, 712, 713, 714, 715]], + [991, [803]], + [1024, [500, 373, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 361, 363, 365, 367, 369, 371, 375, 377, 379, 381, 383, 385, 387, 389, 391, 393, 395, 397, 399, 401, 403, 405, 407, 409, 411, 413, 415, 417, 419, 421, 423, 425, 362, 364, 366, 368, 370, 372, 376, 378, 380, 382, 384, 386, 388, 390, 392, 394, 396, 398, 400, 402, 404, 406, 408, 410, 412, 414, 416, 418, 420, 422, 424, 426, 515, 374, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535]], + [1136, [536, 537, 538, 539]], + [1162, [540, 541, 542, 543, 544, 545, 546, 547]], + [2960, [802]], + [4326, [801]], + [8208, [1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 0, 0, 193, 0, 0, 0, 1076, 1057]], + [8252, [205]], + [8263, [1058, 1059, 1060]], + [8364, [447]], + [8470, [810]], + [8482, [455]], + [8592, [213, 210, 212, 211, 215, 204]], + [8616, [209]], + [8735, [214]], + [8962, [312]], + [9632, [566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 208, 578, 579, 580, 581, 582, 216, 583, 584, 585, 586, 587, 588, 589, 202, 590, 217, 591, 592, 593, 594, 595, 596, 597, 203, 598, 599, 600, 601, 602, 603, 195, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 194, 196, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 1075, 0, 0, 0, 0, 793, 794]], + [9786, [187, 188, 201, 0, 0, 1062, 198, 1063, 197]], + [9812, [1070, 1071, 0, 0, 0, 0, 1072, 1073, 0, 0, 0, 0, 192, 453, 811, 191, 812, 189, 190, 813, 0, 0, 199, 200]], + [9890, [1067, 1068, 1069, 452, 1066, 1061, 1064, 1065]], + [9906, [451]], + [10003, [814, 815, 816, 817, 818, 819]], + [10022, [796, 797]], + [10032, [795]], + [10047, [808, 809]], + [10083, [1074, 0, 800]], + [12289, [1011, 1012, 1013, 0, 1014, 0, 0, 1015, 1016, 806, 807, 1017, 1018, 1019, 1020, 804, 805, 1021, 1022]], + [12316, [1023, 1024, 1025, 1026, 0, 1027, 1028, 1029, 1030, 0, 1031, 1032, 1033, 1034]], + [12336, [1035, 1036, 1037, 1038, 1039, 1040]], + [12353, [820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905]], + [12449, [916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010]], + [21325, [799, 0, 0, 798]], + [65281, [1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179, 906, 907, 908, 909, 910, 911, 912]], + [65507, [913]], + [128067, [1077]], + [128518, [1083]], + [128528, [1081, 1082]], + [128543, [1084, 1080]], + [128565, [1079]], + [128578, [1078, 1085]], +]); + +export const emoji = createFont(sprites, [ + [9752, [1257]], + [9800, [1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254]], + [9889, [1239]], + [9924, [1230]], + [9934, [1255, 1265]], + [10024, [1238]], + [10052, [1231]], + [10084, [1185]], + [11088, [1235]], + [127752, [1226]], + [127775, [1236, 1234]], + [127794, [1213]], + [127800, [1211]], + [127807, [1216, 1258]], + [127815, [1264, 0, 0, 1219, 0, 1266, 0, 1217, 1218, 1261, 0, 0, 0, 0, 1222]], + [127841, [1225]], + [127850, [1259, 0, 1223, 1224]], + [127873, [1229, 0, 1221, 1214, 1233]], + [127922, [1237, 0, 0, 1241, 1242]], + [127936, [1220]], + [128008, [1200]], + [128049, [1201]], + [128062, [1199]], + [128081, [1256]], + [128123, [1198, 0, 0, 0, 1183]], + [128142, [1227, 0, 1215, 0, 0, 0, 1191, 1194, 1192, 1193, 0, 1186, 1187, 1188, 1189]], + [128174, [1212]], + [128293, [1240]], + [128367, [1232]], + [128375, [1196]], + [128420, [1190]], + [128520, [1182, 0, 0, 0, 0, 1181]], + [128568, [1203, 1204, 1202, 1205, 1206, 1207, 1210, 1209, 1208]], + [129300, [1184]], + [129356, [1228]], + [129365, [1263, 0, 0, 0, 0, 1260]], + [129389, [1262]], + [129415, [1197, 0, 0, 0, 1195]], +]); + +export const fontSmall = createFont(sprites, [ + [0, [1267]], + [32, [1629, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1321, 1322, 1323, 1324, 1325, 1326, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341, 1342, 1343, 1344, 1345, 1346, 1347, 1348, 1349, 1350, 1351, 1352, 1353, 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368, 1369, 1370, 1371, 1372, 1373, 1374, 1375, 1376, 1377, 1378, 1379, 1380, 1381, 1382, 1383, 1384, 1385, 1386, 1387, 1388, 1389, 1390, 1391, 1392]], + [161, [1439, 0, 1422, 0, 0, 0, 1288, 0, 0, 1432, 1440, 1436, 0, 1435, 0, 1531, 0, 0, 0, 1530, 0, 1287, 1529, 1736, 0, 1433, 1441, 1438, 1437, 1737, 1434, 1576, 1537, 1632, 1538, 1408, 1409, 1412, 1394, 1577, 1410, 1738, 1739, 1578, 1548, 1633, 1740, 1741, 1431, 1579, 1524, 1525, 1540, 1419, 1424, 1423, 1580, 1549, 1742, 1420, 1560, 1743, 1535, 1399, 1426, 1397, 1539, 1398, 1400, 1411, 1401, 1404, 1396, 1402, 1403, 1407, 1427, 1406, 1405, 0, 1430, 1415, 1428, 1413, 1541, 1414, 1774, 1421, 1417, 1429, 1416, 1395, 1561, 1775, 1418, 1644, 1645, 1630, 1631, 1508, 1509, 1510, 1511, 1646, 1647, 1768, 1769, 1542, 1543, 1544, 1545, 1770, 1771, 1772, 1773, 1776, 1777, 0, 0, 1512, 1513, 1546, 1547, 0, 0, 1570, 1571]], + [304, [1572, 1573]], + [313, [1564, 1565, 0, 0, 1566, 1567, 0, 0, 1522, 1523, 1514, 1515, 0, 0, 1550, 1551, 0, 1638, 1639, 0, 0, 0, 0, 1640, 1641, 0, 0, 1568, 1569, 0, 0, 1552, 1553, 1516, 1517, 0, 0, 1574, 1575, 1554, 1555, 0, 0, 1556, 1557, 0, 0, 0, 0, 1526, 1527, 0, 0, 1558, 1559, 1642, 1643]], + [377, [1518, 1519, 1520, 1521, 1562, 1563]], + [402, [1425]], + [536, [1634, 1635, 1636, 1637]], + [913, [1744, 1745, 1746, 1747, 1748, 1749, 1750, 1751, 1752, 1753, 1754, 1755, 1756, 1757, 1758, 1759, 1760, 0, 1761, 1762, 1763, 1764, 1765, 1766, 1767]], + [991, [1788]], + [1024, [1581, 1454, 1582, 1583, 1584, 1585, 1586, 1587, 1588, 1589, 1590, 1591, 1592, 1593, 1594, 1595, 1442, 1444, 1446, 1448, 1450, 1452, 1456, 1458, 1460, 1462, 1464, 1466, 1468, 1470, 1472, 1474, 1476, 1478, 1480, 1482, 1484, 1486, 1488, 1490, 1492, 1494, 1496, 1498, 1500, 1502, 1504, 1506, 1443, 1445, 1447, 1449, 1451, 1453, 1457, 1459, 1461, 1463, 1465, 1467, 1469, 1471, 1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1505, 1507, 1596, 1455, 1597, 1598, 1599, 1600, 1601, 1602, 1603, 1604, 1605, 1606, 1607, 1608, 1609, 1610, 1611, 1612, 1613, 1614, 1615, 1616]], + [1136, [1617, 1618, 1619, 1620]], + [1162, [1621, 1622, 1623, 1624, 1625, 1626, 1627, 1628]], + [2960, [1787]], + [4326, [1786]], + [8226, [1274, 0, 0, 0, 1808]], + [8252, [1286]], + [8364, [1528]], + [8482, [1536]], + [8592, [1294, 1291, 1293, 1292, 1296, 1285]], + [8616, [1290]], + [8735, [1295]], + [8962, [1393]], + [9632, [1648, 1649, 1650, 1651, 1652, 1653, 1654, 1655, 1656, 1657, 1658, 1659, 1289, 1660, 1661, 1662, 1663, 1664, 1297, 1665, 1666, 1667, 1668, 1669, 1670, 1671, 1283, 1672, 1298, 1673, 1674, 1675, 1676, 1677, 1678, 1679, 1284, 1680, 1681, 1682, 1683, 1684, 1685, 1276, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1693, 1694, 1695, 1696, 1697, 1275, 1277, 1698, 1699, 1700, 1701, 1702, 1703, 1704, 1705, 1706, 1707, 1708, 1709, 1710, 1711, 1712, 1713, 1714, 1715, 1716, 1717, 1718, 1719, 1720, 1721, 1722, 1723, 1724, 1725, 1726, 1727, 1728, 1729, 1730, 1731, 1732, 1733, 1734, 1735, 1807, 0, 0, 0, 0, 1778, 1779]], + [9786, [1268, 1269, 1282, 0, 0, 1794, 1279, 1795, 1278]], + [9812, [1802, 1803, 0, 0, 0, 0, 1804, 1805, 0, 0, 0, 0, 1273, 1534, 0, 1272, 0, 1270, 1271, 0, 0, 0, 1280, 1281]], + [9890, [1799, 1800, 1801, 1533, 1798, 1793, 1796, 1797]], + [9906, [1532]], + [10022, [1781, 1782]], + [10032, [1780]], + [10083, [1806, 0, 1785]], + [12298, [1791, 1792, 0, 0, 0, 0, 1789, 1790]], + [21325, [1784, 0, 0, 1783]], + [65281, [1817, 1818, 1819, 1820, 1821, 1822, 1823, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1831, 1832, 1833, 1834, 1835, 1836, 1837, 1838, 1839, 1840, 1841, 1842, 1843, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1851, 1852, 1853, 1854, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1866, 1867, 1868, 1869, 1870, 1871, 1872, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1889, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1901, 1902, 1903, 1904, 1905, 1906, 1907, 1908, 1909, 1910]], + [128067, [1809]], + [128518, [1815]], + [128528, [1813, 1814]], + [128543, [1816, 1812]], + [128565, [1811]], + [128578, [1810]], +]); + +export const fontMono = createFont(sprites, [ + [0, [1911]], + [33, [1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036]], + [167, [1932]], + [182, [1931]], + [8226, [1918]], + [8252, [1930]], + [8592, [1938, 1935, 1937, 1936, 1940, 1929]], + [8616, [1934]], + [8735, [1939]], + [8962, [2037]], + [9644, [1933]], + [9650, [1941]], + [9658, [1927, 0, 1942]], + [9668, [1928]], + [9675, [1920]], + [9688, [1919, 1921]], + [9786, [1912, 1913, 1926, 0, 0, 0, 1923, 0, 1922]], + [9824, [1917, 0, 0, 1916, 0, 1914, 1915, 0, 0, 0, 1924, 1925]], +]); + +export const fontPal = createFont(sprites2, [ + [0, [7596]], + [32, [13795, 7628, 7629, 7630, 7631, 7632, 7633, 7634, 7635, 7636, 7637, 7638, 7639, 7640, 7641, 7642, 7643, 7644, 7645, 7646, 7647, 7648, 7649, 7650, 7651, 7652, 7653, 7654, 7655, 7656, 7657, 7658, 7659, 7660, 7661, 7662, 7663, 7664, 7665, 7666, 7667, 7668, 7669, 7670, 7671, 7672, 7673, 7674, 7675, 7676, 7677, 7678, 7679, 7680, 7681, 7682, 7683, 7684, 7685, 7686, 7687, 7688, 7689, 7690, 7691, 7692, 7693, 7694, 7695, 7696, 7697, 7698, 7699, 7700, 7701, 7702, 7703, 7704, 7705, 7706, 7707, 7708, 7709, 7710, 7711, 7712, 7713, 7714, 7715, 7716, 7717, 7718, 7719, 7720, 7721]], + [161, [7768, 8186, 7751, 8187, 8188, 8189, 7617, 8190, 8191, 7761, 7769, 7765, 0, 7764, 8192, 7860, 8193, 8195, 8196, 7859, 8197, 7616, 7858, 8064, 8194, 7762, 7770, 7767, 7766, 8065, 7763, 7905, 7866, 7960, 7867, 7737, 7738, 7741, 7723, 7906, 7739, 8066, 8067, 7907, 7877, 7961, 8068, 8069, 7760, 7908, 7853, 7854, 7869, 7748, 7753, 7752, 7909, 7878, 8070, 7749, 7889, 8071, 7864, 7728, 7755, 7726, 7868, 7727, 7729, 7740, 7730, 7733, 7725, 7731, 7732, 7736, 7756, 7735, 7734, 8198, 7759, 7744, 7757, 7742, 7870, 7743, 8199, 7750, 7746, 7758, 7745, 7724, 7890, 8200, 7747, 7972, 7973, 7958, 7959, 7837, 7838, 7839, 7840, 7974, 7975, 8096, 8097, 7871, 7872, 7873, 7874, 8098, 8099, 8100, 8101, 8201, 8202, 8126, 8127, 7841, 7842, 7875, 7876, 8128, 8129, 7899, 7900, 8130, 8131, 8132, 8133, 8134, 8135, 8136, 8137, 8138, 8139, 8140, 8141, 8142, 8143, 8144, 8145, 7901, 7902, 8146, 8147, 8148, 8149, 8150, 8151, 8152, 7893, 7894, 8153, 8154, 7895, 7896, 8155, 8156, 7851, 7852, 7843, 7844, 8157, 8158, 7879, 7880, 8159, 7966, 7967, 8160, 8161, 8162, 8163, 7968, 7969, 8164, 8165, 7897, 7898, 8166, 8167, 7881, 7882, 7845, 7846, 8168, 8169, 7903, 7904, 7883, 7884, 8170, 8171, 7885, 7886, 8172, 8173, 8174, 8175, 7855, 7856, 8176, 8177, 7887, 7888, 7970, 7971, 8178, 8179, 8180, 8181, 8182, 8183, 8184, 7847, 7848, 7849, 7850, 7891, 7892, 8185]], + [402, [7754]], + [536, [7962, 7963, 7964, 7965]], + [714, [8324]], + [913, [8072, 8073, 8074, 8075, 8076, 8077, 8078, 8079, 8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 0, 8089, 8090, 8091, 8092, 8093, 8094, 8095]], + [945, [8102, 8103, 8104, 8105, 8106, 8107, 8108, 8109, 8110, 8111, 8112, 8113, 8114, 8115, 8116, 8117, 8118, 8325, 8119, 8120, 8121, 8122, 8123, 8124, 8125]], + [991, [8213]], + [1024, [7910, 7783, 7911, 7912, 7913, 7914, 7915, 7916, 7917, 7918, 7919, 7920, 7921, 7922, 7923, 7924, 7771, 7773, 7775, 7777, 7779, 7781, 7785, 7787, 7789, 7791, 7793, 7795, 7797, 7799, 7801, 7803, 7805, 7807, 7809, 7811, 7813, 7815, 7817, 7819, 7821, 7823, 7825, 7827, 7829, 7831, 7833, 7835, 7772, 7774, 7776, 7778, 7780, 7782, 7786, 7788, 7790, 7792, 7794, 7796, 7798, 7800, 7802, 7804, 7806, 7808, 7810, 7812, 7814, 7816, 7818, 7820, 7822, 7824, 7826, 7828, 7830, 7832, 7834, 7836, 7925, 7784, 7926, 7927, 7928, 7929, 7930, 7931, 7932, 7933, 7934, 7935, 7936, 7937, 7938, 7939, 7940, 7941, 7942, 7943, 7944, 7945]], + [1136, [7946, 7947, 7948, 7949]], + [1162, [7950, 7951, 7952, 7953, 7954, 7955, 7956, 7957]], + [2960, [8212]], + [4326, [8211]], + [8208, [8451, 8452, 8453, 8454, 8455, 8456, 8457, 8458, 8459, 8460, 8461, 8462, 8463, 8464, 8465, 8466, 0, 0, 7603, 0, 0, 0, 8486, 8467]], + [8252, [7615]], + [8263, [8468, 8469, 8470]], + [8364, [7857]], + [8470, [8220]], + [8482, [7865]], + [8592, [7623, 7620, 7622, 7621, 7625, 7614]], + [8616, [7619]], + [8735, [7624]], + [8962, [7722]], + [9632, [7976, 7977, 7978, 7979, 7980, 7981, 7982, 7983, 7984, 7985, 7986, 7987, 7618, 7988, 7989, 7990, 7991, 7992, 7626, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 7612, 8000, 7627, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 7613, 8008, 8009, 8010, 8011, 8012, 8013, 7605, 8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025, 7604, 7606, 8026, 8027, 8028, 8029, 8030, 8031, 8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039, 8040, 8041, 8042, 8043, 8044, 8045, 8046, 8047, 8048, 8049, 8050, 8051, 8052, 8053, 8054, 8055, 8056, 8057, 8058, 8059, 8060, 8061, 8062, 8063, 8485, 0, 0, 0, 0, 8203, 8204]], + [9786, [7597, 7598, 7611, 0, 0, 8472, 7608, 8473, 7607]], + [9812, [8480, 8481, 0, 0, 0, 0, 8482, 8483, 0, 0, 0, 0, 7602, 7863, 8221, 7601, 8222, 7599, 7600, 8223, 0, 0, 7609, 7610]], + [9890, [8477, 8478, 8479, 7862, 8476, 8471, 8474, 8475]], + [9906, [7861]], + [10003, [8224, 8225, 8226, 8227, 8228, 8229]], + [10022, [8206, 8207]], + [10032, [8205]], + [10047, [8218, 8219]], + [10083, [8484, 0, 8210]], + [12289, [8421, 8422, 8423, 0, 8424, 0, 0, 8425, 8426, 8216, 8217, 8427, 8428, 8429, 8430, 8214, 8215, 8431, 8432]], + [12316, [8433, 8434, 8435, 8436, 0, 8437, 8438, 8439, 8440, 0, 8441, 8442, 8443, 8444]], + [12336, [8445, 8446, 8447, 8448, 8449, 8450]], + [12353, [8230, 8231, 8232, 8233, 8234, 8235, 8236, 8237, 8238, 8239, 8240, 8241, 8242, 8243, 8244, 8245, 8246, 8247, 8248, 8249, 8250, 8251, 8252, 8253, 8254, 8255, 8256, 8257, 8258, 8259, 8260, 8261, 8262, 8263, 8264, 8265, 8266, 8267, 8268, 8269, 8270, 8271, 8272, 8273, 8274, 8275, 8276, 8277, 8278, 8279, 8280, 8281, 8282, 8283, 8284, 8285, 8286, 8287, 8288, 8289, 8290, 8291, 8292, 8293, 8294, 8295, 8296, 8297, 8298, 8299, 8300, 8301, 8302, 8303, 8304, 8305, 8306, 8307, 8308, 8309, 8310, 8311, 8312, 8313, 8314, 8315]], + [12449, [8326, 8327, 8328, 8329, 8330, 8331, 8332, 8333, 8334, 8335, 8336, 8337, 8338, 8339, 8340, 8341, 8342, 8343, 8344, 8345, 8346, 8347, 8348, 8349, 8350, 8351, 8352, 8353, 8354, 8355, 8356, 8357, 8358, 8359, 8360, 8361, 8362, 8363, 8364, 8365, 8366, 8367, 8368, 8369, 8370, 8371, 8372, 8373, 8374, 8375, 8376, 8377, 8378, 8379, 8380, 8381, 8382, 8383, 8384, 8385, 8386, 8387, 8388, 8389, 8390, 8391, 8392, 8393, 8394, 8395, 8396, 8397, 8398, 8399, 8400, 8401, 8402, 8403, 8404, 8405, 8406, 8407, 8408, 8409, 8410, 8411, 8412, 8413, 8414, 8415, 8416, 8417, 8418, 8419, 8420]], + [19968, [8496, 8497, 0, 8498, 0, 0, 0, 8499, 8500, 8501, 8502, 8503, 0, 8504, 8505, 0, 8506, 8507, 0, 8508, 8509, 8510, 8511, 0, 8512, 8513, 8514, 8515, 8516, 8517, 8518, 0, 0, 8519, 8520, 0, 8521, 8522, 8523, 8524, 0, 0, 8525, 8526, 0, 8527, 0, 0, 8528, 0, 8529, 0, 8530, 0, 0, 0, 8531, 8532, 8533, 8534, 8535, 8536, 8537, 0, 0, 0, 0, 8538, 0, 8539, 0, 0, 8540, 8541, 0, 8542, 8543, 8544, 8545, 8546, 8547, 0, 8548, 8549, 8550, 0, 8551, 8552, 8553, 8554, 0, 0, 0, 8555, 8556, 8557, 8558, 8559, 0, 0, 0, 0, 8560]], + [20080, [8561, 8562, 0, 8563]], + [20094, [8564, 0, 8565]], + [20102, [8566, 0, 8567, 8568, 0, 8569, 8570, 0, 8571, 8572, 0, 8573, 8574, 0, 8575, 8576, 0, 0, 8577, 8578, 8579, 8580, 8581, 0, 8582, 8583, 0, 8584, 8585, 0, 8586, 8587, 8588, 8589, 8590, 8591, 0, 8592, 8593, 8594, 8595, 0, 0, 0, 8596, 0, 0, 8597, 0, 0, 0, 0, 8598, 0, 0, 0, 0, 8599, 8600, 8601, 0, 0, 0, 8602, 8603, 8604, 0, 0, 8605, 8606, 0, 8607, 8608, 8609, 0, 8610, 0, 8611, 8612, 8613, 8614, 8615, 8616, 8617]], + [20195, [8618, 8619, 8620, 0, 0, 0, 0, 8621, 0, 8622, 0, 8623, 0, 8624, 0, 8625, 0, 0, 0, 8626, 8627, 0, 0, 0, 8628, 0, 8629, 0, 8630, 0, 8631]], + [20234, [8632, 0, 0, 8633, 8634, 8635, 8636, 8637]], + [20247, [8638, 8639, 8640, 8641, 0, 0, 8642, 8643, 8644, 8645, 0, 8646, 0, 8647, 0, 8648, 0, 0, 0, 8649, 0, 0, 0, 0, 8650, 8651, 0, 0, 0, 8652, 0, 8653, 0, 8654, 0, 8655, 0, 8656, 8657]], + [20291, [8658, 0, 0, 8659]], + [20301, [8660, 8661, 8662, 8663, 8664, 0, 8665, 0, 8666, 0, 0, 0, 8667, 8668, 8669, 8670, 0, 0, 8671, 8672, 0, 0, 8673]], + [20329, [8674, 0, 0, 8675, 0, 0, 8676, 0, 0, 0, 8677, 0, 8678]], + [20348, [8679, 0, 0, 8680, 0, 0, 0, 8681, 8682, 0, 8683, 0, 8684, 0, 0, 8685, 0, 8686, 0, 8687, 0, 8688]], + [20379, [8689, 0, 8690, 0, 0, 8691, 8692, 0, 8693, 0, 8694, 8695, 8696, 8697, 0, 0, 0, 8698, 0, 8699, 8700]], + [20405, [8701, 8702]], + [20415, [8703, 0, 0, 8704, 8705, 8706]], + [20426, [8707, 0, 0, 0, 0, 8708, 8709]], + [20439, [8710, 8711, 0, 0, 0, 0, 8712, 8713, 8714, 8715, 8716, 0, 8717, 0, 0, 0, 0, 8718, 8719, 0, 0, 0, 8720, 8721, 8722, 0, 8723, 0, 8724, 0, 8725, 0, 0, 8726, 0, 8727, 0, 0, 0, 8728]], + [20489, [8729, 0, 8730, 0, 8731, 0, 8732, 0, 0, 8733, 0, 8734, 0, 8735, 0, 8736, 8737, 8738, 0, 0, 0, 0, 8739, 0, 8740, 0, 8741, 8742, 0, 8743, 0, 0, 8744, 8745, 8746, 0, 8747]], + [20534, [8748, 0, 0, 8749, 8750, 0, 8751, 0, 8752]], + [20551, [8753, 0, 8754, 0, 0, 8755, 0, 8756, 8757]], + [20565, [8758, 0, 0, 0, 0, 8759, 0, 8760]], + [20581, [8761]], + [20594, [8762, 0, 8763, 8764, 8765, 8766]], + [20605, [8767, 0, 8768, 8769, 0, 0, 0, 0, 8770]], + [20621, [8771, 0, 0, 0, 8772]], + [20632, [8773, 8774]], + [20643, [8775, 0, 0, 0, 0, 8776, 8777, 0, 0, 8778, 8779, 0, 0, 0, 0, 8780, 8781, 0, 8782, 0, 8783, 0, 0, 0, 8784, 0, 0, 8785]], + [20677, [8786]], + [20685, [8787, 0, 8788, 0, 8789, 0, 0, 0, 8790, 0, 0, 0, 0, 8791, 0, 0, 0, 8792]], + [20711, [8793]], + [20717, [8794]], + [20725, [8795, 0, 0, 0, 8796, 0, 8797, 0, 0, 0, 0, 8798, 0, 0, 0, 8799, 0, 0, 0, 0, 8800]], + [20754, [8801]], + [20767, [8802, 0, 8803]], + [20778, [8804]], + [20786, [8805]], + [20799, [8806, 8807, 8808, 0, 8809, 8810, 8811, 8812, 0, 8813, 8814, 0, 8815, 0, 8816, 8817, 0, 8818, 8819, 8820, 0, 8821]], + [20826, [8822, 0, 8823]], + [20834, [8824, 0, 0, 8825, 0, 0, 8826, 0, 0, 8827, 8828, 8829, 8830, 0, 8831, 8832, 0, 8833, 8834, 8835, 8836, 8837, 8838, 8839, 0, 8840, 8841, 8842, 0, 0, 8843, 0, 0, 0, 0, 8844, 8845, 0, 8846, 8847, 8848, 0, 8849, 8850, 0, 0, 0, 0, 8851, 0, 0, 8852, 0, 8853, 0, 8854, 0, 8855, 8856, 0, 0, 0, 8857, 0, 8858, 0, 8859, 8860, 0, 0, 8861, 0, 0, 0, 8862, 0, 0, 8863, 8864, 0, 8865, 8866, 8867, 8868, 8869, 8870, 0, 0, 0, 8871, 0, 0, 0, 0, 8872, 0, 0, 0, 8873, 0, 8874, 0, 0, 8875, 0, 8876, 8877, 8878, 0, 8879, 0, 8880]], + [20955, [8881, 8882, 8883, 0, 0, 8884, 8885, 0, 0, 8886, 0, 8887, 8888, 0, 0, 8889, 0, 0, 8890, 0, 8891, 8892, 8893, 0, 8894, 0, 0, 8895, 0, 8896, 8897, 8898, 8899, 0, 8900, 0, 8901, 8902, 8903, 0, 8904, 0, 0, 8905, 8906, 8907, 0, 8908]], + [21009, [8909, 8910, 0, 0, 0, 0, 8911, 8912, 8913, 8914, 8915, 0, 8916, 0, 0, 8917, 0, 0, 0, 8918, 8919, 0, 0, 8920, 8921, 0, 8922, 0, 0, 8923, 0, 8924]], + [21046, [8925, 8926, 8927, 8928, 8929, 8930, 0, 8931, 0, 0, 0, 0, 8932, 8933, 0, 0, 0, 8934, 0, 0, 8935, 0, 8936, 8937, 0, 0, 0, 8938, 0, 0, 8939, 0, 8940, 0, 0, 0, 0, 8941]], + [21091, [8942, 8943, 8944, 0, 8945, 0, 8946, 8947, 0, 0, 0, 0, 8948, 8949, 0, 8950, 0, 0, 8951]], + [21119, [8952]], + [21127, [8953, 8954, 8955, 0, 0, 0, 8956]], + [21147, [8957, 0, 8958, 8959, 8960, 8961, 8962, 0, 8963, 0, 0, 0, 0, 8964, 8965, 8966, 8967]], + [21169, [8968, 8969, 8970, 8971, 0, 0, 0, 0, 8972, 0, 0, 0, 0, 8973, 8974, 0, 8975, 0, 8976, 0, 8977, 0, 8978, 0, 8979, 0, 8980]], + [21202, [8981, 0, 0, 8982, 0, 0, 8983, 8984, 0, 0, 0, 8985, 0, 8986, 0, 0, 8987, 0, 8988, 0, 0, 8989]], + [21234, [8990, 8991]], + [21242, [8992, 0, 0, 0, 8993, 8994, 8995, 8996, 8997, 0, 0, 8998, 8999, 0, 9000]], + [21269, [9001, 9002, 9003, 0, 9004]], + [21280, [9005, 9006, 0, 9007]], + [21290, [9008, 0, 0, 0, 9009]], + [21305, [9010, 9011, 9012, 0, 0, 9013, 9014, 0, 9015, 0, 9016, 0, 0, 0, 9017, 9018, 9019, 9020, 0, 0, 8209, 9021, 9022, 8208, 9023, 9024, 9025, 9026, 9027, 9028, 9029, 9030, 0, 9031, 0, 9032, 0, 0, 0, 9033, 9034, 9035, 0, 9036, 0, 9037, 9038, 0, 0, 0, 9039, 0, 0, 0, 9040, 9041, 9042, 0, 9043, 9044, 9045, 0, 9046, 9047]], + [21375, [9048, 0, 0, 9049, 0, 9050, 9051, 9052, 0, 0, 9053, 0, 9054, 9055]], + [21397, [9056, 0, 0, 9057, 0, 9058, 0, 0, 0, 0, 9059, 0, 0, 9060, 0, 0, 9061, 9062, 0, 9063, 9064, 0, 0, 0, 0, 9065, 0, 0, 0, 0, 9066]], + [21435, [9067, 0, 0, 0, 9068, 0, 0, 9069]], + [21448, [9070, 9071, 9072, 9073, 9074, 9075, 9076, 0, 0, 9077, 0, 0, 9078, 0, 9079, 9080, 9081, 9082, 0, 9083, 0, 0, 0, 9084, 9085, 9086, 9087, 9088, 9089, 9090, 9091, 0, 9092, 9093, 9094, 9095, 9096, 9097, 9098, 9099, 9100, 9101, 9102, 9103, 0, 0, 9104, 9105, 9106, 9107, 0, 0, 9108, 9109, 0, 0, 0, 9110, 0, 9111, 9112, 0, 9113, 0, 9114, 9115, 9116, 0, 9117, 9118, 9119, 9120, 9121, 9122, 0, 9123, 0, 9124, 0, 9125, 0, 0, 0, 9126, 0, 9127, 9128, 9129, 9130]], + [21542, [9131, 9132, 9133, 9134, 0, 9135, 9136, 9137, 9138, 9139, 0, 9140, 0, 0, 9141, 9142, 0, 0, 9143, 9144, 0, 9145, 9146, 0, 9147, 0, 9148, 0, 9149, 9150, 0, 0, 9151, 0, 9152, 9153, 9154]], + [21584, [9155, 0, 0, 0, 0, 9156, 0, 9157, 9158, 0, 0, 9159, 9160, 0, 0, 9161, 0, 0, 9162]], + [21608, [9163, 0, 9164]], + [21617, [9165, 0, 9166, 0, 9167, 0, 0, 9168, 0, 0, 9169, 9170, 9171, 0, 0, 9172, 0, 0, 0, 9173, 0, 9174, 0, 0, 0, 0, 9175, 9176, 0, 9177, 9178, 9179, 0, 9180, 0, 9181, 9182, 9183, 0, 0, 9184, 9185]], + [21670, [9186, 9187, 9188, 0, 9189, 0, 9190, 0, 0, 9191, 0, 9192, 9193, 9194, 0, 0, 0, 0, 9195, 0, 0, 0, 0, 9196, 0, 0, 9197, 9198, 0, 0, 9199, 0, 9200, 9201, 9202, 9203, 0, 0, 0, 9204, 9205, 0, 0, 9206, 9207, 0, 0, 0, 0, 9208]], + [21725, [9209, 0, 9210, 0, 9211, 0, 0, 0, 9212, 9213, 9214, 9215, 9216, 9217, 0, 0, 9218, 9219, 0, 0, 0, 9220]], + [21754, [9221, 0, 9222, 9223]], + [21764, [9224, 0, 9225, 9226, 0, 9227]], + [21776, [9228, 0, 0, 0, 9229]], + [21792, [9230, 0, 0, 0, 9231, 0, 0, 9232, 0, 0, 0, 0, 9233, 0, 9234, 9235, 0, 9236]], + [21815, [9237]], + [21822, [9238, 0, 0, 0, 0, 9239, 9240, 0, 9241, 0, 0, 0, 9242, 0, 0, 0, 0, 9243, 0, 0, 0, 9244]], + [21857, [9245, 0, 0, 9246, 9247, 9248, 9249, 0, 0, 9250, 0, 9251]], + [21880, [9252, 0, 0, 0, 9253, 0, 0, 0, 9254, 0, 9255, 9256, 9257, 0, 0, 9258, 0, 9259, 9260, 9261]], + [21908, [9262, 0, 0, 0, 9263, 9264, 9265, 0, 9266, 9267]], + [21927, [9268, 0, 9269, 9270, 9271, 9272, 0, 9273, 0, 9274, 0, 0, 9275, 0, 0, 9276, 9277, 0, 0, 0, 9278, 0, 9279]], + [21957, [9280]], + [21970, [9281, 9282, 9283, 0, 9284]], + [21980, [9285, 0, 0, 9286, 0, 9287, 0, 9288, 9289, 0, 9290, 0, 9291]], + [21999, [9292, 0, 0, 0, 9293]], + [22013, [9294, 0, 0, 9295]], + [22022, [9296, 0, 9297, 9298, 0, 0, 0, 0, 9299]], + [22039, [9300, 9301, 0, 0, 9302, 0, 0, 0, 9303]], + [22057, [9304]], + [22065, [9305, 9306, 0, 9307, 0, 9308, 0, 0, 0, 0, 9309, 0, 0, 0, 9310, 0, 0, 9311]], + [22092, [9312, 0, 9313]], + [22103, [9314, 0, 0, 0, 9315, 9316]], + [22114, [9317]], + [22120, [9318, 9319, 9320, 0, 9321]], + [22132, [9322, 0, 9323]], + [22151, [9324]], + [22158, [9325, 9326, 0, 0, 0, 9327]], + [22179, [9328]], + [22196, [9329, 0, 0, 9330, 0, 0, 0, 0, 9331]], + [22218, [9332]], + [22234, [9333, 9334, 0, 0, 9335, 0, 9336, 0, 9337, 9338]], + [22253, [9339, 0, 0, 9340, 9341, 9342, 9343, 9344]], + [22266, [9345, 0, 0, 9346, 9347, 0, 0, 0, 0, 9348, 0, 0, 9349, 0, 9350, 0, 0, 9351, 0, 0, 0, 9352, 0, 0, 9353, 9354, 0, 0, 0, 0, 9355]], + [22303, [9356, 0, 0, 0, 9357, 0, 0, 0, 9358, 9359, 0, 0, 0, 0, 9360, 0, 0, 9361, 0, 0, 9362]], + [22330, [9363, 0, 0, 0, 9364, 0, 9365, 0, 9366, 0, 0, 0, 0, 9367, 0, 0, 9368, 0, 0, 9369, 9370, 9371, 9372, 9373]], + [22359, [9374, 0, 0, 9375, 9376, 0, 9377, 9378, 9379, 9380, 9381, 0, 0, 9382, 0, 9383, 0, 0, 0, 9384, 0, 0, 0, 0, 9385]], + [22391, [9386]], + [22402, [9387, 9388, 9389]], + [22411, [9390]], + [22418, [9391]], + [22434, [9392, 9393, 0, 0, 9394, 0, 0, 0, 0, 9395, 0, 0, 9396]], + [22467, [9397]], + [22475, [9398, 0, 0, 9399]], + [22484, [9400]], + [22492, [9401, 0, 0, 9402, 9403]], + [22516, [9404, 0, 0, 9405, 0, 9406, 9407, 0, 9408, 0, 0, 0, 9409, 0, 9410, 0, 0, 9411, 9412]], + [22545, [9413, 0, 0, 0, 9414]], + [22561, [9415, 0, 0, 9416]], + [22570, [9417, 0, 0, 0, 0, 9418, 9419, 9420, 0, 0, 9421, 9422, 0, 0, 0, 0, 9423]], + [22592, [9424, 9425]], + [22602, [9426, 0, 9427, 0, 0, 0, 0, 9428, 0, 0, 9429, 0, 0, 9430, 9431, 9432, 9433, 0, 0, 0, 9434]], + [22633, [9435, 0, 9436]], + [22654, [9437, 0, 0, 0, 0, 9438, 0, 9439]], + [22675, [9440, 0, 0, 0, 9441, 0, 9442, 0, 0, 9443, 0, 9444, 9445]], + [22696, [9446, 9447]], + [22707, [9448]], + [22718, [9449, 0, 0, 9450]], + [22727, [9451, 0, 0, 9452, 0, 9453, 0, 0, 0, 0, 9454, 0, 0, 0, 9455, 0, 0, 9456]], + [22750, [9457]], + [22756, [9458]], + [22763, [9459, 9460, 0, 9461, 9462, 9463, 9464, 9465, 9466, 0, 0, 9467, 9468]], + [22781, [9469]], + [22788, [9470, 0, 0, 9471, 0, 9472, 0, 0, 0, 9473, 0, 9474]], + [22805, [9475, 9476, 0, 0, 0, 9477, 0, 9478, 0, 0, 9479, 0, 0, 9480, 0, 0, 0, 0, 9481, 0, 9482, 9483, 9484, 0, 9485, 9486, 0, 0, 9487, 0, 0, 9488, 0, 0, 9489, 9490, 9491, 9492]], + [22850, [9493, 0, 9494, 0, 0, 9495, 9496, 9497, 0, 9498, 0, 0, 9499, 9500, 0, 9501, 0, 0, 9502, 9503, 9504, 9505, 0, 0, 9506]], + [22880, [9507, 0, 9508, 0, 0, 9509, 0, 9510, 9511, 0, 9512, 0, 9513, 0, 9514, 0, 0, 0, 0, 9515, 9516, 0, 9517, 0, 9518, 9519, 0, 0, 0, 9520, 0, 0, 0, 0, 9521, 9522, 9523, 0, 9524, 9525, 9526, 0, 9527]], + [22930, [9528, 9529, 0, 0, 9530, 0, 0, 9531, 0, 0, 0, 0, 9532]], + [22949, [9533, 0, 0, 9534, 0, 0, 0, 9535, 0, 9536]], + [22969, [9537, 0, 9538, 0, 0, 9539]], + [22982, [9540, 0, 0, 9541, 9542, 9543, 0, 0, 0, 0, 9544, 9545, 0, 9546, 9547, 0, 0, 9548, 0, 0, 9549, 0, 9550]], + [23013, [9551, 9552, 0, 9553, 0, 9554, 9555, 9556]], + [23035, [9557, 0, 0, 0, 9558, 0, 9559, 0, 9560, 0, 9561, 0, 9562]], + [23059, [9563, 0, 0, 0, 0, 9564, 0, 0, 0, 9565, 0, 0, 9566, 9567, 0, 0, 0, 0, 9568, 0, 0, 0, 9569]], + [23087, [9570, 0, 9571, 0, 0, 9572, 0, 9573]], + [23100, [9574]], + [23110, [9575, 0, 0, 9576]], + [23130, [9577]], + [23138, [9578, 0, 0, 0, 9579, 0, 0, 0, 9580]], + [23156, [9581, 9582, 9583, 9584]], + [23167, [9585]], + [23186, [9586]], + [23194, [9587, 9588]], + [23218, [9589, 9590]], + [23233, [9591, 9592]], + [23241, [9593, 0, 0, 9594]], + [23252, [9595, 0, 9596]], + [23265, [9597, 0, 9598, 0, 0, 9599, 0, 0, 9600]], + [23305, [9601]], + [23330, [9602]], + [23363, [9603]], + [23376, [9604, 0, 0, 0, 9605, 9606, 0, 9607, 9608, 9609, 9610, 0, 9611, 9612, 0, 9613, 0, 0, 0, 9614, 9615, 0, 9616, 0, 0, 9617, 0, 9618, 0, 0, 0, 0, 9619, 0, 0, 0, 0, 9620]], + [23421, [9621, 0, 0, 0, 9622, 0, 9623, 0, 9624, 0, 9625, 9626, 9627, 0, 9628, 9629, 0, 0, 9630]], + [23445, [9631, 0, 9632, 9633, 9634, 9635, 9636, 9637, 9638, 9639, 9640, 9641, 9642, 9643, 9644, 9645, 9646, 9647, 0, 0, 0, 9648, 9649, 0, 0, 9650, 0, 9651, 0, 0, 9652, 9653, 9654, 9655, 0, 0, 9656, 0, 0, 0, 9657, 9658, 9659, 0, 0, 9660, 0, 9661, 9662, 9663, 9664, 0, 0, 0, 0, 9665, 0, 0, 0, 9666, 0, 9667, 9668]], + [23515, [9669, 0, 9670, 9671, 9672, 0, 9673, 9674, 0, 0, 9675, 9676, 9677, 9678, 9679, 0, 0, 0, 0, 9680, 0, 9681, 0, 0, 0, 0, 9682, 0, 0, 9683, 9684, 9685, 9686, 9687, 0, 9688, 9689, 0, 9690, 9691, 0, 9692, 0, 9693, 9694, 9695, 9696, 9697, 9698, 0, 0, 9699, 9700, 0, 9701, 0, 0, 9702, 0, 9703, 0, 9704, 0, 9705, 0, 0, 9706]], + [23588, [9707, 0, 0, 9708, 0, 0, 0, 0, 9709, 9710, 0, 0, 0, 9711, 0, 0, 9712, 0, 0, 0, 9713, 9714, 9715, 9716, 9717, 9718, 9719, 9720, 9721, 9722, 9723, 0, 0, 9724, 0, 0, 9725, 9726, 9727, 9728, 0, 0, 9729, 9730, 0, 9731, 0, 0, 0, 9732]], + [23646, [9733, 0, 9734, 9735, 0, 0, 9736, 9737]], + [23663, [9738, 0, 9739]], + [23673, [9740]], + [23679, [9741, 0, 9742, 9743]], + [23696, [9744, 9745, 0, 0, 9746, 0, 0, 9747, 0, 0, 9748, 9749]], + [23713, [9750]], + [23721, [9751, 0, 0, 9752, 9753]], + [23731, [9754, 0, 0, 0, 0, 9755]], + [23769, [9756]], + [23776, [9757, 9758, 0, 0, 0, 0, 9759, 0, 9760, 0, 0, 0, 0, 9761, 0, 9762, 9763]], + [23798, [9764, 0, 0, 0, 0, 9765, 0, 9766]], + [23815, [9767]], + [23822, [9768]], + [23828, [9769, 0, 9770, 0, 0, 0, 9771, 9772]], + [23849, [9773, 0, 0, 0, 9774]], + [23869, [9775]], + [23883, [9776, 9777, 0, 0, 0, 9778]], + [23913, [9779]], + [23919, [9780]], + [23947, [9781]], + [23994, [9782]], + [24005, [9783]], + [24012, [9784, 9785]], + [24022, [9786]], + [24029, [9787, 9788, 0, 0, 9789, 9790, 9791, 0, 9792, 9793, 9794, 9795, 9796, 0, 9797, 0, 0, 9798, 0, 0, 9799, 9800, 9801, 9802, 0, 0, 9803, 0, 0, 0, 9804, 0, 9805, 9806, 0, 0, 9807, 9808, 9809, 0, 9810, 9811, 0, 9812, 0, 0, 0, 9813, 0, 0, 0, 9814, 0, 0, 0, 0, 9815, 9816, 0, 9817, 0, 9818, 9819, 9820, 9821]], + [24101, [9822, 9823, 9824, 0, 0, 0, 9825, 0, 9826, 9827, 9828, 9829, 0, 0, 9830, 0, 0, 9831, 9832, 9833, 0, 0, 0, 0, 9834]], + [24133, [9835]], + [24140, [9836]], + [24148, [9837, 9838]], + [24161, [9839, 9840, 9841]], + [24178, [9842, 9843, 9844, 0, 9845, 0, 9846, 9847, 0, 9848, 9849, 9850, 9851, 9852, 0, 9853, 0, 9854, 9855, 0, 9856, 9857, 0, 0, 9858, 0, 0, 0, 0, 9859, 9860, 0, 0, 9861, 9862, 9863, 0, 9864, 0, 9865, 9866, 0, 9867, 0, 9868, 9869]], + [24230, [9870, 9871, 0, 0, 0, 9872, 0, 9873]], + [24245, [9874, 9875, 9876, 9877]], + [24259, [9878]], + [24265, [9879, 9880]], + [24275, [9881, 0, 0, 9882]], + [24287, [9883, 0, 0, 0, 9884]], + [24307, [9885, 0, 0, 9886, 9887, 0, 0, 9888, 9889, 0, 0, 0, 9890, 9891, 9892, 9893, 9894, 9895]], + [24330, [9896, 0, 0, 0, 0, 9897, 9898, 0, 0, 9899, 9900, 9901, 0, 9902, 9903, 0, 0, 9904, 0, 0, 0, 9905, 9906, 0, 0, 0, 0, 9907, 9908, 9909, 0, 9910]], + [24367, [9911, 0, 9912, 0, 0, 0, 9913, 0, 9914, 0, 9915, 9916, 0, 9917, 0, 9918]], + [24392, [9919, 0, 0, 0, 9920]], + [24402, [9921, 9922, 0, 9923, 0, 9924, 0, 9925, 0, 0, 0, 9926, 0, 0, 0, 0, 9927, 0, 0, 0, 9928, 0, 0, 9929, 9930, 9931, 9932, 9933, 0, 0, 9934, 9935]], + [24439, [9936, 0, 9937, 0, 9938, 9939, 0, 0, 0, 9940, 9941, 0, 0, 9942, 9943, 0, 0, 9944, 0, 9945, 9946, 9947, 0, 0, 0, 9948, 0, 9949, 9950, 0, 0, 0, 9951, 9952, 9953, 0, 0, 0, 0, 9954, 0, 9955, 9956]], + [24489, [9957, 9958, 0, 0, 0, 9959, 0, 0, 0, 0, 9960, 9961, 0, 0, 9962, 0, 9963, 0, 0, 0, 9964]], + [24515, [9965, 0, 9966, 9967]], + [24524, [9968, 9969, 0, 9970, 0, 0, 9971, 0, 0, 0, 9972, 9973, 9974, 9975, 0, 0, 9976, 0, 0, 0, 9977, 9978]], + [24551, [9979, 0, 0, 0, 9980]], + [24561, [9981, 0, 0, 0, 9982]], + [24573, [9983, 0, 9984, 9985, 9986, 9987, 0, 0, 9988]], + [24590, [9989, 0, 0, 0, 9990, 0, 9991, 9992, 9993]], + [24604, [9994, 9995, 0, 0, 9996, 9997, 0, 0, 0, 9998, 9999, 10000, 10001, 0, 10002, 0, 0, 0, 0, 10003]], + [24635, [10004]], + [24643, [10005, 0, 0, 10006, 0, 0, 0, 0, 10007, 0, 10008, 0, 0, 10009, 0, 10010, 0, 0, 10011, 0, 0, 0, 10012, 10013]], + [24674, [10014, 10015, 10016, 10017, 0, 0, 10018, 10019, 10020, 0, 10021, 10022, 0, 10023, 10024, 0, 0, 10025, 0, 10026, 10027, 0, 0, 0, 10028, 10029, 10030, 0, 0, 10031, 0, 0, 0, 0, 10032, 0, 0, 0, 0, 10033, 0, 0, 10034, 10035]], + [24724, [10036, 0, 10037, 0, 0, 0, 10038, 0, 0, 0, 0, 10039, 10040, 0, 0, 10041, 0, 0, 10042, 0, 10043, 10044, 10045, 0, 10046, 0, 0, 10047, 0, 0, 10048, 0, 10049, 0, 0, 0, 10050, 0, 0, 10051, 10052]], + [24773, [10053, 0, 10054, 0, 0, 10055, 10056]], + [24785, [10057, 0, 0, 0, 10058, 0, 0, 10059, 0, 10060, 0, 10061, 0, 0, 10062, 10063, 10064, 0, 10065, 0, 0, 10066, 10067, 10068, 10069, 0, 10070, 10071, 10072, 10073, 10074, 10075, 0, 0, 10076, 10077, 0, 10078, 0, 0, 10079, 10080]], + [24833, [10081]], + [24840, [10082, 10083]], + [24847, [10084]], + [24853, [10085, 0, 0, 0, 0, 10086, 10087, 0, 0, 0, 10088, 0, 0, 0, 10089, 10090, 0, 0, 10091, 0, 0, 0, 10092]], + [24892, [10093, 0, 0, 10094, 0, 0, 0, 0, 10095, 0, 0, 0, 10096, 0, 0, 10097, 10098, 0, 10099, 0, 0, 10100, 0, 0, 0, 10101]], + [24930, [10102, 10103, 0, 0, 0, 10104, 10105]], + [24942, [10106, 0, 10107]], + [24950, [10108, 10109]], + [24962, [10110]], + [24971, [10111, 0, 0, 10112, 0, 10113, 0, 0, 0, 10114]], + [24986, [10115]], + [24996, [10116, 0, 0, 10117, 10118, 10119, 0, 0, 10120]], + [25010, [10121, 0, 0, 0, 10122]], + [25022, [10123, 0, 0, 0, 10124, 0, 0, 0, 0, 10125, 10126, 10127, 10128]], + [25040, [10129, 0, 10130]], + [25062, [10131]], + [25074, [10132, 0, 0, 10133, 0, 10134, 10135]], + [25087, [10136]], + [25096, [10137, 0, 10138, 0, 10139, 10140, 10141, 10142, 10143, 10144, 10145, 0, 0, 0, 10146, 0, 10147, 0, 10148, 0, 0, 0, 0, 10149]], + [25126, [10150, 0, 0, 0, 10151, 0, 0, 0, 10152, 10153, 10154, 0, 10155, 10156, 10157, 0, 0, 10158, 10159, 0, 0, 10160, 0, 0, 0, 10161, 10162, 10163]], + [25159, [10164, 10165, 10166, 0, 10167, 0, 10168, 10169, 0, 0, 10170, 10171, 10172, 10173, 10174, 0, 0, 10175, 0, 0, 10176]], + [25187, [10177, 0, 0, 0, 10178, 0, 10179, 0, 10180, 10181, 10182, 10183, 10184, 10185, 10186, 0, 10187, 0, 0, 10188, 0, 0, 10189, 0, 0, 10190, 0, 10191, 10192, 10193, 0, 0, 0, 10194, 0, 0, 0, 0, 10195, 10196]], + [25233, [10197, 10198, 10199, 0, 10200, 10201, 10202, 10203, 0, 10204, 10205, 10206, 0, 10207, 0, 0, 10208, 10209, 0, 10210, 10211, 0, 0, 10212, 0, 0, 10213, 10214, 0, 0, 0, 0, 10215, 0, 0, 0, 10216, 0, 0, 0, 10217, 0, 0, 10218, 10219, 0, 10220, 0, 0, 10221, 0, 0, 10222, 10223, 10224, 10225, 10226, 0, 0, 10227, 10228, 10229, 0, 10230, 0, 10231, 10232, 10233, 0, 10234, 10235, 10236, 10237, 10238, 10239, 10240, 10241, 0, 10242, 10243, 10244, 10245, 10246, 0, 10247, 10248, 10249, 10250, 10251, 0, 0, 10252, 10253, 0, 10254, 0, 10255, 0, 10256, 10257, 0, 10258, 10259, 0, 0, 0, 0, 10260, 10261, 10262, 10263, 0, 10264, 10265, 0, 0, 0, 0, 10266, 0, 10267]], + [25361, [10268, 0, 0, 0, 0, 10269, 0, 0, 10270, 10271, 10272, 0, 10273, 0, 10274, 10275, 10276, 0, 10277, 10278, 10279, 0, 0, 10280, 0, 10281, 10282, 0, 0, 0, 10283]], + [25402, [10284, 0, 0, 10285, 0, 10286, 0, 0, 10287, 0, 0, 10288, 10289, 0, 0, 10290, 0, 0, 0, 10291, 10292, 10293, 10294, 0, 0, 0, 0, 10295, 0, 10296, 0, 0, 0, 0, 10297, 0, 10298, 10299, 0, 10300, 10301, 10302, 0, 0, 0, 10303, 10304]], + [25454, [10305, 0, 0, 0, 10306, 0, 0, 0, 10307, 10308, 0, 0, 10309, 10310, 0, 0, 0, 0, 10311, 0, 0, 10312, 0, 0, 0, 0, 10313, 10314, 0, 0, 10315, 0, 0, 10316, 10317, 0, 10318, 0, 0, 0, 10319, 0, 10320, 0, 0, 10321, 0, 0, 0, 0, 10322, 10323, 10324, 10325, 0, 10326, 0, 10327, 10328, 10329, 10330, 0, 10331]], + [25522, [10332, 10333, 10334, 0, 0, 10335, 0, 0, 10336, 10337]], + [25539, [10338]], + [25545, [10339, 0, 0, 0, 10340, 0, 10341, 10342, 0, 10343, 0, 0, 0, 10344, 0, 0, 0, 10345, 10346]], + [25569, [10347, 0, 10348]], + [25577, [10349, 10350, 0, 0, 10351, 10352]], + [25588, [10353]], + [25594, [10354, 0, 0, 10355, 0, 0, 10356, 10357, 10358, 0, 0, 10359]], + [25613, [10360, 0, 10361, 10362, 0, 0, 10363, 10364, 0, 10365]], + [25628, [10366, 0, 10367]], + [25644, [10368, 10369]], + [25658, [10370, 0, 0, 0, 10371, 0, 0, 0, 10372, 0, 10373, 0, 10374, 10375, 0, 0, 10376]], + [25681, [10377, 10378, 0, 10379, 0, 0, 0, 10380]], + [25703, [10381, 0, 10382]], + [25711, [10383]], + [25720, [10384, 10385, 10386]], + [25730, [10387, 10388, 0, 0, 0, 10389]], + [25745, [10390, 10391, 0, 0, 10392, 0, 0, 0, 0, 10393, 0, 0, 0, 10394]], + [25764, [10395, 0, 0, 0, 0, 10396, 0, 10397, 10398, 10399, 10400, 0, 10401, 0, 10402, 0, 0, 10403, 0, 0, 10404, 0, 0, 0, 10405, 0, 0, 0, 0, 10406, 10407, 0, 0, 10408, 0, 0, 0, 0, 10409, 0, 0, 10410, 10411, 0, 0, 0, 10412]], + [25822, [10413, 0, 0, 0, 10414, 0, 0, 0, 10415]], + [25836, [10416]], + [25856, [10417]], + [25874, [10418]], + [25880, [10419, 0, 0, 0, 0, 10420]], + [25893, [10421]], + [25899, [10422, 0, 0, 0, 10423]], + [25910, [10424, 0, 10425, 10426, 0, 10427, 0, 0, 10428, 10429]], + [25925, [10430, 0, 0, 10431, 0, 0, 0, 10432, 10433, 0, 10434, 0, 10435, 0, 0, 0, 10436, 10437, 10438, 0, 10439, 0, 10440, 0, 10441, 10442, 0, 0, 0, 10443, 10444, 0, 0, 10445]], + [25964, [10446, 0, 0, 0, 10447, 0, 10448, 0, 10449, 10450, 0, 10451]], + [25991, [10452, 0, 10453, 0, 10454, 10455, 0, 10456, 0, 10457, 10458, 0, 10459, 0, 0, 0, 10460, 0, 10461, 0, 0, 10462, 0, 0, 10463, 0, 10464, 0, 0, 10465, 10466, 0, 10467, 0, 10468, 0, 0, 10469, 10470, 0, 10471, 10472]], + [26041, [10473, 0, 0, 10474, 10475, 0, 0, 0, 10476, 0, 0, 0, 10477]], + [26059, [10478, 0, 0, 0, 10479]], + [26071, [10480]], + [26080, [10481, 0, 10482, 0, 0, 10483, 10484, 10485, 10486, 10487, 0, 0, 10488, 10489, 0, 0, 0, 10490, 0, 0, 0, 0, 10491, 10492, 0, 0, 10493]], + [26114, [10494, 0, 0, 0, 10495, 10496, 0, 0, 10497, 0, 10498, 0, 10499, 10500, 0, 0, 0, 10501, 10502]], + [26143, [10503, 10504, 0, 0, 0, 0, 10505, 0, 10506, 10507, 0, 0, 0, 0, 10508, 0, 10509, 0, 0, 0, 0, 10510]], + [26172, [10511, 0, 10512, 0, 0, 0, 10513, 10514, 10515]], + [26187, [10516, 10517, 0, 0, 10518, 0, 0, 10519, 10520, 0, 10521, 10522, 0, 0, 0, 10523, 0, 0, 10524, 0, 10525, 0, 0, 0, 0, 10526, 0, 10527, 0, 10528, 10529, 0, 0, 0, 0, 10530, 10531, 10532, 0, 0, 0, 10533, 0, 10534, 0, 0, 0, 10535, 0, 0, 0, 10536, 0, 0, 10537, 10538, 0, 10539, 0, 0, 10540, 0, 10541]], + [26257, [10542, 0, 0, 0, 0, 10543, 10544]], + [26274, [10545, 0, 0, 0, 10546, 10547, 10548, 0, 0, 10549, 0, 0, 10550]], + [26292, [10551]], + [26311, [10552, 0, 10553]], + [26326, [10554, 0, 0, 10555, 0, 0, 10556, 10557]], + [26342, [10558]], + [26352, [10559, 0, 10560, 10561, 10562, 0, 0, 0, 10563, 10564, 0, 0, 10565, 10566, 10567, 10568, 10569]], + [26376, [10570, 10571, 0, 10572, 0, 10573]], + [26388, [10574, 10575, 0, 10576, 0, 0, 0, 10577, 0, 10578, 0, 10579]], + [26406, [10580, 0, 10581, 0, 10582, 10583, 10584, 10585, 0, 10586, 0, 10587, 0, 0, 10588, 10589, 0, 0, 0, 0, 10590, 0, 0, 10591, 0, 0, 10592, 0, 10593, 10594, 0, 0, 10595, 0, 0, 10596, 0, 0, 0, 0, 10597, 10598, 10599, 10600, 0, 0, 0, 0, 10601]], + [26460, [10602, 0, 10603, 10604, 10605, 10606, 0, 0, 0, 10607, 0, 0, 10608, 0, 0, 0, 0, 10609, 0, 10610, 10611, 10612, 0, 0, 0, 10613, 0, 10614]], + [26494, [10615, 10616, 0, 10617, 0, 0, 10618, 0, 0, 10619, 0, 10620]], + [26512, [10621, 0, 0, 0, 0, 10622, 0, 10623, 0, 0, 10624, 0, 10625, 10626, 0, 0, 10627, 0, 10628, 10629]], + [26538, [10630, 10631, 0, 10632, 0, 10633]], + [26550, [10634, 10635]], + [26564, [10636]], + [26570, [10637, 0, 0, 0, 0, 10638, 10639, 10640, 0, 10641, 10642, 0, 0, 0, 10643, 0, 10644, 0, 10645]], + [26597, [10646]], + [26604, [10647, 0, 0, 10648, 0, 10649, 0, 10650, 10651, 10652]], + [26619, [10653, 0, 0, 10654, 10655, 0, 0, 0, 10656, 10657, 10658, 0, 10659, 10660, 0, 0, 10661, 0, 0, 0, 10662, 0, 10663, 0, 10664, 0, 0, 10665, 10666]], + [26654, [10667, 0, 0, 10668]], + [26665, [10669, 10670]], + [26679, [10671, 10672, 10673, 0, 0, 10674, 10675, 0, 0, 0, 10676, 10677, 10678, 0, 10679, 10680, 0, 10681, 0, 0, 0, 10682, 0, 0, 0, 10683, 10684, 0, 10685, 10686]], + [26716, [10687, 0, 0, 10688, 0, 0, 10689, 10690, 0, 10691, 10692, 10693, 10694, 10695]], + [26742, [10696]], + [26753, [10697, 0, 0, 0, 10698]], + [26771, [10699, 0, 0, 0, 10700, 0, 0, 0, 10701, 0, 10702, 0, 0, 0, 0, 10703, 0, 0, 0, 10704, 10705, 10706, 0, 0, 0, 0, 10707, 0, 10708, 10709, 0, 0, 10710, 0, 10711, 10712]], + [26816, [10713, 0, 0, 0, 10714, 0, 0, 0, 0, 10715, 0, 10716, 0, 10717, 0, 0, 0, 0, 10718, 0, 0, 10719, 0, 0, 10720, 0, 10721, 0, 0, 0, 0, 10722, 10723]], + [26862, [10724, 0, 0, 10725, 10726, 0, 0, 10727, 0, 0, 0, 0, 10728]], + [26880, [10729, 0, 0, 0, 0, 10730]], + [26891, [10731, 0, 10732, 10733, 0, 0, 0, 10734]], + [26907, [10735, 10736]], + [26925, [10737, 0, 0, 10738]], + [26943, [10739]], + [26954, [10740]], + [26963, [10741, 10742, 10743, 0, 0, 0, 0, 10744, 0, 0, 0, 10745, 0, 10746, 0, 10747]], + [26989, [10748, 0, 10749]], + [26997, [10750, 0, 10751, 0, 0, 0, 0, 10752, 10753, 0, 0, 0, 0, 10754, 0, 10755, 0, 10756, 0, 10757, 0, 10758, 0, 0, 0, 10759]], + [27035, [10760, 10761]], + [27048, [10762]], + [27054, [10763]], + [27060, [10764]], + [27067, [10765]], + [27079, [10766, 0, 0, 0, 10767, 10768, 10769, 0, 0, 10770]], + [27096, [10771, 10772, 0, 10773]], + [27131, [10774, 0, 10775, 0, 0, 0, 0, 10776]], + [27146, [10777, 10778]], + [27161, [10779]], + [27167, [10780, 0, 10781, 0, 10782]], + [27177, [10783, 10784, 10785]], + [27185, [10786, 0, 0, 0, 10787, 0, 0, 0, 10788, 10789, 0, 0, 10790]], + [27204, [10791]], + [27211, [10792]], + [27224, [10793, 10794]], + [27231, [10795, 0, 10796]], + [27249, [10797]], + [27264, [10798]], + [27278, [10799, 0, 10800]], + [27292, [10801]], + [27298, [10802]], + [27308, [10803]], + [27330, [10804]], + [27347, [10805]], + [27355, [10806]], + [27387, [10807]], + [27396, [10808]], + [27424, [10809, 10810, 10811, 10812, 10813, 0, 0, 10814]], + [27442, [10815]], + [27450, [10816, 0, 0, 10817, 10818]], + [27463, [10819, 0, 10820, 0, 0, 10821, 0, 10822, 0, 0, 0, 0, 10823]], + [27490, [10824, 10825, 10826, 10827, 10828, 10829, 0, 10830, 10831, 0, 0, 0, 0, 10832, 0, 0, 0, 10833, 10834, 0, 0, 0, 0, 10835, 0, 10836, 10837]], + [27523, [10838, 0, 0, 10839, 0, 0, 10840, 10841, 10842]], + [27542, [10843]], + [27553, [10844]], + [27572, [10845, 10846, 0, 10847, 0, 0, 10848, 10849, 0, 0, 0, 10850, 0, 10851, 0, 0, 0, 10852]], + [27595, [10853, 0, 10854, 10855, 10856, 0, 0, 10857, 10858, 10859, 10860, 0, 10861, 10862, 10863, 0, 10864]], + [27617, [10865]], + [27627, [10866, 10867, 0, 0, 10868]], + [27663, [10869, 0, 10870, 0, 10871, 10872, 0, 10873, 10874, 0, 0, 0, 10875, 0, 0, 0, 10876, 0, 0, 10877, 10878, 0, 0, 10879, 10880, 10881]], + [27694, [10882, 10883, 0, 0, 0, 0, 10884, 0, 0, 10885, 10886]], + [27710, [10887, 0, 10888, 10889, 10890, 0, 0, 0, 0, 10891, 0, 10892, 0, 0, 0, 0, 10893, 0, 10894, 0, 0, 0, 0, 10895, 0, 10896, 0, 0, 10897, 10898, 0, 10899, 10900, 10901, 10902, 10903, 0, 0, 10904]], + [27754, [10905]], + [27760, [10906, 0, 10907, 0, 0, 0, 10908, 0, 0, 10909, 10910, 0, 0, 10911, 10912, 0, 0, 10913, 10914, 10915, 0, 0, 0, 0, 10916, 10917, 0, 0, 10918, 0, 0, 0, 10919, 0, 0, 10920, 0, 0, 10921, 0, 0, 10922, 0, 10923, 0, 0, 0, 10924, 0, 10925, 10926, 0, 0, 10927, 10928, 10929, 0, 0, 10930, 10931, 0, 0, 10932, 0, 0, 0, 0, 10933, 0, 0, 0, 0, 10934, 10935, 0, 10936, 10937, 10938, 10939, 10940, 0, 10941, 0, 0, 10942, 0, 0, 0, 0, 10943, 10944, 0, 10945]], + [27861, [10946]], + [27867, [10947, 0, 0, 10948, 0, 0, 10949, 10950, 10951, 0, 10952, 0, 0, 10953, 0, 10954]], + [27888, [10955, 0, 0, 10956, 0, 10957]], + [27899, [10958, 10959, 10960, 0, 0, 0, 10961]], + [27915, [10962]], + [27922, [10963, 0, 0, 0, 0, 10964, 0, 0, 0, 10965, 0, 0, 10966]], + [27941, [10967, 0, 0, 0, 0, 10968]], + [27954, [10969, 0, 0, 10970, 0, 0, 10971, 0, 0, 10972, 10973, 10974, 10975, 0, 0, 10976, 0, 0, 10977, 10978, 10979, 10980, 0, 0, 10981, 10982, 0, 0, 10983, 10984, 0, 10985, 10986, 10987]], + [27993, [10988, 10989, 0, 10990]], + [28006, [10991, 0, 0, 10992, 10993, 0, 10994, 0, 10995]], + [28020, [10996, 0, 0, 10997, 10998]], + [28034, [10999, 0, 0, 11000, 0, 0, 11001, 11002, 0, 0, 11003, 0, 11004]], + [28053, [11005, 0, 0, 0, 11006, 0, 11007, 0, 0, 0, 11008, 0, 11009, 0, 11010, 11011, 0, 11012, 11013, 11014, 11015]], + [28079, [11016, 0, 0, 11017, 0, 0, 11018, 0, 0, 11019, 0, 0, 0, 11020, 0, 0, 0, 11021, 0, 0, 0, 11022, 0, 11023, 0, 0, 0, 0, 11024, 11025, 0, 0, 0, 0, 11026, 0, 0, 0, 0, 11027, 0, 11028]], + [28129, [11029, 0, 0, 11030, 0, 0, 0, 11031, 0, 0, 11032, 0, 0, 11033, 0, 0, 11034, 0, 11035, 0, 11036, 0, 11037, 0, 11038, 0, 11039]], + [28165, [11040, 0, 11041, 11042, 11043, 11044, 11045, 0, 11046, 11047, 0, 11048, 0, 0, 11049, 11050, 0, 0, 11051, 0, 0, 11052, 11053, 0, 11054, 0, 0, 11055, 11056, 0, 11057, 11058, 11059, 11060, 0, 0, 11061, 0, 0, 11062, 11063, 0, 11064, 0, 0, 11065, 0, 11066, 0, 0, 0, 11067, 0, 11068]], + [28227, [11069, 11070]], + [28234, [11071, 0, 0, 11072]], + [28246, [11073, 0, 11074, 0, 0, 11075]], + [28263, [11076]], + [28271, [11077]], + [28286, [11078, 11079, 11080, 0, 0, 11081, 0, 11082, 0, 0, 0, 11083]], + [28304, [11084]], + [28310, [11085]], + [28316, [11086, 11087, 0, 0, 0, 0, 11088, 0, 0, 11089, 0, 0, 0, 0, 11090, 0, 0, 0, 0, 11091]], + [28342, [11092, 0, 0, 0, 11093]], + [28357, [11094, 0, 11095, 0, 11096, 0, 11097]], + [28369, [11098, 0, 0, 11099, 11100, 0, 0, 0, 0, 11101, 0, 0, 11102, 11103, 0, 0, 11104, 0, 0, 11105, 11106, 0, 0, 11107, 11108]], + [28399, [11109, 0, 0, 0, 0, 11110]], + [28417, [11111, 11112, 0, 0, 0, 11113, 0, 0, 11114]], + [28431, [11115, 0, 0, 0, 11116, 11117, 11118]], + [28448, [11119, 0, 11120, 11121]], + [28457, [11122, 11123, 11124, 11125, 0, 0, 0, 0, 11126, 0, 11127, 0, 0, 0, 0, 11128]], + [28478, [11129]], + [28487, [11130]], + [28500, [11131, 0, 0, 0, 11132, 0, 0, 0, 11133, 0, 0, 11134, 0, 0, 0, 0, 11135, 0, 11136]], + [28525, [11137, 11138, 0, 11139]], + [28545, [11140, 0, 0, 11141, 0, 0, 0, 11142]], + [28558, [11143]], + [28572, [11144, 0, 0, 0, 0, 11145]], + [28586, [11146]], + [28593, [11147, 0, 11148]], + [28608, [11149, 11150, 0, 11151]], + [28626, [11152, 0, 0, 11153]], + [28640, [11154, 11155]], + [28651, [11156, 0, 0, 0, 11157]], + [28689, [11158, 0, 0, 0, 11159, 0, 0, 0, 0, 11160, 11161]], + [28711, [11162, 0, 0, 0, 0, 11163]], + [28748, [11164]], + [28760, [11165]], + [28779, [11166, 0, 11167, 0, 11168, 11169, 0, 0, 0, 0, 11170, 11171, 0, 11172, 0, 0, 0, 11173, 11174, 11175, 11176]], + [28809, [11177, 11178, 0, 0, 0, 11179, 0, 0, 0, 11180, 0, 0, 11181, 11182, 0, 0, 11183]], + [28843, [11184, 11185, 11186, 11187, 11188, 0, 0, 0, 11189, 0, 0, 0, 0, 11190, 11191, 11192, 0, 11193, 11194, 0, 0, 0, 11195, 11196]], + [28872, [11197]], + [28879, [11198]], + [28888, [11199, 11200, 0, 11201, 0, 0, 0, 11202, 0, 0, 0, 0, 11203, 0, 11204, 11205, 0, 0, 0, 11206, 11207, 11208]], + [28921, [11209, 0, 0, 0, 11210]], + [28937, [11211]], + [28949, [11212, 0, 0, 11213, 11214, 11215]], + [28961, [11216, 0, 0, 0, 0, 11217]], + [28976, [11218]], + [28982, [11219]], + [28988, [11220]], + [29001, [11221, 0, 0, 11222, 0, 11223]], + [29017, [11224, 0, 0, 11225, 0, 11226]], + [29028, [11227, 0, 0, 11228, 0, 11229, 0, 0, 0, 0, 11230]], + [29053, [11231]], + [29060, [11232]], + [29066, [11233, 0, 0, 0, 0, 11234, 0, 0, 0, 0, 11235, 0, 0, 0, 0, 11236]], + [29087, [11237]], + [29100, [11238, 0, 0, 0, 0, 11239]], + [29113, [11240]], + [29123, [11241, 0, 0, 0, 0, 11242]], + [29134, [11243, 0, 0, 0, 11244, 0, 0, 11245]], + [29157, [11246, 11247]], + [29165, [11248]], + [29183, [11249]], + [29190, [11250]], + [29226, [11251, 0, 11252, 11253, 0, 0, 0, 11254, 11255, 0, 0, 11256, 11257, 11258, 11259, 11260, 0, 0, 0, 11261, 11262]], + [29255, [11263, 11264, 0, 0, 0, 11265]], + [29266, [11266]], + [29273, [11267, 0, 11268, 0, 0, 0, 11269, 0, 11270, 11271, 0, 0, 0, 0, 11272, 0, 11273]], + [29298, [11274, 0, 0, 11275, 0, 0, 0, 11276, 11277, 0, 0, 11278, 0, 0, 11279, 11280]], + [29322, [11281]], + [29344, [11282]], + [29356, [11283, 0, 0, 11284]], + [29366, [11285, 0, 0, 11286]], + [29378, [11287, 0, 11288, 0, 0, 0, 11289]], + [29392, [11290, 0, 11291, 0, 0, 0, 0, 11292, 0, 11293, 0, 0, 0, 0, 11294, 0, 11295, 11296]], + [29417, [11297, 0, 0, 11298, 11299, 11300, 0, 0, 11301]], + [29432, [11302, 11303, 0, 0, 11304]], + [29454, [11305, 0, 0, 0, 0, 11306, 0, 0, 11307, 0, 0, 0, 0, 11308, 11309, 11310, 0, 11311]], + [29481, [11312, 11313, 11314, 0, 0, 11315]], + [29492, [11316, 0, 11317]], + [29502, [11318, 11319, 0, 0, 0, 0, 11320, 11321]], + [29527, [11322]], + [29539, [11323]], + [29554, [11324]], + [29560, [11325]], + [29572, [11326, 0, 0, 11327, 0, 11328, 0, 11329]], + [29585, [11330, 0, 0, 0, 0, 11331, 0, 0, 0, 0, 11332]], + [29609, [11333, 0, 11334, 0, 0, 0, 11335, 11336, 0, 11337]], + [29626, [11338, 11339, 0, 0, 0, 0, 11340, 0, 11341]], + [29640, [11342, 0, 11343, 0, 0, 11344, 0, 0, 0, 11345]], + [29664, [11346, 0, 0, 0, 0, 11347]], + [29677, [11348]], + [29694, [11349, 0, 0, 0, 0, 11350, 0, 11351, 11352, 0, 0, 11353]], + [29711, [11354, 11355]], + [29723, [11356]], + [29730, [11357, 0, 0, 11358, 11359, 0, 0, 0, 11360]], + [29747, [11361, 11362, 11363, 11364]], + [29756, [11365]], + [29781, [11366, 0, 0, 0, 11367, 11368, 11369, 11370, 0, 11371, 11372, 11373]], + [29808, [11374, 0, 0, 11375, 0, 0, 11376]], + [29822, [11377, 0, 0, 0, 0, 11378, 0, 0, 0, 11379, 0, 0, 0, 11380, 0, 0, 0, 0, 11381]], + [29854, [11382]], + [29863, [11383]], + [29872, [11384]], + [29885, [11385]], + [29916, [11386]], + [29922, [11387, 11388, 0, 0, 11389]], + [29934, [11390]], + [29942, [11391, 11392]], + [29956, [11393]], + [29976, [11394, 0, 11395, 0, 11396, 0, 0, 11397, 0, 0, 0, 11398, 0, 11399, 0, 0, 11400, 11401, 0, 11402, 0, 11403, 0, 0, 11404, 11405, 11406, 11407, 0, 11408, 0, 11409, 11410, 0, 11411, 11412]], + [30021, [11413]], + [30028, [11414, 0, 0, 11415, 0, 11416, 0, 0, 11417, 0, 0, 0, 0, 11418, 0, 0, 11419, 11420, 0, 0, 11421, 0, 11422, 0, 0, 11423, 0, 0, 0, 0, 11424]], + [30064, [11425, 0, 0, 11426, 11427, 0, 0, 0, 11428]], + [30079, [11429]], + [30086, [11430, 0, 0, 0, 11431, 11432, 0, 0, 11433, 11434, 0, 11435]], + [30103, [11436, 0, 11437, 11438]], + [30113, [11439, 0, 11440, 11441]], + [30123, [11442, 0, 0, 11443, 11444, 0, 0, 11445, 0, 0, 11446, 0, 0, 0, 11447, 0, 0, 11448, 0, 11449]], + [30149, [11450, 0, 11451, 0, 11452, 11453]], + [30162, [11454, 0, 11455, 11456, 0, 0, 11457, 0, 0, 11458]], + [30178, [11459]], + [30185, [11460, 11461]], + [30192, [11462, 0, 0, 0, 11463, 0, 0, 0, 0, 11464]], + [30221, [11465]], + [30239, [11466, 0, 0, 11467, 0, 11468, 0, 11469, 0, 0, 11470, 11471, 11472]], + [30264, [11473]], + [30270, [11474, 0, 0, 0, 11475]], + [30284, [11476]], + [30290, [11477, 0, 0, 0, 11478]], + [30315, [11479]], + [30330, [11480, 11481, 0, 11482, 11483, 0, 0, 0, 11484, 0, 11485, 0, 11486, 11487, 11488, 0, 0, 11489, 0, 0, 0, 0, 11490, 11491, 0, 11492, 0, 0, 11493]], + [30382, [11494, 0, 0, 11495]], + [30399, [11496, 0, 0, 0, 11497, 0, 11498, 11499, 0, 11500, 0, 11501, 0, 0, 0, 11502, 11503, 11504, 11505, 11506, 0, 11507, 0, 11508, 11509, 11510, 0, 0, 11511, 11512, 0, 0, 11513, 0, 11514, 0, 11515, 11516]], + [30446, [11517, 11518, 0, 0, 11519, 0, 11520, 0, 0, 0, 11521, 0, 0, 0, 11522, 0, 11523, 0, 0, 11524]], + [30472, [11525, 11526, 0, 11527, 11528]], + [30494, [11529, 11530, 11531]], + [30504, [11532, 11533]], + [30511, [11534]], + [30518, [11535, 11536, 11537, 0, 11538, 0, 11539, 0, 0, 0, 11540, 11541]], + [30544, [11542, 11543]], + [30555, [11544]], + [30561, [11545, 0, 11546, 0, 0, 11547, 0, 11548, 0, 0, 11549, 11550]], + [30585, [11551, 0, 0, 0, 11552, 11553, 11554, 0, 0, 0, 0, 11555, 11556]], + [30606, [11557, 0, 0, 0, 11558]], + [30623, [11559]], + [30629, [11560, 0, 11561, 0, 11562, 11563, 0, 11564, 11565, 0, 0, 11566, 0, 0, 11567]], + [30651, [11568, 0, 0, 0, 11569]], + [30679, [11570, 0, 0, 0, 11571, 11572]], + [30690, [11573, 11574, 0, 11575, 0, 0, 0, 11576, 0, 11577, 0, 11578, 11579, 11580, 0, 0, 0, 11581, 0, 0, 11582]], + [30719, [11583, 0, 11584, 11585]], + [30732, [11586, 11587]], + [30740, [11588, 11589, 11590]], + [30757, [11591, 11592, 11593]], + [30768, [11594, 0, 11595, 0, 11596, 0, 0, 0, 11597]], + [30782, [11598, 0, 11599, 0, 0, 0, 0, 11600]], + [30805, [11601]], + [30813, [11602]], + [30827, [11603, 11604, 0, 11605, 11606]], + [30849, [11607]], + [30857, [11608, 0, 0, 11609, 11610, 11611, 0, 0, 11612, 0, 11613, 0, 0, 0, 11614, 11615]], + [30879, [11616]], + [30887, [11617, 0, 11618]], + [30896, [11619, 11620, 0, 11621]], + [30906, [11622, 0, 0, 0, 11623, 0, 0, 11624, 0, 0, 0, 11625, 0, 0, 0, 0, 11626, 11627, 0, 0, 0, 0, 11628, 0, 0, 0, 0, 11629]], + [30952, [11630]], + [30959, [11631]], + [30967, [11632, 0, 0, 11633]], + [30977, [11634]], + [30990, [11635]], + [31034, [11636, 0, 11637, 0, 11638, 0, 11639, 11640]], + [31047, [11641, 11642, 11643]], + [31056, [11644, 0, 0, 0, 0, 11645, 11646]], + [31069, [11647, 11648, 11649, 11650, 0, 11651, 0, 0, 11652, 0, 0, 11653, 0, 0, 0, 0, 11654, 0, 11655]], + [31095, [11656, 11657, 0, 11658, 0, 0, 0, 0, 11659, 11660, 11661, 0, 0, 11662, 11663]], + [31117, [11664, 11665, 11666]], + [31143, [11667, 0, 0, 11668, 0, 0, 0, 11669, 0, 11670, 11671]], + [31161, [11672, 11673, 11674, 0, 11675, 11676, 0, 11677, 11678, 0, 11679]], + [31177, [11680, 0, 11681, 0, 11682, 0, 0, 0, 11683, 11684]], + [31192, [11685]], + [31199, [11686, 0, 0, 0, 0, 11687, 0, 11688, 11689, 0, 11690]], + [31215, [11691, 11692]], + [31224, [11693, 0, 0, 11694, 0, 11695, 0, 0, 11696]], + [31243, [11697, 0, 11698, 11699]], + [31252, [11700]], + [31258, [11701, 0, 11702, 0, 0, 11703, 11704, 0, 0, 11705]], + [31278, [11706, 0, 0, 0, 11707, 11708, 0, 0, 0, 11709, 0, 0, 0, 11710, 11711, 11712, 0, 11713, 11714, 0, 11715, 0, 0, 0, 11716]], + [31309, [11717, 0, 11718]], + [31319, [11719]], + [31331, [11720]], + [31339, [11721, 0, 0, 0, 0, 11722, 0, 0, 0, 11723, 0, 11724, 11725, 0, 11726, 11727, 0, 0, 0, 0, 11728, 0, 11729, 0, 11730, 11731]], + [31373, [11732, 0, 0, 0, 11733, 11734, 11735, 0, 0, 11736, 11737, 11738, 0, 0, 0, 11739, 11740, 0, 11741]], + [31397, [11742, 11743, 0, 0, 0, 11744, 0, 0, 0, 11745, 11746]], + [31418, [11747, 0, 0, 0, 0, 11748]], + [31435, [11749]], + [31446, [11750, 0, 0, 11751, 0, 0, 11752, 0, 11753, 11754, 11755, 0, 0, 11756, 0, 11757, 0, 0, 0, 0, 11758, 0, 0, 11759, 0, 11760]], + [31478, [11761, 0, 0, 11762, 11763, 0, 0, 0, 0, 11764, 0, 0, 0, 11765, 0, 0, 11766, 0, 11767, 0, 0, 11768]], + [31505, [11769, 0, 0, 11770, 0, 0, 0, 0, 11771, 0, 11772, 0, 0, 0, 0, 11773]], + [31526, [11774, 0, 11775, 0, 0, 0, 11776]], + [31545, [11777, 11778, 0, 11779]], + [31558, [11780, 0, 11781, 11782, 0, 11783, 0, 0, 0, 11784, 11785, 11786, 11787, 0, 11788, 0, 11789, 0, 0, 0, 0, 11790, 0, 11791]], + [31605, [11792, 0, 11793, 0, 11794, 0, 0, 0, 0, 11795, 0, 11796]], + [31623, [11797, 0, 0, 0, 11798, 0, 11799]], + [31636, [11800, 11801, 0, 11802]], + [31649, [11803]], + [31659, [11804, 0, 11805, 0, 0, 0, 11806]], + [31672, [11807]], + [31680, [11808, 0, 0, 0, 11809, 0, 0, 11810, 0, 11811]], + [31712, [11812, 11813, 0, 0, 11814]], + [31726, [11815, 0, 0, 11816]], + [31735, [11817]], + [31751, [11818, 0, 0, 0, 0, 11819]], + [31777, [11820]], + [31783, [11821]], + [31800, [11822]], + [31806, [11823, 11824]], + [31821, [11825]], + [31840, [11826]], + [31859, [11827]], + [31867, [11828, 0, 11829, 11830]], + [31881, [11831, 0, 11832]], + [31890, [11833, 0, 0, 0, 0, 11834, 11835, 0, 0, 11836, 0, 0, 0, 11837, 0, 0, 0, 0, 11838, 11839, 0, 11840, 0, 0, 11841, 0, 0, 0, 11842, 0, 0, 11843]], + [31929, [11844, 0, 0, 0, 0, 11845]], + [31946, [11846]], + [31957, [11847, 11848, 0, 0, 11849, 0, 0, 11850, 0, 0, 11851]], + [31975, [11852]], + [31992, [11853, 0, 0, 11854, 0, 0, 11855, 0, 11856, 0, 0, 0, 11857, 11858, 0, 0, 0, 0, 11859, 11860, 0, 11861, 0, 0, 11862, 0, 0, 0, 11863, 0, 0, 11864, 11865, 11866, 11867, 11868, 0, 0, 0, 0, 11869, 11870, 11871, 0, 0, 0, 0, 11872, 0, 0, 0, 11873, 11874, 0, 0, 11875, 11876, 0, 0, 11877]], + [32057, [11878, 11879]], + [32066, [11880, 11881, 11882, 0, 11883]], + [32076, [11884, 0, 0, 0, 11885]], + [32094, [11886, 0, 0, 11887, 11888, 0, 0, 0, 11889]], + [32110, [11890, 0, 0, 11891, 0, 0, 0, 11892, 11893, 0, 0, 11894]], + [32153, [11895, 11896, 0, 11897]], + [32173, [11898, 0, 0, 0, 11899, 11900, 0, 11901, 0, 0, 0, 11902, 0, 11903, 11904, 0, 0, 11905, 11906]], + [32202, [11907, 11908, 0, 0, 0, 11909, 0, 11910, 11911]], + [32218, [11912]], + [32224, [11913]], + [32232, [11914, 11915]], + [32239, [11916, 0, 0, 0, 0, 11917]], + [32251, [11918]], + [32257, [11919, 0, 0, 11920]], + [32283, [11921, 0, 0, 11922, 0, 0, 0, 0, 11923, 0, 0, 11924, 0, 0, 0, 0, 11925, 0, 0, 11926, 0, 0, 11927]], + [32318, [11928, 0, 0, 11929]], + [32330, [11930, 0, 0, 11931]], + [32340, [11932, 11933]], + [32353, [11934]], + [32363, [11935, 0, 11936, 0, 0, 11937]], + [32386, [11938]], + [32399, [11939]], + [32406, [11940]], + [32416, [11941, 0, 11942, 0, 11943, 0, 11944, 11945, 0, 0, 11946, 0, 11947, 0, 0, 11948, 0, 11949, 11950, 11951, 0, 11952, 11953, 11954, 11955, 11956, 11957, 0, 0, 11958, 0, 11959, 0, 0, 0, 11960, 11961, 11962, 11963, 11964, 11965, 0, 11966, 0, 0, 11967, 11968, 11969, 0, 11970, 11971, 11972, 0, 11973, 0, 0, 11974, 11975, 11976, 11977, 11978, 11979, 11980, 11981, 0, 0, 11982, 11983, 0, 11984, 0, 11985, 0, 11986, 11987, 11988, 0, 11989, 11990, 11991, 11992, 0, 0, 11993, 11994, 11995, 0, 11996, 11997, 0, 0, 0, 11998, 11999, 0, 12000, 12001, 0, 0, 0, 12002, 12003, 12004, 0, 0, 12005, 0, 0, 0, 0, 12006, 0, 0, 0, 0, 12007, 12008, 12009, 12010, 0, 12011, 0, 12012, 0, 0, 12013, 0, 0, 12014, 0, 0, 0, 12015, 0, 0, 0, 0, 12016, 12017, 0, 0, 12018, 12019, 0, 12020, 0, 0, 0, 12021, 0, 12022, 0, 12023, 0, 12024]], + [32592, [12025, 12026, 0, 0, 0, 12027, 0, 12028, 0, 0, 12029]], + [32609, [12030, 12031]], + [32617, [12032, 12033, 0, 0, 0, 12034, 0, 12035, 0, 12036, 0, 0, 12037, 0, 12038]], + [32641, [12039, 0, 0, 0, 12040, 0, 0, 0, 0, 12041, 0, 12042, 0, 12043]], + [32660, [12044]], + [32666, [12045, 0, 0, 0, 12046, 0, 0, 12047, 0, 0, 12048, 0, 0, 0, 12049, 12050]], + [32690, [12051]], + [32697, [12052, 0, 0, 0, 12053, 0, 0, 0, 12054, 0, 0, 0, 12055]], + [32716, [12056, 0, 12057, 0, 0, 0, 12058, 0, 12059, 0, 0, 0, 12060]], + [32735, [12061, 12062]], + [32745, [12063]], + [32752, [12064]], + [32763, [12065, 12066, 0, 0, 0, 12067, 12068, 0, 12069, 0, 12070, 12071]], + [32780, [12072, 12073, 0, 0, 12074, 0, 0, 0, 0, 12075, 0, 12076]], + [32819, [12077, 0, 0, 12078, 0, 12079, 0, 0, 12080, 0, 12081, 0, 12082, 0, 0, 12083, 0, 0, 0, 12084, 0, 0, 0, 12085, 12086, 12087]], + [32852, [12088, 0, 12089, 0, 12090, 0, 12091, 0, 0, 0, 12092, 0, 0, 12093]], + [32874, [12094]], + [32884, [12095, 0, 0, 12096]], + [32893, [12097, 0, 12098, 0, 0, 0, 12099, 0, 0, 12100, 12101, 0, 12102, 0, 12103, 12104]], + [32918, [12105, 0, 12106, 0, 12107, 12108, 0, 12109, 0, 0, 12110, 12111, 12112, 0, 12113, 12114, 0, 0, 0, 12115, 12116, 0, 0, 0, 12117, 12118, 0, 0, 12119, 0, 12120]], + [32954, [12121, 0, 0, 0, 12122, 12123, 12124, 12125, 0, 12126, 0, 0, 12127]], + [32972, [12128, 0, 12129]], + [32982, [12130, 0, 0, 0, 12131, 0, 12132, 0, 12133, 0, 0, 12134, 0, 0, 12135, 12136, 0, 12137]], + [33008, [12138, 12139, 0, 12140, 12141, 0, 12142, 0, 12143, 0, 12144, 0, 0, 12145, 0, 0, 0, 0, 12146, 0, 0, 12147, 12148, 12149, 12150, 12151, 12152, 0, 0, 0, 0, 12153, 12154, 12155, 0, 12156, 0, 0, 12157, 0, 0, 0, 12158]], + [33065, [12159]], + [33071, [12160, 0, 12161, 0, 12162, 0, 0, 0, 0, 12163, 12164, 0, 0, 0, 0, 12165]], + [33098, [12166, 12167, 0, 0, 12168, 0, 12169, 12170, 0, 12171, 12172, 12173]], + [33125, [12174]], + [33131, [12175, 0, 0, 12176, 0, 12177]], + [33144, [12178, 12179, 12180, 12181, 0, 0, 12182, 12183, 12184]], + [33162, [12185, 0, 0, 0, 0, 12186]], + [33178, [12187, 12188, 12189, 12190]], + [33192, [12191]], + [33203, [12192]], + [33210, [12193]], + [33216, [12194, 0, 12195, 0, 0, 0, 12196]], + [33235, [12197]], + [33247, [12198, 0, 0, 0, 12199, 0, 12200, 0, 0, 12201, 0, 12202, 0, 0, 12203]], + [33267, [12204, 12205]], + [33275, [12206, 12207]], + [33285, [12208, 12209, 12210, 12211, 0, 0, 0, 12212, 12213, 12214, 0, 0, 0, 12215, 0, 12216, 0, 0, 12217, 0, 0, 0, 0, 12218, 0, 12219, 12220]], + [33322, [12221, 0, 12222, 0, 0, 0, 12223, 12224, 0, 0, 0, 12225, 12226, 12227, 0, 12228]], + [33351, [12229]], + [33368, [12230]], + [33382, [12231]], + [33391, [12232, 12233, 0, 12234, 12235, 0, 0, 12236, 0, 0, 0, 12237, 0, 0, 0, 12238, 0, 0, 0, 12239]], + [33419, [12240]], + [33426, [12241]], + [33433, [12242, 0, 0, 12243, 12244]], + [33445, [12245, 12246]], + [33452, [12247, 12248, 0, 12249, 0, 12250, 0, 12251, 0, 0, 0, 0, 12252, 12253, 0, 0, 0, 12254]], + [33479, [12255]], + [33485, [12256, 0, 12257, 0, 12258, 0, 0, 12259, 0, 0, 12260, 0, 0, 0, 12261, 0, 0, 12262, 12263]], + [33509, [12264, 12265]], + [33519, [12266, 0, 12267]], + [33529, [12268, 12269]], + [33538, [12270, 12271, 12272, 12273, 0, 0, 0, 12274, 0, 0, 0, 0, 12275]], + [33564, [12276]], + [33575, [12277, 12278, 0, 0, 12279, 12280]], + [33589, [12281, 12282, 0, 12283, 12284]], + [33600, [12285, 0, 0, 12286, 0, 0, 12287, 0, 0, 12288]], + [33616, [12289, 0, 12290, 0, 12291, 0, 0, 0, 12292]], + [33633, [12293, 0, 12294, 0, 0, 0, 12295, 0, 0, 0, 12296, 0, 0, 0, 12297]], + [33655, [12298, 0, 0, 0, 12299, 12300]], + [33673, [12301, 12302, 0, 0, 0, 12303]], + [33694, [12304]], + [33707, [12305]], + [33713, [12306, 12307, 0, 0, 0, 0, 12308, 0, 12309, 12310, 0, 0, 12311, 0, 0, 0, 12312, 0, 0, 0, 12313, 0, 12314, 0, 0, 12315, 0, 12316]], + [33747, [12317, 0, 0, 12318]], + [33756, [12319]], + [33769, [12320, 0, 12321, 0, 0, 0, 12322, 0, 12323, 12324]], + [33795, [12325, 12326]], + [33802, [12327, 0, 12328, 12329, 12330]], + [33821, [12331, 0, 0, 12332, 0, 0, 0, 12333, 12334, 12335, 12336, 12337, 12338, 0, 0, 12339, 0, 0, 0, 0, 12340]], + [33853, [12341]], + [33862, [12342, 0, 0, 12343]], + [33879, [12344, 0, 0, 0, 12345]], + [33889, [12346, 0, 12347, 0, 0, 12348, 0, 0, 0, 0, 12349, 12350, 0, 0, 0, 0, 12351, 0, 0, 0, 12352, 0, 0, 0, 0, 12353]], + [33922, [12354]], + [33931, [12355, 0, 0, 0, 0, 12356, 0, 0, 0, 12357, 0, 0, 0, 0, 12358, 0, 0, 12359]], + [33970, [12360]], + [33976, [12361, 0, 0, 0, 12362]], + [33988, [12363, 0, 0, 0, 0, 12364, 0, 12365]], + [34001, [12366, 0, 12367]], + [34013, [12368]], + [34022, [12369]], + [34028, [12370, 0, 12371]], + [34065, [12372, 0, 12373, 0, 0, 0, 12374, 0, 0, 12375]], + [34081, [12376, 0, 12377, 0, 0, 12378]], + [34092, [12379, 12380]], + [34101, [12381, 0, 12382, 0, 0, 0, 0, 12383, 12384]], + [34115, [12385]], + [34121, [12386, 12387, 0, 0, 0, 12388]], + [34135, [12389]], + [34152, [12390, 0, 12391]], + [34164, [12392]], + [34174, [12393]], + [34180, [12394, 0, 0, 12395]], + [34199, [12396, 0, 12397, 0, 12398]], + [34214, [12399, 0, 0, 12400, 12401, 12402, 12403, 0, 0, 12404]], + [34241, [12405]], + [34249, [12406, 0, 0, 0, 12407, 0, 12408, 12409, 0, 0, 0, 0, 12410]], + [34269, [12411]], + [34276, [12412, 12413, 0, 0, 0, 12414]], + [34299, [12415]], + [34311, [12416]], + [34321, [12417]], + [34349, [12418]], + [34360, [12419]], + [34382, [12420, 12421, 12422, 12423, 0, 0, 12424]], + [34394, [12425, 0, 12426, 0, 12427]], + [34411, [12428]], + [34425, [12429, 0, 12430, 0, 12431, 12432, 0, 12433, 12434, 12435]], + [34442, [12436, 0, 12437]], + [34453, [12438]], + [34496, [12439]], + [34503, [12440, 0, 0, 12441, 12442, 0, 12443]], + [34521, [12444, 0, 12445, 0, 0, 0, 12446, 0, 0, 0, 0, 12447]], + [34542, [12448]], + [34553, [12449, 0, 0, 0, 0, 12450, 0, 12451, 0, 12452]], + [34568, [12453]], + [34578, [12454, 0, 0, 12455, 0, 12456, 12457, 0, 0, 0, 12458, 0, 0, 0, 0, 12459]], + [34615, [12460]], + [34623, [12461]], + [34631, [12462, 0, 12463, 0, 0, 0, 0, 12464]], + [34647, [12465, 0, 12466]], + [34656, [12467]], + [34662, [12468]], + [34676, [12469, 0, 12470]], + [34690, [12471, 12472, 0, 12473]], + [34701, [12474]], + [34746, [12475]], + [34752, [12476]], + [34758, [12477, 0, 0, 0, 0, 12478]], + [34770, [12479]], + [34809, [12480, 0, 0, 0, 0, 12481]], + [34837, [12482]], + [34847, [12483, 0, 0, 12484]], + [34880, [12485, 0, 0, 0, 0, 12486, 12487]], + [34892, [12488, 12489]], + [34899, [12490, 12491, 0, 0, 12492, 0, 12493, 0, 12494, 0, 12495, 12496, 0, 0, 12497, 0, 12498, 0, 12499, 0, 0, 12500, 0, 0, 12501, 12502, 0, 0, 0, 12503]], + [34935, [12504]], + [34943, [12505, 0, 12506, 0, 0, 12507, 12508, 0, 0, 12509, 0, 0, 12510, 0, 12511, 0, 0, 0, 0, 12512, 0, 0, 0, 12513]], + [34972, [12514]], + [34987, [12515, 0, 12516, 0, 0, 0, 12517, 0, 0, 12518]], + [35009, [12519, 12520, 0, 0, 12521]], + [35023, [12522, 0, 0, 0, 0, 12523, 12524, 0, 0, 12525, 12526, 0, 0, 12527, 12528, 0, 12529, 0, 12530, 0, 0, 12531]], + [35059, [12532, 12533, 0, 0, 0, 12534, 12535, 0, 0, 0, 12536, 12537, 0, 0, 0, 12538, 0, 0, 0, 0, 12539]], + [35088, [12540, 0, 12541]], + [35098, [12542]], + [35109, [12543, 0, 0, 0, 0, 12544]], + [35126, [12545]], + [35140, [12546]], + [35158, [12547]], + [35167, [12548]], + [35186, [12549]], + [35199, [12550, 0, 12551, 0, 0, 0, 0, 12552, 12553, 0, 0, 0, 12554, 0, 0, 0, 12555]], + [35222, [12556, 12557, 0, 0, 12558]], + [35239, [12559, 0, 0, 12560]], + [35251, [12561]], + [35261, [12562, 0, 0, 0, 12563, 12564, 0, 12565, 12566, 12567, 0, 12568, 12569]], + [35281, [12570, 12571]], + [35299, [12572, 0, 0, 12573]], + [35328, [12574, 0, 12575, 12576, 0, 0, 0, 0, 12577, 0, 12578, 0, 0, 0, 12579, 0, 0, 0, 0, 12580, 0, 0, 0, 12581, 12582]], + [35359, [12583, 0, 0, 0, 12584]], + [35370, [12585, 0, 0, 12586, 0, 0, 0, 12587, 0, 12588, 12589]], + [35386, [12590, 12591, 12592]], + [35408, [12593, 0, 0, 0, 12594, 12595]], + [35422, [12596, 0, 12597, 0, 12598, 12599, 0, 0, 12600, 0, 0, 12601, 0, 12602, 0, 0, 12603, 0, 12604, 12605, 12606, 12607]], + [35449, [12608]], + [35463, [12609, 0, 12610, 0, 0, 12611, 12612]], + [35475, [12613, 0, 12614, 0, 0, 12615]], + [35486, [12616, 0, 12617, 0, 0, 0, 12618]], + [35500, [12619, 12620, 0, 0, 12621, 0, 12622]], + [35516, [12623, 0, 0, 12624, 0, 0, 0, 0, 12625, 0, 0, 12626, 0, 0, 0, 12627, 0, 0, 0, 12628, 0, 0, 12629, 0, 0, 0, 12630]], + [35548, [12631]], + [35558, [12632, 12633]], + [35565, [12634, 12635]], + [35576, [12636, 0, 12637, 0, 0, 0, 12638, 0, 12639, 12640, 12641, 0, 12642]], + [35598, [12643]], + [35609, [12644, 0, 12645, 0, 12646, 0, 0, 12647, 12648]], + [35641, [12649]], + [35672, [12650, 0, 0, 0, 12651]], + [35686, [12652]], + [35692, [12653, 0, 0, 0, 12654, 0, 12655, 0, 0, 0, 0, 12656]], + [35715, [12657]], + [35728, [12658, 0, 0, 12659]], + [35745, [12660, 12661, 0, 12662, 12663, 0, 0, 12664, 12665, 12666, 0, 0, 12667, 12668, 12669, 12670, 0, 12671, 12672, 0, 0, 12673, 12674, 12675, 12676, 12677, 0, 12678, 12679, 12680, 12681, 12682, 12683, 0, 12684, 12685, 12686, 12687, 0, 12688, 12689, 12690, 0, 0, 12691, 0, 12692, 0, 12693, 0, 0, 0, 12694, 0, 12695, 12696, 12697, 12698, 12699, 0, 12700, 12701, 0, 12702, 12703, 12704, 12705, 0, 12706, 12707, 12708, 0, 12709, 0, 12710, 12711, 12712, 0, 12713, 12714, 12715, 12716, 0, 12717, 12718, 0, 12719, 12720, 0, 12721, 12722, 0, 12723, 12724, 0, 12725, 12726, 0, 12727, 0, 12728, 0, 0, 12729, 0, 12730, 12731, 0, 12732, 12733, 12734, 12735, 12736, 12737, 12738, 0, 12739, 0, 0, 0, 12740, 12741, 12742, 12743, 0, 0, 12744, 0, 0, 12745, 12746, 12747, 0, 12748, 0, 12749, 12750, 0, 0, 12751, 12752, 0, 0, 0, 12753, 0, 0, 12754, 0, 0, 12755]], + [35905, [12756, 0, 0, 0, 0, 12757, 0, 0, 0, 12758]], + [35930, [12759]], + [35937, [12760, 12761]], + [35946, [12762, 12763]], + [35961, [12764]], + [35980, [12765]], + [35997, [12766, 12767, 0, 12768, 12769, 12770, 0, 0, 0, 0, 12771, 12772, 12773, 12774, 12775, 12776, 0, 0, 12777, 12778, 0, 0, 0, 12779, 0, 0, 12780, 12781, 0, 0, 12782, 12783, 0, 0, 12784, 12785, 0, 0, 12786, 12787, 0, 0, 12788, 0, 0, 12789]], + [36049, [12790, 0, 12791]], + [36059, [12792, 12793, 0, 12794, 0, 12795, 0, 12796, 12797, 0, 0, 12798, 0, 0, 0, 12799, 0, 0, 12800]], + [36092, [12801]], + [36104, [12802]], + [36125, [12803, 12804, 12805, 0, 12806, 12807, 12808, 12809, 12810, 12811, 12812, 12813, 12814, 12815, 12816, 12817, 12818, 12819, 12820, 12821, 12822, 0, 0, 12823, 12824, 0, 12825, 12826, 12827, 12828, 12829, 12830, 0, 12831, 12832, 0, 12833, 12834, 12835, 12836]], + [36171, [12837, 12838, 0, 12839, 12840, 12841, 0, 0, 12842, 12843, 0, 12844, 0, 12845, 0, 12846, 12847, 0, 0, 12848, 0, 12849, 12850, 12851, 12852, 12853, 0, 12854, 0, 0, 0, 0, 12855, 0, 0, 0, 0, 12856, 0, 0, 12857, 12858, 12859, 12860, 12861]], + [36225, [12862, 0, 0, 0, 12863, 0, 0, 0, 0, 12864, 12865]], + [36255, [12866, 0, 0, 0, 12867]], + [36275, [12868, 12869]], + [36286, [12870, 0, 0, 0, 0, 12871, 12872]], + [36299, [12873, 12874, 0, 0, 0, 0, 12875]], + [36314, [12876, 12877, 0, 12878, 0, 12879, 0, 12880, 0, 0, 12881, 0, 0, 0, 12882, 0, 12883, 0, 0, 0, 0, 12884, 0, 0, 0, 12885, 0, 12886, 0, 12887, 0, 0, 12888]], + [36361, [12889, 12890, 0, 12891, 0, 0, 12892]], + [36381, [12893, 12894, 0, 0, 0, 12895]], + [36393, [12896, 12897]], + [36401, [12898, 0, 0, 0, 12899]], + [36418, [12900, 0, 12901, 0, 0, 0, 12902, 0, 12903, 12904]], + [36433, [12905]], + [36441, [12906]], + [36447, [12907]], + [36454, [12908]], + [36460, [12909, 12910, 0, 0, 0, 0, 12911, 0, 12912]], + [36479, [12913, 0, 12914]], + [36487, [12915]], + [36493, [12916, 0, 12917]], + [36523, [12918, 12919, 0, 0, 12920, 0, 0, 12921]], + [36538, [12922]], + [36554, [12923, 0, 12924, 12925, 0, 0, 0, 0, 12926]], + [36575, [12927, 0, 0, 12928]], + [36600, [12929, 0, 0, 0, 0, 12930]], + [36611, [12931]], + [36617, [12932]], + [36628, [12933]], + [36637, [12934]], + [36649, [12935, 12936, 0, 0, 0, 0, 12937]], + [36664, [12938]], + [36671, [12939, 0, 0, 0, 0, 12940, 0, 0, 0, 0, 12941, 0, 0, 0, 12942]], + [36703, [12943]], + [36710, [12944, 12945, 12946, 12947, 0, 0, 12948, 0, 12949, 12950, 12951, 0, 12952, 0, 12953]], + [36731, [12954, 12955, 12956, 0, 12957, 0, 0, 0, 12958, 12959, 12960, 12961, 0, 12962, 12963]], + [36752, [12964, 12965, 0, 12966, 0, 12967, 12968, 12969, 12970, 12971, 0, 12972, 12973, 0, 12974, 12975, 0, 0, 0, 12976, 0, 0, 0, 0, 12977, 12978, 0, 12979, 0, 0, 0, 0, 12980, 12981, 12982]], + [36793, [12983, 12984, 12985, 12986, 12987, 12988, 12989, 0, 12990, 12991, 0, 12992, 12993, 0, 12994, 12995]], + [36814, [12996, 0, 12997, 12998, 0, 0, 12999, 0, 0, 0, 13000, 13001, 0, 13002, 13003, 13004, 13005, 13006, 0, 0, 13007, 0, 0, 13008, 13009, 0, 0, 0, 13010, 13011, 0, 13012, 0, 0, 13013]], + [36855, [13014, 13015, 13016, 0, 0, 0, 13017, 0, 0, 13018, 13019, 13020, 13021, 0, 0, 13022, 0, 0, 13023, 13024, 0, 0, 13025, 0, 13026, 13027, 0, 13028, 13029, 13030, 0, 0, 13031, 0, 13032, 13033, 13034, 0, 13035, 13036, 13037, 13038, 0, 13039, 13040]], + [36910, [13041, 0, 0, 13042, 13043, 0, 0, 13044, 0, 0, 13045, 0, 0, 13046, 13047, 0, 13048, 0, 0, 13049, 13050, 0, 0, 13051, 0, 13052, 0, 0, 13053, 13054, 0, 13055, 13056, 13057, 13058, 0, 0, 13059, 13060, 13061, 0, 13062, 0, 13063, 0, 0, 13064, 0, 0, 0, 13065, 13066, 0, 13067, 0, 13068, 0, 0, 0, 13069, 0, 0, 0, 13070, 13071]], + [36981, [13072, 0, 13073, 13074, 0, 13075, 0, 13076, 13077, 0, 13078, 13079, 0, 0, 13080, 13081]], + [37009, [13082, 0, 13083]], + [37026, [13084, 13085, 0, 0, 13086, 0, 0, 0, 13087, 0, 0, 0, 13088, 13089, 0, 13090, 0, 0, 0, 13091, 0, 0, 13092, 13093, 0, 13094]], + [37057, [13095]], + [37066, [13096, 0, 0, 0, 13097, 0, 0, 13098]], + [37085, [13099, 0, 0, 0, 13100]], + [37096, [13101, 0, 0, 0, 0, 13102]], + [37109, [13103, 0, 13104]], + [37117, [13105, 0, 0, 0, 0, 13106]], + [37145, [13107]], + [37165, [13108]], + [37193, [13109, 0, 13110, 13111, 13112, 13113, 0, 0, 0, 13114, 0, 13115]], + [37213, [13116, 0, 0, 0, 0, 13117, 13118, 0, 13119, 0, 0, 0, 0, 13120, 0, 13121, 0, 13122, 0, 0, 13123, 0, 0, 0, 13124, 13125, 13126, 13127]], + [37247, [13128]], + [37255, [13129, 0, 13130, 0, 13131, 0, 13132, 0, 0, 13133, 0, 13134]], + [37276, [13135]], + [37284, [13136]], + [37292, [13137]], + [37304, [13138]], + [37312, [13139]], + [37319, [13140, 13141, 13142, 13143, 0, 13144, 13145, 13146, 13147, 0, 13148]], + [37336, [13149, 0, 0, 0, 13150, 13151]], + [37347, [13152, 0, 0, 0, 13153]], + [37389, [13154]], + [37428, [13155]], + [37444, [13156]], + [37467, [13157]], + [37474, [13158]], + [37489, [13159, 0, 0, 13160]], + [37504, [13161, 0, 0, 13162, 0, 13163]], + [37521, [13164]], + [37528, [13165, 0, 13166]], + [37549, [13167]], + [37586, [13168]], + [37613, [13169]], + [37619, [13170, 0, 0, 0, 0, 13171, 0, 0, 0, 13172]], + [37638, [13173]], + [37648, [13174]], + [37656, [13175]], + [37664, [13176]], + [37670, [13177, 0, 0, 0, 0, 13178, 13179, 0, 0, 13180, 0, 0, 13181]], + [37707, [13182]], + [37723, [13183]], + [37740, [13184]], + [37749, [13185]], + [37772, [13186]], + [37782, [13187]], + [37799, [13188]], + [37805, [13189, 13190]], + [37857, [13191]], + [37912, [13192]], + [37956, [13193]], + [37969, [13194]], + [37995, [13195]], + [38024, [13196, 13197, 13198]], + [38035, [13199, 0, 0, 0, 13200, 0, 13201, 0, 0, 0, 13202, 13203, 13204, 13205, 13206, 13207, 0, 0, 13208, 13209, 13210, 0, 13211, 0, 0, 0, 0, 13212, 0, 0, 13213, 0, 13214, 0, 13215]], + [38075, [13216, 0, 0, 13217, 0, 13218, 13219, 0, 13220, 0, 13221]], + [38094, [13222, 0, 13223]], + [38108, [13224, 13225, 0, 0, 0, 0, 13226]], + [38125, [13227, 13228, 0, 0, 0, 13229, 0, 0, 0, 13230, 0, 13231, 0, 13232, 0, 0, 0, 13233, 0, 13234, 13235, 0, 0, 13236, 13237, 0, 0, 13238, 0, 0, 13239, 13240, 0, 0, 0, 13241]], + [38169, [13242, 13243]], + [38177, [13244, 13245, 13246, 13247, 13248, 13249]], + [38190, [13250, 13251]], + [38197, [13252]], + [38203, [13253, 0, 0, 0, 0, 13254]], + [38215, [13255]], + [38225, [13256, 0, 0, 0, 0, 13257]], + [38236, [13258]], + [38255, [13259, 13260]], + [38262, [13261, 13262]], + [38271, [13263, 13264, 0, 0, 13265]], + [38281, [13266, 0, 13267, 0, 0, 0, 13268, 0, 13269, 0, 13270]], + [38306, [13271, 13272, 13273, 13274]], + [38322, [13275]], + [38343, [13276]], + [38360, [13277]], + [38376, [13278, 0, 13279, 0, 0, 13280, 13281, 13282, 0, 0, 13283, 0, 13284, 13285, 0, 13286, 13287, 13288, 13289, 13290, 0, 13291, 0, 0, 13292, 13293, 0, 0, 0, 13294, 0, 0, 0, 13295, 0, 0, 0, 0, 13296, 0, 13297, 13298, 0, 0, 13299, 0, 0, 0, 0, 13300, 0, 0, 13301, 0, 0, 13302]], + [38442, [13303, 0, 0, 0, 13304, 0, 0, 13305, 13306, 13307, 13308, 13309, 13310, 0, 0, 0, 0, 13311, 0, 0, 0, 13312, 13313, 0, 0, 0, 13314, 13315, 13316, 13317, 13318, 0, 0, 13319, 13320, 13321, 0, 0, 13322, 0, 0, 0, 0, 13323]], + [38491, [13324]], + [38497, [13325, 13326, 13327, 13328, 13329, 0, 0, 13330, 13331, 13332]], + [38512, [13333, 0, 0, 13334, 0, 13335, 13336, 13337, 13338, 0, 13339, 0, 0, 13340]], + [38533, [13341, 13342, 0, 13343, 0, 13344, 13345, 0, 0, 13346, 13347, 13348, 0, 0, 0, 13349, 0, 0, 0, 13350, 13351, 0, 13352, 13353, 0, 0, 0, 13354, 0, 0, 13355, 0, 0, 0, 13356, 0, 0, 13357]], + [38582, [13358, 13359, 0, 0, 0, 13360, 13361, 0, 13362, 0, 13363, 13364, 0, 0, 13365, 13366, 13367, 13368, 0, 0, 0, 0, 13369, 13370, 0, 13371, 0, 13372, 0, 0, 0, 13373]], + [38619, [13374, 13375]], + [38626, [13376, 13377, 0, 0, 0, 0, 13378, 0, 13379, 13380, 0, 0, 0, 13381, 13382, 0, 13383, 13384, 0, 0, 13385, 13386, 0, 0, 0, 13387, 0, 0, 13388, 0, 13389, 0, 0, 0, 13390, 0, 13391, 13392, 0, 13393, 13394, 0, 0, 13395, 13396, 0, 0, 0, 0, 13397, 0, 0, 13398]], + [38684, [13399, 0, 13400]], + [38695, [13401]], + [38706, [13402]], + [38712, [13403, 13404]], + [38738, [13405, 0, 0, 0, 13406, 0, 0, 13407, 0, 0, 13408, 0, 13409, 0, 13410, 13411, 13412]], + [38761, [13413]], + [38771, [13414, 13415, 0, 13416]], + [38788, [13417, 13418]], + [38795, [13419, 0, 13420]], + [38808, [13421]], + [38816, [13422]], + [38829, [13423]], + [38867, [13424]], + [38886, [13425, 13426, 0, 13427, 0, 0, 13428]], + [38899, [13429, 0, 13430, 13431, 0, 0, 0, 0, 13432, 0, 0, 0, 13433, 0, 13434, 13435, 13436, 0, 13437, 13438, 0, 13439, 0, 0, 0, 13440, 0, 0, 0, 13441, 13442, 13443, 13444, 0, 0, 0, 13445, 13446]], + [38956, [13447, 13448, 0, 0, 13449]], + [38971, [13450, 13451]], + [38988, [13452, 13453, 13454]], + [38996, [13455, 13456, 0, 0, 13457, 0, 0, 13458, 0, 0, 13459]], + [39015, [13460]], + [39023, [13461]], + [39029, [13462, 13463, 13464, 0, 13465, 13466, 13467, 13468, 13469, 13470, 13471, 0, 13472, 13473, 0, 13474, 13475, 13476, 13477, 13478, 13479, 13480, 0, 13481, 13482, 0, 0, 13483, 13484, 0, 13485, 0, 0, 13486, 13487, 13488, 0, 13489, 0, 13490, 13491, 0, 0, 13492, 0, 0, 0, 13493, 0, 0, 0, 13494]], + [39087, [13495]], + [39118, [13496]], + [39128, [13497, 0, 0, 13498, 13499, 0, 13500, 13501, 0, 0, 13502]], + [39151, [13503, 0, 0, 13504]], + [39164, [13505, 13506, 13507]], + [39173, [13508, 0, 0, 0, 0, 13509, 0, 13510, 0, 0, 0, 13511, 0, 0, 13512]], + [39208, [13513]], + [39255, [13514]], + [39269, [13515]], + [39276, [13516, 13517, 13518, 0, 13519, 13520, 13521, 0, 0, 13522, 13523, 13524, 0, 0, 13525, 0, 13526, 0, 0, 13527]], + [39301, [13528, 13529, 0, 13530, 0, 0, 13531]], + [39314, [13532, 0, 0, 0, 13533, 0, 0, 13534]], + [39336, [13535, 0, 0, 0, 13536]], + [39347, [13537, 13538]], + [39364, [13539, 13540, 13541, 0, 13542]], + [39376, [13543, 0, 13544, 0, 0, 13545]], + [39423, [13546]], + [39438, [13547, 0, 0, 0, 13548, 13549]], + [39472, [13550]], + [39479, [13551]], + [39501, [13552]], + [39511, [13553, 0, 0, 13554]], + [39532, [13555, 13556, 13557, 13558, 13559, 13560, 0, 13561, 13562, 0, 13563, 0, 0, 13564, 0, 13565, 13566, 0, 13567, 13568, 0, 0, 13569, 0, 13570, 0, 13571, 13572, 0, 0, 0, 0, 13573, 0, 0, 13574, 0, 13575]], + [39575, [13576, 0, 0, 13577]], + [39585, [13578, 0, 0, 13579, 13580, 0, 0, 13581]], + [39607, [13582, 13583, 0, 0, 0, 13584]], + [39620, [13585, 13586]], + [39635, [13587, 0, 0, 0, 0, 13588]], + [39654, [13589, 0, 0, 0, 13590, 0, 0, 13591, 13592]], + [39699, [13593]], + [39729, [13594]], + [39740, [13595, 0, 0, 0, 0, 13596, 13597, 0, 13598, 13599]], + [39759, [13600, 0, 0, 0, 0, 13601]], + [39770, [13602]], + [39791, [13603]], + [39822, [13604]], + [39854, [13605]], + [39881, [13606]], + [39899, [13607]], + [39912, [13608]], + [39983, [13609]], + [40008, [13610]], + [40018, [13611, 0, 0, 0, 0, 13612]], + [40060, [13613, 0, 0, 0, 0, 13614]], + [40077, [13615]], + [40092, [13616]], + [40100, [13617, 0, 0, 0, 13618]], + [40120, [13619]], + [40131, [13620, 13621]], + [40140, [13622, 13623]], + [40150, [13624]], + [40158, [13625]], + [40165, [13626, 0, 0, 0, 13627]], + [40179, [13628, 13629, 0, 13630]], + [40232, [13631]], + [40251, [13632]], + [40284, [13633]], + [40300, [13634]], + [40335, [13635]], + [40372, [13636]], + [40388, [13637]], + [40407, [13638]], + [40434, [13639]], + [40441, [13640, 13641]], + [40479, [13642, 13643, 13644, 0, 13645, 0, 13646, 13647]], + [40493, [13648, 0, 13649, 0, 0, 0, 13650]], + [40509, [13651, 13652, 13653, 0, 0, 0, 13654, 0, 13655, 0, 0, 0, 13656, 13657, 0, 0, 0, 0, 13658]], + [40548, [13659, 0, 13660]], + [40560, [13661, 0, 0, 13662]], + [40575, [13663]], + [40594, [13664, 13665, 0, 0, 0, 13666]], + [40607, [13667]], + [40614, [13668]], + [40633, [13669, 13670, 13671, 0, 13672, 0, 13673, 0, 0, 0, 0, 13674]], + [40654, [13675, 13676, 0, 13677, 13678, 0, 13679, 0, 0, 0, 13680, 13681, 0, 13682, 0, 13683]], + [40687, [13684]], + [40718, [13685, 0, 0, 0, 0, 13686]], + [40736, [13687]], + [40763, [13688, 0, 0, 13689]], + [40778, [13690]], + [40784, [13691]], + [40802, [13692]], + [40831, [13693, 0, 0, 0, 0, 13694, 0, 0, 0, 13695, 0, 0, 13696, 0, 13697]], + [40857, [13698, 13699, 0, 0, 0, 0, 13700]], + [65281, [13701, 13702, 13703, 13704, 13705, 13706, 13707, 13708, 13709, 13710, 13711, 13712, 13713, 13714, 13715, 13716, 13717, 13718, 13719, 13720, 13721, 13722, 13723, 13724, 13725, 13726, 13727, 13728, 13729, 13730, 13731, 13732, 13733, 13734, 13735, 13736, 13737, 13738, 13739, 13740, 13741, 13742, 13743, 13744, 13745, 13746, 13747, 13748, 13749, 13750, 13751, 13752, 13753, 13754, 13755, 13756, 13757, 13758, 13759, 13760, 13761, 13762, 13763, 13764, 13765, 13766, 13767, 13768, 13769, 13770, 13771, 13772, 13773, 13774, 13775, 13776, 13777, 13778, 13779, 13780, 13781, 13782, 13783, 13784, 13785, 13786, 13787, 13788, 13789, 13790, 13791, 13792, 13793, 13794, 8316, 8317, 8318, 8319, 8320, 8321, 8322]], + [65507, [8323]], + [128067, [8487]], + [128518, [8493]], + [128528, [8491, 8492]], + [128543, [8494, 8490]], + [128565, [8489]], + [128578, [8488, 8495]], +]); + +export const emojiPal = createFont(sprites2, [ + [9752, [13872]], + [9800, [13858, 13859, 13860, 13861, 13862, 13863, 13864, 13865, 13866, 13867, 13868, 13869]], + [9889, [13854]], + [9924, [13845]], + [9934, [13870, 13880]], + [10024, [13853]], + [10052, [13846]], + [10084, [13800]], + [11088, [13850]], + [127752, [13841]], + [127775, [13851, 13849]], + [127794, [13828]], + [127800, [13826]], + [127807, [13831, 13873]], + [127815, [13879, 0, 0, 13834, 0, 13881, 0, 13832, 13833, 13876, 0, 0, 0, 0, 13837]], + [127841, [13840]], + [127850, [13874, 0, 13838, 13839]], + [127873, [13844, 0, 13836, 13829, 13848]], + [127922, [13852, 0, 0, 13856, 13857]], + [127936, [13835]], + [128008, [13815]], + [128049, [13816]], + [128062, [13814]], + [128081, [13871]], + [128123, [13813, 0, 0, 0, 13798]], + [128142, [13842, 0, 13830, 0, 0, 0, 13806, 13809, 13807, 13808, 0, 13801, 13802, 13803, 13804]], + [128174, [13827]], + [128293, [13855]], + [128367, [13847]], + [128375, [13811]], + [128420, [13805]], + [128520, [13797, 0, 0, 0, 0, 13796]], + [128568, [13818, 13819, 13817, 13820, 13821, 13822, 13825, 13824, 13823]], + [129300, [13799]], + [129356, [13843]], + [129365, [13878, 0, 0, 0, 0, 13875]], + [129389, [13877]], + [129415, [13812, 0, 0, 0, 13810]], +]); + +export const fontSmallPal = createFont(sprites2, [ + [0, [13882]], + [32, [14244, 13914, 13915, 13916, 13917, 13918, 13919, 13920, 13921, 13922, 13923, 13924, 13925, 13926, 13927, 13928, 13929, 13930, 13931, 13932, 13933, 13934, 13935, 13936, 13937, 13938, 13939, 13940, 13941, 13942, 13943, 13944, 13945, 13946, 13947, 13948, 13949, 13950, 13951, 13952, 13953, 13954, 13955, 13956, 13957, 13958, 13959, 13960, 13961, 13962, 13963, 13964, 13965, 13966, 13967, 13968, 13969, 13970, 13971, 13972, 13973, 13974, 13975, 13976, 13977, 13978, 13979, 13980, 13981, 13982, 13983, 13984, 13985, 13986, 13987, 13988, 13989, 13990, 13991, 13992, 13993, 13994, 13995, 13996, 13997, 13998, 13999, 14000, 14001, 14002, 14003, 14004, 14005, 14006, 14007]], + [161, [14054, 0, 14037, 0, 0, 0, 13903, 0, 0, 14047, 14055, 14051, 0, 14050, 0, 14146, 0, 0, 0, 14145, 0, 13902, 14144, 14351, 0, 14048, 14056, 14053, 14052, 14352, 14049, 14191, 14152, 14247, 14153, 14023, 14024, 14027, 14009, 14192, 14025, 14353, 14354, 14193, 14163, 14248, 14355, 14356, 14046, 14194, 14139, 14140, 14155, 14034, 14039, 14038, 14195, 14164, 14357, 14035, 14175, 14358, 14150, 14014, 14041, 14012, 14154, 14013, 14015, 14026, 14016, 14019, 14011, 14017, 14018, 14022, 14042, 14021, 14020, 0, 14045, 14030, 14043, 14028, 14156, 14029, 14389, 14036, 14032, 14044, 14031, 14010, 14176, 14390, 14033, 14259, 14260, 14245, 14246, 14123, 14124, 14125, 14126, 14261, 14262, 14383, 14384, 14157, 14158, 14159, 14160, 14385, 14386, 14387, 14388, 14391, 14392, 0, 0, 14127, 14128, 14161, 14162, 0, 0, 14185, 14186]], + [304, [14187, 14188]], + [313, [14179, 14180, 0, 0, 14181, 14182, 0, 0, 14137, 14138, 14129, 14130, 0, 0, 14165, 14166, 0, 14253, 14254, 0, 0, 0, 0, 14255, 14256, 0, 0, 14183, 14184, 0, 0, 14167, 14168, 14131, 14132, 0, 0, 14189, 14190, 14169, 14170, 0, 0, 14171, 14172, 0, 0, 0, 0, 14141, 14142, 0, 0, 14173, 14174, 14257, 14258]], + [377, [14133, 14134, 14135, 14136, 14177, 14178]], + [402, [14040]], + [536, [14249, 14250, 14251, 14252]], + [913, [14359, 14360, 14361, 14362, 14363, 14364, 14365, 14366, 14367, 14368, 14369, 14370, 14371, 14372, 14373, 14374, 14375, 0, 14376, 14377, 14378, 14379, 14380, 14381, 14382]], + [991, [14403]], + [1024, [14196, 14069, 14197, 14198, 14199, 14200, 14201, 14202, 14203, 14204, 14205, 14206, 14207, 14208, 14209, 14210, 14057, 14059, 14061, 14063, 14065, 14067, 14071, 14073, 14075, 14077, 14079, 14081, 14083, 14085, 14087, 14089, 14091, 14093, 14095, 14097, 14099, 14101, 14103, 14105, 14107, 14109, 14111, 14113, 14115, 14117, 14119, 14121, 14058, 14060, 14062, 14064, 14066, 14068, 14072, 14074, 14076, 14078, 14080, 14082, 14084, 14086, 14088, 14090, 14092, 14094, 14096, 14098, 14100, 14102, 14104, 14106, 14108, 14110, 14112, 14114, 14116, 14118, 14120, 14122, 14211, 14070, 14212, 14213, 14214, 14215, 14216, 14217, 14218, 14219, 14220, 14221, 14222, 14223, 14224, 14225, 14226, 14227, 14228, 14229, 14230, 14231]], + [1136, [14232, 14233, 14234, 14235]], + [1162, [14236, 14237, 14238, 14239, 14240, 14241, 14242, 14243]], + [2960, [14402]], + [4326, [14401]], + [8226, [13889, 0, 0, 0, 14423]], + [8252, [13901]], + [8364, [14143]], + [8482, [14151]], + [8592, [13909, 13906, 13908, 13907, 13911, 13900]], + [8616, [13905]], + [8735, [13910]], + [8962, [14008]], + [9632, [14263, 14264, 14265, 14266, 14267, 14268, 14269, 14270, 14271, 14272, 14273, 14274, 13904, 14275, 14276, 14277, 14278, 14279, 13912, 14280, 14281, 14282, 14283, 14284, 14285, 14286, 13898, 14287, 13913, 14288, 14289, 14290, 14291, 14292, 14293, 14294, 13899, 14295, 14296, 14297, 14298, 14299, 14300, 13891, 14301, 14302, 14303, 14304, 14305, 14306, 14307, 14308, 14309, 14310, 14311, 14312, 13890, 13892, 14313, 14314, 14315, 14316, 14317, 14318, 14319, 14320, 14321, 14322, 14323, 14324, 14325, 14326, 14327, 14328, 14329, 14330, 14331, 14332, 14333, 14334, 14335, 14336, 14337, 14338, 14339, 14340, 14341, 14342, 14343, 14344, 14345, 14346, 14347, 14348, 14349, 14350, 14422, 0, 0, 0, 0, 14393, 14394]], + [9786, [13883, 13884, 13897, 0, 0, 14409, 13894, 14410, 13893]], + [9812, [14417, 14418, 0, 0, 0, 0, 14419, 14420, 0, 0, 0, 0, 13888, 14149, 0, 13887, 0, 13885, 13886, 0, 0, 0, 13895, 13896]], + [9890, [14414, 14415, 14416, 14148, 14413, 14408, 14411, 14412]], + [9906, [14147]], + [10022, [14396, 14397]], + [10032, [14395]], + [10083, [14421, 0, 14400]], + [12298, [14406, 14407, 0, 0, 0, 0, 14404, 14405]], + [21325, [14399, 0, 0, 14398]], + [65281, [14432, 14433, 14434, 14435, 14436, 14437, 14438, 14439, 14440, 14441, 14442, 14443, 14444, 14445, 14446, 14447, 14448, 14449, 14450, 14451, 14452, 14453, 14454, 14455, 14456, 14457, 14458, 14459, 14460, 14461, 14462, 14463, 14464, 14465, 14466, 14467, 14468, 14469, 14470, 14471, 14472, 14473, 14474, 14475, 14476, 14477, 14478, 14479, 14480, 14481, 14482, 14483, 14484, 14485, 14486, 14487, 14488, 14489, 14490, 14491, 14492, 14493, 14494, 14495, 14496, 14497, 14498, 14499, 14500, 14501, 14502, 14503, 14504, 14505, 14506, 14507, 14508, 14509, 14510, 14511, 14512, 14513, 14514, 14515, 14516, 14517, 14518, 14519, 14520, 14521, 14522, 14523, 14524, 14525]], + [128067, [14424]], + [128518, [14430]], + [128528, [14428, 14429]], + [128543, [14431, 14427]], + [128565, [14426]], + [128578, [14425]], +]); + +export const fontMonoPal = createFont(sprites2, [ + [0, [14526]], + [33, [14558, 14559, 14560, 14561, 14562, 14563, 14564, 14565, 14566, 14567, 14568, 14569, 14570, 14571, 14572, 14573, 14574, 14575, 14576, 14577, 14578, 14579, 14580, 14581, 14582, 14583, 14584, 14585, 14586, 14587, 14588, 14589, 14590, 14591, 14592, 14593, 14594, 14595, 14596, 14597, 14598, 14599, 14600, 14601, 14602, 14603, 14604, 14605, 14606, 14607, 14608, 14609, 14610, 14611, 14612, 14613, 14614, 14615, 14616, 14617, 14618, 14619, 14620, 14621, 14622, 14623, 14624, 14625, 14626, 14627, 14628, 14629, 14630, 14631, 14632, 14633, 14634, 14635, 14636, 14637, 14638, 14639, 14640, 14641, 14642, 14643, 14644, 14645, 14646, 14647, 14648, 14649, 14650, 14651]], + [167, [14547]], + [182, [14546]], + [8226, [14533]], + [8252, [14545]], + [8592, [14553, 14550, 14552, 14551, 14555, 14544]], + [8616, [14549]], + [8735, [14554]], + [8962, [14652]], + [9644, [14548]], + [9650, [14556]], + [9658, [14542, 0, 14557]], + [9668, [14543]], + [9675, [14535]], + [9688, [14534, 14536]], + [9786, [14527, 14528, 14541, 0, 0, 0, 14538, 0, 14537]], + [9824, [14532, 0, 0, 14531, 0, 14529, 14530, 0, 0, 0, 14539, 14540]], +]); + +const palettes: Uint32Array[] = createPalettes('0 0000004b 0000004d 00000059 00000081 000000 003f59 005763 00673f 006f19 0075a3 007a25 009152 0092cc 0097ff 00ab30 00ff9b 020202 03246f 036f28 040303 044200 050505 060606 070a0a 073b15 082647 097900 097a2b 09cc37 0a0a0a 0aff44 0b0a0a 0b0b0b 0c0202 0c9800 0d0b16 0d0d0c 0d0d0d 0f0a05 0f0f0f 0f1012 100f0f 101420 10718d 11090c 110a0d 110f0f 111721 111817 11211e 11720a 121212 131212 131313 131414 14182e 142325 151515 154422 161210 161515 1616164f 161616 161c1e 171111 171118 171616 171716 171717 17181a 176300 180909 181108 181211 18130e 181314 181619 181818 191718 191917 191f16 199bcf 19cf50 1a1515 1a181c 1a1a1d 1a2631 1b1514 1b181c 1b1f21 1b2447 1b2631 1b2731 1b4527 1c1b1c 1c2547 1c312d 1d1121 1d1a1a 1d1b1b 1d1c17 1d592e 1e1446 1e1616 1e1e1e 1e1f1e 1f150b 1f1916 1f1919 1f1d1d 1f436c 1f50cc 1fcc5e 20150e 201919 201d1e 201e20 202020 20241c 20671c 211211 211304 21190f 211a1a 211d11 211e23 213ea6 21a65a 222020 222222 222620 224100 224b17 23110f 231818 231e1e 23221a 232222 232323 232627 232728 233c5f 234873 23572e 235f55 236435 24121b 242330 242f26 244c7a 251810 252629 252e30 25494b 26160e 261c10 262122 262626 264284 26576f 271515 271c1d 271f1b 272421 272624 272727 275048 275049 2789cc 2789cd 27a4b7 282225 282425 282526 282626 282727 282827 28443f 28456d 286d62 2889cd 28dc00 291311 291d2b 291e11 291e1e 292929 294f2a 2a201f 2a2025 2a2626 2a274b 2a3932 2a3933 2a4c4b 2a5c2e 2ab45d 2b2116 2b2119 2b2128 2b2228 2b2318 2b2b32 2b2b45 2b2c2a 2b3234 2b473b 2b4bff 2b4e94 2b4e95 2bdfff 2c0605 2c1d18 2c1e1d 2c1f19 2c2c2c 2c354d 2d0706 2d0807 2d1d18 2d1e19 2d1e1f 2d2a2a 2d302b 2d3031 2d3235 2d590b 2d5e2f 2d5f9b 2e0806 2e0807 2e2011 2e2017 2e2221 2e2314 2e2d2e 2e2e2e 2e3130 2e4e49 2e6c42 2f2423 2f2711 2f633e 301414 301a0a 30211c 302528 30302f 303a3d 304708 306639 30930e 311e1c 31261f 31271f 312720 313130 313947 31803e 31cee5 322211 322425 32281f 322820 322e2d 322f35 323130 323230 323231 32332e 326e46 32b8ec 32ed6a 33261f 332624 33272d 333333 334d22 3363cc 33cc66 342624 342a19 343131 343434 346ba6 351f09 352626 35271e 352927 353233 353535 35363a 35383b 361f1f 362716 362826 362a22 362a29 362d1c 362e0e 362f30 363636 372b26 372f19 373f30 37474f 37843f 382e28 382e31 38332f 383637 385cb7 386c4f 386c51 390000 392125 392b2a 392d2a 393536 393636 393b33 394336 3a2b19 3a8246 3b1b25 3b2514 3b2b2a 3b3635 3b3b3b 3b6554 3b6958 3b6f17 3b7bc7 3b8751 3c2915 3c3c3c 3c3e45 3c4e42 3c6655 3d1220 3d2936 3d3523 3d3662 3d3e3c 3d6e41 3e2080 3e2844 3e2d2e 3e322a 3e551f 3e6543 3e9255 3f1d0f 3f1e0f 3f2f2e 3f332c 3f3721 3f3f44 3f4345 3f8c7f 402222 404973 407cff 40ff80 412719 412e2b 41342f 413625 41443e 416609 41660a 417e49 423b3a 423c3b 424138 42473f 427013 42bee7 42bfe8 432210 434141 434e0d 441717 442625 442b28 442e27 442f2d 443026 443433 444240 444444 452a1b 453524 453627 453730 455a64 457d09 45a435 46006b 462a16 462a17 462e2c 46362c 46372c 463e3f 464548 464646 46b727 470809 472e4e 47372c 47372d 474131 474445 47494b 475b5f 47ce12 482410 483621 483622 483a31 48436b 484444 484d52 485c60 488f49 48a105 48a293 493626 493726 49455a 494a48 4a176f 4a3522 4a3925 4a4f53 4a5055 4a79da 4b303b 4b3735 4b4b4b 4b5156 4c4645 4c4b4b 4c4d49 4c4f45 4c8958 4c895a 4e312e 4e3220 4e3e3a 4e534a 4e9d5d 4f2333 4f4036 4f4f4f 4f5246 4f6c1f 4fa5ef 503628 503d45 503e36 506452 51271d 512d08 514042 514e4b 515200 51565b 52235f 52333f 52472f 525252 52585e 529454 53380c 533c35 533c36 534228 540e2b 544a4b 544c4b 54534e 54705e 551928 553b38 55463f 554f4e 555f67 55687d 558a69 560a04 563c3a 56432f 564632 56480f 56b467 57031b 572524 57432d 575c51 57666e 582d77 58352f 583925 583d3a 584331 58463d 585355 585852 585858 586874 588a60 590b13 591202 591a08 592f20 593d3c 594f40 59513a 595652 595852 595e61 5989e7 59e531 5a0104 5a0c13 5a0c14 5a4a32 5a4b32 5a6167 5a7a24 5b4639 5b482c 5b4b42 5b4c32 5b6063 5c0000 5c1818 5c3b27 5c3e0e 5c503f 5c5134 5c5d5e 5c9519 5ca1d5 5ca751 5d2f17 5d5e5c 5da2d5 5e3d2a 5e5647 5e5e5d 5e5e5e 5e912a 5f2823 5f286d 5f3e13 5f5323 5f636a 5f8771 5f8a55 604238 604536 604b37 605856 606359 60635d 607d8b 60c539 613321 6190aa 6244a8 625d41 626b98 62ab64 634d1e 635d52 63635c 644a30 645144 64595a 645d52 64aacc 64ae69 654e44 656200 65827b 663931 666f9c 66896d 66ad51 66c7ed 66ed8f 673149 673e27 674531 676564 676767 67bb77 683f36 684c44 684e36 684f36 684f37 686f73 686f99 68a27b 68cb58 694e43 694f36 696548 696d5f 6a1832 6a39a4 6a570d 6a5800 6a615f 6a6b60 6a6f73 6b0000 6b0028 6b2233 6b4100 6b4834 6b5800 6b6800 6b6e67 6b788e 6bb46d 6c1f1f 6c3d24 6c7160 6c7275 6d2e28 6d3332 6d381a 6d6028 6dbfee 6e006f 6e2715 6e4a13 6e4b13 6e4c49 6e4e37 6e6e6e 6e706c 6e736a 6e7a80 6ea5c5 6ebfef 6f0000 6f0330 6f1681 6f1818 6f1d58 6f2400 6f3000 6f5d42 6f5e42 6f5e43 6f6803 6f6e67 6f6e74 6f6f54 6f6f6f 6f716d 6f8094 701f1c 704426 705b3d 705d43 707a80 70ae6e 70b57e 715547 715a53 71736e 71787b 719b2d 721946 722f2d 723b2e 724313 724b2c 725848 725944 72ae6e 736046 736967 736a51 737570 737869 737f84 73efe8 744070 74452c 745c3f 746442 746f67 747371 747671 74c51d 756139 756b48 757f82 758083 758184 758285 758386 758487 758588 758689 758699 75c075 765b4a 767173 76746c 76756c 76a6ff 76ed5c 770000 772542 774c3e 777971 777a73 778587 78716a 787462 787575 787879 793312 793e29 79443f 796337 797979 797c74 7a2424 7a2700 7a3fa1 7a4b2d 7a6909 7a8180 7a9f1c 7ab045 7affeb 7b1010 7b1438 7b7c74 7b858a 7c3831 7c4d34 7c5f41 7c6148 7c7654 7c9c50 7cb173 7cca92 7d0300 7d7071 7d7b5c 7d7d7d 7d8d95 7e583c 7e5c52 7ed4ff 7f5233 7fc484 80006c 80447e 805f4d 806a47 808080 808878 809034 815947 816a47 818181 822424 823320 82a692 838b8f 83cc4f 83e86e 842a38 843734 84673c 847a67 847e87 84805a 848378 848381 848c90 85234e 855f39 856f58 857243 858b95 858d91 8591a2 85d69e 867059 867a56 868e92 8691a2 8715c4 87564c 875c44 877c6d 877f6a 878c78 878f93 87969c 8797a9 87999f 88725a 888c83 889077 889094 8897bb 889aa0 89452e 895d3f 89624d 899195 899ba1 8a0e0e 8a2247 8a6245 8a7111 8a735c 8a8c86 8a9195 8a9296 8b1e40 8b574a 8b8161 8b8b8b 8c0a07 8c453f 8c4986 8c5421 8c5b3e 8c6c03 8c755d 8c8a90 8c9dbf 8d2b00 8ddcf2 8e0f24 8e3042 8e453f 8e6944 8e8b7c 8ec488 8f0807 8f0d24 8f4d57 8f563b 8f5738 90320b 90563c 907853 908d7c 91310b 91320c 917e53 918665 919783 921308 921509 922427 92508f 927a55 92968a 92c73b 92d097 92e09e 92e6ae 931309 931313 931409 937a55 93d5f5 94594b 94684c 946a26 94725d 948d8e 949800 94d6f6 950000 9553c1 955817 958e6b 95daf0 96563b 9671c1 969693 969eaa 97a0a5 97aac9 983471 984fcc 985a39 9876ff 98a4ab 997621 99775e 99a19f 99a2a7 9a653e 9a9583 9aa3a8 9af437 9b2e2d 9b9977 9b9c88 9ba4a9 9bafcc 9bc464 9c6141 9c8256 9c8a41 9c9c9c 9ca5aa 9d9d9e 9da6ab 9e1e1e 9e6c5c 9e7431 9e8c73 9e967d 9e9784 9e9999 9ea19a 9ebfd8 9f725f 9f855f a09577 a0a0a0 a0a499 a0a691 a0b442 a1051f a1061f a10620 a11616 a13600 a1371b a17d68 a25048 a261ce a2a2a2 a38964 a38a4e a39693 a3a08c a3a29a a3a7c2 a43700 a4ff63 a54e24 a58361 a58a6d a5b4ba a5cfe0 a63447 a83434 a84f97 a8b6d3 a8b8bf a91bf6 a94b41 a9b2ad a9b6d3 a9b7d4 aa3a4d aa9a9b aaa08b aaa696 ab643f ab6e4d ab7666 ab7b60 ababab ac19cf ac7146 acaa89 ad1416 ada6a5 ada896 aeaeae aeb6c3 af784f afb3a7 afbed9 b05911 b0ab8c b0b2ad b19248 b1ac9a b2360f b24717 b29562 b2b9bc b30e0e b38e68 b3b194 b49c61 b4b4b4 b4bcc0 b4ffda b52a2a b5d2e4 b67aff b6b300 b7346c b7b29a b7c410 b81414 b8a880 b9705e ba81b7 ba987f baa27f bbbbbb bc652b bc7625 bc7a62 bcc8c0 bd6a62 bd6c4d bd934d bdcde5 bdea85 be63ff be946e bebc9c becee6 befa82 bf1da7 bf8f7d bfa17a bfbfbf bfc1ba c05151 c08665 c0a165 c0ccc4 c1dcec c1eef0 c28959 c2a910 c2c2ba c3c3c3 c3ff82 c41042 c41254 c41515 c47e15 c4a624 c4a940 c4c012 c54a3a c54b3a c58347 c5d1e6 c63920 c64035 c64059 c68556 c6d2e7 c72424 c73c3b c8381f c93a3a c966ff c9871d c99560 c9b497 c9ecfd ca32ed ca6a2a ca861c ca861d caa175 cab06d caf0ff caf1fd cb871c cbb767 cbbfa8 cc02c9 cc3374 cc4400 cc5151 cc58ab cc5c08 ccb595 ccb89a ccc133 cccbc0 cccccc ccffc4 ccfff7 cd621a cd835c cd8452 cdb16e cde8f0 ce621a ce631a ce631b ce8452 ce9742 cee3ef cf1717 cf1919 cf2626 cf641d cf9742 cf9866 cfb319 d06fca d09742 d14600 d16549 d19f21 d1b683 d366ed d38755 d39741 d39b4a d3b3dc d3c5a7 d3ceab d3d3ba d3dff0 d43737 d4b070 d4c44c d4cdba d56a69 d60a0a d69f74 d6a260 d6bc74 d6caa5 d77099 d7936c d85543 d8ad7e d8dbd6 d8dc00 d91717 d97532 d97e09 d9a066 d9c759 da468c daa056 daeaf2 db5d2b dbc4a3 dc2d2d dcdcdc dd5c2b ddb75f ddcf71 ddd02a dddddd dea1ff deb028 deb02a df807f df917d dfaf28 dfc588 dfd7be e0a455 e0b028 e0b769 e0fffe e13955 e15151 e1c693 e1f8ff e344cb e38926 e3edfa e3f163 e4b065 e4cf7a e4eefb e586df e5b66e e5eefb e5f615 e6337b e6aa86 e6b66e e6b86f e6e000 e6e5d8 e6eefb e6f1f6 e7559b e78149 e7b280 e7b86f e84c31 e8c28e e9bc88 e9d19b eaed58 ebcd90 ec3232 ecb380 ecd132 ed3232 ed6666 ed943b edb480 edd470 ede1c7 ee8138 ee97e9 eec88f eed39f ef8237 f08a7d f095e9 f0bf4c f0e687 f0efe2 f19b37 f1b339 f1d934 f1d935 f281ed f28b1d f2b339 f2d26a f2da34 f38336 f38f41 f39f4b f3ad52 f3ed3a f495bf f5b332 f5d451 f5dae6 f61553 f6176a f61b1b f65a5a f69e1b f6ca05 f6f117 f7db9d f85592 f86754 f8f455 f8f8f8 f9c0ec f9c957 fa9381 fbffad fc20ff fc82b5 fdbb0b fefefe70 fefefe71 feff74 ff0000 ff00cc ff3c80 ff4090 ff5500 ff6666 ff6741 ff6c22 ff6ed6 ff740b ff7af9 ff9f3b ffae70 ffbaba ffc018 ffcd99 ffdb87 ffdd7a ffdfc1 ffe7b4 ffe98f ffea3b ffeb9e fff07f fff240 fff2cc fffd46 fffda4 fffef1 ffff47 ffffff', [ + [0, 1307, 5, 681, 1248, 1172, 1052, 141, 1306, 1216, 1155, 1283, 1277, 1287, 1278, 1016, 906, 720, 14, 16, 721, 182, 721], + [0, 28, 30, 43, 44, 54, 59, 81, 82, 83, 103, 149, 203, 208, 211, 212, 213, 216, 220, 240, 259, 272, 273, 322, 350, 352, 492, 497, 499, 554, 558, 573, 593, 594, 596, 599, 602, 654, 657, 669, 729, 742, 743, 746, 756, 759, 779, 828, 842, 848, 849, 878, 896, 898, 903, 910, 913, 921, 968, 970, 986, 987, 1002, 1007, 1027, 1050, 1070, 1071, 1077, 1078, 1083, 1090, 1093, 1096, 1106, 1111, 1119, 1121, 1124, 1128, 1129, 1131, 1132, 1134, 1150, 1152, 1156, 1157, 1160, 1162, 1173, 1181, 1188, 1194, 1200, 1209, 1211, 1212, 1220, 1221, 1222, 1223, 1232, 1233, 1236, 1247, 1251, 1254, 1262, 1268, 1279, 1281, 1290, 1294, 1296, 1302, 1307], + [0, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307], + [0, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264], + [0, 1295, 1295, 1295, 1292, 1292, 1288, 1288, 1288, 1158, 1158], + [0, 1307, 1307, 1307, 1304, 1304, 1298, 1298, 1298, 1273, 1273], + [0, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307], + [0, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264], + [0, 1295, 1295, 1292, 1292, 1288, 1288, 1158, 1158, 1158], + [0, 1307, 1307, 1304, 1304, 1298, 1298, 1273, 1273, 1273], + [0, 763], + [0, 1275], + [0, 1274, 1275], + [0, 1274], + [0, 1, 2, 62, 172, 195, 206, 256, 331, 403, 404, 491, 495, 603, 604, 672, 777, 879, 939, 1013, 1049, 1148], + [0, 1, 2, 62, 190, 207, 238, 256, 351, 403, 404, 562, 495, 603, 604, 672, 777, 1235, 939, 1270, 1049, 1148], + [0, 1, 2, 62, 343, 847, 575, 256, 997, 402, 404, 1195, 495, 601, 604, 672, 938, 1035, 938, 1198, 1048, 1146], + [0, 1, 2, 62, 167, 168, 193, 194, 247, 254, 255, 256, 313, 314, 403, 404, 429, 430, 447, 448, 529, 530, 603, 604, 608, 659, 660, 661, 674, 688, 777, 879, 939, 1013, 1049, 1148], + [0, 1, 2, 62, 820, 820, 590, 590, 343, 254, 255, 256, 977, 977, 402, 404, 429, 430, 1073, 1073, 529, 530, 601, 604, 902, 659, 660, 661, 1195, 1195, 776, 1035, 938, 1198, 1048, 1146], + [0, 1, 2, 62, 998, 998, 865, 865, 515, 254, 255, 256, 1091, 1091, 403, 404, 429, 430, 1182, 1182, 529, 530, 603, 604, 1092, 659, 660, 661, 1245, 1245, 777, 1235, 939, 1270, 1049, 1148], + [0, 1, 2, 62, 1003, 1003, 880, 880, 526, 254, 255, 256, 1112, 1112, 403, 404, 429, 430, 1190, 1190, 529, 530, 603, 604, 1092, 659, 660, 661, 1252, 1252, 777, 1235, 939, 1270, 1049, 1148], + [0, 1, 2, 62, 990, 990, 857, 857, 498, 254, 255, 256, 1076, 1076, 403, 404, 429, 430, 1168, 1168, 529, 530, 603, 604, 1092, 659, 660, 661, 1246, 1246, 777, 1235, 939, 1270, 1049, 1148], + [0, 402, 601, 776, 902, 938, 1035, 1040, 1048, 1146, 1198], + [0, 5, 24, 57, 64, 95, 110, 117, 153, 183, 236, 249, 290, 311, 414, 489, 509, 589, 717, 859, 1159], + [0, 122, 286, 465, 684], + [0, 275, 488, 504, 724, 731, 886, 927, 1038, 1055], + [0, 105, 201, 309, 318, 466, 507, 676, 698, 863, 939, 1110], + [0, 54, 73, 78, 123, 151, 156, 185, 233, 235, 281, 295, 323, 419, 434, 435, 534, 671, 923, 1010], + [0, 151, 233, 434, 534, 671, 923, 1010], + [0, 242, 304, 357, 934, 940, 1143], + [0, 54, 60, 73, 164, 202, 310, 357, 413, 434, 534, 940, 956, 1143, 1149, 1180], + [0, 34, 48, 49, 50, 66, 74, 97, 98, 121, 125, 142, 145, 161, 178, 179, 180, 239, 253, 347, 360, 385, 406, 410, 418, 428, 449, 470, 556, 557, 559, 635, 637, 638, 682, 741, 786, 840, 846, 952, 983], + [0, 69, 163, 213, 220, 221, 302, 477, 478, 542, 602, 795, 932, 1100, 1133], + [0, 896, 970, 1090, 1233, 1267], + [0, 8, 11, 12, 15, 36, 89, 172, 192, 195, 206, 252, 331, 343, 417, 454, 480, 491, 560, 575, 641, 694, 704, 723, 732, 748, 798, 838, 847, 950, 969, 975, 994, 997, 1001, 1004, 1056, 1195, 1225, 1293, 1307], + [0, 353, 642, 1122, 1126, 1202, 1211, 1229], + [0, 643, 1126, 1203], + [0, 80, 96, 170, 210, 376, 379, 567, 695, 791, 1184], + [675, 758, 879, 1013], + [0, 69, 158, 213, 216, 220, 221, 302, 473, 477, 478, 599, 602, 986], + [0, 94, 243, 324, 340, 614, 785, 968], + [0, 108, 384, 490, 518, 520, 540, 543, 551, 597, 625, 668, 705, 714, 761, 771, 814, 837, 887, 899, 917, 931, 933, 951, 1009, 1034, 1138, 1169, 1244], + [0, 700, 1185, 1205], + [0, 133, 492, 499, 558, 592, 595, 779, 851, 855, 872, 974, 1030, 1070, 1128, 1160, 1162, 1296], + [0, 5, 24, 57, 64, 95, 110, 117, 153, 183, 222, 236, 249, 290, 311, 414, 489, 509, 589, 717, 859, 1159], + [0, 5, 95, 110, 183, 222, 290, 311, 414, 589, 859, 1159], + [0, 300, 468, 496, 616, 890, 1063, 1253], + [0, 136, 262, 506, 716, 747, 827], + [0, 5, 40, 106, 248, 348, 432, 549, 644, 718, 719, 864, 888, 1044], + [0, 5, 17, 20, 22, 23, 32, 33, 42, 47, 53, 61, 75, 77, 79, 85, 99, 100, 116, 126, 129, 173, 174, 175, 191, 199, 223, 234, 265, 283, 296, 303, 320, 327, 328, 349, 355, 356, 374, 395, 421, 423, 440, 443, 455, 482, 508, 581, 618], + [0, 94, 251, 324, 325, 337, 484, 835, 1050], + [0, 219, 350, 371, 744, 756, 921, 1036], + [0, 69, 213, 216, 220, 221, 302, 473, 477, 599, 602, 986], + [0, 1307], + [0, 69, 131, 187, 215, 302, 338, 462, 463, 561, 586], + [0, 242, 304, 357, 361, 373, 690, 734, 836, 930, 934, 940, 957, 1043, 1143], + [0, 244, 278, 383, 539, 929, 1120], + [0, 1, 948, 1140, 1186], + [0, 291, 316, 391, 636, 730, 852, 991], + [0, 69, 157, 187, 215, 301, 302, 405, 462, 481, 582, 586, 760, 979], + [0, 130, 805, 822, 900, 902, 982, 1035, 1115, 1198, 1224], + [0, 78], + [197, 1014], + [5], + [0, 69, 216, 220, 291, 302, 316, 391, 473, 478, 599, 602, 636, 730, 852, 986, 991], + [0, 138, 275, 504, 552, 724, 788, 886, 980, 1136, 1145, 1226], + [0, 52, 118, 232, 261, 276, 282, 299, 335, 336, 368, 420, 479, 494, 511, 512, 577, 675, 736, 758, 787, 954, 993, 1013], + [0, 139, 275, 504, 724, 808, 886], + [0, 218, 370, 514, 744, 945, 1065, 1269], + [0, 218, 370, 227, 744, 427, 570, 1041], + [0, 40, 56, 90, 184, 204, 217, 341, 362, 471, 607, 858, 960, 1032, 1289], + [0, 80, 200, 201, 218, 219, 224, 227, 300, 318, 342, 350, 370, 371, 376, 411, 427, 468, 472, 496, 507, 514, 567, 570, 603, 616, 669, 698, 699, 744, 756, 777, 791, 839, 844, 863, 867, 890, 921, 939, 945, 1005, 1021, 1028, 1036, 1041, 1054, 1063, 1065, 1110, 1118, 1156, 1220, 1237, 1253, 1269, 1299], + [0, 778, 856, 1218, 1307], + [0, 382, 657, 775, 944, 961, 1096, 1284], + [0, 377, 739, 555, 745, 949, 1127, 1284], + [0, 2, 144, 188, 271, 334, 372, 453, 475, 600, 630], + [0, 245, 353, 354, 400, 401, 450, 476, 505, 541, 642, 643, 670, 767, 778, 856, 912, 967, 1074, 1089, 1116, 1122, 1126, 1167, 1197, 1203, 1211, 1218, 1307], + [0, 58, 65, 104, 135, 158, 162, 186, 214, 262, 287, 297, 329, 366, 387, 412, 486, 493, 506, 533, 610, 644, 686, 765, 888], + [0, 58, 65, 104, 135, 158, 76, 84, 88, 109, 124, 297, 329, 366, 189, 412, 289, 493, 298, 308, 367, 389, 535, 451, 535], + [0, 91, 96, 169, 170, 181, 209, 210, 378, 379, 606, 695, 825, 833, 834, 926, 928, 1184], + [0, 409, 631, 738, 916, 1075], + [0, 26, 111, 150, 229, 333], + [0, 250, 458, 532, 680, 876], + [0, 403, 683, 823, 897, 982, 1109, 1219], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 332, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 783, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 962], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 769, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 1042, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1189], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 624, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1061, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 1259, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 399, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 806, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 973, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 19, 112, 113, 127, 160, 171, 260, 280, 332, 364, 364, 433, 584, 587, 621, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1058, 1037, 1060, 1064, 1098, 1099, 1102, 1104, 1255, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 9, 584, 587, 621, 627, 656, 658, 662, 640, 766, 783, 29, 962, 1017, 1020, 31, 1060, 1064, 1098, 1099, 1102, 1094, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1271], + [0, 6, 7, 10, 13, 19, 113, 113, 128, 160, 171, 260, 279, 332, 364, 364, 433, 584, 587, 621, 622, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1059, 1098, 1099, 1102, 1104, 1199, 1204, 1257, 1256, 1263, 1285, 1286, 1301, 1307], + [0, 6, 71, 10, 13, 18, 112, 113, 127, 160, 408, 525, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 653, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1095, 1104, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1280, 1307], + [0, 102, 143, 146, 201, 285, 307, 318, 459, 485, 507, 547, 623, 698, 863, 939, 978, 1110, 1258], + [0, 2, 277, 822, 982, 1115, 1224], + [0, 277, 629, 800, 820, 822, 977, 982, 1068, 1115, 1198, 1206, 1224], + [0, 2, 3, 743, 995, 1031, 1147, 1193, 1307], + [0, 839, 844, 1021, 1054, 1118, 1156, 1220], + [0, 797, 794, 1088, 1018, 1179, 1215, 1208], + [0, 159, 196, 228, 312, 345, 426, 438, 503, 524, 576, 585, 715, 740, 751, 768, 794, 797, 841, 877, 893, 953, 1018, 1047, 1088, 1125, 1144, 1161, 1176, 1179, 1196, 1208, 1215, 1228], + [0, 503, 740, 841, 893, 953, 1125, 1196, 1228], + [0, 794, 1018, 797, 1161, 1208, 1088, 1179, 1215], + [0, 159, 312, 345, 438, 524, 585, 768, 877], + [0, 196, 426, 751, 576, 715, 1047, 1144, 1176], + [0, 43, 81, 103, 149, 322, 346, 422, 573, 615, 898, 1135], + [0, 25, 147, 120, 294, 439, 398, 51, 609, 784, 784, 1105], + [0, 467, 516, 780, 810, 963, 992, 1084, 1142, 1247, 1249, 1300], + [0, 87, 92, 93, 154, 330, 339, 513, 757], + [0, 87, 148, 93, 258, 416, 425, 588, 781], + [0, 865, 1091, 1182, 1245], + [0, 870, 1112, 1190, 1243], + [0, 857, 1076, 1168, 1231], + [0, 256, 753, 865, 880, 883, 998, 1091, 1182, 1245], + [0, 256, 753, 870, 880, 883, 1003, 1112, 1190, 1243], + [0, 256, 753, 857, 880, 883, 990, 1076, 1168, 1246], + [0, 857, 865, 880, 1076, 1091, 1112, 1168, 1182, 1190, 1231, 1243, 1245], + [0, 830, 1063, 1253, 1297], + [0, 397, 703, 915, 1057], + [0, 41, 70, 86, 115, 126, 152, 173, 174, 199, 266, 292, 319, 355, 358, 423, 440, 508, 566, 581, 618], + [0, 218, 370, 669, 744, 1028, 1237, 1299], + [0, 39, 107, 198, 393, 500, 696, 754, 873, 1024], + [0, 39, 107, 198, 393, 500, 306, 754, 396, 569], + [0, 380, 632, 752, 860, 922], + [0, 326, 632, 645, 764, 829], + [0, 467, 516, 780, 810, 963, 992, 1084, 1247, 1249, 1300], + [0, 578, 583, 728, 854, 981, 1093], + [0, 415, 415, 531, 713, 826, 966], + [0, 1, 5, 130, 822, 982, 1115, 1224], + [0, 590, 820, 971, 976, 1035, 1068, 1073, 1139, 1195], + [0, 502, 649, 907, 782, 907, 907, 907, 928, 972], + [0, 21, 27, 35, 63, 67, 68, 69, 132, 166, 167, 176, 177, 187, 216, 230, 231, 257, 263, 264, 267, 268, 269, 277, 284, 313, 315, 411, 441, 444, 447, 456, 527, 528, 536, 538, 545, 553, 554, 611, 617, 626, 652, 655, 659, 666, 688, 722, 753, 777, 850, 861, 866, 871, 874, 882, 892, 894, 902, 946, 947, 1011, 1035, 1066, 1069, 1079, 1085, 1086, 1097, 1107, 1112, 1113, 1114, 1164, 1166, 1174, 1175, 1178, 1190, 1195, 1206, 1227, 1238, 1239, 1240, 1242, 1243, 1260, 1269, 1277, 1282, 1307], + [0, 293, 415, 537, 634, 750], + [0, 519, 574, 612, 691, 755, 790, 803, 837, 868, 895, 917, 989, 999, 1039, 1093, 1137], + [0, 519, 691, 820, 837, 917, 976, 977, 999, 1068, 1093, 1191, 1206], + [0, 517, 548, 571, 697, 733, 807, 862, 905, 984, 1033, 1067, 1108, 1151, 1201], + [0, 483, 619, 725, 817, 936, 1046, 1154], + [0, 69, 155, 213, 216, 220, 221, 291, 316, 391, 392, 464, 473, 477, 599, 602, 636, 685, 730, 843, 852, 885, 986, 991, 1023, 1072, 1177], + [0, 114, 288, 605, 772, 939], + [0, 683, 897, 988, 1053, 1123, 1210], + [0, 908, 1062, 1171, 1250, 1303, 1276], + [0, 683, 820, 897, 977, 988, 1053, 1068, 1123, 1206, 1210], + [0, 770, 1241], + [0, 664, 762, 935, 1045], + [0, 237, 407], + [0, 510, 522, 663, 881, 959, 1103, 1272, 1305], + [0, 101, 137, 375, 1291], + [0, 119, 305, 735, 1029, 1192], + [0, 274, 487, 687, 964, 1153, 1209, 1234, 1236, 1268], + [0, 546, 550, 891, 1082], + [0, 80, 342, 376, 472, 567, 699, 791, 867, 1209, 1236, 1268], + [0, 546, 639, 651, 884, 891], + [0, 550, 639, 891, 1082], + [0, 664, 762, 889, 1045], + [0, 134, 365, 460, 546, 651, 891], + [0, 762, 1266], + [0, 5, 521, 646, 762, 789, 925, 941, 1045, 1104, 1266, 1307], + [0, 546, 639, 891], + [0, 91, 96, 170, 210, 379, 550, 639, 651, 695, 891, 1184], + [0, 205, 344, 369, 452, 501, 549, 550, 639, 648, 651, 774, 891, 918], + [], + [0, 1, 4, 45, 46, 241, 246, 317, 386, 388, 394, 461, 563, 564, 565, 580, 677, 689, 796, 802, 816, 824, 831, 845, 853, 955, 965, 985, 1080, 1081, 1213], + [0, 1, 4, 37, 38, 165, 269, 390, 445, 568, 579, 628, 647, 667, 678, 692, 702, 726, 737, 749, 832, 875, 942, 996, 1000], + [675, 758, 774, 801, 879, 1013, 1022, 1183, 1217, 1262], + [1092, 1170, 809, 1170, 1235, 1270, 1022, 1141, 1187, 1230], + [909, 1025, 1026, 1035, 1101, 1139, 1165, 1195, 1198], + [687, 1008, 1087, 1153, 1214], + [55, 72, 225, 359, 523, 679, 834, 928, 1012], + [270, 321, 381, 446, 457, 598, 613, 633, 665, 693, 701, 792, 811, 818, 869, 943], + [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1130, 1183, 1217, 1262], + [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1130, 1141, 1187, 1230], + [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1026, 1026, 1101, 1165], + [544, 550, 572, 639, 620, 650, 773, 727, 891, 813, 673, 673, 834, 928], + [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1130, 1163, 1183, 1207, 1217, 1262], + [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1141, 1163, 1141, 1207, 1187, 1230], + [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1026, 1163, 1026, 1207, 1101, 1165], + [0, 2, 675, 758, 774, 801, 879, 1013], + [977, 1015, 1035, 1051, 1117, 1139, 1163, 1195, 1198], + [277, 424, 436, 437, 442, 469, 474, 531, 606, 706, 707, 708, 709, 710, 711, 712, 713, 782, 793, 799, 804, 812, 815, 819, 821, 825, 826, 834, 901, 911, 914, 919, 926, 928, 1006], + [0, 431, 549, 591, 675, 718, 758, 864, 879, 958, 1013, 1019], + [0, 431, 549, 924, 1092, 718, 1170, 864, 1235, 958, 1270, 1019], + [0, 431, 549, 920, 1035, 718, 1195, 864, 1139, 958, 1307, 1019], + [0, 5, 140, 226, 415, 531, 537, 606, 713, 826, 966] +]); + +export const emptySprite = sprites[0]; +export const emptySprite2 = sprites2[0]; + +export const defaultPalette = palettes[0]; +export const emojiPalette = palettes[1]; +export const fontPalette = palettes[2]; +export const fontSupporter1Palette = palettes[3]; +export const fontSupporter2Palette = palettes[4]; +export const fontSupporter3Palette = palettes[5]; +export const fontSmallPalette = palettes[6]; +export const fontSmallSupporter1Palette = palettes[7]; +export const fontSmallSupporter2Palette = palettes[8]; +export const fontSmallSupporter3Palette = palettes[9]; + +export const spriteSheets: SpriteSheet[] = [ + { + src: 'images/pony.png', + data: undefined, texture: undefined, sprites: sprites, palette: false + }, + { + src: 'images/pony2.png', srcA: 'images/pony2a.png', + data: undefined, texture: undefined, sprites: sprites2, palette: true + }, +]; + +export const normalSpriteSheet = spriteSheets[0]; +export const paletteSpriteSheet = spriteSheets[1]; + +export function createSprites(data: string): Sprite[] { + const sprites: Sprite[] = [ + { x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0, type: 0 }, + ]; + + let offset = 0; + const read = bitReaderCustom(() => { + const value = parseInt(data.substr(offset, 2), 16); + offset += 2; + return value; + }); + + while (offset < data.length) { + sprites.push({ + x: read(12), + y: read(12), + w: read(9), + h: read(9), + ox: read(8), + oy: read(8), + type: read(6), + }); + } + + return sprites; +} + +export function createFont(sprites: Sprite[], groups: [number, number[]][]) { + const chars: { code: number; sprite: Sprite; }[] = []; + + for (const [start, codes] of groups) { + for (let i = 0; i < codes.length; i++) { + if (codes[i]) { + chars.push({ code: start + i, sprite: sprites[codes[i]] }); + } + } + } + + return chars; +} + +export function createButton( + border: number, topLeft: number, top: number, topRight: number, left: number, bg: number, right: number, + bottomLeft: number, bottom: number, bottomRight: number +): SpriteBorder { + return { + border, + topLeft: sprites[topLeft], + top: sprites[top], + topRight: sprites[topRight], + left: sprites[left], + bg: sprites[bg], + right: sprites[right], + bottomLeft: sprites[bottomLeft], + bottom: sprites[bottom], + bottomRight: sprites[bottomRight] + }; +} + +export function mapSprites(frames: number[]) { + return frames.map(i => sprites[i]); +} + +export function mapSprites2(frames: number[]) { + return frames.map(i => sprites2[i]); +} + +export function createPalettes(colorsString: string, palettes: number[][]): Uint32Array[] { + const colors = colorsString.split(/ /g).map(parseSpriteColor); + + return palettes.map(palette => { + const result = new Uint32Array(palette.length); + + for (let i = 0; i < palette.length; i++) { + result[i] = colors[palette[i]] >>> 0; + } + + return result; + }); +} + +export function createColorPal(color: number, colors: number): ColorExtra { + return { color: sprites2[color], colors }; +} + +export function colorPal(colors: number) { + return (color: number) => createColorPal(color, colors); +} + +const colorPal3 = colorPal(3); +const colorPal5 = colorPal(5); +const colorPal7 = colorPal(7); +const colorPal9 = colorPal(9); +const colorPal11 = colorPal(11); +const colorPal13 = colorPal(13); +const colorPal17 = colorPal(17); + +export function getPalette(index: number) { + return palettes[index]; +} + +const emptyPalette = new Uint32Array(0); + +export function emptyColorPalette(): ColorExtra { + return { color: sprites2[0], palettes: [emptyPalette] }; +} + +export function createSpritesPalette(sprites: number[], paletteIndexes: number[]): TileSprites { + return { sprites: sprites.map(i => sprites2[i]), palettes: paletteIndexes.map(getPalette) }; +} + +export function createColorPalette(color: number, paletteIndexes: number[]): ColorExtra { + return { color: sprites2[color], palettes: paletteIndexes.map(getPalette) }; +} + +export function createColorExtraPal(color: number, colors: number, extra: number, paletteIndexes: number[]): ColorExtra { + return { color: sprites2[color], colors, extra: sprites2[extra], palettes: paletteIndexes.map(getPalette) }; +} + +export function createShadow(shadow: number): Shadow { + return { shadow: sprites2[shadow] }; +} + +export function createColorShadowPalette(color: number, shadow: number, paletteIndexes: number[]): ColorShadow { + return { color: sprites2[color], shadow: sprites2[shadow], palettes: paletteIndexes.map(getPalette) }; +} + +export function createNose(color: number, colors: number, mouth: number, fangs: number) { + return { color: sprites2[color], colors, mouth: sprites2[mouth], fangs: sprites2[fangs] }; +} + +export function createEye(base: number, irises: number[], shadow?: number, shine?: number): PonyEye { + return { base: sprites2[base], irises: mapSprites2(irises), shadow: sprites2[shadow || 0], shine: sprites2[shine || 0] }; +} + +export function createAnimation(frames: number[]) { + return { frames: mapSprites(frames) }; +} + +export function createAnimationPalette(frames: number[], palette: number) { + return { frames: mapSprites2(frames), palette: getPalette(palette) }; +} + +export function createAnimationShadow(frames: number[], shadow: number, palette: number) { + return { frames: mapSprites2(frames), shadow: sprites2[shadow], palette: getPalette(palette) }; +} + +export { + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, + colorPal3, colorPal5, colorPal7, colorPal9, colorPal11, colorPal13, colorPal17 +}; +export const bubble = createButton(2, 1, 2, 3, 4, 5, 6, 7, 8, 9); +export const bubble2 = createButton(2, 10, 11, 12, 13, 14, 15, 16, 17, 18); +export const dirt_ice_heightmap = sprites[19]; +export const dirt_stone_cave_height_map = sprites[20]; +export const dirt_water_heightmap = sprites[21]; +export const firefly_light = sprites[22]; +export const grass = sprites[23]; +export const jacko_lantern_light = sprites[24]; +export const nipple = sprites[25]; +export const pumpkin_light = sprites[26]; +export const cat_light = createAnimation([27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43]); +export const lantern_light = createAnimation([44, 45, 46, 47, 48, 49, 50, 51]); +export const magic_light = createAnimation([52, 53, 54, 55, 56, 57]); +export const ghost1_hooves_light = createAnimation([0, 0, 0, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 0, 0, 0, 0]); +export const ghost1_light = createAnimation([98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 0, 0, 0, 0, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159]); +export const water_crystal_1_light = createAnimation([160, 161, 162, 163]); +export const magic2_light = createAnimation([164, 165, 166, 167, 168, 169]); +export const torch2_light = createAnimation([170, 171, 172, 173, 174, 175, 176]); +export const water_crystal_2_light = createAnimation([177, 178, 179, 180]); +export const water_crystal_3_light = createAnimation([181, 182, 183, 184]); +export const pixelRect = sprites[185]; +export const light1 = sprites[2038]; +export const light16 = sprites[2039]; +export const light24 = sprites[2040]; +export const light32 = sprites[2041]; +export const light6 = sprites[2042]; +export const light_crystals_1 = sprites[2043]; +export const light_crystals_10 = sprites[2044]; +export const light_crystals_2 = sprites[2045]; +export const light_crystals_3 = sprites[2046]; +export const light_crystals_4 = sprites[2047]; +export const light_crystals_5 = sprites[2048]; +export const light_crystals_6 = sprites[2049]; +export const light_crystals_7 = sprites[2050]; +export const light_crystals_8 = sprites[2051]; +export const light_crystals_9 = sprites[2052]; +export const light_crystals_cart_pile = sprites[2053]; +export const light_crystals_held = sprites[2054]; +export const light_crystal_lantern = sprites[2055]; +export const pixel = sprites[2056]; +export const eyeRight: PonyEyes = [ + undefined, + [createEye(2, [3, 4, 5, 6, 7, 8, 9, 10], 1, 11), createEye(13, [14, 15, 16, 17, 18, 19, 20, 21], 12, 22), createEye(24, [25, 26, 27, 28, 29, 30, 31, 32], 23, 33), createEye(35, [36, 37, 38, 39, 40, 41, 42, 43], 34, 44)], + [createEye(45, [46, 47, 48, 49, 50, 51, 52, 53], 54, 55), createEye(56, [57, 58, 59, 60, 61, 62, 63, 64], 65, 66), createEye(67, [68, 69, 70, 71, 72, 73, 74, 75], 76, 77), createEye(78, [79, 80, 81, 82, 83, 84, 85, 86], 87, 88)], + [createEye(89, [90, 91, 92, 93, 94, 95, 96, 97], 98, 99), createEye(100, [101, 102, 103, 104, 105, 106, 107, 108], 109, 110), createEye(111, [112, 113, 114, 115, 116, 117, 118, 119], 120, 121), createEye(122, [123, 124, 125, 126, 127, 128, 129, 130], 131, 132)], + [createEye(133, [134, 0, 135, 136, 0, 0, 0, 137], 138, 139), createEye(140, [141, 0, 142, 143, 0, 0, 0, 144], 145, 146), createEye(147, [148, 0, 149, 150, 0, 0, 0, 151], 152, 153), createEye(154, [155, 0, 156, 157, 0, 0, 0, 158], 159, 160)], + [createEye(161, [0, 0, 0, 0, 0, 0, 0, 0], 162, 163), createEye(164, [0, 0, 0, 0, 0, 0, 0, 0], 165, 166), createEye(167, [0, 0, 0, 0, 0, 0, 0, 0], 168, 169), createEye(170, [0, 0, 0, 0, 0, 0, 0, 0], 171, 172)], + [createEye(173, [0, 0, 0, 0, 0, 0, 0, 0], 174, 175), createEye(176, [0, 0, 0, 0, 0, 0, 0, 0], 177, 178), createEye(179, [0, 0, 0, 0, 0, 0, 0, 0], 180, 181), createEye(182, [0, 0, 0, 0, 0, 0, 0, 0], 183, 184)], + [createEye(185, [186, 187, 188, 189, 190, 191, 192, 193], 194, 195), createEye(196, [197, 198, 199, 200, 201, 202, 203, 204], 205, 206), createEye(207, [208, 209, 210, 211, 212, 213, 214, 215], 216, 217), createEye(218, [219, 220, 221, 222, 223, 224, 225, 226], 227, 228)], + [createEye(229, [230, 231, 232, 233, 234, 235, 236, 237], 238, 239), createEye(240, [241, 242, 243, 244, 245, 246, 247, 248], 249, 250), createEye(251, [252, 253, 254, 255, 256, 257, 258, 259], 260, 261), createEye(262, [263, 264, 265, 266, 267, 268, 269, 270], 271, 272)], + [createEye(273, [274, 0, 275, 276, 0, 0, 0, 277], 278, 279), createEye(280, [281, 0, 282, 283, 0, 0, 0, 284], 285, 286), createEye(287, [288, 0, 289, 290, 0, 0, 0, 291], 292, 293), createEye(294, [295, 0, 296, 297, 0, 0, 0, 298], 299, 300)], + [createEye(301, [0, 0, 0, 0, 0, 0, 0, 0], 302, 303), createEye(304, [0, 0, 0, 0, 0, 0, 0, 0], 305, 306), createEye(307, [0, 0, 0, 0, 0, 0, 0, 0], 308, 309), createEye(310, [0, 0, 0, 0, 0, 0, 0, 0], 311, 312)], + [createEye(314, [0, 0, 0, 0, 0, 0, 0, 0], 313, 315), createEye(317, [0, 0, 0, 0, 0, 0, 0, 0], 316, 318), createEye(320, [0, 0, 0, 0, 0, 0, 0, 0], 319, 321), createEye(323, [0, 0, 0, 0, 0, 0, 0, 0], 322, 324)], + [createEye(326, [0, 0, 0, 0, 0, 0, 0, 0], 325, 327), createEye(329, [0, 0, 0, 0, 0, 0, 0, 0], 328, 330), createEye(332, [0, 0, 0, 0, 0, 0, 0, 0], 331, 333), createEye(335, [0, 0, 0, 0, 0, 0, 0, 0], 334, 336)], + [createEye(338, [0, 0, 0, 0, 0, 0, 0, 0], 337, 339), createEye(341, [0, 0, 0, 0, 0, 0, 0, 0], 340, 342), createEye(344, [0, 0, 0, 0, 0, 0, 0, 0], 343, 345), createEye(347, [0, 0, 0, 0, 0, 0, 0, 0], 346, 348)], + [createEye(350, [0, 0, 0, 0, 0, 0, 0, 0], 349, 351), createEye(353, [0, 0, 0, 0, 0, 0, 0, 0], 352, 354), createEye(356, [0, 0, 0, 0, 0, 0, 0, 0], 355, 357), createEye(359, [0, 0, 0, 0, 0, 0, 0, 0], 358, 360)], + [createEye(361, [362, 363, 364, 365, 366, 367, 368, 369], 370, 371), createEye(372, [373, 374, 375, 376, 377, 378, 379, 380], 381, 382), createEye(383, [384, 385, 386, 387, 388, 389, 390, 391], 392, 393), createEye(394, [395, 396, 397, 398, 399, 400, 401, 402], 403, 404)], + [createEye(405, [406, 407, 408, 409, 410, 411, 412, 413], 414, 415), createEye(416, [417, 418, 419, 420, 421, 422, 423, 424], 425, 426), createEye(427, [428, 429, 430, 431, 432, 433, 434, 435], 436, 437), createEye(438, [439, 440, 441, 442, 443, 444, 445, 446], 447, 448)], + [createEye(449, [450, 451, 452, 453, 454, 0, 455, 456], 457, 458), createEye(459, [460, 461, 462, 463, 464, 0, 465, 466], 467, 468), createEye(469, [470, 471, 472, 473, 474, 0, 475, 476], 477, 478), createEye(479, [480, 481, 482, 483, 484, 0, 485, 486], 487, 488)], + [createEye(489, [490, 0, 491, 492, 0, 0, 0, 493], 494, 495), createEye(496, [497, 0, 498, 499, 0, 0, 0, 500], 501, 502), createEye(503, [504, 0, 505, 506, 0, 0, 0, 507], 508, 509), createEye(510, [511, 0, 512, 513, 0, 0, 0, 514], 515, 516)], + [createEye(517, [518, 519, 520, 521, 522, 523, 524, 525], 526, 527), createEye(528, [529, 530, 531, 532, 533, 534, 535, 536], 537, 538), createEye(539, [540, 541, 542, 543, 544, 545, 546, 547], 548, 549), createEye(550, [551, 552, 553, 554, 555, 556, 557, 558], 559, 560)], + [createEye(561, [562, 0, 563, 564, 0, 565, 0, 566], 567, 568), createEye(569, [570, 0, 571, 572, 0, 573, 0, 574], 575, 576), createEye(577, [578, 0, 579, 580, 0, 581, 0, 582], 583, 584), createEye(585, [586, 0, 587, 588, 0, 589, 0, 590], 591, 592)], + [createEye(594, [0, 0, 0, 0, 0, 0, 0, 0], 593, 595), createEye(597, [0, 0, 0, 0, 0, 0, 0, 0], 596, 598), createEye(600, [0, 0, 0, 0, 0, 0, 0, 0], 599, 601), createEye(603, [0, 0, 0, 0, 0, 0, 0, 0], 602, 604)], + [createEye(606, [0, 0, 0, 0, 0, 0, 0, 0], 605, 0), createEye(608, [0, 0, 0, 0, 0, 0, 0, 0], 607, 0), createEye(610, [0, 0, 0, 0, 0, 0, 0, 0], 609, 0), createEye(612, [0, 0, 0, 0, 0, 0, 0, 0], 611, 0)], + [createEye(614, [0, 0, 0, 0, 0, 0, 0, 0], 613, 0), createEye(616, [0, 0, 0, 0, 0, 0, 0, 0], 615, 0), createEye(618, [0, 0, 0, 0, 0, 0, 0, 0], 617, 0), createEye(620, [0, 0, 0, 0, 0, 0, 0, 0], 619, 0)], + [createEye(622, [0, 0, 0, 0, 0, 0, 0, 0], 621, 0), createEye(624, [0, 0, 0, 0, 0, 0, 0, 0], 623, 0), createEye(626, [0, 0, 0, 0, 0, 0, 0, 0], 625, 0), createEye(628, [0, 0, 0, 0, 0, 0, 0, 0], 627, 0)] +]; +export const eyeLeft: PonyEyes = [ + undefined, + [createEye(630, [631, 632, 633, 634, 635, 636, 637, 638], 629, 639), createEye(641, [642, 643, 644, 645, 646, 647, 648, 649], 640, 650), createEye(652, [653, 654, 655, 656, 657, 658, 659, 660], 651, 661), createEye(663, [664, 665, 666, 667, 668, 669, 670, 671], 662, 672)], + [createEye(673, [674, 675, 676, 677, 678, 679, 680, 681], 682, 683), createEye(684, [685, 686, 687, 688, 689, 690, 691, 692], 693, 694), createEye(695, [696, 697, 698, 699, 700, 701, 702, 703], 704, 705), createEye(706, [707, 708, 709, 710, 711, 712, 713, 714], 715, 716)], + [createEye(717, [718, 719, 720, 721, 722, 723, 724, 725], 726, 727), createEye(728, [729, 730, 731, 732, 733, 734, 735, 736], 737, 738), createEye(739, [740, 741, 742, 743, 744, 745, 746, 747], 748, 749), createEye(750, [751, 752, 753, 754, 755, 756, 757, 758], 759, 760)], + [createEye(761, [762, 0, 763, 764, 0, 0, 0, 765], 766, 767), createEye(768, [769, 0, 770, 771, 0, 0, 0, 772], 773, 774), createEye(775, [776, 0, 777, 778, 0, 0, 0, 779], 780, 781), createEye(782, [783, 0, 784, 785, 0, 0, 0, 786], 787, 788)], + [createEye(789, [0, 0, 0, 0, 0, 0, 0, 0], 790, 791), createEye(792, [0, 0, 0, 0, 0, 0, 0, 0], 793, 794), createEye(795, [0, 0, 0, 0, 0, 0, 0, 0], 796, 797), createEye(798, [0, 0, 0, 0, 0, 0, 0, 0], 799, 800)], + [createEye(801, [0, 0, 0, 0, 0, 0, 0, 0], 802, 803), createEye(804, [0, 0, 0, 0, 0, 0, 0, 0], 805, 806), createEye(807, [0, 0, 0, 0, 0, 0, 0, 0], 808, 809), createEye(810, [0, 0, 0, 0, 0, 0, 0, 0], 811, 812)], + [createEye(813, [814, 815, 816, 817, 818, 819, 820, 821], 822, 823), createEye(824, [825, 826, 827, 828, 829, 830, 831, 832], 833, 834), createEye(835, [836, 837, 838, 839, 840, 841, 842, 843], 844, 845), createEye(846, [847, 848, 849, 850, 851, 852, 853, 854], 855, 856)], + [createEye(857, [858, 859, 860, 861, 862, 863, 864, 865], 866, 867), createEye(868, [869, 870, 871, 872, 873, 874, 875, 876], 877, 878), createEye(879, [880, 881, 882, 883, 884, 885, 886, 887], 888, 889), createEye(890, [891, 892, 893, 894, 895, 896, 897, 898], 899, 900)], + [createEye(901, [902, 0, 903, 904, 0, 0, 0, 905], 906, 907), createEye(908, [909, 0, 910, 911, 0, 0, 0, 912], 913, 914), createEye(915, [916, 0, 917, 918, 0, 0, 0, 919], 920, 921), createEye(922, [923, 0, 924, 925, 0, 0, 0, 926], 927, 928)], + [createEye(929, [0, 0, 0, 0, 0, 0, 0, 0], 930, 931), createEye(932, [0, 0, 0, 0, 0, 0, 0, 0], 933, 934), createEye(935, [0, 0, 0, 0, 0, 0, 0, 0], 936, 937), createEye(938, [0, 0, 0, 0, 0, 0, 0, 0], 939, 940)], + [createEye(942, [0, 0, 0, 0, 0, 0, 0, 0], 941, 943), createEye(945, [0, 0, 0, 0, 0, 0, 0, 0], 944, 946), createEye(948, [0, 0, 0, 0, 0, 0, 0, 0], 947, 949), createEye(951, [0, 0, 0, 0, 0, 0, 0, 0], 950, 952)], + [createEye(954, [0, 0, 0, 0, 0, 0, 0, 0], 953, 955), createEye(957, [0, 0, 0, 0, 0, 0, 0, 0], 956, 958), createEye(960, [0, 0, 0, 0, 0, 0, 0, 0], 959, 961), createEye(963, [0, 0, 0, 0, 0, 0, 0, 0], 962, 964)], + [createEye(966, [0, 0, 0, 0, 0, 0, 0, 0], 965, 967), createEye(969, [0, 0, 0, 0, 0, 0, 0, 0], 968, 970), createEye(972, [0, 0, 0, 0, 0, 0, 0, 0], 971, 973), createEye(975, [0, 0, 0, 0, 0, 0, 0, 0], 974, 976)], + [createEye(978, [0, 0, 0, 0, 0, 0, 0, 0], 977, 979), createEye(981, [0, 0, 0, 0, 0, 0, 0, 0], 980, 982), createEye(984, [0, 0, 0, 0, 0, 0, 0, 0], 983, 985), createEye(987, [0, 0, 0, 0, 0, 0, 0, 0], 986, 988)], + [createEye(989, [990, 991, 992, 993, 994, 995, 996, 997], 998, 999), createEye(1000, [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008], 1009, 1010), createEye(1011, [1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019], 1020, 1021), createEye(1022, [1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030], 1031, 1032)], + [createEye(1033, [1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041], 1042, 1043), createEye(1044, [1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052], 1053, 1054), createEye(1055, [1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063], 1064, 1065), createEye(1066, [1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074], 1075, 1076)], + [createEye(1077, [1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085], 1086, 1087), createEye(1088, [1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096], 1097, 1098), createEye(1099, [1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107], 1108, 1109), createEye(1110, [1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118], 1119, 1120)], + [createEye(1121, [1122, 0, 1123, 1124, 0, 0, 0, 1125], 1126, 1127), createEye(1128, [1129, 0, 1130, 1131, 0, 0, 0, 1132], 1133, 1134), createEye(1135, [1136, 0, 1137, 1138, 0, 0, 0, 1139], 1140, 1141), createEye(1142, [1143, 0, 1144, 1145, 0, 0, 0, 1146], 1147, 1148)], + [createEye(1149, [1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157], 1158, 1159), createEye(1160, [1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168], 1169, 1170), createEye(1171, [1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179], 1180, 1181), createEye(1182, [1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190], 1191, 1192)], + [createEye(1193, [1194, 1195, 1196, 1197, 1198, 1199, 0, 1200], 1201, 1202), createEye(1203, [1204, 1205, 1206, 1207, 1208, 1209, 0, 1210], 1211, 1212), createEye(1213, [1214, 1215, 1216, 1217, 1218, 1219, 0, 1220], 1221, 1222), createEye(1223, [1224, 1225, 1226, 1227, 1228, 1229, 0, 1230], 1231, 1232)], + [createEye(1234, [0, 0, 0, 0, 0, 0, 0, 0], 1233, 1235), createEye(1237, [0, 0, 0, 0, 0, 0, 0, 0], 1236, 1238), createEye(1240, [0, 0, 0, 0, 0, 0, 0, 0], 1239, 1241), createEye(1243, [0, 0, 0, 0, 0, 0, 0, 0], 1242, 1244)], + [createEye(1246, [0, 0, 0, 0, 0, 0, 0, 0], 1245, 0), createEye(1248, [0, 0, 0, 0, 0, 0, 0, 0], 1247, 0), createEye(1250, [0, 0, 0, 0, 0, 0, 0, 0], 1249, 0), createEye(1252, [0, 0, 0, 0, 0, 0, 0, 0], 1251, 0)], + [createEye(1254, [0, 0, 0, 0, 0, 0, 0, 0], 1253, 1255), createEye(1257, [0, 0, 0, 0, 0, 0, 0, 0], 1256, 1258), createEye(1260, [0, 0, 0, 0, 0, 0, 0, 0], 1259, 1261), createEye(1263, [0, 0, 0, 0, 0, 0, 0, 0], 1262, 1264)], + [createEye(1266, [0, 0, 0, 0, 0, 0, 0, 0], 1265, 0), createEye(1268, [0, 0, 0, 0, 0, 0, 0, 0], 1267, 0), createEye(1270, [0, 0, 0, 0, 0, 0, 0, 0], 1269, 0), createEye(1272, [0, 0, 0, 0, 0, 0, 0, 0], 1271, 0)] +]; +export const noses: PonyNose[][][] = [ + [[createNose(1274, 3, 0, 1273)], [createNose(1276, 3, 0, 1275)], [createNose(1278, 5, 0, 1277)]], + [[createNose(1280, 3, 0, 1279)], [createNose(1282, 3, 0, 1281)], [createNose(1284, 5, 0, 1283)]], + [[createNose(1286, 3, 0, 1285)], [createNose(1288, 3, 0, 1287)], [createNose(1290, 5, 0, 1289)]], + [[createNose(1292, 3, 0, 1291)], [createNose(1294, 3, 0, 1293)], [createNose(1296, 5, 0, 1295)]], + [[createNose(1299, 3, 1298, 1297)], [createNose(1302, 3, 1301, 1300)], [createNose(1305, 5, 1304, 1303)]], + [[createNose(1308, 3, 1307, 1306)], [createNose(1311, 3, 1310, 1309)], [createNose(1314, 5, 1313, 1312)]], + [[createNose(1316, 3, 0, 1315)], [createNose(1318, 3, 0, 1317)], [createNose(1320, 5, 0, 1319)]], + [[createNose(1322, 3, 0, 1321)], [createNose(1324, 3, 0, 1323)], [createNose(1326, 5, 0, 1325)]], + [[createNose(1329, 3, 1328, 1327)], [createNose(1332, 3, 1331, 1330)], [createNose(1335, 5, 1334, 1333)]], + [[createNose(1338, 3, 1337, 1336)], [createNose(1341, 3, 1340, 1339)], [createNose(1344, 5, 1343, 1342)]], + [[createNose(1347, 3, 1346, 1345)], [createNose(1350, 3, 1349, 1348)], [createNose(1353, 5, 1352, 1351)]], + [[createNose(1356, 3, 1355, 1354)], [createNose(1359, 3, 1358, 1357)], [createNose(1362, 5, 1361, 1360)]], + [[createNose(1365, 3, 1364, 1363)], [createNose(1368, 3, 1367, 1366)], [createNose(1371, 5, 1370, 1369)]], + [[createNose(1372, 3, 0, 0)], [createNose(1373, 3, 0, 0)], [createNose(1374, 5, 0, 0)]], + [[createNose(1377, 3, 1376, 1375)], [createNose(1380, 3, 1379, 1378)], [createNose(1383, 5, 1382, 1381)]], + [[createNose(1386, 3, 1385, 1384)], [createNose(1389, 3, 1388, 1387)], [createNose(1392, 5, 1391, 1390)]], + [[createNose(1395, 3, 1394, 1393)], [createNose(1398, 3, 1397, 1396)], [createNose(1401, 5, 1400, 1399)]], + [[createNose(1402, 3, 0, 0)], [createNose(1403, 3, 0, 0)], [createNose(1404, 5, 0, 0)]], + [[createNose(1407, 3, 1406, 1405)], [createNose(1410, 3, 1409, 1408)], [createNose(1413, 5, 1412, 1411)]], + [[createNose(1416, 3, 1415, 1414)], [createNose(1419, 3, 1418, 1417)], [createNose(1422, 5, 1421, 1420)]], + [[createNose(1425, 3, 1424, 1423)], [createNose(1428, 3, 1427, 1426)], [createNose(1431, 5, 1430, 1429)]], + [[createNose(1434, 3, 1433, 1432)], [createNose(1437, 3, 1436, 1435)], [createNose(1440, 5, 1439, 1438)]], + [[createNose(1443, 3, 1442, 1441)], [createNose(1446, 3, 1445, 1444)], [createNose(1449, 5, 1448, 1447)]], + [[createNose(1452, 3, 1451, 1450)], [createNose(1455, 3, 1454, 1453)], [createNose(1458, 5, 1457, 1456)]], + [[createNose(1461, 3, 1460, 1459)], [createNose(1464, 3, 1463, 1462)], [createNose(1467, 5, 1466, 1465)]], + [[createNose(1470, 3, 1469, 1468)], [createNose(1473, 3, 1472, 1471)], [createNose(1476, 5, 1475, 1474)]] +]; +export const blush = sprites2[1477]; +export const ponyShadows = [ + sprites2[1478], + sprites2[1479], + sprites2[1480], + sprites2[1481], + sprites2[1482] +]; +export const ponySelections = [ + sprites2[1483], + sprites2[1484], + sprites2[1485], + sprites2[1486], + sprites2[1487] +]; +export const cms = sprites2[1488]; +export const cmsFlip = sprites2[1489]; +export const frontLegs: AnimatedSprites = [ + undefined, + [[colorPal3(1490)]], + [[colorPal3(1491)]], + [[colorPal3(1492)]], + [[colorPal3(1493)]], + [[colorPal3(1494)]], + [[colorPal3(1495)]], + [[colorPal3(1496)]], + [[colorPal3(1497)]], + [[colorPal3(1498)]], + [[colorPal3(1499)]], + [[colorPal3(1500)]], + [[colorPal3(1501)]], + [[colorPal3(1502)]], + [[colorPal3(1503)]], + [[colorPal3(1504)]], + [[colorPal3(1505)]], + [[colorPal3(1506)]], + [[colorPal3(1507)]], + [[colorPal3(1508)]], + [[colorPal3(1509)]], + [[colorPal3(1510)]], + [[colorPal3(1511)]], + [[colorPal3(1512)]], + [[colorPal3(1513)]], + [[colorPal3(1514)]], + [[colorPal3(1515)]], + [[colorPal3(1516)]], + [[colorPal3(1517)]], + [[colorPal3(1518)]], + [[colorPal3(1519)]], + [[colorPal3(1520)]], + [[colorPal3(1521)]], + [[colorPal3(1522)]], + [[colorPal3(1523)]], + [[colorPal3(1524)]], + [[colorPal3(1525)]], + [[colorPal3(1526)]], + [[colorPal3(1527)]] +]; +export const frontLegHooves: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(1528)], [colorPal3(1529)], [colorPal5(1530)], [colorPal5(1531)], [colorPal5(1532)], [colorPal3(1533)]], + [undefined, [colorPal3(1534)], [colorPal3(1535)], [colorPal5(1536)], [colorPal5(1537)], [colorPal5(1538)], [colorPal3(1539)]], + [undefined, [colorPal3(1540)], [colorPal3(1541)], [colorPal5(1542)], [colorPal5(1543)], [colorPal5(1544)], [colorPal3(1545)]], + [undefined, [colorPal3(1546)], [colorPal3(1547)], [colorPal5(1548)], [colorPal5(1549)], [colorPal5(1550)], [colorPal3(1551)]], + [undefined, [colorPal3(1552)], [colorPal3(1553)], [colorPal5(1554)], [colorPal5(1555)], [colorPal5(1556)], [colorPal3(1557)]], + [undefined, [colorPal3(1558)], [colorPal3(1559)], [colorPal5(1560)], [colorPal5(1561)], [colorPal5(1562)], [colorPal3(1563)]], + [undefined, [colorPal3(1564)], [colorPal3(1565)], [colorPal5(1566)], [colorPal5(1567)], [colorPal5(1568)], [colorPal3(1569)]], + [undefined, [colorPal3(1570)], [colorPal3(1571)], [colorPal5(1572)], [colorPal5(1573)], [colorPal5(1574)], [colorPal3(1575)]], + [undefined, [colorPal3(1576)], [colorPal3(1577)], [colorPal5(1578)], [colorPal5(1579)], [colorPal5(1580)], [colorPal3(1581)]], + [undefined, [colorPal3(1582)], [colorPal3(1583)], [colorPal5(1584)], [colorPal5(1585)], [colorPal5(1586)], [colorPal3(1587)]], + [undefined, [colorPal3(1588)], [colorPal3(1589)], [colorPal5(1590)], [colorPal5(1591)], [colorPal5(1592)], [colorPal3(1593)]], + [undefined, [colorPal3(1594)], [colorPal3(1595)], [colorPal5(1596)], [colorPal5(1597)], [colorPal5(1598)], [colorPal3(1599)]], + [undefined, [colorPal3(1600)], [colorPal3(1601)], [colorPal5(1602)], [colorPal5(1603)], [colorPal5(1604)], [colorPal3(1605)]], + [undefined, [colorPal3(1606)], [colorPal3(1607)], [colorPal5(1608)], [colorPal5(1609)], [colorPal5(1610)], [colorPal3(1611)]], + [undefined, [colorPal3(1612)], [colorPal3(1613)], [colorPal5(1614)], [colorPal5(1615)], [colorPal5(1616)], [colorPal3(1617)]], + [undefined, [colorPal3(1618)], [colorPal3(1619)], [colorPal5(1620)], [colorPal5(1621)], [colorPal5(1622)], [colorPal3(1623)]], + [undefined, [colorPal3(1624)], [colorPal3(1625)], [colorPal5(1626)], [colorPal5(1627)], [colorPal5(1628)], [colorPal3(1629)]], + [undefined, [colorPal3(1630)], [colorPal3(1631)], [colorPal5(1632)], [colorPal5(1633)], [colorPal5(1634)], [colorPal3(1635)]], + [undefined, [colorPal3(1636)], [colorPal3(1637)], [colorPal5(1638)], [colorPal5(1639)], [colorPal5(1640)], [colorPal3(1641)]], + [undefined, [colorPal3(1642)], [colorPal3(1643)], [colorPal5(1644)], [colorPal5(1645)], [colorPal5(1646)], [colorPal3(1647)]], + [undefined, [colorPal3(1648)], [colorPal3(1649)], [colorPal5(1650)], [colorPal5(1651)], [colorPal5(1652)], [colorPal3(1653)]], + [undefined, [colorPal3(1654)], [colorPal3(1655)], [colorPal5(1656)], [colorPal5(1657)], [colorPal5(1658)], [colorPal3(1659)]], + [undefined, [colorPal3(1660)], [colorPal3(1661)], [colorPal5(1662)], [colorPal5(1663)], [colorPal5(1664)], [colorPal3(1665)]], + [undefined, [colorPal3(1666)], [colorPal3(1667)], [colorPal5(1668)], [colorPal5(1669)], [colorPal5(1670)], [colorPal3(1671)]], + [undefined, [colorPal3(1672)], [colorPal3(1673)], [colorPal5(1674)], [colorPal5(1675)], [colorPal5(1676)], [colorPal3(1677)]], + [undefined, [colorPal3(1678)], [colorPal3(1679)], [colorPal5(1680)], [colorPal5(1681)], [colorPal5(1682)], [colorPal3(1683)]], + [undefined, [colorPal3(1684)], [colorPal3(1685)], [colorPal5(1686)], [colorPal5(1687)], [colorPal5(1688)], [colorPal3(1689)]], + [undefined, [colorPal3(1690)], [colorPal3(1691)], [colorPal5(1692)], [colorPal5(1693)], [colorPal5(1694)], [colorPal3(1695)]], + [undefined, [colorPal3(1696)], [colorPal3(1697)], [colorPal5(1698)], [colorPal5(1699)], [colorPal5(1700)], [colorPal3(1701)]], + [undefined, [colorPal3(1702)], [colorPal3(1703)], [colorPal5(1704)], [colorPal5(1705)], [colorPal5(1706)], [colorPal3(1707)]], + [undefined, [colorPal3(1708)], [colorPal3(1709)], [colorPal5(1710)], [colorPal5(1711)], [colorPal5(1712)], [colorPal3(1713)]], + [undefined, [colorPal3(1714)], [colorPal3(1715)], [colorPal5(1716)], [colorPal5(1717)], [colorPal5(1718)], [colorPal3(1719)]], + [undefined, [colorPal3(1720)], [colorPal3(1721)], [colorPal5(1722)], [colorPal5(1723)], [colorPal5(1724)], [colorPal3(1725)]], + [undefined, [colorPal3(1726)], [colorPal3(1727)], [colorPal5(1728)], [colorPal5(1729)], [colorPal5(1730)], [colorPal3(1731)]], + [undefined, [colorPal3(1732)], [colorPal3(1733)], [colorPal5(1734)], [colorPal5(1735)], [colorPal5(1736)], [colorPal3(1737)]], + [undefined, [colorPal3(1738)], [colorPal3(1739)], [colorPal5(1740)], [colorPal5(1741)], [colorPal5(1742)], [colorPal3(1743)]], + [undefined, [colorPal3(1744)], [colorPal3(1745)], [colorPal5(1746)], [colorPal5(1747)], [colorPal5(1748)], [colorPal3(1749)]], + [undefined, [colorPal3(1750)], [colorPal3(1751)], [colorPal5(1752)], [colorPal5(1753)], [colorPal5(1754)], [colorPal3(1755)]] +]; +export const frontLegAccessories: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(1756), colorPal13(1757), colorPal13(1758), colorPal13(1759), colorPal13(1760)]], + [undefined, [colorPal3(1761), colorPal13(1762), colorPal13(1763), colorPal13(1764), colorPal13(1765)]], + [undefined, [colorPal3(1766), colorPal13(1767), colorPal13(1768), colorPal13(1769), colorPal13(1770)]], + [undefined, [colorPal3(1771), colorPal13(1772), colorPal13(1773), colorPal13(1774), colorPal13(1775)]], + [undefined, [colorPal3(1776), colorPal13(1777), colorPal13(1778), colorPal13(1779), colorPal13(1780)]], + [undefined, [colorPal3(1781), colorPal13(1782), colorPal13(1783), colorPal13(1784), colorPal13(1785)]], + [undefined, [colorPal3(1786), colorPal13(1787), colorPal13(1788), colorPal13(1789), colorPal13(1790)]], + [undefined, [colorPal3(1791), colorPal13(1792), colorPal13(1793), colorPal13(1794), colorPal13(1795)]], + [undefined, [colorPal3(1796), colorPal13(1797), colorPal13(1798), colorPal13(1799), colorPal13(1800)]], + [undefined, [colorPal3(1801), colorPal13(1802), colorPal13(1803), colorPal13(1804), colorPal13(1805)]], + [undefined, [colorPal3(1806), colorPal13(1807), colorPal13(1808), colorPal13(1809), colorPal13(1810)]], + [undefined, [colorPal3(1811), colorPal13(1812), colorPal13(1813), colorPal13(1814), colorPal13(1815)]], + [undefined, [colorPal3(1816), colorPal13(1817), colorPal13(1818), colorPal13(1819), colorPal13(1820)]], + [undefined, [colorPal3(1821), colorPal13(1822), colorPal13(1823), colorPal13(1824), colorPal13(1825)]], + [undefined, [colorPal3(1826), colorPal13(1827), colorPal13(1828), colorPal13(1829), colorPal13(1830)]], + [undefined, [colorPal3(1831), colorPal13(1832), colorPal13(1833), colorPal13(1834), colorPal13(1835)]], + [undefined, [colorPal3(1836), colorPal13(1837), colorPal13(1838), colorPal13(1839), colorPal13(1840)]], + [undefined, [colorPal3(1841), colorPal13(1842), colorPal13(1843), colorPal13(1844), colorPal13(1845)]], + [undefined, [colorPal3(1846), colorPal13(1847), colorPal13(1848), colorPal13(1849), colorPal13(1850)]], + [undefined, [colorPal3(1851), colorPal13(1852), colorPal13(1853), colorPal13(1854), colorPal13(1855)]], + [undefined, [colorPal3(1856), colorPal13(1857), colorPal13(1858), colorPal13(1859), colorPal13(1860)]], + [undefined, [colorPal3(1861), colorPal13(1862), colorPal13(1863), colorPal13(1864), colorPal13(1865)]], + [undefined, [colorPal3(1866), colorPal13(1867), colorPal13(1868), colorPal13(1869), colorPal13(1870)]], + [undefined, [colorPal3(1871), colorPal13(1872), colorPal13(1873), colorPal13(1874), colorPal13(1875)]], + [undefined, [colorPal3(1876), colorPal13(1877), colorPal13(1878), colorPal13(1879), colorPal13(1880)]], + [undefined, [colorPal3(1881), colorPal13(1882), colorPal13(1883), colorPal13(1884), colorPal13(1885)]], + [undefined, [colorPal3(1886), colorPal13(1887), colorPal13(1888), colorPal13(1889), colorPal13(1890)]], + [undefined, [colorPal3(1891), colorPal13(1892), colorPal13(1893), colorPal13(1894), colorPal13(1895)]], + [undefined, [colorPal3(1896), colorPal13(1897), colorPal13(1898), colorPal13(1899), colorPal13(1900)]], + [undefined, [colorPal3(1901), colorPal13(1902), colorPal13(1903), colorPal13(1904), colorPal13(1905)]], + [undefined, [colorPal3(1906), colorPal13(1907), colorPal13(1908), colorPal13(1909), colorPal13(1910)]], + [undefined, [colorPal3(1911), colorPal13(1912), colorPal13(1913), colorPal13(1914), colorPal13(1915)]], + [undefined, [colorPal3(1916), colorPal13(1917), colorPal13(1918), colorPal13(1919), colorPal13(1920)]], + [undefined, [colorPal3(1921), colorPal13(1922), colorPal13(1923), colorPal13(1924), colorPal13(1925)]], + [undefined, [colorPal3(1926), colorPal13(1927), colorPal13(1928), colorPal13(1929), colorPal13(1930)]], + [undefined, [colorPal3(1931), colorPal13(1932), colorPal13(1933), colorPal13(1934), colorPal13(1935)]], + [undefined, [colorPal3(1936), colorPal13(1937), colorPal13(1938), colorPal13(1939), colorPal13(1940)]], + [undefined, [colorPal3(1941), colorPal13(1942), colorPal13(1943), colorPal13(1944), colorPal13(1945)]] +]; +export const frontLegSleeves: AnimatedSprites = [ + undefined, + [[colorPal3(1946), colorPal13(1947), colorPal5(1948), colorPal5(1949), colorPal5(1950), colorPal7(1951), colorPal11(1952)], [colorPal3(1953), colorPal9(1954), colorPal11(1955)], [colorPal3(1956), colorPal5(1957)], [colorPal3(1958), colorPal5(1959)]], + [[colorPal3(1960), colorPal13(1961), colorPal5(1962), colorPal5(1963), colorPal5(1964), colorPal7(1965), colorPal11(1966)], [colorPal3(1967), colorPal9(1968), colorPal11(1969)], [colorPal3(1970), colorPal5(1971)], [colorPal3(1972), colorPal5(1973)]], + [[colorPal3(1974), colorPal13(1975), colorPal5(1976), colorPal5(1977), colorPal5(1978), colorPal7(1979), colorPal11(1980)], [colorPal3(1981), colorPal9(1982), colorPal11(1983)], [colorPal3(1984), colorPal5(1985)], [colorPal3(1986), colorPal5(1987)]], + [[colorPal3(1988), colorPal13(1989), colorPal5(1990), colorPal5(1991), colorPal5(1992), colorPal7(1993), colorPal11(1994)], [colorPal3(1995), colorPal9(1996), colorPal11(1997)], [colorPal3(1998), colorPal5(1999)], [colorPal3(2000), colorPal5(2001)]], + [[colorPal3(2002), colorPal13(2003), colorPal5(2004), colorPal5(2005), colorPal5(2006), colorPal7(2007), colorPal11(2008)], [colorPal3(2009), colorPal9(2010), colorPal11(2011)], [colorPal3(2012), colorPal5(2013)], [colorPal3(2014), colorPal5(2015)]], + [[colorPal3(2016), colorPal13(2017), colorPal5(2018), colorPal5(2019), colorPal5(2020), colorPal7(2021), colorPal11(2022)], [colorPal3(2023), colorPal9(2024), colorPal11(2025)], [colorPal3(2026), colorPal5(2027)], [colorPal3(2028), colorPal5(2029)]], + [[colorPal3(2030), colorPal13(2031), colorPal5(2032), colorPal5(2033), colorPal5(2034), colorPal7(2035), colorPal11(2036)], [colorPal3(2037), colorPal9(2038), colorPal11(2039)], [colorPal3(2040), colorPal5(2041)], [colorPal3(2042), colorPal5(2043)]], + [[colorPal3(2044), colorPal13(2045), colorPal5(2046), colorPal5(2047), colorPal5(2048), colorPal7(2049), colorPal11(2050)], [colorPal3(2051), colorPal9(2052), colorPal11(2053)], [colorPal3(2054), colorPal5(2055)], [colorPal3(2056), colorPal5(2057)]], + [[colorPal3(2058), colorPal13(2059), colorPal5(2060), colorPal5(2061), colorPal5(2062), colorPal7(2063), colorPal11(2064)], [colorPal3(2065), colorPal9(2066), colorPal11(2067)], [colorPal3(2068), colorPal5(2069)], [colorPal3(2070), colorPal5(2071)]], + [[colorPal3(2072), colorPal13(2073), colorPal5(2074), colorPal5(2075), colorPal5(2076), colorPal7(2077), colorPal11(2078)], [colorPal3(2079), colorPal9(2080), colorPal11(2081)], [colorPal3(2082), colorPal5(2083)], [colorPal3(2084), colorPal5(2085)]], + [[colorPal3(2086), colorPal13(2087), colorPal5(2088), colorPal5(2089), colorPal5(2090), colorPal7(2091), colorPal11(2092)], [colorPal3(2093), colorPal9(2094), colorPal11(2095)], [colorPal3(2096), colorPal5(2097)], [colorPal3(2098), colorPal5(2099)]], + [[colorPal3(2100), colorPal13(2101), colorPal5(2102), colorPal5(2103), colorPal5(2104), colorPal7(2105), colorPal11(2106)], [colorPal3(2107), colorPal9(2108), colorPal11(2109)], [colorPal3(2110), colorPal5(2111)], [colorPal3(2112), colorPal5(2113)]], + [[colorPal3(2114), colorPal13(2115), colorPal5(2116), colorPal5(2117), colorPal5(2118), colorPal7(2119), colorPal11(2120)], [colorPal3(2121), colorPal9(2122), colorPal11(2123)], [colorPal3(2124), colorPal5(2125)], [colorPal3(2126), colorPal5(2127)]], + [[colorPal3(2128), colorPal13(2129), colorPal5(2130), colorPal5(2131), colorPal5(2132), colorPal7(2133), colorPal11(2134)], [colorPal3(2135), colorPal9(2136), colorPal11(2137)], [colorPal3(2138), colorPal5(2139)], [colorPal3(2140), colorPal5(2141)]], + [[colorPal3(2142), colorPal13(2143), colorPal5(2144), colorPal5(2145), colorPal5(2146), colorPal7(2147), colorPal11(2148)], [colorPal3(2149), colorPal9(2150), colorPal11(2151)], [colorPal3(2152), colorPal5(2153)], [colorPal3(2154), colorPal5(2155)]], + [[colorPal3(2156), colorPal13(2157), colorPal5(2158), colorPal5(2159), colorPal5(2160), colorPal7(2161), colorPal11(2162)], [colorPal3(2163), colorPal9(2164), colorPal11(2165)], [colorPal3(2166), colorPal5(2167)], [colorPal3(2168), colorPal5(2169)]], + [[colorPal3(2170), colorPal13(2171), colorPal5(2172), colorPal5(2173), colorPal5(2174), colorPal7(2175), colorPal11(2176)], [colorPal3(2177), colorPal9(2178), colorPal11(2179)], [colorPal3(2180), colorPal5(2181)], [colorPal3(2182), colorPal5(2183)]], + [[colorPal3(2184), colorPal13(2185), colorPal5(2186), colorPal5(2187), colorPal5(2188), colorPal7(2189), colorPal11(2190)], [colorPal3(2191), colorPal9(2192), colorPal11(2193)], [colorPal3(2194), colorPal5(2195)], [colorPal3(2196), colorPal5(2197)]], + [[colorPal3(2198), colorPal13(2199), colorPal5(2200), colorPal5(2201), colorPal5(2202), colorPal7(2203), colorPal11(2204)], [colorPal3(2205), colorPal9(2206), colorPal11(2207)], [colorPal3(2208), colorPal5(2209)], [colorPal3(2210), colorPal5(2211)]], + [[colorPal3(2212), colorPal13(2213), colorPal5(2214), colorPal5(2215), colorPal5(2216), colorPal7(2217), colorPal11(2218)], [colorPal3(2219), colorPal9(2220), colorPal11(2221)], [colorPal3(2222), colorPal5(2223)], [colorPal3(2224), colorPal5(2225)]], + [[colorPal3(2226), colorPal13(2227), colorPal5(2228), colorPal5(2229), colorPal5(2230), colorPal7(2231), colorPal11(2232)], [colorPal3(2233), colorPal9(2234), colorPal11(2235)], [colorPal3(2236), colorPal5(2237)], [colorPal3(2238), colorPal5(2239)]], + [[colorPal3(2240), colorPal13(2241), colorPal5(2242), colorPal5(2243), colorPal5(2244), colorPal7(2245), colorPal11(2246)], [colorPal3(2247), colorPal9(2248), colorPal11(2249)], [colorPal3(2250), colorPal5(2251)], [colorPal3(2252), colorPal5(2253)]], + [[colorPal3(2254), colorPal13(2255), colorPal5(2256), colorPal5(2257), colorPal5(2258), colorPal7(2259), colorPal11(2260)], [colorPal3(2261), colorPal9(2262), colorPal11(2263)], [colorPal3(2264), colorPal5(2265)], [colorPal3(2266), colorPal5(2267)]], + [[colorPal3(2268), colorPal13(2269), colorPal5(2270), colorPal5(2271), colorPal5(2272), colorPal7(2273), colorPal11(2274)], [colorPal3(2275), colorPal9(2276), colorPal11(2277)], [colorPal3(2278), colorPal5(2279)], [colorPal3(2280), colorPal5(2281)]], + [[colorPal3(2282), colorPal13(2283), colorPal5(2284), colorPal5(2285), colorPal5(2286), colorPal7(2287), colorPal11(2288)], [colorPal3(2289), colorPal9(2290), colorPal11(2291)], [colorPal3(2292), colorPal5(2293)], [colorPal3(2294), colorPal5(2295)]], + [[colorPal3(2296), colorPal13(2297), colorPal5(2298), colorPal5(2299), colorPal5(2300), colorPal7(2301), colorPal11(2302)], [colorPal3(2303), colorPal9(2304), colorPal11(2305)], [colorPal3(2306), colorPal5(2307)], [colorPal3(2308), colorPal5(2309)]], + [[colorPal3(2310), colorPal13(2311), colorPal5(2312), colorPal5(2313), colorPal5(2314), colorPal7(2315), colorPal11(2316)], [colorPal3(2317), colorPal9(2318), colorPal11(2319)], [colorPal3(2320), colorPal5(2321)], [colorPal3(2322), colorPal5(2323)]], + [[colorPal3(2324), colorPal13(2325), colorPal5(2326), colorPal5(2327), colorPal5(2328), colorPal7(2329), colorPal11(2330)], [colorPal3(2331), colorPal9(2332), colorPal11(2333)], [colorPal3(2334), colorPal5(2335)], [colorPal3(2336), colorPal5(2337)]], + [[colorPal3(2338), colorPal13(2339), colorPal5(2340), colorPal5(2341), colorPal5(2342), colorPal7(2343), colorPal11(2344)], [colorPal3(2345), colorPal9(2346), colorPal11(2347)], [colorPal3(2348), colorPal5(2349)], [colorPal3(2350), colorPal5(2351)]], + [[colorPal3(2352), colorPal13(2353), colorPal5(2354), colorPal5(2355), colorPal5(2356), colorPal7(2357), colorPal11(2358)], [colorPal3(2359), colorPal9(2360), colorPal11(2361)], [colorPal3(2362), colorPal5(2363)], [colorPal3(2364), colorPal5(2365)]], + [[colorPal3(2366), colorPal13(2367), colorPal5(2368), colorPal5(2369), colorPal5(2370), colorPal7(2371), colorPal11(2372)], [colorPal3(2373), colorPal9(2374), colorPal11(2375)], [colorPal3(2376), colorPal5(2377)], [colorPal3(2378), colorPal5(2379)]], + [[colorPal3(2380), colorPal13(2381), colorPal5(2382), colorPal5(2383), colorPal5(2384), colorPal7(2385), colorPal11(2386)], [colorPal3(2387), colorPal9(2388), colorPal11(2389)], [colorPal3(2390), colorPal5(2391)], [colorPal3(2392), colorPal5(2393)]], + [[colorPal3(2394), colorPal13(2395), colorPal5(2396), colorPal5(2397), colorPal5(2398), colorPal7(2399), colorPal11(2400)], [colorPal3(2401), colorPal9(2402), colorPal11(2403)], [colorPal3(2404), colorPal5(2405)], [colorPal3(2406), colorPal5(2407)]], + [[colorPal3(2408), colorPal13(2409), colorPal5(2410), colorPal5(2411), colorPal5(2412), colorPal7(2413), colorPal11(2414)], [colorPal3(2415), colorPal9(2416), colorPal11(2417)], [colorPal3(2418), colorPal5(2419)], [colorPal3(2420), colorPal5(2421)]], + [[colorPal3(2422), colorPal13(2423), colorPal5(2424), colorPal5(2425), colorPal5(2426), colorPal7(2427), colorPal11(2428)], [colorPal3(2429), colorPal9(2430), colorPal11(2431)], [colorPal3(2432), colorPal5(2433)], [colorPal3(2434), colorPal5(2435)]], + [[colorPal3(2436), colorPal13(2437), colorPal5(2438), colorPal5(2439), colorPal5(2440), colorPal7(2441), colorPal11(2442)], [colorPal3(2443), colorPal9(2444), colorPal11(2445)], [colorPal3(2446), colorPal5(2447)], [colorPal3(2448), colorPal5(2449)]], + [[colorPal3(2450), colorPal13(2451), colorPal5(2452), colorPal5(2453), colorPal5(2454), colorPal7(2455), colorPal11(2456)], [colorPal3(2457), colorPal9(2458), colorPal11(2459)], [colorPal3(2460), colorPal5(2461)], [colorPal3(2462), colorPal5(2463)]], + [[colorPal3(2464), colorPal13(2465), colorPal5(2466), colorPal5(2467), colorPal5(2468), colorPal7(2469), colorPal11(2470)], [colorPal3(2471), colorPal9(2472), colorPal11(2473)], [colorPal3(2474), colorPal5(2475)], [colorPal3(2476), colorPal5(2477)]] +]; +export const backLegs: AnimatedSprites = [ + undefined, + [[colorPal3(2478)]], + [[colorPal3(2479)]], + [[colorPal3(2480)]], + [[colorPal3(2481)]], + [[colorPal3(2482)]], + [[colorPal3(2483)]], + [[colorPal3(2484)]], + [[colorPal3(2485)]], + [[colorPal3(2486)]], + [[colorPal3(2487)]], + [[colorPal3(2488)]], + [[colorPal3(2489)]], + [[colorPal3(2490)]], + [[colorPal3(2491)]], + [[colorPal3(2492)]], + [[colorPal3(2493)]], + [[colorPal3(2494)]], + [[colorPal3(2495)]], + [[colorPal3(2496)]], + [[colorPal3(2497)]], + [[colorPal3(2498)]], + [[colorPal3(2499)]], + [[colorPal3(2500)]], + [[colorPal3(2501)]], + [[colorPal3(2502)]], + [[colorPal3(2503)]] +]; +export const backLegs2: AnimatedSprites = [ + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [[colorPal3(2504)]], + [[colorPal3(2505)]], + [[colorPal3(2506)]], + [[colorPal3(2507)]], + [[colorPal3(2508)]] +]; +export const backLegHooves: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(2509)], [colorPal3(2510)], [colorPal5(2511)], [colorPal3(2512)]], + [undefined, [colorPal3(2513)], [colorPal3(2514)], [colorPal5(2515)], [colorPal3(2516)]], + [undefined, [colorPal3(2517)], [colorPal3(2518)], [colorPal5(2519)], [colorPal3(2520)]], + [undefined, [colorPal3(2521)], [colorPal3(2522)], [colorPal5(2523)], [colorPal3(2524)]], + [undefined, [colorPal3(2525)], [colorPal3(2526)], [colorPal5(2527)], [colorPal3(2528)]], + [undefined, [colorPal3(2529)], [colorPal3(2530)], [colorPal5(2531)], [colorPal3(2532)]], + [undefined, [colorPal3(2533)], [colorPal3(2534)], [colorPal5(2535)], [colorPal3(2536)]], + [undefined, [colorPal3(2537)], [colorPal3(2538)], [colorPal5(2539)], [colorPal3(2540)]], + [undefined, [colorPal3(2541)], [colorPal3(2542)], [colorPal5(2543)], [colorPal3(2544)]], + [undefined, [colorPal3(2545)], [colorPal3(2546)], [colorPal5(2547)], [colorPal3(2548)]], + [undefined, [colorPal3(2549)], [colorPal3(2550)], [colorPal5(2551)], [colorPal3(2552)]], + [undefined, [colorPal3(2553)], [colorPal3(2554)], [colorPal5(2555)], [colorPal3(2556)]], + [undefined, [colorPal3(2557)], [colorPal3(2558)], [colorPal5(2559)], [colorPal3(2560)]], + [undefined, [colorPal3(2561)], [colorPal3(2562)], [colorPal5(2563)], [colorPal3(2564)]], + [undefined, [colorPal3(2565)], [colorPal3(2566)], [colorPal5(2567)], [colorPal3(2568)]], + [undefined, [colorPal3(2569)], [colorPal3(2570)], [colorPal5(2571)], [colorPal3(2572)]], + [undefined, [colorPal3(2573)], [colorPal3(2574)], [colorPal5(2575)], [colorPal3(2576)]], + [undefined, [colorPal3(2577)], [colorPal3(2578)], [colorPal5(2579)], [colorPal17(2580)]], + [undefined, [colorPal3(2581)], [colorPal3(2582)], [colorPal5(2583)], [colorPal3(2584)]], + [undefined, [colorPal3(2585)], [colorPal3(2586)], [colorPal5(2587)], [colorPal3(2588)]], + [undefined, [colorPal3(2589)], [colorPal3(2590)], [colorPal5(2591)], [colorPal3(2592)]], + [undefined, [colorPal3(2593)], [colorPal3(2594)], [colorPal5(2595)], [colorPal3(2596)]], + [undefined, [colorPal3(2597)], [colorPal3(2598)], [colorPal5(2599)], [colorPal3(2600)]], + [undefined, [colorPal3(2601)], [colorPal3(2602)], [colorPal5(2603)], [colorPal3(2604)]], + [undefined, [colorPal3(2605)], [colorPal3(2606)], [colorPal5(2607)], [colorPal3(2608)]], + [undefined, [colorPal3(2609)], [colorPal3(2610)], [colorPal5(2611)], [colorPal3(2612)]] +]; +export const backLegHooves2: AnimatedSprites = [ + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [undefined, [colorPal3(2613)], [colorPal3(2614)], [colorPal5(2615)], [colorPal3(2616)]], + [undefined, [colorPal3(2617)], [colorPal3(2618)], [colorPal5(2619)], [colorPal3(2620)]], + [undefined, [colorPal3(2621)], [colorPal3(2622)], [colorPal5(2623)], [colorPal3(2624)]], + [undefined, [colorPal3(2625)], [colorPal3(2626)], [colorPal5(2627)], [colorPal3(2628)]], + [undefined, [colorPal3(2629)], [colorPal3(2630)], [colorPal5(2631)], [colorPal3(2632)]] +]; +export const backLegAccessories: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(2633), colorPal13(2634), colorPal13(2635), colorPal13(2636), colorPal13(2637)]], + [undefined, [colorPal3(2638), colorPal13(2639), colorPal13(2640), colorPal13(2641), colorPal13(2642)]], + [undefined, [colorPal3(2643), colorPal13(2644), colorPal13(2645), colorPal13(2646), colorPal13(2647)]], + [undefined, [colorPal3(2648), colorPal13(2649), colorPal13(2650), colorPal13(2651), colorPal13(2652)]], + [undefined, [colorPal3(2653), colorPal13(2654), colorPal13(2655), colorPal13(2656), colorPal13(2657)]], + [undefined, [colorPal3(2658), colorPal13(2659), colorPal13(2660), colorPal13(2661), colorPal13(2662)]], + [undefined, [colorPal3(2663), colorPal13(2664), colorPal13(2665), colorPal13(2666), colorPal13(2667)]], + [undefined, [colorPal3(2668), colorPal13(2669), colorPal13(2670), colorPal13(2671), colorPal13(2672)]], + [undefined, [colorPal3(2673), colorPal13(2674), colorPal13(2675), colorPal13(2676), colorPal13(2677)]], + [undefined, [colorPal3(2678), colorPal13(2679), colorPal13(2680), colorPal13(2681), colorPal13(2682)]], + [undefined, [colorPal3(2683), colorPal13(2684), colorPal13(2685), colorPal13(2686), colorPal13(2687)]], + [undefined, [colorPal3(2688), colorPal13(2689), colorPal13(2690), colorPal13(2691), colorPal13(2692)]], + [undefined, [colorPal3(2693), colorPal13(2694), colorPal13(2695), colorPal13(2696), colorPal13(2697)]], + [undefined, [colorPal3(2698), colorPal13(2699), colorPal13(2700), colorPal13(2701), colorPal13(2702)]], + [undefined, [colorPal3(2703), colorPal13(2704), colorPal13(2705), colorPal13(2706), colorPal13(2707)]], + [undefined, [colorPal3(2708), colorPal13(2709), colorPal13(2710), colorPal13(2711), colorPal13(2712)]], + [undefined, [colorPal3(2713), colorPal13(2714), colorPal13(2715), colorPal13(2716), colorPal13(2717)]], + [undefined, [colorPal3(2718), colorPal13(2719), colorPal13(2720), colorPal13(2721), colorPal13(2722)]], + [undefined, [colorPal3(2723), colorPal13(2724), colorPal13(2725), colorPal13(2726), colorPal13(2727)]], + [undefined, [colorPal3(2728), colorPal13(2729), colorPal13(2730), colorPal13(2731), colorPal13(2732)]], + [undefined, [colorPal3(2733), colorPal13(2734), colorPal13(2735), colorPal13(2736), colorPal13(2737)]], + [undefined, [colorPal3(2738), colorPal13(2739), colorPal13(2740), colorPal13(2741), colorPal13(2742)]], + [undefined, [colorPal3(2743), colorPal13(2744), colorPal13(2745), colorPal13(2746), colorPal13(2747)]], + [undefined, [colorPal3(2748), colorPal13(2749), colorPal13(2750), colorPal13(2751), colorPal13(2752)]], + [undefined, [colorPal3(2753), colorPal13(2754), colorPal13(2755), colorPal13(2756), colorPal13(2757)]], + [undefined, [colorPal3(2758), colorPal13(2759), colorPal13(2760), colorPal13(2761), colorPal13(2762)]] +]; +export const backLegAccessories2: AnimatedSprites = [ + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [undefined, [colorPal3(2763), colorPal13(2764), colorPal13(2765), colorPal13(2766), colorPal13(2767)]], + [undefined, [colorPal3(2768), colorPal13(2769), colorPal13(2770), colorPal13(2771), colorPal13(2772)]], + [undefined, [colorPal3(2773), colorPal13(2774), colorPal13(2775), colorPal13(2776), colorPal13(2777)]], + [undefined, [colorPal3(2778), colorPal13(2779), colorPal13(2780), colorPal13(2781), colorPal13(2782)]], + [undefined, [colorPal3(2783), colorPal13(2784), colorPal13(2785), colorPal13(2786), colorPal13(2787)]] +]; +export const backLegSleeves: AnimatedSprites = [ + undefined, + [[colorPal3(2788), colorPal13(2789)]], + [[colorPal3(2790), colorPal13(2791)]], + [[colorPal3(2792), colorPal13(2793)]], + [[colorPal3(2794), colorPal13(2795)]], + [[colorPal3(2796), colorPal13(2797)]], + [[colorPal3(2798), colorPal13(2799)]], + [[colorPal3(2800), colorPal13(2801)]], + [[colorPal3(2802), colorPal13(2803)]], + [[colorPal3(2804), colorPal13(2805)]], + [[colorPal3(2806), colorPal13(2807)]], + [[colorPal3(2808), colorPal13(2809)]], + [[colorPal3(2810), colorPal13(2811)]], + [[colorPal3(2812), colorPal13(2813)]], + [[colorPal3(2814), colorPal13(2815)]], + [[colorPal3(2816), colorPal13(2817)]], + [[colorPal3(2818), colorPal13(2819)]], + [[colorPal3(2820), colorPal13(2821)]], + [[colorPal3(2822), colorPal13(2823)]], + [[colorPal3(2824), colorPal13(2825)]], + [[colorPal3(2826), colorPal13(2827)]], + [[colorPal3(2828), colorPal13(2829)]], + [[colorPal3(2830), colorPal13(2831)]], + [[colorPal3(2832), colorPal13(2833)]], + [[colorPal3(2834), colorPal13(2835)]], + [[colorPal3(2836), colorPal13(2837)]], + [[colorPal3(2838), colorPal13(2839)]] +]; +export const backLegSleeves2: AnimatedSprites = [ + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [[colorPal3(2840), colorPal13(2841)]], + [[colorPal3(2842), colorPal13(2843)]], + [[colorPal3(2844), colorPal13(2845)]], + [[colorPal3(2846), colorPal13(2847)]], + [[colorPal3(2848), colorPal13(2849)]] +]; +export const body: AnimatedSprites = [ + undefined, + [[colorPal3(2850), colorPal5(2851)]], + [[colorPal3(2852), colorPal5(2853)]], + [[colorPal3(2854), colorPal5(2855)]], + [[colorPal3(2856), colorPal5(2857)]], + [[colorPal3(2858), colorPal5(2859)]], + [[colorPal3(2860), colorPal5(2861)]], + [[colorPal3(2862), colorPal5(2863)]], + [[colorPal3(2864), colorPal5(2865)]], + [[colorPal3(2866), colorPal5(2867)]], + [[colorPal3(2868), colorPal5(2869)]], + [[colorPal3(2870), colorPal5(2871)]], + [[colorPal3(2872), colorPal5(2873)]], + [[colorPal3(2874), colorPal5(2875)]], + [[colorPal3(2876), colorPal5(2877)]], + [[colorPal3(2878), colorPal5(2879)]] +]; +export const wings: AnimatedSprites = [ + [undefined, [colorPal3(2880), colorPal5(2881), colorPal9(2882)], [colorPal5(2883)], [colorPal3(2884), colorPal5(2885), colorPal9(2886)], [colorPal3(2887)]], + [undefined, [colorPal3(2888), colorPal5(2889), colorPal9(2890)], [colorPal5(2891)], [colorPal3(2892), colorPal5(2893), colorPal9(2894)], [colorPal3(2895)]], + [undefined, [colorPal3(2896), colorPal5(2897), colorPal9(2898)], [colorPal5(2899)], [colorPal3(2900), colorPal5(2901), colorPal9(2902)], [colorPal3(2903)]], + [undefined, [colorPal3(2904), colorPal5(2905), colorPal9(2906)], [colorPal5(2907)], [colorPal3(2908), colorPal5(2909), colorPal9(2910)], [colorPal3(2911)]], + [undefined, [colorPal3(2912), colorPal5(2913), colorPal9(2914)], [colorPal5(2915)], [colorPal3(2916), colorPal5(2917), colorPal9(2918)], [colorPal3(2919)]], + [undefined, [colorPal3(2920), colorPal5(2921), colorPal9(2922)], [colorPal5(2923)], [colorPal3(2924), colorPal5(2925), colorPal9(2926)], [colorPal3(2927)]], + [undefined, [colorPal3(2928), colorPal5(2929), colorPal9(2930)], [colorPal5(2931)], [colorPal3(2932), colorPal5(2933), colorPal9(2934)]], + [undefined, [colorPal3(2935), colorPal5(2936), colorPal9(2937)], [colorPal5(2938)], [colorPal3(2939), colorPal5(2940), colorPal9(2941)]], + [undefined, [colorPal3(2942), colorPal5(2943), colorPal9(2944)], [colorPal5(2945)], [colorPal3(2946), colorPal5(2947), colorPal9(2948)]], + [undefined, [colorPal3(2949), colorPal5(2950), colorPal9(2951)], [colorPal5(2952)], [colorPal3(2953), colorPal5(2954), colorPal9(2955)]], + [undefined, [colorPal3(2956), colorPal5(2957), colorPal9(2958)], [colorPal5(2959)], [colorPal3(2960), colorPal5(2961), colorPal9(2962)]], + [undefined, [colorPal3(2963), colorPal5(2964), colorPal9(2965)], [colorPal5(2966)], [colorPal3(2967), colorPal5(2968), colorPal9(2969)]], + [undefined, [colorPal3(2970), colorPal5(2971), colorPal9(2972)], [colorPal5(2973)], [colorPal3(2974), colorPal5(2975), colorPal9(2976)]] +]; +export const tails: AnimatedSprites = [ + [undefined, [colorPal3(2977), colorPal11(2978), colorPal13(2979), colorPal7(2980), colorPal5(2981), colorPal5(2982)], [colorPal3(2983), colorPal13(2984), colorPal9(2985), colorPal7(2986), colorPal5(2987), colorPal5(2988)], [colorPal3(2989), colorPal11(2990), colorPal9(2991), colorPal7(2992), colorPal5(2993), colorPal5(2994)], [colorPal3(2995), colorPal13(2996), colorPal11(2997), colorPal7(2998), colorPal5(2999), colorPal5(3000)], [colorPal3(3001), colorPal9(3002), colorPal13(3003), colorPal7(3004), colorPal5(3005), colorPal5(3006)], [colorPal3(3007), colorPal11(3008), colorPal13(3009), colorPal7(3010), colorPal5(3011), colorPal5(3012), colorPal5(3013)], [colorPal5(3014), colorPal13(3015), colorPal11(3016), colorPal7(3017), colorPal7(3018)], [colorPal3(3019), colorPal11(3020), colorPal13(3021), colorPal5(3022), colorPal7(3023), colorPal5(3024)], [colorPal3(3025), colorPal11(3026), colorPal11(3027), colorPal7(3028), colorPal5(3029), colorPal5(3030), colorPal11(3031)], [colorPal3(3032), colorPal11(3033), colorPal13(3034), colorPal5(3035)], [colorPal3(3036), colorPal11(3037), colorPal11(3038), colorPal7(3039), colorPal5(3040), colorPal5(3041)], [colorPal3(3042), colorPal9(3043), colorPal9(3044), colorPal7(3045), colorPal5(3046)], [colorPal3(3047), colorPal5(3048), colorPal9(3049), colorPal5(3050)], [colorPal5(3051), colorPal11(3052), colorPal9(3053), colorPal7(3054), colorPal7(3055)], [colorPal5(3056), colorPal7(3057)], [colorPal3(3058), colorPal5(3059), colorPal7(3060)], [colorPal5(3061), colorPal11(3062), colorPal9(3063), colorPal7(3064), colorPal7(3065)], [colorPal7(3066), colorPal13(3067), colorPal11(3068), colorPal9(3069), colorPal9(3070)], [colorPal5(3071), colorPal7(3072)], [colorPal3(3073), colorPal11(3074), colorPal13(3075), colorPal7(3076), colorPal5(3077), colorPal5(3078)], [colorPal3(3079), colorPal9(3080), colorPal11(3081), colorPal7(3082), colorPal5(3083), colorPal5(3084)], [colorPal3(3085), colorPal11(3086), colorPal13(3087), colorPal7(3088), colorPal5(3089), colorPal5(3090)], [colorPal3(3091), colorPal9(3092), colorPal11(3093), colorPal7(3094), colorPal5(3095), colorPal5(3096)], [colorPal3(3097), colorPal13(3098), colorPal7(3099)], [colorPal3(3100), colorPal5(3101), colorPal13(3102), colorPal13(3103), colorPal11(3104), colorPal11(3105)], [colorPal3(3106), colorPal9(3107), colorPal13(3108), colorPal13(3109), colorPal13(3110), colorPal11(3111)], [colorPal3(3112), colorPal7(3113), colorPal5(3114), colorPal13(3115), colorPal5(3116)], [colorPal5(3117), colorPal13(3118), colorPal9(3119), colorPal9(3120), colorPal7(3121), colorPal7(3122)], [colorPal5(3123), colorPal13(3124), colorPal9(3125), colorPal9(3126), colorPal7(3127), colorPal7(3128)], [colorPal3(3129), colorPal5(3130), colorPal7(3131)]], + [undefined, undefined, undefined, undefined, undefined, undefined, undefined, [colorPal5(3132), colorPal13(3133), colorPal11(3134), colorPal7(3135), colorPal7(3136)], undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, [colorPal3(3137), colorPal5(3138), colorPal7(3139)], [colorPal5(3140), colorPal9(3141), colorPal9(3142), colorPal7(3143), colorPal7(3144)], [colorPal7(3145), colorPal9(3146), colorPal11(3147), colorPal9(3148), colorPal17(3149)], [colorPal5(3150), colorPal7(3151)], undefined, undefined, undefined, undefined, undefined, [colorPal3(3152), colorPal5(3153), colorPal11(3154), colorPal13(3155), colorPal11(3156), colorPal11(3157)], [colorPal3(3158), colorPal9(3159), colorPal13(3160), colorPal13(3161), colorPal13(3162), colorPal11(3163)], undefined, undefined, [colorPal5(3164), colorPal13(3165), colorPal9(3166), colorPal9(3167), colorPal7(3168), colorPal7(3169)], [colorPal3(3170), colorPal5(3171), colorPal7(3172)]], + [undefined, [colorPal3(3173), colorPal11(3174), colorPal13(3175), colorPal7(3176), colorPal5(3177), colorPal5(3178)], undefined, undefined, [colorPal3(3179), colorPal13(3180), colorPal11(3181), colorPal7(3182), colorPal5(3183), colorPal5(3184)], [colorPal3(3185), colorPal9(3186), colorPal13(3187), colorPal7(3188), colorPal5(3189), colorPal5(3190)], [colorPal3(3191), colorPal11(3192), colorPal13(3193), colorPal7(3194), colorPal5(3195), colorPal5(3196), colorPal5(3197)], [colorPal5(3198), colorPal13(3199), colorPal11(3200), colorPal7(3201), colorPal7(3202)], [colorPal3(3203), colorPal11(3204), colorPal13(3205), colorPal5(3206), colorPal7(3207), colorPal5(3208)], [colorPal3(3209), colorPal11(3210), colorPal11(3211), colorPal7(3212), colorPal5(3213), colorPal5(3214), colorPal11(3215)], [colorPal3(3216), colorPal11(3217), colorPal13(3218), colorPal5(3219)], [colorPal3(3220), colorPal11(3221), colorPal11(3222), colorPal7(3223), colorPal5(3224), colorPal5(3225)], [colorPal3(3226), colorPal9(3227), colorPal9(3228), colorPal7(3229), colorPal5(3230)], [colorPal3(3231), colorPal5(3232), colorPal9(3233), colorPal5(3234)], undefined, [colorPal5(3235), colorPal7(3236)], [colorPal3(3237), colorPal5(3238), colorPal7(3239)], [colorPal3(3240), colorPal3(3241), colorPal3(3242), colorPal3(3243), colorPal3(3244)], [colorPal5(3245), colorPal5(3246), colorPal5(3247), colorPal5(3248), colorPal5(3249)], [colorPal5(3250), colorPal7(3251)], [colorPal3(3252), colorPal11(3253), colorPal13(3254), colorPal7(3255), colorPal5(3256), colorPal5(3257)], undefined, [colorPal3(3258), colorPal11(3259), colorPal13(3260), colorPal7(3261), colorPal5(3262), colorPal5(3263)], [colorPal3(3264), colorPal9(3265), colorPal11(3266), colorPal7(3267), colorPal5(3268), colorPal5(3269)], [colorPal3(3270), colorPal13(3271), colorPal7(3272)], [colorPal3(3273), colorPal3(3274), colorPal3(3275), colorPal13(3276), colorPal11(3277), colorPal11(3278)], [colorPal3(3279), colorPal9(3280), colorPal13(3281), colorPal13(3282), colorPal13(3283), colorPal11(3284)], [colorPal3(3285), colorPal7(3286), colorPal5(3287), colorPal13(3288), colorPal5(3289)], [colorPal5(3290), colorPal13(3291), colorPal9(3292), colorPal9(3293), colorPal7(3294), colorPal7(3295)], [colorPal5(3296), colorPal13(3297), colorPal9(3298), colorPal9(3299), colorPal7(3300), colorPal7(3301)], [colorPal3(3302), colorPal5(3303), colorPal7(3304)]] +]; +export const neckAccessories: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(3305), colorPal5(3306)], [colorPal3(3307), colorPal5(3308), colorPal9(3309), colorPal5(3310)], [colorPal3(3311), colorPal5(3312), colorPal7(3313), colorPal13(3314), colorPal7(3315)], [colorPal3(3316), colorPal5(3317)], [colorPal3(3318), colorPal7(3319), colorPal7(3320), colorPal5(3321), colorPal5(3322)], [colorPal3(3323), colorPal5(3324), colorPal7(3325)], [colorPal3(3326), colorPal5(3327)], [colorPal3(3328)], [colorPal3(3329)], [colorPal3(3330), colorPal5(3331), colorPal9(3332), colorPal5(3333)], [colorPal3(3334), colorPal5(3335), colorPal9(3336)], [colorPal3(3337), colorPal9(3338)], [colorPal3(3339)], [colorPal3(3340)], [colorPal3(3341), colorPal9(3342)]], + [undefined, [colorPal3(3343), colorPal5(3344)], [colorPal3(3345), colorPal5(3346), colorPal9(3347), colorPal5(3348)], [colorPal3(3349), colorPal5(3350), colorPal7(3351), colorPal13(3352), colorPal7(3353)], [colorPal3(3354), colorPal5(3355)], [colorPal3(3356), colorPal7(3357), colorPal7(3358), colorPal5(3359), colorPal5(3360)], [colorPal3(3361), colorPal5(3362), colorPal7(3363)], [colorPal3(3364), colorPal5(3365)], [colorPal3(3366)], [colorPal3(3367)], [colorPal3(3368), colorPal5(3369), colorPal9(3370), colorPal5(3371)], [colorPal3(3372), colorPal5(3373), colorPal9(3374)], [colorPal3(3375), colorPal9(3376)], [colorPal3(3377)], [colorPal3(3378)], [colorPal3(3379), colorPal9(3380)]], + [undefined, [colorPal3(3381), colorPal5(3382)], [colorPal3(3383), colorPal5(3384), colorPal9(3385), colorPal5(3386)], [colorPal3(3387), colorPal5(3388), colorPal7(3389), colorPal13(3390), colorPal7(3391)], [colorPal3(3392), colorPal5(3393)], [colorPal3(3394), colorPal7(3395), colorPal7(3396), colorPal5(3397), colorPal5(3398)], [colorPal3(3399), colorPal5(3400), colorPal7(3401)], [colorPal3(3402), colorPal5(3403)], [colorPal3(3404)], [colorPal3(3405)], [colorPal3(3406), colorPal5(3407), colorPal9(3408), colorPal5(3409)], [colorPal3(3410), colorPal5(3411), colorPal9(3412)], [colorPal3(3413), colorPal9(3414)], [colorPal3(3415)], [colorPal3(3416)], [colorPal3(3417), colorPal9(3418)]], + [undefined, [colorPal3(3419), colorPal5(3420)], [colorPal3(3421), colorPal5(3422), colorPal9(3423), colorPal5(3424)], [colorPal3(3425), colorPal5(3426), colorPal7(3427), colorPal13(3428), colorPal7(3429)], [colorPal3(3430), colorPal5(3431)], [colorPal3(3432), colorPal7(3433), colorPal7(3434), colorPal5(3435), colorPal5(3436)], [colorPal3(3437), colorPal5(3438), colorPal7(3439)], [colorPal3(3440), colorPal5(3441)], [colorPal3(3442)], [colorPal3(3443)], [colorPal3(3444), colorPal5(3445), colorPal9(3446), colorPal5(3447)], [colorPal3(3448), colorPal5(3449), colorPal9(3450)], [colorPal3(3451), colorPal9(3452)], [colorPal3(3453)], [colorPal3(3454)], [colorPal3(3455), colorPal9(3456)]], + [undefined, [colorPal3(3457), colorPal5(3458)], [colorPal3(3459), colorPal5(3460), colorPal9(3461), colorPal5(3462)], [colorPal3(3463), colorPal5(3464), colorPal7(3465), colorPal13(3466), colorPal7(3467)], [colorPal3(3468), colorPal5(3469)], [colorPal3(3470), colorPal7(3471), colorPal7(3472), colorPal5(3473), colorPal5(3474)], [colorPal3(3475), colorPal5(3476), colorPal7(3477)], [colorPal3(3478), colorPal5(3479)], [colorPal3(3480)], [colorPal3(3481)], [colorPal3(3482), colorPal5(3483), colorPal9(3484), colorPal5(3485)], [colorPal3(3486), colorPal5(3487), colorPal9(3488)], [colorPal3(3489), colorPal9(3490)], [colorPal3(3491)], [colorPal3(3492)], [colorPal3(3493), colorPal9(3494)]], + [undefined, [colorPal3(3495), colorPal5(3496)], [colorPal3(3497), colorPal5(3498), colorPal9(3499), colorPal5(3500)], [colorPal3(3501), colorPal5(3502), colorPal7(3503), colorPal13(3504), colorPal7(3505)], [colorPal3(3506), colorPal5(3507)], [colorPal3(3508), colorPal7(3509), colorPal7(3510), colorPal5(3511), colorPal5(3512)], [colorPal3(3513), colorPal5(3514), colorPal7(3515)], [colorPal3(3516), colorPal5(3517)], [colorPal3(3518)], [colorPal3(3519)], [colorPal3(3520), colorPal5(3521), colorPal9(3522), colorPal5(3523)], [colorPal3(3524), colorPal5(3525), colorPal9(3526)], [colorPal3(3527), colorPal9(3528)], [colorPal3(3529)], [colorPal3(3530)], [colorPal3(3531), colorPal9(3532)]], + [undefined, [colorPal3(3533), colorPal5(3534)], [colorPal3(3535), colorPal5(3536), colorPal9(3537), colorPal5(3538)], [colorPal3(3539), colorPal5(3540), colorPal7(3541), colorPal13(3542), colorPal7(3543)], [colorPal3(3544), colorPal5(3545)], [colorPal3(3546), colorPal7(3547), colorPal7(3548), colorPal5(3549), colorPal5(3550)], [colorPal3(3551), colorPal5(3552), colorPal7(3553)], [colorPal3(3554), colorPal5(3555)], [colorPal3(3556)], [colorPal3(3557)], [colorPal3(3558), colorPal5(3559), colorPal9(3560), colorPal5(3561)], [colorPal3(3562), colorPal5(3563), colorPal9(3564)], [colorPal3(3565), colorPal9(3566)], [colorPal3(3567)], [colorPal3(3568)], [colorPal3(3569), colorPal9(3570)]], + [undefined, [colorPal3(3571), colorPal5(3572)], [colorPal3(3573), colorPal5(3574), colorPal9(3575), colorPal5(3576)], [colorPal3(3577), colorPal5(3578), colorPal7(3579), colorPal13(3580), colorPal7(3581)], [colorPal3(3582), colorPal5(3583)], [colorPal3(3584), colorPal7(3585), colorPal7(3586), colorPal5(3587), colorPal5(3588)], [colorPal3(3589), colorPal5(3590), colorPal7(3591)], [colorPal3(3592), colorPal5(3593)], [colorPal3(3594)], [colorPal3(3595)], [colorPal3(3596), colorPal5(3597), colorPal9(3598), colorPal5(3599)], [colorPal3(3600), colorPal5(3601), colorPal9(3602)], [colorPal3(3603), colorPal9(3604)], [colorPal3(3605)], [colorPal3(3606)], [colorPal3(3607), colorPal9(3608)]], + [undefined, [colorPal3(3609), colorPal5(3610)], [colorPal3(3611), colorPal5(3612), colorPal9(3613), colorPal5(3614)], [colorPal3(3615), colorPal5(3616), colorPal7(3617), colorPal13(3618), colorPal7(3619)], [colorPal3(3620), colorPal5(3621)], [colorPal3(3622), colorPal7(3623), colorPal7(3624), colorPal5(3625), colorPal5(3626)], [colorPal3(3627), colorPal5(3628), colorPal7(3629)], [colorPal3(3630), colorPal5(3631)], [colorPal3(3632)], [colorPal3(3633)], [colorPal3(3634), colorPal5(3635), colorPal9(3636), colorPal5(3637)], [colorPal3(3638), colorPal5(3639), colorPal9(3640)], [colorPal3(3641), colorPal9(3642)], [colorPal3(3643)], [colorPal3(3644)], [colorPal3(3645), colorPal9(3646)]], + [undefined, [colorPal3(3647), colorPal5(3648)], [colorPal3(3649), colorPal5(3650), colorPal9(3651), colorPal5(3652)], [colorPal3(3653), colorPal5(3654), colorPal7(3655), colorPal13(3656), colorPal7(3657)], [colorPal3(3658), colorPal5(3659)], [colorPal3(3660), colorPal7(3661), colorPal7(3662), colorPal5(3663), colorPal5(3664)], [colorPal3(3665), colorPal5(3666), colorPal7(3667)], [colorPal3(3668), colorPal5(3669)], [colorPal3(3670)], [colorPal3(3671)], [colorPal3(3672), colorPal5(3673), colorPal9(3674), colorPal5(3675)], [colorPal3(3676), colorPal5(3677), colorPal9(3678)], [colorPal3(3679), colorPal9(3680)], [colorPal3(3681)], [colorPal3(3682)], [colorPal3(3683), colorPal9(3684)]], + [undefined, [colorPal3(3685), colorPal5(3686)], [colorPal3(3687), colorPal5(3688), colorPal9(3689), colorPal5(3690)], [colorPal3(3691), colorPal5(3692), colorPal7(3693), colorPal13(3694), colorPal7(3695)], [colorPal3(3696), colorPal5(3697)], [colorPal3(3698), colorPal7(3699), colorPal7(3700), colorPal5(3701), colorPal5(3702)], [colorPal3(3703), colorPal5(3704), colorPal7(3705)], [colorPal3(3706), colorPal5(3707)], [colorPal3(3708)], [colorPal3(3709)], [colorPal3(3710), colorPal5(3711), colorPal9(3712), colorPal5(3713)], [colorPal3(3714), colorPal5(3715), colorPal9(3716)], [colorPal3(3717), colorPal9(3718)], [colorPal3(3719)], [colorPal3(3720)], [colorPal3(3721), colorPal9(3722)]], + [undefined, [colorPal3(3723), colorPal5(3724)], [colorPal3(3725), colorPal5(3726), colorPal9(3727), colorPal5(3728)], [colorPal3(3729), colorPal5(3730), colorPal7(3731), colorPal13(3732), colorPal7(3733)], [colorPal3(3734), colorPal5(3735)], [colorPal3(3736), colorPal7(3737), colorPal7(3738), colorPal5(3739), colorPal5(3740)], [colorPal3(3741), colorPal5(3742), colorPal7(3743)], [colorPal3(3744), colorPal5(3745)], [colorPal3(3746)], [colorPal3(3747)], [colorPal3(3748), colorPal5(3749), colorPal9(3750), colorPal5(3751)], [colorPal3(3752), colorPal5(3753), colorPal9(3754)], [colorPal3(3755), colorPal9(3756)], [colorPal3(3757)], [colorPal3(3758)], [colorPal3(3759), colorPal9(3760)]], + [undefined, [colorPal3(3761), colorPal5(3762)], [colorPal3(3763), colorPal5(3764), colorPal9(3765), colorPal5(3766)], [colorPal3(3767), colorPal5(3768), colorPal7(3769), colorPal13(3770), colorPal7(3771)], [colorPal3(3772), colorPal5(3773)], [colorPal3(3774), colorPal7(3775), colorPal7(3776), colorPal5(3777), colorPal5(3778)], [colorPal3(3779), colorPal5(3780), colorPal7(3781)], [colorPal3(3782), colorPal5(3783)], [colorPal3(3784)], [colorPal3(3785)], [colorPal3(3786), colorPal5(3787), colorPal9(3788), colorPal5(3789)], [colorPal3(3790), colorPal5(3791), colorPal9(3792)], [colorPal3(3793), colorPal9(3794)], [colorPal3(3795)], [colorPal3(3796)], [colorPal3(3797), colorPal9(3798)]], + [undefined, [colorPal3(3799), colorPal5(3800)], [colorPal3(3801), colorPal5(3802), colorPal9(3803), colorPal5(3804)], [colorPal3(3805), colorPal5(3806), colorPal7(3807), colorPal13(3808), colorPal7(3809)], [colorPal3(3810), colorPal5(3811)], [colorPal3(3812), colorPal7(3813), colorPal7(3814), colorPal5(3815), colorPal5(3816)], [colorPal3(3817), colorPal5(3818), colorPal7(3819)], [colorPal3(3820), colorPal5(3821)], [colorPal3(3822)], [colorPal3(3823)], [colorPal3(3824), colorPal5(3825), colorPal9(3826), colorPal5(3827)], [colorPal3(3828), colorPal5(3829), colorPal9(3830)], [colorPal3(3831), colorPal9(3832)], [colorPal3(3833)], [colorPal3(3834)], [colorPal3(3835), colorPal9(3836)]], + [undefined, [colorPal3(3837), colorPal5(3838)], [colorPal3(3839), colorPal5(3840), colorPal9(3841), colorPal5(3842)], [colorPal3(3843), colorPal5(3844), colorPal7(3845), colorPal13(3846), colorPal7(3847)], [colorPal3(3848), colorPal5(3849)], [colorPal3(3850), colorPal7(3851), colorPal7(3852), colorPal5(3853), colorPal5(3854)], [colorPal3(3855), colorPal5(3856), colorPal7(3857)], [colorPal3(3858), colorPal5(3859)], [colorPal3(3860)], [colorPal3(3861)], [colorPal3(3862), colorPal5(3863), colorPal9(3864), colorPal5(3865)], [colorPal3(3866), colorPal5(3867), colorPal9(3868)], [colorPal3(3869), colorPal9(3870)], [colorPal3(3871)], [colorPal3(3872)], [colorPal3(3873), colorPal9(3874)]] +]; +export const chestAccessoriesBehind: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(3875), colorPal3(3876), colorPal7(3877)], [], [], []], + [undefined, [colorPal3(3878), colorPal3(3879), colorPal7(3880)], [], [], []], + [undefined, [colorPal3(3881), colorPal3(3882), colorPal7(3883)], [], [], []], + [undefined, [colorPal3(3884), colorPal3(3885), colorPal7(3886)], [], [], []], + [undefined, [colorPal3(3887), colorPal3(3888), colorPal7(3889)], [], [], []], + [undefined, [colorPal3(3890), colorPal3(3891), colorPal7(3892)], [], [], []], + [undefined, [colorPal3(3893), colorPal3(3894), colorPal7(3895)], [], [], []], + [undefined, [colorPal3(3896), colorPal3(3897), colorPal7(3898)], [], [], []], + [undefined, [colorPal3(3899), colorPal3(3900), colorPal7(3901)], [], [], []], + [undefined, [colorPal3(3902), colorPal3(3903), colorPal7(3904)], [], [], []], + [undefined, [colorPal3(3905), colorPal3(3906), colorPal7(3907)], [], [], []], + [undefined, [colorPal3(3908), colorPal3(3909), colorPal7(3910)], [], [], []], + [undefined, [colorPal17(3911), colorPal3(3912), colorPal7(3913)], [], [], []], + [undefined, [colorPal3(3914), colorPal3(3915), colorPal7(3916)], [], [], []], + [undefined, [colorPal3(3917), colorPal3(3918), colorPal7(3919)], [], [], []] +]; +export const chestAccessories: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(3920), colorPal5(3921), colorPal9(3922)], [colorPal3(3923), colorPal7(3924), colorPal5(3925), colorPal5(3926), colorPal7(3927), colorPal5(3928), colorPal7(3929), colorPal5(3930), colorPal7(3931), colorPal11(3932), colorPal9(3933), colorPal5(3934), colorPal11(3935)], [colorPal5(3936), colorPal7(3937), colorPal11(3938)], [colorPal5(3939), colorPal7(3940)]], + [undefined, [colorPal3(3941), colorPal5(3942), colorPal9(3943)], [colorPal3(3944), colorPal7(3945), colorPal5(3946), colorPal5(3947), colorPal7(3948), colorPal5(3949), colorPal7(3950), colorPal5(3951), colorPal7(3952), colorPal11(3953), colorPal9(3954), colorPal5(3955), colorPal11(3956)], [colorPal5(3957), colorPal7(3958), colorPal11(3959)], [colorPal5(3960), colorPal7(3961)]], + [undefined, [colorPal3(3962), colorPal5(3963), colorPal9(3964)], [colorPal3(3965), colorPal7(3966), colorPal5(3967), colorPal5(3968), colorPal7(3969), colorPal5(3970), colorPal7(3971), colorPal5(3972), colorPal7(3973), colorPal11(3974), colorPal9(3975), colorPal5(3976), colorPal11(3977)], [colorPal5(3978), colorPal7(3979), colorPal11(3980)], [colorPal5(3981), colorPal7(3982)]], + [undefined, [colorPal3(3983), colorPal5(3984), colorPal9(3985)], [colorPal3(3986), colorPal7(3987), colorPal5(3988), colorPal5(3989), colorPal7(3990), colorPal5(3991), colorPal7(3992), colorPal5(3993), colorPal7(3994), colorPal11(3995), colorPal9(3996), colorPal5(3997), colorPal11(3998)], [colorPal5(3999), colorPal7(4000), colorPal11(4001)], [colorPal5(4002), colorPal7(4003)]], + [undefined, [colorPal3(4004), colorPal5(4005), colorPal9(4006)], [colorPal3(4007), colorPal7(4008), colorPal5(4009), colorPal5(4010), colorPal7(4011), colorPal5(4012), colorPal7(4013), colorPal5(4014), colorPal7(4015), colorPal11(4016), colorPal9(4017), colorPal5(4018), colorPal11(4019)], [colorPal5(4020), colorPal7(4021), colorPal11(4022)], [colorPal5(4023), colorPal7(4024)]], + [undefined, [colorPal3(4025), colorPal5(4026), colorPal9(4027)], [colorPal3(4028), colorPal7(4029), colorPal5(4030), colorPal5(4031), colorPal7(4032), colorPal5(4033), colorPal7(4034), colorPal5(4035), colorPal7(4036), colorPal11(4037), colorPal9(4038), colorPal5(4039), colorPal11(4040)], [colorPal5(4041), colorPal7(4042), colorPal11(4043)], [colorPal5(4044), colorPal7(4045)]], + [undefined, [colorPal3(4046), colorPal5(4047), colorPal9(4048)], [colorPal3(4049), colorPal7(4050), colorPal5(4051), colorPal5(4052), colorPal7(4053), colorPal5(4054), colorPal7(4055), colorPal5(4056), colorPal7(4057), colorPal11(4058), colorPal9(4059), colorPal5(4060), colorPal11(4061)], [colorPal5(4062), colorPal7(4063), colorPal11(4064)], [colorPal5(4065), colorPal7(4066)]], + [undefined, [colorPal3(4067), colorPal5(4068), colorPal9(4069)], [colorPal3(4070), colorPal7(4071), colorPal5(4072), colorPal5(4073), colorPal7(4074), colorPal5(4075), colorPal7(4076), colorPal5(4077), colorPal7(4078), colorPal11(4079), colorPal9(4080), colorPal5(4081), colorPal11(4082)], [colorPal5(4083), colorPal7(4084), colorPal11(4085)], [colorPal5(4086), colorPal7(4087)]], + [undefined, [colorPal3(4088), colorPal5(4089), colorPal9(4090)], [colorPal3(4091), colorPal7(4092), colorPal5(4093), colorPal5(4094), colorPal7(4095), colorPal5(4096), colorPal7(4097), colorPal5(4098), colorPal7(4099), colorPal11(4100), colorPal9(4101), colorPal5(4102), colorPal11(4103)], [colorPal5(4104), colorPal7(4105), colorPal11(4106)], [colorPal5(4107), colorPal7(4108)]], + [undefined, [colorPal3(4109), colorPal5(4110), colorPal9(4111)], [colorPal3(4112), colorPal7(4113), colorPal5(4114), colorPal5(4115), colorPal7(4116), colorPal5(4117), colorPal7(4118), colorPal5(4119), colorPal7(4120), colorPal11(4121), colorPal9(4122), colorPal5(4123), colorPal11(4124)], [colorPal5(4125), colorPal7(4126), colorPal11(4127)], [colorPal5(4128), colorPal7(4129)]], + [undefined, [colorPal3(4130), colorPal5(4131), colorPal9(4132)], [colorPal3(4133), colorPal7(4134), colorPal5(4135), colorPal5(4136), colorPal7(4137), colorPal5(4138), colorPal7(4139), colorPal5(4140), colorPal7(4141), colorPal11(4142), colorPal9(4143), colorPal5(4144), colorPal11(4145)], [colorPal5(4146), colorPal7(4147), colorPal11(4148)], [colorPal5(4149), colorPal7(4150)]], + [undefined, [colorPal3(4151), colorPal5(4152), colorPal9(4153)], [colorPal3(4154), colorPal7(4155), colorPal5(4156), colorPal5(4157), colorPal7(4158), colorPal5(4159), colorPal7(4160), colorPal5(4161), colorPal7(4162), colorPal11(4163), colorPal9(4164), colorPal5(4165), colorPal11(4166)], [colorPal5(4167), colorPal7(4168), colorPal11(4169)], [colorPal5(4170), colorPal7(4171)]], + [undefined, [colorPal3(4172), colorPal5(4173), colorPal9(4174)], [colorPal3(4175), colorPal7(4176), colorPal5(4177), colorPal5(4178), colorPal7(4179), colorPal5(4180), colorPal7(4181), colorPal5(4182), colorPal7(4183), colorPal11(4184), colorPal9(4185), colorPal5(4186), colorPal11(4187)], [colorPal5(4188), colorPal7(4189), colorPal11(4190)], [colorPal5(4191), colorPal7(4192)]], + [undefined, [colorPal3(4193), colorPal5(4194), colorPal9(4195)], [colorPal3(4196), colorPal7(4197), colorPal5(4198), colorPal5(4199), colorPal7(4200), colorPal5(4201), colorPal7(4202), colorPal5(4203), colorPal7(4204), colorPal11(4205), colorPal9(4206), colorPal5(4207), colorPal11(4208)], [colorPal5(4209), colorPal7(4210), colorPal11(4211)], [colorPal5(4212), colorPal7(4213)]], + [undefined, [colorPal3(4214), colorPal5(4215), colorPal9(4216)], [colorPal3(4217), colorPal7(4218), colorPal5(4219), colorPal5(4220), colorPal7(4221), colorPal5(4222), colorPal7(4223), colorPal5(4224), colorPal7(4225), colorPal11(4226), colorPal9(4227), colorPal5(4228), colorPal11(4229)], [colorPal5(4230), colorPal7(4231), colorPal11(4232)], [colorPal5(4233), colorPal7(4234)]] +]; +export const backAccessories: AnimatedSprites = [ + undefined, + [undefined, [colorPal3(4235)], [colorPal3(4236), colorPal7(4237), colorPal13(4238), colorPal5(4239)], [colorPal3(4240), colorPal7(4241), colorPal13(4242), colorPal5(4243)], [colorPal3(4244), colorPal9(4245), colorPal13(4246), colorPal5(4247)], [colorPal3(4248), colorPal3(4249)]], + [undefined, [colorPal3(4250)], [colorPal3(4251), colorPal7(4252), colorPal13(4253), colorPal5(4254)], [colorPal3(4255), colorPal7(4256), colorPal13(4257), colorPal5(4258)], [colorPal3(4259), colorPal9(4260), colorPal13(4261), colorPal5(4262)], [colorPal3(4263), colorPal3(4264)]], + [undefined, [colorPal3(4265)], [colorPal3(4266), colorPal7(4267), colorPal13(4268), colorPal5(4269)], [colorPal3(4270), colorPal7(4271), colorPal13(4272), colorPal5(4273)], [colorPal3(4274), colorPal9(4275), colorPal13(4276), colorPal5(4277)], [colorPal3(4278), colorPal3(4279)]], + [undefined, [colorPal3(4280)], [colorPal3(4281), colorPal7(4282), colorPal13(4283), colorPal5(4284)], [colorPal3(4285), colorPal7(4286), colorPal13(4287), colorPal5(4288)], [colorPal3(4289), colorPal9(4290), colorPal13(4291), colorPal5(4292)], [colorPal3(4293), colorPal3(4294)]], + [undefined, [colorPal3(4295)], [colorPal3(4296), colorPal7(4297), colorPal13(4298), colorPal5(4299)], [colorPal3(4300), colorPal7(4301), colorPal13(4302), colorPal5(4303)], [colorPal3(4304), colorPal9(4305), colorPal13(4306), colorPal5(4307)], [colorPal3(4308), colorPal3(4309)]], + [undefined, [colorPal3(4310)], [colorPal3(4311), colorPal7(4312), colorPal13(4313), colorPal5(4314)], [colorPal3(4315), colorPal7(4316), colorPal13(4317), colorPal5(4318)], [colorPal3(4319), colorPal9(4320), colorPal13(4321), colorPal5(4322)], [colorPal3(4323), colorPal3(4324)]], + [undefined, [colorPal3(4325)], [colorPal3(4326), colorPal7(4327), colorPal13(4328), colorPal5(4329)], [colorPal3(4330), colorPal7(4331), colorPal13(4332), colorPal5(4333)], [colorPal3(4334), colorPal9(4335), colorPal13(4336), colorPal5(4337)], [colorPal3(4338), colorPal3(4339)]], + [undefined, [colorPal3(4340)], [colorPal3(4341), colorPal7(4342), colorPal13(4343), colorPal5(4344)], [colorPal3(4345), colorPal7(4346), colorPal13(4347), colorPal5(4348)], [colorPal3(4349), colorPal9(4350), colorPal13(4351), colorPal5(4352)], [colorPal3(4353), colorPal3(4354)]], + [undefined, [colorPal3(4355)], [colorPal3(4356), colorPal7(4357), colorPal13(4358), colorPal5(4359)], [colorPal3(4360), colorPal7(4361), colorPal13(4362), colorPal5(4363)], [colorPal3(4364), colorPal9(4365), colorPal13(4366), colorPal5(4367)], [colorPal3(4368), colorPal3(4369)]], + [undefined, [colorPal3(4370)], [colorPal3(4371), colorPal7(4372), colorPal13(4373), colorPal5(4374)], [colorPal3(4375), colorPal7(4376), colorPal13(4377), colorPal5(4378)], [colorPal3(4379), colorPal9(4380), colorPal13(4381), colorPal5(4382)], [colorPal3(4383), colorPal3(4384)]], + [undefined, [colorPal3(4385)], [colorPal3(4386), colorPal7(4387), colorPal13(4388), colorPal5(4389)], [colorPal3(4390), colorPal7(4391), colorPal13(4392), colorPal5(4393)], [colorPal3(4394), colorPal9(4395), colorPal13(4396), colorPal5(4397)], [colorPal3(4398), colorPal3(4399)]], + [undefined, [colorPal3(4400)], [colorPal3(4401), colorPal7(4402), colorPal13(4403), colorPal5(4404)], [colorPal3(4405), colorPal7(4406), colorPal13(4407), colorPal5(4408)], [colorPal3(4409), colorPal9(4410), colorPal13(4411), colorPal5(4412)], [colorPal3(4413), colorPal3(4414)]], + [undefined, [colorPal3(4415)], [colorPal3(4416), colorPal7(4417), colorPal13(4418), colorPal5(4419)], [colorPal3(4420), colorPal7(4421), colorPal13(4422), colorPal5(4423)], [colorPal3(4424), colorPal9(4425), colorPal13(4426), colorPal5(4427)], [colorPal3(4428), colorPal3(4429)]], + [undefined, [colorPal3(4430)], [colorPal3(4431), colorPal7(4432), colorPal13(4433), colorPal5(4434)], [colorPal3(4435), colorPal7(4436), colorPal13(4437), colorPal5(4438)], [colorPal3(4439), colorPal9(4440), colorPal13(4441), colorPal5(4442)], [colorPal3(4443), colorPal3(4444)]], + [undefined, [colorPal3(4445)], [colorPal3(4446), colorPal7(4447), colorPal13(4448), colorPal5(4449)], [colorPal3(4450), colorPal7(4451), colorPal13(4452), colorPal5(4453)], [colorPal3(4454), colorPal9(4455), colorPal13(4456), colorPal5(4457)], [colorPal3(4458), colorPal3(4459)]] +]; +export const waistAccessories: AnimatedSprites = [ + undefined, + [undefined, [colorPal9(4460)], [colorPal9(4461)], [colorPal9(4462)]], + [undefined, [colorPal9(4463)], [colorPal9(4464)], [colorPal9(4465)]], + [undefined, [colorPal9(4466)], [colorPal9(4467)], [colorPal9(4468)]], + [undefined, [colorPal9(4469)], [colorPal9(4470)], [colorPal9(4471)]], + [undefined, [colorPal9(4472)], [colorPal9(4473)], [colorPal9(4474)]], + [undefined, [colorPal9(4475)], [colorPal9(4476)], [colorPal9(4477)]], + [undefined, [colorPal9(4478)], [colorPal9(4479)], [colorPal9(4480)]], + [undefined, [colorPal9(4481)], [colorPal9(4482)], [colorPal9(4483)]], + [undefined, [colorPal9(4484)], [colorPal9(4485)], [colorPal9(4486)]], + [undefined, [colorPal9(4487)], [colorPal9(4488)], [colorPal9(4489)]], + [undefined, [colorPal9(4490)], [colorPal9(4491)], [colorPal9(4492)]], + [undefined, [colorPal9(4493)], [colorPal9(4494)], [colorPal9(4495)]], + [undefined, [colorPal9(4496)], [colorPal9(4497)], [colorPal9(4498)]], + [undefined, [colorPal9(4499)], [colorPal9(4500)], [colorPal9(4501)]], + [undefined, [colorPal9(4502)], [colorPal9(4503)], [colorPal9(4504)]], + [undefined, [colorPal9(4505)], [colorPal9(4506)], [colorPal9(4507)]] +]; +export const head: AnimatedSprites = [ + undefined, + [[colorPal3(4508), colorPal7(4509), colorPal7(4510), colorPal7(4511), colorPal7(4512), colorPal7(4513), colorPal11(4514), colorPal11(4515), colorPal9(4516), colorPal9(4517), colorPal5(4518), colorPal5(4519), colorPal9(4520), colorPal5(4521), colorPal7(4522), colorPal7(4523), colorPal7(4524), colorPal7(4525), colorPal5(4526), colorPal11(4527)]], + [[colorPal3(4528)]] +]; +export const earsFar: StaticSprites = [ + [colorPal3(4529), colorPal7(4530)], + [colorPal3(4531), colorPal9(4532)], + [colorPal3(4533), colorPal9(4534)], + [colorPal3(4535), colorPal5(4536)], + [colorPal3(4537), colorPal7(4538), colorPal7(4539)], + [colorPal3(4540), colorPal7(4541), colorPal9(4542)] +]; + +; +export const ears: StaticSprites = [ + [colorPal3(4543), colorPal7(4544)], + [colorPal3(4545), colorPal9(4546)], + [colorPal3(4547), colorPal9(4548)], + [colorPal3(4549), colorPal5(4550)], + [colorPal3(4551), colorPal7(4552), colorPal7(4553)], + [colorPal3(4554), colorPal7(4555), colorPal9(4556)] +]; + +; +export const hornsBehind: StaticSprites = [ + undefined, + undefined, + undefined, + undefined, + [colorPal3(4557), colorPal5(4558)], + [colorPal3(4559), colorPal7(4560)], + [colorPal3(4561), colorPal9(4562), colorPal9(4563)], + [colorPal3(4564), colorPal9(4565), colorPal13(4566)], + [colorPal3(4567), colorPal9(4568), colorPal13(4569)], + [colorPal3(4570), colorPal7(4571)], + [colorPal3(4572), colorPal11(4573), colorPal5(4574), colorPal5(4575)], + [colorPal3(4576), colorPal9(4577), colorPal5(4578), colorPal5(4579)], + [colorPal3(4580), colorPal9(4581), colorPal5(4582)], + [colorPal3(4583), colorPal7(4584)], + [] +]; + +; +export const horns: StaticSprites = [ + undefined, + [colorPal3(4585), colorPal7(4586)], + [colorPal3(4587), colorPal5(4588)], + [colorPal3(4589), colorPal7(4590)], + [colorPal3(4591), colorPal5(4592)], + [colorPal3(4593), colorPal7(4594)], + [colorPal3(4595), colorPal9(4596), colorPal9(4597)], + [colorPal3(4598), colorPal9(4599), colorPal13(4600)], + [colorPal3(4601), colorPal9(4602), colorPal13(4603)], + [colorPal3(4604), colorPal7(4605)], + [colorPal3(4606), colorPal11(4607), colorPal5(4608), colorPal5(4609)], + [colorPal3(4610), colorPal9(4611), colorPal5(4612), colorPal5(4613)], + [colorPal3(4614), colorPal9(4615), colorPal5(4616)], + [colorPal3(4617), colorPal7(4618)], + [colorPal3(4619), colorPal9(4620)] +]; + +; +export const behindManes: StaticSprites = [ + undefined, + [colorPal3(4621), colorPal13(4622), colorPal13(4623), colorPal7(4624), colorPal5(4625), colorPal5(4626)], + undefined, + [colorPal3(4627), colorPal13(4628), colorPal13(4629), colorPal5(4630), colorPal5(4631), colorPal7(4632)], + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal3(4633), colorPal13(4634), colorPal13(4635), colorPal7(4636), colorPal5(4637)], + [colorPal3(4638), colorPal13(4639), colorPal13(4640), colorPal7(4641), colorPal5(4642), colorPal5(4643), colorPal13(4644)], + [colorPal3(4645), colorPal9(4646), colorPal11(4647), colorPal5(4648), colorPal5(4649)], + [colorPal3(4650), colorPal9(4651), colorPal9(4652), colorPal7(4653), colorPal5(4654), colorPal5(4655)], + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal3(4656), colorPal13(4657), colorPal13(4658), colorPal7(4659), colorPal5(4660), colorPal5(4661)], + undefined, + [colorPal3(4662), colorPal11(4663), colorPal11(4664), colorPal7(4665), colorPal5(4666), colorPal5(4667)], + [colorPal3(4668), colorPal13(4669), colorPal13(4670), colorPal7(4671), colorPal5(4672), colorPal5(4673)], + undefined, + undefined, + undefined, + [colorPal3(4674), colorPal7(4675), colorPal5(4676), colorPal13(4677), colorPal5(4678)], + undefined, + [colorPal3(4679), colorPal9(4680), colorPal13(4681), colorPal7(4682), colorPal5(4683), colorPal5(4684)], + [colorPal3(4685), colorPal9(4686), colorPal13(4687), colorPal7(4688), colorPal5(4689), colorPal5(4690)], + undefined, + [colorPal3(4691), colorPal13(4692), colorPal5(4693), colorPal11(4694), colorPal5(4695)], + [] +]; + +; +export const backBehindManes: StaticSprites = [ + undefined, + [colorPal3(4696), colorPal7(4697), colorPal7(4698), colorPal5(4699), colorPal5(4700), colorPal5(4701)], + [colorPal5(4702), colorPal13(4703), colorPal11(4704), colorPal9(4705), colorPal7(4706), colorPal7(4707)], + undefined, + [colorPal5(4708), colorPal11(4709), colorPal11(4710), colorPal7(4711), colorPal7(4712), colorPal7(4713)], + [colorPal3(4714), colorPal7(4715), colorPal7(4716), colorPal5(4717), colorPal5(4718)], + [colorPal3(4719), colorPal9(4720), colorPal11(4721), colorPal7(4722), colorPal5(4723), colorPal5(4724)], + [colorPal3(4725), colorPal9(4726), colorPal5(4727), colorPal5(4728), colorPal5(4729)], + undefined, + [colorPal3(4730), colorPal5(4731), colorPal11(4732), colorPal5(4733)], + undefined, + undefined, + [colorPal3(4734), colorPal5(4735), colorPal5(4736), colorPal5(4737)], + [colorPal3(4738), colorPal11(4739), colorPal7(4740), colorPal5(4741), colorPal5(4742)], + undefined, + undefined, + [colorPal3(4743), colorPal7(4744), colorPal7(4745), colorPal7(4746), colorPal5(4747), colorPal5(4748)], + [colorPal5(4749), colorPal13(4750), colorPal13(4751), colorPal9(4752), colorPal7(4753), colorPal7(4754)], + [colorPal3(4755), colorPal7(4756), colorPal5(4757), colorPal7(4758), colorPal5(4759), colorPal5(4760)], + [colorPal3(4761), colorPal7(4762), colorPal13(4763), colorPal7(4764), colorPal5(4765), colorPal5(4766)], + [colorPal5(4767), colorPal9(4768), colorPal13(4769), colorPal7(4770)], + [colorPal3(4771), colorPal7(4772), colorPal13(4773), colorPal7(4774), colorPal5(4775), colorPal5(4776)], + [colorPal3(4777), colorPal9(4778), colorPal5(4779), colorPal9(4780), colorPal5(4781)], + [colorPal3(4782), colorPal7(4783), colorPal7(4784), colorPal5(4785)], + [colorPal3(4786), colorPal11(4787), colorPal7(4788), colorPal5(4789), colorPal5(4790)], + [colorPal3(4791), colorPal11(4792), colorPal7(4793), colorPal7(4794), colorPal5(4795)], + [colorPal3(4796), colorPal11(4797), colorPal7(4798), colorPal7(4799), colorPal5(4800), colorPal5(4801), colorPal5(4802)], + [colorPal3(4803)], + [colorPal3(4804), colorPal9(4805), colorPal7(4806), colorPal5(4807), colorPal5(4808)] +]; + +; +export const backFrontManes: StaticSprites = [ + undefined, + [colorPal3(4809), colorPal7(4810), colorPal7(4811), colorPal5(4812), colorPal5(4813), colorPal5(4814)], + undefined, + [colorPal3(4815), colorPal9(4816), colorPal9(4817), colorPal7(4818), colorPal5(4819), colorPal5(4820)], + undefined, + undefined, + undefined, + undefined, + [colorPal3(4821), colorPal11(4822), colorPal11(4823), colorPal5(4824)], + undefined, + [colorPal3(4825), colorPal7(4826), colorPal7(4827), colorPal5(4828), colorPal5(4829)], + [colorPal3(4830), colorPal9(4831), colorPal9(4832), colorPal7(4833), colorPal5(4834)], + undefined, + undefined, + [colorPal3(4835), colorPal7(4836), colorPal9(4837), colorPal5(4838), colorPal5(4839), colorPal5(4840)], + [colorPal3(4841), colorPal7(4842), colorPal13(4843), colorPal5(4844), colorPal5(4845), colorPal5(4846)], + [colorPal3(4847), colorPal7(4848), colorPal7(4849), colorPal7(4850), colorPal5(4851), colorPal5(4852)], + [colorPal5(4853), colorPal13(4854), colorPal13(4855), colorPal9(4856), colorPal7(4857), colorPal7(4858)], + [colorPal3(4859), colorPal7(4860), colorPal5(4861), colorPal7(4862), colorPal5(4863), colorPal5(4864)], + undefined, + [colorPal5(4865), colorPal9(4866), colorPal13(4867), colorPal7(4868)], + [colorPal3(4869), colorPal7(4870), colorPal13(4871), colorPal7(4872), colorPal5(4873), colorPal5(4874)], + [colorPal3(4875), colorPal9(4876), colorPal5(4877), colorPal9(4878), colorPal5(4879)], + [], + [], + [], + [], + [], + [] +]; + +; +export const topManes: StaticSprites = [ + undefined, + [colorPal3(4880), colorPal13(4881), colorPal13(4882), colorPal7(4883), colorPal5(4884), colorPal5(4885)], + [colorPal3(4886), colorPal11(4887), colorPal11(4888), colorPal7(4889), colorPal5(4890), colorPal5(4891)], + [colorPal3(4892), colorPal13(4893), colorPal13(4894), colorPal5(4895), colorPal5(4896), colorPal7(4897)], + [colorPal3(4898), colorPal11(4899), colorPal11(4900), colorPal7(4901), colorPal5(4902), colorPal5(4903)], + [colorPal3(4904), colorPal9(4905), colorPal9(4906), colorPal7(4907), colorPal5(4908)], + [colorPal3(4909), colorPal11(4910), colorPal9(4911), colorPal7(4912), colorPal5(4913), colorPal5(4914)], + [colorPal3(4915), colorPal9(4916), colorPal9(4917), colorPal7(4918), colorPal5(4919), colorPal5(4920)], + [colorPal3(4921), colorPal11(4922), colorPal11(4923), colorPal7(4924), colorPal5(4925), colorPal5(4926)], + [colorPal3(4927), colorPal13(4928), colorPal13(4929), colorPal7(4930), colorPal5(4931)], + [colorPal3(4932), colorPal13(4933), colorPal13(4934), colorPal7(4935), colorPal5(4936), colorPal5(4937), colorPal13(4938)], + [colorPal3(4939), colorPal9(4940), colorPal11(4941), colorPal5(4942), colorPal5(4943)], + [colorPal3(4944), colorPal9(4945), colorPal9(4946), colorPal7(4947), colorPal5(4948), colorPal5(4949)], + [colorPal3(4950), colorPal9(4951), colorPal7(4952), colorPal5(4953), colorPal5(4954)], + [colorPal3(4955), colorPal11(4956), colorPal9(4957), colorPal7(4958), colorPal5(4959)], + [colorPal3(4960), colorPal11(4961), colorPal7(4962), colorPal5(4963), colorPal5(4964)], + [colorPal3(4965), colorPal7(4966)], + [colorPal3(4967), colorPal11(4968), colorPal9(4969), colorPal7(4970), colorPal5(4971), colorPal5(4972)], + [colorPal3(4973), colorPal13(4974), colorPal13(4975), colorPal7(4976), colorPal5(4977), colorPal5(4978)], + [colorPal3(4979), colorPal9(4980), colorPal13(4981), colorPal7(4982), colorPal5(4983), colorPal5(4984)], + [colorPal3(4985), colorPal11(4986), colorPal11(4987), colorPal7(4988), colorPal5(4989), colorPal5(4990)], + [colorPal3(4991), colorPal13(4992), colorPal13(4993), colorPal7(4994), colorPal5(4995), colorPal5(4996)], + [colorPal3(4997), colorPal13(4998), colorPal13(4999), colorPal7(5000), colorPal5(5001), colorPal5(5002), colorPal9(5003)], + [colorPal3(5004), colorPal11(5005), colorPal13(5006), colorPal7(5007), colorPal5(5008), colorPal5(5009)], + [colorPal3(5010), colorPal13(5011), colorPal11(5012), colorPal7(5013), colorPal5(5014), colorPal5(5015)], + [colorPal3(5016), colorPal7(5017), colorPal5(5018), colorPal13(5019), colorPal5(5020)], + [colorPal3(5021), colorPal11(5022), colorPal11(5023), colorPal7(5024), colorPal5(5025), colorPal5(5026)], + [colorPal3(5027), colorPal9(5028), colorPal13(5029), colorPal7(5030), colorPal5(5031), colorPal5(5032)], + [colorPal3(5033), colorPal9(5034), colorPal13(5035), colorPal7(5036), colorPal5(5037), colorPal5(5038)], + [colorPal3(5039), colorPal11(5040), colorPal13(5041), colorPal7(5042), colorPal5(5043), colorPal5(5044)], + [colorPal3(5045), colorPal13(5046), colorPal5(5047), colorPal11(5048), colorPal5(5049)], + [colorPal3(5050), colorPal7(5051), colorPal5(5052), colorPal7(5053), colorPal5(5054), colorPal5(5055), colorPal7(5056)] +]; + +; +export const frontManes: StaticSprites = [ + undefined, + [colorPal3(5057), colorPal13(5058), colorPal13(5059), colorPal7(5060), colorPal5(5061), colorPal5(5062)], + [colorPal3(5063), colorPal11(5064), colorPal11(5065), colorPal7(5066), colorPal5(5067), colorPal5(5068)], + [colorPal3(5069), colorPal13(5070), colorPal13(5071), colorPal5(5072), colorPal5(5073), colorPal7(5074)], + [colorPal3(5075), colorPal11(5076), colorPal11(5077), colorPal7(5078), colorPal5(5079), colorPal5(5080)], + [colorPal3(5081), colorPal9(5082), colorPal9(5083), colorPal7(5084), colorPal5(5085)], + [colorPal3(5086), colorPal11(5087), colorPal9(5088), colorPal7(5089), colorPal5(5090), colorPal5(5091)], + undefined, + [colorPal3(5092), colorPal11(5093), colorPal11(5094), colorPal7(5095), colorPal5(5096), colorPal5(5097)], + undefined, + [colorPal3(5098), colorPal13(5099), colorPal13(5100), colorPal7(5101), colorPal5(5102), colorPal5(5103), colorPal13(5104)], + [colorPal3(5105), colorPal9(5106), colorPal11(5107), colorPal5(5108), colorPal5(5109)], + [colorPal3(5110), colorPal9(5111), colorPal9(5112), colorPal7(5113), colorPal5(5114), colorPal5(5115)], + undefined, + [colorPal3(5116), colorPal11(5117), colorPal9(5118), colorPal7(5119), colorPal5(5120)], + undefined, + undefined, + [colorPal3(5121), colorPal11(5122), colorPal9(5123), colorPal7(5124), colorPal5(5125), colorPal5(5126)], + [colorPal3(5127), colorPal13(5128), colorPal13(5129), colorPal7(5130), colorPal5(5131), colorPal5(5132)], + undefined, + [colorPal3(5133), colorPal11(5134), colorPal11(5135), colorPal7(5136), colorPal5(5137), colorPal5(5138)], + [colorPal3(5139), colorPal13(5140), colorPal13(5141), colorPal7(5142), colorPal5(5143), colorPal5(5144)], + [colorPal3(5145), colorPal13(5146), colorPal13(5147), colorPal7(5148), colorPal5(5149), colorPal5(5150), colorPal9(5151)], + undefined, + [colorPal3(5152), colorPal13(5153), colorPal11(5154), colorPal7(5155), colorPal5(5156), colorPal5(5157)], + [colorPal3(5158), colorPal7(5159), colorPal5(5160), colorPal13(5161), colorPal5(5162)], + [colorPal3(5163), colorPal11(5164), colorPal11(5165), colorPal7(5166), colorPal5(5167), colorPal5(5168)], + undefined, + undefined, + [colorPal3(5169), colorPal11(5170), colorPal13(5171), colorPal7(5172), colorPal5(5173), colorPal5(5174)], + undefined, + [colorPal3(5175), colorPal7(5176), colorPal5(5177), colorPal7(5178), colorPal5(5179), colorPal5(5180), colorPal7(5181)] +]; + +; +export const facialHairBehind: StaticSprites = [ + undefined, + [colorPal3(5182)], + [colorPal3(5183), colorPal7(5184)], + undefined, + [colorPal3(5185), colorPal7(5186), colorPal9(5187)], + [colorPal3(5188), colorPal5(5189)], + [colorPal3(5190), colorPal7(5191), colorPal9(5192)], + [colorPal3(5193), colorPal7(5194), colorPal17(5195)], + [colorPal3(5196), colorPal17(5197), colorPal7(5198)], + [colorPal3(5199), colorPal13(5200), colorPal7(5201), colorPal7(5202)], + [colorPal3(5203), colorPal13(5204), colorPal7(5205), colorPal9(5206)], + [colorPal3(5207), colorPal13(5208), colorPal7(5209), colorPal9(5210)], + [colorPal3(5211), colorPal13(5212), colorPal7(5213), colorPal9(5214)], + [colorPal5(5215), colorPal13(5216), colorPal9(5217), colorPal13(5218)], + [colorPal5(5219), colorPal13(5220), colorPal9(5221), colorPal13(5222)], + [colorPal3(5223), colorPal17(5224), colorPal7(5225), colorPal11(5226)] +]; + +; +export const facialHair: StaticSprites = [ + undefined, + undefined, + undefined, + [colorPal3(5227)], + [colorPal3(5228), colorPal7(5229), colorPal9(5230)], + [colorPal3(5231), colorPal5(5232)], + undefined, + [colorPal3(5233), colorPal7(5234), colorPal17(5235)], + [colorPal3(5236), colorPal17(5237), colorPal7(5238)], + [colorPal3(5239), colorPal13(5240), colorPal7(5241), colorPal7(5242)], + [colorPal3(5243), colorPal13(5244), colorPal7(5245), colorPal9(5246)], + [colorPal3(5247), colorPal13(5248), colorPal7(5249), colorPal9(5250)], + [colorPal3(5251), colorPal13(5252), colorPal7(5253), colorPal9(5254)], + [colorPal5(5255), colorPal13(5256), colorPal9(5257), colorPal13(5258)], + [colorPal5(5259), colorPal13(5260), colorPal9(5261), colorPal13(5262)], + [colorPal3(5263), colorPal17(5264), colorPal7(5265), colorPal11(5266)] +]; + +; +export const earAccessoriesBehind: StaticSprites = [ + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal3(5267)], + undefined, + [colorPal3(5268), colorPal7(5269), colorPal7(5270), colorPal7(5271)], + [colorPal3(5272), colorPal11(5273), colorPal11(5274), colorPal11(5275)], + [] +]; + +; +export const earAccessories: StaticSprites = [ + undefined, + [colorPal3(5276)], + [colorPal3(5277)], + [colorPal3(5278)], + [colorPal3(5279)], + [colorPal5(5280)], + [colorPal3(5281), colorPal11(5282)], + [colorPal3(5283)], + [colorPal3(5284)], + [colorPal3(5285)], + [colorPal3(5286), colorPal7(5287), colorPal7(5288), colorPal7(5289)], + [colorPal3(5290), colorPal11(5291), colorPal11(5292), colorPal11(5293)], + [colorPal3(5294)] +]; + +; +export const headAccessoriesBehind: StaticSprites = [ + undefined, + [colorPal3(5295), colorPal5(5296)], + [colorPal3(5297), colorPal9(5298), colorPal9(5299), colorPal9(5300)], + [colorPal3(5301), colorPal5(5302), colorPal7(5303), colorPal9(5304), colorPal7(5305)], + [colorPal3(5306), colorPal5(5307)], + [colorPal3(5308), colorPal7(5309)], + [colorPal3(5310), colorPal7(5311), colorPal13(5312)], + [colorPal3(5313), colorPal11(5314), colorPal11(5315)], + [colorPal3(5316), colorPal11(5317), colorPal13(5318)], + [colorPal3(5319), colorPal11(5320), colorPal7(5321), colorPal9(5322), colorPal9(5323)], + [colorPal3(5324), colorPal9(5325), colorPal13(5326)], + [colorPal5(5327), colorPal11(5328), colorPal13(5329)], + [colorPal3(5330), colorPal7(5331)], + [colorPal3(5332), colorPal9(5333), colorPal13(5334)], + [colorPal3(5335), colorPal9(5336)], + [colorPal3(5337), colorPal9(5338)], + [colorPal3(5339), colorPal7(5340)], + [colorPal3(5341), colorPal7(5342)], + [colorPal3(5343), colorPal7(5344), colorPal5(5345), colorPal5(5346)], + [colorPal3(5347), colorPal7(5348), colorPal11(5349)], + [colorPal5(5350)] +]; + +; +export const headAccessories: StaticSprites = [ + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal5(5351)] +]; + +; +export const faceAccessories: StaticSpritesExtra = [ + undefined, + [createColorExtraPal(5353, 3, 5352, [10])], + [colorPal3(5354), colorPal7(5355), colorPal7(5356)], + [createColorExtraPal(5358, 3, 5357, [11])], + [createColorExtraPal(5360, 3, 5359, [12])], + [createColorExtraPal(5362, 5, 5361, [11])], + [createColorExtraPal(5364, 3, 5363, [11])], + [createColorExtraPal(5366, 3, 5365, [11])], + [colorPal3(5367)], + [colorPal5(5368)], + [colorPal5(5369)], + [colorPal3(5370), colorPal7(5371), colorPal7(5372)], + [createColorExtraPal(5374, 3, 5373, [13])], + [createColorExtraPal(5376, 5, 5375, [13])], + [colorPal3(5377), colorPal5(5378)] +]; +export const faceAccessoriesExtra: StaticSprites = [ + undefined, + [createColorPalette(5352, [10])], + [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], + [createColorPalette(5357, [11])], + [createColorPalette(5359, [12])], + [createColorPalette(5361, [11])], + [createColorPalette(5363, [11])], + [createColorPalette(5365, [11])], + [emptyColorPalette()], + [emptyColorPalette()], + [emptyColorPalette()], + [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], + [createColorPalette(5373, [13])], + [createColorPalette(5375, [13])], + [emptyColorPalette(), emptyColorPalette()], +]; +; +export const faceAccessories2: StaticSpritesExtra = [ + undefined, + undefined, + undefined, + undefined, + [colorPal3(5379)], + [colorPal5(5380)], + undefined, + undefined, + undefined, + [colorPal5(5381)], + undefined, + [colorPal3(5382), colorPal7(5383), colorPal7(5384)], + undefined, + [colorPal5(5385)], + [colorPal3(5386), colorPal5(5387)] +]; +export const faceAccessories2Extra: StaticSprites = [ + undefined, + undefined, + undefined, + undefined, + [emptyColorPalette()], + [emptyColorPalette()], + undefined, + undefined, + undefined, + [emptyColorPalette()], + undefined, + [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], + undefined, + [emptyColorPalette()], + [emptyColorPalette(), emptyColorPalette()], +]; +; +export const extraAccessoriesBehind: StaticSprites = [ + [colorPal11(5388)], + [colorPal5(5389)], + undefined, + [colorPal7(5390)], + [colorPal9(5391)], + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal7(5392)], + [colorPal9(5393)], + [colorPal5(5394)], + [colorPal5(5395)], + [colorPal13(5396)], + [] +]; + +; +export const extraAccessories: StaticSprites = [ + [colorPal11(5397)], + [colorPal5(5398)], + [colorPal5(5399)], + [colorPal7(5400)], + [colorPal9(5401)], + [colorPal13(5402)], + [colorPal5(5403)], + [colorPal11(5404)], + [colorPal9(5405)], + [colorPal11(5406), colorPal11(5407)], + [colorPal9(5408)], + [colorPal9(5409), colorPal9(5410)], + [colorPal7(5411)], + [colorPal9(5412)], + undefined, + undefined, + [colorPal13(5413)], + [colorPal13(5414)] +]; + +; +export const pine_1 = createColorShadowPalette(5415, 5416, [14, 15, 16]); +export const pine_2 = createColorShadowPalette(5417, 5418, [14, 15, 16]); +export const pine_3Stump0 = createColorPalette(5419, [14, 15, 16]); +export const pine_3Crown0_0 = createColorShadowPalette(5420, 5421, [14, 15, 16]); +export const pine_4Stump0 = createColorPalette(5422, [14, 15, 16]); +export const pine_4Crown0_0 = createColorShadowPalette(5423, 5424, [14, 15, 16]); +export const pine_5Stump0 = createColorPalette(5425, [14, 15, 16]); +export const pine_5Crown0_0 = createColorShadowPalette(5426, 5427, [14, 15, 16]); +export const pine_6Stump0 = createColorPalette(5428, [14, 15, 16]); +export const pine_6Crown0_0 = createColorShadowPalette(5429, 5430, [14, 15, 16]); +export const tree_1 = createColorShadowPalette(5431, 5432, [17, 18, 19, 20, 21]); +export const tree_2 = createColorShadowPalette(5433, 5434, [17, 18, 19, 20, 21]); +export const tree_3 = createColorShadowPalette(5435, 5436, [17, 18, 19, 20, 21]); +export const tree_4Stump0 = createColorPalette(5437, [17, 18, 19, 20, 21]); +export const tree_4Trunk0 = createColorPalette(5438, [17, 18, 19, 20, 21]); +export const tree_4Crown0_0 = createColorShadowPalette(5439, 5440, [17, 18, 19, 20, 21]); +export const tree_5Stump0 = createColorPalette(5441, [17, 18, 19, 20, 21]); +export const tree_5Trunk0 = createColorPalette(5442, [17, 18, 19, 20, 21]); +export const tree_5Crown0_0 = createColorShadowPalette(5443, 5444, [17, 18, 19, 20, 21]); +export const tree_6Stump0 = createColorPalette(5445, [17, 18, 19, 20, 21]); +export const tree_6StumpWinter0 = createColorPalette(5446, [22]); +export const tree_6Trunk0 = createColorPalette(5447, [17, 18, 19, 20, 21]); +export const tree_6Crown0_0 = createColorShadowPalette(5448, 5449, [17, 18, 19, 20, 21]); +export const tree_6Crown0_1 = createColorShadowPalette(5448, 5450, [17, 18, 19, 20, 21]); +export const tree_6Stump1 = createColorPalette(5451, [17, 18, 19, 20, 21]); +export const tree_6StumpWinter1 = createColorPalette(5452, [22]); +export const tree_6Trunk1 = createColorPalette(5453, [17, 18, 19, 20, 21]); +export const tree_6Crown1_0 = createColorShadowPalette(5454, 5455, [17, 18, 19, 20, 21]); +export const tree_6Crown1_1 = createColorShadowPalette(5454, 5456, [17, 18, 19, 20, 21]); +export const mine_cart_front = createColorShadowPalette(5457, 5458, [23]); +export const acorn = createColorShadowPalette(5459, 5460, [24]); +export const barrel = createColorShadowPalette(5461, 5462, [25]); +export const basket_bin = createColorShadowPalette(5463, 5464, [26]); +export const boat = createColorPalette(5465, [27]); +export const boat_front = createColorPalette(5466, [28]); +export const boat_rope = createColorPalette(5467, [29]); +export const boat_sail = createColorShadowPalette(5468, 5469, [30]); +export const bookshelf = createColorShadowPalette(5470, 5471, [31]); +export const broom = createColorPalette(5472, [32]); +export const candy = createColorShadowPalette(5473, 5474, [33]); +export const christmastree = createColorPalette(5475, [34]); +export const cloud = createShadow(5476); +export const cookie = createColorPalette(5477, [35]); +export const cookie_pony = createColorPalette(5478, [36]); +export const crystal_lantern = createColorPalette(5479, [37]); +export const grass_tile = createColorPalette(5480, [38]); +export const hammer = createColorPalette(5481, [39]); +export const holly = createColorPalette(5482, [40]); +export const house = createColorShadowPalette(5483, 5484, [41]); +export const letter = createColorPalette(5485, [42]); +export const mango = createColorShadowPalette(5486, 5487, [43]); +export const mine_cart = createColorShadowPalette(5488, 5489, [44]); +export const mine_cart_back = createColorPalette(5490, [45]); +export const banana = createColorShadowPalette(5491, 5492, [46]); +export const mine_closed = createColorPalette(5493, [47]); +export const mine_entrance = createColorShadowPalette(5494, 5495, [48]); +export const mine_rail_fade_up = createColorShadowPalette(5496, 5497, [49]); +export const mistletoe = createColorPalette(5498, [50]); +export const pear = createColorShadowPalette(5499, 5500, [51]); +export const pickaxe = createColorPalette(5501, [52]); +export const rainfall = createColorPalette(5502, [53]); +export const rake = createColorPalette(5503, [54]); +export const rope_rack = createColorShadowPalette(5504, 5505, [55]); +export const rose = createColorShadowPalette(5506, 5507, [56]); +export const route_pole = createColorShadowPalette(5508, 5509, [57]); +export const saw = createColorPalette(5510, [58]); +export const shovel = createColorPalette(5511, [59]); +export const sign_winter = createColorShadowPalette(5512, 5513, [60]); +export const spider = createColorPalette(5514, [61]); +export const web = createColorPalette(5515, [61]); +export const tile = createColorPalette(5516, [62]); +export const tile_none = createColorPalette(5517, [63]); +export const tools_icon = createColorPalette(5518, [64]); +export const wall_h_placeholder = createColorPalette(5519, [53]); +export const wall_map = createColorShadowPalette(5520, 5521, [65]); +export const wall_v_placeholder = createColorPalette(5522, [53]); +export const well = createColorPalette(5523, [66]); +export const table_3 = createColorShadowPalette(5524, 5525, [67]); +export const apple_1 = createColorShadowPalette(5526, 5527, [68, 69]); +export const apple_2 = createColorShadowPalette(5528, 5529, [68, 69]); +export const bench_back = createColorShadowPalette(5530, 5531, [70]); +export const bench_backh = createColorShadowPalette(5532, 5533, [70]); +export const bench_seat = createColorShadowPalette(5534, 5535, [70]); +export const bench_seath = createColorShadowPalette(5536, 5537, [70]); +export const bench_1 = createColorShadowPalette(5538, 5539, [70]); +export const bench_backh2 = createColorShadowPalette(5540, 5541, [70]); +export const box_baskets = createColorShadowPalette(5542, 5543, [71]); +export const box_empty = createColorShadowPalette(5544, 5545, [71]); +export const box_fruits = createColorShadowPalette(5546, 5547, [71]); +export const box_gifts = createColorShadowPalette(5548, 5549, [71]); +export const box_lanterns = createColorShadowPalette(5550, 5551, [71]); +export const candy_cane_1 = createColorPalette(5552, [72]); +export const candy_cane_2 = createColorPalette(5553, [72]); +export const carrot_hold = createColorShadowPalette(5554, 5555, [73, 74]); +export const carrot_1 = createColorShadowPalette(5556, 5557, [73, 74]); +export const carrot_1b = createColorShadowPalette(5558, 5559, [73, 74]); +export const carrot_2 = createColorShadowPalette(5560, 5561, [73, 74]); +export const carrot_2b = createColorShadowPalette(5562, 5563, [73, 74]); +export const carrot_3 = createColorShadowPalette(5564, 5565, [73, 74]); +export const carrot_4 = createColorShadowPalette(5566, 5567, [73, 74]); +export const clover_pick = createColorPalette(5568, [75]); +export const clover_mouth = createColorPalette(5569, [75]); +export const clover_1 = createColorShadowPalette(5570, 5571, [75]); +export const clover_2 = createColorShadowPalette(5572, 5573, [75]); +export const clover_3 = createColorShadowPalette(5574, 5575, [75]); +export const clover_patch3 = createColorPalette(5576, [75]); +export const clover_4 = createColorShadowPalette(5577, 5578, [75]); +export const clover_patch4 = createColorPalette(5579, [75]); +export const clover_5 = createColorShadowPalette(5580, 5581, [75]); +export const clover_patch5 = createColorShadowPalette(5582, 5583, [75]); +export const clover_patch6 = createColorShadowPalette(5584, 5585, [75]); +export const clover_patch7 = createColorShadowPalette(5586, 5587, [75]); +export const cookie_table_1 = createColorShadowPalette(5588, 5589, [76]); +export const cookie_table_2 = createColorShadowPalette(5590, 5591, [76]); +export const crate_1 = createColorShadowPalette(5592, 5593, [77, 78]); +export const crate_2 = createColorShadowPalette(5594, 5595, [77, 78]); +export const crate_3 = createColorShadowPalette(5596, 5597, [77, 78]); +export const crystals_held = createColorShadowPalette(5598, 5599, [79]); +export const crystals_cart_pile = createColorPalette(5600, [79]); +export const crystals_1 = createColorPalette(5601, [79]); +export const crystals_2 = createColorPalette(5602, [79]); +export const crystals_3 = createColorPalette(5603, [79]); +export const crystals_4 = createColorPalette(5604, [79]); +export const crystals_5 = createColorPalette(5605, [79]); +export const crystals_6 = createColorPalette(5606, [79]); +export const crystals_7 = createColorPalette(5607, [79]); +export const crystals_8 = createColorPalette(5608, [79]); +export const crystals_9 = createColorPalette(5609, [79]); +export const crystals_10 = createColorPalette(5610, [79]); +export const cushion_1 = createColorShadowPalette(5611, 5612, [80, 81, 82]); +export const direction_shadow_down_right = createShadow(5613); +export const direction_down_left = createColorPalette(5614, [83]); +export const direction_left_right = createColorPalette(5615, [83]); +export const direction_up_left = createColorPalette(5616, [83]); +export const direction_shadow_up_right = createShadow(5617); +export const direction_shadow_up_left = createShadow(5618); +export const direction_shadow_down_left = createShadow(5619); +export const direction_down_right = createColorPalette(5620, [83]); +export const direction_shadow_left = createShadow(5621); +export const direction_shadow_right = createShadow(5622); +export const direction_up_right = createColorPalette(5623, [83]); +export const direction_pole_3 = createColorShadowPalette(5624, 5625, [83]); +export const direction_pole_4 = createColorShadowPalette(5626, 5627, [83]); +export const direction_pole_5 = createColorShadowPalette(5628, 5629, [83]); +export const egg_1 = createColorShadowPalette(5630, 5631, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_2 = createColorShadowPalette(5632, 5633, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_3 = createColorShadowPalette(5634, 5635, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_4 = createColorShadowPalette(5636, 5637, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_5 = createColorShadowPalette(5638, 5639, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_6 = createColorShadowPalette(5640, 5641, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_7 = createColorShadowPalette(5642, 5643, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_8 = createColorShadowPalette(5644, 5645, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_9 = createColorShadowPalette(5646, 5647, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_10 = createColorShadowPalette(5648, 5649, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_11 = createColorShadowPalette(5650, 5651, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_12 = createColorShadowPalette(5652, 5653, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_13 = createColorShadowPalette(5654, 5655, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_14 = createColorShadowPalette(5656, 5657, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_15 = createColorShadowPalette(5658, 5659, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_16 = createColorShadowPalette(5660, 5661, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_17 = createColorShadowPalette(5662, 5663, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_18 = createColorShadowPalette(5664, 5665, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_19 = createColorShadowPalette(5666, 5667, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_20 = createColorShadowPalette(5668, 5669, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_21 = createColorShadowPalette(5670, 5671, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_22 = createColorShadowPalette(5672, 5673, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_23 = createColorShadowPalette(5674, 5675, [84, 85, 86, 87, 88, 89, 90, 91, 92]); +export const egg_basket_1 = createColorShadowPalette(5676, 5677, [93]); +export const egg_basket_2 = createColorShadowPalette(5678, 5679, [93]); +export const egg_basket_3 = createColorShadowPalette(5680, 5681, [93]); +export const egg_basket_4 = createColorShadowPalette(5682, 5683, [93]); +export const fence_1 = createColorShadowPalette(5684, 5685, [94]); +export const fence_2 = createColorShadowPalette(5686, 5687, [94]); +export const fence_3 = createColorShadowPalette(5688, 5689, [94]); +export const fence_winter_1 = createColorShadowPalette(5690, 5691, [95]); +export const fence_winter_2 = createColorShadowPalette(5692, 5693, [95]); +export const fence_winter_3 = createColorShadowPalette(5694, 5695, [95]); +export const flower_pick = createColorPalette(5696, [96]); +export const flower_1 = createColorShadowPalette(5697, 5698, [96]); +export const flower_patch1 = createColorPalette(5699, [96]); +export const flower_patch2 = createColorPalette(5700, [96]); +export const flower_2 = createColorShadowPalette(5701, 5702, [96]); +export const flower_3 = createColorShadowPalette(5703, 5704, [96]); +export const flower_patch3 = createColorPalette(5705, [96]); +export const flower_patch4 = createColorPalette(5706, [96]); +export const flower_patch5 = createColorShadowPalette(5707, 5708, [96]); +export const flower_patch6 = createColorShadowPalette(5709, 5710, [96]); +export const flower_patch7 = createColorShadowPalette(5711, 5712, [96]); +export const gift_1 = createColorShadowPalette(5713, 5714, [97, 98]); +export const gift_2 = createColorShadowPalette(5715, 5716, [97, 98]); +export const giftpile_pine = createColorShadowPalette(5717, 5718, [99]); +export const giftpile_sign = createColorShadowPalette(5719, 5720, [99]); +export const giftpile_tree = createColorShadowPalette(5721, 5722, [99]); +export const giftpile_1 = createColorShadowPalette(5723, 5724, [99]); +export const giftpile_2 = createColorShadowPalette(5725, 5726, [99]); +export const giftpile_3 = createColorShadowPalette(5727, 5728, [99]); +export const giftpile_4 = createColorShadowPalette(5729, 5730, [99]); +export const giftpile_5 = createColorShadowPalette(5731, 5732, [99]); +export const giftpile_6 = createColorShadowPalette(5733, 5734, [99]); +export const gifts_1 = createColorShadowPalette(5735, 5736, [100, 101, 102, 103]); +export const gifts_2 = createColorShadowPalette(5737, 5738, [100, 101, 102, 103]); +export const gifts_3 = createColorShadowPalette(5739, 5740, [100, 101, 102, 103]); +export const gifts_4 = createColorShadowPalette(5741, 5742, [100, 101, 102, 103]); +export const gifts_5 = createColorShadowPalette(5743, 5744, [100, 101, 102, 103]); +export const gifts_6 = createColorShadowPalette(5745, 5746, [100, 101, 102, 103]); +export const grapes_one = createColorShadowPalette(5747, 5748, [104, 105]); +export const grapes_1 = createColorShadowPalette(5749, 5750, [104, 105]); +export const grapes_2 = createColorShadowPalette(5751, 5752, [104, 105]); +export const grapes_3 = createColorShadowPalette(5753, 5754, [104, 105]); +export const grapes_4 = createColorShadowPalette(5755, 5756, [104, 105]); +export const grapes_5 = createColorShadowPalette(5757, 5758, [104, 105]); +export const grapes_6 = createColorShadowPalette(5759, 5760, [104, 105]); +export const grapes_7 = createColorShadowPalette(5761, 5762, [104, 105]); +export const jacko_lantern_off = createColorShadowPalette(5763, 5764, [106]); +export const jacko_lantern_on = createColorShadowPalette(5765, 5766, [106]); +export const large_leafed_bush_1 = createColorShadowPalette(5767, 5768, [107, 108]); +export const large_leafed_bush_2 = createColorShadowPalette(5769, 5770, [107, 108]); +export const large_leafed_bush_3 = createColorShadowPalette(5771, 5772, [107, 108]); +export const large_leafed_bush_4 = createColorShadowPalette(5773, 5774, [107, 108]); +export const leaf_1 = createColorShadowPalette(5775, 5776, [109, 110, 111]); +export const leaf_2 = createColorShadowPalette(5777, 5778, [109, 110, 111]); +export const leaf_3 = createColorShadowPalette(5779, 5780, [109, 110, 111]); +export const leaf_4 = createColorShadowPalette(5781, 5782, [109, 110, 111]); +export const leaf_5 = createColorShadowPalette(5783, 5784, [109, 110, 111]); +export const leaf_6 = createColorShadowPalette(5785, 5786, [109, 110, 111]); +export const leafpile_big = createColorShadowPalette(5787, 5788, [112, 113, 114]); +export const leafpile_bigstick = createColorShadowPalette(5789, 5790, [112, 113, 114]); +export const leafpile_medium = createColorShadowPalette(5791, 5792, [112, 113, 114]); +export const leafpile_mediumalt = createColorShadowPalette(5793, 5794, [112, 113, 114]); +export const leafpile_small = createColorShadowPalette(5795, 5796, [112, 113, 114]); +export const leafpile_stick = createColorShadowPalette(5797, 5798, [112, 113, 114]); +export const leaves_1 = createColorShadowPalette(5799, 5800, [115]); +export const leaves_2 = createColorShadowPalette(5801, 5802, [115]); +export const leaves_3 = createColorShadowPalette(5803, 5804, [115]); +export const leaves_4 = createColorShadowPalette(5805, 5806, [115]); +export const leaves_5 = createColorShadowPalette(5807, 5808, [115]); +export const lemon_1 = createColorShadowPalette(5809, 5810, [116, 117]); +export const mine_rails_nsw = createColorShadowPalette(5811, 5812, [118]); +export const mine_rails_end_left = createColorShadowPalette(5813, 5814, [118]); +export const mine_rails_end_top = createColorShadowPalette(5815, 5816, [118]); +export const mine_rails_h = createColorShadowPalette(5817, 5818, [118]); +export const mine_rails_ne = createColorShadowPalette(5819, 5820, [118]); +export const mine_rails_nse = createColorShadowPalette(5821, 5822, [118]); +export const mine_rails_end_right = createColorShadowPalette(5823, 5824, [118]); +export const mine_rails_nw = createColorShadowPalette(5825, 5826, [118]); +export const mine_rails_nwe = createColorShadowPalette(5827, 5828, [118]); +export const mine_rails_se = createColorShadowPalette(5829, 5830, [118]); +export const mine_rails_sw = createColorShadowPalette(5831, 5832, [118]); +export const mine_rails_swe = createColorShadowPalette(5833, 5834, [118]); +export const mine_rails_v = createColorShadowPalette(5835, 5836, [118]); +export const orange_1 = createColorShadowPalette(5837, 5838, [119]); +export const orange_2 = createColorShadowPalette(5839, 5840, [119]); +export const picture_1 = createColorPalette(5841, [120, 121]); +export const plank_shadow = createShadow(5842); +export const plank_shadow_short = createShadow(5843); +export const plank_short_1 = createColorPalette(5844, [122, 123]); +export const plank_1 = createColorPalette(5845, [122, 123]); +export const plank_2 = createColorPalette(5846, [122, 123]); +export const plank_shadow2 = createShadow(5847); +export const plank_short_2 = createColorPalette(5848, [122, 123]); +export const plank_3 = createColorPalette(5849, [122, 123]); +export const plank_short_3 = createColorPalette(5850, [122, 123]); +export const plank_4 = createColorPalette(5851, [122, 123]); +export const pumpkin_default = createColorShadowPalette(5852, 5853, [124]); +export const pumpkin_off = createColorShadowPalette(5854, 5855, [124]); +export const pumpkin_on = createColorShadowPalette(5856, 5857, [124]); +export const rock_1 = createColorShadowPalette(5858, 5859, [125, 126]); +export const rock_2 = createColorShadowPalette(5860, 5861, [125, 126]); +export const rock_3 = createColorShadowPalette(5862, 5863, [125, 126]); +export const sign_blank = createColorShadowPalette(5864, 5865, [127]); +export const sign_1 = createColorShadowPalette(5866, 5867, [127]); +export const sign_2 = createColorShadowPalette(5868, 5869, [127]); +export const sign_3 = createColorShadowPalette(5870, 5871, [127]); +export const sign_4 = createColorShadowPalette(5872, 5873, [127]); +export const snowpile_big = createColorPalette(5874, [128, 129]); +export const snowpile_medium = createColorPalette(5875, [128, 129]); +export const snowpile_small = createColorPalette(5876, [128, 129]); +export const snowpile_tinier = createColorPalette(5877, [128, 129]); +export const snowpile_tiny = createColorPalette(5878, [128, 129]); +export const snowpony_1 = createColorPalette(5879, [130]); +export const snowpony_2 = createColorPalette(5880, [130]); +export const snowpony_3 = createColorPalette(5881, [130]); +export const snowpony_4 = createColorPalette(5882, [130]); +export const snowpony_5 = createColorPalette(5883, [130]); +export const snowpony_6 = createColorPalette(5884, [130]); +export const snowpony_7 = createColorPalette(5885, [130]); +export const snowpony_8 = createColorPalette(5886, [130]); +export const snowpony_9 = createColorPalette(5887, [130]); +export const stalactite_1 = createColorPalette(5888, [131]); +export const stalactite_2 = createColorPalette(5889, [131]); +export const stalactite_3 = createColorPalette(5890, [131]); +export const stone_wall_full = createColorShadowPalette(5891, 5892, [132]); +export const stone_wall_horizontal1 = createColorShadowPalette(5893, 5894, [132]); +export const stone_wall_pole1 = createColorShadowPalette(5895, 5896, [132]); +export const stone_wall_vertical1 = createColorPalette(5897, [132]); +export const stone_wall_winter_horizontal1 = createColorPalette(5898, [133]); +export const stone_wall_winter_pole1 = createColorPalette(5899, [133]); +export const stone_wall_winter_vertical1 = createColorPalette(5900, [133]); +export const table_1 = createColorShadowPalette(5901, 5902, [134]); +export const table_2 = createColorShadowPalette(5903, 5904, [134]); +export const tombstone_1 = createColorShadowPalette(5905, 5906, [135]); +export const tombstone_2 = createColorShadowPalette(5907, 5908, [135]); +export const toolbox_empty = createColorShadowPalette(5909, 5910, [136]); +export const toolbox_full = createColorShadowPalette(5911, 5912, [136]); +export const window_1 = createColorPalette(5913, [137]); +export const wooden_fence_pole1 = createColorShadowPalette(5914, 5915, [138, 139]); +export const wooden_fence_horizontal1 = createColorShadowPalette(5916, 5917, [138, 139]); +export const wooden_fence_vertical1 = createColorPalette(5918, [138, 139]); +export const wooden_fence_vertical2 = createColorPalette(5919, [138, 139]); +export const wooden_fence_horizontal2 = createColorShadowPalette(5920, 5921, [138, 139]); +export const wooden_fence_pole2 = createColorShadowPalette(5922, 5923, [138, 139]); +export const wooden_fence_pole3 = createColorShadowPalette(5924, 5925, [138, 139]); +export const wooden_fence_vertical3 = createColorPalette(5926, [138, 139]); +export const wooden_fence_horizontal3 = createColorShadowPalette(5927, 5928, [138, 139]); +export const wooden_fence_pole4 = createColorShadowPalette(5929, 5930, [138, 139]); +export const wooden_fence_horizontal4 = createColorShadowPalette(5931, 5932, [138, 139]); +export const wooden_fence_pole5 = createColorShadowPalette(5933, 5934, [138, 139]); +export const wooden_fence_horizontal5 = createColorShadowPalette(5935, 5936, [138, 139]); +export const wooden_fence_horizontal6 = createColorShadowPalette(5937, 5938, [138, 139]); +export const wooden_fence_winter_horizontal1 = createColorShadowPalette(5939, 5940, [140]); +export const wooden_fence_winter_vertical1 = createColorPalette(5941, [140]); +export const wooden_fence_winter_pole1 = createColorShadowPalette(5942, 5943, [140]); +export const wooden_fence_winter_pole2 = createColorShadowPalette(5944, 5945, [140]); +export const wooden_fence_winter_horizontal2 = createColorShadowPalette(5946, 5947, [140]); +export const wooden_fence_winter_vertical2 = createColorPalette(5948, [140]); +export const wooden_fence_winter_pole3 = createColorShadowPalette(5949, 5950, [140]); +export const wooden_fence_winter_horizontal3 = createColorShadowPalette(5951, 5952, [140]); +export const wooden_fence_winter_vertical3 = createColorPalette(5953, [140]); +export const wooden_fence_winter_pole4 = createColorShadowPalette(5954, 5955, [140]); +export const wooden_fence_winter_horizontal4 = createColorShadowPalette(5956, 5957, [140]); +export const wooden_fence_winter_horizontal5 = createColorShadowPalette(5958, 5959, [140]); +export const wooden_fence_winter_pole5 = createColorShadowPalette(5960, 5961, [140]); +export const wooden_fence_winter_horizontal6 = createColorShadowPalette(5962, 5963, [140]); +export const bubble_2 = createButton(2, 5964, 5965, 5966, 5967, 5968, 5969, 5970, 5971, 5972); +export const bubble2_2 = createButton(2, 5973, 5974, 5975, 5976, 5977, 5978, 5979, 5980, 5981); +export const butterfly = createAnimationPalette([5982, 5983, 5984, 5985], 141); +export const poof2 = createAnimationPalette([5986, 5987, 5988, 5989, 5990, 5991, 5992, 5993, 5994], 142); +export const rain = createAnimationPalette([5995, 5996, 5997, 5998], 53); +export const raindrop = createAnimationPalette([5999, 6000, 6001, 6002], 53); +export const magic = createAnimationPalette([6003, 6004, 6005, 6006, 6007, 6008], 53); +export const bat = createAnimationPalette([6009, 6010, 6011, 6012], 143); +export const bunny = createAnimationShadow([6013, 6014, 6015, 6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024, 6025, 6026, 6027, 6028, 6029, 6030, 6031, 6032, 6033, 6034, 6035, 6036, 6037, 6038, 6039, 6040, 6041, 6042, 6043, 6044, 6045, 6046], 6047, 144); +export const cat = createAnimationPalette([6048, 6049, 6050, 6051, 6052, 6053, 6054, 6055, 6056, 6057, 6058, 6059, 6060, 6061, 6062, 6063, 6064], 145); +export const firefly = createAnimationPalette([6065, 6066, 6067], 146); +export const torch = createAnimationShadow([6068, 6069, 6070, 6071, 6072, 6073, 6074], 6075, 147); +export const splash_boop = createAnimationPalette([6076, 6077, 6078, 6079, 6080, 6081, 6082, 6083, 6084, 6085, 6086, 6087, 6088, 6089, 6090, 6091, 6092, 6093], 148); +export const lantern = createAnimationShadow([6094, 6095, 6096, 6097, 6098, 6099, 6100, 6101], 6102, 149); +export const boat_wake = createAnimationPalette([6103, 6104, 6105, 6106], 150); +export const splash = createAnimationPalette([6107, 6108, 6109, 6110, 6111, 6112, 6113, 6114, 6115, 6116, 6117, 6118, 6119], 151); +export const poof = createAnimationPalette([6120, 6121, 6122, 6123, 6124], 152); +export const pier_leg = createAnimationPalette([6125, 6126, 6127, 6128], 153); +export const ghost1_hooves = createAnimationPalette([0, 0, 0, 6129, 6130, 6131, 6132, 6133, 6134, 6135, 6136, 6137, 6138, 6139, 6140, 6141, 6142, 6143, 6144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6145, 6146, 6147, 6148, 6149, 6150, 6151, 6152, 6153, 6154, 6155, 6156, 6157, 6158, 6159, 6160, 6161, 6162, 6163, 6164, 6165, 6166, 6167, 6168, 0, 0, 0, 0], 154); +export const ghost1 = createAnimationPalette([6169, 6170, 6171, 6172, 6173, 6174, 6175, 6176, 6177, 6178, 6179, 6180, 6181, 6182, 6183, 6184, 6185, 6186, 6187, 6188, 6189, 6190, 6191, 6192, 6193, 6194, 6195, 6196, 6197, 6198, 6199, 6200, 6201, 6202, 6203, 6204, 6205, 6206, 6207, 6208, 0, 6209, 6210, 6211, 6212, 6213, 6214, 6215, 6216, 6217, 6218, 6219, 6220, 6221, 6222, 6223, 6224, 6225, 6226, 6227, 6228, 6229, 6230, 6231, 6232, 6233, 6234, 6235], 155); +export const magic2 = createAnimationPalette([6236, 6237, 6238, 6239, 6240, 6241, 6242, 6243, 6244, 6245, 6246], 53); +export const torch2 = createAnimationShadow([6247, 6248, 6249, 6250, 6251, 6252, 6253], 6254, 147); +export const magic3 = createAnimationPalette([0, 6255, 6256, 6257, 6258, 6259, 6260, 6261, 6262, 0, 0], 53); +export const pony_wake_1 = createAnimationPalette([6263, 6264, 6265, 6266, 6267, 6268, 6269, 6270], 156); +export const pony_wake_2 = createAnimationPalette([6271, 6272, 6273, 6274, 6275, 6276, 6277, 6278], 156); +export const pony_wake_3 = createAnimationPalette([6279, 6280, 6281, 6282, 6283, 6284, 6285, 6286], 156); +export const pony_wake_4 = createAnimationPalette([6287, 6288, 6289, 6290, 6291, 6292, 6293, 6294], 156); +export const pony_wake_5 = createAnimationPalette([6295, 6296, 6297, 6298, 6299, 6300, 6301, 6302], 156); +export const pony_wake_6 = createAnimationPalette([6303, 6304, 6305, 6306, 6307, 6308, 6309, 6310], 156); +export const water_crystal_1 = createAnimationPalette([6311, 6312, 6313, 6314], 157); +export const water_crystal_2 = createAnimationPalette([6315, 6316, 6317, 6318], 157); +export const water_crystal_3 = createAnimationPalette([6319, 6320, 6321, 6322], 157); +export const water_rock_1 = createAnimationPalette([6323, 6324, 6325, 6326], 158); +export const water_rock_2 = createAnimationPalette([6327, 6328, 6329, 6330], 158); +export const water_rock_3 = createAnimationPalette([6331, 6332, 6333, 6334], 158); +export const water_rock_4 = createAnimationPalette([6335, 6336, 6337, 6338], 158); +export const water_rock_5 = createAnimationPalette([6339, 6340, 6341, 6342], 158); +export const water_rock_6 = createAnimationPalette([6343, 6344, 6345, 6346], 158); +export const water_rock_7 = createAnimationPalette([6347, 6348, 6349, 6350], 158); +export const water_rock_8 = createAnimationPalette([6351, 6352, 6353, 6354], 158); +export const water_rock_9 = createAnimationPalette([6355, 6356, 6357, 6358], 158); +export const water_rock_10 = createAnimationPalette([6359, 6360, 6361, 6362], 158); +export const water_rock_11 = createAnimationPalette([6363, 6364, 6365, 6366], 158); +export const grass_2 = /* no palettes */ createColorPalette(6367, [38]); +export const nipple_2 = /* no palettes */ createColorPalette(6368, [53]); +export const nipple_alt_2 = /* no palettes */ createColorPalette(6369, [53]); +export const arrow_down = sprites2[6370]; +export const arrow_left = sprites2[6371]; +export const arrow_right = sprites2[6372]; +export const arrow_up = sprites2[6373]; +export const magic_icon = sprites2[6374]; +export const emote_cry = createAnimationPalette([6375, 6376, 6377, 6378, 6379, 6380, 6381, 6382, 6383, 6384, 6385, 6386, 6387, 6388, 6389, 6390, 6391, 6392], 159); +export const emote_hearts = createAnimationPalette([6393, 6394, 6395, 6396, 6397, 6398, 6399, 6400, 6401, 6402, 6403, 6404, 6405, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413, 6414, 6415, 6416, 6417, 6418, 6419, 6420, 6421, 6422, 6423, 6424, 6425, 6426, 6427, 6428, 6429, 6430, 6431, 6432, 6433, 6434, 6435, 6436, 6437], 159); +export const emote_sneeze = createAnimationPalette([6438, 6439], 159); +export const emote_tears = createAnimationPalette([6440], 159); +export const hold_poof = createAnimationPalette([6441, 6442, 6443, 6444, 6445], 159); +export const emote_sleep1 = createAnimationPalette([6446, 6447, 6448, 6449, 6450, 6451, 6452, 6453, 6454, 6455, 6456, 6457, 6458, 6459, 6460, 6461, 6462, 6463, 6464], 159); +export const emote_sleep1_flip = createAnimationPalette([6465, 6466, 6467, 6468, 6469, 6470, 6471, 6472, 6473, 6474, 6475, 6476, 6477, 6478, 6479, 6480, 6481, 6482, 6483], 159); +export const emote_cry2 = createAnimationPalette([6484, 6485, 6486, 6487, 6488, 6489, 6490, 6491, 6492, 6493, 6494, 6495, 6496], 159); +export const emote_sleep2 = createAnimationPalette([6497, 6498, 6499, 6500, 6501, 6502, 6503, 6504, 6505, 6506, 6507, 6508, 6509, 6510, 6511, 6512, 6513, 6514, 6515, 6516, 6517, 6518, 6519, 6520, 6521, 6522, 6523, 6524, 6525, 6526, 6527, 6528, 6529, 6530, 6531, 6532, 6533, 6534, 6535, 6536], 159); +export const emote_sleep2_flip = createAnimationPalette([6537, 6538, 6539, 6540, 6541, 6542, 6543, 6544, 6545, 6546, 6547, 6548, 6549, 6550, 6551, 6552, 6553, 6554, 6555, 6556, 6557, 6558, 6559, 6560, 6561, 6562, 6563, 6564, 6565, 6566, 6567, 6568, 6569, 6570, 6571, 6572, 6573, 6574, 6575, 6576], 159); +export const wall_wood_full = [ + /* no palettes */ createColorPalette(6577, [160]), + /* no palettes */ createColorPalette(6578, [160]), + /* no palettes */ createColorPalette(6579, [160]), + /* no palettes */ createColorPalette(6580, [160]), + /* no palettes */ createColorPalette(6581, [160]), + /* no palettes */ createColorPalette(6582, [160]), + /* no palettes */ createColorPalette(6583, [160]), + /* no palettes */ createColorPalette(6584, [160]), + /* no palettes */ createColorPalette(6585, [160]), + /* no palettes */ createColorPalette(6586, [160]), + /* no palettes */ createColorPalette(6587, [160]), + /* no palettes */ createColorPalette(6588, [160]), + /* no palettes */ createColorPalette(6589, [160]), + /* no palettes */ createColorPalette(6590, [160]), + /* no palettes */ createColorPalette(6591, [160]), + /* no palettes */ createColorPalette(6592, [160]), + /* no palettes */ createColorPalette(6593, [160]), + /* no palettes */ createColorPalette(6594, [160]), + /* no palettes */ createColorPalette(6595, [160]), + /* no palettes */ createColorPalette(6596, [160]) +]; +export const wall_wood_half = [ + /* no palettes */ createColorPalette(6597, [160]), + /* no palettes */ createColorPalette(6598, [160]), + /* no palettes */ createColorPalette(6599, [160]), + /* no palettes */ createColorPalette(6600, [160]), + /* no palettes */ createColorPalette(6601, [160]), + /* no palettes */ createColorPalette(6602, [160]), + /* no palettes */ createColorPalette(6603, [160]), + /* no palettes */ createColorPalette(6604, [160]), + /* no palettes */ createColorPalette(6605, [160]), + /* no palettes */ createColorPalette(6606, [160]), + /* no palettes */ createColorPalette(6607, [160]), + /* no palettes */ createColorPalette(6608, [160]), + /* no palettes */ createColorPalette(6609, [160]), + /* no palettes */ createColorPalette(6610, [160]), + /* no palettes */ createColorPalette(6611, [160]), + /* no palettes */ createColorPalette(6612, [160]), + /* no palettes */ createColorPalette(6613, [160]), + /* no palettes */ createColorPalette(6614, [160]) +]; +export const wall_stone_full = [ + /* no palettes */ createColorPalette(6615, [161]), + /* no palettes */ createColorPalette(6616, [161]), + /* no palettes */ createColorPalette(6617, [161]), + /* no palettes */ createColorPalette(6618, [161]), + /* no palettes */ createColorPalette(6619, [161]), + /* no palettes */ createColorPalette(6620, [161]), + /* no palettes */ createColorPalette(6621, [161]), + /* no palettes */ createColorPalette(6622, [161]), + /* no palettes */ createColorPalette(6623, [161]), + /* no palettes */ createColorPalette(6624, [161]), + /* no palettes */ createColorPalette(6625, [161]), + /* no palettes */ createColorPalette(6626, [161]), + /* no palettes */ createColorPalette(6627, [161]), + /* no palettes */ createColorPalette(6628, [161]), + /* no palettes */ createColorPalette(6629, [161]), + /* no palettes */ createColorPalette(6630, [161]), + /* no palettes */ createColorPalette(6631, [161]), + /* no palettes */ createColorPalette(6632, [161]), + /* no palettes */ createColorPalette(6633, [161]), + /* no palettes */ createColorPalette(6634, [161]) +]; +export const wall_stone_half = [ + /* no palettes */ createColorPalette(6635, [161]), + /* no palettes */ createColorPalette(6636, [161]), + /* no palettes */ createColorPalette(6637, [161]), + /* no palettes */ createColorPalette(6638, [161]), + /* no palettes */ createColorPalette(6639, [161]), + /* no palettes */ createColorPalette(6640, [161]), + /* no palettes */ createColorPalette(6641, [161]), + /* no palettes */ createColorPalette(6642, [161]), + /* no palettes */ createColorPalette(6643, [161]), + /* no palettes */ createColorPalette(6644, [161]), + /* no palettes */ createColorPalette(6645, [161]), + /* no palettes */ createColorPalette(6646, [161]), + /* no palettes */ createColorPalette(6647, [161]), + /* no palettes */ createColorPalette(6648, [161]), + /* no palettes */ createColorPalette(6649, [161]), + /* no palettes */ createColorPalette(6650, [161]), + /* no palettes */ createColorPalette(6651, [161]), + /* no palettes */ createColorPalette(6652, [161]) +]; +export const pixelRect2 = sprites2[6653]; +export const grassTiles = createSpritesPalette([6654, 6655, 6656, 6657, 6658, 6659, 6660, 6661, 6662, 6663, 6664, 6665, 6666, 6667, 6668, 6669, 6670, 6671, 6672, 6673, 6674, 6675, 6676, 6677, 6678, 6679, 6680, 6681, 6682, 6683, 6684, 6685, 6686, 6687, 6688, 6689, 6690, 6691, 6692, 6693, 6694, 6695, 6696, 6697, 6698, 6699, 6700, 6701, 6702, 6703, 6704, 6705, 6706, 6707, 6708, 6709, 6710, 6711, 6712, 6713, 6714, 6715, 6716, 6717, 6718, 6719], [162, 163]); +export const snowTiles = createSpritesPalette([6720, 6721, 6722, 6723, 6724, 6725, 6726, 6727, 6728, 6729, 6730, 6731, 6732, 6733, 6734, 6735, 6736, 6737, 6738, 6739, 6740, 6741, 6742, 6743, 6744, 6745, 6746, 6747, 6748, 6749, 6750, 6751, 6752, 6753, 6754, 6755, 6756, 6757, 6758, 6759, 6760, 6761, 6762, 6763, 6764, 6765, 6766, 6767, 6768, 6769, 6770, 6771, 6772, 6773, 6774, 6775, 6776, 6777, 6778, 6779, 6780, 6781, 6782, 6783, 6784, 6785], [164]); +export const woodTiles = createSpritesPalette([6786, 6787, 6788, 6789, 6790, 6791, 6792, 6793, 6794, 6795, 6796, 6797, 6798, 6799, 6800, 6801, 6802, 6803, 6804, 6805, 6806, 6807, 6808, 6809, 6810, 6811, 6812, 6813, 6814, 6815, 6816, 6817, 6818, 6819, 6820, 6821, 6822, 6823, 6824, 6825, 6826, 6827, 6828, 6829, 6830, 6831, 6832, 6833, 6834, 6835, 6836, 6837, 6838, 6839, 6840, 6841, 6842, 6843, 6844, 6845, 6846, 6847, 6848, 6849, 6850, 6851], [165]); +export const stoneTiles = createSpritesPalette([6852, 6853, 6854, 6855, 6856, 6857, 6858, 6859, 6860, 6861, 6862, 6863, 6864, 6865, 6866, 6867, 6868, 6869, 6870, 6871, 6872, 6873, 6874, 6875, 6876, 6877, 6878, 6879, 6880, 6881, 6882, 6883, 6884, 6885, 6886, 6887, 6888, 6889, 6890, 6891, 6892, 6893, 6894, 6895, 6896, 6897, 6898, 6899, 6900, 6901, 6902, 6903, 6904, 6905, 6906, 6907, 6908, 6909, 6910, 6911, 6912, 6913, 6914, 6915, 6916, 6917], [166]); +export const stone2Tiles = createSpritesPalette([6918, 6919, 6920, 6921, 6922, 6923, 6924, 6925, 6926, 6927, 6928, 6929, 6930, 6931, 6932, 6933, 6934, 6935, 6936, 6937, 6938, 6939, 6940, 6941, 6942, 6943, 6944, 6945, 6946, 6947, 6948, 6949, 6950, 6951, 6952, 6953, 6954, 6955, 6956, 6957, 6958, 6959, 6960, 6961, 6962, 6963, 6964, 6965, 6966, 6967, 6968, 6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978, 6979, 6980, 6981, 6982, 6983], [167]); +export const waterTiles1 = createSpritesPalette([6984, 6985, 6986, 6987, 6988, 6989, 6990, 6991, 6992, 6993, 6994, 6995, 6996, 6997, 6998, 6999, 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024, 7025, 7026, 7027, 7028, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 7036, 7037, 7038, 7039, 7040, 7041, 7042, 7043, 7044, 7045, 7046, 7047, 7048, 7049], [168, 169, 170, 171]); +export const waterTiles2 = createSpritesPalette([7050, 7051, 7052, 7053, 7054, 7055, 7056, 7057, 7058, 7059, 7060, 7061, 7062, 7063, 7064, 7065, 7066, 7067, 7068, 7069, 7070, 7071, 7072, 7073, 7074, 7075, 7076, 7077, 7078, 7079, 7080, 7081, 7082, 7083, 7084, 7085, 7086, 7087, 7088, 7089, 7090, 7091, 7092, 7093, 7094, 7095, 7096, 7097, 7098, 7099, 7100, 7101, 7102, 7103, 7104, 7105, 7106, 7107, 7108, 7109, 7110, 7111, 7112, 7113, 7114, 7115], [168]); +export const waterTiles3 = createSpritesPalette([7116, 7117, 7118, 7119, 7120, 7121, 7122, 7123, 7124, 7125, 7126, 7127, 7128, 7129, 7130, 7131, 7132, 7133, 7134, 7135, 7136, 7137, 7138, 7139, 7140, 7141, 7142, 7143, 7144, 7145, 7146, 7147, 7148, 7149, 7150, 7151, 7152, 7153, 7154, 7155, 7156, 7157, 7158, 7159, 7160, 7161, 7162, 7163, 7164, 7165, 7166, 7167, 7168, 7169, 7170, 7171, 7172, 7173, 7174, 7175, 7176, 7177, 7178, 7179, 7180, 7181], [168]); +export const waterTiles4 = createSpritesPalette([7182, 7183, 7184, 7185, 7186, 7187, 7188, 7189, 7190, 7191, 7192, 7193, 7194, 7195, 7196, 7197, 7198, 7199, 7200, 7201, 7202, 7203, 7204, 7205, 7206, 7207, 7208, 7209, 7210, 7211, 7212, 7213, 7214, 7215, 7216, 7217, 7218, 7219, 7220, 7221, 7222, 7223, 7224, 7225, 7226, 7227, 7228, 7229, 7230, 7231, 7232, 7233, 7234, 7235, 7236, 7237, 7238, 7239, 7240, 7241, 7242, 7243, 7244, 7245, 7246, 7247], [168]); +export const iceTiles = createSpritesPalette([7248, 7249, 7250, 7251, 7252, 7253, 7254, 7255, 7256, 7257, 7258, 7259, 7260, 7261, 7262, 7263, 7264, 7265, 7266, 7267, 7268, 7269, 7270, 7271, 7272, 7273, 7274, 7275, 7276, 7277, 7278, 7279, 7280, 7281, 7282, 7283, 7284, 7285, 7286, 7287, 7288, 7289, 7290, 7291, 7292, 7293, 7294, 7295, 7296, 7297, 7298, 7299, 7300, 7301, 7302, 7303, 7304, 7305, 7306, 7307, 7308, 7309, 7310, 7311, 7312, 7313], [172, 173, 174]); +export const grassTilesNew = createSpritesPalette([7314, 7315, 7316, 7317, 7318, 7319, 7320, 7321, 7322, 7323, 7324, 7325, 7326, 7327, 7328, 7329, 7330, 7331, 7332, 7333, 7334, 7335, 7336, 7337, 7338, 7339, 7340, 7341, 7342, 7343, 7344, 7345, 7346, 7347, 7348, 7349, 7350, 7351, 7352, 7353, 7354, 7355, 7356, 7357, 7358, 7359, 7360, 7361, 7362, 7363, 7364, 7365, 7366, 7367, 7368, 7369, 7370, 7371, 7372, 7373, 7374, 7375, 7376, 7377, 7378, 7379], [175]); +export const snowOnIceTiles = createSpritesPalette([7380, 7381, 7382, 7383, 7384, 7385, 7386, 7387, 7388, 7389, 7390, 7391, 7392, 7393, 7394, 7395, 7396, 7397, 7398, 7399, 7400, 7401, 7402, 7403, 7404, 7405, 7406, 7407, 7408, 7409, 7410, 7411, 7412, 7413, 7414, 7415, 7416, 7417, 7418, 7419, 7420, 7421, 7422, 7423, 7424, 7425, 7426, 7427, 7428, 7429, 7430, 7431, 7432, 7433, 7434, 7435, 7436, 7437, 7438, 7439, 7440, 7441, 7442, 7443, 7444, 7445], [176]); +export const caveTiles = createSpritesPalette([7446, 7447, 7448, 7449, 7450, 7451, 7452, 7453, 7454, 7455, 7456, 7457, 7458, 7459, 7460, 7461, 7462, 7463, 7464, 7465, 7466, 7467, 7468, 7469, 7470, 7471, 7472, 7473, 7474, 7475, 7476, 7477, 7478, 7479, 7480, 7481, 7482, 7483, 7484, 7485, 7486, 7487, 7488, 7489, 7490, 7491, 7492, 7493, 7494, 7495, 7496, 7497, 7498, 7499, 7500, 7501, 7502, 7503, 7504, 7505, 7506, 7507, 7508, 7509, 7510, 7511], [177]); +export const cliffs_grass_decal_1 = createColorPalette(7512, [178, 179, 180]); +export const cliffs_grass_decal_2 = createColorPalette(7513, [178, 179, 180]); +export const cliffs_grass_decal_3 = createColorPalette(7514, [178, 179, 180]); +export const cliffs_grass_decal_l = createColorPalette(7515, [178, 179, 180]); +export const cliffs_grass_decal_r = createColorPalette(7516, [178, 179, 180]); +export const cliffs_grass_top_nw = createColorPalette(7517, [178, 179, 180]); +export const cliffs_grass_top_n = createColorPalette(7518, [178, 179, 180]); +export const cliffs_grass_top_ne = createColorPalette(7519, [178, 179, 180]); +export const cliffs_grass_top_w = createColorPalette(7520, [178, 179, 180]); +export const cliffs_grass_top_e = createColorPalette(7521, [178, 179, 180]); +export const cliffs_grass_top_sw = createColorPalette(7522, [178, 179, 180]); +export const cliffs_grass_top_se = createColorPalette(7523, [178, 179, 180]); +export const cliffs_grass_top_s1 = createColorPalette(7524, [178, 179, 180]); +export const cliffs_grass_top_s2 = createColorPalette(7525, [178, 179, 180]); +export const cliffs_grass_top_s3 = createColorPalette(7526, [178, 179, 180]); +export const cliffs_grass_mid_sw1 = createColorPalette(7527, [178, 179, 180]); +export const cliffs_grass_mid_sw2 = createColorPalette(7528, [178, 179, 180]); +export const cliffs_grass_mid_s1 = createColorPalette(7529, [178, 179, 180]); +export const cliffs_grass_mid_s2 = createColorPalette(7530, [178, 179, 180]); +export const cliffs_grass_mid_s3 = createColorPalette(7531, [178, 179, 180]); +export const cliffs_grass_mid_se1 = createColorPalette(7532, [178, 179, 180]); +export const cliffs_grass_mid_se2 = createColorPalette(7533, [178, 179, 180]); +export const cliffs_grass_bot_sw = createColorPalette(7534, [178, 179, 180]); +export const cliffs_grass_bot_s1 = createColorPalette(7535, [178, 179, 180]); +export const cliffs_grass_bot_s2 = createColorPalette(7536, [178, 179, 180]); +export const cliffs_grass_bot_s3 = createColorPalette(7537, [178, 179, 180]); +export const cliffs_grass_bot_se = createColorPalette(7538, [178, 179, 180]); +export const cliffs_grass_top_sb = createColorPalette(7539, [178, 179, 180]); +export const cliffs_grass_mid_sb = createColorPalette(7540, [178, 179, 180]); +export const cliffs_grass_bot_sb = createColorPalette(7541, [178, 179, 180]); +export const cliffs_grass_top_trim_left = createColorPalette(7542, [178, 179, 180]); +export const cliffs_grass_mid_trim_left = createColorPalette(7543, [178, 179, 180]); +export const cliffs_grass_bot_trim_left = createColorPalette(7544, [178, 179, 180]); +export const cliffs_grass_top_trim_right = createColorPalette(7545, [178, 179, 180]); +export const cliffs_grass_mid_trim_right = createColorPalette(7546, [178, 179, 180]); +export const cliffs_grass_bot_trim_right = createColorPalette(7547, [178, 179, 180]); +export const cliffs_grass_sw = createColorPalette(7548, [178, 179, 180]); +export const cliffs_grass_s1 = createColorPalette(7549, [178, 179, 180]); +export const cliffs_grass_s2 = createColorPalette(7550, [178, 179, 180]); +export const cliffs_grass_s3 = createColorPalette(7551, [178, 179, 180]); +export const cliffs_grass_sb = createColorPalette(7552, [178, 179, 180]); +export const cliffs_grass_se = createColorPalette(7553, [178, 179, 180]); +export const cave_walls_decal_1 = createColorPalette(7554, [181]); +export const cave_walls_decal_2 = createColorPalette(7555, [181]); +export const cave_walls_decal_3 = createColorPalette(7556, [181]); +export const cave_walls_decal_l = createColorPalette(7557, [181]); +export const cave_walls_decal_r = createColorPalette(7558, [181]); +export const cave_walls_top_nw = createColorPalette(7559, [181]); +export const cave_walls_top_n = createColorPalette(7560, [181]); +export const cave_walls_top_ne = createColorPalette(7561, [181]); +export const cave_walls_top_w = createColorPalette(7562, [181]); +export const cave_walls_top_e = createColorPalette(7563, [181]); +export const cave_walls_top_sw = createColorPalette(7564, [181]); +export const cave_walls_top_se = createColorPalette(7565, [181]); +export const cave_walls_top_s1 = createColorPalette(7566, [181]); +export const cave_walls_top_s2 = createColorPalette(7567, [181]); +export const cave_walls_top_s3 = createColorPalette(7568, [181]); +export const cave_walls_mid_sw1 = createColorPalette(7569, [181]); +export const cave_walls_mid_sw2 = createColorPalette(7570, [181]); +export const cave_walls_mid_s1 = createColorPalette(7571, [181]); +export const cave_walls_mid_s2 = createColorPalette(7572, [181]); +export const cave_walls_mid_s3 = createColorPalette(7573, [181]); +export const cave_walls_mid_se1 = createColorPalette(7574, [181]); +export const cave_walls_mid_se2 = createColorPalette(7575, [181]); +export const cave_walls_bot_sw = createColorPalette(7576, [181]); +export const cave_walls_bot_s1 = createColorPalette(7577, [181]); +export const cave_walls_bot_s2 = createColorPalette(7578, [181]); +export const cave_walls_bot_s3 = createColorPalette(7579, [181]); +export const cave_walls_bot_se = createColorPalette(7580, [181]); +export const cave_walls_top_sb = createColorPalette(7581, [181]); +export const cave_walls_mid_sb = createColorPalette(7582, [181]); +export const cave_walls_bot_sb = createColorPalette(7583, [181]); +export const cave_walls_top_trim_left = createColorPalette(7584, [181]); +export const cave_walls_mid_trim_left = createColorPalette(7585, [181]); +export const cave_walls_bot_trim_left = createColorPalette(7586, [181]); +export const cave_walls_top_trim_right = createColorPalette(7587, [181]); +export const cave_walls_mid_trim_right = createColorPalette(7588, [181]); +export const cave_walls_bot_trim_right = createColorPalette(7589, [181]); +export const cave_walls_sw = createColorPalette(7590, [181]); +export const cave_walls_s1 = createColorPalette(7591, [181]); +export const cave_walls_s2 = createColorPalette(7592, [181]); +export const cave_walls_s3 = createColorPalette(7593, [181]); +export const cave_walls_sb = createColorPalette(7594, [181]); +export const cave_walls_se = createColorPalette(7595, [181]); +export const pixel2 = sprites2[14653]; + +export const head0 = [ + undefined, + [[0, 1, 2, 4, 7, 8, 9, 10, 12, 13, 14, 16, 18, 19].map(i => head[1]![0]![i])], +]; + +export const head1 = [ + undefined, + [[0, 1, 3, 5, 6, 8, 9, 11, 12, 13, 15, 17, 18, 19].map(i => head[1]![0]![i])], +]; + \ No newline at end of file diff --git a/src/ts/graphics/baseSpriteBatch.ts b/src/ts/graphics/baseSpriteBatch.ts new file mode 100644 index 0000000..4054de8 --- /dev/null +++ b/src/ts/graphics/baseSpriteBatch.ts @@ -0,0 +1,202 @@ +import { Sprite, Batch, SpriteBatchBase, SpriteSheet } from '../common/interfaces'; +import { WHITE } from '../common/colors'; +import { colorToFloat, colorToFloatAlpha } from '../common/color'; +import { BaseStateBatch } from './baseStateBatch'; +import { VAO, createVAO } from './webgl/glVao'; +import { VAOAttributeDefinition, getVAOAttributesSize, createVAOAttributes } from './webgl/vaoAttributes'; +import { timeStart, timeEnd } from '../client/timing'; +import { isIdentity } from '../common/mat2d'; + +// const BATCH_BUFFER_SIZE = 2048; // 8kb +// const BATCH_BUFFER_POOL_SIZE = 5; + +// const pool: ArrayBuffer[] = []; + +// function aquireBuffer(size: number): Float32Array | undefined { +// if (size <= BATCH_BUFFER_SIZE) { +// if (!pool.length) console.log('alloc'); +// const buffer = pool.pop() || new ArrayBuffer(BATCH_BUFFER_SIZE); +// return new Float32Array(buffer, 0, size); +// } else { +// DEVELOPMENT && console.warn(`Failed to aquire buffer of size: ${size}`); +// return undefined; +// } +// } + +// function releaseBuffer(buffer: Float32Array) { +// if (pool.length < BATCH_BUFFER_POOL_SIZE) { +// pool.push(buffer.buffer); +// } else { +// console.log('delete'); +// } +// } + +const WHITE_FLOAT = colorToFloat(WHITE); + +export function getColorFloat(color: number, alpha: number) { + return (color === WHITE && alpha === 1) ? WHITE_FLOAT : colorToFloatAlpha(color, alpha); +} + +export abstract class BaseSpriteBatch extends BaseStateBatch implements SpriteBatchBase { + tris = 0; + flushes = 0; + index = 0; + spritesCount = 0; + vertices!: Float32Array; + verticesUint32!: Uint32Array; + vao: VAO | undefined = undefined; + rectSprite: Sprite | undefined = undefined; + vertexBuffer: WebGLBuffer | undefined = undefined; + indexBuffer: WebGLBuffer | undefined = undefined; + spriteSheet: SpriteSheet | undefined = undefined; + floatsPerSprite: number; + batching = false; + private startBatchIndex = 0; + private startBatchSprites = 0; + constructor( + public gl: WebGLRenderingContext, + public capacity: number, + buffer: ArrayBuffer, + vertexBuffer: WebGLBuffer, + indexBuffer: WebGLBuffer, + public attributes: VAOAttributeDefinition[], + ) { + super(); + this.floatsPerSprite = getVAOAttributesSize(gl, attributes); + this.vertices = new Float32Array(buffer, 0, capacity * this.floatsPerSprite); + this.verticesUint32 = new Uint32Array(buffer, 0, capacity * this.floatsPerSprite); + this.vertexBuffer = vertexBuffer; + this.indexBuffer = indexBuffer; + this.vao = createVAO(gl, createVAOAttributes(gl, attributes, vertexBuffer), indexBuffer); + } + dispose() { + disposeBuffers(this.gl, this); + } + begin() { + if (!this.vao) { + throw new Error('Disposed'); + } + + this.batching = false; + this.vao.bind(); + } + end() { + if (!this.vao) { + throw new Error('Disposed'); + } + + this.flush(); + this.vao.unbind(); + } + drawBatch(batch: Batch) { + if (DEVELOPMENT && !isIdentity(this.transform)) { + throw new Error('Cannot transform batch'); + } + + const batchSpriteCount = (batch.length / this.floatsPerSprite) | 0; + + if (this.capacity < (this.spritesCount + batchSpriteCount)) { + this.flush(); + } + + this.vertices.set(batch, this.index); + this.index += batch.length; + this.spritesCount += batchSpriteCount; + this.tris += batchSpriteCount * 2; + } + startBatch() { + if (this.batching) { + throw new Error('Cannot start new batch'); + } + + this.startBatchIndex = this.index; + this.startBatchSprites = this.spritesCount; + this.batching = true; + } + finishBatch(): Batch | undefined { + if (!this.batching) { + throw new Error('Cannot finish batch'); + } + + this.batching = false; + + try { + // const batch = aquireBuffer(this.index - this.startBatchIndex); + + // if (batch) { + // batch.set(this.vertices.subarray(this.startBatchIndex, this.index)); + // } + + // return batch; + return this.vertices.slice(this.startBatchIndex, this.index); + } catch { + return undefined; + } + } + releaseBatch(_batch: Batch) { + // releaseBuffer(batch); + } + flush() { + if (this.index === 0) + return; + + if (!this.vao || !this.vertexBuffer) { + throw new Error('Disposed'); + } + + const gl = this.gl; + + if (this.batching) { + TIMING && timeStart('bufferSubData'); + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.startBatchIndex)); + TIMING && timeEnd(); + + TIMING && timeStart('vao.draw'); + this.vao.draw(this.gl.TRIANGLES, this.startBatchSprites * 6, 0); + TIMING && timeEnd(); + + this.spritesCount -= this.startBatchSprites; + this.index -= this.startBatchIndex; + this.vertices.copyWithin(0, this.startBatchIndex, this.startBatchIndex + this.index); + this.startBatchIndex = 0; + this.startBatchSprites = 0; + } else { + TIMING && timeStart('bufferSubData'); + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.index)); + TIMING && timeEnd(); + + TIMING && timeStart('vao.draw'); + this.vao.draw(this.gl.TRIANGLES, this.spritesCount * 6, 0); + TIMING && timeEnd(); + + this.spritesCount = 0; + this.index = 0; + } + + this.flushes++; + } +} + +function disposeBuffers(gl: WebGLRenderingContext, batch: BaseSpriteBatch) { + try { + if (batch.vao) { + batch.vao.dispose(); + } + + if (batch.vertexBuffer) { + gl.deleteBuffer(batch.vertexBuffer); + } + + if (batch.indexBuffer) { + gl.deleteBuffer(batch.indexBuffer); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } + + batch.vao = undefined; + batch.vertexBuffer = undefined; + batch.indexBuffer = undefined; +} diff --git a/src/ts/graphics/baseStateBatch.ts b/src/ts/graphics/baseStateBatch.ts new file mode 100644 index 0000000..219f31a --- /dev/null +++ b/src/ts/graphics/baseStateBatch.ts @@ -0,0 +1,76 @@ +import { ObjectCache } from '../common/utils'; +import { Matrix2D, Rect } from '../common/interfaces'; +import { createMat2D, copyMat2D, mulMat2D, rotateMat2D, scaleMat2D, translateMat2D } from '../common/mat2d'; +import { rect, copyRect } from '../common/rect'; + +interface SavedState { + globalAlpha: number; + transform: Matrix2D; + hasCrop: boolean; + cropRect: Rect; +} + +function createEmptyState(): SavedState { + return { + transform: createMat2D(), + globalAlpha: 1, + hasCrop: false, + cropRect: rect(0, 0, 0, 0), + }; +} + +const stateCache = new ObjectCache(10, createEmptyState); + +export abstract class BaseStateBatch { + globalAlpha = 1; + transform = createMat2D(); + private savedStates: SavedState[] = []; + protected hasCrop = false; + protected cropRect = rect(0, 0, 0, 0); + crop(x: number, y: number, w: number, h: number) { + // console.error('Crop not supported'); + this.hasCrop = true; + this.cropRect.x = x; + this.cropRect.y = y; + this.cropRect.w = w; + this.cropRect.h = h; + } + clearCrop() { + this.hasCrop = false; + } + save() { + const state = stateCache.get(); + state.globalAlpha = this.globalAlpha; + state.hasCrop = this.hasCrop; + copyMat2D(state.transform, this.transform); + copyRect(state.cropRect, this.cropRect); + this.savedStates.push(state); + + if (DEVELOPMENT && this.savedStates.length > 100) { + console.error('More than 100 save states'); + } + } + restore() { + const state = this.savedStates.pop(); + + if (state !== undefined) { + this.globalAlpha = state.globalAlpha; + this.hasCrop = state.hasCrop; + copyMat2D(this.transform, state.transform); + copyRect(this.cropRect, state.cropRect); + stateCache.put(state); + } + } + translate(x: number, y: number) { + translateMat2D(this.transform, this.transform, x, y); + } + scale(x: number, y: number) { + scaleMat2D(this.transform, this.transform, x, y); + } + rotate(angle: number) { + rotateMat2D(this.transform, this.transform, angle); + } + multiplyTransform(mat: Matrix2D) { + mulMat2D(this.transform, this.transform, mat); + } +} diff --git a/src/ts/graphics/contextSpriteBatch.ts b/src/ts/graphics/contextSpriteBatch.ts new file mode 100644 index 0000000..61227fb --- /dev/null +++ b/src/ts/graphics/contextSpriteBatch.ts @@ -0,0 +1,359 @@ +import { PaletteSpriteBatch, Sprite, Palette, SpriteBatch, SpriteSheet, Matrix2D } from '../common/interfaces'; +import { createCanvas } from '../client/canvasUtils'; +import { colorToRGBA, getR, getG, getB, getAlpha } from '../common/color'; +import { BaseStateBatch } from './baseStateBatch'; +import { commonPalettes } from './graphicsUtils'; +import { isTranslation } from '../common/mat2d'; +import { TRANSPARENT } from '../common/colors'; + +export function drawBatch( + canvas: HTMLCanvasElement, sheet: SpriteSheet, bg: number | undefined, action: (batch: ContextSpriteBatch) => void +) { + const batch = new ContextSpriteBatch(canvas); + batch.start(sheet, bg || TRANSPARENT); + action(batch); + batch.end(); + return canvas; +} + +export function drawCanvas( + width: number, height: number, sheet: SpriteSheet, bg: number | undefined, action: (batch: ContextSpriteBatch) => void +) { + return drawBatch(createCanvas(width, height), sheet, bg, action); +} + +export class ContextSpriteBatch extends BaseStateBatch implements PaletteSpriteBatch, SpriteBatch { + pixelSize = 1; + disableShading = false; + ignoreColor = 0; + palette = false; + private started = false; + private data: ImageData | undefined = undefined; + private sheet: SpriteSheet | undefined = undefined; + private sheetData: ImageData | undefined = undefined; + constructor(public canvas: HTMLCanvasElement) { + super(); + } + start(sheet: SpriteSheet, clearColor: number) { + if (!this.data || this.data.width !== this.canvas.width || this.data.height !== this.canvas.height) { + if (this.canvas && this.canvas.width && this.canvas.height) { + this.data = this.canvas.getContext('2d')!.getImageData(0, 0, this.canvas.width, this.canvas.height); + } + } + + if (this.data) { + const color = clearColor || 0; + const r = getR(color); + const g = getG(color); + const b = getB(color); + const a = getAlpha(color); + const data = this.data.data; + + for (let i = 0; i < data.length; i += 4) { + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = a; + } + } + + this.sheet = sheet; + this.sheetData = sheet.data; + this.palette = this.sheet.palette; + this.started = true; + } + // clear(color?: number) { + // this.end(); + + // const context = this.canvas.getContext('2d')!; + + // if (color !== undefined) { + // context.fillStyle = colorToCSS(color); + // context.fillRect(0, 0, this.canvas.width, this.canvas.height); + // } else { + // context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // } + // } + end() { + if (this.started) { + if (this.data) { + this.canvas.getContext('2d')!.putImageData(this.data, 0, 0); + } + + this.started = false; + } + } + drawSprite(s: Sprite | undefined, color: number, palette: Palette | undefined | number, x: number, y?: number) { + if (s !== undefined) { + if (y === undefined) { + y = x; + x = palette as number; + drawImageNormal( + this.sheetData, this.data, this.transform, this.globalAlpha, + color, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); + } else { + drawImagePalette( + this.sheetData, this.data, this.transform, this.globalAlpha, + this.ignoreColor, this.disableShading, + s.type, color, palette as Palette, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); + } + } + } + drawImage( // SpriteBatch + color: number, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ): void; + drawImage( // PaletteSpriteBatch3 + type: number, color: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void; + drawImage( + colorOrType: number, sxOrColor: number, syOrPalette: Palette | undefined | number, + swOrSx: number, shOrSy: number, dxOrSw: number, dyOrSh: number, + dwOrDx: number, dhOrDy: number, _OrDw?: number, _OrDh?: number, + ) { + if (_OrDh === undefined) { + drawImageNormal( + this.sheetData, this.data, this.transform, this.globalAlpha, + colorOrType, sxOrColor, syOrPalette as number, + swOrSx, shOrSy, dxOrSw, dyOrSh, dwOrDx, dhOrDy); + } else { + drawImagePalette( + this.sheetData, this.data, this.transform, this.globalAlpha, + this.ignoreColor, this.disableShading, + colorOrType, sxOrColor, syOrPalette as Palette | undefined, + swOrSx, shOrSy, dxOrSw, dyOrSh, dwOrDx, dhOrDy, _OrDw!, _OrDh!); + } + } + drawRect(color: number, x: number, y: number, w: number, h: number) { + drawRect(this.data, this.transform, this.globalAlpha, color, x, y, w, h); + } + drawBatch() { + throw new Error('drawBatch not supported'); + } + startBatch() { + } + finishBatch() { + return undefined; + } + releaseBatch() { + } +} + +const min = Math.min; +const typeOffsets = [0, 2, 3, 0, 1, 2, 3]; + +function drawRect( + dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, + color: number, x: number, y: number, w: number, h: number +) { + if (DEVELOPMENT && !isTranslation(transform)) { + console.error('Transform not supported'); + } + + if (!dst) + return; + + x = Math.round(x + transform[4]); + y = Math.round(y + transform[5]); + + const xx = min(0, x, x); + w += xx; + x -= xx; + + const yy = min(0, y, y); + h += yy; + y -= yy; + + w += min(0, dst.width - (x + w)); + h += min(0, dst.height - (y + h)); + + if (w <= 0 && h <= 0) + return; + + const { r, g, b, a } = colorToRGBA(color); + const alpha = (globalAlpha * a) | 0; + + if (alpha === 0) + return; + + const dstData = dst.data; + const dstWidth = dst.width | 0; + + for (let iy = 0; iy < h; iy++) { + for (let ix = 0; ix < w; ix++) { + const dst0 = ((ix + x) + (iy + y) * dstWidth) << 2; + blendPrecise(dstData, dst0, r, g, b, alpha); + } + } +} + +function drawImageNormal( + src: ImageData | undefined, dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, + tint: number, sx: number, sy: number, sw: number, sh: number, + dx: number, dy: number, dw: number, dh: number +) { + if (sw !== dw || sh !== dh) + throw new Error('Different dimentions not supported'); + + if (DEVELOPMENT && !isTranslation(transform)) { + console.error('Transform not supported'); + } + + if (!src || !dst) + return; + + dx = Math.round(dx + transform[4]); + dy = Math.round(dy + transform[5]); + + let w = sw; + let h = sh; + + const xx = min(0, sx, dx); + w += xx; + dx -= xx; + sx -= xx; + + const yy = min(0, sy, dy); + h += yy; + dy -= yy; + sy -= yy; + + w += min(0, src.width - (sx + w), dst.width - (dx + w)); + h += min(0, src.height - (sy + h), dst.height - (dy + h)); + + if (w <= 0 && h <= 0) + return; + + const { r, g, b, a } = colorToRGBA(tint); + const alpha = (globalAlpha * a) | 0; + const dstData = dst.data; + const srcData = src.data; + const dstWidth = dst.width | 0; + const srcWidth = src.width | 0; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const srcO = ((sx + x) + (sy + y) * srcWidth) << 2; + const sr = srcData[srcO]; + const sg = srcData[srcO + 1]; + const sb = srcData[srcO + 2]; + const sa = srcData[srcO + 3]; + const srcAlpha = blendColor(alpha, sa, 255); + + if (srcAlpha !== 0) { + const rr = blendColor(r, sr, 255); + const gg = blendColor(g, sg, 255); + const bb = blendColor(b, sb, 255); + const dst0 = ((dx + x) + (dy + y) * dstWidth) << 2; + blendPrecise(dstData, dst0, rr, gg, bb, srcAlpha); + } + } + } +} + +function drawImagePalette( + src: ImageData | undefined, dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, + ignoreColorOption: number, disableShadingOption: boolean, + type: number, tint: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number +) { + if (sw !== dw || sh !== dh) + throw new Error('Different dimentions not supported'); + + if (DEVELOPMENT && !isTranslation(transform)) { + console.error('Transform not supported'); + } + + if (palette === undefined) { + palette = commonPalettes.defaultPalette; + } + + if (!src || !dst) + return; + + dx = Math.round(dx + transform[4]); + dy = Math.round(dy + transform[5]); + + let w = sw; + let h = sh; + + const xx = min(0, sx, dx); + w += xx; + dx -= xx; + sx -= xx; + + const yy = min(0, sy, dy); + h += yy; + dy -= yy; + sy -= yy; + + w += min(0, src.width - (sx + w), dst.width - (dx + w)); + h += min(0, src.height - (sy + h), dst.height - (dy + h)); + + if (w <= 0 && h <= 0) + return; + + const { r, g, b, a } = colorToRGBA(tint); + const alpha = (globalAlpha * a) | 0; + const colors = palette.colors; + const dstData = dst.data; + const srcData = src.data; + const dstWidth = dst.width | 0; + const srcWidth = src.width | 0; + const ignoreColor = ignoreColorOption >>> 0; + const disableShading = disableShadingOption || type > 2; + const offset = typeOffsets[type]; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const srcO = ((sx + x) + (sy + y) * srcWidth) << 2; + const index = srcData[srcO + offset]; + const color = colors[index]; + const srcAlpha = ignoreColor === color ? 0 : blendColor(getAlpha(color), alpha, 255); + + if (srcAlpha !== 0) { + const shade = disableShading ? 255 : srcData[srcO + 1]; + const rr = blendColor(getR(color), r, shade); + const gg = blendColor(getG(color), g, shade); + const bb = blendColor(getB(color), b, shade); + const dst0 = ((dx + x) + (dy + y) * dstWidth) << 2; + blendPrecise(dstData, dst0, rr, gg, bb, srcAlpha); + } + } + } +} + +function blendColor(base: number, tint: number, shade: number) { + return (((((base * tint) | 0) * shade) | 0) / 65025) | 0; +} + +function blendPrecise(dstData: Uint8ClampedArray, dst0: number, r: number, g: number, b: number, alpha: number) { + if (alpha === 0xff || dstData[dst0 + 3] === 0) { + dstData[dst0] = r; + dstData[dst0 + 1] = g; + dstData[dst0 + 2] = b; + dstData[dst0 + 3] = alpha; + } else { + const dstAlpha = (0xff - alpha) | 0; + dstData[dst0] = ((((r * alpha) | 0) / 255) | 0) + ((((dstData[dst0] * dstAlpha) | 0) / 255) | 0); + dstData[dst0 + 1] = ((((g * alpha) | 0) / 255) | 0) + ((((dstData[dst0 + 1] * dstAlpha) | 0) / 255) | 0); + dstData[dst0 + 2] = ((((b * alpha) | 0) / 255) | 0) + ((((dstData[dst0 + 2] * dstAlpha) | 0) / 255) | 0); + const a = (alpha + ((((dstData[dst0 + 3] * dstAlpha) | 0) / 255) | 0)) | 0; + dstData[dst0 + 3] = a > 0xff ? 0xff : a; + } +} + +// function blendFast(dstData: Uint8ClampedArray, dst0: number, r: number, g: number, b: number, alpha: number) { +// if (alpha === 0xff) { +// dstData[dst0] = r; +// dstData[dst0 + 1] = g; +// dstData[dst0 + 2] = b; +// dstData[dst0 + 3] = alpha; +// } else { +// const dstAlpha = (0xff - alpha) | 0; +// dstData[dst0] = ((r * alpha) >> 8) + ((dstData[dst0] * dstAlpha) >> 8); +// dstData[dst0 + 1] = ((g * alpha) >> 8) + ((dstData[dst0 + 1] * dstAlpha) >> 8); +// dstData[dst0 + 2] = ((b * alpha) >> 8) + ((dstData[dst0 + 2] * dstAlpha) >> 8); +// dstData[dst0 + 3] = min(0xff, alpha + ((dstData[dst0 + 3] * dstAlpha) >> 8)); +// } +// } diff --git a/src/ts/graphics/graphicsUtils.ts b/src/ts/graphics/graphicsUtils.ts new file mode 100644 index 0000000..cb7f014 --- /dev/null +++ b/src/ts/graphics/graphicsUtils.ts @@ -0,0 +1,586 @@ +import { + Says, Rect, SpriteBatch, Sprite, SpriteBorder, MessageType, Entity, Point, Pony, SpriteBatchCommons, + PaletteSpriteBatch, Palette, isPartyMessage, PartyInfo, Camera, CommonPalettes, FontPalettes, PaletteManager, + isWhisperTo, isWhisper, isThinking, isPublicMessage +} from '../common/interfaces'; +import { clamp, contains, intersect, toInt, hasFlag } from '../common/utils'; +import { BLACK, WHITE, OUTLINE_COLOR, getMessageColor, PARTY_COLOR, MESSAGE_COLOR, FRIENDS_COLOR } from '../common/colors'; +import { tileWidth, tileHeight, tileElevation, PONY_TYPE } from '../common/constants'; +import { + HAlign, VAlign, TextOptions, lineBreak, drawTextAligned, measureText, drawText, drawOutlinedText +} from '../graphics/spriteFont'; +import * as sprites from '../generated/sprites'; +import { fontPal, fontSmallPal } from '../client/fonts'; +import { getPonyChatHeight, isPony } from '../common/pony'; +import { worldToScreen } from '../common/camera'; +import { multiplyColor, colorToCSS } from '../common/color'; +import { getTag, getTagPalette } from '../common/tags'; +import { rect } from '../common/rect'; +import { mockPaletteManager } from '../common/ponyInfo'; +import { sortEntities, isHidden, isFriend } from '../common/entityUtils'; + +const baloonTaper = [ + { w: 1, y: 2 }, + { w: 1, y: 1 }, +]; + +const roundTaper = [ + { w: 1, y: 5 }, + { w: 1, y: 3 }, + { w: 2, y: 2 }, + { w: 4, y: 1 }, +]; + +export const commonPalettes = createCommonPalettes(mockPaletteManager); + +type AnyBatch = SpriteBatch | PaletteSpriteBatch; + +export function drawTaperedRect( + batch: AnyBatch, color: number, x: number, y: number, w: number, h: number, taper: { w: number; y: number; }[] +) { + x = Math.round(x) | 0; + y = Math.round(y) | 0; + w = Math.round(w) | 0; + h = Math.round(h) | 0; + + let gap = 0; + + for (let i = 0; i < taper.length; i++) { + const t = taper[i]; + const th = h - t.y * 2; + batch.drawRect(color, x + gap, y + t.y, t.w, th); + batch.drawRect(color, x + w - gap - t.w, y + t.y, t.w, th); + gap += t.w; + } + + batch.drawRect(color, x + gap, y, Math.max(0, w - gap * 2), h); +} + +export function drawRectBaloon(batch: AnyBatch, color: number, x: number, y: number, w: number, h: number) { + drawTaperedRect(batch, color, x, y, w, h, baloonTaper); +} + +export function drawRoundBaloon(batch: AnyBatch, color: number, x: number, y: number, w: number, h: number) { + drawTaperedRect(batch, color, x, y, w, h, roundTaper); +} + +function getMessagePalette(type: MessageType, palettes: FontPalettes) { + if (type === MessageType.Supporter2) { + return palettes.supporter2; + } else if (type === MessageType.Supporter3) { + return palettes.supporter3; + } else { + return undefined; + } +} + +export function drawBaloon( + batch: PaletteSpriteBatch, { message, type = MessageType.Chat, timer = 1, total = 10 }: Says, + x: number, y: number, bounds: Rect, palettes: CommonPalettes +) { + if (!fontPal) + return; + + let { w, h } = measureText(message, fontPal); + + w = Math.max(w, 4); + + const screenPad = 8; + const availableWidth = bounds.w - screenPad * 2; + + if (w > availableWidth) { + message = lineBreak(message, fontPal, availableWidth); + const size = measureText(message, fontPal); + w = size.w; + h = size.h; + } + + const { dy, alpha } = calcAnimation(timer, total); + + y += dy; + + const nippleX = x; + const toTheLeft = Math.max(0, screenPad - x); + const toTheRight = Math.max(0, x - bounds.w + screenPad); + + x = clamp(x, screenPad + w / 2 - toTheLeft, bounds.w - screenPad - w / 2 + toTheRight); + + if (intersect(0, 0, bounds.w, bounds.h, x - w / 2, y - h / 2, w, h)) { + const palette = getMessagePalette(type, palettes.mainFont); + const color = palette ? WHITE : getMessageColor(type); + const options: TextOptions = { + palette: palette || palettes.mainFont.white, + emojiPalette: palettes.mainFont.emoji, + }; + + if (isThinking(type)) { + drawThinkingBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); + } else if (isWhisper(type) || isWhisperTo(type)) { + drawWhisperBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); + } else { + drawSpeechBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); + } + } +} + +export function drawSpeechBaloon( + batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, + alpha: number, nippleX: number, +) { + const pad = 4; + const xx = x - Math.round(w / 2); + const yy = y - h; + const nipple = sprites.nipple_2.color; + + nippleX = clamp(nippleX, xx + pad, xx + w - pad); + + batch.globalAlpha = 0.6 * alpha; + drawRectBaloon(batch, BLACK, xx - pad, yy - pad, w + pad * 2, h + pad * 2); + batch.drawSprite(nipple, BLACK, undefined, nippleX - Math.round(nipple.w / 2), y + pad); + + batch.globalAlpha = alpha; + drawText(batch, text, fontPal, color, xx, yy, options); + batch.globalAlpha = 1; +} + +export function drawWhisperBaloon( + batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, + alpha: number, nippleX: number, +) { + const pad = 4; + const xx = x - Math.round(w / 2); + const yy = y - h; + const nipple = sprites.nipple_alt_2.color; + + nippleX = clamp(nippleX, xx + pad, xx + w - pad); + + batch.globalAlpha = 0.6 * alpha; + + const left = xx - pad; + const top = yy - pad; + const width = w + pad * 2; + const height = h + pad * 2; + + batch.drawRect(BLACK, left + 2, top, width - 4, 1); + batch.drawRect(BLACK, left + 1, top + 1, width - 2, 1); + batch.drawRect(BLACK, left, top + 2, width, height - 4); + batch.drawRect(BLACK, left + 1, top + height - 2, width - 2, 1); + batch.drawRect(BLACK, left + 2, top + height - 1, width - 4, 1); + + const yyy = top + height + 1; + const right = left + width - 1; + + for (let tx = nippleX - 7; (tx + 5) > (left + 1); tx -= 5) { + const shorten = Math.max(0, (left + 1) - tx); + batch.drawRect(BLACK, tx + shorten, yyy, 4 - shorten, 1); + } + + for (let tx = nippleX + 2; tx < right; tx += 5) { + const shorten = Math.max(0, (tx + 5) - right); + batch.drawRect(BLACK, tx, yyy, Math.min(4, 5 - shorten), 1); + } + + batch.drawSprite(nipple, BLACK, undefined, nippleX - 3, y + pad + 2); + + batch.globalAlpha = alpha; + drawText(batch, text, fontPal, color, xx, yy, options); + batch.globalAlpha = 1; +} + +export function drawThinkingBaloon( + batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, + alpha: number, nippleX: number, +) { + const padX = 6; + const padY = 4; + const xx = x - Math.round(w / 2); + const yy = y - h; + const ox = clamp(nippleX, xx, xx + w) - 1; + const oy = y + 12; + + batch.globalAlpha = 0.6 * alpha; + drawRoundBaloon(batch, BLACK, xx - padX, yy - padY, w + padX * 2, h + padY * 2); + batch.drawRect(BLACK, ox, oy, 1, 1); + batch.drawRect(BLACK, ox - 1, oy - 3, 2, 2); + batch.drawRect(BLACK, ox, oy - 7, 3, 3); + + batch.globalAlpha = alpha; + drawText(batch, text, fontPal, color, xx, yy, options); + batch.globalAlpha = 1; +} + +export enum DrawNameFlags { + None = 0, + Party = 1, + Friend = 2, +} + +function getNameColor(flags: DrawNameFlags) { + if (hasFlag(flags, DrawNameFlags.Party)) { + return PARTY_COLOR; + } else if (hasFlag(flags, DrawNameFlags.Friend)) { + return FRIENDS_COLOR; + } else { + return WHITE; + } +} + +export function drawNamePlate( + batch: PaletteSpriteBatch, text: string, x: number, y: number, flags: DrawNameFlags, + palettes: CommonPalettes, tagId?: string, +) { + const tag = getTag(tagId); + const size = measureText(text, fontPal); + const xx = x - Math.round(size.w / 2); + const yy = y - size.h + 6 - (tag ? 3 : 0); + const color = getNameColor(flags); + const options = { palette: palettes.mainFont.white, emojiPalette: palettes.mainFont.emoji }; + drawOutlinedText(batch, text, fontPal, color, OUTLINE_COLOR, xx, yy, options); + + if (tag) { + const tagSize = measureText(tag.label, fontSmallPal); + const textX = x - Math.round(tagSize.w / 2); + const palette = getTagPalette(tag, palettes.smallFont); + drawOutlinedText(batch, tag.label, fontSmallPal, tag.color, OUTLINE_COLOR, textX, yy + 11, { palette }); + } +} + +export function drawBounds(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number) { + if (r) { + batch.drawRect( + color, + Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y), + Math.round(r.w), Math.round(r.h)); + } +} + +export function drawWorldBounds(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number) { + if (r) { + batch.drawRect( + color, + Math.round((e.x + r.x) * tileWidth), Math.round((e.y + r.y) * tileHeight), + Math.round(r.w * tileWidth), Math.round(r.h * tileHeight)); + } +} + + +export function drawBoundsOutline(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number, thickness = 1) { + if (r) { + drawOutline( + batch, color, + Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y), + Math.round(r.w), Math.round(r.h), + thickness); + } +} + +export function drawOutlineRect(batch: SpriteBatchCommons, color: number, { x, y, w, h }: Rect, thickness = 1) { + drawOutline(batch, color, x, y, w, h, thickness); +} + +export function drawOutline(batch: SpriteBatchCommons, color: number, x: number, y: number, w: number, h: number, thickness = 1) { + batch.drawRect(color, x - thickness, y - thickness, w + thickness * 2, thickness); // top + batch.drawRect(color, x - thickness, y + h, w + thickness * 2, thickness); // bottom + batch.drawRect(color, x - thickness, y, thickness, h); // left + batch.drawRect(color, x + w, y, thickness, h); // right +} + +export type DrawRect = (color: number, x: number, y: number, w: number, h: number) => void; + +export function drawCharacter(drawRect: DrawRect, x: number, y: number, color: number, char: string) { + switch (char) { + case '0': + drawRect(color, x, y, 1, 5); + drawRect(color, x + 1, y, 1, 1); + drawRect(color, x + 1, y + 4, 1, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '1': + drawRect(color, x, y + 1, 1, 1); + drawRect(color, x + 1, y, 1, 4); + drawRect(color, x, y + 4, 3, 1); + return 3; + case '2': + drawRect(color, x, y, 3, 1); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x, y + 4, 3, 1); + drawRect(color, x + 2, y + 1, 1, 1); + drawRect(color, x, y + 3, 1, 1); + return 3; + case '3': + drawRect(color, x, y, 2, 1); + drawRect(color, x, y + 2, 2, 1); + drawRect(color, x, y + 4, 2, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '4': + drawRect(color, x, y, 1, 3); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '5': + drawRect(color, x, y, 3, 1); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x, y + 4, 3, 1); + drawRect(color, x, y + 1, 1, 1); + drawRect(color, x + 2, y + 3, 1, 1); + return 3; + case '6': + drawRect(color, x, y, 3, 1); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x, y + 4, 3, 1); + drawRect(color, x, y, 1, 5); + drawRect(color, x + 2, y + 3, 1, 1); + return 3; + case '7': + drawRect(color, x, y, 3, 1); + drawRect(color, x + 2, y + 1, 1, 4); + return 3; + case '8': + drawRect(color, x, y, 1, 5); + drawRect(color, x + 1, y, 1, 1); + drawRect(color, x + 1, y + 2, 1, 1); + drawRect(color, x + 1, y + 4, 1, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '9': + drawRect(color, x, y, 1, 3); + drawRect(color, x + 1, y, 1, 1); + drawRect(color, x + 1, y + 2, 1, 1); + drawRect(color, x, y + 4, 2, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case ':': + drawRect(color, x, y + 1, 1, 1); + drawRect(color, x, y + 3, 1, 1); + return 1; + case '.': + drawRect(color, x, y + 4, 1, 1); + return 1; + case '-': + drawRect(color, x, y + 2, 3, 1); + return 3; + case ' ': + return 2; + default: + drawRect(color, x, y, 3, 5); + return 3; + } +} + +export function drawPixelTextBase(drawRect: DrawRect, x: number, y: number, color: number, text: string) { + for (let i = 0; i < text.length; i++) { + x += drawCharacter(drawRect, x, y, color, text.charAt(i)) + 1; + } +} + +export function drawPixelText(batch: PaletteSpriteBatch, x: number, y: number, color: number, text: string) { + drawPixelTextBase(batch.drawRect.bind(batch), x, y, color, text); +} + +export function fillRect(context: CanvasRenderingContext2D, color: string, x: number, y: number, w: number, h: number) { + context.fillStyle = color; + context.fillRect(x, y, w, h); +} + +export function drawPixelTextOnCanvas(context: CanvasRenderingContext2D, x: number, y: number, color: number, text: string) { + drawPixelTextBase((color, x, y, w, h) => fillRect(context, colorToCSS(color), x, y, w, h), x, y, color, text); +} + +interface Say { + message: Says; + x: number; + y: number; +} + +export function compareSays(a: Say, b: Say) { + return a.message.created - b.message.created; +} + +function isPartyMember(entity: Entity, party: PartyInfo | undefined) { + return entity.type === PONY_TYPE && party !== undefined && party.members.some(p => p.id === entity.id && !p.pending); +} + +export function drawNames( + batch: PaletteSpriteBatch, entities: Entity[], player: Pony | undefined, party: PartyInfo | undefined, + camera: Camera, hover: Point, drawHidden: boolean, palettes: CommonPalettes +) { + sortEntities(entities); + + for (const e of entities) { + if ((!isHidden(e) || drawHidden) && e.name && e !== player) { + const nameOffsetBase = 12; + const bounds = e.interactBounds || e.bounds; + const chatBounds = e.chatBounds || bounds; + + if (chatBounds !== undefined && bounds !== undefined && contains(e.x, e.y, bounds, hover)) { + const { x, y } = worldToScreen(camera, e); + const nameOffset = nameOffsetBase - getChatHeight(e); + const tag = (isHidden(e) && drawHidden) ? 'hidden' : e.tag; + const flags = DrawNameFlags.None | + (isPartyMember(e, party) ? DrawNameFlags.Party : 0) | + (isFriend(e) ? DrawNameFlags.Friend : 0); + drawNamePlate(batch, e.name, x, y + chatBounds.y - nameOffset, flags, palettes, tag); + } + } + } +} + +export function getChatBallonXY(e: Entity, camera: Camera): Point { + const nameOffsetBase = 12; + const bounds = e.interactBounds || e.bounds; + const chatBounds = e.chatBounds || bounds; + const screen = worldToScreen(camera, e); + const nameOffset = nameOffsetBase - getChatHeight(e); + const offset = (nameOffset + 6) + (e.tag ? 5 : 0); + const yy = screen.y + (chatBounds ? chatBounds.y : 0) - offset; + const x = screen.x + toInt(e.chatX); + const y = yy + toInt(e.chatY); + return { x, y }; +} + +function drawChatBaloon(batch: PaletteSpriteBatch, entity: Entity, camera: Camera, palettes: CommonPalettes) { + const { x, y } = getChatBallonXY(entity, camera); + drawBaloon(batch, entity.says!, x, y, camera, palettes); +} + +export function drawChat( + batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, drawHidden: boolean, palettes: CommonPalettes, + hidePublic: boolean +) { + sortEntities(entities); + + for (const entity of entities) { + if ((!isHidden(entity) || drawHidden) && !isPartyMessage(entity.says!.type || MessageType.Chat)) { + if (!hidePublic || !isPublicMessage(entity.says!.type || MessageType.Chat)) { + drawChatBaloon(batch, entity, camera, palettes); + } + } + } + + for (const entity of entities) { + if ((!isHidden(entity) || drawHidden) && isPartyMessage(entity.says!.type || MessageType.Chat)) { + drawChatBaloon(batch, entity, camera, palettes); + } + } +} + +function getChatHeight(entity: Entity): number { + return isPony(entity) ? getPonyChatHeight(entity) : 0; +} + +export const chatAnimationDuration = 0.2; + +export function dismissSays(says: Says) { + if (says.timer !== undefined) { + says.timer = Math.min(says.timer, chatAnimationDuration); + } +} + +function calcAnimation(timer: number, total: number) { + const start = (total - timer) / chatAnimationDuration; + const end = timer / chatAnimationDuration; + + const dys = [3, 2, 1, 0, -1, 0]; + const dys2 = [-4, -3, -2, -1]; + const dyd = start * dys.length; + const dyd2 = end * dys2.length; + const dyi = clamp(Math.round(dyd), 0, dys.length - 1); + const dyi2 = clamp(Math.round(dyd2), 0, dys2.length); + const dy = dyi2 < dys2.length ? dys2[dyi2] : dys[dyi]; + const alpha = Math.min(start, end, 1); + + return { alpha, dy }; +} + +export function drawBox( + batch: SpriteBatchCommons, color: number, shadowColor: number, x: number, y: number, z: number, + w: number, l: number, h: number +) { + const darker = multiplyColor(color, 0.8); + const left = (x - w / 2) * tileWidth; + const bottom = y * tileHeight; + const elevation = z * tileElevation; + const width = w * tileWidth; + const frontHeight = h * tileElevation; + const topHeight = l * tileHeight; + + batch.drawRect(shadowColor, left, bottom - topHeight, width, topHeight); // shadow + batch.drawRect(darker, left, bottom - elevation - frontHeight, width, frontHeight); // front + batch.drawRect(color, left, bottom - elevation - frontHeight - topHeight, width, topHeight); // top +} + +export function drawSpriteBorder( + batch: SpriteBatch, border: SpriteBorder, color: number, x: number, y: number, w: number, h: number +) { + x = Math.round(x) | 0; + y = Math.round(y) | 0; + w = Math.round(w) | 0; + h = Math.round(h) | 0; + + const size = border.border; + const right = x + w - size; + const bottom = y + h - size; + const bgWidth = w - size * 2; + const bgHeight = h - size * 2; + + batch.drawSprite(border.topLeft, color, x, y); + batch.drawSprite(border.topRight, color, right, y); + batch.drawSprite(border.bottomLeft, color, x, bottom); + batch.drawSprite(border.bottomRight, color, right, bottom); + + drawStretched(batch, border.top, color, x + size, y, bgWidth, size); + drawStretched(batch, border.left, color, x, y + size, size, bgHeight); + //drawStretched(batch, border.bg, color, x + size, y + size, bgWidth, bgHeight); + batch.drawRect(color, x + size, y + size, bgWidth, bgHeight); + drawStretched(batch, border.right, color, x + w - size, y + size, size, bgHeight); + drawStretched(batch, border.bottom, color, x + size, y + h - size, bgWidth, size); +} + +function drawStretched(batch: SpriteBatch, sprite: Sprite, color: number, x: number, y: number, w: number, h: number) { + if (sprite.h && sprite.w) { + const sw = Math.min(sprite.w, w); + const sh = Math.min(sprite.h, h); + batch.drawImage(color, sprite.x, sprite.y, sw, sh, x, y, w, h); + } +} + +export function drawSpriteCropped( + batch: PaletteSpriteBatch, s: Sprite, color: number, palette: Palette | undefined, x: number, y: number, + maxY: number +) { + const top = y + s.oy; + const bottom = Math.min(top + s.h, maxY); + + if (bottom > top) { + batch.drawImage(s.type, color, palette, s.x, s.y, s.w, s.h, x + s.ox, top, s.w, bottom - top); + } +} + +export function drawFullScreenMessage(batch: PaletteSpriteBatch, camera: Camera, text: string, palette: Palette) { + const messageHeight = 100; + const messageY = Math.round((camera.h - messageHeight) / 2); + batch.drawRect(MESSAGE_COLOR, 0, messageY, camera.w, messageHeight); + const textRect = rect(0, messageY, camera.w, messageHeight); + drawTextAligned(batch, text, fontPal, WHITE, textRect, HAlign.Center, VAlign.Middle, { palette }); +} + +export function createCommonPalettes(paletteManager: PaletteManager): CommonPalettes { + return { + defaultPalette: paletteManager.addArray(sprites.defaultPalette), + mainFont: { + emoji: paletteManager.addArray(sprites.emojiPalette), + white: paletteManager.addArray(sprites.fontPalette), + supporter1: paletteManager.addArray(sprites.fontSupporter1Palette), + supporter2: paletteManager.addArray(sprites.fontSupporter2Palette), + supporter3: paletteManager.addArray(sprites.fontSupporter3Palette), + }, + smallFont: { + emoji: paletteManager.addArray(sprites.emojiPalette), + white: paletteManager.addArray(sprites.fontSmallPalette), + supporter1: paletteManager.addArray(sprites.fontSmallSupporter1Palette), + supporter2: paletteManager.addArray(sprites.fontSmallSupporter2Palette), + supporter3: paletteManager.addArray(sprites.fontSmallSupporter3Palette), + }, + }; +} diff --git a/src/ts/graphics/paletteManager.ts b/src/ts/graphics/paletteManager.ts new file mode 100644 index 0000000..b9e1e4c --- /dev/null +++ b/src/ts/graphics/paletteManager.ts @@ -0,0 +1,247 @@ +import { Palette, PaletteManager as IPaletteManager } from '../common/interfaces'; +import { Texture2D, resizeTexture, disposeTexture, createEmptyTexture } from './webgl/texture2d'; +import { times, flatten, computeCRC } from '../common/utils'; + +const INITIAL_SIZE = 512; +const MAX_SIZE = 2048; + +export function createPalette(colors: Uint32Array): Palette { + return { x: 0, y: 0, u: 0, v: 0, refs: 1, colors }; +} + +export function releasePalette(palette: Palette | undefined) { + if (palette && palette.refs) { + palette.refs--; + } +} + +export function colorsEqual(a: Uint32Array, b: Uint32Array): boolean { + if (a === b) { + return true; + } + + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +} + +function isInUse(palette: Palette) { + return palette.refs > 0; +} + +export class PaletteManager implements IPaletteManager { + private paletteTexture?: Texture2D; + private palettes = times(512, () => []); + private dirty: Palette[] = []; + private dirtyMinY = 0; + private dirtyMaxY = -1; + private lastX = 0; + private lastY = 0; + private initialized = true; + deduplicate = true; + constructor(private size = INITIAL_SIZE) { + } + get texture() { + return this.paletteTexture; + } + get textureSize() { + return this.size; + } + get pixelSize() { + return 256 / this.size; + } + activePalettes() { + return this.palettes.reduce((sum, p) => sum + p.reduce((sum, p) => sum + (p.refs > 0 ? 1 : 0), 0), 0); + } + add(colorValues: number[]): Palette { + const colors = new Uint32Array(colorValues.length); + + for (let i = 0; i < colorValues.length; i++) { + colors[i] = colorValues[i] >>> 0; + } + + return this.addArray(colors); + } + addArray(colors: Uint32Array): Palette { + const hash = computeCRC(colors) & 0x1ff; + const palettes = this.palettes[hash]; + + if (this.deduplicate) { + for (let i = 0; i < palettes.length; i++) { + const existing = palettes[i]; + + if (colorsEqual(existing.colors, colors)) { + existing.refs = (existing.refs + 1) | 0; + return existing; + } + } + } + + const palette = createPalette(colors); + palettes.push(palette); + this.dirty.push(palette); + return palette; + } + commit(gl: WebGLRenderingContext): boolean { + let changed = this.initialized; + + if (!this.paletteTexture) { + this.initializeTexture(gl, this.size); + changed = true; + } + + if (this.dirty.length) { + if (!this.arrange(this.dirty)) { + this.cleanupPalettes(); + changed = true; + + while (!this.arrange(this.dirty)) { + if (this.size < MAX_SIZE) { + this.initializeTexture(gl, this.size * 2); + } else { + throw new Error('Exceeded maximum palettes limit'); + } + } + } + + this.updateTexture(gl); + this.dirty = []; + } + + this.initialized = false; + return changed; + } + init(gl: WebGLRenderingContext) { + this.initialized = true; + this.paletteTexture = undefined; + this.initializeTexture(gl, this.size); + } + dispose(gl: WebGLRenderingContext | undefined) { + this.paletteTexture = disposeTexture(gl, this.paletteTexture); + + for (let i = 0; i < this.palettes.length; i++) { + if (this.palettes[i].length > 0) { + this.palettes[i] = []; + } + } + + this.size = INITIAL_SIZE; + this.resetPalettes(); + } + cleanup() { + this.cleanupPalettes(); + } + private resetPalettes() { + this.dirty = flatten(this.palettes); + this.lastX = 0; + this.lastY = 0; + } + private cleanupPalettes() { + const palettes = this.palettes; + + for (let i = 0; i < palettes.length; i++) { + if (palettes[i].length > 0) { + palettes[i] = palettes[i].filter(isInUse); + } + } + + this.resetPalettes(); + } + private initializeTexture(gl: WebGLRenderingContext, size: number) { + try { + if (!this.paletteTexture) { + this.paletteTexture = createEmptyTexture(gl, size, size, gl.RGBA, gl.UNSIGNED_BYTE); + } else if (this.paletteTexture.width !== size) { + resizeTexture(gl, this.paletteTexture, size, size); + } + } catch (e) { + throw new Error(`Failed to create/resize texture (${size}) ${e.stack}`); + } + + this.size = size; + this.resetPalettes(); + } + private arrange(palettes: Palette[]) { + if (!palettes.length) { + return true; + } + + const size = this.size | 0; + let x = this.lastX | 0; + let y = this.lastY | 0; + let minY = -1; + let maxY = -1; + + for (let i = 0; i < palettes.length; i++) { + const p = palettes[i]; + const colorCount = p.colors.length | 0; + + if ((size - x) < colorCount) { + x = 0; + y++; + + if (y >= size) { + return false; + } + } + + p.x = x; + p.y = y; + p.u = (x + 0.5) / size; + p.v = (y + 0.5) / size; + x = (x + colorCount) | 0; + + minY = minY === -1 ? y : minY; + maxY = Math.max(maxY, y); + } + + this.lastX = x; + this.lastY = y; + this.dirtyMinY = minY; + this.dirtyMaxY = maxY; + return true; + } + private updateTexture(gl: WebGLRenderingContext) { + if (!this.paletteTexture || this.dirtyMinY > this.dirtyMaxY) + return; + + const width = this.size; + const height = (this.dirtyMaxY - this.dirtyMinY) + 1; + const data = new Uint8Array(width * height * 4); + + for (let k = 0; k < this.palettes.length; k++) { + const palettes = this.palettes[k]; + + for (let i = 0; i < palettes.length; i++) { + const { x, y, colors } = palettes[i]; + + if (y < this.dirtyMinY || y > this.dirtyMaxY) + continue; + + let offset = (x + (y - this.dirtyMinY) * width) << 2; + + for (let j = 0; j < colors.length; j++) { + const c = colors[j]; + data[offset++] = (c >>> 24) & 0xff; + data[offset++] = (c >>> 16) & 0xff; + data[offset++] = (c >>> 8) & 0xff; + data[offset++] = (c >>> 0) & 0xff; + } + } + } + + gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture.handle); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, this.dirtyMinY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); + + this.dirtyMinY = 0; + this.dirtyMaxY = -1; + } +} diff --git a/src/ts/graphics/paletteSpriteBatch.ts b/src/ts/graphics/paletteSpriteBatch.ts new file mode 100644 index 0000000..445667e --- /dev/null +++ b/src/ts/graphics/paletteSpriteBatch.ts @@ -0,0 +1,243 @@ +import { Sprite, Palette, PaletteSpriteBatch as IPaletteSpriteBatch, Matrix2D } from '../common/interfaces'; +import { BaseSpriteBatch, getColorFloat } from './baseSpriteBatch'; +import { colorFromRGBA, colorToFloat } from '../common/color'; +import { createSprite } from '../client/spriteUtils'; +import { createPalette } from './paletteManager'; + +const defaultRectSprite = createSprite(0, 0, 1, 1, 0, 0, 3); + +const types = new Float32Array([ + colorToFloat(colorFromRGBA(255, 0, 0, 0)), // type 0 shade + colorToFloat(colorFromRGBA(0, 0, 255, 0)), // type 2 shade + colorToFloat(colorFromRGBA(0, 0, 0, 0)), // type 3 shade + colorToFloat(colorFromRGBA(255, 0, 0, 255)), // type 0 + colorToFloat(colorFromRGBA(0, 255, 0, 255)), // type 1 + colorToFloat(colorFromRGBA(0, 0, 255, 255)), // type 2 + colorToFloat(colorFromRGBA(0, 0, 0, 255)), // type 3 +]); + +/* +function pushVertex( + vertices: Float32Array, _verticesUint32: Uint32Array, index: number, + x: number, y: number, u: number, v: number, pu: number, pv: number, c: number, c1: number, transform: Matrix2D +) { + vertices[index++] = transform[0] * x + transform[2] * y + transform[4]; + vertices[index++] = transform[1] * x + transform[3] * y + transform[5]; + vertices[index++] = u; + vertices[index++] = v; + vertices[index++] = pu; + vertices[index++] = pv; + vertices[index++] = c; + vertices[index++] = c1; +} + +function pushQuad( + vertices: Float32Array, verticesUint32: Uint32Array, + transform: mat2d, index: number, type: number, color: number, palette: Palette, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number +) { + const c1 = types[type]; + + const y2 = dy + dh; + const x2 = dx + dw; + + const u1 = sx + 0.1; + const v1 = sy + 0.1; + const u2 = sx + sw; + const v2 = sy + sh; + + const pu = palette.u; + const pv = palette.v; + + pushVertex(vertices, verticesUint32, index, dx, dy, u1, v1, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index + 8, x2, dy, u2, v1, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index + 16, x2, y2, u2, v2, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index + 24, dx, y2, u1, v2, pu, pv, color, c1, transform); + + return index + 32; +} +*/ + +function pushQuad( + vertices: Float32Array, //_verticesUint32: Uint32Array, + transform: Matrix2D, index: number, type: number, color: number, palette: Palette, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, +) { + const c1 = types[type]; + + const y2 = dy + dh; + const x2 = dx + dw; + + const u1 = sx + 0.1; + const v1 = sy + 0.1; + const u2 = sx + sw; + const v2 = sy + sh; + + const pu = palette.u; // + palette.v * 1024; + const pv = palette.v; + + const t0 = transform[0]; + const t1 = transform[1]; + const t2 = transform[2]; + const t3 = transform[3]; + const t4 = transform[4]; + const t5 = transform[5]; + + // pushVertex(vertices, index, dx, dy, u1, v1, pu, pv, color, c1, transform); + vertices[(index + 0) | 0] = t0 * dx + t2 * dy + t4; + vertices[(index + 1) | 0] = t1 * dx + t3 * dy + t5; + vertices[(index + 2) | 0] = u1; + vertices[(index + 3) | 0] = v1; + vertices[(index + 4) | 0] = pu; + vertices[(index + 5) | 0] = pv; + vertices[(index + 6) | 0] = color; + vertices[(index + 7) | 0] = c1; + + // pushVertex(vertices, index + 8, x2, dy, u2, v1, pu, pv, color, c1, transform); + vertices[(index + 8) | 0] = t0 * x2 + t2 * dy + t4; + vertices[(index + 9) | 0] = t1 * x2 + t3 * dy + t5; + vertices[(index + 10) | 0] = u2; + vertices[(index + 11) | 0] = v1; + vertices[(index + 12) | 0] = pu; + vertices[(index + 13) | 0] = pv; + vertices[(index + 14) | 0] = color; + vertices[(index + 15) | 0] = c1; + + // pushVertex(vertices, index + 16, x2, y2, u2, v2, pu, pv, color, c1, transform); + vertices[(index + 16) | 0] = t0 * x2 + t2 * y2 + t4; + vertices[(index + 17) | 0] = t1 * x2 + t3 * y2 + t5; + vertices[(index + 18) | 0] = u2; + vertices[(index + 19) | 0] = v2; + vertices[(index + 20) | 0] = pu; + vertices[(index + 21) | 0] = pv; + vertices[(index + 22) | 0] = color; + vertices[(index + 23) | 0] = c1; + + // pushVertex(vertices, index + 24, dx, y2, u1, v2, pu, pv, color, c1, transform); + vertices[(index + 24) | 0] = t0 * dx + t2 * y2 + t4; + vertices[(index + 25) | 0] = t1 * dx + t3 * y2 + t5; + vertices[(index + 26) | 0] = u1; + vertices[(index + 27) | 0] = v2; + vertices[(index + 28) | 0] = pu; + vertices[(index + 29) | 0] = pv; + vertices[(index + 30) | 0] = color; + vertices[(index + 31) | 0] = c1; + + return index + 32; +} + +// function colorWithAlpha(color: number, alpha: number) { +// return ((color & 0xffffff00) | (((color & 0xff) * alpha) & 0xff)) >>> 0; +// } + +export const PALETTE_BATCH_BYTES_PER_VERTEX = 2 * 4 + 4 * 4 + 4 + 4; + +export class PaletteSpriteBatch extends BaseSpriteBatch implements IPaletteSpriteBatch { + palette = true; + defaultPalette: Palette = createPalette(new Uint32Array(0)); + constructor( + gl: WebGLRenderingContext, capacity: number, buffer: ArrayBuffer, vertexBuffer: WebGLBuffer, indexBuffer: WebGLBuffer + ) { + super(gl, capacity, buffer, vertexBuffer, indexBuffer, [ + { name: 'position', size: 2 }, + { name: 'texcoord0', size: 4 }, //, type: gl.UNSIGNED_SHORT }, + { name: 'color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, + { name: 'color1', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, + ]); + } + drawImage( + type: number, color: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } + + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, type, getColorFloat(color, this.globalAlpha), + palette || this.defaultPalette, sx, sy, sw, sh, dx, dy, dw, dh, + ); + this.spritesCount++; + this.tris += 2; + } + drawRect(color: number, x: number, y: number, w: number, h: number) { + if (w !== 0 && h !== 0) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } + + const s = this.rectSprite || defaultRectSprite; + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), + this.defaultPalette, s.x, s.y, s.w, s.h, x, y, w, h, + ); + this.spritesCount++; + this.tris += 2; + } + } + drawSprite(s: Sprite, color: number, palette: Palette | undefined, x: number, y: number) { + if (s.w !== 0 && s.h !== 0) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } + + if (this.hasCrop) { + const crop = this.cropRect; + + let sx = s.x; + let sy = s.y; + let w = s.w; + let h = s.h; + let dx = x + s.ox; + let dy = y + s.oy; + + const cropX = crop.x; // - this.transform[4]; + const cropY = crop.y; // - this.transform[5]; + const shiftLeft = cropX - dx; + const shiftTop = cropY - dy; + const shiftRight = (dx + w) - (cropX + crop.w); + const shiftBottom = (dy + h) - (cropY + crop.h); + + if (shiftLeft > 0) { + sx += shiftLeft; + dx += shiftLeft; + w -= shiftLeft; + } + + if (shiftRight > 0) { + w -= shiftRight; + } + + if (shiftTop > 0) { + sy += shiftTop; + dy += shiftTop; + h -= shiftTop; + } + + if (shiftBottom > 0) { + h -= shiftBottom; + } + + if (w > 0 && h > 0) { + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), + palette || this.defaultPalette, sx, sy, w, h, dx, dy, w, h, + ); + this.spritesCount++; + this.tris += 2; + } + } else { + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), + palette || this.defaultPalette, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h, + ); + this.spritesCount++; + this.tris += 2; + } + } + } +} diff --git a/src/ts/graphics/shaders/basic.glsl b/src/ts/graphics/shaders/basic.glsl new file mode 100644 index 0000000..d967f1c --- /dev/null +++ b/src/ts/graphics/shaders/basic.glsl @@ -0,0 +1,25 @@ +// VERTEX + +attribute vec2 position; +attribute vec2 texcoords; + +uniform mat4 transform; + +varying vec2 textureCoord; + +void main() { + textureCoord = texcoords; + gl_Position = transform * vec4(position, 0, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; + +varying vec2 textureCoord; + +void main() { + gl_FragColor = texture2D(sampler1, textureCoord); +} diff --git a/src/ts/graphics/shaders/light.glsl b/src/ts/graphics/shaders/light.glsl new file mode 100644 index 0000000..25f34ef --- /dev/null +++ b/src/ts/graphics/shaders/light.glsl @@ -0,0 +1,30 @@ +// VERTEX + +attribute vec2 position; +attribute vec2 texcoords; +attribute vec4 vertexColor; + +uniform mat4 transform; +uniform vec4 lighting; + +varying vec2 textureCoord; +varying vec4 vColor; + +void main() { + textureCoord = texcoords; + vColor = vertexColor * lighting; + gl_Position = transform * vec4(position, 0, 1); +} + +// FRAGMENT + +precision mediump float; + +varying vec2 textureCoord; +varying vec4 vColor; + +void main() { + float d = clamp((1.0 - length(textureCoord)) + 0.1, 0.0, 1.0); + float m = d * d * d; + gl_FragColor = vec4(m, m, m, 1) * vColor; +} diff --git a/src/ts/graphics/shaders/palette-depth.glsl b/src/ts/graphics/shaders/palette-depth.glsl new file mode 100644 index 0000000..387b92b --- /dev/null +++ b/src/ts/graphics/shaders/palette-depth.glsl @@ -0,0 +1,39 @@ +// VERTEX + +attribute vec3 position; +attribute vec4 texcoords; +attribute vec4 vertexColor; + +uniform mat4 transform; +uniform vec4 lighting; + +varying vec4 textureCoord; +varying vec4 vColor; + +void main() { + textureCoord = texcoords; + vColor = vertexColor * lighting; + gl_Position = transform * vec4(position, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; // sprite +uniform sampler2D sampler2; // palette +uniform float pixelSize; + +varying vec4 textureCoord; +varying vec4 vColor; + +void main() { + vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); + vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); + vec4 color = vec4(palette.xyz * sprite.y, palette.w) * vColor; + + gl_FragColor = color; + + if (color.a < 0.01) + discard; +} diff --git a/src/ts/graphics/shaders/palette-layers-instanced.glsl b/src/ts/graphics/shaders/palette-layers-instanced.glsl new file mode 100644 index 0000000..f1ee691 --- /dev/null +++ b/src/ts/graphics/shaders/palette-layers-instanced.glsl @@ -0,0 +1,51 @@ +// VERTEX + +attribute vec2 position0; +attribute vec4 position1; +attribute vec4 texcoord0; +attribute vec2 texcoord1; +attribute vec4 vertexColor; +attribute vec4 vertexColor1; + +uniform mat4 transform; +uniform vec4 lighting; + +varying vec2 textureCoord0; +varying vec2 textureCoord1; +varying vec4 vColor; +varying vec4 vColor1; + +void main() { + textureCoord0 = vec2(texcoord0.x + position0.x * texcoord0.z, texcoord0.y + position0.y * texcoord0.w); + textureCoord1 = texcoord1; + vColor = vertexColor * lighting; + vColor1 = vertexColor1; + gl_Position = transform * vec4( + position1.x + position0.x * position1.z, position1.y + position0.y * position1.w, 0, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; // sprite +uniform sampler2D sampler2; // palette +uniform float pixelSize; + +varying vec2 textureCoord0; +varying vec2 textureCoord1; +varying vec4 vColor; +varying vec4 vColor1; + +void main() { + vec4 sprite = texture2D(sampler1, textureCoord0.xy); + + float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); + + vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); + float paletteIndex = dot(mask, sprite); + + vec4 palette = texture2D(sampler2, vec2(textureCoord1.x + paletteIndex * pixelSize, textureCoord1.y)); + + gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; +} diff --git a/src/ts/graphics/shaders/palette-layers.glsl b/src/ts/graphics/shaders/palette-layers.glsl new file mode 100644 index 0000000..0c5fdc1 --- /dev/null +++ b/src/ts/graphics/shaders/palette-layers.glsl @@ -0,0 +1,51 @@ +// VERTEX + +attribute vec2 position; +attribute vec4 texcoords; +attribute vec4 vertexColor; +attribute vec4 vertexColor1; + +uniform mat4 transform; +uniform vec4 lighting; + +varying vec4 textureCoord; +varying vec4 vColor; +varying vec4 vColor1; + +void main() { + textureCoord = texcoords; + // float f = texcoords.z; + // float fr = fract(texcoords.z); + // textureCoord.z = fr; + // textureCoord.w = (f - fr) / 1024.0; + vColor = vertexColor * lighting; + vColor1 = vertexColor1; + gl_Position = transform * vec4(position, 0, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; // sprite +uniform sampler2D sampler2; // palette +uniform float pixelSize; +uniform float textureSize; + +varying vec4 textureCoord; +varying vec4 vColor; +varying vec4 vColor1; + +void main() { + vec4 sprite = texture2D(sampler1, textureCoord.xy / textureSize); + + float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); + + vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); + float paletteIndex = dot(mask, sprite); + + vec2 paletteCoord = textureCoord.zw; + vec4 palette = texture2D(sampler2, vec2(paletteCoord.x + paletteIndex * pixelSize, paletteCoord.y)); + + gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; +} diff --git a/src/ts/graphics/shaders/palette.glsl b/src/ts/graphics/shaders/palette.glsl new file mode 100644 index 0000000..cca0f2a --- /dev/null +++ b/src/ts/graphics/shaders/palette.glsl @@ -0,0 +1,34 @@ +// VERTEX + +attribute vec2 position; +attribute vec4 texcoords; +attribute vec4 vertexColor; + +uniform mat4 transform; +uniform vec4 lighting; + +varying vec4 textureCoord; +varying vec4 vColor; + +void main() { + textureCoord = texcoords; + vColor = vertexColor * lighting; + gl_Position = transform * vec4(position, 0, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; // sprite +uniform sampler2D sampler2; // palette +uniform float pixelSize; + +varying vec4 textureCoord; +varying vec4 vColor; + +void main() { + vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); + vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); + gl_FragColor = vec4(palette.xyz * sprite.y, palette.w) * vColor; +} diff --git a/src/ts/graphics/shaders/sprite.glsl b/src/ts/graphics/shaders/sprite.glsl new file mode 100644 index 0000000..8bb8d66 --- /dev/null +++ b/src/ts/graphics/shaders/sprite.glsl @@ -0,0 +1,31 @@ +// VERTEX + +attribute vec2 position; +attribute vec2 texcoords; +attribute vec4 vertexColor; + +uniform mat4 transform; +uniform vec4 lighting; + +varying vec2 textureCoord; +varying vec4 vColor; + +void main() { + textureCoord = texcoords; + vColor = vertexColor * lighting; + gl_Position = transform * vec4(position, 0, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; +uniform float textureSize; + +varying vec2 textureCoord; +varying vec4 vColor; + +void main() { + gl_FragColor = texture2D(sampler1, textureCoord / textureSize) * vColor; +} diff --git a/src/ts/graphics/shaders/sprite2.glsl b/src/ts/graphics/shaders/sprite2.glsl new file mode 100644 index 0000000..0a90f7f --- /dev/null +++ b/src/ts/graphics/shaders/sprite2.glsl @@ -0,0 +1,29 @@ +// VERTEX + +attribute vec3 position; +attribute vec2 texcoords; +attribute vec4 vertexColor; + +uniform mat4 transform; + +varying vec2 textureCoord; +varying vec4 vColor; + +void main() { + textureCoord = texcoords; + vColor = vertexColor; + gl_Position = transform * vec4(position, 1); +} + +// FRAGMENT + +precision mediump float; + +uniform sampler2D sampler1; + +varying vec2 textureCoord; +varying vec4 vColor; + +void main() { + gl_FragColor = texture2D(sampler1, textureCoord) * vColor; +} diff --git a/src/ts/graphics/spriteBatch.ts b/src/ts/graphics/spriteBatch.ts new file mode 100644 index 0000000..f308d84 --- /dev/null +++ b/src/ts/graphics/spriteBatch.ts @@ -0,0 +1,84 @@ +import { Sprite, SpriteBatch as ISpriteBatch, Matrix2D } from '../common/interfaces'; +import { BaseSpriteBatch, getColorFloat } from './baseSpriteBatch'; + +function vertex( + vertices: Float32Array, _verticesUint32: Uint32Array, index: number, + x: number, y: number, u: number, v: number, c: number, transform: Matrix2D +) { + vertices[index++] = transform[0] * x + transform[2] * y + transform[4]; + vertices[index++] = transform[1] * x + transform[3] * y + transform[5]; + vertices[index++] = u; + vertices[index++] = v; + vertices[index++] = c; +} + +// function colorWithAlpha(color: number, alpha: number) { +// return ((color & 0xffffff00) | (((color & 0xff) * alpha) & 0xff)) >>> 0; +// } + +export class SpriteBatch extends BaseSpriteBatch implements ISpriteBatch { + palette = false; + depth = 0; + constructor( + gl: WebGLRenderingContext, + capacity: number, + buffer: ArrayBuffer, + vertexBuffer: WebGLBuffer, + indexBuffer: WebGLBuffer + ) { + super(gl, capacity, buffer, vertexBuffer, indexBuffer, [ + { name: 'position', size: 2 }, + { name: 'texcoord0', size: 2 }, // , type: gl.UNSIGNED_SHORT }, + { name: 'color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, + ]); + } + drawImage( + color: number, sx: number, sy: number, sw: number, sh: number, + dx: number, dy: number, dw: number, dh: number + ) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } + + const c = getColorFloat(color, this.globalAlpha); + + const x2 = dx + dw; + const y2 = dy + dh; + + const u1 = sx; + const v1 = sy; + const u2 = sx + sw; + const v2 = sy + sh; + + const vertices = this.vertices; + const verticesUint32 = this.verticesUint32; + const transform = this.transform; + + const index = this.index; + + vertex(vertices, verticesUint32, index, dx, dy, u1, v1, c, transform); + vertex(vertices, verticesUint32, index + 5, x2, dy, u2, v1, c, transform); + vertex(vertices, verticesUint32, index + 10, x2, y2, u2, v2, c, transform); + vertex(vertices, verticesUint32, index + 15, dx, y2, u1, v2, c, transform); + + this.index += 20; + this.spritesCount++; + this.tris += 2; + } + drawRect(color: number, x: number, y: number, w: number, h: number) { + if (w && h) { + const rect = this.rectSprite; + + if (rect) { + this.drawImage(color, rect.x, rect.y, rect.w, rect.h, x, y, w, h); + } else { + this.drawImage(color, 0, 0, 1, 1, x, y, w, h); + } + } + } + drawSprite(s: Sprite, color: number, x: number, y: number) { + if (s && s.w && s.h) { + this.drawImage(color, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); + } + } +} diff --git a/src/ts/graphics/spriteBatchUtils.ts b/src/ts/graphics/spriteBatchUtils.ts new file mode 100644 index 0000000..5ac13e5 --- /dev/null +++ b/src/ts/graphics/spriteBatchUtils.ts @@ -0,0 +1,12 @@ +import { colorToFloat, colorFromRGBA } from '../common/color'; + +const type0Shade = colorToFloat(colorFromRGBA(255, 0, 0, 0)); +const type2Shade = colorToFloat(colorFromRGBA(0, 0, 255, 0)); +const type3Shade = colorToFloat(colorFromRGBA(0, 0, 0, 0)); + +const type0 = colorToFloat(colorFromRGBA(255, 0, 0, 255)); +const type1 = colorToFloat(colorFromRGBA(0, 255, 0, 255)); +const type2 = colorToFloat(colorFromRGBA(0, 0, 255, 255)); +const type3 = colorToFloat(colorFromRGBA(0, 0, 0, 255)); + +export const paletteSpriteTypes = [type0Shade, type2Shade, type3Shade, type0, type1, type2, type3]; diff --git a/src/ts/graphics/spriteFont.ts b/src/ts/graphics/spriteFont.ts new file mode 100644 index 0000000..673be5e --- /dev/null +++ b/src/ts/graphics/spriteFont.ts @@ -0,0 +1,276 @@ +import { Rect, Sprite, SpriteBatch, PaletteSpriteBatch, Palette, isPaletteSpriteBatch } from '../common/interfaces'; +import { WHITE } from '../common/colors'; +import { stringToCodesTemp, codesBuffer } from '../common/stringUtils'; +import { createSprite } from '../client/spriteUtils'; + +export const enum HAlign { + Left, + Right, + Center, +} + +export const enum VAlign { + Top, + Bottom, + Middle, +} + +export interface TextOptions { + skipEmotes?: boolean; + colorEmotes?: boolean; + palette?: Palette; + emojiPalette?: Palette; + lineSpacing?: number; + monospace?: boolean; +} + +type Charset = { code: number; sprite: Sprite; }[]; +type Batch = SpriteBatch | PaletteSpriteBatch; + +const SPACE = ' '.charCodeAt(0); +const TAB = '\t'.charCodeAt(0); +const DEFAULT = '?'.charCodeAt(0); +const LINEFEED = '\n'.charCodeAt(0); +const defaultOptions: TextOptions = {}; + +export interface SpriteFont { + lineSpacing: number; + letterSpacing: number; + letterShiftX: number; + letterShiftY: number; + letterShiftWidth: number; + letterShiftHeight: number; + letterHeight: number; + letterHeightReal: number; + letterWidth: number; + chars: Map; + emoji: Map; + defaultChar: Sprite; +} + +export function createSpriteFont(charset: Charset, emojiCharset: Charset, spaceWidth: number): SpriteFont { + const lineSpacing = 2; + const letterSpacing = 1; + const letterShiftX = 0; + const letterShiftY = 0; + const letterShiftWidth = 0; + const letterShiftHeight = 0; + + const chars = new Map(); + const emoji = new Map(); + + charset.filter(c => !!c).forEach(c => chars.set(c.code, c.sprite)); + emojiCharset.filter(e => !!e && !!e.code).forEach(e => emoji.set(e.code, e.sprite)); + + const char0 = chars.get(0)!; + const letterHeight = char0.h; + const letterWidth = char0.w; + const letterHeightReal = char0.h + char0.oy; + chars.set(SPACE, createSprite(0, 0, 0, 0, spaceWidth, char0.h, 0)); + chars.set(TAB, createSprite(0, 0, 0, 0, spaceWidth * 4, char0.h, 0)); + const defaultChar = chars.get(DEFAULT)!; + + return { + lineSpacing, letterSpacing, letterShiftX, letterShiftY, letterShiftWidth, letterShiftHeight, + letterHeight, letterHeightReal, letterWidth, chars, emoji, defaultChar, + }; +} + +function drawChars( + batch: Batch, chars: Uint32Array, length: number, font: SpriteFont, color: number, x: number, y: number, + options: TextOptions +) { + const { lineSpacing = font.lineSpacing, monospace = false } = options; + x = Math.round(x) | 0; + y = Math.round(y) | 0; + let currentX = x; + + for (let i = 0; i < length; i++) { + const code = chars[i]; + + if (code === LINEFEED) { + currentX = x; + y += font.letterHeight + lineSpacing; + } else { + const charWidth = drawChar(batch, font, code, color, currentX, y, options); + currentX += (monospace ? font.letterWidth : charWidth) + font.letterSpacing; + } + } +} + +export function drawText( + batch: Batch, text: string, font: SpriteFont, color: number, x: number, y: number, options = defaultOptions +) { + const length = stringToCodesTemp(text); + drawChars(batch, codesBuffer, length, font, color, x, y, options); +} + +export function drawOutlinedText( + batch: Batch, text: string, font: SpriteFont, color: number, outlineColor: number, x: number, y: number, + options = defaultOptions +) { + const length = stringToCodesTemp(text); + + options.skipEmotes = true; + + drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y - 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y - 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y + 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y + 1, options); + + drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x, y - 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x, y + 1, options); + + options.skipEmotes = false; + + drawChars(batch, codesBuffer, length, font, color, x, y, options); +} + +export function drawTextAligned( + spriteBatch: Batch, text: string, font: SpriteFont, color: number, rect: Rect, halign = HAlign.Left, valign = VAlign.Top, + options: TextOptions = defaultOptions +) { + const length = stringToCodesTemp(text); + const { x, y } = alignChars(font, codesBuffer, length, rect, halign, valign); + drawChars(spriteBatch, codesBuffer, length, font, color, x, y, options); +} + +export function lineBreak(text: string, font: SpriteFont, width: number) { + const lines: string[][] = []; + const spaceWidth = measureChar(font, SPACE) + font.letterSpacing * 2; + + let line: string[] = []; + let lineWidth = 0; + + for (const word of text.split(' ')) { + const { w } = measureText(word, font); + + if (lineWidth) { + lineWidth += spaceWidth; + + if (lineWidth + w > width) { + lines.push(line); + line = []; + lineWidth = 0; + } + } + + line.push(word); + lineWidth += w; + } + + if (line.length) { + lines.push(line); + } + + return lines.map(x => x.join(' ')).join('\n'); +} + +function measureChars(chars: Uint32Array, length: number, font: SpriteFont): { w: number; h: number; } { + let maxW = 0; + let lines = 1; + let w = 0; + + for (let i = 0; i < length; i++) { + const code = chars[i]; + + if (code === LINEFEED) { + maxW = Math.max(maxW, w); + w = 0; + lines++; + } else { + if (w) { + w += font.letterSpacing; + } + w += measureChar(font, code); + } + } + + return { + w: Math.max(maxW, w), + h: lines * font.letterHeight + (lines - 1) * font.lineSpacing + }; +} + +export function measureText(text: string, font: SpriteFont) { + const length = stringToCodesTemp(text); + return measureChars(codesBuffer, length, font); +} + +export function getCharacterSprite(char: string, font: SpriteFont): Sprite | undefined { + let sprite: Sprite | undefined; + let unset = false; + const length = stringToCodesTemp(char); + + for (let i = 0; i < length; i++) { + const code = codesBuffer[i]; + unset = !!sprite; + sprite = font.emoji.get(code) || getChar(font, code); + } + + return unset ? undefined : sprite; +} + +function measureChar(font: SpriteFont, code: number): number { + const sprite = font.emoji.get(code) || getChar(font, code); + return sprite.w + sprite.ox; +} + +function drawChar( + batch: Batch, font: SpriteFont, code: number, color: number, x: number, y: number, options: TextOptions +): number { + const emote = font.emoji.get(code); + const px = x + font.letterShiftX; + const py = y + font.letterShiftY; + + if (emote) { + const skipEmote = !!options.skipEmotes; + const colorEmote = !!options.colorEmotes; + const emoteColor = colorEmote ? color : WHITE; + + if (!skipEmote) { + if (isPaletteSpriteBatch(batch)) { + batch.drawSprite(emote, emoteColor, options.emojiPalette, px, py); + } else { + batch.drawSprite(emote, emoteColor, px, py); + } + } + + return emote.w + emote.ox; + } else { + const c = getChar(font, code); + + if (isPaletteSpriteBatch(batch)) { + batch.drawSprite(c, color, options.palette, px, py); + } else { + batch.drawSprite(c, color, px, py); + } + + return c.w + c.ox + font.letterShiftWidth; + } +} + +function alignChars(font: SpriteFont, chars: Uint32Array, length: number, rect: Rect, halign: HAlign, valign: VAlign) { + let x = rect.x; + let y = rect.y; + + if (halign !== HAlign.Left || valign !== VAlign.Top) { + const size = measureChars(chars, length, font); + + if (halign !== HAlign.Left) { + x += halign === HAlign.Center ? (rect.w - size.w) / 2 : (rect.w - size.w); + } + + if (valign !== VAlign.Top) { + y += valign === VAlign.Middle ? (rect.h - size.h) / 2 : (rect.h - size.h); + } + } + + return { x, y }; +} + +function getChar(font: SpriteFont, code: number): Sprite { + return font.chars.get(code) || font.defaultChar; +} diff --git a/src/ts/graphics/spriteSheetUtils.ts b/src/ts/graphics/spriteSheetUtils.ts new file mode 100644 index 0000000..ced07f7 --- /dev/null +++ b/src/ts/graphics/spriteSheetUtils.ts @@ -0,0 +1,16 @@ +import { SpriteSheet } from '../common/interfaces'; +import { createTexture, disposeTexture } from './webgl/texture2d'; + +export function createTexturesForSpriteSheets(gl: WebGLRenderingContext, sheets: SpriteSheet[], texture = createTexture) { + sheets.forEach(sheet => { + if (sheet.data) { + sheet.texture = texture(gl, sheet.data); + } + }); +} + +export function disposeTexturesForSpriteSheets(gl: WebGLRenderingContext, sheets: SpriteSheet[]) { + sheets.forEach(sheet => { + sheet.texture = disposeTexture(gl, sheet.texture); + }); +} diff --git a/src/ts/graphics/webgl/frameBuffer.ts b/src/ts/graphics/webgl/frameBuffer.ts new file mode 100644 index 0000000..9708c25 --- /dev/null +++ b/src/ts/graphics/webgl/frameBuffer.ts @@ -0,0 +1,48 @@ +import { Texture2D, createEmptyTexture, resizeTexture } from './texture2d'; + +export interface FrameBuffer { + handle: WebGLFramebuffer; + texture: Texture2D; + width: number; + height: number; +} + +export function createFrameBuffer(gl: WebGLRenderingContext, width: number, height: number): FrameBuffer { + const handle = gl.createFramebuffer(); + + if (!handle) { + throw new Error('Failed to create frame buffer'); + } + + const texture = createEmptyTexture(gl, width, height, gl.RGB); + return { handle, texture, width, height }; +} + +export function disposeFrameBuffer(gl: WebGLRenderingContext | undefined, buffer: FrameBuffer | undefined) { + try { + if (gl && buffer) { + gl.deleteFramebuffer(buffer.handle); + gl.deleteTexture(buffer.texture.handle); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } + + return undefined; +} + +export function resizeFrameBuffer(gl: WebGLRenderingContext, frameBuffer: FrameBuffer, width: number, height: number) { + resizeTexture(gl, frameBuffer.texture, width, height); + frameBuffer.width = width; + frameBuffer.height = height; +} + +export function bindFrameBuffer(gl: WebGLRenderingContext, { handle, texture }: FrameBuffer) { + gl.bindFramebuffer(gl.FRAMEBUFFER, handle); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture.handle, 0); +} + +export function unbindFrameBuffer(gl: WebGLRenderingContext) { + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); +} diff --git a/src/ts/graphics/webgl/glFbo.ts b/src/ts/graphics/webgl/glFbo.ts new file mode 100644 index 0000000..c8a4bfe --- /dev/null +++ b/src/ts/graphics/webgl/glFbo.ts @@ -0,0 +1,308 @@ +import { Texture2D, resizeTexture, disposeTexture, createEmptyTexture } from './texture2d'; +import { array } from '../../common/utils'; + +export interface FBOOptions { + /* Upgrade to floating point if available, otherwise fallback to 8bit. (default false) */ + preferFloat?: boolean; + /* Use floating point textures (default false) */ + float?: boolean; + /* The number of color buffers to create (default 1) */ + color?: number; + /* If fbo has a depth buffer (default: true) */ + depth?: boolean; + /* If fbo has a stencil buffer (default: false) */ + stencil?: boolean; +} + +type WebGL = WebGLRenderingContext; +type FBOState = [WebGLFramebuffer | null, WebGLRenderbuffer | null, WebGLTexture | null]; + +let colorAttachmentArrays: number[][] | null = null; + +export interface FrameBuffer { + color: (Texture2D | undefined)[]; + depth: Texture2D | undefined; + handle: WebGLFramebuffer | null; + colorRenderBuffer: WebGLRenderbuffer | null; + depthRenderBuffer: WebGLRenderbuffer | null; + width: number; + height: number; + gl: WebGLRenderingContext; + colorType: number; + useDepth: boolean; + useStencil: boolean; + ext: WEBGL_draw_buffers | null; +} + +export function createFrameBuffer(gl: WebGL, width: number, height: number, options: FBOOptions = {}): FrameBuffer { + const { color = 1, depth = true, stencil = false, float = false, preferFloat = false } = options; + const ext = gl.getExtension('WEBGL_draw_buffers'); + + width = width | 0; + height = height | 0; + + if (!colorAttachmentArrays && ext) { + lazyInitColorAttachments(gl, ext); + } + + const maxFBOSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + + if (maxFBOSize != null && (width < 0 || width > maxFBOSize || height < 0 || height > maxFBOSize)) { + throw new Error('Parameters are too large for FBO'); + } + + const numColors = Math.max(color, 0); + + if (numColors < 0) { + throw new Error('Must specify a nonnegative number of colors'); + } else if (numColors > 1) { + if (!ext) { + throw new Error('Multiple draw buffer extension not supported'); + } else if (numColors > gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL)) { + throw new Error(`Context does not support ${numColors} draw buffers`); + } + } + + let colorType = gl.UNSIGNED_BYTE; + const OES_texture_float = gl.getExtension('OES_texture_float'); + + if (float && numColors > 0) { + if (!OES_texture_float) { + throw new Error('Context does not support floating point textures'); + } + + colorType = gl.FLOAT; + } else if (preferFloat && numColors > 0) { + if (OES_texture_float) { + colorType = gl.FLOAT; + } + } + + const fbo: FrameBuffer = { + gl, width, height, colorType, color: array(numColors, undefined), useDepth: depth, useStencil: stencil, ext, + colorRenderBuffer: null, depth: undefined, depthRenderBuffer: null, handle: null, + }; + + rebuild(fbo); + return fbo; +} + +export function resizeFrameBuffer(fbo: FrameBuffer, w: number, h: number) { + if (fbo.width === w && fbo.height === h) { + return; + } + + const gl = fbo.gl; + const maxFBOSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + + if (maxFBOSize != null && (w < 0 || w > maxFBOSize || h < 0 || h > maxFBOSize)) { + throw new Error(`Can't resize FBO, invalid dimensions`); + } + + fbo.width = w; + fbo.height = h; + + const state = saveFBOState(gl); + + for (const color of fbo.color) { + if (color) { + resizeTexture(gl, color, w, h); + } + } + + if (fbo.colorRenderBuffer) { + gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.colorRenderBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, w, h); + } + + if (fbo.depth) { + resizeTexture(gl, fbo.depth, w, h); + } + + if (fbo.depthRenderBuffer) { + gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.depthRenderBuffer); + + if (fbo.useDepth && fbo.useStencil) { + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h); + } else if (fbo.useDepth) { + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h); + } else if (fbo.useStencil) { + gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, w, h); + } + } + + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.handle); + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + + if (status !== gl.FRAMEBUFFER_COMPLETE) { + disposeFrameBuffer(fbo); + restoreFBOState(gl, state); + throwFBOError(gl, status, `reshape(${w}, ${h})`); + } + + restoreFBOState(gl, state); +} + +export function bindFrameBuffer(fbo: FrameBuffer) { + fbo.gl.bindFramebuffer(fbo.gl.FRAMEBUFFER, fbo.handle); + fbo.gl.viewport(0, 0, fbo.width, fbo.height); +} + +export function disposeFrameBuffer(fbo: FrameBuffer | undefined): undefined { + if (fbo) { + fbo.gl.deleteFramebuffer(fbo.handle); + fbo.handle = null; + fbo.depth = disposeTexture(fbo.gl, fbo.depth); + + if (fbo.depthRenderBuffer) { + fbo.gl.deleteRenderbuffer(fbo.depthRenderBuffer); + fbo.depthRenderBuffer = null; + } + + for (let i = 0; i < fbo.color.length; i++) { + fbo.color[i] = disposeTexture(fbo.gl, fbo.color[i]); + } + + if (fbo.colorRenderBuffer) { + fbo.gl.deleteRenderbuffer(fbo.colorRenderBuffer); + fbo.colorRenderBuffer = null; + } + } + + return undefined; +} + +function rebuild(fbo: FrameBuffer) { + const state = saveFBOState(fbo.gl); + const gl = fbo.gl; + const handle = fbo.handle = gl.createFramebuffer(); + const numColors = fbo.color.length; + const { width, height, ext, useStencil, useDepth, colorType } = fbo; + + gl.bindFramebuffer(gl.FRAMEBUFFER, handle); + + for (let i = 0; i < numColors; ++i) { + fbo.color[i] = initTexture(gl, width, height, colorType, gl.RGBA, gl.COLOR_ATTACHMENT0 + i); + } + + if (numColors === 0) { + fbo.colorRenderBuffer = initRenderBuffer(gl, width, height, gl.RGBA4, gl.COLOR_ATTACHMENT0); + + if (ext) { + ext.drawBuffersWEBGL(colorAttachmentArrays![0]); + } + } else if (numColors > 1) { + if (ext) { + ext.drawBuffersWEBGL(colorAttachmentArrays![numColors]); + } + } + + const WEBGL_depth_texture = gl.getExtension('WEBGL_depth_texture'); + + if (WEBGL_depth_texture) { + if (useStencil) { + fbo.depth = initTexture( + gl, width, height, WEBGL_depth_texture.UNSIGNED_INT_24_8_WEBGL, gl.DEPTH_STENCIL, gl.DEPTH_STENCIL_ATTACHMENT); + } else if (useDepth) { + fbo.depth = initTexture(gl, width, height, gl.UNSIGNED_SHORT, gl.DEPTH_COMPONENT, gl.DEPTH_ATTACHMENT); + } + } else { + if (useDepth && useStencil) { + fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.DEPTH_STENCIL, gl.DEPTH_STENCIL_ATTACHMENT); + } else if (useDepth) { + fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.DEPTH_COMPONENT16, gl.DEPTH_ATTACHMENT); + } else if (useStencil) { + fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.STENCIL_INDEX8, gl.STENCIL_ATTACHMENT); + } + } + + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + + if (status !== gl.FRAMEBUFFER_COMPLETE) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.deleteFramebuffer(fbo.handle); + fbo.handle = null; + fbo.depth = disposeTexture(gl, fbo.depth); + + if (fbo.depthRenderBuffer) { + gl.deleteRenderbuffer(fbo.depthRenderBuffer); + fbo.depthRenderBuffer = null; + } + + for (let i = 0; i < fbo.color.length; i++) { + fbo.color[i] = disposeTexture(gl, fbo.color[i]); + } + + if (fbo.colorRenderBuffer) { + gl.deleteRenderbuffer(fbo.colorRenderBuffer); + fbo.colorRenderBuffer = null; + } + + restoreFBOState(gl, state); + throwFBOError(gl, status); + } + + restoreFBOState(gl, state); +} + +function saveFBOState(gl: WebGL): FBOState { + const fbo = gl.getParameter(gl.FRAMEBUFFER_BINDING); + const rbo = gl.getParameter(gl.RENDERBUFFER_BINDING); + const tex = gl.getParameter(gl.TEXTURE_BINDING_2D); + return [fbo, rbo, tex]; +} + +function restoreFBOState(gl: WebGL, [fbo, rbo, tex]: FBOState) { + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.bindTexture(gl.TEXTURE_2D, tex); +} + +function lazyInitColorAttachments(gl: WebGL, ext: WEBGL_draw_buffers) { + const maxColorAttachments = gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL); + colorAttachmentArrays = []; + + for (let i = 0; i <= maxColorAttachments; ++i) { + const x: number[] = []; + + for (let j = 0; j < i; ++j) { + x.push(gl.COLOR_ATTACHMENT0 + j); + } + + for (let j = i; j < maxColorAttachments; ++j) { + x.push(gl.NONE); + } + + colorAttachmentArrays.push(x); + } +} + +function throwFBOError(gl: WebGL, status: number, message = '') { + switch (status) { + case gl.FRAMEBUFFER_UNSUPPORTED: + throw new Error('Framebuffer unsupported'); + case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + throw new Error('Framebuffer incomplete attachment'); + case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + throw new Error('Framebuffer incomplete dimensions'); + case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + throw new Error('Framebuffer incomplete missing attachment'); + default: + throw new Error(`Framebuffer failed for unspecified reason [${status}] (${message})`); + } +} + +function initTexture(gl: WebGL, width: number, height: number, type: number, format: number, attachment: number) { + const texture = createEmptyTexture(gl, width, height, format, type); + gl.bindTexture(gl.TEXTURE_2D, texture.handle); + gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, texture.handle, 0); + return texture; +} + +function initRenderBuffer(gl: WebGL, width: number, height: number, component: number, attachment: number) { + const result = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, result); + gl.renderbufferStorage(gl.RENDERBUFFER, component, width, height); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, result); + return result; +} diff --git a/src/ts/graphics/webgl/glVao.ts b/src/ts/graphics/webgl/glVao.ts new file mode 100644 index 0000000..0a92240 --- /dev/null +++ b/src/ts/graphics/webgl/glVao.ts @@ -0,0 +1,155 @@ +import { timeStart, timeEnd } from '../../client/timing'; + +export interface VAOAttributes { + name: string; + buffer: WebGLBuffer; + size?: number; + type?: number; + normalized?: boolean; + stride?: number; + offset?: number; + divisor?: number; +} + +export interface VAO { + gl: WebGLRenderingContext; + bind(): void; + unbind(): void; + dispose(): void; + update(attributes: VAOAttributes[], elements: WebGLBuffer, elementsType?: number): void; + draw(mode: number, count: number, offset?: number): void; +} + +interface WebGL extends WebGLRenderingContext { + bindVertexArray?: any; + createVertexArray?: any; + deleteVertexArray?: any; +} + +function extensionShim(gl: WebGL): OES_vertex_array_object { + return { + bindVertexArrayOES: gl.bindVertexArray.bind(gl), + createVertexArrayOES: gl.createVertexArray.bind(gl), + deleteVertexArrayOES: gl.deleteVertexArray.bind(gl), + } as any; +} + +export function createVAO(gl: WebGL, attributes: VAOAttributes[], elements: WebGLBuffer, elementsType?: number): VAO { + const ext = gl.createVertexArray ? extensionShim(gl) : gl.getExtension('OES_vertex_array_object'); + const handle = ext && ext.createVertexArrayOES(); + const vao = (ext && handle) ? new VAONative(gl, ext, handle) : new VAOEmulated(gl); + vao.update(attributes, elements, elementsType); + return vao; +} + +class VAONative implements VAO { + private useElements = false; + private elementsType: number; + private maxAttribs: number; + constructor( + public gl: WebGLRenderingContext, + private ext: OES_vertex_array_object, + public handle: WebGLVertexArrayObjectOES, + ) { + this.elementsType = gl.UNSIGNED_SHORT; + this.maxAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); + } + bind() { + this.ext.bindVertexArrayOES(this.handle); + } + unbind() { + this.ext.bindVertexArrayOES(null); + } + dispose() { + this.ext.deleteVertexArrayOES(this.handle); + } + update(attributes: VAOAttributes[], elements: WebGLBuffer | null, elementsType?: number) { + this.bind(); + bindAttribs(this.gl, elements, attributes, this.maxAttribs); + this.unbind(); + this.useElements = !!elements; + this.elementsType = elementsType || this.gl.UNSIGNED_SHORT; + } + draw(mode: number, count: number, offset = 0) { + TIMING && timeStart('VAONative.draw'); + if (this.useElements) { + this.gl.drawElements(mode, count, this.elementsType, offset); + } else { + this.gl.drawArrays(mode, offset, count); + } + TIMING && timeEnd(); + } +} + +class VAOEmulated implements VAO { + private elements: WebGLBuffer | null = null; + private attributes: VAOAttributes[] | null = null; + private elementsType: number; + private maxAttribs: number; + constructor(public gl: WebGLRenderingContext) { + this.elementsType = gl.UNSIGNED_SHORT; + this.maxAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); + } + bind() { + bindAttribs(this.gl, this.elements, this.attributes, this.maxAttribs); + } + update(attributes: VAOAttributes[], elements: WebGLBuffer | null, elementsType?: number) { + this.elements = elements; + this.attributes = attributes; + this.elementsType = elementsType || this.gl.UNSIGNED_SHORT; + } + dispose() { + } + unbind() { + } + draw(mode: number, count: number, offset = 0) { + TIMING && timeStart('VAOEmulated.draw'); + if (this.elements) { + this.gl.drawElements(mode, count, this.elementsType, offset); + } else { + this.gl.drawArrays(mode, offset, count); + } + TIMING && timeEnd(); + } +} + +function bindAttribs( + gl: WebGLRenderingContext, elements: WebGLBuffer | null, attributes: VAOAttributes[] | null, maxAttribs: number +) { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elements); + + if (attributes) { + if (maxAttribs != null && attributes.length > maxAttribs) { + throw new Error(`Too many vertex attributes ${attributes.length}/${maxAttribs}`); + } + + let i = 0; + + for (; i < attributes.length; ++i) { + const attrib = attributes[i]; + const buffer = attrib.buffer; + const size = attrib.size || 4; + const type = attrib.type || gl.FLOAT; + const normalized = !!attrib.normalized; + const stride = attrib.stride || 0; + const offset = attrib.offset || 0; + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(i); + gl.vertexAttribPointer(i, size, type, normalized, stride, offset); + + if (attrib.divisor !== undefined) { + (gl as any).vertexAttribDivisor(i, attrib.divisor); + } + } + + for (; i < maxAttribs; ++i) { + gl.disableVertexAttribArray(i); + } + } else { + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + for (let i = 0; i < maxAttribs; ++i) { + gl.disableVertexAttribArray(i); + } + } +} diff --git a/src/ts/graphics/webgl/shader.ts b/src/ts/graphics/webgl/shader.ts new file mode 100644 index 0000000..09a29fe --- /dev/null +++ b/src/ts/graphics/webgl/shader.ts @@ -0,0 +1,100 @@ +export interface Shader { + program: WebGLProgram; + vertexShader: WebGLShader; + fragmentShader: WebGLShader; + uniforms: { [key: string]: WebGLUniformLocation; }; +} + +export function createShader(gl: WebGLRenderingContext, source: string | { vertex: string; fragment: string; }): Shader { + if (typeof source === 'string') { + const index = source.indexOf('// FRAGMENT'); + + if (index === -1) { + throw new Error(`Missing fragment shader separator`); + } + + source = { + vertex: source.substring(0, index), + fragment: source.substring(index), + }; + } + + const vertexShader = createWebGLShader(gl, gl.VERTEX_SHADER, source.vertex); + const fragmentShader = createWebGLShader(gl, gl.FRAGMENT_SHADER, source.fragment); + const program = gl.createProgram(); + + if (!program) { + throw new Error('Failed to create shader program'); + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + + const attribs = source.vertex.match(/^attribute [a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)/mg)!; + + for (var i = 0; i < attribs.length; ++i) { + const [, name] = /attribute [a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)/.exec(attribs[i])!; + gl.bindAttribLocation(program, i, name); + } + + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error('Failed to link shader program'); + } + + gl.useProgram(program); + + const uniforms: any = {}; + const samplers: string[] = []; + + for (let i = 0; i < gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); i++) { + const info = gl.getActiveUniform(program, i)!; + uniforms[info.name] = gl.getUniformLocation(program, info.name); + + if (!uniforms[info.name]) { + throw new Error(`Failed to get uniform location (${info.name})`); + } + + if (info.type === gl.SAMPLER_2D) { + samplers.push(info.name); + } + } + + samplers.sort().forEach((name, i) => gl.uniform1i(uniforms[name], i)); + + gl.useProgram(null); + + return { program, vertexShader, fragmentShader, uniforms }; +} + +export function disposeShader(gl: WebGLRenderingContext | undefined, shader: Shader | undefined) { + try { + if (gl && shader) { + gl.deleteProgram(shader.program); + gl.deleteShader(shader.vertexShader); + gl.deleteShader(shader.fragmentShader); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } + + return undefined; +} + +function createWebGLShader(gl: WebGLRenderingContext, type: number, source: string) { + const shader = gl.createShader(type); + + if (!shader) { + throw new Error('Failed to create shader'); + } + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error(gl.getShaderInfoLog(shader) || 'Shader error'); + } + + return shader; +} diff --git a/src/ts/graphics/webgl/texture2d.ts b/src/ts/graphics/webgl/texture2d.ts new file mode 100644 index 0000000..681e022 --- /dev/null +++ b/src/ts/graphics/webgl/texture2d.ts @@ -0,0 +1,98 @@ +type WebGL = WebGLRenderingContext; +type Pixels = ImageBitmap | ImageData | HTMLVideoElement | HTMLImageElement | HTMLCanvasElement; + +export interface Texture2D { + handle: WebGLTexture; + width: number; + height: number; + format: number; + type: number; +} + +export function createEmptyTexture(gl: WebGL, width: number, height: number, format?: number, type?: number): Texture2D { + if (format === undefined) { + format = gl.RGBA; + } + + if (type === undefined) { + type = gl.UNSIGNED_BYTE; + } + + const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null; + + if (maxTextureSize != null && (width < 0 || width > maxTextureSize || height < 0 || height > maxTextureSize)) { + throw new Error('Invalid texture shape'); + } + + if (type === gl.FLOAT && !gl.getExtension('OES_texture_float')) { + throw new Error('Floating point textures not supported on this platform'); + } + + const handle = createTextureHandle(gl); + gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null); + return { handle, width, height, format, type }; +} + +export function createTexture(gl: WebGL, data: Pixels, format?: number, type?: number): Texture2D { + if (format === undefined) { + format = gl.RGBA; + } + + if (type === undefined) { + type = gl.UNSIGNED_BYTE; + } + + const handle = createTextureHandle(gl); + gl.texImage2D(gl.TEXTURE_2D, 0, format, format, type, data); + return { handle, width: data.width, height: data.height, format, type }; +} + +export function disposeTexture(gl: WebGL | undefined, texture: Texture2D | undefined): undefined { + try { + if (gl && texture) { + gl.deleteTexture(texture.handle); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } + + return undefined; +} + +export function bindTexture(gl: WebGL, unit: number, texture: Texture2D | undefined) { + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(gl.TEXTURE_2D, texture ? texture.handle : null); +} + +export function resizeTexture(gl: WebGL, texture: Texture2D, width: number, height: number) { + width = width | 0; + height = height | 0; + + const { format, type } = texture; + const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null; + + if (maxSize != null && (width < 0 || width > maxSize || height < 0 || height > maxSize)) { + throw new Error('Invalid texture size'); + } + + texture.width = width; + texture.height = height; + gl.bindTexture(gl.TEXTURE_2D, texture.handle); + gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null); +} + +function createTextureHandle(gl: WebGL) { + const texture = gl.createTexture(); + + if (!texture) { + throw new Error('Failed to create texture'); + } + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + return texture; +} diff --git a/src/ts/graphics/webgl/vaoAttributes.ts b/src/ts/graphics/webgl/vaoAttributes.ts new file mode 100644 index 0000000..c5d0feb --- /dev/null +++ b/src/ts/graphics/webgl/vaoAttributes.ts @@ -0,0 +1,44 @@ +import { VAOAttributes } from './glVao'; + +export interface VAOAttributeDefinition { + name: string; + size: number; + type?: number; + normalized?: boolean; + divisor?: number; +} + +export function getVAOAttributesSize(gl: WebGLRenderingContext, attributes: VAOAttributeDefinition[]) { + return attributes.reduce((sum, a) => sum + a.size * sizeOfType(gl, a.type), 0); +} + +export function createVAOAttributes( + gl: WebGLRenderingContext, attributes: VAOAttributeDefinition[], buffer: WebGLBuffer +): VAOAttributes[] { + const result: VAOAttributes[] = []; + const stride = getVAOAttributesSize(gl, attributes); + let offset = 0; + + for (const a of attributes) { + result.push({ ...a, stride, buffer, offset }); + offset += a.size * sizeOfType(gl, a.type); + } + + return result; +} + +function sizeOfType(gl: WebGLRenderingContext, type: number | undefined) { + switch (type) { + case gl.BYTE: + case gl.UNSIGNED_BYTE: + return 1; + case gl.SHORT: + case gl.UNSIGNED_SHORT: + return 2; + case gl.FLOAT: + case undefined: + return 4; + default: + throw new Error(`Invalid attribute type (${type})`); + } +} diff --git a/src/ts/graphics/webgl/webglUtils.ts b/src/ts/graphics/webgl/webglUtils.ts new file mode 100644 index 0000000..6dbd7a7 --- /dev/null +++ b/src/ts/graphics/webgl/webglUtils.ts @@ -0,0 +1,67 @@ +import { WEBGL_CREATION_ERROR } from '../../common/errors'; + +export function getRenderTargetSize(width: number, height: number) { + const max = Math.max(width, height); + let pow = 256; + + while (pow < max) { + pow *= 2; + } + + return pow; +} + +export function getWebGLContext(canvas: HTMLCanvasElement): WebGLRenderingContext { + const options: WebGLContextAttributes = { + alpha: false, + premultipliedAlpha: false, + antialias: false, + }; + + const gl = canvas.getContext('webgl2', options) + || canvas.getContext('webgl', options) + || canvas.getContext('experimental-webgl', options); + + if (!gl) { + throw new Error(WEBGL_CREATION_ERROR); + } + + return gl; +} + +export function isWebGL2(gl: WebGLRenderingContext | undefined) { + return !!(gl && gl.MAX_ELEMENT_INDEX); +} + +export function getWebGLError(gl: WebGLRenderingContext) { + const error = gl.getError(); + + switch (error) { + case gl.NO_ERROR: return 'NO_ERROR'; + case gl.INVALID_ENUM: return 'INVALID_ENUM'; + case gl.INVALID_VALUE: return 'INVALID_VALUE'; + case gl.INVALID_OPERATION: return 'INVALID_OPERATION'; + case gl.INVALID_FRAMEBUFFER_OPERATION: return 'INVALID_FRAMEBUFFER_OPERATION'; + case gl.OUT_OF_MEMORY: return 'OUT_OF_MEMORY'; + case gl.CONTEXT_LOST_WEBGL: return 'CONTEXT_LOST_WEBGL'; + default: return `${error}`; + } +} + +export function unbindAllTexturesAndBuffers(gl: WebGLRenderingContext) { + try { + const numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) | 0; + + for (let i = 0; i < numTextureUnits; i++) { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, null); + } + + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } catch (e) { + DEVELOPMENT && console.error(e); + } +} diff --git a/src/ts/lodash.ts b/src/ts/lodash.ts new file mode 100644 index 0000000..1efe199 --- /dev/null +++ b/src/ts/lodash.ts @@ -0,0 +1,102 @@ +type List = ArrayLike; +type PartialDeep = { + [P in keyof T]?: PartialDeep; +}; +interface Dictionary { + [index: string]: T; +} +interface NumericDictionary { + [index: number]: T; +} +type NotVoid = {} | null | undefined; +type ListIteratee = ListIterator | string | [string, any] | PartialDeep; +type ListIterator = (value: T, index: number, collection: List) => TResult; +type ListIterateeCustom = ListIterator | string | [string, any] | PartialDeep; +type ObjectIterator = (value: TObject[keyof TObject], key: string, collection: TObject) => TResult; +type Many = T | T[]; + +interface FlatMap { + ( + collection: List> | Dictionary> | NumericDictionary> | null | undefined + ): T[]; + + ( + collection: object | null | undefined + ): any[]; + + ( + collection: List | null | undefined, + iteratee: ListIterator> + ): TResult[]; + + ( + collection: T | null | undefined, + iteratee: ObjectIterator> + ): TResult[]; + + ( + collection: object | null | undefined, + iteratee: string + ): any[]; + + ( + collection: object | null | undefined, + iteratee: object + ): boolean[]; +} + +type PropertyName = string | number | symbol; +type IterateeShorthand = PropertyName | [PropertyName, any] | PartialDeep; +type ValueIteratee = ((value: T) => NotVoid) | IterateeShorthand; + +export const noop: () => void = require('lodash/noop'); +export const forOwn = require('lodash/forOwn'); +export const escapeRegExp: (string?: string) => string = require('lodash/escapeRegExp'); +export const escape: (string?: string) => string = require('lodash/escape'); +export const sample = require('lodash/sample'); +export const findLastIndex: + (array: List | null | undefined, predicate?: ListIterateeCustom, fromIndex?: number) => number = + require('lodash/findLastIndex'); +export const times: (n: number, iteratee: (num: number) => TResult) => TResult[] = require('lodash/times'); +export const isNumber: (value?: any) => value is number = require('lodash/isNumber'); +export const isString: (value?: any) => value is string = require('lodash/isString'); +export const isBoolean: (value?: any) => value is boolean = require('lodash/isBoolean'); +export const isFunction: (value?: any) => value is (...args: any[]) => any = require('lodash/isFunction'); +export const clamp: (number: number, lower: number, upper: number) => number = require('lodash/clamp'); +export const merge: (object: TObject, source: TSource) => TObject & TSource = require('lodash/merge'); +export const mapValues: (obj: T | null | undefined, callback: ObjectIterator) => + { [P in keyof T]: TResult } = require('lodash/mapValues'); +export const zip: (...arrays: (List | null | undefined)[]) => (T | undefined)[][] = require('lodash/zip'); +export const assignWith = require('lodash/assignWith'); +export const isMatchWith = require('lodash/isMatchWith'); +export const once: any>(func: T) => T = require('lodash/once'); +export const range: (start: number, end?: number, step?: number) => number[] = require('lodash/range'); +export const flatten: (array: List> | null | undefined) => T[] = require('lodash/flatten'); +export const sum = require('lodash/sum'); +export const remove: (array: List, predicate?: ListIteratee) => T[] = require('lodash/remove'); +export const debounce = require('lodash/debounce'); +export const dropRight: (array: List | null | undefined, n?: number) => T[] = require('lodash/dropRight'); +export const padStart: (string?: string, length?: number, chars?: string) => string = require('lodash/padStart'); +export const fill = require('lodash/fill'); +export const includes = require('lodash/includes'); +export const random = require('lodash/random'); +export const max: (collection: List | null | undefined) => T | undefined = require('lodash/max'); +export const uniqueId = require('lodash/uniqueId'); +export const startsWith: (string?: string, target?: string, position?: number) => boolean = require('lodash/startsWith'); +export const repeat: (string?: string, n?: number) => string = require('lodash/repeat'); +export const uniq: (array: List | null | undefined) => T[] = require('lodash/uniq'); +export const flatMap: FlatMap = require('lodash/flatMap'); +export const without: (array: List | null | undefined, ...values: T[]) => T[] = require('lodash/without'); +export const compact: (array: List | null | undefined) => T[] = + require('lodash/compact'); +export const isEqual = require('lodash/isEqual'); +export const dropRightWhile: (array: List | null | undefined, predicate?: ListIteratee) => T[] = + require('lodash/dropRightWhile'); +export const fromPairs = require('lodash/fromPairs'); +export const camelCase = require('lodash/camelCase'); +export const truncate = require('lodash/truncate'); +export const findIndex = require('lodash/findIndex'); +export const last = require('lodash/last'); +export const toPairs: (object?: Dictionary | NumericDictionary) => [string, T][] = require('lodash/toPairs'); +export const groupBy: (collection: List | null | undefined, iteratee?: ValueIteratee) => Dictionary = + require('lodash/groupBy'); diff --git a/src/ts/server/accountUtils.ts b/src/ts/server/accountUtils.ts new file mode 100644 index 0000000..fbf83e1 --- /dev/null +++ b/src/ts/server/accountUtils.ts @@ -0,0 +1,249 @@ +import * as moment from 'moment'; +import { Types } from 'mongoose'; +import { uniq, truncate } from 'lodash'; +import { AccountState, AccountFlags, AuthBase } from '../common/adminInterfaces'; +import { Profile, ModInfo, AccountDataFlags } from '../common/interfaces'; +import { ACCOUNT_NAME_MAX_LENGTH, DAY } from '../common/constants'; +import { fromNow, includes, hasFlag } from '../common/utils'; +import { + isAdmin, getCharacterLimit as getCharacterLimitInternal, getSupporterInviteLimit as getSupporterInviteLimitInternal +} from '../common/accountUtils'; +import { cleanName } from '../client/clientUtils'; +import { + IAccount, IAuth, Account, ID, characterCount as getCharacterCount, findAccount, queryAccount, updateAccount, + FriendRequest, IFriendRequest +} from './db'; +import { assignAuth } from './authUtils'; +import { UserError } from './userError'; +import { system, logger } from './logger'; +import { isActive, supporterLevel, isPastSupporter } from '../common/adminUtils'; +import { IClient } from './serverInterfaces'; +import { providers } from './oauth'; +import { taskQueue } from './utils/taskQueue'; + +export interface SuspiciousCheckers { + isSuspiciousName(name: string): boolean; + isSuspiciousAuth(auth: AuthBase): boolean; +} + +export interface CreateAccountOptions extends SuspiciousCheckers { + userAgent: string | undefined; + browserId: string | undefined; + connectOnly: boolean; + creationLocked: boolean; + canCreateAccounts: boolean; + reportPotentialDuplicates: boolean; + ip: string; + warn: (accountId: string | Types.ObjectId, message: string, desc?: string) => void; +} + +function getBanInfo(value: number | undefined): string | undefined { + return isActive(value) ? (value === -1 ? 'perma' : moment(value).fromNow(true)) : undefined; +} + +export function getModInfo({ accountId, account, country }: IClient): ModInfo { + return { + shadow: getBanInfo(account.shadow), + mute: getBanInfo(account.mute), + note: account.note, + counters: account.counters || {}, + country, + account: `${account.name} [${accountId.substr(-3)}]`, + }; +} + +function findAccountByEmail(emails?: string[]) { + return emails && emails.length ? queryAccount({ emails: { $in: emails } }) : Promise.resolve(undefined); +} + +const availableProviders = providers.filter(a => !a.connectOnly).map(a => a.name).join(', '); + +export const connectOnlySocialError = + `Cannot create new account using this social site, new accounts can only be created using: ${availableProviders}`; + +function createNewAccount(profile: Profile, options: CreateAccountOptions) { + if (!options.canCreateAccounts) { + throw new UserError( + 'Creating accounts is temporarily disabled, try again later'); + } else if (options.connectOnly) { + throw new UserError(connectOnlySocialError); + } else if (options.creationLocked) { + throw new UserError( + 'Could not create account, try again later', { log: `account creation blocked by ACL (${options.ip})` }); + } else if (profile.suspended) { + throw new UserError( + 'Cannot create new account using suspended social site account', { log: 'account creation blocked by suspended' }); + } else { + return new Account(); + } +} + +async function hasDuplicatesAtOrigin(account: IAccount, ip: string) { + const now = Date.now(); + const query = { origins: { $elemMatch: { ip } } }; + const duplicates: IAccount[] = await Account.find(query, '_id ban mute shadow flags name').lean().exec(); + + return duplicates.some(({ _id, ban = 0, mute = 0, shadow = 0, flags = 0, name }) => { + if (_id.toString() === account._id.toString()) + return false; + + if (ban === -1 || ban > now || mute === -1 || mute > now || shadow === -1 || shadow > now) + return true; + + if (hasFlag(flags, AccountFlags.CreatingDuplicates)) + return true; + + if (name === account.name) + return true; + + return false; + }); +} + +const newAccountCheckQueue = taskQueue(); + +async function checkNewAccount(account: IAccount, options: CreateAccountOptions) { + newAccountCheckQueue.push(async () => { + try { + if (options.reportPotentialDuplicates) { + const duplicate = await hasDuplicatesAtOrigin(account, options.ip); + + if (duplicate) { + options.warn(account._id, `Potential duplicate`); + } + } + } catch (e) { + options.warn(account._id, `Error when checking new account`, e.message); + } + }); +} + +export async function findOrCreateAccount(auth: IAuth, profile: Profile, options: CreateAccountOptions): Promise { + let account: IAccount | undefined = undefined; + let isNew = false; + + if (auth.account) { + account = await findAccount(auth.account); + } + + if (!account) { + account = await findAccountByEmail(profile.emails); + } + + if (!account) { + account = createNewAccount(profile, options); + isNew = true; + } + + const assigned = await assignAuth(auth, account); + + if (assigned && options.isSuspiciousAuth(auth)) { + options.warn(account._id, 'Suspicious auth'); + } + + // fix accounts fields + + account.name = account.name || truncate(cleanName(profile.name) || 'Anonymous', { length: ACCOUNT_NAME_MAX_LENGTH }); + account.emails = account.emails || []; + + if (profile.emails.some(e => !includes(account!.emails, e))) { + const suspiciousEmails = profile.emails.filter(options.isSuspiciousName); + + if (suspiciousEmails.length) { + options.warn(account._id, 'Suspicious email', suspiciousEmails.join(', ')); + } + + account.emails = uniq([...account.emails, ...profile.emails]); + } + + account.lastVisit = new Date(); + account.lastUserAgent = options.userAgent || account.lastUserAgent; + account.lastBrowserId = options.browserId || account.lastBrowserId; + + // save account + + if (isNew) { + await account.save(); + system(account._id, `created account "${account.name}"`); + checkNewAccount(account, options); + } else { + const { name, emails, lastVisit, lastUserAgent, lastBrowserId } = account; + await Account.updateOne({ _id: account._id }, { name, emails, lastVisit, lastUserAgent, lastBrowserId }).exec(); + } + + return account; +} + +export function isNew(account: IAccount): boolean { + return !account.createdAt || account.createdAt.getTime() > fromNow(-DAY).getTime(); +} + +export function checkIfNotAdmin(account: IAccount, message: string) { + if (isAdmin(account)) { + logger.warn(`Cannot perform this action on admin user (${message})`); + throw new Error('Cannot perform this action on admin user'); + } else { + return account; + } +} + +export async function updateCharacterCount(account: ID) { + const characterCount = await getCharacterCount(account); + await updateAccount(account, { characterCount }); +} + +export function updateAccountState(account: IAccount, update: (state: AccountState) => void) { + const state = account.state || {}; + update(state); + account.state = state; + updateAccount(account._id, { state: account.state }) + .catch(e => logger.error(e)); +} + +export function getAccountAlertMessage(account: IAccount) { + return (account.alert && account.alert.expires.getTime() > Date.now()) ? account.alert.message : undefined; +} + +async function findFriendRequest(accountId: string, friendId: string): Promise { + const requests = await FriendRequest.find({ + $or: [ + { source: accountId, target: friendId }, + { source: friendId, target: accountId }, + ] + }).exec(); + + return requests[0]; +} + +export async function addFriend(accountId: string, friendId: string) { + const existing = await findFriendRequest(accountId, friendId); + + if (existing) { + throw new Error(`Friend request already exists`); + } + + await FriendRequest.create({ source: accountId, target: friendId }); +} + +export async function removeFriend(accountId: string, friendId: string) { + const existing = await findFriendRequest(accountId, friendId); + + if (existing) { + existing.remove(); + } +} + +export function getCharacterLimit(account: IAccount) { + return getCharacterLimitInternal({ + flags: isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0, + supporter: supporterLevel(account), + }); +} + +export function getSupporterInviteLimit(account: IAccount) { + return getSupporterInviteLimitInternal({ + roles: account.roles, + flags: isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0, + supporter: supporterLevel(account), + }); +} diff --git a/src/ts/server/adminEncoders.ts b/src/ts/server/adminEncoders.ts new file mode 100644 index 0000000..381281d --- /dev/null +++ b/src/ts/server/adminEncoders.ts @@ -0,0 +1,45 @@ +import { dropRightWhile, mapValues } from 'lodash'; +import { BaseValues } from '../common/adminInterfaces'; +import { IEvent } from './db'; + +export interface BaseTimes { + updatedAt: number; + createdAt: number; + lastVisit: number; +} + +export function getBaseDate(items: T[], get: (item: T) => Date): string { + return items.reduce((min, i) => { + const date = get(i); + return date && min.getTime() > date.getTime() ? date : min; + }, new Date(0)).toISOString(); +} + +export function getBaseTimes(base: BaseValues): BaseTimes { + return mapValues(base, (x: string) => (new Date(x)).getTime()) as any; +} + +function trimValues(values: any[]): any[] { + return dropRightWhile(values, x => !x || (Array.isArray(x) && x.length === 0)); +} + +function encodeDate(date: Date | undefined, baseValue: number): number { + return date ? (date.getTime() - baseValue) : 0; +} + +// NOTE: update eventFields +export function encodeEvent(event: IEvent, base: BaseTimes): any[] { + return trimValues([ + event._id, + encodeDate(event.updatedAt, base.updatedAt), + encodeDate(event.createdAt, base.createdAt), + event.type, + event.server, + event.message, + event.desc, + event.count, + event.origin ? { ip: event.origin.ip, country: event.origin.country } : null, + event.account, + event.pony && event.pony.toString(), + ]); +} diff --git a/src/ts/server/adminServerActions.ts b/src/ts/server/adminServerActions.ts new file mode 100644 index 0000000..a2c9c74 --- /dev/null +++ b/src/ts/server/adminServerActions.ts @@ -0,0 +1,598 @@ +import * as moment from 'moment'; +import { Socket, SocketServer, Method, ClientExtensions } from 'ag-sockets'; +import { AccountCounters, Subscription } from '../common/interfaces'; +import { HOUR } from '../common/constants'; +import { fromNow, removeItem, formatDuration } from '../common/utils'; +import { hasRole } from '../common/accountUtils'; +import { + Settings, UpdateOrigin, AccountUpdate, OriginInfo, AccountOrigins, IAdminServerActions, FindPonyQuery, + AuthUpdate, PonyCreator, ServerConfig, GameServerSettings, AccountState, AuthDetails, MergeAccountData, + FindAccountQuery, AdminCache, ClearOrignsOptions, ModelTypes, Stats +} from '../common/adminInterfaces'; +import { ClientAdminActions, ClientUpdate } from '../client/clientAdminActions'; +import { TokenData } from './serverInterfaces'; +import { toAccountData, toPonyObjectAdmin } from './serverUtils'; +import { + IAccount, Account, ICharacter, Character, Auth, checkIfAdmin, ID, findCharacterById, updateAuth, queryAuths, + nullToUndefined, findAccount, findFriendIds +} from './db'; +import { + updateAccountSafe, setRole, addEmail, removeEmail, removeIgnore, updateAccountCounter, timeoutAccount, + addIgnores, setAccountState, findAccounts, getAccountsByEmails, getAccountsByOrigin, + removeAccount, setAccountAlert +} from './api/admin-accounts'; +import { + getAdminState, getChat, kickFromAllServers, notifyUpdate, clearSessions, actionForAllServers, updateOrigin, + getChatForAccounts, shutdownServers, resetUpdating, getUserCounts, getAccountDetails, + EndPoints, getOtherStats, updateGameServerSettings, updateServerSettings, forAllGameServers, +} from './api/admin'; +import { + findPonies, removeCharactersAboveLimit, createCharacter, removeCharacter, assignCharacter, removeAllCharacters +} from './api/ponies'; +import { accountStatus, accountAround, getServer, getLoginServer, RemovedDocument, accountHidden } from './internal'; +import { create } from './reporter'; +import { system } from './logger'; +import { updatePatreonData, updatePastSupporters } from './polling'; +import { AdminService } from './services/adminService'; +import { getOriginStats, clearOrigins, removeAllOrigins, removeOrigins, addOrigin, clearOriginsForAccounts } from './api/origins'; +import { getDuplicateEntries, getAllDuplicatesQuickInfo, getAllDuplicatesWithInfo } from './api/duplicates'; +import { splitAccounts } from './api/merge'; +import { removeAuth, assignAuth } from './api/admin-auths'; +import { getLastPatreonData } from './patreon'; +import { removeFriend, addFriend } from './accountUtils'; + +@Socket({ + id: 'admin', + path: '/ws-admin', + connectionTokens: true, + tokenLifetime: 12 * HOUR, + perMessageDeflate: false, +}) +export class AdminServerActions implements IAdminServerActions, SocketServer { + private account: IAccount; + private cache: AdminCache = {}; + private subscriptions = new Map(); + constructor( + private client: ClientAdminActions & ClientExtensions, + private server: ServerConfig, + private settings: Settings, + private adminService: AdminService, + private endPoints: EndPoints, + private removedDocument: RemovedDocument, + ) { + this.account = (client.tokenData as TokenData).account; + this.subscriptions.set('account:deleted', this.adminService.accountDeleted.subscribe(account => { + if (this.cache.findAccounts) { + removeItem(this.cache.findAccounts.result, account); + } + })); + } + disconnected() { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + clearTimeout(this.updatesTimeout); + } + // other + @Method({ promise: true }) + async getSignedAccount() { + return toAccountData(this.account); + } + @Method({ promise: true }) + async getCounts() { + const characters = await Promise.resolve(Character.estimatedDocumentCount() as any); + + return { + characters, + accounts: this.adminService.accounts.items.length, + auths: this.adminService.auths.items.length, + origins: this.adminService.origins.items.length, + }; + } + @Method({ promise: true }) + async getOtherStats() { + return await getOtherStats(this.adminService); + } + // subscribing + private updates: ClientUpdate[] = []; + private updatesTimeout: any; + private pushUpdate(type: ModelTypes, id: string, update: any) { + const index = this.updates.findIndex(u => u.type === type && u.id === id); + + if (index !== -1) { + this.updates[index].update = update; + } else { + this.updates.push({ type, id, update }); + } + + if (!this.updatesTimeout) { + this.updatesTimeout = setTimeout(() => { + this.client.updates(this.updates); + this.updates = []; + this.updatesTimeout = 0; + }, 50); + } + } + @Method() + subscribe(type: ModelTypes, id: string) { + const key = `${type}:${id}`; + + if (this.subscriptions.has(key)) + return; + + if (type === 'ponies') { + if (!this.adminService.ponies.get(id)) { + this.adminService.ponies.fetch({ _id: id }); + } + } + + let subscription: Subscription | undefined; + + if (type === 'accountAuths') { + subscription = this.adminService.subscribeToAccountAuths(id, update => this.pushUpdate(type, id, update)); + } else if (type === 'accountOrigins') { + subscription = this.adminService.subscribeToAccountOrigins(id, update => this.pushUpdate(type, id, update)); + } else if (type === 'accountPonies') { + subscription = this.adminService.subscribeToAccountPonies(id, update => this.pushUpdate(type, id, update)); + } else if (type in this.adminService) { + subscription = this.adminService[type].subscribe(id, (id, update) => this.pushUpdate(type, id, update)); + } else { + throw new Error(`Invalid model type (${type})`); + } + + if (subscription) { + this.subscriptions.set(key, subscription); + } + } + @Method() + unsubscribe(type: ModelTypes, id: string) { + const key = `${type}:${id}`; + const subscription = this.subscriptions.get(key); + + if (subscription) { + subscription.unsubscribe(); + this.subscriptions.delete(key); + + if (type === 'ponies') { + this.adminService.cleanupPony(id); + } else if (type === 'accountPonies') { + this.adminService.cleanupPoniesList(id); + } + } + } + // state + @Method({ promise: true }) + async clearSessions(accountId: string) { + await clearSessions(accountId); + } + @Method({ promise: true }) + async getState() { + return getAdminState(); + } + @Method({ promise: true }) + async updateSettings(update: Partial) { + await updateServerSettings(this.settings, update); + } + @Method({ promise: true }) + async updateGameServerSettings(serverId: string, update: Partial) { + await updateGameServerSettings(this.settings, serverId, update); + } + @Method({ promise: true }) + async fetchServerStats(serverId: string) { + const server = getServer(serverId); + return await server.api.stats(); + } + @Method({ promise: true }) + async fetchServerStatsTable(serverId: string, stats: Stats) { + const server = getServer(serverId); + return await server.api.statsTable(stats); + } + @Method({ promise: true }) + async report(accountId: string) { + create(this.server, accountId).info(`Reported by ${this.account.name}`); + } + @Method({ promise: true }) + async notifyUpdate(server: string) { + await notifyUpdate(server); + } + @Method({ promise: true }) + async shutdownServers(server: string) { + await shutdownServers(server, true); + } + @Method({ promise: true }) + async resetUpdating(server: string) { + await resetUpdating(server); + } + @Method({ promise: true }) + async action(action: string, accountId: string) { + await actionForAllServers(action, accountId); + } + @Method({ promise: true }) + async kick(accountId: string) { + await kickFromAllServers(accountId); + } + @Method({ promise: true }) + async kickAll(serverId: string) { + const server = getServer(serverId); + await server.api.kickAll(); + } + @Method({ promise: true }) + async getChat(search: string, date: string, caseInsensitive: boolean) { + return await getChat(search, date, caseInsensitive); + } + @Method({ promise: true }) + async getChatForAccounts(accountIds: string[], date: string) { + return await getChatForAccounts(accountIds, date); + } + @Method({ promise: true }) + async getRequestStats() { + const loginServer = getLoginServer('login'); + const requests = await loginServer.api.loginServerStats(); + const userCounts = await getUserCounts(); + return { requests, userCounts }; + } + // live (remove) + @Method({ promise: true }) + async get(endPoint: keyof EndPoints, id: string) { + // console.log('get', endPoint); + // return this.adminService[endPoint].get(id); + return await this.endPoints[endPoint].get(id) as any; + } + @Method({ promise: true }) + async getAll(endPoint: keyof EndPoints, timestamp?: string) { + return await this.endPoints[endPoint].getAll(timestamp) as any; + } + @Method({ promise: true }) + async assignAccount(endPoint: keyof EndPoints, id: string, account: string) { + return await this.endPoints[endPoint].assignAccount(id, account) as any; + } + @Method({ promise: true }) + async removeItem(endPoint: keyof EndPoints, id: string) { + return await this.endPoints[endPoint].removeItem(id) as any; + } + // events + @Method({ promise: true }) + async removeEvent(id: string) { + await this.adminService.events.remove(id); + await this.endPoints.events.removedItem(id); + } + // origins + @Method({ promise: true }) + async updateOrigin(origin: UpdateOrigin) { + await updateOrigin(origin); + } + @Method({ promise: true }) + async getOriginStats() { + return await getOriginStats(this.adminService.accounts.items); + } + @Method({ promise: true }) + async clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions) { + if (!this.adminService.loaded) { + throw new Error('Not loaded yet'); + } else { + await clearOrigins(this.adminService, count, andHigher, options); + } + } + @Method({ promise: true }) + async clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions) { + if (!this.adminService.loaded) { + throw new Error('Not loaded yet'); + } else { + await clearOriginsForAccounts(this.adminService, accounts, options); + } + } + // ponies + @Method({ promise: true }) + async getPony(id: string) { + return await Character.findById(id).exec().then(nullToUndefined) as any; + } + @Method({ promise: true }) + async getPonyInfo(id: string) { + const character = await findCharacterById(id); + return toPonyObjectAdmin(character); + } + @Method({ promise: true }) + async getPoniesCreators(account: string) { + const items: ICharacter[] = await Character.find({ account }, '_id name creator').lean().exec(); + return items.map(({ _id, name, creator }) => { _id, name, creator }); + } + @Method({ promise: true }) + async getPoniesForAccount(account: string) { + return await Character.find({ account }).lean().exec(); + } + @Method({ promise: true }) + async getDetailsForAccount(accountId: string) { + return await getAccountDetails(accountId); + } + @Method({ promise: true }) + async findPonies(query: FindPonyQuery, page: number, _skipTotalCount: boolean) { + return await findPonies(query, page); + } + @Method({ promise: true }) + async createPony(account: string, name: string, info: string) { + await createCharacter(account, name, info); + system(account, `Created character (${name}) ${this.by()}`); + } + @Method({ promise: true }) + async assignPony(ponyId: string, accountId: string) { + await assignCharacter(ponyId, accountId); + } + @Method({ promise: true }) + async removePony(id: string) { + await removeCharacter(this.adminService, id); + } + @Method({ promise: true }) + async removePoniesAboveLimit(account: string) { + await removeCharactersAboveLimit(this.removedDocument, account); + system(account, `Removed ponies above limit ${this.by()}`); + } + @Method({ promise: true }) + async removeAllPonies(account: string) { + await removeAllCharacters(this.removedDocument, account); + system(account, `Removed all ponies ${this.by()}`); + } + // auths + @Method({ promise: true }) + async getAuth(id: string) { + return await Auth.findById(id).exec().then(nullToUndefined) as any; + } + @Method({ promise: true }) + async getAuthsForAccount(accountId: string) { + return await Auth.find({ account: accountId }).exec() as any; + } + @Method({ promise: true }) + async fetchAuthDetails(auths: string[]): Promise { + const items = await queryAuths({ _id: { $in: auths } }, '_id lastUsed'); + + return items.map(a => ({ + id: a._id.toString(), + lastUsed: a.lastUsed && a.lastUsed.toISOString(), + })); + } + @Method({ promise: true }) + async updateAuth(id: string, update: AuthUpdate) { + const auth = await Auth.findById(id).exec(); + await throwOnAdmin(auth && auth.account); + await updateAuth(id, update); + } + @Method({ promise: true }) + async assignAuth(authId: string, accountId: string) { + await assignAuth(authId, accountId); + } + @Method({ promise: true }) + async removeAuth(id: string) { + await removeAuth(this.adminService, id); + } + // accounts + @Method({ promise: true }) + async getAccount(id: string) { + return await findAccount(id) as any; + } + @Method({ promise: true }) + async findAccounts(query: FindAccountQuery) { + return await findAccounts(this.cache, this.adminService, query); + } + @Method({ promise: true }) + async createAccount(name: string): Promise { + const account = await Account.create({ name }); + system(account._id.toString(), `Created account ${this.by()}`); + return account._id.toString(); + } + @Method({ promise: true }) + async getAccountsByEmails(emails: string[]) { + return getAccountsByEmails(this.adminService, emails); + } + @Method({ promise: true }) + async getAccountsByOrigin(ip: string) { + return getAccountsByOrigin(this.adminService, ip); + } + @Method({ promise: true }) + async setName(accountId: string, name: string) { + await updateAccountSafe(accountId, { name }); + system(accountId, `Updated name (${name}) ${this.by()}`); + } + @Method({ promise: true }) + async setAge(accountId: string, age: number) { + if (age === -1) { + await Account.updateOne({ _id: accountId }, { $unset: { birthyear: 1 } }).exec(); + } else { + const birthyear = (new Date()).getFullYear() - age; + await Account.updateOne({ _id: accountId }, { birthyear }).exec(); + } + + system(accountId, `Updated birth year (${age}) ${this.by()}`); + } + @Method({ promise: true }) + async setRole(accountId: string, role: string, set: boolean) { + await setRole(accountId, role, set, hasRole(this.account, 'superadmin')); + system(accountId, `${set ? 'Added' : 'Removed'} role (${role}) ${this.by()}`); + } + @Method({ promise: true }) + async updateAccount(accountId: string, update: AccountUpdate, message?: string) { + await updateAccountSafe(accountId, update); + + if (message) { + system(accountId, `${message} ${this.by()}`); + } + } + @Method({ promise: true }) + async timeoutAccount(accountId: string, timeout: number) { + const message = timeout ? `Timed out ${moment.duration(timeout).humanize()}` : 'Unmuted'; + system(accountId, `${message} ${this.by()}`); + await timeoutAccount(accountId, fromNow(timeout | 0)); + } + @Method({ promise: true }) + async updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number) { + await updateAccountCounter(accountId, name, value); + } + @Method({ promise: true }) + async mergeAccounts(accountId: string, withId: string) { + const server = getLoginServer('login'); + await server.api.mergeAccounts(accountId, withId, this.by(), hasRole(this.account, 'superadmin'), true); + } + @Method({ promise: true }) + async unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { + await splitAccounts(accountId, mergeId, split, keep, this.by()); + } + @Method({ promise: true }) + async getAccountStatus(accountId: string) { + return await accountStatus(accountId); + } + @Method({ promise: true }) + async getAccountAround(accountId: string) { + return await accountAround(accountId); + } + @Method({ promise: true }) + async getAccountHidden(accountId: string) { + return await accountHidden(accountId); + } + @Method({ promise: true }) + async getAccountFriends(accountId: string) { + return findFriendIds(accountId); + } + @Method({ promise: true }) + async removeAccount(accountId: string) { + await removeAccount(this.adminService, accountId); + } + @Method({ promise: true }) + async setAlert(accountId: string, message: string, expiresIn: number) { + await setAccountAlert(accountId, message, fromNow(expiresIn)); + system(accountId, `${expiresIn ? 'Set' : 'Unset'} alert for ${formatDuration(expiresIn)} "${message}" ${this.by()}`); + } + // accounts - origins + @Method({ promise: true }) + async removeAllOrigins(accountId: string) { + await removeAllOrigins(this.adminService, accountId); + } + @Method({ promise: true }) + async removeOriginsForAccount(accountId: string, ips: string[]) { + await removeOrigins(this.adminService, accountId, ips); + } + @Method({ promise: true }) + async removeOriginsForAccounts(origins: AccountOrigins[]) { + await Promise.all(origins.map(o => removeOrigins(this.adminService, o.accountId, o.ips))); + } + @Method({ promise: true }) + async addOriginToAccount(accountId: string, origin: OriginInfo) { + if (origin && origin.ip && origin.country) { + await addOrigin(accountId, origin); + system(accountId, `Added origin (${JSON.stringify(origin)}) ${this.by()}`); + } else { + throw new Error('Invalid origin'); + } + } + // accounts - emails + @Method({ promise: true }) + async addEmail(accountId: string, email: string) { + await addEmail(accountId, email); + system(accountId, `Added email (${email}) ${this.by()}`); + } + @Method({ promise: true }) + async removeEmail(accountId: string, email: string) { + await removeEmail(accountId, email); + system(accountId, `Removed email (${email}) ${this.by()}`); + } + // accounts - ignores + @Method({ promise: true }) + async removeIgnore(accountId: string, ignore: string) { + await removeIgnore(accountId, ignore); + } + @Method({ promise: true }) + async addIgnores(accountId: string, ignores: string[]) { + await addIgnores(accountId, ignores); + } + @Method({ promise: true }) + async setAccountState(accountId: string, state: AccountState) { + await setAccountState(accountId, state); + } + @Method({ promise: true }) + async getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }> { + const [ignores, ignoredBy] = await Promise.all([ + Account + .find({ ignores: { $in: [accountId] } }, '_id') + .lean() + .exec() + .then((accounts: IAccount[]) => accounts.map(a => a._id.toString())), + Account + .findOne({ _id: accountId }, 'ignores') + .lean() + .exec() + .then((account: IAccount | null) => account && account.ignores || []), + ]); + + return { ignores, ignoredBy }; + } + // accounts - friends + @Method({ promise: true }) + async removeFriend(accountId: string, friendId: string) { + await removeFriend(accountId, friendId); + } + @Method({ promise: true }) + async addFriend(accountId: string, friendId: string) { + await addFriend(accountId, friendId); + } + // accounts - duplicates + @Method({ promise: true }) + async getAllDuplicatesQuickInfo(accountId: string) { + return await getAllDuplicatesQuickInfo(this.adminService, accountId); + } + @Method({ promise: true }) + async getAllDuplicates(accountId: string) { + return await getAllDuplicatesWithInfo(this.adminService, accountId); + } + @Method({ promise: true }) + async getDuplicateEntries(force: boolean) { + return await getDuplicateEntries(this.adminService.accounts.items, force); + } + // patreon + @Method({ promise: true }) + async updatePatreon() { + await updatePatreonData(this.server, this.settings); + } + @Method({ promise: true }) + async resetSupporter(accountId: string) { + await Account.updateOne( + { _id: accountId }, + { $unset: { supporter: 1, patreon: 1, supporterDeclinedSince: 1 } }).exec(); + } + @Method({ promise: true }) + async getLastPatreonData() { + const data = await getLastPatreonData(); + + // if (data) { + // data.pledges.forEach(pledge => { + // const auth = this.adminService.auths.items.find(a => a.openId === pledge.user); + // pledge.account = auth && auth.account; + // }); + // } + + return data; + } + @Method({ promise: true }) + async updatePastSupporters() { + await updatePastSupporters(); + } + // other + @Method({ promise: true }) + async getTimings(serverId: string) { + const server = getServer(serverId); + return server.api.getTimings(); + } + @Method({ promise: true }) + async teleportTo(accountId: string) { + const adminAccountId = this.account._id.toString(); + await forAllGameServers(server => server.api.teleportTo(adminAccountId, accountId)); + } + // utils + private by() { + return `by ${this.account.name} [${this.account._id}]`; + } +} + +async function throwOnAdmin(account: ID | null | undefined) { + if (account) { + const isAdmin = await checkIfAdmin(account); + + if (isAdmin) { + throw new Error('Cannot change for admin user'); + } + } +} diff --git a/src/ts/server/api/account.ts b/src/ts/server/api/account.ts new file mode 100644 index 0000000..62bf50e --- /dev/null +++ b/src/ts/server/api/account.ts @@ -0,0 +1,258 @@ +import * as moment from 'moment'; +import { ACCOUNT_NAME_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, MIN_CHATLOG_RANGE, MAX_CHATLOG_RANGE, HIDES_PER_PAGE } from '../../common/constants'; +import { + UpdateAccountData, AccountSettings, AccountData, ModAction, EntitiesEditorInfo, EntityNameTypes +} from '../../common/interfaces'; +import { isMod } from '../../common/accountUtils'; +import { cleanName } from '../../client/clientUtils'; +import { toAccountData, toPonyObject, toSocialSite, toPonyObjectFields, toSocialSiteFields } from '../serverUtils'; +import { + IAccount, FindAccountSafe, FindAuth, FindAuths, FindCharacters, CountAuths, Auth, + ID, findFriends, Account, HideRequest +} from '../db'; +import { UserError } from '../userError'; +import * as entities from '../../common/entities'; +import { includes, clamp, createValidBirthDate, parseISODate, formatISODate } from '../../common/utils'; +import { getAccountAlertMessage } from '../accountUtils'; +import { getAge } from '../../common/adminUtils'; + +export type GetAccountCharacters = ReturnType; +export type UpdateAccount = ReturnType; +export type UpdateSettings = ReturnType; +export type RemoveSite = ReturnType; +export type GetAccountData = ReturnType; + +type LogAccount = (accountId: ID, message: string) => void; + +const exclude = [ + 'getEntityType', 'getEntityTypeName', 'createAnEntity', 'createEntity', 'pony', + 'createBaseEntity', 'getEntityTypesAndNames', +]; + +export const allEntities = Object.keys(entities) + .filter(key => typeof (entities as any)[key] === 'function') + .filter(key => !includes(exclude, key)); + +function getEntityNamesToTypes() { + const result: EntityNameTypes[] = []; + + for (const name of allEntities) { + const created = (entities as any)[name](0, 0); + const array = Array.isArray(created) ? created : [created]; + const types = array.map(e => e.type); + result.push({ name, types }); + } + + return result; +} + +const entitiesInfo: EntitiesEditorInfo = { + typeToName: entities.getEntityTypesAndNames(), + nameToTypes: getEntityNamesToTypes(), + names: allEntities, +}; + +const actions = [ + { name: 'kick', action: ModAction.Kick }, + { name: 'ban', action: ModAction.Ban }, +]; + +export const modCheck = { xcz: { vdw: { qwe: { mnb: {} } } }, actions }; + +function fixUpdateAccountData(update: UpdateAccountData | undefined) { + const fixed: UpdateAccountData = {} as any; + + if (update) { + if (update.name && typeof update.name === 'string') { + const name = cleanName(update.name); + + if (name.length >= ACCOUNT_NAME_MIN_LENGTH && name.length <= ACCOUNT_NAME_MAX_LENGTH) { + fixed.name = name; + } + } + + if (update.birthdate && typeof update.birthdate === 'string') { + fixed.birthdate = update.birthdate; + } + } + + return fixed; +} + +function fixAccountSettings(settings: AccountSettings | undefined) { + const fixed: Partial = {}; + + if (settings) { + if (settings.defaultServer !== undefined) { + fixed.defaultServer = `${settings.defaultServer}`; + } + + if (settings.filterCyrillic !== undefined) { + fixed.filterCyrillic = !!settings.filterCyrillic; + } + + if (settings.filterSwearWords !== undefined) { + fixed.filterSwearWords = !!settings.filterSwearWords; + } + + if (settings.ignorePartyInvites !== undefined) { + fixed.ignorePartyInvites = !!settings.ignorePartyInvites; + } + + if (settings.ignoreFriendInvites !== undefined) { + fixed.ignoreFriendInvites = !!settings.ignoreFriendInvites; + } + + if (settings.ignorePublicChat !== undefined) { + fixed.ignorePublicChat = !!settings.ignorePublicChat; + } + + if (settings.ignoreNonFriendWhispers !== undefined) { + fixed.ignoreNonFriendWhispers = !!settings.ignoreNonFriendWhispers; + } + + if (settings.chatlogOpacity !== undefined) { + fixed.chatlogOpacity = clamp(settings.chatlogOpacity | 0, 0, 100); + } + + if (settings.chatlogRange !== undefined) { + fixed.chatlogRange = clamp(settings.chatlogRange | 0, MIN_CHATLOG_RANGE, MAX_CHATLOG_RANGE); + } + + if (settings.seeThroughObjects !== undefined) { + fixed.seeThroughObjects = !!settings.seeThroughObjects; + } + + if (settings.filterWords !== undefined) { + fixed.filterWords = `${settings.filterWords}`; + } + + if (settings.actions !== undefined) { + fixed.actions = `${settings.actions}`; + } + + if (settings.hidden !== undefined) { + fixed.hidden = !!settings.hidden; + } + } + + return fixed; +} + +export const createGetAccountData = + (findCharacters: FindCharacters, findAuths: FindAuths) => + async (account: IAccount): Promise => { + const [ponies, auths] = await Promise.all([ + findCharacters(account._id, toPonyObjectFields), + findAuths(account._id, toSocialSiteFields), + ]); + + const data = toAccountData(account); + data.ponies = ponies.map(toPonyObject) as any; + data.sites = auths.map(toSocialSite); + data.alert = getAccountAlertMessage(account); + + if (isMod(account)) { + data.check = modCheck; + } + + if (BETA && isMod(account)) { + data.editor = entitiesInfo; + } + + return data; + }; + +export async function getFriends(account: IAccount) { + return findFriends(account._id, true); +} + +export async function getHides(account: IAccount, page: number) { + const hideRequests = await HideRequest + .find({ source: account._id }, '_id name date') + .sort({ date: -1 }) + .skip(page * HIDES_PER_PAGE) + .limit(HIDES_PER_PAGE) + .lean() + .exec(); + + return hideRequests.map((f: any) => ({ + id: f._id.toString(), + name: f.name, + date: moment(f.date).fromNow(), + })); +} + +export const createGetAccountCharacters = + (findCharacters: FindCharacters) => + async (account: IAccount) => { + const ponies = await findCharacters(account._id); + return ponies.map(toPonyObject); + }; + +export const createUpdateAccount = + (findAccount: FindAccountSafe, log: LogAccount) => + async (account: IAccount, update: UpdateAccountData | undefined) => { + const a = await findAccount(account._id); + + if (update) { + const fixed = fixUpdateAccountData(update); + const up: Partial = {}; + + if (fixed.name && fixed.name !== a.name) { + up.name = fixed.name; + log(a._id, `Renamed "${a.name}" => "${fixed.name}"`); + } + + if (fixed.birthdate) { + const { day, month, year } = parseISODate(fixed.birthdate); + const date = createValidBirthDate(day, month, year); + + if ((date && a.birthdate && date.getTime() !== a.birthdate.getTime()) || !a.birthdate) { + up.birthdate = date; + + const from = a.birthdate ? `${formatISODate(a.birthdate)} (${getAge(a.birthdate)}yo)` : `undefined`; + const to = up.birthdate ? `${formatISODate(up.birthdate)} (${getAge(up.birthdate)}yo)` : `undefined`; + log(a._id, `Changed birthdate ${from} => ${to}`); + } + } + + Object.assign(a, up); + await Account.updateOne({ _id: a._id }, up).exec(); + } + + return toAccountData(a); + }; + +export const createUpdateSettings = + (findAccount: FindAccountSafe) => + async (account: IAccount, settings: AccountSettings | undefined) => { + const a = await findAccount(account._id); + account.settings = a.settings = { ...a.settings, ...fixAccountSettings(settings) }; + await Account.updateOne({ _id: account._id }, { settings: account.settings }).exec(); + return toAccountData(a); + }; + +export const createRemoveSite = + (findAuth: FindAuth, countAllVisibleAuths: CountAuths, log: LogAccount) => + async (account: IAccount, siteId: unknown) => { + const [auth, auths] = await Promise.all([ + siteId && typeof siteId === 'string' ? findAuth(siteId, account._id) : Promise.resolve(undefined), + countAllVisibleAuths(account._id), + ]); + + if (!auth || auth.disabled) { + throw new UserError('Social account not found'); + } else if (auths === 1) { + throw new UserError('Cannot remove your only one social account'); + } else { + log(account._id, `removed auth: ${auth.name} [${auth._id}]`); + await Auth.updateOne({ _id: auth._id }, { disabled: true }).exec(); + } + + return {}; + }; + +export async function removeHide(account: IAccount, hideId: string) { + await HideRequest.deleteOne({ source: account._id, _id: hideId }).exec(); +} diff --git a/src/ts/server/api/admin-accounts.ts b/src/ts/server/api/admin-accounts.ts new file mode 100644 index 0000000..d4133f6 --- /dev/null +++ b/src/ts/server/api/admin-accounts.ts @@ -0,0 +1,195 @@ +import { noop, uniq, fromPairs } from 'lodash'; +import { AccountCounters, Dict } from '../../common/interfaces'; +import { + AccountUpdate, AccountState, FindAccountQuery, FindAccountResult, AdminCache, AdminCacheEntry, + Account as AccountInterface +} from '../../common/adminInterfaces'; +import { checkIfNotAdmin } from '../accountUtils'; +import { updateAccount, findAccountSafe, IAccount, MongoUpdate, findAccount, Account } from '../db'; +import { accountChanged } from '../internal'; +import { MINUTE } from '../../common/constants'; +import { fromNow, includes, arraysEqual } from '../../common/utils'; +import { isMuted, isShadowed, filterAccounts, emailName } from '../../common/adminUtils'; +import { AdminService } from '../services/adminService'; + +const banLogLimit = 10; + +async function updateAccountAndNotify(accountId: string, update: MongoUpdate) { + await updateAccount(accountId, update); + await accountChanged(accountId); +} + +export async function timeoutAccount(accountId: string, timeout: Date, message?: string) { + const account = await findAccountSafe(accountId, 'roles mute shadow'); + + checkIfNotAdmin(account, `timeout account: ${accountId}`); + + const update: MongoUpdate = { mute: timeout.getTime() }; + + if (!isMuted(account) && !isShadowed(account)) { + update.$inc = { 'counters.timeouts': 1 }; + + if (message) { + update.$push = { + banLog: { + $each: [{ message, date: new Date() }], + $slice: -banLogLimit, + }, + }; + } + } + + await updateAccountAndNotify(accountId, update); +} + +function incrementAccountCounter(accountId: string, counter: keyof AccountCounters) { + return updateAccountAndNotify(accountId, { $inc: { [`counters.${counter}`]: 1 } }); +} + +export function updateAccountCounter(accountId: string, counter: keyof AccountCounters, value: number) { + return updateAccountAndNotify(accountId, { [`counters.${counter}`]: value }); +} + +let logSwearing: () => void = noop; +let logSpamming: () => void = noop; + +export function initLogSwearingAndSpamming(swearing: typeof logSwearing, spamming: typeof logSpamming) { + logSwearing = swearing; + logSpamming = spamming; +} + +export function reportSwearingAccount(accountId: string) { + logSwearing(); + return incrementAccountCounter(accountId, 'swears'); +} + +export function reportSpammingAccount(accountId: string) { + logSpamming(); + return incrementAccountCounter(accountId, 'spam'); +} + +export async function reportInviteLimitAccount(accountId: string) { + await incrementAccountCounter(accountId, 'inviteLimit'); + const account = await findAccountSafe(accountId, 'counters'); + return account.counters && account.counters.inviteLimit || 0; +} + +export async function reportFriendLimitAccount(accountId: string) { + await incrementAccountCounter(accountId, 'friendLimit'); + const account = await findAccountSafe(accountId, 'counters'); + return account.counters && account.counters.friendLimit || 0; +} + +export async function updateAccountSafe(accountId: string, update: AccountUpdate) { + const keys = Object.keys(update); + const allowAdmin = arraysEqual(keys, ['note']) || arraysEqual(keys, ['supporter']); + const account = await findAccountSafe(accountId); + + if (!allowAdmin) { + checkIfNotAdmin(account, `update account: ${accountId}`); + } + + const isNoteUpdate = 'note' in update && update.note !== account.note; + const accountUpdate = isNoteUpdate ? { ...update, noteUpdated: new Date() } : update; + + await updateAccountAndNotify(accountId, accountUpdate); +} + +export async function setRole(accountId: string, role: string, set: boolean, isSuperadmin: boolean) { + if (role === 'superadmin' || !isSuperadmin) { + throw new Error('Not allowed'); + } else { + await updateAccountAndNotify(accountId, set ? { $addToSet: { roles: [role] } } : { $pull: { roles: role } }); + } +} + +export function addEmail(accountId: string, email: string) { + return updateAccount(accountId, { $addToSet: { emails: [email.trim().toLowerCase()] } }); +} + +export function removeEmail(accountId: string, email: string) { + return updateAccount(accountId, { $pull: { emails: email } }); +} + +export function removeIgnore(accountId: string, ignoredAccount: string) { + return updateAccountAndNotify(ignoredAccount, { $pull: { ignores: accountId } }); +} + +export function addIgnores(accountId: string, ignores: string[]) { + return updateAccountAndNotify(accountId, { $addToSet: { ignores } }); +} + +export function setAccountState(accountId: string, state: AccountState) { + return updateAccountAndNotify(accountId, { state }); +} + +function isValidCache(entry: AdminCacheEntry, query: string, duration: number): boolean { + return entry.query === query && entry.timestamp.getTime() > fromNow(-duration).getTime(); +} + +export async function findAccounts( + cache: AdminCache, service: AdminService, { search, showOnly, not, page, itemsPerPage, force }: FindAccountQuery +): Promise { + const query = JSON.stringify({ search, showOnly, not }); + let found: AccountInterface[]; + + if (force) { + cache.findAccounts = undefined; + } + + if (cache.findAccounts && isValidCache(cache.findAccounts, query, 5 * MINUTE)) { + found = cache.findAccounts.result; + } else { + found = filterAccounts(service.accounts.items, search, showOnly, not); + cache.findAccounts = { + query, + result: found, + timestamp: new Date(), + }; + } + + const start = page * itemsPerPage; + + return { + accounts: found.slice(start, start + itemsPerPage).map(a => a._id), + page, + totalItems: found.length, + }; +} + +export function getAccountsByEmail(service: AdminService, email: string) { + email = email.toLowerCase(); + const name = emailName(email); + const accounts = service.getAccountsByEmailName(name) || []; + return accounts.filter(a => includes(a.emails, email)).map(a => a._id); +} + +export function getAccountsByEmails(service: AdminService, emails: string[]): Dict { + const pairs = uniq(emails) + .map(email => [email, getAccountsByEmail(service, email)] as [string, string[]]) + .filter(([_, accounts]) => accounts.length > 0); + + return fromPairs(pairs); +} + +export function getAccountsByOrigin(service: AdminService, ip: string): string[] { + const origin = service.origins.get(ip); + return origin && origin.accounts && origin.accounts.map(a => a._id) || []; +} + +export async function removeAccount(service: AdminService, accountId: string) { + const account = await findAccount(accountId); + + if (account) { + checkIfNotAdmin(account, `remove account: ${accountId}`); + + await account.remove(); + + service.removedItem('accounts', accountId); + } +} + +export async function setAccountAlert(accountId: string, message: string, expires: Date) { + const update = message ? { alert: { message, expires } } : { $unset: { alert: 1 } }; + await Account.updateOne({ _id: accountId }, update).exec(); +} diff --git a/src/ts/server/api/admin-auths.ts b/src/ts/server/api/admin-auths.ts new file mode 100644 index 0000000..592efcd --- /dev/null +++ b/src/ts/server/api/admin-auths.ts @@ -0,0 +1,36 @@ +import { AdminService } from '../services/adminService'; +import { Auth, Account } from '../db'; +import { checkIfNotAdmin } from '../accountUtils'; + +export async function assignAuth(authId: string, accountId: string) { + const auth = await Auth.findById(authId).exec(); + + if (!auth) + return; + + const [src, dest] = await Promise.all([ + Account.findById(auth.account).exec(), + Account.findById(accountId).exec(), + ]); + + src && checkIfNotAdmin(src, `assign auth from ${src._id}`); + dest && checkIfNotAdmin(dest, `assign auth to ${dest._id}`); + + await Auth.updateOne({ _id: authId }, { account: accountId }).exec(); +} + +export async function removeAuth(service: AdminService, authId: string) { + const auth = await Auth.findById(authId).exec(); + + if (!auth) + return; + + if (auth.account) { + const account = await Account.findById(auth.account).exec(); + account && checkIfNotAdmin(account, `remove auth from ${account._id}`); + } + + await Auth.deleteOne({ _id: authId }).exec(); + + service.auths.removed(authId); +} diff --git a/src/ts/server/api/admin.ts b/src/ts/server/api/admin.ts new file mode 100644 index 0000000..088926d --- /dev/null +++ b/src/ts/server/api/admin.ts @@ -0,0 +1,298 @@ +import * as fs from 'fs'; +import * as moment from 'moment'; +import { + AdminState, eventFields, BaseValues, UpdateOrigin, UserCountStats, AccountDetails, SupporterInvite, + InternalGameServerState, InternalLoginServerState, OtherStats, Settings, GameServerSettings +} from '../../common/adminInterfaces'; +import { execAsync } from '../serverUtils'; +import { + IAccount, Account, Origin, Event, iterate, ISession, Session, SupporterInvite as DBSupporterInvite, + findAccount, ISupporterInvite, ID +} from '../db'; +import { servers, serverStatus, loginServers } from '../internal'; +import { encodeEvent, BaseTimes, getBaseDate, getBaseTimes } from '../adminEncoders'; +import { createLiveEndPoint, LiveEndPoint } from '../liveEndPoint'; +import * as paths from '../paths'; +import { logger } from '../logger'; +import { AdminService } from '../services/adminService'; +import { loadSettings, saveSettings } from '../settings'; +import { flatten } from '../../common/utils'; + +function encodeItems(items: T[], base: BaseValues, encode: (items: T, base: BaseTimes) => any[]): any[][] { + const baseValues = getBaseTimes(base); + return items.map(i => encode(i, baseValues)); +} + +const events = createLiveEndPoint({ + model: Event, + fields: eventFields, + encode(items, base) { + base.createdAt = getBaseDate(items, i => i.createdAt!); + base.updatedAt = getBaseDate(items, i => i.updatedAt); + return encodeItems(items, base, encodeEvent); + }, +}); + +export interface EndPoints { + events: LiveEndPoint; +} + +export function createEndPoints(): EndPoints { + return { events }; +} + +export function getAdminState(): AdminState { + return { + status: serverStatus, + loginServers: loginServers.map(s => s.state), + gameServers: servers.map(s => s.state), + }; +} + +async function forAllLoginServers( + action: (server: InternalLoginServerState) => any, filter = (_: InternalLoginServerState) => true +) { + await Promise.all(loginServers.filter(filter).map(action)); +} + +export async function forAllGameServers( + action: (server: InternalGameServerState) => any, filter = (_: InternalGameServerState) => true +) { + const liveServers = servers.filter(s => !s.state.dead); + await Promise.all(liveServers.filter(filter).map(action)); +} + +export function actionForAllServers(action: string, accountId: string) { + return forAllGameServers(s => s.api.action(action, accountId)); +} + +export function kickFromAllServers(accountId: string) { + return forAllGameServers(s => s.api.kick(accountId, undefined)); +} + +export function kickFromAllServersByCharacter(characterId: string) { + return forAllGameServers(s => s.api.kick(undefined, characterId)); +} + +function createFilter(id: string) { + return (server: { id: string; }) => id === '*' || server.id === id; +} + +export async function notifyUpdate(server: string) { + await Promise.all([ + forAllLoginServers(s => s.api.updateLiveSettings({ updating: true }), createFilter(server)), + forAllGameServers(s => s.api.notifyUpdate(), createFilter(server)), + ]); +} + +export function shutdownServers(server: string, value: boolean) { + return forAllGameServers(s => s.api.shutdownServer(value), createFilter(server)); +} + +export async function resetUpdating(server: string) { + await Promise.all([ + forAllLoginServers(s => s.api.updateLiveSettings({ updating: false }), createFilter(server)), + forAllGameServers(s => s.api.cancelUpdate(), createFilter(server)), + shutdownServers(server, false), + ]); +} + +export async function reloadSettingsOnAllServers() { + await Promise.all([ + forAllLoginServers(s => s.api.reloadSettings()), + forAllGameServers(s => s.api.reloadSettings()), + ]); +} + +export async function getChat(search: string, date: string, caseInsensitive: boolean) { + const query = search + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\./g, '\\.') + .replace(/\*/g, '\\*') + .replace(/\$/g, '\\*') + .replace(/\^/g, '\\*'); + const flags = caseInsensitive ? '-i -E ' : '-E '; + const options = { maxBuffer: 1 * 1024 * 1024 }; // 1MB + + async function fetchChatlog(lines: number) { + const logFile = paths.pathTo('logs', `info.${moment(date).format('YYYYMMDD')}.log`); + const { stdout } = await execAsync(`grep ${flags}"${query}" "${logFile}" | tail -n ${lines}`, options); + return stdout; + } + + try { + if (!search) { + return ''; + } else if (date === 'all') { + const { stdout } = await execAsync(`for f in ${paths.pathTo('logs')}/*.log; do ` + + `echo "$f" | grep -o '[0-9]*';` + + `cat "$f" | grep ${flags}"${query}";` + + `done`, options); + return stdout; + } else { + let lines = 8192; + let more = ''; + + do { + try { + const log = await fetchChatlog(lines); + return more + log; + } catch (e) { + if (e.message !== 'stdout maxBuffer exceeded') { + throw e; + } + } + + lines /= 2; + more = '... more lines ...\n'; + } while (lines > 1); + + return ''; + } + } catch (e) { + console.error('Failed to fetch chatlog: ', e); + return ''; + } +} + +export async function getChatForAccounts(accountIds: string[], date: string) { + const accounts: Partial[] = await Account.find({ _id: { $in: accountIds } }, '_id merges').lean().exec(); + const map = new Map(); + const ids = flatten(accounts.map(a => [a._id.toString(), ...(a.merges || []).map(a => a.id)])); + + for (const a of accounts) { + const index = accountIds.indexOf(a._id.toString()); + map.set(a._id.toString(), index ? `[${index}]` : ``); + + (a.merges || []).forEach(({ id }) => { + map.set(id, index ? `[${index}:merged]` : `[merged]`); + }); + } + + const chat = await getChat(ids.join('|'), date, false); + const fixed = chat.replace(/^([0-9:]+) \[([a-f0-9]{24})\]/gmu, (_, date, id) => + `${date} ${map.has(id) ? map.get(id) : `[${id}]`}`); + + return fixed; +} + +export async function clearSessions(accountId: string) { + const clearIds: string[] = []; + + await iterate(Session.find({ session: { $exists: true } }).lean() as any, session => { + try { + if (session.session) { + const data = JSON.parse(session.session); + const user = data && data.passport && data.passport.user; + + if (user === accountId) { + clearIds.push(session._id); + } + } + } catch (e) { + logger.error('Error when claring session', e, session._id, session.session); + } + }); + + await Session.deleteOne({ _id: { $in: clearIds } }).exec(); +} + +export async function updateOrigin(update: UpdateOrigin) { + await Origin.updateOne({ ip: update.ip }, update, { upsert: true }).exec(); +} + +export async function getUserCounts(): Promise { + const statsFile = paths.pathTo('settings', `user-counts.log`); + + try { + const content = await fs.readFileAsync(statsFile, 'utf8'); + const lines = content.trim().split(/\n/); + return lines.map(line => JSON.parse(line)); + } catch { + return []; + } +} + +function convertInvite(invite: ISupporterInvite): SupporterInvite { + return { + _id: invite._id.toString(), + name: invite.name, + info: invite.info, + source: invite.source.toHexString(), + target: invite.target.toHexString(), + active: invite.active, + updatedAt: invite.updatedAt, + createdAt: invite.createdAt, + }; +} + +export async function getAccountDetails(accountId: ID): Promise { + const [account, invitesReceived, invitesSent] = await Promise.all([ + findAccount(accountId, 'merges supporterLog banLog state'), + DBSupporterInvite.find({ target: accountId }).exec(), + DBSupporterInvite.find({ source: accountId }).exec(), + ]); + + return account ? { + merges: account.merges || [], + banLog: account.banLog || [], + supporterLog: account.supporterLog || [], + invitesReceived: invitesReceived.map(convertInvite), + invitesSent: invitesSent.map(convertInvite), + state: account.state || {}, + } : { + merges: [], + banLog: [], + supporterLog: [], + invitesReceived: [], + invitesSent: [], + state: {}, + }; +} + +export async function getOtherStats(service: AdminService): Promise { + let totalIgnores = 0; + let authsWithEmptyAccount = 0; + let authsWithMissingAccount = 0; + + for (const account of service.accounts.items) { + totalIgnores += account.ignoresCount!; + } + + for (const auth of service.auths.items) { + if (!auth.account) { + authsWithEmptyAccount++; + } else if (!service.accounts.get(auth.account)) { + authsWithMissingAccount++; + } + } + + return { + totalIgnores, + authsWithEmptyAccount, + authsWithMissingAccount, + }; +} + +export async function updateServerSettings(currentSettings: Settings, update: Partial) { + const settings = await Promise.resolve(loadSettings()); + Object.assign(currentSettings, settings, update); + + await saveSettings(currentSettings); + await reloadSettingsOnAllServers(); +} + +export async function updateGameServerSettings( + currentSettings: Settings, serverId: string, update: Partial +) { + const settings = await Promise.resolve(loadSettings()); + Object.assign(currentSettings, settings); + + const serverSettings = currentSettings.servers[serverId] = currentSettings.servers[serverId] || {}; + Object.assign(serverSettings, update); + + await saveSettings(currentSettings); + await reloadSettingsOnAllServers(); +} + diff --git a/src/ts/server/api/duplicates.ts b/src/ts/server/api/duplicates.ts new file mode 100644 index 0000000..9608f99 --- /dev/null +++ b/src/ts/server/api/duplicates.ts @@ -0,0 +1,187 @@ +import { groupBy, uniq, compact } from 'lodash'; +import { + duplicatesCollector, emailName, compareDuplicates, getIdsFromNote, createDuplicateResult +} from '../../common/adminUtils'; +import { Account, Origin, DuplicatesInfo, DuplicateResult } from '../../common/adminInterfaces'; +import { HOUR } from '../../common/constants'; +import { DuplicateInfoEntry } from '../../common/adminInterfaces'; +import { Account as DBAccount, Character, ICharacter, IAccount } from '../db'; +import { removeItem, includes, flatten } from '../../common/utils'; +import { AdminService } from '../services/adminService'; + +// get duplicate entries + +const DUPLICATE_TIMEOUT = 1 * HOUR; + +let duplicateEntries: string[] | undefined = undefined; +let duplicateTimestamp = 0; + +export async function getDuplicateEntries(accounts: Account[], force: boolean) { + if (!duplicateEntries || force || (Date.now() - duplicateTimestamp) > DUPLICATE_TIMEOUT) { + duplicateTimestamp = Date.now(); + duplicateEntries = [ + ...getDuplicateEmails(accounts), + // ...getDuplicateAuths(accounts), + ]; + } + + return duplicateEntries; +} + +export function getDuplicateEmails(accounts: Account[]) { + const duplicates: string[] = []; + const collect = duplicatesCollector(duplicates); + accounts.forEach(a => a.emails !== undefined && a.emails.forEach(collect)); + return duplicates; +} + +export function getDuplicateAuths(accounts: Account[]) { + const duplicates: string[] = []; + const collect = duplicatesCollector(duplicates); + accounts.forEach(a => a.auths !== undefined && a.auths.forEach(a => a.url && collect(a.url))); + return duplicates; +} + +// get duplicate info + +export async function getDuplicateInfo(accountId: string, otherAccounts: string[]): Promise { + const ids = [accountId, ...otherAccounts]; + const [chars, accounts] = await Promise.all([ + Character.find({ account: ids }, 'account name').lean().exec() as Promise, + DBAccount.find({ _id: ids }, '_id lastUserAgent').lean().exec() as Promise, + ]); + + chars.forEach(c => c.name = c.name.toLowerCase()); + const groups = groupBy(chars, c => c.account); + const account = accounts.find(a => a._id.toString() === accountId); + const userAgent = account && account.lastUserAgent || ''; + + return otherAccounts.map(id => { + const account = accounts.find(a => a._id.toString() === id); + + return { + account: id, + userAgent: (account && userAgent && account.lastUserAgent === userAgent) ? userAgent : '', + ponies: getDuplicateNames(groups[accountId], groups[id]), + }; + }); +} + +function getDuplicateNames(mine: ICharacter[] = [], others: ICharacter[] = []): string[] { + return uniq(mine.filter(a => others.some(b => a.name === b.name)).map(c => c.name)); +} + +// get all duplicates + +export async function getAllDuplicatesQuickInfo(service: AdminService, accountId: string): Promise { + const duplicates = await getAllDuplicates(service, accountId); + + return { + generatedAt: Date.now(), + count: duplicates.length, + name: duplicates.some(d => !!d.name), + emails: duplicates.some(d => !!d.emails), + browserId: duplicates.some(d => !!d.browserId), + perma: duplicates.some(d => !!d.perma), + }; +} + +export async function getAllDuplicatesWithInfo(service: AdminService, accountId: string) { + const duplicates = await getAllDuplicates(service, accountId); + const accountIds = duplicates.map(x => x.account); + const duplicatesInfo = await getDuplicateInfo(accountId, accountIds); + + duplicatesInfo.forEach(({ account, ponies, userAgent }) => { + const duplicate = duplicates.find(d => d.account === account); + + if (duplicate) { + duplicate.ponies = ponies; + duplicate.userAgent = userAgent; + } + }); + + duplicates.forEach(d => d.ponies = d.ponies || []); + + return duplicates; +} + +async function getAllDuplicates(service: AdminService, accountId: string): Promise { + const account = service.accounts.get(accountId); + + if (!account) { + return []; + } else { + return uniq([ + ...getDuplicatesByNote(service, account), + ...getDuplicatesByEmail(service, account), + ...getDuplicatesByBrowserId(service, account), + ...getDuplicates(account), + ]) + .filter(a => a !== account) + .map(a => createDuplicateResult(a, account)) + .sort(compareDuplicates) + .slice(0, 50); + } +} + +function getDuplicates(account: Account) { + const accounts: Account[] = []; + const origins: Origin[] = []; + removeItem(accounts, account); + collectDuplicates(accounts, origins, account, 3); + return accounts; +} + +function getDuplicatesByNote(service: AdminService, account: Account) { + const linkedTo = compact(getIdsFromNote(account.note).map(id => service.accounts.get(id))); + const linkedFrom = service.getAccountsByNoteRef(account._id); + return uniqueOtherAccounts([...linkedTo, ...linkedFrom], account); +} + +function getDuplicatesByEmail(service: AdminService, account: Account) { + const accounts = (account.emails || []) + .map(emailName) + .map(name => service.getAccountsByEmailName(name)); + return uniqueOtherAccounts(flatten(accounts), account); +} + +function getDuplicatesByBrowserId(service: AdminService, account: Account) { + const browserId = account.lastBrowserId; + const accounts = browserId && service.getAccountsByBrowserId(browserId) || []; + return uniqueOtherAccounts(accounts, account); +} + +function uniqueOtherAccounts(accounts: Account[], exclude: Account) { + return uniq(accounts.filter(a => a !== exclude)); +} + +function collectDuplicates(accounts: Account[], origins: Origin[], account: Account, level: number) { + if (level > 0 && !includes(accounts, account)) { + accounts.push(account); + account.originsRefs!.forEach(o => { + if (!includes(origins, o.origin)) { + origins.push(o.origin); + + if (o.origin.accounts) { + o.origin.accounts.forEach(a => collectDuplicates(accounts, origins, a, level - 1)); + } + } + }); + } +} + +// unused + +export function getDuplicateEmailNames(accounts: Account[]) { + const set = new Set(); + + return uniq(accounts.reduce((duplicates, a) => { + if (a.emails !== undefined && a.emails.length > 0) { + const names = a.emails.map(e => e.replace(/@.+$/, '')); + duplicates.push(...names.filter(name => set.has(name))); + names.forEach(name => set.add(name)); + } + + return duplicates; + }, [])); +} diff --git a/src/ts/server/api/game.ts b/src/ts/server/api/game.ts new file mode 100644 index 0000000..e2f4b9c --- /dev/null +++ b/src/ts/server/api/game.ts @@ -0,0 +1,94 @@ +import { parse } from 'url'; +import { VERSION_ERROR } from '../../common/errors'; +import { IAccount, IOriginInfo, ICharacter, FindCharacter, HasActiveSupporterInvites } from '../db'; +import { UserError } from '../userError'; +import { InternalGameServerState } from '../../common/adminInterfaces'; +import { JoinResponse } from '../../common/interfaces'; +import { meetsRequirement } from '../../common/accountUtils'; +import { supporterLevel } from '../../common/adminUtils'; +import { isServerOffline } from '../serverUtils'; +import { getAccountAlertMessage } from '../accountUtils'; + +export interface Config { + version: string; + host: string; + debug: boolean; + local: boolean; +} + +export type FindServer = (id: string) => InternalGameServerState | undefined; +export type Join = (server: InternalGameServerState, account: IAccount, pony: ICharacter) => Promise; +export type AddOrigin = (account: IAccount, origin: IOriginInfo) => Promise; +export type JoinGame = ( + account: IAccount, characterId: string, serverId: string, clientVersion: string, url: string, alert: unknown, + origin: IOriginInfo +) => Promise; + +export const createJoinGame = + ( + findServer: FindServer, { version, host, debug, local }: Config, findCharacter: FindCharacter, join: Join, + addOrigin: AddOrigin, hasInvites: HasActiveSupporterInvites + ): JoinGame => { + const waiting = new Map(); + + return async (account, characterId, serverId, clientVersion, url, hasAlert, origin) => { + const accountId = account._id.toString(); + + try { + const [server, supporterInvited] = await Promise.all([ + findServer(serverId), + hasInvites(account._id), + ]); + + if (clientVersion !== version) + throw new UserError(VERSION_ERROR); + + if (parse(url).host !== parse(host).host && !debug && !local) + throw new UserError('Invalid data', { message: 'Invalid host', desc: url }); + + if (!server) + throw new UserError('Invalid data'); + + if (isServerOffline(server)) + throw new UserError('Server is offline'); + + if (server.state.settings.blockJoining) + throw new UserError('Cannot join to the server'); + + if (!meetsRequirement({ roles: account.roles, supporter: supporterLevel(account), supporterInvited }, server.state.require)) + throw new UserError('Server is restricted'); + + if (!characterId || typeof characterId !== 'string') + throw new UserError('Invalid data', { message: 'Invalid pony ID', desc: `"${characterId}"` }); + + const req = waiting.get(accountId); + const time = new Date(); + + if (req) { + throw new UserError('Already waiting for join request'); + } + + const alert = getAccountAlertMessage(account); + + if (alert && !hasAlert) { + return { alert }; + } + + waiting.set(accountId, { characterId, time }); + + const character = await findCharacter(characterId, account._id); + + if (!character) { + throw new UserError('Character does not exist', { + desc: `(join) (account: ${accountId} pony: ${characterId})` + }); + } + + await addOrigin(account, origin); + const token = await join(server, account, character!); + return { token }; + } finally { + waiting.delete(accountId); + } + }; + }; diff --git a/src/ts/server/api/internal-admin.ts b/src/ts/server/api/internal-admin.ts new file mode 100644 index 0000000..064d541 --- /dev/null +++ b/src/ts/server/api/internal-admin.ts @@ -0,0 +1,14 @@ +import { EndPoints } from './admin'; +import { AdminService } from '../services/adminService'; + +export class InternalAdminApi { + constructor(private adminService: AdminService, private endPoints: EndPoints) { + } + removedDocument(model: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) { + if (model in this.endPoints) { + (this.endPoints as any)[model].removedItem(id); + } + this.adminService.removedItem(model, id); + return Promise.resolve(); + } +} diff --git a/src/ts/server/api/internal-common.ts b/src/ts/server/api/internal-common.ts new file mode 100644 index 0000000..9c357d9 --- /dev/null +++ b/src/ts/server/api/internal-common.ts @@ -0,0 +1,4 @@ +export const createReloadSettings = + (reloadSettings: () => Promise) => + async () => + await reloadSettings(); diff --git a/src/ts/server/api/internal-login.ts b/src/ts/server/api/internal-login.ts new file mode 100644 index 0000000..02bcdc4 --- /dev/null +++ b/src/ts/server/api/internal-login.ts @@ -0,0 +1,62 @@ +import { Settings, LoginServerStatus, InternalLoginApi, ServerLiveSettings } from '../../common/adminInterfaces'; +import { StatsTracker } from '../stats'; +import { RemovedDocument } from '../internal'; +import { createReloadSettings } from './internal-common'; +import { mergeAccounts } from './merge'; + +export function createLoginServerStatus(settings: Settings, live: ServerLiveSettings): LoginServerStatus { + return { + canCreateAccounts: !!settings.canCreateAccounts, + isPageOffline: !!settings.isPageOffline, + blockWebView: !!settings.blockWebView, + reportPotentialDuplicates: !!settings.reportPotentialDuplicates, + autoMergeDuplicates: !!settings.autoMergeDuplicates, + + suspiciousNames: settings.suspiciousNames || '', + suspiciousAuths: settings.suspiciousAuths || '', + suspiciousPonies: settings.suspiciousPonies || '', + suspiciousMessages: settings.suspiciousMessages || '', + suspiciousSafeMessages: settings.suspiciousSafeMessages || '', + suspiciousSafeWholeMessages: settings.suspiciousSafeWholeMessages || '', + suspiciousSafeInstantMessages: settings.suspiciousSafeInstantMessages || '', + suspiciousSafeInstantWholeMessages: settings.suspiciousSafeInstantWholeMessages || '', + + updating: live.updating, + dead: false, + }; +} + +export const createLoginServerState = + (settings: Settings, live: ServerLiveSettings) => + async () => + createLoginServerStatus(settings, live); + +export const createLoginServerStats = + (statsTracker: StatsTracker) => + async () => + statsTracker.getStats(); + +export const createUpdateLiveSettings = + (liveSettings: ServerLiveSettings) => + async (update: Partial) => { + Object.assign(liveSettings, update); + }; + +export const createInternalLoginApi = + ( + settings: Settings, live: ServerLiveSettings, statsTracker: StatsTracker, + reloadSettings: () => Promise, removedDocument: RemovedDocument, + ): InternalLoginApi => + ({ + reloadSettings: createReloadSettings(reloadSettings), + state: createLoginServerState(settings, live), + loginServerStats: createLoginServerStats(statsTracker), + updateLiveSettings: createUpdateLiveSettings(live), + mergeAccounts: async (id, withId, reason, allowAdmin, creatingDuplicates) => { + if (live.shutdown) { + throw new Error(`Cannot merge while server is shutdown`); + } + + await mergeAccounts(id, withId, reason, removedDocument, allowAdmin, creatingDuplicates); + }, + }); diff --git a/src/ts/server/api/internal.ts b/src/ts/server/api/internal.ts new file mode 100644 index 0000000..150c3a3 --- /dev/null +++ b/src/ts/server/api/internal.ts @@ -0,0 +1,333 @@ +import { + GameServerState, AccountStatus, ServerConfig, InternalApi, ServerLiveSettings, Stats, StatsTable +} from '../../common/adminInterfaces'; +import { isBanned, supporterLevel } from '../../common/adminUtils'; +import { IClient, TokenService, GetSettings } from '../serverInterfaces'; +import { + ICharacter, IAccount, FindAccountSafe, FindAuth, FindCharacterSafe, findAccountSafe, findCharacterSafe, + findAuth, HasActiveSupporterInvites, hasActiveSupporterInvites +} from '../db'; +import { World, findClientsAroundAccountId, findClientByAccountId } from '../world'; +import { HidingService, saveHidingData } from '../services/hiding'; +import { meetsRequirement } from '../../common/accountUtils'; +import { StatsTracker } from '../stats'; +import { createReloadSettings } from './internal-common'; +import { UserError } from '../userError'; +import { liveSettings } from '../liveSettings'; +import { formatDuration, invalidEnum } from '../../common/utils'; +import { timingEntries } from '../timing'; +import { toPairs, groupBy } from 'lodash'; +import { getSizeOfMap } from '../serverMap'; +import { teleportTo } from '../playerUtils'; + +export const createAccountChanged = + (world: World, tokens: TokenService, findAccount: FindAccountSafe) => + async (accountId: string) => { + const account = await findAccount(accountId); + world.accountUpdated(account); + + if (isBanned(account)) { + tokens.clearTokensForAccount(accountId); + } + }; + +export const createAccountMerged = + (hiding: HidingService) => + async (accountId: string, mergedId: string) => + await hiding.merged(accountId, mergedId); + +function toAccountStatus(client: IClient | undefined, server: ServerConfig): AccountStatus { + return client ? { + online: true, + character: client.characterName, + server: server.id, + map: client.map.id || '-', + x: Math.round(client.pony.x), + y: Math.round(client.pony.y), + userAgent: client.userAgent, + incognito: client.incognito, + duration: formatDuration(Date.now() - client.connectedTime), + } : { online: false }; +} + +export const createAccountStatus = + (world: World, server: ServerConfig) => + async (accountId: string) => + toAccountStatus(findClientByAccountId(world, accountId), server); + +export const createAccountAround = + (world: World) => + async (accountId: string) => + findClientsAroundAccountId(world, accountId); + +export const createHiddenStats = + (hiding: HidingService) => + async (accountId: string) => + hiding.getStatsFor(accountId); + +export const createTeleportTo = + (world: World) => + async (adminAccountId: string, targetAccountId: string) => { + const admin = findClientByAccountId(world, adminAccountId); + const target = findClientByAccountId(world, targetAccountId); + + if (admin && target && admin.map === target.map) { + teleportTo(admin, target.pony.x, target.pony.y); + } + }; + +async function setupPonyAuth(character: ICharacter, account: IAccount, findAuth: FindAuth) { + if (character.site) { + const auth = await findAuth(character.site, account._id); + + if (auth && !auth.disabled && !auth.banned) { + character.auth = auth; + } + } +} + +export const createJoin = + ( + world: World, getSettings: GetSettings, server: ServerConfig, + { clearTokensForAccount, createToken }: TokenService, findAccount: FindAccountSafe, + findCharacter: FindCharacterSafe, findAuth: FindAuth, live: ServerLiveSettings, + hasInvite: HasActiveSupporterInvites + ) => + async (accountId: string, characterId: string) => { + if (getSettings().isServerOffline || live.shutdown) { + throw new UserError('Server is offline'); + } + + const [account, character, supporterInvited] = await Promise.all([ + findAccount(accountId), + findCharacter(characterId, accountId), + hasInvite(accountId), + ]); + + if (!meetsRequirement({ roles: account.roles, supporter: supporterLevel(account), supporterInvited }, server.require)) { + throw new UserError('Server is restricted'); + } + + await setupPonyAuth(character, account, findAuth); + + character.lastUsed = new Date(); + account.settings = { ...account.settings, defaultServer: server.id }; + account.lastVisit = new Date(); + + if (!account.settings.hidden) { + account.lastOnline = new Date(); + account.lastCharacter = character._id; + } + + await Promise.all([character.save(), account.save()]); + + world.kickByAccount(accountId); + clearTokensForAccount(accountId); + + return createToken({ accountId, account, character }); + }; + +function getClientCountOnMainMap(world: World) { + let count = 0; + const map = world.getMainMap(); + + for (const client of world.clients) { + if (client.map === map) { + count++; + } + } + + return count; +} + +export const createGetServerState = + (server: ServerConfig, getSettings: GetSettings, world: World, live: ServerLiveSettings) => + async (): Promise => + ({ + id: server.id, + name: server.name, + path: server.path, + desc: server.desc, + flag: server.flag, + host: server.host, + alert: server.alert, + flags: server.flags, + require: server.require, + dead: false, + shutdown: live.shutdown, + maps: world.maps.length, + online: world.clients.length, + onMain: getClientCountOnMainMap(world), + queued: world.joinQueue.length, + settings: getSettings(), + }); + +export const createGetServerStats = + (statsTracker: StatsTracker) => + async () => + statsTracker.getSocketStats(); + +export const createGetStatsTable = + (world: World) => + async (stats: Stats) => { + switch (stats) { + case Stats.Country: + return getCountryStats(world); + case Stats.Support: + return getSupportStats(world); + case Stats.Maps: + return getMapStats(world); + default: + invalidEnum(stats); + return []; + } + }; + +export const createAction = + (world: World) => + async (action: string, accountId: string) => { + switch (action) { + case 'unstuck': + const client = findClientByAccountId(world, accountId); + + if (client) { + world.resetToSpawn(client); + world.kick(client, 'unstuck'); + } + break; + default: + throw new Error(`Invalid action (${action})`); + } + }; + +export const createKick = + (world: World, { clearTokensForAccount }: TokenService) => + async (accountId: string | undefined, characterId: string | undefined) => { + if (accountId) { + clearTokensForAccount(accountId); + return world.kickByAccount(accountId); + } else if (characterId) { + return world.kickByCharacter(characterId); + } else { + return false; + } + }; + +export const createKickAll = + (world: World, { clearTokensAll }: TokenService) => + async () => { + world.kickAll(); + clearTokensAll(); + }; + +export const createNotifyUpdate = + (world: World, live: ServerLiveSettings) => + async () => { + live.updating = true; + world.notifyUpdate(); + world.saveClientStates(); + }; + +export const createCancelUpdate = + (live: ServerLiveSettings) => + async () => { + live.updating = false; + }; + +export const createShutdownServer = + (world: World, live: ServerLiveSettings) => + async (value: boolean) => { + live.shutdown = value; + + if (live.shutdown) { + world.kickAll(); + saveHidingData(world.hidingService, world.server.id); + } + }; + +/* istanbul ignore next */ +export function createInternalApi( + world: World, server: ServerConfig, reloadSettings: () => Promise, getSettings: GetSettings, + tokens: TokenService, hiding: HidingService, statsTracker: StatsTracker, live: ServerLiveSettings, +): InternalApi { + return { + reloadSettings: createReloadSettings(reloadSettings), + state: createGetServerState(server, getSettings, world, live), + stats: createGetServerStats(statsTracker), + statsTable: createGetStatsTable(world), + action: createAction(world), + join: createJoin( + world, getSettings, server, tokens, findAccountSafe, findCharacterSafe, findAuth, live, hasActiveSupporterInvites), + kick: createKick(world, tokens), + kickAll: createKickAll(world, tokens), + accountChanged: createAccountChanged(world, tokens, findAccountSafe), + accountMerged: createAccountMerged(hiding), + accountStatus: createAccountStatus(world, server), + accountAround: createAccountAround(world), + notifyUpdate: createNotifyUpdate(world, liveSettings), + cancelUpdate: createCancelUpdate(liveSettings), + shutdownServer: createShutdownServer(world, live), + accountHidden: createHiddenStats(hiding), + getTimings: async () => timingEntries(), + teleportTo: createTeleportTo(world), + }; +} + + +function getCountryStats(world: World): StatsTable { + return [ + ['country', 'users'], + ...toPairs(groupBy(world.clients, c => c.country)) + .map(([key, value]) => ({ key, count: value.length })) + .sort((a, b) => b.count - a.count) + .map(({ key, count }) => [key, count.toString()]), + ]; +} + +function getSupportStats(world: World): StatsTable { + let wasmYes = 0; + let wasmNo = 0; + let letAndConstYes = 0; + let letAndConstNo = 0; + + for (const client of world.clients) { + if (client.supportsWasm) { + wasmYes++; + } else { + wasmNo++; + } + + if (client.supportsLetAndConst) { + letAndConstYes++; + } else { + letAndConstNo++; + } + } + + function percent(yes: number, no: number) { + return (yes * 100 / ((yes + no) || 1)).toFixed(0) + '%'; + } + + return [ + ['supports', 'yes', 'no', ''], + ['wasm', wasmYes.toString(), wasmNo.toString(), percent(wasmYes, wasmNo)], + ['let & const', letAndConstYes.toString(), letAndConstNo.toString(), percent(letAndConstYes, letAndConstNo)], + ]; +} + +function getMapStats(world: World): StatsTable { + return [ + ['id', 'instance', 'entities', 'players', 'memory'], + ...world.maps.map(map => { + const { entities, memory } = getSizeOfMap(map); + + return [ + map.id || 'main', + map.instance || '', + entities.toString(), + world.clients.reduce((sum, c) => sum + (c.map === map ? 1 : 0), 0).toString(), + `${(memory / 1024).toFixed()} kb`, + ]; + }), + ]; +} diff --git a/src/ts/server/api/merge.ts b/src/ts/server/api/merge.ts new file mode 100644 index 0000000..8e1c619 --- /dev/null +++ b/src/ts/server/api/merge.ts @@ -0,0 +1,272 @@ +import { assignWith, uniq, uniqBy, clone, mapValues, difference } from 'lodash'; +import { toInt, maxDate, minDate, compareDates } from '../../common/utils'; +import { updateCharacterCount, checkIfNotAdmin } from '../accountUtils'; +import { + Account, Auth, Character, Event, IAccount, SupporterInvite, findAccountSafe, MongoUpdate, ID, FriendRequest, + findFriendIds, findHidesForMerge, HideRequest +} from '../db'; +import { accountChanged, accountMerged, RemovedDocument } from '../internal'; +import { system } from '../logger'; +import { makeQueued } from '../utils/taskQueue'; +import { + AccountBase, MergeData, MergeAccountData, AccountState, AccountFlags, MergeHideData +} from '../../common/adminInterfaces'; +import { kickFromAllServers } from './admin'; + +function mergeBan(a: number | undefined, b: number | undefined): number { + return (a === -1 || b === -1) ? -1 : Math.max(a || 0, b || 0); +} + +function mergeLists(a: T[] | undefined, b: T[] | undefined, limit: number) { + return [...(a || []), ...(b || [])].sort((a, b) => compareDates(a.date, b.date)).slice(-limit); +} + +async function findAccounts(id: ID, withId: ID, allowAdmin = false) { + const accounts = await Account.find({ _id: { $in: [id, withId] } }) + .populate('auths', 'name') + .populate('characters', 'name') + .exec(); + + if (!allowAdmin) { + accounts.forEach(a => checkIfNotAdmin(a, `merge: ${a._id}`)); + } + + const account = accounts.find(a => a._id.toString() === id); + const merge = accounts.find(a => a._id.toString() === withId); + + if (accounts.length !== 2 || !account || !merge) { + throw new Error('Account does not exist'); + } + + return { account, merge }; +} + +function dumpData(account: IAccount, friends: string[], hides: MergeHideData[]): MergeAccountData { + const { + name, note, flags, counters = {}, auths = [], characters = [], ignores = [], emails = [], state = {}, + birthdate, + } = account; + + return { + name, + note, + flags, + state, + birthdate, + emails: emails.slice(), + ignores: ignores.slice(), + counters: clone(counters), + auths: auths.map(({ _id, name }) => ({ id: _id.toString(), name })), + characters: characters.map(({ _id, name }) => ({ id: _id.toString(), name })), + settings: account.settings, + friends: friends.slice(), + hides: hides.slice(), + }; +} + +function mergeStates(a: AccountState | undefined, b: AccountState | undefined) { + if (a && b) { + return { + ...b, + ...a, + gifts: toInt(a.gifts) + toInt(b.gifts), + candies: toInt(a.candies) + toInt(b.candies), + clovers: toInt(a.clovers) + toInt(b.clovers), + toys: toInt(a.toys) | toInt(b.toys), + }; + } else { + return a || b; + } +} + +async function merge( + id: string, withId: string, reason: string, removedDocument: RemovedDocument, allowAdmin = false, + creatingDuplicates = false +) { + const start = Date.now(); + + const [{ account, merge }, accountFriends, mergeFriends, accountHides, mergeHides] = await Promise.all([ + findAccounts(id, withId, allowAdmin), + findFriendIds(id), + findFriendIds(withId), + findHidesForMerge(id), + findHidesForMerge(withId), + ]); + + const data: MergeData = { + account: dumpData(account, accountFriends, accountHides), + merge: dumpData(merge, mergeFriends, mergeHides), + }; + + const origins = uniqBy([...(account.origins || []), ...(merge.origins || [])], x => x.ip); + const ignores = uniq([...(account.ignores || []), ...(merge.ignores || [])]); + const emails = uniq([...(account.emails || []), ...(merge.emails || [])]); + const note = `${account.note || ''}\n${merge.note || ''}`.trim(); + const createdAt = minDate(account.createdAt, merge.createdAt); + const lastVisit = maxDate(account.lastVisit, merge.lastVisit); + const ban = mergeBan(account.ban, merge.ban); + const shadow = mergeBan(account.shadow, merge.shadow); + const mute = mergeBan(account.mute, merge.mute); + const patreon = Math.max(toInt(account.patreon), toInt(merge.patreon)); + const counters = assignWith(account.counters || {}, merge.counters || {}, (a, b) => (a | 0) + (b | 0)); + const creatingDuplicatesFlag = creatingDuplicates ? AccountFlags.CreatingDuplicates : 0; + const flags = account.flags | merge.flags | creatingDuplicatesFlag; + const supporter = toInt(account.supporter) | toInt(merge.supporter); + const birthdate = account.birthdate || merge.birthdate; + const supporterLog = mergeLists(account.supporterLog, merge.supporterLog, 10); + const supporterTotal = toInt(account.supporterTotal) + toInt(merge.supporterTotal); + const banLog = mergeLists(account.banLog, merge.banLog, 10); + const merges = mergeLists(account.merges, merge.merges, 20); + const state = mergeStates(account.state, merge.state); + const alert = account.alert || merge.alert; + merges.push({ id: withId, name: merge.name, date: new Date(), reason, data }); + + const update: Partial> = { + origins, ignores, emails, note, lastVisit, ban, shadow, mute, flags, counters, patreon, supporter, merges, + createdAt, supporterLog, supporterTotal, banLog, state, alert, birthdate, + }; + + await Promise.all([ + Account.updateOne({ _id: account._id }, update).exec(), + Account.updateMany({ ignores: { $exists: true, $ne: [], $in: [withId] } }, { $addToSet: { ignores: id } }).exec() + .then(() => Account.updateMany({ ignores: { $exists: true, $ne: [], $in: [withId] } }, { $pull: { ignores: withId } }).exec()), + Auth.updateMany({ account: merge._id }, { account: account._id }).exec(), + Event.updateMany({ account: merge._id }, { account: account._id }).exec(), + Character.updateMany({ account: merge._id }, { account: account._id }).exec(), + Promise.all([ + SupporterInvite.updateMany({ source: merge._id }, { source: account._id }).exec(), + SupporterInvite.updateMany({ target: merge._id }, { target: account._id }).exec(), + ]).then(() => SupporterInvite.remove({ target: account._id, source: account._id }).exec()), + Promise.all([ + FriendRequest.updateMany({ source: merge._id }, { source: account._id }).exec(), + FriendRequest.updateMany({ target: merge._id }, { target: account._id }).exec(), + ]).then(() => FriendRequest.remove({ target: account._id, source: account._id }).exec()), + Promise.all([ + HideRequest.updateMany({ source: merge._id }, { source: account._id }).exec(), + HideRequest.updateMany({ target: merge._id }, { target: account._id }).exec(), + ]).then(() => HideRequest.remove({ target: account._id, source: account._id }).exec()), + ]); + + await removeDuplicateFriendRequests(id); + await merge.remove(); + await kickFromAllServers(withId); + await removedDocument('accounts', withId); + await updateCharacterCount(id); + await accountMerged(id, withId); + await accountChanged(id); + + system(account._id, `Merged ${account.name} with ${merge.name} [${merge._id}] (${reason}) (${Date.now() - start}ms)`); +} + +async function removeDuplicateFriendRequests(id: string) { + const friendRequests = await FriendRequest.find({ $or: [{ source: id }, { target: id }] }).exec(); + const checked = new Set(); + const removeRequests: ID[] = []; + + for (const request of friendRequests) { + const friendId = request.source.toString() === id ? request.target.toString() : request.source.toString(); + + if (checked.has(friendId)) { + removeRequests.push(request._id); + } else { + checked.add(friendId); + } + } + + if (removeRequests.length) { + await FriendRequest.remove({ _id: { $in: removeRequests } }).exec(); + } +} + +export async function split( + accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData, reason: string +) { + const start = Date.now(); + const account = await findAccountSafe(accountId); + const unmerge = await Account.create({ + name: split.name, + note: split.note, + flags: split.flags || 0, + emails: split.emails, + state: split.state, + ignores: split.ignores, + counters: split.counters, + birthdate: split.birthdate, + settings: split.settings, + }); + + const accountUpdate: MongoUpdate = { + note: `${account.note}\nsplit: [${unmerge._id}]`.trim(), + state: keep.state, + }; + + const removeIgnores = difference(split.ignores, account.ignores || []); + + if (removeIgnores.length) { + accountUpdate.$pull = { ignores: removeIgnores }; + } + + const newCounters = split.counters || {}; + + if (Object.keys(newCounters).length > 0) { + const oldCounters = account.counters || {} as any; + const counters = mapValues(newCounters, (value, key) => Math.max(0, toInt(oldCounters[key]) - toInt(value))); + accountUpdate.counters = counters; + } + + const authIds = split.auths.map(x => x.id); + + await Promise.all([ + Auth.updateMany({ _id: { $in: authIds } }, { account: unmerge._id, disabled: false }).exec(), + Character.updateMany({ _id: { $in: split.characters.map(x => x.id) } }, { account: unmerge._id }).exec(), + Account.updateOne({ _id: account._id }, accountUpdate).exec(), + ]); + + // friends + + const friendsToRemove = [...(keep.friends || []), ...(split.friends || [])]; + + await FriendRequest.deleteMany({ + $or: [ + { target: account._id, source: { $in: friendsToRemove } }, + { source: account._id, target: { $in: friendsToRemove } }, + ], + }).exec(); + + await FriendRequest.create([ + ...(keep.friends || []).map(id => ({ source: account._id, target: id })), + ...(split.friends || []).map(id => ({ source: unmerge._id, target: id })) + ]); + + // hides + + const hidesToRemove = [...(keep.hides || []), ...(split.hides || [])].map(hide => hide.id); + + await HideRequest.deleteMany({ + $or: [ + { source: account._id, target: { $in: hidesToRemove } }, + ], + }).exec(); + + await HideRequest.create([ + ...(keep.hides || []).map(hide => ({ source: account._id, target: hide.id, name: hide.name, date: new Date(hide.date) })), + ...(split.hides || []).map(hide => ({ source: unmerge._id, target: hide.id, name: hide.name, date: new Date(hide.date) })) + ]); + + // other + + if (mergeId) { + await Account.updateOne({ _id: account._id, 'merges._id': mergeId }, { 'merges.$.split': true }).exec(); + } + + await Promise.all([ + updateCharacterCount(accountId), + updateCharacterCount(unmerge._id), + accountChanged(accountId), + ]); + + system(account._id, `Split off ${unmerge.name} [${unmerge._id}] (${reason}) (${Date.now() - start}ms)`); +} + +export const mergeAccounts = makeQueued(merge); +export const splitAccounts = makeQueued(split); diff --git a/src/ts/server/api/origins.ts b/src/ts/server/api/origins.ts new file mode 100644 index 0000000..2a6e44e --- /dev/null +++ b/src/ts/server/api/origins.ts @@ -0,0 +1,113 @@ +import * as Bluebird from 'bluebird'; +import { difference } from 'lodash'; +import { DAY } from '../../common/constants'; +import { fromNow } from '../../common/utils'; +import { Account, AccountOrigins, OriginStats, OriginInfo, Origin, ClearOrignsOptions } from '../../common/adminInterfaces'; +import { updateAccount } from '../db'; +import { AdminService } from '../services/adminService'; + +export async function getOriginStats(accounts: Account[]): Promise { + let totalOrigins = 0; + let totalOriginsIP4 = 0; + let totalOriginsIP6 = 0; + + const distribution: number[] = []; + const uniques = new Set(); + const duplicates = new Set(); + + for (const account of accounts) { + if (account.origins) { + for (const origin of account.origins) { + totalOrigins++; + + if (uniques.has(origin.ip)) { + duplicates.add(origin.ip); + } else { + uniques.add(origin.ip); + } + + if (origin.ip.indexOf(':') !== -1) { + totalOriginsIP6++; + } else { + totalOriginsIP4++; + } + } + } + + const count = account.origins ? account.origins.length : 0; + + while (distribution.length <= count) { + distribution.push(0); + } + + distribution[count]++; + } + + const uniqueOrigins = uniques.size; + const duplicateOrigins = duplicates.size; + const singleOrigins = uniqueOrigins - duplicateOrigins; + + return { + uniqueOrigins, duplicateOrigins, singleOrigins, totalOrigins, totalOriginsIP4, totalOriginsIP6, distribution + }; +} + +export function removeAllOrigins(service: AdminService, accountId: string) { + service.removeOriginsFromAccount(accountId); + return updateAccount(accountId, { origins: [] }); +} + +export function removeOrigins(service: AdminService, accountId: string, ips: string[]) { + service.removeOriginsFromAccount(accountId, ips); + return updateAccount(accountId, { $pull: { origins: { ip: { $in: ips } } } }); +} + +export function addOrigin(accountId: string, { ip, country }: OriginInfo) { + return updateAccount(accountId, { $push: { origins: { ip, country, last: new Date() } } }); +} + +export async function clearOriginsForAccount(service: AdminService, accountId: string, options: ClearOrignsOptions) { + const account = service.accounts.get(accountId); + + if (account) { + const { ips } = getOriginsToRemove(account, options); + await removeOrigins(service, accountId, ips); + } +} + +export async function clearOriginsForAccounts(service: AdminService, accounts: string[], options: ClearOrignsOptions) { + await Bluebird.map(accounts, id => clearOriginsForAccount(service, id, options), { concurrency: 4 }); +} + +export async function clearOrigins( + service: AdminService, count: number, andHigher: boolean, options: ClearOrignsOptions +) { + const origins = service.accounts.items + .filter(a => a.originsRefs && (andHigher ? a.originsRefs.length >= count : a.originsRefs.length === count)) + .map(a => getOriginsToRemove(a, options)) + .filter(({ ips }) => !!ips.length); + + await Bluebird.map(origins, o => removeOrigins(service, o.accountId, o.ips), { concurrency: 4 }); +} + +const isBanned = (origin: Origin) => origin.ban || origin.mute || origin.shadow; + +function getOriginsToRemove(account: Account, { old, singles, trim, veryOld, country }: ClearOrignsOptions): AccountOrigins { + const date = fromNow((veryOld ? -90 : -14) * DAY).getTime(); + const originsRefs = account.originsRefs || []; + const filtered = country ? + originsRefs.filter(({ origin }) => origin.country === country) : + originsRefs.filter(({ last, origin }) => { + return (!old || (!last || last.getTime() < date)) + && (!singles || origin.accounts!.length === 1) + && !isBanned(origin); + }); + + const ips = filtered.map(({ origin }) => origin.ip); + + if (trim) { + ips.push(...difference(originsRefs.map(({ origin }) => origin.ip), ips).slice(10)); + } + + return { accountId: account._id, ips }; +} diff --git a/src/ts/server/api/ponies.ts b/src/ts/server/api/ponies.ts new file mode 100644 index 0000000..b3bbf15 --- /dev/null +++ b/src/ts/server/api/ponies.ts @@ -0,0 +1,112 @@ +import * as Bluebird from 'bluebird'; +import { escapeRegExp } from 'lodash'; +import { FindPonyQuery } from '../../common/adminInterfaces'; +import { updateCharacterCount, getCharacterLimit } from '../accountUtils'; +import { Character, findAccountSafe, ICharacter } from '../db'; +import { RemovedDocument } from '../internal'; +import { MINUTE } from '../../common/constants'; +import { cached } from '../serverUtils'; +import { logRemovedCharacter } from '../characterUtils'; +import { kickFromAllServersByCharacter } from './admin'; +import { AdminService } from '../services/adminService'; + +const ITEMS_PER_PAGE = 20; +const ITEMS_LIMIT = 1000; +const CACHE_TIMEOUT = 10 * MINUTE; + +function createQuery({ search }: FindPonyQuery) { + const and: any[] = []; + + if (search) { + if (search === 'orphan') { + and.push({ account: { $exists: false } }); + } else if (/^exact:/.test(search)) { + and.push({ name: new RegExp(`^${escapeRegExp(search.substr(6))}$`, 'i') }); + } else { + and.push({ name: new RegExp(escapeRegExp(search), 'i') }); + } + } + + return and.length === 0 ? {} : (and.length === 1 ? and[0] : { $and: and }); +} + +async function getPonyIds(query: FindPonyQuery) { + const items: ICharacter[] = await Character + .find(createQuery(query), '_id') + .sort(query.orderBy || 'createdAt') + .limit(ITEMS_LIMIT) + .lean() + .exec(); + + return items.map(i => i._id.toString()); +} + +const cachedGetPonyIds = cached(getPonyIds, CACHE_TIMEOUT); + +export async function findPonies(query: FindPonyQuery, page: number) { + const from = page * ITEMS_PER_PAGE; + const ids = await cachedGetPonyIds(query); + const idsOnPage = ids.slice(from, from + ITEMS_PER_PAGE); + + return { + items: idsOnPage, + totalCount: ids.length + }; +} + +export async function assignCharacter(characterId: string, accountId: string) { + const character = await Character.findById(characterId).exec(); + + if (!character) + return; + + await kickFromAllServersByCharacter(characterId); + await Character.updateOne({ _id: characterId }, { account: accountId }).exec(); + await Promise.all([ + updateCharacterCount(character.account), + updateCharacterCount(accountId), + ]); +} + +export async function removeCharacter(service: AdminService, characterId: string) { + const character = await Character.findById(characterId).exec(); + + if (!character) + return; + + await kickFromAllServersByCharacter(characterId); + await character.remove(); + await updateCharacterCount(character.account); + logRemovedCharacter(character); + service.ponies.removed(characterId); +} + +async function removeCharacters(character: ICharacter[], accountId: string, removedDocument: RemovedDocument) { + await Bluebird.map(character, async c => { + await c.remove(); + await removedDocument('ponies', c._id.toString()); + logRemovedCharacter(c); + }, { concurrency: 4 }); + + await updateCharacterCount(accountId); +} + +export async function removeCharactersAboveLimit(removedDocument: RemovedDocument, accountId: string) { + const [account, items] = await Promise.all([ + findAccountSafe(accountId), + Character.find({ account: accountId }).sort({ lastUsed: -1 }).exec(), + ]); + + const limited = items.slice(getCharacterLimit(account)); + await removeCharacters(limited, accountId, removedDocument); +} + +export async function removeAllCharacters(removedDocument: RemovedDocument, accountId: string) { + const items = await Character.find({ account: accountId }).sort({ lastUsed: -1 }).exec(); + await removeCharacters(items, accountId, removedDocument); +} + +export async function createCharacter(account: string, name: string, info: string) { + await Character.create({ account, name, info }); + await updateCharacterCount(account); +} diff --git a/src/ts/server/api/pony.ts b/src/ts/server/api/pony.ts new file mode 100644 index 0000000..1f9b3cb --- /dev/null +++ b/src/ts/server/api/pony.ts @@ -0,0 +1,148 @@ +import { PonyObject, PonyInfoNumber } from '../../common/interfaces'; +import { CharacterFlags } from '../../common/adminInterfaces'; +import { cleanName, validatePonyName } from '../../client/clientUtils'; +import { Reporter, LogAccountMessage } from '../serverInterfaces'; +import { toPonyObject } from '../serverUtils'; +import { isForbiddenName } from '../../common/security'; +import { colorToHexRGB } from '../../common/color'; +import { IAccount, FindCharacter, FindAuth, CharacterCount, ID, CreateCharacter, ICharacter } from '../db'; +import { isBadCM } from '../cmUtils'; +import { UserError } from '../userError'; +import { CHARACTER_SAVING_ERROR, CHARACTER_LIMIT_ERROR } from '../../common/errors'; +import { decompressPony, compressPony } from '../../common/compressPony'; +import { getCharacterLimit } from '../accountUtils'; +import { PLAYER_DESC_MAX_LENGTH } from '../../common/constants'; + +function colorToText(c: number): string { + return c ? colorToHexRGB(c) : ''; +} + +export type UpdateCharacterCount = (accountId: ID) => Promise; +export type RemoveCharacter = (ponyId: string, accountId: string) => Promise; +export type SavePony = ReturnType; +export type RemovePony = ReturnType; + +export const createSavePony = + ( + findCharacter: FindCharacter, findAuth: FindAuth, characterCount: CharacterCount, + updateCharacterCount: UpdateCharacterCount, createCharacter: CreateCharacter, log: LogAccountMessage, + isSuspiciousName: (name: string) => boolean, isSuspiciousPony: (info: PonyInfoNumber) => boolean, + ) => + async (account: IAccount, data: Partial | undefined, reporter: Reporter): Promise => { + if (!data || !data.info || typeof data.name !== 'string') { + throw new UserError('Invalid data', { data }); + } + + const originalName = data.name; + data.name = cleanName(data.name); + + if (!validatePonyName(data.name)) { + throw new UserError('Invalid name', { desc: JSON.stringify(originalName), data }); + } + + let [character, auth] = await Promise.all([ + data.id ? findCharacter(data.id, account._id) : undefined, + data.site ? findAuth(data.site, account._id, '_id') : undefined, + ]).catch(error => { + throw new UserError('Invalid data', { error, data }); + }); + + let suspicious: string[] = []; + let created = false; + let nameChanged = false; + let oldName: string | undefined; + + try { + if (!character) { + character = createCharacter(account); + created = true; + } + + const deco = decompressPony(data.info); + const info = compressPony(deco); + + // if (data.info !== info) { + // reporter.danger(`Pony info does not match after re-compression`, `original: ${data.info}\nre-compressed: ${info}`); + // } + + const badCM = isBadCM(deco.cm && deco.cm.map(colorToText) || [], colorToHexRGB(deco.coatFill!)); + const forbiddenName = isForbiddenName(data.name); + const flags = + (badCM ? CharacterFlags.BadCM : 0) | + (data.hideSupport ? CharacterFlags.HideSupport : 0) | + (data.respawnAtSpawn ? CharacterFlags.RespawnAtSpawn : 0) | + (forbiddenName ? CharacterFlags.ForbiddenName : 0); + + nameChanged = character.name !== data.name; + oldName = character.name; + + if (nameChanged && isSuspiciousName(data.name)) { + suspicious.push('name'); + } + + if (character.info !== data.info && isSuspiciousPony(deco)) { + suspicious.push('look'); + } + + character.desc = typeof data.desc === 'string' ? data.desc.substr(0, PLAYER_DESC_MAX_LENGTH) : ''; + character.name = data.name; + character.tag = data.tag; + character.site = auth ? auth._id : null; + character.info = info; + character.flags = flags; + character.lastUsed = new Date(); + } catch (error) { + const message = DEVELOPMENT ? `${CHARACTER_SAVING_ERROR} (${error})` : CHARACTER_SAVING_ERROR; + throw new UserError(message, { error, data: { pony: data }, desc: `info: "${data.info}"` }); + } + + const count = created ? await characterCount(account._id) : 0; + + if (count >= getCharacterLimit(account)) { + throw new UserError(CHARACTER_LIMIT_ERROR); + } + + await character.save(); + + if (created) { + await updateCharacterCount(account._id); + } + + if (suspicious.length) { + reporter.setPony(character._id.toString()); + reporter.warn('Suspicious pony created', `"${character.name}" (${suspicious.join(', ')})`); + } + + if (created) { + log(account._id, `created pony "${character.name}"`); + } else if (nameChanged) { + log(account._id, `renamed pony "${oldName}" => "${character.name}"`); + } + + return toPonyObject(character); + }; + +export const createRemovePony = + ( + kickFromAllServersByCharacter: (ponyId: string) => void, + removeCharacter: RemoveCharacter, + updateCharacterCount: UpdateCharacterCount, + removedCharacter: (ponyId: string) => void, + logRemovedCharacter: (character: ICharacter) => void, + ) => + async (ponyId: unknown, accountId: string) => { + if (!ponyId || typeof ponyId !== 'string') { + throw new Error(`Invalid ponyId (${ponyId})`); + } + + await kickFromAllServersByCharacter(ponyId); + const character = await removeCharacter(ponyId, accountId); + await updateCharacterCount(accountId); + + if (character) { + logRemovedCharacter(character); + removedCharacter(ponyId); + } + + return {}; + }; diff --git a/src/ts/server/authUtils.ts b/src/ts/server/authUtils.ts new file mode 100644 index 0000000..1d3b6b1 --- /dev/null +++ b/src/ts/server/authUtils.ts @@ -0,0 +1,106 @@ +import { uniq } from 'lodash'; +import { Types } from 'mongoose'; +import { Profile } from '../common/interfaces'; +import { arraysEqual } from '../common/utils'; +import { IAccount, IAuth, Auth, findAuthByOpenId, updateAuth, UpdateAuth, Account } from './db'; +import { system } from './logger'; +import { UserError } from './userError'; +import { CreateAccountOptions, connectOnlySocialError } from './accountUtils'; + +export async function assignAuth(auth: IAuth, account: IAccount) { + if (!auth.account || !auth.account.equals(account._id)) { + system(account._id, `connected auth ${auth.name} [${auth._id}]`); + await updateAuth(auth._id, { account: account._id }); + return true; + } else { + return false; + } +} + +export async function findOrCreateAuth(profile: Profile, accountId: string | undefined, options: CreateAccountOptions) { + let auth = await findAuthByOpenId(profile.id, profile.provider); + + if (auth) { + await updateAuthInfo(updateAuth, auth, profile, accountId); + } else { + if (options.connectOnly && !accountId) { + if (profile.emails.length) { + const account = await Account.findOne({ emails: { $in: profile.emails } }).exec(); + + if (!account) { + throw new UserError(connectOnlySocialError); + } + } else { + throw new UserError(connectOnlySocialError); + } + } + + auth = await createAuth(profile, accountId); + } + + await verifyOrRestoreAuth(auth, accountId); + return auth; +} + +export async function updateAuthInfo( + updateAuth: UpdateAuth, auth: IAuth | undefined, profile: Profile, accountId: string | undefined +) { + if (!auth) + return; + + const changes: Partial = {}; + + if (profile.url && auth.url !== profile.url) { + changes.url = profile.url; + } + + if (profile.username && auth.name !== profile.username) { + changes.name = profile.username; + } + + if (profile.emails && profile.emails.length) { + if (!auth.emails || !arraysEqual(auth.emails.sort(), profile.emails.sort())) { + changes.emails = uniq([...(auth.emails || []), ...profile.emails]); + } + } + + if (!auth.account && accountId) { + changes.account = Types.ObjectId(accountId); + } + + if (Object.keys(changes).length > 0) { + Object.assign(auth, changes); + await updateAuth(auth._id, changes); + } +} + +async function createAuth(profile: Profile, account: string | undefined) { + if (!profile.id) { + throw new Error('Missing profile ID'); + } + + return await Auth.create({ + account, + openId: profile.id, + provider: profile.provider, + name: profile.username, + url: profile.url, + emails: profile.emails || [], + lastUsed: new Date(), + }); +} + +async function verifyOrRestoreAuth(auth: IAuth, mergeAccount: string | undefined) { + const changes: Partial = { lastUsed: new Date() }; + + if (auth.disabled || auth.banned) { + if (!auth.banned && auth.account && !!mergeAccount) { + changes.disabled = false; + } else { + throw new UserError('Cannot sign-in using this social account'); + } + } + + Object.assign(auth, changes); + await updateAuth(auth._id, changes); +} diff --git a/src/ts/server/boot.ts b/src/ts/server/boot.ts new file mode 100644 index 0000000..6200645 --- /dev/null +++ b/src/ts/server/boot.ts @@ -0,0 +1,19 @@ +/// + +require('source-map-support').install(); + +import 'core-js/stable/promise/finally'; +import 'reflect-metadata'; +import * as Promise from 'bluebird'; +import * as fs from 'fs'; +import { argv } from 'yargs'; + +(global as any).DEVELOPMENT = process.env.NODE_ENV !== 'production'; +(global as any).BETA = !!argv.beta; +(global as any).TOOLS = !!argv.tools; +(global as any).SERVER = true; +(global as any).TIMING = false; +(global as any).TESTS = false; +(global as any).performance = Date; + +Promise.promisifyAll(fs); diff --git a/src/ts/server/canvasUtilsNode.ts b/src/ts/server/canvasUtilsNode.ts new file mode 100644 index 0000000..a9d840a --- /dev/null +++ b/src/ts/server/canvasUtilsNode.ts @@ -0,0 +1,22 @@ +/// + +import { readFileAsync, readFileSync } from 'fs'; +import { createCanvas as createNodeCanvas, Image } from 'canvas'; +import { setup } from '../client/canvasUtils'; + +export const createCanvas = createNodeCanvas; + +export async function loadImage(src: string) { + const buffer = await readFileAsync(src); + const image = new Image(); + image.src = buffer; + return image; +} + +export function loadImageSync(src: string) { + const image = new Image(); + image.src = readFileSync(src); + return image; +} + +setup({ createCanvas: createNodeCanvas, loadImage }); diff --git a/src/ts/server/characterUtils.ts b/src/ts/server/characterUtils.ts new file mode 100644 index 0000000..757919f --- /dev/null +++ b/src/ts/server/characterUtils.ts @@ -0,0 +1,220 @@ +import { repeat } from 'lodash'; +import { toByteArray } from 'base64-js'; +import { encodeString } from 'ag-sockets/dist/utf8'; +import { PonyOptions, EntityState, UpdateFlags } from '../common/interfaces'; +import { ICharacter, IAccount, Character, MongoQuery, queryCharacter } from './db'; +import { isForbiddenName } from '../common/security'; +import { supporterLevel } from '../common/adminUtils'; +import { CharacterFlags, CharacterState, ServerConfig, CharacterStateFlags } from '../common/adminInterfaces'; +import { hasFlag, randomPoint, bitmask, last, computeCRC } from '../common/utils'; +import { log, systemMessage, logger } from './logger'; +import { World } from './world'; +import { ServerEntity, ServerMap, IClient } from './serverInterfaces'; +import { pony as ponyEntity, getEntityType } from '../common/entities'; +import { PONY_INFO_KEY, SWAP_TIMEOUT } from '../common/constants'; +import { decompressPony, compressPony } from '../common/compressPony'; +import { canFly, canMagic } from '../client/ponyUtils'; +import { canUseTag } from '../common/tags'; +import { CounterService } from './services/counter'; +import { replaceEmojis } from '../client/emoji'; +import { setEntityName, pushUpdateEntity } from './entityUtils'; +import { saySystem } from './chat'; +import { isPonyFlying } from '../common/entityUtils'; +import { createCharacterState, updateClientCharacter } from './playerUtils'; +import { encodeExpression } from '../common/encoders/expressionEncoder'; + +export const defaultCharacterState: CharacterState = { x: 0, y: 0 }; + +export function encryptInfo(info: string) { + return bitmask(toByteArray(info), PONY_INFO_KEY); +} + +export function createPony(account: IAccount, character: ICharacter, state: CharacterState) { + const pony = ponyEntity(state.x, state.y) as ServerEntity; + pony.state = hasFlag(state.flags, CharacterStateFlags.Right) ? EntityState.FacingRight : 0; + updatePony(pony, account, character); + updatePonyFromState(pony, state); + cleanupPonyOptions(pony); + return pony; +} + +function createDefaultCharacterState(map: ServerMap): CharacterState { + return { + ...defaultCharacterState, + ...randomPoint(map.spawnArea), + map: map.id, + }; +} + +export function getCharacterState(character: ICharacter, serverId: string, map: ServerMap): CharacterState { + return character.state && character.state[serverId] || createDefaultCharacterState(map); +} + +export async function updateCharacterState(characterId: string, serverId: string, state: CharacterState) { + await Character.updateOne({ _id: characterId }, { [`state.${serverId}`]: state }).exec(); +} + +export function getAndFixCharacterState( + server: ServerConfig, character: ICharacter, world: World, states: CounterService +): CharacterState { + const map = world.getMainMap(); + const savedState = last(states.get(character._id.toString()).items) || getCharacterState(character, server.id, map); + const state = { ...defaultCharacterState, ...savedState }; + + if (hasFlag(character.flags, CharacterFlags.RespawnAtSpawn)) { + Object.assign(state, { map: map.id, ...randomPoint(map.spawnArea) }); + } + + return state; +} + +export function updatePonyFromState(pony: ServerEntity, state: CharacterState) { + if (!pony.options) { + pony.options = {}; + } + + if (state.hold) { + const type = getEntityType(state.hold); + + if (type) { + pony.options.hold = type; + } + } else if (pony.options.hold) { + pony.options.hold = 0; + } + + if (state.toy) { + pony.options.toy = state.toy; + } else if (pony.options.toy) { + pony.options.toy = 0; + } + + pony.options.extra = hasFlag(state.flags, CharacterStateFlags.Extra); +} + +export function cleanupPonyOptions({ options }: ServerEntity) { + if (options) { + if (!options.hold) { + delete options.hold; + } + + if (!options.extra) { + delete options.extra; + } + } +} + +export function filterForbidden(name: string) { + const isForbidden = isForbiddenName(name); + return isForbidden ? repeat('?', name.length) : name; +} + +export function updatePony(pony: ServerEntity, account: IAccount, character: ICharacter) { + const info = character.info || ''; + const ponyInfo = decompressPony(info); + const originalName = replaceEmojis(character.name); + const allowedName = filterForbidden(originalName); + const options: PonyOptions = {}; + const level = supporterLevel(account); + + if (character.tag && canUseTag(account, character.tag)) { + options.tag = character.tag; + } else if (level && !hasFlag(character.flags, CharacterFlags.HideSupport)) { + options.tag = `sup${level}`; + } + + pony.options = options; + pony.extraOptions = createExtraOptions(character); + pony.canFly = canFly(ponyInfo); + pony.canMagic = canMagic(ponyInfo); + + // name + setEntityName(pony, allowedName); + + // info + pony.info = info; + + if (hasFlag(character.flags, CharacterFlags.BadCM) && ponyInfo.cm) { + ponyInfo.cm = undefined; + pony.infoSafe = compressPony(ponyInfo); + pony.encryptedInfoSafe = encryptInfo(pony.infoSafe); + } else { + pony.infoSafe = pony.info; + pony.encryptedInfoSafe = encryptInfo(info); + } + + // crc + pony.crc = createCharacterCRC(account._id.toString(), originalName); +} + +function createCharacterCRC(accountId: string, characterName: string) { + const characterNameBuffer = encodeString(characterName)!; + const accountIdBuffer = encodeString(accountId)!; + const buffer = new Uint32Array(Math.ceil((characterNameBuffer.byteLength + accountIdBuffer.byteLength) / 4)); + const bufferUint8 = new Uint8Array(buffer.buffer); + bufferUint8.set(characterNameBuffer); + bufferUint8.set(accountIdBuffer, characterNameBuffer.byteLength); + return computeCRC(buffer) & 0xffff; +} + +export function createExtraOptions(character: ICharacter) { + const options: any = { + ex: true, + }; + + if (character.auth && !isForbiddenName(character.auth.name)) { + options.site = { + provider: character.auth.provider, + name: character.auth.name, + url: character.auth.url, + }; + } + + return options; +} + +export function logRemovedCharacter({ _id, account, name, info }: ICharacter) { + log(systemMessage(`${account}`, `removed pony [${_id}] "${name}" ${info}`)); +} + +export async function swapCharacter(client: IClient, { server }: World, query: MongoQuery) { + if (client.isSwitchingMap) + return; + + if ((Date.now() - client.lastSwap) < SWAP_TIMEOUT) { + return; + } + + const character = await queryCharacter(query); + + if (!character) { + return saySystem(client, `Can't find character`); + } + + if (isPonyFlying(client.pony) && !canFly(decompressPony(character.info || ''))) { + return saySystem(client, `Can't swap to that character in-flight`); + } + + const state = createCharacterState(client.pony, client.map); + updateCharacterState(client.characterId, server.id, state) + .catch(logger.error); + + Character.updateOne({ _id: character._id }, { lastUsed: new Date() }).exec() + .catch(logger.error); + + updateClientCharacter(client, character); + updatePony(client.pony, client.account, client.character); + updatePonyFromState(client.pony, getCharacterState(character, server.id, client.map)); + const options = client.pony.options as PonyOptions; + options.expr = encodeExpression(undefined); + client.pony.state &= ~EntityState.Magic; + pushUpdateEntity({ + entity: client.pony, options: { hold: 0, toy: 0, ...options }, + flags: UpdateFlags.Info | UpdateFlags.Name | UpdateFlags.Options | UpdateFlags.State, + }); + + cleanupPonyOptions(client.pony); + client.myEntity(client.pony.id, client.characterName, client.character.info!, client.characterId, client.pony.crc || 0); + client.reporter.systemLog(`Swapped to "${client.characterName}"`); + client.lastSwap = Date.now(); +} diff --git a/src/ts/server/chat.ts b/src/ts/server/chat.ts new file mode 100644 index 0000000..db2c3b3 --- /dev/null +++ b/src/ts/server/chat.ts @@ -0,0 +1,305 @@ +import { repeat } from 'lodash'; +import { isForbiddenMessage, createIsSuspiciousMessage } from '../common/security'; +import { + ChatType, MessageType, Action, LeaveReason, isPublicChat, isPartyChat, isPublicMessage, isPartyMessage, + toMessageType, isWhisper, isWhisperTo +} from '../common/interfaces'; +import { trimRepeatedLetters, urlRegexTexts, ipRegexText, urlExceptionRegex } from '../common/filterUtils'; +import { parseExpression } from '../common/expressionUtils'; +import { filterBadWords } from '../common/swears'; +import { cleanMessage } from '../client/clientUtils'; +import { parseCommand, getChatPrefix, RunCommand } from './commands'; +import { IClient, OnSuspiciousMessage, ServerEntity, OnMessageSettings } from './serverInterfaces'; +import { World } from './world'; +import { setEntityExpression, isHiddenBy, isMutedOrShadowed, isIgnored, execAction } from './playerUtils'; +import { Suspicious, GameServerSettings } from '../common/adminInterfaces'; +import { isFriend } from './services/friends'; +import { invalidEnumReturn } from '../common/utils'; +import { isWorldPointWithPaddingVisible } from '../common/camera'; +import { tileWidth } from '../common/constants'; + +function isLaugh(message: string): boolean { + return /(^| )(ha(ha)+|he(he)+|ja(ja)+|ха(ха)+|lol|rofl)$/i.test(message); +} + +function isIP(match: string): boolean { + const parts = match.split(/\./g); + return !parts.some(p => /^0\d+$/.test(p)) && parts.map(x => parseInt(x, 10)).every(x => x >= 0 && x <= 255); +} + +function replaceIP(match: string) { + return isIP(match) ? '[LINK]' : match; +} + +const urlRegexes = urlRegexTexts.map(text => new RegExp(text, 'uig')); +const ipRegex = new RegExp(ipRegexText, 'uig'); + +function replaceLink(value: string) { + return urlExceptionRegex.test(value) ? value : '[LINK]'; +} + +export function filterUrls(message: string): string { + message = message.replace(ipRegex, replaceIP); + + for (const regex of urlRegexes) { + message = message.replace(regex, replaceLink); + } + + return message; +} + +// non-party messages +function getMessageType(client: IClient, type: ChatType) { + switch (type) { + case ChatType.Say: + case ChatType.Party: + return MessageType.Chat; + case ChatType.Supporter: + switch (client.supporterLevel) { + case 1: return MessageType.Supporter1; + case 2: return MessageType.Supporter2; + case 3: return MessageType.Supporter3; + default: return MessageType.Chat; + } + case ChatType.Supporter1: + return client.supporterLevel >= 1 ? MessageType.Supporter1 : MessageType.Chat; + case ChatType.Supporter2: + return client.supporterLevel >= 2 ? MessageType.Supporter2 : MessageType.Chat; + case ChatType.Supporter3: + return client.supporterLevel >= 3 ? MessageType.Supporter3 : MessageType.Chat; + case ChatType.Think: + case ChatType.PartyThink: + return MessageType.Thinking; + case ChatType.Dismiss: + return MessageType.Dismiss; + case ChatType.Whisper: + return MessageType.Whisper; + default: + return invalidEnumReturn(type, MessageType.Chat); + } +} + +export type LogChat = (client: IClient, text: string, type: ChatType, ignored: boolean, target: IClient | undefined) => void; + +export type IsSuspiciousMessage = ReturnType; + +export type Say = ReturnType; + +export const createSay = + ( + world: World, runCommand: RunCommand, log: LogChat, checkSpam: OnMessageSettings, reportSwears: OnMessageSettings, + reportForbidden: OnMessageSettings, reportSuspicious: OnSuspiciousMessage, spamCommands: string[], + random: () => number, isSuspiciousMessage: IsSuspiciousMessage, + ) => + (client: IClient, text: string, chatType: ChatType, target: IClient | undefined, settings: GameServerSettings) => { + text = cleanMessage(text); + + const { command, args, type } = parseCommand(text, chatType); + const whisper = type === ChatType.Whisper; + + if (!command && !args) + return; + + if (whisper && client === target) + return; + + const forbidden = command == null && isPublicChat(type) && isForbiddenMessage(args); + + log(client, text, type, forbidden, target); + + const suspicious = isSuspiciousMessage(args, settings); + + if (suspicious !== Suspicious.No) { + reportSuspicious(client, `${getChatPrefix(type)}${text}`, suspicious); + } + + if (command != null) { + if (runCommand(client, command, args, type, target, settings)) { + if (type !== ChatType.Party && spamCommands.indexOf(command) !== -1) { + if (!client.map.instance) { + checkSpam(client, text, settings); + } + } + } else { + const expression = parseExpression(text.substr(1)); + + if (expression) { + setEntityExpression(client.pony, expression); + } else { + saySystem(client, 'Invalid command'); + } + } + } else { + const message = args; + const think = type === ChatType.Think || type === ChatType.PartyThink; + const expression = (think || whisper) ? undefined : parseExpression(message); + + if (expression) { + setEntityExpression(client.pony, expression); + } else if (!whisper && isLaugh(message)) { + execAction(client, Action.Laugh, settings); + } + + if (isPartyChat(type)) { + sayToParty(client, message, think ? MessageType.PartyThinking : MessageType.Party); + } else { + const friendWhisper = whisper && target !== undefined && isFriend(client, target); + const messageNoLinks = filterUrls(message); + const messageCensored = forbidden ? repeat('*', messageNoLinks.length) : filterBadWords(messageNoLinks); + const trimmedMessage = trimRepeatedLetters(messageNoLinks); + const trimmedCensored = trimRepeatedLetters(messageCensored); + const messageType = getMessageType(client, type); + const swearing = messageNoLinks !== messageCensored; + + if (!friendWhisper) { + if (!client.map.instance) { + checkSpam(client, message, settings); + } + + if (settings.filterSwears && swearing) { + reportSwears(client, message, settings); + } + + if (forbidden) { + reportForbidden(client, message, settings); + } + } + + if (!friendWhisper && swearing && settings.kickSwearing && random() < 0.75) { + if (settings.kickSwearingToSpawn) { + world.resetToSpawn(client); + } + + world.kick(client, 'swearing', LeaveReason.Swearing); + } else if (!friendWhisper && forbidden) { + sayTo(client, client.pony, trimmedMessage, messageType); + } else if (whisper) { + sayWhisper(client, trimmedMessage, trimmedCensored, messageType, target, settings); + } else { + sayToEveryone(client, trimmedMessage, trimmedCensored, messageType, settings); + } + } + } + }; + +export function sayTo(client: IClient, { id }: ServerEntity, message: string, type: MessageType) { + client.saysQueue.push([id, message, type]); +} + +export function saySystem(client: IClient, message: string) { + sayTo(client, client.pony, message, MessageType.System); +} + +function sayToClient( + client: IClient, entity: ServerEntity, message: string, censoredMessage: string, type: MessageType, + settings: GameServerSettings +): boolean { + if (client.pony !== entity && !isWhisperTo(type)) { + const swear = !!settings.hideSwearing && message !== censoredMessage; + + if (isPublicMessage(type)) { + if (swear) { + return false; + } + + if (!isWorldPointWithPaddingVisible(client.camera, entity, tileWidth * 2)) { + return false; + } + } + + if (entity.client) { + if (isIgnored(client, entity.client)) { + return false; + } + + if (!client.isMod && isHiddenBy(client, entity.client)) { + return false; + } + + if (swear && !isFriend(client, entity.client)) { + return false; + } + } + + if (client.accountSettings.filterSwearWords || settings.filterSwears) { + message = censoredMessage; + } + } + + sayTo(client, entity, message, type); + return true; +} + +function sayWhisper( + client: IClient, message: string, censoredMessage: string, type: MessageType, + target: IClient | undefined, settings: GameServerSettings +) { + if (target === undefined || target.shadowed || isHiddenBy(client, target)) { + saySystem(client, `Couldn't find this player`); + } else { + const friend = isFriend(client, target); + + if (!friend && client.accountSettings.ignoreNonFriendWhispers) { + saySystem(client, `You can only whisper to friends`); + } else if (!friend && target.accountSettings.ignoreNonFriendWhispers) { + saySystem(client, `Can't whisper to this player`); + } else { + sayTo(client, target.pony, message, toMessageType(type)); + + if (!isMutedOrShadowed(client)) { + sayToClient(target, client.pony, message, censoredMessage, type, settings); + } + } + } +} + +function sayToParty(client: IClient, message: string, type: MessageType) { + if (!client.party) { + saySystem(client, `you're not in a party`); + } else if (isMutedOrShadowed(client)) { + sayTo(client, client.pony, message, type); + } else { + for (const c of client.party.clients) { + sayTo(c, client.pony, message, type); + } + } +} + +export function sayToAll( + entity: ServerEntity, message: string, censoredMessage: string, type: MessageType, settings: GameServerSettings +) { + if (entity.region) { + for (const client of entity.region.clients) { + sayToClient(client, entity, message, censoredMessage, type, settings); + } + } +} + +export function sayToEveryone( + client: IClient, message: string, censoredMessage: string, type: MessageType, settings: GameServerSettings +) { + if ( + isMutedOrShadowed(client) || + client.accountSettings.ignorePublicChat + ) { + sayTo(client, client.pony, message, type); + } else { + sayToAll(client.pony, message, censoredMessage, type, settings); + } +} + +export function sayToOthers( + client: IClient, message: string, type: MessageType, target: IClient | undefined, settings: GameServerSettings +) { + if (isWhisper(type)) { + sayWhisper(client, message, message, type, target, settings); + } else if (isPartyMessage(type)) { + sayToParty(client, message, type); + } else { + sayToEveryone(client, message, message, type, settings); + } +} + +export const sayToClientTest = sayToClient; +export const sayToPartyTest = sayToParty; +export const sayWhisperTest = sayWhisper; diff --git a/src/ts/server/cmUtils.ts b/src/ts/server/cmUtils.ts new file mode 100644 index 0000000..e6e821c --- /dev/null +++ b/src/ts/server/cmUtils.ts @@ -0,0 +1,518 @@ +import { uniq, compact } from 'lodash'; +import { hex } from 'color-convert'; +import { getDeltaE00, LAB } from 'delta-e'; +import { repeat } from '../common/utils'; +import { CM_SIZE } from '../common/constants'; + +const patterns = [ + [ // 0 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 1 + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + [ // 2 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 3 + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + [ // 4 + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + [ // 5 + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + [ // 6 + 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, + ], + [ // 7 + 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + [ // 8 + 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ // 9 + 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, + ], + [ // 10 + 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, + ], + [ // 11 + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 0, + ], + [ // 12 + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + // short arms + [ // 13 + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 14 + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 15 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + [ // 16 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 0, + ], + // short arms inverted + [ + 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + [ + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + [ // 20 + 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + // long arms + [ + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + [ + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 0, + ], + [ + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + [ + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + // no corners + [ // 25 + 1, 0, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + [ + 1, 1, 0, 0, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 0, 0, 1, 1, + ], + // small + [ + 0, 1, 1, 0, 1, + 0, 0, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 1, 0, 1, 1, + 0, 0, 0, 0, 0, + ], + [ + 1, 1, 0, 1, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 1, 1, 0, + 0, 0, 0, 0, 0, + ], + [ + 0, 0, 0, 0, 0, + 0, 1, 1, 0, 1, + 0, 0, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 1, 0, 1, 1, + ], + [ // 30 + 0, 0, 0, 0, 0, + 1, 1, 0, 1, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ + 0, 1, 0, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 1, 1, + 0, 1, 1, 0, 1, + 0, 0, 0, 0, 0, + ], + [ + 1, 0, 1, 1, 0, + 1, 1, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + ], + [ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 1, 1, + 0, 1, 1, 0, 1, + ], + [ + 0, 0, 0, 0, 0, + 1, 0, 1, 1, 0, + 1, 1, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 0, 1, 0, + ], + // weird shapes + [ // 35 + 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 1, 1, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 1, + ], + [ + 1, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + 1, 1, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 1, 0, 0, 1, + 1, 1, 0, 0, 1, + ], + [ + 1, 0, 0, 1, 1, + 1, 0, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + [ + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + [ // 40 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 0, + ], + [ + 1, 0, 0, 1, 0, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + [ + 1, 0, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 0, 1, 0, 0, 1, + ], + // additional pixels + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 1, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 1, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 45 + 1, 0, 1, 1, 1, + 1, 1, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + // very wonky + [ + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 0, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + [ + 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 0, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + // missing one corner + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 50 + 1, 0, 0, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + // missing arms + [ + 0, 0, 1, 1, 1, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 0, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 55 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 1, 1, 1, 0, 0, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 0, 1, 0, 1, + ], +]; + +export function hexToLab(c: string): LAB { + const [L, A, B] = hex.lab(c); + return { L, A, B }; +} + +// export function colorToGrayscale(c: string) { +// const color = parseColorFast(c); +// const grayscale = toGrayscale(color); +// return getB(grayscale); +// } + +export function theSameColor(a: LAB, b: LAB, delta = 27): boolean { // 27 + return getDeltaE00(a, b) < delta; +} + +export function isBadCM(cmString: string[], coatColor: string | undefined): string | undefined { + if (!cmString || !cmString.length) + return undefined; + + const pad = CM_SIZE * CM_SIZE - cmString.length; + const coat = hexToLab(coatColor || '000000'); + const padded = [...cmString, ...repeat(pad, '')]; + const cmAlpha = padded.map(c => (!c || (coatColor && theSameColor(hexToLab(c), coat, 1))) ? 0 : 1); + const cmAlpha2 = padded.map(c => c ? 1 : 0); + + const hasAlpha = cmString.some(c => !c); + const cm = [...cmString.map(c => c ? hexToLab(c) : coat), ...repeat(pad, coat)]; + const colorsString = compact(uniq([coatColor, ...cmString])); + const colors = colorsString.map(hexToLab); + + // const cmGrayscale = padded.map(colorToGrayscale); + // const grays = compact(uniq(cmGrayscale)); + + let patternIndex = 0; + + for (const pattern of patterns) { + if (matchesAlpha(pattern, cmAlpha)) { + return `alpha(pattern:${patternIndex})`; + } + + if (matchesAlpha(pattern, cmAlpha2)) { + return `alpha2(pattern:${patternIndex})`; + } + + for (const color of colors) { + if (matchesColor(pattern, hasAlpha, color, cm)) { + return `color(pattern:${patternIndex}, color:${colorsString[colors.indexOf(color)]})`; + } + } + + // for (const gray of grays) { + // if (matchesGrayscale(pattern, gray, cmGrayscale, patternIndex > 1 ? 50 : 120)) { + // return `grayscale(pattern:${patternIndex}, gray:${gray})`; + // } + // } + + patternIndex++; + } + + return undefined; +} + +function matchesAlpha(pattern: number[], cm: number[]): boolean { + const length = Math.max(pattern.length, cm.length); + + for (let i = 0; i < length; i++) { + if (pattern[i] !== cm[i]) { + return false; + } + } + + return true; +} + +function matchesColor(pattern: number[], hasAlpha: boolean, color: LAB, cm: LAB[]): boolean { + for (let i = 0; i < pattern.length; i++) { + const delta = hasAlpha ? 27 : 30; + const on = pattern[i] === 1; + const same = theSameColor(cm[i], color, delta); + + if (on !== same) { + return false; + } + } + + return true; +} + +// function matchesGrayscale(pattern: number[], color: number, cm: number[], delta: number): boolean { +// for (let i = 0; i < pattern.length; i++) { +// const on = pattern[i] === 1; +// const same = Math.abs(cm[i] - color) < delta; + +// if (on !== same) { +// return false; +// } +// } + +// return true; +// } diff --git a/src/ts/server/commands.ts b/src/ts/server/commands.ts new file mode 100644 index 0000000..e302e5a --- /dev/null +++ b/src/ts/server/commands.ts @@ -0,0 +1,648 @@ +import { range, compact, escapeRegExp } from 'lodash'; +import { + MessageType, ChatType, Expression, Eye, Muzzle, Action, Season, Holiday, Weather, toAnnouncementMessageType, +} from '../common/interfaces'; +import { hasRole } from '../common/accountUtils'; +import { butterfly, bat, firefly, cloud, getEntityType, getEntityTypeName } from '../common/entities'; +import { emojis } from '../client/emoji'; +import { IClient, ServerMap } from './serverInterfaces'; +import { World } from './world'; +import { NotificationService } from './services/notification'; +import { UserError, isUserError } from './userError'; +import { parseExpression, expression } from '../common/expressionUtils'; +import { filterBadWords } from '../common/swears'; +import { randomString } from '../common/stringUtils'; +import { + getCounter, holdToy, getCollectedToysCount, holdItem, playerSleep, playerBlush, playerLove, playerCry, + setEntityExpression, execAction, teleportTo +} from './playerUtils'; +import { ServerLiveSettings, GameServerSettings } from '../common/adminInterfaces'; +import { isCommand, processCommand, clamp, flatten, includes, randomPoint } from '../common/utils'; +import { createNotifyUpdate, createShutdownServer } from './api/internal'; +import { logger } from './logger'; +import { pathTo } from './paths'; +import { sayTo, sayToEveryone, sayToOthers, sayToAll, saySystem } from './chat'; +import { resetTiles } from './serverRegion'; +import { + findEntities, updateMapState, loadMapFromFile, saveMapToFile, saveEntitiesToFile, getSizeOfMap, + saveMapToFileBinaryAlt, saveRegionCollider, saveMap, loadMap +} from './serverMap'; +import { PARTY_LIMIT, tileWidth, tileHeight, MAP_LOAD_SAVE_TIMEOUT } from '../common/constants'; +import { PartyService } from './services/party'; +import { getRegionGlobal } from '../common/worldMap'; +import { swapCharacter } from './characterUtils'; +import { writeFileAsync } from 'fs'; +import { Account } from './db'; +import { defaultHouseSave, removeToolbox, restoreToolbox } from './maps/houseMap'; + +export interface CommandContext { + world: World; + notifications: NotificationService; + liveSettings: ServerLiveSettings; + party: PartyService; + random: (min: number, max: number, floating?: boolean) => number; +} + +export type CommandHandler = ( + context: CommandContext, client: IClient, message: string, type: ChatType, target: IClient | undefined, + settings: GameServerSettings +) => any; + +export interface Command { + names: string[]; + help: string; + role: string; + spam?: boolean; + handler: CommandHandler; +} + +function hasRoleNull(client: IClient, role: string) { + if (!role || hasRole(client.account, role)) + return true; + + return (role === 'sup1' && (client.supporterLevel >= 1 || client.isMod)) || + (role === 'sup2' && (client.supporterLevel >= 2 || client.isMod)) || + (role === 'sup3' && (client.supporterLevel >= 3 || client.isMod)); +} + +function command(names: string[], help: string, role: string, handler: CommandHandler, spam = false): Command { + return { names, help, role, handler, spam }; +} + +function emote(names: string[], expr: Expression, timeout?: number, cancellable?: boolean) { + return command(names, '', '', ({ }, { pony }) => setEntityExpression(pony, expr, timeout, cancellable)); +} + +function action(names: string[], action: Action) { + return command(names, '', '', ({ }, client, _, __, ___, settings) => execAction(client, action, settings)); +} + +function adminModChat(names: string[], help: string, role: string, type: MessageType) { + return command(names, help, role, ({ }, client, message, _, __, settings) => { + sayToEveryone(client, message, filterBadWords(message), type, settings); + }); +} + +function parseSeason(value: string): Season | undefined { + switch (value.toLowerCase()) { + case 'spring': return Season.Spring; + case 'summer': return Season.Summer; + case 'autumn': return Season.Autumn; + case 'winter': return Season.Winter; + default: return undefined; + } +} + +function parseHoliday(value: string): Holiday | undefined { + switch (value.toLowerCase()) { + case 'none': return Holiday.None; + case 'halloween': return Holiday.Halloween; + case 'christmas': return Holiday.Christmas; + default: return undefined; + } +} + +function parseWeather(value: string): Weather | undefined { + switch (value.toLowerCase()) { + case 'none': return Weather.None; + case 'rain': return Weather.Rain; + default: return undefined; + } +} + +function getSpawnTarget(map: ServerMap, message: string) { + if (message === 'spawn') { + return randomPoint(map.spawnArea); + } + + const spawn = map.spawns.get(message); + + if (spawn) { + return randomPoint(spawn); + } + + const match = /^(\d+) (\d+)$/.exec(message.trim()); + + if (!match) { + throw new UserError('invalid parameters'); + } + + const [, tx, ty] = match; + const x = clamp(+tx, 0, map.width - 0.5 / tileWidth); + const y = clamp(+ty, 0, map.height - 0.5 / tileHeight); + return { x, y }; +} + +function execWithFileName(client: IClient, message: string, action: (fileName: string) => Promise) { + const fileName = message.replace(/[^a-zA-Z0-9_-]/g, ''); + + if (!fileName) { + throw new UserError('invalid file name'); + } + + action(fileName) + .catch(e => (logger.error(e), e.message)) + .then(error => saySystem(client, error || 'saved')); +} + +function shouldNotBeCalled() { + throw new Error('Should not be called'); +} + +function isValidMapForEditing(map: ServerMap, client: IClient, checkTimeout: boolean, onlyLeader: boolean) { + if (map.id !== 'house') { + saySystem(client, 'Can only be done inside the house'); + return false; + } + + if (checkTimeout && ((Date.now() - client.lastMapLoadOrSave) < MAP_LOAD_SAVE_TIMEOUT)) { + saySystem(client, `You need to wait ${Math.floor(MAP_LOAD_SAVE_TIMEOUT / 1000)} seconds before loading or saving again`); + return false; + } + + if (onlyLeader && client.party && client.party.leader !== client) { + saySystem(client, 'Only party leader can do this'); + return false; + } + + return true; +} + +let interval: any; + +export function createCommands(world: World): Command[] { + const commands = compact([ + // chat + command(['help', 'h', '?'], '/help - show help', '', ({ }, client) => { + const help = commands + .filter(c => c.help && hasRoleNull(client, c.role)) + .map(c => c.help) + .join('\n'); + + saySystem(client, help); + }), + command(['roll', 'rand', 'random'], '/roll [[min-]max] - randomize a number', '', + ({ random }, client, args, type, target, settings) => { + const ROLL_MAX = 1000000; + const [, min, max] = /^(?:(\d+)-)?(\d+)$/.exec(args) || ['', '', '']; + const minValue = clamp((min ? parseInt(min, 10) : 1) | 0, 0, ROLL_MAX); + const maxValue = clamp((max ? parseInt(max, 10) : 100) | 0, minValue, ROLL_MAX); + const result = args === '🍎' ? args : random(minValue, maxValue); + const message = `🎲 rolled ${result} of ${minValue !== 1 ? `${minValue}-` : ''}${maxValue}`; + sayToOthers(client, message, toAnnouncementMessageType(type), target, settings); + }, true), + command(['s', 'say'], '/s - say', '', shouldNotBeCalled), + command(['p', 'party'], '/p - party chat', '', shouldNotBeCalled), + command(['t', 'think'], '/t - thinking balloon', '', shouldNotBeCalled), + command(['w', 'whisper'], '/w - whisper to player', '', shouldNotBeCalled), + command(['r', 'reply'], '/r - reply to whisper', '', shouldNotBeCalled), + command(['e'], '/e - set permanent expression', '', ({ }, { pony }, message) => { + pony.exprPermanent = parseExpression(message); + setEntityExpression(pony, undefined, 0); + }), + + // actions + command(['turn'], '/turn - turn head', '', ({ }, client, _, __, ___, settings) => { + execAction(client, Action.TurnHead, settings); + }), + command(['boop', ')'], '/boop or /) - a boop', '', ({ }, client, message, _, __, settings) => { + const expression = parseExpression(message); + + if (expression) { + setEntityExpression(client.pony, expression, 800); + } + + execAction(client, Action.Boop, settings); + }), + command(['drop'], '/drop - drop held item', '', ({ }, client, _, __, ___, settings) => { + execAction(client, Action.Drop, settings); + }), + command(['droptoy'], '/droptoy - drop held toy', '', ({ }, client, _, __, ___, settings) => { + execAction(client, Action.DropToy, settings); + }), + // command(['open'], '/open - open gift', '', ({ }, client) => { + // openGift(client); + // }), + + // counters + command(['gifts'], '/gifts - show gift score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'gifts')} 🎁`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['candies', 'candy'], '/candies - show candy score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'candies')} 🍬`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['eggs'], '/eggs - show egg score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'eggs')} 🥚`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['clovers', 'clover'], '/clovers - show clover score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'clovers')} 🍀`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['toys'], '/toys - show number of collected toys', '', ({ }, client, _, type, target, settings) => { + const { collected, total } = getCollectedToysCount(client); + sayToOthers(client, `collected ${collected}/${total} toys`, toAnnouncementMessageType(type), target, settings); + }), + + // other + command(['unstuck'], '/unstuck - respawn at spawn point', '', ({ world }, client) => { + world.resetToSpawn(client); + world.kick(client, '/unstuck'); + }), + command(['leave'], '/leave - leave the game', '', ({ world }, client) => { + world.kick(client, '/leave'); + }), + + // pony states + command(['sit'], '/sit - sit down or stand up', '', shouldNotBeCalled), + command(['lie', 'lay'], '/lie - lie down or sit up', '', shouldNotBeCalled), + command(['fly'], '/fly - fly up or fly down', '', shouldNotBeCalled), + command(['stand'], '/stand - stand up', '', shouldNotBeCalled), + + // emotes + command(['blush'], '', '', ({ }, { pony }, message) => playerBlush(pony, message)), + command(['love', '<3'], '', '', ({ }, { pony }, message) => playerLove(pony, message)), + command(['sleep', 'zzz'], '', '', ({ }, { pony }, message) => playerSleep(pony, message)), + command(['cry'], '', '', ({ }, { pony }, message) => playerCry(pony, message)), + + // expressions + emote(['smile', 'happy'], expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile)), + emote(['frown'], expression(Eye.Neutral, Eye.Neutral, Muzzle.Frown)), + emote(['angry'], expression(Eye.Angry, Eye.Angry, Muzzle.Frown)), + emote(['sad'], expression(Eye.Sad, Eye.Sad, Muzzle.Frown)), + emote(['thinking'], expression(Eye.Neutral, Eye.Frown2, Muzzle.Concerned)), + + // actions + action(['yawn'], Action.Yawn), + action(['laugh', 'lol', 'haha', 'хаха', 'jaja'], Action.Laugh), + action(['sneeze', 'achoo'], Action.Sneeze), + action(['magic'], Action.Magic), + + // house + command(['savehouse'], '/savehouse - saves current house setup', '', async ({ }, client) => { + if (!isValidMapForEditing(client.map, client, true, false)) + return; + + client.lastMapLoadOrSave = Date.now(); + + const savedMap = JSON.stringify(saveMap(client.map, + { saveTiles: true, saveEntities: true, saveWalls: true, saveOnlyEditableEntities: true })); + + DEVELOPMENT && console.log(savedMap); + + client.account.savedMap = savedMap; + await Account.updateOne({ _id: client.accountId }, { savedMap }).exec(); + + saySystem(client, 'Saved'); + client.reporter.systemLog(`Saved house`); + }), + command(['loadhouse'], '/loadhouse - loads saved house setup', '', ({ world }, client) => { + if (!isValidMapForEditing(client.map, client, true, true)) + return; + + if (!client.account.savedMap) + return saySystem(client, 'No saved map state'); + + client.lastMapLoadOrSave = Date.now(); + + loadMap(world, client.map, JSON.parse(client.account.savedMap), + { loadEntities: true, loadWalls: true, loadEntitiesAsEditable: true }); + + saySystem(client, 'Loaded'); + client.reporter.systemLog(`Loaded house`); + }), + command(['resethouse'], '/resethouse - resets house setup to original state', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, true, true)) + return; + + client.lastMapLoadOrSave = Date.now(); + + if (defaultHouseSave) { + loadMap(world, client.map, defaultHouseSave, + { loadEntities: true, loadWalls: true, loadEntitiesAsEditable: true }); + } + + saySystem(client, 'Reset'); + client.reporter.systemLog(`Reset house`); + }), + command(['lockhouse'], '/lockhouse - prevents other people from changing the house', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; + + client.map.editingLocked = true; + + saySystem(client, 'House locked'); + client.reporter.systemLog(`House locked`); + }), + command(['unlockhouse'], '/unlockhouse - enables editing by other people', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; + + client.map.editingLocked = false; + + saySystem(client, 'House unlocked'); + client.reporter.systemLog(`House unlocked`); + }), + command(['removetoolbox'], '/removetoolbox - removes toolbox from the house', '', ({ world }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; + + removeToolbox(world, client.map); + + saySystem(client, 'Toolbox removed'); + client.reporter.systemLog(`Toolbox removed`); + }), + command(['restoretoolbox'], '/restoretoolbox - restores toolbox to the house', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; + + restoreToolbox(world, client.map); + + saySystem(client, 'Toolbox restored'); + client.reporter.systemLog(`Toolbox restored`); + }), + + // supporters + command(['swap'], '/swap - swap character', '', async ({ world }, client, message) => { + if (!message) { + return saySystem(client, `You need to provide name of the character`); + } + + const regex = new RegExp(`^${escapeRegExp(message)}$`, 'i'); + const query = { account: client.account._id, name: { $regex: regex } }; + await swapCharacter(client, world, query); + }), + command(['s1'], '', 'sup1', shouldNotBeCalled), + command(['s2'], '', 'sup2', shouldNotBeCalled), + command(['s3'], '', 'sup3', shouldNotBeCalled), + command(['ss'], '/ss - supporter text', 'sup1', shouldNotBeCalled), + + // mod + adminModChat(['m'], '/m - mod text', 'mod', MessageType.Mod), + command(['emotetest'], '/emotetest - print all emotes', 'mod', (_context, client) => { + let text = ''; + + for (let i = 0; i < emojis.length;) { + if (text) { + text += '\n'; + } + + for (let j = 0; i < emojis.length && j < 20; j++ , i++) { + text += emojis[i].symbol; + } + } + + sayTo(client, client.pony, text, MessageType.Chat); + }), + command(['goto'], '/goto []', 'mod', ({ world }, client, message) => { + const [id = '', instance] = message.split(' '); + const map = world.maps.find(map => map.id === id && map.instance === instance); + + if (map) { + const { x, y } = randomPoint(map.spawnArea); + world.switchToMap(client, map, x, y); + } + }), + command(['tp'], '/tp | - teleport to location', 'mod', (_context, client, message) => { + const { x, y } = getSpawnTarget(client.map, message); + teleportTo(client, x, y); + }), + + // admin + adminModChat(['a'], '/a - admin text', 'admin', MessageType.Admin), + command(['announce'], '/announce - global announcement', 'admin', ({ }, client, message, _, __, settings) => { + findEntities(client.map, e => e.type === butterfly.type || e.type === bat.type || e.type === firefly.type) + .forEach(e => sayToAll(e, message, filterBadWords(message), MessageType.Admin, settings)); + }), + command(['time'], '/time - change server time', DEVELOPMENT ? '' : 'admin', ({ world }, _client, message) => { + if (!/^\d+$/.test(message)) { + throw new UserError('invalid parameter'); + } + + world.setTime(parseInt(message, 10) % 24); + }), + command(['togglerestore'], '/togglerestore - toggle terrain restoration', 'admin', ({ world: { options } }, client) => { + options.restoreTerrain = !options.restoreTerrain; + saySystem(client, `restoration is ${options.restoreTerrain ? 'on' : 'off'}`); + }), + command(['resettiles'], '/resettiles - reset tiles to original state', 'admin', ({ }, client) => { + for (const region of client.map.regions) { + resetTiles(client.map, region); + } + }), + BETA && command(['season'], '/season []', 'admin', ({ world }, _client, message) => { + const [s = '', h = ''] = message.split(' '); + const season = parseSeason(s); + const holiday = parseHoliday(h); + + if (season === undefined) { + throw new UserError('invalid season'); + } else { + world.setSeason(season, holiday === undefined ? world.holiday : holiday); + } + }), + BETA && command(['weather'], '/weather ', 'admin', ({ }, client, message) => { + const weather = parseWeather(message); + + if (weather === undefined) { + throw new UserError('invalid weather'); + } else { + updateMapState(client.map, { weather }); + } + }), + + // superadmin + command(['update'], '/update - prepare server for update', 'superadmin', ({ world, liveSettings }) => { + createNotifyUpdate(world, liveSettings)(); + }), + command(['shutdown'], '/shutdown - shutdown server for update', 'superadmin', ({ world, liveSettings }) => { + createShutdownServer(world, liveSettings)(true); + }), + + // debug + DEVELOPMENT && command(['map'], '/map - show map info', '', ({ world }, client) => { + const map = client.map; + const { memory, entities } = getSizeOfMap(map); + const message = `[${map.id}:${map.instance || '-'}] ${world.maps.indexOf(map)}/${world.maps.length} ` + + `${(memory / 1024).toFixed(2)} kb ${entities} entities`; + saySystem(client, message); + }), + command(['loadmap'], '/loadmap - load map from file', 'superadmin', ({ world }, client, message) => { + execWithFileName(client, message, fileName => + loadMapFromFile(world, client.map, pathTo('store', `${fileName}.json`), { loadOnlyTiles: true })); + }), + command(['savemap'], '/savemap - save map to file', 'superadmin', (_, client, message) => { + execWithFileName(client, message, async fileName => { + await saveMapToFile(client.map, pathTo('store', `${fileName}.json`), { saveTiles: true }); + // await saveMapToFileBinary(client.map, pathTo('store', `${fileName}.bin`)); + }); + }), + command(['savemapbin'], '/savemapbin - save map to file', 'superadmin', (_, client, message) => { + execWithFileName(client, message, fileName => saveMapToFileBinaryAlt(client.map, pathTo('store', `${fileName}.json`))); + }), + command(['saveentities'], '/saveentities - save entities to file', 'superadmin', (_, client, message) => { + execWithFileName(client, message, fileName => saveEntitiesToFile(client.map, pathTo('store', `${fileName}.txt`))); + }), + command(['savehides'], '/savehides - save hides to file', 'superadmin', async ({ world }, client) => { + const json = world.hidingService.serialize(); + await writeFileAsync(pathTo('store', 'hides.json'), json, 'utf8'); + saySystem(client, 'saved'); + }), + command(['throwerror'], '/throwerror - throw test error', 'superadmin', (_, _client, message) => { + throw new Error(message || 'test'); + }), + BETA && command(['test'], '', 'superadmin', ({ }, client) => { + client.map.regions.forEach(region => { + console.log(region.x, region.y, region.colliders.length); + }); + }), + BETA && command(['spamchat'], '/spamchat - spam chat messages', 'superadmin', + ({ world, random }, client, _, __, ___, settings) => { + if (interval) { + clearInterval(interval); + interval = undefined; + } else { + interval = setInterval(() => { + if (includes(world.clients, client)) { + const message = range(random(1, 10)).map(() => randomString(random(1, 10))).join(' '); + sayToEveryone(client, message, message, MessageType.Chat, settings); + } else { + clearInterval(interval); + } + }, 100); + } + }), + BETA && command(['noclouds'], '/noclouds - remove clouds', 'superadmin', ({ world }, client) => { + findEntities(client.map, e => e.type === cloud.type).forEach(e => world.removeEntity(e, client.map)); + }), + BETA && command(['msg'], '/msg - say random stuff', 'superadmin', ({ }, client, _, __, ___, settings) => { + findEntities(client.map, e => !!e.options && e.name === 'debug 2') + .forEach(e => sayToAll(e, 'Hello there!', 'Hello there!', MessageType.Chat, settings)); + }), + BETA && command(['hold'], '/hold - hold item', 'superadmin', ({ }, client, message) => { + holdItem(client.pony, getEntityType(message)); + }), + BETA && command(['toy'], '/toy - hold toy', 'superadmin', ({ }, client, message) => { + holdToy(client.pony, parseInt(message, 10) | 0); + }), + BETA && command(['dc'], '/dc', 'superadmin', ({ }, client) => { + client.disconnect(true, false); + }), + BETA && command(['disconnect'], '/disconnect', 'superadmin', ({ }, client) => { + client.disconnect(true, true); + }), + BETA && command(['info'], '/info ', 'superadmin', ({ world }, client, message) => { + const id = parseInt(message, 10) | 0; + const entity = world.getEntityById(id); + + if (entity) { + const { id, type, x, y, options } = entity; + const info = { id, type: getEntityTypeName(type), x, y, options }; + saySystem(client, JSON.stringify(info, null, 2)); + } else { + saySystem(client, 'undefined'); + } + }), + BETA && command(['collider'], '/collider', 'superadmin', ({ }, client) => { + const region = getRegionGlobal(client.map, client.pony.x, client.pony.y); + + if (region) { + saveRegionCollider(region); + saySystem(client, 'saved'); + // console.log(region.tileIndices); + } + }), + DEVELOPMENT && command(['testparty'], '', 'superadmin', ({ party }, client) => { + const entities = findEntities(client.map, e => !!e.client && /^debug/.test(e.name || '')); + + for (const e of entities.slice(0, PARTY_LIMIT - 1)) { + party.invite(client, e.client!); + } + }), + ]); + + return commands; +} + +export function getSpamCommandNames(commands: Command[]): string[] { + return flatten(commands.filter(c => c.spam).map(c => c.names)); +} + +export type RunCommand = ReturnType; + +export const createRunCommand = + (context: CommandContext, commands: Command[]) => + (client: IClient, command: string, args: string, type: ChatType, target: IClient | undefined, settings: GameServerSettings) => { + command = command.toLowerCase().trim(); + const func = commands.find(c => c.names.indexOf(command) !== -1); + + try { + if (func && hasRoleNull(client, func.role)) { + func.handler(context, client, args, type, target, settings); + } else { + return false; + } + } catch (e) { + if (isUserError(e)) { + saySystem(client, e.message); + } else { + throw e; + } + } + + return true; + }; + +const chatTypes = new Map(); +chatTypes.set('p', ChatType.Party); +chatTypes.set('party', ChatType.Party); +chatTypes.set('s', ChatType.Say); +chatTypes.set('say', ChatType.Say); +chatTypes.set('t', ChatType.Think); +chatTypes.set('think', ChatType.Think); +chatTypes.set('ss', ChatType.Supporter); +chatTypes.set('s1', ChatType.Supporter1); +chatTypes.set('s2', ChatType.Supporter2); +chatTypes.set('s3', ChatType.Supporter3); +chatTypes.set('r', ChatType.Whisper); +chatTypes.set('reply', ChatType.Whisper); +chatTypes.set('w', ChatType.Whisper); +chatTypes.set('whisper', ChatType.Whisper); + +export function parseCommand(text: string, type: ChatType): { command?: string; args: string; type: ChatType; } { + if (!isCommand(text)) { + return { args: text, type }; + } + + const { command, args } = processCommand(text); + + if (command) { + const chatType = chatTypes.get(command.toLowerCase()); + + if (chatType !== undefined) { + if (chatType === ChatType.Think) { + type = type === ChatType.Party ? ChatType.PartyThink : ChatType.Think; + } else { + type = chatType; + } + + return { args, type }; + } + } + + return { command, args, type }; +} + +export function getChatPrefix(type: ChatType) { + switch (type) { + case ChatType.Party: + case ChatType.PartyThink: + return '/p '; + case ChatType.Supporter: + return '/ss '; + case ChatType.Dismiss: + return '/dismiss '; + case ChatType.Whisper: + return '/w '; + default: + return ''; + } +} diff --git a/src/ts/server/config.ts b/src/ts/server/config.ts new file mode 100644 index 0000000..764c72b --- /dev/null +++ b/src/ts/server/config.ts @@ -0,0 +1,70 @@ +import { argv } from 'yargs'; +import { ServerConfig } from '../common/adminInterfaces'; + +export interface AppConfig { + title: string; + twitterLink?: string; + supporterLink?: string; + contactEmail?: string; + port: number; + adminPort?: number; + host: string; + proxy?: number; + noindex?: boolean; + secret: string; + token: string; + local: string; + adminLocal?: string; + sw?: boolean; + db: string; + pg: any; + rollbar?: { + environment: string; + clientToken: string; + serverToken: string; + gulpToken: string; + }; + analytics?: { + trackingID: string; + }; + assetsPath?: string; + oauth: { [key: string]: any }; + servers: ServerConfig[]; + facebookAppId?: string; +} + +export interface AppPackage { + name: string; + version: string; + description: string; +} + +export interface AppArgs { + port?: string; + login?: boolean; + admin?: boolean; + standaloneadmin?: boolean; + game?: string; + superadmin?: string; + users?: boolean; + tools?: boolean; + webpack?: boolean; + local?: boolean; + nocleanup?: boolean; +} + +export const args = argv as AppArgs; +export const { version, description }: AppPackage = require('../../../package.json'); +export const config: AppConfig = require('../../../config.json'); + +const loginServer: ServerConfig = { id: 'login', filter: false, port: config.port } as any; +const adminServer: ServerConfig = { id: 'admin', filter: false, port: config.adminPort || config.port } as any; + +export const gameServers = config.servers.filter(s => !s.hidden); + +const allServers = [...gameServers, loginServer, adminServer]; +allServers.forEach(s => s.flags = s.flags || {}); +const serverId = args.game || (args.login ? 'login' : (args.admin ? 'admin' : allServers[0].id)); + +export const server = allServers.find(s => s.id === serverId) || allServers[0]; +export const port = (args.port && parseInt(args.port, 10)) || server.port || config.port; diff --git a/src/ts/server/controllerUtils.ts b/src/ts/server/controllerUtils.ts new file mode 100644 index 0000000..05d0030 --- /dev/null +++ b/src/ts/server/controllerUtils.ts @@ -0,0 +1,139 @@ +import { sample } from 'lodash'; +import { ServerEntity, IClient, ServerMap, Interact } from './serverInterfaces'; +import { unholdItem, holdItem } from './playerUtils'; +import { sayTo } from './chat'; +import { MessageType, CreateEntityMethod, EntityState, setAnimationToEntityState } from '../common/interfaces'; +import { World } from './world'; +import * as entities from '../common/entities'; +import { setEntityName, updateEntityState } from './entityUtils'; +import { hasFlag, repeat } from '../common/utils'; + +export function give(type: number, message?: string) { + return (e: ServerEntity, client: IClient) => { + if (client.pony.options && client.pony.options.hold === type) { + unholdItem(client.pony); + } else { + if (message) { + sayTo(client, e, message, MessageType.Announcement); + } + + holdItem(client.pony, type); + } + }; +} + +export function createBoxOfLanterns(x: number, y: number) { + const boxOfLanterns = entities.boxLanterns(x, y) as ServerEntity; + boxOfLanterns.interact = give(entities.lanternOn.type); + setEntityName(boxOfLanterns, 'Box of lanterns'); + return boxOfLanterns; +} + +export function createSign(x: number, y: number, name: string, interact: Interact, create = entities.sign) { + const entity = create(x, y) as ServerEntity; + setEntityName(entity, name); + entity.interact = interact; + return entity; +} + +export function createSignWithText(x: number, y: number, name: string, text: string, create = entities.sign) { + return createSign(x, y, name, (entity, client) => sayTo(client, entity, text, MessageType.System), create); +} + +export function boopLight(this: ServerEntity) { + setTimeout(() => { + if (hasFlag(this.state, EntityState.On)) { + turnOff(this); + this.lightDelay = Date.now() + 3000; + } + }, 300); +} + +export function createAddLight(world: World, map: ServerMap, createEntity: CreateEntityMethod) { + return (x: number, y: number) => { + const entity = world.addEntity(createEntity(x, y), map); + entity.boop = boopLight; + return entity; + }; +} + +export function turnOff(entity: ServerEntity) { + updateEntityState(entity, EntityState.None); +} + +export function turnOn(entity: ServerEntity) { + updateEntityState(entity, setAnimationToEntityState(EntityState.On, 1)); +} + +export function updateLights(entities: ServerEntity[], on: boolean) { + for (const entity of entities) { + if (hasFlag(entity.state, EntityState.On) !== on && Math.random() < 0.2) { + if (entity.lightDelay === undefined || entity.lightDelay < Date.now()) { + if (on) { + turnOn(entity); + } else { + turnOff(entity); + } + } + } + } +} + +export function createFenceMaker( + world: World, map: ServerMap, + size: number, poles: CreateEntityMethod[], beamsH: CreateEntityMethod[], beamsV: CreateEntityMethod[] +) { + const add = (entity: ServerEntity) => world.addEntity(entity, map); + + return (x: number, y: number, length: number, horizontal = true, skipStart = false, skipEnd = false) => { + const dx = horizontal ? size : 0; + const dy = horizontal ? 0 : size; + + for (let i = 0; i < length; i++) { + if (i || !skipStart) { + add(sample(poles)!(x + dx * i, y + dy * i)); + } + + if (horizontal) { + add(sample(beamsH)!(x + dx * i + (size / 2), y)); + } else { + add(sample(beamsV)!(x, y + dy * i)); + } + } + + if (!skipEnd) { + add(sample(poles)!(x + dx * length, y + dy * length)); + } + }; +} + +export function createWoodenFenceMaker(world: World, map: ServerMap) { + return createFenceMaker(world, map, 1, [ + ...repeat(2, entities.woodenFencePole1), + ...repeat(2, entities.woodenFencePole2), + ...repeat(2, entities.woodenFencePole3), + ...repeat(2, entities.woodenFencePole4), + entities.woodenFencePole5, + ], [ + ...repeat(5, entities.woodenFenceBeamH1), + ...repeat(5, entities.woodenFenceBeamH2), + ...repeat(5, entities.woodenFenceBeamH3), + entities.woodenFenceBeamH4, + entities.woodenFenceBeamH5, + entities.woodenFenceBeamH6, + ], [ + entities.woodenFenceBeamV1, + entities.woodenFenceBeamV2, + entities.woodenFenceBeamV3, + ]); +} + +export function createStoneWallFenceMaker(world: World, map: ServerMap) { + return createFenceMaker(world, map, 2, [ + entities.stoneWallPole1, + ], [ + entities.stoneWallBeamH1, + ], [ + entities.stoneWallBeamV1, + ]); +} diff --git a/src/ts/server/controllers.ts b/src/ts/server/controllers.ts new file mode 100644 index 0000000..6c2f5d1 --- /dev/null +++ b/src/ts/server/controllers.ts @@ -0,0 +1,10 @@ +export * from './controllers/testController'; +export * from './controllers/cloudController'; +export * from './controllers/flyingCritterController'; +export * from './controllers/perfController'; +export * from './controllers/collectableController'; +export * from './controllers/wallController'; +export * from './controllers/fakeClientController'; +export * from './controllers/torchController'; +export * from './controllers/updateController'; +export * from './controllers/plantController'; diff --git a/src/ts/server/controllers/cloudController.ts b/src/ts/server/controllers/cloudController.ts new file mode 100644 index 0000000..6c69d50 --- /dev/null +++ b/src/ts/server/controllers/cloudController.ts @@ -0,0 +1,55 @@ +import { tileWidth } from '../../common/constants'; +import { Entity } from '../../common/interfaces'; +import { entitiesIntersect } from '../../common/utils'; +import { cloud } from '../../common/entities'; +import * as sprites from '../../generated/sprites'; +import { Controller, ServerMap, ServerEntity } from '../serverInterfaces'; +import { World } from '../world'; +import { updateEntityVelocity } from '../entityUtils'; +import { timingEnd, timingStart } from '../timing'; + +const spriteWidth = sprites.cloud.shadow!.w / tileWidth; +const cloudVX = -0.5; + +export class CloudController implements Controller { + private clouds: Entity[] = []; + private initialized = false; + constructor(private world: World, private map: ServerMap, private cloudCount: number) { + } + initialize() { + if (this.initialized) + return; + + for (let i = 0; i < this.cloudCount; i++) { + this.addCloud(false, this.world.now / 1000); + } + + this.initialized = true; + } + update(_: number, now: number) { + timingStart('CloudController.update()'); + for (let i = this.clouds.length - 1; i >= 0; i--) { + const cloud = this.clouds[i]; + + if (cloud.x < -spriteWidth) { + this.clouds.splice(i, 1); + this.world.removeEntity(cloud, this.map); + } + } + + if (this.clouds.length < this.cloudCount) { + this.addCloud(true, now); + } + timingEnd(); + } + private addCloud(end: boolean, timestamp: number) { + const x = end ? this.map.width + spriteWidth : this.map.width * Math.random(); + const y = this.map.height * Math.random(); + const entity = cloud(x, y) as ServerEntity; + + if (!this.clouds.some(c => entitiesIntersect(c, entity))) { + this.clouds.push(this.world.addEntity(entity, this.map)); + updateEntityVelocity(entity, cloudVX, 0, timestamp); + } + } +} diff --git a/src/ts/server/controllers/collectableController.ts b/src/ts/server/controllers/collectableController.ts new file mode 100644 index 0000000..0b43133 --- /dev/null +++ b/src/ts/server/controllers/collectableController.ts @@ -0,0 +1,72 @@ +import { remove, sample } from 'lodash'; +import { Entity, CreateEntityMethod } from '../../common/interfaces'; +import { IClient, Controller, ServerEntity, ServerMap } from '../serverInterfaces'; +import { World } from '../world'; +import { timingEnd, timingStart } from '../timing'; +import { canPlaceItem, canBePickedByPlayer, pushRemoveEntityToClient } from '../entityUtils'; + +export function randomPosition(map: ServerMap) { + const x = Math.random() * map.width; + const y = Math.random() * map.height; + return { x, y }; +} + +export class CollectableController implements Controller { + private items: Entity[] = []; + constructor( + private world: World, + private map: ServerMap, + private ctors: CreateEntityMethod[], + public limit: number, + private pick: (client: IClient, entity: ServerEntity) => void, + private check: (client: IClient) => boolean = () => true, + private tries = 1, + private position = randomPosition, + private active = () => true + ) { + } + initialize() { + } + update() { + timingStart('CollectableController.update()'); + + if (this.active()) { + for (let i = 0; i < this.tries; i++) { + if (this.items.length < this.limit) { + this.generateItem(); + } + } + } + + timingEnd(); + } + private generateItem() { + const { world, map } = this; + const { x, y } = this.position(map); + const ctor = sample(this.ctors)!; + const entity = ctor(x, y) as ServerEntity; + + if (!entity.interactRange) { + entity.interactRange = 1.5; + } + + if ( + x > 0 && y > 0 && x < map.width && y < map.height && canPlaceItem(map, entity) && !canBePickedByPlayer(map, entity) + ) { + entity.interact = this.interact; + this.items.push(world.addEntity(entity, map)); + } + } + private interact = (entity: Entity, client: IClient) => { + if (this.check(client)) { + if (client.shadowed) { + pushRemoveEntityToClient(client, entity); + } else { + remove(this.items, e => e === entity); + this.world.removeEntity(entity, this.map); + this.generateItem(); + this.pick(client, entity); + } + } + } +} diff --git a/src/ts/server/controllers/fakeClientController.ts b/src/ts/server/controllers/fakeClientController.ts new file mode 100644 index 0000000..70e094c --- /dev/null +++ b/src/ts/server/controllers/fakeClientController.ts @@ -0,0 +1,150 @@ +import { sample } from 'lodash'; +import { + createBinaryWriter, getWriterBuffer, resetWriter, resizeWriter, writeArrayHeader, writeUint8Array, + writeUint8 +} from 'ag-sockets'; +import { Controller, IClient } from '../serverInterfaces'; +import { World } from '../world'; +import { Character, Account } from '../db'; +import { createClientAndPony } from '../playerUtils'; +import { CounterService } from '../services/counter'; +import { CharacterState, ServerConfig } from '../../common/adminInterfaces'; +import { removeItem, times } from '../../common/utils'; +import { timingStart, timingEnd } from '../timing'; + +interface Options { + count: number; +} + +const mockCharacterStates = new CounterService(0); + +export class FakeClientsController implements Controller { + private clients: IClient[] = []; + private tokens: any[] = []; + private initialized = false; + constructor(private world: World, private server: ServerConfig, private options: Options) { + } + initialize() { + if (this.initialized) + return; + + times(1000, async i => { + try { + const name = `perf-${i}`; + const account = await Account.findOne({ name }).exec(); + + if (!account) + throw new Error(`Missing debug account (${name})`); + + const character = await Character.findOne({ account: account._id }).exec(); + + if (!character) + throw new Error(`Missing debug character (${name})`); + + this.tokens.push({ id: name, account, character }); + } catch (e) { + console.error(e); + } + }); + + this.initialized = true; + } + update() { + } + sparseUpdate() { + timingStart('FakeClientController.sparseUpdate()'); + + if (this.tokens.length) { + for (let i = this.clients.length - 1; i >= 0; i--) { + if (Math.random() < (10 / this.options.count)) { + this.leave(this.clients[i]); + } + } + + if (this.clients.length < this.options.count) { + for (let i = 0; i < 10; i++) { + this.join(); + } + } + } + + timingEnd(); + } + async join() { + try { + const token = sample(this.tokens)!; + + if (!this.clients.some(c => c.tokenId === token.id)) { + const client = await joinFakeClient(token, this.server, this.world); + this.clients.push(client); + } + } catch (e) { + console.error(e); + } + } + async leave(client: IClient) { + this.world.leaveClient(client); + removeItem(this.clients, client); + } +} + +const packetWriter = createBinaryWriter(); +export let lastPacket: Uint8Array | undefined; + +async function joinFakeClient(token: any, server: ServerConfig, world: World): Promise { + const client: Partial = { + tokenId: token.id, + tokenData: token, + disconnect() { + world.leaveClient(client as IClient); + }, + queue() { }, + left() { }, + worldState() { }, + mapState() { }, + myEntity() { }, + mapTest() { }, + updateFriends() { }, + actionParam() { }, + update(_, subscribes, adds, datas) { + do { + try { + resetWriter(packetWriter); + writeUint8(packetWriter, 123); + + if (writeArrayHeader(packetWriter, subscribes)) { + for (let i = 0; i < subscribes.length; i++) { + writeUint8Array(packetWriter, subscribes[i]); + } + } + + writeUint8Array(packetWriter, adds); + + if (writeArrayHeader(packetWriter, datas)) { + for (let i = 0; i < datas.length; i++) { + writeUint8Array(packetWriter, datas[i]); + } + } + + break; + } catch (e) { + if (e instanceof RangeError || /DataView/.test(e.message)) { + resizeWriter(packetWriter); + } else { + throw e; + } + } + } while (true); + + lastPacket = getWriterBuffer(packetWriter); + }, + addNotification() { }, + removeNotification() { }, + }; + + createClientAndPony(client as IClient, [], [], server, world, mockCharacterStates); + + world.joinClientToQueue(client as IClient); + + return client as IClient; +} diff --git a/src/ts/server/controllers/flyingCritterController.ts b/src/ts/server/controllers/flyingCritterController.ts new file mode 100644 index 0000000..c705fe7 --- /dev/null +++ b/src/ts/server/controllers/flyingCritterController.ts @@ -0,0 +1,89 @@ +import { sample } from 'lodash'; +import { Entity, CreateEntityMethod, ServerFlags } from '../../common/interfaces'; +import { Controller, ServerEntity, ServerMap } from '../serverInterfaces'; +import { World } from '../world'; +import { findClosestEntity, findEntities } from '../serverMap'; +import { timingEnd, timingStart } from '../timing'; +import { hasFlag, distanceXY } from '../../common/utils'; +import { moveRandomly, findClosest, moveTowards } from '../entityUtils'; +import { randomPosition } from './collectableController'; + +export class FlyingCritterController implements Controller { + private entities: Entity[] = []; + constructor( + private world: World, private map: ServerMap, private critter: CreateEntityMethod, private speed: number, + private limit: number, private isActive: () => boolean, private spawnOnStart = false + ) { + } + initialize() { + if (this.spawnOnStart) { + for (let i = 0; i < this.limit; i++) { + const { x, y } = randomPosition(this.map); + this.entities.push(this.world.addEntity(this.critter(x, y), this.map)); + } + } + } + update(_: number, now: number) { + timingStart('FlyingCritterController.update()'); + updateTreehidingEntities( + this.entities, this.world, this.map, this.limit, this.speed, now, this.critter, this.isActive); + timingEnd(); + } +} + +function isTreeCrown(entity: ServerEntity) { + return hasFlag(entity.serverFlags || 0, ServerFlags.TreeCrown); +} + +export function findClosestTree(map: ServerMap, x: number, y: number) { + return findClosestEntity(map, x, y, isTreeCrown); +} + +export function findTrees(map: ServerMap) { + return findEntities(map, isTreeCrown); +} + +interface TargetTree extends Entity { + targetTree?: Entity; +} + +export function updateTreehidingEntities( + entities: TargetTree[], world: World, map: ServerMap, limit: number, speed: number, timestamp: number, + create: (x: number, y: number) => Entity, isActive: () => boolean +) { + const offsetY = -2; + + if (isActive()) { + // release new critter + if (entities.length < limit && Math.random() < 0.1) { + const trees = findTrees(map); + const tree = sample(trees); + + if (tree) { + const entity = create(tree.x, tree.y + offsetY); + entities.push(world.addEntity(entity, map)); + moveRandomly(map, entity, speed, 1, timestamp); + } + } + + for (const entity of entities) { + moveRandomly(map, entity, speed, 0.02, timestamp); + } + } else if (entities.length) { + // head to tree and disappear + const trees = findTrees(map); + + for (let i = entities.length - 1; i >= 0; i--) { + const e = entities[i]; + + e.targetTree = e.targetTree || findClosest(e.x, e.y, trees); + + if (distanceXY(e.x, e.y, e.targetTree.x, e.targetTree.y + offsetY) < 0.1) { + entities.splice(i, 1); + world.removeEntity(e, map); + } else { + moveTowards(e, e.targetTree.x, e.targetTree.y + offsetY, speed, timestamp); + } + } + } +} diff --git a/src/ts/server/controllers/perfController.ts b/src/ts/server/controllers/perfController.ts new file mode 100644 index 0000000..da3ba66 --- /dev/null +++ b/src/ts/server/controllers/perfController.ts @@ -0,0 +1,128 @@ +import { range, compact } from 'lodash'; +import { pony } from '../../common/entities'; +import { Entity, EntityState, MessageType, EntityFlags } from '../../common/interfaces'; +import { Controller, ServerEntity, IClient } from '../serverInterfaces'; +import { World } from '../world'; +import { Character } from '../db'; +import { PONY_SPEED_TROT } from '../../common/constants'; +import { setEntityName, updateEntityVelocity } from '../entityUtils'; +import { encryptInfo } from '../characterUtils'; +import { createCamera } from '../../common/camera'; +import { shouldBeFacingRight } from '../../common/movementUtils'; +import { timingEnd, timingStart } from '../timing'; +import { sayToAll } from '../chat'; + +interface Options { + count: number; + moving: number; + saying?: boolean; + unique?: boolean; + spread?: boolean; + x?: number; + y?: number; +} + +export class PerfController implements Controller { + private entities: Entity[] = []; + private limitLeft = 11; + private limitWidth = 30; + private limitTop = 9; + private limitHeight = 25; + private initialized = false; + constructor(private world: World, private options: Options) { + if (options.spread) { + this.limitWidth = 60; + this.limitHeight = 60; + } + + if (options.x !== undefined) { + this.limitLeft = options.x; + } + + if (options.y !== undefined) { + this.limitTop = options.y; + } + } + initialize() { + if (this.initialized) + return; + + const world = this.world; + const map = world.getMainMap(); + + const names = [ + 'performance', + 'performance 2', + ]; + + const query = this.options.unique ? + Promise.resolve(Character.find({ account: '57ae2336a67f4dc52e123ed1' }).limit(this.options.count).exec()) : + Promise.all(names.map(name => Character.findOne({ name }).exec())).then(compact); + + query + .then(characters => { + if (characters.length) { + this.entities = range(this.options.count).map(i => { + const character = characters[i % characters.length]!; + const name = character._id.toString(); + const x = this.limitLeft + this.limitWidth * Math.random(); + const y = this.limitTop + this.limitHeight * Math.random(); + const p = pony(x, y) as ServerEntity; + setEntityName(p, name); + p.flags |= EntityFlags.CanCollide; + p.encryptedInfoSafe = encryptInfo(character.info || ''); + p.client = { + pony: p, + accountId: 'foobar', + characterId: character._id.toString(), + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + account: {} as any, + regions: [], + camera: createCamera(), + updateRegion() { }, + addEntity() { }, + mapTest() { }, + } as Partial as any; + p.client!.camera.x = -10000; + p.vx = this.options.moving ? randomVelocity() : 0; + p.vy = this.options.moving ? randomVelocity() : 0; + p.state = shouldBeFacingRight(p) ? EntityState.FacingRight : EntityState.None; + return world.addEntity(p, map); + }); + } + }); + + this.initialized = true; + } + update(_: number, now: number) { + timingStart('PerfController.update()'); + + const limitBottom = this.limitTop + this.limitHeight; + const limitRight = this.limitTop + this.limitHeight; + + if (this.options.moving) { + for (const entity of this.entities) { + if ((entity.vy > 0 && entity.y > limitBottom) || (entity.vy < 0 && entity.y < this.limitTop)) { + updateEntityVelocity(entity, entity.vx, -entity.vy, now); + } else if ((entity.vx > 0 && entity.x > limitRight) || (entity.vx < 0 && entity.x < this.limitLeft)) { + updateEntityVelocity(entity, -entity.vx, entity.vy, now); + } else if (Math.random() < 0.1) { + updateEntityVelocity(entity, randomVelocity(), randomVelocity(), now); + } + + if (this.options.saying && Math.random() < 0.01) { + sayToAll(entity, 'Hello World', 'Hello World', MessageType.Chat, {}); + } + } + } + + timingEnd(); + } +} + +function randomVelocity() { + const rand = Math.random(); + return rand < 0.333 ? 0 : (rand < 0.666 ? -PONY_SPEED_TROT : +PONY_SPEED_TROT); +} diff --git a/src/ts/server/controllers/plantController.ts b/src/ts/server/controllers/plantController.ts new file mode 100644 index 0000000..49bbd1b --- /dev/null +++ b/src/ts/server/controllers/plantController.ts @@ -0,0 +1,89 @@ +import { sample, random } from 'lodash'; +import { Controller, ServerEntity, ServerMap, Interact } from '../serverInterfaces'; +import { World } from '../world'; +import { timingStart, timingEnd } from '../timing'; +import { Rect, CreateEntityMethod, ServerFlags, TileType } from '../../common/interfaces'; +import { removeItem, randomPoint } from '../../common/utils'; +import { getTile } from '../../common/worldMap'; + +interface Plant extends ServerEntity { + plantStage: number; + plantStageNext: number; +} + +export interface PlantConfig { + area: Rect; + count: number; + stages: CreateEntityMethod[][]; + onPick?: Interact; + growOnlyOn?: TileType; + isActive?: () => boolean; +} + +export class PlantController implements Controller { + private plants: Plant[] = []; + private interact: Interact = (entity, client) => { + this.world.removeEntity(entity, this.map); + removeItem(this.plants, entity); + this.config.onPick && this.config.onPick(entity, client); + } + private nextSpawn = 0; + constructor(private world: World, private map: ServerMap, private config: PlantConfig) { + } + initialize() { + } + update() { + } + sparseUpdate() { + timingStart('PlantController.sparseUpdate()'); + + const now = Date.now(); + const maxStage = this.config.stages.length - 1; + + if ( + this.nextSpawn < now && + (this.config.isActive === undefined || this.config.isActive()) && + this.plants.length < this.config.count + ) { + const { x, y } = randomPoint(this.config.area); + + if (this.config.growOnlyOn === undefined || getTile(this.map, x, y) === this.config.growOnlyOn) { + this.addPlant(x, y, 0); + this.nextSpawn = now + random(10000, 20000); + } + } + + const plantsToRemove: Plant[] = []; + + for (const plant of this.plants) { + if (plant.plantStage < maxStage && plant.plantStageNext < now) { + plantsToRemove.push(plant); + this.addPlant(plant.x, plant.y, plant.plantStage + 1); + } + } + + for (const plant of plantsToRemove) { + this.removePlant(plant); + } + + timingEnd(); + } + private removePlant(plant: Plant) { + removeItem(this.plants, plant); + this.world.removeEntity(plant, this.map); + } + private addPlant(x: number, y: number, stage: number) { + const create = sample(this.config.stages[stage])!; + const plant = create(x, y) as Plant; + plant.plantStage = stage; + plant.plantStageNext = Date.now() + random(15000, 40000); + plant.serverFlags = ServerFlags.DoNotSave; + + if (stage === (this.config.stages.length - 1)) { + plant.interact = this.interact; + } + + this.plants.push(plant); + this.world.addEntity(plant, this.map); + } +} diff --git a/src/ts/server/controllers/testController.ts b/src/ts/server/controllers/testController.ts new file mode 100644 index 0000000..77aa1b8 --- /dev/null +++ b/src/ts/server/controllers/testController.ts @@ -0,0 +1,87 @@ +import { compact } from 'lodash'; +import * as entities from '../../common/entities'; +import { IClient, ServerEntity, Controller, ServerMap } from '../serverInterfaces'; +import { World } from '../world'; +import { Character } from '../db'; +import { setEntityName } from '../entityUtils'; +import { times } from '../../common/utils'; +import { encryptInfo } from '../characterUtils'; +import { createCamera } from '../../common/camera'; +import { timingStart, timingEnd } from '../timing'; +import { createBinaryWriter } from 'ag-sockets'; + +export class TestController implements Controller { + private clients: IClient[] = []; + private initialized = false; + constructor(private world: World, private map: ServerMap) { + } + initialize() { + if (this.initialized) + return; + + const world = this.world; + const map = this.map; + + if (DEVELOPMENT) { + Promise.all(times(10, i => `debug ${i + 1}`).map(name => Character.findOne({ name }).exec())) + .then(compact) + .then(items => items.forEach((item, i) => { + const name = item.name; + const tag = i === 0 ? 'mod' : (i === 2 ? 'sup2' : ''); + const extraOptions = i === 0 ? { + site: { + provider: 'github', + name: 'Test name', + url: 'https://github.com/Microsoft/TypeScript', + } + } : undefined; + const p = entities.pony(57 + 1 * i, 47 + 1 * i) as ServerEntity; + p.options = { tag }; + setEntityName(p, name); + p.encryptedInfoSafe = encryptInfo(item.info || ''); + p.client = { + map, + accountSettings: {}, + account: { id: 'foobar', name: 'Debug account' } as any, + country: 'XY', + regions: [], + saysQueue: { push() { }, length: 0 } as any, + notifications: [], + camera: createCamera(), + accountId: 'foobar', + characterId: '', + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + updateQueue: createBinaryWriter(1), + addEntity() { }, + addNotification() { }, + removeNotification() { }, + updateParty() { }, + mapUpdate() { }, + } as Partial as any; + p.client!.pony = p; + this.clients.push(p.client!); + p.extraOptions = extraOptions; + world.addEntity(p, map); + })); + } + + this.initialized = true; + } + update() { + timingStart('TestController.update()'); + timingEnd(); + } + sparseUpdate() { + timingStart('TestController.sparseUpdate()'); + + for (const client of this.clients) { + for (const notification of client.notifications) { + notification.accept && notification.accept(); + } + } + + timingEnd(); + } +} diff --git a/src/ts/server/controllers/torchController.ts b/src/ts/server/controllers/torchController.ts new file mode 100644 index 0000000..60b196c --- /dev/null +++ b/src/ts/server/controllers/torchController.ts @@ -0,0 +1,33 @@ +import { isNight } from '../../common/timeUtils'; +import { Controller, ServerEntity, ServerMap } from '../serverInterfaces'; +import { World } from '../world'; +import { timingStart, timingEnd } from '../timing'; +import { updateLights } from '../controllerUtils'; +import { hasFlag } from '../../common/utils'; +import { EntityFlags } from '../../common/interfaces'; + +export class TorchController implements Controller { + private lights: ServerEntity[] = []; + constructor(private world: World, private map: ServerMap) { + } + initialize() { + this.lights = []; + + for (const region of this.map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.flags, EntityFlags.OnOff)) { + this.lights.push(entity); + } + } + } + } + update() { + timingStart('TorchController.update()'); + timingEnd(); + } + sparseUpdate() { + timingStart('TorchController.sparseUpdate()'); + updateLights(this.lights, isNight(this.world.time)); + timingEnd(); + } +} diff --git a/src/ts/server/controllers/updateController.ts b/src/ts/server/controllers/updateController.ts new file mode 100644 index 0000000..de97bef --- /dev/null +++ b/src/ts/server/controllers/updateController.ts @@ -0,0 +1,28 @@ +import { Controller, ServerEntity, ServerMap } from '../serverInterfaces'; +import { timingStart, timingEnd } from '../timing'; + +export class UpdateController implements Controller { + private updatables: ServerEntity[] = []; + constructor(private map: ServerMap) { + } + initialize() { + this.updatables = []; + + for (const region of this.map.regions) { + for (const entity of region.entities) { + if (entity.serverUpdate) { + this.updatables.push(entity); + } + } + } + } + update(delta: number, now: number) { + timingStart('TorchController.update()'); + + for (const entity of this.updatables) { + entity.serverUpdate!(delta, now); + } + + timingEnd(); + } +} diff --git a/src/ts/server/controllers/wallController.ts b/src/ts/server/controllers/wallController.ts new file mode 100644 index 0000000..6542ffe --- /dev/null +++ b/src/ts/server/controllers/wallController.ts @@ -0,0 +1,178 @@ +import { fromByteArray, toByteArray } from 'base64-js'; +import { Controller, ServerMap } from '../serverInterfaces'; +import { World } from '../world'; +import { Entity, TileType } from '../../common/interfaces'; +import { tileHeight } from '../../common/constants'; +import { array } from '../../common/utils'; +import { Walls } from '../../common/entities'; + +const createGetAt = (width: number, height: number) => (items: T[], x: number, y: number) => { + return (x < 0 || y < 0 || x >= width || y >= height) ? undefined : items[x + y * width]; +}; + +const createSetAt = (width: number, height: number) => (items: T[], x: number, y: number, value: T) => { + if (x >= 0 && y >= 0 && x < width && y < height) { + items[x + y * width] = value; + } +}; + +export class WallController implements Controller { + top = 0; + isTall = (_x: number, _y: number) => false; + lockOuterWalls = false; + private lockedTiles = new Set(); + private hWalls: (Entity | undefined)[]; + private vWalls: (Entity | undefined)[]; + constructor(world: World, map: ServerMap, walls: Walls) { + const width = map.width + 1; + const height = map.height + 1; + + const getAt = createGetAt(width, height); + const setAt = createSetAt(width, height); + + const hWalls = this.hWalls = array(width * height, undefined); + const vWalls = this.vWalls = array(width * height, undefined); + const cWalls = array(width * height, undefined); + + const yOffset = 3 / tileHeight; + + const { wallHShort, wallVShort, wallH, wallV, wallCorners, wallCornersShort, wallCutR, wallCutL } = walls; + + const calcCorner = (x: number, y: number) => { + // top right bottom left + return (getAt(vWalls, x, y - 1) ? 8 : 0) + + (getAt(hWalls, x, y) ? 4 : 0) + + (getAt(vWalls, x, y) ? 2 : 0) + + (getAt(hWalls, x - 1, y) ? 1 : 0); + }; + + const updateCorner = (x: number, y: number) => { + if (x < 0 || y < 0 || x >= width || y >= height) + return; + + const top = this.top; + const isOutside = x === 0 || y <= top || x === map.width || this.isTall(x, y); + const corners = isOutside ? wallCorners : wallCornersShort; + const current = getAt(cWalls, x, y); + const calc = calcCorner(x, y); + + if (!current || current.type !== corners[calc].type) { + if (current) { + world.removeEntity(current, map); + } + + setAt(cWalls, x, y, calc ? world.addEntity(corners[calc](x, y + yOffset), map) : undefined); + } + }; + + this.toggleWall = (x, y, type) => { + if (x < 0 || y < 0 || x >= width || y >= height) + return; + + if (this.lockedTiles.has(`${x},${y}:${type}`)) + return; + + const walls = type === TileType.WallH ? hWalls : vWalls; + const entity = getAt(walls, x, y); + const top = this.top; + + if (type === TileType.WallH && x === (width - 1)) + return; + + if (type === TileType.WallV && y === (height - 1)) + return; + + if (this.lockOuterWalls) { + if (type === TileType.WallH && (y <= top || y === (width - 1))) + return; + if (type === TileType.WallV && (x === 0 || x === (height - 1) || y < top)) + return; + } + + if (entity) { + world.removeEntity(entity, map); + setAt(walls, x, y, undefined); + } else { + if (type === TileType.WallH) { + const ctor = (y <= top || this.isTall(x, y)) ? + wallH : (x === 0 ? wallCutL : (x === (width - 2) ? wallCutR : wallHShort)); + setAt(walls, x, y, world.addEntity(ctor(x + 0.5, y + yOffset), map)); + } else { + const ctor = (x === 0 || x === (width - 1) || this.isTall(x, y)) ? wallV : wallVShort; + setAt(walls, x, y, world.addEntity(ctor(x, y + 0.5), map)); + } + } + + updateCorner(x, y); + updateCorner(x + 1, y); + updateCorner(x, y + 1); + }; + } + initialize() { + } + update() { + } + toggleWall?: (x: number, y: number, type: TileType) => void; + lockWall(x: number, y: number, type: TileType.WallH | TileType.WallV) { + this.lockedTiles.add(`${x},${y}:${type}`); + } + serialize() { + const data = new Uint8Array(Math.ceil(this.vWalls.length / 8) + Math.ceil(this.hWalls.length / 8)); + let offset = 0; + + for (let i = 0; i < this.vWalls.length; i += 8, offset++) { + let value = 0; + + for (let j = 0; j < 8; j++) { + if (this.vWalls[i + j]) { + value |= (1 << j); + } + } + + data[offset] = value; + } + + for (let i = 0; i < this.hWalls.length; i += 8, offset++) { + let value = 0; + + for (let j = 0; j < 8; j++) { + if (this.hWalls[i + j]) { + value |= (1 << j); + } + } + + data[offset] = value; + } + + return fromByteArray(data); + } + deserialize(width: number, height: number, serialized: string) { + const data = toByteArray(serialized); + const size = (width + 1) * (height + 1); + let offset = 0; + + for (let i = 0; i < size; i += 8, offset++) { + let value = data[offset]; + + for (let j = 0; j < 8; j++) { + if ((!!this.vWalls[i + j]) !== ((value & (1 << j)) !== 0)) { + const x = (i + j) % (width + 1); + const y = Math.floor((i + j) / (width + 1)); + this.toggleWall!(x, y, TileType.WallV); + } + } + } + + for (let i = 0; i < size; i += 8, offset++) { + let value = data[offset]; + + for (let j = 0; j < 8; j++) { + if ((!!this.hWalls[i + j]) !== ((value & (1 << j)) !== 0)) { + const x = (i + j) % (width + 1); + const y = Math.floor((i + j) / (width + 1)); + this.toggleWall!(x, y, TileType.WallH); + } + } + } + } +} diff --git a/src/ts/server/db.ts b/src/ts/server/db.ts new file mode 100644 index 0000000..5a42fd4 --- /dev/null +++ b/src/ts/server/db.ts @@ -0,0 +1,485 @@ +import { model, Schema, Types, Document, Query } from 'mongoose'; +import { + TimestampsBase, EventBase, CharacterBase, AccountBase, AuthBase, OriginBase, OriginInfoBase, CharacterState, + SupporterInviteBase, FriendRequestBase, HideRequestBase, MergeHideData +} from '../common/adminInterfaces'; +import { logger } from './logger'; +import { isAdmin } from '../common/accountUtils'; +import { FriendData } from '../common/interfaces'; +import { replaceEmojis } from '../client/emoji'; +import { filterForbidden } from './characterUtils'; +import { filterName } from '../common/swears'; + +//set('debug', true); // debug mongoose + +export interface Doc extends Document { + updatedAt: Date; +} + +export interface IOriginInfo extends OriginInfoBase { } +export interface ITimestamps extends TimestampsBase { } +export interface IAuth extends AuthBase, Doc { } +export interface IOrigin extends OriginBase, Doc { } +export interface IEvent extends EventBase, Doc { } +export interface ISupporterInvite extends SupporterInviteBase, Doc { } +export interface IFriendRequest extends FriendRequestBase, Doc { } +export interface IHideRequest extends HideRequestBase, Doc { } + +export interface ICharacter extends CharacterBase, Doc { + auth?: IAuth; +} + +export interface IAccount extends AccountBase, Doc { + auths?: IAuth[]; + characters?: ICharacter[]; +} + +export interface ISession extends Doc { + session: string; +} + +// schemas + +const originInfo = { + ip: String, + country: String, + last: Date, +}; + +const mergeInfo = { + id: String, + name: String, + //code: Number, + date: Date, + reason: String, + data: Object, + split: Boolean, +}; + +const logEntry = { + message: String, + date: Date, +}; + +const authSchema = new Schema({ + account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + openId: String, + provider: String, + name: String, + url: String, + emails: [String], + disabled: Boolean, + banned: Boolean, + pledged: Number, + lastUsed: Date, +}, { timestamps: true }); + +authSchema.index({ updatedAt: 1 }); +authSchema.index({ openId: 1, provider: 1 }, { unique: true }); + +const bannedMuted = { + mute: Number, + shadow: Number, + ban: Number, +}; + +const originSchema = new Schema({ + ip: { type: String, index: true }, + country: String, + ...bannedMuted, +}, { timestamps: true }); + +originSchema.index({ updatedAt: 1 }); + +const accountSchema = new Schema({ + name: String, + birthdate: Date, + birthyear: Number, + // code: Number, + emails: { type: [String], index: true }, + lastVisit: Date, + lastUserAgent: String, + lastBrowserId: String, + lastOnline: Date, + lastCharacter: Schema.Types.ObjectId, + roles: [String], + origins: [originInfo], + note: String, + noteUpdated: Date, + ignores: [String], + // friends: [{ type: Schema.Types.ObjectId, unique: true, ref: 'Account' }], + flags: Number, + characterCount: { type: Number, default: 0 }, + // NOTE: use account.markModified('settings') if changed nested field + settings: { type: Schema.Types.Mixed, default: () => ({}) }, + counters: { type: Schema.Types.Mixed, default: () => ({}) }, + patreon: Number, + supporter: Number, + supporterLog: [logEntry], + supporterTotal: Number, + supporterDeclinedSince: Date, + merges: [mergeInfo], + banLog: [logEntry], + mute: Number, + shadow: Number, + ban: Number, + // auths: [{ type: Schema.Types.ObjectId, ref: 'Auth' }], + state: Object, + alert: Object, + savedMap: String, +}, { timestamps: true }); + +accountSchema.virtual('auths', { + ref: 'Auth', + localField: '_id', + foreignField: 'account', +}); + +accountSchema.virtual('characters', { + ref: 'Character', + localField: '_id', + foreignField: 'account', +}); + +accountSchema.index({ updatedAt: 1 }); + +const characterSchema = new Schema({ + account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + site: { type: Schema.Types.ObjectId, ref: 'Auth' }, + name: { type: String, index: true }, + desc: String, + tag: String, + info: String, + flags: { type: Number, default: 0 }, + lastUsed: { type: Date, index: true }, + creator: String, + state: Object, +}, { timestamps: true }); + +characterSchema.index({ updatedAt: 1 }); +characterSchema.index({ createdAt: 1 }); + +const eventSchema = new Schema({ + account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + pony: Schema.Types.ObjectId, + type: String, + server: String, + message: String, + desc: String, + origin: originInfo, + count: { type: Number, default: 1 }, +}, { timestamps: true }); + +eventSchema.index({ updatedAt: 1 }); + +const supporterInviteSchema = new Schema({ + source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + name: String, + info: String, + active: Boolean, +}, { timestamps: true }); + +const friendRequestSchema = new Schema({ + source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, +}); + +const hideRequestSchema = new Schema({ + source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + name: String, + date: Date, +}); + +const sessionSchema = new Schema({ + _id: String, + session: String, +}); + +// models + +export const Auth = model('Auth', authSchema); +export const Event = model('Event', eventSchema); +export const Origin = model('Origin', originSchema); +export const Session = model('session', sessionSchema); +export const Character = model('Character', characterSchema); + +accountSchema.post('remove', function (doc: Document) { + Promise.all([ + Character.deleteMany({ account: doc._id }).exec(), + Event.deleteMany({ account: doc._id }).exec(), + Auth.deleteMany({ account: doc._id }).exec(), + FriendRequest.deleteMany({ $or: [{ target: doc._id }, { source: doc._id }] }).exec(), + HideRequest.deleteMany({ $or: [{ target: doc._id }, { source: doc._id }] }).exec(), + ]).catch(logger.error); +}); + +export const Account = model('Account', accountSchema); +export const SupporterInvite = model('SupporterInvite', supporterInviteSchema); +export const FriendRequest = model('FriendRequest', friendRequestSchema); +export const HideRequest = model('HideRequest', hideRequestSchema); + +// helpers + +export type ID = Types.ObjectId | string; + +export interface MongoQueryExpr { + $exists?: boolean; + $ne?: T; + $in?: T | T[]; + $gt?: T; + $lt?: T; + $not?: MongoQueryExpr; + $size?: number; + $regex?: RegExp; +} + +export interface MongoUpdateExprField { + $inc?: any; + $dec?: any; + $pull?: any; + $push?: any; + $unset?: { + [P in keyof T]?: any; + }; +} + +export interface MongoUpdateExpr extends MongoUpdateExprField { + $addToSet?: any; +} + +export type MongoQuery = { + [P in keyof T]?: T[P] | MongoQueryExpr; +}; + +export type MongoUpdate = { + [P in keyof T]?: T[P] | MongoUpdateExprField; +} & MongoUpdateExpr; + +export function iterate(query: Query, onData: (doc: T) => void) { + return new Promise(resolve => { + query.cursor() + .on('data', onData) + .on('end', resolve); + }); +} + +function throwOnEmpty(message: string): (item: T | undefined) => T { + return item => { + if (item) { + return item; + } else { + throw new Error(message); + } + }; +} + +export function nullToUndefined(item: T | null): T | undefined { + return item === null ? undefined : item; +} + +export const checkCharacterExists = throwOnEmpty('Character does not exist'); +export const checkAccountExists = throwOnEmpty('Account does not exist'); + +// characters + +export type CreateCharacter = (account: IAccount) => ICharacter; +export type CharacterCount = (accountId: ID) => Promise; +export type FindCharacter = (characterId: ID, accountId: ID) => Promise; +export type FindCharacterSafe = (characterId: ID, accountId: ID) => Promise; +export type FindCharacters = (accountId: ID, fields?: string) => Promise; +export type UpdateCharacterState = (characterId: ID, serverName: string, state: CharacterState) => Promise; +export type QueryCharacter = (query: MongoQuery, fields?: string) => Promise; + +export function createCharacter(account: IAccount) { + return new Character({ account: account._id, creator: `${account.name} [${account._id}]` }); +} + +export function characterCount(account: ID): Promise { + return Character.countDocuments({ account }).exec(); +} + +export function findCharacter(pony: ID, account: ID): Promise { + return Character.findOne({ _id: pony, account }).exec().then(nullToUndefined); +} + +export function findCharacterSafe(pony: ID, accountId: ID): Promise { + return findCharacter(pony, accountId) + .then(checkCharacterExists); +} + +export function findCharacterById(id: string): Promise { + return Character.findById(id).exec().then(nullToUndefined); +} + +export const findAllCharacters: FindCharacters = (account, fields) => + Character.find({ account }, fields).lean().exec(); + +export function findLatestCharacters(account: ID, count: number): Promise { + return Character.find({ account }) + .sort('-lastUsed') + .limit(count) + .exec(); +} + +export function removeCharacter(id: ID, account: ID): Promise { + return Character.findOneAndRemove({ _id: id, account }).exec().then(nullToUndefined); +} + +export const updateCharacterState: UpdateCharacterState = (characterId, serverName, state) => + Character.updateOne({ _id: characterId }, { [`state.${serverName}`]: state }).exec().then(nullToUndefined); + +export const queryCharacter: QueryCharacter = (query, fields) => + Character.findOne(query, fields).exec() as any; + +// auths + +export type FindAuth = (authId: ID, accountId: ID, fields?: string) => Promise; +export type FindAuths = (accountId: ID, fields?: string) => Promise; +export type CountAuths = (accountId: ID) => Promise; +export type QueryAuths = (query: MongoQuery, fields?: string) => Promise; +export type UpdateAuth = (authId: ID, update: MongoUpdate) => Promise; + +export const findAuthByOpenId = (openId: string, provider: string): Promise => + Auth.findOne({ openId, provider }).exec().then(nullToUndefined); + +export const findAuthByEmail = (emails: string[]): Promise => + Auth.findOne({ emails: { $in: emails } }).exec().then(nullToUndefined); + +export const findAuth: FindAuth = (auth, account, fields) => + Auth.findOne({ _id: auth, account }, fields).exec().then(nullToUndefined); + +export const findAllAuths: FindAuths = (account, fields) => + Auth.find({ account, fields }).exec(); + +export const findAllVisibleAuths: FindAuths = (account, fields) => + Auth.find({ account, disabled: { $ne: true }, banned: { $ne: true } }, fields).lean().exec(); + +export const countAllVisibleAuths: CountAuths = (account) => + Auth.find({ account, disabled: { $ne: true }, banned: { $ne: true } }).countDocuments().exec(); + +export const queryAuths: QueryAuths = (query, fields) => + Auth.find(query, fields).lean().exec(); + +export const updateAuth: UpdateAuth = (id, update) => + Auth.updateOne({ _id: id }, update).exec(); + +// accounts + +export type FindAccountSafe = (accountId: ID, projection?: string) => Promise; +export type UpdateAccount = (accountId: ID, update: MongoUpdate) => Promise; +export type UpdateAccounts = (query: MongoQuery, update: MongoUpdate) => Promise; +export type QueryAccounts = (query: MongoQuery, fields?: string) => Promise; +export type QueryAccount = (query: MongoQuery, fields?: string) => Promise; + +export const findAccount = (account: ID, projection?: string): Promise => + Account.findById(account, projection).exec().then(nullToUndefined); + +export function checkIfAdmin(account: ID): Promise { + return Account.findOne({ _id: account }, 'roles').lean().exec() + .then(a => a && isAdmin(a)); +} + +export function findAccountSafe(account: ID, projection?: string): Promise { + return findAccount(account, projection) + .then(checkAccountExists); +} + +export const updateAccount: UpdateAccount = (accountId, update) => + Account.updateOne({ _id: accountId }, update).exec(); + +export const updateAccounts: UpdateAccounts = (query, update) => + Account.updateMany(query, update).exec(); + +export const queryAccounts: QueryAccounts = (query, fields) => + Account.find(query, fields).lean().exec(); + +export const queryAccount: QueryAccount = (query, fields) => + Account.findOne(query, fields).exec().then(nullToUndefined); + +// supporter invites + +export type HasActiveSupporterInvites = (accountId: ID) => Promise; + +export const hasActiveSupporterInvites: HasActiveSupporterInvites = (accountId) => + SupporterInvite.countDocuments({ target: accountId, active: true }).exec() + .then(count => count > 0); + +// friend requests + +export async function findFriendIds(accountId: ID) { + const accountIdString = accountId.toString(); + + const friendRequests = await FriendRequest + .find({ $or: [{ source: accountId }, { target: accountId }] }, 'source target') + .lean() + .exec(); + + const friendIds = friendRequests + .map((f: any) => f.source.toString() === accountIdString ? f.target.toString() : f.source.toString()); + + return friendIds; +} + +export async function findFriends(accountId: ID, withCharacters: boolean): Promise { + const friendIds = await findFriendIds(accountId); + const accounts: IAccount[] = await Account.find({ _id: { $in: friendIds } }, '_id name lastOnline lastCharacter').lean().exec(); + let characters: ICharacter[] = []; + + if (withCharacters) { + const characterIds = accounts.map(a => a.lastCharacter).filter(id => id); + characters = await Character.find({ _id: { $in: characterIds } }, '_id name info').lean().exec(); + } + + return accounts.map(a => { + const characterId = a.lastCharacter && a.lastCharacter.toString(); + const character = characterId && characters.find(c => c._id.toString() === characterId); + const name = character && filterForbidden(replaceEmojis(character.name)); + const nameFiltered = name && filterName(name); + + return { + accountId: a._id.toString(), + accountName: a.name, + name, + pony: character && character.info, + nameBad: name !== nameFiltered, + }; + }); +} + +// hide requests + +export async function findHideIds(accountId: ID) { + const hideRequests: IHideRequest[] = await HideRequest.find({ source: accountId }, 'target').lean().exec(); + return hideRequests.map(f => f.target.toString()); +} + +export async function findHideIdsRev(accountId: ID) { + const hideRequests: IHideRequest[] = await HideRequest.find({ target: accountId }, 'source').lean().exec(); + return hideRequests.map(f => f.source.toString()); +} + +export async function findHidesForMerge(accountId: ID): Promise { + const hideRequests: IHideRequest[] = await HideRequest + .find({ source: accountId }, '_id name date') + .lean() + .exec(); + + return hideRequests.map(f => ({ + id: f._id.toString(), + name: f.name, + date: f.date.toString(), + })); +} + +export async function addHide(source: ID, target: ID, name: string) { + if (source.toString() === target.toString()) + return; + + const existing = await HideRequest.findOne({ source, target }, '_id').lean().exec(); + + if (!existing) { + await HideRequest.create({ source, target, name, date: new Date() }); + } +} diff --git a/src/ts/server/entityUtils.ts b/src/ts/server/entityUtils.ts new file mode 100644 index 0000000..a62fd7b --- /dev/null +++ b/src/ts/server/entityUtils.ts @@ -0,0 +1,346 @@ +import { resizeWriter, writeUint8, BinaryWriter, writeUint32, writeUint16 } from 'ag-sockets'; +import { encodeString } from 'ag-sockets/dist/utf8'; +import { + Entity, Rect, EntityState, UpdateFlags, Action, EntityOrPonyOptions, UpdateType, TileType, canWalk, setAnimationToEntityState +} from '../common/interfaces'; +import { normalize, containsPoint, boundsIntersect, clamp, pointInXYWH, hasFlag, setFlag } from '../common/utils'; +import { ServerEntity, ServerEntityWithClient, ServerMap, EntityUpdateBase, IClient } from './serverInterfaces'; +import { + isCritter, isDecal, entityInRange, SIT_ON_BOUNDS_WIDTH, SIT_ON_BOUNDS_HEIGHT, SIT_ON_BOUNDS_OFFSET +} from '../common/entityUtils'; +import { pushUpdateEntityToRegion } from './serverRegion'; +import { getRegion, getRegionGlobal, getTile } from '../common/worldMap'; +import { filterName } from '../common/swears'; +import { shouldBeFacingRight } from '../common/movementUtils'; +import { writeOneEntity, writeOneUpdate } from '../common/encoders/updateEncoder'; +import { PONY_TYPE } from '../common/constants'; +import { grapesPurple, grapesGreen } from '../common/entities'; + +export function isEntityShadowed(entity: ServerEntity): entity is ServerEntityWithClient { + return entity.client !== undefined && entity.client.shadowed; +} + +export function setEntityName(entity: ServerEntity, name: string) { + entity.name = name; + entity.nameBad = name !== filterName(name); + entity.encodedName = encodeString(name)!; +} + +export function getEntityName(entity: ServerEntity, client: IClient) { + if (entity.name && entity.nameBad && client.accountSettings.filterSwearWords) { + return filterName(entity.name); + } else { + return entity.name; + } +} + +const grapeTypes = [...grapesPurple.map(x => x.type), ...grapesGreen.map(x => x.type)]; + +export function isHoldingGrapes(e: ServerEntity) { + const hold = e.options!.hold || 0; + return hold !== 0 && grapeTypes.indexOf(hold) !== -1; +} + +export function canBoopEntity(e: ServerEntity, boopRect: Rect) { + if (e.type === PONY_TYPE) { + return isHoldingGrapes(e); + } else { + return e.boop !== undefined && containsPoint(0, 0, boopRect, e.x + (e.boopX || 0), e.y + (e.boopY || 0)); + } +} + +function distSq(ax: number, ay: number, bx: number, by: number) { + const dx = ax - bx; + const dy = ay - by; + return dx * dx + dy * dy; +} + +export function findClosest(x: number, y: number, entities: Entity[]) { + let closest = entities[0]; + let distance = closest ? distSq(x, y, closest.x, closest.y) : 0; + + for (let i = 1; i < entities.length; i++) { + const entity = entities[i]; + const dist = distSq(x, y, entity.x, entity.y); + + if (dist < distance) { + closest = entity; + distance = dist; + } + } + + return closest; +} + +export function moveRandomly( + map: ServerMap, e: ServerEntity, speed: number, randomness: number, timestamp: number +) { + if (Math.random() < randomness) { + let vx = 0; + let vy = 0; + + if (e.x < 0) { + vx = 1; + } else if (e.x > map.width) { + vx = -1; + } else if (e.y < 0) { + vy = 1; + } else if (e.y > map.height) { + vy = -1; + } else { + vx = Math.random() - 0.5; + vy = Math.random() - 0.5; + } + + updateEntityVelocity(e, vx * speed, vy * speed, timestamp); + } +} + +export function moveTowards(e: ServerEntity, x: number, y: number, speed: number, timestamp: number) { + const v = normalize(x - e.x, y - e.y); + updateEntityVelocity(e, v.x * speed, v.y * speed, timestamp); +} + +// update entity functions + +export function setEntityAnimation(entity: ServerEntity, animation: number, faceRight?: boolean) { + let state = entity.state; + + if (faceRight !== undefined) { + state = setFlag(state, EntityState.FacingRight, faceRight); + } + + state = setAnimationToEntityState(state, animation); + updateEntityState(entity, state); +} + +export function updateEntityVelocity(entity: ServerEntity, vx: number, vy: number, timestamp: number) { + if (vx !== entity.vx || vy !== entity.vy) { + entity.vx = vx; + entity.vy = vy; + entity.timestamp = timestamp; + entity.state = setFlag(entity.state, EntityState.FacingRight, shouldBeFacingRight(entity)); + updateEntity(entity, false); + } +} + +export function updateEntity(entity: ServerEntity, switchRegion: boolean) { + const flags = UpdateFlags.Position | UpdateFlags.State | (switchRegion ? UpdateFlags.SwitchRegion : 0); + const { x, y, vx, vy } = entity; + pushUpdateEntity({ entity, flags, x, y, vx, vy }); +} + +export function updateEntityState(entity: ServerEntity, state: EntityState) { + entity.state = state; + pushUpdateEntity({ entity, flags: UpdateFlags.State }); +} + +export function updateEntityOptions(entity: ServerEntity, options: Partial) { + entity.options = Object.assign(entity.options || {}, options) as any; + pushUpdateEntity({ entity, flags: UpdateFlags.Options, options }); +} + +export function updateEntityNameInfo(entity: ServerEntity) { + pushUpdateEntity({ entity, flags: UpdateFlags.Name | UpdateFlags.Info }); +} + +export function updateEntityExpression(entity: ServerEntity) { + pushUpdateEntity({ entity, flags: UpdateFlags.Expression }); +} + +export function sendAction(entity: ServerEntity, action: Action) { + pushUpdateEntity({ entity, flags: UpdateFlags.Action, action }); +} + +export function pushUpdateEntity(update: EntityUpdateBase) { + const entity = update.entity; + + if (isEntityShadowed(entity)) { + pushUpdateEntityToClient(entity.client, update); + } else if (entity.region) { + pushUpdateEntityToRegion(entity.region, update); + } +} + +export function isOverflowError(e: Error) { + return e instanceof RangeError || /DataView/.test(e.message); +} + +function resizePreserveWriter(error: Error, writer: BinaryWriter, offset: number) { + if (isOverflowError(error)) { + const bytes = writer.bytes; + resizeWriter(writer); + writer.bytes.set(bytes); + writer.offset = offset; + // DEVELOPMENT && logger.debug(`resize writer to ${writer.bytes.byteLength} (${error.message})`); + } else { + throw error; + } +} + +export function pushAddEntityToClient(client: IClient, entity: ServerEntity) { + const writer = client.updateQueue; + const offset = writer.offset; + + while (true) { + try { + writeUint8(writer, UpdateType.AddEntity); + writeOneEntity(writer, entity, client); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } +} + +export function pushUpdateEntityToClient(client: IClient, update: EntityUpdateBase) { + const writer = client.updateQueue; + const offset = writer.offset; + const { entity, flags, x = 0, y = 0, vx = 0, vy = 0, options, action = 0, playerState = 0 } = update; + + while (true) { + try { + writeUint8(writer, UpdateType.UpdateEntity); + writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } +} + +export function pushRemoveEntityToClient(client: IClient, entity: ServerEntity) { + const writer = client.updateQueue; + const offset = writer.offset; + + while (true) { + try { + writeUint8(writer, UpdateType.RemoveEntity); + writeUint32(writer, entity.id); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } +} + +export function pushUpdateTileToClient(client: IClient, x: number, y: number, type: TileType) { + const writer = client.updateQueue; + const offset = writer.offset; + + while (true) { + try { + writeUint8(writer, UpdateType.UpdateTile); + writeUint16(writer, x); + writeUint16(writer, y); + writeUint8(writer, type); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } +} + +// other helpers + +export function findIntersectingEntityByBounds(map: ServerMap, entity: ServerEntity) { + const { x, y } = getRegionGlobal(map, entity.x, entity.y); + const minX = Math.max(x - 1, 0); + const minY = Math.max(y - 1, 0); + const maxX = Math.min(x + 1, map.regionsX - 1); + const maxY = Math.min(y + 1, map.regionsY - 1); + + for (let iy = minY; iy <= maxY; iy++) { + for (let ix = minX; ix <= maxX; ix++) { + const region = getRegion(map, ix, iy); + + for (const e of region.entities) { + if (e !== entity && !isDecal(e) && !isCritter(e) && boundsIntersect(entity.x, entity.y, entity.bounds, e.x, e.y, e.bounds)) { + return e; + } + } + } + } + + return undefined; +} + +export function findPlayerThatCanPickEntity(map: ServerMap, entity: ServerEntity) { + const { x, y } = getRegionGlobal(map, entity.x, entity.y); + const minX = Math.max(x - 1, 0); + const minY = Math.max(y - 1, 0); + const maxX = Math.min(x + 1, map.regionsX - 1); + const maxY = Math.min(y + 1, map.regionsY - 1); + + for (let iy = minY; iy <= maxY; iy++) { + for (let ix = minX; ix <= maxX; ix++) { + const region = getRegion(map, ix, iy); + + for (const e of region.entities) { + if (e.client !== undefined && entityInRange(entity, e)) { + return e; + } + } + } + } + + return undefined; +} + +export function findPlayersThetCanBeSitOn(map: ServerMap, entity: ServerEntity) { + const { x, y } = getRegionGlobal(map, entity.x, entity.y); + const minX = Math.max(x - 1, 0); + const minY = Math.max(y - 1, 0); + const maxX = Math.min(x + 1, map.regionsX - 1); + const maxY = Math.min(y + 1, map.regionsY - 1); + + for (let iy = minY; iy <= maxY; iy++) { + for (let ix = minX; ix <= maxX; ix++) { + const region = getRegion(map, ix, iy); + + for (const e of region.entities) { + if (e !== entity && e.client !== undefined && canBeSitOn(e, entity)) { + return e; + } + } + } + } + + return undefined; +} + +function canBeSitOn(entity: ServerEntity, by: ServerEntity) { + const right = hasFlag(by.state, EntityState.FacingRight); + const entityRight = hasFlag(entity.state, EntityState.FacingRight); + + if (right !== entityRight) { + return false; + } + + const x = by.x + (right ? -SIT_ON_BOUNDS_OFFSET : (SIT_ON_BOUNDS_OFFSET - SIT_ON_BOUNDS_WIDTH)); + const y = by.y - SIT_ON_BOUNDS_HEIGHT / 2; + const w = SIT_ON_BOUNDS_WIDTH; + const h = SIT_ON_BOUNDS_HEIGHT; + return pointInXYWH(entity.x, entity.y, x, y, w, h); +} + +export function canPlaceItem(map: ServerMap, entity: ServerEntity) { + const tile = getTile(map, entity.x, entity.y); + return canWalk(tile) && tile !== TileType.Water && tile !== TileType.Boat && + !findIntersectingEntityByBounds(map, entity); +} + +export function canBePickedByPlayer(map: ServerMap, entity: ServerEntity) { + return !!findPlayerThatCanPickEntity(map, entity); +} + +export function fixPosition(entity: ServerEntity, map: ServerMap, x: number, y: number, safe: boolean) { + entity.x = clamp(x, 0, map.width); + entity.y = clamp(y, 0, map.height); + updateEntity(entity, false); + + if (entity.client) { + entity.client.fixPosition(entity.x, entity.y, safe); + entity.client.fixingPosition = true; + } +} diff --git a/src/ts/server/internal.ts b/src/ts/server/internal.ts new file mode 100644 index 0000000..c98977a --- /dev/null +++ b/src/ts/server/internal.ts @@ -0,0 +1,182 @@ +import * as request from 'request-promise'; +import { noop, flatMap, uniq } from 'lodash'; +import { + InternalGameServerState, ServerStatus, InternalLoginApi, InternalLoginServerState, InternalApi, HidingStats +} from '../common/adminInterfaces'; +import { isMod } from '../common/accountUtils'; +import { findById, flatten, delay } from '../common/utils'; +import { config, args, gameServers } from './config'; +import { logger } from './logger'; +import { IAccount, ICharacter, findAccountSafe, findHideIds, findHideIdsRev } from './db'; +import { createAccountChanged } from './api/internal'; +import { TokenService } from './serverInterfaces'; +import { World } from './world'; +import { InternalAdminApi } from './api/internal-admin'; +import { EndPoints } from './api/admin'; +import { UserError } from './userError'; +import { AdminService } from './services/adminService'; +// import { taskQueue } from './utils/taskQueue'; + +export const serverStatus: ServerStatus = { + diskSpace: '', + memoryUsage: '', + certificateExpiration: '', + lastPatreonUpdate: '', +}; + +export const loginServers: InternalLoginServerState[] = [ + { + id: 'login', + state: { + updating: false, + dead: true, + }, + api: createApi(config.local, 'api-internal-login', config.token), + }, +]; + +export const adminServer = config.adminLocal && !args.admin ? { + id: 'admin', + api: createApi(config.adminLocal, 'api-internal-admin', config.token), +} : undefined; + +export const servers: InternalGameServerState[] = []; + +if (args.login || args.admin) { + servers.push(...gameServers.map(s => ({ + id: s.id, + state: { + ...s, + offline: true, + dead: true, + maps: 0, + online: 0, + onMain: 0, + queued: 0, + shutdown: false, + filter: false, + settings: {}, + }, + api: createApi(s.local, 'api-internal', config.token), + }))); +} + +export function findServer(id: string) { + return findById(servers, id); +} + +export function getLoginServer(_id: string) { + return loginServers[0]; +} + +export function getServer(id: string) { + const server = findServer(id); + + if (!server) { + throw new Error(`Invalid server ID (${id})`); + } + + return server; +} + +export function createApi(host: string, url: string, apiToken: string): T { + return new Proxy({} as any, { + get: (_, key) => + (...args: any[]) => + Promise.resolve(request(`http://${host}/${url}/api`, { + json: true, + headers: { 'api-token': apiToken }, + method: 'post', + body: { method: key, args }, + })), + }); +} + +function mapGameServers(action: (server: InternalGameServerState) => Promise | T) { + return Promise.all(servers.filter(s => !s.state.dead).map(action)); +} + +export function createJoin(): typeof join { + return join; +} + +async function join(joinServer: InternalGameServerState, account: IAccount, character: ICharacter): Promise { + try { + const kicked = await mapGameServers(s => { + if (isMod(account) && s !== joinServer) { + return false; + } else { + return s.api.kick(account._id.toString(), undefined).catch(e => (logger.error(e), false)); + } + }); + + if (kicked.some(x => x)) { + await delay(2000); + } + + return await joinServer.api.join(account._id.toString(), character._id.toString()); + } catch (error) { + if (error.error && error.error.userError) { + throw new UserError(error.error.error); + } else { + logger.error(error); + throw new Error('Internal error'); + } + } +} + +let accountChangedHandler = (_accountId: string) => Promise.resolve(); + +export function init(world: World, tokens: TokenService) { + accountChangedHandler = createAccountChanged(world, tokens, findAccountSafe); +} + +export async function accountChanged(accountId: string) { + if (args.login || args.admin) { + await mapGameServers(s => { + s.api.accountChanged(accountId).catch(noop); + }); + } else { + await accountChangedHandler(accountId); + } +} + +export async function accountMerged(accountId: string, mergedId: string) { + await mapGameServers(s => { s.api.accountMerged(accountId, mergedId).catch(noop); }); +} + +export async function accountStatus(accountId: string) { + const statuses = await mapGameServers(s => s.api.accountStatus(accountId).catch(() => ({ online: false }))); + return statuses.filter(s => !!s.online); +} + +export async function accountAround(accountId: string) { + const users = await mapGameServers(s => s.api.accountAround(accountId).catch(() => [])); + return flatten(users).sort((a, b) => a.distance - b.distance).slice(0, 10); +} + +export async function accountHidden(accountId: string): Promise { + const [users, permaHidden, permaHiddenBy] = await Promise.all([ + mapGameServers(s => s.api.accountHidden(accountId).catch(() => ({ account: '', hidden: [], hiddenBy: [] }))), + findHideIds(accountId), + findHideIdsRev(accountId), + ]); + + return { + account: accountId, + hidden: uniq(flatMap(users, u => u.hidden)), + hiddenBy: uniq(flatMap(users, u => u.hiddenBy)), + permaHidden, + permaHiddenBy, + }; +} + +export type RemovedDocument = ReturnType; + +export const createRemovedDocument = + (endPoints: EndPoints | undefined, adminService: AdminService | undefined) => + (model: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) => { + endPoints && model in endPoints && (endPoints as any)[model].removedItem(id); + adminService && adminService.removedItem(model, id); + return adminServer ? adminServer.api.removedDocument(model, id).catch(noop) : Promise.resolve(); + }; diff --git a/src/ts/server/ipc.ts b/src/ts/server/ipc.ts new file mode 100644 index 0000000..defa8ed --- /dev/null +++ b/src/ts/server/ipc.ts @@ -0,0 +1,98 @@ +import * as ipc from 'node-ipc'; + +export interface LoginServer { + hello(message: string): Promise; +} + +export interface GameServer { + something(): Promise; +} + +interface SocketState { + server: TServer; + client: TClient; +} + +export function startIPCServer( + id: string, createServer: (client: TClient) => TServer +) { + ipc.config.id = id; + ipc.config.retry = 500; + ipc.config.silent = true; + ipc.serve(() => { + const sockets = new Map>(); + + ipc.server.on('connect', (socket) => { + console.log('server:connect'); + const client: TClient = new Proxy({} as any, { + get: (_, key) => (...args: any[]) => { + ipc.server.emit(socket, 'message', [key, args]); + }, + }); + const server = createServer(client); + sockets.set(socket, { server, client }); + }); + + ipc.server.on('message', (data, socket) => { + console.log('server:message', data); + const socketState = sockets.get(socket); + + if (socketState) { + (socketState.server as any)[data[0]](...data[1]); + } else { + console.error('missing server for socket'); + } + }); + + ipc.server.on('error', (error) => { + console.log('server:error', error); + }); + + ipc.server.on('disconnect', (socket) => { + console.log('server:disconnect'); + sockets.delete(socket); + }); + + ipc.server.on('socket.disconnected', (socket, _destroyedSocketID) => { + console.log('server:socket.disconnected'); + sockets.delete(socket); + }); + }); + + ipc.server.start(); +} + +export function startIPCClient( + serverId: string, clientId: string, createClient: (server: TServer) => TClient +) { + ipc.config.id = clientId; + ipc.config.retry = 500; + ipc.config.silent = true; + ipc.connectTo(serverId, () => { + let connected = false; + const socket = ipc.of[serverId]; + const server: TServer = new Proxy({} as any, { + get: (_, key) => (...args: any[]) => { + socket.emit('message', [key, args]); + }, + }); + const client = createClient(server); + + socket.on('connect', () => { + connected = true; + console.log('client:connect'); + }); + + socket.on('disconnect', () => { + if (connected) { + connected = false; + console.log('client:disconnect'); + } + }); + + socket.on('message', (data: any) => { + console.log('client:message', data); + (client as any)[data[0]](...data[1]); + }); + }); +} diff --git a/src/ts/server/liveEndPoint.ts b/src/ts/server/liveEndPoint.ts new file mode 100644 index 0000000..a605eb7 --- /dev/null +++ b/src/ts/server/liveEndPoint.ts @@ -0,0 +1,170 @@ +import * as Promise from 'bluebird'; +import { Model } from 'mongoose'; +import { remove, noop } from 'lodash'; +import { ITEM_LIMIT, LiveResponse, BaseValues } from '../common/adminInterfaces'; +import { MINUTE } from '../common/constants'; +import { fromNow } from '../common/utils'; +import { Doc } from './db'; +import { logger } from './logger'; + +export interface LiveEndPoint { + get(id: string): Promise; + getAll(timestamp?: string): Promise; + assignAccount(id: string, account: string): Promise; + removeItem(id: string): Promise; + removedItem(id: string): void; + encodeItems(items: any[], timestamp: Date, more: boolean): LiveResponse; + destroy(): void; +} + +interface DeletedId { + updatedAt: Date; + id: string; +} + +interface LiveEndPointConfig { + model: Model; + fields: string[]; + fix?: boolean; + encode: (items: T[], base: BaseValues) => any[][]; + beforeDelete?: (item: T) => any; + afterDelete?: (item: T) => any; + beforeAssign?: (item: T, accountId: string) => any; + afterAssign?: (from: string, to: string) => any; +} + +export function createLiveEndPoint( + { model, fields, encode, beforeDelete, afterDelete, beforeAssign, afterAssign, fix = false }: LiveEndPointConfig +): LiveEndPoint { + const removedItems: DeletedId[] = []; + let fixing = false; + + function removedItem(id: string) { + removedItems.push({ id, updatedAt: new Date() }); + } + + function removeItem(id: string) { + return Promise.resolve(model.findById(id).exec()) + .tap(item => item && beforeDelete && beforeDelete(item)) + .tap(item => { + if (item) { + removedItem(item._id.toString()); + return item.remove() as any; + } + }) + .tap(item => item && afterDelete && afterDelete(item)) + .then(noop); + } + + function assignAccount(id: string, account: string) { + return Promise.resolve() + .then(() => model.findById(id, 'account').lean().exec()) + .tap((item: any) => item && beforeAssign && beforeAssign(item, account)) + .tap(() => model.findByIdAndUpdate(id, { account }).exec()) + .tap((item: any) => item && afterAssign && afterAssign(item.account, account)) + .then(noop); + } + + function encodeItems(items: T[], timestamp: Date, more: boolean): LiveResponse { + const base: BaseValues = {}; + const updates = encode(items, base); + const deletes = removedItems + .filter(x => x.updatedAt.getTime() > timestamp.getTime()) + .map(x => x.id); + + return { updates, deletes, base, more }; + } + + function findItems(from: Date): Promise { + return Promise.resolve(model.find({ updatedAt: { $gt: from } }, fields.join(' ')) + // .sort([['updatedAt', 1], ['id', 1]]) + .sort({ updatedAt: 1 }) + .limit(ITEM_LIMIT + 1) + .lean() + .exec()); + } + + function findItemsExact(date: Date): Promise { + return Promise.resolve(model.find({ updatedAt: date }, fields.join(' ')) + .lean() + .exec()); + } + + function hasItem(items: T[], id: string) { + return items.some(i => i._id === id); + } + + function addTailItems(items: T[]): Promise<{ items: T[]; more: boolean; }> { + if (items.length <= ITEM_LIMIT) { + return Promise.resolve({ items, more: false }); + } + + const a = items[items.length - 1]; + const b = items[items.length - 2]; + + if (a.updatedAt.getTime() !== b.updatedAt.getTime()) { + items.pop(); + return Promise.resolve({ items, more: true }); + } + + return findItemsExact(items[items.length - 1].updatedAt) + .then(other => other.filter(i => !hasItem(items, i._id))) + .then(other => [...items, ...other]) + .then(items => ({ items, more: true })); + } + + function getAll(timestamp?: string): Promise { + const from = timestamp ? new Date(timestamp) : new Date(0); + + return findItems(from) + .then(addTailItems) + .tap(({ items }) => { + try { + if (items.length > ITEM_LIMIT * 2) { + fixItems(items); + logger.warn(`Fetching ${items.length} ${model.modelName}s [${items[ITEM_LIMIT + 1].updatedAt.toISOString()}]`); + } + } catch (e) { + logger.error(e); + } + }) + .then(({ items, more }) => encodeItems(items, from, more)); + } + + function fixItems(items: T[]) { + if (fixing || !fix) + return; + + fixing = true; + logger.info(`Fixing ${model.modelName}s`); + + Promise.map(items, item => model.updateOne({ _id: item._id }, { unused: Date.now() % 1000 }).exec(), { concurrency: 1 }) + .then(() => logger.info(`Fixed ${model.modelName}s`)) + .catch(e => logger.error(e)) + .finally(() => fixing = false) + .done(); + } + + function get(id: string) { + return Promise.resolve(model.findById(id).lean().exec()); + } + + const interval = setInterval(() => { + const date = fromNow(-10 * MINUTE); + remove(removedItems, x => x.updatedAt.getTime() < date.getTime()); + }, 1 * MINUTE); + + function destroy() { + clearInterval(interval); + } + + return { + get, + getAll, + assignAccount, + removeItem, + removedItem, + encodeItems, + destroy, + }; +} diff --git a/src/ts/server/liveSettings.ts b/src/ts/server/liveSettings.ts new file mode 100644 index 0000000..93e4e08 --- /dev/null +++ b/src/ts/server/liveSettings.ts @@ -0,0 +1,6 @@ +import { ServerLiveSettings } from '../common/adminInterfaces'; + +export const liveSettings: ServerLiveSettings = { + updating: false, + shutdown: false, +}; diff --git a/src/ts/server/logger.ts b/src/ts/server/logger.ts new file mode 100644 index 0000000..2182d3c --- /dev/null +++ b/src/ts/server/logger.ts @@ -0,0 +1,105 @@ +import { console, dailyfile } from 'tracer'; +import chalk from 'chalk'; +import { ChatType, isPublicChat } from '../common/interfaces'; +import { IClient } from './serverInterfaces'; +import { getChatPrefix } from './commands'; +import { isMutedOrShadowed } from './playerUtils'; +import { ServerConfig } from '../common/adminInterfaces'; +import { pathTo } from './paths'; +import { ID } from './db'; + +const { reset, gray, magenta, cyan, green, yellow, red } = chalk; + +function format(color: (text: string) => string) { + //'[{{timestamp}}] [{{title}}] {{message}} ({{file}}:{{line}})', + return [ + reset('['), + gray('{{timestamp}}'), + reset('] ['), + color('{{title}}'), + reset('] {{message}} '), + gray('({{file}}:{{line}})'), + ].join(''); +} + +export const logger = console({ + level: 0, + dateformat: 'mmm dd HH:MM:ss', + format: [ + format(reset), + { + trace: format(cyan), + debug: format(magenta), + info: format(green), + warn: format(yellow), + error: format(red), + } + ], +} as any); + +const daily = dailyfile({ + root: pathTo('logs'), + maxLogFiles: 14, + dateformat: 'HH:MM:ss', + format: '{{timestamp}} {{message}}', // ({{file}}:{{line}}) +} as any); + +export function log(message: string) { + daily.info(message); +} + +export function formatMessage(accountId: ID, type: string, message: string) { + return `[${accountId}]${type}\t${message}`; +} + +export function systemMessage(accountId: ID, message: string) { + return formatMessage(accountId, '[system]', message); +} + +function adminMessage(accountId: ID, message: string) { + return formatMessage(accountId, '[admin]', message); +} + +export function system(accountId: ID, message: string) { + log(systemMessage(accountId, message)); +} + +export function admin(accountId: ID, message: string) { + log(adminMessage(accountId, message)); +} + +export function logPatreon(message: string) { + log(formatMessage('patreon', '', message)); +} + +export function logServer(message: string) { + log(formatMessage('server', '', message)); +} + +export function logPerformance(message: string) { + log(formatMessage('performance', '', message)); +} + +export function chat( + server: ServerConfig, client: IClient, text: string, type: ChatType, ignored: boolean, target: IClient | undefined +) { + let prefix = getChatPrefix(type); + let mod = ''; + + if (ignored) { + mod = '[ignored]'; + } else if (isMutedOrShadowed(client)) { + mod = '[muted]'; + } else if (client.accountSettings.ignorePublicChat && isPublicChat(type)) { + mod = '[ignorepub]'; + } + + if (type === ChatType.Whisper) { + prefix += `[${target ? `${target.accountId}${target.shadowed ? '][shadowed' : ''}` : 'undefined'}] `; + } + + const message = formatMessage( + client.accountId, `[${server.id}][${client.map.id || 'main'}][${client.characterName}]${mod}`, `${prefix}${text}`); + + log(message); +} diff --git a/src/ts/server/mapUtils.ts b/src/ts/server/mapUtils.ts new file mode 100644 index 0000000..63383c9 --- /dev/null +++ b/src/ts/server/mapUtils.ts @@ -0,0 +1,294 @@ +import { ServerEntity, ServerMap, IClient } from './serverInterfaces'; +import { World } from './world'; +import { Rect, SignEntityOptions, MessageType, CreateEntityMethod, PonyOptions, Point } from '../common/interfaces'; +import { roundPosition } from '../common/positionUtils'; +import { getRegionGlobal } from '../common/worldMap'; +import { addEntityToRegion, getRegionTiles, removeEntityFromRegion } from './serverRegion'; +import * as entities from '../common/entities'; +import { updateTileIndices } from '../client/tileUtils'; +import { generateRegionCollider } from '../common/region'; +import { PONY_TYPE, tileWidth, tileHeight } from '../common/constants'; +import { sayTo, saySystem } from './chat'; +import { setEntityName, updateEntityVelocity, setEntityAnimation } from './entityUtils'; +import { updateAccountState } from './accountUtils'; +import { toInt, includes } from '../common/utils'; +import { holdItem } from './playerUtils'; +import { random, sample, clamp } from 'lodash'; +import { findEntities } from './serverMap'; +import { randomPosition } from './controllers/collectableController'; +import { BunnyAnimation } from '../common/entities'; + +export const worldForTemplates: any = { + featureFlags: {}, + addEntity(entity: ServerEntity, map: ServerMap) { + roundPosition(entity); + const region = getRegionGlobal(map, entity.x, entity.y); + entity.region = region; + addEntityToRegion(region, entity, map); + return entity; + }, + removeEntity(entity: ServerEntity, map: ServerMap) { + let removed = false; + + if (entity.region) { + removed = removeEntityFromRegion(entity.region, entity, map); + } + + return removed; + }, +}; + +export function addSpawnPointIndicators(world: World, map: ServerMap) { + const addSpawn = ({ x, y, w, h }: Rect) => { + world.addEntity(entities.spawnPole(x, y), map); + + if (w && h) { + world.addEntity(entities.spawnPole(x + w, y), map); + world.addEntity(entities.spawnPole(x, y + h), map); + world.addEntity(entities.spawnPole(x + w, y + h), map); + } + }; + + addSpawn(map.spawnArea); + + for (const spawn of Array.from(map.spawns.values())) { + addSpawn(spawn); + } +} + +export function generateTileIndicesAndColliders(map: ServerMap) { + for (const region of map.regions) { + getRegionTiles(region); // initialize encodedTiles + + if (region.tilesDirty) { + updateTileIndices(region, map); + } + } + + for (const region of map.regions) { + if (region.colliderDirty) { + generateRegionCollider(region, map); + } + } +} + +export function removePonies(entities: ServerEntity[]) { + for (let i = entities.length - 1; i >= 0; i--) { + if (entities[i].type === PONY_TYPE) { + entities.splice(i, 1); + } + } +} + +export interface SignDirection { + icon: number; + name: string; +} + +export interface SignConfig { + r?: number; + w?: (SignDirection | undefined)[]; + e?: (SignDirection | undefined)[]; + n?: (SignDirection | undefined)[]; + s?: (SignDirection | undefined)[]; +} + +export function createDirectionSign(x: number, y: number, config: SignConfig) { + const result: ServerEntity[] = []; + const options: SignEntityOptions = { sign: {} }; + const lines: string[] = []; + const { w = [], e = [], s = [], n = [] } = config; + const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5); + const skip = 5 - max; + + function parse(entries: (SignDirection | undefined)[], arrow: string, plates: CreateEntityMethod[], ox: number) { + for (let i = 0; i < entries.length; i++) { + const e = entries[i]; + + if (e) { + lines.push(`${arrow} ${e.name}`); + const nameplate = plates[i](x + ox / tileWidth, y); + setEntityName(nameplate, e.name); + result.push(nameplate); + } + } + } + + if (config.r) { + options.sign.r = config.r; + } + + const ups = config.r ? entities.directionSignUpsRight : entities.directionSignUpsLeft; + const downs = config.r ? entities.directionSignDownsLeft : entities.directionSignDownsRight; + + if (config.n) { + options.sign.n = config.n.map(x => x ? x.icon : -1); + parse(config.n, '↑', ups.slice(skip), 0); + } + + if (config.w) { + options.sign.w = config.w.map(x => x ? x.icon : -1); + parse(config.w, '←', entities.directionSignLefts.slice(skip), -10); + } + + if (config.e) { + options.sign.e = config.e.map(x => x ? x.icon : -1); + parse(config.e, '→', entities.directionSignRights.slice(skip), 10); + } + + if (config.s) { + options.sign.s = config.s.map(x => x ? x.icon : -1); + parse(config.s, '↓', downs.slice(skip), 0); + } + + const text = lines.join('\n'); + const entity = entities.directionSign(x, y, options) as ServerEntity; + entity.interact = (entity, client) => sayTo(client, entity, text, MessageType.System); + result.push(entity); + return result; +} + +const patchTypes = [ + entities.cloverPatch3, entities.cloverPatch4, entities.cloverPatch5, entities.cloverPatch6, entities.cloverPatch7 +].map(x => x.type); + +const eggBasketTypes = entities.eggBaskets.map(b => b.type); + +export function pickCandy(client: IClient) { + let count = 0; + updateAccountState(client.account, state => state.candies = count = toInt(state.candies) + 1); + saySystem(client, `${count} 🍬`); +} + +export function pickGift(client: IClient) { + let count = 0; + updateAccountState(client.account, state => state.gifts = count = toInt(state.gifts) + 1); + saySystem(client, `${count} 🎁`); + holdItem(client.pony, entities.gift2.type); +} + +export function pickClover(client: IClient) { + let count = 0; + updateAccountState(client.account, state => state.clovers = count = toInt(state.clovers) + 1); + saySystem(client, `${count} 🍀`); + holdItem(client.pony, entities.cloverPick.type); +} + +export function pickEgg(client: IClient) { + let count = 0; + updateAccountState(client.account, state => state.eggs = count = toInt(state.eggs) + 1); + saySystem(client, `${count} 🥚`); + + if (Math.random() < 0.05) { + const options = client.pony.options as PonyOptions; + const basketIndex = eggBasketTypes.indexOf(options.hold || 0); + + if (basketIndex >= 0 && basketIndex < (eggBasketTypes.length - 1)) { + holdItem(client.pony, eggBasketTypes[basketIndex + 1]); + } + } +} + +export function pickEntity(client: IClient, entity: ServerEntity) { + holdItem(client.pony, entity.type); +} + +export function checkLantern(client: IClient) { + const options = client.pony.options as PonyOptions; + const canPick = options.hold === entities.jackoLanternOn.type || options.hold === entities.jackoLanternOff.type; + + if (!canPick) { + saySystem(client, 'Get a lantern to collect candies'); + } + + return canPick; +} + +export function checkBasket(client: IClient) { + const options = client.pony.options as PonyOptions; + const canPick = includes(eggBasketTypes, options.hold); + + if (!canPick) { + saySystem(client, 'Get a basket to collect eggs'); + } + + return canPick; +} + +export function checkNotCollecting(client: IClient) { + const options = client.pony.options as PonyOptions; + const canPick = includes(eggBasketTypes, options.hold) || + options.hold === entities.jackoLanternOn.type || + options.hold === entities.jackoLanternOff.type; + return !canPick; +} + +export function positionClover(map: ServerMap) { + const patch = sample(findEntities(map, e => includes(patchTypes, e.type))); + + if (patch && patch.bounds) { + const bounds = patch.bounds; + const position = { + x: patch.x + bounds.x / tileWidth + random(0, bounds.w / tileWidth, true), + y: patch.y + bounds.y / tileHeight + random(0, bounds.h / tileHeight, true), + }; + return position; + } else { + return randomPosition(map); + } +} + +export function createBunny(waypoints: Point[]) { + const { x, y } = waypoints[0]; + const entity = entities.bunny(x, y) as ServerEntity; + const bunnySpeed = 2; + + let waypoint = 0; + let sleepUntil = 0; + + entity.serverUpdate = (_delta, now) => { + if (sleepUntil > now) + return; + + const { x, y } = waypoints[waypoint]; + const reachedX = Math.abs(entity.x - x) < 0.2; + const reachedY = Math.abs(entity.y - y) < 0.2; + + if (reachedX && reachedY) { + const rand = Math.random(); + updateEntityVelocity(entity, 0, 0, now); + + if (rand < 0.1) { + setEntityAnimation(entity, BunnyAnimation.Clean); + sleepUntil = now + 2; + } else if (rand < 0.2) { + setEntityAnimation(entity, BunnyAnimation.Look); + sleepUntil = now + 2; + } else if (rand < 0.3) { + setEntityAnimation(entity, BunnyAnimation.Blink); + sleepUntil = now + 2; + } else if (rand < 0.6) { + setEntityAnimation(entity, BunnyAnimation.Sit); + sleepUntil = now + 2; + } else { + waypoint = (waypoint + 1) % waypoints.length; + setEntityAnimation(entity, BunnyAnimation.Sit); + sleepUntil = now + random(0.2, 2, true); + } + } else { + const vx = reachedX ? 0 : (x < entity.x ? -bunnySpeed : bunnySpeed); + const vy = reachedY ? 0 : (y < entity.y ? -bunnySpeed : bunnySpeed); + + if (entity.vx !== vx || entity.vy !== vy) { + updateEntityVelocity(entity, vx, vy, now); + setEntityAnimation(entity, BunnyAnimation.Walk, vx === 0 ? undefined : vx > 0); + } + } + }; + + if (DEVELOPMENT && false) { + return [entity, ...waypoints.map(({ x, y }) => entities.routePole(x, y))]; + } else { + return [entity]; + } +} diff --git a/src/ts/server/maps/caveMap.ts b/src/ts/server/maps/caveMap.ts new file mode 100644 index 0000000..9114d97 --- /dev/null +++ b/src/ts/server/maps/caveMap.ts @@ -0,0 +1,983 @@ +import * as fs from 'fs'; +import { pathTo } from '../paths'; +import { ServerMap, MapUsage, ServerEntity } from '../serverInterfaces'; +import { World, goToMap } from '../world'; +import { addSpawnPointIndicators } from '../mapUtils'; +import { createServerMap, deserializeMap } from '../serverMap'; +import { TileType, MapType } from '../../common/interfaces'; +import { rect } from '../../common/rect'; +import { TorchController, FlyingCritterController } from '../controllers'; +import * as entities from '../../common/entities'; +import { WallController } from '../controllers/wallController'; +import { createBoxOfLanterns, give } from '../controllerUtils'; +import { holdItem } from '../playerUtils'; + +const mapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'cave.json'), 'utf8')); + +export function createCaveMap(world: World): ServerMap { + const map = createServerMap('cave', MapType.Cave, 7, 7, TileType.None, MapUsage.Public); + + map.spawnArea = rect(27, 52, 1, 2); + map.tilesLocked = true; + + deserializeMap(map, mapData); + + // for (let y = 0; y < map.height; y++) { + // for (let x = 0; x < map.width; x++) { + // const tile = getTile(map, x, y); + // if (tile === TileType.Dirt) { + // setTile(map, x, y, TileType.None); + // } else if (tile === TileType.Grass) { + // setTile(map, x, y, TileType.Dirt); + // } + // } + // } + + const add = (entity: ServerEntity) => world.addEntity(entity, map); + + const caveDecals = [entities.caveDecal1, entities.caveDecal3, entities.caveDecal2]; + + function cracksS(x: number, y: number) { + const code = (Math.random() * 1000) % 64; + const index1 = code & 0b11; + const index2 = (code >> 2) & 0b11; + const index3 = (code >> 4) & 0b11; + index1 && index1 !== 3 && add(caveDecals[index1 - 1](x + 0.5, y - 1)); // no decal 2 here + index2 && add(caveDecals[index2 - 1](x + 0.5, y)); + index3 && add(caveDecals[index3 - 1](x + 0.5, y + 1)); + } + + function cracksSLeft(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.caveDecalL(x + 0.5, y - 1)); + (code & 0b10) && add(entities.caveDecalL(x + 0.5, y)); + } + + function cracksSRight(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.caveDecalR(x + 0.5, y - 1)); + (code & 0b10) && add(entities.caveDecalR(x + 0.5, y)); + } + + function caveSW(x: number, y: number) { + add(entities.caveSW(x + 0.5, y - 2)); + cracksSLeft(x, y); + } + + function caveSE(x: number, y: number) { + add(entities.caveSE(x + 0.5, y - 2)); + cracksSRight(x, y); + } + + function caveS(x: number, y: number) { + add(entities.caveS2(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveSStart(x: number, y: number) { + add(entities.caveS1(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveSEnd(x: number, y: number) { + add(entities.caveS3(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveS1(x: number, y: number) { + add(entities.caveSb(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveN(x: number, y: number) { + add(entities.caveTopN(x + 0.5, y)); + } + + function caveNE(x: number, y: number) { + add(entities.caveTopNE(x + 0.5, y)); + } + + function caveNW(x: number, y: number) { + add(entities.caveTopNW(x + 0.5, y)); + } + + function caveRightWithTrimNoEdge(x: number, y: number, h: number) { + caveRight(x, y, h); + caveTrimRight(x + 1, y, h, false); + } + + function caveLeftWithTrimNoEdge(x: number, y: number, h: number) { + caveLeft(x, y, h); + caveTrimLeft(x, y, h, false); + } + + function caveRightWithTrim(x: number, y: number, h: number) { + caveRight(x, y - 3, h - 3); + caveTrimRight(x + 1, y, h); + } + + function caveLeftWithTrim(x: number, y: number, h: number) { + caveLeft(x, y - 3, h - 3); + caveTrimLeft(x, y, h); + } + + function caveLeft(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.caveTopW(x + 0.5, y - i)); + } + } + + function caveRight(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.caveTopE(x + 0.5, y - i)); + } + } + + function caveTrimLeft(x: number, y: number, h: number, botTrim = true) { + if (botTrim) { + add(entities.caveBotTrimLeft(x - 0.5, y)); + } else { + add(entities.caveMidTrimLeft(x - 0.5, y)); + } + + for (let i = 0; i < (h - 2); i++) { + add(entities.caveMidTrimLeft(x - 0.5, y - 1 - i)); + } + + if (h > 1) { + add(entities.caveTopTrimLeft(x - 0.5, y - h + 1)); + } + } + + function caveTrimRight(x: number, y: number, h: number, botTrim = true) { + if (botTrim) { + add(entities.caveBotTrimRight(x + 0.5, y)); + } else { + add(entities.caveMidTrimRight(x + 0.5, y)); + } + + for (let i = 0; i < (h - 2); i++) { + add(entities.caveMidTrimRight(x + 0.5, y - 1 - i)); + } + + if (h > 1) { + add(entities.caveTopTrimRight(x + 0.5, y - h + 1)); + } + } + + function caveSSection(x: number, y: number, w: number) { + caveSStart(x, y); + + for (let i = 1; i < (w - 1); i++) { + caveS(x + i, y); + } + + caveSEnd(x + w - 1, y); + } + + function caveSESection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + caveSE(x + i, y - i); + } + } + + function caveSWSection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + caveSW(x + i, y + i); + } + } + + // large crypt + caveS1(6, 1); + caveRightWithTrim(5, 3, 4); + caveSESection(4, 4, 2); + caveRightWithTrimNoEdge(3, 8, 6); + caveNE(4, 9); + caveNE(5, 10); + caveNE(6, 11); + caveN(7, 12); + caveNE(8, 12); + caveNE(9, 13); + caveRightWithTrim(9, 19, 6); + caveSE(9, 19); + caveSSection(7, 19, 2); + caveSE(6, 20); + caveRightWithTrim(5, 22, 4); + caveSESection(4, 23, 2); + caveRightWithTrimNoEdge(3, 27, 6); + caveNE(4, 28); + caveRightWithTrimNoEdge(4, 31, 3); + caveNE(5, 32); + caveN(6, 33); + caveNW(7, 32); + caveN(8, 32); + caveN(9, 32); + caveN(10, 32); + caveNW(11, 31); + caveNW(12, 30); + caveLeftWithTrimNoEdge(13, 29, 2); + caveNW(13, 27); + caveNW(14, 26); + caveLeftWithTrimNoEdge(15, 25, 1); + caveNW(15, 24); + caveN(16, 24); + caveN(17, 24); + caveNE(18, 24); + caveNE(19, 25); + caveN(20, 26); + caveNE(21, 26); + caveN(22, 27); + caveNE(23, 27); + caveNE(24, 28); + caveNE(25, 29); + caveNE(26, 30); + caveRightWithTrimNoEdge(26, 31, 1); + caveNE(27, 32); + caveNE(28, 33); + caveRightWithTrim(28, 38, 5); + caveSE(28, 38); + caveS1(27, 38); + caveSW(26, 38); + caveLeftWithTrim(26, 38, 4); + caveSW(25, 36); + caveSSection(23, 35, 2); + caveSE(22, 36); + caveRightWithTrimNoEdge(21, 37, 3); + caveNE(22, 38); + caveRightWithTrim(22, 41, 3); + caveSE(22, 41); + caveSW(21, 41); + caveLeftWithTrim(21, 41, 4); + caveSWSection(18, 37, 3); + caveS1(17, 36); + caveSE(16, 37); + caveRightWithTrim(15, 39, 4); + caveS1(15, 38); + caveSWSection(13, 37, 2); + caveS1(12, 36); + caveSESection(10, 38, 2); + caveRightWithTrimNoEdge(9, 39, 3); + caveNE(10, 40); + caveRightWithTrim(10, 44, 4); + caveSE(10, 44); + caveSSection(5, 44, 2); + caveSE(7, 44); + caveSW(8, 44); + caveS1(9, 44); + caveSE(4, 45); + caveRightWithTrimNoEdge(3, 46, 3); + caveNE(4, 47); + caveN(5, 48); + caveN(6, 48); + caveN(7, 48); + caveN(8, 48); + caveN(9, 48); + caveN(10, 48); + caveNE(11, 48); + caveN(12, 49); + caveNE(13, 49); + caveRightWithTrimNoEdge(13, 50, 1); + caveNE(14, 51); + caveN(15, 52); + caveNW(16, 51); + caveN(17, 51); + caveNW(18, 50); + caveNW(19, 49); + caveNW(20, 48); + caveLeftWithTrimNoEdge(21, 47, 2); + caveNW(21, 45); + caveN(22, 45); + caveNE(23, 45); + caveRightWithTrimNoEdge(23, 47, 2); + caveNE(24, 48); + caveRightWithTrimNoEdge(24, 49, 1); + caveNE(25, 50); + caveRightWithTrimNoEdge(25, 55, 5); + // entrance to caves + caveLeftWithTrimNoEdge(29, 55, 4); + caveNW(29, 51); + caveNW(30, 50); + caveN(31, 50); + caveN(32, 50); + caveNW(33, 49); + caveLeftWithTrimNoEdge(34, 48, 1); + caveNW(34, 47); + caveLeftWithTrimNoEdge(35, 46, 3); + caveNW(35, 43); + caveN(36, 43); + caveN(37, 43); + caveNW(38, 42); + caveN(39, 42); + caveN(40, 42); + caveN(41, 42); + caveN(42, 42); + caveNE(43, 42); + caveN(44, 43); + caveNE(45, 43); + caveN(46, 44); + caveNW(47, 43); + caveN(48, 43); + caveNW(49, 42); + caveN(50, 42); + caveNW(51, 41); + caveNW(52, 40); + caveLeftWithTrimNoEdge(53, 39, 1); + caveNW(53, 38); + caveLeftWithTrimNoEdge(54, 37, 4); + caveSW(53, 35); + caveLeftWithTrim(53, 35, 4); + caveSW(52, 33); + caveLeftWithTrim(52, 33, 5); + caveSWSection(50, 29, 2); + caveS1(49, 28); + caveSW(48, 28); + caveS1(47, 27); + caveSESection(45, 29, 2); + caveS1(44, 29); + caveRightWithTrim(43, 31, 4); + caveSESection(42, 32, 2); + caveRightWithTrim(41, 35, 5); + caveSE(41, 35); + caveRightWithTrim(40, 37, 4); + caveSE(40, 37); + caveSSection(37, 37, 3); + caveSE(36, 38); + caveSSection(33, 38, 3); + caveSW(32, 38); + caveLeftWithTrim(32, 38, 4); + caveNW(32, 34); + // small crypt + + // large crypt + caveLeftWithTrim(13, 18, 4); + caveSW(13, 18); + caveLeftWithTrim(14, 20, 4); + caveSW(14, 20); + caveSESection(15, 20, 2); + caveS1(17, 18); + caveSW(18, 19); + caveS1(19, 19); + caveSW(20, 20); + caveSSection(21, 20, 2); + caveSESection(23, 20, 3); + caveRightWithTrim(25, 18, 4); + caveSE(26, 16); + caveRightWithTrim(26, 16, 4); + caveNE(26, 12); + caveRightWithTrimNoEdge(25, 11, 2); + caveNE(25, 9); + caveRightWithTrimNoEdge(24, 8, 4); + caveSE(25, 6); + caveRightWithTrim(25, 6, 4); + caveSESection(26, 4, 2); + caveS1(28, 2); + caveSE(29, 2); + caveSSection(30, 1, 4); + caveSW(34, 2); + caveS1(35, 2); + caveSW(36, 3); + caveLeftWithTrim(37, 5, 4); + caveSW(37, 5); + caveLeftWithTrim(38, 7, 4); + caveSWSection(38, 7, 3); + caveS1(41, 9); + caveSESection(42, 9, 2); + caveS1(44, 7); + caveSE(45, 7); + caveSSection(46, 6, 2); + caveSW(48, 7); + caveS1(49, 7); + caveSW(50, 8); + caveS1(51, 8); + caveSW(52, 9); + caveLeftWithTrim(53, 11, 4); + caveSW(53, 11); + caveLeftWithTrimNoEdge(54, 15, 6); + caveNW(53, 16); + caveLeftWithTrimNoEdge(53, 18, 2); + caveNW(52, 19); + caveNW(51, 20); + caveNW(50, 21); + caveN(49, 22); + caveNW(48, 22); + caveN(47, 23); + caveNE(46, 22); + caveN(45, 22); + caveNE(44, 21); + caveNE(43, 20); + caveRightWithTrimNoEdge(42, 19, 1); + caveNE(42, 18); + caveN(41, 18); + caveN(40, 18); + caveNE(39, 17); + caveRightWithTrimNoEdge(38, 16, 1); + caveNE(38, 15); + caveN(37, 15); + caveNE(36, 14); + caveN(35, 14); + caveNW(34, 14); + caveNW(33, 15); + caveN(32, 16); + caveNW(31, 16); + caveN(30, 17); + caveNW(29, 17); + caveLeftWithTrimNoEdge(29, 19, 2); + caveNW(28, 20); + caveLeftWithTrimNoEdge(28, 21, 1); + caveNW(27, 22); + caveLeftWithTrim(27, 26, 4); + caveSW(27, 26); + caveS1(28, 26); + caveLeftWithTrim(29, 28, 4); + caveSWSection(29, 28, 2); + caveSSection(31, 29, 2); + // small crypt + + add(entities.caveFill(3, 9)); + add(entities.caveFill(4, 10)); + add(entities.caveFill(5, 11)); + add(entities.caveFill(6, 12)); + add(entities.caveFill(8, 13)); + add(entities.caveFill(3, 28)); + add(entities.caveFill(4, 32)); + add(entities.caveFill(5, 33)); + add(entities.caveFill(7, 33)); + add(entities.caveFill(11, 32)); + add(entities.caveFill(12, 31)); + add(entities.caveFill(13, 30)); + add(entities.caveFill(14, 27)); + add(entities.caveFill(15, 26)); + add(entities.caveFill(18, 25)); + add(entities.caveFill(19, 26)); + add(entities.caveFill(23, 28)); + add(entities.caveFill(24, 29)); + add(entities.caveFill(25, 30)); + add(entities.caveFill(26, 32)); + add(entities.caveFill(27, 33)); + add(entities.caveFill(9, 40)); + add(entities.caveFill(11, 49)); + add(entities.caveFill(13, 51)); + add(entities.caveFill(14, 52)); + add(entities.caveFill(16, 52)); + add(entities.caveFill(18, 51)); + add(entities.caveFill(19, 50)); + add(entities.caveFill(20, 49)); + add(entities.caveFill(21, 48)); + add(entities.caveFill(23, 48)); + add(entities.caveFill(24, 50)); + add(entities.caveFill(30, 51)); + add(entities.caveFill(33, 50)); + add(entities.caveFill(34, 49)); + add(entities.caveFill(35, 47)); + add(entities.caveFill(38, 43)); + add(entities.caveFill(43, 43)); + add(entities.caveFill(45, 44)); + add(entities.caveFill(47, 44)); + add(entities.caveFill(49, 43)); + add(entities.caveFill(51, 42)); + add(entities.caveFill(52, 41)); + add(entities.caveFill(53, 40)); + add(entities.caveFill(54, 38)); + add(entities.caveFill(33, 34)); + add(entities.caveFill(28, 22)); + add(entities.caveFill(31, 17)); + add(entities.caveFill(33, 16)); + add(entities.caveFill(36, 15)); + add(entities.caveFill(38, 17)); + add(entities.caveFill(39, 18)); + add(entities.caveFill(43, 21)); + add(entities.caveFill(44, 22)); + add(entities.caveFill(46, 23)); + add(entities.caveFill(48, 23)); + add(entities.caveFill(50, 22)); + add(entities.caveFill(51, 21)); + add(entities.caveFill(52, 20)); + add(entities.caveFill(53, 19)); + add(entities.caveFill(54, 16)); + add(entities.caveFill(24, 9)); + add(entities.caveFill(25, 12)); + add(entities.caveFill(21, 27)); + + add(entities.trigger3x1(27.5, 55)).trigger = (_, client) => goToMap(world, client, '', 'cave'); + + add(entities.lanternOn(10.38, 18.38)); + add(entities.lanternOn(12.66, 18.38)); + add(entities.lanternOn(18.41, 12.00)); + add(entities.lanternOn(21.41, 12.13)); + add(entities.lanternOn(18.47, 14.21)); + add(entities.lanternOn(21.44, 14.33)); + add(entities.lanternOn(10.41, 11.54)); + add(entities.lanternOn(12.66, 11.50)); + add(entities.lanternOn(13.94, 4.67)); + add(entities.lanternOn(9.44, 4.67)); + add(entities.lanternOn(27.44, 31.54)); + add(entities.lanternOn(21.97, 26.50)); + add(entities.lanternOn(19.31, 21.79)); + add(entities.lanternOn(15.53, 24.17)); + add(entities.lanternOn(9.28, 25.63)); + add(entities.lanternOn(29.34, 17.29)); + add(entities.lanternOn(15.81, 40.42)); + add(entities.lanternOn(11.28, 43.50)); + add(entities.lanternOn(14.66, 48.75)); + add(entities.lanternOn(18.44, 48.33)); + add(entities.lanternOn(16.18, 45.04)); + add(entities.lanternOn(39.53, 41.91)); + add(entities.lanternOn(41.37, 37.41)); + add(entities.lanternOn(45.84, 34.13)); + add(entities.lanternOn(50.50, 38.17)); + add(entities.lanternOn(44.78, 41.12)); + add(entities.lanternOn(37.46, 27.12)); + add(entities.lanternOn(26.22, 51.79)); + add(entities.lanternOn(28.75, 51.75)); + add(entities.lanternOn(23.34, 42.08)); + add(entities.lanternOn(23.88, 45.33)); + add(entities.lanternOn(33.66, 40.20)); + add(entities.lanternOn(34.44, 45.17)); + add(entities.lanternOn(28.81, 44.25)); + add(entities.lanternOn(25.31, 20.50)); + add(entities.lanternOn(28.63, 28.38)); + add(entities.lanternOn(29.28, 36.92)); + add(entities.lanternOn(32.38, 31.38)); + add(entities.lanternOn(33.22, 22.83)); + add(entities.lanternOn(31.84, 49.83)); + add(entities.lanternOn(30.28, 48.91)); + add(entities.lanternOn(14.09, 8.63)); + add(entities.lanternOn(9.41, 8.54)); + + add(entities.waterRock1(8.63, 27.92)); + add(entities.waterRock3(8.75, 28.08)); + add(entities.waterRock6(29.59, 8.79)); + add(entities.waterRock8(29.66, 9.00)); + add(entities.waterRock5(31.81, 6.83)); + add(entities.waterRock4(31.41, 6.58)); + add(entities.waterRock1(32.34, 9.79)); + add(entities.waterRock1(46.56, 13.50)); + add(entities.waterRock10(46.25, 13.71)); + add(entities.waterRock11(49.63, 16.63)); + add(entities.waterRock9(46.50, 11.46)); + add(entities.waterRock4(46.38, 11.63)); + + add(entities.box(32.03, 49.13)); + + add(createBoxOfLanterns(30.97, 49.63)).interact = (_, client) => { + if (client.pony.options!.hold === entities.crystalHeld.type) { + holdItem(client.pony, entities.crystalLantern.type); + } else { + holdItem(client.pony, entities.lanternOn.type); + } + }; + + // top rooms + add(createBoxOfLanterns(9.34, 11.46)); + add(entities.crate1A(17.78, 11.21)); + add(entities.crate1A(18.84, 11.29)); + add(entities.crate1A(7.69, 4.13)); + add(entities.crate1A(8.72, 4.13)); + add(entities.crate1A(10.00, 4.17)); + add(entities.crate1A(8.06, 5.33)); + add(entities.crate1BHigh(8.31, 4.17)); + add(entities.crate1BHigh(18.25, 11.30)); + add(entities.barrel(15.31, 3.79)); + add(entities.barrel(14.66, 4.54)); + add(entities.barrel(15.28, 5.08)); + add(entities.barrel(14.50, 3.75)); + add(entities.barrel(15.34, 6.08)); + add(entities.barrel(19.78, 10.75)); + add(entities.barrel(20.56, 11.25)); + + function railsH(x: number, y: number, length: number) { + for (let i = 0; i < length; i++) { + add(entities.mineRailsH(x + i + 0.5, y)); + } + } + + function railsV(x: number, y: number, length: number) { + for (let i = 0; i < length; i++) { + add(entities.mineRailsV(x + 0.5, y - i)); + } + } + + railsV(30, 40, 8); + add(entities.mineRailsSE(30.5, 32)); + railsH(31, 32, 5); + add(entities.mineRailsEndRight(36.5, 32.5)); + + railsH(5, 46, 13); + add(entities.mineRailsNW(18.5, 46)); + railsV(18, 45, 1); + add(entities.mineRailsNSE(18.5, 44)); + railsH(19, 44, 7); + railsV(18, 43, 1); + add(entities.mineRailsSW(18.5, 42)); + + add(entities.mineCart(49, 40)); + add(entities.crystalsCartPile(49, 40)).interact = give(entities.crystalHeld.type); + + add(entities.mineCart(31.5, 46)); + add(entities.crystalsCartPile(31.5, 46)).interact = give(entities.crystalHeld.type); + + add(entities.mineRailsEndRight(50.5, 40.5)); + railsH(38, 40, 9); + add(entities.mineRailsNWE(47.5, 40)); + railsH(48, 40, 2); + add(entities.mineRailsSE(37.5, 40)); + add(entities.mineRailsNW(37.5, 41)); + railsH(27, 41, 3); + add(entities.mineRailsNWE(30.5, 41)); + railsH(31, 41, 6); + add(entities.mineRailsSE(26.5, 41)); + railsV(26, 43, 2); + add(entities.mineRailsNSW(26.5, 44)); + railsV(26, 45, 1); + add(entities.mineRailsNE(26.5, 46)); + railsH(28, 46, 5); + add(entities.mineRailsEndRight(33.5, 46.5)); + railsH(14, 42, 4); + add(entities.mineRailsEndLeft(13.5, 42.5)); + + railsV(47, 39, 8); + add(entities.mineRailsEndTop(47.5, 32)); + + add(entities.mineRailsSWE(27.5, 46)); + railsV(27, 55, 9); + + add(entities.wallMap(23.97, 36.96)); + add(entities.table3(24.00, 37.2916)); + add(entities.lanternOnTable(24.375, 37.30)); + add(entities.lanternOn(22.50, 38.33)); + add(entities.sandPileSmall(25.50, 40.33)); + add(entities.table3(37.81, 39.41)); + + add(entities.sandPileMedium(31.44, 42.67)); + add(entities.sandPileSmall(32.03, 43.54)); + add(entities.sandPileTiny(28.75, 45.33)); + add(entities.sandPileSmall(32.63, 47.50)); + add(entities.sandPileTiny(33.03, 47.96)); + add(entities.sandPileSmall(27.94, 45.25)); + add(entities.sandPileSmall(29.81, 48.00)); + add(entities.sandPileTiny(29.13, 48.38)); + add(entities.sandPileSmall(35.72, 42.46)); + add(entities.sandPileTiny(32.53, 42.42)); + add(entities.sandPileTinier(36.47, 42.71)); + add(entities.sandPileBig(50.00, 34.96)); + add(entities.sandPileMedium(49.16, 36.00)); + add(entities.sandPileSmall(44.13, 34.42)); + add(entities.sandPileMedium(17.88, 40.58)); + add(entities.sandPileSmall(18.69, 41.29)); + add(entities.sandPileSmall(15.63, 49.88)); + add(entities.sandPileTiny(16.31, 50.33)); + add(entities.sandPileTinier(17.63, 41.58)); + add(entities.sandPileSmall(7.19, 24.29)); + add(entities.sandPileTiny(6.69, 24.79)); + + add(entities.sandPileMedium(6.19, 46.92)); + add(entities.sandPileTiny(5.03, 47.25)); + add(entities.rockB(5.13, 46.46)); + add(entities.rockB(7.31, 47.33)); + add(entities.rock2B(5.00, 47.13)); + add(entities.rock3B(5.72, 46.54)); + add(entities.rock3B(7.91, 47.04)); + add(entities.rock2B(8.03, 45.75)); + add(entities.rock2B(6.72, 47.54)); + add(entities.rock2B(32.03, 49.38)); + add(entities.rock2B(50.63, 39.71)); + add(entities.rock3B(41.59, 37.13)); + add(entities.rock3B(32.13, 31.25)); + add(entities.rock3B(22.34, 22.08)); + add(entities.rock2B(22.91, 22.29)); + add(entities.rock3B(35.63, 4.17)); + add(entities.rock2B(44.66, 9.29)); + add(entities.rockB(7.47, 21.33)); + add(entities.rock2B(7.81, 21.71)); + + add(entities.caveCover(13.125, 11.625)); + add(entities.caveCover(14.125, 11.625)); + add(entities.caveCover(15.125, 11.625)); + add(entities.caveCover(13.125, 15.625)); + add(entities.caveCover(14.125, 15.625)); + add(entities.caveCover(15.125, 15.625)); + add(entities.caveCover(15.875, 15.625)); + add(entities.caveCover(16.875, 16.625)); + add(entities.caveCover(17.875, 16.625)); + add(entities.caveCover(18.875, 16.625)); + add(entities.caveCover(19.875, 16.625)); + add(entities.caveCover(20.875, 16.625)); + add(entities.caveCover(21.875, 16.625)); + add(entities.caveCover(22.875, 16.625)); + add(entities.caveCover(30.88, 27.625)); + add(entities.caveCover(31.88, 27.625)); + add(entities.caveCover(32.88, 27.625)); + add(entities.caveCover(38.125, 30.625)); + add(entities.caveCover(33.125, 34.625)); + add(entities.caveCover(34.03, 34.625)); + add(entities.caveCover(35.03, 34.625)); + add(entities.caveCover(36.03, 34.625)); + add(entities.caveCover(37.03, 34.625)); + add(entities.caveCover(38.03, 34.625)); + add(entities.caveCover(32.88, 35.17)); + + add(entities.crystals1(48.50, 10.42)); + add(entities.crystals1(42.59, 17.17)); + add(entities.crystals2(47.44, 14.42)); + add(entities.crystals3(52.06, 16.25)); + add(entities.crystals4(48.69, 18.96)); + add(entities.crystals5(45.56, 10.21)); + add(entities.crystals6(51.53, 11.58)); + add(entities.crystals7(45.53, 16.50)); + add(entities.crystals9(51.56, 18.71)); + add(entities.crystals10(45.59, 20.67)); + add(entities.crystals10(43.59, 13.33)); + add(entities.crystals3(40.63, 13.46)); + add(entities.crystals9(38.80, 10.04)); + add(entities.crystals8(37.34, 13.58)); + add(entities.crystals8(49.91, 15.21)); + add(entities.crystals1(33.41, 10.46)); + add(entities.crystals1(32.66, 4.33)); + add(entities.crystals2(34.38, 8.71)); + add(entities.crystals3(33.44, 14.67)); + add(entities.crystals3(27.47, 6.38)); + add(entities.crystals4(28.63, 5.79)); + add(entities.crystals5(28.69, 9.46)); + add(entities.crystals5(37.38, 9.79)); + add(entities.crystals6(28.22, 10.96)); + add(entities.crystals7(31.56, 11.75)); + add(entities.crystals8(34.81, 6.21)); + add(entities.crystals9(26.22, 8.04)); + add(entities.crystals10(36.91, 8.21)); + add(entities.crystals2(28.68, 14.54)); + add(entities.crystals8(28.63, 18.79)); + add(entities.crystals7(26.34, 23.71)); + add(entities.crystals6(23.22, 23.29)); + add(entities.crystals5(16.43, 22.29)); + add(entities.crystals3(8.59, 30.33)); + add(entities.crystals5(6.59, 27.58)); + add(entities.crystals2(11.625, 28.42)); + add(entities.crystals8(7.31, 22.54)); + add(entities.crystals4(8.44, 23.17)); + add(entities.crystals6(7.31, 29.83)); + add(entities.crystals1(5.44, 25.38)); + add(entities.crystals3(5.31, 6.50)); + add(entities.crystals7(6.41, 10.42)); + + add(entities.crystals5(49.56, 31.46)); + add(entities.crystals8(50.63, 33.33)); + add(entities.crystals1(47.19, 30.21)); + add(entities.crystals9(46.06, 32.21)); + add(entities.crystals8(17.28, 39.38)); + add(entities.crystals3(12.41, 39.33)); + add(entities.crystals6(11.31, 40.75)); + + add(entities.waterCrystal1(31.34, 7.13)); + add(entities.waterCrystal2(31.03, 6.92)); + add(entities.waterCrysta3(31.00, 7.29)); + add(entities.waterCrysta3(32.34, 10.00)); + add(entities.waterCrystal2(46.06, 13.29)); + add(entities.waterCrysta3(49.44, 16.29)); + add(entities.waterCrysta3(46.72, 11.67)); + add(entities.waterCrysta3(41.75, 12.54)); + add(entities.waterCrystal1(8.47, 27.46)); + add(entities.waterCrysta3(10.47, 27.54)); + + add(entities.stalactite3(17.06, 20.79)); + add(entities.stalactite3(22.25, 22.83)); + add(entities.stalactite2(21.81, 22.46)); + add(entities.stalactite1(17.41, 21.08)); + add(entities.stalactite1(28.31, 28.25)); + add(entities.stalactite2(13.69, 20.63)); + add(entities.stalactite3(6.56, 22.04)); + add(entities.stalactite3(7.38, 28.46)); + add(entities.stalactite1(7.66, 28.54)); + add(entities.stalactite2(6.28, 22.42)); + add(entities.stalactite2(9.69, 20.79)); + add(entities.stalactite1(10.06, 20.58)); + add(entities.stalactite2(12.25, 25.79)); + add(entities.stalactite1(11.91, 26.08)); + add(entities.stalactite3(11.88, 25.71)); + add(entities.stalactite2(31.25, 31.25)); + add(entities.stalactite3(22.72, 42.88)); + add(entities.stalactite1(23.06, 42.71)); + add(entities.stalactite3(10.59, 40.21)); + add(entities.stalactite2(12.19, 38.42)); + add(entities.stalactite1(12.63, 38.42)); + add(entities.stalactite3(47.25, 29.46)); + add(entities.stalactite2(44.34, 31.33)); + add(entities.stalactite1(47.69, 29.58)); + add(entities.stalactite1(51.75, 33.50)); + add(entities.stalactite2(26.31, 18.21)); + add(entities.stalactite3(28.22, 4.33)); + add(entities.stalactite3(37.69, 7.29)); + add(entities.stalactite2(39.44, 10.00)); + add(entities.stalactite2(28.75, 4.13)); + add(entities.stalactite1(27.47, 10.88)); + add(entities.stalactite1(37.28, 7.04)); + add(entities.stalactite3(49.88, 9.42)); + add(entities.stalactite3(50.63, 13.92)); + add(entities.stalactite3(42.84, 14.63)); + add(entities.stalactite3(50.78, 20.96)); + add(entities.stalactite3(44.09, 9.46)); + add(entities.stalactite2(43.16, 15.04)); + add(entities.stalactite2(49.44, 9.21)); + add(entities.stalactite2(46.00, 8.42)); + add(entities.stalactite2(51.47, 14.17)); + add(entities.stalactite1(51.06, 14.29)); + add(entities.stalactite1(42.59, 15.13)); + add(entities.stalactite1(44.31, 9.67)); + add(entities.stalactite1(51.78, 10.33)); + add(entities.stalactite1(50.44, 21.29)); + add(entities.stalactite1(47.41, 17.71)); + add(entities.stalactite2(47.06, 17.71)); + add(entities.stalactite1(28.66, 19.67)); + add(entities.stalactite3(4.34, 6.33)); + add(entities.stalactite2(4.56, 7.00)); + add(entities.stalactite1(5.56, 10.29)); + add(entities.stalactite3(35.38, 11.83)); + add(entities.stalactite2(35.28, 12.21)); + add(entities.stalactite1(34.94, 11.79)); + add(entities.stalactite3(15.03, 22.29)); + add(entities.stalactite1(14.63, 22.17)); + + // storage room + add(entities.table3(37.03, 23.71)); + add(entities.lanternOnTable(37.25, 23.75)); + add(entities.barrel(31.69, 24.71)); + add(entities.barrel(32.03, 25.50)); + add(entities.barrel(31.63, 26.17)); + add(entities.barrel(38.34, 25.75)); + add(entities.barrel(38.41, 27.04)); + add(entities.barrel(37.78, 26.38)); + add(entities.barrel(31.94, 26.96)); + add(entities.toolboxFull(34.28, 22.96)).interact = give(entities.pickaxe.type); + add(entities.box(38.22, 29.96)); + add(entities.ropeRack(34.31, 22.20)).interact = give(entities.rope.type); + add(entities.boxLanterns(35.63, 24.04)).interact = give(entities.lanternOn.type); + add(entities.crate1A(38.19, 28.79)); + add(entities.crate1A(34.81, 28.58)); + add(entities.crate1A(34.72, 29.88)); + add(entities.crate2A(34.81, 30.67)); + + add(entities.ropeRack(34.31, 39.91)).interact = give(entities.rope.type); + add(entities.crate1A(34.22, 43.38)); + add(entities.crate1A(33.91, 44.50)); + + function placeMineCart(x: number, y: number) { + add(entities.mineCartBack(x + 0.22, y - 0.04)); + add(entities.mineCartFront(x + 0.22, y + 0.83)); + } + + placeMineCart(41, 40); + placeMineCart(45, 40); + placeMineCart(16, 42); + placeMineCart(8, 46); + placeMineCart(12, 46); + placeMineCart(15, 46); + placeMineCart(28, 41); + + map.controllers.push(new TorchController(world, map)); + map.controllers.push(new FlyingCritterController(world, map, entities.bat, 2, 10, () => true, true)); + + const wallController = new WallController(world, map, entities.stoneWalls); + map.controllers.push(wallController); + wallController.top = 3; + wallController.isTall = (x, y) => { + if (y === 10 && x >= 17 && x <= 23) + return true; + + if (x >= 31 && x <= 39 && y >= 22 && y <= 25) + return true; + + if (y === 31 && x >= 33 && x <= 34) + return true; + + return false; + }; + + if (wallController.toggleWall) { + // large crypt + for (let x = 7; x <= 15; x++) { + wallController.toggleWall(x, 3, TileType.WallH); + } + + for (let y = 3; y <= 10; y++) { + wallController.toggleWall(16, y, TileType.WallV); + } + + for (let x = 13; x <= 15; x++) { + wallController.toggleWall(x, 11, TileType.WallH); + } + + wallController.toggleWall(13, 11, TileType.WallV); + + for (let x = 13; x <= 16; x++) { + wallController.toggleWall(x, 12, TileType.WallH); + } + + wallController.toggleWall(17, 10, TileType.WallV); + wallController.toggleWall(17, 11, TileType.WallV); + + for (let x = 17; x <= 22; x++) { + wallController.toggleWall(x, 10, TileType.WallH); + } + + for (let y = 10; y <= 15; y++) { + wallController.toggleWall(23, y, TileType.WallV); + } + + for (let x = 17; x <= 22; x++) { + wallController.toggleWall(x, 16, TileType.WallH); + } + + wallController.toggleWall(17, 15, TileType.WallV); + + for (let x = 13; x <= 16; x++) { + wallController.toggleWall(x, 15, TileType.WallH); + } + + // small crypt + wallController.toggleWall(33, 31, TileType.WallH); + wallController.toggleWall(34, 30, TileType.WallV); + wallController.toggleWall(34, 29, TileType.WallV); + wallController.toggleWall(34, 28, TileType.WallV); + wallController.toggleWall(34, 27, TileType.WallV); + wallController.toggleWall(33, 27, TileType.WallH); + wallController.toggleWall(32, 27, TileType.WallH); + wallController.toggleWall(31, 27, TileType.WallH); + wallController.toggleWall(31, 26, TileType.WallV); + wallController.toggleWall(31, 25, TileType.WallV); + wallController.toggleWall(31, 24, TileType.WallV); + wallController.toggleWall(31, 24, TileType.WallH); + wallController.toggleWall(32, 23, TileType.WallV); + wallController.toggleWall(32, 22, TileType.WallV); + wallController.toggleWall(32, 22, TileType.WallH); + wallController.toggleWall(33, 22, TileType.WallH); + wallController.toggleWall(34, 22, TileType.WallH); + wallController.toggleWall(35, 22, TileType.WallV); + wallController.toggleWall(35, 23, TileType.WallH); + wallController.toggleWall(36, 23, TileType.WallH); + wallController.toggleWall(37, 23, TileType.WallH); + wallController.toggleWall(38, 23, TileType.WallV); + wallController.toggleWall(38, 24, TileType.WallV); + wallController.toggleWall(38, 25, TileType.WallH); + wallController.toggleWall(39, 25, TileType.WallV); + wallController.toggleWall(39, 26, TileType.WallV); + wallController.toggleWall(39, 27, TileType.WallV); + wallController.toggleWall(39, 28, TileType.WallV); + wallController.toggleWall(39, 29, TileType.WallV); + wallController.toggleWall(38, 30, TileType.WallH); + wallController.toggleWall(38, 30, TileType.WallV); + wallController.toggleWall(38, 31, TileType.WallV); + wallController.toggleWall(38, 32, TileType.WallV); + wallController.toggleWall(38, 33, TileType.WallV); + wallController.toggleWall(37, 34, TileType.WallH); + wallController.toggleWall(36, 34, TileType.WallH); + wallController.toggleWall(35, 34, TileType.WallH); + wallController.toggleWall(34, 34, TileType.WallH); + wallController.toggleWall(33, 34, TileType.WallH); + wallController.toggleWall(33, 34, TileType.WallV); + } + + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } + + return map; +} diff --git a/src/ts/server/maps/customMap.ts b/src/ts/server/maps/customMap.ts new file mode 100644 index 0000000..9b9d050 --- /dev/null +++ b/src/ts/server/maps/customMap.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import * as entities from '../../common/entities'; +import { rect } from '../../common/rect'; +import { TileType, MapType } from '../../common/interfaces'; +import { createServerMap, deserializeMap } from '../serverMap'; +import { World, goToMap } from '../world'; +import { createSign } from '../controllerUtils'; +import { ServerEntity } from '../serverInterfaces'; +import { pathTo } from '../paths'; + +// load tile data +// To customize the map use in-game editor tools to change tiles, then use `/savemap custom` command, +// your map will be saved to `/store/custom.json` file, move the file to `/src/maps/custom.json` +// and restart the server. +const mapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'custom.json'), 'utf8')); + +export function createCustomMap(world: World) { + // size: 4 by 4 regions -> 32 by 32 tiles + // default tiles: grass + const map = createServerMap('custom', MapType.None, 4, 4, TileType.Grass); + + // initialize tiles + deserializeMap(map, mapData); + + // place default spawn point at the center of the map + map.spawnArea = rect(map.width / 2, map.height / 2, 0, 0); + + // shorthand for adding entities + function add(entity: ServerEntity) { + world.addEntity(entity, map); + } + + // place return sign 2 tiles north of center of the map + add(createSign(map.width / 2, map.height / 2 - 2, 'Go back', (_, client) => goToMap(world, client, '', 'center'))); + + // place barrel at 5, 5 location + add(entities.barrel(5, 5)); + + // place more entities here ... + + return map; +} diff --git a/src/ts/server/maps/houseMap.ts b/src/ts/server/maps/houseMap.ts new file mode 100644 index 0000000..142ab79 --- /dev/null +++ b/src/ts/server/maps/houseMap.ts @@ -0,0 +1,189 @@ +import * as entities from '../../common/entities'; +import { rect } from '../../common/rect'; +import { ServerMap, MapUsage, ServerEntity } from '../serverInterfaces'; +import { World, goToMap } from '../world'; +import { addSpawnPointIndicators } from '../mapUtils'; +import { TileType, MapType, MapFlags, EntityState } from '../../common/interfaces'; +import { createServerMap, setTile, MapData, saveMap } from '../serverMap'; +import { WallController } from '../controllers'; +import { resetRegionUpdates } from '../serverRegion'; +import { getTile } from '../../common/worldMap'; +import { tileHeight, HOUSE_ENTITY_LIMIT } from '../../common/constants'; + +export let defaultHouseSave: MapData | undefined = undefined; + +const toolboxX = 2.125; +const toolboxY = 15.41; + +export function createHouseMap(world: World, instanced: boolean, _template = false): ServerMap { + const map = createServerMap('house', MapType.House, 2, 2, TileType.Wood, instanced ? MapUsage.Party : MapUsage.Public); + + if (getTile(map, 0, 0) !== TileType.None) { + for (let x = 0; x < map.width; x++) { + setTile(map, x, 0, TileType.None); + setTile(map, x, 1, TileType.None); + setTile(map, x, 2, TileType.None); + } + } + + setTile(map, 4, map.height - 1, TileType.Stone); + setTile(map, 5, map.height - 1, TileType.Stone); + + map.usage = instanced ? MapUsage.Party : MapUsage.Public; + map.spawnArea = rect(4, 8 + 6, 2, 1); + map.defaultTile = TileType.None; + map.flags |= MapFlags.EditableWalls | MapFlags.EditableEntities | MapFlags.EditableTiles; + map.editableEntityLimit = HOUSE_ENTITY_LIMIT; + map.editableArea = rect(0, 76 / tileHeight, map.width, map.height); + + const topWall = 3; + const windowY = 76 / tileHeight; + + const add = (entity: ServerEntity) => world.addEntity(entity, map); + const addEditable = (entity: ServerEntity) => (entity.state |= EntityState.Editable, add(entity)); + + add(entities.triggerDoor(5, map.height)) + .trigger = (_, client) => goToMap(world, client, instanced ? 'island' : 'public-island', 'house'); + + addEditable(entities.window1(2, windowY)); + addEditable(entities.window1(5, windowY)); + addEditable(entities.window1(8, windowY)); + addEditable(entities.window1(12, windowY)); + addEditable(entities.window1(14, windowY)); + + addEditable(entities.picture1(3.53, windowY)); + addEditable(entities.picture2(10.09, windowY)); + + addEditable(entities.table1(13.50, 13.20)); + addEditable(entities.table1(2.69, 5.63)); + addEditable(entities.table2(8.69, 11.60)); + + addEditable(entities.lanternOn(3.53, 15.13)); + addEditable(entities.lanternOn(6.56, 15.13)); + addEditable(entities.lanternOn(1.43, 14.04)); + addEditable(entities.lanternOn(9.97, 6.67)); + addEditable(entities.lanternOn(13.50, 6.54)); + addEditable(entities.lanternOn(10.09, 15.13)); + addEditable(entities.lanternOn(14.09, 15.04)); + addEditable(entities.lanternOn(9.44, 4.67)); + addEditable(entities.lanternOn(14.43, 8.54)); + + addEditable(entities.lanternOnWall(8.375, 12.70)); + addEditable(entities.lanternOnWall(9.09, 12.08)); + addEditable(entities.lanternOnWall(13.50, 13.33)); + addEditable(entities.lanternOnWall(2.69, 5.71)); + + addEditable(entities.cushion1(3.94, 4.83)); + addEditable(entities.cushion1(1.56, 4.63)); + addEditable(entities.cushion1(6.91, 10.54)); + addEditable(entities.cushion1(10.38, 11.96)); + addEditable(entities.cushion1(10.56, 10.54)); + addEditable(entities.cushion1(6.91, 12.17)); + addEditable(entities.cushion1(8.88, 13.08)); + addEditable(entities.cushion1(8.84, 9.67)); + addEditable(entities.cushion1(13.25, 4.75)); + addEditable(entities.cushion1(14.50, 3.67)); + addEditable(entities.cushion1(15.38, 5.00)); + addEditable(entities.cushion1(14.13, 6.17)); + addEditable(entities.cushion1(14.69, 12.25)); + addEditable(entities.cushion1(12.31, 12.29)); + addEditable(entities.cushion1(9.94, 5.00)); + addEditable(entities.cushion1(8.03, 5.00)); + + addEditable(entities.boxLanterns(0.72, 15.58)); + addEditable(entities.toolboxFull(toolboxX, toolboxY)); + + const wallController = new WallController(world, map, entities.woodenWalls); + map.controllers.push(wallController); + wallController.top = 3; + + if (wallController.toggleWall) { + for (let x = 0; x < map.width; x++) { + wallController.toggleWall(x, topWall, TileType.WallH); + + if (x !== 4 && x !== 5) { + wallController.toggleWall(x, map.height, TileType.WallH); + } + + if (x !== 5 && x !== 8 && x !== 12) { + wallController.toggleWall(x, 8, TileType.WallH); + } + } + + for (let x = 0; x < 3; x++) { + wallController.toggleWall(x, 13, TileType.WallH); + } + + for (let y = topWall; y < 8; y++) { + wallController.toggleWall(7, y, TileType.WallV); + wallController.toggleWall(11, y, TileType.WallV); + } + + for (let y = 8; y < map.height; y++) { + if (y !== 11 && y !== 14) { + wallController.toggleWall(3, y, TileType.WallV); + } + } + + for (let y = topWall; y < map.height; y++) { + wallController.toggleWall(0, y, TileType.WallV); + wallController.toggleWall(map.width, y, TileType.WallV); + } + } + + wallController.lockOuterWalls = true; + + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } + + for (const region of map.regions) { + resetRegionUpdates(region); + } + + if (!defaultHouseSave) { + defaultHouseSave = saveMap(map, { + saveTiles: true, saveEntities: true, saveOnlyEditableEntities: true, saveWalls: true + }); + } + + return map; +} + +export function resetHouseMap(map: ServerMap) { + for (const { tiles } of map.regions) { + for (let i = 0; i < tiles.length; i++) { + tiles[i] = TileType.Wood; + } + } +} + +function findEntityByType(map: ServerMap, type: number) { + for (const region of map.regions) { + for (const entity of region.entities) { + if (entity.type === type) { + return entity; + } + } + } + + return undefined; +} + +export function removeToolbox(world: World, map: ServerMap) { + const toolbox = findEntityByType(map, entities.toolboxFull.type); + + if (toolbox) { + world.removeEntity(toolbox, map); + } +} + +export function restoreToolbox(world: World, map: ServerMap) { + const toolbox = findEntityByType(map, entities.toolboxFull.type); + + if (!toolbox) { + const entity = entities.toolboxFull(toolboxX, toolboxY); + entity.state |= EntityState.Editable; + world.addEntity(entity, map); + } +} diff --git a/src/ts/server/maps/islandMap.ts b/src/ts/server/maps/islandMap.ts new file mode 100644 index 0000000..2b17869 --- /dev/null +++ b/src/ts/server/maps/islandMap.ts @@ -0,0 +1,386 @@ +import * as fs from 'fs'; +import { sample } from 'lodash'; +import * as entities from '../../common/entities'; +import { pathTo } from '../paths'; +import { ServerMap, MapUsage, ServerEntity } from '../serverInterfaces'; +import { World, goToMap } from '../world'; +import { + addSpawnPointIndicators, generateTileIndicesAndColliders, removePonies, worldForTemplates, createBunny +} from '../mapUtils'; +import { serverMapInstanceFromTemplate, createServerMap, copyMapTiles, deserializeMap } from '../serverMap'; +import { TileType, MapType, Season } from '../../common/interfaces'; +import { rect } from '../../common/rect'; +import { createAddLight, createSignWithText, createWoodenFenceMaker, createBoxOfLanterns } from '../controllerUtils'; +import { setEntityName, updateEntityOptions } from '../entityUtils'; +import { getNextToyOrExtra, holdItem } from '../playerUtils'; +import { tileWidth, tileHeight } from '../../common/constants'; +import { TorchController, UpdateController } from '../controllers'; +import { resetRegionUpdates } from '../serverRegion'; +import { point } from '../../common/utils'; + +const islandMapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'island.json'), 'utf8')); + +let islandMapTemplate: ServerMap | undefined; + +export function createIslandMap(world: World, instanced: boolean, template = false): ServerMap { + if (!template && !islandMapTemplate) { + islandMapTemplate = createIslandMap(worldForTemplates, false, true); + } + + const map = (instanced && islandMapTemplate) ? + serverMapInstanceFromTemplate(islandMapTemplate) : + createServerMap('island', MapType.Island, 7, 7, TileType.Water, instanced ? MapUsage.Party : MapUsage.Public); + + map.usage = instanced ? MapUsage.Party : MapUsage.Public; + map.spawnArea = rect(43.4, 20, 3.3, 2.5); + map.spawns.set('house', rect(27, 24, 2, 2)); + + if (islandMapTemplate) { + copyMapTiles(map, islandMapTemplate); + } else { + deserializeMap(map, islandMapData); + } + + const goto = instanced ? 'house' : 'public-house'; + const add = (entity: ServerEntity) => world.addEntity(entity, map); + const addEntities = (entities: ServerEntity[]) => entities.map(add); + + add(entities.house(28, 23)).interact = (_, client) => goToMap(world, client, goto); + add(entities.triggerHouseDoor(27.40, 22.87)).trigger = (_, client) => goToMap(world, client, goto); + + add(createBoxOfLanterns(25.5, 25.5)); + + const boxOfFruits = add(entities.boxFruits(20.72, 20.88)); + setEntityName(boxOfFruits, 'Box of fruits'); + + const giftPile = add(entities.giftPileInteractive(37.66, 18.21)); + giftPile.interact = (_, client) => updateEntityOptions(client.pony, getNextToyOrExtra(client)); + setEntityName(giftPile, 'Toy stash'); + + const types = entities.stashEntities.map(e => e.type); + + const itemSign = add(entities.signQuest(24.41, 25.00)); + itemSign.interact = (_, client) => { + const index = types.indexOf(client.pony.options!.hold || 0); + holdItem(client.pony, types[(index + 1) % types.length]); + }; + setEntityName(itemSign, 'Item stash'); + + const addTorch = createAddLight(world, map, entities.torch); + + addTorch(25.00, 24.00); + addTorch(39.69, 18.38); + addTorch(39.66, 21.67); + addTorch(23.34, 38.46); + addTorch(28.16, 31.79); + addTorch(29.13, 38.17); + addTorch(30.84, 29.42); + addTorch(29.69, 25.00); + addTorch(20.00, 27.88); + addTorch(22.63, 30.54); + addTorch(19.22, 32.08); + addTorch(25.50, 26.79); + addTorch(15.94, 26.42); + addTorch(15.88, 29.75); + + // pier + + const px = 1 / tileWidth; + const plankWidth = 78 / tileWidth; + const plankHeight = 12 / tileHeight; + const plankOffsets = [0, -1, 0, -2, -1, -1, 0, -2, -1, 0].map(x => x / tileWidth); + + addEntities(entities.fullBoat(45.06, 24)); + add(entities.pierLeg(41.31, 20.54)); + add(entities.pierLeg(43.00, 22.58)); + add(entities.pierLeg(44.84, 22.58)); + add(entities.pierLeg(46.65, 22.58)); + add(entities.barrel(46.83, 23.35)); + add(entities.lanternOn(46.19, 23.38)); + add(entities.lanternOn(42.63, 21.42)); + add(entities.lanternOn(47.00, 18.96)); + add(entities.triggerBoat(45.5, 24.8)).interact = (_, client) => goToMap(world, client, '', 'harbor'); + add(createSignWithText(43, 23.5, 'Return to land', `Hop on the boat to return to the mainland`)); + + for (let y = 0; y < 10; y++) { + const minX = y < 5 ? 0 : 1; + const maxX = (y % 2) ? 4 : 3; + const baseX = 40 + ((y % 2) ? 0 : (plankWidth / 2)) + plankOffsets[y]; + const baseY = 19 - (9 / tileHeight); + + for (let x = minX; x < maxX; x++) { + if ((x === minX && (y % 2)) || (x === (maxX - 1) && (y % 2))) { + const ox = x === minX ? (18 / tileWidth) : (-18 / tileWidth); + const plank = sample(entities.planksShort)!; + add(plank(baseX + ox + x * plankWidth, baseY + y * plankHeight)); + } else { + const plank = sample(entities.planks)!; + add(plank(baseX + x * plankWidth, baseY + y * plankHeight)); + } + } + } + + add(entities.plankShadow(41.31, 20.58)); + add(entities.plankShadowShort(43.03, 21.08)); + add(entities.plankShadowShort(43, 21.08 + plankHeight * 2)); + + const baseY = 18.58; + const baseX = 46.71; + add(entities.plankShadowShort(baseX, baseY)); + add(entities.plankShadowShort(baseX + 2 * px, baseY + plankHeight)); + add(entities.plankShadowShort(baseX, baseY + plankHeight * 2)); + add(entities.plankShadowShort(baseX + 1 * px, baseY + plankHeight * 3)); + add(entities.plankShadowShort(baseX - 1 * px, baseY + plankHeight * 4)); + add(entities.plankShadowShort(baseX + 2 * px, baseY + plankHeight * 5)); + add(entities.plankShadowShort(baseX, baseY + plankHeight * 6)); + add(entities.plankShadowShort(baseX + 1 * px, baseY + plankHeight * 7)); + add(entities.plankShadowShort(baseX - 1 * px, baseY + plankHeight * 8)); + add(entities.plankShadowShort(baseX + 3 * px, baseY + plankHeight * 9)); + + add(entities.collider3x1(40, 18)); + add(entities.collider3x1(43, 18)); + add(entities.collider2x1(46, 18)); + add(entities.collider1x3(47, 19)); + add(entities.collider1x3(47, 22)); + + add(entities.collider3x1(40, 21)); + add(entities.collider1x3(42, 22)); + add(entities.collider1x1(43, 24)); + add(entities.collider3x1(42, 25)); + add(entities.collider3x1(45, 25)); + add(entities.collider1x3(47.6, 23)); + add(entities.collider1x3(41.5, 22)); + + // lone boat + + addEntities(entities.fullBoat(27.12, 42, false)); + add(entities.plankShort3(27.43, 39.67)); + add(entities.plankShort3(27.46, 40.21)); + add(entities.plankShort3(27.43, 40.75)); + add(entities.plankShort3(27.46, 41.21)); + add(entities.pierLeg(27.96, 40.88)); + add(entities.pierLeg(27.02, 40.79)); + add(entities.plankShadowShort(27.5, 39.67)); + add(entities.plankShadowShort(27.5 + 1 * px, 39.67 + plankHeight)); + add(entities.plankShadowShort(27.5, 39.67 + plankHeight * 2)); + add(entities.plankShadowShort(27.5 + 1 * px, 39.67 + plankHeight * 3)); + add(entities.lanternOn(28.77, 42.27)); + + add(entities.collider3x1(24, 41)); + add(entities.collider2x1(24, 42)); + add(entities.collider1x2(26, 40)); + add(entities.collider1x2(28, 40)); + add(entities.collider2x1(28, 41)); + add(entities.collider3x1(24, 43)); + add(entities.collider3x1(27, 43)); + add(entities.collider1x1(29, 42)); + add(entities.collider1x3(29.7, 41)); + add(entities.collider1x3(23.5, 41)); + + const addWoodenFence = createWoodenFenceMaker(world, map); + + addWoodenFence(20, 20, 4); + addWoodenFence(20, 20, 4, false, true); + addWoodenFence(24, 20, 4, false, true); + addWoodenFence(20, 24, 1, true, true); + addWoodenFence(23, 24, 1, true, false, true); + + addEntities(entities.tree(22.41, 18.21, 0)); + addEntities(entities.tree(30.44, 23.46, 1 + 4)); + addEntities(entities.tree5(34.53, 27.04, 0)); + addEntities(entities.tree5(19.28, 21.75, 1)); + add(entities.pumpkin(23.22, 20.58)); + add(entities.pumpkin(20.53, 22.29)); + add(entities.pumpkin(20.91, 23.08)); + add(entities.largeLeafedBush2(35.03, 26.71)); + add(entities.largeLeafedBush4(34.13, 27.54)); + add(entities.largeLeafedBush2(20.34, 24.42)); + add(entities.largeLeafedBush3(19.72, 24.04)); + add(entities.largeLeafedBush3(23.03, 38.67)); + add(entities.largeLeafedBush4(23.72, 38.04)); + add(entities.barrel(39.53, 18.96)); + add(entities.barrel(38.78, 18.38)); + add(entities.barrel(38.47, 19.21)); + add(entities.barrel(24.63, 23.50)); + add(entities.treeStump1(20.25, 36.46)); + add(entities.lanternOn(20.75, 37.00)); + add(entities.lanternOn(36.06, 34.25)); + add(entities.lanternOn(37.63, 36.83)); + add(entities.lanternOn(37.84, 26.94)); + + add(entities.waterRock1(22.63, 41.83)); + add(entities.waterRock1(28.56, 13.75)); + add(entities.waterRock1(16.25, 20.88)); + add(entities.waterRock1(40.62, 29.62)); + add(entities.waterRock1(40.63, 17.96)); + add(entities.waterRock2(40.22, 17.50)); + add(entities.waterRock2(20.34, 15.75)); + add(entities.waterRock2(15.41, 34.13)); + add(entities.waterRock2(36.50, 40.63)); + add(entities.waterRock3(36.13, 40.96)); + add(entities.waterRock3(41.63, 34.38)); + add(entities.waterRock3(15.16, 34.67)); + add(entities.waterRock3(20.88, 15.50)); + add(entities.waterRock4(36.56, 41.13)); + add(entities.waterRock4(40.72, 17.29)); + add(entities.waterRock4(28.22, 13.25)); + add(entities.waterRock5(28.66, 13.17)); + add(entities.waterRock5(16.84, 20.42)); + add(entities.waterRock5(14.44, 30.50)); + add(entities.waterRock5(23.13, 41.88)); + add(entities.waterRock5(30.44, 39.71)); + add(entities.waterRock5(41.84, 34.83)); + add(entities.waterRock5(36.63, 14.75)); + add(entities.waterRock6(36.25, 14.54)); + add(entities.waterRock6(14.84, 34.29)); + add(entities.waterRock6(40.44, 38.71)); + add(entities.waterRock7(42.13, 34.42)); + add(entities.waterRock7(20.34, 15.25)); + add(entities.waterRock7(28.78, 40.79)); + add(entities.waterRock7(22.66, 42.08)); + add(entities.waterRock8(22.25, 41.92)); + add(entities.waterRock8(14.13, 30.79)); + add(entities.waterRock8(16.28, 20.21)); + add(entities.waterRock8(42.19, 34.79)); + add(entities.waterRock9(36.78, 14.29)); + add(entities.waterRock9(13.97, 30.33)); + add(entities.waterRock10(14.50, 25.83)); + add(entities.waterRock10(18.88, 40.75)); + add(entities.waterRock11(25.37, 13.75)); + add(entities.waterRock11(18.44, 40.58)); + add(entities.waterRock11(15.41, 24.33)); + add(entities.waterRock4(15.13, 23.96)); + add(entities.waterRock11(35.38, 31.08)); + add(entities.waterRock8(35.09, 30.71)); + add(entities.waterRock3(34.03, 29.79)); + add(entities.waterRock4(34.31, 29.67)); + add(entities.waterRock4(24.50, 35.92)); + add(entities.waterRock3(33.44, 38.21)); + add(entities.waterRock4(33.16, 37.92)); + add(entities.waterRock1(25.50, 17.79)); + add(entities.waterRock9(25.66, 18.13)); + add(entities.waterRock4(25.97, 17.79)); + + add(entities.flower3Pickable(30.25, 24.92)).interact = (_, { pony }) => holdItem(pony, entities.flowerPick.type); + + add(entities.bench1(37.78, 24.96)); + add(entities.benchSeat(37.75, 28.29)); + add(entities.benchBack(37.75, 29.17)); + + // small island bit + addEntities(entities.tree(9.16, 16.50, 2 + 8)); + add(entities.waterRock1(5.34, 23.71)); + add(entities.waterRock1(13.28, 14.33)); + add(entities.waterRock1(11.19, 24.83)); + add(entities.waterRock3(10.94, 25.21)); + add(entities.waterRock3(6.31, 18.92)); + add(entities.waterRock3(13.59, 14.71)); + add(entities.waterRock5(13.84, 14.17)); + add(entities.waterRock4(11.38, 25.13)); + add(entities.waterRock6(6.41, 15.92)); + add(entities.waterRock8(5.16, 23.13)); + add(entities.waterRock10(6.06, 16.33)); + add(entities.waterRock11(14.66, 20.75)); + add(entities.torch(9.31, 17.83)); + add(entities.torch(9.44, 21.67)); + add(entities.torch(12.75, 18.29)); + + if (world.season === Season.Summer || world.season === Season.Spring) { + add(entities.flowerPatch1(21.16, 26.04)); + add(entities.flowerPatch3(35.81, 34.75)); + add(entities.flowerPatch3(23.09, 32.29)); + add(entities.flowerPatch5(19.63, 33.04)); + add(entities.flowerPatch5(37.38, 26.42)); + add(entities.flowerPatch5(18.75, 26.38)); + add(entities.flowerPatch5(35.38, 33.88)); + add(entities.flowerPatch6(25.78, 31.04)); + add(entities.flowerPatch6(33.00, 33.79)); + add(entities.flowerPatch6(38.72, 26.00)); + add(entities.flowerPatch3(11.34, 19.33)); + add(entities.flowerPatch6(11.09, 17.79)); + add(entities.flowerPatch7(8.63, 21.67)); + } + + if (world.season === Season.Autumn) { + add(entities.leafpileStickRed(31.00, 22.75)); + add(entities.leaves5(18.41, 21.46)); + add(entities.leaves2(21.59, 18.17)); + add(entities.leaves2(23.56, 18.00)); + add(entities.leaves1(22.00, 17.21)); + add(entities.leaves1(22.47, 20.00)); + add(entities.leaves2(33.69, 25.42)); + add(entities.leaves1(33.47, 26.75)); + add(entities.leaves1(35.66, 27.08)); + add(entities.leaves3(30.31, 24.04)); + add(entities.leaves1(31.56, 23.00)); + add(entities.leaves1(29.47, 23.21)); + add(entities.leaves3(8.84, 17.04)); + add(entities.leaves2(8.91, 15.25)); + add(entities.leaves1(10.44, 16.46)); + } + + addEntities(createBunny([ + point(22.19, 27.25), + point(19.75, 26.13), + point(18.44, 29.04), + point(20.41, 31.42), + point(19.94, 33.04), + point(22.88, 33.67), + point(24.91, 31.25), + point(26.09, 32.50), + point(26.34, 34.83), + point(32.50, 35.46), + point(34.03, 34.67), + point(36.06, 36.50), + point(36.47, 35.33), + point(36.22, 34.63), + point(33.63, 34.83), + point(31.88, 33.25), + point(32.00, 28.46), + point(33.22, 25.75), + point(35.09, 24.75), + point(36.13, 25.67), + point(36.72, 27.38), + point(38.31, 27.42), + point(38.63, 26.54), + point(37.31, 26.63), + point(36.06, 26.42), + point(35.34, 24.29), + point(33.13, 25.83), + point(30.06, 25.13), + point(26.50, 26.38), + point(24.66, 28.25), + point(22.25, 28.50), + point(20.97, 27.00), + point(19.13, 27.13), + point(20.69, 29.25), + point(22.34, 29.42), + point(21.53, 31.46), + point(23.50, 31.54), + point(23.81, 29.71), + ])); + + map.controllers.push(new TorchController(world, map)); + map.controllers.push(new UpdateController(map)); + + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } + + if (!islandMapTemplate) { + generateTileIndicesAndColliders(map); + } + + return map; +} + +export function resetIslandMap(map: ServerMap) { + copyMapTiles(map, islandMapTemplate!); + + for (const region of map.regions) { + region.clients = []; + removePonies(region.entities); + removePonies(region.movables); + resetRegionUpdates(region); + } +} diff --git a/src/ts/server/maps/mainMap.ts b/src/ts/server/maps/mainMap.ts new file mode 100644 index 0000000..60fecd1 --- /dev/null +++ b/src/ts/server/maps/mainMap.ts @@ -0,0 +1,4646 @@ +import * as fs from 'fs'; +import { sample, random } from 'lodash'; +import { pathTo } from '../paths'; +import { World, goToMap } from '../world'; +import { createServerMap, deserializeMap, snapshotTiles, setTile, lockTile, lockTiles } from '../serverMap'; +import { TileType, Season, MapType, Holiday, MessageType, ServerFlags, MapFlags } from '../../common/interfaces'; +import { rect } from '../../common/rect'; +import * as entities from '../../common/entities'; +import { + addSpawnPointIndicators, createDirectionSign, pickEntity, checkNotCollecting, pickGift, checkLantern, + pickCandy, checkBasket, pickEgg, pickClover, positionClover, createBunny +} from '../mapUtils'; +import { + give, createWoodenFenceMaker, createStoneWallFenceMaker, createSign, createSignWithText, createAddLight, + boopLight, createBoxOfLanterns +} from '../controllerUtils'; +import { ServerEntity, IClient, ServerMap } from '../serverInterfaces'; +import { logger } from '../logger'; +import { getEntityTypeName, GhostAnimation, CatAnimation, SignIcon } from '../../common/entities'; +import { holdItem, isGift, unholdItem, getNextToyOrExtra } from '../playerUtils'; +import { toInt, hasFlag, point } from '../../common/utils'; +import { updateAccountState } from '../accountUtils'; +import { saySystem, sayToAll } from '../chat'; +import { tileHeight, tileWidth } from '../../common/constants'; +import { updateEntityOptions, setEntityAnimation } from '../entityUtils'; +import { toWorldX } from '../../common/positionUtils'; +import { deserializeTiles } from '../../common/compress'; +import * as ctrl from '../controllers'; +import { isNightTime, isDayTime } from '../../common/timeUtils'; + +const mainMapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'main.json'), 'utf8')); +const mainMapTiles = deserializeTiles(mainMapData.tiles); + +function createCookieTable(x: number, y: number) { + return entities.cookieTable(x, y + 0.5) as ServerEntity; +} + +function createCookieTable2(x: number, y: number) { + return entities.cookieTable2(x, y + 0.5) as ServerEntity; +} + +function createToyStash(x: number, y: number) { + return [ + entities.giftPileSign(x, y - 0.1), + createSign(x, y, 'Toy Stash', (_, client) => updateEntityOptions(client.pony, getNextToyOrExtra(client)), entities.sign), + ]; +} + +function donateGift(_: any, client: IClient) { + if (client.account.state && client.account.state.gifts) { + let count = 0; + updateAccountState(client.account, state => state.gifts = count = Math.max(0, toInt(state.gifts) - 1)); + saySystem(client, `${count} 🎁`); + } + + if (isGift(client.pony.options && client.pony.options.hold)) { + unholdItem(client.pony); + } +} + +function donateCandy(_: any, client: IClient) { + if (client.account.state && client.account.state.candies) { + let count = 0; + updateAccountState(client.account, state => state.candies = count = Math.max(0, toInt(state.candies) - 1)); + saySystem(client, `${count} 🍬`); + } +} + +function donateEgg(_: any, client: IClient) { + if (client.account.state && client.account.state.eggs) { + let count = 0; + updateAccountState(client.account, state => state.eggs = count = Math.max(0, toInt(state.eggs) - 1)); + saySystem(client, `${count} 🥚`); + } +} + +function removeSeasonalObjects(world: World, map: ServerMap) { + const remove: ServerEntity[] = []; + + for (const region of map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.serverFlags, ServerFlags.Seasonal)) { + remove.push(entity); + } + } + } + + for (const entity of remove) { + world.removeEntity(entity, map); + } +} + +function addSeasonalObjects(world: World, map: ServerMap, season: Season, holiday: Holiday) { + const isWinter = season === Season.Winter; + const isAutumn = season === Season.Autumn; + const isSpring = season === Season.Spring; + const isSummer = season === Season.Summer; + const isHalloween = holiday === Holiday.Halloween; + const isChristmas = holiday === Holiday.Christmas; + const isEaster = holiday === Holiday.Easter; + + function add(entity: ServerEntity) { + entity.serverFlags! |= ServerFlags.Seasonal; + return world.addEntity(entity, map); + } + + function addEntities(entities: ServerEntity[]) { + return entities.map(add); + } + + function addHolly(x: number, y: number) { + add(entities.holly(x, y + (1 / tileHeight))); + } + + function addHollyDecoration(x: number, y: number, a = true, b = true, c = true, d = true) { + if (isChristmas) { + a && addHolly(x - 2.8, y); + b && addHolly(x - 1, y); + c && addHolly(x + 1, y); + d && addHolly(x + 2.8, y); + } + } + + addHollyDecoration(65.00, 71.00, true, false, true, false); + addHollyDecoration(64.00, 76.00); + addHollyDecoration(55.00, 76.50, true, false); + + if (isSpring || isSummer) { + add(entities.flowerPatch1(70.00, 55.00)); + add(entities.flowerPatch2(46.50, 78.00)); + add(entities.flowerPatch2(80.00, 49.50)); + add(entities.flowerPatch2(100.60, 75.50)); + add(entities.flowerPatch2(78.00, 92.00)); + add(entities.flowerPatch3(69.50, 69.00)); + add(entities.flowerPatch3(48.80, 82.00)); + add(entities.flowerPatch3(86.50, 53.50)); + add(entities.flowerPatch4(66.00, 61.00)); + add(entities.flowerPatch4(87.50, 63.50)); + add(entities.flowerPatch4(56.00, 89.70)); + add(entities.flowerPatch5(53.50, 68.00)); + add(entities.flowerPatch5(77.70, 96.00)); + add(entities.flowerPatch5(77.00, 71.30)); + add(entities.flowerPatch5(87.50, 69.00)); + add(entities.flowerPatch7(93.40, 65.70)); + add(entities.flowerPatch1(112.09, 64.63)); + add(entities.flowerPatch2(107.70, 53.77)); + add(entities.flowerPatch2(69.16, 107.00)); + add(entities.flowerPatch3(106.73, 94.79)); + add(entities.flowerPatch3(113.59, 61.96)); + add(entities.flowerPatch3(70.50, 104.90)); + add(entities.flowerPatch4(58.03, 112.92)); + add(entities.flowerPatch4(53.31, 99.88)); + add(entities.flowerPatch6(106.72, 93.46)); + add(entities.flowerPatch5(116.31, 50.94)); + + add(entities.flowerPatch1(125.31, 148.25)); + add(entities.flowerPatch1(153.06, 133.58)); + add(entities.flowerPatch1(143.68, 87.20)); + add(entities.flowerPatch1(85.25, 150.75)); + add(entities.flowerPatch1(66.71, 139.75)); + + add(entities.flowerPatch2(79.13, 148.92)); + add(entities.flowerPatch2(65.56, 126.83)); + add(entities.flowerPatch2(149.75, 147.42)); + add(entities.flowerPatch2(131.78, 87.12)); + + add(entities.flowerPatch3(143.13, 151.50)); + add(entities.flowerPatch3(148.19, 131.58)); + add(entities.flowerPatch3(152.81, 95.42)); + add(entities.flowerPatch3(123.94, 83.00)); + add(entities.flowerPatch3(92.38, 156.08)); + add(entities.flowerPatch3(114.88, 134.08)); + add(entities.flowerPatch3(119.13, 154.67)); + add(entities.flowerPatch3(69.41, 133.00)); + add(entities.flowerPatch3(75.69, 154.33)); + + add(entities.flowerPatch4(62.44, 131.25)); + add(entities.flowerPatch4(86.63, 146.58)); + add(entities.flowerPatch4(58.69, 150.83)); + add(entities.flowerPatch4(123.78, 155.08)); + add(entities.flowerPatch4(146.72, 153.25)); + add(entities.flowerPatch4(143.81, 95.75)); + add(entities.flowerPatch4(126.75, 85.92)); + + add(entities.flowerPatch5(144.03, 148.25)); + add(entities.flowerPatch5(150.09, 134.08)); + add(entities.flowerPatch5(133.28, 91.08)); + add(entities.flowerPatch5(151.09, 97.83)); + add(entities.flowerPatch5(124.47, 85.33)); + add(entities.flowerPatch5(85.41, 157.08)); + add(entities.flowerPatch5(100.66, 147.75)); + add(entities.flowerPatch5(114.91, 132.50)); + add(entities.flowerPatch5(90.47, 132.08)); + add(entities.flowerPatch5(84.16, 148.67)); + add(entities.flowerPatch5(66.50, 133.25)); + add(entities.flowerPatch5(67.94, 138.67)); + add(entities.flowerPatch5(59.44, 153.25)); + add(entities.flowerPatch5(74.44, 156.33)); + + add(entities.flowerPatch2(41.28, 96.54)); + add(entities.flowerPatch3(25.19, 88.21)); + add(entities.flowerPatch3(21.66, 109.17)); + add(entities.flowerPatch4(8.53, 102.63)); + add(entities.flowerPatch5(23.28, 112.21)); + add(entities.flowerPatch1(36.00, 126.46)); + add(entities.flowerPatch3(38.50, 123.92)); + add(entities.flowerPatch4(31.50, 126.58)); + add(entities.flowerPatch4(27.81, 122.46)); + add(entities.flowerPatch3(14.91, 140.08)); + add(entities.flowerPatch5(14.75, 137.88)); + add(entities.flowerPatch3(11.66, 152.75)); + add(entities.flowerPatch4(9.69, 150.92)); + add(entities.flowerPatch4(2.66, 117.96)); + add(entities.flowerPatch2(4.44, 113.21)); + add(entities.flowerPatch2(37.97, 64.63)); + add(entities.flowerPatch4(38.94, 62.25)); + add(entities.flowerPatch2(24.28, 61.46)); + add(entities.flowerPatch5(24.53, 60.42)); + add(entities.flowerPatch6(25.84, 66.04)); + add(entities.flowerPatch4(26.25, 45.54)); + add(entities.flowerPatch6(26.56, 43.75)); + add(entities.flowerPatch2(22.63, 24.88)); + add(entities.flowerPatch4(23.47, 21.96)); + add(entities.flowerPatch4(30.69, 14.63)); + add(entities.flowerPatch3(32.81, 16.42)); + add(entities.flowerPatch3(46.13, 4.79)); + add(entities.flowerPatch3(98.97, 10.46)); + add(entities.flowerPatch4(100.94, 44.96)); + add(entities.flowerPatch4(124.34, 43.25)); + add(entities.flowerPatch3(134.41, 45.88)); + add(entities.flowerPatch4(135.47, 27.33)); + add(entities.flowerPatch6(136.63, 29.92)); + add(entities.flowerPatch1(131.78, 4.50)); + add(entities.flowerPatch4(127.97, 4.67)); + add(entities.flowerPatch4(122.09, 28.42)); + add(entities.flowerPatch6(121.53, 30.96)); + add(entities.flowerPatch6(122.31, 41.63)); + add(entities.flowerPatch6(101.91, 47.17)); + add(entities.flowerPatch3(50.66, 39.42)); + add(entities.flowerPatch4(48.47, 38.00)); + add(entities.flowerPatch6(48.78, 41.46)); + add(entities.flowerPatch6(32.63, 46.13)); + add(entities.flowerPatch6(14.94, 55.08)); + add(entities.flowerPatch7(14.25, 56.79)); + + // hill above orchard + add(entities.flowerPatch1(81.56, 17.71)); + add(entities.flowerPatch1(72.44, 12.46)); + add(entities.flowerPatch2(80.38, 13.25)); + add(entities.flowerPatch3(74.09, 21.00)); + add(entities.flowerPatch3(75.25, 5.83)); + add(entities.flowerPatch3(85.31, 10.25)); + add(entities.flowerPatch4(80.25, 4.96)); + add(entities.flowerPatch4(77.38, 18.17)); + add(entities.flowerPatch4(64.22, 5.63)); + add(entities.flowerPatch5(73.69, 19.29)); + add(entities.flowerPatch5(90.22, 11.29)); + add(entities.flowerPatch5(77.34, 3.67)); + add(entities.flowerPatch5(64.88, 12.25)); + add(entities.flowerPatch6(70.34, 12.92)); + add(entities.flowerPatch6(85.75, 9.04)); + add(entities.flowerPatch6(82.91, 3.79)); + add(entities.flowerPatch6(61.38, 5.71)); + add(entities.flowerPatch7(73.63, 3.96)); + add(entities.flowerPatch7(74.53, 18.38)); + add(entities.flowerPatch7(67.19, 11.17)); + + add(entities.cloverPatch4(43.94, 116.83)); + add(entities.cloverPatch4(51.00, 116.29)); + add(entities.cloverPatch4(60.47, 117.92)); + add(entities.cloverPatch4(58.16, 109.04)); + add(entities.cloverPatch3(60.66, 116.17)); + add(entities.cloverPatch3(57.13, 117.50)); + add(entities.cloverPatch3(58.06, 110.46)); + add(entities.cloverPatch5(48.88, 113.58)); + add(entities.cloverPatch5(64.38, 113.75)); + add(entities.cloverPatch5(41.97, 105.54)); + add(entities.cloverPatch4(52.97, 103.71)); + add(entities.cloverPatch3(79.19, 115.25)); + add(entities.cloverPatch3(89.06, 117.08)); + add(entities.cloverPatch4(83.25, 110.67)); + add(entities.cloverPatch5(82.50, 113.54)); + add(entities.cloverPatch6(85.84, 117.92)); + add(entities.cloverPatch3(103.38, 108.17)); + add(entities.cloverPatch3(110.06, 117.38)); + add(entities.cloverPatch5(113.78, 117.92)); + add(entities.cloverPatch5(105.91, 107.00)); + add(entities.cloverPatch5(104.47, 109.75)); + add(entities.cloverPatch4(117.47, 97.17)); + add(entities.cloverPatch6(89.69, 83.46)); + add(entities.cloverPatch6(108.50, 73.88)); + add(entities.cloverPatch3(99.00, 64.80)); + add(entities.cloverPatch5(97.78, 66.63)); + add(entities.cloverPatch5(90.38, 69.29)); + add(entities.cloverPatch6(88.81, 65.21)); + add(entities.cloverPatch6(80.59, 53.42)); + add(entities.cloverPatch6(115.03, 45.58)); + add(entities.cloverPatch7(84.66, 55.29)); + add(entities.cloverPatch7(116.16, 45.79)); + add(entities.cloverPatch5(81.72, 54.29)); + add(entities.cloverPatch5(43.53, 75.83)); + add(entities.cloverPatch7(46.38, 76.63)); + add(entities.cloverPatch3(69.98, 48.58)); + add(entities.cloverPatch3(73.11, 43.50)); + add(entities.cloverPatch5(67.27, 48.31)); + add(entities.cloverPatch5(74.25, 45.19)); + add(entities.cloverPatch6(69.94, 47.40)); + add(entities.cloverPatch7(89.36, 63.65)); + add(entities.clover1(72.00, 70.00)); + add(entities.cloverPatch4(55.38, 148.38)); + add(entities.cloverPatch4(42.91, 141.83)); + add(entities.cloverPatch5(64.66, 145.75)); + add(entities.cloverPatch5(48.78, 131.00)); + add(entities.cloverPatch6(46.34, 131.96)); + add(entities.cloverPatch6(91.16, 146.83)); + add(entities.cloverPatch4(103.41, 137.33)); + add(entities.cloverPatch6(134.22, 149.08)); + add(entities.cloverPatch6(133.78, 135.79)); + add(entities.cloverPatch3(151.78, 128.38)); + add(entities.cloverPatch4(156.84, 109.63)); + add(entities.cloverPatch4(123.19, 108.54)); + add(entities.cloverPatch3(152.09, 91.63)); + add(entities.cloverPatch3(126.94, 97.04)); + add(entities.cloverPatch7(149.22, 81.13)); + add(entities.cloverPatch6(157.34, 74.00)); + } + + if (isAutumn) { + add(entities.leafpileStickRed(151.19, 98.60)); + add(entities.leaves5(150.11, 99.15)); + add(entities.leaves5(143.66, 105.88)); + add(entities.leaves4(149.91, 106.00)); + add(entities.leaves2(143.14, 100.50)); + add(entities.leaves1(67.00, 48.00)); + add(entities.leaves2(68.00, 45.00)); + add(entities.leaves3(61.20, 71.50)); + add(entities.leaves3(46.00, 46.00)); + add(entities.leaves3(86.00, 47.00)); + add(entities.leaves4(83.50, 64.00)); + add(entities.leaves4(88.50, 58.00)); + add(entities.leaves4(71.50, 47.00)); + add(entities.leaves4(45.00, 76.20)); + add(entities.leaves4(84.00, 55.50)); + add(entities.leaves5(82.00, 54.00)); + add(entities.leaves5(69.00, 53.00)); + add(entities.leaves5(42.50, 48.70)); + add(entities.leaves3(70.50, 70.50)); + add(entities.leaves5(53.00, 87.00)); + add(entities.leaves4(57.00, 89.00)); + add(entities.leaves4(117.28, 46.00)); + add(entities.leaves2(61.00, 86.00)); + add(entities.leaves3(66.00, 87.00)); + add(entities.leaves1(69.00, 87.50)); + add(entities.leaves2(72.00, 85.50)); + add(entities.leafpileStickOrange(71.00, 69.50)); + add(entities.leafpileSmallYellow(44.00, 45.50)); + add(entities.leafpileSmallOrange(64.00, 72.00)); + add(entities.leafpileStickOrange(67.50, 47.30)); + add(entities.leafpileStickYellow(84.50, 46.30)); + add(entities.leafpileMediumYellow(47.30, 75.50)); + add(entities.leafpileMediumRed(51.00, 89.00)); + add(entities.leafpileBigYellow(79.00, 54.50)); + add(entities.leafpileBigstickRed(45.00, 52.20)); + add(entities.leafpileBigstickRed(50.50, 88.00)); + add(entities.leafpileSmallOrange(66.40, 58.00)); + add(entities.leafpileBigstickOrange(66.50, 69.80)); + add(entities.leafpileStickOrange(67.50, 71.60)); + add(entities.leafpileSmallOrange(67.00, 70.50)); + add(entities.leafpileSmallYellow(91.00, 95.70)); + add(entities.leafpileBigYellow(84.00, 86.30)); + add(entities.leafpileBigRed(94.00, 91.40)); + add(entities.leafpileBigYellow(76.00, 96.00)); + add(entities.leafpileMediumYellow(77.50, 97.50)); + add(entities.leafpileBigstickRed(85.80, 74.00)); + add(entities.leafpileMediumOrange(87.00, 75.50)); + add(entities.leafpileMediumAltOrange(89.00, 83.50)); + add(entities.leafpileMediumRed(80.00, 92.50)); + add(entities.leafpileSmallOrange(80.00, 84.80)); + add(entities.leaves1(85.00, 90.00)); + add(entities.leaves2(89.00, 95.00)); + add(entities.leaves3(92.00, 93.00)); + add(entities.leaves4(95.00, 88.00)); + add(entities.leaves5(90.00, 85.00)); + add(entities.leaves1(93.00, 77.00)); + add(entities.leaves2(86.00, 83.00)); + add(entities.leaves3(77.00, 82.00)); + add(entities.leaves4(75.00, 84.00)); + add(entities.leaves5(77.00, 91.00)); + add(entities.leaves1(84.00, 92.00)); + add(entities.leaves2(83.00, 97.00)); + add(entities.leaves3(89.00, 95.00)); + add(entities.leaves4(95.00, 94.00)); + add(entities.leaves5(97.00, 88.00)); + add(entities.leaves1(75.00, 81.00)); + add(entities.leaves2(79.00, 95.00)); + add(entities.leaves1(79.00, 77.00)); + add(entities.leaves2(83.00, 73.00)); + add(entities.leaves3(87.00, 71.00)); + add(entities.leaves4(98.00, 73.00)); + add(entities.leafpileBigYellow(96.25, 103.29)); + add(entities.leafpileBigstickYellow(127.69, 95.46)); + add(entities.leafpileBigOrange(118.63, 116.04)); + add(entities.leafpileBigYellow(133.50, 135.38)); + add(entities.leafpileBigRed(118.06, 136.38)); + add(entities.leafpileBigOrange(143.25, 84.21)); + add(entities.leafpileBigstickRed(89.69, 114.71)); + add(entities.leafpileMediumRed(83.19, 112.88)); + add(entities.leafpileMediumOrange(110.81, 126.13)); + add(entities.leafpileMediumYellow(121.81, 137.21)); + add(entities.leafpileMediumRed(137.50, 131.71)); + add(entities.leafpileMediumOrange(123.69, 97.96)); + add(entities.leafpileMediumYellow(109.75, 81.29)); + add(entities.leaves5(101.16, 112.71)); + add(entities.leaves5(152.19, 91.21)); + add(entities.leaves5(100.06, 83.79)); + add(entities.leaves5(151.31, 125.46)); + add(entities.leaves5(147.19, 116.13)); + add(entities.leaves5(141.63, 135.04)); + add(entities.leaves4(115.38, 127.29)); + add(entities.leaves4(143.69, 133.38)); + add(entities.leaves4(148.56, 118.88)); + add(entities.leaves4(150.44, 93.88)); + add(entities.leaves4(107.25, 86.04)); + add(entities.leaves4(122.88, 108.38)); + add(entities.leaves3(153.25, 94.96)); + add(entities.leaves3(156.44, 108.96)); + add(entities.leaves3(142.63, 134.71)); + add(entities.leaves3(151.44, 131.46)); + add(entities.leaves3(101.50, 114.71)); + add(entities.leaves3(102.50, 111.46)); + add(entities.leaves3(116.50, 128.79)); + add(entities.leaves3(107.06, 84.96)); + add(entities.leaves3(122.63, 110.46)); + add(entities.leaves3(132.94, 105.29)); + add(entities.leaves2(148.88, 82.13)); + add(entities.leaves2(109.38, 85.04)); + add(entities.leaves2(103.81, 113.29)); + add(entities.leaves2(130.06, 94.63)); + add(entities.leaves2(120.56, 106.79)); + add(entities.leaves2(148.19, 120.54)); + add(entities.leaves2(142.00, 137.13)); + add(entities.leaves2(152.81, 132.38)); + add(entities.leaves1(157.69, 88.71)); + add(entities.leaves1(149.69, 80.13)); + add(entities.leaves1(105.50, 84.21)); + add(entities.leaves1(108.63, 113.38)); + add(entities.leaves1(116.81, 116.79)); + add(entities.leaves1(117.50, 128.46)); + + add(entities.leaves5(65.09, 24.92)); + add(entities.leaves5(42.56, 22.00)); + add(entities.leaves5(45.13, 28.96)); + add(entities.leaves5(71.13, 32.71)); + add(entities.leaves4(62.31, 32.54)); + add(entities.leaves3(41.78, 28.50)); + add(entities.leaves3(63.97, 24.17)); + add(entities.leaves2(66.75, 29.21)); + add(entities.leaves2(68.91, 31.42)); + add(entities.leaves2(59.72, 43.71)); + add(entities.leaves1(61.00, 42.75)); + add(entities.leaves1(47.53, 28.92)); + add(entities.leaves1(41.00, 25.04)); + add(entities.leaves3(66.09, 22.00)); + add(entities.leaves3(72.25, 21.00)); + add(entities.leaves4(69.91, 20.50)); + add(entities.leaves2(46.09, 43.88)); + add(entities.leafpileMediumRed(43.31, 29.25)); + add(entities.leafpileStickYellow(66.63, 27.75)); + add(entities.leafpileMediumYellow(62.44, 16.54)); + add(entities.leaves3(56.81, 17.17)); + add(entities.leaves1(50.16, 31.04)); + add(entities.leafpileBigYellow(32.69, 69.21)); + add(entities.leafpileMediumAltYellow(12.31, 63.63)); + add(entities.leafpileSmallOrange(24.16, 78.67)); + add(entities.leafpileStickYellow(11.88, 64.33)); + add(entities.leaves5(30.59, 66.50)); + add(entities.leaves5(32.41, 54.00)); + add(entities.leaves5(12.34, 61.67)); + add(entities.leaves4(25.91, 77.17)); + add(entities.leaves4(22.81, 66.79)); + add(entities.leaves4(9.94, 57.79)); + add(entities.leaves3(29.69, 54.54)); + add(entities.leaves3(28.97, 64.92)); + add(entities.leaves3(24.41, 79.54)); + add(entities.leaves3(10.97, 61.04)); + add(entities.leaves2(24.94, 67.25)); + add(entities.leaves2(34.81, 68.46)); + add(entities.leaves2(32.13, 56.08)); + add(entities.leaves2(10.97, 63.83)); + add(entities.leaves2(29.03, 77.38)); + add(entities.leaves2(16.09, 48.46)); + add(entities.leaves4(10.75, 46.75)); + add(entities.leaves1(8.53, 59.79)); + add(entities.leaves1(28.75, 66.67)); + add(entities.leaves1(29.97, 51.79)); + add(entities.leaves1(14.47, 65.58)); + add(entities.leaves1(31.38, 78.63)); + add(entities.leafpileBigOrange(23.44, 36.88)); + add(entities.leafpileMediumAltRed(13.09, 45.21)); + add(entities.leafpileMediumYellow(31.22, 25.71)); + add(entities.leafpileStickYellow(26.00, 26.13)); + add(entities.leafpileSmallOrange(20.75, 19.58)); + add(entities.leaves5(24.03, 38.38)); + add(entities.leaves5(29.88, 38.88)); + add(entities.leaves5(28.16, 22.50)); + add(entities.leaves5(24.94, 16.75)); + add(entities.leaves4(12.78, 42.71)); + add(entities.leaves4(26.53, 36.63)); + add(entities.leaves4(21.34, 20.21)); + add(entities.leaves4(26.84, 26.83)); + add(entities.leaves3(28.19, 43.67)); + add(entities.leaves3(28.94, 42.04)); + add(entities.leaves3(8.81, 47.42)); + add(entities.leaves3(24.50, 20.00)); + add(entities.leaves2(32.19, 35.04)); + add(entities.leaves2(30.81, 23.54)); + add(entities.leaves2(24.63, 20.63)); + add(entities.leaves2(39.19, 15.42)); + add(entities.leaves2(15.47, 46.88)); + add(entities.leafpileSmallYellow(30.19, 40.88)); + add(entities.leafpileBigRed(11.22, 9.63)); + add(entities.leafpileMediumAltYellow(38.22, 7.71)); + add(entities.leafpileStickRed(44.44, 6.25)); + add(entities.leafpileSmallOrange(25.31, 8.46)); + add(entities.leaves5(20.84, 6.17)); + add(entities.leaves5(43.78, 6.04)); + add(entities.leaves4(9.06, 7.88)); + add(entities.leaves4(38.94, 5.75)); + add(entities.leaves4(40.34, 13.96)); + add(entities.leaves3(42.78, 15.04)); + add(entities.leaves3(22.81, 8.50)); + add(entities.leaves3(13.06, 9.71)); + add(entities.leaves2(25.25, 6.83)); + add(entities.leaves2(8.44, 5.50)); + add(entities.leaves1(9.16, 5.17)); + add(entities.leaves1(20.44, 5.21)); + add(entities.leaves1(33.75, 22.92)); + add(entities.leaves1(29.09, 21.21)); + add(entities.leaves1(42.03, 5.17)); + add(entities.leafpileMediumAltOrange(59.41, 5.83)); + add(entities.leafpileMediumAltYellow(90.59, 3.33)); + add(entities.leafpileStickRed(75.69, 5.67)); + add(entities.leafpileSmallOrange(70.53, 23.00)); + add(entities.leaves5(84.19, 21.67)); + add(entities.leaves5(77.75, 5.25)); + add(entities.leaves4(75.16, 3.92)); + add(entities.leaves4(76.31, 15.38)); + add(entities.leaves3(87.00, 20.79)); + add(entities.leaves3(76.91, 14.33)); + add(entities.leaves3(89.41, 4.63)); + add(entities.leaves3(71.38, 6.08)); + add(entities.leaves2(71.50, 7.08)); + add(entities.leaves2(83.16, 23.71)); + add(entities.leaves2(88.84, 2.71)); + add(entities.leaves1(84.00, 23.88)); + add(entities.leaves1(75.03, 16.58)); + add(entities.leaves1(87.81, 4.71)); + add(entities.leaves1(91.88, 2.29)); + add(entities.leaves1(73.13, 8.08)); + add(entities.leaves1(72.03, 23.00)); + add(entities.leafpileStickRed(84.03, 36.21)); + add(entities.leafpileSmallYellow(82.78, 30.21)); + add(entities.leaves5(81.00, 32.67)); + add(entities.leaves5(87.53, 34.88)); + add(entities.leaves4(87.78, 37.00)); + add(entities.leaves4(79.47, 35.29)); + add(entities.leaves4(87.69, 29.83)); + add(entities.leaves3(78.34, 36.75)); + add(entities.leaves3(73.16, 35.21)); + add(entities.leaves3(85.66, 33.13)); + add(entities.leaves2(82.81, 36.13)); + add(entities.leaves2(83.94, 28.75)); + add(entities.leaves2(81.94, 29.71)); + add(entities.leaves2(87.00, 29.50)); + add(entities.leaves1(83.81, 30.38)); + add(entities.leaves1(85.38, 37.54)); + add(entities.leaves1(84.44, 37.00)); + add(entities.leaves1(82.34, 20.67)); + add(entities.leaves5(62.47, 5.17)); + add(entities.leaves4(58.72, 6.75)); + add(entities.leaves2(61.34, 3.29)); + add(entities.leaves1(63.25, 5.17)); + add(entities.leaves2(64.00, 2.79)); + add(entities.leafpileMediumYellow(107.56, 35.00)); + add(entities.leafpileStickYellow(115.25, 22.21)); + add(entities.leafpileBigYellow(119.63, 6.58)); + add(entities.leafpileSmallYellow(105.16, 6.21)); + add(entities.leafpileStickYellow(118.34, 33.83)); + add(entities.leaves5(108.16, 32.17)); + add(entities.leaves5(106.63, 21.54)); + add(entities.leaves5(123.50, 9.67)); + add(entities.leaves5(127.22, 34.83)); + add(entities.leaves4(119.25, 7.67)); + add(entities.leaves4(117.25, 8.50)); + add(entities.leaves4(99.06, 37.79)); + add(entities.leaves4(103.13, 19.50)); + add(entities.leaves4(102.75, 4.29)); + add(entities.leaves3(109.03, 24.79)); + add(entities.leaves3(100.38, 21.04)); + add(entities.leaves3(97.16, 36.79)); + add(entities.leaves3(107.97, 9.58)); + add(entities.leaves3(103.78, 7.21)); + add(entities.leaves3(125.94, 12.38)); + add(entities.leaves3(123.09, 12.08)); + add(entities.leaves3(117.41, 34.42)); + add(entities.leaves3(127.53, 38.17)); + add(entities.leaves2(115.56, 34.75)); + add(entities.leaves2(109.88, 33.42)); + add(entities.leaves2(108.41, 25.54)); + add(entities.leaves2(113.69, 21.25)); + add(entities.leaves2(116.34, 7.29)); + add(entities.leaves2(109.38, 7.79)); + add(entities.leaves2(99.69, 5.67)); + add(entities.leaves2(100.88, 19.29)); + add(entities.leaves2(103.63, 35.42)); + add(entities.leaves2(123.16, 36.71)); + add(entities.leaves1(115.16, 20.00)); + add(entities.leaves1(114.69, 22.75)); + add(entities.leaves1(106.69, 24.67)); + add(entities.leaves1(107.91, 8.17)); + add(entities.leaves1(98.56, 5.67)); + add(entities.leaves1(121.91, 6.42)); + add(entities.leaves1(121.84, 9.00)); + add(entities.leaves1(124.28, 13.38)); + add(entities.leaves1(129.13, 37.21)); + add(entities.leaves1(100.31, 36.67)); + add(entities.leaves1(103.75, 23.79)); + add(entities.leaves3(104.19, 15.67)); + add(entities.leaves2(104.00, 17.63)); + add(entities.leafpileBigOrange(137.00, 24.29)); + add(entities.leafpileMediumAltYellow(150.91, 9.96)); + add(entities.leafpileStickYellow(138.81, 9.75)); + add(entities.leaves5(153.50, 23.21)); + add(entities.leaves5(151.94, 5.50)); + add(entities.leaves5(136.25, 3.79)); + add(entities.leaves4(136.75, 8.54)); + add(entities.leaves4(150.34, 12.29)); + add(entities.leaves3(141.63, 8.67)); + add(entities.leaves3(139.16, 23.92)); + add(entities.leaves2(141.03, 22.71)); + add(entities.leaves2(137.41, 25.92)); + add(entities.leaves2(155.00, 26.13)); + add(entities.leaves2(148.31, 11.00)); + add(entities.leaves3(139.63, 2.50)); + add(entities.leaves1(137.13, 26.92)); + add(entities.leaves1(156.41, 26.67)); + add(entities.leaves1(148.88, 12.33)); + add(entities.leaves1(152.66, 7.13)); + add(entities.leaves4(155.44, 6.29)); + add(entities.leaves4(138.94, 5.00)); + add(entities.leaves3(139.19, 10.50)); + add(entities.leaves3(141.06, 25.46)); + add(entities.leaves1(140.13, 27.13)); + add(entities.leaves1(153.84, 22.63)); + add(entities.leafpileStickRed(141.97, 24.58)); + add(entities.leaves1(115.78, 44.00)); + add(entities.leaves2(117.47, 43.38)); + add(entities.leaves3(103.44, 44.88)); + add(entities.leaves2(102.97, 46.21)); + add(entities.leaves1(104.03, 46.50)); + add(entities.leaves4(97.44, 64.04)); + add(entities.leaves2(96.56, 62.67)); + add(entities.leaves3(118.56, 69.04)); + add(entities.leaves2(116.56, 69.13)); + add(entities.leaves1(117.59, 68.46)); + add(entities.leaves1(119.34, 70.96)); + add(entities.leaves2(87.13, 44.96)); + add(entities.leaves2(83.31, 53.04)); + add(entities.leaves5(155.84, 73.50)); + add(entities.leaves4(156.66, 75.67)); + add(entities.leaves1(154.56, 75.88)); + add(entities.leaves4(148.13, 80.63)); + add(entities.leaves2(147.16, 79.13)); + add(entities.leaves2(139.81, 83.63)); + add(entities.leaves3(141.75, 85.67)); + add(entities.leaves1(141.19, 84.75)); + add(entities.leaves1(133.78, 67.58)); + add(entities.leaves2(134.66, 67.67)); + add(entities.leaves3(136.22, 66.25)); + add(entities.leaves1(135.81, 67.83)); + add(entities.leaves1(142.66, 86.50)); + add(entities.leaves1(157.56, 77.25)); + add(entities.leaves4(151.13, 87.50)); + add(entities.leaves2(150.66, 86.21)); + add(entities.leaves2(158.03, 85.54)); + add(entities.leaves4(144.44, 94.21)); + add(entities.leaves4(127.34, 92.50)); + add(entities.leaves2(126.97, 91.79)); + add(entities.leaves2(131.22, 95.79)); + add(entities.leaves1(125.72, 99.46)); + add(entities.leaves1(125.13, 108.04)); + add(entities.leaves1(155.22, 106.42)); + add(entities.leaves2(152.72, 99.21)); + add(entities.leaves2(147.91, 95.67)); + add(entities.leaves2(132.94, 107.50)); + add(entities.leaves3(131.41, 149.21)); + add(entities.leaves2(131.09, 148.13)); + add(entities.leaves1(131.72, 147.54)); + add(entities.leaves4(134.91, 148.83)); + add(entities.leaves4(137.22, 137.63)); + add(entities.leaves4(110.41, 130.33)); + add(entities.leaves4(118.78, 138.54)); + add(entities.leaves2(116.84, 137.79)); + add(entities.leaves2(112.13, 130.17)); + add(entities.leaves3(25.44, 84.75)); + add(entities.leaves1(26.38, 83.79)); + add(entities.leaves4(86.25, 114.58)); + add(entities.leaves5(89.63, 117.63)); + add(entities.leaves2(86.25, 117.00)); + add(entities.leaves1(84.25, 117.21)); + add(entities.leaves5(41.75, 82.58)); + add(entities.leaves2(38.91, 80.96)); + add(entities.leaves1(40.78, 84.29)); + add(entities.leaves3(40.75, 81.38)); + } + + function addSnowpony(x: number, y: number, type: number) { + const snowpony = entities.snowponies[type - 1]; + const entity = add(snowpony(x, y + 0.5)); + lockTile(map, entity.x - 0.5, entity.y); + lockTile(map, entity.x + 0.5, entity.y); + } + + function addSnowPile(entity: ServerEntity) { + add(entity); + lockTile(map, entity.x - 0.5, entity.y); + lockTile(map, entity.x + 0.5, entity.y); + + if ( + entity.type === entities.snowPileSmall.type || + entity.type === entities.snowPileMedium.type || + entity.type === entities.snowPileBig.type + ) { + lockTile(map, entity.x - 0.5, entity.y - 1); + lockTile(map, entity.x + 0.5, entity.y - 1); + } + + if ( + entity.type === entities.snowPileMedium.type || + entity.type === entities.snowPileBig.type + ) { + lockTile(map, entity.x - 1, entity.y); + lockTile(map, entity.x - 0.5, entity.y + 1); + lockTile(map, entity.x + 0.5, entity.y + 1); + } + + if (entity.type === entities.snowPileBig.type) { + lockTile(map, entity.x + 1, entity.y); + lockTile(map, entity.x - 1.5, entity.y); + lockTile(map, entity.x - 1, entity.y + 1); + lockTile(map, entity.x + 1, entity.y + 1); + } + } + + if (isWinter) { + addSnowpony(44.00, 57.00, 1); + addSnowpony(65.00, 64.00, 1); + addSnowpony(68.00, 65.00, 2); + addSnowpony(84.00, 56.00, 2); + addSnowpony(67.00, 82.00, 1); + addSnowpony(59.00, 87.00, 2); + addSnowpony(85.19, 152.67, 3); + addSnowpony(86.16, 153.04, 6); + addSnowpony(86.09, 122.83, 4); + addSnowpony(115.09, 109.13, 5); + addSnowpony(108.31, 128.29, 3); + addSnowpony(88.44, 104.71, 7); + addSnowpony(106.50, 86.13, 9); + addSnowpony(107.31, 86.67, 8); + addSnowpony(87.88, 92.33, 5); + addSnowpony(107.50, 58.83, 1); + addSnowpony(85.09, 56.92, 1); + addSnowpony(83.38, 57.25, 6); + addSnowpony(66.03, 83.13, 7); + addSnowpony(51.69, 102.38, 1); + addSnowpony(69.41, 112.50, 1); + addSnowpony(67.84, 111.17, 4); + addSnowpony(53.69, 133.33, 9); + addSnowpony(59.75, 119.79, 6); + addSnowpony(65.03, 156.46, 2); + addSnowpony(49.13, 148.38, 3); + addSnowpony(119.19, 149.96, 3); + addSnowpony(136.06, 156.00, 7); + addSnowpony(141.22, 130.92, 5); + addSnowpony(116.06, 136.58, 2); + addSnowpony(147.50, 101.04, 4); + addSnowpony(138.91, 97.92, 3); + addSnowpony(125.50, 88.75, 2); + addSnowpony(137.13, 67.50, 7); + addSnowpony(115.13, 65.88, 2); + addSnowpony(135.53, 44.33, 2); + addSnowpony(153.34, 51.00, 5); + addSnowpony(156.78, 60.08, 3); + addSnowpony(116.88, 46.79, 8); + addSnowpony(123.56, 53.67, 3); + addSnowpony(100.56, 44.75, 4); + addSnowpony(156.31, 124.88, 3); + addSnowpony(149.50, 150.00, 6); + addSnowpony(54.41, 132.83, 8); + + addSnowpony(29.69, 67.63, 1); + addSnowpony(26.72, 44.42, 2); + addSnowpony(23.22, 26.04, 3); + addSnowpony(20.97, 27.21, 4); + addSnowpony(12.97, 14.71, 5); + addSnowpony(37.81, 28.21, 6); + addSnowpony(50.38, 4.00, 7); + addSnowpony(81.78, 18.00, 8); + addSnowpony(80.00, 19.50, 1); + addSnowpony(84.25, 3.08, 2); + addSnowpony(97.19, 21.25, 3); + addSnowpony(86.47, 38.42, 4); + addSnowpony(122.47, 27.42, 5); + addSnowpony(133.44, 7.54, 6); + addSnowpony(130.31, 9.04, 7); + addSnowpony(143.41, 19.42, 8); + addSnowpony(20.63, 78.13, 1); + addSnowpony(25.25, 90.63, 2); + addSnowpony(27.56, 102.08, 3); + addSnowpony(22.19, 109.83, 4); + addSnowpony(5.72, 113.42, 5); + addSnowpony(2.19, 116.75, 6); + addSnowpony(13.75, 138.71, 7); + addSnowpony(3.50, 150.63, 8); + addSnowpony(35.09, 151.63, 1); + addSnowpony(37.75, 123.75, 2); + + addSnowPile(entities.snowPileBig(67.06, 78.46)); + addSnowPile(entities.snowPileMedium(65.25, 77.21)); + addSnowPile(entities.snowPileSmall(61.13, 70.08)); + addSnowPile(entities.snowPileSmall(68.44, 71.25)); + addSnowPile(entities.snowPileBig(74.84, 94.58)); + addSnowPile(entities.snowPileSmall(76.56, 95.50)); + addSnowPile(entities.snowPileBig(52.97, 60.04)); + addSnowPile(entities.snowPileBig(80.63, 54.29)); + addSnowPile(entities.snowPileSmall(82.94, 65.04)); + addSnowPile(entities.snowPileBig(117.19, 80.38)); + addSnowPile(entities.snowPileSmall(122.19, 83.38)); + addSnowPile(entities.snowPileSmall(135.81, 66.58)); + addSnowPile(entities.snowPileBig(150.25, 99.42)); + addSnowPile(entities.snowPileMedium(143.50, 106.67)); + addSnowPile(entities.snowPileSmall(148.28, 98.88)); + addSnowPile(entities.snowPileMedium(115.75, 98.04)); + addSnowPile(entities.snowPileMedium(139.81, 85.25)); + addSnowPile(entities.snowPileMedium(137.53, 98.79)); + addSnowPile(entities.snowPileMedium(115.50, 45.79)); + addSnowPile(entities.snowPileBig(151.88, 138.79)); + addSnowPile(entities.snowPileSmall(150.09, 138.00)); + addSnowPile(entities.snowPileSmall(140.28, 144.50)); + addSnowPile(entities.snowPileSmall(102.56, 147.13)); + addSnowPile(entities.snowPileBig(92.75, 139.75)); + addSnowPile(entities.snowPileBig(74.34, 157.38)); + addSnowPile(entities.snowPileSmall(73.19, 155.96)); + addSnowPile(entities.snowPileSmall(87.09, 147.63)); + addSnowPile(entities.snowPileBig(69.59, 134.54)); + addSnowPile(entities.snowPileMedium(84.66, 130.25)); + addSnowPile(entities.snowPileMedium(77.41, 121.38)); + addSnowPile(entities.snowPileSmall(79.13, 120.75)); + addSnowPile(entities.snowPileSmall(77.34, 133.83)); + addSnowPile(entities.snowPileBig(72.84, 106.33)); + addSnowPile(entities.snowPileMedium(63.41, 121.63)); + addSnowPile(entities.snowPileSmall(64.91, 122.29)); + addSnowPile(entities.snowPileTiny(71.06, 70.04)); + addSnowPile(entities.snowPileTiny(64.25, 77.88)); + addSnowPile(entities.snowPileTiny(44.41, 76.17)); + addSnowPile(entities.snowPileTiny(51.28, 69.00)); + addSnowPile(entities.snowPileTiny(74.56, 83.79)); + addSnowPile(entities.snowPileTinier(64.72, 78.29)); + addSnowPile(entities.snowPileTinier(67.31, 71.04)); + addSnowPile(entities.snowPileTinier(60.56, 69.21)); + addSnowPile(entities.snowPileTinier(51.44, 59.58)); + addSnowPile(entities.snowPileTinier(74.19, 84.25)); + addSnowPile(entities.snowPileTiny(49.75, 87.50)); + addSnowPile(entities.snowPileTiny(77.31, 100.83)); + addSnowPile(entities.snowPileTiny(75.81, 118.29)); + addSnowPile(entities.snowPileTiny(74.25, 106.83)); + addSnowPile(entities.snowPileTinier(76.22, 118.58)); + addSnowPile(entities.snowPileTinier(73.75, 107.00)); + addSnowPile(entities.snowPileTinier(75.63, 95.92)); + addSnowPile(entities.snowPileTiny(62.28, 121.96)); + addSnowPile(entities.snowPileTinier(62.91, 122.42)); + addSnowPile(entities.snowPileTinier(86.19, 147.25)); + addSnowPile(entities.snowPileTinier(78.50, 121.54)); + addSnowPile(entities.snowPileTiny(85.69, 130.50)); + addSnowPile(entities.snowPileTiny(72.44, 156.25)); + addSnowPile(entities.snowPileTiny(101.75, 146.29)); + addSnowPile(entities.snowPileTiny(112.34, 152.42)); + addSnowPile(entities.snowPileTiny(93.28, 110.67)); + addSnowPile(entities.snowPileTiny(103.28, 122.71)); + addSnowPile(entities.snowPileTinier(103.75, 122.96)); + addSnowPile(entities.snowPileTinier(93.72, 111.00)); + addSnowPile(entities.snowPileTiny(116.81, 97.13)); + addSnowPile(entities.snowPileTiny(110.28, 91.79)); + addSnowPile(entities.snowPileTiny(115.78, 80.96)); + addSnowPile(entities.snowPileTinier(116.16, 81.33)); + addSnowPile(entities.snowPileTinier(123.00, 83.42)); + addSnowPile(entities.snowPileTinier(116.66, 45.96)); + addSnowPile(entities.snowPileTinier(140.84, 85.33)); + addSnowPile(entities.snowPileTiny(138.75, 98.96)); + addSnowPile(entities.snowPileTinier(139.09, 98.79)); + addSnowPile(entities.snowPileTiny(148.47, 99.33)); + addSnowPile(entities.snowPileTinier(147.34, 98.58)); + addSnowPile(entities.snowPileTinier(144.66, 106.79)); + addSnowPile(entities.snowPileTiny(149.13, 138.50)); + addSnowPile(entities.snowPileTiny(141.09, 144.63)); + addSnowPile(entities.snowPileTinier(149.63, 138.88)); + addSnowPile(entities.snowPileTinier(140.63, 145.00)); + addSnowPile(entities.snowPileTiny(85.88, 147.67)); + addSnowPile(entities.snowPileTiny(94.34, 139.96)); + addSnowPile(entities.snowPileTinier(82.88, 56.88)); + addSnowPile(entities.snowPileTiny(85.84, 56.54)); + addSnowPile(entities.snowPileTinier(85.31, 57.13)); + addSnowPile(entities.snowPileTiny(66.91, 82.79)); + addSnowPile(entities.snowPileTinier(66.47, 83.21)); + addSnowPile(entities.snowPileTinier(65.38, 82.92)); + addSnowPile(entities.snowPileTinier(68.75, 112.17)); + addSnowPile(entities.snowPileTinier(54.13, 133.38)); + addSnowPile(entities.snowPileTinier(84.44, 152.42)); + addSnowPile(entities.snowPileTiny(85.41, 152.96)); + addSnowPile(entities.snowPileTiny(86.56, 152.04)); + addSnowPile(entities.snowPileTinier(86.97, 152.38)); + addSnowPile(entities.snowPileTinier(105.84, 85.96)); + addSnowPile(entities.snowPileTiny(106.53, 86.29)); + addSnowPile(entities.snowPileTinier(107.91, 86.42)); + addSnowPile(entities.snowPileTinier(155.66, 61.17)); + addSnowPile(entities.snowPileTiny(157.47, 60.42)); + addSnowPile(entities.snowPileTinier(157.72, 60.21)); + addSnowPile(entities.snowPileTinier(116.22, 46.42)); + addSnowPile(entities.snowPileBig(94.25, 68.29)); + addSnowPile(entities.snowPileMedium(73.56, 43.42)); + addSnowPile(entities.snowPileMedium(52.75, 38.46)); + addSnowPile(entities.snowPileSmall(52.03, 39.88)); + addSnowPile(entities.snowPileBig(43.72, 30.13)); + addSnowPile(entities.snowPileSmall(54.22, 17.13)); + addSnowPile(entities.snowPileTiny(48.69, 30.38)); + addSnowPile(entities.snowPileTiny(53.22, 39.46)); + addSnowPile(entities.snowPileTiny(60.19, 34.79)); + addSnowPile(entities.snowPileSmall(79.28, 37.21)); + addSnowPile(entities.snowPileSmall(84.19, 22.21)); + addSnowPile(entities.snowPileBig(99.34, 21.25)); + addSnowPile(entities.snowPileTiny(97.88, 22.29)); + addSnowPile(entities.snowPileSmall(106.94, 42.13)); + addSnowPile(entities.snowPileMedium(120.38, 26.79)); + addSnowPile(entities.snowPileSmall(121.00, 28.00)); + addSnowPile(entities.snowPileMedium(145.56, 20.17)); + addSnowPile(entities.snowPileBig(144.41, 21.29)); + addSnowPile(entities.snowPileBig(134.91, 4.46)); + addSnowPile(entities.snowPileSmall(120.56, 7.88)); + addSnowPile(entities.snowPileSmall(155.88, 55.21)); + addSnowPile(entities.snowPileSmall(148.28, 114.38)); + addSnowPile(entities.snowPileSmall(138.84, 154.50)); + addSnowPile(entities.snowPileMedium(139.88, 153.13)); + addSnowPile(entities.snowPileMedium(60.38, 150.25)); + addSnowPile(entities.snowPileBig(36.41, 149.33)); + addSnowPile(entities.snowPileMedium(37.31, 150.63)); + addSnowPile(entities.snowPileMedium(16.25, 138.88)); + addSnowPile(entities.snowPileSmall(15.31, 139.83)); + addSnowPile(entities.snowPileSmall(2.63, 149.38)); + addSnowPile(entities.snowPileMedium(4.19, 113.96)); + addSnowPile(entities.snowPileSmall(4.97, 115.08)); + addSnowPile(entities.snowPileSmall(29.34, 102.83)); + addSnowPile(entities.snowPileMedium(25.66, 89.00)); + addSnowPile(entities.snowPileSmall(26.91, 89.88)); + addSnowPile(entities.snowPileSmall(32.91, 69.38)); + addSnowPile(entities.snowPileSmall(21.13, 76.46)); + addSnowPile(entities.snowPileMedium(15.81, 53.46)); + addSnowPile(entities.snowPileMedium(25.94, 27.25)); + addSnowPile(entities.snowPileSmall(24.22, 28.50)); + addSnowPile(entities.snowPileBig(13.53, 17.13)); + addSnowPile(entities.snowPileSmall(12.34, 16.38)); + addSnowPile(entities.snowPileSmall(22.59, 5.46)); + addSnowPile(entities.snowPileMedium(52.38, 3.96)); + addSnowPile(entities.snowPileTiny(51.13, 4.71)); + addSnowPile(entities.snowPileTinier(50.94, 13.33)); + addSnowPile(entities.snowPileSmall(89.22, 2.71)); + } + + function addXmasTree(x: number, y: number) { + x -= 33; + y -= 24; + addEntities(entities.pine(x + 33, y + 24, 1)); + add(entities.xmasLights(x + 33, y + 24)); + add(entities.xmasLight(x + 33.22, y + 24.88)); + add(entities.xmasLight(x + 33.59, y + 24.04)); + add(entities.xmasLight(x + 32.28, y + 24.08)); + add(entities.xmasLight(x + 31.84, y + 24.42)); + add(entities.xmasLight(x + 30.97, y + 24.00)); + add(entities.xmasLight(x + 32.31, y + 22.63)); + add(entities.xmasLight(x + 31.63, y + 21.75)); + add(entities.xmasLight(x + 32.75, y + 20.92)); + add(entities.xmasLight(x + 32.19, y + 19.50)); + add(entities.xmasLight(x + 32.75, y + 18.17)); + add(entities.xmasLight(x + 33.38, y + 18.42)); + add(entities.xmasLight(x + 33.38, y + 19.79)); + add(entities.xmasLight(x + 33.94, y + 20.42)); + add(entities.xmasLight(x + 33.63, y + 22.33)); + add(entities.xmasLight(x + 34.00, y + 22.46)); + add(entities.xmasLight(x + 34.38, y + 21.83)); + add(entities.xmasLight(x + 35.00, y + 23.63)); + add(entities.xmasLight(x + 34.56, y + 24.17)); + } + + function addGraveWithGhost(x: number, y: number, tombType: number) { + const tombs = [entities.tombstone1, entities.tombstone2]; + const tomb = add(tombs[tombType](x, y)); + const createGhost = tombType === 0 ? entities.ghost1 : entities.ghost2; + const createGhostHooves = tombType === 0 ? entities.ghostHooves1 : entities.ghostHooves2; + const ghost = add(createGhost(x + toWorldX(1), y)); + const hooves = add(createGhostHooves(x + toWorldX(1), y)); + return { tomb, ghost, hooves }; + } + + const addGhost = (x: number, y: number, tombType: number, anims?: number[]) => { + const { ghost, hooves, tomb } = addGraveWithGhost(x, y, tombType); + const randomDelay = () => random(1 * 60, 5 * 60, true); + let delay = randomDelay(); + let resetDelay = 0; + let reset = true; + + ghost.serverUpdate = delta => { + delay -= delta; + resetDelay -= delta; + + if (delay < 0) { + const flip = Math.random() > 0.5; + const anim = sample(anims || (tomb.type === entities.tombstone1.type ? [1, 3] : [1, 2, 3]))!; + setEntityAnimation(ghost, anim, flip); + setEntityAnimation(hooves, anim, flip); + delay = randomDelay(); + reset = false; + resetDelay = 5; + } else if (!reset && resetDelay < 0) { + setEntityAnimation(ghost, GhostAnimation.None); + setEntityAnimation(hooves, GhostAnimation.None); + reset = true; + } + }; + }; + + const createEyes = (x: number, y: number) => { + const entity = add(entities.eyes(x, y)); + let delay = 5; + let open = true; + + entity.serverUpdate = delta => { + delay -= delta; + + if (delay < 0) { + if (open) { + setEntityAnimation(entity, 1); + delay = 0.2; + open = false; + } else { + setEntityAnimation(entity, 0); + delay = random(5, 10, true); + open = true; + } + } + }; + }; + + const addJacko = (x: number, y: number) => { + add(entities.jacko(x, y)).boop = boopLight; + }; + + const addJackoLanternSpot = (x: number, y: number) => { + const giveLantern = give(entities.jackoLanternOn.type, 'Now go collect some candies!'); + + add(createSign(x, y, 'Jack-o-Lanterns', giveLantern, entities.signQuest)); + + addJacko(x + 0.5, y - 0.3); + addJacko(x - 0.3, y + 0.3); + + add(entities.jackoLanternOff(x + 0.2, 17.7 + y - 18.5)); + add(entities.jackoLanternOff(x + 0.7, 17.8 + y - 18.5)); + add(entities.jackoLanternOff(x + 0.3, y + 0.2)); + add(entities.jackoLanternOff(x + 0.7, y + 0.5)); + add(entities.jackoLanternOn(x, 19.2 + y - 18.5)); + add(entities.jackoLanternOn(x + 0.5, 19.4 + y - 18.5)); + add(entities.jackoLanternOn(x - 0.9, 19.2 + y - 18.5)); + }; + + if (isHalloween) { + const donateX = 64, donateY = 79; + add(createSign(donateX, donateY, 'Donate candies', donateCandy, entities.signDonate)); + add(entities.box(donateX + 0.1, donateY + 1.2)).interact = donateCandy; + + add(entities.jackoOn(132.69, 108.79)); + add(entities.jackoOn(131.31, 134.79)); + add(entities.jackoOn(134.55, 139.38)); + add(entities.jackoOn(122.94, 105.38)); + add(entities.jackoOn(126.63, 107.38)); + add(entities.jackoOn(127.75, 110.88)); + add(entities.jackoOn(126.13, 124.38)); + add(entities.jackoOn(126.44, 133.71)); + add(entities.jackoOn(126.88, 137.71)); + add(entities.jackoOn(132.38, 136.88)); + + addGhost(149.53, 135.58, 1); + addGhost(144.38, 100.21, 1); + addGhost(149.84, 100.58, 1); + add(entities.tombstone2(144.56, 105.25)); + addGhost(146.34, 105.29, 1); + add(entities.tombstone1(146.28, 100.50)); + addGhost(148.25, 100.50, 0); + add(entities.tombstone1(148.34, 105.38)); + add(entities.tombstone1(150.03, 105.21)); + addGhost(102.28, 93.29, 1); + addGhost(66.59, 57.46, 1); + addGhost(72.19, 69.21, 1); + + createEyes(155.52, 102.73); + createEyes(156.81, 104.02); + createEyes(155.66, 104.71); + createEyes(158.05, 106.65); + createEyes(147.25, 111.96); + createEyes(146.59, 109.33); + createEyes(148.19, 110.27); + createEyes(141.31, 111.52); + createEyes(145.23, 115.35); + createEyes(144.16, 114.44); + createEyes(142.39, 107.33); + createEyes(141.19, 108.63); + createEyes(142.39, 111.02); + createEyes(149.59, 113.52); + createEyes(150.22, 113.06); + createEyes(140.02, 107.44); + createEyes(142.83, 115.50); + createEyes(138.78, 111.50); + createEyes(137.22, 112.27); + + createEyes(144.59, 93.00); + createEyes(145.43, 93.95); + createEyes(147.40, 91.25); + createEyes(146.90, 93.04); + createEyes(148.68, 92.37); + createEyes(153.15, 94.29); + createEyes(154.46, 94.95); + createEyes(152.40, 96.08); + createEyes(154.87, 97.20); + createEyes(155.53, 97.79); + + add(entities.jackoLanternOn(131.97, 149.77)); + add(entities.jackoLanternOn(132.02, 83.46)); + add(entities.jackoLanternOn(131.98, 149.85)); + add(entities.jackoLanternOn(106.45, 150.31)); + add(entities.jackoLanternOn(107.14, 149.79)); + add(entities.jackoLanternOn(80.33, 93.19)); + + addGhost(45.34, 152.83, 0); + addGhost(45.75, 106.08, 0); + addGhost(59.78, 96.83, 0); + addGhost(81.00, 93.00, 0); + addGhost(83.62, 132.875, 0); + addGhost(115.96, 45.79, 0); + addGhost(149.75, 81.58, 0); + addGhost(52.40, 138.70, 1); + addGhost(52.12, 88.29, 1); + addGhost(49.06, 64.83, 1); + addGhost(48.43, 50.83, 1); + addGhost(80.72, 115.83, 1); + addGhost(86.50, 46.83, 1); + addGhost(113.812, 107.37, 1); + addGhost(118.062, 125.12, 1); + addGhost(106.59, 149.83, 1); + addGhost(132.43, 83.12, 1); + addGhost(135.46, 67.66, 1); + addGhost(131.53, 149.50, 1); + + add(entities.jackoLanternOn(45.84, 153.20)); + add(entities.jackoLanternOn(46.15, 152.75)); + add(entities.jackoLanternOn(53.00, 138.70)); + add(entities.jackoLanternOn(48.56, 64.58)); + add(entities.jackoLanternOn(48.93, 51.04)); + add(entities.jackoLanternOn(72.53, 69.58)); + add(entities.jackoLanternOn(80.31, 116.16)); + add(entities.jackoLanternOn(83.06, 133.29)); + add(entities.jackoLanternOn(86.15, 47.12)); + add(entities.jackoLanternOn(115.50, 46.13)); + add(entities.jackoLanternOn(114.25, 107.83)); + add(entities.jackoLanternOn(114.53, 107.41)); + add(entities.jackoLanternOn(132.03, 83.41)); + add(entities.jackoLanternOn(132.93, 83.08)); + add(entities.jackoLanternOn(115.50, 46.12)); + add(entities.jackoLanternOn(149.15, 81.37)); + add(entities.jackoLanternOn(144.15, 105.58)); + add(entities.jackoLanternOn(146.75, 100.83)); + add(entities.jackoLanternOn(150.63, 105.58)); + + add(entities.jackoLanternOn(52.68, 139.04)); + add(entities.jackoLanternOn(46.28, 106.41)); + add(entities.jackoLanternOn(52.53, 88.62)); + add(entities.jackoLanternOn(135.84, 67.91)); + + add(entities.jackoLanternOff(52.62, 88.12)); + add(entities.jackoLanternOff(106.09, 150.04)); + add(entities.jackoLanternOff(51.93, 138.58)); + + addJacko(71.90, 70.00); + addJacko(90.20, 66.40); + addJacko(80.00, 53.80); + addJacko(67.30, 88.60); + addJacko(86.20, 97.30); + addJacko(99.70, 92.00); + addJacko(91.00, 86.10); + addJacko(85.70, 75.10); + addJacko(84.50, 91.40); + addJacko(99.00, 87.00); + addJacko(91.60, 55.90); + addJacko(95.60, 47.50); + addJacko(98.40, 44.20); + addJacko(100.10, 49.00); + addJacko(87.20, 46.50); + addJacko(69.70, 47.20); + addJacko(49.10, 89.50); + addJacko(59.10, 97.60); + addJacko(46.70, 98.10); + addJacko(52.20, 77.40); + addJacko(62.50, 72.20); + addJacko(66.70, 71.40); + + addJacko(97.50, 100.79); + addJacko(110.34, 92.79); + addJacko(117.34, 99.38); + addJacko(108.94, 101.50); + addJacko(109.63, 102.21); + addJacko(108.00, 85.00); + addJacko(93.50, 106.17); + addJacko(113.22, 108.17); + addJacko(117.56, 117.38); + addJacko(118.81, 125.33); + addJacko(117.25, 127.71); + addJacko(118.03, 129.50); + addJacko(93.22, 135.46); + addJacko(78.94, 147.54); + addJacko(82.00, 147.67); + addJacko(102.06, 146.83); + addJacko(96.59, 146.58); + addJacko(108.75, 141.33); + addJacko(110.03, 146.71); + addJacko(147.88, 148.04); + addJacko(145.41, 150.08); + addJacko(147.28, 153.67); + addJacko(150.78, 153.00); + addJacko(150.78, 149.08); + addJacko(152.88, 131.38); + addJacko(143.25, 137.17); + addJacko(141.22, 116.13); + addJacko(138.16, 132.46); + addJacko(141.34, 102.04); + addJacko(141.31, 103.88); + addJacko(134.56, 86.71); + addJacko(139.34, 85.29); + addJacko(148.50, 81.71); + addJacko(150.09, 82.54); + addJacko(129.78, 98.67); + addJacko(136.75, 66.38); + addJacko(117.63, 70.33); + addJacko(131.81, 52.63); + addJacko(116.72, 46.21); + addJacko(137.22, 55.25); + addJacko(131.91, 49.21); + addJacko(124.63, 47.08); + addJacko(122.69, 51.58); + + addJacko(125.97, 145.21); + addJacko(115.91, 139.92); + addJacko(119.59, 145.54); + + addJacko(53.59, 133.58); + addJacko(48.78, 136.08); + addJacko(48.22, 142.75); + addJacko(55.22, 143.50); + addJacko(58.34, 135.21); + addJacko(58.94, 140.63); + + addJacko(76.63, 125.00); + addJacko(76.59, 128.88); + addJacko(77.06, 120.88); + addJacko(86.91, 121.08); + addJacko(86.84, 129.33); + addJacko(76.91, 134.63); + addJacko(86.47, 135.50); + addJacko(137.40, 97.60); + + addJacko(44.53, 152.91); + addJacko(49.56, 65.16); + addJacko(81.50, 92.41); + addJacko(81.18, 116.25); + addJacko(116.71, 46.20); + addJacko(118.81, 125.33); + addJacko(148.50, 81.70); + addJacko(150.09, 82.54); + addJacko(151.46, 99.08); + + addJacko(45.09, 105.75); + + add(entities.jackoLanternOff(66.60, 72.10)); + add(entities.jackoLanternOff(67.20, 71.90)); + + addJackoLanternSpot(101.00, 100.00); + addJackoLanternSpot(147.00, 135.00); + addJackoLanternSpot(190.00, 176.00); + addJackoLanternSpot(107.00, 173.00); + addJackoLanternSpot(165.00, 95.00); + } + + if (isChristmas) { + addEntities(createToyStash(103.87, 86.12)); + + const donateX = 64, donateY = 79; + add(createSign(donateX, donateY, 'Donate gifts', donateGift, entities.signDonate)); + add(entities.boxGifts(donateX + 0.1, donateY + 1.2)).interact = donateGift; + + const xmasTreeY = 49.5, xmasTreeX = 40.25; + addXmasTree(33 + xmasTreeX, 24 + xmasTreeY); + add(entities.giftPilePine(33 + xmasTreeX, 24.5 + xmasTreeY)); + add(entities.giftPile6(31.13 + xmasTreeX, 25.13 + xmasTreeY)); + add(entities.giftPile1(34.75 + xmasTreeX, 25.71 + xmasTreeY)); + add(createCookieTable(32.7 + xmasTreeX, 26.5 + xmasTreeY)); + + add(entities.mistletoe(86.40, 83.00)); + add(entities.mistletoe(92.00, 93.00)); + add(entities.giftPile4(43.50, 53.50)); + add(entities.giftPileTree(81.10, 92.50)); + add(entities.giftPile1(76.00, 90.00)); + add(entities.giftPile3(79.00, 53.00)); + add(entities.giftPile6(85.00, 54.50)); + + add(entities.giftPileTree(97.28, 64.38)); + add(entities.giftPilePine(63.03, 98.38)); + add(entities.giftPile5(65.25, 98.83)); + add(entities.giftPile6(61.03, 99.29)); + add(entities.giftPile1(65.09, 112.96)); + add(entities.giftPile3(63.06, 113.67)); + add(entities.giftPile4(64.59, 114.71)); + add(entities.giftPile2(57.50, 146.04)); + add(entities.giftPile6(58.25, 147.67)); + add(entities.giftPileTree(132.38, 149.08)); + add(entities.giftPile5(134.03, 149.33)); + add(entities.giftPile2(147.47, 126.58)); + add(entities.giftPileTree(137.84, 115.21)); + add(entities.giftPile5(134.06, 121.79)); + add(entities.giftPilePine(147.59, 101.86)); + add(entities.giftPile1(143.16, 98.99)); + add(entities.giftPile4(134.41, 67.58)); + add(entities.giftPilePine(154.81, 55.63)); + add(entities.giftPile1(144.13, 45.54)); + add(entities.giftPile3(132.66, 47.25)); + add(entities.giftPileTree(118.78, 48.25)); + + add(entities.giftPile1(51.31, 31.33)); + add(entities.giftPile2(58.66, 31.75)); + add(entities.giftPile4(72.28, 37.50)); + add(entities.giftPile6(91.06, 38.13)); + add(entities.giftPile3(100.00, 38.17)); + add(entities.giftPileTree(143.56, 14.21)); + add(entities.giftPile1(126.03, 12.08)); + add(entities.giftPileInteractive(148.91, 34.50)); + add(entities.giftPile6(96.97, 11.79)); + add(entities.giftPile2(77.22, 6.46)); + add(entities.giftPileTree(62.66, 6.13)); + add(entities.giftPile3(20.78, 6.71)); + add(entities.giftPile1(8.41, 15.75)); + add(entities.giftPile6(15.13, 32.08)); + add(entities.giftPileInteractive(14.22, 33.04)); + add(entities.giftPileTree(25.66, 59.29)); + add(entities.giftPile6(16.03, 77.17)); + add(entities.giftPile1(22.09, 105.88)); + add(entities.giftPileInteractive(9.34, 96.17)); + add(entities.giftPile3(24.25, 144.25)); + add(entities.giftPile6(14.88, 155.13)); + add(entities.giftPile1(33.19, 133.13)); + add(entities.giftPile6(41.56, 122.17)); + + add(entities.holly(121.00, 145.17)); + add(entities.holly(122.03, 145.17)); + add(entities.holly(123.00, 145.13)); + add(entities.holly(125.00, 145.13)); + add(entities.holly(118.00, 140.17)); + add(entities.holly(121.03, 140.13)); + add(entities.holly(119.03, 140.21)); + add(entities.holly(135.00, 144.17)); + add(entities.holly(137.03, 144.17)); + add(entities.holly(138.03, 144.17)); + add(entities.holly(137.00, 140.13)); + add(entities.holly(139.00, 140.13)); + add(entities.holly(140.00, 140.04)); + add(entities.holly(142.00, 140.13)); + add(entities.holly(143.00, 144.58)); + add(entities.holly(144.97, 144.63)); + add(entities.holly(121.00, 100.13)); + add(entities.holly(124.00, 100.13)); + add(entities.holly(119.03, 100.13)); + add(entities.holly(117.97, 100.13)); + add(entities.holly(118.03, 105.21)); + add(entities.holly(120.00, 105.17)); + add(entities.holly(79.00, 147.21)); + add(entities.holly(82.03, 147.17)); + add(entities.holly(76.97, 147.21)); + add(entities.holly(84.00, 147.17)); + add(entities.holly(85.03, 147.17)); + add(entities.holly(86.00, 147.08)); + add(entities.holly(77.03, 136.17)); + add(entities.holly(80.03, 136.13)); + add(entities.holly(81.00, 136.17)); + add(entities.holly(83.00, 137.17)); + add(entities.holly(86.00, 137.17)); + add(entities.holly(87.00, 137.08)); + add(entities.holly(88.00, 137.17)); + add(entities.holly(73.03, 100.88)); + add(entities.holly(76.81, 100.88)); + add(entities.holly(81.03, 97.04)); + add(entities.holly(83.63, 100.67)); + add(entities.holly(87.44, 100.71)); + add(entities.holly(92.22, 96.08)); + add(entities.holly(96.72, 100.58)); + add(entities.holly(45.50, 53.13)); + add(entities.holly(49.31, 53.13)); + + add(entities.giftPile3(120.69, 145.46)); + add(entities.mistletoe(130.81, 149.46)); + add(entities.mistletoe(125.56, 139.38)); + add(entities.mistletoe(140.09, 131.50)); + add(entities.mistletoe(154.81, 107.88)); + add(entities.mistletoe(132.06, 123.25)); + add(entities.mistletoe(148.16, 114.54)); + add(entities.giftPile6(137.34, 131.17)); + add(entities.giftPile3(47.94, 142.42)); + add(entities.giftPile2(76.00, 147.75)); + + add(createCookieTable(29.43, 70.20)); + add(createCookieTable(104.75, 58.92)); + add(createCookieTable(79.31, 6.70)); + add(createCookieTable(110.06, 93.00)); + add(createCookieTable(146.47, 145.21)); + } + + if (isEaster) { + const giveBasket = give(entities.basket.type); + + // spot 1 + add(entities.basketBin(73.00, 74.00)).interact = giveBasket; + add(entities.eggBasket2(73.53, 74.88)); + add(entities.eggBasket3(74.41, 73.92)); + add(entities.eggBasket4(74.16, 74.17)); + add(createSign(74.00, 73.80, 'Egg baskets', giveBasket, entities.signQuest)); + + // spot 2 + add(entities.basketBin(33 + 70, 34 + 53)).interact = giveBasket; + add(entities.eggBasket2(33.53 + 70, 34.88 + 53)); + add(entities.eggBasket3(34.41 + 70, 33.92 + 53)); + add(entities.eggBasket4(34.16 + 70, 34.17 + 53)); + add(createSign(34 + 70, 33.8 + 53, 'Egg baskets', giveBasket, entities.signQuest)); + + // donation spot + add(createSign(62.00, 78.00, 'Donate eggs', donateEgg, entities.signDonate)); + add(entities.barrel(62.15, 78.87)).interact = donateEgg; + } +} + +export function updateMainMapSeason(world: World, map: ServerMap, season: Season, holiday: Holiday) { + removeSeasonalObjects(world, map); + addSeasonalObjects(world, map, season, holiday); + + const isWinter = season === Season.Winter; + + for (let y = 0, i = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++ , i++) { + const tile = mainMapTiles[i]; + + if (isWinter) { + if (x > 18 && (tile === TileType.Water || tile === TileType.WalkableWater || tile === TileType.Boat)) { + setTile(map, x, y, tile === TileType.Water ? TileType.Ice : TileType.WalkableIce); + } else { + setTile(map, x, y, tile); + } + } else { + if (tile === TileType.Ice || tile === TileType.SnowOnIce) { + setTile(map, x, y, TileType.Water); + } else if (tile === TileType.WalkableIce) { + setTile(map, x, y, TileType.WalkableWater); + } else { + setTile(map, x, y, tile); + } + } + } + } + + snapshotTiles(map); + + for (const controller of map.controllers) { + controller.initialize(world.now / 1000); + } +} + +export function createMainMap(world: World): ServerMap { + const mapSize = 20; + const map = createServerMap('', MapType.None, mapSize, mapSize, TileType.Grass); + + map.flags |= MapFlags.EdibleGrass; + + // spawns + + map.spawnArea = rect(51, 21, 8, 8); + + map.spawns.set('harbor', rect(5.2, 72.2, 3.4, 2.6)); + map.spawns.set('cave', rect(75.5, 27, 2, 2)); + + map.spawns.set('lake', rect(134, 68, 2, 1)); + map.spawns.set('bridge', rect(107, 37, 2, 2)); + map.spawns.set('forest', rect(105, 91, 3, 3)); + map.spawns.set('graveyard', rect(146, 101, 3, 3)); + map.spawns.set('pumpkins', rect(71, 125, 3, 3)); + + map.spawns.set('center', rect(74, 74, 2, 2)); + map.spawns.set('topleft', rect(17, 10, 3, 3)); + map.spawns.set('topright', rect(131, 17, 3, 3)); + map.spawns.set('bottomleft', rect(17, 149, 3, 3)); + map.spawns.set('bottomright', rect(154, 140, 3, 3)); + + // tiles + + deserializeMap(map, mainMapData); + + if (!DEVELOPMENT) { + snapshotTiles(map); + } + + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } + + const giveLantern = give(entities.lanternOn.type); + + const isWinter = world.season === Season.Winter; + const isHalloween = world.holiday === Holiday.Halloween; + + const addWoodenFence = createWoodenFenceMaker(world, map); + const addStoneWall = createStoneWallFenceMaker(world, map); + + function add(entity: ServerEntity) { + if (entity.x < 0 || entity.x > map.width || entity.y < 0 || entity.y > map.height) { + if (DEVELOPMENT) { + logger.warn(`skipped entity (${getEntityTypeName(entity.type)}) outside map (${entity.x} ${entity.y})`); + } + + return { x: entity.x, y: entity.y } as ServerEntity; + } + + return world.addEntity(entity, map); + } + + function addEntities(entities: ServerEntity[]) { + return entities.map(add); + } + + function addTree(x: number, y: number, variant: number, web = false, spider = false) { + addEntities(entities.tree(x, y, variant, web, spider && !isWinter)); + } + + function cliffNE(x: number, y: number) { + add(entities.cliffTopNE(x + 0.5, y)); + lockTiles(map, x - 1, y - 1, 3, 3); + } + + const cliffDecals = [entities.cliffDecal1, entities.cliffDecal3, entities.cliffDecal2]; + + function cracksS(x: number, y: number) { + const code = (Math.random() * 1000) % 64; + const index1 = code & 0b11; + const index2 = (code >> 2) & 0b11; + const index3 = (code >> 4) & 0b11; + index1 && index1 !== 3 && add(cliffDecals[index1 - 1](x + 0.5, y - 1)); // no decal 2 here + index2 && add(cliffDecals[index2 - 1](x + 0.5, y)); + index3 && add(cliffDecals[index3 - 1](x + 0.5, y + 1)); + } + + function cracksSLeft(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.cliffDecalL(x + 0.5, y - 1)); + (code & 0b10) && add(entities.cliffDecalL(x + 0.5, y)); + } + + function cracksSRight(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.cliffDecalR(x + 0.5, y - 1)); + (code & 0b10) && add(entities.cliffDecalR(x + 0.5, y)); + } + + function cliffSW(x: number, y: number) { + add(entities.cliffSW(x + 0.5, y - 2)); + lockTiles(map, x - 1, y - 4, 3, 7); + cracksSLeft(x, y); + } + + function cliffSE(x: number, y: number) { + add(entities.cliffSE(x + 0.5, y - 2)); + lockTiles(map, x - 1, y - 3, 3, 6); + cracksSRight(x, y); + } + + function cliffS(x: number, y: number) { + add(entities.cliffS2(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffSStart(x: number, y: number) { + add(entities.cliffS1(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffSEnd(x: number, y: number) { + add(entities.cliffS3(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffS1(x: number, y: number) { + add(entities.cliffSb(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffS1Entrance(x: number, y: number) { + add(entities.cliffSbEntrance(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + } + + function cliffRightWithTrimNoEdge(x: number, y: number, h: number) { + cliffRight(x, y, h); + cliffTrimRight(x + 1, y, h, false); + } + + function cliffRightWithTrim(x: number, y: number, h: number) { + cliffRight(x, y - 3, h - 3); + cliffTrimRight(x + 1, y, h); + } + + function cliffLeftWithTrim(x: number, y: number, h: number) { + cliffLeft(x, y - 3, h - 3); + cliffTrimLeft(x, y, h); + } + + function cliffLeft(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.cliffTopW(x + 0.5, y - i)); + lockTiles(map, x, y - i - 1, 2, 3); + } + } + + function cliffRight(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.cliffTopE(x + 0.5, y - i)); + lockTiles(map, x - 1, y - i - 1, 2, 3); + } + } + + function cliffTrimLeft(x: number, y: number, h: number) { + add(entities.cliffBotTrimLeft(x - 0.5, y)); + + for (let i = 0; i < (h - 2); i++) { + add(entities.cliffMidTrimLeft(x - 0.5, y - 1 - i)); + } + + add(entities.cliffTopTrimLeft(x - 0.5, y - h + 1)); + } + + function cliffTrimRight(x: number, y: number, h: number, botTrim = true) { + if (botTrim) { + add(entities.cliffBotTrimRight(x + 0.5, y)); + } else { + add(entities.cliffMidTrimRight(x + 0.5, y)); + } + + for (let i = 0; i < (h - 2); i++) { + add(entities.cliffMidTrimRight(x + 0.5, y - 1 - i)); + } + + if (h > 1) { + add(entities.cliffTopTrimRight(x + 0.5, y - h + 1)); + } + } + + function cliffSSection(x: number, y: number, w: number) { + cliffSStart(x, y); + + for (let i = 1; i < (w - 1); i++) { + cliffS(x + i, y); + } + + cliffSEnd(x + w - 1, y); + } + + function cliffSESection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + cliffSE(x + i, y - i); + } + } + + function cliffSWSection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + cliffSW(x + i, y + i); + } + } + + // actual cliffs + cliffS(0, 49); + cliffS(1, 49); + cliffSEnd(2, 49); + cliffSE(3, 49); + cliffSSection(4, 48, 2); + cliffSWSection(6, 49, 2); + cliffSSection(8, 50, 4); + cliffSW(12, 51); + cliffSSection(13, 51, 4); + cliffSE(17, 51); + cliffRightWithTrim(17, 51, 6); + // harbor ramp + cliffLeftWithTrim(23, 50, 6); + cliffSW(23, 50); + cliffSSection(24, 50, 5); + cliffSE(29, 50); + cliffRightWithTrim(29, 50, 7); + cliffSESection(30, 45, 2); + cliffRightWithTrim(31, 44, 5); + cliffSESection(32, 41, 1); + cliffRightWithTrim(32, 41, 4); + cliffSESection(33, 39, 2); + cliffRightWithTrim(34, 38, 5); + cliffNE(34, 33); + // no path spot + cliffRightWithTrimNoEdge(33, 32, 6); + cliffSE(34, 28); + cliffRightWithTrim(34, 28, 5); + cliffSESection(35, 25, 2); + cliffS1(37, 23); + cliffSE(38, 23); + cliffRightWithTrim(38, 23, 4); + cliffSESection(39, 21, 2); + cliffRightWithTrim(40, 20, 4); + cliffSESection(41, 18, 1); + cliffS1(42, 17); + cliffSE(43, 17); + // corner + cliffSSection(44, 16, 2); + cliffSE(46, 16); + cliffS1(47, 15); + cliffSW(48, 16); + cliffSSection(49, 16, 2); + cliffSESection(51, 16, 2); + // end corner + cliffSSection(53, 14, 3); + cliffSW(56, 15); + cliffSSection(57, 15, 2); + cliffSE(59, 15); + cliffSSection(60, 14, 3); + cliffSWSection(63, 15, 2); + cliffS1(65, 16); + cliffSW(66, 17); + cliffLeftWithTrim(67, 19, 4); + cliffSW(67, 19); + cliffLeftWithTrim(68, 23, 6); + cliffSW(68, 23); + cliffLeftWithTrim(69, 25, 4); + cliffSW(69, 25); + cliffSSection(70, 25, 4); + cliffSE(74, 25); + + // mine entrance + cliffS1Entrance(75, 24); + cliffS1Entrance(76, 24); + cliffS1Entrance(77, 24); + + cliffSW(78, 25); + cliffS1(79, 25); + cliffSW(80, 26); + cliffSSection(81, 26, 3); + cliffSE(84, 26); + cliffSSection(85, 25, 2); + cliffSE(87, 25); + cliffRightWithTrim(87, 25, 5); + cliffNE(87, 20); + cliffNE(86, 19); + cliffRightWithTrimNoEdge(85, 18, 1); + cliffNE(85, 17); + cliffRightWithTrimNoEdge(84, 16, 3); + cliffSESection(85, 15, 2); + cliffS1(87, 13); + cliffSW(88, 14); + cliffSSection(89, 14, 2); + cliffSE(91, 14); + cliffRightWithTrim(91, 14, 6); + // river ramp + cliffLeftWithTrim(96, 14, 6); + cliffSW(96, 14); + cliffSSection(97, 14, 3); + cliffSE(100, 14); + cliffSSection(101, 13, 3); + cliffSE(104, 13); + cliffRightWithTrim(104, 13, 5); + cliffSESection(105, 10, 1); + cliffRightWithTrim(105, 10, 5); + cliffSESection(106, 7, 1); + cliffS1(107, 6); + cliffSE(108, 6); + cliffRightWithTrim(108, 6, 4); + cliffSESection(109, 4, 2); + cliffRightWithTrim(110, 3, 4); + + const plankWidth = 78 / tileWidth; + const plankHeight = 12 / tileHeight; + + const plank = () => sample(entities.planks)!; + const shortPlank = () => sample(entities.planksShort)!; + + // barrel storage + addWoodenFence(100, 24, 6); + addWoodenFence(100, 24, 2, false, true, false); + addWoodenFence(100, 29, 3, false, false, true); + addWoodenFence(106, 24, 8, false, true, true); + addWoodenFence(100, 32, 6); + add(entities.barrel(104.56, 24.70)); + add(entities.barrel(105.34, 25.08)); + add(entities.barrel(104.90, 25.70)); + add(entities.barrel(100.84, 24.50)); + add(entities.barrel(101.75, 24.50)); + add(entities.barrel(100.72, 31.25)); + add(entities.barrel(101.44, 31.54)); + add(entities.barrel(101.75, 30.75)); + add(entities.barrel(105.25, 29.79)); + add(entities.barrel(105.50, 30.71)); + add(entities.barrel(105.25, 31.67)); + add(entities.barrel(103.91, 31.04)); + add(entities.barrel(106.94, 24.08)); + add(entities.barrel(107.75, 24.88)); + add(entities.barrel(105.59, 32.83)); + add(entities.barrel(99.34, 24.13)); + add(entities.barrel(99.41, 25.21)); + add(entities.barrel(98.31, 24.88)); + add(entities.lanternOn(100.50, 25.63)); + add(entities.lanternOn(100.50, 28.96)); + add(entities.lanternOn(105.53, 25.88)); + + // orchard / mine entrance + const mineEntrance = add(entities.mineEntrance(76.5, 23.08)); + // const mineClosed = add(entities.mineClosed(76.5, 23.08)); + // setEntityName(mineClosed, 'Mine Closed'); + + if (true) { + add(entities.collider1x1(74.50, 25.17)); + add(entities.collider1x1(77.50, 25.21)); + mineEntrance.interact = (_, client) => goToMap(world, client, 'cave'); + add(entities.triggerHouseDoor(76.50, 25.88)).trigger = (_, client) => goToMap(world, client, 'cave'); + add(entities.mineRailsFadeUp(76.5, 25)); + add(entities.mineRailsV(76.5, 26)); + add(entities.mineRailsV(76.5, 27)); + add(entities.mineRailsNW(76.5, 28)); + add(entities.mineRailsH(75.5, 28)); + add(entities.mineRailsH(74.5, 28)); + add(entities.mineRailsH(73.5, 28)); + add(entities.mineRailsEndLeft(72.5, 28.5)); + add(entities.mineCart(74, 28)); + } + + add(entities.rock(79.09, 27.33)); + add(entities.rock2(79.47, 27.71)); + add(entities.rock3(73.53, 27.21)); + add(entities.rock2(73.88, 27.42)); + add(entities.lanternOn(74.59, 29.20)); + add(entities.lanternOn(78.88, 27.67)); + add(createBoxOfLanterns(79.84, 29.00)); + + // addEntities(entities.tree5(74.25, 30.00, 0)); + // addEntities(entities.tree5(78.53, 30.04, 0)); + // addEntities(entities.tree5(76.34, 33.00, 1)); + add(entities.tree5Stump(74.25, 30.00)); + add(entities.tree5Stump(78.53, 30.04)); + + addEntities(entities.tree5(78.59, 36.63, 2)); + addEntities(entities.tree4(80.97, 33.29, 1)); + addEntities(entities.tree5(83.50, 36.54, 2)); + addEntities(entities.tree5(83.16, 29.63, 0)); + addEntities(entities.tree5(86.00, 33.00, 1)); + addEntities(entities.tree5(87.75, 36.54, 2)); + addEntities(entities.tree5(87.66, 29.54, 0)); + addEntities(entities.tree5(73.91, 36.17, 1)); + addEntities(entities.tree5(72.00, 32.83, 2)); + addEntities(entities.tree5(69.59, 36.04, 0)); + addWoodenFence(62, 38, 9); + addWoodenFence(71, 38, 1, false, true, true); + addWoodenFence(71, 39, 3); + addWoodenFence(78, 39, 3); + addWoodenFence(81, 39, 1, false, true, true); + addWoodenFence(81, 40, 9); + addWoodenFence(69, 43, 8); + addWoodenFence(90, 28, 12, false, false, true); + add(entities.lanternOn(77.97, 36.42)); + add(entities.lanternOn(73.81, 39.21)); + add(entities.box(81.91, 39.13)); + add(entities.box(83.03, 39.50)); + + // bridge + add(plank()(110.7, 36.60)); + add(shortPlank()(112.56, 36.60)); + add(shortPlank()(110.18, 37.08)); + add(plank()(112.06, 37.08)); + add(plank()(110.71, 37.58)); + add(shortPlank()(112.59, 37.58)); + add(shortPlank()(110.21, 38.08)); + add(plank()(112.09, 38.08)); + add(plank()(110.63, 38.58)); + add(shortPlank()(112.50, 38.58)); + add(entities.plankShadow(111.46, 38.50)); + add(entities.pierLeg(111.46, 38.50)); + + add(entities.collider3x1(110, 36)); + add(entities.collider3x1(110, 39)); + + // pier + const pierX = 0; + const pierY = 8 / tileHeight; + + add(createSignWithText(pierX + 8.5, pierY + 71.1, 'Party Island', + `Hop on the boat to travel to an island, that is unique to your party`)); + + add(entities.triggerBoat(7.5, 70)).trigger = (_, client) => goToMap(world, client, 'island'); + + addEntities(entities.fullBoat(7, 69.66)); + add(entities.pierLeg(pierX + 10, pierY + 72.6)); + add(entities.pierLeg(pierX + 11.9, pierY + 72.6)); + add(entities.pierLeg(pierX + 5, pierY + 74.6)); + add(entities.pierLeg(pierX + 6.9, pierY + 74.6)); + add(entities.pierLeg(pierX + 8.8, pierY + 74.6)); + const plankOffsets = [0, -1, 0, -2, -1, -1, 0, -2, -1].map(x => x / tileWidth); + + for (let y = 0; y < 9; y++) { + const maxX = y < 5 ? ((y % 2) ? 5 : 4) : ((y % 2) ? 3 : 2); + const baseX = pierX + 4.5 + ((y % 2) ? 0 : (plankWidth / 2)) + plankOffsets[y]; + const baseY = pierY + 71 - (9 / tileHeight); + + for (let x = 0; x < maxX; x++) { + if ((x === 0 && (y % 2)) || (x === (maxX - 1) && (y % 2))) { + const ox = x === 0 ? (18 / tileWidth) : (-18 / tileWidth); + add(shortPlank()(baseX + ox + x * plankWidth, baseY + y * plankHeight)); + } else { + add(plank()(baseX + x * plankWidth, baseY + y * plankHeight)); + } + } + } + + add(entities.collider1x3(3.5, 69)); + add(entities.collider1x3(3.5, 72)); + add(entities.collider1x1(3.5, 75)); + + add(entities.collider3x1(4, 69)); + add(entities.collider3x1(7, 69)); + add(entities.collider1x1(9, 70)); + add(entities.collider1x2(9.6, 69)); + add(entities.collider3x1(10, 70.3)); + add(entities.collider2x1(4, 70)); + add(entities.collider1x3(4, 71)); + add(entities.collider1x2(4, 73)); + add(entities.collider1x2(4, 73)); + add(entities.collider3x1(4, 75)); + add(entities.collider3x1(7, 75)); + add(entities.collider1x2(9, 73)); + add(entities.collider3x1(10, 73)); + + add(entities.plankShadowShort(pierX + 5.09, pierY + 74.08 - plankHeight * 6)); + add(entities.plankShadowShort(pierX + 5.06, pierY + 74.08 - plankHeight * 4)); + add(entities.plankShadowShort(pierX + 5.09, pierY + 74.08 - plankHeight * 2)); + add(entities.plankShadowShort(pierX + 5.06, pierY + 74.08)); + + add(entities.plankShadowShort(pierX + 8.84, pierY + 74.12 - plankHeight * 2)); + add(entities.plankShadowShort(pierX + 8.78, pierY + 74.12 - plankHeight)); + add(entities.plankShadowShort(pierX + 8.81, pierY + 74.12)); + + add(entities.plankShadow(pierX + 11.68, pierY + 72.62)); + add(entities.plankShadow2(pierX + 11.68 - plankWidth, pierY + 72.62)); + add(entities.plankShadowShort(pierX + 13.5, pierY + 72.45)); + + add(entities.plankShadow2(pierX + 5.75, pierY + 74.62)); + add(entities.plankShadow(pierX + 5.75 + plankWidth, pierY + 74.62)); + + add(entities.lanternOn(pierX + 13.25, pierY + 70.83)); + add(entities.lanternOn(pierX + 13.31, pierY + 73.04)); + add(entities.lanternOn(pierX + 9.31, pierY + 73.10)); + add(entities.lanternOn(pierX + 9.63, pierY + 70.88)); + add(entities.lanternOn(pierX + 4.72, pierY + 70.96)); + add(entities.lanternOn(pierX + 4.69, pierY + 74.96)); + + add(entities.barrel(pierX + 13.75, pierY + 73.71)); + add(entities.barrel(pierX + 13.56, pierY + 74.46)); + add(entities.barrel(pierX + 14.34, pierY + 74.17)); + add(entities.barrel(pierX + 16.56, pierY + 76.62)); + add(entities.barrel(pierX + 5.41, pierY + 71.21)); + add(entities.barrel(pierX + 13.96, pierY + 67.67)); + add(entities.barrel(pierX + 14.75, pierY + 67.96)); + + // plants + add(entities.largeLeafedBush1(13.59, 64.79)); + add(entities.largeLeafedBush2(22.09, 68.75)); + add(entities.largeLeafedBush1(31.72, 65.50)); + add(entities.largeLeafedBush2(30.25, 53.67)); + add(entities.largeLeafedBush1(31.46, 52.66)); + add(entities.largeLeafedBush2(42.50, 47.71)); + add(entities.largeLeafedBush1(43.44, 52.50)); + add(entities.largeLeafedBush2(37.72, 25.54)); + add(entities.largeLeafedBush1(38.53, 25.17)); + add(entities.largeLeafedBush2(43.94, 25.04)); + add(entities.largeLeafedBush1(53.13, 16.50)); + add(entities.largeLeafedBush2(53.03, 17.42)); + add(entities.largeLeafedBush1(63.50, 31.46)); + add(entities.largeLeafedBush2(62.19, 32.33)); + add(entities.largeLeafedBush1(68.53, 28.79)); + add(entities.largeLeafedBush2(67.38, 21.04)); + add(entities.largeLeafedBush1(68.97, 36.84)); + add(entities.largeLeafedBush2(70.69, 45.71)); + add(entities.largeLeafedBush1(76.91, 43.58)); + add(entities.largeLeafedBush2(77.75, 44.17)); + add(entities.largeLeafedBush1(63.59, 46.58)); + add(entities.largeLeafedBush2(76.71, 60.76)); + add(entities.largeLeafedBush1(77.59, 60.00)); + add(entities.largeLeafedBush2(80.88, 53.21)); + add(entities.largeLeafedBush1(89.38, 57.71)); + add(entities.largeLeafedBush2(104.41, 45.79)); + add(entities.largeLeafedBush1(104.88, 46.58)); + add(entities.largeLeafedBush2(105.81, 44.88)); + add(entities.largeLeafedBush1(104.47, 32.75)); + add(entities.largeLeafedBush2(103.72, 33.33)); + add(entities.largeLeafedBush1(88.03, 15.58)); + add(entities.largeLeafedBush2(88.84, 16.17)); + add(entities.largeLeafedBush1(88.09, 16.63)); + add(entities.largeLeafedBush2(105.91, 16.46)); + add(entities.largeLeafedBush1(106.65, 9.20)); + add(entities.largeLeafedBush2(103.69, 6.46)); + add(entities.largeLeafedBush1(100.94, 5.38)); + add(entities.largeLeafedBush2(73.22, 6.71)); + add(entities.largeLeafedBush1(57.66, 6.08)); + add(entities.largeLeafedBush2(56.97, 6.67)); + add(entities.largeLeafedBush1(32.53, 24.25)); + add(entities.largeLeafedBush2(33.47, 24.54)); + add(entities.largeLeafedBush1(25.19, 19.13)); + add(entities.largeLeafedBush2(13.94, 49.08)); + add(entities.largeLeafedBush1(13.50, 48.21)); + add(entities.largeLeafedBush2(11.09, 11.71)); + add(entities.largeLeafedBush1(11.81, 12.63)); + add(entities.largeLeafedBush2(23.72, 7.42)); + add(entities.largeLeafedBush1(23.22, 8.25)); + add(entities.largeLeafedBush2(25.84, 38.33)); + add(entities.largeLeafedBush1(9.03, 61.50)); + add(entities.largeLeafedBush2(125.03, 37.50)); + add(entities.largeLeafedBush1(119.56, 47.92)); + add(entities.largeLeafedBush2(124.69, 26.54)); + add(entities.largeLeafedBush1(125.56, 26.00)); + add(entities.largeLeafedBush2(139.97, 9.25)); + add(entities.largeLeafedBush1(150.41, 11.83)); + add(entities.largeLeafedBush2(154.31, 25.71)); + add(entities.largeLeafedBush1(153.46, 26.20)); + add(entities.largeLeafedBush2(139.00, 25.79)); + add(entities.largeLeafedBush1(121.69, 7.92)); + add(entities.largeLeafedBush2(157.03, 66.88)); + add(entities.largeLeafedBush1(152.81, 89.54)); + add(entities.largeLeafedBush2(151.81, 81.04)); + add(entities.largeLeafedBush1(152.06, 82.00)); + add(entities.largeLeafedBush2(146.66, 94.96)); + add(entities.largeLeafedBush1(156.56, 106.71)); + add(entities.largeLeafedBush2(156.63, 107.92)); + add(entities.largeLeafedBush1(152.81, 129.96)); + add(entities.largeLeafedBush2(143.50, 135.79)); + add(entities.largeLeafedBush1(143.19, 136.75)); + add(entities.largeLeafedBush2(131.53, 121.92)); + add(entities.largeLeafedBush1(132.44, 133.67)); + add(entities.largeLeafedBush2(132.84, 134.83)); + add(entities.largeLeafedBush1(139.00, 131.58)); + add(entities.largeLeafedBush2(125.00, 137.83)); + add(entities.largeLeafedBush1(123.41, 138.33)); + add(entities.largeLeafedBush2(119.81, 136.96)); + add(entities.largeLeafedBush1(122.38, 120.63)); + add(entities.largeLeafedBush2(111.19, 126.54)); + add(entities.largeLeafedBush1(113.66, 130.13)); + add(entities.largeLeafedBush2(112.50, 118.17)); + add(entities.largeLeafedBush1(107.94, 113.83)); + add(entities.largeLeafedBush2(108.06, 114.79)); + add(entities.largeLeafedBush1(105.28, 105.88)); + add(entities.largeLeafedBush2(100.16, 111.25)); + add(entities.largeLeafedBush1(101.06, 111.88)); + add(entities.largeLeafedBush2(121.81, 107.25)); + add(entities.largeLeafedBush1(122.84, 107.63)); + add(entities.largeLeafedBush2(88.69, 115.50)); + add(entities.largeLeafedBush1(89.06, 116.42)); + add(entities.largeLeafedBush2(81.00, 112.33)); + add(entities.largeLeafedBush1(81.69, 113.21)); + add(entities.largeLeafedBush2(91.53, 151.42)); + add(entities.largeLeafedBush1(90.81, 150.75)); + add(entities.largeLeafedBush2(99.44, 153.25)); + add(entities.largeLeafedBush1(99.03, 153.96)); + add(entities.largeLeafedBush2(108.50, 147.17)); + add(entities.largeLeafedBush1(117.75, 153.38)); + add(entities.largeLeafedBush2(118.44, 152.58)); + add(entities.largeLeafedBush1(132.91, 148.54)); + add(entities.largeLeafedBush2(125.53, 144.92)); + add(entities.largeLeafedBush1(124.72, 145.50)); + add(entities.largeLeafedBush2(145.03, 144.92)); + add(entities.largeLeafedBush1(118.06, 124.92)); + add(entities.largeLeafedBush2(116.03, 94.42)); + add(entities.largeLeafedBush1(103.25, 81.96)); + add(entities.largeLeafedBush2(103.78, 82.88)); + add(entities.largeLeafedBush1(87.47, 89.54)); + add(entities.largeLeafedBush2(87.97, 90.50)); + add(entities.largeLeafedBush1(79.59, 84.50)); + add(entities.largeLeafedBush2(77.88, 85.25)); + add(entities.largeLeafedBush1(80.34, 91.42)); + add(entities.largeLeafedBush2(90.81, 78.63)); + add(entities.largeLeafedBush1(98.00, 63.71)); + add(entities.largeLeafedBush2(42.41, 84.79)); + add(entities.largeLeafedBush1(39.19, 82.92)); + add(entities.largeLeafedBush2(30.78, 77.13)); + add(entities.largeLeafedBush1(31.16, 78.08)); + add(entities.largeLeafedBush2(34.47, 70.63)); + add(entities.largeLeafedBush1(14.09, 90.08)); + add(entities.largeLeafedBush2(14.97, 89.17)); + add(entities.largeLeafedBush1(32.41, 111.13)); + add(entities.largeLeafedBush2(31.59, 111.58)); + add(entities.largeLeafedBush1(52.81, 110.38)); + add(entities.largeLeafedBush2(53.44, 110.04)); + add(entities.largeLeafedBush1(31.59, 109.83)); + add(entities.largeLeafedBush2(51.13, 95.04)); + add(entities.largeLeafedBush1(50.34, 95.54)); + add(entities.largeLeafedBush2(52.25, 96.21)); + add(entities.largeLeafedBush1(67.65, 81.45)); + + add(entities.largeLeafedBush3(63.50, 32.21)); + add(entities.largeLeafedBush4(69.28, 35.67)); + add(entities.largeLeafedBush3(66.56, 20.08)); + add(entities.largeLeafedBush4(64.25, 23.33)); + add(entities.largeLeafedBush3(67.81, 29.42)); + add(entities.largeLeafedBush4(72.44, 27.38)); + add(entities.largeLeafedBush3(52.22, 17.21)); + add(entities.largeLeafedBush4(43.59, 24.08)); + add(entities.largeLeafedBush3(38.47, 25.88)); + add(entities.largeLeafedBush4(46.38, 29.21)); + add(entities.largeLeafedBush3(41.69, 47.96)); + add(entities.largeLeafedBush4(44.16, 53.21)); + add(entities.largeLeafedBush3(31.16, 53.67)); + add(entities.largeLeafedBush4(31.00, 65.92)); + add(entities.largeLeafedBush3(20.72, 69.42)); + add(entities.largeLeafedBush4(12.88, 65.04)); + add(entities.largeLeafedBush3(9.47, 62.29)); + add(entities.largeLeafedBush4(13.03, 49.04)); + add(entities.largeLeafedBush3(25.13, 38.79)); + add(entities.largeLeafedBush4(15.38, 34.33)); + add(entities.largeLeafedBush3(33.34, 25.17)); + add(entities.largeLeafedBush4(25.31, 19.92)); + add(entities.largeLeafedBush3(24.06, 8.58)); + add(entities.largeLeafedBush4(10.91, 12.58)); + add(entities.largeLeafedBush3(9.72, 7.38)); + add(entities.largeLeafedBush4(40.19, 16.13)); + add(entities.largeLeafedBush3(57.59, 7.33)); + add(entities.largeLeafedBush4(73.03, 7.54)); + add(entities.largeLeafedBush3(71.41, 22.75)); + add(entities.largeLeafedBush4(87.32, 16.17)); + add(entities.largeLeafedBush3(85.25, 27.29)); + add(entities.largeLeafedBush4(100.81, 6.17)); + add(entities.largeLeafedBush3(106.44, 10.25)); + add(entities.largeLeafedBush4(105.72, 17.71)); + add(entities.largeLeafedBush3(105.28, 7.13)); + add(entities.largeLeafedBush4(104.56, 33.75)); + add(entities.largeLeafedBush3(105.63, 45.58)); + add(entities.largeLeafedBush4(117.41, 49.58)); + add(entities.largeLeafedBush3(73.91, 66.96)); + add(entities.largeLeafedBush4(76.68, 59.87)); + add(entities.largeLeafedBush3(84.06, 64.33)); + add(entities.largeLeafedBush4(81.50, 53.67)); + add(entities.largeLeafedBush3(76.72, 44.54)); + add(entities.largeLeafedBush4(62.81, 46.92)); + add(entities.largeLeafedBush3(70.41, 46.63)); + add(entities.largeLeafedBush4(34.47, 69.75)); + add(entities.largeLeafedBush3(30.34, 78.54)); + add(entities.largeLeafedBush4(39.84, 83.38)); + add(entities.largeLeafedBush3(42.06, 85.42)); + add(entities.largeLeafedBush4(50.94, 96.17)); + add(entities.largeLeafedBush3(30.97, 110.46)); + add(entities.largeLeafedBush4(13.75, 95.92)); + add(entities.largeLeafedBush3(14.41, 95.67)); + add(entities.largeLeafedBush4(14.88, 90.13)); + add(entities.largeLeafedBush3(28.66, 119.33)); + add(entities.largeLeafedBush4(31.53, 133.17)); + add(entities.largeLeafedBush3(53.47, 110.92)); + add(entities.largeLeafedBush4(90.69, 151.54)); + add(entities.largeLeafedBush3(99.72, 154.13)); + add(entities.largeLeafedBush4(95.78, 146.63)); + add(entities.largeLeafedBush3(108.44, 148.00)); + add(entities.largeLeafedBush4(93.25, 131.96)); + add(entities.largeLeafedBush3(125.56, 145.83)); + add(entities.largeLeafedBush4(123.97, 139.04)); + add(entities.largeLeafedBush3(120.59, 137.38)); + add(entities.largeLeafedBush4(118.44, 153.63)); + add(entities.largeLeafedBush3(111.16, 152.63)); + add(entities.largeLeafedBush4(133.06, 149.46)); + add(entities.largeLeafedBush3(144.16, 144.96)); + add(entities.largeLeafedBush4(143.72, 137.46)); + add(entities.largeLeafedBush3(132.09, 135.33)); + add(entities.largeLeafedBush4(138.16, 131.88)); + add(entities.largeLeafedBush3(152.63, 131.21)); + add(entities.largeLeafedBush4(145.72, 120.67)); + add(entities.largeLeafedBush3(131.56, 122.79)); + add(entities.largeLeafedBush4(122.16, 121.50)); + add(entities.largeLeafedBush3(122.22, 107.92)); + add(entities.largeLeafedBush4(117.28, 125.38)); + add(entities.largeLeafedBush3(111.34, 127.42)); + add(entities.largeLeafedBush4(112.44, 119.13)); + add(entities.largeLeafedBush3(107.25, 115.04)); + add(entities.largeLeafedBush4(114.28, 130.79)); + add(entities.largeLeafedBush3(105.34, 106.79)); + add(entities.largeLeafedBush4(100.31, 112.17)); + add(entities.largeLeafedBush3(87.47, 91.08)); + add(entities.largeLeafedBush4(79.84, 85.33)); + add(entities.largeLeafedBush3(103.19, 83.25)); + add(entities.largeLeafedBush4(98.38, 85.96)); + add(entities.largeLeafedBush3(114.09, 92.21)); + add(entities.largeLeafedBush4(115.81, 95.21)); + add(entities.largeLeafedBush3(117.38, 70.04)); + add(entities.largeLeafedBush4(147.13, 95.75)); + add(entities.largeLeafedBush3(142.53, 98.54)); + add(entities.largeLeafedBush4(157.44, 107.33)); + add(entities.largeLeafedBush3(152.19, 90.08)); + add(entities.largeLeafedBush4(158.34, 113.71)); + add(entities.largeLeafedBush3(151.38, 82.42)); + add(entities.largeLeafedBush4(157.28, 67.79)); + add(entities.largeLeafedBush3(157.97, 57.04)); + add(entities.largeLeafedBush4(154.00, 26.54)); + add(entities.largeLeafedBush3(138.13, 26.17)); + add(entities.largeLeafedBush4(125.06, 27.17)); + add(entities.largeLeafedBush3(125.66, 38.25)); + add(entities.largeLeafedBush4(119.09, 48.54)); + add(entities.largeLeafedBush3(116.81, 35.83)); + add(entities.largeLeafedBush4(141.38, 10.13)); + add(entities.largeLeafedBush3(149.03, 11.33)); + add(entities.largeLeafedBush4(152.53, 5.75)); + add(entities.largeLeafedBush3(121.41, 8.63)); + add(entities.largeLeafedBush4(109.75, 26.25)); + add(entities.largeLeafedBush3(109.38, 35.46)); + + // north-west hill + addEntities(entities.pine(37.91, 88.33, 0)); + addEntities(entities.pine(39.91, 91.29, 0)); + addEntities(entities.pine(15.53, 83.54, 0)); + addEntities(entities.pine(17.91, 82.17, 0)); + addEntities(entities.pine(21.59, 86.71, 0)); + addEntities(entities.pine(27.56, 81.38, 0)); + addEntities(entities.pine(2.88, 36.17, 0)); + addEntities(entities.pine(4.34, 40.79, 0)); + addEntities(entities.pine(2.59, 56.46, 0)); + addEntities(entities.pine(0.72, 53.33, 0)); + addEntities(entities.pine(8.72, 26.92, 0)); + addEntities(entities.pine(10.19, 29.46, 0)); + addEntities(entities.pine(3.38, 20.33, 0)); + add(entities.lanternOn(7.06, 17.92)); + add(entities.lanternOn(7.78, 22.96)); + add(entities.lanternOn(11.72, 19.04)); + add(entities.boxLanterns(5.66, 21.88)).interact = giveLantern; + addStoneWall(6, 32, 3); + addStoneWall(6, 32, 4, false, true, true); + addStoneWall(12, 32, 1, false, true); + addStoneWall(12, 38, 1, false, false, true); + addStoneWall(6, 40, 3); + add(entities.barrel(11.34, 33.08)); + add(entities.barrel(10.31, 33.46)); + add(entities.barrel(11.22, 33.96)); + add(entities.barrel(6.63, 39.08)); + add(entities.barrel(7.75, 39.21)); + add(entities.lanternOnWall(7.97, 32.29)); + add(entities.lanternOnWall(10.00, 40.33)); + add(entities.lanternOnWall(6.00, 36.33)); + add(entities.lanternOnWall(12.00, 34.33)); + + // north hill + addWoodenFence(52, 12.5, 11); + addWoodenFence(104.25, 9.25, 2, false, false, true); + addWoodenFence(101.25, 11.25, 3); + + // bridge + addWoodenFence(104, 36, 5); + addWoodenFence(105, 41, 3); + + // north-east fields + addWoodenFence(116, 13, 9); + addWoodenFence(116, 13, 11, false, true, true); + addWoodenFence(116, 24, 2); + addWoodenFence(118, 24, 2, false, true, true); + addWoodenFence(118, 26, 7); + addWoodenFence(125, 13, 6, false, true); + addWoodenFence(125, 22, 4, false, false, true); + addWoodenFence(140, 25, 12); + addWoodenFence(140, 25, 9, false, true, true); + addWoodenFence(152, 25, 9, false, true, true); + addWoodenFence(140, 34, 5); + addWoodenFence(148, 34, 4); + + // harbor + addStoneWall(13.2, 67, 1); + addStoneWall(15.2, 61, 3, false, false, true); + addStoneWall(20, 64, 3, false, false, true); + addStoneWall(20, 70, 4); + addStoneWall(16, 76, 3); + + // north hills + addEntities(entities.tree(15.06, 48.38, 0)); + addEntities(entities.tree(12.25, 44.21, 1 + 8)); + addEntities(entities.tree(29.50, 41.29, 2)); + addEntities(entities.tree(24.88, 37.67, 0 + 4)); + addEntities(entities.tree(45.91, 28.38, 1)); + addEntities(entities.tree(42.94, 24.33, 2)); + addEntities(entities.tree(31.78, 24.71, 0)); + addEntities(entities.tree(29.53, 22.29, 1 + 4)); + addEntities(entities.tree(26.69, 26.50, 2)); + addEntities(entities.tree(41.69, 13.25, 0)); + addEntities(entities.tree(65.13, 23.38, 1 + 8)); + addEntities(entities.tree(69.81, 19.75, 2 + 8)); + addEntities(entities.tree(70.91, 22.00, 0 + 4)); + addEntities(entities.tree(67.63, 28.33, 1)); + addEntities(entities.tree(62.84, 31.67, 2 + 4)); + addEntities(entities.tree(108.62, 8.71, 0)); + addEntities(entities.tree(105.09, 16.96, 1)); + addEntities(entities.tree(103.03, 19.25, 2 + 8)); + addEntities(entities.tree(107.81, 23.50, 0 + 4)); + addEntities(entities.tree(97.81, 38.63, 1)); + addEntities(entities.tree(108.63, 35.13, 2)); + addEntities(entities.tree(61.13, 43.67, 0)); + addEntities(entities.tree(24.44, 7.92, 0 + 4)); + addEntities(entities.tree(23.06, 16.54, 1 + 8)); + addEntities(entities.tree(24.66, 19.46, 2)); + addEntities(entities.tree(38.97, 6.96, 0)); + addEntities(entities.tree(41.19, 4.54, 1)); + addEntities(entities.tree(62.44, 2.96, 2 + 4)); + addEntities(entities.tree(58.50, 6.38, 0 + 4)); + addEntities(entities.tree(75.19, 5.04, 1 + 8)); + addEntities(entities.tree(89.91, 4.00, 2)); + addEntities(entities.tree(104.47, 6.58, 0)); + addEntities(entities.tree(102.53, 3.42, 1)); + addEntities(entities.tree(21.72, 6.04, 2 + 4)); + addEntities(entities.tree(12.06, 10.75, 0 + 8)); + addEntities(entities.tree(9.66, 6.46, 1)); + addEntities(entities.tree5(13.50, 7.63, 0)); + addEntities(entities.tree5(21.22, 20.17, 1)); + addEntities(entities.tree5(43.88, 6.79, 2)); + addEntities(entities.tree5(62.53, 5.88, 0)); + addEntities(entities.tree5(72.44, 7.29, 1)); + addEntities(entities.tree5(100.38, 5.63, 2)); + addEntities(entities.pine(1.66, 6.29, 0)); + addEntities(entities.pine(3.91, 9.67, 0)); + addEntities(entities.pine(1.59, 24.67, 0)); + addEntities(entities.pine(26.75, 4.13, 0)); + + // north-east fields + add(entities.pumpkin(142.84, 27.29)); + add(entities.pumpkin(143.59, 27.92)); + add(entities.pumpkin(142.75, 28.88)); + add(entities.pumpkin(150.81, 29.33)); + add(entities.pumpkin(151.22, 30.00)); + add(entities.pumpkin(148.53, 26.54)); + add(entities.pumpkin(140.69, 32.58)); + add(entities.pumpkin(141.03, 32.96)); + add(entities.pumpkin(150.47, 33.21)); + add(entities.pumpkin(150.91, 32.54)); + add(entities.pumpkin(149.91, 32.29)); + add(entities.pumpkin(150.38, 31.54)); + add(entities.pumpkin(144.44, 30.29)); + addEntities(entities.tree(153.59, 24.96, 0)); + addEntities(entities.tree(153.28, 5.58, 1 + 4)); + addEntities(entities.tree(155.03, 8.08, 2 + 8)); + addEntities(entities.tree(149.78, 11.13, 0)); + addEntities(entities.tree(124.56, 11.67, 1)); + addEntities(entities.tree(121.00, 7.25, 2 + 4)); + addEntities(entities.tree(114.47, 21.38, 0 + 8)); + addEntities(entities.tree(136.50, 5.79, 1 + 4)); + addEntities(entities.tree(138.53, 4.04, 2)); + addEntities(entities.tree(140.72, 9.58, 0)); + addEntities(entities.tree(138.22, 25.13, 1 + 4)); + addEntities(entities.tree(140.09, 23.54, 2 + 8)); + addEntities(entities.tree(125.72, 36.83, 0)); + addEntities(entities.tree(116.44, 35.00, 1)); + addEntities(entities.tree(117.53, 33.17, 2 + 4)); + addEntities(entities.pine(141.28, 37.29, 0)); + addEntities(entities.pine(155.88, 34.67, 0)); + addEntities(entities.pine(135.19, 34.88, 0)); + addEntities(entities.pine(155.16, 11.54, 0)); + addEntities(entities.pine(149.75, 17.25, 0)); + addEntities(entities.pine(151.88, 19.79, 0)); + + // harbor road + addEntities(entities.tree(12.91, 64.13, 0)); + addEntities(entities.tree(30.03, 77.58, 1)); + addEntities(entities.tree(21.22, 69.17, 2)); + addEntities(entities.tree(30.69, 53.00, 0)); + addEntities(entities.tree(33.72, 69.96, 1)); + addEntities(entities.tree(30.94, 64.96, 2)); + addEntities(entities.tree(39.72, 82.25, 0)); + addEntities(entities.tree(10.81, 59.25, 1)); + addEntities(entities.pine(37.78, 58.79, 0)); + + // south-west forest + addEntities(entities.pine(28.22, 132.04, 0)); + addEntities(entities.pine(30.91, 130.67, 0)); + addEntities(entities.pine(32.03, 145.79, 0)); + addEntities(entities.pine(27.75, 152.04, 0)); + addEntities(entities.pine(28.59, 113.42, 0)); + addEntities(entities.pine(40.34, 119.54, 0)); + addEntities(entities.pine(18.75, 114.63, 0)); + addEntities(entities.pine(16.69, 117.79, 0)); + addEntities(entities.pine(20.91, 122.38, 0)); + addEntities(entities.pine(23.31, 121.71, 0)); + addEntities(entities.pine(23.69, 126.58, 0)); + addEntities(entities.pine(6.25, 141.58, 0)); + addEntities(entities.pine(4.66, 147.21, 0)); + addEntities(entities.pine(8.06, 144.71, 0)); + addEntities(entities.pine(3.47, 123.46, 0)); + addEntities(entities.pine(35.00, 105.04, 0)); + addEntities(entities.pine(32.00, 101.96, 0)); + addEntities(entities.pine(16.69, 96.46, 0)); + addEntities(entities.pine(17.75, 100.00, 0)); + addEntities(entities.pine(21.03, 95.13, 0)); + addEntities(entities.pine(36.94, 134.46, 0)); + addEntities(entities.pine(37.75, 136.83, 0)); + add(entities.rock(20.84, 98.25)); + add(entities.rock(29.09, 119.00)); + add(entities.rock(36.81, 141.83)); + add(entities.rock(10.00, 149.58)); + add(entities.rock(5.53, 121.88)); + add(entities.rock(3.28, 103.38)); + add(entities.rock(36.84, 122.42)); + add(entities.lanternOn(33.19, 135.83)); + add(entities.lanternOn(28.16, 135.38)); + add(entities.lanternOn(26.41, 137.92)); + add(entities.lanternOn(26.91, 143.96)); + add(entities.lanternOn(34.25, 140.50)); + add(entities.lanternOn(30.88, 143.71)); + add(entities.lanternOn(20.63, 146.96)); + add(entities.lanternOn(15.06, 147.46)); + add(entities.lanternOn(14.94, 152.50)); + add(entities.lanternOn(19.13, 154.92)); + add(entities.lanternOn(21.00, 152.71)); + add(entities.lanternOn(29.84, 105.04)); + add(entities.lanternOn(24.34, 108.92)); + add(entities.lanternOn(29.59, 109.92)); + add(entities.boxLanterns(15.66, 154.88)).interact = giveLantern; + add(entities.boxLanterns(31.72, 134.50)).interact = giveLantern; + add(entities.boxLanterns(138.97, 27.33)).interact = giveLantern; + add(entities.boxLanterns(139.47, 10.83)).interact = giveLantern; + add(entities.treeStump1(23.09, 145.67)); + add(entities.treeStump2(30.78, 132.92)); + add(entities.treeStump1(35.31, 143.04)); + add(entities.treeStump2(18.31, 124.13)); + add(entities.treeStump1(7.50, 147.38)); + add(entities.treeStump2(5.22, 128.71)); + add(entities.treeStump1(22.22, 147.25)); + add(entities.treeStump2(24.91, 149.33)); + add(entities.treeStump1(9.63, 139.50)); + add(entities.treeStump2(11.59, 141.79)); + add(entities.treeStump1(12.16, 139.67)); + add(entities.treeStump2(17.09, 125.88)); + addEntities(entities.pine(12.81, 133.33, 0)); + addEntities(entities.pine(9.38, 115.92, 0)); + addEntities(entities.pine(11.63, 112.17, 0)); + addEntities(entities.pine(20.56, 134.58, 0)); + addEntities(entities.pine(21.81, 156.33, 0)); + addEntities(entities.pine5(10.94, 136.04, 0)); + addEntities(entities.pine5(24.44, 158.46, 0)); + addEntities(entities.pine5(22.47, 133.29, 0)); + addEntities(entities.pine5(10.47, 110.38, 0)); + addEntities(entities.pine4(9.72, 133.50, 0)); + addEntities(entities.pine4(23.88, 136.75, 0)); + addEntities(entities.pine4(19.63, 158.33, 0)); + add(entities.treeStump1(13.66, 104.42)); + add(entities.treeStump1(29.03, 100.88)); + add(entities.treeStump1(3.19, 110.88)); + add(entities.treeStump1(35.56, 92.04)); + add(entities.treeStump1(13.13, 88.33)); + add(entities.treeStump1(34.81, 90.54)); + add(entities.treeStump1(38.59, 115.58)); + addWoodenFence(9, 95.5, 5); + addWoodenFence(13.5, 90, 1, false, false, true); + addWoodenFence(13.5, 91, 3); + add(entities.lanternOn(12.69, 90.08)); + add(entities.lanternOn(8.88, 94.42)); + add(entities.lanternOn(8.97, 90.75)); + add(entities.boxLanterns(12.38, 87.96)).interact = giveLantern; + addEntities(entities.pine(72.63, 153.08, 0)); + addEntities(entities.pine(70.41, 157.92, 0)); + + addEntities(entities.pine5(4.84, 125.75, 0)); + addEntities(entities.pine5(30.91, 154.46, 0)); + addEntities(entities.pine5(42.56, 155.46, 0)); + addEntities(entities.pine5(31.88, 106.21, 0)); + addEntities(entities.pine5(1.28, 128.04, 0)); + addEntities(entities.pine5(15.59, 87.58, 0)); + addEntities(entities.pine5(37.41, 94.33, 0)); + addEntities(entities.pine5(29.63, 84.88, 0)); + addEntities(entities.pine5(13.41, 101.63, 0)); + addEntities(entities.pine5(4.25, 108.21, 0)); + addEntities(entities.pine5(27.28, 115.88, 0)); + addEntities(entities.pine5(36.00, 139.08, 0)); + addEntities(entities.pine5(36.41, 61.08, 0)); + addEntities(entities.pine5(26.69, 57.04, 0)); + addEntities(entities.pine5(6.28, 54.46, 0)); + addEntities(entities.pine5(7.22, 43.50, 0)); + addEntities(entities.pine5(6.41, 30.54, 0)); + addEntities(entities.pine5(7.88, 14.13, 0)); + addEntities(entities.pine5(132.75, 38.08, 0)); + addEntities(entities.pine5(148.16, 21.42, 0)); + addEntities(entities.pine5(157.44, 14.13, 0)); + addEntities(entities.pine5(154.25, 32.38, 0)); + + addEntities(entities.tree5(41.69, 85.00, 0)); + addEntities(entities.tree5(24.91, 79.29, 1)); + addEntities(entities.tree5(24.06, 68.17, 2)); + addEntities(entities.tree5(10.00, 62.00, 0)); + addEntities(entities.tree5(8.94, 47.38, 1)); + addEntities(entities.tree5(33.06, 56.04, 2)); + addEntities(entities.tree5(32.94, 35.21, 0)); + addEntities(entities.tree5(39.88, 15.58, 1)); + addEntities(entities.tree5(42.38, 29.96, 2)); + addEntities(entities.tree5(69.53, 30.46, 0)); + addEntities(entities.tree5(85.94, 21.08, 1)); + addEntities(entities.tree5(84.38, 23.33, 2)); + addEntities(entities.tree5(103.91, 22.46, 0)); + addEntities(entities.tree5(110.50, 33.71, 1)); + addEntities(entities.tree5(103.63, 46.04, 2)); + addEntities(entities.tree5(62.81, 46.25, 0)); + + addEntities(entities.tree4(136.78, 8.79, 0)); + addEntities(entities.tree4(155.53, 26.58, 1)); + addEntities(entities.tree4(127.63, 38.21, 2)); + addEntities(entities.tree4(117.03, 8.42, 0)); + addEntities(entities.tree4(109.47, 25.83, 1)); + addEntities(entities.tree4(100.47, 20.96, 2)); + addEntities(entities.tree4(108.94, 1.83, 0)); + addEntities(entities.tree4(76.25, 15.42, 1)); + addEntities(entities.tree4(77.81, 5.88, 2)); + addEntities(entities.tree4(42.78, 15.25, 0)); + addEntities(entities.tree4(27.84, 28.21, 1)); + addEntities(entities.tree4(28.25, 43.75, 2)); + addEntities(entities.tree4(10.66, 46.75, 0)); + addEntities(entities.tree4(29.34, 66.25, 1)); + addEntities(entities.tree4(25.59, 84.79, 2)); + addEntities(entities.pine4(4.81, 59.46, 0)); + addEntities(entities.pine4(12.88, 85.58, 0)); + addEntities(entities.pine4(10.81, 99.63, 0)); + addEntities(entities.pine4(19.47, 127.13, 0)); + addEntities(entities.pine4(3.19, 130.63, 0)); + addEntities(entities.pine4(11.91, 148.50, 0)); + addEntities(entities.pine4(28.06, 155.58, 0)); + addEntities(entities.pine4(39.81, 147.38, 0)); + addEntities(entities.pine4(40.75, 132.83, 0)); + addEntities(entities.pine4(25.63, 113.83, 0)); + addEntities(entities.pine4(36.47, 102.96, 0)); + addEntities(entities.pine4(5.75, 109.96, 0)); + addEntities(entities.pine(5.66, 156.04, 0)); + addEntities(entities.pine(8.25, 158.83, 0)); + addEntities(entities.pine(37.38, 158.75, 0)); + addEntities(entities.pine(0.75, 139.38, 0)); + addEntities(entities.pine5(2.00, 137.63, 0)); + addEntities(entities.pine4(10.16, 156.88, 0)); + addEntities(entities.pine4(35.28, 157.71, 0)); + addEntities(entities.pine5(34.09, 148.13, 0)); + addEntities(entities.pine3(22.28, 143.50, 0)); + addEntities(entities.pine3(2.09, 141.83, 0)); + addEntities(entities.pine3(42.22, 121.67, 0)); + addEntities(entities.pine3(29.94, 121.00, 0)); + addEntities(entities.pine3(16.47, 120.25, 0)); + addEntities(entities.pine3(38.50, 105.08, 0)); + addEntities(entities.pine3(15.38, 103.58, 0)); + addEntities(entities.pine3(41.34, 94.21, 0)); + addEntities(entities.pine3(27.84, 87.21, 0)); + addEntities(entities.pine3(25.63, 58.58, 0)); + addEntities(entities.pine3(6.66, 15.96, 0)); + addEntities(entities.pine3(1.47, 39.46, 0)); + addEntities(entities.pine3(28.63, 6.08, 0)); + add(entities.rock(15.78, 34.00)); + add(entities.rock(32.63, 25.42)); + add(entities.rock(8.91, 7.29)); + add(entities.rock(51.63, 13.17)); + add(entities.rock(86.00, 22.29)); + add(entities.rock(104.91, 7.92)); + add(entities.rock(108.53, 24.21)); + add(entities.rock(98.63, 38.04)); + add(entities.rock(117.22, 24.63)); + + add(createSignWithText(70.5, 70.5, 'Pony Town', ' Pony Town\n[under construction]', entities.sign)); + + addEntities(createToyStash(47.00, 55.00)); + + addEntities(entities.pine3(72.78, 64.13, 0)); + + const addCat = (x: number, y: number) => { + const entity = add(entities.cat(x, y)); + let delay = 5; + let boopDelay = 5; + let hideDelay = 5; + let hidden = false; + + entity.boopY = -0.1; + entity.boop = () => { + if (!hidden && boopDelay < 0) { + setTimeout(() => sayToAll(entity, '😠', '😠', MessageType.Thinking, {}), 500); + boopDelay = random(5, 10, true); + } + }; + + entity.serverUpdate = delta => { + delay -= delta; + boopDelay -= delta; + hideDelay -= delta; + + if (hideDelay < 0 && delay < 0) { + if (hidden) { + hidden = false; + setEntityAnimation(entity, CatAnimation.Enter); + hideDelay = random(30, 60, true); + delay = random(2, 4, true); + } else { + hidden = true; + setEntityAnimation(entity, CatAnimation.Exit); + hideDelay = random(15, 30, true); + } + } else if (!hidden && delay < 0) { + const rand = Math.random(); + + if (rand < 0.1) { + sayToAll(entity, 'meow', 'meow', MessageType.System, {}); + delay = random(2, 4, true); + } else if (rand < 0.5) { + setEntityAnimation(entity, CatAnimation.Wag); + delay = random(2, 4, true); + } else { + setEntityAnimation(entity, CatAnimation.Blink); + delay = random(2, 4, true); + } + } + }; + }; + + add(createSign(70, 61.5, 'Letter Sign', give(entities.letter.type, `Here's your letter!`), entities.sign)); + + add(entities.mistletoe(43.00, 48.00)); + add(entities.mistletoe(78.00, 85.70)); + add(entities.fence3(46.50, 53.00)); + add(entities.fence3(65.00, 71.00)); + add(entities.fence3(64.00, 76.00)); + add(entities.fence3(55.00, 76.50)); + add(entities.fence2(92.00, 96.00)); + add(entities.fence2(85.00, 97.00)); + add(entities.fence1(48.00, 75.00)); + + // pine trees + add(entities.pine1(48.50, 65.50)); + add(entities.pine2(51.00, 65.00)); + + addEntities(entities.pine3(52.00, 63.00, 0)); + addEntities(entities.pine4(53.00, 67.00, 0)); + addEntities(entities.pine5(43.00, 62.00, 0)); + addEntities(entities.pine(48.50, 63.00, 0)); + addEntities(entities.pine(46.00, 67.00, 0)); + addEntities(entities.pine(42.00, 69.00, 0)); + addEntities(entities.pine3(113.00, 104.00, 0)); + add(createCookieTable2(74.00, 66.00)); + + addEntities(entities.pine(93.00, 53.00, 0)); + addEntities(entities.pine(97.00, 57.00, 0)); + addEntities(entities.pine(94.00, 62.00, 0)); + addEntities(entities.pine(64.00, 91.00, 0)); + addEntities(entities.pine(59.00, 95.00, 0)); + addEntities(entities.pine(63.00, 98.00, 0)); + + // small trees + add(entities.trees1[0](51.50, 69.00)); + add(entities.trees1[1](75.30, 72.20)); + add(entities.trees1[2](93.00, 67.00)); + add(entities.trees1[0](87.60, 47.00)); + add(entities.trees2[0](71.70, 78.70)); + add(entities.trees2[1](94.80, 94.60)); + add(entities.trees2[2](87.00, 52.70)); + add(entities.trees3[2](87.50, 45.50)); + add(entities.trees3[0](88.30, 82.50)); + add(entities.trees3[2](95.50, 94.00)); + add(entities.trees3[0](84.50, 97.50)); + add(entities.trees3[1](74.00, 83.80)); + add(entities.trees3[2](46.00, 77.50)); + + addEntities(entities.tree4(61.70, 70.50, 0)); + addEntities(entities.tree4(47.50, 74.50, 1)); + addEntities(entities.tree4(69.00, 53.50, 2)); + addEntities(entities.tree4(88.50, 57.70, 0)); + addEntities(entities.tree4(83.50, 64.50, 1)); + addEntities(entities.tree4(76.00, 82.00, 2)); + addEntities(entities.tree4(90.00, 82.70, 1)); + addEntities(entities.tree4(95.00, 80.00, 2)); + addEntities(entities.tree4(97.00, 88.00, 0)); + addEntities(entities.tree4(83.50, 96.50, 0)); + addEntities(entities.tree4(84.00, 91.00, 1)); + addEntities(entities.tree5(86.00, 46.50, 0)); + addEntities(entities.tree5(84.00, 55.00, 1)); + addEntities(entities.tree5(63.00, 71.60, 1)); + addEntities(entities.tree5(45.00, 76.00, 0)); + addEntities(entities.tree5(82.00, 80.50, 0)); + addEntities(entities.tree5(89.00, 81.50, 2)); + addEntities(entities.tree5(98.00, 85.50, 2)); + addEntities(entities.tree5(90.00, 95.00, 0)); + addEntities(entities.tree5(78.00, 90.50, 0)); + + // trees - top + addTree(45.00, 45.00, 1); + addTree(42.00, 47.00, 2 + 4, false, isHalloween); + addTree(48.00, 50.00, 0 + 8, true, true); + addTree(82.00, 53.00, 1); + + addEntities(entities.tree4(71.50, 47.00, 0)); + addEntities(entities.tree5(70.00, 46.00, 1)); + add(entities.trees2[2](68.50, 48.00)); + + // trees - bottom right + addTree(83.00, 87.00, 1); + addTree(85.00, 82.00, 0 + 8, isHalloween); + addTree(87.00, 90.00, 1 + 4); + addTree(92.00, 85.00, 2, true, true); + addTree(79.00, 85.00, 3 + 4); + addTree(81.00, 92.00, 0 + 8, isHalloween, isHalloween); + addTree(90.00, 79.00, 1 + 4); + addTree(94.00, 76.00, 2); + addTree(96.00, 87.00, 3 + 4); + addTree(93.00, 92.00, 0 + 8, isHalloween); + addTree(97.00, 82.00, 1 + 4, false, isHalloween); + + // tree stumps - right + add(entities.treeStump2(91.50, 68.50)); + add(entities.treeStump1(92.50, 66.50)); + add(entities.treeStump2(88.50, 64.50)); + + // trees - 80x80 + addTree(102.71, 82.41, 0); + addTree(102.09, 89.41, 1 + 4); + addTree(108.71, 82.37, 2 + 8); + addTree(111.37, 86.58, 0); + addTree(116.65, 95.12, 1 + 8); + addTree(118.43, 98.41, 2); + addTree(104.59, 106.54, 0 + 4); + addTree(101.12, 110.79, 1 + 8); + addTree(107.28, 114.20, 2, isHalloween); + addTree(119.62, 114.91, 0); + addTree(96.53, 94.95, 1 + 4); + addTree(94.75, 102.37, 2 + 4); + addTree(81.81, 112.12, 0 + 8, isHalloween); + addTree(88.03, 115.83, 1); + addTree(111.84, 118.62, 2 + 4); + addTree(118.78, 47.50, 2 + 8); + + // top right + addTree(135.00, 66.90, 1); + addTree(156.38, 75.17, 0); + addTree(149.38, 78.33, 1 + 4, isHalloween); + addTree(151.13, 81.33, 2); + addTree(156.88, 86.58, 0 + 4); + addTree(152.31, 88.83, 1 + 8, isHalloween, isHalloween); + addTree(155.50, 90.17, 2); + addTree(140.88, 83.92, 0); + addTree(147.56, 95.00, 1 + 8); + + addEntities(entities.pine(141.13, 45.67, 0)); + addEntities(entities.pine(138.31, 52.33, 0)); + addEntities(entities.pine(148.25, 49.00, 0)); + addEntities(entities.pine(155.06, 46.17, 0)); + addEntities(entities.pine(157.38, 49.17, 0)); + addEntities(entities.pine(150.56, 60.42, 0)); + addEntities(entities.tree5(147.88, 80.42, 2)); + addEntities(entities.tree5(154.31, 93.33, 0)); + addEntities(entities.pine5(155.72, 52.13, 0)); + addEntities(entities.pine5(139.66, 54.50, 0)); + addEntities(entities.pine4(149.72, 51.04, 0)); + addEntities(entities.pine4(142.09, 57.75, 0)); + addEntities(entities.pine4(153.34, 48.21, 0)); + addEntities(entities.pine3(147.63, 53.96, 0)); + addEntities(entities.pine3(143.47, 44.25, 0)); + addEntities(entities.pine3(136.06, 53.88, 0)); + add(entities.treeStump1(136.91, 45.67)); + add(entities.treeStump2(132.69, 51.63)); + add(entities.treeStump1(131.41, 48.08)); + add(entities.lanternOn(136.09, 67.29)); + add(entities.lanternOn(132.38, 68.38)); + add(entities.lanternOn(138.53, 67.75)); + add(entities.rock(151.16, 63.21)); + add(entities.rock(148.66, 79.38)); + add(entities.treeStump1(154.72, 54.83)); + add(entities.treeStump2(152.22, 63.29)); + add(entities.treeStump1(149.84, 67.58)); + add(entities.treeStump2(152.81, 70.54)); + add(entities.treeStump1(154.06, 64.63)); + add(entities.treeStump2(156.50, 67.38)); + add(entities.treeStump1(156.56, 61.29)); + add(entities.treeStump2(155.44, 60.38)); + add(entities.treeStump1(157.41, 56.67)); + + // forest + addTree(122.69, 106.50, 0); + addTree(131.69, 105.58, 1); + addTree(130.94, 122.33, 2 + 4, isHalloween); + addTree(132.69, 118.08, 0 + 8); + addTree(124.38, 138.25, 1 + 8); + addTree(134.94, 137.42, 2, isHalloween, isHalloween); + addTree(131.94, 134.25, 0); + addTree(138.25, 130.92, 1 + 8); + addTree(121.69, 121.00, 2 + 4); + addTree(111.94, 126.83, 0 + 4, isHalloween); + addTree(115.06, 125.25, 1 + 8); + addTree(114.44, 129.58, 2); + addTree(120.69, 136.25, 0); + addTree(125.00, 98.92, 1 + 8); + addTree(128.56, 93.92, 2); + addTree(140.88, 111.33, 0); + addTree(137.94, 114.83, 1); + addTree(147.19, 113.58, 2, isHalloween); + addTree(143.94, 118.17, 0 + 4); + addTree(149.94, 122.92, 1 + 8); + addTree(148.13, 125.67, 2, isHalloween, isHalloween); + addTree(144.19, 136.08, 0); + addTree(152.13, 130.33, 1 + 4); + addTree(155.88, 107.25, 2 + 8); + addTree(132.31, 148.75, 0, isHalloween); + addEntities(entities.tree5(125.81, 109.00, 0)); + addEntities(entities.tree5(119.31, 122.75, 1)); + addEntities(entities.tree5(141.88, 114.58, 2)); + addEntities(entities.tree5(139.13, 138.50, 0)); + addEntities(entities.tree5(150.25, 115.92, 1)); + addEntities(entities.tree5(118.69, 138.50, 2)); + addEntities(entities.tree5(133.56, 121.67, 0)); + addEntities(entities.tree4(130.50, 97.17, 0)); + addEntities(entities.tree4(116.00, 116.08, 1)); + addEntities(entities.tree4(133.44, 108.00, 2)); + addEntities(entities.tree4(110.50, 130.33, 0)); + addEntities(entities.tree4(145.31, 120.25, 1)); + addEntities(entities.tree4(149.40, 129.33, 2)); + addEntities(entities.tree4(141.81, 85.67, 0)); + addEntities(entities.tree4(158.19, 108.67, 1)); + add(entities.tree3(123.50, 122.50)); + add(entities.tree3(139.63, 116.25)); + add(entities.tree3(112.75, 130.83)); + add(entities.tree3(156.63, 92.42)); + add(entities.tree3(137.00, 138.08)); + add(entities.rock(133.81, 137.54)); + add(entities.rock(122.97, 139.54)); + add(entities.rock(130.22, 121.79)); + add(entities.rock(126.25, 109.54)); + add(entities.rock(151.00, 129.83)); + add(entities.treeStump1(141.53, 117.29)); + add(entities.treeStump2(139.31, 132.38)); + add(entities.treeStump1(117.78, 125.63)); + add(entities.treeStump2(125.66, 111.50)); + add(entities.treeStump1(123.06, 113.79)); + add(entities.treeStump2(124.13, 117.71)); + add(entities.treeStump1(129.25, 99.88)); + add(entities.lanternOn(52.97, 134.75)); + add(entities.lanternOn(49.19, 138.25)); + add(entities.lanternOn(49.25, 142.08)); + add(entities.lanternOn(54.97, 142.96)); + add(entities.lanternOn(45.53, 146.04)); + + // graveyard + addStoneWall(142, 98, 2, false, true); // left (1) + addStoneWall(142, 104, 2, false, false, true); // left (2) + addStoneWall(142, 98, 5); // top + addStoneWall(152, 98, 5, false, true, true); // right + addStoneWall(142, 108, 5); // bottom + + add(entities.lanternOnWall(148.00, 98.20)); + add(entities.lanternOnWall(144.00, 108.20)); + add(entities.lanternOnWall(152.00, 108.20)); + addCat(149, 108.19); + + // forest path + add(entities.boxLanterns(52.50, 133.96)).interact = giveLantern; + add(entities.boxLanterns(124.53, 112.67)).interact = giveLantern; + add(entities.boxLanterns(142.59, 118.54)).interact = giveLantern; + + addWoodenFence(132, 108, 6, false); + addWoodenFence(125, 121, 4, false); + addWoodenFence(131, 130, 3, false); + addWoodenFence(125, 132, 4, false); + addWoodenFence(118, 100, 6); + addWoodenFence(117, 105, 3); + + add(entities.lanternOn(123.59, 112.67)); + add(entities.lanternOn(124.03, 116.42)); + add(entities.lanternOn(141.50, 118.25)); + add(entities.lanternOn(138.25, 116.63)); + add(entities.lanternOn(145.63, 121.33)); + add(entities.lanternOn(135.78, 122.00)); + add(entities.lanternOn(137.78, 124.17)); + add(entities.lanternOn(138.53, 120.54)); + add(entities.lanternOn(143.44, 119.04)); + add(entities.lanternOn(94.41, 132.92)); + add(entities.lanternOn(95.53, 136.17)); + add(entities.lanternOn(91.31, 133.71)); + add(entities.lanternOn(90.56, 136.58)); + add(entities.lanternOn(93.34, 138.17)); + + // bottom left + addEntities(entities.pine(45.00, 126.33, 0)); + addEntities(entities.pine(52.69, 130.42, 0)); + addEntities(entities.pine(49.81, 133.58, 0)); + addEntities(entities.pine(46.75, 140.92, 0)); + addEntities(entities.pine(57.56, 143.92, 0)); + addEntities(entities.pine(52.38, 150.42, 0)); + addEntities(entities.pine(41.50, 144.83, 0)); + addEntities(entities.pine(44.06, 151.17, 0)); + addEntities(entities.pine(95.56, 122.83, 0)); + addEntities(entities.pine(105.81, 120.92, 0)); + addEntities(entities.pine(103.19, 131.00, 0)); + addEntities(entities.pine(100.81, 129.67, 0)); + addEntities(entities.pine(97.88, 133.33, 0)); + addEntities(entities.pine(108.13, 137.92, 0)); + addEntities(entities.pine(105.13, 139.50, 0)); + addEntities(entities.pine5(45.31, 143.75, 0)); + addEntities(entities.pine5(53.94, 153.50, 0)); + addEntities(entities.pine5(41.56, 130.83, 0)); + addEntities(entities.pine5(58.63, 132.58, 0)); + addEntities(entities.pine5(60.06, 147.17, 0)); + addEntities(entities.pine5(43.69, 133.25, 0)); + addEntities(entities.pine4(55.63, 133.92, 0)); + addEntities(entities.pine4(50.31, 152.83, 0)); + addEntities(entities.pine4(62.31, 143.92, 0)); + addEntities(entities.pine4(98.50, 139.08, 0)); + addEntities(entities.pine4(107.38, 123.75, 0)); + addEntities(entities.pine4(89.63, 125.33, 0)); + addEntities(entities.pine4(64.19, 149.00, 0)); + addEntities(entities.pine3(57.00, 127.75, 0)); + addEntities(entities.pine3(62.31, 151.17, 0)); + addEntities(entities.pine3(51.00, 155.58, 0)); + addEntities(entities.pine3(110.25, 140.25, 0)); + addEntities(entities.pine3(90.06, 130.33, 0)); + addEntities(entities.pine3(105.75, 124.75, 0)); + + // bottom center + addStoneWall(93, 141.5, 3); + addStoneWall(101, 141.5, 3); + addStoneWall(90, 146, 3); + addStoneWall(103, 146.5, 3); + + // bottom right + addWoodenFence(118, 140, 4); + addWoodenFence(121, 145, 4); + addWoodenFence(136, 140, 6); + addWoodenFence(134, 144, 6); + addWoodenFence(143, 144.5, 2); + + // pumpkin field + addEntities(entities.pine5(100.38, 120.58, 0)); + addEntities(entities.pine5(93.31, 128.58, 0)); + addEntities(entities.pine5(100.00, 137.75, 0)); + + addWoodenFence(76, 120, 12); // top + addWoodenFence(76, 120, 5, false, true); // left 1 + addWoodenFence(76, 129, 7, false, false); // left 2 + addWoodenFence(88, 120, 10, false, true); // right 1 + addWoodenFence(88, 130, 1, true, true, true); // right 2 + addWoodenFence(89, 130, 7, false, false, true); // right 3 + addWoodenFence(76, 136, 6); // bottom 1 + addWoodenFence(82, 136, 1, false, true, true); // bottom 2 + addWoodenFence(82, 137, 7); // bottom 3 + + addWoodenFence(82, 147, 6); // top + addWoodenFence(75, 147, 4); // top + addWoodenFence(88, 147, 10, false, true); // right + addWoodenFence(75, 157, 13, true, false, true); // bottom + addWoodenFence(75, 147, 10, false, true, true); // left + + if (true) { + add(entities.pumpkin(77.44, 124.67)); + add(entities.pumpkin(78.31, 125.75)); + add(entities.pumpkin(80.63, 124.00)); + add(entities.pumpkin(84.25, 123.83)); + add(entities.pumpkin(82.81, 126.67)); + add(entities.pumpkin(80.75, 129.17)); + add(entities.pumpkin(82.19, 128.42)); + add(entities.pumpkin(82.38, 129.42)); + add(entities.pumpkin(81.56, 129.67)); + add(entities.pumpkin(76.94, 131.25)); + add(entities.pumpkin(78.25, 134.67)); + add(entities.pumpkin(80.56, 134.00)); + add(entities.pumpkin(84.88, 124.17)); + add(entities.pumpkin(83.81, 124.83)); + add(entities.pumpkin(84.56, 132.50)); + add(entities.pumpkin(84.19, 133.83)); + add(entities.pumpkin(84.75, 135.83)); + add(entities.pumpkin(87.13, 132.50)); + add(entities.pumpkin(86.56, 134.00)); + add(entities.pumpkin(87.70, 134.50)); + add(entities.pumpkin(88.31, 135.67)); + add(entities.pumpkin(82.25, 122.17)); + } + + add(entities.treeStump1(91.25, 123.67)); + add(entities.treeStump1(94.94, 126.33)); + add(entities.treeStump1(92.56, 131.42)); + + // rock circle 2 + add(entities.rock(111.63, 152.33)); + add(entities.rock(107.63, 147.92)); + add(entities.rock(105.31, 152.33)); + add(entities.rock(108.50, 156.33)); + add(entities.rock(115.44, 155.67)); + add(entities.rock(115.38, 149.67)); + add(entities.rock(113.69, 147.33)); + add(entities.rock(105.63, 150.50)); + add(entities.rock(112.88, 157.25)); + add(entities.rock(117.38, 152.83)); + addEntities(entities.pine3(106.38, 148.58, 0)); + addEntities(entities.pine4(115.94, 148.25, 0)); + addEntities(entities.pine4(114.13, 157.92, 0)); + addEntities(entities.pine5(104.25, 150.50, 0)); + + addEntities(entities.tree5(85.28, 116.79, 1)); + addEntities(entities.tree5(91.03, 118.33, 2)); + addEntities(entities.tree5(104.46, 115.70, 0)); + addEntities(entities.tree5(112.96, 107.33, 1)); + addEntities(entities.tree5(100.84, 92.20, 2)); + addEntities(entities.tree5(107.62, 84.29, 0)); + addEntities(entities.tree5(97.43, 64.04, 1)); + addEntities(entities.tree5(116.28, 44.75, 2)); + addEntities(entities.tree5(118.68, 70.62, 0)); + addEntities(entities.tree4(89.81, 103.54, 0)); + addEntities(entities.tree4(110.78, 92.29, 1)); + addEntities(entities.tree4(117.09, 69.66, 2)); + + add(entities.trees3[0](76.37, 118.08)); + + addEntities(entities.pine(92.28, 104.58, 0)); + addEntities(entities.pine(116.15, 105.75, 0)); + addEntities(entities.pine(112.12, 103.45, 0)); + addEntities(entities.pine(118.93, 88.95, 0)); + addEntities(entities.pine(111.34, 77.33, 0)); + addEntities(entities.pine(114.62, 75.66, 0)); + addEntities(entities.pine(98.43, 117.79, 0)); + addEntities(entities.pine(42.59, 115.91, 0)); + addEntities(entities.pine(47.09, 117.70, 0)); + addEntities(entities.pine5(44.59, 104.50, 0)); + addEntities(entities.pine4(95.34, 106.58, 0)); + addEntities(entities.pine3(47.78, 104.75, 0)); + addEntities(entities.pine3(58.78, 110.04, 0)); + + add(entities.pine2(57.50, 109.00)); + add(entities.fence3(86.43, 100.58)); + add(entities.fence3(93.96, 100.50)); + + add(entities.treeStump1(105.31, 89.12)); + add(entities.treeStump2(108.25, 88.16)); + add(entities.treeStump1(113.37, 91.54)); + add(entities.treeStump2(114.12, 95.45)); + add(entities.treeStump1(109.53, 100.58)); + add(entities.treeStump2(107.46, 100.62)); + add(entities.treeStump1(99.34, 101.04)); + add(entities.treeStump2(101.12, 95.12)); + add(entities.treeStump1(102.43, 91.79)); + add(entities.treeStump2(80.18, 113.66)); + add(entities.treeStump1(81.81, 114.95)); + add(entities.treeStump2(77.62, 118.58)); + add(entities.treeStump1(46.93, 106.70)); + add(entities.treeStump2(51.81, 117.41)); + add(entities.treeStump1(53.50, 115.58)); + add(entities.treeStump2(117.03, 48.66)); + + add(entities.rock(104.81, 88.83)); + add(entities.rock(114.53, 95.25)); + add(entities.rock(103.96, 106.25)); + add(entities.rock(84.50, 116.21)); + add(entities.rock(54.25, 116.58)); + add(entities.rock(118.06, 49.20)); + add(entities.rock(104.56, 47.65)); + + add(entities.boxLanterns(113.45, 95.16)).interact = giveLantern; + add(entities.box(105.90, 88.81)); + add(entities.lanternOn(113.31, 95.48)); + add(entities.lanternOn(112.64, 94.42)); + add(entities.lanternOn(105.98, 89.17)); + add(entities.lanternOn(106.81, 100.67)); + add(entities.lanternOn(102.94, 92.25)); + + add(entities.fence1(80.03, 97.00)); + add(entities.fence2(74.81, 100.79)); + + // rocks + add(entities.rock(46.00, 50.00)); + add(entities.rock(48.50, 81.50)); + add(entities.rock(45.50, 84.50)); + add(entities.rock(44.50, 88.50)); + add(entities.rock(46.50, 93.50)); + add(entities.rock(50.50, 94.50)); + add(entities.rock(55.50, 93.50)); + add(entities.rock(56.50, 91.50)); + add(entities.rock(56.50, 86.50)); + add(entities.rock(55.50, 83.50)); + add(entities.rock(52.50, 81.50)); + add(entities.rock(50.50, 87.50)); + add(entities.rock(70.50, 62.30)); + add(entities.rock(74.50, 67.50)); + add(entities.rock(86.50, 52.50)); + add(entities.rock(91.50, 55.50)); + add(entities.rock(88.50, 58.50)); + add(entities.rock(52.00, 68.50)); + add(entities.rock(52.50, 69.50)); + add(entities.rock(61.50, 71.50)); + add(entities.rock(83.50, 91.50)); + add(entities.rock(90.50, 83.50)); + + // pumpkins + add(entities.pumpkin(74.50, 88.50)); + add(entities.pumpkin(74.00, 90.00)); + add(entities.pumpkin(68.00, 87.00)); + add(entities.pumpkin(69.00, 93.00)); + add(entities.pumpkin(68.00, 95.00)); + add(entities.pumpkin(69.70, 61.80)); + + addCat(67.06, 71); + + // lights + + const addJacko = createAddLight(world, map, entities.jacko); + const addTorch = createAddLight(world, map, entities.torch); + + // jack-o-lanterns + // top left + addJacko(45, 48); + + add(entities.lanternOn(66.75, 62)); + add(entities.lanternOn(67.7, 62.8)); + add(entities.lanternOn(69.5, 62.2)); + add(entities.boxLanterns(67.6, 62.6)).interact = giveLantern; + add(entities.boxLanterns(66.5, 63.3)).interact = giveLantern; + + add(entities.treeStump1(70, 62.5)); + + // top left + addTorch(50.00, 54.00); + addTorch(46.50, 55.00); + // top + addTorch(76.00, 53.00); + addTorch(63.00, 50.00); + addTorch(65.90, 64.00); + addTorch(68.00, 47.00); + addTorch(71.00, 50.00); + addTorch(70.00, 57.00); + addTorch(68.30, 62.00); + addTorch(70.70, 61.50); + // top right + addTorch(92.00, 59.00); + addTorch(88.00, 53.00); + addTorch(94.00, 45.00); + addTorch(89.00, 45.00); + addTorch(92.00, 49.00); + // left + addTorch(44.00, 65.00); + addTorch(49.00, 69.00); + addTorch(55.00, 65.00); + // center + addTorch(70.00, 69.00); + // right + addTorch(84.00, 65.00); + addTorch(80.00, 68.00); + addTorch(86.00, 71.00); + addTorch(98.00, 66.50); + // bot left + addTorch(47.00, 80.50); + addTorch(43.00, 87.00); + addTorch(49.00, 94.00); + addTorch(57.00, 91.00); + addTorch(56.00, 82.00); + addTorch(49.00, 87.00); + addTorch(60.00, 88.00); + // bot + addTorch(72.00, 79.00); + addTorch(68.00, 81.00); + addTorch(69.00, 88.80); + // bot right + addTorch(87.00, 94.00); + addTorch(101.50, 94.00); + addTorch(94.50, 93.80); + addTorch(100.00, 100.00); + addTorch(99.00, 95.00); + // 80x80 + addTorch(47.81, 106.50); + addTorch(52.00, 110.85); + addTorch(56.75, 116.96); + addTorch(52.03, 116.88); + addTorch(59.88, 100.50); + addTorch(63.81, 101.67); + addTorch(60.53, 104.63); + addTorch(62.00, 102.88); + addTorch(63.47, 106.33); + addTorch(66.97, 104.00); + addTorch(77.22, 116.17); + addTorch(73.13, 100.13); + addTorch(76.78, 100.42); + addTorch(85.53, 105.50); + addTorch(89.84, 106.33); + addTorch(86.78, 109.25); + addTorch(116.34, 110.83); + addTorch(112.00, 107.60); + addTorch(109.94, 111.46); + addTorch(113.78, 113.83); + addTorch(112.97, 110.83); + addTorch(96.09, 114.88); + addTorch(100.09, 50.75); + addTorch(112.59, 45.96); + addTorch(116.03, 47.92); + addTorch(116.94, 59.08); + addTorch(109.06, 64.29); + addTorch(111.94, 69.96); + addTorch(104.94, 71.21); + addTorch(102.91, 49.42); + addTorch(115.00, 79.79); + addTorch(116.63, 77.46); + addTorch(113.19, 78.17); + addTorch(64.36, 103.92); + // 15x15 + addTorch(117.09, 140.33); + addTorch(120.13, 144.79); + addTorch(123.34, 140.13); + addTorch(126.31, 144.79); + addTorch(134.00, 143.75); + addTorch(133.88, 139.00); + addTorch(131.66, 135.79); + addTorch(126.03, 137.58); + addTorch(125.94, 133.08); + addTorch(130.06, 129.50); + addTorch(125.81, 128.67); + addTorch(125.88, 123.29); + addTorch(130.09, 123.08); + addTorch(126.19, 118.75); + addTorch(127.22, 114.00); + addTorch(132.54, 111.75); + addTorch(127.13, 109.50); + addTorch(130.72, 106.33); + addTorch(121.47, 106.10); + addTorch(123.69, 100.71); + addTorch(126.69, 101.96); + addTorch(119.28, 100.58); + addTorch(117.89, 104.67); + addTorch(138.34, 139.08); + addTorch(139.69, 144.17); + addTorch(142.78, 139.75); + addTorch(145.16, 143.96); + addTorch(148.34, 138.88); + addTorch(148.53, 143.79); + addTorch(142.50, 144.42); + addTorch(138.34, 147.29); + addTorch(140.72, 147.71); + addTorch(143.81, 148.00); + addTorch(142.91, 151.58); + addTorch(139.47, 151.08); + addTorch(136.34, 150.79); + addTorch(136.41, 144.88); + addTorch(140.72, 155.04); + addTorch(152.69, 152.17); + addTorch(155.78, 152.42); + addTorch(153.84, 155.54); + addTorch(150.88, 131.13); + addTorch(153.38, 130.83); + addTorch(113.78, 146.13); + addTorch(110.94, 141.38); + addTorch(107.06, 141.83); + addTorch(107.13, 145.71); + addTorch(100.63, 141.79); + addTorch(101.97, 146.08); + addTorch(96.44, 145.92); + addTorch(98.41, 141.71); + addTorch(92.63, 141.79); + addTorch(90.28, 145.83); + addTorch(88.22, 140.79); + addTorch(88.03, 138.17); + addTorch(83.50, 138.33); + addTorch(79.59, 136.88); + addTorch(75.69, 136.75); + addTorch(75.19, 132.13); + addTorch(75.22, 121.33); + addTorch(78.63, 111.50); + addTorch(81.69, 105.92); + addTorch(82.78, 100.71); + addTorch(69.84, 115.58); + addTorch(68.94, 119.21); + addTorch(69.03, 122.75); + addTorch(69.00, 126.42); + addTorch(69.25, 131.79); + addTorch(69.59, 136.83); + addTorch(86.09, 145.33); + addTorch(82.09, 145.00); + addTorch(78.16, 144.79); + addTorch(73.78, 144.96); + addTorch(74.41, 149.21); + addTorch(66.16, 142.13); + addTorch(69.09, 140.71); + addTorch(69.59, 144.63); + addTorch(71.13, 147.83); + addTorch(58.22, 149.33); + addTorch(61.09, 150.92); + addTorch(63.88, 140.50); + addTorch(59.91, 138.63); + addTorch(56.72, 136.50); + addTorch(124.56, 46.29); + addTorch(127.19, 48.63); + addTorch(123.91, 50.33); + addTorch(126.97, 52.75); + addTorch(131.50, 46.63); + addTorch(130.56, 50.42); + addTorch(134.09, 49.17); + addTorch(132.59, 53.79); + addTorch(126.63, 56.58); + addTorch(129.38, 54.63); + addTorch(151.78, 62.96); + addTorch(155.75, 63.33); + addTorch(149.00, 66.21); + addTorch(148.84, 70.75); + addTorch(153.31, 71.71); + addTorch(157.34, 68.79); + addTorch(157.75, 65.54); + addTorch(151.16, 69.21); + addTorch(75.31, 124.79); + addTorch(75.47, 128.88); + // cookie stands lights + addTorch(33.19 + 0.25, 26.29 + 9.5); + addTorch(65.78, 114.08); + addTorch(89.78, 132.17); + addTorch(124.78, 147.25); + addTorch(132.97, 150.29); + addTorch(144.13, 98.58); + addTorch(120.34, 48.38); + addTorch(144.62, 125.20); + + // top left hill + addTorch(22.40, 45.12); + addTorch(18.19, 40.75); + addTorch(21.13, 35.96); + addTorch(20.91, 30.63); + addTorch(14.22, 30.54); + addTorch(27.78, 30.83); + addTorch(30.16, 34.46); + addTorch(18.13, 24.42); + addTorch(16.31, 17.83); + addTorch(17.69, 13.46); + addTorch(20.88, 10.00); + addTorch(14.22, 10.92); + addTorch(21.13, 16.75); + addTorch(14.09, 22.88); + addTorch(28.03, 12.96); + addTorch(31.19, 8.17); + addTorch(36.38, 10.83); + addTorch(44.56, 8.33); + addTorch(51.28, 11.88); + addTorch(60.13, 9.33); + addTorch(62.91, 11.88); + addTorch(71.00, 8.25); + addTorch(76.28, 12.54); + addTorch(82.66, 9.58); + // center + addTorch(53.09, 71.08); + addTorch(60.78, 70.88); + addTorch(67.19, 76.38); + addTorch(60.53, 76.63); + addTorch(52.38, 76.79); + addTorch(48.13, 75.29); + addTorch(58.97, 55.75); + addTorch(53.75, 51.00); + addTorch(58.81, 42.42); + addTorch(53.97, 40.00); + addTorch(59.59, 37.00); + addTorch(53.34, 30.46); + addTorch(58.09, 30.29); + addTorch(69.84, 38.92); + addTorch(68.09, 41.92); + addTorch(79.78, 39.79); + addTorch(78.19, 43.83); + addTorch(92.19, 39.00); + addTorch(99.25, 39.21); + addTorch(99.19, 45.21); + addTorch(103.19, 36.58); + addTorch(104.63, 41.71); + addTorch(100.00, 58.58); + addTorch(92.22, 30.46); + addTorch(96.06, 28.88); + addTorch(90.88, 24.08); + addTorch(95.34, 22.50); + addTorch(90.81, 17.21); + addTorch(95.88, 15.83); + addTorch(96.28, 10.33); + addTorch(91.25, 8.83); + addTorch(94.34, 2.38); + addTorch(97.16, 6.71); + addTorch(88.78, 4.54); + addTorch(50.69, 20.42); + addTorch(59.38, 20.29); + addTorch(50.28, 29.21); + addTorch(48.72, 32.92); + addTorch(42.66, 36.50); + addTorch(38.19, 31.17); + addTorch(37.53, 37.88); + // top right + addTorch(115.50, 36.17); + addTorch(113.94, 40.13); + addTorch(109.72, 35.71); + addTorch(123.44, 37.00); + addTorch(131.59, 39.75); + addTorch(138.38, 38.13); + addTorch(137.66, 43.00); + addTorch(144.16, 36.17); + addTorch(148.09, 36.71); + addTorch(146.03, 41.00); + addTorch(150.97, 41.00); + addTorch(155.88, 37.67); + addTorch(152.56, 34.83); + addTorch(127.00, 17.88); + addTorch(127.88, 25.21); + addTorch(131.88, 15.21); + addTorch(135.78, 21.92); + addTorch(132.41, 28.33); + addTorch(122.81, 32.38); + addTorch(127.66, 33.33); + addTorch(141.53, 13.67); + addTorch(145.75, 13.75); + addTorch(141.59, 17.25); + addTorch(145.69, 17.71); + addTorch(135.72, 9.29); + addTorch(131.59, 7.63); + addTorch(129.69, 11.71); + // harbor + addTorch(22.50, 75.92); + addTorch(20.34, 70.58); + addTorch(28.66, 70.29); + addTorch(36.72, 70.88); + addTorch(32.41, 77.00); + addTorch(15.03, 67.63); + addTorch(20.03, 63.00); + addTorch(15.59, 60.63); + addTorch(16.88, 53.79); + addTorch(21.75, 54.21); + addTorch(15.47, 76.13); + // south-west forest + addTorch(36.84, 84.25); + addTorch(30.28, 89.29); + addTorch(29.84, 96.92); + addTorch(23.34, 98.54); + addTorch(21.59, 105.33); + addTorch(15.28, 107.83); + addTorch(14.81, 115.33); + addTorch(9.41, 120.79); + addTorch(15.59, 125.63); + addTorch(14.16, 135.13); + addTorch(14.97, 94.67); + addTorch(17.16, 90.21); + addTorch(22.25, 92.71); + + add(createSign(77.06, 60.16, 'Rose sign', give(entities.rose.type, `Here's your rose!`), entities.sign)); + + if (world.featureFlags.test) { + add(createSign(14.5, 70, 'Public Island', (_, client) => goToMap(world, client, 'public-island'), entities.signDebug)); + add(createSignWithText(60.7, 60.2, 'Pickable items', `Click on the item to carry it around`, entities.signDebug)); + add(entities.flower3Pickable(60, 60)).interact = (_, { pony }) => holdItem(pony, entities.flowerPick.type); + add(entities.apple(61, 61)).interact = (_, { pony }) => holdItem(pony, entities.apple.type); + add(entities.appleGreen2(61.3, 61.4)).interact = (_, { pony }) => holdItem(pony, entities.appleGreen2.type); + add(entities.orange(60.3, 61.2)).interact = (_, { pony }) => holdItem(pony, entities.orange.type); + add(entities.grapesPurple[0](60.67, 61.63)).interact = (_, { pony }) => holdItem(pony, entities.grapesPurple[0].type); + } + + if (BETA) { + const objects = [ + entities.fence1, entities.box, entities.boxLanterns, entities.gift3, entities.pumpkin, entities.sign, + entities.rope, + ].map(e => e.type); + + add(createSign(62.7, 58.2, 'Jack-o-Lanterns', give(entities.jackoLanternOn.type, 'Have a lantern'), entities.signDebug)); + add(createSign(62.7, 60.2, 'Pickable objects', (_, client) => holdItem(client.pony, sample(objects)!), entities.signDebug)); + add(createSign(77.0, 69.0, 'Palette', (_, client) => goToMap(world, client, 'palette'), entities.signDebug)); + } + + // new added on Halloween + addEntities(entities.tree4(155.18, 100.04, 1)); + addEntities(entities.tree4(144.97, 96.25, 0)); + addEntities(entities.tree(153.60, 99.54, 2)); + addEntities(entities.pine(157.50, 96.79, 0)); + add(entities.treeStump1(158.03, 110.12)); + add(entities.treeStump1(158.75, 112.91)); + add(entities.treeStump2(157.00, 111.50)); + + // center lakes + add(entities.waterRock1(77.66, 61.67)); + add(entities.waterRock7(76.56, 65.58)); + add(entities.waterRock8(76.97, 65.79)); + add(entities.waterRock10(83.47, 67.63)); + + // sea + add(entities.waterRock1(3.78, 61.96)); + add(entities.waterRock1(12.53, 75.88)); + add(entities.waterRock1(9.38, 80.50)); + add(entities.waterRock1(5.34, 95.42)); + add(entities.waterRock3(4.91, 95.71)); + add(entities.waterRock3(8.44, 85.79)); + add(entities.waterRock3(8.50, 64.00)); + add(entities.waterRock2(3.19, 61.42)); + add(entities.waterRock2(8.97, 86.00)); + add(entities.waterRock2(3.31, 100.88)); + add(entities.waterRock4(4.75, 95.21)); + add(entities.waterRock4(12.44, 77.58)); + add(entities.waterRock4(8.44, 63.54)); + add(entities.waterRock5(3.38, 61.96)); + add(entities.waterRock5(7.22, 90.79)); + add(entities.waterRock6(3.66, 100.25)); + add(entities.waterRock6(12.22, 78.04)); + add(entities.waterRock7(7.97, 63.88)); + add(entities.waterRock7(8.25, 86.38)); + add(entities.waterRock8(9.03, 80.83)); + add(entities.waterRock8(1.31, 101.71)); + add(entities.waterRock9(11.34, 79.17)); + add(entities.waterRock10(9.97, 66.79)); + add(entities.waterRock10(4.25, 94.38)); + add(entities.waterRock11(9.47, 66.88)); + add(entities.waterRock11(12.28, 70.71)); + add(entities.waterRock11(7.00, 91.25)); + + // river + add(entities.waterRock1(114.06, 1.96)); + add(entities.waterRock1(112.13, 17.92)); + add(entities.waterRock1(112.84, 28.38)); + add(entities.waterRock1(111.66, 35.75)); + add(entities.waterRock2(109.15, 16.50)); + add(entities.waterRock2(114.66, 32.50)); + add(entities.waterRock3(112.91, 20.33)); + add(entities.waterRock3(110.38, 9.75)); + add(entities.waterRock4(109.59, 16.75)); + add(entities.waterRock4(113.41, 7.96)); + add(entities.waterRock4(111.63, 35.13)); + add(entities.waterRock5(114.34, 33.04)); + add(entities.waterRock5(110.63, 9.38)); + add(entities.waterRock6(109.63, 16.58)); + add(entities.waterRock6(113.28, 28.58)); + add(entities.waterRock7(114.50, 1.54)); + add(entities.waterRock7(111.97, 35.38)); + add(entities.waterRock8(113.28, 28.17)); + add(entities.waterRock8(111.44, 22.79)); + add(entities.waterRock8(110.72, 9.63)); + add(entities.waterRock9(107.94, 12.75)); + add(entities.waterRock9(113.97, 6.58)); + add(entities.waterRock10(108.38, 12.63)); + add(entities.waterRock10(114.44, 33.46)); + add(entities.waterRock11(112.31, 18.25)); + add(entities.waterRock11(111.34, 12.25)); + add(entities.waterRock11(110.59, 39.75)); + + // river+lake + add(entities.waterRock1(107.75, 45.83)); + add(entities.waterRock1(105.63, 50.92)); + add(entities.waterRock1(118.56, 58.83)); + add(entities.waterRock1(127.59, 59.13)); + add(entities.waterRock1(131.50, 67.58)); + add(entities.waterRock1(124.25, 68.54)); + add(entities.waterRock2(117.44, 53.67)); + add(entities.waterRock2(128.41, 59.17)); + add(entities.waterRock2(105.06, 50.30)); + add(entities.waterRock2(111.56, 43.54)); + add(entities.waterRock3(112.69, 56.08)); + add(entities.waterRock3(105.53, 50.29)); + add(entities.waterRock3(127.88, 59.46)); + add(entities.waterRock3(134.09, 57.88)); + add(entities.waterRock4(111.38, 43.21)); + add(entities.waterRock4(112.97, 55.83)); + add(entities.waterRock4(124.66, 68.88)); + add(entities.waterRock5(110.47, 52.58)); + add(entities.waterRock5(118.16, 58.54)); + add(entities.waterRock5(132.63, 70.75)); + add(entities.waterRock6(108.03, 46.17)); + add(entities.waterRock6(116.94, 54.04)); + add(entities.waterRock6(129.66, 55.92)); + add(entities.waterRock7(109.91, 52.46)); + add(entities.waterRock7(120.31, 53.54)); + add(entities.waterRock7(133.09, 71.08)); + add(entities.waterRock8(132.59, 71.13)); + add(entities.waterRock8(110.22, 52.04)); + add(entities.waterRock8(111.63, 43.00)); + add(entities.waterRock8(110.81, 39.88)); + add(entities.waterRock9(109.81, 48.83)); + add(entities.waterRock10(120.41, 54.08)); + add(entities.waterRock10(131.13, 67.96)); + add(entities.waterRock11(121.75, 64.25)); + add(entities.waterRock10(121.31, 63.60)); + add(entities.waterRock1(121.13, 75.83)); + add(entities.waterRock1(125.47, 78.67)); + add(entities.waterRock1(140.31, 78.96)); + add(entities.waterRock1(146.44, 71.42)); + add(entities.waterRock1(143.47, 50.71)); + add(entities.waterRock1(142.00, 63.75)); + add(entities.waterRock2(139.69, 67.21)); + add(entities.waterRock2(147.75, 61.83)); + add(entities.waterRock2(125.94, 78.92)); + add(entities.waterRock2(140.81, 78.46)); + add(entities.waterRock3(140.06, 67.67)); + add(entities.waterRock3(145.31, 74.92)); + add(entities.waterRock3(137.53, 58.75)); + add(entities.waterRock3(143.84, 50.88)); + add(entities.waterRock3(121.63, 75.42)); + add(entities.waterRock3(133.47, 82.50)); + add(entities.waterRock4(125.94, 78.46)); + add(entities.waterRock4(148.03, 62.17)); + add(entities.waterRock4(143.91, 50.42)); + add(entities.waterRock5(137.81, 59.13)); + add(entities.waterRock5(142.47, 64.04)); + add(entities.waterRock5(146.13, 71.96)); + add(entities.waterRock5(129.16, 80.54)); + add(entities.waterRock5(146.44, 52.88)); + add(entities.waterRock6(140.38, 78.25)); + add(entities.waterRock6(133.06, 82.21)); + add(entities.waterRock6(139.31, 62.96)); + add(entities.waterRock6(138.19, 69.79)); + add(entities.waterRock6(147.38, 65.83)); + add(entities.waterRock7(129.63, 80.79)); + add(entities.waterRock7(145.56, 74.54)); + add(entities.waterRock7(146.88, 52.67)); + add(entities.waterRock8(140.09, 67.21)); + add(entities.waterRock10(133.41, 82.04)); + add(entities.waterRock11(147.66, 62.33)); + add(entities.waterRock8(145.97, 71.42)); + add(entities.waterRock8(121.56, 75.92)); + add(entities.waterRock10(122.38, 73.54)); + add(entities.waterRock8(129.53, 81.46)); + add(entities.waterRock11(136.40, 80.92)); + add(entities.waterRock8(136.88, 80.54)); + add(entities.waterRock11(144.94, 58.71)); + add(entities.waterRock8(133.75, 57.75)); + add(entities.waterRock6(126.63, 80.29)); + add(entities.waterRock1(128.59, 74.63)); + add(entities.waterRock3(129.47, 74.33)); + add(entities.waterRock6(129.16, 74.83)); + add(entities.waterRock8(128.75, 75.00)); + add(entities.waterRock7(140.59, 73.54)); + add(entities.waterRock10(140.94, 73.67)); + add(entities.waterRock8(140.59, 74.08)); + + // pine forest lakes + add(entities.waterRock1(34.81, 118.79)); + add(entities.waterRock1(31.38, 115.79)); + add(entities.waterRock1(51.63, 111.79)); + add(entities.waterRock2(62.38, 111.75)); + add(entities.waterRock2(49.63, 112.83)); + add(entities.waterRock2(31.72, 115.25)); + add(entities.waterRock2(36.69, 109.75)); + add(entities.waterRock3(34.56, 119.04)); + add(entities.waterRock3(32.63, 122.50)); + add(entities.waterRock3(52.03, 112.08)); + add(entities.waterRock4(62.59, 111.38)); + add(entities.waterRock4(51.59, 112.17)); + add(entities.waterRock4(31.91, 115.54)); + add(entities.waterRock5(34.28, 118.46)); + add(entities.waterRock5(33.44, 112.67)); + add(entities.waterRock6(30.66, 118.25)); + add(entities.waterRock6(55.28, 114.13)); + add(entities.waterRock7(33.75, 112.21)); + add(entities.waterRock7(29.94, 116.67)); + add(entities.waterRock7(49.78, 113.29)); + add(entities.waterRock8(49.97, 112.88)); + add(entities.waterRock8(32.72, 122.96)); + add(entities.waterRock9(53.41, 111.63)); + add(entities.waterRock10(64.34, 110.96)); + add(entities.waterRock10(36.91, 110.13)); + add(entities.waterRock10(30.44, 116.50)); + add(entities.waterRock8(30.31, 116.79)); + add(entities.waterRock10(32.94, 122.67)); + + // bottom lake + add(entities.waterRock1(95.34, 148.83)); + add(entities.waterRock2(98.56, 150.54)); + add(entities.waterRock3(95.44, 151.63)); + add(entities.waterRock4(95.75, 151.58)); + add(entities.waterRock5(92.44, 150.67)); + add(entities.waterRock6(92.72, 150.92)); + add(entities.waterRock8(92.75, 150.50)); + add(entities.waterRock9(98.81, 150.96)); + add(entities.waterRock10(98.41, 150.92)); + add(entities.waterRock4(95.03, 149.00)); + + // forest puddle + add(entities.waterRock5(140.44, 122.71)); + add(entities.waterRock4(140.13, 122.67)); + + add(entities.bench1(18.69, 88.50)); + add(entities.bench1(27.94, 104.21)); + add(entities.bench1(22.56, 70.50)); + add(entities.bench1(25.28, 70.46)); + add(entities.bench1(44.16, 31.46)); + add(entities.bench1(46.84, 31.50)); + add(entities.bench1(30.03, 29.46)); + add(entities.bench1(87.00, 3.33)); + add(entities.bench1(101.91, 8.25)); + add(entities.bench1(142.13, 11.54)); + add(entities.bench1(144.84, 11.58)); + add(entities.benchSeat(142.00, 15.88)); + add(entities.benchSeat(144.78, 15.83)); + add(entities.benchBack(142.00, 16.7917)); + add(entities.benchBack(144.78, 16.71)); + add(entities.benchSeat(45.78, 36.42)); + add(entities.benchBack(45.78, 37.33)); + add(entities.bench1(93.59, 146.33)); + add(entities.bench1(144.13, 138.75)); + add(entities.bench1(146.66, 138.83)); + add(entities.bench1(133.22, 150.71)); + add(entities.benchSeat(133.13, 153.71)); + add(entities.benchBack(133.13, 154.63)); + add(entities.bench1(117.28, 62.13)); + add(entities.benchSeat(117.25, 65.13)); + add(entities.benchBack(117.25, 66.04)); + add(entities.bench1(123.81, 53.08)); + add(entities.bench1(127.09, 53.04)); + add(entities.lanternOn(117.09, 64.08)); + add(entities.lanternOn(20.34, 89.04)); + add(entities.benchSeatH(147.14, 13.02)); + add(entities.benchBackH2(147.66, 15.70)); + add(entities.benchSeatH(27.44, 117.21)); + add(entities.benchBackH(26.97, 119.92)); + add(entities.benchSeatH(136.63, 151.29)); + add(entities.benchSeatH(129.34, 151.25)); + add(entities.benchBackH(128.85, 153.96)); + add(entities.benchBackH2(137.16, 153.96)); + + updateMainMapSeason(world, map, world.season, world.holiday); + + addEntities(createBunny([ + point(72.34, 56.79), + point(70.69, 56.08), + point(69.56, 55.67), + point(68.03, 57.67), + point(70.28, 57.96), + point(71.28, 58.79), + point(70.00, 59.54), + point(68.72, 58.67), + point(67.22, 59.96), + point(65.38, 61.17), + point(65.31, 62.50), + point(64.25, 64.25), + point(65.69, 66.00), + point(68.47, 69.50), + point(69.59, 69.79), + point(69.56, 68.21), + point(71.06, 68.79), + point(72.94, 64.67), + point(74.31, 63.13), + point(74.41, 60.79), + point(75.50, 60.13), + point(76.03, 57.75), + point(78.19, 54.42), + point(77.47, 52.75), + point(78.38, 50.46), + point(79.03, 51.67), + point(80.50, 50.96), + point(81.69, 50.42), + point(82.75, 51.54), + point(83.75, 51.96), + point(83.75, 52.96), + point(86.13, 54.25), + point(87.28, 54.04), + point(87.59, 55.13), + point(86.31, 55.50), + point(84.22, 56.38), + point(82.06, 56.25), + point(81.47, 54.71), + point(80.84, 54.71), + point(81.41, 56.13), + point(82.19, 59.58), + point(84.03, 62.25), + point(86.44, 63.92), + point(88.00, 63.21), + point(87.16, 64.29), + point(85.78, 65.13), + point(82.50, 65.42), + point(81.47, 66.38), + point(80.25, 66.58), + point(79.47, 68.04), + point(77.84, 68.04), + point(75.91, 67.92), + point(75.16, 66.58), + point(74.72, 64.96), + point(73.16, 63.54), + point(71.97, 62.25), + point(72.56, 60.79), + point(71.16, 59.83), + point(72.19, 58.17), + ])); + + addEntities(createBunny([ + point(111.84, 63.92), + point(114.81, 64.79), + point(111.50, 67.54), + point(115.94, 69.58), + point(112.09, 75.42), + point(105.78, 79.75), + point(111.13, 80.96), + point(117.09, 80.29), + point(120.88, 87.38), + point(124.22, 91.50), + point(128.81, 88.71), + point(125.81, 85.21), + point(131.44, 91.17), + point(129.19, 95.38), + point(132.38, 96.21), + point(137.06, 96.50), + point(142.28, 95.75), + point(140.19, 89.46), + point(145.56, 87.33), + point(150.13, 89.83), + point(151.75, 84.92), + point(148.28, 82.29), + point(150.13, 80.04), + point(153.78, 79.17), + point(150.44, 70.71), + point(153.19, 67.96), + point(152.38, 65.08), + point(154.38, 62.63), + point(152.44, 58.17), + point(148.56, 57.00), + point(149.56, 53.58), + point(152.41, 51.21), + point(151.03, 47.79), + point(148.66, 45.25), + point(143.31, 46.46), + point(140.75, 49.58), + point(141.78, 54.04), + point(140.94, 56.08), + point(139.19, 57.33), + point(137.41, 55.13), + point(134.91, 55.88), + point(131.03, 52.08), + point(128.63, 48.67), + point(124.88, 49.25), + point(124.56, 44.46), + point(122.47, 45.83), + point(121.50, 42.83), + point(117.72, 45.63), + point(115.34, 46.13), + point(114.97, 43.25), + point(115.94, 40.38), + point(114.19, 37.83), + point(108.16, 37.75), + point(108.66, 40.25), + point(108.53, 41.63), + point(107.59, 42.67), + point(101.91, 44.00), + point(102.09, 46.67), + point(100.34, 46.08), + point(100.56, 48.25), + point(102.28, 51.83), + point(105.06, 52.92), + point(104.75, 55.75), + point(108.25, 55.67), + point(106.88, 57.92), + point(106.97, 62.38), + point(111.09, 60.42), + point(112.84, 62.63), + ])); + + addEntities(createBunny([ + point(85.00, 88.54), + point(81.91, 89.75), + point(77.06, 88.46), + point(75.75, 90.58), + point(78.06, 92.46), + point(76.88, 92.96), + point(78.59, 94.13), + point(76.72, 95.29), + point(79.94, 95.71), + point(82.34, 93.63), + point(85.81, 95.04), + point(88.88, 93.58), + point(92.66, 94.04), + point(95.06, 92.21), + point(98.00, 92.96), + point(98.25, 94.92), + point(98.13, 100.46), + point(96.94, 102.71), + point(96.09, 104.46), + point(98.00, 106.46), + point(100.41, 103.96), + point(102.25, 107.50), + point(103.97, 109.42), + point(105.66, 108.04), + point(107.19, 106.13), + point(107.69, 102.42), + point(108.78, 101.96), + point(109.88, 104.42), + point(108.53, 106.38), + point(111.22, 109.50), + point(114.34, 109.58), + point(114.75, 112.63), + point(114.28, 116.50), + point(117.38, 118.50), + point(117.88, 123.04), + point(119.38, 124.38), + point(119.25, 127.21), + point(117.72, 129.13), + point(114.41, 133.63), + point(115.28, 134.75), + point(115.03, 137.63), + point(117.81, 145.83), + point(122.03, 148.08), + point(120.56, 151.42), + point(119.28, 154.29), + point(121.28, 154.71), + point(123.44, 152.13), + point(123.34, 150.17), + point(126.38, 150.25), + point(125.91, 154.08), + point(124.91, 155.33), + point(131.81, 157.13), + point(139.19, 156.83), + point(144.88, 153.63), + point(147.00, 150.38), + point(145.63, 149.13), + point(150.34, 148.50), + point(150.78, 151.04), + point(149.28, 154.17), + point(145.00, 151.04), + point(150.50, 145.29), + point(150.56, 138.17), + point(153.09, 136.33), + point(156.03, 136.29), + point(154.78, 133.42), + point(151.63, 134.08), + point(148.56, 133.21), + point(146.16, 129.38), + point(146.34, 124.08), + point(148.03, 119.42), + point(145.09, 113.92), + point(142.81, 110.83), + point(140.09, 109.42), + point(135.94, 105.75), + point(138.91, 98.96), + point(141.97, 94.92), + point(143.66, 92.50), + point(141.00, 91.29), + point(144.03, 89.58), + point(141.38, 87.79), + point(138.75, 90.54), + point(134.84, 89.50), + point(131.41, 88.00), + point(131.38, 90.63), + point(128.03, 88.46), + point(125.25, 88.08), + point(125.50, 84.13), + point(122.72, 84.42), + point(119.22, 87.17), + point(113.50, 83.25), + point(110.56, 80.33), + point(105.22, 79.96), + point(102.34, 77.04), + point(100.69, 78.54), + point(98.09, 78.04), + point(99.97, 75.92), + point(95.53, 76.25), + point(94.31, 78.00), + point(91.00, 75.92), + point(86.34, 77.46), + point(87.31, 80.75), + point(86.19, 83.83), + point(89.47, 84.04), + point(86.28, 86.33), + ])); + + addEntities(createBunny([ + point(60.94, 119.21), + point(60.44, 116.92), + point(57.34, 117.92), + point(58.25, 114.08), + point(57.41, 111.25), + point(55.63, 109.79), + point(53.94, 104.42), + point(51.97, 105.04), + point(53.31, 101.04), + point(51.34, 96.96), + point(49.16, 96.54), + point(43.22, 97.79), + point(43.47, 99.42), + point(40.50, 99.17), + point(41.00, 96.54), + point(38.16, 98.25), + point(33.91, 98.88), + point(30.03, 101.04), + point(29.25, 102.17), + point(26.34, 102.71), + point(24.63, 102.96), + point(23.81, 106.83), + point(21.50, 108.38), + point(22.63, 110.17), + point(20.72, 111.79), + point(23.06, 113.33), + point(24.44, 117.58), + point(25.19, 120.79), + point(28.19, 121.92), + point(26.88, 123.79), + point(32.19, 126.29), + point(31.97, 128.25), + point(34.28, 130.54), + point(35.91, 130.00), + point(38.59, 129.88), + point(37.25, 125.92), + point(40.75, 123.96), + point(42.25, 128.00), + point(45.91, 129.33), + point(46.59, 132.08), + point(48.53, 129.29), + point(52.03, 126.50), + point(47.31, 122.00), + point(50.09, 118.42), + point(52.94, 119.54), + point(55.59, 119.38), + ])); + + addEntities(createBunny([ + point(65.28, 127.13), + point(62.63, 129.54), + point(67.25, 130.13), + point(64.53, 130.38), + point(62.81, 133.17), + point(67.31, 133.25), + point(69.09, 134.88), + point(64.97, 136.25), + point(67.03, 139.29), + point(64.94, 141.13), + point(64.66, 143.71), + point(66.75, 144.08), + point(67.56, 141.71), + point(69.50, 142.04), + point(68.16, 144.79), + point(66.25, 147.50), + point(64.25, 146.58), + point(62.41, 148.42), + point(59.22, 150.04), + point(59.88, 151.33), + point(58.94, 153.00), + point(57.66, 151.92), + point(55.47, 149.42), + point(55.91, 147.79), + point(53.06, 147.25), + point(50.38, 144.21), + point(47.00, 145.54), + point(44.06, 145.33), + point(42.84, 143.79), + point(43.78, 142.50), + point(42.38, 140.29), + point(44.84, 138.46), + point(45.84, 133.29), + point(48.72, 132.13), + point(50.16, 128.92), + point(52.63, 129.42), + point(54.78, 127.42), + point(56.31, 129.00), + point(56.22, 130.42), + point(59.56, 130.38), + ])); + + addEntities(createBunny([ + point(62.75, 128.29), + point(62.50, 130.54), + point(66.16, 128.46), + point(67.94, 129.42), + point(67.47, 133.38), + point(63.84, 132.25), + point(65.50, 130.67), + point(67.78, 136.50), + point(66.28, 139.96), + point(64.69, 141.96), + point(65.88, 146.08), + point(68.63, 143.75), + point(65.88, 143.08), + point(68.34, 141.58), + point(68.00, 140.38), + point(67.41, 144.13), + point(67.22, 139.92), + point(67.41, 134.38), + ])); + + addEntities(createBunny([ + point(66.56, 130.17), + point(67.41, 128.33), + point(66.25, 127.21), + point(64.41, 128.00), + point(62.97, 128.46), + point(63.53, 130.92), + point(62.16, 131.96), + point(64.66, 133.42), + point(67.34, 134.13), + point(68.25, 134.83), + point(64.47, 137.50), + point(65.09, 139.58), + point(64.59, 142.83), + point(66.56, 144.63), + point(67.75, 141.71), + point(69.63, 142.79), + point(68.34, 144.83), + point(63.63, 146.50), + point(62.03, 148.67), + point(58.63, 149.83), + point(58.78, 152.63), + point(60.06, 150.38), + point(62.19, 146.71), + point(65.66, 146.75), + point(69.34, 144.58), + point(71.59, 143.54), + point(72.19, 141.54), + point(68.09, 141.21), + point(66.75, 138.88), + point(67.09, 135.96), + point(63.63, 133.46), + ])); + + addEntities(createBunny([ + point(77.28, 149.21), + point(77.44, 150.83), + point(79.34, 151.38), + point(77.00, 153.88), + point(82.84, 153.00), + point(83.47, 155.21), + point(86.59, 154.17), + point(85.94, 151.04), + point(84.69, 149.54), + point(85.44, 147.92), + point(81.41, 149.38), + ])); + + addEntities(createBunny([ + point(77.59, 149.08), + point(78.03, 152.38), + point(76.63, 152.13), + point(76.81, 154.54), + point(79.34, 153.75), + point(81.22, 154.71), + point(83.19, 154.54), + point(84.56, 152.42), + point(81.09, 151.25), + point(81.94, 149.46), + point(80.09, 149.46), + point(84.00, 153.17), + point(85.91, 149.29), + point(86.34, 153.42), + ])); + + addEntities(createBunny([ + point(86.09, 148.13), + point(84.44, 149.13), + point(86.25, 152.13), + point(86.78, 150.29), + point(84.63, 151.46), + point(86.00, 153.79), + point(84.03, 154.88), + point(81.81, 152.25), + point(82.19, 150.88), + point(80.19, 149.42), + point(78.09, 149.42), + point(77.72, 152.25), + point(80.28, 152.54), + point(77.63, 154.33), + point(79.22, 155.21), + point(82.78, 153.58), + point(83.75, 150.38), + point(82.97, 149.04), + ])); + + addEntities(createBunny([ + point(86.09, 148.13), + point(84.44, 149.13), + point(86.25, 152.13), + point(86.78, 150.29), + point(84.63, 151.46), + point(86.00, 153.79), + point(84.03, 154.88), + point(81.81, 152.25), + point(82.19, 150.88), + point(80.19, 149.42), + point(78.09, 149.42), + point(77.72, 152.25), + point(80.28, 152.54), + point(77.63, 154.33), + point(79.22, 155.21), + point(82.78, 153.58), + point(83.75, 150.38), + point(82.97, 149.04), + point(9.09, 102.54), + point(6.84, 104.00), + point(8.75, 105.00), + point(6.94, 106.58), + point(8.59, 110.33), + point(7.56, 112.79), + point(6.00, 114.46), + point(4.63, 113.46), + point(2.34, 113.75), + point(1.19, 116.25), + point(2.50, 117.54), + point(5.00, 115.92), + point(6.16, 119.08), + point(7.44, 123.25), + point(6.53, 124.42), + point(7.13, 128.13), + point(6.00, 131.71), + point(7.56, 136.38), + point(10.88, 139.00), + point(10.91, 140.54), + point(12.69, 141.08), + point(14.06, 141.92), + point(16.09, 141.42), + point(14.59, 140.08), + point(14.78, 137.63), + point(12.34, 137.79), + point(10.38, 138.25), + point(7.97, 136.75), + point(6.69, 131.33), + point(8.56, 127.42), + point(7.25, 123.58), + point(7.00, 119.13), + point(4.34, 117.67), + point(3.53, 114.75), + point(4.75, 114.58), + point(6.34, 115.63), + point(7.19, 113.21), + point(7.81, 108.46), + point(9.22, 106.58), + point(11.47, 104.83), + point(11.63, 102.63), + point(10.34, 103.17), + ])); + + addEntities(createBunny([ + point(21.72, 63.50), + point(23.09, 65.46), + point(24.53, 63.54), + point(25.59, 65.13), + point(25.28, 66.71), + point(27.69, 67.92), + point(30.22, 67.58), + point(32.94, 67.79), + point(36.38, 68.92), + point(37.16, 67.83), + point(35.88, 65.79), + point(38.34, 65.25), + point(39.50, 67.38), + point(40.19, 64.96), + point(39.34, 62.88), + point(37.28, 64.04), + point(34.44, 62.67), + point(32.44, 61.17), + point(32.53, 59.63), + point(29.41, 58.88), + point(29.22, 61.79), + point(26.50, 61.83), + point(24.53, 61.58), + point(22.88, 61.50), + point(23.00, 63.38), + point(25.78, 62.88), + ])); + + addEntities(createBunny([ + point(21.38, 28.04), + point(19.81, 26.08), + point(21.22, 23.75), + point(23.19, 25.29), + point(25.09, 22.92), + point(25.81, 25.79), + point(26.56, 22.83), + point(28.31, 19.92), + point(29.41, 16.79), + point(31.00, 15.25), + point(32.41, 16.83), + point(32.72, 18.38), + point(34.63, 16.67), + point(33.53, 15.50), + point(38.81, 13.92), + point(41.16, 14.88), + point(43.09, 13.96), + point(46.19, 12.46), + point(45.22, 7.96), + point(46.31, 5.96), + point(45.34, 5.08), + point(47.00, 4.50), + point(47.41, 5.58), + point(55.22, 4.75), + point(56.09, 7.83), + point(57.91, 8.00), + point(60.94, 7.58), + point(62.13, 6.50), + point(63.09, 7.38), + point(64.34, 6.42), + point(65.09, 6.96), + point(67.06, 11.17), + point(65.78, 12.29), + point(67.66, 13.33), + point(70.09, 12.71), + point(71.13, 14.67), + point(70.22, 16.04), + point(73.09, 16.75), + point(74.78, 14.00), + point(74.47, 17.83), + point(73.19, 21.04), + point(74.38, 21.50), + point(77.09, 20.13), + point(77.72, 19.17), + point(79.22, 20.88), + point(80.19, 22.88), + point(81.84, 22.71), + point(82.47, 21.67), + point(82.50, 20.08), + point(81.47, 19.25), + point(81.00, 17.58), + point(82.47, 16.08), + point(80.63, 15.42), + point(78.59, 15.88), + point(78.50, 14.38), + point(79.78, 13.00), + point(81.81, 12.54), + point(83.19, 12.29), + point(84.22, 10.38), + point(87.00, 10.83), + point(87.91, 10.08), + point(89.41, 11.38), + point(90.38, 8.83), + point(91.91, 6.96), + point(97.44, 8.38), + point(99.56, 9.46), + point(98.50, 10.83), + point(97.28, 10.46), + point(97.72, 7.42), + point(95.28, 7.83), + point(95.06, 11.75), + point(94.84, 16.21), + point(96.50, 17.17), + point(98.66, 17.71), + point(99.00, 19.54), + point(97.06, 20.92), + point(95.50, 20.63), + point(90.22, 21.67), + point(89.28, 24.21), + point(89.22, 26.50), + point(88.66, 26.50), + point(88.38, 28.63), + point(85.84, 28.46), + point(85.31, 30.67), + point(83.59, 31.71), + point(81.63, 31.25), + point(79.53, 31.88), + point(73.91, 31.63), + point(71.19, 31.08), + point(69.97, 32.08), + point(66.94, 31.63), + point(65.47, 34.58), + point(62.72, 36.08), + point(60.00, 34.92), + point(57.09, 32.92), + point(52.00, 32.50), + point(52.59, 36.88), + point(50.75, 39.54), + point(48.63, 39.33), + point(48.72, 41.38), + point(47.03, 40.46), + point(43.28, 41.63), + point(40.88, 44.96), + point(35.88, 44.00), + point(32.72, 46.67), + point(32.13, 50.00), + point(33.47, 52.88), + point(32.91, 54.71), + point(30.38, 55.67), + point(28.06, 54.71), + point(25.66, 53.38), + point(24.03, 54.21), + point(22.78, 53.50), + point(21.19, 56.54), + point(18.25, 56.83), + point(15.22, 57.00), + point(13.69, 55.83), + point(14.88, 55.08), + point(16.47, 55.33), + point(18.31, 53.42), + point(19.25, 51.54), + point(19.38, 49.29), + point(21.84, 44.83), + point(22.34, 43.04), + point(24.00, 43.04), + point(25.31, 44.71), + point(25.03, 45.83), + point(26.38, 46.88), + point(27.28, 45.67), + point(26.94, 43.83), + point(26.88, 41.58), + point(28.31, 39.96), + point(27.81, 37.63), + point(25.72, 35.71), + point(24.16, 35.54), + point(22.50, 30.58), + point(23.81, 29.88), + point(24.13, 27.54), + ])); + + addEntities(createBunny([ + point(133.75, 6.38), + point(131.81, 5.29), + point(128.88, 5.42), + point(128.69, 7.00), + point(131.72, 8.96), + point(133.38, 8.13), + point(133.09, 10.63), + point(129.16, 9.33), + point(128.84, 8.25), + point(125.84, 9.96), + point(124.91, 8.96), + point(122.41, 10.83), + point(119.44, 10.46), + point(117.00, 10.25), + point(115.16, 10.67), + point(114.88, 11.58), + point(113.97, 13.88), + point(114.66, 15.33), + point(114.34, 16.33), + point(115.09, 17.75), + point(115.41, 19.83), + point(115.47, 24.79), + point(116.84, 25.92), + point(117.34, 28.17), + point(120.56, 29.50), + point(122.09, 30.50), + point(121.28, 32.08), + point(123.25, 29.92), + point(126.31, 29.46), + point(128.69, 33.00), + point(131.22, 32.67), + point(132.50, 31.25), + point(134.47, 32.13), + point(135.31, 29.88), + point(136.41, 29.75), + point(136.25, 28.25), + point(134.59, 27.75), + point(135.84, 25.38), + point(136.22, 23.29), + point(137.22, 23.25), + point(137.78, 20.25), + point(138.69, 19.29), + point(137.34, 17.00), + point(137.31, 14.21), + point(134.41, 13.25), + point(131.91, 11.83), + point(132.34, 9.88), + ])); + + if (true) { + const toLake = { icon: SignIcon.Lake, name: 'Lake' }; + const toHarbor = { icon: SignIcon.Boat, name: 'Harbor' }; + const toSpawn = { icon: SignIcon.Spawn, name: 'Spawn' }; + const toTownCenter = { icon: SignIcon.TownCenter, name: 'Town Center' }; + const toPineForest = { icon: SignIcon.PineForest, name: 'Pine Forest' }; + const toPartyIsland = { icon: SignIcon.Boat, name: 'Party Island' }; + const toGiftPile = { icon: SignIcon.GiftPile, name: 'Gift Pile' }; + const toMountains = { icon: SignIcon.Mountains, name: 'Mountains' }; + const toForest = { icon: SignIcon.Forest, name: 'Forest' }; + const toPumpkinFarm = { icon: SignIcon.Pumpkins, name: 'Pumpkin Farm' }; + const toFlowerField = { icon: SignIcon.Fields, name: 'Flower Field' }; + const toBarrelStorage = { icon: SignIcon.Barrels, name: 'Barrel Storage' }; + const toMines = { icon: SignIcon.Mines, name: 'Mines' }; + const toBridge = { icon: SignIcon.Bridge, name: 'Bridge' }; + const toCarrots = { icon: SignIcon.Carrots, name: 'Carrot farm' }; + + addEntities(createDirectionSign(77, 72, { + w: [toSpawn, toGiftPile, toHarbor, toPineForest, undefined], + e: [toLake, toCarrots, toMines, toBarrelStorage], + s: [toForest, toPumpkinFarm], + })); + + addEntities(createDirectionSign(54.33, 70.58, { + r: 1, + n: [toSpawn, toMines], + w: [toPineForest, toHarbor, toMountains], + e: [toTownCenter, toLake], + })); + + addEntities(createDirectionSign(36.00, 75.98, { + w: [toHarbor, toMountains], + e: [toSpawn, toTownCenter, toMines, toLake], + s: [toPineForest], + })); + + addEntities(createDirectionSign(19.34, 71.00, { + n: [toMountains], + w: [undefined, toPartyIsland], + e: [toSpawn, toTownCenter, toPineForest], + })); + + addEntities(createDirectionSign(24.86, 9.98, { + r: 1, + e: [toBridge, toMines, toLake], + s: [toHarbor, toPineForest], + })); + + addEntities(createDirectionSign(58.66, 54.88, { + w: [toGiftPile], + })); + + addEntities(createDirectionSign(54.38, 39.29, { + r: 1, + n: [toSpawn], + e: [toMines, toBridge, toLake], + s: [toTownCenter, toHarbor], + })); + + addEntities(createDirectionSign(99.00, 40.15, { + n: [toMountains, toBarrelStorage], + w: [toSpawn, toMines, toHarbor], + e: [toBridge, toCarrots], + s: [toLake, toTownCenter, toForest], + })); + + addEntities(createDirectionSign(122.75, 37.00, { + r: 1, + w: [toTownCenter, toSpawn, toMines], + n: [toCarrots], + })); + + addEntities(createDirectionSign(103.75, 70.10, { + r: 1, + n: [toBridge, toMountains, toCarrots], + w: [toTownCenter, toHarbor], + e: [toLake, toForest], + })); + + addEntities(createDirectionSign(128.16, 102.13, { + w: [toSpawn, toTownCenter, toHarbor], + e: [toLake], + s: [toFlowerField], + })); + + addEntities(createDirectionSign(129.53, 140.75, { + w: [toPumpkinFarm, toPineForest, toHarbor], + e: [toFlowerField], + n: [toForest, toLake, toTownCenter], + })); + + addEntities(createDirectionSign(70.98, 135.85, { + r: 1, + n: [toSpawn, toTownCenter, toHarbor, toMines], + w: [undefined, toPineForest], + e: [toForest, undefined, toFlowerField], + })); + + addEntities(createDirectionSign(54.91, 7.92, { + w: [toHarbor, toPineForest], + e: [toBridge, toMines, toLake], + })); + + addEntities(createDirectionSign(90.17, 5.35, { + w: [toHarbor, toPineForest], + s: [toBridge, toMines, toLake], + })); + + addEntities(createDirectionSign(78.41, 96.46, { + w: [toTownCenter, toHarbor], + e: [toForest, toLake], + s: [toPumpkinFarm, toPineForest], + })); + + addEntities(createDirectionSign(95.84, 25.33, { + e: [toBarrelStorage], + })); + + addEntities(createDirectionSign(77.15, 39.20, { + n: [toMines], + w: [undefined, toSpawn], + e: [toBridge, toCarrots, toLake], + })); + + addEntities(createDirectionSign(106.80, 95.46, { + r: 1, + w: [toTownCenter, toPumpkinFarm, toHarbor], + n: [toLake, toMines, toCarrots], + e: [undefined, toFlowerField], + })); + + addEntities(createDirectionSign(17.67, 138.90, { + n: [toHarbor, toMountains, toTownCenter], + })); + } + + const apples = [entities.apple, entities.apple2, entities.apple, entities.apple2, entities.appleGreen, entities.appleGreen2]; + const otherFruits = [entities.orange, entities.orange2, entities.pear, entities.banana]; + + const ctrls = map.controllers; + + ctrls.push(new ctrl.UpdateController(map)); + ctrls.push(new ctrl.TorchController(world, map)); + ctrls.push(new ctrl.CloudController(world, map, 5)); + ctrls.push(new ctrl.CollectableController(world, map, apples, 8, pickEntity, checkNotCollecting)); + ctrls.push(new ctrl.CollectableController(world, map, otherFruits, 3, pickEntity, checkNotCollecting)); + + ctrls.push(new ctrl.CollectableController( + world, map, [entities.gift1, entities.gift2], 50, pickGift, undefined, undefined, undefined, + () => world.holiday === Holiday.Christmas)); + + ctrls.push(new ctrl.CollectableController( + world, map, [entities.candy], 60, pickCandy, checkLantern, undefined, undefined, + () => world.holiday === Holiday.Halloween)); + + ctrls.push(new ctrl.CollectableController( + world, map, entities.eggs, 200, pickEgg, checkBasket, 5, undefined, + () => world.holiday === Holiday.Easter)); + + ctrls.push(new ctrl.CollectableController( + world, map, [entities.fourLeafClover], 2, pickClover, checkNotCollecting, 1, positionClover, + () => world.season === Season.Spring || world.season === Season.Summer)); + + ctrls.push(new ctrl.PlantController(world, map, { + area: rect(116.2, 14.2, 7.8, 9.6), + count: 100, + stages: [ + [entities.carrot4], + [entities.carrot3], + [entities.carrot2, entities.carrot2b], + [entities.carrot1, entities.carrot1b], + ], + growOnlyOn: TileType.Dirt, + onPick: (_, client) => holdItem(client.pony, entities.carrotHeld.type), + isActive: () => world.season !== Season.Winter, + })); + + if (!DEVELOPMENT) { + ctrls.push(new ctrl.FlyingCritterController( + world, map, entities.bat, 2, 20, () => isNightTime(world.time))); + ctrls.push(new ctrl.FlyingCritterController( + world, map, entities.firefly, 1, 40, () => world.season !== Season.Winter && isNightTime(world.time))); + ctrls.push(new ctrl.FlyingCritterController( + world, map, entities.butterfly, 1.5, 40, () => world.season !== Season.Winter && isDayTime(world.time))); + } + + if (BETA) { + ctrls.push(new ctrl.WallController(world, map, entities.woodenWalls)); + } + + return map; +} diff --git a/src/ts/server/maps/paletteMap.ts b/src/ts/server/maps/paletteMap.ts new file mode 100644 index 0000000..4e056a3 --- /dev/null +++ b/src/ts/server/maps/paletteMap.ts @@ -0,0 +1,44 @@ +import * as entities from '../../common/entities'; +import { rect } from '../../common/rect'; +import { TileType, MapType } from '../../common/interfaces'; +import { createServerMap } from '../serverMap'; +import { World, goToMap } from '../world'; +import { allEntities } from '../api/account'; +import { createSign } from '../controllerUtils'; +import { ServerEntity } from '../serverInterfaces'; +import { setEntityName } from '../entityUtils'; + +export function createPaletteMap(world: World) { + const map = createServerMap('palette', MapType.None, 10, 10, TileType.Grass); + + map.spawnArea = rect(map.width / 2, map.height / 2, 0, 0); + + function add(entity: ServerEntity) { + world.addEntity(entity, map); + } + + add(createSign(map.width / 2, map.height / 2, 'Go back', (_, client) => goToMap(world, client, '', 'center'))); + + const pad = 5; + let x = pad; + let y = pad; + + for (const name of allEntities) { + const entityOrEntities = (entities as any)[name](x, y); + const ents = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities]; + + for (const entity of ents) { + add(entity); + setEntityName(entity, name); + } + + x += 3; + + if (x > (map.width - pad)) { + x = pad; + y += 3; + } + } + + return map; +} diff --git a/src/ts/server/move.ts b/src/ts/server/move.ts new file mode 100644 index 0000000..7f9bef3 --- /dev/null +++ b/src/ts/server/move.ts @@ -0,0 +1,219 @@ +import * as moment from 'moment'; +import chalk from 'chalk'; +import { IClient } from './serverInterfaces'; +import { decodeMovement, dirToVector, flagsToSpeed, isMovingRight } from '../common/movementUtils'; +import { setPonyState, isSittingState, isLyingState } from '../common/entityUtils'; +import { setupCamera } from '../common/camera'; +import { logger } from './logger'; +import { hasFlag, setFlag } from '../common/utils'; +import { EntityState } from '../common/interfaces'; +import { kickClient, setEntityExpression } from './playerUtils'; +import { CounterService } from './services/counter'; +import { roundPositionX, roundPositionY } from '../common/positionUtils'; +import { tileWidth, tileHeight, SECOND } from '../common/constants'; +import { GameServerSettings } from '../common/adminInterfaces'; +import { updateEntity, fixPosition } from './entityUtils'; +import { isStaticCollision, isOutsideMap } from '../common/collision'; + +const teleportReportLimit = 10; +const maxLagLimitSeconds = 15; +const maxLagLimit = maxLagLimitSeconds * SECOND; + +export type Move = ReturnType; + +export const createMove = + (teleportCounter: CounterService) => + (client: IClient, now: number, a: number, b: number, c: number, d: number, e: number, settings: GameServerSettings) => { + if (client.loading || client.fixingPosition || client.isSwitchingMap) + return; + + const connectionDuration = (now - client.connectedTime) >>> 0; + const pony = client.pony; + const { x, y, dir, flags, time, camera } = decodeMovement(a, b, c, d, e); + const v = dirToVector(dir); + const speed = flagsToSpeed(flags); + + if (checkOutsideMap(client, x, y)) + return; + + setupCamera(client.camera, camera.x, camera.y, camera.w, camera.h, client.map); + + if (checkLagging(client, time, connectionDuration, settings)) + return; + + if (checkTeleporting(client, x, y, time, settings, teleportCounter)) + return; + + if (!isStaticCollision(pony, client.map, true)) { + client.safeX = pony.x; + client.safeY = pony.y; + } + + pony.x = x; + pony.y = y; + + if (isStaticCollision(pony, client.map)) { + pony.x = client.safeX; + pony.y = client.safeY; + + if (!isStaticCollision(pony, client.map)) { + if (settings.logFixingPosition) { + client.reporter.systemLog(`Fixed colliding (${x} ${y}) -> (${pony.x} ${pony.y})`); + } + + DEVELOPMENT && !TESTS && logger.warn(`Fixing position due to collision`); + fixPosition(pony, client.map, client.safeX, client.safeY, false); + } else { + pony.x = x; + pony.y = y; + } + } + + pony.vx = v.x * speed; + pony.vy = v.y * speed; + + let ponyState = pony.state || 0; + + const facingRight = hasFlag(ponyState, EntityState.FacingRight); + const right = isMovingRight(pony.vx, facingRight); + + if (facingRight !== right) { + ponyState = setFlag(ponyState, EntityState.FacingRight, right); + ponyState = setFlag(ponyState, EntityState.HeadTurned, false); + } + + if ((pony.vx || pony.vy) && (isSittingState(ponyState) || isLyingState(ponyState))) { + ponyState = setPonyState(ponyState, EntityState.PonyStanding); + } + + pony.state = ponyState; + + updateEntity(pony, false); + + if (pony.exprCancellable) { + setEntityExpression(pony, undefined); + } + + pony.timestamp = now / 1000; + + client.lastX = pony.x; + client.lastY = pony.y; + client.lastTime = time; + client.lastVX = pony.vx; + client.lastVY = pony.vy; + }; + +function checkOutsideMap(client: IClient, x: number, y: number): boolean { + if (isOutsideMap(x, y, client.map)) { + const message = `map: [${client.map.id || 'main'}] coords: [${x.toFixed(2)}, ${y.toFixed(2)}]`; + + if (!client.shadowed) { + client.reporter.warn(`Outside map`, message); + } + + kickClient(client, `outside ${message}`); + return true; + } + + return false; +} + +function checkLagging(client: IClient, time: number, connectionTime: number, settings: GameServerSettings): boolean { + const dt = time - connectionTime; + const lagging = (dt > maxLagLimit) || (dt < -maxLagLimit); + + if (lagging) { + if (settings.logLagging) { + // logger.warn(`Time delta > ${maxLagLimitSeconds}s (${dt}) account: ${client.account.name} [${client.accountId}]`); + client.reporter.systemLog(`Time delta > ${maxLagLimitSeconds}s (${dt})`); + client.logDisconnect = true; + } + + if (settings.kickLagging) { + client.reporter.systemLog(`Lagging (dt: ${dt} time: ${time} connectionTime: ${connectionTime})`); + kickClient(client, 'lagging'); + return true; + } + } + + return false; +} + +function checkTeleporting( + client: IClient, x: number, y: number, time: number, settings: GameServerSettings, counter: CounterService +): boolean { + if (!client.lastTime) + return false; + + const pony = client.pony; + const borderX = 0.5; + const borderY = 0.5; + const delta = ((time - client.lastTime) / 1000) * 1; + + const afterX = roundPositionX(client.lastX + client.lastVX * delta); + const afterY = roundPositionY(client.lastY + client.lastVY * delta); + + const afterMinX = client.lastVX === 0 ? afterX - Math.abs(client.lastVY) : afterX; + const afterMaxX = client.lastVX === 0 ? afterX + Math.abs(client.lastVY) : afterX; + const afterMinY = client.lastVY === 0 ? afterY - Math.abs(client.lastVX) : afterY; + const afterMaxY = client.lastVY === 0 ? afterY + Math.abs(client.lastVX) : afterY; + + const minX = Math.floor((Math.min(client.lastX, afterMinX) - borderX) * tileWidth) / tileWidth; + const maxX = Math.ceil((Math.max(client.lastX, afterMaxX) + borderX) * tileWidth) / tileWidth; + const minY = Math.floor((Math.min(client.lastY, afterMinY) - borderY) * tileHeight) / tileHeight; + const maxY = Math.ceil((Math.max(client.lastY, afterMaxY) + borderY) * tileHeight) / tileHeight; + + const outX = x < minX || x > maxX; + const outY = y < minY || y > maxY; + + if (outX || outY) { + if (settings.logTeleporting) { + const colX = outX ? chalk.red : chalk.reset; + const colY = outY ? chalk.red : chalk.reset; + + logger.log( + `[${chalk.gray(moment().format('MMM DD HH:mm:ss'))}] [${chalk.yellow('teleport')}] ` + + `[${chalk.gray(client.accountId)}] (${client.account.name})\n` + + `\tdx: ${client.lastX.toFixed(5)} -> ${colX(x.toFixed(5))} [${minX.toFixed(5)}-${maxX.toFixed(5)}]\n` + + `\tdy: ${client.lastY.toFixed(5)} -> ${colY(y.toFixed(5))} [${minY.toFixed(5)}-${maxY.toFixed(5)}]\n` + + `\tdt: ${delta.toFixed(5)}`); + } + + if (settings.reportTeleporting) { + const { count } = counter.add(client.accountId); + + if (count > teleportReportLimit) { + counter.remove(client.accountId); + client.reporter.warn(`Teleporting (x${teleportReportLimit})`); + } + } + + if (settings.kickTeleporting) { + kickClient(client, 'teleporting'); + return true; + } + + if (settings.fixTeleporting) { + pony.vx = 0; + pony.vy = 0; + client.reporter.systemLog(`Fixed teleporting (${x} ${y}) -> (${pony.x} ${pony.y})`); + fixPosition(client.pony, client.map, pony.x, pony.y, false); + return true; + } + } + + const dx = Math.abs(x - pony.x); + const dy = Math.abs(y - pony.y); + + if (dx > 8 || dy > 8) { + if (settings.fixTeleporting) { + pony.vx = 0; + pony.vy = 0; + client.reporter.systemLog(`Fixed teleporting (too far) (${x} ${y}) -> (${pony.x} ${pony.y})`); + fixPosition(client.pony, client.map, pony.x, pony.y, false); + return true; + } + } + + return false; +} diff --git a/src/ts/server/oauth.ts b/src/ts/server/oauth.ts new file mode 100644 index 0000000..abadcb9 --- /dev/null +++ b/src/ts/server/oauth.ts @@ -0,0 +1,150 @@ +/// + +import { Request } from 'express'; +import { isString } from 'lodash'; +import { Strategy as GoogleStrategy } from '@passport-next/passport-google-oauth2'; +import { Strategy as TwitterStrategy } from 'passport-twitter'; +import { Strategy as FacebookStrategy } from '@passport-next/passport-facebook'; +import { Strategy as GithubStrategy } from 'passport-github2'; +import { Strategy as VKontakteStrategy } from 'passport-vkontakte'; +import { Strategy as PatreonStrategy } from 'passport-patreon'; +import { Profile } from '../common/interfaces'; +import { PATREON_COLOR } from '../common/colors'; +import { colorToCSS } from '../common/color'; +import { config } from './config'; +import { IAccount } from './db'; + +export type OAuthProfileName = string | { familyName: string; givenName: string; }; + +export interface OAuthProfile { + id?: string; + name?: OAuthProfileName; + username?: string; + displayName?: string; + emails?: { value: string; }[]; + provider: string; + gender?: string; + profileUrl?: string; + _raw: string; + _json: any; +} + +export interface Strategy { + new( + options: any, + callback: ( + req: Request, + accessToken: string, + refreshToken: string, + profile: OAuthProfile, + callback: (err: Error | null, user: IAccount | null) => void) => void): any; +} + +export interface OAuthProviderInfo { + id: string; + name: string; + color: string; + strategy: Strategy; + auth?: any; + connectOnly?: boolean; + additionalOptions?: any; +} + +const providerList: OAuthProviderInfo[] = [ + { + id: 'google', + name: 'Google', + color: '#DC4A3D', + strategy: GoogleStrategy, + }, + { + id: 'twitter', + name: 'Twitter', + color: '#55ACEE', + strategy: TwitterStrategy, + }, + { + id: 'facebook', + name: 'Facebook', + color: '#3765A3', + strategy: FacebookStrategy, + }, + { + id: 'github', + name: 'GitHub', + color: '#800080', + strategy: GithubStrategy, + }, + { + id: 'vkontakte', + name: 'VKontakte', + color: '#4C75A3', + strategy: VKontakteStrategy, + }, + { + id: 'patreon', + name: 'Patreon', + color: colorToCSS(PATREON_COLOR), + strategy: PatreonStrategy, + }, +]; + +providerList.forEach(p => p.auth = config.oauth[p.id]); +providerList.filter(p => p.auth && p.auth.connectOnly).forEach(p => p.connectOnly = true); + +export const providers = providerList.filter(p => !!p.auth); + +export function getProfileUrl(profile: OAuthProfile): string | undefined { + if (profile.provider === 'twitter') { + return `https://twitter.com/${profile.username}`; + } else if (profile.provider === 'tumblr') { + return `http://${profile.username}.tumblr.com/`; + } else if (profile.provider === 'facebook') { + return `http://www.facebook.com/${profile.id}`; + } else if (profile._json.attributes && profile._json.attributes.url) { // patreon + return profile._json.attributes.url; + } else { + return profile.profileUrl || profile._json.url; + } +} + +export function getProfileEmails(profile: OAuthProfile): string[] { + if (profile.emails && profile.emails.length) { + return profile.emails.map(e => e.value); + } else if (profile._json && profile._json.attributes && profile._json.attributes.email) { // patreon + return [profile._json.attributes.email]; + } else { + return []; + } +} + +export function getProfileUsername(profile: OAuthProfile): string | undefined { + return profile.username || profile.displayName || getProfileNameInternal(profile.name); +} + +export function getProfileName(profile: OAuthProfile): string | undefined { + return profile.displayName || profile.username || getProfileNameInternal(profile.name); +} + +function getProfileNameInternal(name: OAuthProfileName | undefined): string | undefined { + if (!name || isString(name)) { + return name; + } else { + return `${name.givenName} ${name.familyName}`.trim(); + } +} + +export function getProfile(provider: string, profile: OAuthProfile): Profile { + const emails = getProfileEmails(profile).map(e => e.toLowerCase()); + + return { + id: profile.id || profile.username || '', + provider: profile.provider || provider, + username: getProfileUsername(profile) || emails[0], + name: getProfileName(profile) || emails[0], + emails, + url: getProfileUrl(profile), + createdAt: profile._json && profile._json.created_at && new Date(profile._json.created_at), + suspended: profile._json && profile._json.suspended, + }; +} diff --git a/src/ts/server/originUtils.ts b/src/ts/server/originUtils.ts new file mode 100644 index 0000000..d566315 --- /dev/null +++ b/src/ts/server/originUtils.ts @@ -0,0 +1,45 @@ +import { Request } from 'express'; +import { IOriginInfo, IAccount, Account } from './db'; +import { config } from './config'; +import { logger } from './logger'; +import { OriginInfoBase } from '../common/adminInterfaces'; + +const get_ip = require('ipware')().get_ip; + +export function getIP(req: { headers: any; }) { + return req.headers['cf-connecting-ip'] || (get_ip(req) ? get_ip(req).clientIp : null); +} + +export function getOriginFromHTTP(req: { headers: any; }): IOriginInfo { + const ip = getIP(req) || '0.0.0.0'; + const ipcountry = (ip === '127.0.0.1' || ip === '::ffff:127.0.0.1' || ip === '::1') ? 'LOCAL' : ''; + const country = ipcountry || req.headers['cf-ipcountry'] || '??'; + return { ip, country, last: new Date() }; +} + +export function getOrigin(req: Request): IOriginInfo { + const origin = getOriginFromHTTP(req); + + if (origin.country === '??' && config.proxy) { + logger.warn('Invalid IP', JSON.stringify(req.ips)); + //create(null, null, null).danger('Invalid IP', JSON.stringify(req.ips)); + } + + return origin; +} + +export async function addOrigin(account: IAccount, origin: IOriginInfo) { + try { + const _id = account._id; + const existingOrigin = account.origins && account.origins + .find(o => o.ip === origin.ip) as (OriginInfoBase & { _id: any }) | undefined; + + if (existingOrigin) { + await Account.updateOne({ _id, 'origins._id': existingOrigin._id }, { $set: { 'origins.$.last': new Date() } }).exec(); + } else { + await Account.updateOne({ _id }, { $push: { origins: origin } }).exec(); + } + } catch (e) { + logger.error('Failed to add origin', e); + } +} diff --git a/src/ts/server/paths.ts b/src/ts/server/paths.ts new file mode 100644 index 0000000..3655112 --- /dev/null +++ b/src/ts/server/paths.ts @@ -0,0 +1,8 @@ +import * as path from 'path'; + +export const root = path.join(__dirname, '..', '..', '..'); +export const store = path.join(root, 'store'); + +export function pathTo(...parts: string[]) { + return path.join(root, ...parts); +} diff --git a/src/ts/server/patreon.ts b/src/ts/server/patreon.ts new file mode 100644 index 0000000..cdbec4a --- /dev/null +++ b/src/ts/server/patreon.ts @@ -0,0 +1,237 @@ +import * as Bluebird from 'bluebird'; +import { toPairs, groupBy, max } from 'lodash'; +import { patreon, PatronData } from 'patreon'; +import { IAuth, IAccount, UpdateAccounts, UpdateAccount, QueryAuths, QueryAccounts, UpdateAuth } from './db'; +import { PatreonFlags, PatreonPledge, PatreonData, PatreonReward } from '../common/adminInterfaces'; +import { Dict } from '../common/interfaces'; +import { DAY, SECOND, MONTH, rewardLevel1, rewardLevel2, rewardLevel3 } from '../common/constants'; +import { LogMessage, LogAccountMessage } from './serverInterfaces'; +import { delay, fromNow } from '../common/utils'; +import { logPatreon } from './logger'; + +export const declinedDayLimit = 14; +export const declinedTimeLimit = declinedDayLimit * DAY; +export const supporterLogLimit = 10; + +export const SUPPORTER_REWARD_IDS: Dict = { + [rewardLevel1]: PatreonFlags.Supporter1, + [rewardLevel2]: PatreonFlags.Supporter2, + [rewardLevel3]: PatreonFlags.Supporter3, +}; + +export type RemoveOldSupporters = ReturnType; +export type UpdateSupporters = ReturnType; +export type AddTotalPledged = ReturnType; + +let lastPatreonData: PatreonData | undefined = undefined; + +export function getLastPatreonData() { + return lastPatreonData; +} + +/* istanbul ignore next */ +export function createPatreonClient(accessToken: string): (path: string) => Promise { + const timeoutLimit = 10 * SECOND; + const client = patreon(accessToken); + client.setStore({ sync() { } }); + + return (path: string) => Promise.race([ + delay(timeoutLimit).then(() => { throw new Error('Patreon request timed out'); }), + client(path), + ]); +} + +export async function fetchPatreonData(client: (path: string) => Promise, log: LogMessage): Promise { + const campaignData = await client('/current_user/campaigns'); + const rewards = campaignData.rawJson.included + .filter(x => x.type === 'reward') + .map(x => ({ + id: x.id, + title: x.attributes.title || '', + description: x.attributes.description || '', + })); + + const campaignId = campaignData.rawJson.data[0].id; + const pledges: PatreonPledge[] = []; + const queryParams = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since'; + const query = 'page%5Bcount%5D=100&sort=created'; + let url = `/campaigns/${campaignId}/pledges?${query}`; + let pages = 0; + + do { + const pledgeData = await client(`${url}${queryParams}`); + const pledgeItems = pledgeData.rawJson.data + .filter(x => x.relationships.patron.data && x.relationships.reward.data) + .map(x => ({ + user: x.relationships.patron.data.id, + reward: x.relationships.reward.data.id, + total: x.attributes.total_historical_amount_cents || 0, + declinedSince: x.attributes.declined_since || undefined, + })); + + pledges.push(...pledgeItems); + url = (pledgeData.rawJson.links.next || '').replace('https://www.patreon.com/api/oauth2/api', ''); + pages++; + + if (pages > 100) { + throw new Error('Exceeded 100 pages of patreon data'); + } + } while (url); + + log(`fetched patreon data (pages: ${pages}, pledges: ${pledges.length}, rewards: ${rewards.length})`); + + return lastPatreonData = { pledges, rewards }; +} + +export const createUpdatePatreonInfo = + ( + queryAuths: QueryAuths, queryAccounts: QueryAccounts, removeOldSupporters: RemoveOldSupporters, + updateSupporters: UpdateSupporters, updateTotalPledged: AddTotalPledged + ) => + async ({ pledges }: PatreonData, now: Date) => { + const ids = pledges.map(p => p.user); + const query = { + provider: 'patreon', + openId: { $in: ids }, + account: { $exists: true }, + banned: { $ne: true }, + disabled: { $ne: true }, + }; + + const patreonAuths = await queryAuths(query, '_id account openId pledged'); + const accountsWithPatreon = await queryAccounts({ patreon: { $exists: true, $ne: 0 } }, '_id patreon supporterDeclinedSince'); + // removes support from accounts without any non-banned patreon auth + await removeOldSupporters(patreonAuths, accountsWithPatreon); + await updateSupporters(patreonAuths, accountsWithPatreon, pledges, now); + await updateTotalPledged(patreonAuths, pledges); + }; + +export const createRemoveOldSupporters = + (updateAccounts: UpdateAccounts, log: LogAccountMessage) => + async (auths: IAuth[], accounts: IAccount[]) => { + const clear = accounts + .filter(account => auths.every(auth => !auth.account || !account._id.equals(auth.account))) + .map(account => account._id); + + clear.forEach(id => log(`${id}`, `removed supporter`)); + + await updateAccounts({ _id: { $in: clear } }, { + $unset: { patreon: 1, supporterDeclinedSince: 1 }, + $push: { + supporterLog: { + $each: [{ date: new Date(), message: 'removed supporter' }], + $slice: -supporterLogLimit, + }, + }, + }); + + await updateAccounts( + { supporterDeclinedSince: { $exists: true, $lt: fromNow(-2 * MONTH) } }, + { $unset: { supporterDeclinedSince: 1 } }); + }; + +export const createUpdateSupporters = + (updateAccount: UpdateAccount, log: LogAccountMessage) => + async (auths: IAuth[], accountsWithPatreon: IAccount[], pledges: PatreonPledge[], now: Date) => { + const start = Date.now(); + const pledgesMap = new Map(); + const accountsWithPatreonMap = new Map(); + + for (const pledge of pledges) { + pledgesMap.set(pledge.user, pledge); + } + + for (const account of accountsWithPatreon) { + accountsWithPatreonMap.set(account._id.toString(), account); + } + + const setup = auths + .filter(auth => auth.account) + .map(auth => { + const accountId = auth.account!.toString(); + const pledge = auth.openId && pledgesMap.get(auth.openId); + const pledgeFlags = pledge && SUPPORTER_REWARD_IDS[pledge.reward] || PatreonFlags.None; + const declinedSince = (pledge && pledge.declinedSince) ? new Date(pledge.declinedSince) : undefined; + const account = accountsWithPatreonMap.get(accountId!); + const declined = isDeclined(declinedSince, now); + const patreon = declined ? PatreonFlags.None : pledgeFlags; + const current = account && account.patreon || 0; + const declinedChanged = !!account && !datesEqual(account.supporterDeclinedSince, declinedSince); + const hadPatreon = !!account; + + return { + account: accountId, patreon, declinedSince, declinedChanged, declined, current, hadPatreon + }; + }); + + const grouped = toPairs(groupBy(setup, x => x.account)) + .map(([account, items]) => { + const current = max(items.map(i => i.current))!; + const patreon = max(items.map(i => i.patreon))!; + + return { + account, + changed: items.some(i => !i.hadPatreon) || current !== patreon, + declinedChanged: items.some(i => i.declinedChanged), + patreon, + declinedSince: items.map(i => i.declinedSince).find(x => !!x), + declined: items.some(i => i.declined), + hadPatreon: items.some(i => i.hadPatreon), + }; + }) + .filter(({ changed, declinedChanged }) => changed || declinedChanged); + + grouped + .filter(g => g.changed) + .map(g => ({ account: g.account, message: supporterMessage(g.patreon, g.declined, g.hadPatreon) })) + .filter(({ message }) => !!message) + .forEach(({ account, message }) => log(`${account}`, message!)); + + logPatreon(`update supporters (${Date.now() - start}ms) ` + + `[auths: ${auths.length}, grouped: ${grouped.length}, pledges: ${pledges.length}, ` + + `accountsWithPatreon: ${accountsWithPatreon.length}]`); + + await Bluebird.map(grouped, ({ account, patreon, declinedSince, changed, declined, hadPatreon }) => { + const message = changed ? supporterMessage(patreon, declined, hadPatreon) : undefined; + + return updateAccount(account, { + supporterDeclinedSince: declinedSince, + ...(changed ? { patreon } : {}), + ...(message ? { + $push: { + supporterLog: { + $each: [{ date: new Date(), message }], + $slice: -supporterLogLimit, + }, + } + } : {}), + }); + }, { concurrency: 4 }); + }; + +function isDeclined(declinedSince: Date | undefined, now: Date): boolean { + return !!declinedSince && ( + now.getDate() > declinedDayLimit || + (now.getTime() - declinedSince.getTime()) > declinedTimeLimit); +} + +function supporterMessage(patreon: PatreonFlags, declined: boolean, hadPatreon: boolean) { + return patreon ? + `added supporter (${patreon})` : + (hadPatreon ? `removed supporter${declined ? ' (declined)' : ''}` : undefined); +} + +function datesEqual(a: Date | undefined, b: Date | undefined) { + return (!a && !b) || (a && b && a.getTime() === b.getTime()); +} + +export const createAddTotalPledged = + (updateAuth: UpdateAuth) => + async (auths: IAuth[], pledges: PatreonPledge[]) => { + const setup = auths + .map(auth => ({ auth, pledge: pledges.find(p => p.user === auth.openId) })) + .filter(({ auth, pledge }) => pledge && pledge.total !== auth.pledged); + + await Bluebird.map(setup, ({ auth, pledge }) => + updateAuth(auth._id, { pledged: pledge!.total }), { concurrency: 4 }); + }; diff --git a/src/ts/server/playerUtils.ts b/src/ts/server/playerUtils.ts new file mode 100644 index 0000000..c2d4c46 --- /dev/null +++ b/src/ts/server/playerUtils.ts @@ -0,0 +1,811 @@ +import { sample } from 'lodash'; +import { createBinaryWriter, resetWriter } from 'ag-sockets'; +import { IClient, ServerEntity, Reporter, TokenData, ServerMap } from './serverInterfaces'; +import { IAccount, ICharacter, UpdateAccount, IOriginInfo, findFriendIds } from './db'; +import * as entities from '../common/entities'; +import { isShadowed, isMuted, supporterLevel } from '../common/adminUtils'; +import { handlePromiseDefault } from './serverUtils'; +import { + removeItem, hasFlag, distance, toInt, includes, array, flatten, containsPointWitBorder, distanceXY, setFlag, invalidEnum +} from '../common/utils'; +import { CharacterState, ServerConfig, AccountState, CharacterStateFlags, GameServerSettings } from '../common/adminInterfaces'; +import { + EntityState, Expression, PonyOptions, Action, ExpressionExtra, Eye, Muzzle, CLOSED_MUZZLES, + isExpressionAction, EntityPlayerState, UpdateFlags, InteractAction +} from '../common/interfaces'; +import { encodeExpression, EMPTY_EXPRESSION, decodeExpression } from '../common/encoders/expressionEncoder'; +import { EXPRESSION_TIMEOUT, DAY, FLY_DELAY, SECOND, PONY_TYPE } from '../common/constants'; +import { World } from './world'; +import { centerCameraOn, createCamera } from '../common/camera'; +import { findEntitiesInBounds } from './serverMap'; +import { CounterService } from './services/counter'; +import { create } from './reporter'; +import { updateAccountState } from './accountUtils'; +import { isMod } from '../common/accountUtils'; +import { getOriginFromHTTP } from './originUtils'; +import { createPony, getAndFixCharacterState, updateCharacterState } from './characterUtils'; +import { + updateEntityOptions, canBoopEntity, findPlayersThetCanBeSitOn, updateEntityState, updateEntityExpression, + sendAction, pushUpdateEntityToClient, fixPosition, isHoldingGrapes +} from './entityUtils'; +import { replaceEmojis } from '../client/emoji'; +import { expression, parseExpression } from '../common/expressionUtils'; +import { + canBoop2, isPonySitting, isPonyStanding, getBoopRect, canStand, isPonyFlying, setPonyState, canSit, canLie +} from '../common/entityUtils'; +import { withBorder } from '../common/rect'; +import { isOnlineFriend } from './services/friends'; +import { grapePurple, grapeGreen, tools } from '../common/entities'; +import { saySystem } from './chat'; + +export function isMutedOrShadowed(client: IClient) { + return client.shadowed || isMuted(client.account); +} + +export function isIgnored(ignoring: IClient, target: IClient): boolean { + return target.ignores.has(ignoring.accountId); +} + +export function kickClient(client: IClient, reason = 'kicked') { + client.leaveReason = reason; + client.disconnect(true, true); +} + +export function getCounter(client: IClient, key: keyof AccountState) { + return toInt(client.account.state && client.account.state[key]); +} + +export function createClientAndPony( + client: IClient, friends: string[], hides: string[], server: ServerConfig, world: World, states: CounterService +) { + const { account, character } = client.tokenData as TokenData; + const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest); + const reporter = create(server, account._id, character._id, origin); + const state = getAndFixCharacterState(server, character, world, states); + + client.characterState = state; + const pony = createPony(account, character, state); + pony.client = createClient(client, account, friends, hides, character, pony, world.getMainMap(), reporter, origin); + centerCameraOn(client.camera, pony); +} + +export function updateClientCharacter(client: IClient, character: ICharacter) { + client.character = character; + client.characterId = client.character._id.toString(); + client.characterName = replaceEmojis(client.character.name); +} + +export function createClient( + client: IClient, account: IAccount, friends: string[], hides: string[], character: ICharacter, pony: ServerEntity, + defaultMap: ServerMap, reporter: Reporter, origin: IOriginInfo | undefined +): IClient { + updateClientCharacter(client, character); + + client.ip = origin && origin.ip || ''; + client.country = origin && origin.country || '??'; + client.userAgent = client.originalRequest && client.originalRequest.headers['user-agent']; + + client.accountId = account._id.toString(); + client.accountName = account.name; + client.ignores = new Set(account.ignores); + client.hides = new Set(); + client.permaHides = new Set(hides); + client.friends = new Set(friends); + client.friendsCRC = undefined; + client.accountSettings = { ...account.settings }; + client.supporterLevel = supporterLevel(account); + client.isMod = isMod(account); + + client.reporter = reporter; + client.account = account; + client.character = character; + client.pony = pony; + client.map = defaultMap; + client.isSwitchingMap = false; + + client.notifications = []; + client.regions = []; + + client.shadowed = isShadowed(account); + client.country = origin && origin.country || '??'; + + client.camera = createCamera(); + client.camera.w = 800; + client.camera.h = 600; + + client.safeX = pony.x; + client.safeY = pony.y; + + client.lastPacket = Date.now(); + client.lastAction = 0; + client.lastBoopAction = 0; + client.lastExpressionAction = 0; + client.lastSays = []; + client.lastX = pony.x; + client.lastY = pony.y; + client.lastTime = 0; + client.lastVX = 0; + client.lastVY = 0; + client.lastMapSwitch = 0; + + client.lastSitX = 0; + client.lastSitY = 0; + client.lastSitTime = 0; + client.sitCount = 0; + + client.lastSwap = 0; + client.lastMapLoadOrSave = 0; + + client.lastCameraX = 0; + client.lastCameraY = 0; + client.lastCameraW = 0; + client.lastCameraH = 0; + + client.updateQueue = createBinaryWriter(128); + client.regionUpdates = []; + client.saysQueue = []; + client.unsubscribes = []; + client.subscribes = []; + + client.positions = []; + + return client; +} + +export function resetClientUpdates(client: IClient) { + resetWriter(client.updateQueue); + client.regionUpdates.length = 0; + client.saysQueue.length = 0; + client.unsubscribes.length = 0; + client.subscribes.length = 0; +} + +export function createCharacterState(entity: ServerEntity, map: ServerMap): CharacterState { + const options = entity.options as PonyOptions; + const flags: CharacterStateFlags = + (hasFlag(entity.state, EntityState.FacingRight) ? CharacterStateFlags.Right : 0) | + (options.extra ? CharacterStateFlags.Extra : 0); + const state: CharacterState = { x: entity.x, y: entity.y }; + + if (flags) { + state.flags = flags; + } + + if (map.id) { + state.map = map.id; + } + + if (options.hold) { + state.hold = entities.getEntityTypeName(options.hold); + } + + if (options.toy) { + state.toy = options.toy; + } + + return state; +} + +export async function createAndUpdateCharacterState(client: IClient, server: ServerConfig) { + const state = createCharacterState(client.pony, client.map); + await updateCharacterState(client.characterId, server.id, state); +} + +// utils + +export function addIgnore(target: IClient, accountId: string) { + target.account.ignores = target.account.ignores || []; + target.account.ignores.push(accountId); + target.ignores.add(accountId); +} + +export function removeIgnore(target: IClient, accountId: string) { + if (target.account.ignores) { + removeItem(target.account.ignores, accountId); + } + + target.ignores.delete(accountId); +} + +export const createIgnorePlayer = + (updateAccount: UpdateAccount, handlePromise = handlePromiseDefault) => + (client: IClient, target: IClient, ignored: boolean) => { + if (target.accountId === client.accountId) + return; + + const id = client.accountId; + const is = isIgnored(client, target); + + if (ignored === is) + return; + + if (ignored) { + addIgnore(target, id); + } else { + removeIgnore(target, id); + } + + handlePromise(updateAccount(target.accountId, { [ignored ? '$push' : '$pull']: { ignores: id } }) + .then(() => updateEntityPlayerState(client, target.pony)) + .then(() => { + const { accountId, account, character } = target; + const message = `${ignored ? 'ignored' : 'unignored'} ${character.name} (${account.name}) [${accountId}]`; + client.reporter.systemLog(message); + }), client.reporter.error); + }; + +export function findClientByEntityId(self: IClient, entityId: number): IClient | undefined { + const selected = self.selected; + + if (selected && selected.id === entityId && selected.client) { + return selected.client; + } + + if (self.party) { // TODO: remove ? + const client = self.party.clients.find(c => c.pony.id === entityId); + + if (client) { + //this.logger.log('client from party'); + return client; + } + + const pending = self.party.pending.find(c => c.client.pony.id === entityId); + + if (pending) { + //this.logger.log('pending from party'); + return pending.client; + } + } + + const notification = self.notifications.find(c => c.entityId === entityId); + + if (notification) { + //this.logger.log('sender from notification'); + return notification.sender; + } + + return undefined; +} + +export function cancelEntityExpression(entity: ServerEntity) { + if (entity.exprCancellable) { + setEntityExpression(entity, undefined); + } +} + +export function setEntityExpression( + entity: ServerEntity, expression: Expression | undefined, timeout = EXPRESSION_TIMEOUT, cancellable = false +) { + expression = expression || entity.exprPermanent; + const expr = encodeExpression(expression); + + (entity.options as PonyOptions).expr = expr; + + if (expression && timeout) { + entity.exprTimeout = Date.now() + timeout; + } else { + entity.exprTimeout = undefined; + } + + const sleeping = expression !== undefined && hasFlag(expression.extra, ExpressionExtra.Zzz); + entity.exprCancellable = cancellable || sleeping; + + updateEntityExpression(entity); +} + +export function playerBlush(pony: ServerEntity, args = '') { + const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral); + expr.extra |= ExpressionExtra.Blush; + setEntityExpression(pony, expr, DAY, !!pony.exprCancellable); +} + +export function parseOrCurrentExpression(pony: ServerEntity, message: string) { + return parseExpression(message) + || decodeExpression((!pony.options || pony.options.expr == null) ? EMPTY_EXPRESSION : pony.options.expr); +} + +export function playerSleep(pony: ServerEntity, args = '') { + if (pony.vx === 0 && pony.vy === 0) { + const base = parseOrCurrentExpression(pony, args) || expression(Eye.Closed, Eye.Closed, Muzzle.Neutral); + const muzzle = CLOSED_MUZZLES.indexOf(base.muzzle) !== -1 ? base.muzzle : Muzzle.Neutral; + const expr = { ...base, muzzle, left: Eye.Closed, right: Eye.Closed, extra: ExpressionExtra.Zzz }; + setEntityExpression(pony, expr, 0, true); + } +} + +export function playerLove(pony: ServerEntity, args = '') { + const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile); + expr.extra |= ExpressionExtra.Hearts; + setEntityExpression(pony, expr, DAY, !!pony.exprCancellable); +} + +export function playerCry(pony: ServerEntity, args = '') { + const expr = parseExpression(args) || expression(Eye.Sad, Eye.Sad, Muzzle.Frown); + expr.extra = expr.extra | ExpressionExtra.Cry; + setEntityExpression(pony, expr, 0); +} + +const fruitTypes = entities.fruits.map(f => f.type); + +export function interactWith(client: IClient, target: ServerEntity | undefined) { + if (target) { + const pony = client.pony; + + if (target.interact && (!target.interactRange || distance(pony, target) < target.interactRange)) { + target.interact(target, client); + } else if (target.triggerBounds && target.trigger) { + if (containsPointWitBorder(target.x, target.y, target.triggerBounds, pony.x, pony.y, 3)) { + target.trigger(target, client); + } else { + DEVELOPMENT && console.warn(`outside trigger bounds ` + + `(bounds: ${target.x} ${target.y} ${JSON.stringify(target.triggerBounds)} point: ${pony.x} ${pony.y})`); + } + } else if (target.interactAction) { + switch (target.interactAction) { + case InteractAction.Toolbox: { + switchTool(client, false); + break; + } + case InteractAction.GiveLantern: { + if (client.pony.options!.hold === entities.lanternOn.type) { + unholdItem(pony); + } else { + holdItem(pony, entities.lanternOn.type); + } + break; + } + case InteractAction.GiveFruits: { + const index = fruitTypes.indexOf(client.pony.options!.hold || 0) + 1; + holdItem(client.pony, fruitTypes[index % fruitTypes.length]); + break; + } + case InteractAction.GiveCookie1: { + const hold = client.pony.options!.hold; + let cookie = hold; + + while (hold === cookie) { + cookie = sample(entities.candies1Types)!; + } + + holdItem(client.pony, cookie!); + break; + } + case InteractAction.GiveCookie2: { + const hold = client.pony.options!.hold; + let cookie = hold; + + while (hold === cookie) { + cookie = sample(entities.candies2Types)!; + } + + holdItem(client.pony, cookie!); + break; + } + default: + invalidEnum(target.interactAction); + } + } + } +} + +export function useHeldItem(client: IClient) { + const hold = client.pony.options!.hold || 0; + + if (isGift(hold)) { + openGift(client); + } +} + +export function canPerformAction(client: IClient) { + return client.lastAction < Date.now(); +} + +export function updateEntityPlayerState(client: IClient, entity: ServerEntity) { + const playerState = getPlayerState(client, entity); + pushUpdateEntityToClient(client, { entity, flags: UpdateFlags.PlayerState, playerState }); +} + +// actions + +export function turnHead(client: IClient) { + if (canPerformAction(client)) { + updateEntityState(client.pony, client.pony.state ^ EntityState.HeadTurned); + } +} + +const purpleGrapeTypes = entities.grapesPurple.map(x => x.type); +const greenGrapeTypes = entities.grapesGreen.map(x => x.type); + +export function boop(client: IClient, now: number) { + if (canPerformAction(client) && canBoop2(client.pony) && client.lastBoopAction < now) { + cancelEntityExpression(client.pony); + sendAction(client.pony, Action.Boop); + + if (!client.shadowed && (isPonySitting(client.pony) || isPonyStanding(client.pony))) { + const boopRect = getBoopRect(client.pony); + const boopBounds = withBorder(boopRect, 1); + const entities = findEntitiesInBounds(client.map, boopBounds); + const entity = entities.find(e => canBoopEntity(e, boopRect)); + + if (entity) { + if (entity.boop) { + entity.boop(client); + } else if (entity.type === PONY_TYPE) { + const clientHold = client.pony.options!.hold || 0; + + if (isHoldingGrapes(entity) && clientHold !== grapeGreen.type && clientHold !== grapePurple.type) { + let index = purpleGrapeTypes.indexOf(entity.options!.hold || 0); + + if (index !== -1) { + holdItem(client.pony, grapePurple.type); + + if (index === (purpleGrapeTypes.length - 1)) { + unholdItem(entity); + } else { + holdItem(entity, purpleGrapeTypes[index + 1]); + } + } else { + let index = greenGrapeTypes.indexOf(entity.options!.hold || 0); + + if (index !== -1) { + holdItem(client.pony, grapeGreen.type); + + if (index === (greenGrapeTypes.length - 1)) { + unholdItem(entity); + } else { + holdItem(entity, greenGrapeTypes[index + 1]); + } + } + } + } + } + } + } + + client.lastBoopAction = now + 500; + } +} + +export function stand(client: IClient) { + if (canPerformAction(client) && canStand(client.pony, client.map)) { + if (!isPonyFlying(client.pony)) { + cancelEntityExpression(client.pony); + } + + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyStanding)); + } +} + +const SIT_MAX_TIME = 2 * SECOND; +const SIT_MAX_DIST = 1; +const SIT_MAX_COUNT = 5; + +function checkSuspiciousSitting(client: IClient) { + const now = Date.now(); + const { x, y } = client.pony; + const dist = distanceXY(x, y, client.lastSitX, client.lastSitY); + + if ((now - client.lastSitTime) < SIT_MAX_TIME && dist < SIT_MAX_DIST && findPlayersThetCanBeSitOn(client.map, client.pony)) { + client.sitCount++; + + if (client.sitCount > SIT_MAX_COUNT) { + client.reporter.warn(`Suspicious sitting`); + client.sitCount = 0; + } + } else { + client.sitCount = 1; + } + + client.lastSitX = x; + client.lastSitY = y; + client.lastSitTime = now; +} + +export function sit(client: IClient, settings: GameServerSettings) { + if (canPerformAction(client) && canSit(client.pony, client.map)) { + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonySitting)); + + if (settings.reportSitting) { + checkSuspiciousSitting(client); + } + } +} + +export function lie(client: IClient) { + if (canPerformAction(client) && canLie(client.pony, client.map)) { + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyLying)); + } +} + +export function fly(client: IClient) { + if (canPerformAction(client) && client.pony.canFly && !isPonyFlying(client.pony)) { + cancelEntityExpression(client.pony); + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyFlying)); + client.pony.inTheAirDelay = FLY_DELAY; + } +} + +export function expressionAction(client: IClient, action: Action) { + if (canPerformAction(client) && isExpressionAction(action) && client.lastExpressionAction < Date.now()) { + cancelEntityExpression(client.pony); + sendAction(client.pony, action); + client.lastExpressionAction = Date.now() + 500; + } +} + +// hold + +export function holdItem(entity: ServerEntity, hold: number) { + if (entity.options && entity.options.hold !== hold) { + updateEntityOptions(entity, { hold }); + } +} + +export function unholdItem(entity: ServerEntity) { + if (entity.options && entity.options.hold) { + updateEntityOptions(entity, { hold: 0 }); + delete entity.options.hold; + } +} + +// toy + +export function holdToy(entity: ServerEntity, toy: number) { + if (entity.options && entity.options.toy !== toy) { + updateEntityOptions(entity, { toy }); + } +} + +export function unholdToy(entity: ServerEntity) { + if (entity.options && entity.options.toy) { + updateEntityOptions(entity, { toy: 0 }); + delete entity.options.toy; + } +} + +// gifts and toys + +const giftTypes = [entities.gift2.type]; + +const toys = [ + // hat + { type: 0, multiplier: 20 }, + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 5 }, + { type: 0, multiplier: 1 }, // pink + // snowpony + { type: 0, multiplier: 20 }, + { type: 0, multiplier: 10 }, // clothes + { type: 0, multiplier: 1 }, // evil + // gift + { type: 0, multiplier: 20 }, + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 5 }, + { type: 0, multiplier: 1 }, + // hanging thing + { type: 0, multiplier: 20 }, // bell + { type: 0, multiplier: 10 }, // mistletoe + { type: 0, multiplier: 5 }, // cookie + { type: 0, multiplier: 1 }, // spider + // teddy + { type: 0, multiplier: 20 }, // brown + { type: 0, multiplier: 10 }, // brown angel + { type: 0, multiplier: 20 }, // black + { type: 0, multiplier: 10 }, // black angel + { type: 0, multiplier: 5 }, // brown clothes + { type: 0, multiplier: 5 }, // black clothes + { type: 0, multiplier: 1 }, // white santa + // xmas tree + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 5 }, + // deer + { type: 0, multiplier: 5 }, + { type: 0, multiplier: 1 }, // with clothes + // candy horns + { type: 0, multiplier: 10 }, // one + { type: 0, multiplier: 2 }, // two + { type: 0, multiplier: 1 }, // two (alt) + // star + { type: 0, multiplier: 5 }, + // halo + { type: 0, multiplier: 5 }, +]; + +toys.forEach((toy, i) => toy.type = i + 1); + +const toyTypes = flatten(toys.map(x => array(x.multiplier, x.type))); + +function hasToyUnlocked(type: number, collectedToys: number) { + const index = toys.findIndex(t => t.type === type); + return hasFlag(collectedToys, 1 << index); +} + +function unlockToy(type: number, collectedToys: number) { + const index = toys.findIndex(t => t.type === type); + return collectedToys | (1 << index); +} + +export function getCollectedToysCount(client: IClient) { + const stateToys = toInt((client.account.state || {}).toys); + const total = toys.length; + let collected = 0; + + for (let i = 0, bit = 1; i < total; i++ , bit <<= 1) { + if (stateToys & bit) { + collected++; + } + } + + return { collected, total }; +} + +export function getNextToyOrExtra(client: IClient) { + const collectedToys = toInt((client.account.state || {}).toys); + const options = client.pony.options || {}; + const extra = !!options.extra; + const toy = toInt(options.toy); + + if (extra) { + return { extra: false, toy: 0 }; + } else { + for (let i = toys.findIndex(t => t.type === toy) + 1; i < toys.length; i++) { + const type = toys[i].type; + + if (hasToyUnlocked(type, collectedToys)) { + return { extra: false, toy: type }; + } + } + + return { extra: true, toy: 0 }; + } +} + +export function openGift(client: IClient) { + const options = client.pony.options || {}; + + if (isGift(options.hold)) { + let toyType = 0; + + do { + toyType = sample(toyTypes)!; + } while (toyType === options.toy); + + sendAction(client.pony, Action.HoldPoof); + unholdItem(client.pony); + setTimeout(() => holdToy(client.pony, toyType), 200); + + const state = client.account.state || {}; + + if (!hasToyUnlocked(toyType, toInt(state.toys))) { + updateAccountState(client.account, state => { + state.toys = unlockToy(toyType, toInt(state.toys)); + }); + } + } +} + +export function isGift(type: number | undefined) { + return type !== undefined && includes(giftTypes, type); +} + +export function isHiddenBy(a: IClient, b: IClient) { + return a.hides.has(b.accountId) || b.hides.has(a.accountId) || + a.permaHides.has(b.accountId) || b.permaHides.has(a.accountId); +} + +export function getPlayerState(client: IClient, entity: ServerEntity): EntityPlayerState { + let state = EntityPlayerState.None; + + if (entity.client !== undefined) { + if (isIgnored(client, entity.client)) { + state |= EntityPlayerState.Ignored; + } + + if (isHiddenBy(client, entity.client)) { + state |= EntityPlayerState.Hidden; + } + + if (isOnlineFriend(client, entity.client)) { + state |= EntityPlayerState.Friend; + } + } + + return state; +} + +export async function reloadFriends(client: IClient) { + const friends = await findFriendIds(client.accountId); + client.friends = new Set(friends); + client.friendsCRC = undefined; + client.actionParam(0, Action.FriendsCRC, undefined); +} + +export function execAction(client: IClient, action: Action, settings: GameServerSettings) { + switch (action) { + case Action.Boop: + boop(client, Date.now()); + break; + case Action.TurnHead: + turnHead(client); + break; + case Action.Stand: + stand(client); + break; + case Action.Sit: + sit(client, settings); + break; + case Action.Lie: + lie(client); + break; + case Action.Fly: + fly(client); + break; + case Action.Drop: + unholdItem(client.pony); + break; + case Action.Sleep: + playerSleep(client.pony); + break; + case Action.Blush: + playerBlush(client.pony); + break; + case Action.Cry: + playerCry(client.pony); + break; + case Action.Love: + playerLove(client.pony); + break; + case Action.DropToy: + unholdToy(client.pony); + updateEntityOptions(client.pony, { extra: false }); + break; + case Action.Magic: + if (client.pony.canMagic) { + const has = hasFlag(client.pony.state, EntityState.Magic); + updateEntityState(client.pony, setFlag(client.pony.state, EntityState.Magic, !has)); + } + break; + case Action.SwitchTool: + switchTool(client, false); + break; + case Action.SwitchToolRev: + switchTool(client, true); + break; + case Action.SwitchToPlaceTool: + holdItem(client.pony, entities.hammer.type); + break; + case Action.SwitchToTileTool: + holdItem(client.pony, entities.shovel.type); + break; + default: + if (isExpressionAction(action)) { + expressionAction(client, action); + } else { + throw new Error(`Invalid action (${action})`); + } + break; + } +} + +export function switchTool(client: IClient, reverse: boolean) { + const hold = client.pony.options!.hold || 0; + const index = tools.findIndex(t => t.type === hold); + const unholdIndex = reverse ? 0 : tools.length - 1; + + if (index === unholdIndex) { + unholdItem(client.pony); + } else { + const newIndex = reverse ? (index === -1 ? tools.length - 1 : index - 1) : ((index + 1) % tools.length); + const tool = tools[newIndex]; + holdItem(client.pony, tool.type); + saySystem(client, tool.text); + } +} + +export function teleportTo(client: IClient, x: number, y: number) { + fixPosition(client.pony, client.map, x, y, true); + client.safeX = client.pony.x; + client.safeY = client.pony.y; + client.lastTime = 0; +} diff --git a/src/ts/server/polling.ts b/src/ts/server/polling.ts new file mode 100644 index 0000000..cd18208 --- /dev/null +++ b/src/ts/server/polling.ts @@ -0,0 +1,244 @@ +import * as fs from 'fs'; +import * as Bluebird from 'bluebird'; +import { + InternalGameServerState, BannedMuted, Settings, ServerConfig, InternalLoginServerState, SupporterFlags +} from '../common/adminInterfaces'; +import { fromNow, delay } from '../common/utils'; +import { logger, logPatreon, system, logPerformance } from './logger'; +import { Auth, updateAccounts, updateAccount, queryAuths, queryAccounts, updateAuth, Account, SupporterInvite } from './db'; +import { getDiskSpace, getCertificateExpirationDate, getMemoryUsage } from './serverUtils'; +import { HOUR, MINUTE, DAY, SECOND, YEAR } from '../common/constants'; +import { + fetchPatreonData, createPatreonClient, createUpdatePatreonInfo, createRemoveOldSupporters, + createUpdateSupporters, createAddTotalPledged +} from './patreon'; +import { create } from './reporter'; +import { servers, serverStatus, loginServers, RemovedDocument } from './internal'; +import * as paths from './paths'; +import { updateSupporterInvites } from './services/supporterInvites'; +import { AdminService } from './services/adminService'; +import { clearOrigins } from './api/origins'; +import { config } from './config'; + +let updatingPatreonPromise: Promise | undefined; + +async function updatePatreonDataInternal(server: ServerConfig, accessToken: string) { + try { + const removeOldSupporters = createRemoveOldSupporters(updateAccounts, system); + const updateSupporters = createUpdateSupporters(updateAccount, system); + const addTotalPledged = createAddTotalPledged(updateAuth); + const updatePatreonInfo = createUpdatePatreonInfo( + queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged); + + const client = createPatreonClient(accessToken); + const data = await fetchPatreonData(client, logPatreon); + await updatePatreonInfo(data, new Date()); + + serverStatus.lastPatreonUpdate = (new Date()).toISOString(); + } catch (e) { + const message = e.error ? (e.error.message || e.error.statusText || `${e}`) : e.message; + const stack = (e.error ? e.error.stack : e.stack) || ''; + create(server).danger('Patreon update failed', `${message}\n${stack}`.trim()); + logger.error(e); + } finally { + updatingPatreonPromise = undefined; + } +} + +export async function updatePatreonData(server: ServerConfig, { patreonToken }: Settings) { + if (patreonToken && config.supporterLink) { + return updatingPatreonPromise = updatingPatreonPromise || updatePatreonDataInternal(server, patreonToken); + } +} + +async function clearOldIgnores() { + const start = Date.now(); + await updateAccounts({ + ignores: { $exists: true, $not: { $size: 0 } }, + lastVisit: { $lt: fromNow(-YEAR) }, + }, { ignores: [] }); + logPerformance(`[async] clearOldIgnores (${Date.now() - start}ms)`); +} + +async function cleanupBanField(field: keyof BannedMuted) { + const start = Date.now(); + await updateAccounts({ [field]: { $exists: true, $gt: 0, $lt: Date.now() } }, { $unset: { [field]: 1 } }); + logPerformance(`[async] cleanupBanField (${field}) (${Date.now() - start}ms)`); +} + +async function cleanupBans() { + const start = Date.now(); + await cleanupBanField('ban'); + await cleanupBanField('shadow'); + await cleanupBanField('mute'); + logPerformance(`[async] cleanupBans (${Date.now() - start}ms)`); +} + +async function cleanupMerges() { + const start = Date.now(); + const date = fromNow(-30 * DAY); + await updateAccounts({ merges: { $exists: true, $not: { $size: 0 } } }, { $pull: { merges: { date: { $lt: date } } } }); + await updateAccounts({ merges: { $exists: true, $size: 0 } }, { $unset: { merges: 1 } }); + logPerformance(`[async] cleanupMerges (${Date.now() - start}ms)`); +} + +async function cleanupAccountAlerts() { + const start = Date.now(); + await updateAccounts( + { alert: { $exists: true }, 'alert.expires': { $lt: new Date() } } as any, + { $unset: { alert: 1 } }); + logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); +} + +export async function updatePastSupporters() { + const start = Date.now(); + const auths = await Auth.find({ + pledged: { $exists: true, $gt: 0 }, + disabled: { $ne: true }, + banned: { $ne: true } + }, 'account').exec(); + + const accounts = await Account.find({ + supporter: { $exists: true, $bitsAllSet: SupporterFlags.PastSupporter } + }, '_id').exec(); + + const shouldBeFlagged = new Set(); + const areFlagged = new Set(); + + for (const auth of auths) { + if (auth.account) { + shouldBeFlagged.add(auth.account.toString()); + } + } + + for (const account of accounts) { + areFlagged.add(account._id.toString()); + } + + for (const auth of auths) { + if (auth.account) { + if (!areFlagged.has(auth.account.toString())) { + await Account.updateOne({ _id: auth.account }, { $bit: { supporter: { or: SupporterFlags.PastSupporter } } }).exec(); + } + } + } + + for (const account of accounts) { + if (!shouldBeFlagged.has(account._id.toString())) { + await Account.updateOne({ _id: account._id }, { $bit: { supporter: { and: ~SupporterFlags.PastSupporter } } }).exec(); + } + } + + logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); +} + +const cleanupStrayAuths = (removedDocument: RemovedDocument) => + async () => { + const start = Date.now(); + const date = fromNow(-1 * DAY); + const query = { account: { $exists: false }, updatedAt: { $lt: date }, createdAt: { $lt: date } }; + const items = await queryAuths(query, '_id'); + await Auth.deleteMany(query).exec(); + await Bluebird.map(items, item => removedDocument('auths', item._id.toString()), { concurrency: 4 }); + logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); + }; + +async function updateServerState(server: InternalGameServerState | InternalLoginServerState) { + try { + const state = await server.api.state(); + Object.assign(server.state, state); + } catch { + server.state.dead = true; + } +} + +let lastVisitedTodayCheck = (new Date()).getDate(); + +async function countUsersVisitedToday() { + const start = Date.now(); + const day = (new Date()).getDate(); + + if (lastVisitedTodayCheck !== day) { + lastVisitedTodayCheck = day; + const statsFile = paths.pathTo('settings', `user-counts.log`); + const count = await Account.countDocuments({ lastVisit: { $gt: fromNow(-1 * DAY) } }).exec(); + const json = JSON.stringify({ count, date: (new Date()).toISOString() }); + await fs.appendFileAsync(statsFile, `${json}\n`, 'utf8'); + logPerformance(`[async] countUsersVisitedToday (${Date.now() - start}ms)`); + } +} + +async function mergePotentialDuplicates(service: AdminService) { + if (loginServers[0].state.autoMergeDuplicates) { + await service.mergePotentialDuplicates(); + } +} + +export async function poll(action: () => any, delayTime: number) { + try { + await delay(delayTime); + await action(); + } catch (e) { + console.error(e); + } finally { + poll(action, delayTime); + } +} + +export async function pollImmediate(action: () => any, delayTime: number) { + try { + await action(); + } catch (e) { + console.error(e); + } finally { + await delay(delayTime); + poll(action, delayTime); + } +} + +export function pollServers() { + return poll(() => Promise.all([...loginServers, ...servers].map(updateServerState)), 1 * SECOND); +} + +export function pollPatreon(server: ServerConfig, settings: Settings) { + return poll(() => updatePatreonData(server, settings), 10 * MINUTE); +} + +export const pollDiskSpace = () => pollImmediate(() => + getDiskSpace().then(value => serverStatus.diskSpace = value), HOUR); +export const pollMemoryUsage = () => pollImmediate(() => + getMemoryUsage().then(value => serverStatus.memoryUsage = value), 10 * MINUTE); +export const pollCertificateExpirationDate = () => pollImmediate(() => + getCertificateExpirationDate().then(value => serverStatus.certificateExpiration = value), HOUR); + +export const startBansCleanup = () => poll(cleanupBans, DAY + 10 * MINUTE); +export const startMergesCleanup = () => poll(cleanupMerges, DAY + 15 * MINUTE); +export const startStrayAuthsCleanup = (removedDocument: RemovedDocument) => + poll(cleanupStrayAuths(removedDocument), DAY + 35 * MINUTE); +export const startClearOldIgnores = () => poll(clearOldIgnores, DAY + 20 * MINUTE); +export const startCollectingUsersVisitedCount = () => poll(countUsersVisitedToday, 10 * MINUTE); +export const startSupporterInvitesCleanup = () => poll(() => updateSupporterInvites(SupporterInvite), HOUR); +export const startPotentialDuplicatesCleanup = (service: AdminService) => + poll(() => mergePotentialDuplicates(service), 10 * MINUTE); +export const startAccountAlertsCleanup = () => poll(cleanupAccountAlerts, DAY + 25 * MINUTE); +export const startUpdatePastSupporters = () => poll(updatePastSupporters, DAY + 30 * MINUTE); + +export function startClearTo10Origns(adminService: AdminService) { + return poll(async () => { + if (adminService.loaded) { + const start = Date.now(); + await clearOrigins(adminService, 10, true, { old: false, singles: true, trim: true }); + logPerformance(`[async] startClearTo10Origns (${Date.now() - start}ms)`); + } + }, DAY + 35 * MINUTE); +} + +export function startClearVeryOldOrigns(adminService: AdminService) { + return poll(async () => { + if (adminService.loaded) { + const start = Date.now(); + await clearOrigins(adminService, 1, true, { old: true, singles: false, trim: false }); + logPerformance(`[async] startClearVeryOldOrigns (${Date.now() - start}ms)`); + } + }, DAY + 50 * MINUTE); +} diff --git a/src/ts/server/pool.ts b/src/ts/server/pool.ts new file mode 100644 index 0000000..225bc14 --- /dev/null +++ b/src/ts/server/pool.ts @@ -0,0 +1,30 @@ +export interface Pool { + create(): T; + dispose(value: T): boolean; +} + +export function createPool(count: number, createNew: () => T, reset: (value: T) => void): Pool { + const pool: T[] = []; + + const create = () => { + const existing = pool.pop(); + + if (existing) { + reset(existing); + return existing; + } else { + return createNew(); + } + }; + + const dispose = (value: T) => { + if (pool.length < count) { + pool.push(value); + return true; + } else { + return false; + } + }; + + return { create, dispose }; +} diff --git a/src/ts/server/regionUtils.ts b/src/ts/server/regionUtils.ts new file mode 100644 index 0000000..70c5bc6 --- /dev/null +++ b/src/ts/server/regionUtils.ts @@ -0,0 +1,301 @@ +import { createBinaryWriter, getWriterBuffer, BinaryWriter } from 'ag-sockets'; +import { removeItem, pointInRect, clamp, includes } from '../common/utils'; +import { ServerEntity, IClient, ServerRegion, ServerMap } from './serverInterfaces'; +import { + tickTilesRestoration, resetRegionUpdates, pushRemoveEntityToRegion, removeEntityFromRegion, addEntityToRegion +} from './serverRegion'; +import { updateEntity, isEntityShadowed, isOverflowError, pushAddEntityToClient } from './entityUtils'; +import { writeRegion, writeUpdate } from '../common/encoders/updateEncoder'; +import { toWorldX, toWorldY } from '../common/positionUtils'; +import { isRectVisible } from '../common/camera'; +import { timingStart, timingEnd } from './timing'; +import { getRegion } from '../common/worldMap'; +import { logger } from './logger'; +import { EntityFlags } from '../common/interfaces'; +import { REGION_SIZE } from '../common/constants'; + +let updatesBuffer = new ArrayBuffer(4096); +let updatesBufferOffset = 0; + +export function resetEncodeUpdate() { + updatesBufferOffset = 0; +} + +function resizeUpdatesBuffer(e: Error) { + if (isOverflowError(e)) { + updatesBuffer = new ArrayBuffer(updatesBuffer.byteLength * 2); + updatesBufferOffset = 0; + DEVELOPMENT && logger.debug(`resize buffer to ${updatesBuffer.byteLength} (${e.message})`); + } else { + throw e; + } +} + +function createUpdatesWriter() { + const buffer = new Uint8Array(updatesBuffer, updatesBufferOffset, updatesBuffer.byteLength - updatesBufferOffset); + return createBinaryWriter(buffer); +} + +function commitUpdatesWriter(writer: BinaryWriter) { + const result = getWriterBuffer(writer); + updatesBufferOffset += result.byteLength; + return result; +} + +function encodeUpdate(region: ServerRegion): Uint8Array { + timingStart('encodeUpdate()'); + + let result: Uint8Array; + + while (true) { + try { + const writer = createUpdatesWriter(); + writeUpdate(writer, region); + result = commitUpdatesWriter(writer); + break; + } catch (e) { + resizeUpdatesBuffer(e); + } + } + + timingEnd(); + + return result; +} + +function encodeRegion(region: ServerRegion, client: IClient): Uint8Array { + timingStart('encodeRegion()'); + + let result: Uint8Array; + + while (true) { + try { + const writer = createUpdatesWriter(); + writeRegion(writer, region, client); + result = commitUpdatesWriter(writer); + break; + } catch (e) { + resizeUpdatesBuffer(e); + } + } + + timingEnd(); + + return result; +} + +export function subscribeToRegionsInRange(client: IClient) { + timingStart('subscribeToRegionsInRange()'); + + const { map, camera } = client; + const maxX = clamp(Math.floor(toWorldX(camera.x + camera.w) / REGION_SIZE) + 1, 0, map.regionsX - 1); + const maxY = clamp(Math.floor(toWorldY(camera.y + camera.h) / REGION_SIZE) + 1, 0, map.regionsY - 1); + const minX = clamp(Math.floor(toWorldX(camera.x) / REGION_SIZE) - 1, 0, maxX); + const minY = clamp(Math.floor(toWorldY(camera.y) / REGION_SIZE) - 1, 0, maxY); + + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + const region = getRegion(map, x, y); + + if (isRectVisible(camera, region.subscribeBounds)) { + if (!isSubscribedToRegion(client, region)) { + timingStart('subscribeToRegion()'); + region.clients.push(client); + client.regions.push(region); + client.subscribes.push(encodeRegion(region, client)); + timingEnd(); + } + } + } + } + + timingEnd(); +} + +export function unsubscribeFromOutOfRangeRegions(client: IClient) { + timingStart('unsubscribeFromOutOfRangeRegions()'); + + const regions = client.regions; + + for (let i = regions.length - 1; i >= 0; i--) { + const region = regions[i]; + + if (!isRectVisible(client.camera, region.unsubscribeBounds)) { + if (includes(region.entities, client.pony)) { + DEVELOPMENT && logger.warn(`Trying to unsubscribe client from region they are in`); + } else { + removeItem(region.clients, client); + regions.splice(i, 1); + client.unsubscribes.push(region.x, region.y); + } + } + } + + timingEnd(); +} + +export function unsubscribeFromAllRegions(client: IClient, silent: boolean) { + for (const region of client.regions) { + removeItem(region.clients, client); + + if (!silent) { + client.unsubscribes.push(region.x, region.y); + } + } + + client.regions = []; +} + +export function getExpectedRegion({ x, y, flags, region }: ServerEntity, map: ServerMap) { + if (region !== undefined && (flags & EntityFlags.Movable) !== 0 && pointInRect(x, y, region.boundsWithBorder)) { + return region; + } else { + const rx = clamp(Math.floor(x / REGION_SIZE), 0, map.regionsX - 1) | 0; + const ry = clamp(Math.floor(y / REGION_SIZE), 0, map.regionsY - 1) | 0; + return map.regions[(rx + ((ry * map.regionsX) | 0)) | 0]; + } +} + +export function updateRegion(entity: ServerEntity, map: ServerMap) { + const expectedRegion = getExpectedRegion(entity, map); + + if (expectedRegion !== entity.region) { + transferToRegion(entity, expectedRegion, map); + } +} + +const moves: { entity: ServerEntity, region: ServerRegion; map: ServerMap; }[] = []; + +export function updateRegions(maps: ServerMap[]) { + timingStart('updateRegions()'); + + moves.length = 0; + + // TODO: only update changed entities + timingStart('getExpectedRegion'); + for (const map of maps) { + for (const region of map.regions) { + for (const entity of region.movables) { + const expectedRegion = getExpectedRegion(entity, map); + + if (expectedRegion !== entity.region) { + moves.push({ entity, region: expectedRegion, map }); + } + } + } + } + timingEnd(); + + timingStart('transferToRegion'); + for (const { entity, region, map } of moves) { + transferToRegion(entity, region, map); + } + timingEnd(); + + moves.length = 0; + + timingEnd(); +} + +export function commitRegionUpdates(regions: ServerRegion[]) { + timingStart('commitRegionUpdates()'); + + for (const region of regions) { + if (region.entityUpdates.length || region.entityRemoves.length || region.tileUpdates.length) { + if (region.clients.length) { + const data = encodeUpdate(region); + + for (const client of region.clients) { + client.regionUpdates.push(data); + } + } + + resetRegionUpdates(region); + } + } + + timingEnd(); +} + +export function transferToRegion(entity: ServerEntity, region: ServerRegion, map: ServerMap) { + const oldRegion = entity.region; + + if (oldRegion) { + removeEntityFromRegion(oldRegion, entity, map); + updateEntity(entity, true); + } + + entity.region = region; + addEntityToRegion(region, entity, map); + + if (!isEntityShadowed(entity)) { + for (const client of region.clients) { + if (!oldRegion || !isSubscribedToRegion(client, oldRegion)) { + pushAddEntityToClient(client, entity); + } + } + } +} + +export function addToRegion(entity: ServerEntity, region: ServerRegion, map: ServerMap) { + entity.region = region; + addEntityToRegion(region, entity, map); + + if (isEntityShadowed(entity)) { + pushAddEntityToClient(entity.client, entity); + } else { + for (const client of region.clients) { + pushAddEntityToClient(client, entity); + } + } +} + +export function removeFromRegion(entity: ServerEntity, region: ServerRegion, map: ServerMap) { + const removed = removeEntityFromRegion(region, entity, map); + pushRemoveEntityToRegion(region, entity); + return removed; +} + +export function isSubscribedToRegion(client: IClient, region: ServerRegion) { + return includes(client.regions, region); +} + +export function sparseRegionUpdate(map: ServerMap, region: ServerRegion, options: { restoreTerrain: boolean; }) { + if (options.restoreTerrain) { + tickTilesRestoration(map, region); + } +} + +// timing helpers + +function writingTiming() { + timingStart('write'); +} + +function sendingTiming() { + timingEnd(); + timingStart('send'); +} + +function doneTiming() { + timingEnd(); +} + +function noop() { +} + +export function setupTiming(client: any) { + if (client.__internalHooks) { + client.__internalHooks.writing = writingTiming; + client.__internalHooks.sending = sendingTiming; + client.__internalHooks.done = doneTiming; + } +} + +export function clearTiming(client: any) { + if (client.__internalHooks) { + client.__internalHooks.writing = noop; + client.__internalHooks.sending = noop; + client.__internalHooks.done = noop; + } +} diff --git a/src/ts/server/reporter.ts b/src/ts/server/reporter.ts new file mode 100644 index 0000000..84f555e --- /dev/null +++ b/src/ts/server/reporter.ts @@ -0,0 +1,103 @@ +import { Request } from 'express'; +import { truncate } from 'lodash'; +import { Reporter } from './serverInterfaces'; +import { Event, IEvent, IOriginInfo, ID, IAccount } from './db'; +import { logger, system } from './logger'; +import { ServerConfig } from '../common/adminInterfaces'; +import { getOrigin } from './originUtils'; + +const maxDescLength = 300; + +/* istanbul ignore next */ +const createLogEvent = + (config: ServerConfig) => + ( + account: ID | undefined, pony: ID | undefined, originInfo: IOriginInfo | undefined, type: string, + message: string, desc?: string + ) => { + const server = config.id; + + if (desc) { + desc = truncate(desc, { length: maxDescLength }); + } + + const origin = originInfo && { ip: originInfo.ip, country: originInfo.country }; + + Event.findOne({ server, account, pony, type, message, origin }).exec() + .then(event => { + if (event) { + if (!event.desc || (event.desc.length < maxDescLength && desc && event.desc.indexOf(desc) === -1)) { + event.desc = `${event.desc || ''}\n${desc || ''}`.trim(); + } + + return Event.updateOne({ _id: event._id }, { desc: event.desc, count: event.count + 1 }).exec(); + } else { + return Event.create({ server, account, pony, type, message, origin, desc }); + } + }) + .catch(logger.error); + + return null; + }; + +const ignoreWarnings = ['Suspicious message', 'Spam']; + +/* istanbul ignore next */ +export function create(server: ServerConfig, account?: ID, pony?: ID, originInfo?: IOriginInfo): Reporter { + const logEvent = createLogEvent(server); + const accountId = `${account}`; + + function log(type: string, message: string, desc?: string) { + logEvent(account, pony, originInfo, type, message, desc); + + if (DEVELOPMENT) { + logger.debug('[event]', `[${type}]`, message); + } + } + + return { + info(message: string, desc?: string) { + log('info', message, desc); + }, + warn(message: string, desc?: string) { + log('warning', message, desc); + + if (ignoreWarnings.indexOf(message) === -1) { + system(accountId, message); + } + }, + warnLog(message: string) { + logger.warn(message); + }, + danger(message: string, desc?: string) { + log('danger', message, desc); + logger.error(message, desc || ''); + }, + error(error: Error, desc?: string) { + log('danger', error.message, desc); + logger.error(error, desc || ''); + }, + system(message: string, desc?: string, logEvent = true) { + if (logEvent) { + log('info', message, desc); + } + + system(accountId, message); + }, + systemLog(message: string) { + system(accountId, message); + DEVELOPMENT && logger.log(message); + }, + setPony(newPony: any) { + pony = newPony; + }, + }; +} + +/* istanbul ignore next */ +export function createFromRequest(server: ServerConfig, req: Request, pony?: any) { + const user = req && req.user as IAccount | undefined; + const account = user ? user.id : undefined; + const origin = req ? getOrigin(req) : undefined; + return create(server, account, pony, origin); +} diff --git a/src/ts/server/reporting.ts b/src/ts/server/reporting.ts new file mode 100644 index 0000000..24256bf --- /dev/null +++ b/src/ts/server/reporting.ts @@ -0,0 +1,100 @@ +import { HOUR } from '../common/constants'; +import { isMuted } from '../common/adminUtils'; +import { fromNow } from '../common/utils'; +import { isNew } from './accountUtils'; +import { ReportAccount, TimeoutAccount, ReportInviteLimit, OnSuspiciousMessage, OnMessageSettings } from './serverInterfaces'; +import { CounterService } from './services/counter'; +import { handlePromiseDefault } from './serverUtils'; +import { Suspicious } from '../common/adminInterfaces'; + +type Counter = CounterService; + +export const SPAM_TIMEOUT = 1 * HOUR; +export const SWEAR_TIMEOUT = 10 * HOUR; +export const FORBIDDEN_TIMEOUT = 1 * HOUR; + +export const createReportSuspicious = + (counter: Counter): OnSuspiciousMessage => + (client, message, suspicious) => { + const { accountId, account, reporter, shadowed } = client; + const limit = 5; + const { count, items } = counter.add(accountId, message); + + if (count > limit || suspicious === Suspicious.Very) { + const msg = items.join('\n'); + counter.remove(accountId); + + if (!(isMuted(account) || shadowed)) { + reporter.warn('Suspicious messages', msg); + } + } + }; + +export const createReportSwears = + ( + counter: Counter, reportSwearing: ReportAccount, timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault, + ): OnMessageSettings => + (client, message, settings) => { + const { accountId, account, reporter, shadowed } = client; + const limit = 5; // isNew ? 3 : 6; + const { count, items } = counter.add(accountId, message); + + if (count > limit) { + const msg = items.join('\n'); + const timeout = settings.autoBanSwearing && !(isMuted(account) || shadowed); + const duration = SWEAR_TIMEOUT * (settings.doubleTimeouts ? 2 : 1); + counter.remove(accountId); + + handlePromise(Promise.resolve() + .then(() => reportSwearing(accountId)) + .then(() => timeout ? timeoutAccount(accountId, fromNow(duration), 'Timed out for swearing') : undefined) + .then(() => { + if (timeout) { + reporter.system('Timed out for swearing', msg, !!settings.reportSwears); + } else if (!(isMuted(account) || shadowed)) { + reporter.warn('Swearing', msg); + } + }), reporter.error); + } + }; + +export const createReportForbidden = + ( + counter: Counter, timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault + ): OnMessageSettings => + (client, message, settings) => { + const { accountId, account, reporter, shadowed } = client; + const newAccount = isNew(account); + const limit = newAccount ? 5 : 10; + const { count, items } = counter.add(accountId, message); + const mutedOrShadowed = isMuted(account) || shadowed; + + if (!mutedOrShadowed) { + if (count >= limit) { + const msg = items.join('\n'); + const duration = FORBIDDEN_TIMEOUT * (settings.doubleTimeouts ? 2 : 1); + counter.remove(accountId); + + if (newAccount || settings.autoBanSwearing) { + handlePromise(timeoutAccount(accountId, fromNow(duration)) + .then(() => reporter.system('Timed out for forbidden messages', msg)), reporter.error); + } else { + reporter.warn('Forbidden messages', msg); + } + } + } + }; + +export const reportInviteLimit = + ( + reportInviteLimitAccount: (account: string) => Promise, message: string, handlePromise = handlePromiseDefault + ): ReportInviteLimit => + ({ accountId, reporter }) => + handlePromise(reportInviteLimitAccount(accountId) + .then(count => { + reporter.systemLog(message); + + if (count % 10 === 0) { + reporter.warn(`${message} (${count})`); + } + }), reporter.error); diff --git a/src/ts/server/requestUtils.ts b/src/ts/server/requestUtils.ts new file mode 100644 index 0000000..84600de --- /dev/null +++ b/src/ts/server/requestUtils.ts @@ -0,0 +1,217 @@ +import { Request, Response, NextFunction, RequestHandler } from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as moment from 'moment'; +import * as ExpressBrute from 'express-brute'; +import { noop } from 'lodash'; +import { HASH } from '../generated/hash'; +import { isAdmin } from '../common/accountUtils'; +import { ACCOUNT_ERROR, VERSION_ERROR, OFFLINE_ERROR } from '../common/errors'; +import { logger } from './logger'; +import { createFromRequest } from './reporter'; +import { AppConfig } from './config'; +import { isUserError, reportUserError } from './userError'; +import { Settings, ServerConfig } from '../common/adminInterfaces'; +import { getIP } from './originUtils'; +import { IAccount } from './db'; + +const ROLLBAR_IP = '35.184.69.251'; + +export const notFound: RequestHandler = (_, res) => { + res.setHeader('Cache-Control', 'public, max-age=0'); + res.sendStatus(404); +}; + +export const validAccount = (server: ServerConfig): RequestHandler => (req, res, next) => { + const account = req.user as IAccount | undefined; + const accountId = req.body.accountId as string; + const accountName = req.body.accountName as string; + + if (!account || account.id !== accountId) { + if (!/#$/.test(accountId)) { + createFromRequest(server, req).warn(ACCOUNT_ERROR, `${accountName} [${accountId}] (${req.path})`); + } + //logger.warn(ACCOUNT_ERROR, `${accountName} [${accountId}] (${req.path})`); + res.status(403).json({ error: ACCOUNT_ERROR }); + } else { + next(null); + } +}; + +export const blockMaps = (debug: boolean, local: boolean): RequestHandler => (req, res, next) => { + if (!debug && !local && /\.map$/.test(req.path) && getIP(req) !== ROLLBAR_IP) { + res.sendStatus(404); + } else { + next(null); + } +}; + +export const hash: RequestHandler = (req, res, next) => { + const apiVersion = req.get('api-version'); + + if (apiVersion !== HASH) { + res.status(400).json({ error: VERSION_ERROR }); + } else { + next(null); + } +}; + +export const offline = (settings: Settings): RequestHandler => (_req, res, next) => { + if (settings.isPageOffline) { + res.status(503).send(OFFLINE_ERROR); + } else { + next(null); + } +}; + +export const internal = (config: AppConfig, server: ServerConfig): RequestHandler => (req, res, next) => { + if (req.get('api-token') === config.token) { + next(null); + } else { + createFromRequest(server, req).warn('Unauthorized internal api call', req.originalUrl); + res.sendStatus(403); + } +}; + +export const auth: RequestHandler = (req, res, next) => { + if (req.isAuthenticated()) { + next(null); + } else { + //createFromRequest(req).warn('Unauthorized access', req.originalUrl); + res.setHeader('X-Robots-Tag', 'noindex'); + res.sendStatus(403); + } +}; + +export const admin = (server: ServerConfig): RequestHandler => (req, res, next) => { + if (req.isAuthenticated() && req.user && isAdmin(req.user)) { + next(null); + } else { + if (!/Googlebot/.test(req.get('User-Agent')!)) { + createFromRequest(server, req).warn(`Unauthorized access (admin)`, req.originalUrl); + } + + res.setHeader('X-Robots-Tag', 'noindex, nofollow'); + res.sendStatus(403); + } +}; + +const store = new ExpressBrute.MemoryStore(); + +export function limit(freeRetries: number, lifetime: number) { + const options: any = { + freeRetries, + lifetime, + failCallback(req: Request, res: Response, _next: NextFunction, nextValidRequestDate: any) { + logger.warn(`rate limit ${req.url} ${req.ip}`); + res.status(429).send(`Too many requests, please try again ${moment(nextValidRequestDate).fromNow()}`); + } + }; + + return (new (ExpressBrute as any)(store, options)).prevent; +} + +function reportError(e: Error, server: ServerConfig, req: Request) { + createFromRequest(server, req).danger(`Req error: ${e.message}`, `${req.method.toUpperCase()} ${req.originalUrl}`); + logger.error(e); +} + +export function handleError(server: ServerConfig, req: Request, res: Response) { + return (e: Error) => { + if (isUserError(e)) { + reportUserError(e, server, req); + res.status(422).json({ error: e.message, userError: true }); + } else { + reportError(e, server, req); + res.status(500).json({ error: 'Error occurred' }); + } + }; +} + +let logRequest: (req: Request, result: any, url?: string) => void = noop; + +export function initLogRequest(func: typeof logRequest) { + logRequest = func; +} + +export function handleJSON(server: ServerConfig, req: Request, res: Response, result: any): any { + Promise.resolve(result) + .then(result => { + res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0'); + res.json(result); + return result; + }) + .then(result => logRequest(req, result)) + .catch(handleError(server, req, res)); +} + +export function wrap(server: ServerConfig, handle: (req: Request) => any): RequestHandler { + return (req, res) => handleJSON(server, req, res, handle(req)); +} + +export function wrapApi(server: ServerConfig, api: any) { + return wrap(server, ({ body: { method, args = [] } }) => { + if (api[method]) { + return api[method](...args); + } else { + return Promise.reject(new Error(`Invalid method (${method})`)); + } + }); +} + +interface StaticFile { + buffer: Buffer; + mimeType: string; +} + +function readFiles(files: Map, dir: string, url: string) { + const mimeTypes: any = { + '.js': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.png': 'image/png', + '.jpg': 'image/jpeg', + }; + + for (const file of fs.readdirSync(dir)) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + readFiles(files, filePath, `${url}/${file}`); + } else { + const ext = path.extname(file); + const mimeType = mimeTypes[ext]; + + if (mimeType) { + const buffer = fs.readFileSync(filePath); + files.set(`${url}/${file}`, { mimeType, buffer }); + } + } + } +} + +export function inMemoryStaticFiles(assetsPath: string, assetsUrl: string, maxAge: number): RequestHandler { + const staticFiles = new Map(); + const cacheControl = `public, max-age=${Math.floor(maxAge / 1000)}`; + readFiles(staticFiles, assetsPath, assetsUrl); + + return (req, res, next) => { + if (req.method !== 'GET' && req.method !== 'HEAD') { + return next(); + } + + const staticFile = staticFiles.get(req.path); + + if (!staticFile) { + return next(); + } + + try { + res.setHeader('Content-Type', staticFile.mimeType); + res.setHeader('Cache-Control', cacheControl); + res.status(200).end(staticFile.buffer); + } catch (e) { + next(e); + } + }; +} diff --git a/src/ts/server/routes/api-account.ts b/src/ts/server/routes/api-account.ts new file mode 100644 index 0000000..a3bfb3a --- /dev/null +++ b/src/ts/server/routes/api-account.ts @@ -0,0 +1,36 @@ +import { Router } from 'express'; +import { offline as createOffline, validAccount as createValidAccount, hash, wrap, limit } from '../requestUtils'; +import { + createUpdateAccount, createRemoveSite, createUpdateSettings, createGetAccountCharacters, removeHide, getHides, getFriends +} from '../api/account'; +import { findAccountSafe, findAuth, findAllCharacters, countAllVisibleAuths, IAccount } from '../db'; +import { system } from '../logger'; +import { Settings, ServerConfig } from '../../common/adminInterfaces'; + +export default function (server: ServerConfig, settings: Settings) { + const validAccount = createValidAccount(server); + const offline = createOffline(settings); + const app = Router(); + + const getAccountCharacters = createGetAccountCharacters(findAllCharacters); + const updateAccount = createUpdateAccount(findAccountSafe, system); + const updateSettings = createUpdateSettings(findAccountSafe); + const removeSite = createRemoveSite(findAuth, countAllVisibleAuths, system); + + app.post('/account-characters', offline, hash, validAccount, limit(60, 60), wrap(server, req => + getAccountCharacters(req.user as IAccount))); + app.post('/account-update', offline, hash, validAccount, limit(60, 60), wrap(server, req => + updateAccount(req.user as IAccount, req.body.account))); + app.post('/account-settings', offline, hash, validAccount, limit(60, 60), wrap(server, req => + updateSettings(req.user as IAccount, req.body.settings))); + app.post('/remove-site', offline, hash, validAccount, limit(60, 60), wrap(server, req => + removeSite(req.user as IAccount, req.body.siteId))); + app.post('/remove-hide', offline, hash, validAccount, limit(60, 60), wrap(server, req => + removeHide(req.user as IAccount, req.body.hideId))); + app.post('/get-hides', offline, hash, validAccount, limit(60, 60), wrap(server, req => + getHides(req.user as IAccount, req.body.page || 0))); + app.post('/get-friends', offline, hash, validAccount, limit(120, 60), wrap(server, req => + getFriends(req.user as IAccount))); + + return app; +} diff --git a/src/ts/server/routes/api-game.ts b/src/ts/server/routes/api-game.ts new file mode 100644 index 0000000..009b6d0 --- /dev/null +++ b/src/ts/server/routes/api-game.ts @@ -0,0 +1,35 @@ +import { Router } from 'express'; +import { limit, offline as createOffline, wrap, validAccount as createValidAccount, hash } from '../requestUtils'; +import { createJoinGame, Config } from '../api/game'; +import { findServer } from '../internal'; +import { findCharacter, hasActiveSupporterInvites, IAccount } from '../db'; +import { createJoin } from '../internal'; +import { Settings, ServerConfig } from '../../common/adminInterfaces'; +import { getOrigin, addOrigin } from '../originUtils'; + +export default function (server: ServerConfig, settings: Settings, config: Config) { + const offline = createOffline(settings); + const validAccount = createValidAccount(server); + const join = createJoin(); + const app = Router(); + + let inQueue = 0; + + const joinGame = createJoinGame(findServer, config, findCharacter, join, addOrigin, hasActiveSupporterInvites); + + app.post('/game/join', offline, limit(60, 5 * 60), hash, validAccount, wrap(server, async req => { + if (inQueue > 100) { + return {}; + } else { + try { + inQueue++; + const { ponyId, serverId, version, url, alert } = req.body; + return await joinGame(req.user as IAccount, ponyId, serverId, version, url, alert, getOrigin(req)); + } finally { + inQueue--; + } + } + })); + + return app; +} diff --git a/src/ts/server/routes/api-pony.ts b/src/ts/server/routes/api-pony.ts new file mode 100644 index 0000000..6bed886 --- /dev/null +++ b/src/ts/server/routes/api-pony.ts @@ -0,0 +1,37 @@ +import { Router } from 'express'; +import { offline as createOffline, validAccount as createValidAccount, hash, wrap } from '../requestUtils'; +import { createFromRequest } from '../reporter'; +import { createSavePony, createRemovePony } from '../api/pony'; +import { findAuth, findCharacter, characterCount, createCharacter, removeCharacter, IAccount } from '../db'; +import { updateCharacterCount } from '../accountUtils'; +import { system } from '../logger'; +import { kickFromAllServersByCharacter } from '../api/admin'; +import { createIsSuspiciousName, createIsSuspiciousPony } from '../../common/security'; +import { Settings, ServerConfig } from '../../common/adminInterfaces'; +import { RemovedDocument } from '../internal'; +import { logRemovedCharacter } from '../characterUtils'; + +export default function (server: ServerConfig, settings: Settings, removedDocument: RemovedDocument) { + const offline = createOffline(settings); + const validAccount = createValidAccount(server); + const app = Router(); + + const isSuspiciousName = createIsSuspiciousName(settings); + const isSuspiciousPony = createIsSuspiciousPony(settings); + + const savePonyHandler = createSavePony( + findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, system, + isSuspiciousName, isSuspiciousPony); + + const removePonyHandler = createRemovePony( + kickFromAllServersByCharacter, removeCharacter, updateCharacterCount, + id => removedDocument('ponies', id), logRemovedCharacter); + + app.post('/pony/save', offline, hash, validAccount, wrap(server, req => + savePonyHandler(req.user as IAccount, req.body.pony, createFromRequest(server, req)))); + + app.post('/pony/remove', offline, hash, validAccount, wrap(server, req => + removePonyHandler(req.body.id, (req.user as IAccount).id))); + + return app; +} diff --git a/src/ts/server/routes/api-tools.ts b/src/ts/server/routes/api-tools.ts new file mode 100644 index 0000000..74d7423 --- /dev/null +++ b/src/ts/server/routes/api-tools.ts @@ -0,0 +1,95 @@ +import { Router } from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; +import { repeat } from 'lodash'; +import { randomString } from '../../common/stringUtils'; +import { execAsync } from '../serverUtils'; +import { offline as createOffline, handleJSON, auth } from '../requestUtils'; +import * as paths from '../paths'; +import { findAllCharacters, IAccount } from '../db'; +import { createGetAccountCharacters } from '../api/account'; +import { Settings, ServerConfig } from '../../common/adminInterfaces'; +import { World } from '../world'; +import { ToolsMapInfo } from '../../components/tools/tools-map/tools-map'; +import { flatten } from '../../common/utils'; +import { serializeMap } from '../serverMap'; + +export default function (server: ServerConfig, settings: Settings, world: World | undefined) { + const offline = createOffline(settings); + const app = Router(); + + app.use(auth); + + app.get('/ponies', offline, (req, res) => { + handleJSON(server, req, res, createGetAccountCharacters(findAllCharacters)(req.user as IAccount)); + }); + + app.get('/animation/:id', offline, (req, res) => { + const filePath = path.join(paths.store, req.params.id); + + res.sendFile(filePath); + }); + + app.post('/animation', offline, (req, res) => { + const name = randomString(10); + const filePath = path.join(paths.store, name); + + fs.writeFileAsync(filePath, req.body.animation, 'utf8') + .then(() => res.send({ name })); + }); + + app.post('/animation-gif', offline, (req, res) => { + const image: string = req.body.image; + const width: number = req.body.width || 80; + const height: number = req.body.height || 80; + const fps: number = req.body.fps || 24; + const remove: number = req.body.remove || 0; + + const name = randomString(10); + const filePath = path.join(paths.store, name + '.png'); + const header = 'data:image/gif;base64,'; + const buffer = Buffer.from(image.substr(header.length), 'base64'); + const magick = /^win/.test(process.platform) ? 'magick' : 'convert'; + const command = `${magick} -dispose 3 -delay ${100 / fps} -loop 0 "${filePath}" -crop ${width}x${height} ` + + `+repage${repeat(' +delete', remove)} "${filePath.replace(/png$/, 'gif')}"`; + + fs.writeFileAsync(filePath, buffer) + .then(() => execAsync(command)) + .then(() => res.send({ name })); + }); + + app.get('/maps', offline, (_, res) => { + if (world) { + res.json(world.maps.map(m => m.id)); + } else { + res.sendStatus(400); + } + }); + + app.get('/map', offline, (req, res) => { + if (world) { + const id = req.query.map || ''; + const map = world.maps.find(m => m.id === id); + + if (map) { + const mapInfo: ToolsMapInfo = { + ...serializeMap(map), + defaultTile: map.defaultTile, + type: map.type, + info: { + season: world.season, + entities: flatten(map.regions.map(r => r.entities)) + .map(({ type, x, y, order, id }) => ({ type, x, y, order, id })), + }, + }; + + res.json(mapInfo); + return; + } + } + + res.sendStatus(400); + }); + + return app; +} diff --git a/src/ts/server/routes/api.ts b/src/ts/server/routes/api.ts new file mode 100644 index 0000000..e6192ed --- /dev/null +++ b/src/ts/server/routes/api.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { auth } from '../requestUtils'; +import { Settings, ServerConfig } from '../../common/adminInterfaces'; +import { RemovedDocument } from '../internal'; +import { Config } from '../api/game'; +import apiAccount from './api-account'; +import apiPony from './api-pony'; +import apiGame from './api-game'; + +export default function (server: ServerConfig, settings: Settings, config: Config, removedDocument: RemovedDocument) { + const app = Router(); + + app.use(auth); + + app.use(apiAccount(server, settings)); + app.use(apiPony(server, settings, removedDocument)); + app.use(apiGame(server, settings, config)); + + return app; +} diff --git a/src/ts/server/routes/api1.ts b/src/ts/server/routes/api1.ts new file mode 100644 index 0000000..017b198 --- /dev/null +++ b/src/ts/server/routes/api1.ts @@ -0,0 +1,60 @@ +import { Router } from 'express'; +import { noop } from 'lodash'; +import { findAllCharacters, findAllVisibleAuths, IAccount, Account } from '../db'; +import { offline, hash, handleJSON } from '../requestUtils'; +import { createGetAccountData } from '../api/account'; +import { Settings, ServerConfig } from '../../common/adminInterfaces'; +import { includes } from '../../common/utils'; + +const blockApps: string[] = []; +const MAX_CONCURRENT_REQUESTS = 100; +let requests = 0; + +export default function (server: ServerConfig, settings: Settings) { + const app = Router(); + + const getAccountData = createGetAccountData(findAllCharacters, findAllVisibleAuths); + + async function handleAccountRequest(account: IAccount, userAgent?: string, browserId?: string) { + if (requests < MAX_CONCURRENT_REQUESTS) { + requests++; + + try { + const lastUserAgent = userAgent || account.lastUserAgent; + const lastBrowserId = browserId || account.lastBrowserId; + + if ((lastUserAgent && account.lastUserAgent !== lastUserAgent) || + (lastBrowserId && account.lastBrowserId !== lastBrowserId)) { + account.lastUserAgent = lastUserAgent; + account.lastBrowserId = lastBrowserId; + Account.updateOne({ _id: account._id }, { lastUserAgent, lastBrowserId }, noop); + } + + return await getAccountData(account); + } finally { + requests--; + } + } else { + return { limit: true }; + } + } + + app.post('/account', offline(settings), hash, (req, res) => { + req.session!.touch(); + + let account = req.user as IAccount | undefined; + const browserId = req.get('Api-Bid'); + const userAgent = req.get('User-Agent') || ''; + const requestedWith = req.get('X-Requested-With'); + const isWebViewUserAgent = /Chrome\/\d+\.0\.0\.0 Mobile|; wv\)/.test(userAgent); + const isWebView = requestedWith || isWebViewUserAgent; + + if (!account || (settings.blockWebView && isWebView && includes(blockApps, requestedWith))) { + handleJSON(server, req, res, null); + } else { + handleJSON(server, req, res, handleAccountRequest(account, userAgent, browserId)); + } + }); + + return app; +} diff --git a/src/ts/server/routes/api2.ts b/src/ts/server/routes/api2.ts new file mode 100644 index 0000000..9a22ca7 --- /dev/null +++ b/src/ts/server/routes/api2.ts @@ -0,0 +1,71 @@ +import { Router } from 'express'; +import { GameStatus, ServerInfo, ServerInfoShort } from '../../common/interfaces'; +import { InternalGameServerState, Settings, ServerLiveSettings } from '../../common/adminInterfaces'; +import { offline } from '../requestUtils'; +import { servers } from '../internal'; +import { version } from '../config'; +import { isServerOffline } from '../serverUtils'; +import { StatsTracker } from '../stats'; +import { MIN_ADULT_AGE } from '../../common/constants'; + +function isServerSafe(server: InternalGameServerState) { + return server.state.alert !== '18+'; +} + +function toServerState(server: InternalGameServerState): ServerInfo { + const { name, path, desc, flag, alert, online, settings, require, host } = server.state; + + return { + id: server.id, + name, + path, + desc, + host, + flag, + alert, + dead: false, + online, + offline: isServerOffline(server), + filter: !!settings.filterSwears, + require, + }; +} + +function toServerStateShort(server: InternalGameServerState): ServerInfoShort { + return { + id: server.id, + online: server.state.online, + offline: isServerOffline(server), + }; +} + +function getGameStatus( + servers: InternalGameServerState[], live: ServerLiveSettings, short: boolean, age: number +): GameStatus { + const adult = age >= MIN_ADULT_AGE; + + return { + version, + update: live.updating ? true : undefined, + servers: servers + .filter(s => isServerSafe(s) || adult) + .map(short ? toServerStateShort : toServerState), + }; +} + +export default function (settings: Settings, live: ServerLiveSettings, statsTracker: StatsTracker) { + const app = Router(); + + app.get('/game/status', offline(settings), (req, res) => { + const status = getGameStatus(servers, live, req.query.short === 'true', req.query.d | 0); + res.json(status); + statsTracker.logRequest(req, status); + }); + + app.post('/csp', offline(settings), (_, res) => { + //logger.warn('CSP report', getIPFromRequest(req), req.body['csp-report']); + res.sendStatus(200); + }); + + return app; +} diff --git a/src/ts/server/routes/auth.ts b/src/ts/server/routes/auth.ts new file mode 100644 index 0000000..bd18565 --- /dev/null +++ b/src/ts/server/routes/auth.ts @@ -0,0 +1,317 @@ +import { Router, Request, Response, RequestHandler } from 'express'; +import { use, authenticate, AuthenticateOptions } from 'passport'; +import { Strategy as LocalStrategy } from 'passport-local'; +import { remove } from 'lodash'; +import { MINUTE } from '../../common/constants'; +import { fromNow, hasFlag, includes } from '../../common/utils'; +import { BannedMuted, Settings, ServerConfig, AccountFlags, ServerLiveSettings } from '../../common/adminInterfaces'; +import { Account, IAccount, Origin, IOrigin } from '../db'; +import { limit, auth as authRequest, wrap } from '../requestUtils'; +import { CreateAccountOptions, findOrCreateAccount, SuspiciousCheckers, getAccountAlertMessage } from '../accountUtils'; +import { create, createFromRequest } from '../reporter'; +import { logger, logServer, system } from '../logger'; +import { providers, getProfile } from '../oauth'; +import { accountChanged, RemovedDocument } from '../internal'; +import { UserError, isUserError, reportUserError } from '../userError'; +import { kickFromAllServers } from '../api/admin'; +import { createIsSuspiciousName, createIsSuspiciousAuth } from '../../common/security'; +import { isBanned, isActive } from '../../common/adminUtils'; +import { mergeAccounts } from '../api/merge'; +import { findOrCreateAuth } from '../authUtils'; +import { getOriginFromHTTP, getOrigin, addOrigin } from '../originUtils'; +import { Profile } from '../../common/interfaces'; + +interface MergeRequest { + accountId: string; + time: number; +} + +const FRESH_ACCOUNT_TIME = 1 * MINUTE; +const mergeRequests: MergeRequest[] = []; + +/* tslint:disable */ +const ignoreErrors = [ + 'Service unavailable', // replacement for twitter HTTP error + 'Internal error', + 'User denied your request', + 'Code was already redeemed.', + 'Code is invalid or expired.', + 'This authorization code has expired.', + 'This authorization code has been used.', + 'Failed to fetch user profile', + 'Failed to obtain access token', + 'Failed to find request token in session', + 'User authorization failed: user is deactivated.', + 'User authorization failed: user revoke access for this token.', + 'Backend Error', + 'TokenError', + 'Bad Request', + 'Rate limit exceeded', + `Sorry, this feature isn't available right now: An error occurred while processing this request. Please try again later.`, + 'Przepraszamy, ta funkcja nie jest obecnie dostępna: Podczas przetwarzania żądania wystąpił błąd. Spróbuj ponownie później.', + 'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже', + 'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже.', + 'Desculpe, esse recurso não está disponível no momento: Ocorreu um erro ao processar essa solicitação. Tente novamente mais tarde.', + 'Esta aplicación no está disponible: La aplicación que intentas usar ya no está disponible o tiene el acceso restringido.', + 'Xin lỗi, tính năng này không khả dụng ngay bây giờ: Đã xảy ra lỗi khi xử lý yêu cầu này. Vui lòng thử lại sau.', + 'Lo sentimos, esta función no está disponible ahora: Ocurrió un error mientras se procesaba la solicitud. Vuelve a intentarlo más tarde.', + `The access token is invalid since the user hasn't engaged the app in longer than 90 days.`, + `Application Unavailable: The application you're trying to use is either no longer available or access is restricted.`, + 'An unexpected error has occurred. Please retry your request later.', + 'Code was invalid or expired. ', + 'Internal server error: could not check access_token now, check later.', + 'failed to fetch user profile', + 'Failed to obtain request token', + 'User canceled the Dialog flow', + 'Internal Error', + 'Bad Authentication data.', + 'Diese Function ist vorübergehend nicht verfügbar', + 'Diese Funktion ist vorübergehend nicht verfügbar', + 'User authorization failed: no access_token passed.', + 'Ungültiges Anfrage-Token.', + 'Invalid Credentials', + 'Invalid code.', + 'Internal server error: Database problems, try later', + 'An invalid Platform session was found.: An invalid Platform session was found.', + `Cannot read property 'id' of undefined`, // patreon error + 'User Rate Limit Exceeded. Rate of requests for user exceed configured project quota. You may consider re-evaluating expected per-user traffic to the API and adjust project quota limits accordingly. You may monitor aggregate quota usage and adjust limits in the API Console: https://console.developers.google.com/apis/api/plus.googleapis.com/quotas?project=200390553857', +]; + +function kickCurrentUser(req: Request) { + const user = req.user as IAccount | undefined; + + if (user) { + kickFromAllServers(user.id) + .catch(e => logger.error(e)); + } +} + +function logIn(req: Request, account: IAccount) { + return new Promise((resolve, reject) => { + kickCurrentUser(req); + req.logIn(account, e => e ? reject(e) : resolve()); + }); +} + +function isMerge(accountId: string) { + const minTime = fromNow(-10 * MINUTE).getTime(); + remove(mergeRequests, r => r.time < minTime); + return mergeRequests.some(r => r.accountId === accountId); +} + +function getIP(req: Request) { + return req.ip || req.ips[0]; +} + +function reportError(server: ServerConfig, message: string, e: Error, req: Request) { + createFromRequest(server, req).danger(message, e.toString()); + logger.error(message, e); +} + +function fixTwitterErrorMessage(message: string) { + return /^/.test(message) ? 'Service unavailable' : message; +} + +async function checkBanField( + server: ServerConfig, account: IAccount, field: keyof BannedMuted, message: string, origin: IOrigin +) { + if (isActive(origin[field]) && !isActive(account[field])) { + create(server, account._id, undefined, origin).warn(message); + account[field] = origin[field]; + await account.save(); + } +} + +async function loginUser(server: ServerConfig, req: Request, res: Response, account: IAccount) { + const origin = await Origin.findOne({ ip: getIP(req) }).exec(); + + await addOrigin(account, getOrigin(req)); + + if (origin) { + await checkBanField(server, account, 'mute', 'Muted account by origin', origin); + await checkBanField(server, account, 'shadow', 'Shadowed account by origin', origin); + await checkBanField(server, account, 'ban', 'Banned account by origin', origin); + } + + if (isBanned(account)) { + // const message = isTemporarilyBanned(account) ? `Account locked()` : 'Account locked'; + throw new UserError('Account locked', undefined, getAccountAlertMessage(account)); + } + + await logIn(req, account); + await accountChanged(account._id.toString()); + + const isFresh = account.createdAt && account.createdAt.getTime() > fromNow(-FRESH_ACCOUNT_TIME).getTime(); + res.redirect(isFresh ? '/account' : '/'); +} + +async function mergeUser(req: Request, res: Response, account: IAccount, removedDocument: RemovedDocument) { + const user = req.user as IAccount; + const userId = user._id.toString(); + const accountId = account._id.toString(); + + remove(mergeRequests, r => r.accountId === userId); + + if (userId !== accountId) { + await mergeAccounts(userId, accountId, 'by user', removedDocument, false); + } + + res.redirect('/account?merged=true'); +} + +function handleErrorAndRedirect( + server: ServerConfig, url: string, message: string, e: Error, req: Request, res: Response +) { + if (isUserError(e)) { + reportUserError(e, server, req); + url += `?error=${encodeURIComponent(e.message)}`; + + if (e.userInfo) { + url += `&alert=${encodeURIComponent(e.userInfo)}`; + } + } else { + reportError(server, `Auth error: ${message}`, e, req); + url += `?error=${encodeURIComponent(message)}`; + } + + res.redirect(url); +} + +async function handleAuth( + server: ServerConfig, live: ServerLiveSettings, removedDocument: RemovedDocument, + req: Request, res: Response, error: Error | null, account: IAccount | null, +) { + const user = req.user as IAccount | undefined; + const merge = isMerge(user && user.id); + + try { + if (error) { + if (isUserError(error)) { + throw error; + } + + const message = fixTwitterErrorMessage(error.message); + const ignore = includes(ignoreErrors, message); + throw new UserError(message, ignore ? undefined : { error, desc: `url: ${req.path}` }); + } + + if (!account) { + throw new UserError('No account'); + } + + if (merge && !hasFlag(account.flags, AccountFlags.BlockMerging)) { + if (live.shutdown) { + throw new Error(`Cannot merge while server is shutdown`); + } + + await mergeUser(req, res, account, removedDocument); + } else { + await loginUser(server, req, res, account); + } + } catch (e) { + const message = merge ? 'Account merge error' : 'Authentication error'; + handleErrorAndRedirect(server, merge ? '/account' : '/', message, e, req, res); + } +} + +function createHandler( + server: ServerConfig, live: ServerLiveSettings, id: string, options: AuthenticateOptions, + removedDocument: RemovedDocument +): RequestHandler { + return (req, res, next) => { + const handler = authenticate(id, options, (error: Error | null, account: IAccount | null) => + handleAuth(server, live, removedDocument, req, res, error, account)); + + return handler(req, res, next); + }; +} + +export function authRoutes( + host: string, server: ServerConfig, settings: Settings, live: ServerLiveSettings, mockLogin: boolean, + removedDocument: RemovedDocument +) { + const failureRedirect = `/?error=${encodeURIComponent('Authentication failed')}`; + const app = Router(); + const checkers: SuspiciousCheckers = { + isSuspiciousName: createIsSuspiciousName(settings), + isSuspiciousAuth: createIsSuspiciousAuth(settings), + }; + + providers.filter(p => !!p.auth).forEach(({ id, strategy, auth, connectOnly, additionalOptions = {} }) => { + const callbackURL = `${host}auth/${id}/callback`; + const scope = id === 'patreon' ? ['users'] : ['email']; + const options = { + ...additionalOptions, + ...auth, + callbackURL, + includeEmail: true, + profileFields: ['id', 'displayName', 'name', 'emails'], + passReqToCallback: true, + }; + + async function signInOrSignUp(req: Request, profile: Profile) { + const user = req.user as IAccount | undefined; + const userId = user && user._id.toString(); + const mergeAccount = (userId && isMerge(userId)) ? userId : undefined; + const createAccountOptions = createOptions(req, !!connectOnly, server, settings, checkers); + const auth = await findOrCreateAuth(profile, mergeAccount, createAccountOptions); + const account = await findOrCreateAccount(auth, profile, createAccountOptions); + const { ip, userAgent } = createAccountOptions; + system(account._id, `signed-in with "${auth.name}" [${auth._id}] [${ip}] [${userAgent}]`); + return account; + } + + use(id, new strategy(options, (req, _accessToken, _refreshToken, oauthProfile, callback) => { + const profile = getProfile(id, oauthProfile); + + signInOrSignUp(req, profile) + .then(account => { + callback(null, account); + }) + .catch((error: Error) => { + logServer(`failed to sign-in ${JSON.stringify(profile)}`); + callback(error, null); + }); + })); + + app.get(`/${id}`, limit(120, 3600), createHandler(server, live, id, { scope, failureRedirect }, removedDocument)); + app.get(`/${id}/callback`, limit(120, 3600), createHandler(server, live, id, { failureRedirect }, removedDocument)); + app.get(`/${id}/merge`, limit(120, 3600), authRequest, (req, res) => { + const accountId = (req.user as IAccount)._id.toString(); + mergeRequests.push({ accountId, time: Date.now() }); + res.redirect(`/auth/${id}`); + }); + }); + + app.post('/sign-out', wrap(server, req => { + kickCurrentUser(req); + req.logout(); + return { success: true }; + })); + + if (mockLogin) { + use(new LocalStrategy((login, _pass, done) => Account.findById(login, done))); + app.get('/local', authenticate('local', { successRedirect: '/', failureRedirect: '/failed-login' })); + } + + return app; +} + +function createOptions( + req: Request, connectOnly: boolean, server: ServerConfig, settings: Settings, checkers: SuspiciousCheckers +): CreateAccountOptions { + const acl = req.cookies && req.cookies.acl; + const origin = getOriginFromHTTP(req); + + return { + ip: getIP(req), + userAgent: req.get('User-Agent'), + browserId: req.get('Api-Bid'), + connectOnly: !!connectOnly, + creationLocked: acl && acl > (new Date()).toISOString(), + canCreateAccounts: !!settings.canCreateAccounts, + reportPotentialDuplicates: !!settings.reportPotentialDuplicates, + warn: (accountId, message, desc) => create(server, accountId, undefined, origin).warn(message, desc), + ...checkers, + }; +} diff --git a/src/ts/server/routes/index.ts b/src/ts/server/routes/index.ts new file mode 100644 index 0000000..13c0dec --- /dev/null +++ b/src/ts/server/routes/index.ts @@ -0,0 +1,167 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { compileFile } from 'pug'; +import { RequestHandler } from 'express'; +import { ClientOptions, Server, writeObject } from 'ag-sockets'; +import { OAuthProvider } from '../../common/interfaces'; +import { providers, OAuthProviderInfo } from '../oauth'; +import { config, version, description } from '../config'; +import { TokenData } from '../serverInterfaces'; +import { logger } from '../logger'; +import { pathTo } from '../paths'; +import { writeBinary } from '../../common/binaryUtils'; + +interface RevFile { + name: string; + path: string; + url: string; +} + +interface PageOptions { + isPublic?: boolean; + production: boolean; + base: string; + assets?: string; + style: string; + script: string; + scriptES: string; + token?: string; + noindex?: boolean; + socketOptions?: ClientOptions; + webpack?: boolean; + local?: boolean; +} + +function getFiles(urlBase: string, dir: string, sub: string): RevFile[] { + try { + return fs.readdirSync(path.join(dir, sub)) + .filter(file => /\.(js|css|png)$/.test(file)) + .map(file => ({ + name: file.replace(/-[a-f0-9]{10}\.(js|css|png)$/, '.$1'), + path: path.join(dir, sub, file), + url: `${urlBase}/${sub}/${file}`, + })); + } catch { + return []; + } +} + +export function createIndex(assetsPath: string, adminAssetsPath: string) { + function toOAuthProvider({ id, name, color, auth, connectOnly }: OAuthProviderInfo): OAuthProvider { + return { id, name, color, disabled: auth ? undefined : true, connectOnly }; + } + + const revServer = new Map(); + + [ + ...getFiles('assets', assetsPath, 'styles'), + ...getFiles('assets', assetsPath, 'scripts'), + ...getFiles('assets', assetsPath, 'images'), + ...getFiles('assets-admin', adminAssetsPath, 'styles'), + ...getFiles('assets-admin', adminAssetsPath, 'scripts'), + ].forEach(file => revServer.set(file.name, file)); + + function revUrlGetter(dir: string) { + return (name: string) => { + const file = revServer.get(name); + return file && file.url || `assets/${dir}/${name}`; + }; + } + + function getRevPath(name: string) { + return (revServer.get(name) && revServer.get(name)!.path) || path.join(assetsPath, name); + } + + const getRevScriptURL = revUrlGetter('scripts'); + const getRevStyleURL = revUrlGetter('styles'); + const getRevImageURL = revUrlGetter('images'); + + const template = compileFile(pathTo('views', 'index.pug')); + const inlineStyle = fs.readFileSync(getRevPath('style-inline.css'), 'utf8'); + const loadingImage = fs.readFileSync(getRevPath('logo-gray.png')); + const oauthProviders = providers.map(toOAuthProvider); + + function encodeSocketOptions(options: ClientOptions | undefined): string { + if (options) { + const data = writeBinary(writer => writeObject(writer, options)); + const buffer = Buffer.from(data); + return buffer.toString('base64'); + } else { + return ''; + } + } + + function renderPage( + { isPublic, style, script, scriptES, production, noindex, base, socketOptions, token, local }: PageOptions + ) { + return template({ + doctype: 'html', + host: config.host, + title: config.title, + twitterLink: config.twitterLink, + supporterLink: config.supporterLink, + email: config.contactEmail, + logo: `${config.host}${getRevImageURL('logo-120.png')}`, + loadingImage: `data:image/png;base64,${loadingImage.toString('base64')}`, + version, + description, + base, + token, + sw: config.sw ? 'true' : undefined, + noindex: noindex || config.noindex, + production, + local: local ? 'true' : undefined, + socketOptions: encodeSocketOptions(socketOptions), + inlineStyle, + style, + script, + scriptES, + oauthProviders, + facebookAppId: config.facebookAppId, + isPublic: isPublic ? 'true' : undefined, + }); + } + + function admin( + production: boolean, base: string, assetsBase: string, scriptName: string, socket: Server + ): RequestHandler { + const socketOptions = socket.options(); + const style = `${assetsBase}/${getRevStyleURL('style-admin.css')}`; + const script = `${assetsBase}/${getRevScriptURL(scriptName)}`; + const scriptES = script; + + return (req, res) => { + try { + const token = socket.token({ account: req.user } as TokenData); + res.send(renderPage({ production, base, style, script, scriptES, noindex: true, socketOptions, token })); + } catch (e) { + logger.error(e); + res.sendStatus(500); + } + }; + } + + function user( + production: boolean, base: string, styleName: string, scriptName: string, scriptESName: string, + socketOptions: ClientOptions | undefined, noindex: boolean, local: boolean, isPublic: boolean, + ) { + const style = `/${getRevStyleURL(styleName)}`; + const script = `/${getRevScriptURL(scriptName)}`; + const scriptES = `/${getRevScriptURL(scriptESName)}`; + const sprites1 = DEVELOPMENT ? `/assets/images/pony.png` : `/${getRevImageURL('pony.png')}`; + const sprites2 = DEVELOPMENT ? `/assets/images/pony2.png` : `/${getRevImageURL('pony2.png')}`; + + const page = renderPage({ isPublic, production, base, style, script, scriptES, socketOptions, noindex, local }); + + const preload = [ + `<${script}>; rel=preload; as=script`, + `<${style}>; rel=preload; as=style`, + `<${sprites1}>; rel=preload; as=fetch; crossorigin`, + `<${sprites2}>; rel=preload; as=fetch; crossorigin`, + ]; + + return { page, preload }; + } + + return { admin, user, getRevScript: getRevScriptURL, getRevStyle: getRevStyleURL }; +} diff --git a/src/ts/server/server.ts b/src/ts/server/server.ts new file mode 100644 index 0000000..a303a3b --- /dev/null +++ b/src/ts/server/server.ts @@ -0,0 +1,420 @@ +import './boot'; +import * as fs from 'fs'; +import * as Bluebird from 'bluebird'; +import * as mongoose from 'mongoose'; +import * as http from 'http'; +import * as morgan from 'morgan'; +import * as bodyParser from 'body-parser'; +import * as expressSession from 'express-session'; +import * as serveFavicon from 'serve-favicon'; +import * as Rollbar from 'rollbar'; +import * as passport from 'passport'; +import * as connectMongo from 'connect-mongo'; +import * as express from 'express'; +// import { WebSocketServer } from '@clusterws/cws'; +import { WebSocketServer } from 'clusterws-uws'; +import { compact, once } from 'lodash'; +import { copySync, removeSync, ensureDirSync } from 'fs-extra'; +import { createServerHost, createClientOptions, ServerOptions, ClientExtensions, Packet } from 'ag-sockets'; +import { config, port, server, args, version } from './config'; +import { YEAR, WEEK } from '../common/constants'; +import { rollbarCheckIgnore } from '../common/rollbar'; +import { isBanned } from '../common/adminUtils'; +import { includes } from '../common/utils'; +import { STAMP } from '../generated/hash'; +import { ClientActions } from '../client/clientActions'; +import { ClientAdminActions } from '../client/clientAdminActions'; +import { ServerActions } from './serverActions'; +import { AdminServerActions } from './adminServerActions'; +import { IAccount, Account } from './db'; +import { logger } from './logger'; +import { settings, reloadSettings } from './settings'; +import { SocketErrorHandler } from './utils/socketErrorHandler'; +import { tokenService } from './serverUtils'; +import { + admin as isAdmin, auth, blockMaps, wrapApi, internal, initLogRequest, notFound, inMemoryStaticFiles +} from './requestUtils'; +import { StatsTracker } from './stats'; +import { start } from './start'; +import { createServerActionsFactory } from './serverActionsManager'; +import { init, createRemovedDocument } from './internal'; +import { + pollServers, pollDiskSpace, pollCertificateExpirationDate, pollPatreon, startBansCleanup, pollMemoryUsage, + startMergesCleanup, startStrayAuthsCleanup, startClearOldIgnores, startCollectingUsersVisitedCount, + startSupporterInvitesCleanup, startPotentialDuplicatesCleanup, startAccountAlertsCleanup, startUpdatePastSupporters, + startClearTo10Origns, startClearVeryOldOrigns +} from './polling'; +import { pathTo } from './paths'; +import { liveSettings } from './liveSettings'; +import { createIndex } from './routes/index'; +import { authRoutes } from './routes/auth'; +import api from './routes/api'; +import api1 from './routes/api1'; +import api2 from './routes/api2'; +import apiTools from './routes/api-tools'; +import { createInternalApi } from './api/internal'; +import { createInternalLoginApi } from './api/internal-login'; +import { initLogSwearingAndSpamming } from './api/admin-accounts'; +import { InternalAdminApi } from './api/internal-admin'; +import { AdminService } from './services/adminService'; +import { createEndPoints } from './api/admin'; +import { World } from './world'; + +function getServiceWorker() { + try { + return fs.readFileSync(pathTo('build', 'sw.min.js')); + } catch { + return ''; + } +} + +mongoose.connect(config.db, { + reconnectTries: Number.MAX_VALUE, + useNewUrlParser: true, + useCreateIndex: true, + useFindAndModify: false, +}); + +const MongoStore = connectMongo(expressSession); +const app = express(); +const production = app.get('env') === 'production'; +const maxAge = production ? YEAR : 0; +const etag = false; +const limit = !production || args.tools ? '100mb' : '100kb'; + +Bluebird.config({ warnings: false, longStackTraces: !production }); + +const rollbar = config.rollbar && Rollbar.init({ + accessToken: config.rollbar.serverToken, + environment: config.rollbar.environment, + handleUncaughtExceptions: true, + handleUnhandledRejections: true, + captureUncaught: true, + checkIgnore: rollbarCheckIgnore, +} as any); + +let assetsPath = pathTo('build', 'assets'); +let adminAssetsPath = pathTo('build', 'assets-admin'); + +ensureDirSync(pathTo('build-copy')); + +if (production && args.login) { + const newAssetsPath = pathTo('build-copy', 'assets'); + removeSync(newAssetsPath); + copySync(assetsPath, newAssetsPath); + assetsPath = newAssetsPath; +} + +if (production && args.admin) { + const newAssetsPath = pathTo('build-copy', 'assets-admin'); + removeSync(newAssetsPath); + copySync(adminAssetsPath, newAssetsPath); + adminAssetsPath = newAssetsPath; +} + +app.set('port', port); +app.set('views', pathTo('views')); +app.set('view engine', 'pug'); +app.set('view options', { doctype: 'html' }); +app.set('x-powered-by', false); +app.set('etag', false); + +if (config.proxy) { + app.set('trust proxy', config.proxy); +} + +if (production) { + app.use(require('hsts')({ maxAge })); + app.use(require('frameguard')({ action: 'sameorigin' })); + // app.use(require('shrink-ray-current')()); +} + +if (args.login || args.admin) { + app.use(serveFavicon(pathTo('favicons', 'favicon.ico'))); +} + +app.use(morgan('dev', { skip: (_, res) => res.statusCode < 500 || res.statusCode === 503 })); + +const serviceWorker = getServiceWorker(); + +if (serviceWorker) { + app.get('/sw.js', (_, res) => { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.send(serviceWorker); + }); +} else { + app.get('/sw.js', notFound); +} + +if (args.login || args.admin) { + if (production) { + app.use(inMemoryStaticFiles(assetsPath, '/assets', maxAge)); + } + + app.use('/assets', blockMaps(DEVELOPMENT, !!args.local), express.static(assetsPath, { maxAge, etag })); + app.use(express.static(pathTo('public'), { maxAge, etag })); + app.use(express.static(pathTo('favicons'), { maxAge, etag })); +} + +app.use(bodyParser.json({ type: ['json', 'application/csp-report'], limit })); +app.use(bodyParser.urlencoded({ extended: true, limit })); +app.use(require('cookie-parser')()); + +if (args.login || args.admin) { + passport.serializeUser((account, done) => done(null, account._id.toString())); + passport.deserializeUser((id, done) => + Account.findById(id, (err, a) => done(err, a && !isBanned(a) ? a : false))); +} + +const ignore = [ + 'RangeNotSatisfiableError', + 'PreconditionFailedError', +]; + +app.use((err: any, req: any, res: express.Response, next: any) => { + const ignored = err instanceof Error && includes(ignore, err.name); + return next(ignored ? null : err, req, res); +}); + +if (rollbar) { + app.use(rollbar.errorHandler()); +} + +if (!production) { + app.use('/assets-admin', express.static(pathTo('assets'))); + app.use('/assets-admin', express.static(pathTo('src'))); + app.use('/assets', express.static(pathTo('assets'))); + app.use('/assets', express.static(pathTo('src'))); + app.use(require('errorhandler')()); +} + +const httpServer = http.createServer(app); +const errorHandler = new SocketErrorHandler(rollbar, server); +const createSession = () => expressSession({ + secret: config.secret, + resave: false, + saveUninitialized: false, + cookie: { + maxAge: WEEK * 2, + }, + store: new MongoStore({ mongooseConnection: mongoose.connection }), +}); + +const statsPath = pathTo('logs', `stats-${server.id}.csv`); +const stats = new StatsTracker(statsPath); +const sessionMiddlewares = once(() => [createSession(), passport.initialize(), passport.session()] as express.RequestHandler[]); +const adminMiddlewares = once(() => [...sessionMiddlewares(), isAdmin(server)]); +const socketOptionsBase: ServerOptions = { + ws: { Server: WebSocketServer }, + hash: STAMP, +}; + +initLogRequest(stats.logRequest); +initLogSwearingAndSpamming(stats.logSwearing, stats.logSpamming); + +const host = createServerHost(httpServer, { + path: args.standaloneadmin && !args.game ? '/admin/ws-admin' : server.path, + ws: { Server: WebSocketServer }, + perMessageDeflate: false, + errorHandler, +}); + +let theWorld: World | undefined = undefined; +let sent = 0, received = 0; +let sentPackets = 0, receivedPackets = 0; + +if (args.game) { + const getSettings = () => settings.servers[server.id] || {}; + + const { world, createServerActions, hiding } = createServerActionsFactory( + server, settings, getSettings, { + stats: () => { + const result = { sent, received, sentPackets, receivedPackets }; + sent = 0; + received = 0; + sentPackets = 0; + receivedPackets = 0; + return result; + } + }); + + const options = { + ...socketOptionsBase, + verifyClient: () => !getSettings().isServerOffline && !liveSettings.shutdown, + forceBinary: true, + onSend: (packet: Packet) => { + sent += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0); + sentPackets++; + stats.logSendStats(packet); + }, + onRecv: (packet: Packet) => { + received += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0); + receivedPackets++; + stats.logRecvStats(packet); + }, + }; + + const gameSocket = host.socket(ServerActions, ClientActions, createServerActions as any, options); + const tokens = tokenService(gameSocket); + + start(world, server); + init(world, tokens); + + theWorld = world; + + const apiInternal = createInternalApi( + world, server, reloadSettings, getSettings, tokens, hiding, stats, liveSettings); + app.use('/api-internal', internal(config, server), wrapApi(server, apiInternal)); +} + +const endPoints = args.admin ? createEndPoints() : undefined; +const adminService = args.admin ? new AdminService() : undefined; +const removedDocument = createRemovedDocument(endPoints, adminService); +const index = createIndex(assetsPath, adminAssetsPath); + +if (args.admin) { + if (args.standaloneadmin) { + app.use('/admin/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag })); + app.get('/admin/assets-admin/*', (_, res) => res.sendStatus(404)); + + const adminApi = new InternalAdminApi(adminService!, endPoints!); + app.use('/api-internal-admin', internal(config, server), wrapApi(server, adminApi)); + } + + const createClient = (client: ClientAdminActions & ClientExtensions) => + new AdminServerActions(client, server, settings, adminService!, endPoints!, removedDocument); + + const base = '/admin'; + const assetsBase = args.standaloneadmin ? '/admin' : ''; + const adminSocket = host.socket(AdminServerActions, ClientAdminActions, createClient, socketOptionsBase); + const sendAdminPage = index.admin(production, `${base}/`, assetsBase, 'bootstrap-admin.js', adminSocket); + + app.get(`${base}`, ...adminMiddlewares(), sendAdminPage); + app.get(`${base}/*`, ...adminMiddlewares(), sendAdminPage); +} + +if (args.tools) { + const toolsPage = index.user( + production, '/tools/', 'style-tools.css', 'bootstrap-tools.js', 'bootstrap-tools.js', undefined, true, !!args.local, false); + + app.get('/tools', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page)); + app.get('/tools/*', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page)); + + app.use('/api-tools', ...sessionMiddlewares(), apiTools(server, settings, theWorld)); + app.get('/api-tools/*', (_, res) => res.sendStatus(404)); +} + +if (args.login) { + const socketOptions = createClientOptions(ServerActions, ClientActions, socketOptionsBase); + const userPage = index.user( + production, '/', 'style.css', 'bootstrap.js', 'bootstrap-es.js', socketOptions, false, !!args.local, !production); + const offlinePage = fs.readFileSync(pathTo('public', 'offline.html'), 'utf8'); + + const script = `${config.host}${index.getRevScript('bootstrap.js')}`; + const scriptES = `${config.host}${index.getRevScript('bootstrap-es.js')}`; + const analytics = config.analytics ? 'https://www.google-analytics.com' : ''; + // const workbox = 'https://storage.googleapis.com/workbox-cdn'; + // const rollbarScripts = + // rollbar ? 'https://d37gvrvc0wt4s1.cloudfront.net https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/' : ''; + const csp = `object-src 'none';` + + `frame-src 'self';` + + `frame-ancestors 'self';` + + `worker-src ${config.host}sw.js;` + + `script-src 'unsafe-eval' ${script} ${scriptES} ${analytics};` + // + `report-uri /api2/csp` + ; + + const linkPreloads: string[] = [ + ...userPage.preload, + ]; + + app.use('/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag })); + app.use('/auth', ...sessionMiddlewares(), authRoutes( + config.host, server, settings, liveSettings, args.local || DEVELOPMENT, removedDocument)); + app.use('/api', ...sessionMiddlewares(), api( + server, settings, { version, host: config.host, debug: DEVELOPMENT, local: !!args.local }, removedDocument)); + app.use('/api1', ...sessionMiddlewares(), api1(server, settings)); + app.use('/api2', api2(settings, liveSettings, stats)); + + const loginApi = createInternalLoginApi(settings, liveSettings, stats, reloadSettings, removedDocument); + app.use('/api-internal-login', internal(config, server), wrapApi(server, loginApi)); + + app.get('/assets-admin/*', notFound); + app.get('/assets/*', notFound); + app.get('/auth/*', notFound); + app.get('/api/*', notFound); + app.get('/api1/*', notFound); + app.get('/api2/*', notFound); + + app.get('/*', (req, res) => { + if (settings.isPageOffline) { + res.send(offlinePage); + } else { + if (production && !args.local) { + res.setHeader('Content-Security-Policy', csp); + res.setHeader('Link', linkPreloads); + } + + res.setHeader('Referrer-Policy', 'no-referrer'); + // res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + res.send(userPage.page); + + stats.logRequest(req, userPage.page, '/'); + } + }); +} + +app.use((err: any, req: any, res: express.Response, next: any) => { + if (err instanceof URIError) { + res.redirect(config.host); + } else { + return next(err, req, res); + } +}); + +reloadSettings().then(() => { + if (args.login || args.game) { + stats.startStatTracking(); + } + + if (args.login || args.admin) { + pollServers(); + } + + if (args.admin && !args.nocleanup) { + startStrayAuthsCleanup(removedDocument); + startClearOldIgnores(); + startMergesCleanup(); + startBansCleanup(); + startCollectingUsersVisitedCount(); + startSupporterInvitesCleanup(); + startPotentialDuplicatesCleanup(adminService!); + startAccountAlertsCleanup(); + startUpdatePastSupporters(); + startClearTo10Origns(adminService!); + startClearVeryOldOrigns(adminService!); + pollPatreon(server, settings); + } + + if (args.admin) { + pollDiskSpace(); + pollMemoryUsage(); + pollCertificateExpirationDate(); + } + + httpServer.listen(port, () => { + const options = compact([ + app.get('env'), + args.login && 'login', + args.admin && 'admin', + args.standaloneadmin && '(standaloneadmin)', + args.tools && 'tools', + args.game && `game:${server.id}`, + ]); + + logger.info(`Listening on port ${port} (${options.join(', ')})`); + }); +}); diff --git a/src/ts/server/serverActions.ts b/src/ts/server/serverActions.ts new file mode 100644 index 0000000..682caef --- /dev/null +++ b/src/ts/server/serverActions.ts @@ -0,0 +1,762 @@ +import { Socket, Method, SocketServer, Bin, getMethods } from 'ag-sockets'; +import { + PlayerAction, ModAction, ChatType, PonyData, IServerActions, TileType, Action, EditorAction, Entity, + EntityOrPonyOptions, LeaveReason, SupporterInvite, InfoFlags, AccountSettings, FriendStatusFlags, + SelectFlags, UpdateFlags, isValidModTile, isValidTile, MapFlags, EntityState, houseTiles +} from '../common/interfaces'; +import { CharacterState, ServerConfig } from '../common/adminInterfaces'; +import { PARTY_LIMIT, OFFLINE_PONY, TILE_CHANGE_RANGE, MIN_HIDE_TIME, MAX_HIDE_TIME, PONY_TYPE } from '../common/constants'; +import { toInt, formatDuration, includes, clamp, hasFlag, distanceXY, computeFriendsCRC } from '../common/utils'; +import { banMessage } from '../common/adminUtils'; +import * as entities from '../common/entities'; +import { IClient, IgnorePlayer, FindClientByEntityId, AccountService, GetSettings } from './serverInterfaces'; +import { getModInfo } from './accountUtils'; +import { NotificationService } from './services/notification'; +import { PartyService } from './services/party'; +import { World, findClientByAccountId, findAllOnlineFriends } from './world'; +import { HidingService } from './services/hiding'; +import { + createCharacterState, interactWith, useHeldItem, setEntityExpression, getPlayerState, execAction, + updateEntityPlayerState +} from './playerUtils'; +import { allEntities } from './api/account'; +import { CounterService } from './services/counter'; +import { decodeExpression, isCancellableExpression } from '../common/encoders/expressionEncoder'; +import { updateEntity, pushUpdateEntityToClient, pushUpdateTileToClient } from './entityUtils'; +import { SupporterInvitesService } from './services/supporterInvites'; +import { Move } from './move'; +import { logger } from './logger'; +import { findFriends } from './db'; +import { Say, saySystem } from './chat'; +import { getTile } from '../common/worldMap'; +import { updateRegion, getExpectedRegion } from './regionUtils'; +import { findEntities } from './serverMap'; +import { FriendsService, toFriendOnline } from './services/friends'; +import { setupCamera } from '../common/camera'; +import { swapCharacter } from './characterUtils'; +import { isOutsideMap } from '../common/collision'; +import { createAnEntity } from '../common/entities'; +import { mockPaletteManager } from '../common/ponyInfo'; + +interface AddedEntity { + name: string; + entities: Entity[]; +} + +const modActionNames = ['None', 'Report', 'Mute', 'Shadow', 'Kick', 'Ban']; +const playerActionNames = [ + 'None', + 'Ignore', + 'Unignore', + 'InviteToParty', + 'RemoveFromParty', + 'PromotePartyLeader', + 'HidePlayer', + 'InviteToSupporterServers', + 'AddFriend', + 'RemoveFriend', +]; + +const editorAdded = new Map(); +const debugRate = DEVELOPMENT ? '1000/s' : ''; + +@Socket({ + id: 'game', + debug: false, + connectionTokens: true, + pingInterval: 3000, + connectionTimeout: 10000, + reconnectTimeout: 500, + transferLimit: 4000, + perMessageDeflate: false, + keepOriginalRequest: true, +}) +export class ServerActions implements IServerActions, SocketServer { + constructor( + private readonly client: IClient, + private readonly world: World, + private readonly notificationService: NotificationService, + private readonly partyService: PartyService, + private readonly supporterInvites: SupporterInvitesService, + private readonly getSettings: GetSettings, + private readonly server: ServerConfig, + private readonly chatSay: Say, + private readonly moveFunc: Move, + private readonly hiding: HidingService, + private readonly states: CounterService, + private readonly accountService: AccountService, + private readonly ignorePlayer: IgnorePlayer, + private readonly findClientByEntityId: FindClientByEntityId, + private readonly friends: FriendsService, + ) { + } + private get account() { + return this.client.account; + } + private get pony() { + return this.client.pony; + } + private get map() { + return this.client.map; + } + connected() { + this.client.connectedTime = Date.now(); + this.client.lastPacket = Date.now(); + this.client.loading = true; + this.client.reporter.systemLog(`joined [${this.server.id}] as "${this.client.characterName}" [${this.client.ip}]`); + + if (DEVELOPMENT && /slow/.test(this.client.characterName)) { + setTimeout(() => this.world.joinClientToQueue(this.client), 5000); + } else { + this.world.joinClientToQueue(this.client); + } + } + async disconnected() { + const state = createCharacterState(this.pony, this.client.map); + const duration = Date.now() - this.client.connectedTime; + const leaveReason = this.client.leaveReason || 'disconnected'; + + if (this.client.logDisconnect) { + logger.warn(`disconnected (${leaveReason}) account: ${this.client.account.name} [${this.client.accountId}]`); + } + + this.client.offline = true; + this.client.offlineAt = new Date(); + this.client.reporter.systemLog(`left [${this.server.id}] (${leaveReason}) (${formatDuration(duration)})`); + this.world.leaveClient(this.client); + this.partyService.clientDisconnected(this.client); + this.friends.clientDisconnected(this.client); + this.states.add(this.client.characterId, state); + + await Promise.all([ + this.accountService.updateAccount(this.client.accountId, { lastVisit: new Date(), state: this.account.state }), + this.accountService.updateCharacterState(this.client.characterId, state), + ]); + } + @Method({ rateLimit: '2/s', binary: [Bin.U32, Bin.Str, Bin.U8] }) + say(entityId: number, text: string, chatType: ChatType) { + validateNumber(entityId, 'entityId'); + validateString(text, 'text'); + validateNumber(chatType, 'chatType'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + const target = entityId ? this.world.getEntityById(entityId) : undefined; + this.chatSay(this.client, text, chatType, target && target.client, this.getSettings()); + } + @Method({ rateLimit: '3/s', binary: [Bin.U32, Bin.U8] }) + select(entityId: number, flags: SelectFlags) { + validateNumber(entityId, 'entityId'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + const entity = entityId === 0 ? undefined : (this.world.getEntityById(entityId) || this.getEntityFromClients(entityId)); + const mod = this.client.isMod; + this.client.selected = entity; + + if (entity && entity.client && entity !== this.client.pony) { + if (flags) { + const baseOptions: Partial = mod ? { modInfo: getModInfo(entity.client) } : {}; + const options = { ...baseOptions, ...entity.extraOptions }; + + if (hasFlag(flags, SelectFlags.FetchInfo)) { + const playerState = getPlayerState(this.client, entity); + const flags = UpdateFlags.Options | UpdateFlags.Name | UpdateFlags.Info | UpdateFlags.PlayerState; + pushUpdateEntityToClient(this.client, { entity, flags, options, playerState }); + } else if (hasFlag(flags, SelectFlags.FetchEx) || mod) { + pushUpdateEntityToClient(this.client, { entity, flags: UpdateFlags.Options, options }); + } + } + } else if (entityId) { + this.client.updateSelection(entityId, 0); + } + } + @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U32] }) + interact(entityId: number) { + validateNumber(entityId, 'entityId'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + interactWith(this.client, this.world.getEntityById(entityId)); + } + @Method({ rateLimit: '2/s', binary: [] }) + use() { + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + useHeldItem(this.client); + } + @Method({ rateLimit: '3/s', serverRateLimit: '8/s', binary: [Bin.U8] }) + action(action: Action) { + validateNumber(action, 'action'); + this.updateLastAction(); + + switch (action) { + case Action.KeepAlive: + break; + case Action.UnhideAllHiddenPlayers: + this.hiding.requestUnhideAll(this.client); + break; + default: + if (this.client.isSwitchingMap) + return; + + execAction(this.client, action, this.getSettings()); + break; + } + } + @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U8, Bin.Obj] }) + actionParam(action: Action, param: any) { + validateNumber(action, 'action'); + + switch (action) { + case Action.CancelSupporterInvite: + validateString(param, 'param'); + // TODO: ... + break; + case Action.SwapCharacter: { + validateString(param, 'param'); + this.updateLastAction(); + + if (param !== this.client.characterId) { + swapCharacter(this.client, this.world, { account: this.client.account._id, _id: param }) + .catch(e => this.client.reporter.error(e)); + } + + break; + } + case Action.RemoveFriend: { + validateString(param, 'param'); + this.updateLastAction(); + const target = findClientByAccountId(this.world, param); + + if (target) { + this.friends.remove(this.client, target); + } else { + this.friends.removeByAccountId(this.client, param); + } + + break; + } + case Action.FriendsCRC: { + validateNumber(param, 'param'); + const crc = param >>> 0; + + if (this.client.friendsCRC === undefined) { + this.client.friendsCRC = computeFriendsCRC(Array.from(this.client.friends.values())); + } + + if (this.client.friendsCRC !== crc) { + findFriends(this.client.accountId, true) + .then(friends => { + this.client.updateFriends(friends.map(f => { + const client = findClientByAccountId(this.world, f.accountId); + + return { + accountId: f.accountId, + accountName: f.accountName, + entityId: client && client.pony.id, + status: client ? FriendStatusFlags.Online : FriendStatusFlags.None, + name: f.name, + nameBad: f.nameBad, + info: f.pony, + }; + }), true); + }) + .catch(e => logger.error(e)); + } + + break; + } + case Action.RemoveEntity: { + validateNumber(param, 'param'); + this.updateLastAction(); + const entity = this.world.getEntityById(param | 0); + + if ( + entity && hasFlag(entity.state, EntityState.Editable) && this.pony.options!.hold === entities.broom.type && + this.map.regions.some(r => includes(r.entities, entity)) + ) { + if (this.isHouseLocked()) { + saySystem(this.client, `House is locked`); + } else { + this.world.removeEntity(entity, this.map); + } + } + + break; + } + case Action.PlaceEntity: { + if ( + !param || typeof param !== 'object' || typeof param.x !== 'number' || typeof param.y !== 'number' || + typeof param.type !== 'number' + ) { + return; + } + + this.updateLastAction(); + + const { x, y, type } = param as { x: number; y: number; type: number; }; + + if ( + isOutsideMap(x, y, this.map) || + !entities.placeableEntities.some(x => x.type === type) || + !hasFlag(this.map.flags, MapFlags.EditableEntities) + ) { + return saySystem(this.client, `Cannot place object`); + } + + if (this.isHouseLocked()) { + return saySystem(this.client, `House is locked`); + } + + let totalEditableEntities = 0; + + for (const region of this.map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.state, EntityState.Editable)) { + totalEditableEntities++; + } + } + } + + if (totalEditableEntities >= this.map.editableEntityLimit) { + return saySystem(this.client, `Object limit reached`); + } + + const entity = createAnEntity(type, 0, x, y, {}, mockPaletteManager, this.world); + entity.state |= EntityState.Editable; + this.world.addEntity(entity, this.map); + break; + } + default: + throw new Error(`Invalid Action (${action})`); + } + } + @Method({ rateLimit: '10/s', serverRateLimit: '20/s', binary: [Bin.U8, Bin.Obj] }) + actionParam2(action: Action, param: any) { + validateNumber(action, 'action'); + + switch (action) { + case Action.Info: + validateNumber(param, 'param'); + this.client.incognito = hasFlag(param, InfoFlags.Incognito); + this.client.supportsWasm = hasFlag(param, InfoFlags.SupportsWASM); + this.client.supportsLetAndConst = hasFlag(param, InfoFlags.SupportsLetAndConst); + break; + case Action.RequestEntityInfo: { + validateNumber(param, 'param'); + + const entity = this.world.getEntityById(param | 0); + + if (entity && entity.client) { + this.client.entityInfo(entity.id, entity.name || '', entity.crc || 0, !!entity.nameBad); + } + + break; + } + default: + throw new Error(`Invalid Action (${action})`); + } + } + @Method({ promise: true, rateLimit: '1/s', binary: [] }) + async getInvites(): Promise { + return [ + { id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, + { id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, + { id: 'c', info: OFFLINE_PONY, name: 'Molly', active: false }, + ]; + } + @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U32] }) + expression(expression: number) { + validateNumber(expression, 'expression'); + this.updateLastAction(); + + const expr = decodeExpression(expression); + const cancellable = !!expr && isCancellableExpression(expr); + + if (cancellable) { + setEntityExpression(this.pony, expr, 0, true); + } else { + this.pony.exprPermanent = expr; + setEntityExpression(this.pony, undefined, 0); + } + } + @Method({ rateLimit: '3/s', binary: [Bin.U32, Bin.U8, Bin.Obj] }) + playerAction(entityId: number, action: PlayerAction, param: unknown) { + validateNumber(entityId, 'entityId'); + validateNumber(action, 'action'); + this.updateLastAction(); + + const target = this.getClientByEntityId(entityId); + + if (!target) { + this.client.reporter.warnLog(`No client for: ${playerActionNames[action]} [${action}], id: ${entityId}`); + return; + } + + switch (action) { + case PlayerAction.Ignore: + case PlayerAction.Unignore: + this.ignorePlayer(this.client, target, action === PlayerAction.Ignore); + break; + case PlayerAction.InviteToParty: + this.partyService.invite(this.client, target); + break; + case PlayerAction.RemoveFromParty: + this.partyService.remove(this.client, target); + break; + case PlayerAction.PromotePartyLeader: + this.partyService.promoteLeader(this.client, target); + break; + case PlayerAction.HidePlayer: + const hideFor = toInt(param); + if (hideFor === 0) { + this.hiding.requestHide(this.client, target, 0); + } else { + this.hiding.requestHide(this.client, target, clamp(hideFor, MIN_HIDE_TIME, MAX_HIDE_TIME)); + } + break; + case PlayerAction.InviteToSupporterServers: + this.supporterInvites.requestInvite(this.client, target); + break; + case PlayerAction.AddFriend: + this.friends.add(this.client, target); + break; + case PlayerAction.RemoveFriend: + this.friends.remove(this.client, target); + break; + default: + throw new Error(`Invalid player action (${playerActionNames[action]}) [${action}]`); + } + } + @Method({ rateLimit: '1/s', binary: [] }) + leaveParty() { + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + if (this.client.party) { + this.partyService.remove(this.client.party.leader, this.client); + } + } + @Method({ promise: true, rateLimit: '10/s', binary: [Bin.U32, Bin.U8, Bin.I32] }) + async otherAction(entityId: number, action: ModAction, param: number) { + validateNumber(entityId, 'entityId'); + validateNumber(action, 'action'); + validateNumber(param, 'param'); + this.updateLastAction(); + + const target = this.getClientForModAction(entityId, modActionNames[action]); + + switch (action) { + case ModAction.Report: + await this.logModAction(target, 'Reported'); + break; + case ModAction.Mute: + await this.setBan(target, 'mute', param); + break; + case ModAction.Shadow: + await this.setBan(target, 'shadow', param); + break; + case ModAction.Ban: + await this.setBan(target, 'ban', -1); + break; + case ModAction.Kick: + await this.world.kick(target, 'mod kick'); + break; + default: + throw new Error(`Invalid mod action (${action})`); + } + } + private setBan(target: IClient, field: 'mute' | 'shadow' | 'ban', value: number) { + const timeout = value > 0 ? Date.now() + value : value; + this.logModAction(target, banMessage(field, timeout)); + return this.accountService.update(target.accountId, { [field]: timeout }); + } + @Method({ promise: true, binary: [Bin.U32, Bin.Str] }) + async setNote(entityId: number, text: string) { + validateNumber(entityId, 'entityId'); + validateString(text, 'text', true); + this.updateLastAction(); + + const client = this.getClientForModAction(entityId, 'setNote'); + await this.accountService.update(client.accountId, { note: text }); + } + @Method({ rateLimit: '5/s', binary: [Bin.Obj] }) + async saveSettings(settings: AccountSettings) { + this.updateLastAction(); + + const wasHidden = !!this.client.accountSettings.hidden; + await this.accountService.updateSettings(this.account, settings); + this.client.accountSettings = { ...this.account.settings }; + this.client.reporter.systemLog(`Saved settings`); + const isHidden = !!this.client.accountSettings.hidden; + + if (wasHidden !== isHidden) { + for (const friend of findAllOnlineFriends(this.world, this.client)) { + if (isHidden) { + friend.updateFriends([{ accountId: this.client.accountId, status: FriendStatusFlags.None }], false); + } else { + friend.updateFriends([toFriendOnline(this.client)], false); + } + + updateEntityPlayerState(friend, this.client.pony); + } + } + } + @Method({ binary: [Bin.U16] }) + acceptNotification(id: number) { + validateNumber(id, 'id'); + this.updateLastAction(); + this.notificationService.acceptNotification(this.client, id); + } + @Method({ binary: [Bin.U16] }) + rejectNotification(id: number) { + validateNumber(id, 'id'); + this.updateLastAction(); + this.notificationService.rejectNotification(this.client, id); + } + @Method({ binary: [[Bin.U32]] }) + getPonies(ids: number[]) { + const party = this.client.party; + + const createPonyData = ({ pony }: IClient): PonyData => { + return [ + pony.id, + pony.options, + pony.encodedName, + pony.encryptedInfoSafe, + getPlayerState(this.client, pony), + !!pony.nameBad, + ]; + }; + + if (party && ids && ids.length && ids.length <= PARTY_LIMIT) { + const ponies = party.clients + .filter(c => includes(ids, c.pony.id)) + .map(createPonyData); + this.client.updatePonies(ponies); + } + } + @Method({ binary: [] }) + loaded() { + this.client.loading = false; + } + @Method({ binary: [] }) + fixedPosition() { + this.client.fixingPosition = false; + } + @Method({ binary: [Bin.U32, Bin.U32, Bin.U16, Bin.U16] }) + updateCamera(x: number, y: number, width: number, height: number) { + validateNumber(x, 'x'); + validateNumber(y, 'y'); + validateNumber(width, 'width'); + validateNumber(height, 'height'); + this.updateLastAction(); + + setupCamera(this.client.camera, x, y, width, height, this.client.map); + } + @Method({ binary: [Bin.U32, Bin.U32, Bin.U32, Bin.U32, Bin.U16] }) + move(a: number, b: number, c: number, d: number, e: number) { + validateNumber(a, 'a'); + validateNumber(b, 'b'); + validateNumber(c, 'c'); + validateNumber(d, 'd'); + validateNumber(e, 'e'); + this.updateLastAction(); + + this.moveFunc(this.client, Date.now(), a, b, c, d, e, this.getSettings()); + } + private isHouseLocked() { + return this.map.editingLocked && this.client.party && this.client.party.leader !== this.client; + } + @Method({ rateLimit: debugRate || '3/s', serverRateLimit: debugRate || '7/s', binary: [Bin.U16, Bin.U16, Bin.U8] }) + changeTile(x: number, y: number, type: TileType) { + validateNumber(x, 'x'); + validateNumber(y, 'y'); + validateNumber(type, 'type'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + const wallTile = type === TileType.WallH || type === TileType.WallV; + + if (hasFlag(this.map.flags, MapFlags.EditableWalls) && wallTile) { + if (this.isHouseLocked()) { + saySystem(this.client, `House is locked`); + } else { + this.world.toggleWall(this.map, x, y, type); + } + } else if (BETA && this.client.isMod && wallTile) { + this.world.toggleWall(this.map, x, y, type); + } else if (hasFlag(this.map.flags, MapFlags.EditableTiles) && this.pony.options!.hold === entities.shovel.type) { + if (!houseTiles.some(t => t.type === type)) + return; + + if (this.isHouseLocked()) + return saySystem(this.client, `House is locked`); + + this.world.setTile(this.map, x, y, type); + } else if (BETA && this.client.isMod && isValidModTile(type)) { + this.world.setTile(this.map, x, y, type); + } else if (isValidTile(type)) { + if ((BETA || distanceXY(x, y, this.pony.x, this.pony.y) < TILE_CHANGE_RANGE)) { + const tile = getTile(this.map, x, y); + + if (tile === TileType.Dirt || tile === TileType.Grass) { + if (this.client.shadowed) { + pushUpdateTileToClient(this.client, x, y, type); + } else { + this.world.setTile(this.map, x, y, type); + } + } + } + } + } + @Method({ rateLimit: '1/s', binary: [] }) + leave() { + this.client.leaveReason = 'leave'; + this.client.left(LeaveReason.None); + } + @Method({ binary: [Bin.Obj] }) + editorAction(action: EditorAction) { + if (this.server.flags.objects && this.client.isMod) { + const added = editorAdded.get(this.client.accountId) || []; + editorAdded.set(this.client.accountId, added); + + switch (action.type) { + case 'place': + if (includes(allEntities, action.entity)) { + const name = action.entity; + const entity = (entities as any)[name](action.x, action.y); + const toAdd = Array.isArray(entity) ? entity : [entity]; + toAdd.forEach(e => this.world.addEntity(e, this.map)); + added.push({ name, entities: toAdd }); + } else { + saySystem(this.client, 'Invalid entity'); + } + break; + case 'move': + for (const { id, x, y } of action.entities) { + const entity = this.world.getEntityById(id); + + if (entity && entity.type !== PONY_TYPE) { + entity.x = x; + entity.y = y; + updateEntity(entity, false); + updateRegion(entity, this.client.map); + getExpectedRegion(entity, this.client.map).colliderDirty = true; + } + } + break; + case 'undo': + const remove = added.pop(); + remove && remove.entities.forEach(e => this.world.removeEntityFromSomeMap(e)); + break; + case 'clear': + added.forEach(x => x.entities.forEach(e => this.world.removeEntityFromSomeMap(e))); + added.length = 0; + break; + case 'list': + const existingEntities = added + .filter(({ entities }) => entities.some(e => !!this.world.getEntityById(e.id))) + .map(({ name, entities: [{ x, y }] }) => ({ name, x, y })); + this.client.entityList(existingEntities); + break; + case 'remove': + for (const id of action.entities) { + const entity = this.world.getEntityById(id); + + if (entity && entity.type !== PONY_TYPE) { + this.world.removeEntityFromSomeMap(entity); + } + } + break; + case 'tile': { + const { x, y, tile, size } = action; + + if (isValidModTile(tile)) { + for (let iy = 0; iy < size; iy++) { + for (let ix = 0; ix < size; ix++) { + this.world.setTile(this.map, x + ix, y + iy, tile); + } + } + } + + break; + } + case 'party': { + const entities = findEntities(this.map, e => !!e.client && /^debug/.test(e.name || '')); + + for (const e of entities.slice(0, PARTY_LIMIT - 1)) { + this.partyService.invite(this.client, e.client!); + } + + break; + } + default: + throw new Error(`Invalid editor action (${action})`); + } + } + } + private logModAction(client: IClient, title: string) { + client.reporter.system(`${title} by ${this.account.name}`); + } + private getClientForModAction(entityId: number, action: string) { + if (!this.client.isMod) { + this.client.disconnect(true, true); + throw new Error(`Action not allowed (${action})`); + } + + const client = this.world.getClientByEntityId(entityId); + + if (!client) { + throw new Error(`Client does not exist (${action})`); + } + + if (client.accountId === this.client.accountId) { + throw new Error(`Cannot perform action on self (${action})`); + } + + return client; + } + private getEntityFromClients(entityId: number) { + const client = this.getClientByEntityId(entityId); + return client && client.pony; + } + private getClientByEntityId(entityId: number) { + return this.world.getClientByEntityId(entityId) || this.findClientByEntityId(this.client, entityId); + } + private updateLastAction() { + this.client.lastPacket = Date.now(); + } +} + +function validateNumber(value: number, fieldName: string) { + if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) { + throw new Error(`Not a number (${fieldName})`); + } +} + +function validateString(value: string, fieldName: string, allowNull = false) { + if (typeof value !== 'string' && !(allowNull && value === null)) { + throw new Error(`Not a string (${fieldName})`); + } +} + +if (DEVELOPMENT) { + /* istanbul ignore next */ + getMethods(ServerActions) + .forEach(m => m.options.binary || console.error(`Missing binary encoding for ServerActions.${m.name}()`)); +} diff --git a/src/ts/server/serverActionsManager.ts b/src/ts/server/serverActionsManager.ts new file mode 100644 index 0000000..2235cbe --- /dev/null +++ b/src/ts/server/serverActionsManager.ts @@ -0,0 +1,118 @@ +import { SocketClient, ClientExtensions } from 'ag-sockets'; +import * as fs from 'fs'; +import { noop, random } from 'lodash'; +import { HOUR, SECOND, SEASON, HOLIDAY, UNHIDE_TIMEOUT, MINUTE } from '../common/constants'; +import { CharacterState, ServerConfig, Settings } from '../common/adminInterfaces'; +import { ClientActions } from '../client/clientActions'; +import { + updateAccountSafe, timeoutAccount, reportInviteLimitAccount, reportSwearingAccount, reportSpammingAccount, + reportFriendLimitAccount +} from './api/admin-accounts'; +import { ServerActions } from './serverActions'; +import { IClient, AccountService, GetSettings, SocketStats, TokenData } from './serverInterfaces'; +import { createReportSwears, createReportForbidden, reportInviteLimit, createReportSuspicious } from './reporting'; +import { createSpamChecker } from './spamChecker'; +import { NotificationService } from './services/notification'; +import { CounterService } from './services/counter'; +import { HidingService, pollHidingDataSave, hidingDataPath } from './services/hiding'; +import { PartyService } from './services/party'; +import { World, findClientByAccountId } from './world'; +import { log, chat } from './logger'; +import { createSay, LogChat } from './chat'; +import { createRunCommand, createCommands, getSpamCommandNames } from './commands'; +import { createIgnorePlayer, findClientByEntityId, createClientAndPony } from './playerUtils'; +import { createUpdateSettings } from './api/account'; +import { findAccountSafe, updateAccount, SupporterInvite, Account, IAccount, findFriendIds, findHideIds } from './db'; +import { SupporterInvitesService } from './services/supporterInvites'; +import { createMove } from './move'; +import { liveSettings } from './liveSettings'; +import { createIsSuspiciousMessage } from '../common/security'; +import { updateCharacterState } from './characterUtils'; +import { FriendsService } from './services/friends'; + +async function refreshSettings(account: IAccount) { + const a = await Account.findOne({ _id: account._id }, 'settings').exec(); + + if (a) { + account.settings = a.settings; + } +} + +export function createServerActionsFactory( + server: ServerConfig, settings: Settings, getSettings: GetSettings, socketStats: SocketStats +) { + const reportInviteLimitFunc = reportInviteLimit(reportInviteLimitAccount, `Party invite limit`); + const reportFriendLimitFunc = reportInviteLimit(reportFriendLimitAccount, `Friend request limit`); + const notifications = new NotificationService(); + const party = new PartyService(notifications, reportInviteLimitFunc); + const supporterInvites = new SupporterInvitesService(SupporterInvite, notifications, log); + const friends = new FriendsService(notifications, reportFriendLimitFunc); + let world: World; + const hiding = new HidingService(UNHIDE_TIMEOUT, notifications, accountId => findClientByAccountId(world, accountId), log); + world = new World(server, party, friends, hiding, notifications, getSettings, liveSettings, socketStats); + const spamCounter = new CounterService(2 * HOUR); + const rapidCounter = new CounterService(1 * MINUTE); + const swearsCounter = new CounterService(2 * HOUR); + const forbiddenCounter = new CounterService(4 * HOUR); + const suspiciousCounter = new CounterService(4 * HOUR); + const teleportCounter = new CounterService(1 * HOUR); + const statesCounter = new CounterService(10 * SECOND); + const logChatMessage: LogChat = (client, text, type, ignored, target) => chat(server, client, text, type, ignored, target); + + world.season = SEASON; + world.holiday = HOLIDAY; + + try { + const hidingData = fs.readFileSync(hidingDataPath(server.id), 'utf8'); + + if (hidingData) { + hiding.deserialize(hidingData); + } + } catch { } + + pollHidingDataSave(hiding, server.id); + + hiding.changes.subscribe(({ by, who }) => world.notifyHidden(by, who)); + hiding.unhidesAll.subscribe(by => world.kickByAccount(by)); + + hiding.start(); + spamCounter.start(); + swearsCounter.start(); + forbiddenCounter.start(); + suspiciousCounter.start(); + + const commands = createCommands(world); + const spamCommands = getSpamCommandNames(commands); + const runCommand = createRunCommand({ world, notifications, random, liveSettings, party }, commands); + const updateSettings = createUpdateSettings(findAccountSafe); + const accountService: AccountService = { + update: updateAccountSafe, + updateSettings: (account, settings) => updateSettings(account, settings).then(noop), + refreshSettings, + updateAccount, + updateCharacterState: (characterId, state) => updateCharacterState(characterId, server.id, state), + }; + const reportSwears = createReportSwears(swearsCounter, reportSwearingAccount, timeoutAccount); + const reportForbidden = createReportForbidden(forbiddenCounter, timeoutAccount); + const reportSuspicious = createReportSuspicious(suspiciousCounter); + const checkSpam = createSpamChecker(spamCounter, rapidCounter, reportSpammingAccount, timeoutAccount); + const isSuspiciousMessage = createIsSuspiciousMessage(settings); + const say = createSay( + world, runCommand, logChatMessage, checkSpam, reportSwears, reportForbidden, reportSuspicious, spamCommands, + Math.random, isSuspiciousMessage); + const move = createMove(teleportCounter); + const ignorePlayer = createIgnorePlayer(updateAccount); + + async function createServerActions(client: ClientActions & SocketClient & ClientExtensions & IClient) { + const { account } = client.tokenData as TokenData; + const [friendIds, hideIds] = await Promise.all([findFriendIds(account._id), findHideIds(account._id)]); + createClientAndPony(client, friendIds, hideIds, server, world, statesCounter); + + return new ServerActions( + client, world, notifications, party, supporterInvites, getSettings, server, say, move, hiding, statesCounter, + accountService, ignorePlayer, findClientByEntityId, friends + ); + } + + return { world, hiding, createServerActions }; +} diff --git a/src/ts/server/serverInterfaces.ts b/src/ts/server/serverInterfaces.ts new file mode 100644 index 0000000..cadce85 --- /dev/null +++ b/src/ts/server/serverInterfaces.ts @@ -0,0 +1,308 @@ +import { ClientExtensions, BinaryWriter } from 'ag-sockets'; +import { ClientActions } from '../client/clientActions'; +import { + Entity, ServerFlags, AccountSettings, NotificationFlags, Expression, Camera, SayData, Region, TileUpdate, + Rect, IMap, MapType, TileType, MapState, UpdateFlags, Action, EntityOrPonyOptions, EntityPlayerState, MapFlags +} from '../common/interfaces'; +import { IAccount, ICharacter, UpdateAccount } from './db'; +import { AccountUpdate, CharacterState, GameServerSettings, Suspicious } from '../common/adminInterfaces'; + +export interface EntityUpdate { + entity: Entity; + flags: UpdateFlags; + + // NOTE: need to use different position than current entity.x/y + // otherwise we get jump at the start of movement due to + // updated position after the frame + x: number; + y: number; + vx: number; + vy: number; + + action: Action; + playerState: EntityPlayerState; + options: EntityOrPonyOptions | undefined; +} + +export type EntityUpdateBase = Partial & { entity: ServerEntity; flags: UpdateFlags; }; + +export interface Reporter { + info(message: string, desc?: string): void; + warn(message: string, desc?: string): void; + warnLog(message: string): void; + danger(message: string, desc?: string): void; + error(error: Error, desc?: string): void; + system(message: string, desc?: string, logEvent?: boolean): void; + systemLog(message: string): void; + setPony(pony: any): void; +} + +export interface ServerNotification { + id: number; + name: string; + message: string; + note?: string; + flags?: NotificationFlags; + entityId?: number; + accept?(): void; + reject?(): void; + sender?: IClient; +} + +export interface ServerParty { + id: string; + leader: IClient; + leaderTimeout?: any; + clients: IClient[]; + pending: { client: IClient; notificationId: number; }[]; + cleanup?: number; +} + +export interface TokenData { + accountId: string; + account: IAccount; + character: ICharacter; +} + +export interface TokenService { + clearTokensForAccount(accountId: string): void; + clearTokensAll(): void; + createToken(token: TokenData): string; +} + +export interface LastSay { + message: string; + count: number; + age: number; +} + +export interface QueuedSay { + id: number; + type: number; + message: string; +} + +export interface ServerRegion extends Region { + tiles: Uint8Array; + entities: ServerEntity[]; + movables: ServerEntity[]; + colliders: ServerEntity[]; + // entityAdds: any[]; + reusedUpdates: number; + entityUpdates: EntityUpdate[]; + entityRemoves: number[]; + tileUpdates: TileUpdate[]; + clients: IClient[]; // subscribed clients + bounds: Readonly; + boundsWithBorder: Readonly; + subscribeBounds: Readonly; // screen space + unsubscribeBounds: Readonly; // screen space + tilesSnapshot: Uint8Array | undefined; + tilesTimeouts: Uint8Array | undefined; + encodedTiles: Uint8Array | undefined; +} + +export const enum MapUsage { + Public, + Party, +} + +export interface ServerMap extends IMap { + id: string; + usage: MapUsage; + flags: MapFlags; + readonly type: MapType; + readonly width: number; + readonly height: number; + readonly regionsX: number; + readonly regionsY: number; + instance: string | undefined; + defaultTile: TileType; + state: MapState; + regions: ServerRegion[]; + spawnArea: Rect; + spawns: Map; + lockedTiles: Set; + lastUsed: number; + controllers: Controller[]; + dontUpdateTilesAndColliders: boolean; + tilesLocked: boolean; + editableEntityLimit: number; + editableArea?: Rect; + editingLocked: boolean; +} + +export interface IClient extends ClientActions, ClientExtensions { + // origin info + ip: string; + country: string; + userAgent?: string; + + // browser info + incognito?: boolean; + supportsWasm?: boolean; + supportsLetAndConst?: boolean; + + // quick access account fields + accountId: string; + accountName: string; + accountSettings: AccountSettings; + account: IAccount; + friends: Set; + friendsCRC: number | undefined; + + // quick access character fields + characterId: string; + characterName: string; + character: ICharacter; + + isMod: boolean; + shadowed: boolean; + supporterLevel: number; + pony: ServerEntity; + regions: ServerRegion[]; + map: ServerMap; + isSwitchingMap: boolean; + camera: Camera; + characterState: CharacterState; + offline?: boolean; + offlineAt?: Date; + selected?: ServerEntity; + reporter: Reporter; + notifications: ServerNotification[]; + party?: ServerParty; + ignores: Set; + hides: Set; + permaHides: Set; + lastSwap: number; + lastMapLoadOrSave: number; + + // last state + safeX: number; + safeY: number; + lastPacket: number; + lastAction: number; + lastBoopAction: number; + lastExpressionAction: number; + lastSays: LastSay[]; + lastX: number; + lastY: number; + lastTime: number; + lastVX: number; + lastVY: number; + + // sitting reporting + lastSitX: number; + lastSitY: number; + lastSitTime: number; + sitCount: number; + + // subscription checking + lastCameraX: number; + lastCameraY: number; + lastCameraW: number; + lastCameraH: number; + + // flags + lastMapSwitch: number; + // queuedMapSwitch?: { map: ServerMap; x: number; y: number; }; + logDisconnect?: boolean; + loading?: boolean; + fixingPosition?: boolean; + connectedTime: number; + leaveReason?: string; + + // pending data + updateQueue: BinaryWriter; + regionUpdates: Uint8Array[]; + saysQueue: SayData[]; + unsubscribes: number[]; + subscribes: Uint8Array[]; + + // error reporting + rateLimitMessage?: string; + rateLimitCount?: number; + + // debug + positions: { frame: number; x: number; y: number; moved: boolean; }[]; +} + +export type Interact = (target: ServerEntity, client: IClient) => void; + +export interface ServerEntity extends Entity { + // flags + serverFlags?: ServerFlags; + canFly?: boolean; + canMagic?: boolean; + + // state + region?: ServerRegion; + client?: IClient; + + // interaction + interact?: Interact; + + // trigger + trigger?: Interact; + + // boop + boop?(client: IClient): void; + boopX?: number; + boopY?: number; + + // expression + exprTimeout?: number; + exprCancellable?: boolean; + exprPermanent?: Expression; + + // other + lightDelay?: number; + + // cached info & name + nameBad?: boolean; + encodedName?: Uint8Array; + info?: string; + infoSafe?: string; + encryptedInfoSafe?: Uint8Array; + + // update + serverUpdate?: (delte: number, now: number) => void; +} + +export interface ServerEntityWithClient extends ServerEntity { + client: IClient; +} + +export interface Controller { + initialize(now: number): void; + update(delta: number, now: number): void; + sparseUpdate?(): void; + toggleWall?(x: number, y: number, type: TileType): void; +} + +export interface AccountService { + update(accountId: string, update: AccountUpdate): Promise; + updateSettings(account: IAccount, settings: AccountSettings): Promise; + refreshSettings(account: IAccount): Promise; + updateAccount: UpdateAccount; + updateCharacterState(characterId: string, state: CharacterState): Promise; +} + +export interface SocketStats { + stats(): { sent: number; sentPackets: number; received: number; receivedPackets: number; }; +} + +export type OnMessage = (client: IClient, message: string) => void; +export type OnMessageSettings = (client: IClient, message: string, settings: GameServerSettings) => void; +export type OnSuspiciousMessage = (client: IClient, message: string, suspicious: Suspicious) => void; +export type IgnorePlayer = (client: IClient, target: IClient, ignored: boolean) => void; +export type FindClientByEntityId = (client: IClient, entityId: number) => IClient | undefined; +export type ReportAccount = (accountId: string) => Promise; +export type TimeoutAccount = (accountId: string, timeout: Date, message?: string) => Promise; +export type ReportInviteLimit = (client: IClient) => void; +export type ReportError = (message: string, data: any) => void; + +export type LogAccountMessage = (accountId: string, message: string) => void; +export type LogMessage = (message: string) => void; + +export type GetSettings = () => GameServerSettings; diff --git a/src/ts/server/serverMap.ts b/src/ts/server/serverMap.ts new file mode 100644 index 0000000..560c84b --- /dev/null +++ b/src/ts/server/serverMap.ts @@ -0,0 +1,529 @@ +import { writeFileAsync, readFileAsync, writeFileSync } from 'fs'; +import { fromByteArray } from 'base64-js'; +import { + TileType, MapInfo, MapState, defaultMapState, Rect, MapType, ServerFlags, EntityFlags, MapFlags, EntityState +} from '../common/interfaces'; +import { getRegionGlobal, getTile, getRegion } from '../common/worldMap'; +import { distanceSquaredXY, containsPoint, hasFlag } from '../common/utils'; +import { POSITION_MAX } from '../common/movementUtils'; +import { getEntityTypeName, getEntityType, createAnEntity } from '../common/entities'; +import { deserializeTiles } from '../common/compress'; +import { rect } from '../common/rect'; +import { snapshotRegionTiles, setRegionTile, createServerRegion, getSizeOfRegion, cloneServerRegion } from './serverRegion'; +import { ServerEntity, ServerRegion, ServerMap, MapUsage } from './serverInterfaces'; +import { getTileColor } from '../common/colors'; +import { World } from './world'; +import { REGION_SIZE, REGION_HEIGHT, REGION_WIDTH, tileWidth, tileHeight } from '../common/constants'; +import { pathTo } from './paths'; +import { createCanvas } from './canvasUtilsNode'; +import { mockPaletteManager } from '../common/ponyInfo'; +import { setEntityName } from './entityUtils'; +import { WallController } from './controllers/wallController'; + +export interface EntityData { + type: string; + x: number; + y: number; + options?: any; + name?: string; +} + +export interface MapData { + width: number; + height: number; + tiles?: string; + entities?: EntityData[]; + walls?: string; +} + +export interface MapLoadOptions { + offsetX?: number; + offsetY?: number; + loadOnlyTiles?: boolean; + loadEntities?: boolean; + loadEntitiesAsEditable?: boolean; + loadWalls?: boolean; +} + +export interface MapSaveOptions { + saveTiles?: boolean; + saveEntities?: boolean; + saveOnlyEditableEntities?: boolean; + saveWalls?: boolean; +} + +export function createServerMap( + id: string, type: MapType, regionsX: number, regionsY: number, defaultTile = TileType.None, usage = MapUsage.Public, + initRegions = true +): ServerMap { + const width = regionsX * REGION_SIZE; + const height = regionsY * REGION_SIZE; + const regions: ServerRegion[] = []; + const state: MapState = { ...defaultMapState }; + const spawnArea = rect(0, 0, 1, 1); + const lockedTiles = new Set(); + + if (regionsX <= 0 || regionsY <= 0 || width > POSITION_MAX || height > POSITION_MAX) { + throw new Error('Invalid map parameters'); + } + + if (initRegions) { + for (let ry = 0; ry < regionsY; ry++) { + for (let rx = 0; rx < regionsX; rx++) { + regions.push(createServerRegion(rx, ry, defaultTile)); + } + } + } + + return { + id, usage, type, flags: MapFlags.None, width, height, state, regions, regionsX, regionsY, defaultTile, spawnArea, + lockedTiles, spawns: new Map(), instance: undefined, lastUsed: Date.now(), controllers: [], + dontUpdateTilesAndColliders: false, tilesLocked: false, editableEntityLimit: 0, editingLocked: false, + }; +} + +export function serverMapInstanceFromTemplate(map: ServerMap): ServerMap { + const { + id, usage, type, flags, width, height, state, regionsX, regionsY, defaultTile, spawnArea, lockedTiles, + editableEntityLimit + } = map; + + return { + id, usage, type, flags, width, height, state: { ...state }, + regions: map.regions.map(cloneServerRegion), + regionsX, regionsY, defaultTile, spawnArea, + lockedTiles, spawns: new Map(), instance: undefined, lastUsed: Date.now(), controllers: [], + dontUpdateTilesAndColliders: true, tilesLocked: map.tilesLocked, + editableEntityLimit, editingLocked: false, + }; +} + +export function copyMapTiles(target: ServerMap, source: ServerMap) { + for (let i = 0; i < target.regions.length; i++) { + const srcRegion = source.regions[i]; + const tgtRegion = target.regions[i]; + tgtRegion.tiles.set(srcRegion.tiles); + tgtRegion.tileIndices.set(srcRegion.tileIndices); + tgtRegion.encodedTiles = srcRegion.encodedTiles; + tgtRegion.colliderDirty = true; + } +} + +export function getMapInfo(map: ServerMap): MapInfo { + return { + type: map.type, + flags: map.flags, + regionsX: map.regionsX, + regionsY: map.regionsY, + defaultTile: map.defaultTile, + editableArea: map.editableArea, + }; +} + +export function getSizeOfMap(map: ServerMap) { + const memory = map.regions.reduce((sum, r) => sum + getSizeOfRegion(r), 0); + const entities = map.regions.reduce((sum, r) => sum + r.entities.length, 0); + return { memory, entities }; +} + +export function setTile(map: ServerMap, x: number, y: number, type: TileType) { + const region = getRegionGlobal(map, x, y); + + if (region) { + const regionX = Math.floor(x) - region.x * REGION_SIZE; + const regionY = Math.floor(y) - region.y * REGION_SIZE; + setRegionTile(map, region, regionX, regionY, type); + } +} + +export function snapshotTiles(map: ServerMap) { + for (const region of map.regions) { + snapshotRegionTiles(region); + } +} + +export function lockTile(map: ServerMap, x: number, y: number) { + const index = ((x | 0) + (y | 0) * map.width) | 0; + map.lockedTiles.add(index); +} + +export function lockTiles(map: ServerMap, x: number, y: number, w: number, h: number) { + for (let iy = 0; iy < h; iy++) { + for (let ix = 0; ix < w; ix++) { + lockTile(map, x + ix, y + iy); + } + } +} + +export function isTileLocked(map: ServerMap, x: number, y: number) { + const index = ((x | 0) + (y | 0) * map.width) | 0; + return map.lockedTiles.has(index); +} + +export function serializeTiles(map: ServerMap) { + const tilesData: number[] = []; + const data: number[] = []; + const { width, height } = map; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + tilesData.push(getTile(map, x, y)); + } + } + + for (let i = 0; i < tilesData.length; i++) { + const tile = tilesData[i]; + let count = 1; + + while (tilesData.length > (i + 1) && tilesData[i + 1] === tile && count < 255) { + count++; + i++; + } + + data.push(count, tile); + } + + return new Uint8Array(data); +} + +export function serializeMap(map: ServerMap): MapData { + const { width, height } = map; + const tiles = fromByteArray(serializeTiles(map)); + return { width, height, tiles }; +} + +export function deserializeMap(map: ServerMap, { tiles, width }: MapData, { offsetX = 0, offsetY = 0 }: MapLoadOptions = {}) { + const decodedTiles = deserializeTiles(tiles!); + + for (let i = 0; i < decodedTiles.length; i++) { + const x = i % width; + const y = Math.floor(i / width); + setTile(map, x + offsetX, y + offsetY, decodedTiles[i]); + } +} + +export function saveMap(map: ServerMap, saveOptions: MapSaveOptions): MapData { + const data: MapData = { width: map.width, height: map.height }; + + if (saveOptions.saveTiles) { + data.tiles = serializeMap(map).tiles; + } + + if (saveOptions.saveEntities) { + data.entities = []; + + for (const region of map.regions) { + for (const entity of region.entities) { + if (!hasFlag(entity.serverFlags, ServerFlags.DoNotSave) && !hasFlag(entity.flags, EntityFlags.Debug)) { + if (saveOptions.saveOnlyEditableEntities && !hasFlag(entity.state, EntityState.Editable)) + continue; + + const options = entity.options && Object.keys(entity.options).length > 0 ? entity.options : undefined; + const name = entity.name; + data.entities.push({ type: getEntityTypeName(entity.type), x: entity.x, y: entity.y, options, name }); + } + } + } + } + + if (saveOptions.saveWalls) { + const controller = map.controllers.find(c => c.toggleWall) as WallController | undefined; + + if (controller) { + data.walls = controller.serialize(); + } + } + + return data; +} + +export async function saveMapToFile(map: ServerMap, fileName: string, options: MapSaveOptions) { + const data = saveMap(map, options); + const json = JSON.stringify(data, null, 2); + await writeFileAsync(fileName, json, 'utf8'); +} + +export async function saveMapToFileBinary(map: ServerMap, fileName: string) { + const tiles = serializeTiles(map); + const buffer = new Uint8Array(4 + 4 + tiles.byteLength); + const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); + view.setInt32(0, map.width, true); + view.setInt32(4, map.height, true); + buffer.set(tiles, 8); + await writeFileAsync(fileName, Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength)); +} + +export async function saveMapToFileBinaryAlt(map: ServerMap, fileName: string) { + const buffer = Buffer.alloc(4 + 4 + map.width * map.height); + buffer.writeUInt32LE(map.width, 0); + buffer.writeUInt32LE(map.height, 4); + + for (let y = 0, i = 8; y < map.height; y++) { + for (let x = 0; x < map.width; x++ , i++) { + buffer.writeUInt8(getTile(map, x, y), i); + } + } + + await writeFileAsync(fileName, buffer); +} + +export async function saveEntitiesToFile(map: ServerMap, fileName: string) { + const lines: string[] = []; + + for (const region of map.regions) { + for (const entity of region.entities) { + lines.push(`${getEntityTypeName(entity.type)} ${entity.x} ${entity.y}`); + } + } + + await writeFileAsync(fileName, lines.join('\n'), 'utf8'); +} + +export function loadMap(world: World, map: ServerMap, data: MapData, loadOptions: MapLoadOptions) { + if (data.tiles) { + deserializeMap(map, data, loadOptions); + } + + if (loadOptions.loadOnlyTiles) + return; + + if (loadOptions.loadEntitiesAsEditable) { + const entitiesToRemove: ServerEntity[] = []; + + for (const region of map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.state, EntityState.Editable)) { + entitiesToRemove.push(entity); + } + } + } + + for (const entity of entitiesToRemove) { + world.removeEntity(entity, map); + } + } + + if (loadOptions.loadEntities && data.entities) { + for (const { x, y, type, name, options } of data.entities) { + const typeNumber = getEntityType(type); + const entity = createAnEntity(typeNumber, 0, x, y, options, mockPaletteManager, world); + + if (name) { + setEntityName(entity, name); + } + + if (loadOptions.loadEntitiesAsEditable) { + entity.state |= EntityState.Editable; + } + + world.addEntity(entity, map); + } + } + + if (loadOptions.loadWalls && data.walls) { + const controller = map.controllers.find(c => c.toggleWall) as WallController | undefined; + + if (controller) { + controller.deserialize(data.width, data.height, data.walls); + } + } +} + +export async function loadMapFromFile(world: World, map: ServerMap, fileName: string, options: MapLoadOptions) { + const json = await readFileAsync(fileName, 'utf8'); + const data = JSON.parse(json); + loadMap(world, map, data, options); +} + +export function saveRegionCollider(region: ServerRegion) { + const canvas = createCanvas(REGION_WIDTH, REGION_HEIGHT); + const context = canvas.getContext('2d')!; + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + + context.fillStyle = '#eee'; + + for (let y = 0, i = 0; y < REGION_SIZE; y++ , i++) { + for (let x = 0; x < REGION_SIZE; x++ , i++) { + if ((i % 2) === 0) { + context.fillRect(x * tileWidth, y * tileHeight, tileWidth, tileHeight); + } + } + } + + context.globalAlpha = 0.8; + context.fillStyle = 'red'; + + for (let y = 0; y < REGION_HEIGHT; y++) { + for (let x = 0; x < REGION_WIDTH; x++) { + if (region.collider[x + y * REGION_WIDTH]) { + context.fillRect(x, y, 1, 1); + } + } + } + + writeFileSync(pathTo('store', 'collider.png'), canvas.toBuffer()); +} + +function distanceSquaredToRegion(x: number, y: number, region: ServerRegion) { + const left = region.x * REGION_SIZE; + const top = region.y * REGION_SIZE; + const right = left + REGION_SIZE; + const bottom = top + REGION_SIZE; + const dx = x < left ? (left - x) : (x > right ? (x - right) : 0); + const dy = y < left ? (top - y) : (y > bottom ? (y - bottom) : 0); + return dx * dx + dy * dy; +} + +export function findClosestEntity( + map: ServerMap, originX: number, originY: number, predicate: (entity: ServerEntity) => boolean +): ServerEntity | undefined { + let minX = Math.floor(originX / REGION_SIZE); + let minY = Math.floor(originY / REGION_SIZE); + let maxX = minX; + let maxY = minY; + + let closest: ServerEntity | undefined = undefined; + let closestDist = Number.MAX_VALUE; + + while (minX >= 0 || minY >= 0 || maxX < map.regionsX || maxY < map.regionsY) { + let regionsChecked = 0; + + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x = (x === maxX || y === minY || y === maxY) ? x + 1 : maxX) { + if (x >= 0 && y >= 0 && x < map.regionsX && y < map.regionsY) { + const region = getRegion(map, x, y); + + if (distanceSquaredToRegion(originX, originY, region) < closestDist) { + regionsChecked++; + + for (const entity of region.entities) { + if (predicate(entity)) { + const dist = distanceSquaredXY(originX, originY, entity.x, entity.y); + + if (dist < closestDist) { + closest = entity; + closestDist = dist; + } + } + } + } + } + } + } + + if (!regionsChecked) { + break; + } + + minX -= 1; + minY -= 1; + maxX += 1; + maxY += 1; + } + + return closest; +} + +export function findEntities(map: ServerMap, predicate: (entity: ServerEntity) => boolean): ServerEntity[] { + const entities: ServerEntity[] = []; + + for (const region of map.regions) { + for (const entity of region.entities) { + if (predicate(entity)) { + entities.push(entity); + } + } + } + + return entities; +} + +// TODO: maybe only regions in bounds, instead of adding 1 region border ? +function forEachRegionInBounds(map: ServerMap, bounds: Rect, callback: (region: ServerRegion) => void) { + const minX = Math.max(0, Math.floor(bounds.x / REGION_SIZE) - 1) | 0; + const minY = Math.max(0, Math.floor(bounds.y / REGION_SIZE) - 1) | 0; + const maxX = Math.min(Math.floor((bounds.x + bounds.w) / REGION_SIZE) + 1, map.regionsX - 1) | 0; + const maxY = Math.min(Math.floor((bounds.y + bounds.h) / REGION_SIZE) + 1, map.regionsY - 1) | 0; + + for (let ry = minY; ry <= maxY; ry++) { + for (let rx = minX; rx <= maxX; rx++) { + const region = getRegion(map, rx, ry); + callback(region); + } + } +} + +export function findEntitiesInBounds(map: ServerMap, bounds: Rect) { + const result: ServerEntity[] = []; + + forEachRegionInBounds(map, bounds, region => { + for (const entity of region.entities) { + if (containsPoint(0, 0, bounds, entity.x, entity.y)) { + result.push(entity); + } + } + }); + + return result; +} + +export function updateMapState(map: ServerMap, update: Partial) { + Object.assign(map.state, update); + + for (const region of map.regions) { + for (const client of region.clients) { + client.mapUpdate(map.state); + } + } +} + +export function hasAnyClients(map: ServerMap) { + for (const region of map.regions) { + if (region.clients.length > 0) { + return true; + } + } + + return false; +} + +export function createMinimap(world: World, map: ServerMap) { + const { width, height } = map; + const buffer = new Uint32Array(width * height); + + for (let y = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++) { + const tile = getTile(map, x, y); + buffer[x + y * width] = getTileColor(tile, world.season); + } + } + + // map.entities = info.entities + // .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager)); + + // for (let i = 1; i <= 2; i++) { + // for (const e of map.entities) { + // if (e.minimap && e.minimap.order === i) { + // const { color, rect } = e.minimap; + // mapContext.fillStyle = colorToCSS(color); + // mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h); + // } + // } + // } + + // canvas.width = mapCanvas.width * scale; + // canvas.height = mapCanvas.height * scale; + // const context = canvas.getContext('2d')!; + // context.save(); + + // if (scale >= 1) { + // disableImageSmoothing(context); + // } + + // context.scale(scale, scale); + // context.drawImage(mapCanvas, 0, 0); + // context.restore(); + + return new Uint8Array(buffer.buffer); +} diff --git a/src/ts/server/serverRegion.ts b/src/ts/server/serverRegion.ts new file mode 100644 index 0000000..19e24c5 --- /dev/null +++ b/src/ts/server/serverRegion.ts @@ -0,0 +1,242 @@ +import { random } from 'lodash'; +import { ServerEntity, ServerRegion, EntityUpdateBase, ServerMap } from './serverInterfaces'; +import { TileType, EntityFlags, UpdateFlags, canWalk } from '../common/interfaces'; +import { compressTiles } from '../common/compress'; +import { rect, withBorder, withPadding } from '../common/rect'; +import { + REGION_BORDER, tileHeight, tileWidth, REGION_SIZE, TILES_RESTORE_MIN_SEC, TILES_RESTORE_MAX_SEC +} from '../common/constants'; +import { rectToScreen } from '../common/positionUtils'; +import { removeItem, hasFlag } from '../common/utils'; +import { canCollideWith } from '../common/collision'; +import { invalidateRegionsCollider, getRegionTile } from '../common/region'; +import { setColliderDirty, setTilesDirty } from '../common/worldMap'; + +const subscribeBoundsBottomPad = 3; + +const randoms = new Uint8Array(REGION_SIZE * REGION_SIZE); + +export function createServerRegion(x: number, y: number, defaultTile = TileType.Dirt): ServerRegion { + const bounds = rect(x * REGION_SIZE, y * REGION_SIZE, REGION_SIZE, REGION_SIZE); + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + const tileIndices = new Int16Array(REGION_SIZE * REGION_SIZE); + const collider = new Uint8Array(REGION_SIZE * REGION_SIZE * tileWidth * tileHeight); + + tileIndices.fill(-1); + + if (defaultTile !== 0) { + for (let i = 0; i < tiles.length; i++) { + tiles[i] = defaultTile; + } + } + + return { + x, y, + entityUpdates: [], + entityRemoves: [], + tileUpdates: [], + clients: [], + entities: [], + movables: [], + colliders: [], + collider, + colliderDirty: true, + randoms, + tiles, + tileIndices, + tilesDirty: true, + tilesSnapshot: undefined, + tilesTimeouts: undefined, + encodedTiles: undefined, + reusedUpdates: 0, + bounds, + boundsWithBorder: withBorder(bounds, REGION_BORDER), + subscribeBounds: rectToScreen(withPadding( + bounds, REGION_SIZE, REGION_SIZE, REGION_SIZE + subscribeBoundsBottomPad, REGION_SIZE)), + unsubscribeBounds: rectToScreen(withPadding( + bounds, REGION_SIZE + 1, REGION_SIZE + 1, REGION_SIZE + subscribeBoundsBottomPad + 1, REGION_SIZE + 1)), + }; +} + +export function cloneServerRegion(region: ServerRegion): ServerRegion { + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + tiles.set(region.tiles); + + return { + x: region.x, + y: region.y, + entityUpdates: [], + entityRemoves: [], + tileUpdates: [], + clients: [], + entities: [], + movables: [], + colliders: [], + collider: region.collider, + colliderDirty: false, + randoms, + tiles, + tileIndices: region.tileIndices, + tilesDirty: false, + tilesSnapshot: undefined, + tilesTimeouts: undefined, + encodedTiles: region.encodedTiles, + reusedUpdates: 0, + bounds: region.bounds, + boundsWithBorder: region.boundsWithBorder, + subscribeBounds: region.subscribeBounds, + unsubscribeBounds: region.unsubscribeBounds, + }; +} + +export function getSizeOfRegion(region: ServerRegion) { + let size = region.tiles.byteLength; + size += region.tileIndices.byteLength; + size += region.tilesSnapshot ? region.tilesSnapshot.byteLength : 0; + size += region.tilesTimeouts ? region.tilesTimeouts.byteLength : 0; + size += region.encodedTiles ? region.encodedTiles.byteLength : 0; + size += region.collider ? region.collider.byteLength : 0; + return size; +} + +export function addEntityToRegion(region: ServerRegion, entity: ServerEntity, map: ServerMap) { + region.entities.push(entity); + + if (canCollideWith(entity)) { + region.colliders.push(entity); + invalidateRegionsCollider(region, map); + } + + if (hasFlag(entity.flags, EntityFlags.Movable)) { + region.movables.push(entity); + } +} + +export function removeEntityFromRegion(region: ServerRegion, entity: ServerEntity, map: ServerMap) { + const removed = removeItem(region.entities, entity); + + if (canCollideWith(entity)) { + removeItem(region.colliders, entity); + invalidateRegionsCollider(region, map); + } + + removeItem(region.movables, entity); + return removed; +} + +export function pushUpdateEntityToRegion(region: ServerRegion, update: EntityUpdateBase) { + const index = findUpdate(region, update.entity); + + if (index === -1) { + region.entityUpdates.push({ x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined, ...update }); + } else { + region.reusedUpdates++; + const existing = region.entityUpdates[index]; + existing.flags |= update.flags; + + if (hasFlag(update.flags, UpdateFlags.Position)) { + const { x = 0, y = 0, vx = 0, vy = 0 } = update; + existing.x = x; + existing.y = y; + existing.vx = vx; + existing.vy = vy; + } + + if (hasFlag(update.flags, UpdateFlags.Options)) { + existing.options = { ...existing.options, ...update.options }; + } + + if (hasFlag(update.flags, UpdateFlags.PlayerState)) { + existing.playerState = update.playerState!; + } + + if (hasFlag(update.flags, UpdateFlags.Action)) { + existing.action = update.action!; + } + } +} + +export function pushRemoveEntityToRegion(region: ServerRegion, entity: ServerEntity) { + region.entityRemoves.push(entity.id); +} + +export function setRegionTile(map: ServerMap, region: ServerRegion, x: number, y: number, type: TileType, skipRestore = false) { + const old = getRegionTile(region, x, y); + + if (type === old) + return; + + const index = x | (y << 3); + region.tiles[index] = type; + region.tileUpdates.push({ x, y, type: type }); + region.encodedTiles = undefined; + + if (region.tilesTimeouts && !skipRestore) { + region.tilesTimeouts[index] = random(TILES_RESTORE_MIN_SEC, TILES_RESTORE_MAX_SEC); + } + + if (canWalk(old) !== canWalk(type)) { + setTilesDirty(map, region.x * REGION_SIZE + x - 1, region.y * REGION_SIZE + y - 1, 3, 3); + setColliderDirty(map, region, x, y); + } +} + +export function resetRegionUpdates(region: ServerRegion) { + region.entityUpdates.length = 0; + region.entityRemoves.length = 0; + region.tileUpdates.length = 0; + region.reusedUpdates = 0; +} + +export function snapshotRegionTiles(region: ServerRegion) { + region.tilesSnapshot = region.tiles.slice(); + region.tilesTimeouts = new Uint8Array(region.tiles.length); +} + +export function getRegionTiles(region: ServerRegion) { + if (region.encodedTiles === undefined) { + region.encodedTiles = compressTiles(region.tiles); + } + + return region.encodedTiles; +} + +export function resetTiles(map: ServerMap, region: ServerRegion) { + if (region.tilesSnapshot && region.tilesTimeouts) { + for (let i = 0; i < region.tilesTimeouts.length; i++) { + region.tilesTimeouts[i] = 0; + + if (region.tiles[i] !== region.tilesSnapshot[i]) { + const x = i % REGION_SIZE; + const y = Math.floor(i / REGION_SIZE); + setRegionTile(map, region, x, y, region.tilesSnapshot[i], true); + } + } + } +} + +export function tickTilesRestoration(map: ServerMap, region: ServerRegion) { + if (region.tilesSnapshot && region.tilesTimeouts) { + for (let i = 0; i < region.tilesTimeouts.length; i++) { + if (region.tilesTimeouts[i] > 0) { + region.tilesTimeouts[i]--; + + if (region.tilesTimeouts[i] === 0 && region.tiles[i] !== region.tilesSnapshot[i]) { + const x = i % REGION_SIZE; + const y = Math.floor(i / REGION_SIZE); + setRegionTile(map, region, x, y, region.tilesSnapshot[i], true); + } + } + } + } +} + +function findUpdate({ entityUpdates }: ServerRegion, entity: ServerEntity) { + for (let i = 0; i < entityUpdates.length; i++) { + if (entityUpdates[i].entity === entity) { + return i; + } + } + + return -1; +} diff --git a/src/ts/server/serverUtils.ts b/src/ts/server/serverUtils.ts new file mode 100644 index 0000000..d4dba09 --- /dev/null +++ b/src/ts/server/serverUtils.ts @@ -0,0 +1,145 @@ +import * as fs from 'fs'; +import { exec, ExecOptions } from 'child_process'; +import { noop } from 'lodash'; +import { Server } from 'ag-sockets'; +import { AccountData, SocialSite, PonyObject, AccountDataFlags } from '../common/interfaces'; +import { IAccount, ICharacter, IAuth } from './db'; +import { TokenService, TokenData } from './serverInterfaces'; +import { AccountFlags, CharacterFlags, InternalGameServerState } from '../common/adminInterfaces'; +import { supporterLevel, isPastSupporter } from '../common/adminUtils'; +import { hasFlag, cloneDeep, formatISODate } from '../common/utils'; +import * as paths from './paths'; + +export function tokenService(socket: Server): TokenService { + return { + clearTokensForAccount(accountId: string) { + socket.clearTokens((_, data: TokenData) => data.accountId === accountId); + }, + clearTokensAll() { + socket.clearTokens(() => true); + }, + createToken(token: TokenData) { + return socket.token(token); + } + }; +} + +export function isServerOffline(server: InternalGameServerState) { + return server.state.dead || !!server.state.settings.isServerOffline || !!server.state.shutdown; +} + +export function toAccountData(account: IAccount): AccountData { + const { _id, name, birthdate, birthyear, characterCount, roles, settings, flags } = account; + + return { + id: _id.toString(), + name, characterCount, + birthdate: birthdate && formatISODate(birthdate) || '', + birthyear, + settings: cloneDeep(settings || {}), + supporter: supporterLevel(account) || undefined, + roles: (roles && roles.length) ? [...roles] : undefined, + flags: (hasFlag(flags, AccountFlags.DuplicatesNotification) ? AccountDataFlags.Duplicates : 0) | + (isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0), + }; +} + +export const toPonyObjectFields = '_id name info desc site tag lastUsed flags'; + +export function toPonyObject(character: ICharacter): PonyObject; +export function toPonyObject(character: ICharacter | undefined): PonyObject | null; +export function toPonyObject(character: ICharacter | undefined): PonyObject | null { + return character ? { + id: character._id.toString(), + name: character.name, + desc: character.desc || '', + info: character.info || '', + site: character.site ? character.site.toString() : undefined, + tag: character.tag || undefined, + lastUsed: character.lastUsed && character.lastUsed.toISOString(), + hideSupport: hasFlag(character.flags, CharacterFlags.HideSupport) ? true : undefined, + respawnAtSpawn: hasFlag(character.flags, CharacterFlags.RespawnAtSpawn) ? true : undefined, + } : null; +} + +export function toPonyObjectAdmin(character: ICharacter): PonyObject; +export function toPonyObjectAdmin(character: ICharacter | undefined): PonyObject | null; +export function toPonyObjectAdmin(character: ICharacter | undefined): PonyObject | null { + return character ? { ...toPonyObject(character)!, creator: character.creator } : null; +} + +export const toSocialSiteFields = '_id name provider url'; + +export function toSocialSite({ _id, name, provider, url }: IAuth): SocialSite { + return { id: _id.toString(), name, provider, url }; +} + +/* istanbul ignore next */ +export function execAsync(command: string, options?: ExecOptions) { + return new Promise<{ stdout: string; stderr: string; }>((resolve, reject) => { + exec(command, options || {}, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve({ stdout, stderr }); + } + }); + }); +} + +/* istanbul ignore next */ +export async function logErrorToFile(message: string, data: any) { + const fileName = `error-${Date.now()}.json`; + const filePath = paths.pathTo('store', fileName); + await fs.writeFileAsync(filePath, JSON.stringify({ message, data }, null, 2), 'utf8'); + return fileName; +} + +/* istanbul ignore next */ +export async function getDiskSpace() { + // NOTE: add your own code here + return ''; +} + +/* istanbul ignore next */ +export async function getCertificateExpirationDate() { + // NOTE: add your own code here + return ''; +} + +/* istanbul ignore next */ +export async function getMemoryUsage() { + // NOTE: add your own code here + return `0%`; +} + +/* istanbul ignore next */ +export function handlePromiseDefault(promise: Promise, errorHandler: any = noop) { + Promise.resolve(promise).catch(errorHandler); +} + +export function cached(func: T, cacheTimeout = 1000): T & { clear(...args: any[]): void; } { + const cacheMap = new Map(); + + const cachedFunc: any = (...args: any[]) => { + const cacheKey = JSON.stringify(args); + const cache = cacheMap.get(cacheKey); + + if (cache) { + clearTimeout(cache.timeout); + cache.timeout = setTimeout(() => cacheMap.delete(cacheKey), cacheTimeout); + return cache.result; + } else { + const result = func(...args); + const timeout = setTimeout(() => cacheMap.delete(cacheKey), cacheTimeout); + cacheMap.set(cacheKey, { result, timeout }); + return result; + } + }; + + cachedFunc.clear = (...args: any[]) => { + cacheMap.delete(JSON.stringify(args)); + }; + + return cachedFunc; +} diff --git a/src/ts/server/services/actionLimiter.ts b/src/ts/server/services/actionLimiter.ts new file mode 100644 index 0000000..970432d --- /dev/null +++ b/src/ts/server/services/actionLimiter.ts @@ -0,0 +1,44 @@ +import { CounterService } from './counter'; +import { IClient } from '../serverInterfaces'; +import { isMutedOrShadowed, isIgnored } from '../playerUtils'; + +export const enum LimiterResult { + Yes = 0, + SameAccount = 1, + MutedOrShadowed = 2, + Ignored = 3, + LimitReached = 4, + TargetOffline = 5, +} + +export class ActionLimiter { + private counters: CounterService; + constructor(clearTimeout: number, private countLimit: number) { + this.counters = new CounterService(clearTimeout); + this.counters.start(); + } + canExecute(requester: IClient, target: IClient): LimiterResult { + if (requester === target || requester.accountId === target.accountId) + return LimiterResult.SameAccount; + + if (target.offline) + return LimiterResult.TargetOffline; + + if (isMutedOrShadowed(requester)) + return LimiterResult.MutedOrShadowed; + + if (isIgnored(requester, target) || isIgnored(target, requester)) + return LimiterResult.Ignored; + + if (this.counters.get(requester.accountId).count >= this.countLimit) + return LimiterResult.LimitReached; + + return LimiterResult.Yes; + } + count(requester: IClient) { + return this.counters.add(requester.accountId).count; + } + dispose() { + this.counters.stop(); + } +} diff --git a/src/ts/server/services/adminService.ts b/src/ts/server/services/adminService.ts new file mode 100644 index 0000000..90751f3 --- /dev/null +++ b/src/ts/server/services/adminService.ts @@ -0,0 +1,542 @@ +import { sort } from 'timsort'; +import { remove } from 'lodash'; +import { Subject } from 'rxjs'; +import * as db from '../db'; +import { LiveList } from './liveList'; +import { removeItem, includes, toInt, fromNow } from '../../common/utils'; +import { + Account, Auth, Origin, OriginRef, OriginInfo, Character, ListListener, OriginInfoBase, Event, eventFields, PonyIdDateName +} from '../../common/adminInterfaces'; +import { + addToMap, removeFromMap, emailName, getIdsFromNote, compareAccounts, compareOriginRefs, compareByName, + compareAuths, createIdStore, getPotentialDuplicates, createPotentialDuplicatesFilter +} from '../../common/adminUtils'; +import { logger, logPerformance } from '../logger'; +import { ObservableList } from './observableList'; +import { HOUR } from '../../common/constants'; +import { getLoginServer } from '../internal'; + +function addAuthToAccount(account: Account, auth: Auth, log: string) { + const existingAuth = account.auths!.find(a => a._id === auth._id); + + if (existingAuth) { // TODO: remove + console.log('duplicate auth', auth._id, 'to', account._id, log); + } else { + account.authsList!.pushOrdered(auth, compareAuths); + } +} + +function pushUnique(list: T[], item: T) { + if (list.indexOf(item) === -1) { + list.push(item); + } +} + +function removeAuthFromAccount(account: Account, auth: Auth) { + return account.authsList!.remove(auth); +} + +function addPonyToAccount(account: Account, pony: Character) { + if (account.poniesList) { + account.poniesList.pushOrdered(pony, compareByName); + } +} + +function removePonyFromAccount(account: Account, pony: Character) { + if (account.poniesList) { + return account.poniesList.remove(pony); + } else { + return false; + } +} + +function getTotalPledged(auths: Auth[] | undefined) { + return Math.floor((auths || []).reduce((sum, a) => sum + toInt(a.pledged), 0) / 100); +} + +export class AdminService { + readonly accounts: LiveList; + readonly origins: LiveList; + readonly auths: LiveList; + readonly ponies: LiveList; + readonly events: LiveList; + readonly accountDeleted = new Subject(); + private emailMap = new Map(); + private noteRefMap = new Map(); + private browserIdMap = new Map(); + private unassignedAuths: Auth[] = []; + private unassignedPonies: Character[] = []; + constructor() { + const accountId = createIdStore(); + + this.accounts = new LiveList(db.Account, { + fields: [ + '_id', 'updatedAt', 'createdAt', 'lastVisit', 'name', 'birthdate', 'origins', 'ignores', 'emails', 'note', + 'counters', 'mute', 'shadow', 'ban', 'flags', 'roles', 'characterCount', 'patreon', 'supporter', + 'supporterDeclinedSince', 'lastBrowserId', 'noteUpdated', 'alert', 'birthyear' + ], + clean: ({ + _id, createdAt, updatedAt, lastVisit, name, birthdate, origins, ignoresCount, emails, note, counters, mute, + shadow, ban, flags, roles, characterCount, patreon, supporter, supporterDeclinedSince, auths, noteUpdated, + alert, birthyear, + }) => + ({ + _id, createdAt, updatedAt, lastVisit, name, birthdate, origins, ignoresCount: toInt(ignoresCount), + emails, note, counters, mute, shadow, ban, flags: toInt(flags), roles, birthyear, + characterCount: toInt(characterCount), patreon: toInt(patreon), supporter: toInt(supporter), + supporterDeclinedSince, totalPledged: getTotalPledged(auths), noteUpdated, alert, + }), + fix: account => { + account._id = accountId(account._id); + account.nameLower = account.name.toLowerCase(); + account.ignoresCount = account.ignores ? account.ignores.length : 0; + account.ignores = undefined; + account.origins = (account.origins || []).map(o => ({ ip: o.ip, country: o.country, last: o.last })); + }, + onAdd: account => { + account.auths = []; + // account.ponies = []; + account.originsRefs = []; + account.authsList = new ObservableList(account.auths!, a => a._id); + + if (account.lastBrowserId) { + this.addBrowserIdToMap(account.lastBrowserId, account); + } + + if (account.emails) { + for (const e of account.emails) { + this.addEmailToMap(e, account); + } + } + + this.addNoteRefsToMap(account.note, account); + this.updateOriginRefs(account); + this.accountsForPotentialDuplicatesCheck.push(account); + }, + onUpdate: (oldAccount, newAccount) => { + if (oldAccount.emails) { + for (const e of oldAccount.emails) { + if (!includes(newAccount.emails, e)) { + this.removeEmailFromMap(e, oldAccount); + } + } + } + + if (newAccount.emails) { + for (const e of newAccount.emails) { + if (!includes(oldAccount.emails, e)) { + this.addEmailToMap(e, oldAccount); + } + } + } + + if (oldAccount.note !== newAccount.note) { + this.removeNoteRefsFromMap(oldAccount.note, oldAccount); + this.addNoteRefsToMap(newAccount.note, oldAccount); + } + + if (oldAccount.lastBrowserId !== newAccount.lastBrowserId) { + oldAccount.lastBrowserId && this.removeBrowserIdFromMap(oldAccount.lastBrowserId, oldAccount); + newAccount.lastBrowserId && this.addBrowserIdToMap(newAccount.lastBrowserId, oldAccount); + } + + Object.assign(oldAccount, newAccount); + + if (newAccount.birthyear === undefined) { + oldAccount.birthyear = undefined; + } + + if (newAccount.alert === undefined) { + oldAccount.alert = undefined; + } + + if (newAccount.patreon === undefined) { + oldAccount.patreon = undefined; + } + + if (newAccount.supporter === undefined) { + oldAccount.supporter = undefined; + } + + this.updateOriginRefs(oldAccount); + this.accountsForPotentialDuplicatesCheck.push(oldAccount); + }, + onAddedOrUpdated: () => { + this.assignItems(this.unassignedAuths, (account, auth) => addAuthToAccount(account, auth, 'onAddedOrUpdated')); + this.assignItems(this.unassignedPonies, addPonyToAccount); + }, + onDelete: account => { + account.origins = []; + this.updateOriginRefs(account); + + if (account.emails) { + for (const email of account.emails) { + this.removeEmailFromMap(email, account); + } + } + + account.lastBrowserId && this.removeBrowserIdFromMap(account.lastBrowserId, account); + this.removeNoteRefsFromMap(account.note, account); + this.accountDeleted.next(account); + }, + onFinished: () => { + sort(this.accounts.items, compareAccounts); + this.auths.start(); + }, + }); + + this.origins = new LiveList(db.Origin, { + fields: ['_id', 'updatedAt', 'ip', 'country', 'mute', 'shadow', 'ban'], + clean: ({ _id, updatedAt, ip, country, mute, shadow, ban, accounts }) => + ({ _id, updatedAt, ip, country, mute, shadow, ban, accountsCount: accounts ? accounts.length : 0 }), + onAdd: origin => { + origin.accounts = []; + }, + onSubscribeToMissing: ip => ({ ip, country: '??' }) as any, + }, origin => origin.ip); + + this.auths = new LiveList(db.Auth, { + fields: ['_id', 'updatedAt', 'account', 'provider', 'name', 'url', 'disabled', 'banned', 'pledged', 'lastUsed'], + clean: ({ _id, updatedAt, account, provider, name, url, disabled, banned, pledged, lastUsed }) => + ({ _id, updatedAt, account, provider, name, url, disabled, banned, pledged, lastUsed }), + fix: auth => { + if (auth.account) { + auth.account = accountId(auth.account.toString()); + } + }, + onAdd: auth => { + this.assignAccount(auth, this.unassignedAuths, account => addAuthToAccount(account, auth, 'onAdd')); + }, + onUpdate: this.createUpdater({ + remove: (account, auth) => removeAuthFromAccount(account, auth) || removeItem(this.unassignedAuths, auth), + add: (account, auth) => + account ? addAuthToAccount(account, auth, 'onUpdate') : pushUnique(this.unassignedAuths, auth), + }), + onDelete: auth => { + removeItem(this.unassignedAuths, auth); + this.accounts.for(auth.account, account => removeAuthFromAccount(account, auth)); + }, + onFinished: () => { + logger.info('Admin service loaded'); + }, + }); + + this.ponies = new LiveList(db.Character, { + fields: ['_id', 'createdAt', 'updatedAt', 'lastUsed', 'account', 'name', 'flags'], + noStore: true, + clean: ({ _id, createdAt, updatedAt, account, name, flags, lastUsed }) => + ({ _id, createdAt, updatedAt, account, name, flags, lastUsed }), + fix: pony => { + if (pony.account) { + pony.account = accountId(pony.account.toString()); + } + }, + ignore: pony => { + const account = this.accounts.get(pony.account); + return account === undefined || account.ponies === undefined; + }, + onAdd: pony => { + this.assignAccount(pony, this.unassignedPonies, account => addPonyToAccount(account, pony)); + }, + onUpdate: this.createUpdater({ + remove: (account, pony) => removePonyFromAccount(account, pony) || removeItem(this.unassignedPonies, pony), + add: (account, pony) => account ? addPonyToAccount(account, pony) : pushUnique(this.unassignedPonies, pony), + }), + onDelete: pony => { + removeItem(this.unassignedPonies, pony); + this.accounts.for(pony.account, account => removePonyFromAccount(account, pony)); + }, + // afterAssign: (from, to) => Promise.all([updateCharacterCount(from), updateCharacterCount(to)]), + }); + + this.events = new LiveList(db.Event, { + fields: eventFields, + clean: ({ _id, createdAt, updatedAt, message, desc, account, pony, origin }) => + ({ _id, createdAt, updatedAt, message, desc, account, pony, origin }), + }); + + setTimeout(() => this.events.start(), 100); + setTimeout(() => this.ponies.start(), 200); + setTimeout(() => this.origins.start(), 300); + setTimeout(() => this.accounts.start(), 400); + } + get loaded() { + return this.accounts.loaded && this.origins.loaded && this.auths.loaded; + } + removedItem(type: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) { + if (type === 'accounts') { + this.accounts.removed(id); + } else if (type === 'origins') { + this.origins.removed(id); + } else if (type === 'auths') { + this.auths.removed(id); + } else if (type === 'ponies') { + this.ponies.removed(id); + } else { + console.warn(`Unhandled removedItem for type: ${type}`); + } + } + getAccountsByNoteRef(accountId: string) { + return this.noteRefMap.get(accountId) || []; + } + getAccountsByEmailName(emailName: string) { + return this.emailMap.get(emailName) || []; + } + getAccountsByBrowserId(browserId: string) { + return this.browserIdMap.get(browserId); + } + removeOriginsFromAccount(accountId: string, ips?: string[]) { + const account = this.accounts.get(accountId); + + if (account) { + if (ips) { + if (remove(account.origins, o => includes(ips, o.ip)).length) { + this.updateOriginRefs(account); + } + } else if (account.origins.length) { + account.origins = []; + this.updateOriginRefs(account); + } + } + } + subscribeToAccountAuths(accountId: string, listener: ListListener) { + const account = this.accounts.get(accountId); + + if (account) { + return account.authsList!.subscribe(listener); + } else { + return undefined; + } + } + subscribeToAccountOrigins(accountId: string, listener: ListListener) { + const account = this.accounts.get(accountId); + + if (account) { + if (!account.originsList) { + account.originsList = new ObservableList( + account.originsRefs!, ({ origin, last }) => ({ ip: origin.ip, country: origin.country, last })); + } + + return account.originsList.subscribe(listener); + } + + return undefined; + } + subscribeToAccountPonies(accountId: string, listener: ListListener) { + const account = this.accounts.get(accountId); + + if (account) { + if (!account.ponies) { + account.ponies = this.ponies.items.filter(p => p.account === account._id); + this.ponies.fetch({ account: account._id }); + } + + if (!account.poniesList) { + account.poniesList = new ObservableList( + account.ponies!, p => ({ id: p._id, name: p.name, date: p.lastUsed ? p.lastUsed.getTime() : 0 })); + } + + return account.poniesList.subscribe(listener); + } + + return undefined; + } + cleanupOriginsList(accountId: string) { + const account = this.accounts.get(accountId); + + if (account && account.originsList && !account.originsList.hasSubscribers()) { + account.originsList = undefined; + } + } + cleanupPoniesList(accountId: string) { + const account = this.accounts.get(accountId); + + if (account && account.ponies && account.poniesList && !account.poniesList.hasSubscribers()) { + const ponies = account.ponies; + account.ponies = undefined; + account.poniesList = undefined; + + for (const pony of ponies) { + this.cleanupPony(pony._id); + } + } + } + cleanupPony(ponyId: string) { + const pony = this.ponies.get(ponyId); + + if (pony && !this.ponies.hasSubscriptions(ponyId)) { + const account = this.accounts.get(pony.account); + + if (!account || !account.ponies) { + this.ponies.discard(ponyId); + } + } + } + private duplicateFilter = createPotentialDuplicatesFilter(id => this.browserIdMap.get(id)); + private accountsForPotentialDuplicatesCheck: Account[] = []; + async mergePotentialDuplicates() { + const start = Date.now(); + const duplicateFilter = this.duplicateFilter; + + while (this.accountsForPotentialDuplicatesCheck.length) { + const popedAccount = this.accountsForPotentialDuplicatesCheck.pop()!; + const account = this.getAccount(popedAccount._id); + + if (account && duplicateFilter(account)) { + const threshold = fromNow(-1 * HOUR).getTime(); + const duplicates = getPotentialDuplicates(account, id => this.getAccountsByBrowserId(id)) + .filter(a => a.createdAt && a.createdAt.getTime() < threshold); + + if (duplicates.length) { + const server = getLoginServer('login'); + const duplicate = duplicates[0]; + const accountIsOlder = account.lastVisit && duplicate.lastVisit + && account.lastVisit.getTime() < duplicate.lastVisit.getTime(); + const accountId = accountIsOlder ? duplicate._id : account._id; + const withId = accountIsOlder ? account._id : duplicate._id; + logPerformance(`mergePotentialDuplicates (${Date.now() - start}ms) [yes]`); + await server.api.mergeAccounts(accountId, withId, `by server`, false, true); + return accountId; + } + } + } + + this.accountsForPotentialDuplicatesCheck = []; + + logPerformance(`mergePotentialDuplicates (${Date.now() - start}ms) [no]`); + return undefined; + } + // helpers + private addEmailToMap(email: string, account: Account) { + addToMap(this.emailMap, emailName(email), account); + } + private removeEmailFromMap(email: string, account: Account) { + removeFromMap(this.emailMap, emailName(email), account); + } + private addNoteRefsToMap(note: string, account: Account) { + for (let id of getIdsFromNote(note)) { + if (id !== account._id) { + addToMap(this.noteRefMap, id, account); + } + } + } + private removeNoteRefsFromMap(note: string, account: Account) { + for (let id of getIdsFromNote(note)) { + if (id !== account._id) { + removeFromMap(this.noteRefMap, id, account); + } + } + } + private addBrowserIdToMap(browserId: string, account: Account) { + addToMap(this.browserIdMap, browserId, account); + } + private removeBrowserIdFromMap(browserId: string, account: Account) { + removeFromMap(this.browserIdMap, browserId, account); + } + private getAccount(id: string | undefined) { + return id ? this.accounts.get(id) : undefined; + } + private getOrCreateOrigin({ ip, country }: OriginInfo): Origin { + return this.origins.get(ip) + || this.origins.add({ _id: '', ip, country, accounts: [], updatedAt: new Date(0), createdAt: new Date(0) }); + } + private assignAccount(item: T, unassigned: T[], action: (account: Account) => void) { + const account = this.getAccount(item.account); + + if (account) { + action(account); + } else { + pushUnique(unassigned, item); + } + } + private updateOriginRefs(a: Account) { + if (a.originsRefs) { + for (const o of a.originsRefs) { + removeById(o.origin.accounts!, a._id); + } + } + + const oldOriginRefs = a.originsRefs; + a.originsRefs = a.origins.map(o => { origin: this.getOrCreateOrigin(o), last: o.last }); + + sort(a.originsRefs, compareOriginRefs); + + for (const o of a.originsRefs) { + if (o.origin.accounts && !includes(o.origin.accounts, a)) { + o.origin.accounts.push(a); + } + } + + if (oldOriginRefs) { + for (const o of oldOriginRefs) { + if (!o.origin._id && o.origin.accounts!.length === 0) { + this.origins.removed(o.origin.ip); + } else { + this.origins.trigger(o.origin.ip, o.origin); + } + } + } + + if (a.originsList) { + a.originsList.replace(a.originsRefs); + } + } + private assignItems(unassigned: T[], push: (account: Account, item: T) => void) { + remove(unassigned, item => { + const account = item.account && this.getAccount(item.account); + + if (account) { + push(account, item); + return true; + } else { + return false; + } + }); + } + private createUpdater( + { add, remove }: { + remove: (account: Account, item: T) => void; + add: (account: Account | undefined, item: T) => void; + } + ) { + return (oldItem: T, newItem: T) => { + const oldAccountId = oldItem.account; + const newAccountId = newItem.account; + + Object.assign(oldItem, newItem); + + if (oldAccountId !== newAccountId) { + const oldAccount = this.getAccount(oldAccountId); + const newAccount = this.getAccount(newAccountId); + + if (oldAccount) { + remove(oldAccount, oldItem); + } + + add(newAccount, oldItem); + } + }; + } +} + +function findIndexById(items: T[], id: U): number { + for (let i = 0; i < items.length; i++) { + if (items[i]._id === id) { + return i; + } + } + + return -1; +} + +function removeById(items: T[], id: U): T | undefined { + const index = findIndexById(items, id); + + if (index !== -1) { + const item = items[index]; + items.splice(index, 1); + return item; + } else { + return undefined; + } +} diff --git a/src/ts/server/services/counter.ts b/src/ts/server/services/counter.ts new file mode 100644 index 0000000..480174e --- /dev/null +++ b/src/ts/server/services/counter.ts @@ -0,0 +1,51 @@ +interface Counter { + date: number; + count: number; + items: T[]; +} + +const zeroCounter: Counter = { date: 0, count: 0, items: [] }; + +export class CounterService { + private counters = new Map>(); + private interval: any; + constructor(private clearTimeout: number) { + } + get(id: string): Counter { + return this.counters.get(id) || zeroCounter; + } + add(id: string, item?: T, count = 1) { + const counter = this.counters.get(id) || { date: 0, count: 0, items: [] }; + counter.date = Date.now(); + counter.count += count; + + if (item) { + counter.items.push(item); + } + + this.counters.set(id, counter); + return counter; + } + remove(id: string) { + this.counters.delete(id); + } + cleanup() { + const threshold = Date.now() - this.clearTimeout; + const remove: string[] = []; + + this.counters.forEach((value, key) => { + if (value.date < threshold) { + remove.push(key); + } + }); + + remove.forEach(id => this.remove(id)); + } + start() { + this.interval = this.interval || setInterval(() => this.cleanup(), this.clearTimeout / 10); + } + stop() { + clearInterval(this.interval); + this.interval = undefined; + } +} diff --git a/src/ts/server/services/friends.ts b/src/ts/server/services/friends.ts new file mode 100644 index 0000000..e5c59a8 --- /dev/null +++ b/src/ts/server/services/friends.ts @@ -0,0 +1,207 @@ +import { NotificationFlags, FriendStatusFlags, FriendStatusData } from '../../common/interfaces'; +import { HOUR, FRIENDS_LIMIT } from '../../common/constants'; +import { AccountFlags } from '../../common/adminInterfaces'; +import { hasFlag } from '../../common/utils'; +import { IClient } from '../serverInterfaces'; +import { addFriend, removeFriend } from '../accountUtils'; +import { saySystem } from '../chat'; +import { logger } from '../logger'; +import { NotificationService } from './notification'; +import { ActionLimiter, LimiterResult } from './actionLimiter'; +import { updateEntityPlayerState } from '../playerUtils'; +import { getEntityName } from '../entityUtils'; + +export const PENDING_LIMIT = 2; +export const REJECTED_LIMIT = 5; +export const REJECTED_TIMEOUT = 2 * HOUR; + +export function isFriend(client: IClient, friend: IClient) { + return client.friends.has(friend.accountId); +} + +export function isOnlineFriend(client: IClient, friend: IClient) { + return client.friends.has(friend.accountId) && !friend.accountSettings.hidden; +} + +export function toFriendOnline(client: IClient): FriendStatusData { + return { + accountId: client.accountId, + accountName: client.accountName, + status: FriendStatusFlags.Online, + entityId: client.pony.id, + crc: client.pony.crc, + name: client.pony.name, + nameBad: client.pony.nameBad, + info: client.pony.infoSafe, + }; +} + +export function toFriendOffline(client: IClient): FriendStatusData { + return { + accountId: client.accountId, + accountName: client.accountName, + status: FriendStatusFlags.None, + entityId: 0, + }; +} + +export function toFriendRemove(client: IClient): FriendStatusData { + return { + accountId: client.accountId, + status: FriendStatusFlags.Remove, + }; +} + +export function toFriend(client: IClient): FriendStatusData { + if (client.isConnected) { + return toFriendOnline(client); + } else { + return toFriendOffline(client); + } +} + +export class FriendsService { + private limiter = new ActionLimiter(REJECTED_TIMEOUT, REJECTED_LIMIT); + private pending = new Map>(); + constructor( + private notificationService: NotificationService, + private reportInviteLimit: (client: IClient) => void + ) { + } + dispose() { + this.limiter.dispose(); + } + clientDisconnected(client: IClient) { + for (const key of Array.from(this.pending.keys())) { + const pending = this.pending.get(key)!; + pending.delete(client.accountId); + + if (!pending.size) { + this.pending.delete(key); + } + } + } + remove(client: IClient, friend: IClient) { + removeFriend(client.accountId, friend.accountId).catch(e => logger.error(e)); + client.friends.delete(friend.accountId); + client.friendsCRC = undefined; + friend.friends.delete(client.accountId); + friend.friendsCRC = undefined; + client.reporter.systemLog(`Removed friend [${friend.accountId}]`); + client.updateFriends([{ accountId: friend.accountId, status: FriendStatusFlags.Remove }], false); + friend.updateFriends([{ accountId: client.accountId, status: FriendStatusFlags.Remove }], false); + updateEntityPlayerState(client, friend.pony); + updateEntityPlayerState(friend, client.pony); + } + removeByAccountId(client: IClient, friendAccountId: string) { + removeFriend(client.accountId, friendAccountId).catch(e => logger.error(e)); + client.friends.delete(friendAccountId); + client.friendsCRC = undefined; + client.reporter.systemLog(`Removed friend [${friendAccountId}]`); + client.updateFriends([{ accountId: friendAccountId, status: FriendStatusFlags.Remove }], false); + } + add(client: IClient, target: IClient) { + const can = this.limiter.canExecute(client, target); + + if (can === LimiterResult.LimitReached) { + return saySystem(client, 'Reached request rejection limit'); + } else if (can !== LimiterResult.Yes) { + return saySystem(client, 'Cannot send request'); + } + + const pending = this.pending.get(client.accountId) || new Set(); + + if (pending.has(target.accountId)) + return saySystem(client, 'Already sent request'); + + if (isFriend(client, target)) + return saySystem(client, 'Already on friends list'); + + if (client.friends.size >= FRIENDS_LIMIT) + return saySystem(client, 'Your friend list is full'); + + if (target.friends.size >= FRIENDS_LIMIT) + return saySystem(client, 'Target player friend list is full'); + + if (hasFlag(client.account.flags, AccountFlags.BlockFriendRequests)) + return saySystem(client, 'Cannot send request'); + + if (target.accountSettings.ignoreFriendInvites) + return saySystem(client, 'Cannot send request'); + + if (pending.size >= PENDING_LIMIT) + return saySystem(client, 'Too many pending requests'); + + const notificationId = this.addInviteNotification(client, target); + + if (!notificationId) { + return saySystem(client, 'Cannot send request'); + } + + pending.add(target.accountId); + this.pending.set(client.accountId, pending); + client.reporter.systemLog(`Friend request [${target.accountId}]`); + } + private acceptInvitation(client: IClient, friend: IClient, notificationId: number) { + client.reporter.systemLog(`Friend request accepted by [${friend.accountId}]`); + saySystem(client, `Friend request accepted by ${getEntityName(friend.pony, client)}`); + + addFriend(client.accountId, friend.accountId) + .catch(e => { + if (e.message !== `Friend request already exists`) { + logger.error(e); + } + }); + + client.friends.add(friend.accountId); + client.friendsCRC = undefined; + friend.friends.add(client.accountId); + friend.friendsCRC = undefined; + this.removePending(client, friend); + this.notificationService.removeNotification(friend, notificationId); + client.updateFriends([toFriend(friend)], false); + friend.updateFriends([toFriend(client)], false); + updateEntityPlayerState(client, friend.pony); + updateEntityPlayerState(friend, client.pony); + } + private rejectInvitation(client: IClient, friend: IClient, notificationId: number) { + client.reporter.systemLog(`Friend request rejected by [${friend.accountId}]`); + saySystem(client, `Friend request rejected by ${getEntityName(friend.pony, client)}`); + this.removePending(client, friend); + this.notificationService.removeNotification(friend, notificationId); + this.countReject(client); + } + private removePending(client: IClient, friend: IClient) { + const pending = this.pending.get(client.accountId); + + if (pending) { + pending.delete(friend.accountId); + + if (pending.size === 0) { + this.pending.delete(client.accountId); + } + } + } + private countReject(invitedBy: IClient) { + const count = this.limiter.count(invitedBy); + + if (count >= REJECTED_LIMIT) { + this.reportInviteLimit(invitedBy); + } + } + private addInviteNotification(client: IClient, friend: IClient) { + const notificationId = this.notificationService.addNotification(friend, { + id: 0, + sender: client, + name: client.pony.name || '', + entityId: client.pony.id, + message: `
Friend request
#NAME# wants to add you to their friends`, + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | + (client.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.acceptInvitation(client, friend, notificationId), + reject: () => this.rejectInvitation(client, friend, notificationId), + }); + + return notificationId; + } +} diff --git a/src/ts/server/services/hiding.ts b/src/ts/server/services/hiding.ts new file mode 100644 index 0000000..4e38517 --- /dev/null +++ b/src/ts/server/services/hiding.ts @@ -0,0 +1,364 @@ +import * as fs from 'fs'; +import { Subject } from 'rxjs'; +import { HIDE_LIMIT, MINUTE } from '../../common/constants'; +import { NotificationFlags } from '../../common/interfaces'; +import { IClient, ServerNotification } from '../serverInterfaces'; +import { systemMessage, logger } from '../logger'; +import { NotificationService } from './notification'; +import { includes } from '../../common/utils'; +import { isFriend } from './friends'; +import { pathTo } from '../paths'; +import { addHide } from '../db'; +import { HidingStats } from '../../common/adminInterfaces'; +import { getEntityName } from '../entityUtils'; +import { saySystem } from '../chat'; + +interface Hide { + by: string; + who: string; +} + +const hidePlayerLimit = 'Cannot hide any more players.'; +const cannotHidePlayerInParty = 'Cannot hide players from your party.'; +const cannotHideFriends = 'Cannot hide friends.'; +const unhideAllLimit = 'Cannot unhide hidden players, try again later.'; +const unhideAllLimitNote = 'You can only do this once per hour.'; + +function clientInfo({ accountId, account, characterName }: IClient) { + return `${characterName} (${account.name}) [${accountId}]`; +} + +function simpleNotification(message: string, note?: string): ServerNotification { + return { id: 0, name: '', message, note, flags: NotificationFlags.Ok }; +} + +export function hidingDataPath(serverId: string) { + return pathTo('settings', `hiding-${serverId}.json`); +} + +export async function saveHidingData(hiding: HidingService, serverId: string) { + if (!TESTS) { + try { + const data = hiding.serialize(); + await fs.writeFileAsync(hidingDataPath(serverId), data, 'utf8'); + } catch (e) { + logger.error(e); + } + } +} + +export function pollHidingDataSave(hiding: HidingService, serverId: string) { + setInterval(() => saveHidingData(hiding, serverId), 10 * MINUTE); +} + +export class HidingService { + changes = new Subject(); + unhidesAll = new Subject(); + private hides = new Map>(); + private unhides = new Map(); + private interval: any; + constructor( + private clearUnhides: number, + private notifications: NotificationService, + private findClient: (accountId: string) => IClient | undefined, + private log: (message: string) => void, + ) { + } + serialize() { + const hides: any = {}; + const unhides: any = {}; + + this.hides.forEach((hidesMap, by) => { + const list: any = {}; + hidesMap.forEach((value, key) => list[key] = value); + hides[by] = list; + }); + + this.unhides.forEach((value, key) => unhides[key] = value); + + return JSON.stringify({ hides, unhides }); + } + deserialize(data: string) { + try { + const { hides, unhides } = JSON.parse(data); + + for (const by of Object.keys(hides)) { + const hidesMap = new Map(); + + for (const key of Object.keys(hides[by])) { + hidesMap.set(key, hides[by][key]); + } + + this.hides.set(by, hidesMap); + } + + for (const key of Object.keys(unhides)) { + this.unhides.set(key, unhides[key]); + } + + this.cleanup(); + } catch (e) { + logger.error(e); + } + } + getStatsFor(account: string): HidingStats { + const hides = this.hides.get(account); + const hidden = hides ? Array.from(hides.keys()) : []; + const hiddenBy: string[] = []; + + this.hides.forEach((hides, by) => { + if (hides.has(account)) { + hiddenBy.push(by); + } + }); + + return { account, hidden, hiddenBy, permaHidden: [], permaHiddenBy: [] }; + } + connected(client: IClient) { + const hides = this.hides.get(client.accountId); + + if (hides) { + for (const id of Array.from(hides.keys())) { + client.hides.add(id); + } + } + } + requestHide(requester: IClient, target: IClient, timeout: number) { + const hides = this.hides.get(requester.accountId); + const count = hides && hides.size || 0; + + if (requester.accountId === target.accountId) { + saySystem(requester, `Cannot hide yourself`); + } else if (requester.party && includes(requester.party.clients, target)) { + this.notifications.addNotification(requester, simpleNotification(cannotHidePlayerInParty)); + } else if (isFriend(requester, target)) { + this.notifications.addNotification(requester, simpleNotification(cannotHideFriends)); + } else if (count >= HIDE_LIMIT) { + this.notifications.addNotification(requester, simpleNotification(hidePlayerLimit)); + } else { + this.notifications.addNotification(requester, { + id: 0, + name: target.pony.name || '', + entityId: target.pony.id, + message: `Are you sure you want to hide #NAME# ?`, + flags: NotificationFlags.Yes | NotificationFlags.No | (target.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.confirmHide(requester, target, timeout), + }); + } + } + requestUnhideAll(requester: IClient) { + const unhideTimestamp = this.unhides.get(requester.accountId) || 0; + + if (unhideTimestamp > Date.now()) { + this.notifications.addNotification(requester, simpleNotification(unhideAllLimit, unhideAllLimitNote)); + } else { + this.notifications.addNotification(requester, { + id: 0, + name: '', + message: 'Are you sure you want to unhide all temporarily hidden players ?', + note: 'You can only do this once per hour. This action will require re-joining the game.', + flags: NotificationFlags.Yes | NotificationFlags.No, + accept: () => this.unhideAll(requester), + }); + } + } + confirmHide(requester: IClient, target: IClient, timeout: number) { + if (this.hide(requester, target, timeout)) { + let message = `${requester.characterName} (${requester.account.name}) hides ${clientInfo(target)}`; + + if (timeout === 0) { + message += ' (permanent)'; + } + + this.log(systemMessage(requester.accountId, message)); + } + } + private isHiddenInner(who: string, from: string): boolean { + const hides = this.hides.get(who); + return hides !== undefined && hides.has(from); + } + isHidden(who: string, from: string): boolean { + return this.isHiddenInner(who, from) || this.isHiddenInner(from, who); + } + isHiddenClient(who: IClient, from: IClient) { + return this.isHidden(who.accountId, from.accountId); + } + hide(byClient: IClient, whoClient: IClient, timeout: number) { + const by = byClient.accountId; + const who = whoClient.accountId; + + if (timeout === 0) { // permanent + addHide(by, who, getEntityName(whoClient.pony, byClient) || '[none]') + .then(() => { + byClient.permaHides.add(who); + this.notify([{ by, who }]); + }) + .catch(e => logger.error(e)); + return true; + } else { + + if (by === who) + return false; + + if (this.isHiddenInner(by, who)) + return false; + + const hides = this.hides.get(by) || new Map(); + hides.set(who, Date.now() + timeout); + this.hides.set(by, hides); + byClient.hides.add(who); + this.notify([{ by, who }]); + return true; + } + } + // TODO: remove ? + unhide(byClient: IClient, whoClient: IClient) { + const by = byClient.accountId; + const who = whoClient.accountId; + const hides = this.hides.get(by); + + if (hides) { + if (hides.has(who)) { + hides.delete(who); + + if (hides.size === 0) { + this.hides.delete(by); + } + + byClient.hides.delete(who); + + this.notify([{ by, who }]); + } + } + } + unhideAll(byClient: IClient) { + const by = byClient.accountId; + + if (this.unhides.has(by)) + return; + + const hides = this.hides.get(by); + + if (hides) { + const notify: Hide[] = []; + hides.forEach((_, who) => notify.push({ by, who })); + this.hides.delete(by); + this.unhides.set(by, Date.now() + this.clearUnhides); + byClient.hides.clear(); + this.notify(notify); + this.unhidesAll.next(by); + } + + this.log(systemMessage(by, 'unhide all')); + } + merged(target: string, merge: string) { + const targetHides = this.hides.get(target); + const mergeHides = this.hides.get(merge); + const notify: Hide[] = []; + + if (targetHides) { + targetHides.delete(merge); + } + + if (mergeHides) { + const targetClient = this.findClient(target); + mergeHides.delete(target); + + if (targetHides) { + for (const id of Array.from(mergeHides.keys())) { + const who = targetHides.get(id); + targetHides.set(id, Math.max(who || 0, mergeHides.get(id)!)); + targetClient && targetClient.hides.add(id); + + if (!who) { + notify.push({ by: target, who: id }); + notify.push({ by: merge, who: id }); + } + } + } else { + this.hides.set(target, mergeHides); + + for (const id of Array.from(mergeHides.keys())) { + targetClient && targetClient.hides.add(id); + notify.push({ by: target, who: id }); + notify.push({ by: merge, who: id }); + } + } + + this.hides.delete(merge); + } + + const targetUnhides = this.unhides.get(target); + const mergeUnhides = this.unhides.get(merge); + + if (mergeUnhides) { + this.unhides.set(target, Math.max(targetUnhides || 0, mergeUnhides)); + this.unhides.delete(merge); + } + + this.hides.forEach((_, by) => { + const hides = this.hides.get(by)!; + const mergeHide = hides.get(merge); + + if (mergeHide) { + hides.set(target, Math.max(mergeHide, hides.get(target) || 0)); + hides.delete(merge); + const client = this.findClient(by); + + if (client) { + client.hides.delete(merge); + + if (target !== by) { + client && client.hides.add(target); + } + } + + notify.push({ by, who: target }); + notify.push({ by, who: merge }); + } + }); + + this.notify(notify); + } + cleanup() { + const now = Date.now(); + const notify: Hide[] = []; + + for (const by of Array.from(this.hides.keys())) { + const hides = this.hides.get(by)!; + + for (const who of Array.from(hides.keys())) { + if (hides.get(who)! < now) { + hides.delete(who); + const client = this.findClient(by); + client && client.hides.delete(who); + notify.push({ by, who }); + } + } + + if (hides.size === 0) { + this.hides.delete(by); + } + } + + this.notify(notify); + + for (const key of Array.from(this.unhides.keys())) { + if (this.unhides.get(key)! < now) { + this.unhides.delete(key); + } + } + } + start() { + this.interval = this.interval || setInterval(() => this.cleanup(), 10 * MINUTE); + } + stop() { + clearInterval(this.interval); + this.interval = undefined; + } + private notify(hides: Hide[]) { + for (const hide of hides) { + this.changes.next(hide); + } + } +} diff --git a/src/ts/server/services/liveList.ts b/src/ts/server/services/liveList.ts new file mode 100644 index 0000000..e14299d --- /dev/null +++ b/src/ts/server/services/liveList.ts @@ -0,0 +1,200 @@ +import { Model } from 'mongoose'; +import { removeItem, maxDate } from '../../common/utils'; +import { logger as defaultLogger } from '../logger'; +import { Document } from '../../common/adminInterfaces'; +import { iterate } from '../db'; + +const tickInterval = 1000; + +type Listener = (id: string, item: any) => void; + +export interface LiveListConfig { + fields: (keyof T)[]; + noStore?: boolean; + ignore?: (item: T) => boolean; + clean: (item: T) => Partial; // clean before sending to client + fix?: (item: T) => void; // fix after getting from DB + onSubscribeToMissing?: (id: string) => T; + // events + onAdd?: (item: T) => void; + onUpdate?: (oldItem: T, newItem: T) => void; + onDelete?: (item: T) => void; + onFinished?: () => void; + onAddedOrUpdated?: () => void; +} + +function fixDocumentId(item: T) { + item._id = item._id.toString(); + return item; +} + +export class LiveList { + items: T[] = []; + private itemsMap = new Map(); + private listeners = new Map(); + private timestamp = new Date(0); + private finished = false; + private running = false; + private timeout: any; + private fieldsString: string; + constructor( + private model: Model, + private config: LiveListConfig, + private getId = (item: T) => item._id, + private logger = defaultLogger, + ) { + this.fieldsString = config.fields.join(' '); + } + get loaded() { + return this.finished; + } + start() { + if (this.config.noStore) { + this.timestamp = new Date(); + } + + this.running = true; + this.tick(); + } + stop() { + this.running = false; + clearTimeout(this.timeout); + } + get(id: string) { + return this.itemsMap.get(id); + } + for(id: string | undefined, callback: (item: T) => void) { + const item = id ? this.get(id) : undefined; + item && callback(item); + } + add(item: T) { + const id = this.getId(item); + this.items.push(item); + this.itemsMap.set(id, item); + + if (this.config.onAdd) { + this.config.onAdd(item); + } + + this.trigger(id, item); + return item; + } + // NOTE: only _id + async remove(id: string) { + await this.model.deleteOne({ _id: id }).exec(); + this.removed(id); + } + removed(id: string) { + const item = this.get(id); + + if (item) { + this.trigger(id, undefined); + this.itemsMap.delete(id); + removeItem(this.items, item); + this.config.onDelete && this.config.onDelete(item); + } + } + discard(id: string) { + const item = this.get(id); + + if (item) { + this.itemsMap.delete(id); + removeItem(this.items, item); + } + } + trigger(id: string, item: T | undefined) { + const listeners = this.listeners.get(id); + + if (listeners) { + const cleaned = item ? this.config.clean(item) : item; + listeners.forEach(listener => listener(id, cleaned as any)); + } + } + subscribe(id: string, listener: Listener) { + const listeners = this.listeners.get(id) || []; + listeners.push(listener); + this.listeners.set(id, listeners); + + const item = this.get(id); + + if (item) { + listener(id, this.config.clean(item)); + } else if (this.config.onSubscribeToMissing) { + this.add(this.config.onSubscribeToMissing(id)); + } + + return { + unsubscribe: () => { + const listeners = this.listeners.get(id) || []; + removeItem(listeners, listener); + + if (listeners.length === 0) { + this.listeners.delete(id); + } + } + }; + } + hasSubscriptions(id: string) { + return !!this.listeners.get(id); + } + async tick() { + if (this.running) { + clearTimeout(this.timeout); + + try { + await this.update(); + } catch (e) { + this.logger.error(e); + } finally { + this.timeout = setTimeout(() => this.tick(), tickInterval); + } + } + } + async fetch(search: any) { + await this.internalUpdate(search, true); + } + async update() { + await this.internalUpdate({ updatedAt: { $gt: this.timestamp } }, false); + } + private async internalUpdate(search: any, fetching: boolean) { + const query = this.model.find(search, this.fieldsString); + const applyUpdate = this.config.onUpdate || Object.assign; + let addedOrUpdated = false; + + await iterate(query.lean(), update => { + try { + fixDocumentId(update); + + if (this.config.fix) { + this.config.fix(update); + } + + if (!fetching) { + this.timestamp = maxDate(this.timestamp, update.updatedAt)!; + } + + const doc = this.get(this.getId(update)); + + if (doc !== undefined) { + applyUpdate(doc, update); + this.trigger(this.getId(doc), doc); + } else if (fetching || !(this.config.ignore && this.config.ignore(update))) { + this.add(update); + } + + addedOrUpdated = true; + } catch (e) { + console.error(e); + } + }); + + if (addedOrUpdated && this.config.onAddedOrUpdated) { + this.config.onAddedOrUpdated(); + } + + if (!this.finished) { + this.finished = true; + this.config.onFinished && this.config.onFinished(); + } + } +} diff --git a/src/ts/server/services/notification.ts b/src/ts/server/services/notification.ts new file mode 100644 index 0000000..095f3af --- /dev/null +++ b/src/ts/server/services/notification.ts @@ -0,0 +1,71 @@ +import { findById, removeById } from '../../common/utils'; +import { IClient, ServerNotification } from '../serverInterfaces'; + +const NOTIFICATION_LIMIT = 10; + +function getId(notifications: ServerNotification[]) { + for (let id = 1; id <= 0xffff; id++) { + if (!findById(notifications, id)) { + return id; + } + } + + /* istanbul ignore next */ + throw new Error('Unable to get unique id for notification'); +} + +function hasNotification(client: IClient, notification: ServerNotification) { + return client.notifications.some(n => + n.message === notification.message && + n.flags === notification.flags && + n.note === notification.note && + n.sender === notification.sender && + n.entityId === notification.entityId); +} + +export class NotificationService { + addNotification(client: IClient, notification: ServerNotification) { + if (client.notifications.length >= NOTIFICATION_LIMIT || hasNotification(client, notification)) { + return 0; + } else { + notification.id = getId(client.notifications); + client.notifications.push(notification); + const { id, entityId = 0, name, message, note = '', flags = 0 } = notification; + client.addNotification(id, entityId, name, message, note, flags); + return notification.id; + } + } + removeNotification(client: IClient, id: number) { + if (removeById(client.notifications, id)) { + client.removeNotification(id); + return true; + } else { + return false; + } + } + acceptNotification(client: IClient, id: number) { + const notification = findById(client.notifications, id); + this.removeNotification(client, id); + + if (notification && notification.accept) { + notification.accept(); + } + } + rejectNotification(client: IClient, id: number) { + const notification = findById(client.notifications, id); + this.removeNotification(client, id); + + if (notification && notification.reject) { + notification.reject(); + } + } + rejectAll(client: IClient) { + client.notifications.slice() + .forEach(n => this.rejectNotification(client, n.id)); + } + dismissAll(client: IClient) { + while (client.notifications.length) { + this.removeNotification(client, client.notifications[0].id); + } + } +} diff --git a/src/ts/server/services/observableList.ts b/src/ts/server/services/observableList.ts new file mode 100644 index 0000000..faa26f6 --- /dev/null +++ b/src/ts/server/services/observableList.ts @@ -0,0 +1,48 @@ +import { removeItem } from '../../common/utils'; +import { ListListener, IObservableList } from '../../common/adminInterfaces'; +import { pushOrdered } from '../../common/adminUtils'; + +export class ObservableList implements IObservableList { + private listeners: ListListener[] = []; + constructor(private list: T[], private map: (item: T) => V) { + } + hasSubscribers() { + return this.listeners.length > 0; + } + trigger() { + if (this.listeners.length) { + const items = this.list.map(this.map); + + for (const listener of this.listeners) { + listener(items); + } + } + } + push(item: T) { + this.list.push(item); + this.trigger(); + } + pushOrdered(item: T, compare: (a: T, b: T) => number) { + pushOrdered(this.list, item, compare); + this.trigger(); + } + remove(item: T) { + const removed = removeItem(this.list, item); + this.trigger(); + return removed; + } + replace(list: T[]) { + this.list = list; + this.trigger(); + } + subscribe(listener: ListListener) { + this.listeners.push(listener); + this.trigger(); + + return { + unsubscribe: () => { + removeItem(this.listeners, listener); + } + }; + } +} diff --git a/src/ts/server/services/party.ts b/src/ts/server/services/party.ts new file mode 100644 index 0000000..3465e88 --- /dev/null +++ b/src/ts/server/services/party.ts @@ -0,0 +1,317 @@ +import { Subject } from 'rxjs'; +import { remove } from 'lodash'; +import { PartyFlags, NotificationFlags } from '../../common/interfaces'; +import { AccountFlags } from '../../common/adminInterfaces'; +import { removeItem, hasFlag, includes } from '../../common/utils'; +import { PARTY_LIMIT, HOUR, SECOND } from '../../common/constants'; +import { ServerParty, IClient } from '../serverInterfaces'; +import { NotificationService } from './notification'; +import { ActionLimiter, LimiterResult } from './actionLimiter'; +import { saySystem } from '../chat'; +import { isFriend } from './friends'; + +export const LEADER_TIMEOUT = 5 * SECOND; +export const INVITE_LIMIT = 5; +export const INVITE_REJECTED_LIMIT = 5; +export const INVITE_REJECTED_TIMEOUT = 1 * HOUR; + +function toPartyMember(client: IClient, pending: boolean, leader: boolean): [number, PartyFlags] { + const flags = (pending ? PartyFlags.Pending : 0) + | (leader ? PartyFlags.Leader : 0) + | (client.offline ? PartyFlags.Offline : 0); + + return [client.pony.id, flags]; +} + +function findClientInParties(parties: ServerParty[], accountId: string) { + for (const party of parties) { + for (let index = 0; index < party.clients.length; index++) { + if (party.clients[index].accountId === accountId) { + return { party, index }; + } + } + } + + return { party: undefined, index: 0 }; +} + +export class PartyService { + parties: ServerParty[] = []; + partyChanged = new Subject(); + private id = 0; + private limiter = new ActionLimiter(INVITE_REJECTED_TIMEOUT, INVITE_REJECTED_LIMIT); + constructor( + private notificationService: NotificationService, + private reportInviteLimit: (client: IClient) => void, + ) { + } + dispose() { + this.limiter.dispose(); + } + clientConnected(client: IClient) { + const { party, index } = findClientInParties(this.parties, client.accountId); + + if (party) { + const existing = party.clients[index]; + party.clients[index] = client; + client.party = party; + + if (party.leader === existing) { + party.leader = client; + clearTimeout(party.leaderTimeout); + } + + existing.party = undefined; + existing.offlineAt = new Date(); + + this.sendPartyUpdateToAll(party); + } + } + clientDisconnected(client: IClient) { + const party = client.party; + + if (party) { + this.sendPartyUpdateToAll(party); + + party.leaderTimeout = setTimeout(() => { + const newLeader = party.clients.find(c => c !== client && !c.offline); + + if (newLeader) { + this.promoteLeader(client, newLeader); + } else { + this.destroyParty(party); + } + }, LEADER_TIMEOUT); + } else { + const pendingParty = this.parties.find(p => p.pending.some(x => x.client === client)); + + if (pendingParty) { + remove(pendingParty.pending, x => x.client === client); + this.sendPartyUpdateToAll(pendingParty); + } + } + } + remove(leader: IClient, client: IClient) { + const party = leader.party; + + if (!party || party.leader !== leader) + return; + + if (includes(party.clients, client)) { + removeItem(party.clients, client); + client.party = undefined; + + if (party.leader === client && party.clients[0]) { + party.leader = party.clients[0]; + } + + client.updateParty(undefined); + this.sendPartyUpdateToAll(party); + this.partyChanged.next(client); + } else { + const pending = party.pending.find(p => p.client === client); + + if (pending) { + leader.reporter.systemLog(`Invite cancelled for [${client.accountId}]`); + removeItem(party.pending, pending); + this.notificationService.removeNotification(pending.client, pending.notificationId); + this.sendPartyUpdateToAll(party); + this.countReject(leader); + } + } + + this.cleanupParty(party); + } + invite(leader: IClient, client: IClient) { + let party = leader.party; + + const can = this.limiter.canExecute(leader, client); + + if (can === LimiterResult.LimitReached) { + return saySystem(leader, 'Reached invite rejection limit'); + } else if (can !== LimiterResult.Yes) { + return saySystem(leader, 'Cannot invite'); + } + + if (client.shadowed) + return saySystem(leader, 'Cannot invite'); + + if (hasFlag(leader.account.flags, AccountFlags.BlockPartyInvites)) + return saySystem(leader, 'Cannot invite'); + + if (party && party.leader !== leader) + return saySystem(leader, 'You need to be party leader'); + + if (party && (party.clients.length + party.pending.length) >= PARTY_LIMIT) + return saySystem(leader, 'Party is full'); + + if (client.party) + return saySystem(leader, 'Already in a party'); + + if (party && party.pending.some(p => p.client === client)) + return saySystem(leader, 'Already invited'); + + if (client.accountSettings.ignorePartyInvites && !isFriend(client, leader)) + return saySystem(leader, 'Cannot invite'); + + if (this.parties.reduce((sum, p) => sum + p.pending.filter(x => x.client === client).length, 0) >= INVITE_LIMIT) + return saySystem(leader, 'Too many pending invites'); + + const partyExisted = !!leader.party; + + if (!partyExisted) { + party = this.createParty(leader); + } + + /* istanbul ignore next */ + if (!party) + throw new Error(`Party not created`); + + const notificationId = this.addInviteNotification(client, leader, party); + + if (!notificationId) { + if (!partyExisted) { + leader.party = undefined; + removeItem(this.parties, party); + } + + return saySystem(leader, 'Cannot invite'); + } + + party.pending.push({ client, notificationId }); + this.sendPartyUpdateToAll(party); + leader.reporter.systemLog(`Invite to party [${client.accountId}]`); + + if (!partyExisted) { + this.partyChanged.next(leader); + } + } + leave(client: IClient) { + if (client.party) { + this.remove(client.party.leader, client); + } + } + promoteLeader(leader: IClient, client: IClient) { + const party = leader.party; + + if (!party) + return; + + if (leader === client) + return; + + if (client.offline) + return saySystem(leader, 'Player is offline'); + + if (party.leader !== leader) + return saySystem(leader, 'You need to be party leader'); + + if (!includes(party.clients, client)) + return saySystem(leader, 'Not in the party'); + + party.leader = client; + this.sendPartyUpdateToAll(party); + } + cleanupParties() { + const now = Date.now(); + + for (let i = this.parties.length - 1; i >= 0; i--) { + const party = this.parties[i]; + + if (party.clients.every(c => c.offline)) { + party.cleanup = party.cleanup || now; + + if ((now - party.cleanup) > (10 * SECOND)) { + this.destroyParty(party); + } + } else if (party.cleanup !== undefined) { + party.cleanup = undefined; + } + } + } + private createParty(leader: IClient) { + const party: ServerParty = { + id: `party-${this.id++}`, + leader, + clients: [leader], + pending: [], + }; + + leader.party = party; + this.parties.push(party); + return party; + } + private destroyParty(party: ServerParty) { + const clients = party.clients; + clients.forEach(c => c.party = undefined); + clients.forEach(c => c.updateParty(undefined)); + party.pending.forEach(p => this.notificationService.removeNotification(p.client, p.notificationId)); + party.clients = []; + party.pending = []; + removeItem(this.parties, party); + clients.forEach(c => this.partyChanged.next(c)); + } + private sendPartyUpdate(client: IClient, party: ServerParty) { + const clients = party.clients.map(c => toPartyMember(c, false, c === party.leader)); + const pending = party.pending.map(c => toPartyMember(c.client, true, false)); + client.updateParty([...clients, ...pending]); + } + private sendPartyUpdateToAll(party: ServerParty) { + party.clients + .filter(c => !c.offline) + .forEach(c => this.sendPartyUpdate(c, party)); + } + private cleanupParty(party: ServerParty) { + if (party.clients.length === 0 || (party.clients.length + party.pending.length) <= 1) { + this.destroyParty(party); + } + } + private acceptInvitation(party: ServerParty, client: IClient, invitedBy: IClient) { + const removed = remove(party.pending, p => p.client === client)[0]; + + if (!client.party && removed) { + party.leader.reporter.systemLog(`Invite accepted by [${client.accountId}]`); + party.clients.push(client); + client.party = party; + this.notificationService.removeNotification(client, removed.notificationId); + this.sendPartyUpdateToAll(party); + + this.parties + .filter(p => p.pending.some(x => x.client === client)) + .forEach(p => this.rejectInvitation(p, client, invitedBy)); + + this.partyChanged.next(client); + } + } + private rejectInvitation(party: ServerParty, client: IClient, invitedBy: IClient) { + const removed = remove(party.pending, p => p.client === client)[0]; + + if (removed) { + party.leader.reporter.systemLog(`Invite rejected by [${client.accountId}]`); + this.notificationService.removeNotification(client, removed.notificationId); + this.sendPartyUpdateToAll(party); + this.cleanupParty(party); + this.countReject(invitedBy); + } + } + private countReject(invitedBy: IClient) { + const count = this.limiter.count(invitedBy); + + if (count >= INVITE_REJECTED_LIMIT) { + this.reportInviteLimit(invitedBy); + } + } + private addInviteNotification(client: IClient, leader: IClient, party: ServerParty) { + return this.notificationService.addNotification(client, { + id: 0, + sender: leader, + name: leader.pony.name || '', + entityId: leader.pony.id, + message: `
Party invite
#NAME# invited you to a party`, + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | + (client.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.acceptInvitation(party, client, leader), + reject: () => this.rejectInvitation(party, client, leader), + }); + } +} diff --git a/src/ts/server/services/supporterInvites.ts b/src/ts/server/services/supporterInvites.ts new file mode 100644 index 0000000..b8b8fb2 --- /dev/null +++ b/src/ts/server/services/supporterInvites.ts @@ -0,0 +1,127 @@ +import { groupBy, toPairs, compact } from 'lodash'; +import { IClient } from '../serverInterfaces'; +import { NotificationService } from './notification'; +import { NotificationFlags, SupporterInvite } from '../../common/interfaces'; +import { systemMessage } from '../logger'; +import { UserError } from '../userError'; +import { compareDates, fromNow, flatten } from '../../common/utils'; +import { DAY, HOUR } from '../../common/constants'; +import { Model } from 'mongoose'; +import { ISupporterInvite, IAccount } from '../db'; +import { ActionLimiter, LimiterResult } from './actionLimiter'; +import { saySystem } from '../chat'; +import { getSupporterInviteLimit } from '../accountUtils'; + +export const INVITE_REJECTED_TIMEOUT = 1 * HOUR; +export const INVITE_REJECTED_LIMIT = 5; + +function formatMessage(requester: IClient, target: IClient, message: string) { + const requesterInfo = `${requester.characterName} (${requester.account.name})`; + const targetInfo = `${target.characterName} (${target.account.name}) [${target.accountId}]`; + return systemMessage(requester.accountId, `${requesterInfo} ${message} ${targetInfo}`); +} + +export class SupporterInvitesService { + private limiter = new ActionLimiter(INVITE_REJECTED_TIMEOUT, INVITE_REJECTED_LIMIT); + constructor( + private model: Model, + private notifications: NotificationService, + private log: (message: string) => void, + ) { + } + dispose() { + this.limiter.dispose(); + } + async getInvites(source: IClient): Promise { + const items = await this.model.find({ source: source.account._id }).exec(); + return items.map(({ _id, name, info, active }) => ({ id: _id.toString(), name, info, active })); + } + async isInvited(target: IClient): Promise { + const count = await this.model.countDocuments({ target: target.account._id, active: true }).exec(); + return count > 0; + } + async requestInvite(requester: IClient, target: IClient) { + const items = await this.getInvites(requester); + const limit = getSupporterInviteLimit(requester.account); + + if (items.length >= limit) + return saySystem(requester, 'Invite limit reached'); + + if (this.limiter.canExecute(requester, target) !== LimiterResult.Yes) + return saySystem(requester, 'Cannot invite'); + + this.log(formatMessage(requester, target, 'invited to supporter server')); + + this.notifications.addNotification(target, { + id: 0, + sender: requester, + name: requester.pony.name || '', + entityId: requester.pony.id, + message: `#NAME# invited you to supporter servers`, + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | + (requester.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.acceptInvite(requester, target), + reject: () => this.rejectInvite(requester, target), + }); + } + acceptInvite(requester: IClient, target: IClient) { + this.log(formatMessage(requester, target, 'supporter invite accepted by')); + this.invite(requester, target); + } + rejectInvite(requester: IClient, target: IClient) { + this.log(formatMessage(requester, target, 'supporter invite rejected by')); + this.limiter.count(requester); + } + async invite(requester: IClient, target: IClient) { + const limit = getSupporterInviteLimit(requester.account); + const items = await this.getInvites(requester); + + if (items.length >= limit) { + throw new UserError('Invite limit reached'); + } + + await this.model.create({ + source: requester.account._id, + target: target.account._id, + name: target.characterName, + info: target.character.info, + active: true, + }); + } + uninvite(requester: IClient, inviteId: string) { + return Promise.resolve(this.model.deleteOne({ _id: inviteId, source: requester.account._id }).exec()); + } +} + +type LeanInvite = ISupporterInvite & { source: IAccount }; + +export async function updateSupporterInvites(model: Model) { + const invites: LeanInvite[] = await model.find({}, '_id active') + .populate('source', '_id supporter patreon roles') + .lean() + .exec(); + + const itemsBySource = toPairs(groupBy(invites, i => i.source._id as string)); + const itemsToUpdate = itemsBySource + .map(([_, items]) => { + const source = items[0].source; + const limit = getSupporterInviteLimit(source); + + return compact(items + .sort((a, b) => compareDates(a.createdAt, b.createdAt)) + .map((item, i) => { + const active = i < limit; + return item.active === active ? undefined : { id: item._id, active }; + })); + }); + + const groups = toPairs(groupBy(flatten(itemsToUpdate), i => i.active)); + + await Promise.all(groups.map(([_, items]) => { + const active = items[0].active; + const ids = items.map(i => i.id); + return model.updateMany({ _id: { $in: ids } }, { active }).exec(); + })); + + await model.deleteMany({ active: false, updatedAt: { $lt: fromNow(-100 * DAY) } }).exec(); +} diff --git a/src/ts/server/settings.ts b/src/ts/server/settings.ts new file mode 100644 index 0000000..2cdf645 --- /dev/null +++ b/src/ts/server/settings.ts @@ -0,0 +1,45 @@ +import { readFileAsync, writeFileAsync } from 'fs'; +import { Settings } from '../common/adminInterfaces'; +import { cloneDeep } from '../common/utils'; +import * as paths from './paths'; + +const defaultSettings: Settings = { + canCreateAccounts: true, + servers: {}, +}; + +export const settings: Settings = cloneDeep(defaultSettings); + +const settingsPath = paths.pathTo('settings', `settings.json`); + +/* istanbul ignore next */ +export async function loadSettings() { + const json = await readFileAsync(settingsPath, 'utf8'); + return JSON.parse(json) as Settings; +} + +/* istanbul ignore next */ +export async function saveSettings(settings: Settings) { + const json = JSON.stringify(settings, undefined, 2); + await writeFileAsync(settingsPath, json, 'utf8'); +} + +/* istanbul ignore next */ +export async function updateSettings(update: Partial) { + let settings = { ...defaultSettings }; + + try { + settings = await loadSettings(); + } catch { } + + Object.assign(settings, update); + await saveSettings(settings); +} + +/* istanbul ignore next */ +export async function reloadSettings() { + try { + const current = await loadSettings(); + Object.assign(settings, current); + } catch { } +} diff --git a/src/ts/server/spamChecker.ts b/src/ts/server/spamChecker.ts new file mode 100644 index 0000000..35772d2 --- /dev/null +++ b/src/ts/server/spamChecker.ts @@ -0,0 +1,148 @@ +import { ReportAccount, TimeoutAccount, IClient, LastSay, OnMessageSettings } from './serverInterfaces'; +import { CounterService } from './services/counter'; +import { fromNow } from '../common/utils'; +import { SPAM_TIMEOUT } from './reporting'; +import { isMutedOrShadowed } from './playerUtils'; +import { handlePromiseDefault } from './serverUtils'; +import { SECOND } from '../common/constants'; +import { GameServerSettings } from '../common/adminInterfaces'; + +const TINY_MESSAGE_LENGTH = 3; +const SHORT_MESSAGE_LENGTH = 8; +const LONG_MESSAGE_LENGTH = 50; + +export const MULTIPLE_MATCH_COUNT = 5; +export const REPORT_AFTER_LIMIT = 5; +export const MUTE_AFTER_LIMIT = 7; + +export const TINY_MESSAGE_MUL = 1.5; // was 4 +export const SHORT_MESSAGE_MUL = 1.5; // was 2 +export const LONG_MESSAGE_MUL = 0.75; + +export const RAPID_MESSAGE_COUNT = 35; +export const RAPID_MESSAGE_TIMEOUT = 30 * SECOND; + +export const createSpamChecker = + ( + spamCounter: CounterService, rapidCounter: CounterService, countSpamming: ReportAccount, + timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault, + ): OnMessageSettings => { + async function countAndTimeout( + client: IClient, timeout: boolean, message: string, items: string[], settings: GameServerSettings + ) { + const timeoutTime = fromNow(SPAM_TIMEOUT * (settings.doubleTimeouts ? 2 : 1)); + + await countSpamming(client.accountId); + + if (!isMutedOrShadowed(client)) { + if (timeout && settings.autoBanSpamming) { + await timeoutAccount(client.accountId, timeoutTime, 'Timed out for spamming'); + + if (settings.reportSpam) { + client.reporter.system('Timed out for spamming', items.join('\n')); + } else { + client.reporter.systemLog('Timed out for spamming'); + } + } else if (settings.reportSpam && !ignoreReporting(message)) { + client.reporter.warn('Spam', message); + } + } + } + + async function countAndTimeoutForSpam(client: IClient, message: string, settings: GameServerSettings) { + const increment = message.length >= LONG_MESSAGE_LENGTH ? 2 : 1; + const { count, items } = spamCounter.add(client.accountId, message, increment); + const timeout = count >= MUTE_AFTER_LIMIT; + await countAndTimeout(client, timeout, message, items, settings); + } + + return (client, message, settings) => { + if (client.isMod) + return; + + if (message === '.') + return; + + const lastSays = client.lastSays; + const lastMatch = findLastSayByPartialString(lastSays, message); + + if (lastMatch) { + lastMatch.count++; + lastMatch.age = 0; + + const spamLimit = REPORT_AFTER_LIMIT * getLengthMultiplier(message); + + if (lastMatch.count >= spamLimit) { + lastMatch.count = 0; + handlePromise(countAndTimeoutForSpam(client, message, settings), client.reporter.error); + } + } else { + if (lastSays.length < MULTIPLE_MATCH_COUNT) { + lastSays.push({ message, count: 1, age: 0 }); + } else { + lastSays.sort(byAge); + const lastSay = lastSays[lastSays.length - 1]; + lastSay.message = message; + lastSay.count = 1; + lastSay.age = 0; + } + } + + for (const say of lastSays) { + say.age++; + } + + const now = Date.now(); + const threshold = now - RAPID_MESSAGE_TIMEOUT; + const counter = rapidCounter.add(client.accountId, now); + + while (counter.items.length && counter.items[0] < threshold) { + counter.items.shift(); + counter.count--; + } + + if (counter.count > RAPID_MESSAGE_COUNT) { + countAndTimeout(client, true, 'rapid messages', ['rapid messages'], settings); + rapidCounter.remove(client.accountId); + } + }; + }; + +function findLastSayByPartialString(lastSays: LastSay[], message: string) { + for (const say of lastSays) { + if (partialString(say.message, message)) { + return say; + } + } + + return undefined; +} + +function byAge(a: LastSay, b: LastSay) { + return b.age - a.age; +} + +function ignoreReporting(message: string) { + return message.length <= 3 || /^[aаz]+$|^\/roll/i.test(message); +} + +function partialString(a: string, b: string): boolean { + if (a === b) { + return true; + } else { + const length = Math.floor(Math.min(a.length, b.length) * 0.75); + return length > 8 && a.substr(0, length) === b.substr(0, length); + } +} + +function getLengthMultiplier(message: string) { + if (message.length >= LONG_MESSAGE_LENGTH) { + return LONG_MESSAGE_MUL; + } else if (message.length <= TINY_MESSAGE_LENGTH) { + return TINY_MESSAGE_MUL; + } else if (message.length <= SHORT_MESSAGE_LENGTH) { + return SHORT_MESSAGE_MUL; + } else { + return 1; + } +} diff --git a/src/ts/server/start.ts b/src/ts/server/start.ts new file mode 100644 index 0000000..4bbd93f --- /dev/null +++ b/src/ts/server/start.ts @@ -0,0 +1,105 @@ +import { readFileSync } from 'fs'; +import * as ctrl from './controllers'; +import { World, goToMap } from './world'; +import { create } from './reporter'; +import { logger } from './logger'; +import { SERVER_FPS } from '../common/constants'; +import { ServerConfig } from '../common/adminInterfaces'; +import { timingReset, timingStart, timingEnd } from './timing'; +import { initializeTileHeightmaps } from '../client/tileUtils'; +import { normalSpriteSheet } from '../generated/sprites'; +import { pathTo } from './paths'; +import { createMainMap } from './maps/mainMap'; +import { createIslandMap } from './maps/islandMap'; +import { createHouseMap } from './maps/houseMap'; +import { createPaletteMap } from './maps/paletteMap'; +import { createCaveMap } from './maps/caveMap'; +import { createCustomMap } from './maps/customMap'; +import { createSign } from './controllerUtils'; +import { signQuestion } from '../common/entities'; + +export function start(world: World, server: ServerConfig) { + const data = readFileSync(pathTo('src', 'ts', 'generated', 'pony.bin')); + + normalSpriteSheet.data = { + width: 512, + height: 512, + data: new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), + }; + + initializeTileHeightmaps(); + + world.maps.push(createMainMap(world)); + world.maps.push(createCaveMap(world)); + + // custom map + if (DEVELOPMENT) { // remove `if` when you're ready to publish your map + // place sign that will teleport the player to your custom map + world.addEntity(createSign( + 75, 69, 'Go to custom map', (_, client) => goToMap(world, client, 'custom'), signQuestion), world.getMainMap()); + + // add map to the world, go to `/src/ts/server/maps/customMap.ts` to customize your map + world.maps.push(createCustomMap(world)); + } + + if (world.featureFlags.test) { + const island = createIslandMap(world, false); + island.id = 'public-island'; + world.maps.push(island); + + const house = createHouseMap(world, false); + house.id = 'public-house'; + world.maps.push(house); + } + + if (BETA) { + world.maps.push(createPaletteMap(world)); + } + + if (DEVELOPMENT) { + world.controllers.push(new ctrl.TestController(world, world.getMainMap())); + + // world.controllers.push(new ctrl.PerfController(world, { + // count: 2000, moving: 1000, unique: true, spread: false, saying: false, x: 20, y: 20 + // })); + + // world.controllers.push(new ctrl.FakeClientsController(world, server, { + // count: 1000, + // })); + + world.setTime(12); + } + + let last = Date.now(); + let frames = 0; + + world.initialize(last); + + if (!DEVELOPMENT) { + create(server).info(`Server started`); + } + + setInterval(() => { + timingReset(); + timingStart('frame'); + + try { + const now = Date.now(); + world.update(now - last, now); + last = now; + frames++; + + if (frames >= SERVER_FPS) { + frames = 0; + world.sparseUpdate(now); + } + } catch (e) { + create(server).danger(e.message); + logger.error(e); + } + + timingEnd(); + }, 1000 / SERVER_FPS); + + return world; +} diff --git a/src/ts/server/stats.ts b/src/ts/server/stats.ts new file mode 100644 index 0000000..9a93a95 --- /dev/null +++ b/src/ts/server/stats.ts @@ -0,0 +1,224 @@ +import * as fs from 'fs'; +import * as moment from 'moment'; +import { compact } from 'lodash'; +import { Request } from 'express'; +import { Packet } from 'ag-sockets'; +import { RequestStats, ServerStats } from '../common/adminInterfaces'; +import { HOUR } from '../common/constants'; +import { ByteSize } from './utils/byteSize'; + +interface Stats { + count: number; + size: ByteSize; + totalCount: number; + lastHourCount: number; + lastHourTotal: string; + lastHourAverage: string; + lastHourOrder: string; + lastHourSize: ByteSize; +} + +interface SocketStats { + id: number; + name: string; + countStr: number; + countBin: number; + size: ByteSize; + lastHourCountStr: number; + lastHourCountBin: number; + lastHourTotal: string; + lastHourAverage: string; + lastHourOrder: string; + lastHourSize: ByteSize; +} + +const MB = 1024 * 1024; +const SEPARATOR = ';'; +const statsHeaders = ['date', 'requests count', 'requests size', 'swearing', 'spamming']; + +function encodeCSV(values: any[]) { + return `${values.join(SEPARATOR)}\n`; +} + +function getDate() { + return (new Date()).getDate(); +} + +function getAverage({ bytes, mbytes }: ByteSize, count: number): string { + if (!count) { + return '0'; + } else if (mbytes >= 1) { + return `${Math.floor((mbytes / count) * MB)} b`; + } else { + return `${Math.floor(bytes / count)} b`; + } +} + +function updateHourlySocketStats(stat: SocketStats | undefined) { + if (stat) { + stat.lastHourCountBin = stat.countBin; + stat.lastHourCountStr = stat.countStr; + stat.lastHourSize = stat.size; + stat.lastHourTotal = stat.size.toHumanReadable(); + stat.lastHourAverage = getAverage(stat.size, stat.countBin + stat.countStr); + stat.lastHourOrder = stat.size.toSortableString(); + stat.countBin = 0; + stat.countStr = 0; + stat.size = new ByteSize(); + } +} + +export class StatsTracker { + private stats = new Map(); + private recvStats: (SocketStats | undefined)[] = []; + private sendStats: (SocketStats | undefined)[] = []; + private dailyDate = getDate(); + private dailyRequestCount = 0; + private dailyRequestSize = new ByteSize(); + private dailySwearing = 0; + private dailySpamming = 0; + constructor(private statsPath: string) { + } + logRequest = (req: Request, result: any, url?: string) => { + if (result && !/^\/api-internal/.test(req.baseUrl)) { + this.logStat( + url || (req.baseUrl + req.path), typeof result === 'string' ? result.length : JSON.stringify(result).length); + } + } + logSwearing = () => { + this.dailySwearing++; + } + logSpamming = () => { + this.dailySpamming++; + } + logRecvStats = (packet: Packet) => { + this.logSocketStats(this.recvStats, packet); + } + logSendStats = (packet: Packet) => { + this.logSocketStats(this.sendStats, packet); + } + private logSocketStats(stats: (SocketStats | undefined)[], { id, name, binary, json }: Packet) { + const entry = stats[id] || (stats[id] = { + id, + name, + countStr: 0, + countBin: 0, + size: new ByteSize(), + lastHourCountStr: 0, + lastHourCountBin: 0, + lastHourTotal: '0', + lastHourAverage: '0', + lastHourOrder: '0', + lastHourSize: new ByteSize(), + }); + + if (!!binary) { + entry.countBin++; + } else { + entry.countStr++; + } + + entry.size.addBytes(binary ? (binary.length || binary.byteLength) : (json ? json.length : 0)); + } + getStats(): RequestStats[] { + const result: RequestStats[] = []; + this.stats.forEach(({ lastHourCount, lastHourTotal, lastHourAverage, lastHourOrder, totalCount }, path) => { + result.push({ + path, + totalCount, + count: lastHourCount, + average: lastHourAverage, + total: lastHourTotal, + order: lastHourOrder, + }); + }); + return result.sort((a, b) => b.order.localeCompare(a.order)); + } + private createActionsStats(type: string, stats: (SocketStats | undefined)[]) { + return compact(stats).map(s => ({ + id: s.id, + name: s.name, + type, + countBin: s.lastHourCountBin, + countStr: s.lastHourCountStr, + average: s.lastHourAverage, + total: s.lastHourTotal, + order: s.lastHourOrder, + })); + } + getSocketStats(): ServerStats { + return { + actions: [ + ...this.createActionsStats('recv', this.recvStats), + ...this.createActionsStats('send', this.sendStats), + ].sort((a, b) => b.order.localeCompare(a.order)), + }; + } + private logStat(path: string, bytes: number) { + const entry = this.stats.get(path); + + if (entry) { + entry.count++; + entry.size.addBytes(bytes); + } else { + this.stats.set(path, { + count: 1, + size: new ByteSize(bytes), + totalCount: 0, + lastHourCount: 0, + lastHourTotal: '-', + lastHourAverage: '-', + lastHourOrder: '', + lastHourSize: new ByteSize(), + }); + } + } + private submitDailyStats(statsPath: string) { + const statsEntry = [ + moment().format('MMM DD'), // DD-MM-YY HH:mm:ss + this.dailyRequestCount.toString(), + this.dailyRequestSize.toString(), + this.dailySwearing.toString(), + this.dailySpamming.toString(), + ]; + + fs.appendFileAsync(statsPath, encodeCSV(statsEntry), { encoding: 'utf8' }) + .catch(console.error) + .done(); + } + startStatTracking() { + if (!fs.existsSync(this.statsPath)) { + fs.writeFileSync(this.statsPath, encodeCSV(statsHeaders), { encoding: 'utf8' }); + } + + setInterval(() => { + const date = getDate(); + + if (date !== this.dailyDate) { + this.submitDailyStats(this.statsPath); + this.dailyDate = date; + this.dailyRequestCount = 0; + this.dailySwearing = 0; + this.dailySpamming = 0; + this.dailyRequestSize = new ByteSize(); + } + + this.stats.forEach(entry => { + entry.lastHourCount = entry.count; + entry.lastHourSize = entry.size; + entry.lastHourTotal = entry.size.toHumanReadable(); + entry.lastHourAverage = getAverage(entry.size, entry.count); + entry.lastHourOrder = entry.size.toSortableString(); + entry.totalCount += entry.count; + entry.count = 0; + entry.size = new ByteSize(); + + this.dailyRequestCount += entry.lastHourCount; + this.dailyRequestSize.add(entry.lastHourSize); + }); + + this.sendStats.forEach(updateHourlySocketStats); + this.recvStats.forEach(updateHourlySocketStats); + }, 1 * HOUR); + } +} diff --git a/src/ts/server/timing.ts b/src/ts/server/timing.ts new file mode 100644 index 0000000..56ffe57 --- /dev/null +++ b/src/ts/server/timing.ts @@ -0,0 +1,65 @@ +import { TimingEntry, TimingEntryType } from '../common/adminInterfaces'; + +const ENABLED = true; +const ENTRIES_LIMIT = 50000; + +const entries: TimingEntry[] = []; +let entriesCount = 0; + +let now: () => number; + +if (typeof window !== 'undefined') { + now = performance.now; +} else { + const hrtime = process.hrtime; + const getNanoSeconds = () => { + const hr = hrtime(); + return hr[0] * 1e9 + hr[1]; + }; + const nodeLoadTime = getNanoSeconds() - process.uptime() * 1e9; + now = () => (getNanoSeconds() - nodeLoadTime) / 1e6; +} + +if (ENABLED) { + for (let i = 0; i < ENTRIES_LIMIT; i++) { + entries.push({ type: 0, time: 0, name: undefined }); + } +} + +export function timingStart(name: string) { + if (ENABLED) { + if (entriesCount < ENTRIES_LIMIT) { + const entry = entries[entriesCount]; + entry.type = TimingEntryType.Start; + entry.time = now(); + entry.name = name; + entriesCount++; + } else { + console.warn(`exceeded timing entry limit`); + } + } +} + +export function timingEnd() { + if (ENABLED) { + if (entriesCount < ENTRIES_LIMIT) { + const entry = entries[entriesCount]; + entry.type = TimingEntryType.End; + entry.time = now(); + entry.name = undefined; + entriesCount++; + } else { + console.warn(`exceeded timing entry limit`); + } + } +} + +export function timingReset() { + if (ENABLED) { + entriesCount = 0; + } +} + +export function timingEntries() { + return entries.slice(0, entriesCount); +} diff --git a/src/ts/server/userError.ts b/src/ts/server/userError.ts new file mode 100644 index 0000000..d53a314 --- /dev/null +++ b/src/ts/server/userError.ts @@ -0,0 +1,52 @@ +import { Request } from 'express'; +import { logger } from './logger'; +import { createFromRequest } from './reporter'; +import { IClient, Reporter } from './serverInterfaces'; +import { ServerConfig } from '../common/adminInterfaces'; + +export interface UserErrorInfo { + error?: Error; + message?: string; + desc?: string; + data?: any; + log?: string; +} + +export class UserError extends Error { + //static name: string; + constructor(public message: string, public info?: UserErrorInfo, public userInfo?: string) { + super(message); + Object.defineProperty(this, 'name', { value: 'UserError' }); + Error.captureStackTrace(this, UserError); + } +} + +export function isUserError(e: Error): e is UserError { + return e.name === 'UserError'; +} + +function report(message: string, info: UserErrorInfo, reporter: Reporter | undefined, extra = '') { + const keys = Object.keys(info); + + if (keys.length === 1 && keys[0] === 'log') { + logger.log(info.log); + } else { + if (reporter) { + reporter.warn((info.error && info.error.message) || info.message || message || '', info.desc); + } + + logger.warn(info.error || info.message || message || '', info.desc || '', info.data || '', extra); + } +} + +export function reportUserError(e: UserError, server: ServerConfig, req: Request) { + if (e.info) { + report(e.message, e.info, createFromRequest(server, req), `${req.url} ${req.ip}`); + } +} + +export function reportUserError2(e: UserError, client: IClient | undefined) { + if (e.info) { + report(e.message, e.info, client && client.reporter); + } +} diff --git a/src/ts/server/utils/byteSize.ts b/src/ts/server/utils/byteSize.ts new file mode 100644 index 0000000..cdfb502 --- /dev/null +++ b/src/ts/server/utils/byteSize.ts @@ -0,0 +1,31 @@ +const MB = 1024 * 1024; + +export class ByteSize { + constructor(public bytes = 0, public mbytes = 0) { + this.reduce(); + } + add({ bytes, mbytes }: ByteSize) { + this.addBytes(bytes, mbytes); + } + addBytes(bytes: number, mbytes = 0) { + this.mbytes += mbytes; + this.bytes += bytes; + this.reduce(); + return this; + } + toString() { + return this.mbytes ? `${this.mbytes.toString()}${this.bytes.toString().padStart(6, '0')}` : this.bytes.toString(); + } + toSortableString() { + return `${this.mbytes.toString().padStart(9, '0')}-${this.bytes.toString().padStart(6, '0')}`; + } + toHumanReadable() { + return this.mbytes >= 1 ? + `${this.mbytes} mb` : + (this.bytes >= 2048 ? `${Math.floor(this.bytes / 1024)} kb` : `${this.bytes} b`); + } + private reduce() { + this.mbytes += Math.floor(this.bytes / MB); + this.bytes = this.bytes % MB; + } +} diff --git a/src/ts/server/utils/socketErrorHandler.ts b/src/ts/server/utils/socketErrorHandler.ts new file mode 100644 index 0000000..b94f878 --- /dev/null +++ b/src/ts/server/utils/socketErrorHandler.ts @@ -0,0 +1,156 @@ +import { escapeRegExp } from 'lodash'; +import { ErrorHandler, getMethods } from 'ag-sockets'; +import * as Rollbar from 'rollbar'; +import { IClient } from '../serverInterfaces'; +import { logger } from '../logger'; +import { isUserError, reportUserError2 } from '../userError'; +import { includes } from '../../common/utils'; +import { create } from '../reporter'; +import { ServerConfig } from '../../common/adminInterfaces'; +import { getOriginFromHTTP } from '../originUtils'; +import { ServerActions } from '../serverActions'; + +const reporterIgnore = /^rate limit exceeded/i; +const ignoreErrors = [ + 'String message while forced binary', +]; +const rollbarIgnore = new RegExp('^(' + [ + 'reserved fields must be empty', + 'rate limit exceeded', + 'transfer limit exceeded', + 'some error', + 'Invalid token', + 'Action not allowed', + 'Client does not exist', + 'Cannot perform this action on admin user', + 'Account creation is temporarily disabled', + 'Not a number', + 'Not a string', + ...ignoreErrors, +].map(escapeRegExp).join('|') + ')', 'i'); + +let lastError = ''; +let lastErrorTime = 0; + +function formatMessage(message: string | Uint8Array | null | undefined) { + if (message === null) { + return ''; + } else if (message === undefined) { + return ''; + } else if (typeof message === 'string') { + return message; + } else if (message instanceof Uint8Array) { + return `<${Array.from(message).toString()}>`; + } else { + return `<${JSON.stringify(message)}>`; + } +} + +function getPerson(client: IClient | undefined) { + return client && client.account ? { + id: client.accountId, + username: client.account.name, + } : {}; +} + +function reportError(rollbar: Rollbar | undefined, e: Error, client: IClient | undefined, config: ServerConfig) { + if (isUserError(e)) { + reportUserError2(e, client); + return e; + } else { + if (client && client.reporter && !reporterIgnore.test(e.message)) { + client.reporter.error(e); + } else if (client && client.originalRequest) { + const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest); + create(config, undefined, undefined, origin).error(e); + } else { + create(config).error(e); + } + + if (!rollbarIgnore.test(e.message)) { + rollbar && rollbar.error(e, null as any, { person: getPerson(client) }); + } + + return new Error('Error occurred'); + } +} + +const serverMethods = getMethods(ServerActions); + +function getMethodNameFromPacket(packet: string | Uint8Array) { + try { + if (typeof packet === 'string') { + const values = JSON.parse(packet); + return serverMethods[values[0]].name; + } else { + return serverMethods[packet[0]].name; + } + } catch { + return '???'; + } +} + +function reportRateLimit(client: IClient, e: Error, message: string) { + let reported = false; + + if (client.rateLimitMessage === e.message && client.rateLimitCount) { + if (++client.rateLimitCount > 5) { + reported = true; + client.reporter.warn(`${e.message} (x5)`, message); + client.rateLimitCount = 1; + client.disconnect(true, true); + } + } else { + client.rateLimitMessage = e.message; + client.rateLimitCount = 1; + } + + return reported; +} + +export class SocketErrorHandler implements ErrorHandler { + constructor(private rollbar: Rollbar | undefined, private config: ServerConfig) { + } + handleError(client: IClient | null, e: Error) { + if (!/no server for given id/i.test(e.message)) { + reportError(this.rollbar, e, client || undefined, this.config); + } + } + handleRejection(client: IClient, e: Error) { + if (/^rate limit exceeded/i.test(e.message)) { + reportRateLimit(client, e, 'rejection'); + return new Error('Error occurred'); + } else { + return reportError(this.rollbar, e, client, this.config); + } + } + handleRecvError(client: IClient, e: Error, socketMessage: string | Uint8Array) { + if (lastError === e.message && Date.now() < (lastErrorTime + 5000)) + return; + + const message = formatMessage(socketMessage); + const method = getMethodNameFromPacket(socketMessage); + let reported = false; + + if (client.reporter) { + if (/^rate limit exceeded/i.test(e.message)) { + reported = reportRateLimit(client, e, message); + } else if (/^transfer limit exceeded/i.test(e.message)) { + reported = true; + const desc = e.message.replace(/transfer limit exceeded /i, ''); + client.reporter.warn('Transfer limit exceeded', `${desc} - (${method}) ${message}`); + } else if (!includes(ignoreErrors, e.message)) { + reported = true; + client.reporter.error(e, `(${method}) ${message}`); + } + } + + lastError = e.message; + lastErrorTime = Date.now(); + + if (!reported && !rollbarIgnore.test(e.message || '')) { + logger.error(`recv error: ${e.stack || e}\n\n message: ${message}`); + this.rollbar && this.rollbar.error(e, null as any, { custom: { message }, person: getPerson(client) }); + } + } +} diff --git a/src/ts/server/utils/taskQueue.ts b/src/ts/server/utils/taskQueue.ts new file mode 100644 index 0000000..8bd47b4 --- /dev/null +++ b/src/ts/server/utils/taskQueue.ts @@ -0,0 +1,60 @@ +import { noop } from 'lodash'; + +export interface TaskQueue { + push(action: () => Promise | T): Promise; + wait(): Promise; +} + +interface Task { + resolve: (value: any) => void; + reject: (error: any) => void; + action: () => any; +} + +export function taskQueue(): TaskQueue { + const queue: Task[] = []; + let working = false; + + function next() { + const task = queue.shift(); + + if (task) { + exec(task); + } else { + working = false; + } + } + + function exec({ action, resolve, reject }: Task) { + working = true; + + Promise.resolve() + .then(action) + .then(resolve, reject) + .catch(console.error) + .finally(next); + } + + function push(action: () => any): Promise { + return new Promise((resolve, reject) => { + const task: Task = { action, resolve, reject }; + + if (working) { + queue.push(task); + } else { + exec(task); + } + }); + } + + function wait() { + return push(noop); + } + + return { push, wait }; +} + +export function makeQueued any>(action: T): T { + const queue = taskQueue(); + return ((...args: any[]) => queue.push(() => action(...args))) as T; +} diff --git a/src/ts/server/world.ts b/src/ts/server/world.ts new file mode 100644 index 0000000..b00cb60 --- /dev/null +++ b/src/ts/server/world.ts @@ -0,0 +1,999 @@ +import { getWriterBuffer } from 'ag-sockets'; +import { remove, compact } from 'lodash'; +import { + TileType, WorldState, Season, Holiday, WorldStateFlags, LeaveReason, NotificationFlags, UpdateFlags, Action, +} from '../common/interfaces'; +import { removeItem, distance, clamp, randomPoint, includes, fromNow } from '../common/utils'; +import { HOUR_LENGTH, DAY_LENGTH } from '../common/timeUtils'; +import { + AFK_TIMEOUT, MAP_DISCARD_TIMEOUT, JOINS_PER_UPDATE, REMOVE_INTERVAL, REMOVE_TIMEOUT, + MAP_SWITCHES_PER_UPDATE, MINUTE, SERVER_FPS, HOUR +} from '../common/constants'; +import { + IClient, ServerEntity, Controller, GetSettings, ServerNotification, SocketStats, ServerMap, MapUsage +} from './serverInterfaces'; +import { isTileLocked, getMapInfo, setTile, hasAnyClients, createMinimap } from './serverMap'; +import { getModInfo } from './accountUtils'; +import { IAccount } from './db'; +import { PartyService } from './services/party'; +import { NotificationService } from './services/notification'; +import { AroundEntry, ServerLiveSettings, ServerConfig } from '../common/adminInterfaces'; +import { fixPosition, updateEntity, pushRemoveEntityToClient, pushUpdateEntityToClient } from './entityUtils'; +import { isBanned, isShadowed } from '../common/adminUtils'; +import { + createAndUpdateCharacterState, setEntityExpression, reloadFriends, updateEntityPlayerState, resetClientUpdates, isMutedOrShadowed +} from './playerUtils'; +import { + sparseRegionUpdate, addToRegion, removeFromRegion, unsubscribeFromOutOfRangeRegions, + subscribeToRegionsInRange, unsubscribeFromAllRegions, commitRegionUpdates, updateRegions, setupTiming, + clearTiming, resetEncodeUpdate +} from './regionUtils'; +import { roundPosition, roundPositionXMidPixel, roundPositionYMidPixel } from '../common/positionUtils'; +import { logger } from './logger'; +import { updateCamera, centerCameraOn } from '../common/camera'; +import { timingStart, timingEnd } from './timing'; +import { getRegionGlobal, getTile } from '../common/worldMap'; +import { getEntityTypeName } from '../common/entities'; +import { toFriendOnline, toFriendOffline, FriendsService } from './services/friends'; +// import { Pool, createPool } from './pool'; +import { isStaticCollision, getCollisionStats, fixCollision, updatePosition } from '../common/collision'; +import { HidingService } from './services/hiding'; +import { generateRegionCollider } from '../common/region'; +import { updateTileIndices } from '../client/tileUtils'; +import { removeEntityFromRegion } from './serverRegion'; +import { createIslandMap } from './maps/islandMap'; +import { createHouseMap } from './maps/houseMap'; +import { updateMainMapSeason } from './maps/mainMap'; + +interface MapSwitch { + map: ServerMap; + x: number; + y: number; + client: IClient; +} + +interface ReservedID { + id: number; + time: number; +} + +export class World { + season = Season.Summer; + holiday = Holiday.None; + maps: ServerMap[] = []; + controllers: Controller[] = []; + options = { + restoreTerrain: !DEVELOPMENT, + }; + clients: IClient[] = []; + clientsByAccount = new Map(); + joinQueue: IClient[] = []; + mapSwitchQueue: MapSwitch[] = []; + now = 0; + start = 0; + // mapPools = new Map>(); + private maxId = 0 >>> 0; + private offlineClients: IClient[] = []; + private baseTime = 0; + private entityById = new Map(); + private reservedIds = new Map(); + private reservedIdsByKey = new Map(); + constructor( + public readonly server: ServerConfig, + private readonly partyService: PartyService, + private readonly friendsService: FriendsService, + public readonly hidingService: HidingService, + private readonly notifications: NotificationService, + private readonly getSettings: GetSettings, + private readonly liveSettings: ServerLiveSettings, + private readonly socketStats: SocketStats, + ) { + // this.mapPools.set('island', createPool(10, () => createIslandMap(this, true), resetIslandMap)); + // this.mapPools.set('house', createPool(10, () => createHouseMap(this, true), resetHouseMap)); + + partyService.partyChanged.subscribe(client => { + if (client.isConnected && client.map.usage === MapUsage.Party) { + if ( + client.party && client.party.leader === client && client.map.instance === client.accountId && + !this.maps.some(m => m.id === client.map.id && m.instance === client.party!.id) + ) { + client.map.instance = client.party.id; + } else { + refreshMap(this, client); + } + } + }); + } + get featureFlags() { + return this.server.flags; + } + // entities + get time() { + return this.baseTime + Date.now(); + } + setTime(hour: number) { + let newBaseTime = hour * HOUR_LENGTH - (Date.now() % DAY_LENGTH); + + while (newBaseTime < 0) { + newBaseTime += DAY_LENGTH; + } + + this.baseTime = newBaseTime; + this.updateWorldState(); + } + setTile(map: ServerMap, x: number, y: number, type: TileType) { + if (!BETA && map.tilesLocked) + return; + + if (x >= 0 && y >= 0 && x < map.width && y < map.height && !isTileLocked(map, x, y) && type !== getTile(map, x, y)) { + setTile(map, x, y, type); + } + } + toggleWall(map: ServerMap, x: number, y: number, type: TileType) { + for (const controller of map.controllers) { + if (controller.toggleWall) { + controller.toggleWall(x, y, type); + } + } + } + getState(): WorldState { + return { + time: this.time, + season: this.season, + holiday: this.holiday, + flags: this.getSettings().filterSwears ? WorldStateFlags.Safe : WorldStateFlags.None, + featureFlags: this.featureFlags, + }; + } + setSeason(season: Season, holiday: Holiday) { + this.season = season; + this.holiday = holiday; + this.updateWorldState(); + updateMainMapSeason(this, this.getMainMap(), season, holiday); + } + private updateWorldState() { + const state = this.getState(); + + for (const client of this.clients) { + client.worldState(state, false); + } + } + getEntityById(id: number) { + return this.entityById.get(id); + } + getNewEntityId() { + do { + this.maxId = (this.maxId + 1) >>> 0; + } while (this.maxId === 0 || this.entityById.has(this.maxId) || this.reservedIds.has(this.maxId)); + + return this.maxId; + } + addEntity(entity: ServerEntity, map: ServerMap) { + if (DEVELOPMENT) { + if (entity.update) { + console.error('Entity update() method is only for client-side use'); + } + + if (entity.id && this.entityById.has(entity.id)) { + console.error(`Entity already added to the world ${getEntityTypeName(entity.type)} [${entity.id}]`); + } + } + + entity.id = entity.id || this.getNewEntityId(); + entity.timestamp = this.now / 1000; + this.entityById.set(entity.id, entity); + roundPosition(entity); + const region = getRegionGlobal(map, entity.x, entity.y); + addToRegion(entity, region, map); + return entity; + } + removeEntity(entity: ServerEntity, map: ServerMap) { + let removed = false; + + if (entity.region) { + removed = removeFromRegion(entity, entity.region, map); + } + + this.entityById.delete(entity.id); + return removed; + } + removeEntityFromSomeMap(entity: ServerEntity) { + const map = this.maps.find(m => m.regions.some(r => includes(r.entities, entity))); + + if (map) { + this.removeEntity(entity, map); + } else { + DEVELOPMENT && logger.error(`Missing map for entity`); + } + } + // map + getMainMap() { + return this.maps[0]; + } + switchToMap(client: IClient, map: ServerMap, x: number, y: number) { + if (client.map === map) { + DEVELOPMENT && logger.error(`Switching to the same map`); + return; + } + + if (this.mapSwitchQueue.some(x => x.client === client)) { + DEVELOPMENT && logger.error(`Already in map switch queue`); + return; + } + + this.mapSwitchQueue.push({ client, map, x, y }); + + client.isSwitchingMap = true; + client.pony.vx = 0; + client.pony.vy = 0; + updateEntity(client.pony, false); + client.mapSwitching(); + } + actualSwitchToMap(client: IClient, map: ServerMap, x: number, y: number) { + unsubscribeFromAllRegions(client, false); + + if (client.pony.region) { + removeFromRegion(client.pony, client.pony.region, client.map); + } + + x = clamp(x, 0, map.width); + y = clamp(y, 0, map.height); + + resetClientUpdates(client); + + client.mapState(getMapInfo(map), map.state); + client.map = map; + client.pony.x = x; + client.pony.y = y; + client.safeX = x; + client.safeY = y; + client.lastTime = 0; + client.lastMapSwitch = Date.now(); + client.loading = true; + client.lastCameraX = 0; + client.lastCameraY = 0; + client.lastCameraW = 0; + client.lastCameraH = 0; + client.isSwitchingMap = false; + + addToRegion(client.pony, getRegionGlobal(map, x, y), map); + fixPosition(client.pony, map, x, y, true); + + client.reporter.systemLog(`Switched map to [${client.map.id || 'main'}]`); + } + // main + initialize(now: number) { + this.start = now; + this.now = now; + const nowSeconds = now / 1000; + + for (const controller of this.controllers) { + controller.initialize(nowSeconds); + } + + for (const map of this.maps) { + for (const controller of map.controllers) { + controller.initialize(nowSeconds); + } + } + } + update(delta: number, now: number) { + const started = Date.now(); + + timingStart('world.update()'); + + resetEncodeUpdate(); + + this.now = now; + + const nowSeconds = now / 1000; + const deltaSeconds = delta / 1000; + + timingStart('update tiles'); + for (const map of this.maps) { + if (!map.dontUpdateTilesAndColliders) { + for (const region of map.regions) { + if (region.tilesDirty) { + updateTileIndices(region, map); + } + } + } + } + timingEnd(); + + timingStart('update colliders'); + for (const map of this.maps) { + if (!map.dontUpdateTilesAndColliders) { + for (const region of map.regions) { + if (region.colliderDirty) { + generateRegionCollider(region, map); + } + } + } + } + timingEnd(); + + timingStart('update positions'); + for (const map of this.maps) { + for (const region of map.regions) { + // TODO: update only moving entities, separate list of movingEntities + for (const entity of region.movables) { + // TODO: make sure timestamp is initialized if entity is moving + const delta = nowSeconds - entity.timestamp; + + if (delta > 0) { + if (entity.vx !== 0 || entity.vy !== 0) { + timingStart('updatePosition()'); + updatePosition(entity, delta, map); + timingEnd(); + } + + entity.timestamp = nowSeconds; + } + } + } + } + timingEnd(); + + timingStart('updateCamera + updateSubscriptions'); + for (const client of this.clients) { + if (updateClientCamera(client)) { + unsubscribeFromOutOfRangeRegions(client); + subscribeToRegionsInRange(client); + } + } + timingEnd(); + + timingStart('update controllers'); + for (const controller of this.controllers) { + controller.update(deltaSeconds, nowSeconds); + } + + for (const map of this.maps) { + for (const controller of map.controllers) { + controller.update(deltaSeconds, nowSeconds); + } + } + timingEnd(); + + timingStart('actualSwitchToMap'); + for (let i = 0; i < MAP_SWITCHES_PER_UPDATE && this.mapSwitchQueue.length; i++) { + const { client, map, x, y } = this.mapSwitchQueue.shift()!; + this.actualSwitchToMap(client, map, x, y); + } + timingEnd(); + + timingStart('updateRegions'); + updateRegions(this.maps); // NOTE: creates transfers + timingEnd(); + + timingStart('timeoutEntityExpression + inTheAirDelay'); + for (const { pony } of this.clients) { + // timeout expressions + if (pony.exprTimeout && pony.exprTimeout < now) { + setEntityExpression(pony, undefined); // NOTE: creates updates + } + + // count down in-the-air delay + if (pony.inTheAirDelay !== undefined && pony.inTheAirDelay > 0) { + pony.inTheAirDelay -= deltaSeconds; + } + } + timingEnd(); + + // const { totalUpdates, reusedUpdates } = this.updatesStats(); + + timingStart(`commitRegionUpdates`); // [${totalUpdates} / ${reusedUpdates}]`); + for (const map of this.maps) { + commitRegionUpdates(map.regions); + } + timingEnd(); + + let clientsWithAdds = 0; + let clientsWithUpdates = 0; + let clientsWithSays = 0; + let totalSays = 0; + + timingStart(`send updates`); + for (const client of this.clients) { + const { updateQueue, regionUpdates, saysQueue, unsubscribes, subscribes } = client; + const updateBuffer = updateQueue.offset ? getWriterBuffer(updateQueue) : null; + const total = updateQueue.offset + regionUpdates.length + saysQueue.length + unsubscribes.length + subscribes.length; + + if (total !== 0) { + if (updateQueue.offset > 0) + clientsWithAdds++; + if (regionUpdates.length > 0) + clientsWithUpdates++; + if (saysQueue.length > 0) + clientsWithSays++; + totalSays += saysQueue.length; + + setupTiming(client); + timingStart('client.update()'); + client.update(unsubscribes, subscribes, updateBuffer, regionUpdates, saysQueue); + timingEnd(); + clearTiming(client); + + resetClientUpdates(client); + } + } + timingEnd(); + + timingStart('joinQueuedClients'); + if (Date.now() < (started + (1000 / SERVER_FPS))) { + for (let i = 0; i < JOINS_PER_UPDATE && this.joinQueue.length > 0; i++) { + this.joinClientToWorld(this.joinQueue.shift()!); // NOTE: creates adds + } + } + timingEnd(); + + const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); + + timingStart(`adds [${clientsWithAdds}]\n` + + `updates [${clientsWithUpdates}]\n` + + `says [${totalSays} / ${clientsWithSays}]\n` + + `sockets [${this.socketStatsText()}]\n` + + `collisions [${isCollidingObjectCount} / ${isCollidingCount}]`); + this.cleanupOfflineClients(); + timingEnd(); + + timingEnd(); + } + sparseUpdate(now: number) { + timingStart('world.sparseUpdate()'); + + timingStart('sparse update controllers'); + for (const controller of this.controllers) { + if (controller.sparseUpdate !== undefined) { + controller.sparseUpdate(); + } + } + + for (const map of this.maps) { + for (const controller of map.controllers) { + if (controller.sparseUpdate !== undefined) { + controller.sparseUpdate(); + } + } + } + timingEnd(); + + timingStart('sparseRegionUpdate'); + for (const map of this.maps) { + for (const region of map.regions) { + sparseRegionUpdate(map, region, this.options); + } + } + timingEnd(); + + timingStart('kick afk clients'); + for (const client of this.clients) { + if ((now - client.lastPacket) > AFK_TIMEOUT) { + this.kick(client, 'afk'); + } + } + timingEnd(); + + timingStart('send queue status (join)'); + for (let i = 0; i < this.joinQueue.length; i++) { + this.joinQueue[i].queue(i + 1); + } + timingEnd(); + + timingStart('send queue status (map)'); + for (let i = 0; i < this.mapSwitchQueue.length; i++) { + this.mapSwitchQueue[i].client.queue(i + 1); + } + timingEnd(); + + timingStart('cleanup unused maps'); + const mapDiscardThreshold = now - MAP_DISCARD_TIMEOUT; + + for (const map of this.maps) { + if (map.instance && (hasAnyClients(map) || this.mapSwitchQueue.some(q => q.map === map))) { + map.lastUsed = now; + } + } + + for (let i = this.maps.length - 1; i > 0; i--) { + const map = this.maps[i]; + + if (map.instance && map.lastUsed < mapDiscardThreshold) { + this.maps.splice(i, 1); + // const pool = this.mapPools.get(map.id); + + // if (pool && pool.dispose(map)) { + // for (const region of map.regions) { + // resetRegionUpdates(region); + // } + // } else { + for (const region of map.regions) { + for (const entity of region.entities) { + this.entityById.delete(entity.id); + } + } + // } + } + } + timingEnd(); + + timingStart('cleanup reserved ids'); + const threshold = Date.now() - 5 * MINUTE; + + for (const key of Array.from(this.reservedIdsByKey.keys())) { // TODO: avoid doing this to save gc + const { id, time } = this.reservedIdsByKey.get(key)!; + + if (time < threshold) { + this.reservedIdsByKey.delete(key); + this.reservedIds.delete(id); + } + } + timingEnd(); + + timingStart('cleanup parties'); + this.partyService.cleanupParties(); + timingEnd(); + + timingEnd(); + } + private socketStatsText() { + const { sent, received, sentPackets, receivedPackets } = this.socketStats.stats(); + return `sent: ${(sent / 1024).toFixed(2)} kb (${sentPackets}), ` + + `recv: ${(received / 1024).toFixed(2)} kb (${receivedPackets})`; + } + updatesStats() { + timingStart('updatesStats()'); + + let totalUpdates = 0, reusedUpdates = 0; + + for (const map of this.maps) { + for (const region of map.regions) { + totalUpdates += region.entityUpdates.length; + reusedUpdates += region.reusedUpdates; + } + } + + timingEnd(); + + return { totalUpdates, reusedUpdates }; + } + // clients + private lastCleanup = 0; + private cleanupOfflineClients() { + const now = Date.now(); + + if ((now - this.lastCleanup) > REMOVE_INTERVAL) { + this.lastCleanup = now; + const removeFrom = now - REMOVE_TIMEOUT; + + remove(this.offlineClients, c => c.offline && !c.party && c.offlineAt && c.offlineAt.getTime() < removeFrom); + } + } + joinClientToQueue(client: IClient) { + if (this.liveSettings.shutdown) { + client.leaveReason = 'shutdown'; + client.disconnect(false, true); + return; + } + + const { tokenId } = client; + + function findClientsToKick(clients: IClient[]) { + return clients.filter(c => c.tokenId === tokenId); + } + + const clientsToKick = [ + ...findClientsToKick(this.clients), + ...findClientsToKick(this.joinQueue), + ]; + + for (const client of clientsToKick) { + const reason = client.tokenId === tokenId ? 'kicked [joining again]' : 'kicked [alone on ip]'; + this.kick(client, reason, LeaveReason.None, true); + } + + // TODO: wait for all clients to be kicked before adding to the queue + // another queue before joinQueue + + this.joinQueue.push(client); + } + joinClientToWorld(client: IClient) { + timingStart('joinClientToWorld()'); + + const key = `${client.accountId}:${client.characterId}`; + const reserved = this.reservedIdsByKey.get(key); + + if (reserved) { + client.pony.id = reserved.id; + this.reservedIdsByKey.delete(client.accountId); + this.reservedIds.delete(reserved.id); + } else { + client.pony.id = this.getNewEntityId(); + } + + client.myEntity(client.pony.id, client.characterName, client.character.info!, client.characterId, client.pony.crc || 0); + + this.clients.push(client); + this.clientsByAccount.set(client.accountId, client); + this.partyService.clientConnected(client); + this.hidingService.connected(client); + + let map = findOrCreateMapForClient(this, client.characterState.map || '', client); + + if (!map) { + map = this.getMainMap(); + const { x, y } = randomPoint(map.spawnArea); + client.pony.x = x; + client.pony.y = y; + } + + if (isStaticCollision(client.pony, map)) { + if (!fixCollision(client.pony, map)) { + const { x, y } = randomPoint(map.spawnArea); + client.pony.x = x; + client.pony.y = y; + } + } + + client.pony.x = roundPositionXMidPixel(client.pony.x); + client.pony.y = roundPositionYMidPixel(client.pony.y); + client.map = map; + + centerCameraOn(client.camera, client.pony); + + client.worldState(this.getState(), true); + client.mapState(getMapInfo(client.map), client.map.state); + + if (BETA) { + timingStart('minimap'); + client.mapTest(client.map.width, client.map.height, createMinimap(this, client.map)); + timingEnd(); + } + + updateCamera(client.camera, client.pony, map); + + this.addEntity(client.pony, client.map); + + const visibleOnlineFriends: IClient[] = []; + + for (const c of this.clients) { + if (c.selected && c.selected.client && c.selected.client.accountId === client.accountId) { + c.updateSelection(c.selected.id, client.pony.id); + } + + if (client.friends.has(c.accountId)) { + if (!c.accountSettings.hidden && !c.shadowed) { + visibleOnlineFriends.push(c); + } + + if (!client.accountSettings.hidden) { + c.updateFriends([toFriendOnline(client)], false); + } + + if (!c.friends.has(client.accountId)) { + reloadFriends(c).catch(e => logger.error(e)); + } + } else if (c.friends.has(client.accountId)) { + reloadFriends(c).catch(e => logger.error(e)); + } + } + + if (visibleOnlineFriends.length) { + client.updateFriends(visibleOnlineFriends.map(toFriendOnline), false); + } + + if (this.liveSettings.updating) { + this.notifications.addNotification(client, updateNotification()); + } + + // TEMP: duplicate pony bug + if (client.map.instance) { + for (const region of client.map.regions) { + for (const entity of region.entities) { + if (entity.client !== undefined && entity !== client.pony && entity.client.accountId === client.accountId) { + const sameClients = this.clients.filter(c => c.accountId === client.accountId).length; + + client.reporter.systemLog(`Client pony already on map ` + + `(old: ${entity.id}, new: ${client.pony.id}, sameClients: ${sameClients})`); + } + } + } + } + + timingEnd(); + } + getClientByEntityId(entityId: number) { + if (entityId === 0) { + return undefined; + } else { + const byPonyId = (c: IClient) => c.pony.id === entityId; + return this.clients.find(byPonyId) || this.offlineClients.find(byPonyId); + } + } + private removeEntityFromAnyMap(entity: ServerEntity) { + for (const map of this.maps) { + for (const region of map.regions) { + const index = region.entities.indexOf(entity); + + if (index !== - 1) { + removeEntityFromRegion(region, entity, map); + return map; + } + } + } + + return undefined; + } + leaveClient(client: IClient) { + const friends = findAllOnlineFriends(this, client); + + if (!client.accountSettings.hidden) { + for (const friend of friends) { + friend.updateFriends([toFriendOffline(client)], false); + } + } + + this.reservedIds.set(client.pony.id, client.accountId); + this.reservedIdsByKey.set(`${client.accountId}:${client.characterId}`, { id: client.pony.id, time: Date.now() }); + + if (!this.removeEntity(client.pony, client.map)) { + const map = this.removeEntityFromAnyMap(client.pony); + client.reporter.systemLog(`Removing from any map (` + + `expected: ${client.map && client.map.id} [${client.map && client.map.instance}], ` + + `actual: ${map && map.id} [${map && map.instance}])`); + } + + unsubscribeFromAllRegions(client, true); + removeItem(this.joinQueue, client); + removeItem(this.clients, client); + this.clientsByAccount.delete(client.accountId); + this.offlineClients.push(client); + + const index = this.mapSwitchQueue.findIndex(x => x.client === client); + + if (index !== -1) { + this.mapSwitchQueue.splice(index, 1); + } + } + notifyHidden(by: string, who: string) { + const byClient = findClientByAccountId(this, by); + const whoClient = findClientByAccountId(this, who); + + if (byClient && whoClient) { + updateEntityPlayerState(byClient, whoClient.pony); + updateEntityPlayerState(whoClient, byClient.pony); + } + } + resetToSpawn(client: IClient) { + Object.assign(client.pony, randomPoint(client.map.spawnArea)); + } + kick(client: IClient | undefined, leaveReason = 'kicked', reason = LeaveReason.None, force = false) { + if (client) { + removeItem(this.joinQueue, client); + this.notifications.rejectAll(client); + this.leaveClient(client); + client.leaveReason = leaveReason; + client.left(reason); + + if (force) { + client.disconnect(true); + } else { + setTimeout(() => { + if (client.isConnected) { + client.disconnect(true); + } + }, 200); + } + } + + return !!client; + } + kickAll() { + this.clients.slice().forEach(c => this.kick(c, 'kickAll')); + this.joinQueue.slice().forEach(c => c.disconnect()); + this.joinQueue = []; + } + kickByAccount(accountId: string) { + return this.kick(findClientByAccountId(this, accountId), 'kickByAccount'); + } + kickByCharacter(characterId: string) { + return this.kick(findClientByCharacterId(this, characterId), 'kickByCharacter'); + } + accountUpdated(account: IAccount) { + const accountId = account._id.toString(); + const client = findClientByAccountId(this, accountId); + + if (client) { + this.updateClientAccount(client, account); + + for (const c of this.clients) { + if (c.isMod && c.selected === client.pony) { + pushUpdateEntityToClient(c, { entity: client.pony, flags: UpdateFlags.Options, options: { modInfo: getModInfo(client) } }); + } + } + } + } + private updateClientAccount(client: IClient, newAccount: IAccount) { + const oldAccount = client.account; + client.account = newAccount; + + if (oldAccount.ban !== newAccount.ban && isBanned(newAccount)) { + sendAcl(client); + this.kick(client, 'kick (ban)'); + return; + } + + if (oldAccount.shadow !== newAccount.shadow) { + if (isShadowed(newAccount)) { + this.shadow(client); + } else if (isShadowed(oldAccount)) { + sendAcl(client); + this.kick(client, 'kick (unshadow)'); + return; + } + } + + const shouldSendAcl = oldAccount.mute !== newAccount.mute + || oldAccount.ban !== newAccount.ban + || oldAccount.shadow !== newAccount.shadow; + + if (shouldSendAcl) { + sendAcl(client); + } + } + private shadow(client: IClient) { + client.shadowed = true; + this.partyService.clientDisconnected(client); + this.friendsService.clientDisconnected(client); + this.notifications.dismissAll(client); + + if (client.pony.region) { + for (const c of client.pony.region.clients) { + if (c !== client) { + pushRemoveEntityToClient(c, client.pony); + } + } + } + } + // update notification + notifyUpdate() { + for (const client of this.clients) { + this.notifications.addNotification(client, updateNotification()); + } + } + saveClientStates() { + for (const client of this.clients) { + createAndUpdateCharacterState(client, this.server); + } + } +} + +// account creation lock +function sendAcl(client: IClient) { + const acl = isMutedOrShadowed(client) ? fromNow(12 * HOUR) : new Date(0); + client.actionParam(client.pony.id, Action.ACL, acl.toISOString()); +} + +function updateNotification(): ServerNotification { + return { + id: 0, + name: '', + message: 'Server will restart shortly for updates and maintenance', + flags: NotificationFlags.Ok, + }; +} + +export function refreshMap(world: World, client: IClient) { + const map = findOrCreateMapForClient(world, client.map.id, client); + + if (map) { + world.switchToMap(client, map, client.pony.x, client.pony.y); + } else { + logger.warn(`Missing map: ${client.map.id}`); + } +} + +export function goToMap(world: World, client: IClient, id: string, spawn?: string) { + const map = findOrCreateMapForClient(world, id, client); + + if (map) { + const area = spawn && map.spawns.get(spawn) || map.spawnArea; + const { x, y } = randomPoint(area); + world.switchToMap(client, map, x, y); + } else { + logger.warn(`Missing map: ${id}`); + } +} + +function findOrCreateMapInstance(world: World, id: string, instance: string) { + let map = world.maps.find(m => m.id === id && m.instance === instance); + + if (!map) { + // const pool = world.mapPools.get(id); + + // if (!pool) { + // throw new Error(`Invalid map id: ${id}`); + // } + + switch (id) { + case 'house': + map = createHouseMap(world, true); + break; + case 'island': + map = createIslandMap(world, true); + break; + default: + throw new Error(`Invalid map id: ${id}`); + } + + // map = pool.create(); + map.instance = instance; + map.lastUsed = Date.now(); + map.controllers.forEach(c => c.initialize(world.now / 1000)); + world.maps.push(map); + } + + return map; +} + +function findOrCreateMapForClient(world: World, id: string, client: IClient) { + const map = world.maps.find(m => !m.instance && m.id === id); + + if (map) { + return map; + } else { + if (client.party) { + return findOrCreateMapInstance(world, id, client.party.id); + } else { + return findOrCreateMapInstance(world, id, client.accountId); + } + } +} + +function updateClientCamera(client: IClient) { + const camera = client.camera; + + updateCamera(camera, client.pony, client.map); + + if ( + client.lastCameraX !== camera.x || client.lastCameraY !== camera.y || + client.lastCameraW !== camera.w || client.lastCameraH !== camera.h + ) { + client.lastCameraX = camera.x; + client.lastCameraY = camera.y; + client.lastCameraW = camera.w; + client.lastCameraH = camera.h; + return true; + } else { + return false; + } +} + +export function findAllOnlineFriends(world: World, client: IClient) { + return compact(Array.from(client.friends.keys()) + .map(account => findClientByAccountId(world, account))); +} + +export function findClientByAccountId(world: World, accountId: string) { + return world.clientsByAccount.get(accountId); +} + +export function findClientByCharacterId(world: World, characterId: string) { + return world.clients.find(c => c.characterId === characterId); +} + +export function findClientsAroundAccountId(world: World, accountId: string): AroundEntry[] { + const client = findClientByAccountId(world, accountId); + + return client ? world.clients + .filter(c => c !== client && c.map === client.map) + .map(c => ({ + account: c.accountId, + distance: distance(client.pony, c.pony), + party: !!(c.party && c.party === client.party), + })) + .filter(x => x.distance < 5 || x.party) + .sort((a, b) => a.distance - b.distance) + .slice(0, 12) : []; +} diff --git a/src/ts/tests/client/canvasUtils.spec.ts b/src/ts/tests/client/canvasUtils.spec.ts new file mode 100644 index 0000000..941ff26 --- /dev/null +++ b/src/ts/tests/client/canvasUtils.spec.ts @@ -0,0 +1,78 @@ +import '../lib'; +import { expect } from 'chai'; +import { resizeCanvas, resizeCanvasWithRatio } from '../../client/canvasUtils'; + +describe('canvasUtils', () => { + describe('resizeCanvas()', () => { + it('should resize the canvas', () => { + const canvas = { width: 100, height: 200 } as any; + + resizeCanvas(canvas as any, 50, 300); + + expect(canvas.width).equal(50); + expect(canvas.height).equal(300); + }); + + it('should resize height of the canvas', () => { + const canvas = { width: 100, height: 200 } as any; + + resizeCanvas(canvas as any, 100, 300); + + expect(canvas.width).equal(100); + expect(canvas.height).equal(300); + }); + + it('should leave canvas the same size', () => { + const canvas = { width: 100, height: 200 } as any; + + resizeCanvas(canvas as any, 100, 200); + + expect(canvas.width).equal(100); + expect(canvas.height).equal(200); + }); + }); + + describe('resizeCanvasWithRatio()', () => { + it('should resize the canvas', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + + resizeCanvasWithRatio(canvas as any, 50, 300); + + expect(canvas.width).equal(50); + expect(canvas.height).equal(300); + expect(canvas.style.width).equal('50px'); + expect(canvas.style.height).equal('300px'); + }); + + it('should resize height of the canvas', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + + resizeCanvasWithRatio(canvas as any, 100, 300); + + expect(canvas.width).equal(100); + expect(canvas.height).equal(300); + expect(canvas.style.width).equal('100px'); + expect(canvas.style.height).equal('300px'); + }); + + it('should leave canvas the same size', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + + resizeCanvasWithRatio(canvas as any, 100, 200); + + expect(canvas.width).equal(100); + expect(canvas.height).equal(200); + expect(canvas.style.width).equal('100px'); + expect(canvas.style.height).equal('200px'); + }); + + it('should not update the style if passed false', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + + resizeCanvasWithRatio(canvas as any, 50, 300, false); + + expect(canvas.style.width).equal(''); + expect(canvas.style.height).equal(''); + }); + }); +}); diff --git a/src/ts/tests/client/clientActions.spec.ts b/src/ts/tests/client/clientActions.spec.ts new file mode 100644 index 0000000..6fb9225 --- /dev/null +++ b/src/ts/tests/client/clientActions.spec.ts @@ -0,0 +1,388 @@ +import { stubClass, resetStubMethods } from '../lib'; +import { NgZone } from '@angular/core'; +import { encodeString } from 'ag-sockets/dist/utf8'; +import { Subject } from 'rxjs'; +import { expect } from 'chai'; +import { stub, assert, SinonStub } from 'sinon'; +import { + MessageType, Action, PartyFlags, NotificationFlags, LeaveReason, TileType, Weather, InfoFlags, Pony +} from '../../common/interfaces'; +import * as handlers from '../../client/handlers'; +import { pony } from '../../common/entities'; +import { ClientActions } from '../../client/clientActions'; +import { GameService } from '../../components/services/gameService'; +import { PonyTownGame } from '../../client/game'; +import { mock, entity, mockSubject, MockSubject } from '../mocks'; +import { encodeUpdateSimple } from '../../common/encoders/updateEncoder'; +import { ServerActions } from '../../server/serverActions'; +import { PaletteManager } from '../../graphics/paletteManager'; +import { createCamera } from '../../common/camera'; +import { commonPalettes } from '../../graphics/graphicsUtils'; +import { Model } from '../../components/services/model'; +import { createRegion } from '../../common/region'; +import { getTile, addEntity, setRegion, createWorldMap } from '../../common/worldMap'; +import { createServerRegion } from '../../server/serverRegion'; +import { ServerRegion } from '../../server/serverInterfaces'; + +describe('ClientActions', () => { + let zone: NgZone; + let model = stubClass(Model); + let gameService = stubClass(GameService); + let server = stubClass(ServerActions); + let game: PonyTownGame; + let clientActions: ClientActions; + let onMessage: MockSubject; + let onPonyAddOrUpdate: MockSubject; + + beforeEach(() => { + zone = { run: (f: any) => f() } as any; + resetStubMethods(gameService, 'left', 'joined', 'disconnected'); + resetStubMethods(server, 'action', 'getPonies', 'fixedPosition'); + resetStubMethods(model); + onMessage = mockSubject(); + onPonyAddOrUpdate = mockSubject(); + game = mock(PonyTownGame); + game.fallbackPonies = new Map(); + game.map = createWorldMap({ type: 0, flags: 0, regionsX: 2, regionsY: 2, defaultTile: TileType.None }); + setRegion(game.map, 0, 0, createRegion(0, 0)); + game.camera = createCamera(); + game.notifications = []; + game.send = f => f(server); + game.apply = f => f(); + game.onMessage = onMessage as any; + game.onPonyAddOrUpdate = onPonyAddOrUpdate as any; + game.onPartyUpdate = new Subject(); + game.paletteManager = new PaletteManager(); + game.onActionsUpdate = new Subject(); + game.webgl = { palettes: commonPalettes } as any; + game.settings = { + account: {}, + } as any; + model.ponies = []; + clientActions = new ClientActions(gameService as any, game, model as any, zone as any); + }); + + it('can be created with defaults', () => { + clientActions = new ClientActions(gameService as any, game, model as any, zone as any); + }); + + describe('connected()', () => { + it('resets game player', () => { + game.player = {} as any; + + clientActions.connected(); + + expect(game.player).undefined; + }); + + it('resets game map', () => { + game.map = {} as any; + + clientActions.connected(); + + expect(game.map).not.undefined; + }); + + it('notifies game service', () => { + clientActions.connected(); + + assert.calledOnce(gameService.joined); + }); + + it('notifies game', () => { + const joined = stub(game, 'joined'); + + clientActions.connected(); + + assert.calledOnce(joined); + }); + + it('sends info', () => { + clientActions.connected(); + + assert.calledWith( + server.actionParam2, Action.Info, InfoFlags.SupportsWASM | InfoFlags.SupportsLetAndConst); + }); + }); + + describe('disconnected()', () => { + it('notifies game service', () => { + clientActions.disconnected(); + + assert.calledOnce(gameService.disconnected); + }); + }); + + describe('worldState()', () => { + it('sets game state', () => { + const state = {} as any; + const setWorldState = stub(game, 'setWorldState'); + + clientActions.worldState(state, true); + + assert.calledWith(setWorldState, state, true); + }); + }); + + describe('mapState()', () => { + it('initializes game map', () => { + game.map = undefined as any; + + clientActions.mapState( + { type: 0, flags: 0, regionsX: 1, regionsY: 2, defaultTile: TileType.Water }, + { weather: Weather.None }); + + expect(game.map).not.undefined; + expect(game.map.regionsX).equal(1); + expect(game.map.regionsY).equal(2); + expect(game.map.defaultTile).equal(TileType.Water); + }); + }); + + describe('myEntity()', () => { + it('sets player fields', () => { + clientActions.myEntity(123, 'name', 'info', 'charid', 456); + + expect(game.playerId).equal(123); + expect(game.playerName).equal('name'); + expect(game.playerInfo).equal('info'); + expect(game.playerCRC).equal(456); + }); + + it('updates self flag for party members', () => { + game.party = { + leaderId: 0, + members: [ + { id: 321, leader: false, offline: false, pending: false, pony: {} as any, self: true }, + { id: 123, leader: false, offline: false, pending: false, pony: {} as any, self: false }, + ], + }; + + clientActions.myEntity(123, '', '', '', 0); + + expect(game.party.members[0].self).false; + expect(game.party.members[1].self).true; + }); + }); + + describe('updateRegions()', () => { + let handleUpdateEntity: SinonStub; + let handleSays: SinonStub; + let region: ServerRegion; + + beforeEach(() => { + region = createServerRegion(1, 2); + handleSays = stub(handlers, 'handleSays'); + handleUpdateEntity = stub(handlers, 'handleUpdateEntity'); + }); + + afterEach(() => { + handleSays.restore(); + handleUpdateEntity.restore(); + }); + + it('does nothing for empty update list', () => { + const emptyUpdate = encodeUpdateSimple(region); + + clientActions.update([], [], null, [emptyUpdate], []); + }); + + it('updates map tiles', () => { + region.x = region.y = 0; + region.tileUpdates.push({ x: 1, y: 2, type: 3 }, { x: 3, y: 2, type: 1 }); + const data = encodeUpdateSimple(region); + + clientActions.update([], [], null, [data], []); + + expect(getTile(game.map, 1, 2)).equal(3); + expect(getTile(game.map, 3, 2)).equal(1); + }); + + it('calls handleSays for each entry', () => { + clientActions.update([], [], null, [], [[1, 'foo', MessageType.Chat], [2, 'var', MessageType.Party]]); + + assert.calledTwice(handleSays); + assert.calledWith(handleSays, game, 1, 'foo', MessageType.Chat); + assert.calledWith(handleSays, game, 2, 'var', MessageType.Party); + }); + }); + + describe('fixPosition()', () => { + it('updates player position', () => { + game.player = {} as any; + + clientActions.fixPosition(123, 456, true); + + expect(game.player!.x).equal(123); + expect(game.player!.y).equal(456); + }); + + it('sends fixed position message back to server', () => { + clientActions.fixPosition(123, 456, true); + + assert.calledOnce(server.fixedPosition); + }); + + it('does nothing if no player', () => { + game.player = undefined; + + clientActions.fixPosition(123, 456, true); + }); + }); + + describe('left()', () => { + it('resets game player', () => { + game.player = {} as any; + + clientActions.left(LeaveReason.None); + + expect(game.player).undefined; + }); + + it('resets game map', () => { + game.map = {} as any; + + clientActions.left(LeaveReason.None); + + expect(game.map).not.undefined; + }); + + it('notifies game service', () => { + clientActions.left(LeaveReason.Swearing); + + assert.calledWith(gameService.left, 'clientActions.left', LeaveReason.Swearing); + }); + }); + + describe('addNotification()', () => { + it('adds notification to game', () => { + const e = entity(456, 0, 0, pony.type, { ponyState: {} } as any); + addEntity(game.map, e); + + clientActions.addNotification(123, 456, 'name', 'test', 'note', NotificationFlags.Ok); + + expect(game.notifications).eql([ + { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Ok, open: false, fresh: true, pony: e } + ]); + }); + + it('sets pony to offline pony if no pony is provided', () => { + const pony = game.offlinePony = { offlinePony: true } as any; + + clientActions.addNotification(123, 0, 'name', 'test', 'note', NotificationFlags.Ok); + + expect(game.notifications).eql([ + { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Ok, open: false, fresh: true, pony } + ]); + }); + + it('sets pony to supporter pony if supporter pony flag is set', () => { + const pony = game.supporterPony = { supporterPony: true } as any; + + clientActions.addNotification(123, 0, 'name', 'test', 'note', NotificationFlags.Supporter); + + expect(game.notifications).eql([ + { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Supporter, open: false, fresh: true, pony } + ]); + }); + }); + + describe('removeNotification()', () => { + it('removes notification with given id', () => { + game.notifications.push({ id: 123 } as any); + + clientActions.removeNotification(123); + + expect(game.notifications).eql([]); + }); + + it('removes notification in digest cycle', () => { + const run = stub(zone, 'run'); + + clientActions.removeNotification(123); + + assert.calledOnce(run); + }); + }); + + describe('updateSelection()', () => { + it('selects new entity', () => { + const newPony = entity(456, 0, 0, pony.type, { ponyState: {} } as any); + game.selected = entity(123) as Pony; + addEntity(game.map, newPony); + const select = stub(game, 'select'); + + clientActions.updateSelection(123, 456); + + assert.calledWith(select, newPony as any); + }); + + it('does nothing if selected ID is not current ID', () => { + const select = stub(game, 'select'); + + clientActions.updateSelection(123, 456); + + assert.notCalled(select); + }); + }); + + describe('updateParty()', () => { + it('clears party if passed undefined', () => { + game.party = {} as any; + + clientActions.updateParty(undefined); + + expect(game.party).undefined; + }); + + it('clears party if passed empty list', () => { + game.party = {} as any; + + clientActions.updateParty([]); + + expect(game.party).undefined; + }); + + it('updates party', () => { + clientActions.updateParty([ + [123, PartyFlags.Leader], + ]); + + expect(game.party).eql({ + leaderId: 123, + members: [ + { id: 123, leader: true, offline: false, pending: false, pony: undefined, self: false }, + ], + }); + }); + + it('gets missing pony info from server', () => { + clientActions.updateParty([ + [123, PartyFlags.Leader], + ]); + + assert.calledWithMatch(server.getPonies, [123]); + }); + }); + + describe('ponies()', () => { + it('does nothing for empty list', () => { + clientActions.updatePonies([]); + }); + + it('updates party pony', () => { + game.party = { + leaderId: 0, + members: [ + { id: 123, pony: undefined, leader: true, pending: false, offline: false, self: false }, + ], + }; + + clientActions.updatePonies([ + [123, {}, encodeString('foo')!, new Uint8Array([1, 2, 3]), 0, false], + ]); + + expect(game.party.members[0].pony).not.undefined; + }); + }); +}); diff --git a/src/ts/tests/client/clientUtils.spec.ts b/src/ts/tests/client/clientUtils.spec.ts new file mode 100644 index 0000000..bf35ec2 --- /dev/null +++ b/src/ts/tests/client/clientUtils.spec.ts @@ -0,0 +1,232 @@ +import '../lib'; +import { expect } from 'chai'; +import { + cleanName, cleanMessage, filterString, isSpamMessage, toSocialSiteInfo +} from '../../client/clientUtils'; +import { repeat, removeItem } from '../../common/utils'; +import { SAY_MAX_LENGTH } from '../../common/constants'; +import { oauthProviders } from '../../client/data'; + +const cleanNameTests: [string | undefined, string, string][] = [ + [undefined, '', 'undefined'], + ['', '', 'empty string'], + ['rainbow dash', 'rainbow dash', 'valid name'], + ['foo—bar', 'foo—bar', 'dash'], + [' pony ', 'pony', 'trimming'], + ['a pony', 'a pony', 'multiple spaces'], + ['a_po-ny(yay)[foo].,/|&#@!?aaa', 'a_po-ny(yay)[foo].,/|&#@!?aaa', 'allowed symbols'], + ['a\t\r\npony1', 'apony1', 'other whitespace symbols'], + ['a\u0000\u0008\u009f\u007fpony2', 'apony2', 'control'], + ['a\u0300\u0359\u036a\u05c2\ua94fpony3', 'apony3', 'mark, nonspacing'], + ['a\ufe00\ufe0fpony4', 'apony4', 'variation'], + ['a▇▗pony5', 'apony5', 'blocks'], + ['a⠟⠳⠀pony6', 'apony6', 'braile'], + ['aᶌᶗᶭpony7', 'apony7', 'phonetic extensions'], + ['aʰʷ〮pony8', 'apony8', 'modifiers'], + ['aⅨⅩⅪpony9', 'apony9', 'roman numerals'], + ['aᏅᏆᏇpony10', 'apony10', 'cherokee'], + ['\ud800apony11', 'apony11', 'invalid unicode'], + ['😺🦇🤡⏰', '😺🦇🤡⏰', 'emoji'], + ['aponߦy߃߄߅13', 'apony13', 'NKo'], + ['ap҉ony꙰14', 'apony14', 'Mark, Enclosing'], + ['ap󠀗ony󠀩15', 'apony15', 'Tags'], + ['apA$zony16', 'apA$zony16', 'Romaji'], + ['apony🖕17', 'apony17', 'filtered emoji'], + ['[△▽△]❥Pony™✔18', '[△▽△]❥Pony™✔18', 'triangles and symbols'], + ['ニキフォーオブ', 'ニキフォーオブ', 'allow katakana'], + ['ﷺ ﷻ﷽long', 'long', 'Weird long symbols'], + ['꧁Adam', 'Adam', 'Weird symbols'], + ['⎝Perro', 'Perro', 'weird long symbol'], + ['aaa\u1160bbb', 'aaa bbb', 'Converts hangul space to regular space'], + ['aaa\u3000bbb', 'aaa bbb', 'Converts ideographic space to regular space'], + ['aaa\u3164bbb', 'aaa bbb', 'Converts hangul filler to regular space'], + ['sắp sáng rồi', 'sắp sáng rồi', 'Vietnamese'], + ['x\u00ady', 'xy', 'Remove soft hyphen'], + ['a\u2800b', 'ab', 'Remove braille pattern blank'], +]; + +const cleanMessageTests: [string | undefined, string][] = [ + [undefined, ''], + ['', ''], + ['hello', 'hello'], + ['😺🦇🤡⏰', '😺🦇🤡⏰'], // emoji + ['🍪🥚', '🍪🥚'], // egg + ['a_po-ny(yay)[foo].,/|&#@!?aaa', 'a_po-ny(yay)[foo].,/|&#@!?aaa'], // allowed symbols + ['E̸̢͕̬̹̠̘̬̲̠͖͓͂̾ͧ̈́ͮͮ̈́̄͛̉ͪͤ͒͊̏̅́͘͘R̸̴̅̌͋ͯͦ̔͊̎͊͑҉̶̪͕̳̙̦̤̞̹̀R̃͛̂ͣ͊ͤ̔', 'ERR'], + ['ap󠀗ony󠀩15', 'apony15'], // Tags + ['a\u0000\u0008\u009f\u007fpony2', 'apony2'], // control + ['apA$zony16', 'apA$zony16'], // Romaji + ['spe⦅⦆。「」、・ ̄ˊcial', 'spe⦅⦆。「」、・ ̄ˊcial'], // Special + ['ニキフォーオブ', 'ニキフォーオブ'], // allow katakana + ['、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵', + '、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵'], // allow special japanese/chinese characters + ['ﷺ ﷻ﷽long', 'long'], // Weird long symbols + ['aaa\u1160bbb', 'aaa bbb'], // Converts hangul space to regular space + ['aaa\u3000bbb', 'aaa bbb'], // Converts ideographic space to regular space + ['aaa\u3164bbb', 'aaa bbb'], // Converts hangul filler to regular space + ['aaa‐‑‒–—bbb‰‱′″‴', 'aaa‐‑‒–—bbb‰‱′″‴'], // General punctuation + ['ققفقلسخهقسل', 'ققفقلسخهقسل'], // Arabic + ['₠₡₢₣₤₥', '₠₡₢₣₤₥'], // Currency symbols + ['Hi! 근데 왜-호ㅔ', 'Hi! 근데 왜-호ㅔ'], // Hangul + ['sắp sáng rồi', 'sắp sáng rồi'], // Vietnamese + ['דברים נראים כחולים', 'דברים נראים כחולים'], // Hebrew + ['℀℁ℂ℃℄℅℆ℇ℈℉', '℀℁ℂ℃℄℅℆ℇ℈℉'], // Letterlike Symbols + ['🇦🇧🇿', '🇦🇧🇿'], // regional indicators + ['誒ㄟㄝㄍ', '誒ㄟㄝㄍ'], // Bopomofo + ['⌂⌃⌄⌅⌆⌇', '⌂⌃⌄⌅⌆⌇'], // Technical + ['⅐⅑⅒⅓ⅢⅣⅤ', '⅐⅑⅒⅓ⅢⅣⅤ'], // Number Forms + ['︰︱︲︳︴︵︶', '︰︱︲︳︴︵︶'], // CJK Compatibility Forms + ['ஐஜ', 'ஐஜ'], // Tamil + ['߶߷߸߹', '߶߷߸߹'], // NKo + ['ತಥದಠ', 'ತಥದಠ'], // Kannada + ['ԳԴԵԶԷ', 'ԳԴԵԶԷ'], // Armenian + ['Τι συμβαίνει', 'Τι συμβαίνει'], // Greek + ['ႠႡႢႣႤႥ', 'ႠႡႢႣႤႥ'], // Georgian + ['╣╤╥', '╣╤╥'], // Box Drawing + ['🃉🃊🃋🃌🃍🃎🃏', '🃉🃊🃋🃌🃍🃎🃏'], // Playing Cards + ['🀀🀁🀂🀃🀄', '🀀🀁🀂🀃🀄'], // Mahjong Tiles + ['⡳⡣⡤⡥', '⡳⡣⡤⡥'], // Braille Patterns + ['ऒओऔकख', 'ऒओऔकख'], // Devanagari + ['ᐁᐂᐃᐄᐅᐆ', 'ᐁᐂᐃᐄᐅᐆ'], // Unified Canadian Aboriginal Syllabics + ['X\u200eX', 'XX'], // Remove LEFT-TO-RIGHT MARK + ['x\u00ady', 'xy'], // Remove soft hyphen +]; + +describe('clientUtils', () => { + describe('cleanName()', () => { + cleanNameTests.forEach(([value, expected, test]) => it(`cleans '${value}' to '${expected}' (${test})`, () => { + expect(cleanName(value)).equal(expected); + })); + }); + + describe('cleanMessage()', () => { + cleanMessageTests.forEach(([value, expected], i) => it(`cleans '${value}' to '${expected}' (${i})`, () => { + expect(cleanMessage(value)).equal(expected); + })); + }); + + describe('toSocialSiteInfo()', () => { + const provider = { id: 'prov', name: 'prov', color: '#123456' }; + + beforeEach(() => { + oauthProviders.push(provider); + }); + + afterEach(() => { + removeItem(oauthProviders, provider); + }); + + it('returns social site info', () => { + oauthProviders.push(); + + expect(toSocialSiteInfo({ id: 'foo', name: 'Foo', url: 'www.foo.com', provider: 'prov' })).eql({ + id: 'foo', + name: 'Foo', + url: 'www.foo.com', + icon: 'prov', + color: '#123456', + }); + }); + + it('return undefined icon and color for missing provider', () => { + oauthProviders.push(); + + expect(toSocialSiteInfo({ id: 'foo', name: 'Foo', url: 'www.foo.com', provider: 'non-prov' })).eql({ + id: 'foo', + name: 'Foo', + url: 'www.foo.com', + icon: undefined, + color: undefined, + }); + }); + }); + + describe('filterString()', () => { + it('returns empty string for empty string', () => { + expect(filterString('', () => false)).equal(''); + }); + + it('returns empty string for undefined', () => { + expect(filterString(undefined, () => false)).equal(''); + }); + + it('return the same string for no filtered characters', () => { + expect(filterString('hello', () => true)).equal('hello'); + }); + + it('removes filtered characters', () => { + expect(filterString('hello world', x => x !== 'o'.charCodeAt(0))).equal('hell wrld'); + }); + + it('removes all filtered characters', () => { + expect(filterString('hello world', () => false)).equal(''); + }); + + it('removes 4 byte filtered characters', () => { + expect(filterString('hello😺', x => x !== '😺'.codePointAt(0))).equal('hello'); + }); + + it('removes invalid utf-16 characters', () => { + expect(filterString('hello\udb40world', () => true)).equal('helloworld'); + }); + }); + + describe('isSpamMessage()', () => { + it('returns false for no last messages', () => { + expect(isSpamMessage('hello', [])).false; + }); + + it('returns false for mismatching last messages', () => { + expect(isSpamMessage('hello', ['boop'])).false; + }); + + it('returns false for command', () => { + expect(isSpamMessage('/command', ['/command'])).false; + }); + + it('returns true for same last message', () => { + expect(isSpamMessage('hello', ['hello'])).true; + }); + + it('returns true for doubled message', () => { + expect(isSpamMessage('hellohello', ['hello'])).true; + }); + + it('returns true for trippled message', () => { + expect(isSpamMessage('hellohellohello', ['hello'])).true; + }); + + it('returns false for really short doubled message "a"', () => { + expect(isSpamMessage('aa', ['a'])).false; + }); + + it('returns false for really short doubled message "ha"', () => { + expect(isSpamMessage('haha', ['ha'])).false; + }); + + it('returns false for really short doubled message "lol"', () => { + expect(isSpamMessage('lollol', ['lol'])).false; + }); + + it('returns true for multiplied cut to length message message', () => { + const message = repeat(100, 'hello').join('').substr(0, SAY_MAX_LENGTH); + expect(isSpamMessage(message, ['hello'])).true; + }); + + it('returns true for added one character', () => { + expect(isSpamMessage('message!', ['message'])).true; + }); + + it('returns true for added two characters', () => { + expect(isSpamMessage('message!!', ['message'])).true; + }); + + it('returns false for added one character if message is too short', () => { + expect(isSpamMessage('ha!', ['ha'])).false; + }); + + it('returns true for added one character (in prev message)', () => { + expect(isSpamMessage('message', ['message!'])).true; + }); + }); +}); diff --git a/src/ts/tests/client/emoji.spec.ts b/src/ts/tests/client/emoji.spec.ts new file mode 100644 index 0000000..128e859 --- /dev/null +++ b/src/ts/tests/client/emoji.spec.ts @@ -0,0 +1,114 @@ +import '../lib'; +import { expect } from 'chai'; +import { splitEmojis, findEmoji, replaceEmojis, emojis, hasEmojis, autocompleteMesssage } from '../../client/emoji'; + +describe('emotes', () => { + describe('findEmote()', () => { + let appleEmoji: any; + + before(() => { + appleEmoji = emojis.find(e => e.symbol === '🍎'); + expect(appleEmoji).not.undefined; + }); + + it('returns found emoji by symbol', () => { + expect(findEmoji('🍎')).equal(appleEmoji); + }); + + it('returns found emoji by name', () => { + expect(findEmoji('apple')).equal(appleEmoji); + }); + + it('returns undefined if emoji does not exists', () => { + expect(findEmoji('foobar')).undefined; + }); + }); + + describe('replaceEmotes()', () => { + it('replaces emoji names with emojis', () => { + expect(replaceEmojis(':apple:')).equal('🍎'); + }); + + it('works with additional text around emoji', () => { + expect(replaceEmojis('text :apple: hi')).equal('text 🍎 hi'); + }); + + it('works with multiple emojis', () => { + expect(replaceEmojis(':orange: text :apple: hi')).equal('🍊 text 🍎 hi'); + }); + + it('does nothing if does not contain any emoji names', () => { + expect(replaceEmojis('plain text')).equal('plain text'); + }); + + it('does nothing if emojis are already converted', () => { + expect(replaceEmojis('text 🍎 hi')).equal('text 🍎 hi'); + }); + + it('does nothing if emoji name is not found', () => { + expect(replaceEmojis('text :foo: hi')).equal('text :foo: hi'); + }); + + it('returns empty string for undefined', () => { + expect(replaceEmojis(undefined)).equal(''); + }); + }); + + describe('splitEmotes()', () => { + it('returns array for plain text', () => { + expect(splitEmojis('foo bar')).eql(['foo bar']); + }); + + it('returns array for plain text', () => { + expect(splitEmojis('foo 🍎 bar')).eql(['foo ', '🍎', ' bar']); + }); + }); + + describe('hasEmotes()', () => { + it('returns true if contains emotes', () => { + expect(hasEmojis('foo 🍎 bar')).true; + }); + + it('returns false if does not contain emotes', () => { + expect(hasEmojis('foo bar')).false; + }); + }); + + describe('autocompleteMesssage()', () => { + it('does nothing for empty text', () => { + expect(autocompleteMesssage('', false, {})).equal(''); + }); + + it('does nothing for regular text', () => { + const state = {}; + expect(autocompleteMesssage('hello world', false, state)).equal('hello world'); + expect(state).eql({}); + }); + + it('autocompletes an emote', () => { + const state = {}; + expect(autocompleteMesssage('hello :app', false, state)).equal('hello :apple:'); + expect(state).eql({ lastEmoji: ':app' }); + }); + + it('autocompletes considering previous autocomplete', () => { + const state = {}; + expect(autocompleteMesssage('hello :a', false, state)).equal('hello :angry:'); + expect(autocompleteMesssage('hello :angry:', false, state)).equal('hello :apple:'); + expect(state).eql({ lastEmoji: ':a' }); + }); + + it('autocompletes with reversed order', () => { + const state = {}; + expect(autocompleteMesssage('hello :b', false, state)).equal('hello :banana:'); + expect(autocompleteMesssage('hello :bat:', false, state)).equal('hello :black_heart:'); + expect(autocompleteMesssage('hello :black_heart:', true, state)).equal('hello :bat:'); + expect(state).eql({ lastEmoji: ':b' }); + }); + + it('does nothing if does not match any emoji', () => { + const state = { lastEmoji: 'xyz' }; + expect(autocompleteMesssage('hello :abc', false, state)).equal('hello :abc'); + }); + }); +}); diff --git a/src/ts/tests/client/gameLoop.spec.ts b/src/ts/tests/client/gameLoop.spec.ts new file mode 100644 index 0000000..d0894d1 --- /dev/null +++ b/src/ts/tests/client/gameLoop.spec.ts @@ -0,0 +1,103 @@ +import '../lib'; +import { noop } from 'lodash'; +import { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers, SinonStub, stub, assert } from 'sinon'; +import { startGameLoop } from '../../client/gameLoop'; + +describe('gameLoop', () => { + let clock: SinonFakeTimers; + let requestAnimationFrame: SinonStub; + // let cancelAnimationFrame: SinonStub; + + beforeEach(() => { + clock = useFakeTimers(); + (global as any).requestAnimationFrame = requestAnimationFrame = stub(); + (global as any).cancelAnimationFrame = stub(); + }); + + afterEach(() => { + clock.restore(); + }); + + after(() => { + delete (global as any).requestAnimationFrame; + delete (global as any).cancelAnimationFrame; + }); + + describe('startGameLoop()', () => { + it('returns started promise', async () => { + await startGameLoop({ draw: noop, update: noop, init: noop, load: noop, fps: 0 }, noop).started; + }); + + it('loads game', async () => { + const load = stub().resolves(); + + await startGameLoop({ draw: noop, update: noop, init: noop, load, fps: 0 }, noop).started; + + assert.calledOnce(load); + }); + + it('initializes game after load finishes', async () => { + const load = stub(); + const init = stub(); + + await startGameLoop({ draw: noop, update: noop, init, load, fps: 0 }, noop).started; + + assert.calledOnce(load); + assert.calledOnce(init); + assert.callOrder(load, init); + }); + + // it('updates and draws the game', async () => { + // const update = stub(); + // const draw = stub(); + // clock.setSystemTime(0); + + // await startGameLoop({ draw, update, init: noop, load: noop, fps: 0 }, noop).started; + + // clock.setSystemTime(123); + + // assert.calledOnce(requestAnimationFrame); + // requestAnimationFrame.args[0][0](); + + // assert.calledWith(update, 0.123, 123); + // assert.calledOnce(draw); + // assert.callOrder(update, draw); + // }); + + it('updates the game using backup timer if animation frame did not fire for 0.1s', async () => { + const update = stub(); + const draw = stub(); + clock.setSystemTime(0); + + await startGameLoop({ draw, update, init: noop, load: noop, fps: 0 }, noop).started; + + clock.setSystemTime(100); + clock.tick(101); + + assert.calledWith(update, 0.1); + assert.notCalled(draw); + }); + + it('reports error during update', async () => { + const error = new Error('test'); + const update = stub().throws(error); + const onError = stub(); + + await startGameLoop({ draw: noop, update, init: noop, load: noop, fps: 0 }, onError).started; + + assert.calledOnce(requestAnimationFrame); + requestAnimationFrame.args[0][0](123); + + assert.calledWith(onError, error); + }); + + it('throws if cancelled before init', async () => { + const { started, cancel } = startGameLoop({ draw: noop, update: noop, init: noop, load: noop, fps: 0 }, noop); + + cancel(); + + await expect(started).rejectedWith('Cancelled'); + }); + }); +}); diff --git a/src/ts/tests/client/htmlUtils.spec.ts b/src/ts/tests/client/htmlUtils.spec.ts new file mode 100644 index 0000000..93d429c --- /dev/null +++ b/src/ts/tests/client/htmlUtils.spec.ts @@ -0,0 +1,222 @@ +import '../lib'; +import { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import { + textNode, element, removeAllNodes, removeFirstChild, createHtmlNodes, replaceNodes, findParentElement +} from '../../client/htmlUtils'; +import { removeItem } from '../../common/utils'; + +class MockHTMLElement { + nodeType = 'element'; + children: any[] = []; + events: any = {}; + attributes: any = {}; + className?: string; + parentElement?: MockHTMLElement; + constructor(public tagName: string) { } + get firstChild() { + return this.children[0]; + } + get lastChild() { + return this.children[this.children.length - 1]; + } + setAttribute(key: string, value: any) { + this.attributes[key] = value; + } + addEventListener(key: string, callback: any) { + this.events[key] = callback; + } + appendChild(node: any) { + this.children.push(node); + } + removeChild(node: any) { + removeItem(this.children, node); + } +} + +class MockTextNode { + constructor(public nodeValue: string) { } +} + +describe('htmlUtils', () => { + let querySelectorAll: SinonStub; + + beforeEach(() => { + querySelectorAll = stub(); + + (global as any).document = { + createElement(tagName: string) { + return new MockHTMLElement(tagName); + }, + createTextNode(value: string) { + return new MockTextNode(value); + }, + querySelectorAll, + }; + }); + + afterEach(() => { + delete (global as any).document; + }); + + describe('createHtmlNodes()', () => { + it('creates single text node with given text', () => { + const nodes = createHtmlNodes('foo bar', 1); + + expect(nodes.length).equal(1); + expect(nodes[0]).instanceof(MockTextNode); + expect(nodes[0].nodeValue).equal('foo bar'); + }); + + it('creates empty array for empty text', () => { + const nodes = createHtmlNodes('', 1); + + expect(nodes.length).equal(0); + }); + }); + + describe('textNode()', () => { + it('creates new text node', () => { + const node = textNode('foo'); + + expect(node).instanceof(MockTextNode); + expect(node.nodeValue).equal('foo'); + }); + }); + + describe('element()', () => { + it('creates new HTML element', () => { + const div = element('div', 'foo-bar'); + + expect(div).instanceof(MockHTMLElement); + expect(div.tagName).equal('div'); + }); + + it('sets class name', () => { + const div = element('div', 'foo-bar'); + + expect(div.className).equal('foo-bar'); + }); + + it('sets attribute values', () => { + const div = element('div', undefined, undefined, { foo: 'bar', test: 'boo' }); + + expect(div.attributes).eql({ foo: 'bar', test: 'boo' }); + }); + + it('sets event listeners', () => { + const click = () => { }; + const touch = () => { }; + + const div: any = element('div', undefined, undefined, undefined, { click, touch }); + + expect(div.events.click).equal(click); + expect(div.events.touch).equal(touch); + }); + + it('appends child nodes', () => { + const a = element('a'); + const b = element('b'); + + const div = element('div', undefined, [a, b]); + + expect(div.children[0]).equal(a); + expect(div.children[1]).equal(b); + }); + + it('skips undefined nodes when appending child nodes', () => { + const a = element('a'); + const b = element('b'); + + const div = element('div', undefined, [a, undefined, b]); + + expect(div.children[0]).equal(a); + expect(div.children[1]).equal(b); + }); + }); + + describe('removeAllNodes()', () => { + it('removes all child nodes from element', () => { + const div = element('div', undefined, [element('a'), element('b')]); + + removeAllNodes(div as any); + + expect(div.children).eql([]); + }); + + it('does nothing for no child nodes', () => { + const div = element('div', undefined, []); + + removeAllNodes(div as any); + + expect(div.children).eql([]); + }); + }); + + describe('removeFirstChild()', () => { + it('removes all child nodes from element', () => { + const a = element('a'); + const b = element('b'); + const div = element('div', undefined, [a, b]); + + removeFirstChild(div as any); + + expect(div.children.length).equal(1); + expect(div.children[0]).equal(b); + }); + + it('does nothing for no child nodes', () => { + const div = element('div', undefined, []); + + removeFirstChild(div as any); + + expect(div.children).eql([]); + }); + }); + + describe('replaceNodes()', () => { + it('adds new child nodes', () => { + const div = element('div', undefined, []); + + replaceNodes(div as any, 'foo bar'); + + expect(div.children.length).equal(1); + expect(div.children[0]).instanceof(MockTextNode); + expect(div.children[0].nodeValue).equal('foo bar'); + }); + + it('removes all but first node', () => { + const a = textNode('a'); + const b = textNode('b'); + const div = element('div', undefined, [a, b]); + + replaceNodes(div as any, 'foo bar'); + + expect(div.children.length).equal(1); + expect(div.children[0]).equal(a); + expect(div.children[0].nodeValue).equal('foo bar'); + }); + }); + + describe('findParentElement()', () => { + it('returns matched parent element', () => { + const div1 = element('div'); + const div2 = element('div'); + (div1 as any).parentElement = div2; + querySelectorAll.withArgs('foo > bar').returns([div2]); + + expect(findParentElement(div1, 'foo > bar')).equal(div2); + }); + + it('returns matched parent element', () => { + const div1 = element('div'); + const div2 = element('div'); + const div3 = element('div'); + (div1 as any).parentElement = div2; + (div2 as any).parentElement = div3; + querySelectorAll.withArgs('foo > bar').returns([div3]); + + expect(findParentElement(div1, 'foo > bar')).equal(div3); + }); + }); +}); diff --git a/src/ts/tests/client/paletteManager.spec.ts b/src/ts/tests/client/paletteManager.spec.ts new file mode 100644 index 0000000..d634489 --- /dev/null +++ b/src/ts/tests/client/paletteManager.spec.ts @@ -0,0 +1,110 @@ +import '../lib'; +import { expect } from 'chai'; +import { Palette } from '../../common/interfaces'; +import { releasePalette, PaletteManager } from '../../graphics/paletteManager'; + +describe('PaletteManager', () => { + describe('releasePalette()', () => { + it('reduces ref count on palette', () => { + const palette: Palette = { x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0), refs: 5 }; + + releasePalette(palette); + + expect(palette.refs).equal(4); + }); + + it('does not reduce ref count below 0', () => { + const palette: Palette = { x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0), refs: 0 }; + + releasePalette(palette); + + expect(palette.refs).equal(0); + }); + + it('does nothing if refs are undefined', () => { + const palette = {} as any; + + releasePalette(palette); + + expect(palette.refs).undefined; + }); + + it('does nothing if palette is undefined', () => { + releasePalette(undefined); + }); + }); + + describe('PaletteManager', () => { + let paletteManager: PaletteManager; + + beforeEach(() => { + paletteManager = new PaletteManager(); + }); + + after(() => { + paletteManager = undefined as any; + }); + + describe('.add()', () => { + it('returns new palette', () => { + expect(paletteManager.add([1, 2, 3])) + .eql({ x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array([1, 2, 3]), refs: 1 }); + }); + + it('returns existing palette for the same colors', () => { + const first = paletteManager.add([1, 2, 3]); + + expect(paletteManager.add([1, 2, 3])).equal(first); + }); + + it('increments ref count for existing palette', () => { + paletteManager.add([1, 2, 3]); + + expect(paletteManager.add([1, 2, 3]).refs).equal(2); + }); + + it('returns new palette for the different colors', () => { + const first = paletteManager.add([1, 2, 3]); + + expect(paletteManager.add([5, 6, 7])).not.equal(first); + }); + + it('converts all colors to unsigned integers', () => { + expect(paletteManager.add([-1, 2.5, 'bleh' as any]).colors) + .eql(new Uint32Array([4294967295, 2, 0])); + }); + }); + + describe('.addArray()', () => { + it('returns new palette', () => { + expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))) + .eql({ x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array([1, 2, 3]), refs: 1 }); + }); + + it('returns existing palette for the same colors', () => { + const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); + + expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))).equal(first); + }); + + it('returns new palette palette for the same colors if deduplication is turned off', () => { + const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); + paletteManager.deduplicate = false; + + expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))).not.equal(first); + }); + + it('increments ref count for existing palette', () => { + paletteManager.addArray(new Uint32Array([1, 2, 3])); + + expect(paletteManager.addArray(new Uint32Array([1, 2, 3])).refs).equal(2); + }); + + it('returns new palette for the different colors', () => { + const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); + + expect(paletteManager.addArray(new Uint32Array([5, 6, 7]))).not.equal(first); + }); + }); + }); +}); diff --git a/src/ts/tests/client/partyUtils.spec.ts b/src/ts/tests/client/partyUtils.spec.ts new file mode 100644 index 0000000..6b793b2 --- /dev/null +++ b/src/ts/tests/client/partyUtils.spec.ts @@ -0,0 +1,193 @@ +import '../lib'; +import { expect } from 'chai'; +import { PartyInfo, PartyMember } from '../../common/interfaces'; +import { updateParty, isPonyInParty, isPartyLeader, isInParty } from '../../client/partyUtils'; +import { PonyTownGame } from '../../client/game'; + +describe('partyUtils', () => { + describe('updateParty()', () => { + it('creates new party', () => { + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ]; + + const party = updateParty(undefined, info); + + expect(party).eql({ + leaderId: 2, + members: info, + }); + }); + + it('adds new members', () => { + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + ], + }; + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ]; + + updateParty(party, info); + + expect(party).eql({ + leaderId: 2, + members: info, + }); + }); + + it('removes members', () => { + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ], + }; + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: true, pending: false, offline: false }, + ]; + + updateParty(party, info); + + expect(party).eql({ + leaderId: 1, + members: info, + }); + }); + + it('updates members', () => { + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: true, offline: true }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ], + }; + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: true, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: true, leader: false, pending: false, offline: false }, + ]; + + updateParty(party, info); + + expect(party).eql({ + leaderId: 1, + members: info, + }); + }); + + it('does nothing for undefined/empty party and info', () => { + expect(updateParty(undefined, undefined)).undefined; + expect(updateParty(undefined, [])).undefined; + }); + }); + + describe('isPonyInParty()', () => { + it('returns false for undefined party', () => { + expect(isPonyInParty(undefined, {} as any, false)).false; + }); + + it('returns true if pony is in party', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: pony, self: false, leader: false, pending: false, offline: true }, + ], + }; + + expect(isPonyInParty(party, pony, false)).true; + }); + + it('returns false if pony is in party but pending', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: pony, self: false, leader: false, pending: true, offline: true }, + ], + }; + + expect(isPonyInParty(party, pony, false)).false; + }); + + it('returns true if pony is in party and pending, but pending flag is true', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: pony, self: false, leader: false, pending: true, offline: true }, + ], + }; + + expect(isPonyInParty(party, pony, true)).true; + }); + + it('returns false if pony is not in party', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'boo' } as any, self: false, leader: false, pending: false, offline: true }, + ], + }; + + expect(isPonyInParty(party, pony, false)).false; + }); + }); + + describe('isPartyLeader()', () => { + let game: PonyTownGame; + + beforeEach(() => { + game = {} as any; + }); + + it('returns true if player is party leader', () => { + game.player = { id: 123 } as any; + game.party = { leaderId: 123, members: [] }; + + expect(isPartyLeader(game)).true; + }); + + it('returns false if player is not party leader', () => { + game.player = { id: 123 } as any; + game.party = { leaderId: 321, members: [] }; + + expect(isPartyLeader(game)).false; + }); + + it('returns false if player is not initialized', () => { + expect(isPartyLeader(game)).false; + }); + }); + + describe('isInParty()', () => { + let game: PonyTownGame; + + beforeEach(() => { + game = {} as any; + }); + + it('returns true if player is in party', () => { + game.party = { leaderId: 0, members: [{ id: 123 } as any] }; + + expect(isInParty(game)).true; + }); + + it('returns false if player is not in a party', () => { + expect(isInParty(game)).false; + }); + + it('returns false if party is empty', () => { + game.party = { leaderId: 0, members: [] }; + + expect(isInParty(game)).false; + }); + }); +}); diff --git a/src/ts/tests/client/ponyDraw.spec.ts b/src/ts/tests/client/ponyDraw.spec.ts new file mode 100644 index 0000000..bd42ec5 --- /dev/null +++ b/src/ts/tests/client/ponyDraw.spec.ts @@ -0,0 +1,153 @@ +/* tslint:disable:max-line-length */ +import { compareCanvases, loadSprites, loadImageAsCanvas, clearCompareResults } from '../lib'; +import * as path from 'path'; +import { TRANSPARENT, blushColor, WHITE } from '../../common/colors'; +import { defaultPonyState, defaultDrawPonyOptions } from '../../client/ponyHelpers'; +import { mockPaletteManager, createDefaultPony } from '../../common/ponyInfo'; +import { PonyState, PalettePonyInfo, PonyInfo, DrawPonyOptions, NoDraw } from '../../common/interfaces'; +import { sit, trot, sitDown, laugh, fly } from '../../client/ponyAnimations'; +import { drawCanvas } from '../../graphics/contextSpriteBatch'; +import { parseExpression } from '../../common/expressionUtils'; +import { compressPonyString, decodePonyInfo } from '../../common/compressPony'; +import { apple, jackoLanternOn, letter } from '../../common/entities'; +import { candy, paletteSpriteSheet } from '../../generated/sprites'; +import { drawPony } from '../../client/ponyDraw'; +import { pathTo } from '../../server/paths'; +import { createCanvas } from '../../server/canvasUtilsNode'; + +const baseFilePath = pathTo('src', 'tests', 'pony'); + +function createTests(): [string, PonyState, DrawPonyOptions, string][] { + const state = defaultPonyState(); + const sitting: PonyState = { ...state, animation: sit, animationFrame: 0 }; + const sittingDown: PonyState = { ...state, animation: sitDown, animationFrame: 0 }; + const wingOpen: PonyState = { ...state, animation: fly, animationFrame: 0 }; + // const lying: PonyState = { ...state, animation: lie, animationFrame: 0 }; + const trotting: PonyState = { ...state, animation: trot, animationFrame: 10 }; + const blushing: PonyState = { ...state, expression: parseExpression('o//o') }; + //const turned: PonyState = { ...defaultState, headTurned: true }; + const holding: PonyState = { ...state, holding: apple(0, 0) }; + const holdingLantern: PonyState = { ...state, holding: jackoLanternOn(0, 0) }; + const holdingLetter: PonyState = { ...state, holding: letter(0, 0) }; + const laughing: PonyState = { ...state, headAnimation: laugh }; + const blinking: PonyState = { ...state, blinkFrame: 4 }; + const blinkingAngry: PonyState = { ...state, expression: parseExpression('|B)'), blinkFrame: 5 }; + const blinkingSkip: PonyState = { ...state, expression: parseExpression('|B)'), blinkFrame: 2 }; + const faceExtra: PonyState = { + ...state, + drawFaceExtra: batch => batch.drawSprite(candy.color, WHITE, mockPaletteManager.addArray(candy.palettes![0]), 25, 40), + }; + const faceExtraHoldingLetter: PonyState = { ...faceExtra, holding: letter(0, 0) }; + + const options: DrawPonyOptions = { ...defaultDrawPonyOptions(), shadow: true }; + const flipped: DrawPonyOptions = { ...options, flipped: true }; + const selected: DrawPonyOptions = { ...options, selected: true }; + const extra: DrawPonyOptions = { ...options, extra: true }; + + const base = 'CAP////apSDaICA2QAJpDhAvAAwAIA=='; + const baseWing = 'CAP////apSDaICA2QAJpIhAvAAwAIQgIAA=='; + const whitePetal = 'CAiidXWCfnZaUERMTEzb29vl5eXj4diysrI2oIIAAhiBVgCfgAYAGoAOEBpAYQFCAJCAkwAoQgDIgB4AG4AgACQgJAQgJCAAIA=='; + const cmSocks = 'CAdZp7LapSBFjaJtZDXm5uZWVlb85VlWsACAAIE5CADhAE/AAwAOgBIQGIBhAQCEaBdddn8f8AcDgcAf8fg='; + const griffon = 'CAamno3apSD/1wDdrljp6ellX1I2QCZkJcAQKbcIBJAG8AtoDiBDUYBowLIgGEBA'; + + return [ + ['none.png', state, options, ''], + ['base.png', state, options, base], + ['selected.png', state, selected, base], + ['offline.png', state, options, 'CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='], + ['deer.png', state, options, 'CA18ZlPTh1Gfj3ofGRiqaUe9f12wnWHIw7Q0NDTcxl+ccU7S0YX///82wQIAAgTiBAAHWAJ+ICpCAwjVFZmiGquzYEgJnQAQhAJhAQiElWEQpZsA3vamBgAAAGBgAAAGBg=='], + ['all.png', state, options, 'CFTTzbqagEXaTSAgftqjmpqYj4Bi2iAA/88Ad///APWUjn0AmRgAs38AZrOzAKyyqYxWR0dzaU00ISHdVlZ9Jib/5VgekP8yzTLacNbcFDx//9Sqkg8AWrMCkAKZOJWaAB8+s4uzmxukr5BTcxl2r6g+jdkkcGcQQXBS/wBevD++0EifszfAwMBMTEyYmJgaGhro8pDWu1Tugu6nRKeVhW9nS0NPT09SUlJoWkZILyc3Nzc5OTn/pQDWmy68mFWpl3a4r55XOwhlShttWjVzZk+Jg3kzIwUsIAowJhIsJRonJiPV1dWysrKQkJBvb2//////9OH/6sP/04L/xl54bAkEgihOBBAwgAKGrAEABDiBIoAsYNHD4gIQIhInAAFBWpqAWLmDJo2AcOnj6BDENALFzBk0bAOHTx9AhpaALFzBk0BEdPH0EIAjJIgIlTCdRCAKSqEAVlkgoFi5gIjp6gqAtLmFxo2AvOnmCBDMMAxLmDJoDI6ePoJQIALgA7GdAZlzBk0bAaHTx9AhhGgMy5gyaNgNDp4+gQwjQGZcwZNGwGh08fQIYioDMuYMmjYDQ6ePoEMIMGrZu4By6dvIygGZcwZNAaHTx9BANAZlzBk0bAaHTx9Ahy9fP4EGFDiRY0eRJlS5k2dPb0KNKnUA'], + ['dragon.png', state, options, 'CAeokpLapSBgYGBSUlJqamqAcHBEREQ2oAIAIhkJEAT8ADAA3gDsAcgQMqICbgA='], + ['paws.png', state, options, 'CAa8q3/apiDapSD/1wCIXCjc0K82oCYAApkJUAQLkkADCA2gFtAbwIaRAGiAQA=='], + ['paws-socks.png', state, options, 'CAe8q3/apiDapSD/1wCIXCjc0K9GRkZWqAmAAKZChAEC5JAAwgNoBbQG8CGkQBogEIQDwgHA'], + ['glasses.png', state, options, 'CAPW0bTapSBZQSw2QAJpOgCfgAYAGIA4QGEBASAAAA=='], + ['griffon.png', state, options, griffon], + ['griffon-lantern.png', holdingLantern, options, griffon], + ['griffon-socks.png', state, options, 'CAemno3apSD/1wDdrljp6elf/8ZlX1JWkAmZChAECm3CASQBvQl1oDiiBuRgHjAsiAYQESgGhAMA'], + ['sit.png', sitting, options, base], + ['sit-wing.png', sitting, options, baseWing], + ['sit-socks.png', sitting, options, cmSocks], + ['sit-socks-cm.png', { ...sittingDown, animationFrame: 8 }, options, cmSocks], + ['sit-fetlocks.png', sitting, options, whitePetal], + ['sit-paws.png', sitting, options, 'CAmmno3apSD/1wDdrljp6ellX1JQODDb29v///82QCZiBVgCBIzOEASQBvAFtAcQIKowBowKkQDCAgEQoPEQDdZgAA=='], + ['sit-sword.png', sitting, options, 'CAf////apSDaICCVhW9nS0NPT09SUlI2QAJkKcIFcADAAgACIGEu4A=='], + ['sit-neck-accessory.png', sitting, options, 'CAeH5o7VplTH8K4yvVL/36cbGxtXV1c2wIIAIALkIAN8AT8ADAA1AEhAcgQLkIChAHCAMIhIq/DbYb5DZA=='], + ['trot.png', trotting, options, whitePetal], + ['holding.png', holding, options, whitePetal], + ['laughing.png', laughing, options, whitePetal], + ['blinking.png', blinking, options, whitePetal], + ['blinking-angry.png', blinkingAngry, options, whitePetal], + ['blinking-skip.png', blinkingSkip, options, whitePetal], + ['face-extra.png', faceExtra, options, whitePetal], + ['face-extra-holding-letter.png', faceExtraHoldingLetter, options, whitePetal], + ['cm-flip.png', state, flipped, 'CAeCQpnapSD/1wD////ugu7/pQD/4at2pAAmQoQBPwAMADEAOEBhAQEIBEI16y43AA3AA3A3A3A='], + ['freckles-flip.png', state, flipped, 'CA3/AADapSAnJyf/1wCK8f/////u7u5U3OkyzTKVhW9nS0NPT09SUlI2wAIAAAbiBAAHOAJ+ABgAYgBIQGEBAJhIrEQoPExgwq80AA=='], + //['head-turned.png', turned, options, whitePetal], + ['extra.png', state, extra, 'CAWVlZW5ubn///9SUlIvLy82QIxkI0IEcgAYAGIAsIDCA4AAFAoootFFoFA='], + ['blush.png', blushing, options, base], + ['hat-horns.png', state, options, 'CAfMoYvPWVnapSD/1wA8PDzj0M3uVVU2QAJkJkAQLkkADAA8AFhAYQGcAwTAHAA='], + ['hat-bald.png', state, options, 'CAbi38/PWVnapSD/1wBwZmbuVVU2oJoAApkJkAQLkkQFsACcABBEAYA='], + ['holding-letter.png', holdingLetter, options, 'CAPHY2MghtoA/202QAJpLgCfgAYAGIA7QGEBCIBA'], + ['mustache.png', state, options, 'CATexazapSD/1wC8UCw2QAJkJcAQKbcADAAwAEAjAQA='], + ['hair-in-front-of-wing.png', state, options, 'CAOMx+LapSD04HY2QAJpIgCfgAYAGIA6AGEBQgIA'], + ['hair-behind-neck-accessory.png', state, options, 'CAb///9xcXHaICDbQEBra2t+fn42oAIAAhkJ8IFcADAA5AEqAcgQNgBYAYA='], + // TEMP: gryphon wing pattern ['lie.png', lying, options, 'CAb///9xcXHaICDHx8eqqqq9vb02QAJkJEIFcADAAwgEnAcgQNiMS4A='], + ['no-body.png', state, { ...options, no: NoDraw.Body }, baseWing], + ['wing-open.png', wingOpen, options, griffon], + ['sitting-with-skirt-and-socks.png', sitting, options, 'DAb////CwsL/pQAjVdk9ySPugu42QAJkKsAT8ADAAnAASAAcICBCASIAqIA0ACA='], + ['cape.png', state, options, 'DASmlJTX19fHwJxbVVU2QAJkKkAT8ADAAxADhAYQEABCAQA='], + // ['no-behind.png', state, { ...options, noBehind: true }, baseWing], + // ['no-behind-leg.png', state, { ...options, noBehindLeg: true }, baseWing], + // ['no-behind-body.png', state, { ...options, noBehindBody: true }, baseWing], + // ['no-front.png', state, { ...options, noFront: true }, baseWing], + ]; +} + +function createOtherTests(): [string, string, Partial, PonyState, DrawPonyOptions][] { + const state = defaultPonyState(); + const options: DrawPonyOptions = { ...defaultDrawPonyOptions(), shadow: true }; + + return [ + ['eyes-0.png', `doesn't allow 0 value for eyes`, { eyeOpennessLeft: 0, eyeOpennessRight: 0, lockEyes: false }, state, options], + ]; +} + +describe('ponyUtils', () => { + before(loadSprites); + before(() => clearCompareResults('pony')); + + describe('drawPony()', () => { + createTests().forEach(([file, state, options, data]) => it(`correct for ${file}`, () => { + const filePath = path.join(baseFilePath, file); + const expected = loadImageAsCanvas(filePath); + const info = decodePonyInfo(data, mockPaletteManager); + const actual = drawPonyCanvas(TRANSPARENT, info, state, options); + compareCanvases(expected, actual, filePath, 'pony'); + })); + + createOtherTests().forEach(([file, name, data, state, options]) => it(name, () => { + const filePath = path.join(baseFilePath, file); + const expected = loadImageAsCanvas(filePath); + const ponyInfo = { ...createDefaultPony(), ...data }; + const compressed = compressPonyString(ponyInfo); + const info = decodePonyInfo(compressed, mockPaletteManager); + const actual = drawPonyCanvas(TRANSPARENT, info, state, options); + compareCanvases(expected, actual, filePath, 'pony'); + })); + }); +}); + +function drawPonyCanvas(bg: number, info: PalettePonyInfo, state: PonyState, options: DrawPonyOptions) { + state.blushColor = blushColor(info.coatPalette.colors[1]); + const canvas = drawCanvas(80, 80, paletteSpriteSheet, bg, batch => drawPony(batch, info, state, 40, 70, options)); + + if (options.flipped) { + const flipped = createCanvas(canvas.width, canvas.height); + const context = flipped.getContext('2d')!; + context.scale(-1, 1); + context.drawImage(canvas, -canvas.width, 0); + return flipped; + } else { + return canvas; + } +} diff --git a/src/ts/tests/client/ponyStates.spec.ts b/src/ts/tests/client/ponyStates.spec.ts new file mode 100644 index 0000000..35db6da --- /dev/null +++ b/src/ts/tests/client/ponyStates.spec.ts @@ -0,0 +1,121 @@ +import '../lib'; +import { expect } from 'chai'; +import { + trotting, flying, standing, lying, sitting, hovering, toBoopState, booping, boopingSitting, + boopingLying, boopingFlying, sittingDown, isFlyingUp, flyingUp, trottingToFlying, flyingToTrotting, + flyingDown, isFlyingUpOrDown, isFlyingDown +} from '../../client/ponyStates'; +import { EntityState } from '../../common/interfaces'; +import { flagsToState } from '../../common/pony'; + +describe('ponyStates', () => { + describe('flagsToState()', () => { + it('returns trotting if moving', () => { + expect(flagsToState(EntityState.PonyStanding, true, false)).equal(trotting); + }); + + it('returns flying if moving and flying', () => { + expect(flagsToState(EntityState.PonyFlying, true, false)).equal(flying); + }); + + it('returns standing for standing state', () => { + expect(flagsToState(EntityState.PonyStanding, false, false)).equal(standing); + }); + + it('returns trotting for walking state', () => { + expect(flagsToState(EntityState.PonyWalking, false, false)).equal(trotting); + }); + + it('returns trotting for trotting state', () => { + expect(flagsToState(EntityState.PonyTrotting, false, false)).equal(trotting); + }); + + it('returns sitting for sitting state', () => { + expect(flagsToState(EntityState.PonySitting, false, false)).equal(sitting); + }); + + it('returns lying for lying state', () => { + expect(flagsToState(EntityState.PonyLying, false, false)).equal(lying); + }); + + it('returns hovering for flying state', () => { + expect(flagsToState(EntityState.PonyFlying, false, false)).equal(hovering); + }); + + it('throws on invalid state', () => { + expect(() => flagsToState(112, false, false)).throw('Invalid pony state (112)'); + }); + }); + + describe('isFlyingUp()', () => { + it('returns true if flying up', () => { + expect(isFlyingUp(flyingUp)).true; + }); + + it('returns true if transitioning from trotting to flying', () => { + expect(isFlyingUp(trottingToFlying)).true; + }); + + it('returns false for any other state', () => { + expect(isFlyingUp(standing)).false; + }); + }); + + describe('isFlyingDown()', () => { + it('returns true if flying down', () => { + expect(isFlyingDown(flyingDown)).true; + }); + + it('returns true if transitioning from flying to trotting', () => { + expect(isFlyingDown(flyingToTrotting)).true; + }); + + it('returns false for any other state', () => { + expect(isFlyingDown(standing)).false; + }); + }); + + describe('isFlyingUpOrDown()', () => { + it('returns true if flying up', () => { + expect(isFlyingUpOrDown(flyingUp)).true; + }); + + it('returns true if transitioning from trotting to flying', () => { + expect(isFlyingUpOrDown(trottingToFlying)).true; + }); + + it('returns true if flying down', () => { + expect(isFlyingUpOrDown(flyingDown)).true; + }); + + it('returns true if transitioning from flying to trotting', () => { + expect(isFlyingUpOrDown(flyingToTrotting)).true; + }); + + it('returns false for any other state', () => { + expect(isFlyingUpOrDown(standing)).false; + }); + }); + + describe('toBoopState()', () => { + it('returns booping for standing state', () => { + expect(toBoopState(standing)).equal(booping); + }); + + it('returns boopingSitting for sitting state', () => { + expect(toBoopState(sitting)).equal(boopingSitting); + }); + + it('returns boopingLying for lying state', () => { + expect(toBoopState(lying)).equal(boopingLying); + }); + + it('returns boopingFlying for hovering state', () => { + expect(toBoopState(hovering)).equal(boopingFlying); + }); + + it('returns undefined for all other states', () => { + expect(toBoopState(sittingDown)).undefined; + }); + }); +}); diff --git a/src/ts/tests/client/spriteFont.spec.ts b/src/ts/tests/client/spriteFont.spec.ts new file mode 100644 index 0000000..b9a8663 --- /dev/null +++ b/src/ts/tests/client/spriteFont.spec.ts @@ -0,0 +1,67 @@ +import { compareCanvases, loadSprites, loadImageAsCanvas, clearCompareResults } from '../lib'; +import { expect } from 'chai'; +import { WHITE, BLACK } from '../../common/colors'; +import { fontPal } from '../../client/fonts'; +import { drawCanvas, ContextSpriteBatch } from '../../graphics/contextSpriteBatch'; +import { pathTo } from '../../server/paths'; +import { paletteSpriteSheet } from '../../generated/sprites'; +import { lineBreak, drawText } from '../../graphics/spriteFont'; +import { commonPalettes } from '../../graphics/graphicsUtils'; + +const tests = [ + ['Tiny Pony Face!', 'ascii.png'], + ['New lines\nanother line', 'newlines.png'], + ['ŚŃĄjGiýŽžĹ弾Ŕŕ', 'special.png'], + ['👃🙂😵😠😐', 'tiny.png'], + ['АаБбВвГг', 'russian.png'], + ['ΑΒΓΔΕΖΗΘ', 'greek.png'], + ['ぁあぃいぅうぇえ', 'hiragana.png'], + ['ァアィイゥウェエ', 'katakana.png'], + ['漢字', 'kanji.png'], + ['ABCDEFabcdef', 'romaji.png'], + ['emoji: 💚🍎🌠🎲', 'emoji.png'], + ['◠◡◯◰▤▥▦●', 'shapes.png'], + ['abcde\ufe0efg\ufe0fhij', 'variants.png'], + ['‒⁇;⁈⁉“_”.,`', 'punctuation.png'], + ['口古句另叨 龈龋龍龟', 'chinese.png'], +]; + +describe('SpriteFont', () => { + before(loadSprites); + before(() => clearCompareResults('font')); + + describe('drawText()', () => { + const width = 100; + const height = 30; + + function test(file: string, draw: (batch: ContextSpriteBatch) => void) { + const filePath = pathTo('src', 'tests', 'font', file); + const expected = loadImageAsCanvas(filePath); + const actual = drawCanvas(width, height, paletteSpriteSheet, WHITE, draw); + compareCanvases(expected, actual, filePath, 'font'); + } + + tests.forEach(([text, file]) => it(`correct for "${text}" (${file})`, () => { + test(file, batch => drawText(batch, text, fontPal, BLACK, 5, 5, { + palette: commonPalettes.mainFont.white, emojiPalette: commonPalettes.mainFont.emoji + })); + })); + + describe('lineBreak()', () => { + it('does not break short text', () => { + const text = lineBreak('hello world', fontPal, 90); + expect(text).equal('hello world'); + }); + + it('breaks into multiple lines', () => { + const text = lineBreak('this text is too long to fit', fontPal, 90); + expect(text).equal('this text is too\nlong to fit'); + }); + + it('does not break single word', () => { + const text = lineBreak('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', fontPal, 90); + expect(text).equal('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); + }); + }); + }); +}); diff --git a/src/ts/tests/client/webglUtils.spec.ts b/src/ts/tests/client/webglUtils.spec.ts new file mode 100644 index 0000000..6e5f61e --- /dev/null +++ b/src/ts/tests/client/webglUtils.spec.ts @@ -0,0 +1,49 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub } from 'sinon'; +import { getRenderTargetSize, isWebGL2, getWebGLContext } from '../../graphics/webgl/webglUtils'; +import { WEBGL_CREATION_ERROR } from '../../common/errors'; + +describe('webglUtils', () => { + describe('getRenderTargetSize()', () => { + it('returns correct size for 150x200', () => { + expect(getRenderTargetSize(150, 200)).equal(256); + }); + + it('returns correct size for 256x512', () => { + expect(getRenderTargetSize(256, 512)).equal(512); + }); + }); + + describe('getWebGLContext()', () => { + it('gets weblg2 context', () => { + const context = {} as any; + + expect(getWebGLContext({ getContext: stub().withArgs('webgl2').returns(context) } as any)).equal(context); + }); + + it('falls back to weblg context', () => { + const context = {} as any; + + expect(getWebGLContext({ getContext: stub().withArgs('webgl').returns(context) } as any)).equal(context); + }); + + it('throws if context is not returned', () => { + expect(() => getWebGLContext({ getContext: stub().returns(undefined) } as any)).throw(WEBGL_CREATION_ERROR); + }); + }); + + describe('isWebGL2()', () => { + it('returns true for webgl 2 context', () => { + expect(isWebGL2({ MAX_ELEMENT_INDEX: 1 } as any)).true; + }); + + it('returns false for webgl 1 context', () => { + expect(isWebGL2({} as any)).false; + }); + + it('returns false for undefined', () => { + expect(isWebGL2(undefined)).false; + }); + }); +}); diff --git a/src/ts/tests/client/worldMap.spec.ts b/src/ts/tests/client/worldMap.spec.ts new file mode 100644 index 0000000..1fdee5f --- /dev/null +++ b/src/ts/tests/client/worldMap.spec.ts @@ -0,0 +1,244 @@ +import '../lib'; +import { Subject } from 'rxjs'; +import { expect } from 'chai'; +import { useFakeTimers, SinonFakeTimers } from 'sinon'; +import { getRegion, addEntity, setRegion, createWorldMap } from '../../common/worldMap'; +import { PonyTownGame } from '../../client/game'; +import { MessageType, Entity, Pony, TileType, EntityState, DecodedUpdate } from '../../common/interfaces'; +import { entity, mock } from '../mocks'; +import { rect } from '../../common/rect'; +import { pony, apple } from '../../common/entities'; +import { createAnimator } from '../../common/animator'; +import { defaultPonyState } from '../../client/ponyHelpers'; +import { createRegion } from '../../common/region'; +import { createCamera } from '../../common/camera'; +import { chatAnimationDuration } from '../../graphics/graphicsUtils'; +import { handleUpdateEntity, handleSays } from '../../client/handlers'; + +describe('worldMap', () => { + describe('updateEntityInternal()', () => { + let game: PonyTownGame; + const def: DecodedUpdate = { + id: 0, x: 0, y: 0, vx: 0, vy: 0, state: 0, expression: 0, playerState: 0, switchRegion: false, + options: undefined, name: undefined, info: undefined, crc: undefined, type: undefined, action: undefined, + filterName: false, + }; + + beforeEach(() => { + game = {} as any; + game.map = createWorldMap({ type: 0, flags: 0, regionsX: 2, regionsY: 1, defaultTile: 0 }); + setRegion(game.map, 0, 0, createRegion(0, 0)); + setRegion(game.map, 1, 0, createRegion(1, 0)); + game.onActionsUpdate = new Subject(); + }); + + it('updates x, y, vx, vy', () => { + const e = entity(123, 0, 0, 2); + addEntity(game.map, e); + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, state: 123, expression: 234 }); + + expect(e.x).equal(2); + expect(e.y).equal(3); + expect(e.vx).equal(4); + expect(e.vy).equal(5); + }); + + it('switches regions', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); + + handleUpdateEntity(game, { ...def, id: 123, x: 12, y: 3, vx: 4, vy: 5, switchRegion: true }); + + expect(getRegion(game.map, 1, 0)!.entities).includes(e); + }); + + it('does not update x, y, vx, vy for player', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); + game.playerId = e.id; + game.player = e as any; + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + + expect(e.x).equal(0); + expect(e.y).equal(0); + expect(e.vx).equal(0); + expect(e.vy).equal(0); + }); + + it('updates flags', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, state: 123 }); + + expect(e.state).equal(123); + }); + + it('updates expression', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.expr = 0; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, expression: 234 }); + + expect(e.expr).equal(234); + }); + + it('overrides right flag for player', () => { + const player = entity(123, 0, 0, 1); + game.player = player as any; + game.player!.ponyState = defaultPonyState(); + game.rightOverride = true; + addEntity(game.map, player); + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + + expect(player.state).equal(EntityState.FacingRight); + }); + + it('overrides headTurned flag for player', () => { + const player = entity(123, 0, 0, 1); + game.player = player as any; + game.player!.ponyState = defaultPonyState(); + game.headTurnedOverride = true; + addEntity(game.map, player); + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + + expect(player.state).equal(EntityState.HeadTurned); + }); + + it('overrides sitting flag for player', () => { + const player = entity(123, 0, 0, 1); + game.player = player as any; + game.player!.ponyState = defaultPonyState(); + game.stateOverride = EntityState.PonySitting; + addEntity(game.map, player); + + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + + expect(player.state).equal(EntityState.PonySitting); + }); + }); + + describe('handleSays()', () => { + let e: Entity; + let clock: SinonFakeTimers; + let game: PonyTownGame; + + beforeEach(() => { + game = mock(PonyTownGame); + e = entity(1, 1, 1, 2, { bounds: rect(0, 0, 10, 10) }); + (e as Pony).animator = createAnimator(); + (e as Pony).ponyState = defaultPonyState(); + game.map = createWorldMap({ type: 0, flags: 0, regionsX: 1, regionsY: 1, defaultTile: TileType.None }); + setRegion(game.map, 0, 0, createRegion(0, 0)); + addEntity(game.map, e); + game.camera = createCamera(); + game.settings = { account: {} } as any; + game.messageQueue = []; + game.findEntityFromChatLog = () => undefined; + (game as any).model = { friends: [] }; + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('adds says object to entity', () => { + handleSays(game, 1, 'test', MessageType.Chat); + + expect(e.says).eql({ message: 'test', timer: 5.1875, total: 5.1875, type: MessageType.Chat, created: 0 }); + }); + + // it('does nothing if entity is not on the map', () => { + // game.map.removeEntity(1); + + // handleSays(game, 1, 'test', MessageType.Chat); + + // expect(e.says).undefined; + // }); + + it('does nothing if entity is not visible', () => { + e.x = 1000; + + handleSays(game, 1, 'test', MessageType.Chat); + + expect(e.says).undefined; + }); + + // it('does nothing if entity is ignored', () => { + // e.playerState = setFlag(e.playerState, EntityPlayerState.Ignored, true); + + // handleSays(game, 1, 'test', MessageType.Chat); + + // expect(e.says).undefined; + // }); + + it('emits chat message', () => { + e.name = 'foo'; + e.type = pony.type; + e.crc = 123; + + handleSays(game, 1, 'test', MessageType.Chat); + + expect(game.messageQueue).eql([ + { id: 1, name: 'foo', crc: 123, message: 'test', type: MessageType.Chat }, + ]); + }); + + it('emits chat message for party message even if entity is not visible', () => { + e.name = 'foo'; + e.type = pony.type; + e.x = 1000; + e.crc = 123; + + handleSays(game, 1, 'test', MessageType.Party); + + expect(game.messageQueue).eql([ + { id: 1, name: 'foo', crc: 123, message: 'test', type: MessageType.Party }, + ]); + }); + + it('does nothing if entity is not visible', () => { + e.type = pony.type; + e.x = 1000; + + handleSays(game, 1, 'test', MessageType.Chat); + + expect(game.messageQueue).eql([]); + }); + + it('does not call game.onMessage for non-pony entities', () => { + e.type = apple.type; + + handleSays(game, 1, 'test', MessageType.Chat); + + expect(game.messageQueue).eql([]); + }); + + it('does not call game.onMessage for "." message', () => { + e.type = pony.type; + + handleSays(game, 1, '.', MessageType.Chat); + + expect(game.messageQueue).eql([]); + }); + + it('dismisses previous message for "." message', () => { + e.type = pony.type; + handleSays(game, 1, 'test', MessageType.Chat); + + handleSays(game, 1, '.', MessageType.Chat); + + expect(e.says!.timer).equal(chatAnimationDuration); + }); + }); +}); diff --git a/src/ts/tests/common/accountUtils.spec.ts b/src/ts/tests/common/accountUtils.spec.ts new file mode 100644 index 0000000..4295751 --- /dev/null +++ b/src/ts/tests/common/accountUtils.spec.ts @@ -0,0 +1,153 @@ +import '../lib'; +import { expect } from 'chai'; +import { isAdmin, isMod, isDev, meetsRequirement, getSupporterInviteLimit } from '../../common/accountUtils'; +import { account } from '../mocks'; +import { SupporterFlags } from '../../common/adminInterfaces'; + +describe('accountUtils [client]', () => { + describe('isAdmin()', () => { + it('returns true if target account has admin role', () => { + expect(isAdmin(account({ roles: ['admin'] }))).true; + }); + + it('returns true if target account has superadmin role', () => { + expect(isAdmin(account({ roles: ['superadmin'] }))).true; + }); + + it('returns false if target account has no roles', () => { + expect(isAdmin(account({}))).false; + }); + + it('returns false if target account has no admin or superadmin roles', () => { + expect(isAdmin(account({ roles: ['foo'] }))).false; + }); + }); + + describe('isMod()', () => { + it('returns true if target account has mod role', () => { + expect(isMod(account({ roles: ['mod'] }))).true; + }); + + it('returns true if target account has admin role', () => { + expect(isMod(account({ roles: ['admin'] }))).true; + }); + + it('returns true if target account has superadmin role', () => { + expect(isMod(account({ roles: ['superadmin'] }))).true; + }); + + it('returns false if target account has no roles', () => { + expect(isMod(account({}))).false; + }); + + it('returns false if target account has no admin or superadmin roles', () => { + expect(isMod(account({ roles: ['foo'] }))).false; + }); + }); + + describe('isDev()', () => { + it('returns true if target account has dev role', () => { + expect(isDev(account({ roles: ['dev'] }))).true; + }); + + it('returns false if target account has no roles', () => { + expect(isDev(account({}))).false; + }); + + it('returns false if target account has no dev role', () => { + expect(isDev(account({ roles: ['foo'] }))).false; + }); + }); + + describe('meetsRequirement()', () => { + it('returns true for undefined requirement', () => { + expect(meetsRequirement({}, undefined)).true; + }); + + it('returns true for empty requirement', () => { + expect(meetsRequirement({}, '')).true; + }); + + it('returns true if requirement matches role', () => { + expect(meetsRequirement({ roles: ['mod'] }, 'mod')).true; + }); + + it('returns true if matches supporter 1 requirement', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'sup1')).true; + }); + + it('returns true if matches supporter 2 requirement', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter2 }, 'sup2')).true; + }); + + it('returns true if matches supporter 3 requirement', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter3 }, 'sup3')).true; + }); + + it('returns false if supporter is lower level', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'sup2')).false; + }); + + it('returns true if requires supporter but is mod', () => { + expect(meetsRequirement({ roles: ['mod'] }, 'sup2')).true; + }); + + it('returns true if requires supporter but is dev', () => { + expect(meetsRequirement({ roles: ['dev'] }, 'sup2')).true; + }); + + it('returns false if supporter is undefined', () => { + expect(meetsRequirement({}, 'sup2')).false; + }); + + it('returns false if requirement is not met', () => { + expect(meetsRequirement({}, 'mod')).false; + }); + + it('returns false if sup2 requirement is not met', () => { + expect(meetsRequirement({ supporter: 0 }, 'sup2')).false; + }); + + it('returns false if inv requirement is not met', () => { + expect(meetsRequirement({}, 'inv')).false; + }); + + it('returns true if inv requirement is met (supporter)', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'inv')).true; + }); + + it('returns true if inv requirement is met (role)', () => { + expect(meetsRequirement({ roles: ['dev'] }, 'inv')).true; + }); + + it('returns true if inv requirement is met (invited)', () => { + expect(meetsRequirement({ supporterInvited: true }, 'inv')).true; + }); + }); + + describe('getSupporterInviteLimit()', () => { + it('returns 100 for mod', () => { + expect(getSupporterInviteLimit({ roles: ['mod'] })).equal(100); + }); + + it('returns 100 for dev', () => { + expect(getSupporterInviteLimit({ roles: ['dev'] })).equal(100); + }); + + it('returns 1 for supporter level 1', () => { + expect(getSupporterInviteLimit({ supporter: 1 })).equal(1); + }); + + it('returns 5 for supporter level 2', () => { + expect(getSupporterInviteLimit({ supporter: 2 })).equal(5); + }); + + it('returns 10 for supporter level 3', () => { + expect(getSupporterInviteLimit({ supporter: 3 })).equal(10); + }); + + it('returns 0 otherwise', () => { + expect(getSupporterInviteLimit({})).equal(0); + }); + }); +}); diff --git a/src/ts/tests/common/adminUtils.spec.ts b/src/ts/tests/common/adminUtils.spec.ts new file mode 100644 index 0000000..1fc039f --- /dev/null +++ b/src/ts/tests/common/adminUtils.spec.ts @@ -0,0 +1,125 @@ +import '../lib'; +import { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers } from 'sinon'; +import { SupporterFlags, PatreonFlags } from '../../common/adminInterfaces'; +import { supporterLevel, isMuted, isActive, pushOrdered, compareByName } from '../../common/adminUtils'; +import { account } from '../mocks'; + +describe('adminUtils', () => { + describe('pushOrdered()', () => { + it('pushes one item to empty array', () => { + const items: ({ name: string; })[] = []; + + pushOrdered(items, { name: 'foo' }, compareByName); + + expect(items).eql([{ name: 'foo' }]); + }); + + it('pushes item after existing one', () => { + const items = [{ name: 'a' }]; + + pushOrdered(items, { name: 'b' }, compareByName); + + expect(items).eql([{ name: 'a' }, { name: 'b' }]); + }); + + it('pushes item before existing one', () => { + const items = [{ name: 'b' }]; + + pushOrdered(items, { name: 'a' }, compareByName); + + expect(items).eql([{ name: 'a' }, { name: 'b' }]); + }); + + it('pushes item between existing ones', () => { + const items = [{ name: 'a' }, { name: 'c' }]; + + pushOrdered(items, { name: 'b' }, compareByName); + + expect(items).eql([{ name: 'a' }, { name: 'b' }, { name: 'c' }]); + }); + }); + + describe('supporterLevel()', () => { + it('returns 1 if is supporter on patreon', () => { + expect(supporterLevel(account({ patreon: PatreonFlags.Supporter1 }))).equal(1); + }); + + it('returns 1 if has supporter flag', () => { + expect(supporterLevel(account({ supporter: SupporterFlags.Supporter1 }))).equal(1); + }); + + it('returns max level if has supporter flag and patreon', () => { + expect(supporterLevel(account({ + patreon: PatreonFlags.Supporter3, + supporter: SupporterFlags.Supporter1 + }))).equal(3); + }); + + it('returns 0 if patreon info is empty', () => { + expect(supporterLevel(account({}))).equal(0); + }); + + it('returns 0 if has patreon info but has ignore flag set', () => { + expect(supporterLevel(account({ + patreon: PatreonFlags.Supporter1, + supporter: SupporterFlags.IgnorePatreon, + }))).equal(0); + }); + }); + + describe('isMuted()', () => { + it('returns true if account has muted flag', () => { + expect(isMuted(account({ mute: -1 }))).true; + }); + + it('returns true if account has timeout after current date', () => { + expect(isMuted(account({ mute: Date.now() + 10000 }))).true; + }); + + it('returns false if account has timeout before current date', () => { + expect(isMuted(account({ mute: Date.now() - 10000 }))).false; + }); + + it('returns false if account has not timeout, mute or shadow', () => { + expect(isMuted(account({}))).false; + }); + }); + + describe('isActive()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + clock = undefined as any; + }); + + it('returns false for undefined', () => { + expect(isActive(undefined)).false; + }); + + it('returns false for 0', () => { + expect(isActive(0)).false; + }); + + it('returns true for -1', () => { + expect(isActive(-1)).true; + }); + + it('returns true for time larger than current time', () => { + clock.setSystemTime(2000); + + expect(isActive(3000)).true; + }); + + it('returns false for time smaller than current time', () => { + clock.setSystemTime(3000); + + expect(isActive(2000)).false; + }); + }); +}); diff --git a/src/ts/tests/common/animator.spec.ts b/src/ts/tests/common/animator.spec.ts new file mode 100644 index 0000000..f16edd3 --- /dev/null +++ b/src/ts/tests/common/animator.spec.ts @@ -0,0 +1,384 @@ +import '../lib'; +import { expect } from 'chai'; +import { + Animator, animatorState, anyState, animatorTransition, getAnimation, getAnimationFrame, setAnimatorState, + updateAnimator, createAnimator +} from '../../common/animator'; + +const sit = { name: 'sit', fps: 6, loop: true, frames: { length: 12 } as any }; +const sit2 = { name: 'sit2', fps: 6, loop: true, frames: { length: 12 } as any }; +const stand = { name: 'stand', fps: 6, loop: true, frames: { length: 6 } as any }; +const run = { name: 'run', fps: 6, loop: true, frames: { length: 6 } as any }; + +describe('Animator', () => { + let animator: Animator; + + beforeEach(() => { + animator = createAnimator(); + }); + + after(() => { + animator = undefined as any; + }); + + it('returns undefined animation by default', () => { + expect(getAnimation(animator)).undefined; + }); + + it('returns 0 frame by default', () => { + expect(getAnimationFrame(animator)).equal(0); + }); + + it('sets initial state', () => { + const sitting = animatorState('sitting', sit); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + + expect(animator.state).equal(sitting); + expect(getAnimation(animator)).equal(sit); + }); + + it('does not switch to new state immediately', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(sit); + + setAnimatorState(animator, running); + + expect(getAnimation(animator)).equal(sit); + }); + + it('updates animation frame', () => { + const sitting = animatorState('sitting', sit); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + updateAnimator(animator, 1); + + expect(getAnimationFrame(animator)).equal(6); + }); + + it('loops animation frame', () => { + const state = animatorState('sitting', sit); + + setAnimatorState(animator, state); + updateAnimator(animator, 0); + updateAnimator(animator, 3); + + expect(getAnimationFrame(animator)).equal(6); + }); + + it('switches to next state immediately if exitAfter is set to 0', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('switches to next state after half of the animation if exitAfter is set 0.5', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0.5 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + setAnimatorState(animator, running); + updateAnimator(animator, 0); + updateAnimator(animator, 0.1); + expect(getAnimation(animator)).equal(sit, 'after 0.1s'); + updateAnimator(animator, 0.4); + expect(getAnimation(animator)).equal(sit, 'after 0.5s'); + updateAnimator(animator, 0.6); + expect(getAnimation(animator)).equal(run, 'after 1.1s'); + }); + + it('switches to next state immediately if exitAfter is set 0.5 and time is already past 0.5', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0.5 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + updateAnimator(animator, 1.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0); + expect(getAnimation(animator)).equal(run); + }); + + it('switches to middle of next state if enterTime is set to 0.5', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0, enterTime: 0.5 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + setAnimatorState(animator, running); + updateAnimator(animator, 0); + + expect(getAnimation(animator)).equal(run); + expect(getAnimationFrame(animator)).equal(3); + }); + + it('keeps time of animation after switching if keepTime is set', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0, keepTime: true }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + updateAnimator(animator, 0.5); + + expect(getAnimationFrame(animator)).equal(3, 'before'); + + setAnimatorState(animator, running); + updateAnimator(animator, 0); + + expect(getAnimation(animator)).equal(run); + expect(getAnimationFrame(animator)).equal(3, 'after'); + }); + + it('switches to state if set to switch from any', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('does not use switch from any state as intermediate transition', () => { + const lying = animatorState('lying', sit); + const sitting = animatorState('sitting', sit); + const standing = animatorState('standing', sit); + const flying = animatorState('flying', sit); + const hovering = animatorState('hovering', sit); + animatorTransition(lying, sitting); + animatorTransition(sitting, standing); + animatorTransition(anyState, flying); + animatorTransition(flying, hovering); + animatorTransition(standing, hovering); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, hovering); + updateAnimator(animator, 3); + expect(animator.state).equal(sitting); + updateAnimator(animator, 3); + expect(animator.state).equal(standing); + updateAnimator(animator, 3); + expect(animator.state).equal(hovering); + }); + + it('does not use switch from any state is another transition is possible', () => { + const lying = animatorState('lying', sit); + const standing = animatorState('standing', sit); + const hovering = animatorState('hovering', sit); + animatorTransition(lying, standing); + animatorTransition(anyState, hovering); + animatorTransition(standing, hovering); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, hovering); + updateAnimator(animator, 3); + expect(animator.state).equal(standing); + updateAnimator(animator, 3); + expect(animator.state).equal(hovering); + }); + + it('uses switch from any state is another transition is possible but longer than 2 jumps', () => { + const lying = animatorState('lying', sit); + const sitting = animatorState('sitting', sit); + const standing = animatorState('standing', sit); + const hovering = animatorState('hovering', sit); + animatorTransition(lying, sitting); + animatorTransition(sitting, standing); + animatorTransition(anyState, hovering); + animatorTransition(standing, hovering); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, hovering); + updateAnimator(animator, 3); + expect(animator.state).equal(hovering); + }); + + it('does not switch to next state immediately if exitNow is not set', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(sit); + }); + + it('switches to next state when animation is finished', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(run); + expect(getAnimationFrame(animator)).equal(0); + }); + + it('does nothing if switching to already targeted state', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(run); + }); + + it('does nothing if transition is no possible', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(sit); + }); + + it('switches through intermediate state', () => { + const sitting = animatorState('sitting', sit); + const other = animatorState('other', sit); + const standing = animatorState('standing', stand); + const running = animatorState('running', run); + animatorTransition(sitting, standing); + animatorTransition(other, running); + animatorTransition(standing, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(stand); + + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(run); + }); + + it('switches directly to target state if exitNow is set on both states', () => { + const sitting = animatorState('sitting', sit); + const standing = animatorState('standing', stand); + const running = animatorState('running', run); + animatorTransition(sitting, standing, { exitAfter: 0 }); + animatorTransition(standing, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('cancells switch to target state', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + setAnimatorState(animator, sitting); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(sit); + }); + + it('handles loops in states', () => { + const sitting = animatorState('sitting', sit); + const standing0 = animatorState('standing0', sit); + const standing1 = animatorState('standing1', stand); + const standing2 = animatorState('standing2', stand); + const running = animatorState('running', run); + animatorTransition(sitting, standing0, { exitAfter: 0 }); + animatorTransition(running, standing1, { exitAfter: 0 }); + animatorTransition(running, standing2, { exitAfter: 0 }); + animatorTransition(standing0, standing2, { exitAfter: 0 }); + animatorTransition(standing1, running, { exitAfter: 0 }); + animatorTransition(standing2, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('returns correct animation variant', () => { + const sitting = animatorState('sitting', sit, { alt: sit2 }); + + animator.variant = 'alt'; + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(sit2); + }); + + it('does not use shorter transition route if onlyDirectTo is set', () => { + const standing = animatorState('standing', run); + const standingUp = animatorState('standing-up', run); + const sitting = animatorState('sitting', run); + const sittingUp = animatorState('sitting-up', run); + const lying = animatorState('lying', run); + const lyingToTrotting = animatorState('lying-to-trotting', run); + const trotting = animatorState('trotting', run); + animatorTransition(lying, sittingUp); + animatorTransition(sittingUp, sitting); + animatorTransition(sitting, standingUp); + animatorTransition(standingUp, standing); + animatorTransition(lying, lyingToTrotting, { onlyDirectTo: trotting }); + animatorTransition(lyingToTrotting, standing); + animatorTransition(lyingToTrotting, trotting); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, standing); + updateAnimator(animator, 1.1); + expect(animator.state).equal(sittingUp); + updateAnimator(animator, 1.1); + expect(animator.state).equal(sitting); + updateAnimator(animator, 1.1); + expect(animator.state).equal(standingUp); + updateAnimator(animator, 1.1); + expect(animator.state).equal(standing); + }); +}); diff --git a/src/ts/tests/common/bitUtils.spec.ts b/src/ts/tests/common/bitUtils.spec.ts new file mode 100644 index 0000000..d713bd9 --- /dev/null +++ b/src/ts/tests/common/bitUtils.spec.ts @@ -0,0 +1,198 @@ +import '../lib'; +import { expect } from 'chai'; +import { map, range, random } from 'lodash'; +import { bitWriter, bitReader, numberToBitCount, countBits } from '../../common/bitUtils'; + +function toArray(buffer: Uint8Array): number[] { + return map(buffer, x => x); +} + +describe('numberToBitCount()', () => { + it('returns 0 for 0', () => { + expect(numberToBitCount(0)).equal(0); + }); + + it('returns 1 for 1', () => { + expect(numberToBitCount(1)).equal(1); + }); + + it('returns 3 for 7', () => { + expect(numberToBitCount(7)).equal(3); + }); + + it('returns 4 for 8', () => { + expect(numberToBitCount(8)).equal(4); + }); + + it('returns 16 for 0xffff', () => { + expect(numberToBitCount(0xffff)).equal(16); + }); + + it('returns 32 for 0xffffffff', () => { + expect(numberToBitCount(0xffffffff)).equal(32); + }); + + it('returns 32 for -1', () => { + expect(numberToBitCount(0xffffffff)).equal(32); + }); +}); + +describe('countBits()', () => { + it('returns 0 for 0', () => { + expect(countBits(0)).equal(0); + }); + + it('returns 1 for 1', () => { + expect(countBits(1)).equal(1); + }); + + it('returns 16 for 0x55555555', () => { + expect(countBits(0x55555555)).equal(16); + }); + + it('returns 16 for 0xffff', () => { + expect(countBits(0xffff)).equal(16); + }); + + it('returns 32 for 0xffffffff', () => { + expect(countBits(0xffffffff)).equal(32); + }); + + it('returns 32 for -1', () => { + expect(countBits(-1)).equal(32); + }); +}); + +describe('bitWriter', () => { + it('writes 1 bit', () => { + const buffer = bitWriter(write => write(1, 1)); + expect(toArray(buffer)).eql([0x80]); + }); + + it('writes 8 bits', () => { + const buffer = bitWriter(write => write(123, 8)); + expect(toArray(buffer)).eql([123]); + }); + + it('writes 32 bits', () => { + const buffer = bitWriter(write => write(0xaabbccdd, 32)); + expect(toArray(buffer)).eql([0xaa, 0xbb, 0xcc, 0xdd]); + }); + + it('writes multiple values', () => { + const buffer = bitWriter(write => { + write(1, 1); + write(3, 2); + write(1, 1); + }); + expect(toArray(buffer)).eql([0xf0]); + }); + + it('writes across bytes', () => { + const buffer = bitWriter(write => { + write(1, 4); + write(1, 8); + write(1, 4); + }); + expect(toArray(buffer)).eql([0x10, 0x11]); + }); + + it('writes a lot of values', () => { + const values = range(0, 200).map(() => random(0, 255)); + const buffer = bitWriter(write => values.forEach(value => write(value, 8))); + expect(toArray(buffer)).eql(values); + }); + + it('trims values that do not fit into given amount of bits', () => { + const buffer = bitWriter(write => { + write(0xff, 4); + write(0, 4); + }); + expect(toArray(buffer)).eql([0xf0]); + }); + + it('throws for incorrect bit count', () => { + bitWriter(write => expect(() => write(0, 33)).throw('Invalid bit count')); + }); + + it('throws for incorrect bit count', () => { + bitWriter(write => expect(() => write(0, -1)).throw('Invalid bit count')); + }); +}); + +describe('bitReader', () => { + it('reads 1 bit', () => { + const read = bitReader(new Uint8Array([0x80])); + expect(read(1)).equal(1); + }); + + it('reads 8 bits', () => { + const read = bitReader(new Uint8Array([123])); + expect(read(8)).equal(123); + }); + + it('reads 32 bits', () => { + const read = bitReader(new Uint8Array([0x0a, 0xbb, 0xcc, 0xdd])); + expect(read(32)).equal(0x0abbccdd); + }); + + it('reads always unsigned', () => { + const read = bitReader(new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd])); + expect(read(32)).equal(0xaabbccdd); + }); + + it('reads multiple values', () => { + const read = bitReader(new Uint8Array([0xf0])); + expect(read(1)).equal(1); + expect(read(2)).equal(3); + expect(read(1)).equal(1); + }); + + it('reads multiple bytes', () => { + const read = bitReader(new Uint8Array([0x01, 0x01])); + expect(read(8)).equal(1); + expect(read(8)).equal(1); + }); + + it('reads across bytes', () => { + const read = bitReader(new Uint8Array([0x10, 0x11])); + expect(read(4)).equal(1); + expect(read(8)).equal(1); + expect(read(4)).equal(1); + }); + + it('reads a lot of values', () => { + const values = range(0, 200).map(() => random(0, 255)); + const read = bitReader(new Uint8Array(values)); + values.forEach(value => expect(read(8)).equal(value)); + }); + + it('throws for incorrect bit count', () => { + const read = bitReader(new Uint8Array(1)); + expect(() => read(33)).throw('Invalid bit count'); + }); + + it('throws for incorrect bit count', () => { + const read = bitReader(new Uint8Array(1)); + expect(() => read(-1)).throw('Invalid bit count'); + }); + + it('throws for reading past end', () => { + const read = bitReader(new Uint8Array(1)); + read(8); + expect(() => read(1)).throw('Reading past end'); + }); +}); + +describe('bitWriter + bitReader', () => { + const tests = [ + [[1, 1, 1, 1], [7, 5, 3, 1]], + ]; + + tests.forEach(([values, bits]) => it(`should work for ${JSON.stringify([values, bits])}`, () => { + const buffer = bitWriter(write => bits.forEach((b, i) => write(values[i], b))); + const read = bitReader(buffer); + const result = bits.map(b => read(b)); + expect(result).eql(values); + })); +}); diff --git a/src/ts/tests/common/camera.spec.ts b/src/ts/tests/common/camera.spec.ts new file mode 100644 index 0000000..6135f8b --- /dev/null +++ b/src/ts/tests/common/camera.spec.ts @@ -0,0 +1,172 @@ +import '../lib'; +import { expect } from 'chai'; +import { + updateCamera, centerCameraOn, isWorldPointVisible, isEntityVisible, isAreaVisible, + isRectVisible, screenToWorld, worldToScreen, createCamera +} from '../../common/camera'; +import { rect } from '../../common/rect'; +import { entity } from '../mocks'; +import { toScreenX, toScreenY } from '../../common/positionUtils'; + +describe('Camera', () => { + describe('updateCamera()', () => { + it('should not move camera if already in view', () => { + const camera = createCamera(); + camera.w = 512; + camera.h = 480; + camera.x = 1344; + camera.actualY = camera.y = 960; + + updateCamera(camera, { x: 50, y: 50 }, { width: 100, height: 100 }); + + expect(camera.x).equal(1344, 'x'); + expect(camera.y).equal(960, 'y'); + }); + + it('should move camera if player is not in view', () => { + const camera = createCamera(); + camera.w = 512; + camera.h = 480; + camera.x = 1344; + camera.actualY = camera.y = 960; + + updateCamera(camera, { x: 75, y: 75 }, { width: 100, height: 100 }); + + expect(camera.x).equal(2067, 'x'); + expect(camera.y).equal(1463, 'y'); + }); + + it('does not restrict camera position if camera is not near the edge of map', () => { + const camera = createCamera(); + camera.w = toScreenX(50); + camera.h = toScreenY(50); + camera.x = toScreenX(25); + camera.actualY = camera.y = toScreenY(25); + + updateCamera(camera, { x: 50, y: 50 }, { width: 100, height: 100 }); + + expect(camera.x).equal(toScreenX(25), 'x'); + expect(camera.y).equal(toScreenY(25), 'y'); + }); + + it('restricts camera position to top left edge of map', () => { + const camera = createCamera(); + camera.w = toScreenX(50); + camera.h = toScreenY(50); + + updateCamera(camera, { x: 0, y: 0 }, { width: 100, height: 100 }); + + expect(camera.x).equal(0, 'x'); + expect(camera.y).equal(0, 'y'); + }); + + it('restricts camera position to bottom right edge of map', () => { + const camera = createCamera(); + camera.w = toScreenX(50); + camera.h = toScreenY(50); + + updateCamera(camera, { x: 100, y: 100 }, { width: 100, height: 100 }); + + expect(camera.x).equal(toScreenX(50), 'x'); + expect(camera.y).equal(toScreenY(50), 'y'); + }); + + it('centers map in view if map is smaller than camera view', () => { + const camera = createCamera(); + camera.w = toScreenX(100); + camera.h = toScreenY(100); + + updateCamera(camera, { x: 25, y: 25 }, { width: 50, height: 50 }); + + expect(camera.x).equal(toScreenX(-25), 'x'); + expect(camera.y).equal(toScreenY(-25), 'y'); + }); + }); + + describe('centerCameraOn()', () => { + it('centers camera on point', () => { + const camera = createCamera(); + + centerCameraOn(camera, { x: 0, y: 0 }); + + expect(camera.x).equal(-50, 'x'); + expect(camera.y).equal(-75, 'y'); + }); + }); + + describe('isWorldPointVisible()', () => { + it('returns true if entity position is in view of camera', () => { + expect(isWorldPointVisible(createCamera(), entity(0, 1, 1))).true; + }); + + it('returns false if entity position is out of view of camera', () => { + expect(isWorldPointVisible(createCamera(), entity(0, 10, 10))).false; + }); + }); + + describe('isEntityVisible()', () => { + it('returns true if entity bounds are in view of camera', () => { + expect(isEntityVisible(createCamera(), entity(0, 1, 1, 1, { bounds: rect(0, 0, 0.1, 0.1) }))).true; + }); + + it('returns false if entity bounds are out of view of camera', () => { + expect(isEntityVisible(createCamera(), entity(0, 10, 10, 1, { bounds: rect(0, 0, 0.1, 0.1) }))).false; + }); + }); + + describe('isAreaVisible()', () => { + it('returns true if rectangle intersects camera view', () => { + const camera = createCamera(); + + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + + expect(isAreaVisible(camera, 50, 50, 100, 100)).true; + }); + + it('returns false if rectangle does not intersect camera view', () => { + const camera = createCamera(); + + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + + expect(isAreaVisible(camera, 101, 101, 100, 100)).false; + }); + }); + + describe('isRectVisible()', () => { + it('returns true if rectangle intersects camera view', () => { + const camera = createCamera(); + + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + + expect(isRectVisible(camera, rect(50, 50, 100, 100))).true; + }); + + it('returns false if rectangle does not intersect camera view', () => { + const camera = createCamera(); + + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + + expect(isRectVisible(camera, rect(101, 101, 100, 100))).false; + }); + }); + + describe('screenToWorld()', () => { + it('maps position from screen to world coordinates', () => { + const camera = createCamera(); + camera.x = 32; + camera.actualY = camera.y = 24; + + expect(screenToWorld(camera, { x: 64, y: 48 })).eql({ x: 3, y: 3 }); + }); + }); + + describe('worldToScreen()', () => { + it('maps position from world to screen coordinates', () => { + const camera = createCamera(); + camera.x = 32; + camera.actualY = camera.y = 24; + + expect(worldToScreen(camera, { x: 3, y: 3 })).eql({ x: 64, y: 48 }); + }); + }); +}); diff --git a/src/ts/tests/common/collision.spec.ts b/src/ts/tests/common/collision.spec.ts new file mode 100644 index 0000000..6290da7 --- /dev/null +++ b/src/ts/tests/common/collision.spec.ts @@ -0,0 +1,148 @@ +import '../lib'; +import { expect } from 'chai'; +import { createServerMap } from '../../server/serverMap'; +import { TileType, IMap, EntityFlags, Entity, Collider } from '../../common/interfaces'; +import { entity, setupCollider } from '../mocks'; +import { updatePosition } from '../../common/collision'; +import { ServerRegion } from '../../server/serverInterfaces'; +import { PONY_TYPE, tileWidth, tileHeight } from '../../common/constants'; +import { generateRegionCollider } from '../../common/region'; +import { ponyColliders } from '../../common/mixins'; +import { updateTileIndices } from '../../client/tileUtils'; + +export function colliders(x: number, y: number, w: number, h: number, tall = true, exact = false): Collider[] { + return [{ x, y, w, h, tall, exact }]; +} + +export function updateColliders(map: IMap) { + for (const region of map.regions) { + updateTileIndices(region, map); + } + + for (const region of map.regions) { + generateRegionCollider(region, map); + } +} + +describe('collision', () => { + describe('updatePosition()', () => { + let map: IMap; + let ent: Entity; + + beforeEach(() => { + map = createServerMap('', 0, 10, 10, TileType.Dirt); + ent = entity(1, 0, 0, PONY_TYPE); + ent.colliders = colliders(0, 0, tileWidth, tileHeight); + }); + + it('does not update position if not moving', () => { + updatePosition(ent, 1, map); + + expect(ent.x).equal(0); + expect(ent.y).equal(0); + }); + + it('updates position if moving', () => { + ent.vx = 2; + ent.vy = 1; + + updatePosition(ent, 1, map); + + expect(ent.x).equal(2); + expect(ent.y).equal(1); + }); + + it('updates position if moving and not colliding', () => { + ent.vx = 2; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + + updatePosition(ent, 1, map); + + expect(ent.x).equal(2); + expect(ent.y).equal(1); + }); + + it('clips move if colliding', () => { + setupCollider(map, 9, 1); + ent.x = 8; + ent.y = 1; + ent.vx = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = colliders(-12, -9, 24, 18); + updateColliders(map); + + updatePosition(ent, 1, map); + + expect(ent.x).equal(8.124969482421875); + }); + + it('clips move Y if colliding in Y direction', () => { + setupCollider(map, 2, 9); + setupCollider(map, 1, 9); + ent.x = 1; + ent.y = 8; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = ponyColliders; + updateColliders(map); + + updatePosition(ent, 1, map); + + expect(ent.x).equal(2, 'x'); + expect(ent.y).equal(8.333292643229166, 'y'); + }); + + it('clips move X if colliding in X direction', () => { + setupCollider(map, 10, 1); + ent.x = 9; + ent.y = 1; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = ponyColliders; + updateColliders(map); + + updatePosition(ent, 1, map); + + expect(ent.x).equal(9.624969482421875, 'x'); + expect(ent.y).equal(2.0833333333333335, 'y'); + }); + + it('updates position if moving and colliding but already in colliding position', () => { + setupCollider(map, 8, 8); + setupCollider(map, 9, 9); + setupCollider(map, 8, 9); + setupCollider(map, 9, 8); + ent.x = 8; + ent.y = 8; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = colliders(-16, -12, 32, 24); + updateColliders(map); + + updatePosition(ent, 1, map); + + expect(ent.x).equal(9); + expect(ent.y).equal(9); + }); + + it('does not update position if moving, colliding, already in colliding position but going outside the map', () => { + setupCollider(map, 11, 11); + ent.x = 110; + ent.y = 110; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = colliders(-16, -12, 32, 24); + updateColliders(map); + + updatePosition(ent, 1, map); + + expect(ent.x).equal(110); + expect(ent.y).equal(110); + }); + }); +}); diff --git a/src/ts/tests/common/color.spec.ts b/src/ts/tests/common/color.spec.ts new file mode 100644 index 0000000..b1258b8 --- /dev/null +++ b/src/ts/tests/common/color.spec.ts @@ -0,0 +1,259 @@ +import '../lib'; +import { expect } from 'chai'; +import { + getR, getG, getB, getAlpha, colorToRGBA, colorToHSVA, colorToCSS, colorToHexRGB, colorToFloatArray, + colorToFloat, colorFromRGBA, colorFromHSVA, colorFromHSVAObject, parseColorFast, parseColor, + parseColorWithAlpha, makeTransparent, lerpColors, h2rgb, hsv2rgb, rgb2hsv, colorToFloatAlpha, + multiplyColor, withAlphaFloat, withAlpha +} from '../../common/color'; + +describe('color', () => { + describe('getR()', () => { + it('returns red color value', () => { + expect(getR(0x12345678)).equals(0x12); + }); + }); + + describe('getG()', () => { + it('returns green color value', () => { + expect(getG(0x12345678)).equals(0x34); + }); + }); + + describe('getB()', () => { + it('returns blue color value', () => { + expect(getB(0x12345678)).equals(0x56); + }); + }); + + describe('getAlpha()', () => { + it('returns alpha color value', () => { + expect(getAlpha(0x12345678)).equals(0x78); + }); + }); + + describe('colorToRGBA()', () => { + it('splits color into r, g, b, a values', () => { + expect(colorToRGBA(0x12345678)).eql({ + r: 0x12, + g: 0x34, + b: 0x56, + a: 0x78, + }); + }); + }); + + describe('colorToHSVA()', () => { + it('converts color to HSV values', () => { + expect(colorToHSVA(0xffd500ff)).eql({ + h: 50.11764705882353, + s: 1, + v: 1, + a: 1, + }); + }); + }); + + describe('colorToCSS()', () => { + it('converts opaque color to hex code', () => { + expect(colorToCSS(0x123456ff)).equal('#123456'); + }); + + it('converts transparent color to rgba(...) code', () => { + expect(colorToCSS(0x123456cc)).equal('rgba(18,52,86,0.8)'); + }); + }); + + describe('colorToHexRGB()', () => { + it('returns RGB hex code', () => { + expect(colorToHexRGB(0x123456ff)).equal('123456'); + }); + }); + + describe('colorToFloatArray()', () => { + it('returns values of r, g, b, a as array of floats', () => { + expect(Array.from(colorToFloatArray(0x123456ff))) + .eql([0.07058823853731155, 0.20392157137393951, 0.33725491166114807, 1]); + }); + }); + + describe('colorToFloat()', () => { + it('returns float value of color', () => { + expect(colorToFloat(0x123456ff)).equal(-7.118128890449717e+37); + }); + }); + + describe('colorToFloatAlpha()', () => { + it('returns float value of color', () => { + expect(colorToFloatAlpha(0x123456ff, 1)).equal(-7.118128890449717e+37); + }); + + it('returns float value of color with alpha channel', () => { + expect(colorToFloatAlpha(0x123456ff, 0)).equal(7.916531978116694e-39); + }); + }); + + describe('colorFromRGBA()', () => { + it('creates color from r, g, b, a values', () => { + expect(colorFromRGBA(0x12, 0x34, 0x56, 0xff)).equal(0x123456ff); + }); + }); + + describe('colorFromHSVA()', () => { + it('creates color from h, s, v, a values', () => { + expect(colorFromHSVA(50.11764705882353, 1, 1, 1)).equal(0xffd500ff); + }); + }); + + describe('colorFromHSVAObject()', () => { + it('creates color from h, s, v, a values', () => { + expect(colorFromHSVAObject({ h: 50.11764705882353, s: 1, v: 1, a: 1 })).equal(0xffd500ff); + }); + }); + + describe('parseColorFast()', () => { + it('returns transparent for invalid type', () => { + expect(parseColorFast(1 as any)).equal(0); + expect(parseColorFast(null as any)).equal(0); + expect(parseColorFast({} as any)).equal(0); + expect(parseColorFast(undefined as any)).equal(0); + }); + + it('parses hex code', () => { + expect(parseColorFast('123456')).equal(0x123456ff); + }); + + it('parses any other color format as opaque color', () => { + expect(parseColorFast('rgba(255, 0, 1, 0.5)')).equal(0xff0001ff); + }); + }); + + describe('parseColor()', () => { + it('returns transparent for invalid type', () => { + expect(parseColor(1 as any)).equal(0); + expect(parseColor(null as any)).equal(0); + expect(parseColor({} as any)).equal(0); + expect(parseColor(undefined as any)).equal(0); + }); + + it('returns transparent for empty string, none and transparent', () => { + expect(parseColor('')).equal(0); + expect(parseColor('none')).equal(0); + expect(parseColor('transparent')).equal(0); + }); + + it('parses short form hex code', () => { + expect(parseColor('#123')).equal(0x112233ff); + }); + + it('parses hex code', () => { + expect(parseColor('#123456')).equal(0x123456ff); + }); + + it('parses hex code with alpha', () => { + expect(parseColor('#12345678')).equal(0x12345678); + }); + + it('parses rgb(...) format', () => { + expect(parseColor('rgb(255, 0, 1)')).equal(0xff0001ff); + }); + + it('parses rgba(...) format', () => { + expect(parseColor('rgba(255, 0, 1, 0.5)')).equal(0xff00017f); + }); + + it('parses named color', () => { + expect(parseColor('red')).equal(0xff0000ff); + }); + + it('return black for invalid color', () => { + expect(parseColor('*$^@&')).equal(0xff); + }); + }); + + describe('parseColorWithAlpha()', () => { + it('parses color with given alpha', () => { + expect(parseColorWithAlpha('#12345678', 0.5)).equal(0x1234567f); + }); + }); + + describe('withAlpha()', () => { + it('returns color with given alpha', () => { + expect(withAlpha(0x12345678, 127)).equal(0x1234567f); + }); + }); + + describe('withAlphaFloat()', () => { + it('returns color with given alpha', () => { + expect(withAlphaFloat(0x12345678, 0.5)).equal(0x1234567f); + }); + }); + + describe('makeTransparent()', () => { + it('adjust transparency', () => { + expect(makeTransparent(0x1234567f, 0.5)).equal(0x1234563f); + }); + }); + + describe('multiplyColor()', () => { + it('multiplies r, g, b values', () => { + expect(multiplyColor(0x0a0a0aff, 0.5)).equal(0x050505ff); + }); + + it('clamps r, g, b values', () => { + expect(multiplyColor(0x0a0a0aff, 100)).equal(0xffffffff); + }); + }); + + describe('lerpColors()', () => { + it('returns inbetween color', () => { + expect(lerpColors(0xff00ff00, 0x00ff00ff, 0.5)).equal(0x7f7f7f7f); + }); + }); + + describe('rgb2hsv()', function () { + it('returns HSVA representation of RGBA values #1', function () { + expect(rgb2hsv(255, 0, 0, 0.5)).eql({ h: 0, s: 1, v: 1, a: 0.5 }); + }); + + it('returns HSVA representation of RGBA values #2', function () { + expect(rgb2hsv(0, 255, 0, 0.5)).eql({ h: 120, s: 1, v: 1, a: 0.5 }); + }); + + it('returns HSVA representation of RGBA values #3', function () { + expect(rgb2hsv(0, 0, 255, 0.5)).eql({ h: 240, s: 1, v: 1, a: 0.5 }); + }); + + it('returns HSVA representation of RGBA values #4', function () { + expect(rgb2hsv(255, 0, 255, 0.5)).eql({ h: 300, s: 1, v: 1, a: 0.5 }); + }); + + it('returns HSVA representation of RGBA values with retained hue', function () { + expect(rgb2hsv(0, 0, 0, 0.5, 100)).eql({ h: 100, s: 0, v: 0, a: 0.5 }); + }); + }); + + describe('hsv2rgb()', function () { + it('returns correct RGBA values', function () { + expect(hsv2rgb(0, 1, 1)).eql({ r: 255, g: 0, b: 0 }); + expect(hsv2rgb(60, 1, 1)).eql({ r: 255, g: 255, b: 0 }); + expect(hsv2rgb(120, 1, 1)).eql({ r: 0, g: 255, b: 0 }); + expect(hsv2rgb(240, 1, 1)).eql({ r: 0, g: 0, b: 255 }); + expect(hsv2rgb(180, 1, 1)).eql({ r: 0, g: 255, b: 255 }); + expect(hsv2rgb(300, 1, 1)).eql({ r: 255, g: 0, b: 255 }); + expect(hsv2rgb(360, 1, 1)).eql({ r: 255, g: 0, b: 0 }); + }); + }); + + describe('h2rgb()', function () { + it('returns correct RGB values', function () { + expect(h2rgb(0)).eql({ r: 255, g: 0, b: 0 }); + expect(h2rgb(60)).eql({ r: 255, g: 255, b: 0 }); + expect(h2rgb(120)).eql({ r: 0, g: 255, b: 0 }); + expect(h2rgb(240)).eql({ r: 0, g: 0, b: 255 }); + expect(h2rgb(180)).eql({ r: 0, g: 255, b: 255 }); + expect(h2rgb(300)).eql({ r: 255, g: 0, b: 255 }); + expect(h2rgb(360)).eql({ r: 255, g: 0, b: 255 }); + }); + }); +}); diff --git a/src/ts/tests/common/colors.spec.ts b/src/ts/tests/common/colors.spec.ts new file mode 100644 index 0000000..0158bbe --- /dev/null +++ b/src/ts/tests/common/colors.spec.ts @@ -0,0 +1,46 @@ +import '../lib'; +import { expect } from 'chai'; +import { + fillToOutline, fillToOutlineColor, getMessageColor, SYSTEM_COLOR, ADMIN_COLOR, MOD_COLOR, + ANNOUNCEMENT_COLOR, PARTY_COLOR, WHITE, THINKING_COLOR, PARTY_THINKING_COLOR, SUPPORTER1_COLOR, + SUPPORTER2_COLOR, SUPPORTER3_COLOR +} from '../../common/colors'; +import { MessageType } from '../../common/interfaces'; + +describe('colors', () => { + describe('fillToOutline()', () => { + it('returns undefined for undefined', () => { + expect(fillToOutline(undefined)).undefined; + }); + + it('returns outline color', () => { + expect(fillToOutline('ff0000')).equals('b30000'); + }); + }); + + describe('fillToOutlineColor()', () => { + it('returns outline color', () => { + expect(fillToOutlineColor(0xff0000ff)).equals(0xb30000ff); + }); + }); + + describe('getMessageColor()', () => { + it('returns correct color for each message type', () => { + expect(getMessageColor(MessageType.System)).equals(SYSTEM_COLOR); + expect(getMessageColor(MessageType.Admin)).equals(ADMIN_COLOR); + expect(getMessageColor(MessageType.Mod)).equals(MOD_COLOR); + expect(getMessageColor(MessageType.Announcement)).equals(ANNOUNCEMENT_COLOR); + expect(getMessageColor(MessageType.Party)).equals(PARTY_COLOR); + expect(getMessageColor(MessageType.Thinking)).equals(THINKING_COLOR); + expect(getMessageColor(MessageType.PartyThinking)).equals(PARTY_THINKING_COLOR); + expect(getMessageColor(MessageType.Supporter1)).equals(SUPPORTER1_COLOR); + expect(getMessageColor(MessageType.Supporter2)).equals(SUPPORTER2_COLOR); + expect(getMessageColor(MessageType.Supporter3)).equals(SUPPORTER3_COLOR); + }); + + it('returns white color for other types', () => { + expect(getMessageColor(MessageType.Chat)).equals(WHITE); + expect(getMessageColor(999)).equals(WHITE); + }); + }); +}); diff --git a/src/ts/tests/common/compress.spec.ts b/src/ts/tests/common/compress.spec.ts new file mode 100644 index 0000000..b9e1b1d --- /dev/null +++ b/src/ts/tests/common/compress.spec.ts @@ -0,0 +1,23 @@ +import '../lib'; +import { expect } from 'chai'; +import { random } from 'lodash'; +import { compressTiles, decompressTiles } from '../../common/compress'; +import { REGION_SIZE } from '../../common/constants'; + +describe('compress', () => { + describe('compressTiles() + decompressTiles()', () => { + it('test', () => { + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + + for (let i = 0; i < tiles.length; i++) { + tiles[i] = random(0, 5); + } + + // console.log(Array.from(tiles).join(', ')); + const compressed = compressTiles(new Uint8Array(tiles)); + const decompressed = decompressTiles(compressed); + //console.log(`${JSON.stringify(test)}: ${test.length} -> ${compressed.length}`); + expect(Array.from(decompressed)).eql(Array.from(tiles), `compressed: [${Array.from(compressed).join(', ')}]`); + }); + }); +}); diff --git a/src/ts/tests/common/compressPony.spec.ts b/src/ts/tests/common/compressPony.spec.ts new file mode 100644 index 0000000..d67c24d --- /dev/null +++ b/src/ts/tests/common/compressPony.spec.ts @@ -0,0 +1,696 @@ +import '../lib'; +import * as fs from 'fs'; +import { expect } from 'chai'; +import { merge } from 'lodash'; +import { bitWriter, bitReader } from '../../common/bitUtils'; +import { + PrecompressedSet, writeSet, readSet, Precompressed, readPony, writePony, postdecompressPony, + precompressPony, compressPonyString, decompressPonyString, VERSION, precompressCM, fastPostdecompressPony, + decompressPony +} from '../../common/compressPony'; +import { RED, ORANGE, CYAN, GREEN, BLUE, BLACK, WHITE, TRANSPARENT } from '../../common/colors'; +import { PonyInfoNumber, PonyInfo } from '../../common/interfaces'; +import { repeat } from '../../common/utils'; +import { toByteArray } from 'base64-js'; +import { pathTo } from '../../server/paths'; + +// const poniesPath = pathTo('src', 'tests', 'ponies'); + +function base(black: T, white: T) { + return { + head: undefined, + nose: undefined, + ears: undefined, + horn: undefined, + wings: undefined, + frontHooves: undefined, + backHooves: undefined, + mane: undefined, + backMane: undefined, + tail: undefined, + facialHair: undefined, + headAccessory: undefined, + earAccessory: undefined, + faceAccessory: undefined, + neckAccessory: undefined, + frontLegAccessory: undefined, + backLegAccessory: undefined, + frontLegAccessoryRight: undefined, + backLegAccessoryRight: undefined, + lockBackLegAccessory: undefined, + unlockFrontLegAccessory: false, + unlockBackLegAccessory: false, + backAccessory: undefined, + waistAccessory: undefined, + chestAccessory: undefined, + sleeveAccessory: undefined, + extraAccessory: undefined, + coatFill: black, + coatOutline: black, + lockCoatOutline: undefined, + eyelashes: 0, + eyeColorLeft: black, + eyeColorRight: black, + eyeWhites: white, + eyeWhitesLeft: white, + eyeOpennessLeft: 0, + eyeOpennessRight: 0, + eyeshadow: false, + eyeshadowColor: undefined, + lockEyes: false, + lockEyeColor: false, + unlockEyeWhites: false, + fangs: 0, + muzzle: 0, + freckles: 0, + frecklesColor: undefined, + cm: undefined, + cmFlip: undefined, + customOutlines: false, + freeOutlines: false, + darkenLockedOutlines: undefined, + unlockEyelashColor: false, + eyelashColor: black, + eyelashColorLeft: black, + magicColor: white, + }; +} + +describe('compressPony', () => { + describe('writeSet() + readSet()', () => { + const tests: [PrecompressedSet, number, boolean][] = [ + [{ type: 1, pattern: 1, colors: 1, fillLocks: 0, fills: [1], outlineLocks: 0, outlines: [] }, 1, false], + [{ type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [5, 7], outlineLocks: 0, outlines: [] }, 4, false], + [{ type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [5, 7], outlineLocks: 2, outlines: [3, 4] }, 4, true], + ]; + + tests.forEach(test => it(`works for set: ${JSON.stringify(test)}`, () => { + const [set, colorBits, customOutlines] = test; + const buffer = bitWriter(write => writeSet(write, colorBits, customOutlines, set)); + // console.log(map(buffer, x => x).map(x => x.toString(2).padStart(8, '0')).join(' ')); + const result = readSet(bitReader(buffer), colorBits, customOutlines); + expect(result).eql(set); + })); + }); + + describe('writePony() + readPony()', () => { + const set1: PrecompressedSet = { + type: 1, pattern: 1, colors: 1, fillLocks: 0, fills: [1], outlineLocks: 0, outlines: [] + }; + const set2: PrecompressedSet = { + type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [1, 2], outlineLocks: 2, outlines: [3, 4] + }; + const base: Precompressed = { + version: VERSION, + colors: [], + booleanFields: [], + numberFields: [], + colorFields: [], + setFields: [], + cm: [], + }; + + const tests: Precompressed[] = [ + base, + merge({}, base, { colors: [RED, BLUE, GREEN] }), + merge({}, base, { colors: [RED, BLUE, GREEN], cm: [1, 2, 1, 1, 2] }), + merge({}, base, { colors: [RED, BLUE, GREEN], setFields: [set1] }), + merge({}, base, { colors: [RED, BLUE, GREEN, ORANGE, CYAN], setFields: { nose: set2 }, booleanFields: [true] }), + merge({}, base, { colors: [RED, BLUE, GREEN], cm: [1, 2, 1, 1, 2], booleanFields: [true, false, true, false] }), + merge({}, base, { booleanFields: [true, false] }), + ]; + + tests.forEach(test => it(`works for pony: ${JSON.stringify(test)}`, () => { + const buffer = bitWriter(write => writePony(write, test)); + const result = readPony(bitReader(buffer)); + expect(result).eql(test); + })); + }); + + describe('precompressCM()', () => { + it('converts colors to indexes', () => { + const cm = [RED, RED, RED]; + expect(precompressCM(cm, () => 1)).eql([1, 1, 1]); + }); + + it('handles holey arrays', () => { + const cm = [RED, , , RED]; + expect(precompressCM(cm, c => c === undefined ? 0 : 1)).eql([1, 0, 0, 1]); + }); + + it('empty CM', () => { + expect(precompressCM([], () => 1)).eql([]); + }); + + it('trims CM', () => { + const cm = [RED, RED, RED, TRANSPARENT, TRANSPARENT]; + expect(precompressCM(cm, () => 1)).eql([1, 1, 1]); + }); + + it('trims CM size to 25', () => { + const cm = repeat(50, RED); + expect(precompressCM(cm, () => 1).length).equal(25); + }); + }); + + describe('precompressPony() + postdecompressPony()', () => { + const BASE = base(BLACK, WHITE); + + function test(input: Partial, expected?: Partial) { + return () => { + const data = precompressPony(input as any, BLACK, x => x); + const result1 = postdecompressPony(data, x => x); + const result2 = fastPostdecompressPony(data); + expect(result1).eql({ ...BASE, ...(expected || input) }, 'postdecompressPony'); + expect(result2).eql({ ...BASE, ...(expected || input) }, 'fastPostdecompressPony'); + }; + } + + it('empty', test({}, { + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); + + it('colors', test({ + coatFill: RED, + coatOutline: RED, + eyeColorLeft: RED, + eyeColorRight: RED, + eyeWhites: RED, + eyeshadowColor: RED, + frecklesColor: undefined, + customOutlines: true, + freckles: 0, + eyeOpennessLeft: 0, + eyeOpennessRight: 0, + fangs: 0, + muzzle: 0, + eyelashes: 0, + eyeshadow: true, + lockEyes: false, + lockEyeColor: false, + lockCoatOutline: false, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); + + it('booleans', test({ + customOutlines: true, + lockEyes: true, + lockEyeColor: true, + lockCoatOutline: true, + }, { + customOutlines: true, + lockEyes: true, + lockEyeColor: true, + lockCoatOutline: true, + coatOutline: undefined, + eyeColorLeft: undefined, + eyeOpennessLeft: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); + + it('removes lockBackLegAccessory', test({ + lockBackLegAccessory: true + }, { + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); + + it('set', test({ + customOutlines: true, + lockCoatOutline: true, + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + tail: { + type: 1, + pattern: 1, + fills: [RED, BLUE, GREEN, ORANGE, CYAN], + lockFills: [false, false, false, false, false, false], + outlines: [RED, RED, RED, RED, RED], + lockOutlines: [false, false, false, false, false, true], + } + })); + + it('missing set fields', test({ + customOutlines: true, + tail: { + type: 1, + pattern: 1 + } + }, { + customOutlines: true, + lockCoatOutline: false, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + tail: { + type: 1, + pattern: 1, + fills: [BLACK, BLACK, BLACK, BLACK, BLACK], + lockFills: [false, false, false, false, false, false], + outlines: [BLACK, BLACK, BLACK, BLACK, BLACK], + lockOutlines: [false, false, false, false, false, true], + } + })); + + it('missing set color', test({ + customOutlines: true, + tail: { + type: 1, + pattern: 1, + fills: [RED, BLUE, undefined, ORANGE, CYAN], + lockFills: [false, false, false, false, false, false], + outlines: [RED, undefined, RED, RED, RED], + lockOutlines: [false, false, false, false, false, true], + } + }, { + customOutlines: true, + lockCoatOutline: false, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + tail: { + type: 1, + pattern: 1, + fills: [RED, BLUE, BLACK, ORANGE, CYAN], + lockFills: [false, false, false, false, false, false], + outlines: [RED, BLACK, RED, RED, RED], + lockOutlines: [false, false, false, false, false, true], + } + })); + + it('cm', test({ + cm: [undefined, RED, undefined, BLUE] as any, + }, { + cm: [TRANSPARENT, RED, TRANSPARENT, BLUE], + cmFlip: false, + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); + }); + + describe('decompressPony()', () => { + it('works for empty string', () => { + expect(decompressPony('')).eql(base(BLACK, WHITE)); + }); + + it('decompresses a pony', () => { + const pony = decompressPony('CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='); + + expect(pony.coatFill).equal(0x959595ff); + expect(pony.mane!.type).equal(2); + expect(pony.mane!.pattern).equal(0); + expect(pony.mane!.fills).eql([0x2f2f2fff]); + }); + + it('decompresses a pony from buffer', () => { + const pony = decompressPony(toByteArray('CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA==')); + + expect(pony.coatFill).equal(0x959595ff); + expect(pony.mane!.type).equal(2); + expect(pony.mane!.pattern).equal(0); + expect(pony.mane!.fills).eql([0x2f2f2fff]); + }); + }); + + describe('decompressPonyString()', () => { + it('works for empty string', () => { + expect(decompressPonyString('')).eql(base('000000', 'ffffff')); + }); + + it('works for empty string (editable: true)', () => { + decompressPonyString('', true); + }); + }); + + describe('compressPony() + decompressPony()', () => { + const BASE = base('000000', 'ffffff'); + + function test(input: Partial, expected?: Partial) { + return () => { + const data = compressPonyString(input as any); + const result = decompressPonyString(data, false); + expect(result).eql({ ...BASE, ...(expected || input) }); + }; + } + + it('empty', test({}, {})); + + it('coatFill', test({ coatFill: 'ff0000' }, { coatFill: 'ff0000', coatOutline: 'b30000' })); + + it('eyeColorLeft', test({ + eyeColorLeft: 'ff00ff', + }, { + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: 'ff00ff', + eyeColorRight: '000000', + })); + + it('eyeColorLeft + eyeColorRight', test({ + eyeColorLeft: 'ff00ff', + eyeColorRight: '00ff00', + }, { + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: 'ff00ff', + eyeColorRight: '00ff00', + })); + + it('eyeColorLeft + eyeColorRight (locked)', test({ + lockEyeColor: true, + eyeColorLeft: '00ff00', + eyeColorRight: 'ff00ff', + }, { + lockEyes: false, + lockEyeColor: true, + customOutlines: false, + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: 'ff00ff', + eyeColorRight: 'ff00ff', + })); + + it('eyeshadow', test({ + eyeshadow: true, + eyeshadowColor: 'ff00ff', + }, { + eyeshadow: true, + eyeshadowColor: 'ff00ff', + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: '000000', + eyeColorRight: '000000', + eyeWhites: 'ffffff', + lockEyes: false, + lockEyeColor: false, + customOutlines: false, + })); + + it('cm', test({ cm: ['ff0000', '', '00ff00'] }, { cm: ['ff0000', '', '00ff00'], cmFlip: false })); + + it('cm (flip)', test({ + cm: ['ff0000', '', '00ff00'], + cmFlip: true, + }, { + cm: ['ff0000', '', '00ff00'], + cmFlip: true, + })); + + it('all locked', test({ + tail: { + type: 2, + pattern: 1, + fills: [], + lockFills: [true, true, true, true, true, true], + } + }, { + tail: { + type: 2, + pattern: 1, + fills: [undefined, undefined, undefined, undefined, undefined, undefined], + lockFills: [true, true, true, true, true, true], + outlines: [undefined, undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('set', test({ + mane: { + type: 1, + pattern: 1, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0', 'ff000f'], + lockFills: [false, false, false, false, false, false], + } + }, { + mane: { + type: 1, + pattern: 1, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0', 'ff000f'], + lockFills: [false, false, false, false, false, false], + outlines: ['a8a8a8', 'b30000', 'b3a800', 'b30b00', 'b300a8', 'b3000a'], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('0 colors', test({ + ears: { + type: 0, + pattern: 0, + fills: ['000000'], + lockFills: [false], + } + }, { + ears: { + type: 0, + pattern: 0, + fills: ['000000'], + lockFills: [false, false, false, false, false, false], + outlines: ['000000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('extraAccessory', test({ + extraAccessory: { + type: 0, + pattern: 0, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0'], + lockFills: [false, false, false, false, false], + } + }, { + extraAccessory: { + type: 0, + pattern: 0, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0'], + lockFills: [false, false, false, false, false, false], + outlines: ['a8a8a8', 'b30000', 'b3a800', 'b30b00', 'b300a8', undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('neckAccessory', test({ + neckAccessory: { type: 0, pattern: 0 } + }, {})); + + it('locked fills', test({ + mane: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [], + }, + backMane: { + type: 15, + pattern: 1, + fills: ['ffffff', '00ff00', 'ffffff'], + lockFills: [true, false, true], + }, + }, { + mane: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + backMane: { + type: 15, + pattern: 1, + fills: ['ff0000', '00ff00', 'ff0000'], + lockFills: [true, false, true, false, false, false], + outlines: ['b30000', '00b300', 'b30000', undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + })); + + it('locked back hooves', test({ + frontHooves: { + type: 0, + pattern: 0, + fills: ['ff0000'], + }, + backHooves: { + type: 1, + pattern: 0, + lockFills: [true], + } + }, { + frontHooves: { + type: 0, + pattern: 0, + fills: ['ff0000'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + backHooves: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [true, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('back mane', test({ + mane: { + type: 3, + pattern: 1, + fills: ['000000', 'ffffff'], + }, + backMane: { + type: 2, + pattern: 1, + fills: ['ffffff', 'ff0000'], + lockFills: [false, false, true, true, true, true], + } + }, { + mane: { + type: 3, + pattern: 1, + fills: ['000000', 'ffffff', '000000', '000000', '000000', '000000'], + lockFills: [false, false, false, false, false, false], + outlines: ['000000', 'b3b3b3', '000000', '000000', '000000', '000000'], + lockOutlines: [true, true, true, true, true, true], + }, + backMane: { + type: 2, + pattern: 1, + fills: ['ffffff', 'ff0000', 'ffffff', 'ffffff', 'ffffff', 'ffffff'], + lockFills: [false, false, true, true, true, true], + outlines: ['b3b3b3', 'b30000', 'b3b3b3', 'b3b3b3', 'b3b3b3', 'b3b3b3'], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('chest accessory (adds default sleeve if missing)', test({ + chestAccessory: { + type: 2, + pattern: 8, + fills: ['ff0000', '00ff00', '0000ff'], + } + }, { + chestAccessory: { + type: 2, + pattern: 8, + fills: ['ff0000', '00ff00', '0000ff'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', '00b300', '0000b3', undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + sleeveAccessory: { + type: 0, + pattern: 0, + fills: ['ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000'], + lockFills: [true, true, true, true, true, true], + outlines: ['b30000', 'b30000', 'b30000', 'b30000', 'b30000', 'b30000'], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('back leg accessory', test({ + backLegAccessory: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [false], + } + }, { + lockBackLegAccessory: false, + backLegAccessory: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); + + it('cm (with undefined)', test({ + cm: [ + , , , 'fde9cd', , + 'fde9cd', , , , , + , , 'fde9cd', , 'fde9cd', + , , , , , + , , 'fde9cd', , + ] as any + }, { + cm: [ + '', '', '', 'fde9cd', '', + 'fde9cd', '', '', '', '', + '', '', 'fde9cd', '', 'fde9cd', + '', '', '', '', '', + '', '', 'fde9cd' + ], + cmFlip: false, + })); + + it('back leg accessory (editable)', () => { + const data = compressPonyString({ + lockBackLegAccessory: false, + backLegAccessory: { + type: 1, + pattern: 1, + fills: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff'], + lockFills: [false], + } + } as any); + + const result = decompressPonyString(data, true); + + expect(result.lockBackLegAccessory).false; + expect(result.backLegAccessory).eql({ + type: 1, + pattern: 1, + fills: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', '00b300', '0000b3', 'b3b300', '00b3b3', 'b300b3'], + lockOutlines: [true, true, true, true, true, true], + }); + }); + + it('black colors (editable)', () => { + const data = compressPonyString({ coatFill: '000000' } as any); + const result = decompressPonyString(data, true); + expect(result.coatFill).equal('000000'); + }); + + it('neckAccessory: { type: 0 }', () => { + const data = compressPonyString({ neckAccessory: { type: 0, pattern: 0 } } as any); + const result = decompressPonyString(data, true); + expect(result.neckAccessory!.type).eql(0); + }); + + it('mane: { type: 0 }', () => { + const data = compressPonyString({ mane: { type: 0, pattern: 0 } } as any); + const result = decompressPonyString(data, true); + expect(result.mane!.type).eql(0); + }); + + // fs.readdirSync(poniesPath).forEach(f => it(`(${f})`, () => { + // const json = JSON.parse(fs.readFileSync(path.join(poniesPath, f), 'utf8')); + // const data = compressPonyString(json); + // const result = decompressPonyString(data, true); + // expect(result).eql(json); + // })); + + it.skip('error test', () => { + const json = JSON.parse(fs.readFileSync(pathTo('tools', 'data', 'error-1504869659641.json'), 'utf8')); + const infoJson = json.data.info; + const compressedTemp = 'CAjNzc3////apSD/1wAekP8yzTLacNbcFDw+oCoACJiRngCBNET8ADjAcAAlSUCrPH6QGAA='; + console.log(Array.from(toByteArray(compressedTemp)).map(x => x.toString(16).padStart(2, '0')).join(' ')); + console.log(Array.from(toByteArray(json.data.compressed)).map(x => x.toString(16).padStart(2, '0')).join(' ')); + const compressed = compressPonyString(infoJson); + //const result = decompressPonyString(compressed, true); + expect(compressed).eql(json.data.compressed); + }); + }); +}); diff --git a/src/ts/tests/common/encoders/updateEncoder.spec.ts b/src/ts/tests/common/encoders/updateEncoder.spec.ts new file mode 100644 index 0000000..f187f4b --- /dev/null +++ b/src/ts/tests/common/encoders/updateEncoder.spec.ts @@ -0,0 +1,280 @@ +import '../../lib'; +import { expect } from 'chai'; +import { encodeUpdateSimple, encodeRegionSimple } from '../../../common/encoders/updateEncoder'; +import { EntityState, DecodedRegionUpdate, UpdateFlags, TileType } from '../../../common/interfaces'; +import { entity, mockClient, serverEntity } from '../../mocks'; +import { encodeString } from 'ag-sockets/dist/utf8'; +import { emptyUpdate, decodeUpdate } from '../../../common/encoders/updateDecoder'; +import { createServerRegion } from '../../../server/serverRegion'; +import { IClient, ServerRegion } from '../../../server/serverInterfaces'; +import { setEntityName } from '../../../server/entityUtils'; +import { compressTiles } from '../../../common/compress'; +import { REGION_SIZE } from '../../../common/constants'; + +describe('updateEncoder', () => { + describe('encodeUpdate() + decodeUpdate()', () => { + const def = { x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }; + const out = emptyUpdate(0); + const exp: DecodedRegionUpdate = { x: 0, y: 0, updates: [], removes: [], tiles: [], tileData: null }; + let region: ServerRegion; + + function testEncodeDecode(input: ServerRegion, expected: DecodedRegionUpdate) { + const encoded = encodeUpdateSimple(input); + const decoded = decodeUpdate(encoded); + expect(decoded).eql(expected); + } + + beforeEach(() => { + region = createServerRegion(0, 0); + }); + + it('encodes empty updates', () => { + testEncodeDecode(region, { ...exp }); + }); + + it('encodes region x, y', () => { + region.x = 1; + region.y = 2; + + testEncodeDecode(region, { ...exp, x: 1, y: 2 }); + }); + + it('encodes empty removes', () => { + testEncodeDecode(region, { ...exp, removes: [] }); + }); + + it('encodes removes', () => { + region.entityRemoves.push(1, 2, 3); + + testEncodeDecode(region, { ...exp, removes: [1, 2, 3] }); + }); + + it('encodes empty tiles', () => { + testEncodeDecode(region, { ...exp, tiles: [] }); + }); + + it('encodes tiles', () => { + region.tileUpdates.push({ x: 1, y: 2, type: 3 }, { x: 7, y: 56, type: 2 }); + + testEncodeDecode(region, { ...exp, tiles: [{ x: 1, y: 2, type: 3 }, { x: 7, y: 56, type: 2 }] }); + }); + + it('encodes flags', () => { + region.entityUpdates.push({ ...def, entity: { ...entity(123), state: EntityState.PonySitting }, flags: UpdateFlags.State }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, state: EntityState.PonySitting }] }); + }); + + it('encodes flags with switch region', () => { + region.entityUpdates.push({ + ...def, entity: { ...entity(123), state: EntityState.PonySitting }, flags: UpdateFlags.State | UpdateFlags.SwitchRegion + }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, state: EntityState.PonySitting, switchRegion: true }] }); + }); + + it('encodes expression', () => { + region.entityUpdates.push({ ...def, entity: { ...entity(123), options: { expr: 555 } }, flags: UpdateFlags.Expression }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, expression: 555 }] }); + }); + + it('encodes position', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22 }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11, y: 22, state: 0 }] }); + }); + + it('encodes type', () => { + region.entityUpdates.push({ ...def, entity: entity(123, 0, 0, 111), flags: UpdateFlags.Type }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, type: 111 }] }); + }); + + it('encodes options', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Options, options: { tag: 'bar' } }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, options: { tag: 'bar' } }] }); + }); + + it('encodes info', () => { + const e = serverEntity(123); + e.encryptedInfoSafe = new Uint8Array([1, 2, 3, 4, 5]); + + region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Info }); + + const decoded = decodeUpdate(encodeUpdateSimple(region)); + + expect(Array.from(decoded.updates[0].info!)).eql([1, 2, 3, 4, 5]); + }); + + it('encodes action', () => { + region.entityUpdates.push({ ...def, entity: entity(123), action: 5, flags: UpdateFlags.Action }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, action: 5 }] }); + }); + + it('encodes name', () => { + const e = serverEntity(123); + e.encodedName = encodeString('foobar')!; + + region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Name }); + + const decoded = decodeUpdate(encodeUpdateSimple(region)); + + expect(decoded.updates[0].name).eql('foobar'); + }); + + it('encodes bad name', () => { + const e = serverEntity(123); + e.encodedName = encodeString('foobar')!; + e.nameBad = true; + + region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Name }); + + const decoded = decodeUpdate(encodeUpdateSimple(region)); + + expect(decoded.updates[0].name).eql('foobar'); + expect(decoded.updates[0].filterName).true; + }); + + it('encodes position and velocity', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22, vx: 1, vy: 1 }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11, y: 22, vx: 1, vy: 1, state: 0 }] }); + }); + + it('encodes position and velocity (2)', () => { + region.entityUpdates.push({ + ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11.125, y: 22.5, vx: 0.125, vy: 2.5 + }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11.125, y: 22.5, vx: 0.125, vy: 2.5, state: 0 }] }); + }); + + it('encodes position and velocity (3)', () => { + region.entityUpdates.push({ + ...def, entity: entity(123), flags: UpdateFlags.Position, x: -11.125, y: -22.5, vx: -0.125, vy: -2.5 + }); + + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: -11.125, y: -22.5, vx: -0.125, vy: -2.5, state: 0 }] }); + }); + + it('throws on invalid velocity', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 0, y: 0, vx: 100, vy: 0 }); + + expect(() => testEncodeDecode(region, { ...exp, updates: [] })) + .throw('Exceeded max velocity (100)'); + }); + + it('encodes update with all fields', () => { + region.entityUpdates.push({ + ...def, entity: { ...entity(123, 0, 0, 111), state: EntityState.PonySitting }, + flags: UpdateFlags.Position | UpdateFlags.Type | UpdateFlags.Options | UpdateFlags.Action, + x: 11, y: 22, vx: 1, vy: 1, action: 5, options: { tag: 'bar' }, + }); + + testEncodeDecode( + region, + { + ...exp, + updates: [{ + ...out, id: 123, type: 111, x: 11, y: 22, vx: 1, vy: 1, state: EntityState.PonySitting, + action: 5, playerState: undefined, options: { tag: 'bar' } + }] + }); + }); + + it('encodes multiple updates', () => { + region.entityUpdates.push( + { ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22 }, + { ...def, entity: { ...entity(321), state: EntityState.PonySitting }, flags: UpdateFlags.State } + ); + + testEncodeDecode( + region, + { + ...exp, + updates: [ + { ...out, id: 123, x: 11, y: 22, state: 0 }, + { ...out, id: 321, state: EntityState.PonySitting }, + ] + }); + }); + }); + + describe('writeRegion() + decodeUpdate()', () => { + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + tiles.fill(TileType.Dirt); + const emptyTileData = compressTiles(tiles); + + function testEncodeDecode(region: ServerRegion, client: IClient, expected: DecodedRegionUpdate) { + const encoded = encodeRegionSimple(region, client); + const decoded = decodeUpdate(encoded); + expect(decoded).eql(expected); + } + + it('empty region', () => { + const region = createServerRegion(1, 2); + const client = mockClient(); + + testEncodeDecode(region, client, { x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [] }); + }); + + it('encodes single entity', () => { + const region = createServerRegion(1, 2); + const entity = serverEntity(123, 10, 20, 32); + const client = mockClient(); + region.entities.push(entity); + + testEncodeDecode(region, client, { + x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [ + { + id: 123, x: 10, y: 20, vx: 0, vy: 0, type: 32, + name: undefined, switchRegion: false, crc: undefined, info: undefined, + state: 0, expression: undefined, action: undefined, options: undefined, + playerState: undefined, filterName: false, + }, + ] + }); + }); + + it('encodes single entity with more fields', () => { + const region = createServerRegion(1, 2); + const entity = serverEntity(123, 10, 20, 32); + const client = mockClient(); + region.entities.push(entity); + setEntityName(entity, 'foo'); + const info = new Uint8Array([1, 2, 3]); + entity.client = mockClient(); + entity.state = 123; + entity.options = { toy: 5, expr: 123 }; + entity.encryptedInfoSafe = info; + entity.vx = 1; + entity.vy = 2; + client.hides.add(entity.client.accountId); + + testEncodeDecode(region, client, { + x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [ + { + id: 123, x: 10, y: 20, vx: 1, vy: 2, type: 32, + name: 'foo', switchRegion: false, crc: 0, info, + state: 123, expression: undefined, action: undefined, options: { toy: 5, expr: 123 }, + playerState: 2, filterName: false, + }, + ], + }); + }); + + it('skips shadowed entities', () => { + const region = createServerRegion(1, 2); + const entity = serverEntity(123, 10, 20, 32); + const client = mockClient(); + region.entities.push(entity); + entity.client = mockClient(); + entity.client.shadowed = true; + + testEncodeDecode(region, client, { x: 1, y: 2, removes: [], tiles: [], updates: [], tileData: emptyTileData }); + }); + }); +}); diff --git a/src/ts/tests/common/entityUtils.spec.ts b/src/ts/tests/common/entityUtils.spec.ts new file mode 100644 index 0000000..f134c7c --- /dev/null +++ b/src/ts/tests/common/entityUtils.spec.ts @@ -0,0 +1,39 @@ +import '../lib'; +import { expect } from 'chai'; +import { entity } from '../mocks'; +import { compareEntities } from '../../common/entityUtils'; + +describe('entityUtils [common]', () => { + describe('compareEntities()', () => { + it('compares by y position', () => { + const a = entity(0, 0, 1); + const b = entity(0, 0, 2); + + expect(compareEntities(a, b)).lt(0); + expect(compareEntities(b, a)).gt(0); + }); + + it('compares by x position', () => { + const a = entity(0, 1, 1); + const b = entity(0, 2, 1); + + expect(compareEntities(a, b)).lt(0); + expect(compareEntities(b, a)).gt(0); + }); + + it('compares by id', () => { + const a = entity(1, 1, 1); + const b = entity(2, 1, 1); + + expect(compareEntities(a, b)).gt(0); + expect(compareEntities(b, a)).lt(0); + }); + + it('returns 0 for identical entities', () => { + const a = entity(1, 1, 1); + const b = entity(1, 1, 1); + + expect(compareEntities(a, b)).equal(0); + }); + }); +}); diff --git a/src/ts/tests/common/expressionUtils.spec.ts b/src/ts/tests/common/expressionUtils.spec.ts new file mode 100644 index 0000000..0b0233b --- /dev/null +++ b/src/ts/tests/common/expressionUtils.spec.ts @@ -0,0 +1,103 @@ +import '../lib'; +import { expect } from 'chai'; +import { parseExpression, expression } from '../../common/expressionUtils'; +import { expressions } from '../../common/expressions'; +import { Expression, Iris, Eye, Muzzle, ExpressionExtra } from '../../common/interfaces'; +import { decodeExpression, encodeExpression } from '../../common/encoders/expressionEncoder'; +import { flipIris } from '../../client/ponyUtils'; + +function toExpression([right, left, muzzle, rightIris = 0, leftIris = 0, extra = 0]: any): Expression { + return { left, right, muzzle, rightIris, leftIris, extra }; +} + +describe('expressionUtils', () => { + describe('parseExpression()', () => { + expressions.forEach(([input, expected]) => { + it(JSON.stringify(input), () => { + if (expected) { + expect(parseExpression(input)).eql(toExpression(expected)); + } else { + expect(parseExpression(input)).undefined; + } + }); + }); + + it('should return the same expression each time', () => { + const expected = { + right: Eye.ClosedHappy2, + left: Eye.ClosedHappy2, + muzzle: Muzzle.Smile, + rightIris: Iris.Forward, + leftIris: Iris.Forward, + extra: ExpressionExtra.None, + }; + + const expr = parseExpression('^^'); + expect(expr).eql(expected, '1st'); + expr!.extra = 999; + + expect(parseExpression('^^')).eql(expected, '2nd'); + }); + + it('should return nothing for "constructor" expression', () => { + expect(parseExpression('constructor')).undefined; + }); + }); + + describe('encodeExpression() + decodeExpression()', () => { + function test(expression: Expression | undefined) { + return decodeExpression(encodeExpression(expression)); + } + + it('works for null and undefined', () => { + expect(test(null as any)).undefined; + expect(test(undefined)).undefined; + }); + + expressions.filter(([, x]) => !!x).forEach(([input, expected]) => { + it(JSON.stringify(input), () => { + const expression = toExpression(expected); + expect(test(expression)).eql(expression); + }); + }); + }); + + describe('flipIris()', () => { + it('returns the same iris for non flippable irises', () => { + expect(flipIris(Iris.Forward)).equal(Iris.Forward); + expect(flipIris(Iris.Up)).equal(Iris.Up); + expect(flipIris(Iris.Shocked)).equal(Iris.Shocked); + }); + + it('returns flipped iris', () => { + expect(flipIris(Iris.Left)).equal(Iris.Right); + expect(flipIris(Iris.Right)).equal(Iris.Left); + expect(flipIris(Iris.UpLeft)).equal(Iris.UpRight); + expect(flipIris(Iris.UpRight)).equal(Iris.UpLeft); + }); + }); + + describe('expression()', () => { + it('creates expression with all parameters', () => { + expect(expression(Eye.Angry, Eye.ClosedHappy, Muzzle.Smile, Iris.Left, Iris.Right, ExpressionExtra.Blush)).eql({ + right: Eye.Angry, + left: Eye.ClosedHappy, + muzzle: Muzzle.Smile, + rightIris: Iris.Left, + leftIris: Iris.Right, + extra: ExpressionExtra.Blush, + }); + }); + + it('creates expression with defaults', () => { + expect(expression(Eye.Angry, Eye.ClosedHappy, Muzzle.Smile)).eql({ + right: Eye.Angry, + left: Eye.ClosedHappy, + muzzle: Muzzle.Smile, + rightIris: Iris.Forward, + leftIris: Iris.Forward, + extra: ExpressionExtra.None, + }); + }); + }); +}); diff --git a/src/ts/tests/common/mat4.spec.ts b/src/ts/tests/common/mat4.spec.ts new file mode 100644 index 0000000..140e756 --- /dev/null +++ b/src/ts/tests/common/mat4.spec.ts @@ -0,0 +1,23 @@ +import '../lib'; +import { expect } from 'chai'; +import { createMat4, ortho } from '../../common/mat4'; + +describe('mat4', () => { + describe('createMat4()', () => { + expect(createMat4()).eql(new Float32Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + ])); + }); + + describe('ortho()', () => { + expect(ortho(createMat4(), 100, 200, 300, 400, 10, 20)).eql(new Float32Array([ + 0.019999999552965164, 0, 0, 0, + 0, 0.019999999552965164, 0, 0, + 0, 0, -0.20000000298023224, 0, + -3, -7, -3, 1 + ])); + }); +}); diff --git a/src/ts/tests/common/movementUtils.spec.ts b/src/ts/tests/common/movementUtils.spec.ts new file mode 100644 index 0000000..eacf590 --- /dev/null +++ b/src/ts/tests/common/movementUtils.spec.ts @@ -0,0 +1,136 @@ +import '../lib'; +import { expect } from 'chai'; +import { EntityState } from '../../common/interfaces'; +import { + flagsToSpeed, dirToVector, vectorToDir, isMovingRight, encodeMovement, decodeMovement, shouldBeFacingRight +} from '../../common/movementUtils'; +import { PONY_SPEED_TROT, PONY_SPEED_WALK, tileWidth, tileHeight } from '../../common/constants'; +import { rect } from '../../common/rect'; +import { roundPositionX, roundPositionY } from '../../common/positionUtils'; + +describe('movementUtils', () => { + describe('flagsToSpeed()', () => { + it('returns trotting speed for trotting state', () => { + expect(flagsToSpeed(EntityState.PonyTrotting)).equal(PONY_SPEED_TROT); + }); + + it('returns trotting speed for mixed trotting state', () => { + expect(flagsToSpeed(EntityState.PonyTrotting | EntityState.FacingRight)).equal(PONY_SPEED_TROT); + }); + + it('returns walk speed for trotting state', () => { + expect(flagsToSpeed(EntityState.PonyWalking)).equal(PONY_SPEED_WALK); + }); + + it('returns 0 for standing state', () => { + expect(flagsToSpeed(EntityState.PonyStanding)).equal(0); + }); + }); + + describe('dirToVector()', () => { + it('returns vector for given direction', () => { + expect(dirToVector(0)).eql({ x: 0, y: -1 }); + }); + }); + + describe('vectorToDir()', () => { + it('returns direction for given vector', () => { + expect(vectorToDir(0, -1)).eql(0); + }); + + it('returns direction for given vector', () => { + expect(vectorToDir(-1, -1)).eql(14); + }); + }); + + describe('encodeMovement() + decodeMovement()', () => { + const tests: [number, number, number, EntityState][] = [ + [0.015625, 0.020833333333333332, 0, EntityState.None], + [10.015625, 20.020833333333332, 1, EntityState.PonyTrotting], + [5.515625, 2.2708333333333335, 1, EntityState.PonyTrotting], + [99999.015625, 88888.02083333333, 1, EntityState.PonyTrotting], + ]; + + tests.forEach(movement => { + it(JSON.stringify(movement), () => { + const [x, y, dir, flags] = movement; + const camera = rect(roundPositionX(x) * tileWidth, roundPositionY(y) * tileHeight, 100, 100); + const [a, b, c, d, e] = encodeMovement(x, y, dir, flags, 123, camera); + expect(decodeMovement(a, b, c, d, e)).eql({ x, y, dir, flags, time: 123, camera }); + }); + }); + + it('decodes camera rect', () => { + const [a, b, c, d, e] = encodeMovement(100, 100, 1, EntityState.None, 123, rect(300, 200, 800, 600)); + + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 100.015625, y: 100.02083333333333, dir: 1, flags: EntityState.None, time: 123, + camera: rect(300, 200, 800, 600) + }); + }); + + it('clamps negative values', () => { + const [a, b, c, d, e] = encodeMovement(-10, -10, 1, EntityState.None, 123, rect(0, 0, 100, 100)); + + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 0.015625, y: 0.020833333333333332, dir: 1, flags: EntityState.None, time: 123, + camera: rect(0, 0, 100, 100) + }); + }); + + it('clamps values above 100000', () => { + const [a, b, c, d, e] = encodeMovement(100001, 100001, 1, EntityState.None, 123, rect(100000 * 32, 100000 * 24, 100, 100)); + + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 100000.015625, y: 100000.020833333333334, dir: 1, flags: EntityState.None, time: 123, + camera: rect(100000 * 32, 100000 * 24, 100, 100) + }); + }); + + it('handles invalid direction', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 999, EntityState.None, 123, rect(0, 0, 100, 100)); + + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 10.015625, y: 10.020833333333334, dir: 231, flags: EntityState.None, time: 123, + camera: rect(0, 0, 100, 100) + }); + }); + + it('handles invalid flags', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 1, 999, 123, rect(0, 0, 100, 100)); + + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 10.015625, y: 10.020833333333334, dir: 1, flags: 231, time: 123, + camera: rect(0, 0, 100, 100) + }); + }); + }); + + describe('isMovingRight()', () => { + it('returns false for negative velocity', () => { + expect(isMovingRight(-1, true)).false; + }); + + it('returns true for positive velocity', () => { + expect(isMovingRight(1, false)).true; + }); + + it('returns previous facing for 0 velocity', () => { + expect(isMovingRight(0, true)).true; + }); + }); + + describe('shouldBeFacingRight()', () => { + it('returns false for negative velocity', () => { + expect(shouldBeFacingRight({ vx: -1, state: 0 } as any)).false; + }); + + it('returns true for positive velocity', () => { + expect(shouldBeFacingRight({ vx: 1, state: 0 } as any)).true; + }); + + it('returns true for zero velocity if already facing right', () => { + expect(shouldBeFacingRight({ vx: 0, state: EntityState.FacingRight } as any)).true; + }); + }); +}); diff --git a/src/ts/tests/common/ponyHelpers.spec.ts b/src/ts/tests/common/ponyHelpers.spec.ts new file mode 100644 index 0000000..ff8439a --- /dev/null +++ b/src/ts/tests/common/ponyHelpers.spec.ts @@ -0,0 +1,15 @@ +import '../lib'; +import { expect } from 'chai'; +import { defaultPonyState, isStateEqual } from '../../client/ponyHelpers'; + +describe('interfaces', () => { + describe('isStateEqual()', () => { + it('returns true if two states are equal', () => { + expect(isStateEqual(defaultPonyState(), defaultPonyState())).true; + }); + + it('returns true if two states are not equal', () => { + expect(isStateEqual({ ...defaultPonyState(), blinkFrame: 5 }, defaultPonyState())).false; + }); + }); +}); diff --git a/src/ts/tests/common/ponyInfo.spec.ts b/src/ts/tests/common/ponyInfo.spec.ts new file mode 100644 index 0000000..61a9445 --- /dev/null +++ b/src/ts/tests/common/ponyInfo.spec.ts @@ -0,0 +1,359 @@ +import '../lib'; +import { expect } from 'chai'; +import { + syncLockedPonyInfo, syncLockedSpriteSet, syncLockedPonyInfoNumber, toPaletteSet, releasePalettes +} from '../../common/ponyInfo'; +import { SpriteSet, PonyInfo, PaletteManager, ColorExtraSets, Palette } from '../../common/interfaces'; +import { RED, BLUE, YELLOW, ORANGE, TRANSPARENT } from '../../common/colors'; +import { repeat, times, flatten } from '../../common/utils'; + +type Set = SpriteSet; + +function ponyInfo(info: Partial): PonyInfo { + return info as PonyInfo; +} + +describe('ponyInfo', () => { + describe('syncLockedSpriteSet()', () => { + it('does nothing for undefined', () => { + syncLockedSpriteSet(undefined, false, f => f, 'ff0000', '00ff00'); + }); + + it('does nothing for missing fills', () => { + syncLockedSpriteSet({}, false, f => f, 'ff0000', '00ff00'); + }); + + it('does nothing for missing lockFills', () => { + syncLockedSpriteSet({ fills: ['00ff00'] }, false, f => f, 'ff0000', '00ff00'); + }); + + it('uses base fill if fill is locked', () => { + const set: Set = { + fills: [], + lockFills: [true], + }; + + syncLockedSpriteSet(set, false, f => f, 'ff0000', '00ff00'); + + expect(set.fills![0]).equal('ff0000'); + }); + + it('uses first fill if fill is locked', () => { + const set: Set = { + fills: ['ff0000', '0000ff', '0000ff'], + lockFills: [false, true, false], + }; + + syncLockedSpriteSet(set, false, f => f, 'ffffff', '000000'); + + expect(set.fills).eql(['ff0000', 'ff0000', '0000ff']); + }); + + it('uses generated outline if custom outlines is false', () => { + const set: Set = { + fills: ['ff0000', '00ff00'], + outlines: ['000000', '000000'], + lockOutlines: [false, false], + }; + + syncLockedSpriteSet(set, false, f => f + 'x', 'ff0000', '00ff00'); + + expect(set.lockOutlines).eql([true, true]); + expect(set.outlines).eql(['ff0000x', '00ff00x']); + }); + + it('uses generated outline if outline is locked', () => { + const set: Set = { + fills: ['ff0000', '00ff00'], + outlines: ['000000', '000000'], + lockOutlines: [false, true], + }; + + syncLockedSpriteSet(set, true, f => f + 'x', 'ff0000', '00ff00'); + + expect(set.outlines).eql(['000000', '00ff00x']); + }); + + it('uses base outline for first fill if fill is locked too', () => { + const set: Set = { + fills: ['ff0000', '00ff00'], + lockFills: [true, false], + outlines: ['000000', '000000'], + lockOutlines: [true, false], + }; + + syncLockedSpriteSet(set, true, f => f + 'x', 'ff0000', 'ffffff'); + + expect(set.outlines).eql(['ffffff', '000000']); + }); + }); + + describe('syncLockedPonyInfo()', () => { + it('syncs coatOutline if customOutlines is false', () => { + const pony = ponyInfo({ + customOutlines: false, + coatFill: 'ff0000', + }); + + syncLockedPonyInfo(pony); + + expect(pony.coatOutline).equal('b30000'); + }); + + it('syncs coatOutline if lockCoatOutline is true', () => { + const pony = ponyInfo({ + customOutlines: true, + lockCoatOutline: true, + coatFill: 'ff0000', + }); + + syncLockedPonyInfo(pony); + + expect(pony.coatOutline).equal('b30000'); + }); + + it('does not sync coatOutline if customOutline is false and lockCoatOutline is false', () => { + const pony = ponyInfo({ + customOutlines: true, + lockCoatOutline: false, + coatFill: 'ff0000', + coatOutline: '00ff00', + }); + + syncLockedPonyInfo(pony); + + expect(pony.coatOutline).equal('00ff00'); + }); + + it('syncs eyeOpennessLeft if lockEyes is true', () => { + const pony = ponyInfo({ + lockEyes: true, + eyeOpennessLeft: 3, + eyeOpennessRight: 2, + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeOpennessLeft).equal(2); + }); + + it('does not sync eyeOpennessLeft if lockEyes is false', () => { + const pony = ponyInfo({ + lockEyes: false, + eyeOpennessLeft: 3, + eyeOpennessRight: 2, + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeOpennessLeft).equal(3); + }); + + it('syncs eyeColorLeft if lockEyeColor is true', () => { + const pony = ponyInfo({ + lockEyeColor: true, + eyeColorLeft: 'ff0000', + eyeColorRight: '00ff00', + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeColorLeft).equal('00ff00'); + }); + + it('does not sync eyeColorLeft if lockEyeColor is false', () => { + const pony = ponyInfo({ + lockEyeColor: false, + eyeColorLeft: 'ff0000', + eyeColorRight: '00ff00', + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeColorLeft).equal('ff0000'); + }); + + const fields: (keyof PonyInfo)[] = [ + 'nose', 'ears', 'horn', 'wings', 'frontHooves', 'backHooves', 'mane', 'backMane', 'tail', 'facialHair', + 'headAccessory', 'earAccessory', 'faceAccessory', 'neckAccessory', 'frontLegAccessory', 'backLegAccessory', + 'backAccessory', 'waistAccessory', 'chestAccessory', 'sleeveAccessory', + ]; + + fields.forEach(field => it(`syncs '${field}' sprite set`, () => { + const pony = ponyInfo({ + [field]: { + fills: ['ff0000'], + outlines: ['00ff00'], + lockOutlines: [true], + }, + }); + + syncLockedPonyInfo(pony); + + expect(pony[field]).eql({ + fills: ['ff0000'], + outlines: ['b30000'], + lockOutlines: [true], + }); + })); + + it('syncs extraAccessory colors to correct pony colors', () => { + const pony = ponyInfo({ + customOutlines: true, + coatFill: 'ffffff', + coatOutline: '000000', + eyeColorLeft: 'aaaaaa', + eyeColorRight: 'bbbbbb', + mane: { type: 1, fills: ['ff0000'], outlines: ['aa0000'] }, + backMane: { type: 1, fills: ['00ff00'], outlines: ['00aa00'] }, + tail: { type: 1, fills: ['0000ff'], outlines: ['0000aa'] }, + extraAccessory: { + fills: repeat(5, '111111'), + lockFills: repeat(5, true), + outlines: repeat(5, '111111'), + lockOutlines: repeat(5, true), + }, + }); + + syncLockedPonyInfo(pony); + + expect(pony.extraAccessory).eql({ + fills: ['ffffff', 'bbbbbb', 'ff0000', '00ff00', '0000ff'], + lockFills: [true, true, true, true, true], + outlines: ['000000', 'bbbbbb', 'aa0000', '00aa00', '0000aa'], + lockOutlines: [true, true, true, true, true], + }); + }); + }); + + describe('syncLockedPonyInfoNumber()', () => { + it('syncs coatOutline if customOutlines is false', () => { + const pony: any = { + customOutlines: false, + coatFill: 0xff0000ff, + }; + + syncLockedPonyInfoNumber(pony); + + expect(pony.coatOutline).equal(0xb30000ff); + }); + }); + + describe('toPaletteSet()', () => { + const getColorsForSet = (set: SpriteSet, count: number) => new Uint32Array([ + TRANSPARENT, + ...flatten(times(count, i => [ + set.fills && set.fills[i] || 0, + set.outlines && set.outlines[i] || 0 + ])), + ]); + let manager: PaletteManager; + + beforeEach(() => { + manager = { + add: x => x && Array.from(x) as any, + addArray: x => x && Array.from(x) as any, + init() { }, + }; + }); + + it('returns default object for empty set', () => { + expect(toPaletteSet({}, undefined, manager, getColorsForSet, false, true)).eql({ + type: 0, + pattern: 0, + palette: [TRANSPARENT], + extraPalette: undefined, + }); + }); + + it('returns type pattern and palette fields from set', () => { + const set: SpriteSet = { + type: 1, + pattern: 2, + fills: [], + outlines: [], + }; + + expect(toPaletteSet(set, [], manager, getColorsForSet, false, true)).eql({ + type: 1, + pattern: 2, + palette: [TRANSPARENT], + extraPalette: undefined, + }); + }); + + it('trims palette to set color count', () => { + const set: SpriteSet = { + type: 1, + pattern: 2, + fills: [RED, BLUE, 1, 2, 3, 4], + outlines: [ORANGE, YELLOW, 5, 6, 7, 8], + }; + + const sets: ColorExtraSets = [ + [], + [ + { color: {} as any }, + { color: {} as any }, + { color: {} as any, colors: 5 }, + ], + ]; + + expect(toPaletteSet(set, sets, manager, getColorsForSet, false, true)).eql({ + type: 1, + pattern: 2, + palette: [TRANSPARENT, RED, ORANGE, BLUE, YELLOW], + extraPalette: undefined, + }); + }); + + it('returns extra palette from sets', () => { + const set: SpriteSet = { + type: 0, + pattern: 0, + }; + + const sets: ColorExtraSets = [ + [ + { + color: {} as any, + palettes: [ + new Uint32Array([BLUE, YELLOW]), + ], + }, + ], + ]; + + expect(toPaletteSet(set, sets, manager, getColorsForSet, true, true)).eql({ + type: 0, + pattern: 0, + palette: [TRANSPARENT], + extraPalette: [BLUE, YELLOW], + }); + }); + }); + + describe('releasePalettes()', () => { + it('ignores other fields', () => { + releasePalettes({ foo: 'bar', bar: 12, test: { a: 4 } } as any); + }); + + it('releases palette field', () => { + const palette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; + + releasePalettes({ foo: palette } as any); + + expect(palette.refs).equal(0); + }); + + it('releases palette in object field', () => { + const palette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; + const extraPalette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; + + releasePalettes({ foo: { palette, extraPalette } } as any); + + expect(palette.refs).equal(0); + expect(extraPalette.refs).equal(0); + }); + }); +}); diff --git a/src/ts/tests/common/rect.spec.ts b/src/ts/tests/common/rect.spec.ts new file mode 100644 index 0000000..8e1cc54 --- /dev/null +++ b/src/ts/tests/common/rect.spec.ts @@ -0,0 +1,11 @@ +import '../lib'; +import { expect } from 'chai'; +import { rect, centerPoint } from '../../common/rect'; + +describe('rect', () => { + describe('centerPoint()', () => { + it('returns center point', () => { + expect(centerPoint(rect(10, 20, 20, 40))).eql({ x: 20, y: 40 }); + }); + }); +}); diff --git a/src/ts/tests/common/timeUtils.spec.ts b/src/ts/tests/common/timeUtils.spec.ts new file mode 100644 index 0000000..cdd7343 --- /dev/null +++ b/src/ts/tests/common/timeUtils.spec.ts @@ -0,0 +1,54 @@ +import '../lib'; +import { expect } from 'chai'; +import { + formatHourMinutes, getLightColor, HOUR_LENGTH, isDay, isNight, LightData, createLightData +} from '../../common/timeUtils'; +import { WHITE } from '../../common/colors'; +import { Season } from '../../common/interfaces'; + +const LIGHT_DAY = WHITE; +const LIGHT_NIGHT = 0x2b3374ff; + +describe('timeUtils', () => { + let lightData: LightData; + + beforeEach(() => { + lightData = createLightData(Season.Spring); + }); + + describe('getLightColor()', () => { + it('returns day light in day time', () => { + expect(getLightColor(lightData, 12 * HOUR_LENGTH)).equal(LIGHT_DAY); + }); + + it('returns night light in night time', () => { + expect(getLightColor(lightData, 0)).equal(LIGHT_NIGHT); + }); + }); + + describe('formatHourMinutes()', () => { + it('formats hour and minute for the in-game day', () => { + expect(formatHourMinutes(948765)).equal('07:54'); + }); + }); + + describe('isDay()', () => { + it('returns true if daytime', () => { + expect(isDay(12 * HOUR_LENGTH)).true; + }); + + it('returns falst if not daytime', () => { + expect(isDay(0)).false; + }); + }); + + describe('isNight()', () => { + it('returns true if not daytime', () => { + expect(isNight(12 * HOUR_LENGTH)).false; + }); + + it('returns falst if daytime', () => { + expect(isNight(0)).true; + }); + }); +}); diff --git a/src/ts/tests/common/utils.spec.ts b/src/ts/tests/common/utils.spec.ts new file mode 100644 index 0000000..0525a4a --- /dev/null +++ b/src/ts/tests/common/utils.spec.ts @@ -0,0 +1,456 @@ +import '../lib'; +import { expect } from 'chai'; +import { spy, assert, SinonFakeTimers, useFakeTimers } from 'sinon'; +import { + arraysEqual, removeItem, removeById, dispose, fromNow, clamp, normalize, + toInt, hasFlag, setFlag, at, findById, distance, contains, maxDate, minDate, + flatten, includes, isCommand, processCommand, collidersIntersect, formatDuration, att +} from '../../common/utils'; +import { rect } from '../../common/rect'; + +describe('utils', () => { + describe('fromNow()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('returns date object', () => { + expect(fromNow(0)).instanceof(Date); + }); + + it('returns current time for 0 offset', () => { + clock.setSystemTime(123); + + expect(fromNow(0).getTime()).equal(123); + }); + + it('returns current time offset by given amount', () => { + clock.setSystemTime(123); + + expect(fromNow(100).getTime()).equal(223); + }); + }); + + describe('maxDate()', () => { + it('returns larger of two dates', () => { + expect(maxDate(new Date(123), new Date(122))!.getTime()).equal(123); + }); + + it('returns larger of two dates (2)', () => { + expect(maxDate(new Date(123), new Date(124))!.getTime()).equal(124); + }); + + it('returns non-undefined date', () => { + expect(maxDate(new Date(123), undefined)!.getTime()).equal(123); + }); + + it('returns non-undefined date (2)', () => { + expect(maxDate(undefined, new Date(123))!.getTime()).equal(123); + }); + + it('returns undefined if both dates are undefined', () => { + expect(maxDate(undefined, undefined)).undefined; + }); + }); + + describe('minDate()', () => { + it('returns smaller of two dates', () => { + expect(minDate(new Date(123), new Date(122))!.getTime()).equal(122); + }); + + it('returns smaller of two dates (2)', () => { + expect(minDate(new Date(123), new Date(124))!.getTime()).equal(123); + }); + + it('returns non-undefined date', () => { + expect(minDate(new Date(123), undefined)!.getTime()).equal(123); + }); + + it('returns non-undefined date (2)', () => { + expect(minDate(undefined, new Date(123))!.getTime()).equal(123); + }); + + it('returns undefined if both dates are undefined', () => { + expect(minDate(undefined, undefined)).undefined; + }); + }); + + describe('formatDuration()', () => { + it('returns 0s for 0 duration', () => { + expect(formatDuration(0)).equal('0s'); + }); + + it('returns duration seconds', () => { + expect(formatDuration(15000)).equal('15s'); + }); + + it('returns duration minutes and seconds', () => { + expect(formatDuration(15 * 60 * 1000 + 6 * 1000)).equal('15m 6s'); + }); + + it('returns duration hours and minutes', () => { + expect(formatDuration(15 * 3600 * 1000 + 13 * 60 * 1000 + 6 * 1000)).equal('15h 13m'); + }); + + it('returns days and hours', () => { + expect(formatDuration((10 + 24 * 2) * 3600 * 1000 + 13 * 60 * 1000 + 6 * 1000)).equal('2d 10h'); + }); + }); + + describe('clamp()', () => { + it('returns given value if within range', () => { + expect(clamp(2, 1, 3)).equal(2); + }); + + it('returns minimum for value below minimum', () => { + expect(clamp(0, 1, 3)).equal(1); + }); + + it('returns maximum for value above maximum', () => { + expect(clamp(5, 1, 3)).equal(3); + }); + + it('returns minimum for NaN', () => { + expect(clamp(NaN, 1, 3)).equal(1); + }); + }); + + describe('normalize()', () => { + it('returns given values as vector', () => { + expect(normalize(1, 0)).eql({ x: 1, y: 0 }); + }); + + it('returns normalized vector', () => { + expect(normalize(0, -5)).eql({ x: 0, y: -1 }); + }); + }); + + describe('toInt()', () => { + it('returns given integer value', () => { + expect(toInt(1)).equal(1); + expect(toInt(-5)).equal(-5); + }); + + it('converts float number to integer value', () => { + expect(toInt(1.5)).eql(1); + expect(toInt(0.15)).eql(0); + expect(toInt(123.9)).eql(123); + }); + + it('converts string to integer value', () => { + expect(toInt('1.5')).eql(1); + expect(toInt('5')).eql(5); + }); + + it('converts null or undefined to 0', () => { + expect(toInt(null)).eql(0); + expect(toInt(undefined)).eql(0); + }); + + it('converts any object or array to 0', () => { + expect(toInt([])).eql(0); + expect(toInt({})).eql(0); + }); + }); + + describe('hasFlag()', () => { + enum Foo { + Aaa = 1, + Bbb = 2, + } + + it('returns true if flag is set', () => { + expect(hasFlag(Foo.Aaa, Foo.Aaa)).true; + }); + + it('returns true if flag is also set', () => { + expect(hasFlag(Foo.Aaa | Foo.Bbb, Foo.Aaa)).true; + }); + + it('returns false if flag is not set', () => { + expect(hasFlag(Foo.Bbb, Foo.Aaa)).false; + }); + }); + + describe('setFlag()', () => { + enum Foo { + Aaa = 1, + Bbb = 2, + } + + it('sets flag', () => { + expect(setFlag(0, Foo.Aaa, true)).equal(Foo.Aaa); + }); + + it('unsets flag', () => { + expect(setFlag(Foo.Aaa, Foo.Aaa, false)).equal(0); + }); + + it('does nothing if already set', () => { + expect(setFlag(Foo.Aaa, Foo.Aaa, true)).equal(Foo.Aaa); + }); + + it('does nothing if already unset', () => { + expect(setFlag(0, Foo.Aaa, false)).equal(0); + }); + + it('sets with another flag', () => { + expect(setFlag(Foo.Bbb, Foo.Aaa, true)).equal(Foo.Aaa | Foo.Bbb); + }); + + it('unsets flag with another flag', () => { + expect(setFlag(Foo.Bbb | Foo.Aaa, Foo.Aaa, false)).equal(Foo.Bbb); + }); + }); + + describe('includes()', () => { + it('returns true if array includes given element', () => { + expect(includes(['a', 'b', 'c'], 'b')).true; + }); + + it('returns false if array does not include given element', () => { + expect(includes(['a', 'b', 'c'], 'd')).false; + }); + + it('returns false if array is undefined', () => { + expect(includes(undefined, 'b')).false; + }); + }); + + describe('flatten()', () => { + it('returns empty array for ampty array', () => { + expect(flatten([])).eql([]); + }); + + it('flattens single array', () => { + expect(flatten([[1, 2, 3]])).eql([1, 2, 3]); + }); + + it('flattens multiple arrays', () => { + expect(flatten([[1, 2, 3], [4, 5]])).eql([1, 2, 3, 4, 5]); + }); + }); + + describe('at()', () => { + it('returns item at given index', () => { + expect(at(['a', 'b', 'c'], 1)).equal('b'); + }); + + it('clamps index to array length', () => { + expect(at(['a', 'b', 'c'], 4)).equal('c'); + }); + + it('clamps index to 0', () => { + expect(at(['a', 'b', 'c'], -1)).equal('a'); + }); + + it('converts index to integer', () => { + expect(at(['a', 'b', 'c'], '1.3')).equal('b'); + }); + + it('returns undefined for empty items list', () => { + expect(at([], 0)).undefined; + }); + }); + + describe('att()', () => { + it('returns item at given index', () => { + expect(att(['a', 'b', 'c'], 1)).equal('b'); + }); + + it('clamps index to array length', () => { + expect(att(['a', 'b', 'c'], 4)).equal('c'); + }); + + it('clamps index to 0', () => { + expect(att(['a', 'b', 'c'], -1)).equal('a'); + }); + + it('converts index to integer', () => { + expect(att(['a', 'b', 'c'], '1.3')).equal('b'); + }); + + it('returns undefined for undefined items list', () => { + expect(att(undefined, 0)).undefined; + }); + + it('returns undefined for null items list', () => { + expect(att(null, 0)).undefined; + }); + + it('returns undefined for empty items list', () => { + expect(att([], 0)).undefined; + }); + }); + + describe('findById()', () => { + it('returns item with given ID', () => { + expect(findById([{ id: 'a' }, { id: 'b' }, { id: 'c' }], 'b')).eql({ id: 'b' }); + }); + + it('returns undefined if not found', () => { + expect(findById([{ id: 'a' }, { id: 'b' }, { id: 'c' }], 'd')).undefined; + }); + }); + + describe('arraysEqual()', () => { + it('returns false for identical arrays', () => { + expect(arraysEqual([1, 2], [1, 2])).true; + }); + + it('returns false for different size arrays', () => { + expect(arraysEqual([1], [1, 2])).false; + }); + + it('returns false for different values in arrays', () => { + expect(arraysEqual([1, 3], [1, 2])).false; + }); + }); + + describe('removeItem()', () => { + it('removes given item', () => { + const array = [1, 2, 3]; + expect(removeItem(array, 2)).true; + expect(array).eql([1, 3]); + }); + + it('removes only first instance of element', () => { + const array = [1, 2, 3, 2]; + expect(removeItem(array, 2)).true; + expect(array).eql([1, 3, 2]); + }); + + it('does nothing for empty array', () => { + const array: any[] = []; + expect(removeItem(array, 2)).false; + expect(array).eql([]); + }); + + it('does nothing if iten does not exist', () => { + const array = [1, 2, 3]; + expect(removeItem(array, 5)).false; + expect(array).eql([1, 2, 3]); + }); + }); + + describe('removeById()', () => { + it('removes item with given ID', () => { + const x = { id: 2 }; + const array = [{ id: 1 }, x, { id: 3 }]; + expect(removeById(array, 2)).equal(x); + expect(array).eql([{ id: 1 }, { id: 3 }]); + }); + + it('removes only first instance of element', () => { + const array = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 2 }]; + removeById(array, 2); + expect(array).eql([{ id: 1 }, { id: 3 }, { id: 2 }]); + }); + + it('does nothing for empty array', () => { + const array: any[] = []; + expect(removeById(array, 2)).undefined; + expect(array).eql([]); + }); + + it('does nothing if iten does not exist', () => { + const array = [{ id: 1 }, { id: 2 }, { id: 3 }]; + expect(removeById(array, 5)).undefined; + expect(array).eql([{ id: 1 }, { id: 2 }, { id: 3 }]); + }); + }); + + describe('dispose()', () => { + it('calls dispose method on give object', () => { + const disp = spy(); + dispose({ dispose: disp }); + assert.called(disp); + }); + + it('returns undefined', () => { + expect(dispose({ dispose() { return 5; } })).undefined; + }); + + it('does nothing for undefined', () => { + dispose(undefined); + }); + }); + + describe('distance()', () => { + it('returns distance between points', () => { + expect(distance({ x: 10, y: 0 }, { x: 20, y: 0 })).equal(10); + }); + }); + + describe('contains()', () => { + it('returns true if point is inside moved bounds', () => { + expect(contains(10, 10, { x: 0, y: 0, w: 100, h: 100 }, { x: 12, y: 12 })).true; + }); + + it('returns false if point is not inside moved bounds', () => { + expect(contains(10, 10, { x: 0, y: 0, w: 100, h: 100 }, { x: 0, y: 0 })).false; + }); + }); + + describe('collidersIntersect()', () => { + it('returns true if two colliders intersect', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 0, 0, rect(15, 15, 20, 20))).true; + }); + + it('returns true if two colliders intersect when moved', () => { + expect(collidersIntersect(15, 15, rect(10, 10, 10, 10), 0, 0, rect(30, 30, 10, 10))).true; + expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), -15, -15, rect(30, 30, 10, 10))).true; + }); + + it('returns true if two colliders do not intersect', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 0, 0, rect(50, 50, 20, 20))).false; + }); + + it('returns true if two colliders touch edges', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), 0, 0, rect(20, 10, 20, 10))).false; + expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), 0, 0, rect(10, 20, 10, 20))).false; + }); + + it('returns false if two colliders do not intersect when moved', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 20, 20, rect(15, 15, 20, 20))).false; + }); + }); + + describe('isCommand()', () => { + it('returns false for regular text', () => { + expect(isCommand('hello')).false; + }); + + it('returns true for text starting with /', () => { + expect(isCommand('/test')).true; + }); + + it('returns true for "/"', () => { + expect(isCommand('/')).true; + }); + }); + + describe('processCommand()', () => { + it('returns command and args', () => { + expect(processCommand('/foo bar')).eql({ command: 'foo', args: 'bar' }); + }); + + it('parses command without args', () => { + expect(processCommand('/foo')).eql({ command: 'foo', args: '' }); + }); + + it('trims command name', () => { + expect(processCommand('/foo ')).eql({ command: 'foo', args: '' }); + }); + + it('trims args', () => { + expect(processCommand('/foo bar ')).eql({ command: 'foo', args: 'bar' }); + }); + }); +}); diff --git a/src/ts/tests/generated/sprites.spec.ts b/src/ts/tests/generated/sprites.spec.ts new file mode 100644 index 0000000..84f57f5 --- /dev/null +++ b/src/ts/tests/generated/sprites.spec.ts @@ -0,0 +1,52 @@ +import '../lib'; +import { expect } from 'chai'; +import { map } from 'lodash'; +import * as sprites from '../../generated/sprites'; + +/* tslint:disable */ +const sets: [keyof typeof sprites, number, number, (number[] | null)[]][] = [ + // name, index, frames, expected patterns colors counts + // ['topManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], [3, 9, 9, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], [3, 9, 7, 5, 5], [3, 11, 9, 7, 5], [3, 11, 7, 5, 5], [3, 7], [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], [3, 11, 13, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5]]], + ['topManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], [3, 9, 9, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], [3, 9, 7, 5, 5], [3, 11, 9, 7, 5], [3, 11, 7, 5, 5], [3, 7], [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], [3, 11, 13, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 13, 5, 11, 5], [3, 7, 5, 7, 5, 5, 7]]], + // ['frontManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], null, [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, [3, 11, 9, 7, 5], null, null, [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], null, [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5]]], + ['frontManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], null, [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, [3, 11, 9, 7, 5], null, null, [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], null, [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5], [3, 11, 11, 7, 5, 5], null, null, [3, 11, 13, 7, 5, 5], null, [3, 7, 5, 7, 5, 5, 7]]], + // ['behindManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], null, [3, 13, 13, 5, 5, 7], null, null, null, null, null, [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, null, null, null, null, [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, null, null, [3, 7, 5, 13, 5]]], + ['behindManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], null, [3, 13, 13, 5, 5, 7], null, null, null, null, null, [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, null, null, null, null, [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, null, null, [3, 7, 5, 13, 5], null, [3, 9, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], null, [3, 13, 5, 11, 5], []]], + // ['backFrontManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], null, [3, 9, 9, 7, 5, 5], null, null, null, null, [3, 11, 11, 5], null, [3, 7, 7, 5, 5], [3, 9, 9, 7, 5], null, null, [3, 7, 9, 5, 5, 5], [3, 7, 13, 5, 5, 5], [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], null, [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5]]], + ['backFrontManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], null, [3, 9, 9, 7, 5, 5], null, null, null, null, [3, 11, 11, 5], null, [3, 7, 7, 5, 5], [3, 9, 9, 7, 5], null, null, [3, 7, 9, 5, 5, 5], [3, 7, 13, 5, 5, 5], [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], null, [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5], [], [], [], [], [], []]], + // ['backBehindManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], [5, 13, 11, 9, 7, 7], null, [5, 11, 11, 7, 7, 7], [3, 7, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 9, 5, 5, 5], null, [3, 5, 11, 5], null, null, [3, 5, 5, 5], [3, 11, 7, 5, 5], null, null, [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], [3, 7, 13, 7, 5, 5], [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5]]], + ['backBehindManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], [5, 13, 11, 9, 7, 7], null, [5, 11, 11, 7, 7, 7], [3, 7, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 9, 5, 5, 5], null, [3, 5, 11, 5], null, null, [3, 5, 5, 5], [3, 11, 7, 5, 5], null, null, [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], [3, 7, 13, 7, 5, 5], [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5], [3, 7, 7, 5], [3, 11, 7, 5, 5], [3, 11, 7, 7, 5], [3, 11, 7, 7, 5, 5, 5], [3], [3, 9, 7, 5, 5]]], + ['horns', -1, 1, [null, [3, 7], [3, 5], [3, 7], [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], [3, 9]]], + ['hornsBehind', -1, 1, [null, null, null, null, [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], []]], + ['ears', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], + ['earsFar', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], + ['frontLegHooves', 1, 39, [null, [3], [3], [5], [5], [5], [3]]], + ['backLegHooves', 1, 27, [null, [3], [3], [5], [3]]], + ['wings', 1, 13, [null, [3, 5, 9], [5], [3, 5, 9], [3]]], + ['tails', 0, 3, [null, [3, 11, 13, 7, 5, 5], [3, 13, 9, 7, 5, 5], [3, 11, 9, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5, 5], [5, 13, 11, 7, 7], [3, 11, 13, 5, 7, 5], [3, 11, 11, 7, 5, 5, 11], [3, 11, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 5, 9, 5], [5, 11, 9, 7, 7], [5, 7], [3, 5, 7], [5, 11, 9, 7, 7], [7, 13, 11, 9, 9], [5, 7], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 13, 7], [3, 5, 13, 13, 11, 11], [3, 9, 13, 13, 13, 11], [3, 7, 5, 13, 5], [5, 13, 9, 9, 7, 7], [5, 13, 9, 9, 7, 7], [3, 5, 7]]], + ['noses', 0, 26, [[3], [3], [5]]], + ['facialHair', -1, 1, [null, null, null, [3], [3, 7, 9], [3, 5], null, [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], + ['facialHairBehind', -1, 1, [null, [3], [3, 7], null, [3, 7, 9], [3, 5], [3, 7, 9], [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], + ['headAccessoriesBehind', -1, 1, [null, [3, 5], [3, 9, 9, 9], [3, 5, 7, 9, 7], [3, 5], [3, 7], [3, 7, 13], [3, 11, 11], [3, 11, 13], [3, 11, 7, 9, 9], [3, 9, 13], [5, 11, 13], [3, 7], [3, 9, 13], [3, 9], [3, 9], [3, 7], [3, 7], [3, 7, 5, 5], [3, 7, 11]]], + ['neckAccessories', 1, 16, [null, [3, 5], [3, 5, 9, 5], [3, 5, 7, 13, 7], [3, 5], [3, 7, 7, 5, 5], [3, 5, 7], [3, 5], [3], [3], [3, 5, 9, 5], [3, 5, 9], [3, 9], [3], [3]]], + ['frontLegAccessories', 1, 39, [null, [3, 13, 13, 13, 13]]], + ['backLegAccessories', 1, 27, [null, [3, 13, 13, 13, 13]]], + ['chestAccessories', 1, 16, [null, [3, 5, 9], [3, 7, 5, 5, 7, 5, 7, 5, 7, 11, 9, 5, 11], [5, 7, 11], [5, 7]]], + ['backAccessories', 1, 16, [null, [3], [3, 7, 13, 5], [3, 7, 13, 5], [3, 9, 13, 5], [3, 3]]], + ['waistAccessories', 1, 17, [null, [9], [9], [9]]], + ['earAccessories', -1, 1, [null, [3], [3], [3], [3], [5], [3, 11], [3], [3], [3], [3, 7, 7, 7], [3, 11, 11, 11], [3]]], + ['earAccessoriesBehind', -1, 1, [null, null, null, null, null, null, null, null, [3], null, [3, 7, 7, 7], [3, 11, 11, 11], []]], + ['extraAccessories', -1, 1, [[11], [5], [5], [7], [9], [13], [5], [11], [9], [11, 11], [9], [9, 9], [7], [9], null, null, [13], [13]]], + ['extraAccessoriesBehind', -1, 1, [[11], [5], null, [7], [9], null, null, null, null, null, null, null, [7], [9], [5], [5], [13], []]], +]; + +describe('sprites', () => { + describe('pony', () => { + sets.map(([name, index, frames, expectedPatterns]) => it(name, () => { + const field = sprites[name] as any; + const counts = map(index !== -1 ? field[index] : field, (x: any[] | undefined) => x ? x.map(y => y.colors) : null); + expect(index === -1 ? 1 : field.length).eql(frames, 'frame count'); + expect(counts).eql(expectedPatterns, `\n\nACT: ${JSON.stringify(counts)}\nEXP: ${JSON.stringify(expectedPatterns)}\n\n`); + })); + }); +}); diff --git a/src/ts/tests/graphics/graphicsUtils.spec.ts b/src/ts/tests/graphics/graphicsUtils.spec.ts new file mode 100644 index 0000000..9cd170d --- /dev/null +++ b/src/ts/tests/graphics/graphicsUtils.spec.ts @@ -0,0 +1,189 @@ +import { compareCanvases, loadSprites, loadImageAsCanvas, clearCompareResults } from '../lib'; +import * as path from 'path'; +import { SpriteBatch, MessageType, PaletteSpriteBatch } from '../../common/interfaces'; +import { rect } from '../../common/rect'; +import { + drawBaloon, drawNamePlate, drawPixelText, drawOutline, drawBounds, createCommonPalettes, DrawNameFlags +} from '../../graphics/graphicsUtils'; +import { drawCanvas } from '../../graphics/contextSpriteBatch'; +import { ORANGE } from '../../common/colors'; +import { entity } from '../mocks'; +import { pathTo } from '../../server/paths'; +import { paletteSpriteSheet } from '../../generated/sprites'; +import { mockPaletteManager } from '../../common/ponyInfo'; + +const baseFilePath = pathTo('src', 'tests', 'graphics'); +const palettes = createCommonPalettes(mockPaletteManager); +const created = Date.now(); + +function test(file: string, draw: (batch: SpriteBatch & PaletteSpriteBatch) => void) { + return () => { + const filePath = path.join(baseFilePath, file); + const expected = loadImageAsCanvas(filePath); + const actual = drawCanvas(100, 50, paletteSpriteSheet, undefined, draw); + compareCanvases(expected, actual, filePath, 'graphics'); + }; +} + +describe('graphicsUtils', () => { + before(loadSprites); + before(() => clearCompareResults('graphics')); + + describe('drawBaloon()', () => { + it('draws regular text', test('hello.png', batch => { + drawBaloon(batch, { message: 'hello', created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws regular text with emoji', test('hello-apple.png', batch => { + drawBaloon(batch, { message: 'hello 🍎', created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws text with new lines', test('newline.png', batch => { + drawBaloon(batch, { message: 'hello\nworld!', created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('does not draw if outside bounds', test('outside.png', batch => { + drawBaloon(batch, { message: 'cant see me', created }, 50, 25, rect(0, 0, 10, 10), palettes); + })); + + it('draws party message', test('party.png', batch => { + drawBaloon(batch, { message: 'party', type: MessageType.Party, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws moderator message', test('mod.png', batch => { + drawBaloon(batch, { message: 'moderator', type: MessageType.Mod, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws admin message', test('admin.png', batch => { + drawBaloon(batch, { message: 'admin', type: MessageType.Admin, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws supporter1 message', test('sup1.png', batch => { + drawBaloon(batch, { message: 'supporter 1', type: MessageType.Supporter1, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws supporter2 message', test('sup2.png', batch => { + drawBaloon(batch, { message: 'supporter 2', type: MessageType.Supporter2, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws supporter3 message', test('sup3.png', batch => { + drawBaloon(batch, { message: 'supporter 3', type: MessageType.Supporter3, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws announcement message', test('announcement.png', batch => { + drawBaloon( + batch, { message: 'announcement', type: MessageType.Announcement, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws party announcement message', test('party-announcement.png', batch => { + drawBaloon( + batch, { message: 'announcement', type: MessageType.PartyAnnouncement, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws system message', test('system.png', batch => { + drawBaloon(batch, { message: 'system', type: MessageType.System, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws thinking bubble', test('thinking.png', batch => { + drawBaloon(batch, { message: 'thinking...', type: MessageType.Thinking, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('draws party thinking bubble', test('party-thinking.png', batch => { + drawBaloon( + batch, { message: 'party thinking...', type: MessageType.PartyThinking, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('fades in text bubble (time: 10)', test('fade-in-0.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 10, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('fades in text bubble (time: 9.95)', test('fade-in-1.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 9.95, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('fades in text bubble (time: 9.7)', test('fade-in-2.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 9.7, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('fades out text bubble (time: 0)', test('fade-out-0.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 0, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('fades out text bubble (time: 0.05)', test('fade-out-1.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 0.05, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it('fades out text bubble (time: 0.3)', test('fade-out-2.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 0.3, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + const chatTypes: [string, MessageType][] = [ + ['chat', MessageType.Chat], + ['think', MessageType.Thinking], + ]; + + chatTypes.forEach(([name, type]) => { + it(`breaks words on small screen (${name})`, test(`break-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit in one line', type, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); + + it(`moves ballon right to fit on screen (${name})`, test(`move-right-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, 25, 25, rect(0, 0, 100, 100), palettes); + })); + + it(`moves ballon left to fit on screen (${name})`, test(`move-left-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, 75, 25, rect(0, 0, 100, 100), palettes); + })); + + it(`does not move baloon originating from outside bounds (${name})`, test(`outside-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, -25, 25, rect(0, 0, 100, 100), palettes); + })); + + it(`adjusts nipple when moving baloon (${name})`, test(`move-right-nipple-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, 1, 25, rect(0, 0, 100, 100), palettes); + })); + }); + }); + + describe('drawBounds()', () => { + it('draws bounds', test('bounds.png', batch => { + drawBounds(batch, entity(0, 0.1, 0.2), rect(5, 6, 25, 20), ORANGE); + })); + + it('draws nothing if rect is missing', test('bounds-none.png', batch => { + drawBounds(batch, entity(0, 0.1, 0.2), undefined, ORANGE); + })); + }); + + describe('drawOutline()', () => { + it('draws outline', test('outline.png', batch => { + drawOutline(batch, ORANGE, 10, 15, 30, 20); + })); + }); + + describe('drawNamePlate()', () => { + it('draws name', test('name.png', batch => { + drawNamePlate(batch, 'name', 50, 25, DrawNameFlags.None, palettes, undefined); + })); + + it('draws name with emoji', test('name-apple.png', batch => { + drawNamePlate(batch, 'name 🍎', 50, 25, DrawNameFlags.None, palettes, undefined); + })); + + it('draws party member name', test('name-party.png', batch => { + drawNamePlate(batch, 'party', 50, 25, DrawNameFlags.Party, palettes, undefined); + })); + + const tags = ['mod', 'dev', 'sup1', 'sup2', 'sup3']; + + tags.forEach(tag => it(`draws name with ${tag} tag`, test(`name-${tag}.png`, batch => { + drawNamePlate(batch, `A ${tag}`, 50, 25, DrawNameFlags.None, palettes, tag); + }))); + }); + + describe('drawPixelText()', () => { + it('draws numbers', test('pixel-numbers.png', batch => { + drawPixelText(batch, 20, 20, 0x20B2AAff, '0 123456789X'); + })); + }); +}); diff --git a/src/ts/tests/graphics/spriteSheetUtils.spec.ts b/src/ts/tests/graphics/spriteSheetUtils.spec.ts new file mode 100644 index 0000000..cd80f86 --- /dev/null +++ b/src/ts/tests/graphics/spriteSheetUtils.spec.ts @@ -0,0 +1,72 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, assert } from 'sinon'; +import { createTexturesForSpriteSheets } from '../../graphics/spriteSheetUtils'; +import { createCanvas } from '../../client/canvasUtils'; +import { SpriteSheet } from '../../common/interfaces'; + +function createImageData() { + return createCanvas(10, 10).getContext('2d')!.getImageData(0, 0, 10, 10); +} + +describe('spriteSheetUtils', () => { + describe('createTexturesForSpriteSheets()', () => { + it('creates texture from image', () => { + const gl = {} as any; + const data = createImageData(); + const tex = {} as any; + const createTexture = stub().returns(tex); + const sheet: SpriteSheet[] = [ + { sprites: [], src: 'foo', data, texture: undefined, palette: false }, + ]; + + createTexturesForSpriteSheets(gl, sheet, createTexture); + + assert.calledWith(createTexture, gl, data); + expect(sheet[0].texture).equal(tex); + }); + + it('handles empty sprites', () => { + const createTexture = stub().returns({}); + const sheet: SpriteSheet[] = [ + { sprites: [undefined] as any, src: 'foo', data: createImageData(), texture: undefined, palette: false }, + ]; + + createTexturesForSpriteSheets({} as any, sheet, createTexture); + }); + }); + + // describe('releaseTexturesForSpriteSheets()', () => { + // it('disposes textures', () => { + // const deleteTexture = stub(); + // const sheet: SpriteSheet[] = [ + // { sprites: [], src: 'foo', texture: { gl: { deleteTexture } } as any, palette: false }, + // ]; + + // disposeTexturesForSpriteSheets(sheet); + + // assert.calledOnce(deleteTexture); + // expect(sheet[0].texture).undefined; + // }); + + // it('handles empty texture', () => { + // const sheet: SpriteSheet[] = [ + // { sprites: [], src: 'foo', texture: undefined, palette: false }, + // ]; + + // disposeTexturesForSpriteSheets(sheet); + // }); + + // it('handles empty sprites', () => { + // const sheet: SpriteSheet[] = [ + // { sprites: [undefined], src: 'foo', texture: undefined, palette: false }, + // ]; + + // disposeTexturesForSpriteSheets(sheet); + // }); + + // it('does nothing for empty list', () => { + // disposeTexturesForSpriteSheets([]); + // }); + // }); +}); diff --git a/src/ts/tests/lib.ts b/src/ts/tests/lib.ts new file mode 100644 index 0000000..0276b23 --- /dev/null +++ b/src/ts/tests/lib.ts @@ -0,0 +1,136 @@ +/// + +import '../server/boot'; +import * as mongoose from 'mongoose'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as del from 'del'; +import { once, mapValues } from 'lodash'; +import { spawnSync } from 'child_process'; +import { createStubInstance, SinonStubbedInstance, stub } from 'sinon'; +import { loadAndInitSheets } from '../client/spriteUtils'; +import '../server/canvasUtilsNode'; +import { setPaletteManager } from '../common/mixins'; +import { mockPaletteManager } from '../common/ponyInfo'; +import { spriteSheets } from '../generated/sprites'; +import { pathTo } from '../server/paths'; +import { loadImage, loadImageSync, createCanvas } from '../server/canvasUtilsNode'; + +require('chai').use(require('chai-as-promised')); + +(mongoose as any).models = {}; +(mongoose as any).modelSchemas = {}; +(global as any).TESTS = true; +(global as any).TOOLS = true; +(global as any).performance = Date; + +setPaletteManager(mockPaletteManager); + +export function loadImageServer(src: string) { + return loadImage(pathTo('assets', src)); +} + +export const loadSprites = once(() => loadAndInitSheets(spriteSheets, loadImageServer)); + +export function loadImageAsCanvas(filePath: string): HTMLCanvasElement { + try { + const image = loadImageSync(filePath); + const expected = createCanvas(image.width, image.height); + expected.getContext('2d')!.drawImage(image, 0, 0); + return expected; + } catch (e) { + console.error(e); + } + + return createCanvas(0, 0); +} + +export function generateDiff(expectedPath: string, actualPath: string) { + spawnSync( + 'magick', ['compare', actualPath, expectedPath, actualPath.replace(/\.png$/, '-diff.png')], { encoding: 'utf8' }); +} + +export async function clearCompareResults(group: string) { + await del([pathTo('tools', 'temp', group, '*.png').replace(/\\/g, '/')]); +} + +export function compareCanvases( + expected: HTMLCanvasElement | undefined, actual: HTMLCanvasElement | undefined, + filePath: string, group: string, diff = true +) { + try { + if (expected === actual) + return; + if (!expected) + throw new Error(`Expected canvas is null`); + if (!actual) + throw new Error(`Actual canvas is null`); + if (expected.width !== actual.width || expected.height !== actual.height) + throw new Error(`Canvas size is different than expected`); + + const expectedData = expected.getContext('2d')!.getImageData(0, 0, expected.width, expected.height); + const actualData = actual.getContext('2d')!.getImageData(0, 0, actual.width, actual.height); + const length = expectedData.width * expectedData.height * 4; + + for (let i = 0; i < length; i++) { + if (expectedData.data[i] !== actualData.data[i]) { + const x = Math.floor(i / 4) % actualData.width; + const y = Math.floor((i / 4) / actualData.width); + throw new Error(`Actual canvas different than expected at (${x}, ${y})`); + } + } + } catch (e) { + if (actual && diff) { + const tempRoot = pathTo('tools', 'temp', group); + const tempPath = path.join(tempRoot, filePath ? path.basename(filePath) : `${Date.now()}-failed-test.png`); + fs.writeFileSync(tempPath, actual.toBuffer()); + + if (filePath) { + generateDiff(filePath, tempPath); + } + } + + throw e; + } +} + +const testsPath = pathTo('src', 'tests', 'filters'); + +export function readTestsFile(fileName: string): string[] { + const lines = fs.readFileSync(path.join(testsPath, fileName), 'utf8') + .split(/\r?\n/g) + .map(x => x.trim()) + .filter(x => !!x); + + for (let i = lines.length - 1; i > 0; i--) { + if (lines.indexOf(lines[i]) < i) { + console.error(`Duplicate line "${lines[i]}" in ${fileName}`); + } + } + + return lines; +} + +export function createFunctionWithPromiseHandler(ctor: any, ...deps: any[]): any { + return (...args: any[]) => { + let result: any; + const func = ctor(...deps, (promise: Promise, handleError: any) => result = promise.catch(handleError)); + func(...args); + return result; + }; +} + +export function stubClass(ctor: new (...args: any[]) => T): SinonStubbedInstance { + return createStubInstance(ctor) as any; +} + +export function stubFromInstance(instance: any): SinonStubbedInstance { + return mapValues(instance, () => stub()) as any; +} + +export function resetStubMethods(stub: SinonStubbedInstance, ...methods: (keyof T)[]) { + methods.forEach(method => { + (stub[method] as any).resetBehavior(); + (stub[method] as any).reset(); + }); +} diff --git a/src/ts/tests/mocks.ts b/src/ts/tests/mocks.ts new file mode 100644 index 0000000..38a65fd --- /dev/null +++ b/src/ts/tests/mocks.ts @@ -0,0 +1,150 @@ +import { mapValues } from 'lodash'; +import { Types } from 'mongoose'; +import { Subject } from 'rxjs'; +import { SinonStubbedInstance, stub } from 'sinon'; +import { IClient, Reporter, ServerEntity, ServerRegion } from '../server/serverInterfaces'; +import { IAccount, ICharacter, IAuth } from '../server/db'; +import { ClientActions } from '../client/clientActions'; +import { Entity, IMap, defaultWorldState } from '../common/interfaces'; +import { createServerMap } from '../server/serverMap'; +import { createCamera } from '../common/camera'; +import { getRegionGlobal } from '../common/worldMap'; +import { mixColliderRect } from '../common/mixins'; +import { createBinaryWriter } from 'ag-sockets'; +import { PONY_TYPE } from '../common/constants'; + +export function auth(item: Partial): IAuth { + return item as IAuth; +} + +export function account(item: Partial): IAccount { + return item as IAccount; +} + +export function character(item: Partial): ICharacter { + return item as ICharacter; +} + +export function mock(ctor: new (...args: any[]) => T, fields: any = {}): T { + const object: any = {}; + const prototype = ctor.prototype; + + Object.getOwnPropertyNames(prototype) + .filter(key => !Object.getOwnPropertyDescriptor(prototype, key)!.get && typeof prototype[key] === 'function') + .forEach(key => object[key] = function () { }); + + return Object.assign(object, fields); +} + +export function entity(id: number, x = 0, y = 0, type = 0, more: Partial = {}): Entity { + return { + id, x, y, z: 0, vx: 0, vy: 0, type, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0, + options: {}, ...more + }; +} + +export function serverEntity(id: number, x = 0, y = 0, type = 0, more: Partial = {}): ServerEntity { + return { + id, x, y, z: 0, vx: 0, vy: 0, type, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0, + options: {}, ...more + }; +} + +export function clientPony(): ServerEntity { + return mockClient().pony; +} + +let id = 1; +let ponyId = 1; + +export function genId() { + return (++id).toString(16).padStart(24, '0'); +} + +export function genObjectId() { + return Types.ObjectId(genId()); +} + +export function mockClient(fields: any = {}): IClient { + const pony = entity(++ponyId, 0, 0, PONY_TYPE); + const accountId = genId(); + const characterId = genId(); + + pony.options = {}; + + const partial: Partial = { + accountId, + characterId, + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + friends: new Set(), + accountSettings: {}, + originalRequest: { headers: {} }, + account: { id: accountId, _id: Types.ObjectId(accountId), ignores: [] }, + character: { id: characterId, _id: Types.ObjectId(characterId) }, + isMod: false, + pony, + map: createServerMap('', 0, 1, 1), + notifications: [], + regions: [], + updateQueue: createBinaryWriter(128), + regionUpdates: [], + unsubscribes: [], + subscribes: [], + saysQueue: [], + lastSays: [], + lastAction: 0, + lastBoopAction: 0, + lastExpressionAction: 0, + viewWidth: 3, + viewHeight: 3, + screenSize: { width: 20, height: 20 }, + reporter: mockReporter(), + camera: createCamera(), + reportInviteLimit() { }, + disconnect() { }, + ...fields, + }; + + const client = mock(ClientActions, partial) as IClient; + + client.pony.client = client; + return client; +} + +export function mockReporter(): Reporter { + return { + info() { }, + warn() { }, + warnLog() { }, + danger() { }, + error() { }, + system() { }, + systemLog() { }, + setPony() { }, + }; +} + +export type MockSubject = Subject & { values: (T | undefined)[]; }; + +export function mockSubject(): MockSubject { + const values: (T | undefined)[] = []; + + return { + values, + next(value?: T) { + values.push(value); + }, + } as any; +} + +export function createStubFromInstance(instance: T): SinonStubbedInstance { + return mapValues(instance, () => stub()) as any; +} + +export function setupCollider(map: IMap, x: number, y: number) { + const entity = serverEntity(0, x, y, 0); + mixColliderRect(-16, -12, 32, 24)(entity, {}, defaultWorldState); + getRegionGlobal(map, x, y).colliders.push(entity); +} diff --git a/src/ts/tests/server/accountUtils.spec.ts b/src/ts/tests/server/accountUtils.spec.ts new file mode 100644 index 0000000..30b972e --- /dev/null +++ b/src/ts/tests/server/accountUtils.spec.ts @@ -0,0 +1,89 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, assert, SinonStub } from 'sinon'; +import { DAY } from '../../common/constants'; +import { fromNow } from '../../common/utils'; +import { getModInfo, checkIfNotAdmin, isNew } from '../../server/accountUtils'; +import { account, mockClient, genObjectId } from '../mocks'; +import { logger } from '../../server/logger'; + +describe('accountUtils [server]', () => { + describe('getModInfo()', () => { + it('returns account info', () => { + const accountId = genObjectId(); + const client = mockClient({ + accountId: accountId.toString(), + account: account({ + _id: accountId, + name: 'foo', + shadow: -1, + mute: fromNow(1.1 * DAY).getTime(), + note: 'foo', + counters: { spam: 1 }, + }), + country: 'XY', + }); + + expect(getModInfo(client)).eql({ + shadow: 'perma', + mute: 'a day', + note: 'foo', + counters: { spam: 1 }, + country: 'XY', + account: `foo [${accountId.toString().substr(-3)}]`, + }); + }); + + it('returns undefined for past timeouts', () => { + const client = mockClient({ + account: account({ + _id: genObjectId(), + shadow: 1000, + mute: 2000, + }), + }); + + const result = getModInfo(client); + + expect(result.mute).undefined; + expect(result.shadow).undefined; + }); + }); + + describe('checkIfAdmin()', () => { + let warn: SinonStub; + + beforeEach(() => { + warn = stub(logger, 'warn'); + }); + + afterEach(() => { + warn.restore(); + }); + + it('does nothing if not admin', () => { + checkIfNotAdmin({} as any, ''); + }); + + it('throws if admin', () => { + expect(() => checkIfNotAdmin({ roles: ['admin'] } as any, 'test')) + .throw('Cannot perform this action on admin user'); + + assert.calledWith(warn, 'Cannot perform this action on admin user (test)'); + }); + }); + + describe('isNew()', () => { + it('returns true if createdAt date is not set', () => { + expect(isNew(account({}))).true; + }); + + it('returns true if created less than a day ago', () => { + expect(isNew(account({ createdAt: fromNow(-DAY + 1000) }))).true; + }); + + it('returns false if created more than a day ago', () => { + expect(isNew(account({ createdAt: fromNow(-2 * DAY) }))).false; + }); + }); +}); diff --git a/src/ts/tests/server/api/account.spec.ts b/src/ts/tests/server/api/account.spec.ts new file mode 100644 index 0000000..d1b8a83 --- /dev/null +++ b/src/ts/tests/server/api/account.spec.ts @@ -0,0 +1,445 @@ +import '../../lib'; +import { expect } from 'chai'; +import { assert, stub, SinonStub } from 'sinon'; +import { + createUpdateAccount, UpdateAccount, createRemoveSite, RemoveSite, createUpdateSettings, UpdateSettings, + createGetAccountCharacters, GetAccountCharacters, GetAccountData, createGetAccountData, modCheck +} from '../../../server/api/account'; +import { account, genObjectId } from '../../mocks'; +import * as db from '../../../server/db'; + +describe('api account', () => { + describe('getAccountData()', () => { + let findCharacters: SinonStub; + let findAuths: SinonStub; + let getAccountData: GetAccountData; + let findFriends: SinonStub; + + beforeEach(() => { + findCharacters = stub(); + findAuths = stub(); + findFriends = stub(db, 'findFriends').resolves([]); + getAccountData = createGetAccountData(findCharacters, findAuths); + }); + + afterEach(() => { + findFriends.restore(); + }); + + it('returns account data', async () => { + findCharacters.resolves([]); + findAuths.resolves([]); + const _id = genObjectId(); + + const result = await getAccountData(account({ _id, name: 'foo', birthdate: new Date(123), characterCount: 5 })); + + expect(result).eql({ + id: _id.toString(), + name: 'foo', + birthdate: '1970-01-01', + birthyear: undefined, + characterCount: 5, + ponies: [], + settings: {}, + sites: [], + alert: undefined, + supporter: undefined, + flags: 0, + roles: undefined, + }); + }); + + it('adds mod check if account is mod', async () => { + findCharacters.resolves([]); + findAuths.resolves([]); + + const result = await getAccountData(account({ _id: genObjectId(), roles: ['mod'] })); + + expect(result.check).eql(modCheck); + }); + }); + + describe('getAccountCharacters()', () => { + let findCharacters: SinonStub; + let getAccountCharacters: GetAccountCharacters; + + beforeEach(() => { + findCharacters = stub(); + getAccountCharacters = createGetAccountCharacters(findCharacters); + }); + + it('returns empty array', async () => { + findCharacters.resolves([]); + + const result = await getAccountCharacters(account({})); + + expect(result).eql([]); + }); + + it('returns chracters array', async () => { + const accountId = genObjectId(); + const characterId = genObjectId(); + findCharacters.withArgs(accountId).resolves([ + { + _id: characterId, + name: 'foo', + info: 'info', + lastUsed: new Date(123), + }, + ]); + + const result = await getAccountCharacters(account({ _id: accountId })); + + expect(result).eql([ + { + id: characterId.toString(), + name: 'foo', + desc: '', + info: 'info', + lastUsed: '1970-01-01T00:00:00.123Z', + site: undefined, + tag: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }, + ]); + }); + }); + + describe('updateAccount()', () => { + let findAccount: SinonStub; + let updateAccount: UpdateAccount; + let updateOne: SinonStub; + let log: SinonStub; + + beforeEach(() => { + findAccount = stub(); + log = stub(); + updateOne = stub(db.Account, 'updateOne').returns({ exec: stub().resolves() } as any); + updateAccount = createUpdateAccount(findAccount, log); + }); + + afterEach(() => { + updateOne.restore(); + }); + + it('resolves to account data', async () => { + const account = { + _id: genObjectId(), + name: 'name', + birthdate: new Date(123), + birthyear: 123, + roles: ['role'], + settings: { foo: 'bar' }, + characterCount: 5, + save: stub(), + } as any; + findAccount.withArgs(account._id).resolves(account); + + const result = await updateAccount(account, {} as any); + + expect(result).eql({ + id: account._id.toString(), + name: 'name', + birthdate: '1970-01-01', + birthyear: 123, + roles: ['role'], + settings: { foo: 'bar' }, + supporter: undefined, + characterCount: 5, + flags: 0, + }); + }); + + it('saves account', async () => { + const acc = account({ _id: genObjectId() }); + findAccount.resolves(acc); + + await updateAccount(acc, {} as any); + + assert.calledWith(updateOne, { _id: acc._id }, {}); + }); + + it('updates account name', async () => { + const acc = account({ _id: genObjectId() }); + findAccount.resolves(acc); + + await updateAccount(acc, { name: 'foo', birthdate: '' }); + + expect(acc.name).equal('foo'); + }); + + it('logs account rename', async () => { + const acc = account({ _id: genObjectId(), name: 'bar' }); + findAccount.resolves(acc); + + await updateAccount(acc, { name: 'foo', birthdate: '' }); + + assert.calledWith(log, acc._id, 'Renamed "bar" => "foo"'); + }); + + it('cleans account name before updating', async () => { + const acc = account({ _id: genObjectId() }); + findAccount.resolves(acc); + + await updateAccount(acc, { name: 'f\t\r\noo', birthdate: '' }); + + expect(acc.name).equal('foo'); + }); + + const invalidNameValues: any[] = [ + '', + // 'a', + 'string_that_is_exceeding_character_limit_for_account_names_aaaaaaaaaaaaaaaaaaaaaaaaa', + { foo: 'bar' }, + 123, + null, + ]; + + invalidNameValues.forEach(name => it(`does not update account name if it is invalid (${name})`, async () => { + const account = { _id: genObjectId(), name: 'name', save: stub() } as any; + findAccount.resolves(account); + + await updateAccount(account, { name, birthdate: '' }); + + expect(account.name).equal('name'); + })); + + it('updates account birthdate', async () => { + const account = { _id: genObjectId(), save: stub() } as any; + findAccount.resolves(account); + + await updateAccount(account, { name: 'foo', birthdate: '2000-02-03' }); + + expect(account.birthdate.getTime()).equal(new Date('2000-02-03').getTime()); + }); + + it('does not update birthday if it has invalid value', async () => { + const account = { _id: genObjectId(), save: stub(), birthdate: new Date(321) } as any; + findAccount.resolves(account); + + await updateAccount(account, { name: 'foo', birthdate: '0123-00-01' }); + + expect(account.birthdate.getTime()).equal(new Date(321).getTime()); + }); + + it('logs birthday change', async () => { + const account = { _id: genObjectId(), name: 'bar', save: stub(), birthdate: new Date(12345) } as any; + findAccount.resolves(account); + + await updateAccount(account, { name: 'bar', birthdate: '2000-02-03' }); + + assert.calledWith(log, account._id, 'Changed birthdate 1970-01-01 (49yo) => 2000-02-03 (19yo)'); + }); + }); + + describe('updateSettings()', () => { + let findAccount: SinonStub; + let updateOne: SinonStub; + let updateSettings: UpdateSettings; + + beforeEach(() => { + findAccount = stub(); + updateOne = stub(db.Account, 'updateOne').returns({ exec: stub().resolves() } as any); + updateSettings = createUpdateSettings(findAccount); + }); + + afterEach(() => { + updateOne.restore(); + }); + + it('returns account data', async () => { + const _id = genObjectId(); + + findAccount.resolves({ + _id, + name: 'foo', + birthdate: new Date(123), + birthyear: 123, + roles: ['mod'], + settings: { foo: 'bar' }, + characterCount: 4, + flags: 1, + supporter: 1, + save() { }, + }); + + const result = await updateSettings(account({}), {}); + + expect(result).eql({ + id: _id.toString(), + name: 'foo', + birthdate: '1970-01-01', + birthyear: 123, + roles: ['mod'], + settings: { foo: 'bar' }, + characterCount: 4, + flags: 0, + supporter: 1, + }); + }); + + it('updates account settings', async () => { + const save = stub(); + const acc = { _id: genObjectId(), save, settings: undefined as any }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), { + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + }); + + expect(acc.settings).eql({ + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + }); + assert.calledWith(updateOne, { _id: acc._id }, { + settings: { + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + } + }); + }); + + it('merges account settings', async () => { + const save = stub(); + const acc = { + _id: genObjectId(), + save, + settings: { + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + }, + }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), { + filterSwearWords: false, + filterCyrillic: true, + ignorePartyInvites: true, + }); + + expect(acc.settings).eql({ + filterSwearWords: false, + filterCyrillic: true, + ignorePartyInvites: true, + }); + assert.calledWith(updateOne, { _id: acc._id }, { + settings: { + filterSwearWords: false, + filterCyrillic: true, + ignorePartyInvites: true, + } + }); + }); + + it('ignores missing fields', async () => { + const acc = { _id: genObjectId(), settings: undefined as any }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), {}); + + expect(acc.settings).eql({}); + assert.calledWith(updateOne, { _id: acc._id }, { settings: {} }); + }); + + it('ignores missing settings', async () => { + const acc = { _id: genObjectId(), settings: undefined as any }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), undefined); + + expect(acc.settings).eql({}); + assert.calledWith(updateOne, { _id: acc._id }, { settings: {} }); + }); + }); + + describe('removeSite()', () => { + let findAuth: SinonStub; + let countAuths: SinonStub; + let log: SinonStub; + let removeSite: RemoveSite; + let updateOne: SinonStub; + + beforeEach(() => { + findAuth = stub(); + countAuths = stub(); + log = stub(); + updateOne = stub(db.Auth, 'updateOne').returns({ exec: () => stub().resolves() } as any); + removeSite = createRemoveSite(findAuth, countAuths, log); + }); + + afterEach(() => { + updateOne.restore(); + }); + + it('returns empty object', async () => { + findAuth.resolves({}); + countAuths.resolves(2); + + const result = await removeSite(account({}), 'SITE_ID'); + + expect(result).eql({}); + }); + + it('disables auth', async () => { + const authId = genObjectId(); + const accountId = genObjectId(); + const auth = { _id: authId, disabled: false }; + const acc = account({ _id: accountId }); + findAuth.withArgs('SITE_ID', accountId).resolves(auth); + countAuths.withArgs(accountId).resolves(2); + + await removeSite(acc, 'SITE_ID'); + + assert.calledWithMatch(updateOne, { _id: authId }, { disabled: true }); + }); + + it('throws if siteId is not string', async () => { + findAuth.resolves(account({})); + countAuths.resolves(2); + + await expect(removeSite(account({}), {})) + .rejectedWith('Social account not found'); + }); + + it('throws if auth does not exist', async () => { + findAuth.resolves(undefined); + countAuths.resolves(2); + + await expect(removeSite(account({}), 'SITE_ID')) + .rejectedWith('Social account not found'); + }); + + it('throws if auth is disabled', async () => { + findAuth.resolves({ disabled: true }); + countAuths.resolves(2); + + await expect(removeSite(account({}), 'SITE_ID')) + .rejectedWith('Social account not found'); + }); + + it('throws if user has only one auth', async () => { + findAuth.resolves({}); + countAuths.resolves(1); + + await expect(removeSite(account({}), 'SITE_ID')) + .rejectedWith('Cannot remove your only one social account'); + }); + + it('logs auth removal', async () => { + const accountId = genObjectId(); + const authId = genObjectId(); + findAuth.resolves({ _id: authId, name: 'foo' }); + countAuths.resolves(2); + + await removeSite(account({ _id: accountId }), 'SITE_ID'); + + assert.calledWith(log, accountId, `removed auth: foo [${authId}]`); + }); + }); +}); diff --git a/src/ts/tests/server/api/game.spec.ts b/src/ts/tests/server/api/game.spec.ts new file mode 100644 index 0000000..f1731a1 --- /dev/null +++ b/src/ts/tests/server/api/game.spec.ts @@ -0,0 +1,176 @@ +import '../../lib'; +import { SinonStub, stub, assert } from 'sinon'; +import { expect } from 'chai'; +import { createJoinGame, JoinGame } from '../../../server/api/game'; +import { VERSION_ERROR } from '../../../common/errors'; +import { PatreonFlags, InternalGameServerState } from '../../../common/adminInterfaces'; +import { delay, fromNow } from '../../../common/utils'; +import { genObjectId, account } from '../../mocks'; + +describe('api game', () => { + describe('joinGame()', () => { + let joinGame: JoinGame; + let findCharacter: SinonStub; + let join: SinonStub; + let addOrigin: SinonStub; + let hasInvites: SinonStub; + let server: InternalGameServerState; + + beforeEach(() => { + findCharacter = stub(); + join = stub(); + addOrigin = stub(); + hasInvites = stub(); + server = { state: { settings: {} } } as any; + const findServer = stub(); + findServer.withArgs('serverid').returns(server); + + joinGame = createJoinGame( + findServer, { version: '1', host: 'http://foo.bar/', debug: false, local: false }, findCharacter, + join, addOrigin, hasInvites); + }); + + it('returns join token', async () => { + const a = account({ _id: genObjectId() }); + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + join.withArgs(server, a, character).returns('tokenid'); + + await expect(joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .eventually.eql({ token: 'tokenid' }); + }); + + it('resolves if meets server requirement', async () => { + server.state.require = 'sup2'; + const a = account({ _id: genObjectId(), patreon: PatreonFlags.Supporter2 }); + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + join.withArgs(server, a, character).returns('tokenid'); + + await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); + }); + + it('resolves if meets server requirement (invited)', async () => { + server.state.require = 'inv'; + const a = account({ _id: genObjectId() }); + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + hasInvites.resolves(true); + join.withArgs(server, a, character).returns('tokenid'); + + await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); + }); + + it('adds origin to account', async () => { + const a = account({ _id: genObjectId() }); + const origin = {} as any; + findCharacter.withArgs('charid').returns({}); + + await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, origin); + + assert.calledWith(addOrigin, a, origin); + }); + + it('returns alert if has account alert', async () => { + const a = account({ _id: genObjectId(), alert: { message: 'test alert', expires: fromNow(9999) } }); + const origin = {} as any; + findCharacter.withArgs('charid').returns({}); + + const result = await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, origin); + + expect(result).eql({ alert: 'test alert' }); + }); + + it('does not returl alert if alredy has alert', async () => { + const a = account({ _id: genObjectId(), alert: { message: 'test alert', expires: fromNow(9999) } }); + const origin = {} as any; + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + join.withArgs(server, a, character).returns('tokenid'); + + const result = await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', true, origin); + + expect(result).eql({ token: 'tokenid' }); + }); + + it('rejects if passed version is different than server version', async () => { + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '2', 'http://foo.bar/', false, {} as any)) + .rejectedWith(VERSION_ERROR); + }); + + it('rejects if passed url is different than server url', async () => { + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://im.invalid/', false, {} as any)) + .rejectedWith('Invalid data'); + }); + + it('rejects if server is not found', async () => { + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'doesnotexist', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Invalid data'); + }); + + it('rejects if server is offline', async () => { + server.state.settings.isServerOffline = true; + + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Server is offline'); + }); + + it('rejects if server is restricted', async () => { + server.state.require = 'mod'; + + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Server is restricted'); + }); + + it('rejects if character ID is missing', async () => { + await expect(joinGame( + account({ _id: genObjectId() }), undefined as any, 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Invalid data'); + }); + + it('rejects if character ID is not string', async () => { + await expect(joinGame( + account({ _id: genObjectId() }), { foo: 'bar' } as any, 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Invalid data'); + }); + + it('rejects if character does not exist', async () => { + await expect(joinGame( + account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Character does not exist'); + }); + + it('rejects if already joining', async () => { + findCharacter.withArgs('charid').returns({}); + join.returns(new Promise(() => { })); + const _id = genObjectId(); + + joinGame(account({ _id }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); + + await delay(1); + await expect(joinGame(account({ _id }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Already waiting for join request'); + }); + + it('does not reject if other client is already joining', async () => { + findCharacter.withArgs('charid').returns({}); + findCharacter.withArgs('charid2').returns({}); + join.returns(new Promise(() => { })); + join.returns('tokenid'); + + joinGame(account({ _id: genObjectId() }), 'charid2', 'serverid', '1', 'http://foo.bar/', false, {} as any); + + await delay(1); + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .eventually.eql({ token: 'tokenid' }); + }); + + it('rejects if joining is blocked', async () => { + server.state.settings.blockJoining = true; + findCharacter.withArgs('charid').returns({}); + + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Cannot join to the server'); + }); + }); +}); diff --git a/src/ts/tests/server/api/internal-common.spec.ts b/src/ts/tests/server/api/internal-common.spec.ts new file mode 100644 index 0000000..90b634b --- /dev/null +++ b/src/ts/tests/server/api/internal-common.spec.ts @@ -0,0 +1,21 @@ +import '../../lib'; +import { SinonStub, stub, assert } from 'sinon'; +import { createReloadSettings } from '../../../server/api/internal-common'; + +describe('internal-common', () => { + describe('reloadSettings', () => { + let func: ReturnType; + let reloadSettings: SinonStub; + + beforeEach(() => { + reloadSettings = stub(); + func = createReloadSettings(reloadSettings); + }); + + it('reloads settings', async () => { + await func(); + + assert.calledOnce(reloadSettings); + }); + }); +}); diff --git a/src/ts/tests/server/api/internal.spec.ts b/src/ts/tests/server/api/internal.spec.ts new file mode 100644 index 0000000..c1673d9 --- /dev/null +++ b/src/ts/tests/server/api/internal.spec.ts @@ -0,0 +1,570 @@ +import { stubClass, resetStubMethods } from '../../lib'; +import { expect } from 'chai'; +import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, SinonStubbedInstance, createStubInstance } from 'sinon'; +import { + createAccountChanged, createAccountStatus, createJoin, createGetServerState, createKick, createKickAll, + createGetServerStats, createAccountMerged, createAccountAround, createAction, createShutdownServer, + createNotifyUpdate, createCancelUpdate, createHiddenStats +} from '../../../server/api/internal'; +import { World } from '../../../server/world'; +import { mock, mockClient, genObjectId } from '../../mocks'; +import { + PatreonFlags, ServerConfig, GameServerSettings, ServerLiveSettings, ServerStats, HidingStats +} from '../../../common/adminInterfaces'; +import { ICharacter, IAccount } from '../../../server/db'; +import { Types } from 'mongoose'; +import { HidingService } from '../../../server/services/hiding'; +import { StatsTracker } from '../../../server/stats'; + +describe('api internal', () => { + describe('accountChanged()', () => { + let func: ReturnType; + let world: World; + let findAccount: SinonStub; + let clearTokensForAccount: SinonStub; + + beforeEach(() => { + world = new World( + {} as any, { partyChanged: { subscribe() { } } } as any, {} as any, {} as any, {} as any, + () => ({}), {} as any, {} as any); + clearTokensForAccount = stub(); + findAccount = stub(); + func = createAccountChanged(world, { clearTokensForAccount } as any, findAccount); + }); + + it('notifies world of account update', async () => { + const account: any = { _id: genObjectId() }; + findAccount.withArgs('foobar').resolves(account); + const accountUpdated = stub(world, 'accountUpdated'); + + await func('foobar'); + + assert.calledWith(accountUpdated, account); + }); + + it('clears tokens for account if account is banned', async () => { + const account = { _id: genObjectId(), ban: -1 }; + findAccount.withArgs('foobar').resolves(account); + + await func('foobar'); + + assert.calledWith(clearTokensForAccount, 'foobar'); + }); + }); + + describe('accountMerged()', () => { + const hiding = stubClass(HidingService); + let func: ReturnType; + + beforeEach(() => { + resetStubMethods(hiding, 'merged'); + func = createAccountMerged(hiding as any); + }); + + it('notifies hiding service of merge', async () => { + await func('foo', 'bar'); + + assert.calledWith(hiding.merged, 'foo', 'bar'); + }); + }); + + describe('accountStatus()', () => { + let func: ReturnType; + let world: World; + let server: ServerConfig; + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + world = { clientsByAccount: new Map() } as any; + server = { id: 'foo' } as any; + func = createAccountStatus(world, server); + }); + + afterEach(() => { + clock.restore(); + }); + + it('returns client account status', async () => { + const client = mockClient(); + client.characterName = 'derpy'; + client.pony.name = '?????'; + client.pony.x = 5.2; + client.pony.y = 6.1; + client.userAgent = 'useragent'; + client.connectedTime = 0; + clock.setSystemTime(12 * 1000); + world.clientsByAccount.set('bar', client); + + await expect(func('bar')).eventually.eql({ + online: true, + incognito: undefined, + character: 'derpy', + duration: '12s', + server: 'foo', + map: '-', + x: 5, + y: 6, + userAgent: 'useragent', + }); + }); + + it('returns offline status for missing client', async () => { + await expect(func('bar')).eventually.eql({ online: false }); + }); + }); + + describe('accountAround()', () => { + let func: ReturnType; + + beforeEach(() => { + func = createAccountAround({ clientsByAccount: new Map() } as any); + }); + + it('returns client arount given account', async () => { + await expect(func('bar')).eventually.eql([]); + }); + }); + + describe('hiddenStats()', () => { + let hiddenStats: ReturnType; + let hiding: SinonStubbedInstance; + + beforeEach(() => { + hiding = createStubInstance(HidingService); + hiddenStats = createHiddenStats(hiding as any); + }); + + it('returns hiding stats for given account', async () => { + const result: HidingStats = {} as any; + hiding.getStatsFor.withArgs('bar').returns(result); + + await expect(hiddenStats('bar')).eventually.equal(result); + }); + }); + + describe('join()', () => { + let func: (accountId: string, ponyId: string) => Promise; + let world: World; + let server: ServerConfig; + let settings: GameServerSettings; + let clearTokensForAccount: SinonStub; + let createToken: SinonStub; + let findAccount: SinonStub; + let findCharacter: SinonStub; + let findAuth: SinonStub; + let hasInvite: SinonStub; + let account: IAccount; + let character: ICharacter; + let clock: SinonFakeTimers; + let liveSettings: ServerLiveSettings; + + beforeEach(() => { + account = { save() { }, _id: new Types.ObjectId('5983e1f7519f95530becdf7d') } as any; + character = { save() { }, _id: new Types.ObjectId('5983e1f7519f95530becdf7a') } as any; + world = mock(World); + server = { id: 'foo' } as any; + settings = {}; + liveSettings = {} as any; + clearTokensForAccount = stub(); + createToken = stub(); + findAccount = stub().withArgs('foo').resolves(account); + findCharacter = stub().withArgs('bar', 'foo').resolves(character); + findAuth = stub(); + hasInvite = stub(); + func = createJoin( + world, () => settings, server, { clearTokensForAccount, createToken } as any, findAccount, findCharacter, + findAuth, liveSettings, hasInvite); + clock = useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + it('returns new token id', () => { + createToken.returns('lalala'); + + return expect(func('foo', 'bar')).eventually.equal('lalala'); + }); + + it('creates token using fetched account and character', () => { + return func('foo', 'bar') + .then(() => { + expect(createToken.args[0][0].account).equal(account); + expect(createToken.args[0][0].character).equal(character); + }); + }); + + it('kicks all other clients with the same account', () => { + const kickByAccount = stub(world, 'kickByAccount'); + + return func('foo', 'bar') + .then(() => { + assert.calledWith(kickByAccount, 'foo'); + assert.calledWith(clearTokensForAccount, 'foo'); + }); + }); + + it('updates account default server', () => { + const save = stub(account, 'save'); + server.id = 'someidhere'; + + return func('foo', 'bar') + .then(() => { + expect(account.settings).eql({ defaultServer: 'someidhere' }); + assert.calledOnce(save); + }); + }); + + it('updates account default server (with existing settings)', () => { + const save = stub(account, 'save'); + server.id = 'someidhere'; + account.settings = { ignorePartyInvites: true }; + + return func('foo', 'bar') + .then(() => { + expect(account.settings).eql({ ignorePartyInvites: true, defaultServer: 'someidhere' }); + assert.calledOnce(save); + }); + }); + + it('updates account last visit', () => { + const save = stub(account, 'save'); + clock.setSystemTime(123); + + return func('foo', 'bar') + .then(() => { + expect(account.lastVisit.toISOString()).equal(new Date(123).toISOString()); + assert.calledOnce(save); + }); + }); + + it('updates character last used', () => { + const save = stub(character, 'save'); + clock.setSystemTime(123); + + return func('foo', 'bar') + .then(() => { + expect(character.lastUsed!.toISOString()).equal(new Date(123).toISOString()); + assert.calledOnce(save); + }); + }); + + it('rejects if server is offline', () => { + settings.isServerOffline = true; + + return expect(func('foo', 'bar')).rejectedWith('Server is offline'); + }); + + it('rejects if server is restricted from user', () => { + server.require = 'mod'; + + return expect(func('foo', 'bar')).rejectedWith('Server is restricted'); + }); + + it('resolves if user meets server restrictions', () => { + server.require = 'mod'; + account.roles = ['mod']; + + return func('foo', 'bar'); + }); + + it('resolves if user meets server restrictions (supporter)', () => { + server.require = 'sup1'; + account.patreon = PatreonFlags.Supporter2; + + return func('foo', 'bar'); + }); + + it('resolves if user meets server restrictions (invited)', () => { + server.require = 'inv'; + hasInvite.resolves(true); + + return func('foo', 'bar'); + }); + + it('sets up character social site', async () => { + const site = {}; + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(site); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).equal(site); + }); + + it('does not set up character social site if its missing', async () => { + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(undefined); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).undefined; + }); + + it('does not set up character social site if its disabled', async () => { + const site = { disabled: true }; + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(site); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).undefined; + }); + + it('does not set up character social site if its banned', async () => { + const site = { banned: true }; + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(site); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).undefined; + }); + }); + + describe('getServerState()', () => { + let func: ReturnType; + let world: World; + let server: ServerConfig; + let settings: GameServerSettings; + let liveSettings: ServerLiveSettings; + + beforeEach(() => { + world = mock(World); + server = { flags: {} } as any; + settings = {}; + liveSettings = { updating: false, shutdown: false }; + func = createGetServerState(server, () => settings, world, liveSettings); + }); + + it('returns combined server state', async () => { + Object.assign(server, { id: 'aaa', name: 'bbb', path: 'ccc', desc: 'ddd', alert: 'eee', require: 'mod' }); + world.clients = [{} as any, {} as any]; + world.joinQueue = [{} as any, {} as any, {} as any]; + world.maps = [{} as any, {} as any]; + settings.isServerOffline = true; + settings.filterSwears = true; + + const result = await func(); + + expect(result).eql({ + id: 'aaa', + name: 'bbb', + path: 'ccc', + desc: 'ddd', + alert: 'eee', + dead: false, + maps: 2, + online: 2, + onMain: 2, + queued: 3, + require: 'mod', + flags: {}, + flag: undefined, + host: undefined, + settings, + shutdown: false, + }); + }); + + it('uses defaults for missing values', async () => { + Object.assign(server, { id: 'aaa', name: 'bbb', path: 'ccc', desc: 'ddd' }); + world.clients = [{} as any, {} as any]; + world.joinQueue = []; + world.maps = []; + + const result = await func(); + + expect(result).eql({ + id: 'aaa', + name: 'bbb', + path: 'ccc', + desc: 'ddd', + alert: undefined, + dead: false, + maps: 0, + online: 2, + onMain: 2, + queued: 0, + require: undefined, + host: undefined, + flags: {}, + flag: undefined, + settings, + shutdown: false, + }); + }); + }); + + describe('getServerStats()', () => { + let stats: SinonStubbedInstance; + let func: ReturnType; + + beforeEach(() => { + stats = createStubInstance(StatsTracker); + func = createGetServerStats(stats as any); + }); + + it('returns socket stats', async () => { + const result: ServerStats = {} as any; + stats.getSocketStats.returns(result); + + await expect(func()).eventually.equal(result); + }); + }); + + describe('action()', () => { + let action: ReturnType; + + beforeEach(() => { + action = createAction({} as any); + }); + + it('throws if action is invalid', async () => { + await expect(action('foo', 'foobar')).rejectedWith('Invalid action (foo)'); + }); + }); + + describe('kick()', () => { + let func: ReturnType; + let world: World; + let clearTokensForAccount: SinonStub; + + beforeEach(() => { + world = mock(World); + clearTokensForAccount = stub(); + func = createKick(world, { clearTokensForAccount } as any); + }); + + it('kicks clients by account ID', async () => { + const kickByAccount = stub(world, 'kickByAccount'); + + await func('foo', undefined); + + assert.calledWith(kickByAccount, 'foo'); + }); + + it('clears tokens by account ID', async () => { + await func('foo', undefined); + + assert.calledWith(clearTokensForAccount, 'foo'); + }); + + it('kicks clients by character ID', async () => { + const kickByCharacter = stub(world, 'kickByCharacter'); + + await func(undefined, 'bar'); + + assert.calledWith(kickByCharacter, 'bar'); + }); + + it('does nothing if ID is not provided', async () => { + await func(undefined, undefined); + }); + }); + + describe('kickAll()', () => { + let func: ReturnType; + let world: World; + let clearTokensAll: SinonStub; + + beforeEach(() => { + world = mock(World); + clearTokensAll = stub(); + func = createKickAll(world, { clearTokensAll } as any); + }); + + it('kicks all clients', async () => { + const kickAll = stub(world, 'kickAll'); + + await func(); + + assert.calledOnce(kickAll); + }); + + it('clears all tokens', async () => { + await func(); + + assert.calledOnce(clearTokensAll); + }); + }); + + describe('notifyUpdate()', () => { + let func: ReturnType; + let world = stubClass(World); + let liveSettings: ServerLiveSettings; + + beforeEach(() => { + resetStubMethods(world, 'notifyUpdate', 'saveClientStates'); + liveSettings = {} as any; + func = createNotifyUpdate(world as any, liveSettings); + }); + + it('notifies world of update', async () => { + await func(); + + assert.calledOnce(world.notifyUpdate); + }); + + it('updates character state', async () => { + await func(); + + assert.calledOnce(world.saveClientStates); + }); + }); + + describe('cancelUpdate()', () => { + let func: ReturnType; + let live: ServerLiveSettings; + + beforeEach(() => { + live = {} as any; + func = createCancelUpdate(live); + }); + + it('sets updating to false', async () => { + live.updating = true; + + await func(); + + expect(live.updating).false; + }); + }); + + describe('shutdownServer()', () => { + let shutdownServer: ReturnType; + let world: World; + let liveSettings: ServerLiveSettings; + + beforeEach(() => { + world = mock(World); + (world as any).server = { id: 'foo' }; + liveSettings = {} as any; + shutdownServer = createShutdownServer(world, liveSettings); + }); + + it('updates shutdown option in live settings to true', async () => { + await shutdownServer(true); + + expect(liveSettings.shutdown).true; + }); + + it('updates shutdown option in live settings to false', async () => { + await shutdownServer(false); + + expect(liveSettings.shutdown).false; + }); + + it('kicks all players', async () => { + const kickAll = stub(world, 'kickAll'); + + await shutdownServer(true); + + assert.calledOnce(kickAll); + }); + }); +}); diff --git a/src/ts/tests/server/api/pony.spec.ts b/src/ts/tests/server/api/pony.spec.ts new file mode 100644 index 0000000..e5d8e0a --- /dev/null +++ b/src/ts/tests/server/api/pony.spec.ts @@ -0,0 +1,400 @@ +import '../../lib'; +import { SinonStub, stub, useFakeTimers, SinonFakeTimers, assert, SinonStubbedInstance } from 'sinon'; +import { expect } from 'chai'; +import { Types } from 'mongoose'; +import { OFFLINE_PONY, PLAYER_NAME_MAX_LENGTH } from '../../../common/constants'; +import { randomString } from '../../../common/stringUtils'; +import { ICharacter } from '../../../server/db'; +import { SavePony, createSavePony, createRemovePony, RemovePony } from '../../../server/api/pony'; +import { CharacterFlags, PatreonFlags } from '../../../common/adminInterfaces'; +import { getCharacterLimit } from '../../../common/accountUtils'; +import { genId, genObjectId, account } from '../../mocks'; +import { Reporter } from '../../../server/serverInterfaces'; + +const info = OFFLINE_PONY; + +describe('api pony', () => { + describe('savePony()', () => { + let savePony: SavePony; + let findCharacter: SinonStub; + let findAuth: SinonStub; + let characterCount: SinonStub; + let updateCharacterCount: SinonStub; + let createCharacter: SinonStub; + let log: SinonStub; + let isSuspiciousName: SinonStub; + let isSuspiciousPony: SinonStub; + let clock: SinonFakeTimers; + let reporter: SinonStubbedInstance; + + beforeEach(() => { + findCharacter = stub(); + findAuth = stub(); + characterCount = stub(); + updateCharacterCount = stub(); + createCharacter = stub(); + log = stub(); + isSuspiciousName = stub(); + isSuspiciousPony = stub(); + clock = useFakeTimers(); + reporter = { + danger: stub(), + setPony: stub(), + warn: stub(), + } as any; + + savePony = createSavePony( + findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, log, + isSuspiciousName, isSuspiciousPony); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('for existing character', () => { + const characterId = genId(); + const characterObjectId = Types.ObjectId(characterId); + let character: ICharacter; + let account = { _id: 'accid' } as any; + + beforeEach(() => { + character = { + name: 'oldname', + _id: characterObjectId, + createdAt: new Date(10), + save() { return this; } + } as any; + + findCharacter.withArgs(characterId, 'accid').resolves(character); + }); + + it('returns pony object', async () => { + clock.setSystemTime(123); + + await expect(savePony(account, { id: characterId, name: 'foo', info }, reporter)).eventually.eql({ + id: characterId, + info, + lastUsed: '1970-01-01T00:00:00.123Z', + name: 'foo', + desc: '', + site: undefined, + tag: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); + + it('saves character', async () => { + const save = stub(character, 'save').resolves(character); + + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + + assert.calledOnce(save); + }); + + it('updates character fields', async () => { + clock.setSystemTime(123); + + await savePony(account, { id: characterId, name: 'foo', tag: 'tag', info }, reporter); + + expect(character.name).equal('foo'); + expect(character.tag).equal('tag'); + expect(character.info).equal(info); + expect(character.lastUsed!.toISOString()).equal((new Date()).toISOString()); + }); + + it('does not reject if character limit is reached', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 0 }) * 2); + + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + }); + + it('logs name change', async () => { + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + + assert.calledWith(log, account._id, 'renamed pony "oldname" => "foo"'); + }); + + it('does not log if nothing changed', async () => { + await savePony(account, { id: characterId, name: 'oldname', info }, reporter); + + assert.notCalled(log); + }); + + it('reports suspicious name', async () => { + isSuspiciousName.withArgs('moderator').returns(true); + + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + + assert.calledWith(reporter.setPony, characterId); + assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (name)'); + }); + + it('does not report suspicious name if not changed', async () => { + character.name = 'moderator'; + isSuspiciousName.withArgs('moderator').returns(true); + + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + }); + + it('reports suspicious look', async () => { + isSuspiciousPony.returns(true); + + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + + assert.calledWith(reporter.setPony, characterId); + assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (look)'); + }); + + it('does not report suspicious look if not changed', async () => { + character.info = info; + isSuspiciousPony.returns(true); + + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + }); + + it('reports suspicious name & look', async () => { + isSuspiciousName.withArgs('moderator').returns(true); + isSuspiciousPony.returns(true); + + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + + assert.calledWith(reporter.setPony, characterId); + assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (name, look)'); + }); + + it('rejects on decoding error', async () => { + await expect(savePony(account, { id: characterId, name: 'foo', info: 'xxyf@hs' }, reporter)) + .rejectedWith('Error saving character'); + }); + + it('sets bad CM flag', async () => { + const info = 'CASZlZXapSD/1wBAPTk2QAJkI0AT8ADAAxADhAYQHMMkhhMkkJhkkMA='; + + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + + expect(character.flags).equal(CharacterFlags.BadCM); + }); + + it('sets hide support pony flag', async () => { + await savePony(account, { id: characterId, name: 'foo', info, hideSupport: true }, reporter); + + expect(character.flags).equal(CharacterFlags.HideSupport); + }); + + it('sets respawn at spawn pony flag', async () => { + await savePony(account, { id: characterId, name: 'foo', info, respawnAtSpawn: true }, reporter); + + expect(character.flags).equal(CharacterFlags.RespawnAtSpawn); + }); + + it('sets auth', async () => { + const authid = {} as any; + findAuth.withArgs('authid', 'accid').resolves({ _id: authid }); + + await savePony(account, { id: characterId, name: 'foo', site: 'authid', info }, reporter); + + expect(character.site).equal(authid); + }); + + it('does not set auth if not found', async () => { + await savePony(account, { id: characterId, name: 'foo', site: 'authid', info }, reporter); + + expect(character.site).null; + }); + }); + + describe('for new character', () => { + const characterId = genId(); + let acc = account({ _id: genObjectId() }); + let character: ICharacter; + + beforeEach(() => { + character = { + _id: Types.ObjectId(characterId), + save() { return this; } + } as any; + + createCharacter.withArgs(acc).returns(character); + }); + + it('returns pony object', async () => { + clock.setSystemTime(123); + + await expect(savePony(acc, { name: 'foo', info }, reporter)).eventually.eql({ + id: characterId, + info, + lastUsed: '1970-01-01T00:00:00.123Z', + name: 'foo', + desc: '', + site: undefined, + tag: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); + + it('saves character', async () => { + const save = stub(character, 'save').resolves(character); + + await savePony(acc, { name: 'foo', info }, reporter); + + assert.calledOnce(save); + }); + + it('sets character fields', async () => { + clock.setSystemTime(123); + + await savePony(acc, { id: characterId, name: 'foo', tag: 'tag', info }, reporter); + + expect(character.name).equal('foo'); + expect(character.tag).equal('tag'); + expect(character.info).equal(info); + expect(character.lastUsed!.toISOString()).equal((new Date()).toISOString()); + }); + + it('rejects if character limit is reached', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 0 })); + + await expect(savePony(acc, { name: 'foo', info }, reporter)) + .rejectedWith('Character limit reached'); + }); + + it('logs character creation', async () => { + stub(character, 'save').resolves({ name: 'foo', createdAt: new Date() } as any); + + await savePony(acc, { name: 'foo', info }, reporter); + + assert.calledWith(log, acc._id, 'created pony "foo"'); + }); + + describe('for supporters', () => { + beforeEach(() => { + acc.patreon = PatreonFlags.Supporter1; + }); + + it('has larger limit', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 0 })); + const save = stub(character, 'save').resolves(character); + + await savePony(acc, { name: 'foo', info }, reporter); + + assert.calledOnce(save); + }); + + it('rejects if character limit is reached', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 1 })); + + await expect(savePony(acc, { name: 'foo', info }, reporter)).rejectedWith('Character limit reached'); + }); + }); + }); + + it('rejects on missing pony', async () => { + await expect(savePony({} as any, undefined as any, reporter)).rejectedWith('Invalid data'); + }); + + it('rejects on missing pony name', async () => { + await expect(savePony({} as any, { info }, reporter)).rejectedWith('Invalid data'); + }); + + it('rejects on non-string pony name', async () => { + await expect(savePony({} as any, { name: {} as any, info }, reporter)).rejectedWith('Invalid data'); + }); + + it('rejects on missing pony info', async () => { + await expect(savePony({} as any, { name: 'foo' }, reporter)).rejectedWith('Invalid data'); + }); + + it('rejects on too long pony name', async () => { + await expect(savePony({} as any, { name: randomString(PLAYER_NAME_MAX_LENGTH + 1), info }, reporter)) + .rejectedWith('Invalid name'); + }); + + it('rejects on database error', async () => { + findCharacter.rejects(new Error('test')); + + await expect(savePony({} as any, { id: 'charid', name: 'foo', info }, reporter)).rejectedWith('Invalid data'); + }); + }); + + describe('removePony()', () => { + let removePony: RemovePony; + let kickFromAllServersByCharacter: SinonStub; + let removeCharacter: SinonStub; + let updateCharacterCount: SinonStub; + let removedCharacter: SinonStub; + let logRemovedCharacter: SinonStub; + + beforeEach(() => { + kickFromAllServersByCharacter = stub(); + removeCharacter = stub(); + updateCharacterCount = stub(); + removedCharacter = stub(); + logRemovedCharacter = stub(); + + removePony = createRemovePony( + kickFromAllServersByCharacter, removeCharacter, updateCharacterCount, removedCharacter, logRemovedCharacter); + }); + + it('kicks user from all servers', async () => { + await removePony('ponid', 'accid'); + + assert.calledWith(kickFromAllServersByCharacter, 'ponid'); + }); + + it('removes character', async () => { + await removePony('ponid', 'accid'); + + assert.calledWith(removeCharacter, 'ponid', 'accid'); + }); + + it('updates character count', async () => { + await removePony('ponid', 'accid'); + + assert.calledWith(updateCharacterCount, 'accid'); + }); + + it('logs character removed', async () => { + const character = { name: 'test', info: 'INFO' }; + removeCharacter.resolves(character); + + await removePony('ponid', 'accid'); + + assert.calledWith(logRemovedCharacter, character); + }); + + it('notifies of character removal', async () => { + removeCharacter.resolves({ name: 'test' }); + + await removePony('ponid', 'accid'); + + assert.calledWith(removedCharacter, 'ponid'); + }); + + it('does not log character removal if character is not found', async () => { + removeCharacter.resolves(undefined); + + await removePony('ponid', 'accid'); + + assert.notCalled(logRemovedCharacter); + }); + + it('does not notify of character removal if character is not found', async () => { + removeCharacter.resolves(undefined); + + await removePony('ponid', 'accid'); + + assert.notCalled(removedCharacter); + }); + + it('rejects if pony ID is not a string', async () => { + await expect(removePony({} as any, 'accid')).rejectedWith('Invalid ponyId ([object Object])'); + }); + + it('rejects if pony ID is empty', async () => { + await expect(removePony('', 'accid')).rejectedWith('Invalid ponyId ()'); + }); + }); +}); diff --git a/src/ts/tests/server/authUtils.spec.ts b/src/ts/tests/server/authUtils.spec.ts new file mode 100644 index 0000000..e813e62 --- /dev/null +++ b/src/ts/tests/server/authUtils.spec.ts @@ -0,0 +1,86 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, assert } from 'sinon'; +import { Types } from 'mongoose'; +import { updateAuthInfo } from '../../server/authUtils'; +import { auth, genId } from '../mocks'; +import { Profile } from '../../common/interfaces'; + +function profile(options: Partial): Profile { + return options as Profile; +} + +describe('authUtils', () => { + describe('updateAuthInfo()', () => { + it('updates url and name fields', async () => { + const updateAuth = stub(); + const a = auth({ _id: 'bar' }); + + await updateAuthInfo(updateAuth, a, profile({ username: 'foo', url: 'bar' }), undefined); + + expect(a.name).eql('foo'); + expect(a.url).eql('bar'); + assert.calledWith(updateAuth, 'bar', { name: 'foo', url: 'bar' }); + }); + + it('updates email field', async () => { + const a = auth({ emails: ['a'] }); + + await updateAuthInfo(stub(), a, profile({ emails: ['b', 'c'] }), undefined); + + expect(a.emails).eql(['a', 'b', 'c']); + }); + + it('updates email field (from empty)', async () => { + const updateAuth = stub(); + const a = auth({ _id: 'bar' }); + + await updateAuthInfo(updateAuth, a, profile({ emails: ['b', 'c'] }), undefined); + + expect(a.emails).eql(['b', 'c']); + assert.calledWith(updateAuth, 'bar', { emails: ['b', 'c'] }); + }); + + it('saves updated auth', async () => { + const updateAuth = stub(); + + await updateAuthInfo(updateAuth, auth({ _id: 'bar' }), profile({ username: 'foo' }), undefined); + + assert.calledWith(updateAuth, 'bar', { name: 'foo' }); + }); + + it('updates account if passed account ID', async () => { + const a = auth({}); + const accountId = genId(); + + await updateAuthInfo(stub(), a, profile({ username: 'foo', url: 'bar' }), accountId); + + expect(a.account).eql(Types.ObjectId(accountId)); + }); + + it('does not save auth if nothing changed', async () => { + const updateAuth = stub(); + + await updateAuthInfo(updateAuth, auth({ _id: 'bar', name: 'foo' }), profile({ username: 'foo' }), undefined); + + assert.notCalled(updateAuth); + }); + + it('does nothing if email list is the same', async () => { + const updateAuth = stub(); + const a = auth({ _id: 'bar', emails: ['a', 'b'] }); + + await updateAuthInfo(updateAuth, a, profile({ emails: ['b', 'a'] }), undefined); + + assert.notCalled(updateAuth); + }); + + it('does nothing if auth is undefined', async () => { + const updateAuth = stub(); + + await updateAuthInfo(updateAuth, undefined, profile({}), undefined); + + assert.notCalled(updateAuth); + }); + }); +}); diff --git a/src/ts/tests/server/characterUtils.spec.ts b/src/ts/tests/server/characterUtils.spec.ts new file mode 100644 index 0000000..338b003 --- /dev/null +++ b/src/ts/tests/server/characterUtils.spec.ts @@ -0,0 +1,365 @@ +import '../lib'; +import { expect } from 'chai'; +import { encodeString } from 'ag-sockets/dist/utf8'; +import { createExtraOptions, updatePony, encryptInfo, createPony, getAndFixCharacterState } from '../../server/characterUtils'; +import { account, character, serverEntity, entity, genObjectId } from '../mocks'; +import { CharacterFlags, CharacterState, SupporterFlags, CharacterStateFlags } from '../../common/adminInterfaces'; +import { OFFLINE_PONY } from '../../common/constants'; +import * as entities from '../../common/entities'; +import { EntityState, EntityFlags } from '../../common/interfaces'; +import { rect } from '../../common/rect'; +import { createServerMap } from '../../server/serverMap'; +import { CounterService } from '../../server/services/counter'; +import { createCharacterState } from '../../server/playerUtils'; +import { hasFlag } from '../../common/utils'; + +describe('characterUtils', () => { + describe('createPony()', () => { + const defaultState: CharacterState = { x: 0, y: 0, flags: 0 }; + + it('creates pony entity', () => { + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), defaultState); + + expect(pony).not.undefined; + expect(pony.type).equal(entities.pony.type); + }); + + it('sets initial position for character from state', () => { + const main: CharacterState = { ...defaultState, x: 1, y: 2 }; + + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + + expect(pony.x).eql(1, 'x'); + expect(pony.y).eql(2, 'y'); + }); + + it('sets facing from state', () => { + const main: CharacterState = { ...defaultState, flags: CharacterStateFlags.Right }; + + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + + expect(pony.state).equal(EntityState.FacingRight); + }); + + it('sets extra flag from state', () => { + const main: CharacterState = { ...defaultState, flags: CharacterStateFlags.Extra }; + + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + + expect(pony.options!.extra).true; + }); + + it('sets held item from state', () => { + const main: CharacterState = { ...defaultState, hold: 'apple' }; + + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + + expect(pony.options!.hold).equal(entities.apple.type); + }); + + it('ignores held item from state if type is invalid', () => { + const main: CharacterState = { ...defaultState, hold: 'does_not_exist' }; + + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + + expect(pony.options!.hold).undefined; + }); + + it('sets canCollide flag', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + + expect(hasFlag(pony.flags, EntityFlags.CanCollide)).true; + }); + + it('sets canFly flag to false for ponies without wings', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + + expect(pony.canFly).false; + }); + + it('sets canFly flag to true for ponies with wings', () => { + const info = 'CAb///9xcXHaICDHx8eqqqq9vb02QAJkJEIFcADAAwgEnAcgQNiMS4A='; + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info }), {} as any); + + expect(pony.canFly).true; + }); + + it('sets character name', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + + expect(pony.name).equal('foo'); + }); + + it('sets extra options name', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + + expect(pony.extraOptions).eql(createExtraOptions(character({ name: 'foo' }))); + }); + }); + + describe('updatePony()', () => { + it('sets name', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo' })); + + expect(entity.name).equal('Foo'); + }); + + it('sets options', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId(), roles: ['mod'] }), character({ name: 'Foo', tag: 'mod' })); + + expect(entity.options).eql({ tag: 'mod' }); + }); + + it('sets extra options', () => { + const entity = serverEntity(1); + + updatePony( + entity, + account({ _id: genObjectId() }), + { name: 'Foo', auth: { provider: 'github', name: 'FooAcc', url: 'foo.com' } } as any); + + expect(entity.extraOptions).eql({ + ex: true, + site: { + provider: 'github', + name: 'FooAcc', + url: 'foo.com', + } + }); + }); + + it('sets canFly flag', () => { + const entity1 = serverEntity(1); + const entity2 = serverEntity(2); + + updatePony(entity1, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); + updatePony( + entity2, account({ _id: '' }), character({ name: 'Bar', info: 'DAT/AADapSD/1wC7Li42QAJkJEAT8ADAAxADhAYQFGAQAA==' })); + + expect(entity1.canFly).false; + expect(entity2.canFly).true; + }); + + it('sets info and encrypted info', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); + + expect(entity.info).equal(OFFLINE_PONY); + expect(entity.encryptedInfoSafe).eql(encryptInfo(OFFLINE_PONY)); + }); + + it('sets encoded name fields', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Fuck Foo' })); + + expect(entity.encodedName).eql(encodeString('Fuck Foo')); + }); + + it('sets info safe fields', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); + + expect(entity.infoSafe).eql(OFFLINE_PONY); + expect(entity.encryptedInfoSafe).eql(encryptInfo(OFFLINE_PONY)); + }); + + it('sets info safe fields to info with removed CM if bad CM flag is true', () => { + const entity = serverEntity(1); + const offlinePonyWithoutCM = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBAA=='; + + updatePony( + entity, + account({ _id: genObjectId() }), + character({ name: 'Foo', info: OFFLINE_PONY, flags: CharacterFlags.BadCM })); + + expect(entity.infoSafe).eql(offlinePonyWithoutCM); + expect(entity.encryptedInfoSafe).eql(encryptInfo(offlinePonyWithoutCM)); + }); + + it('sets options', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo' })); + + expect(entity.name).equal('foo'); + }); + + it('fills in missing info', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo' })); + + expect(entity.name).equal('foo'); + }); + + it('includes tag', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId(), roles: ['mod'] }), character({ name: 'foo', tag: 'mod' })); + + expect(entity.options!.tag).equal('mod'); + }); + + it('prioritazes set tag', () => { + const entity = serverEntity(1); + + updatePony( + entity, + account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1, roles: ['mod'] }), + character({ name: 'foo', tag: 'mod' })); + + expect(entity.options!.tag).equal('mod'); + }); + + it('creates supporter tag', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1 }), character({ name: 'foo' })); + + expect(entity.options!.tag).equal('sup1'); + }); + + it('does not create support tag if hide support flag is true', () => { + const entity = serverEntity(1); + + updatePony( + entity, + account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1 }), + character({ name: 'foo', flags: CharacterFlags.HideSupport })); + + expect(entity.options!.tag).undefined; + }); + + it('does not include tag if role is missing', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo', tag: 'mod' })); + + expect(entity.options!.tag).undefined; + }); + }); + + describe('createExtraOptions()', () => { + it('sets ex flag', () => { + expect(createExtraOptions(character({}))).eql({ + ex: true, + }); + }); + + it('sets site object from auth', () => { + expect(createExtraOptions(character({ + auth: { + provider: 'github', + name: 'foo', + url: 'foo.com', + } as any, + }))).eql({ + ex: true, + site: { + provider: 'github', + name: 'foo', + url: 'foo.com', + } + }); + }); + }); + + describe('getAndFixCharacterState()', () => { + it('returns saved state', () => { + const server = { id: 'srvr' } as any; + const character = { _id: genObjectId(), state: { srvr: { x: 100, y: 321, map: 'bar' } } } as any; + const map = { id: 'bar', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 100, y: 321, map: 'bar' }); + }); + + it('returns state from from counter service if available', () => { + const server = { id: 'srvr' } as any; + const character = { _id: genObjectId(), state: { srvr: { x: 100, y: 321, map: 'bar' } } } as any; + const map = { id: 'bar', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + states.add(character._id.toString(), { + x: 4, y: 7, map: 'foo', flags: CharacterStateFlags.Right | CharacterStateFlags.Extra + }); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 4, y: 7, map: 'foo', flags: CharacterStateFlags.Right | CharacterStateFlags.Extra }); + }); + + it('creates default state if none is provided', () => { + const server = {} as any; + const character = { _id: genObjectId() } as any; + const map = { id: 'foo', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 10, y: 20, map: 'foo' }); + }); + + it('spawns on main map at spawn point if RespawnAtSpawn flag is set', () => { + const server = { id: 'srvr' } as any; + const character = { + _id: genObjectId(), + state: { srvr: { x: 100, y: 321, map: 'bar' } }, + flags: CharacterFlags.RespawnAtSpawn, + } as any; + const map = { id: 'foo', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 10, y: 20, map: 'foo' }); + }); + }); + + describe('createCharacterState()', () => { + const map = createServerMap('foo', 0, 1, 1); + + it('returns state of character', () => { + expect(createCharacterState(entity(0, 12, 23), map)).eql({ + x: 12, + y: 23, + map: 'foo', + }); + }); + + it('encodes right flag', () => { + const state = createCharacterState(entity(0, 12, 23, 0, { state: EntityState.FacingRight }), map); + + expect(hasFlag(state.flags, CharacterStateFlags.Right)).true; + }); + + it('encodes held object', () => { + const state = createCharacterState(entity(0, 12, 23, 0, { options: { hold: entities.apple.type } }), map); + + expect(state.hold).equal('apple'); + }); + + it('encodes extra flag', () => { + const state = createCharacterState(entity(0, 12, 23, 0, { options: { extra: true } }), map); + + expect(hasFlag(state.flags, CharacterStateFlags.Extra)).true; + }); + }); +}); diff --git a/src/ts/tests/server/chat.spec.ts b/src/ts/tests/server/chat.spec.ts new file mode 100644 index 0000000..9fa731e --- /dev/null +++ b/src/ts/tests/server/chat.spec.ts @@ -0,0 +1,954 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, assert, SinonStub } from 'sinon'; +import { ChatType, Action, MessageType, LeaveReason } from '../../common/interfaces'; +import { GameServerSettings } from '../../common/adminInterfaces'; +import { World } from '../../server/world'; +import { createServerMap } from '../../server/serverMap'; +import { IClient, ServerRegion } from '../../server/serverInterfaces'; +import { mock, mockClient, serverEntity, entity } from '../mocks'; +import { parseExpression } from '../../common/expressionUtils'; +import { + createSay, filterUrls, Say, sayTo, sayToEveryone, sayToOthers, + sayToClientTest as sayToClient, sayToPartyTest as sayToParty, sayWhisperTest as sayWhisper +} from '../../server/chat'; +import { encodeExpression } from '../../common/encoders/expressionEncoder'; +import { toScreenX, toScreenY } from '../../common/positionUtils'; +import { createServerRegion } from '../../server/serverRegion'; +import * as playerUtils from '../../server/playerUtils'; + +describe('chat', () => { + describe('say()', () => { + let client: IClient; + let region: ServerRegion; + let world: World; + let runCommand: SinonStub; + let log: SinonStub; + let say: Say; + let checkSpam: SinonStub; + let reportSwears: SinonStub; + let reportForbidden: SinonStub; + let reportSuspicious: SinonStub; + let isSuspiciousMessage: SinonStub; + let execAction: SinonStub; + + beforeEach(() => { + execAction = stub(playerUtils, 'execAction'); + region = createServerRegion(1, 1); + client = mockClient(); + client.pony.region = region; + region.clients.push(client); + world = mock(World); + const map = createServerMap('', 0, 1, 1); + stub(world, 'getMainMap').returns(map); + runCommand = stub(); + log = stub(); + checkSpam = stub(); + reportSwears = stub(); + reportForbidden = stub(); + reportSuspicious = stub(); + isSuspiciousMessage = stub(); + const spamCommands = ['roll']; + say = createSay( + world, runCommand, log, checkSpam, reportSwears, reportForbidden, + reportSuspicious, spamCommands, () => 0, isSuspiciousMessage); + }); + + afterEach(() => { + execAction.restore(); + }); + + it('does nothing if whispering to self', () => { + say(client, 'hey me', ChatType.Whisper, client, {}); + + assert.notCalled(log); + }); + + it('sends back error message if whispering to missing client', () => { + say(client, 'hey no one', ChatType.Whisper, undefined, {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + }); + + it('runs commands for whispers', () => { + runCommand.returns(true); + const target = mockClient(); + + say(client, '/gifts', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + assert.calledWith(runCommand, client, 'gifts', '', ChatType.Whisper, target, {}); + }); + + it('sends back error message if whispering to non-friend when having non-friend whispers disabled', () => { + client.accountSettings.ignoreNonFriendWhispers = true; + + say(client, 'hey you', ChatType.Whisper, mockClient(), {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([ + [client.pony.id, 'You can only whisper to friends', MessageType.System], + ]); + }); + + it('sends whisper to target', () => { + const target = mockClient(); + + say(client, 'hey you', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + expect(target.saysQueue).eql([[client.pony.id, 'hey you', MessageType.Whisper]]); + }); + + it('does not send whisper to target if shadowed', () => { + const target = mockClient(); + client.shadowed = true; + + say(client, 'hey you', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([[target.pony.id, 'hey you', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([]); + }); + + it('does not send whisper to target if muted', () => { + const target = mockClient(); + client.account.mute = Date.now() + 10000; + + say(client, 'hey you', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([[target.pony.id, 'hey you', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([]); + }); + + it('does not send whisper to target if target is shadowed', () => { + const target = mockClient(); + target.shadowed = true; + + say(client, 'hey you', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + expect(target.saysQueue).eql([]); + }); + + it('does not send whisper to target if target is hidden', () => { + const target = mockClient(); + target.hides.add(client.accountId); + + say(client, 'hey you', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + expect(target.saysQueue).eql([]); + }); + + it('does not check for spam if whispering to friend', () => { + const target = mockClient(); + client.friends.add(target.accountId); + + say(client, 'hey you', ChatType.Whisper, target, {}); + + assert.notCalled(checkSpam); + }); + + it('logs chat message', () => { + say(client, 'hey there', ChatType.Say, undefined, {}); + + assert.calledWith(log, client, 'hey there', ChatType.Say, false); + }); + + it('logs party chat message', () => { + say(client, 'hey there', ChatType.Party, undefined, {}); + + assert.calledWith(log, client, 'hey there', ChatType.Party, false); + }); + + it('trims text', () => { + say(client, ' test ', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); + + it('sends say to everyone in the world', () => { + say(client, 'test', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); + + it('sends say for say command in party chat', () => { + say(client, '/s test', ChatType.Party, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); + + it('sends say for invalid type', () => { + say(client, 'test', 100, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); + + it('sends think message to everyone', () => { + say(client, 'test', ChatType.Think, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Thinking]]); + }); + + it('sends think message to everyone', () => { + say(client, '/t test', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Thinking]]); + }); + + it('ignores empty say command', () => { + say(client, '/s ', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([]); + }); + + it(`reports suspicious message`, () => { + isSuspiciousMessage.withArgs('foo bar').returns(true); + + say(client, 'foo bar', ChatType.Say, undefined, {}); + + assert.calledWith(reportSuspicious, client, 'foo bar'); + }); + + it(`reports suspicious party message with prefix`, () => { + isSuspiciousMessage.withArgs('foo bar').returns(true); + + say(client, 'foo bar', ChatType.Party, undefined, {}); + + assert.calledWith(reportSuspicious, client, '/p foo bar'); + }); + + describe('in a party', () => { + beforeEach(() => { + client.party = { id: '', leader: client, clients: [client], pending: [] }; + }); + + it('sends party message for party type', () => { + say(client, 'test', ChatType.Party, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); + + it('sends party message for party command', () => { + say(client, '/p test', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); + + it('ignores empty party command', () => { + say(client, '/p ', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([]); + }); + + it('sends party think message to party if in party chat', () => { + say(client, '/t test', ChatType.Party, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.PartyThinking]]); + }); + }); + + it('does not set expression in party think command', () => { + client.pony.options!.expr = 123; + + say(client, '/t :)', ChatType.Party, undefined, {}); + + expect(client.pony.options!.expr).equal(123); + }); + + it('runs command if text is command', () => { + runCommand.returns(true); + + say(client, '/test arg', ChatType.Say, undefined, {}); + + assert.calledWith(runCommand, client, 'test', 'arg'); + }); + + it('notifies of invalid command', () => { + runCommand.returns(false); + + say(client, '/test arg', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'Invalid command', MessageType.System]]); + }); + + it('sets expression', () => { + say(client, 'hi :)', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + }); + + it('sets invisible expression', () => { + runCommand.returns(false); + + say(client, '/:)', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + expect(client.saysQueue).eql([]); + }); + + it('sets invisible expression (with space)', () => { + runCommand.returns(false); + + say(client, '/ :)', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + expect(client.saysQueue).eql([]); + }); + + it('does not set expression in think command', () => { + client.pony.options!.expr = 123; + + say(client, '/t :)', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(123); + }); + + it('calls laugh action', () => { + say(client, 'haha', ChatType.Say, undefined, {}); + + assert.calledWith(execAction, client, Action.Laugh); + }); + + it('checks for spam', () => { + const settings = {}; + + say(client, 'test', ChatType.Say, undefined, settings); + + assert.calledWith(checkSpam, client, 'test', settings); + }); + + it('does not check for spam in party chat', () => { + say(client, 'test', ChatType.Party, undefined, {}); + + assert.notCalled(checkSpam); + }); + + it('does not trim repeated letters in party chat', () => { + client.party = { id: '', leader: client, clients: [client], pending: [] }; + + say(client, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ChatType.Party, undefined, {}); + + expect(client.saysQueue).eql([ + [client.pony.id, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', MessageType.Party], + ]); + }); + + it('trims repeated letters', () => { + say(client, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'AAAAAAAAAAAAAAAA…', MessageType.Chat]]); + }); + + it('trims repeated emoji', () => { + say(client, '🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🌸🌸🌸🌸🌸🌸🌸🌸…', MessageType.Chat]]); + }); + + describe('supporter', () => { + it('sends supporter message', () => { + client.supporterLevel = 1; + + say(client, 'hello', ChatType.Supporter, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter1]]); + }); + + it('sends supporter message of correct level (2)', () => { + client.supporterLevel = 2; + + say(client, 'hello', ChatType.Supporter, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter2]]); + }); + + it('sends supporter message of correct level (3)', () => { + client.supporterLevel = 3; + + say(client, 'hello', ChatType.Supporter, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter3]]); + }); + + it('sends supporter message as regular message for non-supporters', () => { + say(client, 'hello', ChatType.Supporter, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); + }); + + it('sends supporter 1 message', () => { + client.supporterLevel = 3; + + say(client, 'hello', ChatType.Supporter1, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter1]]); + }); + + it('sends supporter 2 message', () => { + client.supporterLevel = 3; + + say(client, 'hello', ChatType.Supporter2, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter2]]); + }); + + it('sends supporter 3 message', () => { + client.supporterLevel = 3; + + say(client, 'hello', ChatType.Supporter3, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter3]]); + }); + + it('sends chat message if supporter level is lower than message level', () => { + client.supporterLevel = 2; + + say(client, 'hello', ChatType.Supporter3, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); + }); + + it('sends chat message if non-supporter sends supporter messsage', () => { + say(client, 'hello', ChatType.Supporter, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); + }); + }); + + describe('urls', () => { + it('removes url in regular chat', () => { + say(client, 'hey www.google.com', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'hey [LINK]', MessageType.Chat]]); + }); + + it('removes url in censored messages', () => { + say(client, 'fuck www.google.com', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'fuck [LINK]', MessageType.Chat]]); + }); + + it('does not remove url in party chat', () => { + client.party = { id: '', leader: client, clients: [client], pending: [] }; + + say(client, 'hey www.google.com', ChatType.Party, undefined, {}); + + expect(client.saysQueue).eql([ + [client.pony.id, 'hey www.google.com', MessageType.Party], + ]); + }); + + it('does not report url as swearing', () => { + say(client, 'hey www.google.com', ChatType.Say, undefined, { filterSwears: true }); + + assert.notCalled(reportSwears); + }); + }); + + describe('commands', () => { + it('checks for spam', () => { + runCommand.returns(true); + const settings = {}; + + say(client, '/roll', ChatType.Say, undefined, settings); + + assert.calledWith(checkSpam, client, '/roll', settings); + }); + + it('does not check for spam in party chat', () => { + runCommand.returns(true); + + say(client, '/test', ChatType.Party, undefined, {}); + + assert.notCalled(checkSpam); + }); + + it('does not check for spam for /:)', () => { + runCommand.returns(false); + + say(client, '/:)', ChatType.Say, undefined, {}); + + assert.notCalled(checkSpam); + }); + + it('does not check for spam for /e', () => { + runCommand.returns(true); + + say(client, '/e :)', ChatType.Say, undefined, {}); + + assert.notCalled(checkSpam); + }); + }); + + describe('swear message', () => { + it('does not report normal message', () => { + say(client, 'test', ChatType.Say, undefined, {}); + + assert.notCalled(reportSwears); + }); + + it('does not report is filterSwears is false', () => { + say(client, 'fuck', ChatType.Say, undefined, { filterSwears: false }); + + assert.notCalled(reportSwears); + }); + + it('reports', () => { + const settings = { filterSwears: true }; + + say(client, 'fuck', ChatType.Say, undefined, settings); + + assert.calledWith(reportSwears, client, 'fuck', settings); + }); + }); + + describe('kicking', () => { + let kick: SinonStub; + + beforeEach(() => { + kick = stub(world, 'kick'); + }); + + it('kicks player if messages contain swears and kickSwearing settings is true', () => { + say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true }); + + assert.calledWith(kick, client, 'swearing', LeaveReason.Swearing); + }); + + it('does not kick player if messages contain swears and kickSwearing setting is true but is party message', () => { + say(client, 'fuck', ChatType.Party, undefined, { kickSwearing: true }); + + assert.notCalled(kick); + }); + + it('does not reset player to spawn if kickSwearingToSpawn setting is false', () => { + const resetToSpawn = stub(world, 'resetToSpawn'); + + say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true, kickSwearingToSpawn: false }); + + assert.notCalled(resetToSpawn); + }); + + it('resets player to spawn if kickSwearingToSpawn setting is true', () => { + const resetToSpawn = stub(world, 'resetToSpawn'); + + say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true, kickSwearingToSpawn: true }); + + assert.calledWith(resetToSpawn, client); + }); + }); + }); + + describe('filterUrls()', () => { + [ + '', + 'test', + 'hello there.', + '12.3254', + '999.999.999.999', + 'www...', + 'Pare...Com Isto...', + 'Solo tengo 1.000.000.000.000', + // 'Is a smol .com', + 'battle.net', + 'paint.net', + 'fimfiction.net', + 'fanfiction.net', + 'Serio,com licença', + 'Sim,net caiu', + ].forEach(message => it(`returns the same message: "${message}"`, () => { + expect(filterUrls(message)).equal(message); + })); + + [ + ['1.1.1.1', '[LINK]'], + ['192.168.0.255', '[LINK]'], + ['hello 192.168.0.255 aaa', 'hello [LINK] aaa'], + ['hello192.168.0.255aaa', 'hello[LINK]aaa'], + ['192.168.0.255 aaa 192.168.0.255', '[LINK] aaa [LINK]'], + ['ip:147.230.64.174', 'ip:[LINK]'] + ].forEach(([message, expected]) => it(`replaces ip addresses: "${message}"`, () => { + expect(filterUrls(message)).equal(expected); + })); + + [ + ['http://google.com/', '[LINK]'], + ['HTTP://GOOGLE.COM/', '[LINK]'], + ['https://foo', '[LINK]'], + ['https//last_name_is_.net', '[LINK]'], + ['www.test.pl', '[LINK]'], + ['foo,com/bar', '[LINK]'], + ['a123,net/bar/123', '[LINK]'], + ['foo.com', '[LINK]'], + ['foo.c0m', '[LINK]'], + ['foo.net', '[LINK]'], + ['foo.net/abc/xyz', '[LINK]/abc/xyz'], + ['WWW.test.pl', '[LINK]'], + ['foo.COM', '[LINK]'], + ['hello http://google.com/', 'hello [LINK]'], + ['hello foo.com aaa', 'hello [LINK] aaa'], + ['hellohttp://google.com/', 'hello[LINK]'], + ['http://google.com/ aaa http://google.com/', '[LINK] aaa [LINK]'], + ['foo.com foo.com', '[LINK] [LINK]'], + ['goo.gl/Cjy2Qj', '[LINK]'], + ['goo,gl/Cjy2Qj', '[LINK]'], + ['bit.ly/1mHSR3x', '[LINK]'], + ['adf.ly/13ajex', '[LINK]'], + ['dhttps://www.twitch.tv/foobar', 'd[LINK]'], + ['bestgore. com', '[LINK]'], + [ + 'https:/ mlpfanart.fandom .com/wiki/Banned_From_Equestria_(Daily)', + 'https:/ [LINK]/wiki/Banned_From_Equestria_(Daily)' + ], + ].forEach(([message, expected]) => it(`replaces urls addresses: "${message}"`, () => { + expect(filterUrls(message)).equal(expected); + })); + }); + + describe('sayToClient()', () => { + let client: IClient; + let settings: GameServerSettings; + + beforeEach(() => { + client = mockClient(); + client.camera.w = toScreenX(10); + client.camera.h = toScreenY(10); + settings = {}; + }); + + it('sends message to client', () => { + sayToClient(client, serverEntity(5), 'foo', 'foo', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends original message if filterSwearWords is false', () => { + client.accountSettings = { filterSwearWords: false }; + + sayToClient(client, serverEntity(5), 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends censored message if filterSwearWords is true', () => { + client.accountSettings = { filterSwearWords: true }; + + sayToClient(client, serverEntity(5), 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'bar', MessageType.Chat], + ]); + }); + + it('sends censored message if filterSwears is true', () => { + settings.filterSwears = true; + + sayToClient(client, serverEntity(5), 'foo', '***', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, '***', MessageType.Chat], + ]); + }); + + it('sends original message to self', () => { + client.pony.id = 5; + + sayToClient(client, client.pony, 'foo', 'foo', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends original message to self if filterSwearWords is true', () => { + client.pony.id = 5; + client.account.settings = { filterSwearWords: true }; + + sayToClient(client, client.pony, 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends original message to self if filterSwears is true', () => { + settings.filterSwears = true; + client.pony.id = 5; + + sayToClient(client, client.pony, 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends cyrillic message to self if filterCyrillic setting is true', () => { + client.accountSettings = { filterCyrillic: true }; + client.pony.id = 5; + + sayToClient(client, client.pony, 'Здравствуй', 'Здравствуй', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'Здравствуй', MessageType.Chat], + ]); + }); + + it('ignores messages if ignored', () => { + const e = serverEntity(5); + e.client = mockClient(); + e.client.ignores.add(client.accountId); + + expect(sayToClient(client, e, 'test', 'test', MessageType.Chat, settings)).false; + + expect(client.saysQueue).eql([]); + }); + + it('ignores messages if hidden', () => { + const e = serverEntity(5); + e.client = mockClient(); + client.hides.add(e.client.accountId); + + expect(sayToClient(client, e, 'test', 'test', MessageType.Chat, settings)).false; + + expect(client.saysQueue).eql([]); + }); + + it('ignores messages if swearing in whisper to non-friend', () => { + const e = serverEntity(5); + e.client = mockClient(); + settings.hideSwearing = true; + + expect(sayToClient(client, e, 'test', '****', MessageType.Whisper, settings)).false; + + expect(client.saysQueue).eql([]); + }); + + it('ignores messages if outside client camera', () => { + const e = serverEntity(5, 10, 10); + client.camera.x = toScreenX(20); + client.camera.y = toScreenY(20); + client.camera.w = toScreenX(10); + client.camera.h = toScreenY(10); + client.pony.x = 25; + client.pony.y = 25; + + sayToClient(client, e, 'test', 'test', MessageType.Chat, settings); + + expect(client.saysQueue).eql([]); + }); + + it('ignores messages if contain swears and hideSwearing setting is true', () => { + settings.hideSwearing = true; + + sayToClient(client, serverEntity(5), 'fuck', '****', MessageType.Chat, settings); + + expect(client.saysQueue).eql([]); + }); + + it('does not ignore messages if contain swears and hideSwearing setting is true but sending to self', () => { + settings.hideSwearing = true; + + sayToClient(client, client.pony, 'fuck', '****', MessageType.Chat, settings); + + expect(client.saysQueue).eql([[client.pony.id, 'fuck', MessageType.Chat]]); + }); + + it('does not ignore messages if contain swears and hideSwearing setting is true but is party message', () => { + settings.hideSwearing = true; + + sayToClient(client, serverEntity(6), 'fuck', '****', MessageType.Party, settings); + + expect(client.saysQueue).eql([[6, 'fuck', MessageType.Party]]); + }); + + it('sends party messages even if outside client camera', () => { + const e = serverEntity(5, 1, 1); + client.camera.x = toScreenX(20); + client.camera.y = toScreenY(20); + client.camera.w = toScreenX(10); + client.camera.h = toScreenY(10); + client.pony.x = 25; + client.pony.y = 25; + + sayToClient(client, e, 'test', 'test', MessageType.Party, settings); + + expect(client.saysQueue).eql([[5, 'test', MessageType.Party]]); + }); + + it('sends messages if hidden but client is moderator', () => { + const e = serverEntity(5); + e.client = mockClient(); + client.isMod = true; + client.hides.add(e.client.accountId); + + sayToClient(client, e, 'test', 'test', MessageType.Chat, settings); + + expect(client.saysQueue).eql([[5, 'test', MessageType.Chat]]); + }); + }); + + describe('sayTo()', () => { + it('adds message to message queue', () => { + const client = mockClient(); + + sayTo(client, entity(123), 'test', MessageType.Chat); + + expect(client.saysQueue).eql([[123, 'test', MessageType.Chat]]); + }); + }); + + describe('sayToParty()', () => { + it('sends message to all party members', () => { + const client = mockClient(); + const client2 = mockClient(); + client.party = { clients: [client, client2] } as any; + + sayToParty(client, 'test', MessageType.Party); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + expect(client2.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); + + it('sends message only to client if muted or shadowed', () => { + const client = mockClient(); + const client2 = mockClient(); + client.shadowed = true; + client.party = { clients: [client, client2] } as any; + + sayToParty(client, 'test', MessageType.Party); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + expect(client2.saysQueue).eql([]); + }); + + it('sends error message to client if not in party', () => { + const client = mockClient(); + + sayToParty(client, 'test', MessageType.Party); + + expect(client.saysQueue).eql([[client.pony.id, `you're not in a party`, MessageType.System]]); + }); + }); + + describe('sayWhisper()', () => { + it('sends whisper to client and target', () => { + const client = mockClient(); + const target = mockClient(); + + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); + + expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.Whisper]]); + }); + + it('sends announcement whisper to client and target', () => { + const client = mockClient(); + const target = mockClient(); + + sayWhisper(client, 'hey there', 'hey there', MessageType.WhisperAnnouncement, target, {}); + + expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperToAnnouncement]]); + expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.WhisperAnnouncement]]); + }); + + it('sends error message to client if target is undefined', () => { + const client = mockClient(); + + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + }); + + it('sends error message to client if target is ignoring whispers', () => { + const client = mockClient(); + const target = mockClient(); + target.accountSettings.ignoreNonFriendWhispers = true; + + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); + + expect(client.saysQueue).eql([[client.pony.id, `Can't whisper to this player`, MessageType.System]]); + expect(target.saysQueue).eql([]); + }); + + it('sends whisper to client and target if target is ignoring whispers but is friend of client', () => { + const client = mockClient(); + const target = mockClient(); + target.accountSettings.ignoreNonFriendWhispers = true; + client.friends.add(target.accountId); + + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); + + expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.Whisper]]); + }); + }); + + describe('sayToEveryone()', () => { + it('sends message to all clients', () => { + const client = mockClient(); + const client2 = mockClient(); + const entity = client.pony; + entity.region = createServerRegion(0, 0); + entity.region.clients.push(client, client2); + + sayToEveryone(client, 'test', 'test2', MessageType.Chat, {}); + + expect(client.saysQueue).eql([[entity.id, 'test', MessageType.Chat]]); + expect(client2.saysQueue).eql([[entity.id, 'test', MessageType.Chat]]); + }); + + it('sends message only to client if muted or shadowed', () => { + const client = mockClient(); + const client2 = mockClient(); + client.shadowed = true; + const entity = client.pony; + entity.region = createServerRegion(0, 0); + entity.region.clients.push(client, client2); + + sayToEveryone(client, 'test', 'test', MessageType.Chat, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + expect(client2.saysQueue).eql([]); + }); + + it('does nothing if message is empty', () => { + const client = mockClient(); + + sayToEveryone(client, '', '', MessageType.Chat, {}); + + expect(client.saysQueue).eql([]); + }); + }); + + describe('sayToOthers()', () => { + it('sends message to party if party message', () => { + const client = mockClient(); + client.party = { id: '', leader: client, clients: [client], pending: [] }; + + sayToOthers(client, 'test', MessageType.Party, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); + + it('sends message to everyone if not party message', () => { + const client = mockClient(); + client.pony.region = createServerRegion(0, 0); + client.pony.region.clients.push(client); + + sayToOthers(client, 'test', MessageType.Chat, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); + }); +}); diff --git a/src/ts/tests/server/cmUtils.spec.ts b/src/ts/tests/server/cmUtils.spec.ts new file mode 100644 index 0000000..c9ca783 --- /dev/null +++ b/src/ts/tests/server/cmUtils.spec.ts @@ -0,0 +1,450 @@ +import '../lib'; +import * as fs from 'fs'; +import { expect } from 'chai'; +import { isBadCM, theSameColor, hexToLab } from '../../server/cmUtils'; +import { clearCompareResults } from '../lib'; +import { createCanvas } from '../../server/canvasUtilsNode'; +import { pathTo } from '../../server/paths'; + +const positives: { [key: string]: [string, string[]]; } = { + blackOnTransparent: ['ff0000', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + shortArm: ['ff0000', [ + ' ', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + noCoat: ['', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + blackOnWhite: ['ff0000', [ + '000000', 'ffffff', '000000', '000000', '000000', + '000000', 'ffffff', '000000', 'ffffff', 'ffffff', + '000000', '000000', '000000', '000000', '000000', + 'ffffff', 'ffffff', '000000', 'ffffff', '000000', + '000000', '000000', '000000', 'ffffff', '000000', + ]], + fadingEnds: ['ff0000', [ + ' ', ' ', 'ffa500', 'ffa500', ' ', + 'ffa500', ' ', 'ffa500', ' ', ' ', + 'ffa500', 'ffdb99', 'fff2db', 'ffdb99', 'ffa500', + ' ', ' ', 'ffdb99', ' ', 'ffa500', + ' ', 'ffa500', 'ffa500', + ]], + shortLong: ['000000', [ + ' ', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', ' ', 'ffffff', ' ', 'ffffff', + 'ffffff', 'ffffff', 'ffffff', + ]], + longShort: ['000000', [ + 'ffffff', ' ', 'ffffff', 'ffffff', ' ', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', ' ', 'ffffff', ' ', 'ffffff', + ' ', 'ffffff', 'ffffff', ' ', 'ffffff', + ]], + fadingEndsOnRed: ['ff0000', [ + 'ff0000', 'ff0000', 'ffa500', 'ffa500', 'ff0000', + 'ffa500', 'ff0000', 'ffa500', 'ff0000', 'ff0000', + 'ffa500', 'ffdb99', 'fff2db', 'ffdb99', 'ffa500', + 'ff0000', 'ff0000', 'ffdb99', 'ff0000', 'ffa500', + 'ff0000', 'ffa500', 'ffa500', 'ff0000', 'ff0000', + ]], + fadedRedRect: ['ff0000', [ + '443737', 'e6cdcd', '0c0b0b', '443737', '443737', + '443737', ' ', '773e3e', ' ', 'e6cdcd', + '0c0b0b', '773e3e', '443737', '773e3e', '0c0b0b', + 'e6cdcd', ' ', '773e3e', ' ', '443737', + '443737', '443737', '0c0b0b', 'e6cdcd', '443737', + ]], + fadedRedRectOnWhite: ['ff0000', [ + '443737', 'e6cdcd', '0c0b0b', '443737', '443737', + '443737', 'ffffff', '773e3e', 'ffffff', 'e6cdcd', + '0c0b0b', '773e3e', '443737', '773e3e', '0c0b0b', + 'e6cdcd', 'ffffff', '773e3e', 'ffffff', '443737', + '443737', '443737', '0c0b0b', 'e6cdcd', '443737', + ]], + grayOnGray: ['999595', [ + '403d39', '999595', '403d39', '403d39', '403d39', + '403d39', '999595', '403d39', '999595', '999595', + '403d39', '403d39', '403d39', '403d39', '403d39', + '999595', '999595', '403d39', '999595', '403d39', + '403d39', '403d39', '403d39', '999595', '403d39', + ]], + whiteWithCornersAndEnds: ['000000', [ + 'a5a5a5', '443434', 'a07575', 'ffffff', 'a5a5a5', + 'ffffff', '3d0000', 'ffffff', '3d0000', '443434', + 'a07575', 'ffffff', 'ffffff', 'ffffff', 'a07575', + '443434', '3d0000', 'ffffff', '3d0000', 'ffffff', + 'a5a5a5', 'ffffff', 'a07575', '443434', 'a5a5a5', + ]], + aryanne: ['ebebeb', [ + '000000', '000000', '000000', ' ', '000000', + ' ', 'ffb5b5', '000000', 'ffb5b5', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', 'ffb5b5', '000000', 'ffb5b5', ' ', + '000000', ' ', '000000', '000000', '000000', + ]], + blackWithRedGlowOnPink: ['f9c6ff', [ + '6f2b2b', 'ff9d9d', '52004e', '52004e', '6f2b2b', + '52004e', 'ff6363', '000000', 'ff6363', 'ff9d9d', + '52004e', '000000', '000000', '000000', '52004e', + 'ff9d9d', 'ff6363', '000000', 'ff6363', '52004e', + '6f2b2b', '52004e', '52004e', 'ff9d9d', '6f2b2b', + ]], + rainbod: ['000000', [ + 'b62709', ' ', 'b62709', 'b62709', 'b62709', + 'ee7b11', ' ', 'ee7b11', ' ', ' ', + 'eeb711', 'eeb711', 'eeb711', 'eeb711', 'eeb711', + ' ', ' ', '178c0d', ' ', '178c0d', + '4296df', '4296df', '4296df', ' ', '4296df', + ]], + asymetric: ['aa957e', [ + '000000', '000000', '000000', ' ', ' ', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + ' ', ' ', '000000', '000000', '000000', + ]], + oneLongArm: ['ffffff', [ + ' ', ' ', '000000', '000000', ' ', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', '000000', '000000', ' ', '000000', + ]], + noCorners: ['ffffff', [ + '000000', '000000', ' ', ' ', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', '000000', '000000', '000000', ' ', + '000000', ' ', '000000', ' ', ' ', + '000000', ' ', ' ', '000000', '000000', + ]], + noCornersReverse: ['ffffff', [ + '000000', ' ', ' ', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + ' ', '000000', '000000', '000000', ' ', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', ' ', ' ', '000000', + ]], + faded: ['6d8064', [ + '363636', '6e4b4b', '363636', '363636', '363636', + '363636', '8f4e4e', '363636', '8f4e4e', '6e4b4b', + '363636', '363636', '363636', '363636', '363636', + '6e4b4b', '8f4e4e', '363636', '8f4e4e', '363636', + '363636', '363636', '363636', '6e4b4b', '363636', + ]], + missingBit: ['5d617c', [ + ' ', '464d6e', '464d6e', ' ', ' ', + ' ', ' ', '464d6e', ' ', '464d6e', + '464d6e', '464d6e', '464d6e', '464d6e', '464d6e', + '464d6e', ' ', '464d6e', ' ', ' ', + ' ', ' ', '464d6e', '464d6e', + ]], + smallTopRight: ['', [ + ' ', '160404', '160404', ' ', '160404', + ' ', ' ', '160404', '160404', '160404', + ' ', '160404', '160404', '160404', ' ', + ' ', '160404', ' ', '160404', '160404', + ' ', ' ', ' ', ' ', ' ', + ]], + smallTopLeft: ['', [ + '160404', '160404', ' ', '160404', ' ', + ' ', '160404', '160404', '160404', ' ', + '160404', '160404', '160404', ' ', ' ', + '160404', ' ', '160404', '160404', ' ', + ' ', ' ', ' ', ' ', ' ', + ]], + smallBottomRight: ['', [ + ' ', ' ', ' ', ' ', ' ', + ' ', '160404', '160404', ' ', '160404', + ' ', ' ', '160404', '160404', '160404', + ' ', '160404', '160404', '160404', ' ', + ' ', '160404', ' ', '160404', '160404', + ]], + smallBottomLeft: ['', [ + ' ', ' ', ' ', ' ', ' ', + '160404', '160404', ' ', '160404', ' ', + ' ', '160404', '160404', '160404', ' ', + '160404', '160404', '160404', ' ', ' ', + '160404', ' ', '160404', '160404', ' ', + ]], + smallTopRightRev: ['', [ + ' ', '160404', ' ', '160404', '160404', + ' ', '160404', '160404', '160404', ' ', + ' ', ' ', '160404', '160404', '160404', + ' ', '160404', '160404', ' ', '160404', + ' ', ' ', ' ', ' ', ' ', + ]], + weirdShape: ['', [ + 'fffffe', '000000', '000000', 'fffffe', '000000', + 'fffffe', 'fff9ee', '000000', 'fff9ee', '000000', + 'fffbf4', '000000', '000000', '000000', '000000', + 'fff9ee', '000000', 'fffffe', '000000', 'fff9ee', + 'fff9ee', '000000', 'fffffe', '000000', '000000', + ]], + wonky: ['ffffff', [ + '000000', ' ', '000000', '000000', ' ', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', '000000', ' ', ' ', '000000', + '000000', '000000', ' ', ' ', '000000', + ]], + wonky2: ['ffffff', [ + '000000', ' ', ' ', '000000', '000000', + '000000', ' ', ' ', '000000', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', '000000', '000000', ' ', '000000', + ]], + wonky3: ['ffffff', [ + ' ', ' ', 'ff0000', 'ff0000', 'ff0000', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + 'ff0000', 'ff0000', ' ', ' ', 'ff0000', + ]], + wonky4: ['ffffff', [ + 'ff0000', ' ', 'ff0000', 'ff0000', 'ff0000', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + 'ff0000', 'ff0000', ' ', ' ', ' ', + ]], + wonky5: ['ffffff', [ + 'ff0000', ' ', ' ', 'ff0000', ' ', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + 'ff0000', 'ff0000', ' ', ' ', 'ff0000', + ]], + wonky6: ['ffffff', [ + 'ff0000', ' ', ' ', 'ff0000', 'ff0000', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + ' ', 'ff0000', ' ', ' ', 'ff0000', + ]], + wonky7: ['000000', [ + 'ffffff', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', 'ffffff', 'ffffff', ' ', 'ffffff', + ]], + wonky8: ['000000', [ + 'ffffff', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', 'ffffff', 'ffffff', ' ', 'ffffff', + 'ffffff', 'ffffff', 'ffffff', ' ', 'ffffff', + ]], + wonky9: ['ffffff', [ + '000000', '000000', '000000', 'ffffff', '000000', + 'ffffff', 'ffffff', '160b0b', 'ffffff', '000000', + '000000', '000000', '000000', '000000', 'ffffff', + '000000', 'ffffff', '000000', 'ffffff', 'ffffff', + 'ffffff', 'ffffff', '000000', '000000', '000000', + ]], + wonky10: ['ffffff', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + ' ', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + // layered: ['f0dcff', [ + // '080808', 'd9f4c8', '080808', '6d6d6d', '080808', + // '6d6d6d', 'd9f4c8', '080808', 'd9f4c8', 'd9f4c8', + // '080808', '080808', '6d6d6d', '080808', '080808', + // 'd9f4c8', 'd9f4c8', '080808', 'd9f4c8', '6d6d6d', + // '080808', '6d6d6d', '080808', 'd9f4c8', '080808', + // ]], + // layered2: ['d1b078', [ + // '000000', 'b8ffce', '000000', '065e21', '000000', + // '065e21', 'b8ffce', '065e21', 'b8ffce', 'b8ffce', + // '000000', '065e21', '065e21', '065e21', '000000', + // 'b8ffce', 'b8ffce', '065e21', 'b8ffce', '065e21', + // '000000', '065e21', '000000', 'b8ffce', '000000', + // ]], + // redOrange: ['fed395', [ + // 'ff0000', 'ffa859', 'ff0000', 'ff0000', 'ff0000', + // 'ff0000', 'ffa859', 'ff0000', 'ffa859', 'ffa859', + // 'ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000', + // 'ffa859', 'ffa859', 'ff0000', 'ffa859', 'ff0000', + // 'ff0000', 'ff0000', 'ff0000', 'ffa859', 'ff0000', + // ]], + grayOnGray2: ['808080', [ + '808080', ' ', '808080', '808080', '808080', + '808080', ' ', '808080', ' ', ' ', + '808080', '808080', '808080', '808080', '808080', + ' ', ' ', '808080', ' ', '808080', + '808080', '808080', '808080', ' ', '808080', + ]], + missingArm1: ['ffffff', [ + ' ', ' ', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + missingArm2: ['ffffff', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', ' ', ' ', + ]], + missingArm3: ['ffffff', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', ' ', '000000', ' ', '000000', + ]], +}; + +const negatives: { [key: string]: [string, string[]]; } = { + empty: ['ff0000', []], + short: ['ff0000', ['ff0000', '00ff00']], + black: ['ff0000', [ + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + ]], + random: ['ff0000', [ + '000000', ' ', ' ', '000000', '000000', + '000000', '000000', '000000', ' ', '000000', + '000000', ' ', ' ', '000000', '000000', + ' ', '000000', '000000', '000000', ' ', + '000000', ' ', '000000', '000000', '000000', + ]], + almostSwastika: ['ff0000', [ + '000000', '111111', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', '111111', + '000000', '000000', '000000', '000000', '000000', + '111111', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', '111111', '000000', + ]], + flower: ['d7d8ef', [ + ' ', 'eb7db2', ' ', 'df639f', ' ', + 'df639f', 'ad517d', ' ', 'ad517d', 'eb7db2', + ' ', ' ', 'e8b2cc', ' ', ' ', + 'eb7db2', '8f4569', ' ', '8f4569', 'ad517d', + ' ', 'ad517d', ' ', 'eb7db2', ' ', + ]], + // swirl: ['fee9f8', [ + // ' ', 'ab4842', ' ', ' ', ' ', + // ' ', 'ab4842', ' ', 'ab4842', 'ab4842', + // ' ', ' ', 'ab4842', ' ', ' ', + // 'ab4842', 'ab4842', ' ', 'ab4842', ' ', + // ' ', ' ', ' ', 'ab4842', ' ', + // ]], + floweryThing: ['a636df', [ + ' ', '00b6b9', '208be8', '00026c', ' ', + '00026c', ' ', '13824b', ' ', '00b6b9', + '3c077a', '13824b', ' ', 'bb83fe', '3c077a', + '00b6b9', ' ', 'bb83fe', ' ', '00026c', + ' ', '00026c', '208be8', '00b6b9', ' ', + ]], + hashTag: ['d5d4d4', [ + ' ', '173953', ' ', '1084dc', ' ', + '1084dc', '1084dc', '173953', '1084dc', '173953', + ' ', '173953', '0090fe', '173953', ' ', + '173953', '1084dc', '173953', '1084dc', '1084dc', + ' ', '1084dc', ' ', '173953', ' ', + ]], + flower2: ['ffab8b', [ + ' ', 'ff446a', 'ff6484', 'ff7a95', ' ', + 'ff7a95', 'ff446a', 'ff6484', 'ff446a', 'ff446a', + 'ff6484', 'ff6484', ' ', 'ff6484', 'ff6484', + 'ff446a', 'ff446a', 'ff6484', 'ff446a', 'ff7a95', + ' ', 'ff7a95', 'ff6484', 'ff446a', ' ', + ]], +}; + +function createCMCanvas(coat: string, cm: string[]) { + const canvas = createCanvas(7, 7); + const context = canvas.getContext('2d')!; + context.fillStyle = `#${coat}`; + context.fillRect(0, 0, canvas.width, canvas.height); + + for (let y = 0, i = 0; y < 5; y++) { + for (let x = 0; x < 5; x++ , i++) { + if ((cm[i] || '').trim()) { + context.fillStyle = `#${cm[i]}`; + context.fillRect(x + 1, y + 1, 1, 1); + } + } + } + + return canvas; +} + +function check(name: string, [coat, cm]: [string, string[]], expected: boolean) { + const result = isBadCM(cm.map(x => x.trim()), coat); + + if (!!result !== expected) { + const canvas = createCMCanvas(coat, cm); + fs.writeFileSync(pathTo('tools', 'temp', 'cms', `${name}-${expected ? 'bad' : 'good'}.png`), canvas.toBuffer()); + } + + expect(!!result).equal(expected, result); +} + +function checkColor(a: string, b: string) { + return theSameColor(hexToLab(a), hexToLab(b)); +} + +describe('cmUtils', () => { + before(() => clearCompareResults('cms')); + + describe('isBadCM()', () => { + Object.keys(positives).forEach(key => { + it(`returns true for positive (${key})`, () => { + check(key, positives[key], true); + }); + }); + + Object.keys(negatives).forEach(key => { + it(`returns false for negative (${key})`, () => { + check(key, negatives[key], false); + }); + }); + }); + + describe('checkColor()', () => { + it('returns false for 000000 and ffffff', () => { + expect(checkColor('000000', 'ffffff')).false; + }); + + it('returns true for 000000 and 000000', () => { + expect(checkColor('000000', '000000')).true; + }); + + it('returns true for 000000 and 52004e', () => { + //console.log('000000', '52004e', getDeltaE00(hexToLab('000000'), hexToLab('52004e'))); + expect(checkColor('000000', '52004e')).true; + }); + + it('returns false for eb7db2 and d7d8ef', () => { + //console.log('eb7db2', 'd7d8ef', getDeltaE00(hexToLab('eb7db2'), hexToLab('d7d8ef'))); + expect(checkColor('eb7db2', 'd7d8ef')).false; + }); + }); +}); diff --git a/src/ts/tests/server/commands.spec.ts b/src/ts/tests/server/commands.spec.ts new file mode 100644 index 0000000..73fd47a --- /dev/null +++ b/src/ts/tests/server/commands.spec.ts @@ -0,0 +1,615 @@ +import '../lib'; +import { expect } from 'chai'; +import { assert, stub, SinonStub } from 'sinon'; +import { mockClient } from '../mocks'; +import { ChatType, MessageType, Action, ExpressionExtra, Eye, Muzzle } from '../../common/interfaces'; +import { IClient } from '../../server/serverInterfaces'; +import { UserError } from '../../server/userError'; +import { + getChatPrefix, parseCommand, createRunCommand, CommandContext, Command, createCommands, RunCommand +} from '../../server/commands'; +import { parseExpression, expression } from '../../common/expressionUtils'; +import { encodeExpression } from '../../common/encoders/expressionEncoder'; +import { createServerMap } from '../../server/serverMap'; +import * as playerUtils from '../../server/playerUtils'; + +describe('commands', () => { + describe('parseCommand()', () => { + it('returns text and type for regular text', () => { + expect(parseCommand('hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Say }); + }); + + it('returns party chat type for /p command', () => { + expect(parseCommand('/p hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Party }); + }); + + it('returns supporter chat type for /ss command', () => { + expect(parseCommand('/ss hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter }); + }); + + it('returns supporter 1 chat type for /s1 command', () => { + expect(parseCommand('/s1 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter1 }); + }); + + it('returns supporter 2 chat type for /s2 command', () => { + expect(parseCommand('/s2 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter2 }); + }); + + it('returns supporter 3 chat type for /s3 command', () => { + expect(parseCommand('/s3 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter3 }); + }); + + it('returns say chat type for /s command', () => { + expect(parseCommand('/s hello', ChatType.Party)).eql({ args: 'hello', type: ChatType.Say }); + }); + + it('returns think chat type for /t command', () => { + expect(parseCommand('/t hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Think }); + }); + + it('returns think chat type for /T command', () => { + expect(parseCommand('/T hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Think }); + }); + + it('returns party think chat type for /t command in party chat', () => { + expect(parseCommand('/t hello', ChatType.Party)).eql({ args: 'hello', type: ChatType.PartyThink }); + }); + + it('returns correct command', () => { + expect(parseCommand('/test hello', ChatType.Say)).eql({ command: 'test', args: 'hello', type: ChatType.Say }); + }); + + it('returns correct command with no arguments', () => { + expect(parseCommand('/test', ChatType.Say)).eql({ command: 'test', args: '', type: ChatType.Say }); + }); + + it('keeps the same chat type for commands', () => { + expect(parseCommand('/test', ChatType.Party)).eql({ command: 'test', args: '', type: ChatType.Party }); + }); + + it('trims args', () => { + expect(parseCommand('/test foo bar ', ChatType.Say)).eql({ command: 'test', args: 'foo bar', type: ChatType.Say }); + }); + + it('returns command name lowercased', () => { + expect(parseCommand('/Te&$St foo', ChatType.Say)).eql({ command: 'Te&$St', args: 'foo', type: ChatType.Say }); + }); + }); + + describe('getChatPrefix()', () => { + it('returns empty string for regular chat', () => { + expect(getChatPrefix(ChatType.Say)).equal(''); + }); + + it('returns "/p " for party chat', () => { + expect(getChatPrefix(ChatType.Party)).equal('/p '); + }); + + it('returns "/p " for party thinking', () => { + expect(getChatPrefix(ChatType.PartyThink)).equal('/p '); + }); + + it('returns "" for thinking', () => { + expect(getChatPrefix(ChatType.Think)).equal(''); + }); + + it('returns "/ss " for supporter chat', () => { + expect(getChatPrefix(ChatType.Supporter)).equal('/ss '); + }); + + it('returns "" for supporter 1 chat', () => { + expect(getChatPrefix(ChatType.Supporter1)).equal(''); + }); + + it('returns "" for supporter 2 chat', () => { + expect(getChatPrefix(ChatType.Supporter2)).equal(''); + }); + + it('returns "" for supporter 3 chat', () => { + expect(getChatPrefix(ChatType.Supporter3)).equal(''); + }); + }); + + describe('runCommand()', () => { + let client: IClient; + let context: CommandContext; + let command: Command; + let runCommand: RunCommand; + let handler: SinonStub; + + beforeEach(() => { + handler = stub(); + command = { names: ['test'], handler, help: '', role: '' }; + client = mockClient(); + context = { + liveSettings: {} as any, + world: { sayTo() { } } as any, + notifications: {} as any, + party: {} as any, + random: () => 0, + }; + runCommand = createRunCommand(context, [command]); + }); + + it('runs given command', () => { + runCommand(client, 'test', 'args', ChatType.Say, undefined, {}); + + assert.calledWith(handler, context, client, 'args', ChatType.Say); + }); + + it('returns true if run command', () => { + expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).true; + }); + + it('returns true if command does not exist', () => { + expect(runCommand(client, 'foo', '', ChatType.Say, undefined, {})).false; + }); + + it('should not run command if client is missing required role', () => { + command.role = 'admin'; + + expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).false; + }); + + it('should run command if client has required role', () => { + client.account.roles = ['admin']; + command.role = 'admin'; + + expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).true; + }); + + it('sends user error to user', () => { + handler.throws(new UserError('test error')); + + runCommand(client, 'test', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([ + [client.pony.id, 'test error', MessageType.System], + ]); + }); + + it('rethrows non-user error', () => { + expect(() => { + handler.throws(new Error('test error')); + runCommand(client, 'test', '', ChatType.Say, undefined, {}); + }).throw('test error'); + }); + + it('should find correct command case-insensitive', () => { + expect(runCommand(client, 'TeSt', '', ChatType.Say, undefined, {})).true; + }); + }); + + describe('individual commands', () => { + let client: IClient; + let context: CommandContext; + let runCommand: RunCommand; + let execAction: SinonStub; + + beforeEach(() => { + execAction = stub(playerUtils, 'execAction'); + client = mockClient(); + client.map = createServerMap('', 0, 3, 3); + client.pony.region = client.map.regions[0]; + client.pony.region.clients.push(client); + client.account.roles = ['mod', 'admin']; + client.isMod = true; + context = { + liveSettings: {} as any, + world: { + featureFlags: { flying: true, swap: true, friends: true }, + getSettings: () => ({}), + action() { }, + unholdItem() { }, + sayTo() { }, + sayToOthers() { }, + sayToEveryone() { }, + setTime() { }, + resetToSpawn() { }, + kick() { }, + fixPosition() { }, + } as any, + notifications: {} as any, + party: {} as any, + random: () => 0, + }; + const commands = createCommands(context.world); + runCommand = createRunCommand(context, commands); + }); + + afterEach(() => { + execAction.restore(); + }); + + describe('/help', () => { + it('prints commands help', () => { + runCommand(client, 'help', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue.length).equal(1); + expect(client.saysQueue[0][0]).equal(client.pony.id); + expect(client.saysQueue[0][2]).equal(MessageType.System); + }); + }); + + describe('/roll', () => { + it('rolls random number from 1 to 100 without args', () => { + stub(context, 'random').withArgs(1, 100).returns(12); + + runCommand(client, 'roll', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 12 of 100', MessageType.Announcement]]); + }); + + it('rolls random number from 1 to given number', () => { + stub(context, 'random').withArgs(1, 50).returns(12); + + runCommand(client, 'roll', '50', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 12 of 50', MessageType.Announcement]]); + }); + + it('rolls random number between given numbers', () => { + stub(context, 'random').withArgs(50, 200).returns(123); + + runCommand(client, 'roll', '50-200', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 123 of 50-200', MessageType.Announcement]]); + }); + + it('clamps minimum and maximum', () => { + stub(context, 'random').withArgs(1000000, 1000000).returns(1000000); + + runCommand(client, 'roll', '999999999-999999999', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 1000000 of 1000000-1000000', MessageType.Announcement]]); + }); + + it(`uses default behaviour if args don't match pattern`, () => { + stub(context, 'random').withArgs(1, 100).returns(50); + + runCommand(client, 'roll', 'foo bar', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 50 of 100', MessageType.Announcement]]); + }); + + it('rolls apple', () => { + runCommand(client, 'roll', '🍎', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 🍎 of 100', MessageType.Announcement]]); + }); + }); + + describe('/e', () => { + it('updates permanent expression', () => { + runCommand(client, 'e', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.exprPermanent).eql(parseExpression('>:|')); + }); + + it('resets existing expression', () => { + runCommand(client, 'e', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression('>:|'))); + }); + }); + + describe('/boop /)', () => { + it('invokes action', () => { + runCommand(client, 'boop', '', ChatType.Say, undefined, {}); + + assert.calledWith(execAction, client, Action.Boop); + }); + + it('sets expression', () => { + runCommand(client, 'boop', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression('>:|'))); + }); + }); + + describe('/drop', () => { + it('invokes action', () => { + runCommand(client, 'drop', '', ChatType.Say, undefined, {}); + + assert.calledWith(execAction, client, Action.Drop); + }); + }); + + describe('/turn', () => { + it('invokes action', () => { + runCommand(client, 'turn', '', ChatType.Say, undefined, {}); + + assert.calledWith(execAction, client, Action.TurnHead); + }); + }); + + describe('/blush', () => { + it('sets expression with blush', () => { + runCommand(client, 'blush', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...parseExpression('>:|'), + extra: ExpressionExtra.Blush, + } as any)); + }); + + it('uses current expression if available', () => { + client.pony.options = { expr: encodeExpression(parseExpression(':(')) }; + + runCommand(client, 'blush', '', ChatType.Say, undefined, {}); + + expect(client.pony.options.expr).equal(encodeExpression({ + ...parseExpression(':('), + extra: ExpressionExtra.Blush, + } as any)); + }); + + it('uses default expression if not provided', () => { + runCommand(client, 'blush', '', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral), + extra: ExpressionExtra.Blush, + })); + }); + + it('passes cancellable flag', () => { + client.pony.exprCancellable = true; + + runCommand(client, 'blush', '', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral), + extra: ExpressionExtra.Blush, + })); + }); + }); + + describe('/gifts', () => { + it('announces collected gifts count', () => { + client.account.state = { gifts: 5 }; + + runCommand(client, 'gifts', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🎁', MessageType.Announcement]]); + }); + + it('announces 0 collected gifts if missing gifts entry', () => { + runCommand(client, 'gifts', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🎁', MessageType.Announcement]]); + }); + }); + + describe('/candies', () => { + it('announces collected candies count', () => { + client.account.state = { candies: 5 }; + + runCommand(client, 'candies', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🍬', MessageType.Announcement]]); + }); + + it('announces 0 collected candies if missing candies entry', () => { + runCommand(client, 'candies', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🍬', MessageType.Announcement]]); + }); + }); + + describe('/clovers', () => { + it('announces collected clovers count', () => { + client.account.state = { clovers: 5 }; + + runCommand(client, 'clovers', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🍀', MessageType.Announcement]]); + }); + + it('announces 0 collected clovers if missing clovers entry', () => { + runCommand(client, 'clovers', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🍀', MessageType.Announcement]]); + }); + }); + + describe('/unstuck', () => { + it('resets client to spawn', () => { + const resetToSpawn = stub(context.world, 'resetToSpawn'); + + runCommand(client, 'unstuck', '', ChatType.Say, undefined, {}); + + assert.calledWith(resetToSpawn, client); + }); + + it('kicks client', () => { + const kick = stub(context.world, 'kick'); + + runCommand(client, 'unstuck', '', ChatType.Say, undefined, {}); + + assert.calledWith(kick, client); + }); + }); + + describe('/sleep', () => { + it('sets expression with sleeping', () => { + runCommand(client, 'sleep', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...parseExpression('>:|'), + left: Eye.Closed, + right: Eye.Closed, + extra: ExpressionExtra.Zzz, + } as any)); + }); + + it('uses current expression if available', () => { + client.pony.options = { expr: encodeExpression(parseExpression(':(')) }; + + runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); + + expect(client.pony.options.expr).equal(encodeExpression({ + ...parseExpression(':('), + left: Eye.Closed, + right: Eye.Closed, + extra: ExpressionExtra.Zzz, + } as any)); + }); + + it('uses default expression if not provided', () => { + runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Closed, Eye.Closed, Muzzle.Neutral), + extra: ExpressionExtra.Zzz, + })); + }); + + it('closes mouth', () => { + runCommand(client, 'sleep', ':D', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Closed, Eye.Closed, Muzzle.Neutral), + extra: ExpressionExtra.Zzz, + })); + }); + + it('does nothing if moving', () => { + client.pony.vx = 1; + client.pony.options!.expr = 123; + + runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(123); + }); + }); + + describe('/cry', () => { + it('sets expression with tears', () => { + runCommand(client, 'cry', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...parseExpression('>:|'), + extra: ExpressionExtra.Cry, + } as any)); + }); + + it('uses default expression if not provided', () => { + runCommand(client, 'cry', '', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Sad, Eye.Sad, Muzzle.Frown), + extra: ExpressionExtra.Cry, + } as any)); + }); + }); + + describe('/smile', () => { + it('sets expression', () => { + runCommand(client, 'smile', '', ChatType.Say, undefined, {}); + + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + }); + }); + + describe('/yawn', () => { + it('invokes action', () => { + runCommand(client, 'yawn', '', ChatType.Say, undefined, {}); + + assert.calledWith(execAction, client, Action.Yawn); + }); + }); + + describe('/m', () => { + it('send mod message', () => { + runCommand(client, 'm', 'message', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'message', MessageType.Mod]]); + }); + }); + + describe('/a', () => { + it('send admin message', () => { + runCommand(client, 'a', 'message', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'message', MessageType.Admin]]); + }); + }); + + describe('/time', () => { + it('sets world time', () => { + const setTime = stub(context.world, 'setTime'); + + runCommand(client, 'time', '12', ChatType.Say, undefined, {}); + + assert.calledWith(setTime, 12); + }); + + it('does modulo 24 on hour', () => { + const setTime = stub(context.world, 'setTime'); + + runCommand(client, 'time', '26', ChatType.Say, undefined, {}); + + assert.calledWith(setTime, 2); + }); + + it('prints error if args are invalid', () => { + runCommand(client, 'time', 'foo', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([ + [client.pony.id, 'invalid parameter', MessageType.System], + ]); + }); + }); + + describe('/tp', () => { + it('fixes location of player', () => { + const fixPosition = stub(client, 'fixPosition'); + + runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); + + assert.calledWith(fixPosition, 10, 20, true); + expect(client.pony.x).equal(10); + expect(client.pony.y).equal(20); + }); + + it('updates safe position', () => { + client.pony.x = 10; + client.pony.y = 20; + + runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); + + expect(client.safeX).equal(10); + expect(client.safeY).equal(20); + }); + + it('resets lastTime to zero', () => { + runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); + + expect(client.lastTime).equal(0); + }); + + it('throws on invalid parameters', () => { + runCommand(client, 'tp', '10', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([ + [client.pony.id, 'invalid parameters', MessageType.System], + ]); + }); + }); + + const placeholderCommands = [ + 's', 'say', 'p', 'party', 't', 'think', 'ss', 's1', 's2', 's3', 'sit', 'stand', 'lie', 'lay', 'fly', + 'w', 'whisper', 'r', 'reply', + ]; + + placeholderCommands.forEach(command => { + describe(`/${command}`, () => { + it('throws', () => { + expect(() => runCommand(client, command, 'foo bar', ChatType.Say, undefined, {})) + .throw('Should not be called'); + }); + }); + }); + }); +}); diff --git a/src/ts/tests/server/entityUtils.spec.ts b/src/ts/tests/server/entityUtils.spec.ts new file mode 100644 index 0000000..c3e6412 --- /dev/null +++ b/src/ts/tests/server/entityUtils.spec.ts @@ -0,0 +1,141 @@ +import '../lib'; +import { getWriterBuffer } from 'ag-sockets'; +import { expect } from 'chai'; +import { stub, assert } from 'sinon'; +import { findClosest, updateEntityOptions, updateEntityState, fixPosition } from '../../server/entityUtils'; +import { mockClient, serverEntity } from '../mocks'; +import { createServerRegion } from '../../server/serverRegion'; +import { UpdateFlags } from '../../common/interfaces'; +import { IClient } from '../../server/serverInterfaces'; +import { createServerMap } from '../../server/serverMap'; + +describe('entityUtils [server]', () => { + describe('findClosest()', () => { + it('returns undefined for empty list', () => { + expect(findClosest(0, 0, [])); + }); + + it('returns closest entity', () => { + const entities: any[] = [ + { x: 1, y: 0 }, + { x: 0, y: 0 }, + { x: 0, y: 1 }, + ]; + + expect(findClosest(0, 0, entities)).equal(entities[1]); + }); + }); + + describe('updateEntityOptions()', () => { + it('updates entity options field', () => { + const entity = serverEntity(2, 0, 0, 0, { options: { tag: 'bar' } }); + + updateEntityOptions(entity, { expr: 5 }); + + expect(entity.options).eql({ tag: 'bar', expr: 5 }); + }); + + it('handles undefined options field', () => { + const entity = serverEntity(2); + entity.options = undefined; + + updateEntityOptions(entity, { expr: 5 }); + + expect(entity.options).eql({ expr: 5 }); + }); + + it('adds update to region', () => { + const entity = serverEntity(2, 0, 0, 0, { options: { tag: 'bar' } }); + entity.region = createServerRegion(0, 0); + entity.region.clients.push(mockClient(), mockClient()); + + updateEntityOptions(entity, { expr: 5 }); + + expect(entity.region.entityUpdates).eql([ + { entity, flags: UpdateFlags.Options, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: { expr: 5 } }, + ]); + }); + }); + + describe('updateEntityState()', () => { + it('sets flags on entity', () => { + const entity = serverEntity(0); + + updateEntityState(entity, 123); + + expect(entity.state).equal(123); + }); + + it('adds flag update to region updates', () => { + const entity = serverEntity(0); + const region = createServerRegion(0, 0); + entity.client = mockClient({}); + entity.region = region; + + updateEntityState(entity, 123); + + expect(region.entityUpdates).eql([ + { entity, flags: UpdateFlags.State, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }, + ]); + }); + + it('sends flag update to client if shadowed', () => { + const entity = serverEntity(12); + entity.region = createServerRegion(0, 0); + entity.client = mockClient({ shadowed: true }); + + updateEntityState(entity, 123); + + expect(getWriterBuffer(entity.client.updateQueue)).eql(new Uint8Array([2, 0, 4, 0, 0, 0, 12, 123])); + }); + }); + + describe('fixPosition()', () => { + let client: IClient; + + beforeEach(() => { + client = mockClient(); + client.map = createServerMap('', 0, 1, 1); + }); + + it('updates entity position to given position', () => { + const entity = serverEntity(1); + entity.x = 10; + entity.y = 5; + + fixPosition(entity, client.map, 1, 2, false); + + expect(entity.x).equal(1); + expect(entity.y).equal(2); + }); + + it('submits entity update to region', () => { + const region = createServerRegion(0, 0); + const entity = serverEntity(1); + entity.region = region; + + fixPosition(entity, client.map, 1, 2, false); + + expect(region.entityUpdates).eql([ + { + entity, flags: UpdateFlags.Position | UpdateFlags.State, x: 1, y: 2, vx: 0, vy: 0, + action: 0, playerState: 0, options: undefined + }, + ]); + }); + + it('sends fix position message to client', () => { + const fixPositionStub = stub(client, 'fixPosition'); + + fixPosition(client.pony, client.map, 1, 2, false); + + assert.calledWith(fixPositionStub, 1, 2, false); + }); + + it('sets fixing flag on client', () => { + fixPosition(client.pony, client.map, 1, 2, false); + + expect(client.fixingPosition).true; + }); + }); +}); diff --git a/src/ts/tests/server/liveEndPoint.spec.ts b/src/ts/tests/server/liveEndPoint.spec.ts new file mode 100644 index 0000000..8aeb15d --- /dev/null +++ b/src/ts/tests/server/liveEndPoint.spec.ts @@ -0,0 +1,259 @@ +import '../lib'; +import { Model } from 'mongoose'; +import { expect } from 'chai'; +import { stub, match, assert, useFakeTimers, SinonFakeTimers, SinonStub } from 'sinon'; +import { LiveEndPoint, createLiveEndPoint } from '../../server/liveEndPoint'; +import { Doc } from '../../server/db'; +import { ITEM_LIMIT } from '../../common/adminInterfaces'; +import { MINUTE } from '../../common/constants'; +import { times } from '../../common/utils'; + +interface Thing extends Doc { + name: string; + desc: string; +} + +describe('liveEndPoint', () => { + let clock: SinonFakeTimers; + let model: Model; + let encode: SinonStub; + let beforeDelete: SinonStub; + let afterDelete: SinonStub; + let afterAssign: SinonStub; + let liveEndPoint: LiveEndPoint; + + function stubFind(items: any) { + return stub(model, 'find').returns({ + sort: stub().withArgs(match({ updatedAt: 1 })).returns({ + limit: stub().withArgs(ITEM_LIMIT + 1).returns({ + lean: stub().returns({ + exec: () => Promise.resolve(items) + }) + }) + }) + } as any); + } + + beforeEach(() => { + clock = useFakeTimers(); + model = { + findByIdAndUpdate() { }, + findById() { }, + find() { }, + } as any; + encode = stub(); + beforeDelete = stub(); + afterDelete = stub(); + afterAssign = stub(); + liveEndPoint = createLiveEndPoint({ + model, fields: ['_id', 'name', 'desc'], encode, beforeDelete, afterDelete, afterAssign + }); + }); + + afterEach(() => { + clock.restore(); + liveEndPoint.destroy(); + }); + + it('clears removed items after 10 minutes', () => { + const item = { _id: 'foo', remove: stub() }; + (stub(model, 'findById') as any).withArgs('foo').returns({ exec: () => Promise.resolve(item) }); + clock.setSystemTime(10000); + stubFind([]); + + return liveEndPoint.removeItem('foo') + .then(() => clock.tick(1 * MINUTE + 100)) + .then(() => liveEndPoint.getAll()) + .then(result => expect(result.deletes).eql(['foo'])); + }); + + describe('get()', () => { + it('finds item by id', () => { + const item = {}; + (stub(model, 'findById') as any).withArgs('foo').returns({ + lean: stub().returns({ + exec: stub().resolves(item) + }) + }); + + return liveEndPoint.get('foo') + .then(result => expect(result).equal(item)); + }); + }); + + describe('getAll()', () => { + it('returns encoded items', () => { + stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); + encode.returns('encoded'); + + return liveEndPoint.getAll() + .then(result => expect(result).eql({ + base: {}, + deletes: [], + updates: 'encoded', + more: false, + })); + }); + + it('passes 0 timestamp to find method by default', () => { + const find = stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); + + return liveEndPoint.getAll() + .then(() => assert.calledWithMatch(find, { updatedAt: { $gt: new Date(0) } }, '_id name desc')); + }); + + it('passes given timestamp to find method', () => { + const timestamp = '2017-09-09T16:39:54.199Z'; + const find = stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); + + return liveEndPoint.getAll(timestamp) + .then(() => assert.calledWithMatch(find, { updatedAt: { $gt: new Date(timestamp) } }, '_id name desc')); + }); + + describe('if items exceed limit', () => { + describe('if last 2 items have different updatedAt', () => { + beforeEach(() => { + const items = times(ITEM_LIMIT + 1, i => ({ _id: `foo_${i}`, updatedAt: new Date(i) })); + stubFind(items); + }); + + it('removes last item', () => { + return liveEndPoint.getAll() + .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT)); + }); + + it('returns more flag', () => { + return liveEndPoint.getAll() + .then(({ more }) => expect(more).true); + }); + }); + + describe('if last 2 items have the same updatedAt', () => { + let items: any[]; + let restItems: any[]; + let find: SinonStub; + + beforeEach(() => { + items = times(ITEM_LIMIT, i => ({ _id: `foo_${i}`, updatedAt: new Date(i) })); + items.push({ _id: 'bar', updatedAt: items[items.length - 1].updatedAt }); + restItems = [{ _id: 'bar1' }, { _id: 'bar2' }]; + find = stubFind(items).onSecondCall().returns({ + lean: stub().returns({ + exec: stub().resolves(restItems) + }) + } as any); + }); + + it('fetches additional items if last items have the same date', () => { + return liveEndPoint.getAll() + .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT + 1 + 2)); + }); + + it('filters out duplicate items', () => { + restItems.push(items[items.length - 1]); + + return liveEndPoint.getAll() + .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT + 1 + 2)); + }); + + it('fetches more items using timestamp of last element', () => { + const timestamp = items[items.length - 1].updatedAt; + + return liveEndPoint.getAll() + .then(() => assert.calledWithMatch(find, { updatedAt: timestamp }, '_id name desc')); + }); + + it('returns more flag', () => { + return liveEndPoint.getAll() + .then(({ more }) => expect(more).true); + }); + }); + }); + }); + + describe('removeItem()', () => { + describe('if item exists', () => { + let item: { _id: string; remove: SinonStub; }; + + beforeEach(() => { + item = { _id: 'foo', remove: stub() }; + (stub(model, 'findById') as any).withArgs('foo').returns({ exec: stub().resolves(item) }); + }); + + it('removes item', () => { + return liveEndPoint.removeItem('foo') + .then(() => assert.calledOnce(item.remove)); + }); + + it('calls beforeDelete hook', () => { + return liveEndPoint.removeItem('foo') + .then(() => assert.calledWith(beforeDelete, item)); + }); + + it('calls afterDelete hook', () => { + return liveEndPoint.removeItem('foo') + .then(() => assert.calledWith(afterDelete, item)); + }); + + it('adds item ID to removed items', () => { + clock.setSystemTime(10000); + stubFind([]); + + return liveEndPoint.removeItem('foo') + .then(() => liveEndPoint.getAll()) + .then(result => expect(result.deletes).eql(['foo'])); + }); + }); + + describe('if item does not exist', () => { + let item: { remove: SinonStub }; + + beforeEach(() => { + item = { remove: stub() }; + (stub(model, 'findById') as any).withArgs('bar').returns({ exec: stub().resolves(null as any) }); + }); + + it('does nothing if item does not exist', () => { + return liveEndPoint.removeItem('bar') + .then(() => assert.notCalled(item.remove)); + }); + + it('does not call onDelete hook', () => { + return liveEndPoint.removeItem('bar') + .then(() => assert.notCalled(beforeDelete)); + }); + + it('does not call hook', () => { + return liveEndPoint.removeItem('bar') + .then(() => assert.notCalled(afterDelete)); + }); + }); + }); + + describe('assignAccount()', () => { + const item = { account: 'origacc' }; + + beforeEach(() => { + (stub(model, 'findById') as any).withArgs('foo', 'account') + .returns({ lean: stub().returns({ exec: stub().resolves(item) }) }); + }); + + it('assigns account to item', () => { + const exec = stub(); + const findByIdAndUpdate = stub(model, 'findByIdAndUpdate').returns({ exec } as any); + + return liveEndPoint.assignAccount('foo', 'bar') + .then(() => { + assert.calledWithMatch(findByIdAndUpdate as any, 'foo', { account: 'bar' }); + assert.calledOnce(exec); + }); + }); + + it('calls afterAssign hook', () => { + stub(model, 'findByIdAndUpdate').returns({ exec: stub() } as any); + + return liveEndPoint.assignAccount('foo', 'bar') + .then(() => assert.calledWith(afterAssign, 'origacc', 'bar')); + }); + }); +}); diff --git a/src/ts/tests/server/mapUtils.spec.ts b/src/ts/tests/server/mapUtils.spec.ts new file mode 100644 index 0000000..3e031ed --- /dev/null +++ b/src/ts/tests/server/mapUtils.spec.ts @@ -0,0 +1,137 @@ +import '../lib'; +import { expect } from 'chai'; +import { createServerMap, findEntities, findClosestEntity } from '../../server/serverMap'; +import { serverEntity } from '../mocks'; +import { ServerEntity, ServerMap, ServerRegion } from '../../server/serverInterfaces'; +import { addEntityToRegion } from '../../server/serverRegion'; +import { getRegion } from '../../common/worldMap'; + +describe('mapUtils', () => { + let map: ServerMap; + + function addEntities(region: ServerRegion, ...entities: ServerEntity[]) { + entities.forEach(e => addEntityToRegion(region, e, map)); + } + + beforeEach(() => { + map = createServerMap('', 0, 10, 10); + }); + + describe('findEntities()', () => { + it('returns all entities matching given predicate (1)', () => { + const entity = serverEntity(3); + addEntities(getRegion(map, 3, 4), serverEntity(1), serverEntity(2), entity); + + expect(findEntities(map, e => e.id === 3)).eql([entity]); + }); + + it('returns all entities matching given predicate (2)', () => { + const entity3 = serverEntity(3); + const entity2 = serverEntity(3); + addEntities(getRegion(map, 3, 4), serverEntity(1), entity2, entity3); + + expect(findEntities(map, e => e.id > 1)).eql([entity2, entity3]); + }); + + it('returns empty array if not found', () => { + expect(findEntities(map, e => e.id === 3)).eql([]); + }); + }); + + describe('findClosestEntity()', () => { + it('returns undefined for empty map', () => { + const map = createServerMap('', 0, 1, 1); + + const result = findClosestEntity(map, 0, 0, () => true); + + expect(result).undefined; + }); + + it('returns first matching entity (first)', () => { + const map = createServerMap('', 0, 1, 1); + const entity = serverEntity(1); + map.regions[0].entities.push(entity); + + const result = findClosestEntity(map, 0, 0, () => true); + + expect(result).equal(entity); + }); + + it('returns first matching entity (second)', () => { + const map = createServerMap('', 0, 1, 1); + const entity1 = serverEntity(1); + const entity2 = serverEntity(2); + map.regions[0].entities.push(entity1, entity2); + + const result = findClosestEntity(map, 0, 0, e => e.id === 2); + + expect(result).equal(entity2); + }); + + it('returns first matching entity (in 2nd region)', () => { + const map = createServerMap('', 0, 2, 2); + const entity1 = serverEntity(1); + const entity2 = serverEntity(2); + map.regions[0].entities.push(entity1); + map.regions[1].entities.push(entity2); + + const result = findClosestEntity(map, 0, 0, e => e.id === 2); + + expect(result).equal(entity2); + }); + + it('returns closest matching entity (2nd is closest)', () => { + const map = createServerMap('', 0, 1, 1); + const entity1 = serverEntity(1, 0, 0); + const entity2 = serverEntity(2, 1, 1); + map.regions[0].entities.push(entity1, entity2); + + const result = findClosestEntity(map, 1, 1, () => true); + + expect(result).equal(entity2); + }); + + it('stops searching if found in first region', () => { + const map = createServerMap('', 0, 2, 2); + const entity1 = serverEntity(1, 0, 0); + const entity2 = serverEntity(2, 11, 11); + map.regions[0].entities.push(entity1); + map.regions[1].entities.push(entity2); + let checks = 0; + + const result = findClosestEntity(map, 1, 1, () => (checks++ , true)); + + expect(checks).equal(1); + expect(result).equal(entity1); + }); + + it('searches for entity in ring pattern', () => { + const map = createServerMap('', 0, 5, 5); + + for (let y = 0; y < 5; y++) { + for (let x = 0; x < 5; x++) { + getRegion(map, x, y).entities.push(serverEntity(0, x * 8 + 1, y * 8 + 1, 1, { name: `${x},${y}` })); + } + } + + let checks: string[] = []; + + const result = findClosestEntity(map, map.width / 2, map.height / 2, e => (checks.push(e.name!), false)); + + expect(checks).eql([ + '2,2', + + '1,1', '2,1', '3,1', + '1,2', /* */ '3,2', + '1,3', '2,3', '3,3', + + '0,0', '1,0', '2,0', '3,0', '4,0', + '0,1', /* */ '4,1', + '0,2', /* */ '4,2', + '0,3', /* */ '4,3', + '0,4', '1,4', '2,4', '3,4', '4,4', + ]); + expect(result).undefined; + }); + }); +}); diff --git a/src/ts/tests/server/move.spec.ts b/src/ts/tests/server/move.spec.ts new file mode 100644 index 0000000..c72f2bb --- /dev/null +++ b/src/ts/tests/server/move.spec.ts @@ -0,0 +1,321 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, assert, SinonStub } from 'sinon'; +import { encodeMovement } from '../../common/movementUtils'; +import { Move, createMove } from '../../server/move'; +import { stubClass, resetStubMethods } from '../lib'; +import { IClient } from '../../server/serverInterfaces'; +import { mockClient } from '../mocks'; +import { CounterService } from '../../server/services/counter'; +import { EntityState, Camera, UpdateFlags } from '../../common/interfaces'; +import { createCamera } from '../../common/camera'; +import { EMPTY_EXPRESSION } from '../../common/encoders/expressionEncoder'; +import { createServerRegion } from '../../server/serverRegion'; +import { createServerMap } from '../../server/serverMap'; +import { rect } from '../../common/rect'; +import { PONY_SPEED_TROT } from '../../common/constants'; +import * as collision from '../../common/collision'; + +describe('move', () => { + describe('move()', () => { + let camera: Camera; + let client: IClient; + let counter = stubClass>(CounterService); + let move: Move; + let isStaticCollision: SinonStub; + + beforeEach(() => { + resetStubMethods(counter, 'add', 'remove'); + + isStaticCollision = stub(collision, 'isStaticCollision'); + camera = createCamera(); + client = mockClient(); + client.map = createServerMap('', 0, 10, 10); + move = createMove(counter as any); + }); + + afterEach(() => { + isStaticCollision.restore(); + }); + + it('does nothing if loading flag is true', () => { + client.loading = true; + + move(client, 0, 1, 2, 3, 4, 5, {}); + + expect(client.pony.x).equal(0, 'x'); + expect(client.pony.y).equal(0, 'y'); + }); + + it('does nothing if fixing position', () => { + client.loading = true; + + move(client, 0, 1, 2, 3, 4, 5, {}); + + expect(client.pony.x).equal(0, 'x'); + expect(client.pony.y).equal(0, 'y'); + }); + + it('updates pony coordinates', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.x).equal(12.015625, 'x'); + expect(client.pony.y).equal(34.020833333333336, 'y'); + }); + + it('updates last coordinates, velocity and time', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.lastX).equal(12.015625, 'lastX'); + expect(client.lastY).equal(34.020833333333336, 'lastY'); + expect(client.lastVX).equal(0, 'lastVX'); + expect(client.lastVY).equal(0, 'lastVY'); + expect(client.lastTime).equal(123, 'lastTime'); + }); + + it('updates pony coordinates (has last time)', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); + client.lastTime = 1; + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.x).equal(12.015625, 'x'); + expect(client.pony.y).equal(34.020833333333336, 'y'); + }); + + it('updates pony velocity', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.vx).equal(PONY_SPEED_TROT, 'vx'); + expect(client.pony.vy).equal(-PONY_SPEED_TROT, 'vy'); + }); + + it('updates safe position if not colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + isStaticCollision.returns(false); + client.pony.x = 10; + client.pony.y = 30; + + move(client, 0, a, b, c, d, e, {}); + + expect(client.safeX).equal(10, 'safeX'); + expect(client.safeY).equal(30, 'safeY'); + assert.calledWith(isStaticCollision, client.pony, client.map, true); + }); + + it('does not update safe position if colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + isStaticCollision.returns(false); + isStaticCollision.withArgs(client.pony, client.map, true).returns(true); + client.pony.x = 10; + client.pony.y = 30; + client.safeX = 1; + client.safeY = 3; + + move(client, 0, a, b, c, d, e, {}); + + expect(client.safeX).equal(1, 'safeX'); + expect(client.safeY).equal(3, 'safeY'); + }); + + it('resets pony to safe position if colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + const fixPositionStub = stub(client, 'fixPosition'); + isStaticCollision.onCall(0).returns(true); + isStaticCollision.onCall(1).returns(true); + isStaticCollision.onCall(2).returns(false); + client.safeX = 1; + client.safeY = 3; + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.x).equal(1, 'x'); + expect(client.pony.y).equal(3, 'y'); + assert.calledWith(fixPositionStub, 1, 3, false); + }); + + it('does not reset pony to safe position if safe position is colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + const fixPositionStub = stub(client, 'fixPosition'); + isStaticCollision.onCall(0).returns(true); + isStaticCollision.onCall(1).returns(true); + isStaticCollision.onCall(2).returns(true); + client.safeX = 1; + client.safeY = 3; + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.x).equal(12.015625, 'x'); + expect(client.pony.y).equal(34.020833333333336, 'y'); + assert.notCalled(fixPositionStub); + }); + + it('updates pony right flag', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); + }); + + it('resets head turned flag if turning', () => { + client.pony.state = EntityState.HeadTurned; + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); + }); + + it('resets sitting flag', () => { + client.pony.state = EntityState.PonySitting; + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); + }); + + it('adds entity update', () => { + const region = createServerRegion(0, 0); + client.pony.region = region; + const [a, b, c, d, e] = encodeMovement(0, 0, 0, 0, 0, rect(0, 0, 0, 0)); + + move(client, 0, a, b, c, d, e, {}); + + expect(region.entityUpdates).eql([ + { + entity: client.pony, flags: UpdateFlags.Position | UpdateFlags.State, + x: 0.015625, y: 0.020833333333333332, vx: 0, vy: -0, + action: 0, playerState: 0, options: undefined + }, + ]); + }); + + it('clears cancellable expression', () => { + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + + const [a, b, c, d, e] = encodeMovement(0, 0, 0, 0, 0, rect(0, 0, 0, 0)); + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + expect(client.pony.exprCancellable).false; + }); + + it('reports client outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + const warn = stub(client.reporter, 'warn'); + Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); + + move(client, 0, a, b, c, d, e, {}); + + assert.calledWith(warn, 'Outside map', 'map: [foo] coords: [10000.02, 10000.02]'); + }); + + it('logs client outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.leaveReason).equal('outside map: [foo] coords: [10000.02, 10000.02]'); + }); + + it('disconnects client outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + const disconnect = stub(client, 'disconnect'); + Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); + + move(client, 0, a, b, c, d, e, {}); + + assert.calledWith(disconnect, true, true); + }); + + it('does not update pony position when coordinates are outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + Object.assign(client.map, { width: 100, height: 100 }); + + move(client, 0, a, b, c, d, e, {}); + + expect(client.pony.x).equal(0, 'x'); + expect(client.pony.y).equal(0, 'y'); + }); + + it('logs lagging player if logLagging setting is true', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 16000, camera); + const systemLog = stub(client.reporter, 'systemLog'); + client.account.name = 'Foo'; + client.accountId = 'foo'; + + move(client, 0, a, b, c, d, e, { logLagging: true }); + + assert.calledWith(systemLog, 'Time delta > 15s (16000)'); + expect(client.logDisconnect).true; + }); + + it('kicks player for lagging if kickLagging setting is true', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 16000, camera); + const disconnect = stub(client, 'disconnect'); + + move(client, 0, a, b, c, d, e, { kickLagging: true }); + + assert.calledWith(disconnect, true, true); + expect(client.leaveReason).equal('lagging'); + }); + + it('counts teleporting', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + counter.add.returns({ count: 1, items: [], date: 0 }); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + + move(client, 0, a, b, c, d, e, { reportTeleporting: true }); + + assert.calledWith(counter.add, client.accountId); + }); + + it('reports teleporting if counter exceeded limit', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + counter.add.returns({ count: 20, items: [], date: 0 }); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + const warn = stub(client.reporter, 'warn'); + + move(client, 0, a, b, c, d, e, { reportTeleporting: true }); + + assert.calledWith(counter.add, client.accountId); + assert.calledWith(counter.remove, client.accountId); + assert.calledWith(warn, 'Teleporting (x10)'); + }); + + it('kicks player for teleporting', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + const disconnect = stub(client, 'disconnect'); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + + move(client, 0, a, b, c, d, e, { kickTeleporting: true }); + + assert.calledWith(disconnect, true, true); + expect(client.leaveReason).equal('teleporting'); + }); + + it('fixes player position if teleporting', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + const systemLog = stub(client.reporter, 'systemLog'); + const fixPositionStub = stub(client, 'fixPosition'); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + + move(client, 0, a, b, c, d, e, { fixTeleporting: true, logFixingPosition: true }); + + expect(client.pony.vx).equal(0); + expect(client.pony.vy).equal(0); + assert.calledWith(fixPositionStub, 0, 0, false); + assert.calledWith(systemLog, 'Fixed teleporting (10.015625 10.020833333333334) -> (0 0)'); + }); + }); +}); diff --git a/src/ts/tests/server/originUtils.spec.ts b/src/ts/tests/server/originUtils.spec.ts new file mode 100644 index 0000000..fc1a613 --- /dev/null +++ b/src/ts/tests/server/originUtils.spec.ts @@ -0,0 +1,46 @@ +import '../lib'; +import { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers, stub, assert, SinonStub } from 'sinon'; +import { account } from '../mocks'; +import { addOrigin } from '../../server/originUtils'; +import { Account } from '../../server/db'; + +describe('originUtils', () => { + describe.skip('addOrigin()', () => { + let clock: SinonFakeTimers; + let update: SinonStub; + + beforeEach(() => { + clock = useFakeTimers(); + update = stub(Account, 'update').returns({ exec: () => Promise.resolve() } as any); + }); + + afterEach(() => { + clock.restore(); + update.restore(); + }); + + it('adds origin to account', async () => { + const acc = account({}); + const origin = { foo: 'bar' } as any; + + await addOrigin(acc, origin); + + expect(acc.origins).contains(origin); + assert.calledWithMatch(update, { _id: acc._id }, { $push: { origins: origin } }); + }); + + it('updates date of existing origin', async () => { + const acc = account({ + origins: [{ _id: 'foo', ip: '1.2.3.4', last: new Date(9999) }], + } as any); + const origin = { ip: '1.2.3.4' } as any; + clock.setSystemTime(12345); + + await addOrigin(acc, origin); + + expect(acc.origins).eql([{ _id: 'foo', ip: '1.2.3.4', last: new Date(12345) }]); + assert.calledWith(update, { _id: acc._id, 'origins._id': 'foo' }); //, { 'origins.$.last': new Date(12345) }); + }); + }); +}); diff --git a/src/ts/tests/server/other.spec.ts b/src/ts/tests/server/other.spec.ts new file mode 100644 index 0000000..007ca5c --- /dev/null +++ b/src/ts/tests/server/other.spec.ts @@ -0,0 +1,12 @@ +import '../lib'; +import { expect } from 'chai'; +import { CHANGELOG } from '../../generated/changelog'; + +describe('other', () => { + it('package version is the same as latest changelog version entry', () => { + const packageJson: any = require('../../../../package.json'); + const packageVersion = packageJson.version.replace(/-alpha$/, ''); + const changelogVersion = CHANGELOG[0].version.replace(/^v/, ''); + expect(packageVersion).equal(changelogVersion, `package: ${packageVersion}, changelog: ${changelogVersion}`); + }); +}); diff --git a/src/ts/tests/server/patreon.spec.ts b/src/ts/tests/server/patreon.spec.ts new file mode 100644 index 0000000..49bb182 --- /dev/null +++ b/src/ts/tests/server/patreon.spec.ts @@ -0,0 +1,650 @@ +import '../lib'; +import { noop } from 'lodash'; +import { Types } from 'mongoose'; +import { expect } from 'chai'; +import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, match } from 'sinon'; +import { + createRemoveOldSupporters, createUpdateSupporters, fetchPatreonData, createUpdatePatreonInfo, + RemoveOldSupporters, UpdateSupporters, createAddTotalPledged, AddTotalPledged +} from '../../server/patreon'; +import { genId, auth, account } from '../mocks'; +import { PatreonFlags, PatreonData } from '../../common/adminInterfaces'; +import { fromNow, fromDate } from '../../common/utils'; +import { DAY, rewardLevel1, rewardLevel2 } from '../../common/constants'; + +const dbId = () => Types.ObjectId(genId()); +const queryBase = 'page%5Bcount%5D=100&sort=created'; +const queryAdd = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since'; +const query = `${queryBase}${queryAdd}`; + +describe('patreon', () => { + describe('fetchPatreonData()', () => { + let client: SinonStub; + + beforeEach(() => { + client = stub(); + client.withArgs('/current_user/campaigns') + .resolves({ rawJson: { data: [{ id: 'foo' }], included: [] } }); + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ rawJson: { data: [], links: {} } }); + }); + + it('returns rewards', async () => { + client.withArgs('/current_user/campaigns') + .resolves({ + rawJson: { + data: [{ id: 'foo', attributes: {} }], + included: [ + { type: 'reward', id: '123', attributes: { title: 'title', description: 'desc' } }, + { type: 'other', id: '111', attributes: { title: 'ttt', description: 'ddd' } }, + ], + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.rewards).eql([ + { id: '123', title: 'title', description: 'desc' }, + ]); + }); + + it('fixes incomplete data', async () => { + client.withArgs('/current_user/campaigns') + .resolves({ + rawJson: { + data: [{ id: 'foo' }], + included: [ + { type: 'reward', id: '123', attributes: {} }, + ], + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.rewards).eql([ + { id: '123', title: '', description: '' }, + ]); + }); + + it('returns pledges', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: { total_historical_amount_cents: 100 }, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + { + attributes: { total_historical_amount_cents: 200 }, + relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } }, + }, + ], + links: {}, + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.pledges).eql([ + { user: 'patid1', reward: 'rewid1', total: 100, declinedSince: undefined }, + { user: 'patid2', reward: 'rewid2', total: 200, declinedSince: undefined }, + ]); + }); + + it('skips pledges with missing data', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + { + attributes: {}, + relationships: { patron: {}, reward: { data: { id: 'rewid2' } } }, + }, + { + attributes: {}, + relationships: { patron: { data: { id: 'patid3' } }, reward: {} }, + }, + ], + links: {}, + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.pledges).eql([ + { user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined }, + ]); + }); + + it('fetches multiple pages of pledges (with correct parameters for next page)', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + ], + links: { + next: 'https://www.patreon.com/api/oauth2/api/link-to-next-page?page=5', + }, + }, + }); + client.withArgs('/link-to-next-page?page=5' + queryAdd) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } }, + }, + ], + links: {}, + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + assert.calledWith(client, '/link-to-next-page?page=5' + queryAdd); + expect(result.pledges).eql([ + { user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined }, + { user: 'patid2', reward: 'rewid2', total: 0, declinedSince: undefined }, + ]); + }); + + it('throws on too many pages', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + ], + links: { + next: `/campaigns/foo/pledges?${queryBase}`, + }, + }, + }); + + await expect(fetchPatreonData(client as any, noop)).rejectedWith('Exceeded 100 pages of patreon data'); + }); + }); + + describe('updatePatreonInfo()', () => { + let queryAuths: SinonStub; + let queryAccounts: SinonStub; + let removeOldSupporters: SinonStub; + let updateSupporters: SinonStub; + let addTotalPledged: SinonStub; + let updatePatreonInfo: (data: PatreonData, now: Date) => any; + + beforeEach(() => { + queryAuths = stub(); + queryAccounts = stub(); + removeOldSupporters = stub(); + updateSupporters = stub(); + addTotalPledged = stub(); + updatePatreonInfo = createUpdatePatreonInfo( + queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged); + }); + + it('queries auths', async () => { + queryAuths.resolves([]); + queryAccounts.resolves([]); + + await updatePatreonInfo({ + pledges: [ + { reward: '123', user: 'foo', total: 0 }, + { reward: '123', user: 'bar', total: 0 }, + ], + rewards: [], + }, new Date()); + + assert.calledWith(queryAuths, match({ + provider: 'patreon', + openId: { $in: ['foo', 'bar'] }, + account: { $exists: true }, + banned: { $ne: true }, + disabled: { $ne: true }, + }), '_id account openId pledged'); + }); + + it('queries accounts', async () => { + queryAuths.resolves([]); + queryAccounts.resolves([]); + + await updatePatreonInfo({ pledges: [], rewards: [] }, new Date()); + + assert.calledWith( + queryAccounts, match({ patreon: { $exists: true, $ne: 0 } }), '_id patreon supporterDeclinedSince'); + }); + + it('removes old supporters', async () => { + const auths = [{}] as any; + const accounts = [{}] as any; + queryAuths.resolves(auths); + queryAccounts.resolves(auths); + + await updatePatreonInfo({ pledges: [], rewards: [] }, new Date()); + + assert.calledWith(removeOldSupporters, auths, accounts); + }); + + it('adds new supporters', async () => { + const auths = [{}] as any; + const accounts = [{}] as any; + const pledges = [{}] as any; + const now = new Date(); + queryAuths.resolves(auths); + queryAccounts.resolves(auths); + + await updatePatreonInfo({ pledges, rewards: [] }, now); + + assert.calledWith(updateSupporters, auths, accounts, pledges, now); + }); + + it('adds total pledged', async () => { + const auths = [{}] as any; + const pledges = [{}] as any; + queryAuths.resolves(auths); + queryAccounts.resolves(auths); + + await updatePatreonInfo({ pledges, rewards: [] }, new Date()); + + assert.calledWith(addTotalPledged, auths, pledges); + }); + }); + + describe('removeOldSupporters()', () => { + let updateAccounts: SinonStub; + let log: SinonStub; + let removeOldSupporters: RemoveOldSupporters; + let clock: SinonFakeTimers; + + const update = { + $unset: { patreon: 1, supporterDeclinedSince: 1 }, + $push: { + supporterLog: { + $each: [{ date: new Date(1234), message: 'removed supporter' }], + $slice: -10, + }, + }, + }; + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(1234); + updateAccounts = stub(); + log = stub(); + removeOldSupporters = createRemoveOldSupporters(updateAccounts, log); + }); + + afterEach(() => { + clock.restore(); + }); + + it('does nothing if list of auth and accounts are empty', async () => { + await removeOldSupporters([], []); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [] } }, update); + }); + + it('unsets patreon for all accounts without corresponding auths', async () => { + const accountId = dbId(); + + await removeOldSupporters([], [account({ _id: accountId })]); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update); + assert.calledWith(log, accountId.toHexString(), `removed supporter`); + }); + + it('unsets patreon for all accounts without corresponding auths (2)', async () => { + const account1Id = dbId(); + const account2Id = dbId(); + + await removeOldSupporters( + [auth({ account: Types.ObjectId(account2Id.toHexString()) })], + [account({ _id: account1Id }), account({ _id: account2Id })]); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [account1Id] } }, update); + assert.calledWith(log, account1Id.toHexString(), `removed supporter`); + }); + + it('works with unassigned auths', async () => { + const accountId = dbId(); + + await removeOldSupporters( + [auth({ account: undefined })], + [account({ _id: accountId })]); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update); + }); + }); + + describe('updateSupporters()', () => { + let updateAccount: SinonStub; + let log: SinonStub; + let updateSupporters: UpdateSupporters; + let clock: SinonFakeTimers; + + function push(message: string) { + return { + supporterLog: { + $each: [{ date: new Date(1234), message }], + $slice: -10, + }, + }; + } + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(1234); + updateAccount = stub(); + log = stub(); + updateSupporters = createUpdateSupporters(updateAccount, log); + }); + + afterEach(() => { + clock.restore(); + }); + + it('does nothing if list of auth and accounts are empty', async () => { + await updateSupporters([], [], [], new Date()); + + assert.notCalled(updateAccount); + }); + + it('adds patreon info to account', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [], + [{ reward: rewardLevel1, user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter1, + supporterDeclinedSince: undefined, + $push: push('added supporter (1)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (1)'); + }); + + it('handles duplicate auths', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })], + [], + [{ reward: rewardLevel2, user: '321', total: 0 }, { reward: rewardLevel1, user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter2, + supporterDeclinedSince: undefined, + $push: push('added supporter (2)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (2)'); + }); + + it('handles unassigned auths', async () => { + await updateSupporters( + [auth({ account: undefined, openId: '123' })], + [], + [{ reward: rewardLevel2, user: '123', total: 0 }], + new Date()); + + assert.notCalled(updateAccount); + }); + + it('sets supporter to none if pledge is missing', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: accountId, patreon: PatreonFlags.Supporter1 })], + [], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: undefined, + $push: push('removed supporter'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter'); + }); + + it('updates existing info', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel2, user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter2, + supporterDeclinedSince: undefined, + $push: push('added supporter (2)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (2)'); + }); + + it('does not update if supporter level did not change (2 patreon accounts)', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })], + [{ reward: rewardLevel2, user: '123', total: 0 }], + new Date()); + + assert.notCalled(updateAccount); + assert.notCalled(log); + }); + + it('does not remove support if decline is set but day of month is < 7', async () => { + const accountId = dbId(); + const now = new Date('2018-04-02T09:00:00.000Z'); + const date = fromDate(now, -1 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + supporterDeclinedSince: date, + }); + assert.notCalled(log); + }); + + it('removes support if decline is set and day of month is > 14', async () => { + const accountId = dbId(); + const now = new Date('2018-04-16T09:00:00.000Z'); + const date = fromDate(now, -1 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: date, + $push: push('removed supporter (declined)'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter (declined)'); + }); + + it('removes support if decline is set and day of month is < 14 and decline is > 14 days old', async () => { + const accountId = dbId(); + const now = new Date('2018-04-02T09:00:00.000Z'); + const date = fromDate(now, -16 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: date, + $push: push('removed supporter (declined)'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter (declined)'); + }); + + it('does not add log if declined but supporter is already removed', async () => { + const accountId = dbId(); + const now = new Date('2018-04-16T09:00:00.000Z'); + const date = fromDate(now, -1 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: date, + }); + assert.notCalled(log); + }); + + it('updates declined date', async () => { + const accountId = dbId(); + const date = fromNow(-100); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter2, + supporterDeclinedSince: date, + $push: push('added supporter (2)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (2)'); + }); + + it('updates declined date even if patreon is not changed', async () => { + const accountId = dbId(); + const date = fromNow(-100); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })], + [{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + supporterDeclinedSince: date, + }); + assert.notCalled(log); + }); + + it('sets supporter to none if reward has invalid ID', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: 'invalid', user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: undefined, + $push: push('removed supporter'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter'); + }); + + it('does nothing if patreon info is already set', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0 }], + new Date()); + + assert.notCalled(updateAccount); + }); + + it('does nothing if declined date is the same', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ + _id: Types.ObjectId(accountId.toHexString()), + patreon: PatreonFlags.Supporter1, + supporterDeclinedSince: new Date(1234), + })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: (new Date(1234).toISOString()) }], + new Date()); + + assert.notCalled(updateAccount); + }); + }); + + describe('addTotalPledged()', () => { + let updateAuth: SinonStub; + let addTotalPledged: AddTotalPledged; + + beforeEach(() => { + updateAuth = stub(); + addTotalPledged = createAddTotalPledged(updateAuth); + }); + + it('does nothing if lists of auth and pledges are empty', async () => { + await addTotalPledged([], []); + + assert.notCalled(updateAuth); + }); + + it('does nothing if cannot find auths for pledges', async () => { + await addTotalPledged([], [{ reward: 'some', user: '123', total: 0 }]); + + assert.notCalled(updateAuth); + }); + + it('does nothing if cannot find pledges for auth', async () => { + await addTotalPledged([auth({ openId: '123' })], []); + + assert.notCalled(updateAuth); + }); + + it('does nothing if total is already correct', async () => { + await addTotalPledged([auth({ openId: '123', pledged: 10 })], [{ reward: 'some', user: '123', total: 10 }]); + + assert.notCalled(updateAuth); + }); + + it('updates total if different', async () => { + const authId = dbId(); + + await addTotalPledged([auth({ _id: authId, openId: '123' })], [{ reward: 'some', user: '123', total: 10 }]); + + assert.calledWith(updateAuth, authId, { pledged: 10 }); + }); + }); +}); diff --git a/src/ts/tests/server/playerUtils.spec.ts b/src/ts/tests/server/playerUtils.spec.ts new file mode 100644 index 0000000..7836a3c --- /dev/null +++ b/src/ts/tests/server/playerUtils.spec.ts @@ -0,0 +1,1056 @@ +import { expect } from 'chai'; +import { useFakeTimers, SinonFakeTimers, SinonStub, stub, assert } from 'sinon'; +import { + createClient, createIgnorePlayer, findClientByEntityId, setEntityExpression, interactWith, + cancelEntityExpression, canPerformAction, createClientAndPony, addIgnore, turnHead, boop, stand, + sit, lie, fly, expressionAction, isIgnored, resetClientUpdates, holdItem, unholdItem, holdToy, unholdToy +} from '../../server/playerUtils'; +import { mockClient, clientPony, serverEntity, genObjectId, setupCollider } from '../mocks'; +import { IClient } from '../../server/serverInterfaces'; +import { createFunctionWithPromiseHandler } from '../lib'; +import { parseExpression } from '../../common/expressionUtils'; +import { encodeExpression, decodeExpression, EMPTY_EXPRESSION } from '../../common/encoders/expressionEncoder'; +import { EXPRESSION_TIMEOUT } from '../../common/constants'; +import { CharacterState } from '../../common/adminInterfaces'; +import { Action, PonyOptions, EntityState, UpdateFlags } from '../../common/interfaces'; +import { createServerRegion } from '../../server/serverRegion'; +import { rect } from '../../common/rect'; +import { CounterService } from '../../server/services/counter'; +import { createCamera } from '../../common/camera'; +import { getRegion } from '../../common/worldMap'; +import { createServerMap } from '../../server/serverMap'; +import { createBinaryWriter, getWriterBuffer } from 'ag-sockets'; +import { sendAction } from '../../server/entityUtils'; +import { updateColliders } from '../common/collision.spec'; + +describe('playerUtils', () => { + const def = { x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }; + + let client: IClient; + + beforeEach(() => { + client = mockClient(); + }); + + describe('isIgnored()', () => { + it('returns true if target account id is on ignored list', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set(['foo']) } as any)).true; + }); + + it('returns false if target account id is not on ignored list', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set(['bar']) } as any)).false; + }); + + it('returns false if account ignore list is empty', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set() } as any)).false; + }); + + it('returns false if account does not have ignore list', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set() } as any)).false; + }); + }); + + describe('createClientAndPony()', () => { + it('sets up client and pony', () => { + const account = { _id: genObjectId() }; + const character = { _id: genObjectId(), name: 'Foo' }; + const client = { + tokenData: { account, character }, + } as any; + const map = { spawnArea: rect(0, 0, 0, 0) }; + const world = { + getMainMap: () => map, + getMap: () => map, + isColliding: stub(), + }; + + createClientAndPony(client, [], [], { name: 'test' } as any, world as any, new CounterService(1)); + + // ... + }); + }); + + describe('createClient()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(123); + }); + + afterEach(() => { + clock.restore(); + }); + + it('initializes client fields', () => { + const originalRequest = { headers: { 'user-agent': 'test' } }; + const client = { originalRequest } as any; + const account = { _id: genObjectId(), _account: 1, name: 'Foo' } as any; + const character = { _id: genObjectId(), _character: 1, name: 'Im :apple:' } as any; + const pony = { _pony: 1, x: 10, y: 20 } as any; + const reporter = { _reporter: 1 } as any; + const origin = { ip: '', country: 'XY' }; + const map = {} as any; + + const result = createClient(client, account, [], [], character, pony, map, reporter, origin); + + expect(result).equal(client); + expect(result).eql({ + accountId: account._id.toString(), + accountName: 'Foo', + characterId: character._id.toString(), + characterName: 'Im 🍎', + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + friends: new Set(), + friendsCRC: undefined, + accountSettings: {}, + originalRequest, + supporterLevel: 0, + isMod: false, + userAgent: 'test', + reporter, + account, + character, + ip: '', + map, + isSwitchingMap: false, + pony, + notifications: [], + updateQueue: createBinaryWriter(128), + regionUpdates: [], + saysQueue: [], + unsubscribes: [], + subscribes: [], + regions: [], + camera: Object.assign(createCamera(), { w: 800, h: 600 }), + lastSays: [], + lastSwap: 0, + shadowed: false, + country: 'XY', + safeX: 10, + safeY: 20, + lastPacket: 123, + lastAction: 0, + lastBoopAction: 0, + lastExpressionAction: 0, + lastX: 10, + lastY: 20, + lastTime: 0, + lastVX: 0, + lastVY: 0, + lastCameraX: 0, + lastCameraY: 0, + lastCameraW: 0, + lastCameraH: 0, + lastMapSwitch: 0, + sitCount: 0, + lastSitX: 0, + lastSitY: 0, + lastSitTime: 0, + lastMapLoadOrSave: 0, + positions: [], + }); + }); + + it('sets shadowed field if is shadowed', () => { + const originalRequest = { headers: { 'user-agent': 'test' } }; + const client = { originalRequest } as any; + const account = { _id: genObjectId(), shadow: -1 } as any; + const character = { _id: genObjectId() } as any; + const pony = {} as any; + const reporter = {} as any; + const map = {} as any; + + const result = createClient(client, account, [], [], character, pony, map, reporter, undefined); + + expect(result.shadowed).true; + expect(result.country).equal('??'); + }); + }); + + describe('resetClientUpdates()', () => { + it('resets all queues', () => { + const client = mockClient(); + client.updateQueue.offset = 100; + client.regionUpdates.push({} as any); + client.saysQueue.push({} as any); + client.unsubscribes.push({} as any); + client.subscribes.push({} as any); + + resetClientUpdates(client); + + expect(client.updateQueue.offset).equal(0); + expect(client.regionUpdates).eql([]); + expect(client.saysQueue).eql([]); + expect(client.unsubscribes).eql([]); + expect(client.subscribes).eql([]); + }); + }); + + describe('ignorePlayer()', () => { + let ignorePlayer: (client: IClient, target: IClient, ignored: boolean) => void; + let updateAccount: SinonStub; + + beforeEach(() => { + updateAccount = stub().resolves(); + ignorePlayer = createFunctionWithPromiseHandler(createIgnorePlayer, updateAccount); + }); + + it('adds client to targets ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + + await ignorePlayer(client, target, true); + + assert.calledWithMatch(updateAccount, target.accountId, { $push: { ignores: client.accountId } }); + }); + + it('removes client from targets ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + addIgnore(target, client.accountId); + + await ignorePlayer(client, target, false); + + assert.calledWithMatch(updateAccount, target.accountId, { $pull: { ignores: client.accountId } }); + }); + + it('does nothing if already ignored', async () => { + const client = mockClient(); + const target = mockClient(); + addIgnore(target, client.accountId); + + await ignorePlayer(client, target, true); + + assert.notCalled(updateAccount); + }); + + it('does nothing if already unignored', async () => { + const client = mockClient(); + const target = mockClient(); + + await ignorePlayer(client, target, false); + + assert.notCalled(updateAccount); + }); + + it('does nothing if called for self', async () => { + const client = mockClient(); + + await ignorePlayer(client, client, true); + + assert.notCalled(updateAccount); + }); + + it('adds client to targets account instance ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + target.account.ignores = undefined; + + await ignorePlayer(client, target, true); + + expect(target.account.ignores).eql([client.accountId]); + }); + + it('removes client from targets account instance ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + addIgnore(target, client.accountId); + + await ignorePlayer(client, target, false); + + expect(target.account.ignores).eql([]); + }); + + it('sends target player state update to client', async () => { + const client = mockClient(); + const target = mockClient(); + target.pony.id = 123; + + await ignorePlayer(client, target, true); + + expect(Array.from(getWriterBuffer(client.updateQueue))) + .eql([2, 4, 0, 0, 0, 0, 123, 1]); + }); + }); + + describe('findClientByEntityId()', () => { + it('returns client from selected pony', () => { + const self = mockClient(); + const client = mockClient(); + self.selected = client.pony; + + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); + + it('returns client from party clients', () => { + const self = mockClient(); + const client = mockClient(); + self.party = { + id: '', + clients: [client], + leader: client, + pending: [], + }; + + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); + + it('returns client from party pending', () => { + const self = mockClient(); + const client = mockClient(); + self.party = { + id: '', + clients: [], + leader: client, + pending: [{ client, notificationId: 0 }], + }; + + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); + + it('returns undefined if not found in party', () => { + const self = mockClient(); + const client = mockClient(); + self.party = { + id: '', + clients: [], + leader: mockClient(), + pending: [], + }; + + expect(findClientByEntityId(self, client.pony.id)).undefined; + }); + + it('returns client from notifications', () => { + const self = mockClient(); + const client = mockClient(); + self.notifications = [ + { id: 0, name: 'name', message: '', entityId: client.pony.id, sender: client }, + ]; + + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); + + it('returns undefined if not found', () => { + const self = mockClient(); + + expect(findClientByEntityId(self, 1)).undefined; + }); + }); + + describe('cancelEntityExpression()', () => { + it('cancels expression', () => { + const entity = serverEntity(0); + entity.options = { expr: 1234 } as PonyOptions; + entity.exprCancellable = true; + entity.exprPermanent = decodeExpression(1111); + + cancelEntityExpression(entity); + + expect(entity.options).eql({ expr: 1111 }); + expect(entity.exprCancellable).false; + }); + + it('does nothing if entity does not have cancellable expression', () => { + const entity = serverEntity(0); + entity.options = { expr: 1234 } as PonyOptions; + + cancelEntityExpression(entity); + + expect(entity.options).eql({ expr: 1234 }); + }); + }); + + describe('setEntityExpression()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('sets expression on pony options', () => { + const pony = clientPony(); + const expression = parseExpression(':)'); + + setEntityExpression(pony, expression); + + expect(pony.options!.expr).equal(encodeExpression(expression)); + }); + + it('sets empty expression by default', () => { + const pony = clientPony(); + + setEntityExpression(pony, undefined); + + expect(pony.options!.expr).equal(encodeExpression(undefined)); + }); + + it('uses permanent expression if no expression provided', () => { + const pony = clientPony(); + const expression = parseExpression(':)'); + pony.exprPermanent = expression; + + setEntityExpression(pony, undefined); + + expect(pony.options!.expr).equal(encodeExpression(expression)); + }); + + it('sets default expression timeout', () => { + const pony = clientPony(); + clock.setSystemTime(1234); + + setEntityExpression(pony, parseExpression(':)')); + + expect(pony.exprTimeout).equal(1234 + EXPRESSION_TIMEOUT); + }); + + it('sets custom expression timeout if provided', () => { + const pony = clientPony(); + clock.setSystemTime(1234); + + setEntityExpression(pony, parseExpression(':)'), 123); + + expect(pony.exprTimeout).equal(1234 + 123); + }); + + it('unsets expression timeout if given 0 for timeout', () => { + const pony = clientPony(); + pony.exprTimeout = 1234; + + setEntityExpression(pony, parseExpression(':)'), 0); + + expect(pony.exprTimeout).undefined; + }); + + it('sets expression cancellable flag', () => { + const pony = clientPony(); + + setEntityExpression(pony, parseExpression(':)'), 123, true); + + expect(pony.exprCancellable).true; + }); + + it('sets expression cancellable flag', () => { + const pony = clientPony(); + + setEntityExpression(pony, parseExpression(':)'), 123, false); + + expect(pony.exprCancellable).false; + }); + + it('adds expression to region updates', () => { + const entity = clientPony(); + const region = createServerRegion(0, 0); + entity.region = region; + + setEntityExpression(entity, parseExpression(':)'), 123, false); + + expect(region.entityUpdates).eql([ + { entity, flags: UpdateFlags.Expression, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }, + ]); + }); + + it('sends expression update to user instead of region updates for shadowed client', () => { + const pony = clientPony(); + pony.region = createServerRegion(0, 0); + pony.client!.shadowed = true; + pony.id = 123; + + setEntityExpression(pony, parseExpression(':)'), 123, false); + + expect(Array.from(getWriterBuffer(pony.client!.updateQueue))) + .eql([2, 0, 8, 0, 0, 0, 123, 0, 0, 4, 32]); + }); + }); + + describe('interactWith()', () => { + it('calls interact method on target', () => { + const client = mockClient(); + const interact = stub(); + const target = serverEntity(1, 0, 0, 1, { interact }); + + interactWith(client, target); + + assert.calledWith(interact, target, client); + }); + + it('calls interact method on target if within range', () => { + const client = mockClient(); + const interact = stub(); + const target = serverEntity(1, 10, 10, 1, { interact, interactRange: 5 }); + client.pony.x = 9; + client.pony.y = 11; + + interactWith(client, target); + + assert.calledWith(interact, target, client); + }); + + it('does not call interact if out of range', () => { + const client = mockClient(); + const interact = stub(); + const target = serverEntity(1, 10, 10, 1, { interact, interactRange: 5 }); + client.pony.x = 2; + client.pony.y = 1; + + interactWith(client, target); + + assert.notCalled(interact); + }); + + it('does nothing for undefined entity', () => { + interactWith(mockClient(), undefined); + }); + + it('does nothing for entity without interact', () => { + interactWith(mockClient(), serverEntity(1)); + }); + }); + + describe('canPerformAction()', () => { + it('returns true if last action date is below current time', () => { + expect(canPerformAction(mockClient({ lastAction: 1234 }))).true; + }); + + it('returns false if last action date is ahead or current time', () => { + expect(canPerformAction(mockClient({ lastAction: Date.now() + 1000 }))).false; + }); + }); + + describe('sendAction()', () => { + it('adds action to region', () => { + const entity = serverEntity(1); + const region = createServerRegion(0, 0); + entity.region = region; + + sendAction(entity, Action.Boop); + + expect(region.entityUpdates).eql([ + { + entity, flags: UpdateFlags.Action, x: 0, y: 0, vx: 0, vy: 0, action: Action.Boop, + playerState: 0, options: undefined, + }, + ]); + }); + + it('sends only to entity client if shadowed', () => { + const client = mockClient(); + client.shadowed = true; + const entity = client.pony; + const region = createServerRegion(0, 0); + entity.region = region; + entity.region.clients.push(mockClient(), client); + client.pony.id = 123; + + sendAction(entity, Action.Boop); + + expect(Array.from(getWriterBuffer(client.updateQueue))).eql([2, 0, 128, 0, 0, 0, 123, 1]); + expect(region.entityUpdates).eql([]); + }); + }); + + describe('boop()', () => { + let client: IClient; + + beforeEach(() => { + client = mockClient(); + client.map = createServerMap('foo', 0, 1, 1); + client.pony.region = client.map.regions[0]; + client.lastAction = 0; + }); + + it('sends boop action', () => { + boop(client, 1000); + + expect(client.pony.region!.entityUpdates).eql([ + { + entity: client.pony, flags: UpdateFlags.Action, x: 0, y: 0, vx: 0, vy: 0, action: Action.Boop, + playerState: 0, options: undefined, + }, + ]); + }); + + it('cancels expression', () => { + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + + boop(client, 1000); + + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); + + it('updates last boop action', () => { + client.lastBoopAction = 0; + + boop(client, 100); + + expect(client.lastBoopAction).equal(100 + 500); + }); + + it('executes boop on found entity', () => { + const boop = stub(); + client.pony.x = 5; + client.pony.y = 5; + getRegion(client.map, 0, 0).entities.push(serverEntity(0, 4.2, 5, 0, { boop })); + + boop(client); + + assert.calledWith(boop, client); + }); + + it('does not execute boop on found entity if shadowed', () => { + const stubBoop = stub(); + client.pony.x = 5; + client.pony.y = 5; + client.shadowed = true; + getRegion(client.map, 0, 0).entities.push(serverEntity(0, 4.2, 5, 0, { boop: stubBoop })); + + boop(client, 0); + + assert.notCalled(stubBoop); + }); + + it('does nothing if cannot perform action', () => { + client.lastAction = 1000; + + boop(client, 0); + + expect(client.pony.region!.entityUpdates).eql([]); + }); + + it('does nothing if moving', () => { + client.pony.vx = 1; + + boop(client, 0); + + expect(client.pony.region!.entityUpdates).eql([]); + }); + }); + + describe('turnHead()', () => { + it('updates HeadTurned flag', () => { + const client = mockClient(); + client.pony.state = 0; + + turnHead(client); + + expect(client.pony.state).equal(EntityState.HeadTurned); + }); + + it('does not update flags if cannot perform action', () => { + const client = mockClient(); + client.lastAction = Date.now() + 1000; + client.pony.state = 0; + + turnHead(client); + + expect(client.pony.state).equal(0); + }); + }); + + describe('stand()', () => { + beforeEach(() => { + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + }); + + it('updates entity flag to standing', () => { + client.pony.state = EntityState.PonySitting; + + stand(client); + + expect(client.pony.state).equal(EntityState.PonyStanding); + }); + + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonySitting | EntityState.FacingRight; + + stand(client); + + expect(client.pony.state).equal(EntityState.PonyStanding | EntityState.FacingRight); + }); + + it('cancels expression', () => { + client.pony.state = EntityState.PonySitting; + + stand(client); + + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); + + it('does not cancel expression if transitioning from flying', () => { + client.pony.state = EntityState.PonyFlying; + + stand(client); + + expect(client.pony.options!.expr).equal(123); + }); + + it('does nothing if already standing', () => { + client.pony.state = EntityState.PonyStanding; + + stand(client); + + expect(client.pony.state).equal(EntityState.PonyStanding); + expect(client.pony.options!.expr).equal(123); + }); + + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; + + stand(client); + + expect(client.pony.state).equal(0); + expect(client.pony.options!.expr).equal(123); + }); + + it('does nothing if cannot land', () => { + client.pony.state = EntityState.PonyFlying; + client.pony.x = 0.5; + client.pony.y = 0.5; + setupCollider(client.map, 0.5, 0.5); + updateColliders(client.map); + + stand(client); + + expect(client.pony.state).equal(EntityState.PonyFlying); + expect(client.pony.options!.expr).equal(123); + }); + }); + + describe('sit()', () => { + it('updates entity flag to sitting', () => { + client.pony.state = EntityState.PonyStanding; + + sit(client, {}); + + expect(client.pony.state).equal(EntityState.PonySitting); + }); + + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; + + sit(client, {}); + + expect(client.pony.state).equal(EntityState.PonySitting | EntityState.FacingRight); + }); + + it('does nothing if already sitting', () => { + client.pony.state = EntityState.PonySitting; + + sit(client, {}); + + expect(client.pony.state).equal(EntityState.PonySitting); + }); + + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; + + sit(client, {}); + + expect(client.pony.state).equal(0); + }); + + it('does nothing if moving', () => { + client.pony.vx = 1; + client.pony.state = 0; + + sit(client, {}); + + expect(client.pony.state).equal(0); + }); + }); + + describe('lie()', () => { + it('updates entity flag to lying', () => { + client.pony.state = EntityState.PonyStanding; + + lie(client); + + expect(client.pony.state).equal(EntityState.PonyLying); + }); + + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; + + lie(client); + + expect(client.pony.state).equal(EntityState.PonyLying | EntityState.FacingRight); + }); + + it('does nothing if already lying', () => { + client.pony.state = EntityState.PonyLying; + + lie(client); + + expect(client.pony.state).equal(EntityState.PonyLying); + }); + + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; + + lie(client); + + expect(client.pony.state).equal(0); + }); + + it('does nothing if moving', () => { + client.pony.vx = 1; + client.pony.state = 0; + + lie(client); + + expect(client.pony.state).equal(0); + }); + }); + + describe('fly()', () => { + beforeEach(() => { + client.pony.canFly = true; + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + }); + + it('updates entity flag to flying', () => { + client.pony.state = EntityState.PonyStanding; + + fly(client); + + expect(client.pony.state).equal(EntityState.PonyFlying | EntityState.Flying); + }); + + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; + + fly(client); + + expect(client.pony.state).equal(EntityState.PonyFlying | EntityState.FacingRight | EntityState.Flying); + }); + + it('cancels expression', () => { + fly(client); + + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); + + it('does nothing if already flying', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = EntityState.PonyFlying; + + fly(client); + + expect(client.pony.state).equal(EntityState.PonyFlying); + expect(client.pony.options!.expr).equal(123); + }); + + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; + + fly(client); + + expect(client.pony.state).equal(0); + expect(client.pony.options!.expr).equal(123); + }); + + it('does nothing if cannot fly', () => { + client.pony.canFly = false; + client.pony.state = 0; + + fly(client); + + expect(client.pony.state).equal(0); + expect(client.pony.options!.expr).equal(123); + }); + }); + + describe('expressionAction()', () => { + beforeEach(() => { + client.pony.region = createServerRegion(1, 1); + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + }); + + it('sends given action', () => { + expressionAction(client, Action.Yawn); + + expect(client.pony.region!.entityUpdates).eql([ + { ...def, entity: client.pony, flags: UpdateFlags.Expression | UpdateFlags.Action, action: Action.Yawn }, + ]); + }); + + it('cancels expression', () => { + expressionAction(client, Action.Yawn); + + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); + + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + + expressionAction(client, Action.Yawn); + + expect(client.pony.region!.entityUpdates).eql([]); + }); + + it('does nothing if not expression action', () => { + client.pony.canFly = false; + + expressionAction(client, Action.Boop); + + expect(client.pony.region!.entityUpdates).eql([]); + }); + + it('updates last expression action', () => { + client.lastExpressionAction = 0; + + expressionAction(client, Action.Yawn); + + expect(client.lastExpressionAction).greaterThan(Date.now()); + }); + }); + + describe('holdItem()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); + + holdItem(entity, 456); + + expect(entity.options).eql({ hold: 456 }); + }); + + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + + holdItem(entity, 456); + + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { hold: 456 } }, + ]); + }); + + it('does not send entity update if hold is already set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { hold: 456 }; + + holdItem(entity, 456); + + expect(region.entityUpdates).eql([]); + }); + }); + + describe('unholdItem()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); + entity.options = { hold: 456 }; + + unholdItem(entity); + + expect(entity.options).eql({}); + }); + + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { hold: 456 }; + + unholdItem(entity); + + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { hold: 0 } }, + ]); + }); + + it('does not send entity update if hold is not set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + + unholdItem(entity); + + expect(region.entityUpdates).eql([]); + }); + }); + + describe('holdToy()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); + + holdToy(entity, 456); + + expect(entity.options).eql({ toy: 456 }); + }); + + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + + holdToy(entity, 456); + + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { toy: 456 } }, + ]); + }); + + it('does not send entity update if hold is already set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { toy: 456 }; + + holdToy(entity, 456); + + expect(region.entityUpdates).eql([]); + }); + }); + + describe('unholdToy()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); + entity.options = { toy: 456 }; + + unholdToy(entity); + + expect(entity.options).eql({}); + }); + + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { toy: 456 }; + + unholdToy(entity); + + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { toy: 0 } }, + ]); + }); + + it('does not send entity update if toy is not set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + + unholdToy(entity); + + expect(region.entityUpdates).eql([]); + }); + }); +}); diff --git a/src/ts/tests/server/regionUtils.spec.ts b/src/ts/tests/server/regionUtils.spec.ts new file mode 100644 index 0000000..bfa73f9 --- /dev/null +++ b/src/ts/tests/server/regionUtils.spec.ts @@ -0,0 +1,389 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, assert } from 'sinon'; +import { + addToRegion, removeFromRegion, commitRegionUpdates, transferToRegion, isSubscribedToRegion, + getExpectedRegion, unsubscribeFromOutOfRangeRegions, subscribeToRegionsInRange, unsubscribeFromAllRegions, + updateRegions +} from '../../server/regionUtils'; +import { IClient, ServerRegion } from '../../server/serverInterfaces'; +import { addEntityToRegion, createServerRegion } from '../../server/serverRegion'; +import { createServerMap } from '../../server/serverMap'; +import { mockClient, serverEntity } from '../mocks'; +import { getRegion } from '../../common/worldMap'; +import { EntityFlags } from '../../common/interfaces'; + +describe('regionUtils', () => { + let client: IClient; + let region: ServerRegion; + + beforeEach(() => { + client = mockClient(); + region = createServerRegion(2, 3); + }); + + describe('getExpectedRegion()', () => { + it('returns only available region on the map', () => { + const map = createServerMap('', 0, 1, 1); + const entity = serverEntity(1); + + expect(getExpectedRegion(entity, map)).equal(map.regions[0]); + }); + + it('returns region at location if entity region is undefined', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 8 * 5, 8 * 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); + }); + + it('returns current region if entity is inside current region', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 8 * 5, 8 * 5); + entity.region = getRegion(map, 5, 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); + }); + + it('returns edge region if entity is outside the map', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 10000, 8 * 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 9, 5)); + }); + + it('returns the same region if entity is outside region but inside region border', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 8 * 5 + 0.1, 8 * 5, 1, { flags: EntityFlags.Movable }); + entity.region = getRegion(map, 5, 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); + }); + }); + + describe('subscribeToRegions()', () => { + it('subscribes to regions that are in camera view', () => { + const map = createServerMap('', 0, 2, 1); + Object.assign(client.camera, { x: -10, y: 0, w: 5, h: 5 }); + client.map = map; + + subscribeToRegionsInRange(client); + + expect(client.subscribes).eql([ + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]), + ]); + }); + }); + + describe('unsubscribeFromRegions()', () => { + it('adds unsubscribes to client for regions that are not in camera view', () => { + const region1 = createServerRegion(1, 1); + const region2 = createServerRegion(1, 2); + client.regions = [region1, region2]; + Object.assign(client.camera, { x: -10, y: 0, w: 5, h: 5 }); + + unsubscribeFromOutOfRangeRegions(client); + + expect(client.unsubscribes).eql([1, 2]); + }); + }); + + describe('updateRegions()', () => { + it('does nothing for empty map', () => { + const map = createServerMap('', 0, 1, 1); + + updateRegions([map]); + }); + + it('does nothing if no regions are changed', () => { + const map = createServerMap('', 0, 3, 3); + const entity = serverEntity(1, 5, 5); + addEntityToRegion(getRegion(map, 0, 0), entity, map); + const region = getRegion(map, 0, 0); + entity.region = region; + + updateRegions([map]); + + expect(entity.region).equal(region); + }); + + it('transfers entity to another region', () => { + const map = createServerMap('', 0, 3, 3); + const entity = serverEntity(1, 15, 15); + entity.flags |= EntityFlags.Movable; + addEntityToRegion(getRegion(map, 0, 0), entity, map); + entity.region = getRegion(map, 0, 0); + + updateRegions([map]); + + expect(entity.region).equal(getRegion(map, 1, 1)); + }); + }); + + describe('commitRegionUpdates()', () => { + it('creates update packets', () => { + const client1 = mockClient(); + const client2 = mockClient(); + region.clients.push(client1, client2); + const entityUpdates = [] as any; + const entityRemoves = [{} as any]; + const tileUpdates = [[] as any]; + region.x = 5; + region.y = 6; + region.entityUpdates = entityUpdates; + region.entityRemoves = entityRemoves; + region.tileUpdates = tileUpdates; + + commitRegionUpdates([region]); + + expect(client1.regionUpdates.length).equal(1); + expect(client2.regionUpdates.length).equal(1); + }); + + it('does not send any updates if all lists are empty', () => { + region.clients.push(mockClient(), mockClient()); + const updateEntities1 = stub(region.clients[0], 'update'); + const updateEntities2 = stub(region.clients[1], 'update'); + + commitRegionUpdates([region]); + + assert.notCalled(updateEntities1); + assert.notCalled(updateEntities2); + }); + + it('resets region updates', () => { + region.tileUpdates.push({ x: 1, y: 2, type: 3 }); + region.entityUpdates = [{}, {}] as any; + + commitRegionUpdates([region]); + + expect(region.entityUpdates).eql([]); + }); + }); + + describe('transferToRegion()', () => { + const map = createServerMap('', 0, 1, 1); + + it('removes entity from current region', () => { + const entity = serverEntity(1); + const oldRegion = entity.region = createServerRegion(0, 0); + addEntityToRegion(entity.region, entity, map); + + transferToRegion(entity, region, map); + + expect(oldRegion.entities).not.contain(entity); + }); + + it('adds entity to new region', () => { + const entity = serverEntity(1); + + transferToRegion(entity, region, map); + + expect(region.entities).contain(entity); + }); + + it('updates entity region', () => { + const entity = serverEntity(1); + + transferToRegion(entity, region, map); + + expect(entity.region).equal(region); + }); + + it('adds entity update to current region', () => { + const entity = serverEntity(1); + entity.region = createServerRegion(0, 0); + + transferToRegion(entity, region, map); + + expect(region.entityUpdates).eql([ + // ... + ]); + }); + + // it('sends addEntity message to clients subscribed to destination region', () => { + // const entity = serverEntity(1); + // const otherClient = mockClient(); + // const addEntity = stub(otherClient, 'addEntity'); + // region.clients.push(otherClient); + // otherClient.regions.push(region); + + // transferToRegion(entity, region, {}, {} as any); + + // assert.calledWith(addEntity as any, entity.id); + // }); + + // it('does not sent addEntity message to clients subscribed to destination region if entity is shadowed', () => { + // const entity = serverEntity(1); + // entity.client = mockClient({ shadowed: true }); + // const otherClient = mockClient(); + // const addEntity = stub(otherClient, 'addEntity'); + // region.clients.push(otherClient); + // otherClient.regions.push(region); + + // transferToRegion(entity, region, {}, {} as any); + + // assert.notCalled(addEntity); + // }); + + // it('does not send addEntity message to clients subscribed to destination and source regions', () => { + // const entity = serverEntity(1); + // entity.region = createServerRegion(0, 0, 0); + // const otherClient = mockClient(); + // const addEntity = stub(otherClient, 'addEntity'); + // region.clients.push(otherClient); + // entity.region.clients.push(otherClient); + // otherClient.regions.push(entity.region, region); + + // transferToRegion(entity, region, {}, {} as any); + + // assert.notCalled(addEntity); + // }); + }); + + describe('addToRegion()', () => { + const map = createServerMap('', 0, 1, 1); + + it('adds entity to region', () => { + const entity = serverEntity(1); + + addToRegion(entity, region, map); + + expect(region.entities).contain(entity); + }); + + it('sets entity region', () => { + const entity = serverEntity(1); + + addToRegion(entity, region, map); + + expect(entity.region).equal(region); + }); + + // it('sends addEntity message to all clients', () => { + // region.clients.push(mockClient(), mockClient()); + // const addEntity1 = stub(region.clients[0], 'addEntity'); + // const addEntity2 = stub(region.clients[1], 'addEntity'); + + // addToRegion(serverEntity(1), region, {}, hiding); + + // assert.calledOnce(addEntity1); + // assert.calledOnce(addEntity2); + // }); + + // it('only sents addEntity message to entity client if shadowed', () => { + // region.clients.push(mockClient()); + // const addEntity1 = stub(region.clients[0], 'addEntity'); + // const entity = serverEntity(1); + // entity.client = mockClient({ shadowed: true }); + // const addEntity2 = stub(entity.client, 'addEntity'); + + // addToRegion(entity, region, {}, hiding); + + // assert.notCalled(addEntity1); + // assert.calledOnce(addEntity2); + // }); + + it('adds entity to region even if shadowed', () => { + const entity = serverEntity(1); + entity.client = mockClient({ shadowed: true }); + + addToRegion(entity, region, map); + + expect(region.entities).contain(entity); + }); + + it('sets entity region even if shadowed', () => { + const entity = serverEntity(1); + entity.client = mockClient({ shadowed: true }); + + addToRegion(entity, region, map); + + expect(entity.region).equal(region); + }); + }); + + describe('removeFromRegion()', () => { + const map = createServerMap('', 0, 1, 1); + + it('removes entity from region', () => { + const entity = serverEntity(1); + addEntityToRegion(region, entity, map); + + removeFromRegion(entity, region, map); + + expect(region.entities).not.contain(entity); + }); + + it('unsets entity region', () => { + const entity = serverEntity(1); + addEntityToRegion(region, entity, map); + + removeFromRegion(entity, region, map); + + expect(entity.region).undefined; + }); + + it('adds entity to removed entities list', () => { + const entity = serverEntity(123); + + removeFromRegion(entity, region, map); + + expect(region.entityRemoves).eql([123]); + }); + }); + + describe('isSubscribedToRegion()', () => { + it('returns true if subscribed to region', () => { + client.regions.push(region); + region.clients.push(client); + + expect(isSubscribedToRegion(client, region)).true; + }); + + it('returns false if not subscribed to region', () => { + expect(isSubscribedToRegion(client, region)).false; + }); + }); + + describe('unsubscribeFromAllRegions()', () => { + it('removes client from region', () => { + region.clients.push(client); + client.regions.push(region); + + unsubscribeFromAllRegions(client, false); + + expect(region.clients).not.contain(client); + }); + + it('removes region from client', () => { + client.regions.push(region); + + unsubscribeFromAllRegions(client, false); + + expect(client.regions).not.contain(region); + }); + + it('adds unsubscribes to client with region coordinates', () => { + client.regions.push(createServerRegion(2, 3)); + + unsubscribeFromAllRegions(client, false); + + expect(client.unsubscribes).eql([2, 3]); + }); + + it('adds unsubscribes to client with all regions coordinates', () => { + client.regions.push(createServerRegion(2, 3), createServerRegion(5, 6)); + + unsubscribeFromAllRegions(client, false); + + expect(client.unsubscribes).eql([2, 3, 5, 6]); + }); + + it('does not add unsibscribes to client if silent flag is set', () => { + unsubscribeFromAllRegions(client, true); + + expect(client.unsubscribes).eql([]); + }); + }); +}); diff --git a/src/ts/tests/server/reporting.spec.ts b/src/ts/tests/server/reporting.spec.ts new file mode 100644 index 0000000..55290d8 --- /dev/null +++ b/src/ts/tests/server/reporting.spec.ts @@ -0,0 +1,250 @@ +import { createFunctionWithPromiseHandler } from '../lib'; +import { stub, assert, SinonStub } from 'sinon'; +import { GameServerSettings } from '../../common/adminInterfaces'; +import { CounterService } from '../../server/services/counter'; +import { IClient, ReportInviteLimit, OnMessageSettings } from '../../server/serverInterfaces'; +import { mock, mockClient } from '../mocks'; +import { createReportSwears, createReportForbidden, reportInviteLimit } from '../../server/reporting'; + +describe('reporting', () => { + describe('reportSwears()', () => { + let client: IClient; + let counter: CounterService; + let settings: GameServerSettings; + let reportSwearingAccount: SinonStub; + let timeoutAccount: SinonStub; + let reportSwears: OnMessageSettings; + + beforeEach(() => { + client = mockClient(); + counter = mock>(CounterService); + settings = { filterSwears: true }; + reportSwearingAccount = stub().resolves(); + timeoutAccount = stub(); + reportSwears = createFunctionWithPromiseHandler( + createReportSwears, counter, reportSwearingAccount, timeoutAccount); + }); + + it('increments counter', async () => { + const add = stub(counter, 'add').returns({ count: 0, items: [], date: 0 }); + + await reportSwears(client, 'test', settings); + + assert.calledWith(add, client.accountId, 'test'); + }); + + describe('after excceded limit', () => { + beforeEach(() => { + stub(counter, 'add').returns({ count: 6, items: ['test'], date: 0 }); + }); + + it('reports swearing', async () => { + await reportSwears(client, 'test', settings); + + assert.calledWith(reportSwearingAccount, client.accountId); + }); + + it('timeouts account for 10 hours', async () => { + settings.autoBanSwearing = true; + + await reportSwears(client, 'test', settings); + + assert.calledWith(timeoutAccount, client.accountId); // , fromNow(10 * HOUR) + }); + + it('doesnt timeout account if autoBanSwearing is false', async () => { + await reportSwears(client, 'test', settings); + + assert.notCalled(timeoutAccount); + }); + + it('reports timing out', async () => { + const system = stub(client.reporter, 'system'); + settings.autoBanSwearing = true; + settings.reportSwears = true; + + await reportSwears(client, 'test', settings); + + assert.calledWith(system, 'Timed out for swearing', 'test', true); + }); + + it('does not report timing out if turned off in settings', async () => { + const system = stub(client.reporter, 'system'); + settings.autoBanSwearing = true; + settings.reportSwears = false; + + await reportSwears(client, 'test', settings); + + assert.calledWith(system, 'Timed out for swearing', 'test', false); + }); + + it('reports swearing if not timed out', async () => { + const warn = stub(client.reporter, 'warn'); + + await reportSwears(client, 'test', settings); + + assert.calledWith(warn, 'Swearing', 'test'); + }); + + it('does not timeout if already muted', async () => { + client.account.mute = -1; + settings.autoBanSwearing = true; + + await reportSwears(client, 'test', settings); + + assert.notCalled(timeoutAccount); + }); + + // reports unhandled rejection + it.skip('handles error', async () => { + const err = new Error('test1'); + timeoutAccount.rejects(err); + const error = stub(client.reporter, 'error'); + settings.autoBanSwearing = true; + + await reportSwears(client, 'test', settings); + + assert.calledWith(error, err); + }); + }); + }); + + describe('reportForbidden()', () => { + let client: IClient; + let counter: CounterService; + let settings: GameServerSettings; + let onTimeoutAccount: SinonStub; + let reportForbidden: OnMessageSettings; + + beforeEach(() => { + client = mockClient(); + counter = mock>(CounterService); + settings = {}; + onTimeoutAccount = stub().resolves(); + reportForbidden = createFunctionWithPromiseHandler(createReportForbidden, counter, onTimeoutAccount); + }); + + it('increments counter', async () => { + const add = stub(counter, 'add').returns({ count: 0, items: [], date: 0 }); + + await reportForbidden(client, 'test', settings); + + assert.calledWith(add, client.accountId, 'test'); + }); + + describe('after excceded limit', () => { + beforeEach(() => { + stub(counter, 'add').returns({ count: 12, items: ['test'], date: 0 }); + }); + + it('timeouts if account is new', async () => { + client.account.createdAt = new Date(); + + await reportForbidden(client, 'test', settings); + + assert.calledWith(onTimeoutAccount, client.accountId); + }); + + it('timeouts if autoBanSwearing is true', async () => { + settings.autoBanSwearing = true; + + await reportForbidden(client, 'test', settings); + + assert.calledWith(onTimeoutAccount, client.accountId); + }); + + it('does not timeout account if autoBanSwearing is false and account is old', async () => { + client.account.createdAt = new Date(0); + + await reportForbidden(client, 'test', settings); + + assert.notCalled(onTimeoutAccount); + }); + + it('reports timing out', async () => { + const system = stub(client.reporter, 'system'); + settings.autoBanSwearing = true; + + await reportForbidden(client, 'test', settings); + + assert.calledWith(system, 'Timed out for forbidden messages', 'test'); + }); + + it('reports forbidden if not timed out', async () => { + const warn = stub(client.reporter, 'warn'); + settings.autoBanSwearing = false; + client.account.createdAt = new Date(0); + + await reportForbidden(client, 'test', settings); + + assert.calledWith(warn, 'Forbidden messages', 'test'); + }); + + it('does not timeout if already muted', async () => { + client.account.mute = -1; + settings.autoBanSwearing = true; + + await reportForbidden(client, 'test', settings); + + assert.notCalled(onTimeoutAccount); + }); + + it('handles error', async () => { + const err = new Error('test2'); + onTimeoutAccount.rejects(err); + const error = stub(client.reporter, 'error'); + settings.autoBanSwearing = true; + + await reportForbidden(client, 'test', settings); + + assert.calledWith(error, err); + }); + }); + }); + + describe('reportInviteLimit()', () => { + let client: IClient; + let reportInviteLimitAccount: SinonStub; + let report: ReportInviteLimit; + + beforeEach(() => { + client = mockClient(); + reportInviteLimitAccount = stub().resolves(); + report = createFunctionWithPromiseHandler(reportInviteLimit, reportInviteLimitAccount, 'Invite limit reached'); + }); + + it('reports spamming account', async () => { + await report(client); + + assert.calledWith(reportInviteLimitAccount, client.accountId); + }); + + it('reports error during invite limit reporting', async () => { + const error = new Error('test3'); + reportInviteLimitAccount.rejects(error); + const reporterError = stub(client.reporter, 'error'); + + await report(client); + + assert.calledWith(reporterError, error); + }); + + it('logs invite limit reached', async () => { + reportInviteLimitAccount.resolves(5); + const systemLog = stub(client.reporter, 'systemLog'); + + await report(client); + + assert.calledWith(systemLog, 'Invite limit reached'); + }); + + it('reports warning every tenth invite limit report', async () => { + reportInviteLimitAccount.resolves(10); + const reporterWarn = stub(client.reporter, 'warn'); + + await report(client); + + assert.calledWith(reporterWarn, 'Invite limit reached (10)'); + }); + }); +}); diff --git a/src/ts/tests/server/serverActions.spec.ts b/src/ts/tests/server/serverActions.spec.ts new file mode 100644 index 0000000..594aeee --- /dev/null +++ b/src/ts/tests/server/serverActions.spec.ts @@ -0,0 +1,1041 @@ +import { stubClass, stubFromInstance, resetStubMethods } from '../lib'; +import { expect } from 'chai'; +import { stub, assert, spy, SinonSpy, SinonFakeTimers, useFakeTimers, SinonStub } from 'sinon'; +import { range } from 'lodash'; +import { getWriterBuffer } from 'ag-sockets'; +import { encodeString } from 'ag-sockets/dist/utf8'; +import { + ChatType, TileType, Action, PlayerAction, ModAction, Eye, Muzzle, SelectFlags, InfoFlags +} from '../../common/interfaces'; +import { CharacterState, ServerConfig, GameServerSettings } from '../../common/adminInterfaces'; +import { IClient, AccountService } from '../../server/serverInterfaces'; +import { ServerActions } from '../../server/serverActions'; +import { World } from '../../server/world'; +import { setTile, createServerMap } from '../../server/serverMap'; +import { PartyService } from '../../server/services/party'; +import { NotificationService } from '../../server/services/notification'; +import { HidingService } from '../../server/services/hiding'; +import { mockClient, serverEntity } from '../mocks'; +import { createCharacterState } from '../../server/playerUtils'; +import { CounterService } from '../../server/services/counter'; +import { createExpression } from '../../client/clientUtils'; +import { encodeExpression } from '../../common/encoders/expressionEncoder'; +import { SupporterInvitesService } from '../../server/services/supporterInvites'; +import { createCamera } from '../../common/camera'; +import { FriendsService } from '../../server/services/friends'; +import * as playerUtils from '../../server/playerUtils'; + +describe('ServerActions', () => { + let accountService = stubFromInstance({ + update() { }, + updateAccount() { }, + updateSettings() { }, + updateCharacterState() { } + }); + let notifications = stubClass(NotificationService); + let partyService = stubClass(PartyService); + let hiding = stubClass(HidingService); + let friends = stubClass(FriendsService); + let states = stubClass>(CounterService); + let teleports = stubClass>(CounterService); + let supporterInvites = stubClass(SupporterInvitesService); + let client: IClient; + let world: World; + let serverActions: ServerActions; + let settings: GameServerSettings; + let server: ServerConfig; + let ignorePlayer: SinonSpy; + let findClientByEntityId: SinonStub; + let say: SinonSpy; + let move: SinonSpy; + let execAction: SinonStub; + + beforeEach(() => { + resetStubMethods(accountService, 'update', 'updateSettings'); + resetStubMethods(notifications, 'acceptNotification', 'rejectNotification'); + resetStubMethods(partyService, 'invite', 'remove', 'promoteLeader'); + resetStubMethods(hiding, 'requestUnhideAll', 'requestHide'); + resetStubMethods(friends, 'add', 'remove', 'removeByAccountId'); + resetStubMethods(teleports); + + execAction = stub(playerUtils, 'execAction'); + client = mockClient(); + world = new World( + { flags: { friends: true } } as any, { partyChanged: { subscribe() { } } } as any, {} as any, {} as any, + {} as any, () => ({}), {} as any, {} as any); + const map = createServerMap('', 0, 1, 1); + client.map = map; + world.maps.push(map); + settings = {}; + server = { flags: {} } as any; + ignorePlayer = spy(); + findClientByEntityId = stub(); + say = spy(); + move = spy(); + + serverActions = new ServerActions( + client, world, notifications, partyService as any, supporterInvites as any, () => settings, server, say, + move, hiding as any, states as any, accountService, ignorePlayer, + findClientByEntityId, friends as any); + }); + + afterEach(() => { + execAction.restore(); + }); + + describe('connected()', () => { + // TODO: ... + }); + + describe('disconnected()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('sets client offline flag to true', () => { + serverActions.disconnected(); + + expect(client.offline).true; + }); + + it('leaves client from world', () => { + const leaveClient = stub(world, 'leaveClient'); + + serverActions.disconnected(); + + assert.calledWith(leaveClient, client); + }); + + it('notifies party service', () => { + serverActions.disconnected(); + + assert.calledWith(partyService.clientDisconnected, client); + }); + + it('updates last visit', () => { + clock.setSystemTime(1234); + + serverActions.disconnected(); + + assert.calledWith(accountService.updateAccount, client.accountId, { lastVisit: new Date(1234), state: undefined }); + }); + + it('updates character state', () => { + client.pony.x = 123; + client.pony.y = 321; + + serverActions.disconnected(); + + assert.calledWith( + accountService.updateCharacterState, client.characterId, createCharacterState(client.pony, client.map)); + }); + + it('adds state to counter service', () => { + client.pony.x = 123; + client.pony.y = 321; + + serverActions.disconnected(); + + assert.calledWithMatch(states.add, client.characterId, { x: 123, y: 321 }); + }); + + it('logs client leaving', () => { + const systemLog = stub(client.reporter, 'systemLog'); + server.id = 'server_id'; + clock.setSystemTime(12 * 1000); + client.connectedTime = 0; + + serverActions.disconnected(); + + assert.calledWith(systemLog, 'left [server_id] (disconnected) (12s)'); + }); + }); + + describe('say()', () => { + it('calls chatSay', async () => { + serverActions.say(0, 'hello', ChatType.Say); + + assert.calledWith(say, client, 'hello', ChatType.Say, undefined, settings); + }); + + it('throws if message is not a string', () => { + expect(() => serverActions.say(0, {} as any, ChatType.Say)).throw('Not a string (text)'); + expect(() => serverActions.say(0, 123 as any, ChatType.Say)).throw('Not a string (text)'); + expect(() => serverActions.say(0, null as any, ChatType.Say)).throw('Not a string (text)'); + }); + + it('throws if type is not a number', () => { + expect(() => serverActions.say(0, 'test', {} as any)).throw('Not a number (chatType)'); + expect(() => serverActions.say(0, 'test', '1' as any)).throw('Not a number (chatType)'); + expect(() => serverActions.say(0, 'test', null as any)).throw('Not a number (chatType)'); + }); + }); + + describe('select()', () => { + it('sets selected entity', () => { + const entity = serverEntity(1); + stub(world, 'getEntityById').withArgs(123).returns(entity); + + serverActions.select(123, SelectFlags.FetchEx); + + expect(client.selected).equal(entity); + }); + + it('sets selected entity from other sources', () => { + const entity = { id: 123 }; + findClientByEntityId.withArgs(client, 123).returns({ pony: entity }); + stub(world, 'getEntityById').returns(undefined); + + serverActions.select(123, SelectFlags.FetchEx); + + expect(client.selected).equal(entity); + }); + + it('sends extra data for selected entity', () => { + const entity = serverEntity(1, 0, 0, 0, { client: {} as any, extraOptions: { foo: 5 } }); + stub(world, 'getEntityById').withArgs(123).returns(entity); + + serverActions.select(123, SelectFlags.FetchEx); + + expect(Array.from(getWriterBuffer(client.updateQueue))) + .eql([2, 0, 32, 0, 0, 0, 1, 129, 3, 102, 111, 111, 165]); + }); + + it('does not send extra data for selected entity if fetch flag is false', () => { + const entity = serverEntity(1, 0, 0, 0, { client: {} as any, extraOptions: { foo: 5 } }); + stub(world, 'getEntityById').withArgs(123).returns(entity); + + serverActions.select(123, SelectFlags.None); + + expect(Array.from(getWriterBuffer(client.updateQueue))).eql([]); + }); + + it('sends extra mod data for selected entity', () => { + const entity = serverEntity(1, 0, 0, 0, { + client: { + accountId: '12345678901234567890aa', + account: { + name: 'foobar', + shadow: 0, + mute: -1, + note: 'bar' + } + } as any + }); + stub(world, 'getEntityById').withArgs(123).returns(entity); + client.isMod = true; + + serverActions.select(123, SelectFlags.FetchEx); + + expect(Array.from(getWriterBuffer(client.updateQueue))).eql([ + 2, 0, 32, 0, 0, 0, 1, 129, 7, 109, 111, 100, 73, 110, 102, 111, 134, 6, 115, 104, 97, 100, 111, + 119, 0, 4, 109, 117, 116, 101, 69, 112, 101, 114, 109, 97, 4, 110, 111, 116, 101, 67, 98, 97, + 114, 8, 99, 111, 117, 110, 116, 101, 114, 115, 128, 7, 99, 111, 117, 110, 116, 114, 121, 0, 7, + 97, 99, 99, 111, 117, 110, 116, 76, 102, 111, 111, 98, 97, 114, 32, 91, 48, 97, 97, 93 + ]); + }); + }); + + describe('interact()', () => { + it('calls interaction with client and entity', () => { + const entity = serverEntity(1); + const interact = stub(); + entity.interact = interact; + stub(world, 'getEntityById').withArgs(123).returns(entity); + + serverActions.interact(123); + + assert.calledOnce(interact); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.interact(123); + + expect(client.lastPacket).not.equal(0); + }); + + it('throws on not a number', () => { + expect(() => serverActions.interact('foo' as any)).throw('Not a number (entityId)'); + }); + }); + + describe('use()', () => { + // TODO: ... + }); + + describe('action()', () => { + it('calls unhideAll on hiding service', () => { + serverActions.action(Action.UnhideAllHiddenPlayers); + + assert.calledWith(hiding.requestUnhideAll, client); + }); + + it('does nothing for KeepAlive action', () => { + serverActions.action(Action.KeepAlive); + + assert.notCalled(execAction); + }); + + it('executes player action', () => { + serverActions.action(Action.TurnHead); + + assert.calledWith(execAction, client, Action.TurnHead); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.action(Action.Boop); + + expect(client.lastPacket).not.equal(0); + }); + + it('throws on not a number', () => { + expect(() => serverActions.action('foo' as any)).throw('Not a number (action)'); + }); + }); + + describe('actionParam()', () => { + it('on RemoveFriend: calls friends.remove()', () => { + const friend = mockClient(); + world.clientsByAccount.set('some_account_id', friend); + + serverActions.actionParam(Action.RemoveFriend, 'some_account_id'); + + assert.calledWith(friends.remove, client, friend); + }); + + it('on RemoveFriend: calls friends.removeByAccountId() if cannot find client', () => { + serverActions.actionParam(Action.RemoveFriend, 'some_account_id'); + + assert.calledWith(friends.removeByAccountId, client, 'some_account_id'); + }); + }); + + describe('actionParam2()', () => { + it('on Info: update client flags', () => { + serverActions.actionParam2(Action.Info, InfoFlags.SupportsWASM | InfoFlags.SupportsLetAndConst); + + expect(client.supportsWasm).true; + expect(client.supportsLetAndConst).true; + }); + + it('throws on invalid action', () => { + expect(() => serverActions.actionParam2(99 as any, undefined)).throws('Invalid Action (99)'); + }); + }); + + describe('expression()', () => { + it('sets expression for player character', () => { + const expression = createExpression(Eye.Angry, Eye.Closed, Muzzle.Blep); + + serverActions.expression(encodeExpression(expression)); + + expect(client.pony.options!.expr).equal(encodeExpression(expression)); + expect(client.pony.exprPermanent).eql(expression); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.expression(0); + + expect(client.lastPacket).not.equal(0); + }); + + it('throws on not a number', () => { + expect(() => serverActions.expression('foo' as any)).throw('Not a number (expression)'); + }); + }); + + describe('playerAction()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + it('ignores player', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.Ignore, undefined); + + assert.calledWith(ignorePlayer, client, target, true); + }); + + it('unignores player', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.Unignore, undefined); + + assert.calledWith(ignorePlayer, client, target, false); + }); + + it('invites player to party', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.InviteToParty, undefined); + + assert.calledWith(partyService.invite, client, target); + }); + + it('removes player from party', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.RemoveFromParty, undefined); + + assert.calledWith(partyService.remove, client, target); + }); + + it('promotes player to party leader', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.PromotePartyLeader, undefined); + + assert.calledWith(partyService.promoteLeader, client, target); + }); + + it('hides player', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.HidePlayer, 12345678); + + assert.calledWith(hiding.requestHide, client, target, 12345678); + }); + + it('invites player to supporters', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); + + serverActions.playerAction(123, PlayerAction.InviteToSupporterServers, undefined); + + assert.calledWith(supporterInvites.requestInvite, client, target); + }); + + it('updates last action', () => { + stub(world, 'getClientByEntityId').returns({} as any); + client.lastPacket = 0; + clock.setSystemTime(123); + + serverActions.playerAction(1, PlayerAction.InviteToParty, undefined); + + expect(client.lastPacket).equal(123); + }); + + it('logs warning if cannot find target player', () => { + const warnLog = stub(client.reporter, 'warnLog'); + + serverActions.playerAction(1, PlayerAction.Ignore, undefined); + + assert.calledOnce(warnLog); + }); + + it('AddFriend: calls friends.add()', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').returns(target); + + serverActions.playerAction(1, PlayerAction.AddFriend, undefined); + + assert.calledWith(friends.add, client, target); + }); + + it('RemoveFriend: calls friends.remove()', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').returns(target); + + serverActions.playerAction(1, PlayerAction.RemoveFriend, undefined); + + assert.calledWith(friends.remove, client, target); + }); + + it('throws on entityId not a number', () => { + expect(() => serverActions.playerAction('foo' as any, PlayerAction.Ignore, undefined)) + .throw('Not a number (entityId)'); + }); + + it('throws on action not a number', () => { + expect(() => serverActions.playerAction(1, 'foo' as any, undefined)) + .throw('Not a number (action)'); + }); + + it('throws on invalid action', () => { + stub(world, 'getClientByEntityId').returns({} as any); + + expect(() => serverActions.playerAction(1, 999, undefined)) + .throw('Invalid player action (undefined) [999]'); + }); + }); + + describe('leaveParty()', () => { + it('removes client from party', () => { + const leader = {} as any; + client.party = { id: '', clients: [leader, client], leader, pending: [] }; + + serverActions.leaveParty(); + + assert.calledWith(partyService.remove, leader, client); + }); + + it('does nothing if not in a party', () => { + serverActions.leaveParty(); + + assert.notCalled(partyService.remove); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.leaveParty(); + + expect(client.lastPacket).not.equal(0); + }); + }); + + describe('otherAction()', () => { + let target: IClient; + let clock: SinonFakeTimers; + let getClientByEntityId: SinonStub; + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(123456); + target = mockClient(); + getClientByEntityId = stub(world, 'getClientByEntityId').withArgs(222).returns(target); + client.account.roles = ['admin']; + client.account.name = 'Acc'; + client.character.name = 'Char'; + client.isMod = true; + }); + + afterEach(() => { + clock.restore(); + }); + + it('reports target client', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Report, 0); + + assert.calledWith(system, 'Reported by Acc'); + }); + + it('mutes target client', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Mute, -1); + + assert.calledWith(system, 'Muted by Acc'); + assert.calledWith(accountService.update, target.accountId, { mute: -1 }); + }); + + it('mutes target client for given amount of time', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Mute, 123); + + assert.calledWith(system, 'Muted for (a few seconds) by Acc'); + assert.calledWith(accountService.update, target.accountId, { mute: Date.now() + 123 }); + }); + + it('unmutes target client', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Mute, 0); + + assert.calledWith(system, 'Unmuted by Acc'); + assert.calledWith(accountService.update, target.accountId, { mute: 0 }); + }); + + it('shadows target client', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Shadow, -1); + + assert.calledWith(system, 'Shadowed by Acc'); + assert.calledWith(accountService.update, target.accountId, { shadow: -1 }); + }); + + it('shadows target client for given amount of time', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Shadow, 123); + + assert.calledWith(system, 'Shadowed for (a few seconds) by Acc'); + assert.calledWith(accountService.update, target.accountId, { shadow: Date.now() + 123 }); + }); + + it('unshadows target client', async () => { + const system = stub(target.reporter, 'system'); + + await serverActions.otherAction(222, ModAction.Shadow, 0); + + assert.calledWith(system, 'Unshadowed by Acc'); + assert.calledWith(accountService.update, target.accountId, { shadow: 0 }); + }); + + it('updates last action', async () => { + client.lastPacket = 0; + clock.tick(1000); + + await serverActions.otherAction(222, 1, 1); + + expect(client.lastPacket).not.equal(0); + }); + + it('rejects on missing client', async () => { + await expect(serverActions.otherAction(111, ModAction.Report, 0)).rejectedWith('Client does not exist (Report)'); + }); + + it('rejects on non admin user', async () => { + client.account.roles = []; + client.isMod = false; + + await expect(serverActions.otherAction(111, ModAction.Report, 0)).rejectedWith('Action not allowed (Report)'); + }); + + it('disconnectes on non admin user', async () => { + client.account.roles = []; + client.isMod = false; + const disconnect = stub(client, 'disconnect'); + + try { + await serverActions.otherAction(111, ModAction.Report, 0); + } catch { } + + assert.calledWith(disconnect, true, true); + }); + + it('rejects on action on self', async () => { + getClientByEntityId.withArgs(1).returns(client); + + await expect(serverActions.otherAction(1, ModAction.Report, 0)).rejectedWith('Cannot perform action on self (Report)'); + }); + + it('rejects on invalid action', async () => { + await expect(serverActions.otherAction(222, 123, 0)).rejectedWith('Invalid mod action (123)'); + }); + + it('rejects on entityId not a number', async () => { + await expect(serverActions.otherAction('foo' as any, 1, 1)).rejectedWith('Not a number (entityId)'); + }); + + it('rejects on action not a number', async () => { + await expect(serverActions.otherAction(1, 'foo' as any, 1)).rejectedWith('Not a number (action)'); + }); + + it('rejects on param not a number', async () => { + await expect(serverActions.otherAction(1, 1, 'foo' as any)).rejectedWith('Not a number (param)'); + }); + }); + + describe('setNote()', () => { + it('updates last action', async () => { + const other = mockClient(); + other.accountId = 'dlfhigdh'; + client.lastPacket = 0; + client.account.roles = ['mod']; + client.isMod = true; + stub(world, 'getClientByEntityId').withArgs(1).returns(other); + + await serverActions.setNote(1, 'foo'); + + expect(client.lastPacket).not.equal(0); + }); + + it('updates account note', async () => { + const other = mockClient(); + other.accountId = 'gooboo'; + client.account.roles = ['mod']; + client.isMod = true; + stub(world, 'getClientByEntityId').withArgs(1).returns(other); + + await serverActions.setNote(1, 'foo'); + + assert.calledWithMatch(accountService.update, 'gooboo', { note: 'foo' }); + }); + + it('throws if user is not a mod', async () => { + const other = mockClient(); + other.accountId = 'gooboo'; + stub(world, 'getClientByEntityId').withArgs(1).returns(other); + + await expect(serverActions.setNote(1, 'foo')).rejectedWith('Action not allowed (setNote)'); + }); + + it('throws on not a number', async () => { + await expect(serverActions.setNote('foo' as any, 'foo')).rejectedWith('Not a number (entityId)'); + }); + + it('throws on not a string', async () => { + await expect(serverActions.setNote(1, 5 as any)).rejectedWith('Not a string (text)'); + }); + }); + + describe('saveSettings()', () => { + it('updates last action', () => { + accountService.updateSettings.resolves(); + client.lastPacket = 0; + + serverActions.saveSettings({}); + + expect(client.lastPacket).not.equal(0); + }); + + it('updates account settings', () => { + accountService.updateSettings.resolves(); + const settings = {}; + + serverActions.saveSettings(settings); + + assert.calledWith(accountService.updateSettings, client.account, settings); + }); + }); + + describe('acceptNotification()', () => { + it('accepts notification', () => { + serverActions.acceptNotification(123); + + assert.calledWith(notifications.acceptNotification, client, 123); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.acceptNotification(123); + + expect(client.lastPacket).not.equal(0); + }); + + it('throws on not a number', () => { + expect(() => serverActions.acceptNotification('foo' as any)).throw('Not a number (id)'); + }); + }); + + describe('rejectNotification()', () => { + it('rejects notification', () => { + serverActions.rejectNotification(123); + + assert.calledWith(notifications.rejectNotification, client, 123); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.rejectNotification(123); + + expect(client.lastPacket).not.equal(0); + }); + + it('throws on not a number', () => { + expect(() => serverActions.rejectNotification('foo' as any)).throw('Not a number (id)'); + }); + }); + + describe('getPonies()', () => { + it('sends ponies to client', () => { + const updatePonies = stub(client, 'updatePonies'); + const name1 = encodeString('foo')!; + const name2 = encodeString('bar')!; + const name3 = encodeString('xxx')!; + const info1 = new Uint8Array([1, 2, 3]); + const info2 = new Uint8Array([4, 5, 6]); + const info3 = new Uint8Array([7, 8, 9]); + client.party = { + clients: [ + { pony: { id: 1, options: {}, encodedName: name1, encryptedInfoSafe: info1 } }, + { pony: { id: 2, options: {}, encodedName: name2, encryptedInfoSafe: info2 } }, + { pony: { id: 3, options: {}, encodedName: name3, encryptedInfoSafe: info3 } }, + ], + } as any; + + serverActions.getPonies([1, 2]); + + assert.calledWithMatch(updatePonies, [ + [1, {}, name1, info1, 0, false], + [2, {}, name2, info2, 0, false], + ]); + }); + + it('does nothing if not in party', () => { + const updatePonies = stub(client, 'updatePonies'); + + serverActions.getPonies([1, 2]); + + assert.notCalled(updatePonies); + }); + + it('does nothing if ids is null or empty', () => { + const updatePonies = stub(client, 'updatePonies'); + + serverActions.getPonies(null as any); + serverActions.getPonies([]); + + assert.notCalled(updatePonies); + }); + + it('does nothing if requesting too many ponies', () => { + const updatePonies = stub(client, 'updatePonies'); + + serverActions.getPonies(range(20)); + + assert.notCalled(updatePonies); + }); + }); + + describe('loaded()', () => { + it('sets ignoreUpdates flag to false', () => { + client.loading = true; + + serverActions.loaded(); + + expect(client.loading).false; + }); + }); + + describe('fixedPosition()', () => { + it('sets fixing position flag to false', () => { + client.fixingPosition = true; + + serverActions.fixedPosition(); + + expect(client.fixingPosition).false; + }); + }); + + describe('updateCamera()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('sets up camera', () => { + serverActions.updateCamera(1, 2, 3, 4); + + expect(client.camera).eql({ ...createCamera(), x: 1, y: 2, w: 64, h: 64 }); + }); + + it('updates last action', () => { + client.lastPacket = 0; + clock.setSystemTime(123); + + serverActions.updateCamera(0, 0, 0, 0); + + expect(client.lastPacket).equal(123); + }); + + it('throws on "a" not a number', () => { + expect(() => serverActions.updateCamera('foo' as any, 0, 0, 0)).throw('Not a number (x)'); + }); + + it('throws on "b" not a number', () => { + expect(() => serverActions.updateCamera(0, 'foo' as any, 0, 0)).throw('Not a number (y)'); + }); + + it('throws on "c" not a number', () => { + expect(() => serverActions.updateCamera(0, 0, 'foo' as any, 0)).throw('Not a number (width)'); + }); + + it('throws on "d" not a number', () => { + expect(() => serverActions.updateCamera(0, 0, 0, 'foo' as any)).throw('Not a number (height)'); + }); + }); + + describe('update()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('calls move', () => { + serverActions.move(1, 2, 3, 4, 5); + + assert.calledWith(move, client, 0, 1, 2, 3, 4, 5); + }); + + it('updates last action', () => { + client.lastPacket = 0; + clock.setSystemTime(123); + + serverActions.move(0, 0, 0, 0, 0); + + expect(client.lastPacket).equal(123); + }); + + it('throws on "a" not a number', () => { + expect(() => serverActions.move('foo' as any, 0, 0, 0, 0)).throw('Not a number (a)'); + }); + + it('throws on "b" not a number', () => { + expect(() => serverActions.move(0, 'foo' as any, 0, 0, 0)).throw('Not a number (b)'); + }); + + it('throws on "c" not a number', () => { + expect(() => serverActions.move(0, 0, 'foo' as any, 0, 0)).throw('Not a number (c)'); + }); + + it('throws on "d" not a number', () => { + expect(() => serverActions.move(0, 0, 0, 'foo' as any, 0)).throw('Not a number (d)'); + }); + + it('throws on "e" not a number', () => { + expect(() => serverActions.move(0, 0, 0, 0, 'foo' as any)).throw('Not a number (e)'); + }); + }); + + describe('changeTile()', () => { + it('sets tile', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const setTileStub = stub(world, 'setTile'); + + serverActions.changeTile(1, 2, TileType.Dirt); + + assert.calledWith(setTileStub, client.map, 1, 2, TileType.Dirt); + }); + + it.skip('sets tile for mod', () => { + const setTile = stub(world, 'setTile'); + client.isMod = true; + + serverActions.changeTile(1, 2, TileType.Wood); + + assert.calledWith(setTile, client.map, 1, 2, TileType.Wood); + }); + + it('does not set tile if shadowed', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const setTileStub = stub(world, 'setTile'); + client.shadowed = true; + + serverActions.changeTile(1, 2, TileType.Dirt); + + assert.notCalled(setTileStub); + }); + + it('sends update to client if shadowed', () => { + setTile(client.map, 1, 2, TileType.Dirt); + client.shadowed = true; + + serverActions.changeTile(1, 2, TileType.Dirt); + + expect(getWriterBuffer(client.updateQueue)).eql(new Uint8Array([4, 0, 1, 0, 2, 1])); + }); + + it('does nothing if invalid tile type', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const setTileStub = stub(world, 'setTile'); + + serverActions.changeTile(1, 2, 999); + + assert.notCalled(setTileStub); + expect(getWriterBuffer(client.updateQueue)).eql(new Uint8Array([])); + }); + + it.skip('toggles wall', () => { + setTile(client.map, 1, 2, TileType.Dirt); + client.account.roles = ['mod']; + client.isMod = true; + const toggleWall = stub(world, 'toggleWall'); + + serverActions.changeTile(1, 2, TileType.WallH); + serverActions.changeTile(3, 4, TileType.WallV); + + assert.calledWith(toggleWall, client.map, 1, 2, TileType.WallH); + assert.calledWith(toggleWall, client.map, 3, 4, TileType.WallV); + }); + + it('does not toggle wall for non moderators', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const toggleWall = stub(world, 'toggleWall'); + + serverActions.changeTile(1, 2, TileType.WallH); + + assert.notCalled(toggleWall); + }); + + it('updates last action', () => { + client.lastPacket = 0; + + serverActions.changeTile(0, 0, TileType.Dirt); + + expect(client.lastPacket).not.equal(0); + }); + + it('throws on "x" not a number', () => { + expect(() => serverActions.changeTile('foo' as any, 0, TileType.Dirt)).throw('Not a number (x)'); + }); + + it('throws on "y" not a number', () => { + expect(() => serverActions.changeTile(0, 'foo' as any, TileType.Dirt)).throw('Not a number (y)'); + }); + + it('throws on "type" not a number', () => { + expect(() => serverActions.changeTile(0, 0, 'foo' as any)).throw('Not a number (type)'); + }); + }); + + describe('leave()', () => { + it('notifies client', () => { + const left = stub(client, 'left'); + + serverActions.leave(); + + assert.calledOnce(left); + }); + }); + + describe('editorAction()', () => { + it('places', () => { + client.isMod = true; + server.flags.objects = true; + + serverActions.editorAction({ type: 'place', x: 0, y: 0, entity: 'foo' }); + }); + + it('undos', () => { + client.isMod = true; + server.flags.objects = true; + + serverActions.editorAction({ type: 'undo' }); + }); + + it('clears', () => { + client.isMod = true; + server.flags.objects = true; + + serverActions.editorAction({ type: 'clear' }); + }); + + it('does nothing for non-mod client', () => { + client.isMod = false; + server.flags.objects = true; + + serverActions.editorAction({ type: 'undo' }); + }); + }); +}); diff --git a/src/ts/tests/server/serverMap.spec.ts b/src/ts/tests/server/serverMap.spec.ts new file mode 100644 index 0000000..b38a085 --- /dev/null +++ b/src/ts/tests/server/serverMap.spec.ts @@ -0,0 +1,53 @@ +import '../lib'; +import { expect } from 'chai'; +import { createServerMap } from '../../server/serverMap'; +import { getRegion, getRegionGlobal } from '../../common/worldMap'; +import { ServerMap } from '../../server/serverInterfaces'; + +describe('serverMap', () => { + let map: ServerMap; + + beforeEach(() => { + map = createServerMap('', 0, 10, 10); + }); + + it('throws when provided zero size for any of the parameters', () => { + expect(() => createServerMap('', 0, 0, 1)).throw('Invalid map parameters'); + expect(() => createServerMap('', 0, 1, 0)).throw('Invalid map parameters'); + }); + + it('gets total width of map', () => { + expect(map.width).equal(80); + }); + + it('gets total height of map', () => { + expect(map.height).equal(80); + }); + + describe('getRegion()', () => { + it('returns region', () => { + expect(getRegion(map, 0, 0)).equal(map.regions[0]); + }); + + it('throws if out of range', () => { + expect(() => getRegion(map, -1, 0)).throw('Invalid region coords (-1, 0)'); + expect(() => getRegion(map, 0, -1)).throw('Invalid region coords (0, -1)'); + expect(() => getRegion(map, 99, 0)).throw('Invalid region coords (99, 0)'); + expect(() => getRegion(map, 0, 99)).throw('Invalid region coords (0, 99)'); + }); + }); + + describe('getRegionGlobal()', () => { + it('returns region at given position', () => { + expect(getRegionGlobal(map, 0.5, 8.5)).equal(getRegion(map, 0, 1)); + }); + + it('clamps position outside of the map (1)', () => { + expect(getRegionGlobal(map, -0.5, -10)).equal(getRegion(map, 0, 0)); + }); + + it('clamps position outside of the map (2)', () => { + expect(getRegionGlobal(map, 1000, 2000)).equal(getRegion(map, 9, 9)); + }); + }); +}); diff --git a/src/ts/tests/server/serverRegion.spec.ts b/src/ts/tests/server/serverRegion.spec.ts new file mode 100644 index 0000000..580cdb8 --- /dev/null +++ b/src/ts/tests/server/serverRegion.spec.ts @@ -0,0 +1,121 @@ +import '../lib'; +import { expect } from 'chai'; +import { + resetRegionUpdates, setRegionTile, pushUpdateEntityToRegion, pushRemoveEntityToRegion, createServerRegion +} from '../../server/serverRegion'; +import { serverEntity } from '../mocks'; +import { TileType, UpdateFlags } from '../../common/interfaces'; +import { getRegionTile } from '../../common/region'; +import { ServerRegion, EntityUpdate } from '../../server/serverInterfaces'; + +describe('serverRegion', () => { + let region: ServerRegion; + + beforeEach(() => { + region = createServerRegion(1, 2); + }); + + it('has correct bounds', () => { + expect(region.bounds).eql({ x: 8, y: 16, w: 8, h: 8 }); + }); + + it('has correct boundsWithBorder', () => { + expect(region.boundsWithBorder).eql({ x: 7, y: 15, w: 10, h: 10 }); + }); + + it('sets and gets tile at given position', () => { + setRegionTile({} as any, region, 1, 2, TileType.Grass); + + expect(getRegionTile(region, 1, 2)).equal(TileType.Grass); + }); + + describe('addUpdate()', () => { + it('adds entity update to update list', () => { + const entity = serverEntity(1, 5, 4); + + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position, x: 5, y: 4, vx: 0, vy: 0 }); + + expect(region.entityUpdates).eql([ + { + entity, + flags: UpdateFlags.Position, + x: 5, + y: 4, + vx: 0, + vy: 0, + action: 0, + playerState: 0, + options: undefined, + }, + ]); + }); + + it('updates existing entity update', () => { + const entity = serverEntity(1, 5, 4); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.None }); + + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position | UpdateFlags.Expression, x: 10, y: 11, vx: 5, vy: 3 }); + + expect(region.entityUpdates).eql([ + { + entity, + flags: UpdateFlags.Position | UpdateFlags.Expression, + x: 10, + y: 11, + vx: 5, + vy: 3, + action: 0, + playerState: 0, + options: undefined, + }, + ]); + }); + + it('does not update position of existing entry if position flag is false', () => { + const entity = serverEntity(1, 5, 4); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position, x: 5, y: 4, vx: 0, vy: 0 }); + entity.x = 10; + entity.y = 11; + entity.vx = 5; + entity.vy = 3; + + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Expression }); + + expect(region.entityUpdates).eql([ + { + entity, + flags: UpdateFlags.Position | UpdateFlags.Expression, + x: 5, + y: 4, + vx: 0, + vy: 0, + action: 0, + playerState: 0, + options: undefined, + }, + ]); + }); + }); + + describe('addRemove()', () => { + it('adds entity remove to remove list', () => { + pushRemoveEntityToRegion(region, serverEntity(123)); + + expect(region.entityRemoves).eql([123]); + }); + }); + + describe('resetRegionUpdates()', () => { + it('resets all update lists to empty lists', () => { + region.entityUpdates = [{}, {}] as any; + region.entityRemoves = [{}, {}] as any; + region.tileUpdates = [{}, {}] as any; + + resetRegionUpdates(region); + + expect(region.entityUpdates).eql([]); + expect(region.entityRemoves).eql([]); + expect(region.tileUpdates).eql([]); + }); + }); +}); diff --git a/src/ts/tests/server/serverUtils.spec.ts b/src/ts/tests/server/serverUtils.spec.ts new file mode 100644 index 0000000..ee70ee8 --- /dev/null +++ b/src/ts/tests/server/serverUtils.spec.ts @@ -0,0 +1,171 @@ +import '../lib'; +import { stub, assert, SinonStub } from 'sinon'; +import { expect } from 'chai'; +import { Types } from 'mongoose'; +import { toPonyObject, toSocialSite, tokenService, toPonyObjectAdmin } from '../../server/serverUtils'; +import { character, genId, auth } from '../mocks'; +import { CharacterFlags } from '../../common/adminInterfaces'; +import { TokenService } from '../../server/serverInterfaces'; + +describe('serverUtils', () => { + describe('tokenService()', () => { + let service: TokenService; + let socket: { + clearTokens: SinonStub; + token: SinonStub; + }; + + beforeEach(() => { + socket = { + clearTokens: stub(), + token: stub(), + }; + + service = tokenService(socket as any); + }); + + it('clears tokens for account', () => { + service.clearTokensForAccount('foo'); + + const filter = socket.clearTokens.args[0][0]; + expect(filter('', { accountId: 'foo' })).true; + expect(filter('', { accountId: 'bar' })).false; + assert.calledOnce(socket.clearTokens); + }); + + it('clears all tokens', () => { + service.clearTokensAll(); + + const filter = socket.clearTokens.args[0][0]; + expect(filter('', {})).true; + assert.calledOnce(socket.clearTokens); + }); + + it('creates token', () => { + const token = { account: {}, character: {} } as any; + + service.createToken(token); + + assert.calledWith(socket.token, token); + }); + }); + + describe('toPonyObject()', () => { + it('returns pony object', () => { + const id = genId(); + + expect(toPonyObject(character({ + _id: Types.ObjectId(id), + name: 'foo', + desc: 'aaa', + info: 'info', + site: Types.ObjectId('000000000000000000000002'), + tag: 'tag', + lastUsed: new Date(123), + }))).eql({ + id: id, + name: 'foo', + desc: 'aaa', + info: 'info', + site: '000000000000000000000002', + tag: 'tag', + lastUsed: '1970-01-01T00:00:00.123Z', + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); + + it('handles empty fields', () => { + const id = genId(); + + expect(toPonyObject(character({ + _id: Types.ObjectId(id), + name: 'foo', + }))).eql({ + id: id, + name: 'foo', + desc: '', + info: '', + site: undefined, + tag: undefined, + lastUsed: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); + + it('sets hide support field', () => { + const output = toPonyObject(character({ + _id: Types.ObjectId(genId()), + name: 'foo', + flags: CharacterFlags.HideSupport, + })); + + expect(output!.hideSupport).true; + }); + + it('sets respawn at spawn field', () => { + const output = toPonyObject(character({ + _id: Types.ObjectId(genId()), + name: 'foo', + flags: CharacterFlags.RespawnAtSpawn, + })); + + expect(output!.respawnAtSpawn).true; + }); + + it('returns null for undefined character', () => { + expect(toPonyObject(undefined)).null; + }); + }); + + describe('toPonyObjectAdmin()', () => { + it('returns pony object', () => { + const id = genId(); + + expect(toPonyObjectAdmin(character({ + _id: Types.ObjectId(id), + name: 'foo', + desc: 'aaa', + info: 'info', + site: Types.ObjectId('000000000000000000000001'), + tag: 'tag', + lastUsed: new Date(123), + creator: 'foo bar', + }))).eql({ + id: id, + name: 'foo', + desc: 'aaa', + info: 'info', + site: '000000000000000000000001', + tag: 'tag', + lastUsed: '1970-01-01T00:00:00.123Z', + hideSupport: undefined, + respawnAtSpawn: undefined, + creator: 'foo bar', + }); + }); + + it('returns null for undefined character', () => { + expect(toPonyObjectAdmin(undefined)).null; + }); + }); + + describe('toSocialSite()', () => { + it('returns site object', () => { + const id = genId(); + + expect(toSocialSite(auth({ + _id: Types.ObjectId(id), + name: 'foo', + provider: 'github', + url: 'foo.com', + }))).eql({ + id: id, + name: 'foo', + provider: 'github', + url: 'foo.com', + }); + }); + }); +}); diff --git a/src/ts/tests/server/services/friends.spec.ts b/src/ts/tests/server/services/friends.spec.ts new file mode 100644 index 0000000..c936e6f --- /dev/null +++ b/src/ts/tests/server/services/friends.spec.ts @@ -0,0 +1,4 @@ +import '../../lib'; + +describe('FriendsService', () => { +}); diff --git a/src/ts/tests/server/services/hiding.spec.ts b/src/ts/tests/server/services/hiding.spec.ts new file mode 100644 index 0000000..5da9406 --- /dev/null +++ b/src/ts/tests/server/services/hiding.spec.ts @@ -0,0 +1,608 @@ +import { stubClass, resetStubMethods } from '../../lib'; +import { bufferCount, first } from 'rxjs/operators'; +import { expect } from 'chai'; +import { useFakeTimers, SinonFakeTimers, stub, SinonStub, assert, match } from 'sinon'; +import { MINUTE, HIDE_LIMIT, HOUR, DAY } from '../../../common/constants'; +import { HidingService } from '../../../server/services/hiding'; +import { NotificationService } from '../../../server/services/notification'; +import { ServerNotification, IClient } from '../../../server/serverInterfaces'; +import { systemMessage } from '../../../server/logger'; +import { mockClient } from '../../mocks'; +import { times } from '../../../common/utils'; + +const DURATION = 24 * HOUR; + +function isHidden(a: IClient, b: IClient) { + return a.hides.has(b.accountId) || b.hides.has(a.accountId); +} + +describe('HidingService', () => { + let notifications = stubClass(NotificationService); + let service: HidingService; + let clock: SinonFakeTimers; + let log: SinonStub; + let clients: Map; + + function addClients(...items: IClient[]) { + items.forEach(c => clients.set(c.accountId, c)); + } + + beforeEach(() => { + resetStubMethods(notifications, 'addNotification'); + clock = useFakeTimers(); + clock.setSystemTime(DAY); + log = stub(); + clients = new Map(); + service = new HidingService(HOUR, notifications, id => clients.get(id), log); + service.start(); + }); + + afterEach(() => { + clock.restore(); + service.stop(); + }); + + describe('requestHide()', () => { + it('adds hide notification', () => { + const requester = mockClient(); + const target = mockClient(); + target.pony.name = 'foo'; + + service.requestHide(requester, target, DURATION); + + assert.calledWith(notifications.addNotification, requester, match({ + name: 'foo', + message: `Are you sure you want to hide #NAME# ?`, + entityId: target.pony.id, + })); + }); + + it('adds hide limit notification if reached hide limit', () => { + const requester = mockClient(); + const target = mockClient(); + times(HIDE_LIMIT, () => service.hide(requester, mockClient(), DURATION)); + + service.requestHide(requester, target, DURATION); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Cannot hide any more players.', + })); + }); + + it('accepting notification hides target player', () => { + const requester = mockClient(); + requester.characterName = 'req_pony'; + requester.account.name = 'req'; + requester.accountId = 'REQ'; + const target = mockClient(); + target.characterName = 'tgt_pony'; + target.account.name = 'tgt'; + target.accountId = 'TGT'; + service.requestHide(requester, target, DURATION); + + const notification = notifications.addNotification.args[0][1] as ServerNotification; + notification.accept!(); + + expect(isHidden(requester, target)).true; + expect(service.isHidden('REQ', 'TGT')).true; + assert.calledWith(log, '[REQ][system]\treq_pony (req) hides tgt_pony (tgt) [TGT]'); + }); + + it('does not log message if already hidden', () => { + const requester = mockClient(); + requester.accountId = 'REQ'; + const target = mockClient(); + target.accountId = 'TGT'; + service.requestHide(requester, target, DURATION); + service.hide(requester, target, DURATION); + + const notification = notifications.addNotification.args[0][1] as ServerNotification; + notification.accept!(); + + assert.notCalled(log); + }); + + it('prevents hiding party members', () => { + const requester = mockClient(); + const target = mockClient(); + requester.party = target.party = { id: '', clients: [requester, target], leader: requester, pending: [] }; + + service.requestHide(requester, target, DURATION); + + expect(isHidden(requester, target)).false; + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Cannot hide players from your party.', + })); + }); + }); + + describe('requestUnhideAll()', () => { + it('adds unhide notification', () => { + const requester = mockClient(); + + service.requestUnhideAll(requester); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Are you sure you want to unhide all temporarily hidden players ?', + })); + }); + + it('adds unhide notification if previous limit timed out', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.stop(); + clock.tick(HOUR + 1); + + service.requestUnhideAll(requester); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Are you sure you want to unhide all temporarily hidden players ?', + })); + }); + + it('adds unhide limit notification if already used unhideAll', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + + service.requestUnhideAll(requester); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Cannot unhide hidden players, try again later.', + })); + }); + + it('accepting notification unhides all players', () => { + const requester = mockClient(); + const target = mockClient(); + service.hide(requester, target, DURATION); + service.requestUnhideAll(requester); + expect(isHidden(requester, target)).true; + + const notification = notifications.addNotification.args[0][1] as ServerNotification; + notification.accept!(); + + expect(isHidden(requester, target)).false; + expect(service.isHidden(requester.accountId, target.accountId)).false; + assert.calledWith(log, systemMessage(requester.accountId, 'unhide all')); + }); + }); + + describe('hide()', () => { + it('hides target user from source user', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + + service.hide(requester, target, DURATION); + + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); + + it('does nothing if already hidden', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + + service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); + + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); + + it('hides source user from target user', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + + service.hide(requester, target, DURATION); + + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(target, requester)).true; + }); + + it('does not hide user from themselves', () => { + const requester = mockClient(); + addClients(requester); + + service.hide(requester, requester, DURATION); + + expect(isHidden(requester, requester)).false; + expect(service.isHiddenClient(requester, requester)).false; + }); + + it('does not clean hides too early', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + + service.hide(requester, target, DURATION); + + clock.tick(25 * MINUTE); + + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); + + it('cleans up old hides', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + + service.hide(requester, target, DURATION); + + clock.tick(25 * HOUR); + + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); + + it('clears hide after given duration', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + + service.hide(requester, target, HOUR / 2); + + clock.tick(HOUR); + + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); + + it('triggers change event', done => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + service.changes.subscribe(hide => { + expect(hide).eql({ by: requester.accountId, who: target.accountId }); + done(); + }); + + service.hide(requester, target, DURATION); + }); + }); + + describe('isHiddenClient()', () => { + it('hides target user from source user', () => { + const who = mockClient(); + const from = mockClient(); + service.hide(who, from, DURATION); + + expect(isHidden(who, from)).true; + expect(service.isHiddenClient(who, from)).true; + }); + }); + + describe('unhide()', () => { + it('unhides user', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + + service.unhide(requester, target); + + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); + + it('unhides only given user', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + + service.hide(requester, target1, DURATION); + service.hide(requester, target2, DURATION); + + service.unhide(requester, target1); + + expect(isHidden(requester, target1)).false; + expect(isHidden(requester, target2)).true; + expect(service.isHiddenClient(requester, target1)).false; + expect(service.isHiddenClient(requester, target2)).true; + }); + + it('does nothing if not hidden', () => { + const requester = mockClient(); + const target = mockClient(); + + service.unhide(requester, target); + + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); + + it('does nothing if not hidden (2)', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + + service.hide(requester, target1, DURATION); + + service.unhide(requester, target2); + + expect(isHidden(requester, target1)).true; + expect(isHidden(requester, target2)).false; + expect(service.isHiddenClient(requester, target1)).true; + expect(service.isHiddenClient(requester, target2)).false; + }); + + it('triggers change event', done => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.changes.subscribe(hide => { + expect(hide).eql({ by: requester.accountId, who: target.accountId }); + done(); + }); + + service.unhide(requester, target); + }); + }); + + describe('unhideAll()', () => { + it('does nothing if no users are hidden', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + + service.unhideAll(requester); + + expect(isHidden(requester, target1)).false; + expect(isHidden(requester, target2)).false; + expect(service.isHiddenClient(requester, target1)).false; + expect(service.isHiddenClient(requester, target2)).false; + }); + + it('unhides all', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + + service.hide(requester, target1, DURATION); + service.hide(requester, target2, DURATION); + + service.unhideAll(requester); + + expect(isHidden(requester, target1)).false; + expect(isHidden(requester, target2)).false; + expect(service.isHiddenClient(requester, target1)).false; + expect(service.isHiddenClient(requester, target2)).false; + }); + + it('does not count to limit if noone is hidden', () => { + const requester = mockClient(); + const target = mockClient(); + + service.unhideAll(requester); + service.hide(requester, target, DURATION); + service.unhideAll(requester); + + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); + + it('prevents unhide all if used too fast', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.hide(requester, target, DURATION); + + service.unhideAll(requester); + + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); + + it('does not clean up old unhides too early', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.hide(requester, target, DURATION); + + clock.tick(25 * MINUTE); + + service.unhideAll(requester); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); + + it('cleans up old unhides', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.hide(requester, target, DURATION); + + clock.tick(2 * HOUR); + + service.unhideAll(requester); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); + + it('triggers unhidesAll event', done => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhidesAll.subscribe(id => { + expect(id).equal(requester.accountId); + done(); + }); + + service.unhideAll(requester); + }); + }); + + describe('merged()', () => { + it('transfers hide list to new account ID', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2); + + service.hide(requester1, target, DURATION); + + service.merged(requester2.accountId, requester1.accountId); + + expect(isHidden(requester2, target)).true; + expect(service.isHiddenClient(requester2, target)).true; + }); + + it('transfers hide list to new account ID (no client)', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + + service.hide(requester1, target, DURATION); + + service.merged(requester2.accountId, requester1.accountId); + + expect(service.isHiddenClient(requester2, target)).true; + }); + + it('merges hide lists', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester2, target1, target2); + + service.hide(requester1, target1, DURATION); + service.hide(requester2, target2, DURATION); + + service.merged(requester2.accountId, requester1.accountId); + + expect(isHidden(requester2, target1)).true; + expect(isHidden(requester2, target2)).true; + expect(service.isHiddenClient(requester2, target1)).true; + expect(service.isHiddenClient(requester2, target2)).true; + }); + + it('does not allow to be hidden by yourself after merge', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(target); + + service.hide(requester, target, DURATION); + // service.hide(target, requester, DURATION); + + service.merged(target.accountId, requester.accountId); + + expect(isHidden(target, target)).false; + // expect(isHidden(requester, target)).false; // requester client does not exist at this point + // expect(isHidden(target, requester)).false; // requester client does not exist at this point + expect(service.isHiddenClient(target, target)).false; + expect(service.isHiddenClient(requester, target)).false; + expect(service.isHiddenClient(target, requester)).false; + }); + + it('updates in hidden lists', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester, target2); + + service.hide(requester, target1, DURATION); + + service.merged(target2.accountId, target1.accountId); + + expect(isHidden(requester, target2)).true; + expect(service.isHiddenClient(requester, target2)).true; + }); + + it('picks newer date in hidden lists', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester, target2); + + clock.setSystemTime(HOUR); + service.hide(requester, target1, DURATION); + clock.setSystemTime(5 * HOUR); + service.hide(requester, target2, DURATION); + + service.merged(target2.accountId, target1.accountId); + + clock.tick(22 * HOUR); + expect(isHidden(requester, target2)).true; + expect(service.isHiddenClient(requester, target2)).true; + }); + + it('transfers unhide countdown', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2, target); + + service.hide(requester1, target, DURATION); + service.unhideAll(requester1); + + service.merged(requester2.accountId, requester1.accountId); + + service.hide(requester2, target, DURATION); + service.unhideAll(requester2); + expect(isHidden(requester2, target)).true; + expect(service.isHiddenClient(requester2, target)).true; + }); + + it('does not trigger change event if state of user didn\'t change', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2, target); + + service.hide(requester1, target, DURATION); + service.hide(requester2, target, DURATION); + + service.merged(requester2.accountId, requester1.accountId); + }); + + it('triggers change events (1)', done => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2, target); + + service.hide(requester1, target, DURATION); + service.changes.pipe(bufferCount(2), first()).subscribe(([hide1, hide2]) => { + expect(hide1).eql({ by: requester2.accountId, who: target.accountId }); + expect(hide2).eql({ by: requester1.accountId, who: target.accountId }); + done(); + }); + + service.merged(requester2.accountId, requester1.accountId); + }); + + it('triggers change events (2)', done => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester, target2); + + service.hide(requester, target1, DURATION); + service.changes.pipe(bufferCount(2), first()).subscribe(([hide1, hide2]) => { + expect(hide1).eql({ by: requester.accountId, who: target2.accountId }); + expect(hide2).eql({ by: requester.accountId, who: target1.accountId }); + done(); + }); + + service.merged(target2.accountId, target1.accountId); + }); + }); +}); diff --git a/src/ts/tests/server/services/notification.spec.ts b/src/ts/tests/server/services/notification.spec.ts new file mode 100644 index 0000000..f4629cc --- /dev/null +++ b/src/ts/tests/server/services/notification.spec.ts @@ -0,0 +1,207 @@ +import '../../lib'; +import { uniqueId } from 'lodash'; +import { expect } from 'chai'; +import { stub, spy, assert } from 'sinon'; +import { IClient } from '../../../server/serverInterfaces'; +import { NotificationService } from '../../../server/services/notification'; +import { times } from '../../../common/utils'; + +describe('NotificationService', () => { + let notificationService: NotificationService; + let client: IClient; + + function createClient(): IClient { + return { + notifications: [], + addNotification() { }, + removeNotification() { }, + } as any; + } + + beforeEach(() => { + notificationService = new NotificationService(); + client = createClient(); + }); + + after(() => { + notificationService = undefined as any; + client = undefined as any; + }); + + describe('addNotification()', () => { + it('adds notification to client', () => { + const notification = { id: 0, name: 'name', message: 'test' }; + + notificationService.addNotification(client, notification); + + expect(client.notifications).contain(notification); + }); + + it('returns new notification ID', () => { + const notification = { id: 0, name: 'name', message: 'test' }; + + expect(notificationService.addNotification(client, notification)).equal(1); + }); + + it('assigns ID to notification', () => { + const notification = { id: 0, name: 'name', message: 'test1' }; + client.notifications.push({ id: 1, name: 'name', message: 'test2' }); + + notificationService.addNotification(client, notification); + + expect(notification.id).not.equal(0); + }); + + it('sends addNotification', () => { + const notification = { id: 0, name: 'name', message: 'test', note: 'note', flags: 123 }; + const addNotification = stub(client, 'addNotification'); + + notificationService.addNotification(client, notification); + + assert.calledWith(addNotification, 1, 0, 'name', 'test', 'note', 123); + }); + + it('does not add notification to client if limit is reached', () => { + times(10, () => notificationService.addNotification(client, { id: 0, name: 'name', message: uniqueId('test') })); + const addNotification = stub(client, 'addNotification'); + + expect(notificationService.addNotification(client, { id: 0, name: 'name', message: uniqueId('test') })).equal(0); + + assert.notCalled(addNotification); + }); + + it('does not add notification to client if identical notification already exists', () => { + const addNotification = stub(client, 'addNotification'); + + expect(notificationService.addNotification(client, { id: 0, name: 'name', message: 'foo' })).not.equal(0); + expect(notificationService.addNotification(client, { id: 0, name: 'name', message: 'foo' })).equal(0); + + expect(client.notifications.length).equal(1); + assert.calledOnce(addNotification); + }); + }); + + describe('removeNotification()', () => { + it('removes notification from client', () => { + const notification = { id: 1, name: 'name', message: 'test' }; + client.notifications.push(notification); + + notificationService.removeNotification(client, 1); + + expect(client.notifications).not.contain(notification); + }); + + it('does nothing if notification does not exist', () => { + const removeNotification = stub(client, 'removeNotification'); + + notificationService.removeNotification(client, 1); + + assert.notCalled(removeNotification); + }); + + it('sends removeNotification', () => { + client.notifications.push({ id: 1, name: 'name', message: 'test' }); + const removeNotification = stub(client, 'removeNotification'); + + notificationService.removeNotification(client, 1); + + assert.calledWith(removeNotification, 1); + }); + + it('returns true if notification is removed', () => { + client.notifications.push({ id: 1, name: 'name', message: 'test' }); + + expect(notificationService.removeNotification(client, 1)).true; + }); + + it('returns false if notification does not exist', () => { + expect(notificationService.removeNotification(client, 1)).false; + }); + }); + + describe('acceptNotification()', () => { + it('calls accept callback', () => { + const accept = spy(); + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept }); + + notificationService.acceptNotification(client, 1); + + assert.calledOnce(accept); + }); + + it('removes notification', () => { + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept() { } }); + const removeNotification = stub(notificationService, 'removeNotification'); + + notificationService.acceptNotification(client, 1); + + assert.calledWith(removeNotification, client, 1); + }); + + it('does nothing if notification does not exist', () => { + const removeNotification = stub(client, 'removeNotification'); + + notificationService.acceptNotification(client, 1); + + assert.notCalled(removeNotification); + }); + + it('works if notification does not have accept callback', () => { + const notification = { id: 0, name: 'name', message: 'test' }; + notificationService.addNotification(client, notification); + + notificationService.acceptNotification(client, 1); + + expect(client.notifications).not.include(notification); + }); + }); + + describe('rejectNotification()', () => { + it('calls reject callback', () => { + const reject = spy(); + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', reject }); + + notificationService.rejectNotification(client, 1); + + assert.calledOnce(reject); + }); + + it('removes notification', () => { + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept() { } }); + const removeNotification = stub(notificationService, 'removeNotification'); + + notificationService.rejectNotification(client, 1); + + assert.calledWith(removeNotification, client, 1); + }); + + it('does nothing if notification does not exist', () => { + const removeNotification = stub(client, 'removeNotification'); + + notificationService.rejectNotification(client, 1); + + assert.notCalled(removeNotification); + }); + + it('works if notification does not have reject callback', () => { + const notification = { id: 0, name: 'name', message: 'test' }; + notificationService.addNotification(client, notification); + + notificationService.rejectNotification(client, 1); + + expect(client.notifications).not.include(notification); + }); + }); + + describe('rejectAll()', () => { + it('rejects all notifications', () => { + client.notifications = [{ id: 1 }, { id: 2 }] as any; + const rejectNotification = stub(notificationService, 'rejectNotification'); + + notificationService.rejectAll(client); + + assert.calledWith(rejectNotification, client, 1); + assert.calledWith(rejectNotification, client, 2); + }); + }); +}); diff --git a/src/ts/tests/server/services/party.spec.ts b/src/ts/tests/server/services/party.spec.ts new file mode 100644 index 0000000..12cd457 --- /dev/null +++ b/src/ts/tests/server/services/party.spec.ts @@ -0,0 +1,873 @@ +import '../../lib'; +import { range } from 'lodash'; +import { expect } from 'chai'; +import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, match } from 'sinon'; +import { PartyFlags, NotificationFlags } from '../../../common/interfaces'; +import { AccountFlags } from '../../../common/adminInterfaces'; +import { PARTY_LIMIT, HOUR } from '../../../common/constants'; +import { NotificationService } from '../../../server/services/notification'; +import { IClient, ServerParty } from '../../../server/serverInterfaces'; +import { + PartyService, LEADER_TIMEOUT, INVITE_LIMIT, INVITE_REJECTED_LIMIT, INVITE_REJECTED_TIMEOUT +} from '../../../server/services/party'; +import { mockClient } from '../../mocks'; +import { addIgnore } from '../../../server/playerUtils'; +import { times } from '../../../common/utils'; + +describe('PartyService', () => { + let notificationService: NotificationService; + let partyService: PartyService; + let leader: IClient; + let client: IClient; + let addNotification: SinonStub; + let leaderUpdateParty: SinonStub; + let clientUpdateParty: SinonStub; + let reportInviteLimit: SinonStub; + let clock: SinonFakeTimers; + + function createClient(id: number, characterId?: string, accountId?: string): IClient { + return mockClient({ + accountId, + characterId, + pony: { id }, + character: { id: characterId }, + account: { id: accountId }, + }); + } + + function createParty(leader: IClient, clients: IClient[] = [], pending: IClient[] = []): ServerParty { + const party: ServerParty = { + id: 'some_id', + leader, + clients: [leader, ...clients], + pending: pending.map(client => ({ client, notificationId: 5 })), + }; + party.clients.forEach(c => c.party = party); + partyService.parties.push(party); + return party; + } + + beforeEach(() => { + clock = useFakeTimers(Date.now()); + leader = createClient(1, 'foo', 'foofoo'); + client = createClient(2, 'bar', 'barbar'); + leaderUpdateParty = stub(leader, 'updateParty'); + clientUpdateParty = stub(client, 'updateParty'); + reportInviteLimit = stub(); + notificationService = new NotificationService(); + addNotification = stub(notificationService, 'addNotification').returns(1); + partyService = new PartyService(notificationService, reportInviteLimit); + }); + + afterEach(() => { + clock.restore(); + partyService.dispose(); + }); + + describe('clientConnected()', () => { + it('does nothing if there is no party for client', () => { + partyService.clientConnected(client); + + assert.notCalled(clientUpdateParty); + }); + + describe('rejoining', () => { + beforeEach(() => { + partyService.invite(leader, client); + addNotification.firstCall.args[1].accept(); + }); + + it('replaces matching client', () => { + const newClient = createClient(5, 'bar', 'barbar'); + + partyService.clientConnected(newClient); + + expect(leader.party!.clients[1]).equal(newClient); + }); + + it('replaces matching client for the same account', () => { + const newClient = createClient(5, 'abc', 'barbar'); + + partyService.clientConnected(newClient); + + expect(leader.party!.clients[1]).equal(newClient); + }); + + it('updates leader if leader reconnected', () => { + const newLeader = createClient(5, 'foo', 'foofoo'); + + partyService.clientConnected(newLeader); + + expect(client.party!.leader).equal(newLeader); + }); + + it('sends party update', () => { + const newClient = createClient(5, 'bar', 'barbar'); + const updateParty = stub(newClient, 'updateParty'); + leaderUpdateParty.reset(); + + partyService.clientConnected(newClient); + + assert.calledOnce(updateParty); + assert.calledOnce(leaderUpdateParty); + }); + + it('sets party for new client', () => { + const newClient = createClient(5, 'bar', 'barbar'); + + partyService.clientConnected(newClient); + + expect(newClient.party).equal(leader.party); + }); + + it('unsets party for old client', () => { + const newClient = createClient(5, 'bar', 'barbar'); + + partyService.clientConnected(newClient); + + expect(client.party).undefined; + }); + + it('sets offlineAt to current time', () => { + const newClient = createClient(5, 'bar', 'barbar'); + + clock.setSystemTime(100); + partyService.clientConnected(newClient); + + expect(client.offlineAt!.getTime()).equal(new Date().getTime()); + }); + }); + + it('cancels new leader promotion', () => { + const party = createParty(leader, [client, createClient(9, 'x', 'xx')]); + const promoteLeader = stub(partyService, 'promoteLeader'); + const newLeader = createClient(10, 'foo', 'foofoo'); + + leader.offline = true; + partyService.clientDisconnected(leader); + partyService.clientConnected(newLeader); + clock.tick(LEADER_TIMEOUT + 100); + + expect(party.leader).equal(newLeader); + assert.notCalled(promoteLeader); + }); + }); + + describe('clientDisconnected()', () => { + it('sends party update', () => { + createParty(leader, [client]); + + client.offline = true; + partyService.clientDisconnected(client); + + assert.calledWith(leaderUpdateParty, [ + [1, PartyFlags.Leader], + [2, PartyFlags.Offline], + ]); + }); + + it('does not send party update to disconnected client', () => { + createParty(leader, [client]); + + client.offline = true; + partyService.clientDisconnected(client); + + assert.notCalled(clientUpdateParty); + }); + + it('does nothing if not member of a party', () => { + createParty(leader, [createClient(9)]); + + client.offline = true; + partyService.clientDisconnected(client); + + assert.notCalled(leaderUpdateParty); + }); + + it('promotes new leader after timeout', () => { + createParty(leader, [client, createClient(9)]); + const promoteLeader = stub(partyService, 'promoteLeader'); + + leader.offline = true; + partyService.clientDisconnected(leader); + clock.tick(LEADER_TIMEOUT + 100); + + assert.calledWith(promoteLeader, leader, client); + }); + + it('does not promote offline player as the new leader', () => { + const anotherClient = createClient(9); + createParty(leader, [client, anotherClient]); + const promoteLeader = stub(partyService, 'promoteLeader'); + + leader.offline = true; + client.offline = true; + partyService.clientDisconnected(leader); + clock.tick(LEADER_TIMEOUT + 100); + + assert.calledWith(promoteLeader, leader, anotherClient); + }); + + it('removes client if pending', () => { + const party = createParty(leader, [createClient(9)], [client]); + + client.offline = true; + partyService.clientDisconnected(client); + + expect(party.pending).empty; + }); + + it('sends party update (pending)', () => { + createParty(leader, [createClient(9)], [client]); + + client.offline = true; + partyService.clientDisconnected(client); + + assert.calledOnce(leaderUpdateParty); + }); + + it('disbands party if cant find new leader after timeout', () => { + const party = createParty(leader, [], [client, createClient(9)]); + + leader.offline = true; + partyService.clientDisconnected(leader); + clock.tick(LEADER_TIMEOUT + 100); + + expect(partyService.parties).not.include(party); + }); + }); + + describe('invite()', () => { + it('creates new party on the leader if none exists', () => { + partyService.invite(leader, client); + + expect(leader.party).not.empty; + expect(leader.party!.leader).equal(leader); + expect(leader.party!.clients).eql([leader]); + expect(leader.party!.pending).eql([{ client, notificationId: 1 }]); + expect(partyService.parties).contain(leader.party!); + }); + + it('adds client to pending members', () => { + partyService.invite(leader, client); + + expect(leader.party!.pending[0].client).equal(client); + }); + + it('logs party invitation', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + + partyService.invite(leader, client); + + assert.calledWith(systemLog, 'Invite to party [barbar]'); + }); + + it('sends invite notice the the client', () => { + leader.pony.name = 'foo'; + + partyService.invite(leader, client); + + assert.calledWith(addNotification, client, match({ + name: 'foo', + entityId: leader.pony.id, + message: '
Party invite
#NAME# invited you to a party', + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore, + })); + }); + + it('sends invite notice the the client (existing party)', () => { + leader.party = createParty(leader, [createClient(3)]); + + partyService.invite(leader, client); + + assert.calledOnce(addNotification); + }); + + it('sends party update to the leader', () => { + partyService.invite(leader, client); + + assert.calledWithMatch(leaderUpdateParty, [ + [1, PartyFlags.Leader], + [2, PartyFlags.Pending], + ]); + }); + + it('does nothing if already is in party and not a leader', () => { + const someone = createClient(3); + someone.party = createParty(leader, [someone]); + + partyService.invite(someone, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if client is already in a party', () => { + client.party = createParty(client); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if client is already pending', () => { + leader.party = createParty(leader, [], [client]); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if party is already at member limit', () => { + leader.party = createParty(leader, range(0, PARTY_LIMIT - 1).map(i => createClient(i + 10))); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if party is already at member limit (pending)', () => { + leader.party = createParty(leader, [], range(0, PARTY_LIMIT - 1).map(i => createClient(i + 10))); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if leader is ignored', () => { + leader.party = createParty(leader, [createClient(9)], []); + addIgnore(leader, client.accountId); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if leader is timedout', () => { + leader.account.mute = Date.now() + HOUR; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if leader is muted', () => { + leader.account.mute = -1; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if leader is shadowed', () => { + leader.shadowed = true; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if client is offline', () => { + leader.party = createParty(leader, [createClient(9)], []); + client.offline = true; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if inviting self (client)', () => { + partyService.invite(leader, leader); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if inviting self (account)', () => { + client.accountId = leader.accountId; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + function setupRejectLimit() { + range(0, INVITE_REJECTED_LIMIT).forEach(i => { + const c = createClient(10 + i); + partyService.invite(leader, c); + addNotification.firstCall.args[1].reject(); + addNotification.reset(); + addNotification.returns(1); + }); + } + + it('does nothing if reached invite limit', () => { + setupRejectLimit(); + addNotification.reset(); + addNotification.returns(1); + leaderUpdateParty.reset(); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('reports if reached invite limit', () => { + setupRejectLimit(); + + assert.calledWith(reportInviteLimit, leader); + }); + + it('resets invite limit periodically', () => { + setupRejectLimit(); + addNotification.reset(); + addNotification.returns(1); + leaderUpdateParty.reset(); + clock.tick(INVITE_REJECTED_TIMEOUT * 2); + + partyService.invite(leader, client); + + assert.calledOnce(addNotification); + assert.calledOnce(leaderUpdateParty); + }); + + it('does nothing if leader has party invites blocked', () => { + leader.party = createParty(leader, [createClient(9)], []); + leader.account.flags = AccountFlags.BlockPartyInvites; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if user ignores all party requests', () => { + leader.party = createParty(leader, [createClient(9)], []); + client.accountSettings = { ignorePartyInvites: true }; + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if user already reached party request limit', () => { + range(0, INVITE_LIMIT).forEach(i => partyService.invite(createClient(10 + i), client)); + addNotification.reset(); + + partyService.invite(leader, client); + + assert.notCalled(addNotification); + }); + + it('does nothing if add notification returns 0', () => { + addNotification.returns(0); + + partyService.invite(leader, client); + + expect(leader.party).undefined; + assert.notCalled(leaderUpdateParty); + assert.notCalled(clientUpdateParty); + }); + + it('does nothing for existing party if add notification returns 0', () => { + const party = createParty(leader, [createClient(9)], []); + leader.party = party; + addNotification.returns(0); + + partyService.invite(leader, client); + + expect(leader.party).equal(party); + assert.notCalled(clientUpdateParty); + }); + + describe('notification.accept()', () => { + function accept() { + addNotification.firstCall.args[1].accept(); + } + + it('removes client from pending', () => { + partyService.invite(leader, client); + + accept(); + + expect(leader.party!.pending).empty; + }); + + it('removes notification', () => { + const removeNotification = stub(notificationService, 'removeNotification'); + partyService.invite(leader, client); + + accept(); + + assert.calledWith(removeNotification, client, 1); + }); + + it('adds client to clients', () => { + partyService.invite(leader, client); + + accept(); + + expect(leader.party!.clients).contain(client); + }); + + it('sets party for client', () => { + partyService.invite(leader, client); + + accept(); + + expect(client.party).equal(leader.party); + }); + + it('sends party update', () => { + partyService.invite(leader, client); + leaderUpdateParty.reset(); + clientUpdateParty.reset(); + + accept(); + + assert.calledOnce(leaderUpdateParty); + assert.calledOnce(clientUpdateParty); + }); + + it('logs accept', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + partyService.invite(leader, client); + + accept(); + + assert.calledWith(systemLog, 'Invite accepted by [barbar]'); + }); + + it('rejects all other party invites', () => { + const leader2 = createClient(8); + const leader3 = createClient(9); + partyService.invite(leader, client); + partyService.invite(leader2, client); + partyService.invite(leader3, client); + + accept(); + + expect(leader2.party).undefined; + expect(leader3.party).undefined; + }); + + it('does nothing if not in pending', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + partyService.remove(leader, client); + leaderUpdateParty.reset(); + + accept(); + + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if party does not exist', () => { + partyService.invite(leader, client); + partyService.remove(leader, client); + leaderUpdateParty.reset(); + + accept(); + + assert.notCalled(leaderUpdateParty); + }); + }); + + describe('notification.reject()', () => { + function reject() { + addNotification.firstCall.args[1].reject(); + } + + it('removes client from pending', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + + reject(); + + expect(leader.party!.pending).empty; + }); + + it('removes notification', () => { + const removeNotification = stub(notificationService, 'removeNotification'); + partyService.invite(leader, client); + + reject(); + + assert.calledWith(removeNotification, client, 1); + }); + + it('sends party update', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + leaderUpdateParty.reset(); + + reject(); + + assert.calledOnce(leaderUpdateParty); + }); + + it('logs rejection', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + partyService.invite(leader, client); + + reject(); + + assert.calledWith(systemLog, 'Invite rejected by [barbar]'); + }); + + it('disbands party if less than 2 users', () => { + partyService.invite(leader, client); + leaderUpdateParty.reset(); + + reject(); + + expect(leader.party).undefined; + }); + + it('does nothing if client is not pending anymore', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + partyService.remove(leader, client); + leaderUpdateParty.reset(); + + reject(); + + assert.notCalled(leaderUpdateParty); + }); + }); + }); + + describe('remove()', () => { + it('removes client from the party', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; + + partyService.remove(leader, client); + + expect(party.clients).not.include(client); + }); + + it('removes party from the client', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; + + partyService.remove(leader, client); + + expect(client.party).undefined; + }); + + it('removes pending client from the party', () => { + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; + + partyService.remove(leader, client); + + expect(party.pending).empty; + }); + + it('logs cancel if removed pending client', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; + + partyService.remove(leader, client); + + assert.calledWith(systemLog, 'Invite cancelled for [barbar]'); + }); + + it('counts invite limit for cancels', () => { + const party = createParty(leader, [createClient(3)], []); + + times(INVITE_REJECTED_LIMIT, () => { + client.party = party; + party.pending.push({ client, notificationId: 0 }); + partyService.remove(leader, client); + }); + + assert.calledWith(reportInviteLimit, leader); + }); + + it('removes pending client notification', () => { + const removeNotification = stub(notificationService, 'removeNotification'); + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; + + partyService.remove(leader, client); + + assert.calledWith(removeNotification, client, 5); + }); + + it('sends party update to all clients', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; + + partyService.remove(leader, client); + + assert.calledOnce(leaderUpdateParty); + }); + + it('sends party update to all clients (pending)', () => { + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; + + partyService.remove(leader, client); + + assert.calledOnce(leaderUpdateParty); + }); + + it('sends undefined party update to the client', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; + + partyService.remove(leader, client); + + assert.calledWith(clientUpdateParty, undefined); + }); + + it('does nothing if given leader is not the leader of the party', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; + + partyService.remove(createClient(4), client); + + expect(party.clients).include(client); + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if given client is not in the party', () => { + const party = createParty(leader, [createClient(3), createClient(4)]); + + partyService.remove(leader, client); + + expect(party.clients).not.include(client); + assert.notCalled(leaderUpdateParty); + }); + + it('selects new party leader', () => { + const party = createParty(leader, [client, createClient(3)]); + + partyService.remove(leader, leader); + + expect(party.leader).equal(client); + }); + + it('disbands party if less than 2 members are left', () => { + createParty(leader, [client]); + + partyService.remove(leader, client); + + expect(leader.party).undefined; + expect(client.party).undefined; + }); + + it('disbands party if cannot find new leader', () => { + const party = createParty(leader, [], [createClient(9), createClient(10)]); + + partyService.remove(leader, leader); + + expect(partyService.parties).not.include(party); + }); + + it('disbands party and clear all its fields', () => { + const party = createParty(leader, [client], []); + + partyService.remove(leader, leader); + + expect(party.clients).empty; + expect(party.pending).empty; + }); + }); + + describe('leave()', () => { + it('calls removeFromParty', () => { + createParty(leader, [client]); + const remove = stub(partyService, 'remove'); + + partyService.leave(client); + + assert.calledWith(remove, leader, client); + }); + + it('does not call removeFromParty if not in party', () => { + const remove = stub(partyService, 'remove'); + + partyService.leave(client); + + assert.notCalled(remove); + }); + }); + + describe('promoteLeader()', () => { + it('sets client as leader', () => { + const party = createParty(leader, [client]); + + partyService.promoteLeader(leader, client); + + expect(party.leader).equal(client); + }); + + it('sends party update', () => { + createParty(leader, [client]); + + partyService.promoteLeader(leader, client); + + assert.calledWithMatch(leaderUpdateParty, [ + [1, PartyFlags.None], + [2, PartyFlags.Leader], + ]); + }); + + it('does nothing if client is already the leader', () => { + createParty(leader, [client]); + + partyService.promoteLeader(leader, leader); + + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if client is offline', () => { + createParty(leader, [client]); + client.offline = true; + + partyService.promoteLeader(leader, client); + + assert.notCalled(leaderUpdateParty); + }); + + it('does nothing if leader is not in party', () => { + createParty(createClient(9), [client]); + + partyService.promoteLeader(leader, client); + + assert.notCalled(clientUpdateParty); + }); + + it('does nothing if leader is not a leader', () => { + createParty(createClient(9), [leader, client]); + + partyService.promoteLeader(leader, client); + + assert.notCalled(clientUpdateParty); + }); + + it('does nothing if client is not in party', () => { + createParty(leader, [createClient(9)]); + + partyService.promoteLeader(leader, client); + + assert.notCalled(leaderUpdateParty); + }); + }); +}); diff --git a/src/ts/tests/server/services/supporterInvites.spec.ts b/src/ts/tests/server/services/supporterInvites.spec.ts new file mode 100644 index 0000000..bf2f4d0 --- /dev/null +++ b/src/ts/tests/server/services/supporterInvites.spec.ts @@ -0,0 +1,381 @@ +import { stubClass, resetStubMethods } from '../../lib'; +import { assert, SinonStub, stub, SinonStubbedInstance, SinonFakeTimers, useFakeTimers, match } from 'sinon'; +import { Model } from 'mongoose'; +import { expect } from 'chai'; +import { SupporterInvitesService, updateSupporterInvites } from '../../../server/services/supporterInvites'; +import { NotificationService } from '../../../server/services/notification'; +import { ISupporterInvite } from '../../../server/db'; +import { mockClient } from '../../mocks'; +import { SupporterFlags } from '../../../common/adminInterfaces'; +import { IClient } from '../../../server/serverInterfaces'; +import { MessageType } from '../../../common/interfaces'; +import { addIgnore } from '../../../server/playerUtils'; +import { DAY } from '../../../common/constants'; + +function exec(value: any): any { + return { exec: stub().resolves(value) }; +} + +describe('SupporterInvitesService', () => { + let model: SinonStubbedInstance> & { countDocuments: SinonStub; }; + let notifications = stubClass(NotificationService); + let log: SinonStub; + let service: SupporterInvitesService; + + beforeEach(() => { + resetStubMethods(notifications, 'addNotification'); + model = { + find: stub(), + countDocuments: stub(), + create: stub(), + deleteOne: stub(), + deleteMany: stub(), + updateMany: stub(), + } as any; + log = stub(); + service = new SupporterInvitesService(model as any, notifications, log); + }); + + afterEach(() => { + service.dispose(); + }); + + describe('getInvites()', () => { + it('returns all invites from given client', async () => { + const client = mockClient(); + model.find.withArgs({ source: client.account._id }).returns({ + exec: stub().resolves([ + { _id: 'foo', name: 'Foo', info: 'info', active: true, anotherField: 'xyz' }, + ]) + } as any); + + const result = await service.getInvites(client); + + expect(result).eql([ + { id: 'foo', name: 'Foo', info: 'info', active: true }, + ]); + }); + }); + + describe('isInvited()', () => { + it('returns true if has any active invites', async () => { + const client = mockClient(); + model.countDocuments.withArgs({ target: client.account._id, active: true }).returns(exec(1)); + + const result = await service.isInvited(client); + + expect(result).true; + }); + + it('returns false if doesn not have any active invites', async () => { + const client = mockClient(); + model.countDocuments.withArgs({ target: client.account._id, active: true }).returns(exec(0)); + + const result = await service.isInvited(client); + + expect(result).false; + }); + }); + + describe('requestInvite()', () => { + let requester: IClient; + let target: IClient; + + beforeEach(() => { + requester = mockClient(); + requester.account.supporter = SupporterFlags.Supporter1; + target = mockClient(); + model.find.returns(exec([])); + }); + + it('adds notification', async () => { + requester.pony.name = 'Foo'; + + await service.requestInvite(requester, target); + + assert.calledWith(notifications.addNotification, target, match.any); + const notification = notifications.addNotification.args[0][1]; + expect(notification.sender).equal(requester); + expect(notification.entityId).equal(requester.pony.id); + expect(notification.message).equal(`#NAME# invited you to supporter servers`); + }); + + it('fails if inviting self', async () => { + await service.requestInvite(requester, requester); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if requester is shadowed', async () => { + requester.shadowed = true; + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if requester is muted', async () => { + requester.account.mute = Date.now() + 10000; + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if target is offline', async () => { + target.offline = true; + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if target ignores requester', async () => { + addIgnore(requester, target.accountId); + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if requester ignores target', async () => { + addIgnore(target, requester.accountId); + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if already reached invite limit', async () => { + model.find.returns(exec([{ _id: 'foo' }])); + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Invite limit reached', MessageType.System], + ]); + }); + + it('fails if exceeded reject limit', async () => { + for (let i = 0; i < 5; i++) { + await service.requestInvite(requester, target); + notifications.addNotification.args[i][1].reject!(); + } + + notifications.addNotification.reset(); + + await service.requestInvite(requester, target); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('logs invite', async () => { + requester.character.name = 'Foo'; + + await service.requestInvite(requester, target); + + assert.calledOnce(log); + }); + + it('accepts invite when accept callback is invoked', async () => { + const acceptInvite = stub(service, 'acceptInvite'); + + await service.requestInvite(requester, target); + + const { accept } = notifications.addNotification.args[0][1]; + accept!(); + + assert.calledWith(acceptInvite, requester, target); + }); + + it('rejects invite when reject callback is invoked', async () => { + const rejectInvite = stub(service, 'rejectInvite'); + + await service.requestInvite(requester, target); + + const { reject } = notifications.addNotification.args[0][1]; + reject!(); + + assert.calledWith(rejectInvite, requester, target); + }); + }); + + describe('acceptInvite()', () => { + it('invites user', () => { + const requester = mockClient(); + const target = mockClient(); + const invite = stub(service, 'invite'); + + service.acceptInvite(requester, target); + + assert.calledOnce(invite); + }); + + it('logs accepted invite', () => { + const requester = mockClient(); + const target = mockClient(); + stub(service, 'invite'); + + service.acceptInvite(requester, target); + + assert.calledOnce(log); + }); + }); + + describe('rejectInvite()', () => { + it('logs rejected invite', () => { + const requester = mockClient(); + const target = mockClient(); + + service.rejectInvite(requester, target); + + assert.calledOnce(log); + }); + }); + + describe('invite()', () => { + it('creates new invite', async () => { + const requester = mockClient(); + requester.account.supporter = SupporterFlags.Supporter1; + const target = mockClient(); + model.find.returns(exec([])); + + await service.invite(requester, target); + + assert.calledWithMatch(model.create, { + source: requester.account._id, + target: target.account._id, + name: target.character.name, + info: target.character.info, + active: true, + }); + }); + + it('throws if reached invite limit', async () => { + const requester = mockClient(); + requester.account.supporter = SupporterFlags.Supporter1; + const target = mockClient(); + model.find.returns(exec([{}])); + + await expect(service.invite(requester, target)).rejectedWith(); + + assert.notCalled(model.create); + }); + }); + + describe('uninvite()', () => { + it('removes invite', async () => { + const requester = mockClient(); + model.deleteOne.returns({ exec: stub() } as any); + + await service.uninvite(requester, 'foobar'); + + assert.calledWithMatch(model.deleteOne, { _id: 'foobar', source: requester.account._id }); + }); + }); + + describe('updateSupporterInvites()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('activates inactive items', async () => { + const data = [ + { _id: 'aaa', active: false, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: true, source: { supporter: SupporterFlags.Supporter1 } }, + ]; + model.find.withArgs({}, '_id active') + .returns({ + populate: stub().withArgs('source', '_id supporter patreon roles') + .returns({ lean: stub().returns(exec(data)) }) + } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['aaa'] } }, { active: true }); + }); + + it('deactivates active items', async () => { + const data = [ + { _id: 'aaa', active: true, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: true, source: {} }, + ]; + model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['bbb'] } }, { active: false }); + }); + + it('activates and deactivates items', async () => { + const data = [ + { _id: 'aaa', active: false, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: true, source: {} }, + ]; + model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['aaa'] } }, { active: true }); + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['bbb'] } }, { active: false }); + }); + + it('does nothing if all items have correct active flag', async () => { + const data = [ + { _id: 'aaa', active: true, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: false, source: {} }, + ]; + model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.notCalled(model.updateMany); + }); + + it('removes old inactive entries', async () => { + model.find.returns({ populate: stub().returns({ lean: stub().returns({ exec: stub() }) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + clock.setSystemTime(123 * DAY); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.deleteMany, { active: false, updatedAt: { $lt: new Date(23 * DAY) } }); + }); + }); +}); diff --git a/src/ts/tests/server/spamChecker.spec.ts b/src/ts/tests/server/spamChecker.spec.ts new file mode 100644 index 0000000..90d6689 --- /dev/null +++ b/src/ts/tests/server/spamChecker.spec.ts @@ -0,0 +1,272 @@ +import { createFunctionWithPromiseHandler } from '../lib'; +import { expect } from 'chai'; +import { stub, assert, SinonStub } from 'sinon'; +import { GameServerSettings } from '../../common/adminInterfaces'; +import { CounterService } from '../../server/services/counter'; +import { IClient, OnMessageSettings } from '../../server/serverInterfaces'; +import { fromNow, times as utilsTimes } from '../../common/utils'; +import { randomString } from '../../common/stringUtils'; +import { DAY, SAY_MAX_LENGTH } from '../../common/constants'; +import { + REPORT_AFTER_LIMIT, MUTE_AFTER_LIMIT, SHORT_MESSAGE_MUL, LONG_MESSAGE_MUL, TINY_MESSAGE_MUL, + createSpamChecker, RAPID_MESSAGE_COUNT +} from '../../server/spamChecker'; +import { mockClient } from '../mocks'; +import { SPAM_TIMEOUT } from '../../server/reporting'; + +function times(count: number, action: (i: number) => any) { + return Promise.all(utilsTimes(count, action)); +} + +describe('SpamChecker', () => { + describe('check()', () => { + let client: IClient; + let settings: GameServerSettings; + let spamCounter: CounterService; + let rapidCounter: CounterService; + let countSpamming: SinonStub; + let timeoutAccount: SinonStub; + let spamChecker: OnMessageSettings; + + beforeEach(() => { + client = mockClient(); + client.account.createdAt = fromNow(-2 * DAY); + settings = { + reportSpam: true, + autoBanSpamming: true, + }; + spamCounter = new CounterService(1000); + rapidCounter = new CounterService(1000); + countSpamming = stub().resolves(); + timeoutAccount = stub().resolves(); + spamChecker = createFunctionWithPromiseHandler( + createSpamChecker, spamCounter, rapidCounter, countSpamming, timeoutAccount); + }); + + it('does not count spam for mods', async () => { + client.isMod = true; + + await times(10, () => spamChecker(client, 'long_spam_text', settings)); + + assert.notCalled(countSpamming); + }); + + it('counts spam if reporting is turned off', async () => { + settings.reportSpam = false; + + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledOnce(countSpamming); + }); + + it('does not report if reporting is turned off', async () => { + settings.reportSpam = false; + const warn = stub(client.reporter, 'warn'); + + await times(10, () => spamChecker(client, 'long_spam_text', settings)); + + assert.notCalled(warn); + }); + + it('reports spam', async () => { + const warn = stub(client.reporter, 'warn'); + + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(warn, 'Spam', 'long_spam_text'); + }); + + it(`counts spam after ${REPORT_AFTER_LIMIT} messages`, async () => { + await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, 'long_spam_text', settings)); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`counts spam after ${REPORT_AFTER_LIMIT} commands`, async () => { + await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, '/random 1000', settings)); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, '/random 1000', settings)); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`counts spam after ${REPORT_AFTER_LIMIT * SHORT_MESSAGE_MUL} messages for short messages`, async () => { + await times(REPORT_AFTER_LIMIT * SHORT_MESSAGE_MUL - 1, () => spamChecker(client, 'short', settings)); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, 'short', settings)); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`counts spam after ${REPORT_AFTER_LIMIT * TINY_MESSAGE_MUL} messages for tiny messages`, async () => { + await times(REPORT_AFTER_LIMIT * TINY_MESSAGE_MUL - 1, () => spamChecker(client, 'abc', settings)); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, 'abc', settings)); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`counts spam after ${REPORT_AFTER_LIMIT} messages mixed with other messages`, async () => { + await times(REPORT_AFTER_LIMIT - 1, async () => { + await spamChecker(client, '1long_spam_text1', settings); + await spamChecker(client, '2long_spam_text2', settings); + }); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, '1long_spam_text1', settings)); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`counts spam after ${REPORT_AFTER_LIMIT} message mixed with ${REPORT_AFTER_LIMIT} other messages`, async () => { + await times(REPORT_AFTER_LIMIT, async () => { + await spamChecker(client, '1long_spam_text1', settings); + await spamChecker(client, '2long_spam_text2', settings); + await spamChecker(client, '3long_spam_text3', settings); + await spamChecker(client, '4long_spam_text4', settings); + await spamChecker(client, '5long_spam_text5', settings); + }); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`forgets message after ${REPORT_AFTER_LIMIT} other messages`, async () => { + await spamChecker(client, 'long_spam_text1', settings); + + for (let i = 0; i < REPORT_AFTER_LIMIT; i++) { + await spamChecker(client, i + '2long_spam_text2', settings); + await spamChecker(client, i + '3long_spam_text3', settings); + await spamChecker(client, i + '4long_spam_text4', settings); + await spamChecker(client, i + '5long_spam_text5', settings); + await spamChecker(client, i + '6long_spam_text6', settings); + await spamChecker(client, i + '7long_spam_text7', settings); + } + + for (let i = 2; i < REPORT_AFTER_LIMIT; i++) { + await spamChecker(client, 'long_spam_text1', settings); + } + + assert.notCalled(countSpamming); + }); + + it(`counts spam with timeout if autoBanSpamming option is on and counter is ${MUTE_AFTER_LIMIT}`, async () => { + settings.autoBanSpamming = true; + stub(spamCounter, 'add').returns({ count: MUTE_AFTER_LIMIT, items: ['long_spam_text'], date: 0 }); + + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(countSpamming, client.accountId); + }); + + it('adds entry to spam counter', async () => { + const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); + + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(add, client.accountId, 'long_spam_text', 1); + }); + + it('adds entry to spam counter with increment of 2 for max length message', async () => { + const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); + const message = randomString(SAY_MAX_LENGTH); + + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, message, {})); + + assert.calledWith(add, client.accountId, message, 2); + }); + + it('adds entry to spam counter with increment of 2 for long messages', async () => { + const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); + const message = 'AAAAALGIRNGLRINGLISAHGLEISRHGLISRHGLISRHGLISRHGISRXY'; + + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, message, {})); + + assert.calledWith(add, client.accountId, message, 2); + }); + + const longLimit = Math.ceil(REPORT_AFTER_LIMIT * LONG_MESSAGE_MUL); + + it(`counts spam after ${longLimit} LONG messages`, async () => { + const longestMessage = 'kgfdjhskgdfhgkdlufhgkdfghdfgudhrkughrdkughkdruhgkdurhgkdurhgkudh'; + + await times(longLimit - 1, () => spamChecker(client, longestMessage, {})); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, longestMessage, {})); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`matches spam`, async () => { + await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, `some message`, {})); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, `some message`, {})); + + assert.calledWith(countSpamming, client.accountId); + }); + + it(`matches partial spam `, async () => { + await times(REPORT_AFTER_LIMIT - 1, i => spamChecker(client, `common message part ${i} aaa`, {})); + + assert.notCalled(countSpamming); + + await times(1, () => spamChecker(client, `common message part x aaa`, {})); + + assert.calledWith(countSpamming, client.accountId); + }); + + it('timeouts for spamming', async () => { + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(timeoutAccount, client.accountId); + }); + + it('uses double timeout length is doubleTimeouts setting is set', async () => { + settings.doubleTimeouts = true; + + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(timeoutAccount, client.accountId); + expect(timeoutAccount.args[0][1].getTime()).greaterThan(fromNow(SPAM_TIMEOUT * 1.9).getTime()); + }); + + it('reports timing out', async () => { + const system = stub(client.reporter, 'system'); + + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(system, 'Timed out for spamming'); + }); + + it('logs timing out if reporting is turned off', async () => { + settings.reportSpam = false; + const system = stub(client.reporter, 'system'); + const systemLog = stub(client.reporter, 'systemLog'); + + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + + assert.calledWith(systemLog, 'Timed out for spamming'); + assert.notCalled(system); + }); + + it('counts a lot of rapid messages as spam', async () => { + await times(RAPID_MESSAGE_COUNT + 1, i => spamChecker(client, i + '-' + i, {})); + + assert.calledWith(countSpamming, client.accountId); + }); + }); +}); diff --git a/src/ts/tests/server/utils/byteSize.spec.ts b/src/ts/tests/server/utils/byteSize.spec.ts new file mode 100644 index 0000000..36ba76d --- /dev/null +++ b/src/ts/tests/server/utils/byteSize.spec.ts @@ -0,0 +1,102 @@ +import '../../lib'; +import { expect } from 'chai'; +import { ByteSize } from '../../../server/utils/byteSize'; + +describe('ByteSize ', () => { + it('starts with zeroed fields', () => { + const size = new ByteSize(); + + expect(size.bytes).equal(0); + expect(size.mbytes).equal(0); + }); + + it('can be created with inital values', () => { + const size = new ByteSize(5, 6); + + expect(size.bytes).equal(5); + expect(size.mbytes).equal(6); + }); + + it('reduces overflowing value of bytes when created', () => { + const size = new ByteSize(6 * 1024 * 1024 + 5, 4); + + expect(size.bytes).equal(5); + expect(size.mbytes).equal(10); + }); + + it('adds two values together', () => { + const size = new ByteSize(6, 4); + const x = new ByteSize(6, 4); + + size.add(x); + + expect(size.bytes).equal(12); + expect(size.mbytes).equal(8); + }); + + it('adds value directly', () => { + const size = new ByteSize(6, 4); + + size.addBytes(6, 4); + + expect(size.bytes).equal(12); + expect(size.mbytes).equal(8); + }); + + it('adds value directly with just bytes', () => { + const size = new ByteSize(6, 4); + + size.addBytes(6); + + expect(size.bytes).equal(12); + expect(size.mbytes).equal(4); + }); + + describe('.toString()', () => { + it('get string value for only bytes', () => { + const size = new ByteSize(6); + + expect(size.toString()).equal('6'); + }); + + it('get string value for bytes and megabytes', () => { + const size = new ByteSize(6, 5); + + expect(size.toString()).equal('5000006'); + }); + }); + + describe('.toSortableString()', () => { + it('get padded string value for only bytes', () => { + const size = new ByteSize(6); + + expect(size.toSortableString()).equal('000000000-000006'); + }); + + it('get padded string value for bytes and megabytes', () => { + const size = new ByteSize(6, 5); + + expect(size.toSortableString()).equal('000000005-000006'); + }); + }); + + describe('.toHumanReadable()', () => { + it('get value in b for values below 2 kb', () => { + const size = new ByteSize(6); + + expect(size.toHumanReadable()).equal('6 b'); + }); + + it('get value in kb for values above 2 kb', () => { + const size = new ByteSize(6 * 1024); + + expect(size.toHumanReadable()).equal('6 kb'); + }); + + it('get value in mb for values above 1 mb', () => { + const size = new ByteSize(5, 2); + + expect(size.toHumanReadable()).equal('2 mb'); + }); + }); +}); diff --git a/src/ts/tests/server/utils/taskQueue.spec.ts b/src/ts/tests/server/utils/taskQueue.spec.ts new file mode 100644 index 0000000..295911d --- /dev/null +++ b/src/ts/tests/server/utils/taskQueue.spec.ts @@ -0,0 +1,119 @@ +import '../../lib'; +import { expect } from 'chai'; +import { stub, assert, SinonFakeTimers, useFakeTimers } from 'sinon'; +import { taskQueue, TaskQueue, makeQueued } from '../../../server/utils/taskQueue'; + +describe('taskQueue', () => { + let queue: TaskQueue; + let clock: SinonFakeTimers; + + beforeEach(() => { + queue = taskQueue(); + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + clock = undefined as any; + queue = undefined as any; + }); + + it('waits on empty queue', async () => { + await queue.wait(); + }); + + it('executes given action', async () => { + const action = stub(); + + queue.push(action); + await queue.wait(); + + assert.calledOnce(action); + }); + + it('executes two actions', async () => { + const action1 = stub(); + const action2 = stub(); + + queue.push(action1); + queue.push(action2); + await queue.wait(); + + assert.calledOnce(action1); + assert.calledOnce(action2); + }); + + it('executes actions in order', async () => { + const order = [0]; + const action1 = stub().callsFake(() => order.push(1)); + const action2 = stub().callsFake(() => order.push(2)); + + queue.push(action1); + queue.push(action2); + await queue.wait(); + + expect(order).eql([0, 1, 2]); + }); + + it('executes two actions with wait', async () => { + const action1 = stub(); + const action2 = stub(); + + queue.push(action1); + await queue.wait(); + queue.push(action2); + await queue.wait(); + + assert.calledOnce(action1); + assert.calledOnce(action2); + }); + + it('returns promise that resolves after passed action', async () => { + const action = stub(); + + const promise = queue.push(action); + assert.notCalled(action); + await promise.then(() => assert.calledOnce(action)); + }); + + it('returns promise that resolves to value returned from action', async () => { + const action = stub().returns('foo'); + + await expect(queue.push(action)).eventually.equal('foo'); + }); + + it('returns promise that resolves to value resolved from action', async () => { + const action = stub().resolves('foo'); + + await expect(queue.push(action)).eventually.equal('foo'); + }); + + it('returns promise that rejects to error thrown from action', async () => { + const action = stub().throws(new Error('test')); + + await expect(queue.push(action)).rejectedWith('test'); + }); + + it('returns promise that rejects to error rejected from action', async () => { + const action = stub().rejects(new Error('test')); + + await expect(queue.push(action)).rejectedWith('test'); + }); + + it('returns promise that rejects to error rejected from action', async () => { + const action = stub().rejects(new Error('test')); + + await expect(queue.push(action)).rejectedWith('test'); + }); + + describe('makeQueued()', () => { + it('creates queued function', async () => { + const action = stub(); + const queued = makeQueued(action); + + await queued(); + + assert.calledOnce(action); + }); + }); +}); diff --git a/src/ts/tests/server/world.spec.ts b/src/ts/tests/server/world.spec.ts new file mode 100644 index 0000000..d26294c --- /dev/null +++ b/src/ts/tests/server/world.spec.ts @@ -0,0 +1,347 @@ +import { stubClass, resetStubMethods } from '../lib'; +import { Subject } from 'rxjs'; +import { expect } from 'chai'; +import { stub, assert, SinonFakeTimers, useFakeTimers, SinonStub } from 'sinon'; +import { World } from '../../server/world'; +import { mockClient, serverEntity } from '../mocks'; +import { PartyService } from '../../server/services/party'; +import { NotificationService } from '../../server/services/notification'; +import { IClient, ServerMap } from '../../server/serverInterfaces'; +import { NotificationFlags, WorldState, MapState, EntityFlags, UpdateFlags } from '../../common/interfaces'; +import { addEntityToRegion, createServerRegion, pushUpdateEntityToRegion } from '../../server/serverRegion'; +import { ServerLiveSettings } from '../../common/adminInterfaces'; +import { createServerMap } from '../../server/serverMap'; +import { FriendsService } from '../../server/services/friends'; +import { HidingService } from '../../server/services/hiding'; + +describe('World', () => { + const partyService = stubClass(PartyService); + const friendsService = stubClass(FriendsService); + const hidingService = stubClass(HidingService); + partyService.partyChanged = new Subject(); + const notifications = stubClass(NotificationService); + let liveSettings: ServerLiveSettings; + let getSettings: SinonStub; + let world: World; + let map: ServerMap; + let client: IClient; + + beforeEach(() => { + resetStubMethods(partyService, 'leave'); + resetStubMethods(notifications, 'rejectAll'); + liveSettings = {} as any; + getSettings = stub().returns({}); + client = mockClient(); + world = new World( + { flags: {} } as any, partyService as any, friendsService as any, hidingService as any, + notifications, getSettings, liveSettings, { stats() { return {}; } } as any); + world.maps.push(map = createServerMap('', 0, 1, 1)); + client.map = world.getMainMap(); + }); + + describe('joinClientToWorld()', () => { + let client: IClient; + + beforeEach(() => { + client = mockClient(); + client.accountId = 'foobar'; + client.map = world.getMainMap(); + client.characterState = {} as any; + }); + + it('sends world state', () => { + const worldState = stub(client, 'worldState'); + const state: WorldState = {} as any; + stub(world, 'getState').returns(state); + + world.joinClientToWorld(client); + + assert.calledWith(worldState, state, true); + }); + + it('sends map info', () => { + const mapState = stub(client, 'mapState'); + const state: MapState = {} as any; + map.state = state; + + world.joinClientToWorld(client); + + assert.calledWith(mapState, { type: 0, editableArea: undefined, flags: 0, defaultTile: 0, regionsX: 1, regionsY: 1 }, state); + }); + + it('adds client to clients list', () => { + world.joinClientToWorld(client); + + expect(world.clients).contain(client); + }); + + it('initializes client pony ID', () => { + stub(world, 'getNewEntityId').returns(1234); + + world.joinClientToWorld(client); + + expect(client.pony.id).equal(1234); + }); + + it('sends client pony ID', () => { + stub(world, 'getNewEntityId').returns(1234); + const myEntity = stub(client, 'myEntity'); + client.characterName = 'charname'; + client.character.info = client.pony.info = 'INFO'; + client.pony.crc = 456; + + world.joinClientToWorld(client); + + assert.calledWith(myEntity, 1234, 'charname', 'INFO', client.characterId, 456); + }); + + it('adds client pony to the world', () => { + const addEntity = stub(world, 'addEntity'); + + world.joinClientToWorld(client); + + assert.calledWith(addEntity, client.pony, client.map); + }); + + it('updates selection for other clients', () => { + const otherClient = mockClient(); + otherClient.selected = { id: 123, client: { accountId: 'foobar' } } as any; + const otherClient2 = mockClient(); + const updateSelection = stub(otherClient, 'updateSelection'); + const updateSelection2 = stub(otherClient2, 'updateSelection'); + stub(world, 'getNewEntityId').returns(321); + world.clients.push(otherClient); + world.clients.push(otherClient2); + + world.joinClientToWorld(client); + + assert.calledWith(updateSelection, 123, 321); + assert.notCalled(updateSelection2); + }); + + it('adds update notification to client if updating', () => { + liveSettings.updating = true; + + world.joinClientToWorld(client); + + assert.calledWith(notifications.addNotification, client, { + id: 0, name: '', message: 'Server will restart shortly for updates and maintenance', flags: NotificationFlags.Ok + }); + }); + }); + + describe('initialize()', () => { + it('initializes all controllers', () => { + const initialize = stub(); + world.controllers.push({ initialize, update() { } }); + + world.initialize(123); + + assert.calledWith(initialize, 0.123); + }); + }); + + describe('update()', () => { + it('updates entities positions', () => { + const e = serverEntity(1, 1, 1, 0); + e.flags |= EntityFlags.Movable; + e.vx = 1; + e.vy = 1; + e.timestamp = 1; + addEntityToRegion(map.regions[0], e, map); + + world.update(1000, 2000); + + expect(e.x).equal(2); + expect(e.y).equal(2); + }); + + it('does not update entity position if timestamp is in the future', () => { + const e = serverEntity(1, 1, 1, 0); + e.vx = 1; + e.vy = 1; + addEntityToRegion(map.regions[0], e, map); + + world.update(1000, 2000); + + expect(e.x).equal(1); + expect(e.y).equal(1); + }); + + // it('updates regions', () => { + // world.update(123, 123); + + // ... + // }); + + it('updates all controllers', () => { + const update = stub(); + world.controllers.push({ update, initialize() { } }); + + world.update(123, 123000); + + assert.calledWith(update, 0.123); + }); + + // timeouts expressions + + it('commits region updates', () => { + const region = createServerRegion(0, 0); + const entity = serverEntity(1); + const client = mockClient(); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position }); + region.clients.push(client); + client.regions.push(region); + map.regions = [region]; + const update = stub(client, 'update'); + + world.update(123, 123000); + + expect(update); + }); + + it('joins queued clients', () => { + const client = mockClient(); + world.joinClientToQueue(client); + const joinClientToWorld = stub(world, 'joinClientToWorld'); + + world.update(123, 123000); + + assert.calledWith(joinClientToWorld, client); + }); + }); + + describe('kick()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.tick(99999); + clock.restore(); + }); + + it('does nothing for undefined', () => { + expect(world.kick(undefined)).false; + }); + + it('returns true if kicked', () => { + const client = mockClient(); + + expect(world.kick(client)).true; + }); + + it('rejects all notifications', () => { + const client = mockClient(); + + world.kick(client); + + assert.calledWith(notifications.rejectAll, client); + }); + + it('notifies client of leaving', () => { + const client = mockClient(); + const left = stub(client, 'left'); + + world.kick(client); + + assert.calledOnce(left); + }); + + it('sets client leave reason', () => { + const client = mockClient(); + + world.kick(client); + + expect(client.leaveReason).equal('kicked'); + }); + + it('disconnects client after timeout', () => { + const client = mockClient({ isConnected: true }); + const disconnect = stub(client, 'disconnect'); + + world.kick(client); + + clock.tick(201); + assert.calledOnce(disconnect); + }); + + it('skips disconnecting if client already disconnected', () => { + const client = mockClient({ isConnected: false }); + const disconnect = stub(client, 'disconnect'); + + world.kick(client); + + clock.tick(201); + assert.notCalled(disconnect); + }); + }); + + describe('kickAll()', () => { + it('does nothing for no clients', () => { + world.kickAll(); + }); + + it('kicks all clients', () => { + const a = mockClient(); + const b = mockClient(); + world.clients.push(a); + world.clients.push(b); + const kick = stub(world, 'kick'); + + world.kickAll(); + + assert.calledWith(kick, a); + assert.calledWith(kick, b); + }); + + it('works while removing clients', () => { + const a = mockClient(); + const b = mockClient(); + world.clients.push(a); + world.clients.push(b); + const aLeft = stub(a, 'left'); + const bLeft = stub(b, 'left'); + + world.kickAll(); + + assert.calledOnce(aLeft); + assert.calledOnce(bLeft); + }); + }); + + describe('kickByAccount()', () => { + it('does nothing if not found', () => { + world.kickByAccount('foo'); + }); + + it('kick client by account', () => { + const client = mockClient(); + world.clients.push(client); + world.clientsByAccount.set(client.accountId, client); + const kick = stub(world, 'kick'); + + world.kickByAccount(client.accountId); + + assert.calledWith(kick, client); + }); + }); + + describe('kickByCharacter()', () => { + it('does nothing if not found', () => { + world.kickByCharacter('foo'); + }); + + it('kick client by account', () => { + const client = mockClient(); + world.clients.push(client); + const kick = stub(world, 'kick'); + + world.kickByCharacter(client.characterId); + + assert.calledWith(kick, client); + }); + }); +}); diff --git a/src/ts/tests/services/errorReporter.spec.ts b/src/ts/tests/services/errorReporter.spec.ts new file mode 100644 index 0000000..78f390d --- /dev/null +++ b/src/ts/tests/services/errorReporter.spec.ts @@ -0,0 +1,64 @@ +import '../lib'; +import { SinonStub, stub, assert } from 'sinon'; +import { ClientErrorHandler, ClientOptions } from 'ag-sockets'; +import { ErrorReporter } from '../../components/services/errorReporter'; +import { times } from '../../common/utils'; + +describe('ErrorReporter', () => { + describe('createClientErrorHandler()', () => { + let socketOptions: ClientOptions; + let errorReporter: ErrorReporter; + let clientErrorHandler: ClientErrorHandler; + let reportError: SinonStub; + + beforeEach(() => { + socketOptions = {} as any; + errorReporter = new ErrorReporter(); + clientErrorHandler = errorReporter.createClientErrorHandler(socketOptions); + reportError = stub(errorReporter, 'reportError'); + }); + + it('reports error without data', () => { + const error = new Error('foo'); + + clientErrorHandler.handleRecvError(error, undefined as any); + + assert.calledWithMatch(reportError, error, { data: undefined, method: undefined }); + }); + + it('sends stringified data and method from socket options', () => { + const error = new Error('foo'); + socketOptions.client = ['bar']; + + clientErrorHandler.handleRecvError(error, new Uint8Array([0, 1, 2])); + + assert.calledWithMatch(reportError, error, { data: '<0,1,2>', method: 'bar' }); + }); + + it('handles 0 length buffer', () => { + const error = new Error('foo'); + socketOptions.client = ['bar']; + + clientErrorHandler.handleRecvError(error, new Uint8Array(0)); + + assert.calledWithMatch(reportError, error, { data: '<>', method: undefined }); + }); + + it('trims buffer value after 200 elements', () => { + const error = new Error('foo'); + socketOptions.client = [['bar', {}]]; + + clientErrorHandler.handleRecvError(error, new Uint8Array(300)); + + assert.calledWithMatch(reportError, error, { data: `<${times(200, () => '0').join(',')}...>`, method: 'bar' }); + }); + + it('does nothing if error has no message', () => { + const error = new Error(''); + + clientErrorHandler.handleRecvError(error, undefined as any); + + assert.notCalled(reportError); + }); + }); +}); diff --git a/src/ts/tests/services/gameService.spec.ts b/src/ts/tests/services/gameService.spec.ts new file mode 100644 index 0000000..b2132fd --- /dev/null +++ b/src/ts/tests/services/gameService.spec.ts @@ -0,0 +1,69 @@ +import { stubClass, stubFromInstance, resetStubMethods } from '../lib'; +import { ErrorHandler } from '@angular/core'; +import { stub, assert } from 'sinon'; +import { GameService } from '../../components/services/gameService'; +import { Model } from '../../components/services/model'; +import { PonyTownGame } from '../../client/game'; +import { ErrorReporter } from '../../components/services/errorReporter'; +import { StorageService } from '../../components/services/storageService'; + +describe('GameService', () => { + let model = stubClass(Model); + let game = stubClass(PonyTownGame); + let errorHandler = stubClass(ErrorHandler); + let errorReporter = stubClass(ErrorReporter); + let storage = stubClass(StorageService); + let window = stubFromInstance({ addEventListener() { } }); + let gameService: GameService; + // let connectSocket: SinonStub; + // let startLoop: SinonStub; + + beforeEach(() => { + resetStubMethods(model); + resetStubMethods(game, 'leave'); + resetStubMethods(errorHandler); + resetStubMethods(errorReporter); + resetStubMethods(window); + resetStubMethods(storage); + const zone = { + run: (f: () => void) => f(), + runOutsideAngular: (f: () => void) => f(), + }; + gameService = new GameService( + model as any, game as any, zone as any, errorHandler as any, errorReporter as any, storage as any); + + (global as any).WebSocket = {}; + }); + + afterEach(() => { + delete (global as any).WebSocket; + }); + + // describe('join()', () => { + // it('joins to the game', async () => { + // model.join.resolves({ token: 'token' }); + // startLoop.returns({}); + // gameService.server = { id: 'serverid' } as any; + + // await gameService.join('ponyid'); + + // assert.calledWith(model.join, 'serverid', 'ponyid'); + // }); + // }); + + describe('leave()', () => { + // it('notifies game of leaving', () => { + // gameService.leave('test'); + + // assert.calledOnce(game.leave); + // }); + + it('calls left()', () => { + const left = stub(gameService, 'left'); + + gameService.leave('test'); + + assert.calledOnce(left); + }); + }); +}); diff --git a/src/ts/tests/services/liveCollection.spec.ts b/src/ts/tests/services/liveCollection.spec.ts new file mode 100644 index 0000000..09d7ddf --- /dev/null +++ b/src/ts/tests/services/liveCollection.spec.ts @@ -0,0 +1,462 @@ +import '../lib'; +import { SocketService } from 'ag-sockets'; +import { expect } from 'chai'; +import { stub, assert, SinonFakeTimers, useFakeTimers, SinonStub } from 'sinon'; +import { LiveCollection, Options } from '../../components/services/liveCollection'; +import { ClientAdminActions } from '../../client/clientAdminActions'; +import { IAdminServerActions, LiveResponse } from '../../common/adminInterfaces'; + +interface Thing { + _id: string; + name: string; + updatedAt: Date; + deleted?: boolean; +} + +describe('LiveCollection', () => { + let socket: SocketService; + let collection: LiveCollection; + let clock: SinonFakeTimers; + let options: Options; + let logError: SinonStub; + + beforeEach(() => { + socket = { + server: { + getAll() { }, + removeItem() { }, + assignAccount() { }, + }, + } as any; + + options = { + decode: x => ({ _id: x[0], name: x[1], updatedAt: new Date(x[2]) }), + }; + + logError = stub(); + collection = new LiveCollection('events', 2000, i => i._id, options, socket, '2010-01-01T10:00:00.000Z', logError); + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + after(() => { + socket = undefined as any; + collection = undefined as any; + clock = undefined as any; + options = undefined as any; + logError = undefined as any; + }); + + describe('.push()', () => { + it('adds item to collection', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + + collection.push(item); + + expect(collection.items).contain(item); + expect(collection.get('foo')).equal(item); + }); + }); + + describe('.remove()', () => { + it('sends remove request to server', () => { + const removeItem = stub(socket.server, 'removeItem').returns(Promise.resolve()); + + return collection.remove('foo') + .then(() => assert.calledWith(removeItem, 'events', 'foo')); + }); + + it('removes item from list', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + stub(socket.server, 'removeItem').returns(Promise.resolve()); + options.deleteItems = true; + collection.push(item); + + return collection.remove('foo') + .then(() => { + expect(collection.items).not.contain(item); + expect(collection.get('foo')).undefined; + }); + }); + }); + + describe('.removeItem()', () => { + it('does not send remove request to server', () => { + const removeItem = stub(socket.server, 'removeItem').returns(Promise.resolve()); + + collection.removeItem('foo'); + + assert.notCalled(removeItem); + }); + + it('removes item from list', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = true; + collection.push(item); + + collection.removeItem('foo'); + + expect(collection.items).not.contain(item); + expect(collection.get('foo')).undefined; + }); + + it('does not remove item is deleteItems flag is set to false', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = false; + collection.push(item); + + collection.removeItem('foo', true); + + expect(collection.items).contain(item); + expect(item.deleted).true; + }); + + it('does nothing deleteItems flag is set to false and deleted flag is false', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = false; + collection.push(item); + + collection.removeItem('foo'); + + expect(collection.items).contain(item); + expect(item.deleted).not.true; + }); + }); + + describe('.assignAccount()', () => { + it('send assign account request to server', () => { + const assignAccount = stub(socket.server, 'assignAccount').returns(Promise.resolve()); + + return collection.assignAccount('foo', 'bar') + .then(() => assert.calledWith(assignAccount, 'events', 'foo', 'bar')); + }); + }); + + describe('.live()', () => { + it('returns promise if running', () => { + return collection.live(); + }); + + it('schedules next update', () => { + return collection.live() + .then(() => { + const live = stub(collection, 'live'); + clock.tick(2100); + assert.calledOnce(live); + }); + }); + + it('schedules next update fast if more flag is true', () => { + stub(socket.server, 'getAll').resolves({} as any); + stub(collection, 'read').returns(true); + socket.isConnected = true; + + return collection.live() + .then(() => { + const live = stub(collection, 'live'); + clock.tick(200); + assert.calledOnce(live); + }); + }); + + it('polls server for updates if connected', () => { + const data = {}; + stub(socket.server, 'getAll').withArgs('events', '2010-01-01T10:00:00.000Z').resolves(data as any); + const read = stub(collection, 'read'); + socket.isConnected = true; + + return collection.live() + .then(() => assert.calledWith(read, data as any)); + }); + + it('does not poll the server if not connected', () => { + const getAll = stub(socket.server, 'getAll'); + stub(collection, 'read'); + socket.isConnected = false; + + return collection.live() + .then(() => assert.notCalled(getAll)); + }); + + it('does not poll the server if stopped', async () => { + const getAll = stub(socket.server, 'getAll'); + stub(collection, 'read'); + socket.isConnected = true; + + collection.stop(); + + await collection.live(); + assert.notCalled(getAll); + }); + + it('does not reject for server error', async () => { + stub(socket.server, 'getAll').rejects(new Error('test')); + socket.isConnected = true; + + await collection.live(); + }); + }); + + describe('.read()', () => { + const base: LiveResponse = { + updates: [], + deletes: [], + base: {}, + more: false, + }; + + it('returns more flag', () => { + expect(collection.read({ ...base, more: false })).false; + expect(collection.read({ ...base, more: true })).true; + }); + + it('updates timestamp', async () => { + const getAll = stub(socket.server, 'getAll').resolves({} as any); + socket.isConnected = true; + + collection.read({ + ...base, + updates: [ + ['bar', 'Bar', '2010-01-03T10:00:00.000Z'], + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }); + + stub(collection, 'read'); + await collection.live(); + assert.calledWith(getAll, 'events', '2010-01-03T10:00:00.000Z'); + }); + + it('does not update timestamp if not live fetch', async () => { + const getAll = stub(socket.server, 'getAll').resolves({} as any); + socket.isConnected = true; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }, false); + + stub(collection, 'read'); + await collection.live(); + assert.calledWith(getAll, 'events', '2010-01-01T10:00:00.000Z'); + }); + + it('adds new items', () => { + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }); + + expect(collection.items[0]).eql({ + _id: 'foo', + name: 'Foo', + updatedAt: new Date('2010-01-02T10:00:00.000Z'), + }); + }); + + it('does not add new items if ignored', () => { + options.ignore = () => true; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }); + + expect(collection.items).empty; + }); + + it('adds items anyway if ignored but not live fetch', () => { + options.ignore = () => true; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }, false); + + expect(collection.items[0]).eql({ + _id: 'foo', + name: 'Foo', + updatedAt: new Date('2010-01-02T10:00:00.000Z'), + }); + }); + + it('updates existing items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + collection.push(item); + + collection.read({ + ...base, + updates: [ + ['foo', 'Bar', '2010-01-03T10:00:00.000Z'], + ], + }); + + expect(item).eql({ + _id: 'foo', + name: 'Bar', + updatedAt: new Date('2010-01-03T10:00:00.000Z'), + }); + }); + + it('removes deleted items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = true; + collection.push(item); + + collection.read({ + ...base, + deletes: ['foo'], + }); + + expect(collection.items).not.contain(item); + }); + + it('sets deleted flag on items if deleteItems is false', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = false; + collection.push(item); + + collection.read({ + ...base, + deletes: ['foo'], + }); + + expect(collection.items).contain(item); + expect(item.deleted).true; + }); + + it('calls beforeUpdate hook with items', () => { + const beforeUpdate = stub(); + options.beforeUpdate = beforeUpdate; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-03T10:00:00.000Z'], + ], + }); + + assert.calledWithMatch(beforeUpdate, [ + { _id: 'foo', name: 'Foo', updatedAt: new Date('2010-01-03T10:00:00.000Z') }, + ]); + }); + + it('calls onUpdated hook with added and all items', () => { + const onUpdated = stub(); + options.onUpdated = onUpdated; + collection.push({ _id: 'foo', name: 'Foo', updatedAt: new Date() }); + + collection.read({ + ...base, + updates: [ + ['bar', 'Bar', '2010-01-04T10:00:00.000Z'], + ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], + ], + }); + + expect(onUpdated.args[0][0]).eql([ + { _id: 'bar', name: 'Bar', updatedAt: new Date('2010-01-04T10:00:00.000Z') }, + ], 'added'); + + expect(onUpdated.args[0][1]).eql([ + { _id: 'bar', name: 'Bar', updatedAt: new Date('2010-01-04T10:00:00.000Z') }, + { _id: 'foo', name: 'Foo2', updatedAt: new Date('2010-01-03T10:00:00.000Z') }, + ], 'all'); + }); + + it('does not call onUpdated hook for no items', () => { + const onUpdated = stub(); + options.onUpdated = onUpdated; + + collection.read({ + ...base, + updates: [], + }); + + assert.notCalled(onUpdated); + }); + + it('calls onDelete hook for deleted items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + const onDelete = stub(); + options.onDelete = onDelete; + collection.push(item); + + collection.read({ + ...base, + deletes: ['foo'], + }); + + assert.calledWith(onDelete, item); + }); + + it('calls onFinished hook when more flag is false', () => { + const onFinished = stub(); + options.onFinished = onFinished; + + collection.read({ ...base, more: false }); + + assert.calledOnce(onFinished); + }); + + it('calls onFinished hook only once', () => { + const onFinished = stub(); + options.onFinished = onFinished; + + collection.read({ ...base, more: false }); + collection.read({ ...base, more: false }); + + assert.calledOnce(onFinished); + }); + + it('does not call onFinished hook for non live fetch calls', () => { + const onFinished = stub(); + options.onFinished = onFinished; + + collection.read({ ...base, more: false }, false); + + assert.notCalled(onFinished); + }); + + it('calls onUpdate hook for updated items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + const onUpdate = stub(); + options.onUpdate = onUpdate; + collection.push(item); + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], + ], + }); + + assert.calledWith(onUpdate, item); + }); + + it('does not call onUpdate hook for added items', () => { + const onUpdate = stub(); + options.onUpdate = onUpdate; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], + ], + }); + + assert.notCalled(onUpdate); + }); + }); +}); diff --git a/src/ts/tests/services/liveList.spec.ts b/src/ts/tests/services/liveList.spec.ts new file mode 100644 index 0000000..5f432d1 --- /dev/null +++ b/src/ts/tests/services/liveList.spec.ts @@ -0,0 +1,463 @@ +import '../lib'; +import { expect } from 'chai'; +import { stub, SinonStub, assert, SinonFakeTimers, useFakeTimers } from 'sinon'; +import { LiveList, LiveListConfig } from '../../server/services/liveList'; + +describe('LiveList', () => { + let model: { + find: SinonStub, + findById: SinonStub; + update: SinonStub; + deleteOne: SinonStub; + }; + let liveList: LiveList; + let config: LiveListConfig; + let logger: any; + + beforeEach(() => { + model = { + find: stub(), + findById: stub(), + update: stub(), + deleteOne: stub(), + }; + config = { fields: ['foo', 'bar'], clean: item => item }; + logger = {}; + liveList = new LiveList(model as any, config, undefined, logger); + }); + + it('returns loaded state', () => { + (liveList as any).finished = true; + expect(liveList.loaded).true; + (liveList as any).finished = false; + expect(liveList.loaded).false; + }); + + describe('for()', () => { + it('calls callback for item', () => { + const callback = stub(); + const item = { _id: 'foo' }; + liveList.add(item); + + liveList.for('foo', callback); + + assert.calledWith(callback, item); + }); + + it('does nothing if item is not found', () => { + const callback = stub(); + + liveList.for('foo', callback); + + assert.notCalled(callback); + }); + + it('does nothing for undefined id', () => { + const callback = stub(); + + liveList.for(undefined, callback); + + assert.notCalled(callback); + }); + }); + + describe('add()', () => { + it('adds item to items', () => { + const item = { _id: 'foo' }; + + liveList.add(item); + + expect(liveList.items).contain(item); + }); + + it('adds item to item map', () => { + const item = { _id: 'foo' }; + + liveList.add(item); + + expect(liveList.get('foo')).equal(item); + }); + + it('triggers event listeners', () => { + const item = { _id: 'foo' }; + const listener = stub(); + liveList.subscribe('foo', listener); + + liveList.add(item); + + assert.calledWith(listener, 'foo', item); + }); + + it('returns item', () => { + const item = { _id: 'foo' }; + + expect(liveList.add(item)).equal(item); + }); + }); + + describe('remove()', () => { + it('removes item from database', async () => { + model.deleteOne.returns({ exec: stub().resolves() }); + + await liveList.remove('foo'); + + assert.calledWithMatch(model.deleteOne, { _id: 'foo' }); + }); + + it('notifies removal of item', async () => { + model.deleteOne.returns({ exec: stub().resolves() }); + const removed = stub(liveList, 'removed'); + + await liveList.remove('foo'); + + assert.calledWith(removed, 'foo'); + }); + }); + + describe('removed()', () => { + it('removed item from items', () => { + const item = { _id: 'foo' }; + liveList.add(item); + + liveList.removed('foo'); + + expect(liveList.items).not.contain(item); + }); + + it('removed item from items map', () => { + const item = { _id: 'foo' }; + liveList.add(item); + + liveList.removed('foo'); + + expect(liveList.get('foo')).undefined; + }); + + it('triggers event listeners', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const trigger = stub(liveList, 'trigger'); + + liveList.removed('foo'); + + assert.calledWith(trigger, 'foo', undefined); + }); + + it('calls onDelete event handler', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const onDelete = stub(); + config.onDelete = onDelete; + + liveList.removed('foo'); + + assert.calledWith(onDelete, item); + }); + + it('does nothing if item is not found', () => { + const trigger = stub(liveList, 'trigger'); + + liveList.removed('foo'); + + assert.notCalled(trigger); + }); + }); + + describe('trigger() / subscribe()', () => { + it('calls all listeners', () => { + const item = { _id: 'foo' }; + const listener1 = stub(); + const listener2 = stub(); + liveList.subscribe('foo', listener1); + liveList.subscribe('foo', listener2); + + liveList.trigger('foo', item); + + assert.calledWith(listener1, 'foo', item); + assert.calledWith(listener2, 'foo', item); + }); + + it('does nothing for no listeners', () => { + const item = { _id: 'foo' }; + + liveList.trigger('foo', item); + }); + + it('lets unsubscribe listeners', () => { + const item = { _id: 'foo' }; + const listener1 = stub(); + const listener2 = stub(); + const subscription = liveList.subscribe('foo', listener1); + liveList.subscribe('foo', listener2); + + subscription.unsubscribe(); + liveList.trigger('foo', item); + + assert.notCalled(listener1); + assert.calledWith(listener2, 'foo', item); + }); + + it('does nothing if unsubscribed twice', () => { + const item = { _id: 'foo' }; + const listener = stub(); + + const subscription = liveList.subscribe('foo', listener); + subscription.unsubscribe(); + subscription.unsubscribe(); + liveList.trigger('foo', item); + + assert.notCalled(listener); + }); + + it('cleans document before sending', () => { + const item = { _id: 'foo' }; + const listener = stub(); + liveList.subscribe('foo', listener); + config.clean = item => ({ ...item, bar: 5 }); + + liveList.trigger('foo', item); + + assert.calledWithMatch(listener, 'foo', { _id: 'foo', bar: 5 }); + }); + + it('skips cleanig if document is undefined', () => { + const listener = stub(); + liveList.subscribe('foo', listener); + config.clean = item => item.error; + + liveList.trigger('foo', undefined); + + assert.calledWithMatch(listener, 'foo', undefined); + }); + + it('calls listener on subscribe if found document', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const listener = stub(); + + liveList.subscribe('foo', listener); + + assert.calledWith(listener, 'foo', item); + }); + + it('cleans document when sending it on subscribe', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const listener = stub(); + config.clean = item => ({ ...item, bar: 5 }); + + liveList.subscribe('foo', listener); + + assert.calledWithMatch(listener, 'foo', { _id: 'foo', bar: 5 }); + }); + }); + + describe('start()', () => { + it('starts first tick', () => { + const tick = stub(liveList, 'tick'); + + liveList.start(); + + assert.calledOnce(tick); + }); + }); + + describe('tick()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('does nothing if not running', async () => { + const update = stub(liveList, 'update').resolves(); + + await liveList.tick(); + + assert.notCalled(update); + }); + + it('calls update', async () => { + const update = stub(liveList, 'update').resolves(); + (liveList as any).running = true; + + await liveList.tick(); + + assert.calledOnce(update); + }); + + it('schedules next tick with new timestamp', async () => { + const update = stub(liveList, 'update').resolves(); + (liveList as any).running = true; + + await liveList.tick(); + clock.tick(2000); + + assert.calledTwice(update); + }); + + it('stop() prevents further ticks', async () => { + const update = stub(liveList, 'update').resolves(); + (liveList as any).running = true; + + await liveList.tick(); + + liveList.stop(); + + clock.tick(2000); + + assert.calledOnce(update); + }); + }); + + describe('update()', () => { + function createQuery(items: T[]) { + return { + cursor() { + return { + on(event: string, callback: any) { + if (event === 'data') { + items.forEach(callback); + } else if (event === 'end') { + callback(); + } + return this; + } + }; + } + }; + } + + function setupFind(items: T[]) { + model.find.returns({ lean: stub().returns(createQuery(items)) }); + } + + it('queried database with current timestamp', async () => { + const timestamp = new Date(12345); + (liveList as any).timestamp = timestamp; + setupFind([]); + + await liveList.update(); + + assert.calledWithMatch(model.find, { updatedAt: { $gt: timestamp } }, 'foo bar'); + }); + + it('adds new item', async () => { + const item = { _id: 'foo' }; + setupFind([item]); + + await liveList.update(); + + expect(liveList.items).contain(item); + }); + + it('calls onAddedOrUpdated event handler', async () => { + const onAddedOrUpdated = stub(); + config.onAddedOrUpdated = onAddedOrUpdated; + setupFind([{ _id: 'foo' }]); + + await liveList.update(); + + assert.calledOnce(onAddedOrUpdated); + }); + + it('does not call onAddedOrUpdated event handler if did not fetch any items', async () => { + const onAddedOrUpdated = stub(); + config.onAddedOrUpdated = onAddedOrUpdated; + setupFind([]); + + await liveList.update(); + + assert.notCalled(onAddedOrUpdated); + }); + + it('calls onAdd event when adding', () => { + const item = { _id: 'foo' }; + const onAdd = stub(); + config.onAdd = onAdd; + setupFind([item]); + + liveList.update(); + + assert.calledWith(onAdd, item); + }); + + it('updates existing item', () => { + const item = { _id: 'foo', value: 1 }; + liveList.add(item); + setupFind([{ _id: 'foo', value: 2 }]); + + liveList.update(); + + expect(item.value).equal(2); + }); + + it('fixes documents before updating', () => { + const item = { _id: 'foo', value: 1 }; + config.fix = item => item.field = 'bar'; + liveList.add(item); + setupFind([{ _id: 'foo', value: 2 }]); + + liveList.update(); + + expect(item).eql({ _id: 'foo', value: 2, field: 'bar' }); + }); + + it('triggers listeners updates existing item', () => { + const trigger = stub(liveList, 'trigger'); + const item = { _id: 'foo', value: 1 }; + liveList.add(item); + setupFind([{ _id: 'foo', value: 2 }]); + + liveList.update(); + + assert.calledWith(trigger, 'foo', item); + }); + + it('calls onUpdate event when updating', () => { + const item = { _id: 'foo', value: 1 }; + const update = { _id: 'foo', value: 2 }; + const onUpdate = stub(); + config.onUpdate = onUpdate; + liveList.add(item); + setupFind([update]); + + liveList.update(); + + assert.calledWith(onUpdate, item, update); + }); + + it('sets loaded to true ', async () => { + setupFind([]); + + await liveList.update(); + + expect(liveList.loaded).true; + }); + + it('calls onFinished event handler', async () => { + const onFinished = stub(); + config.onFinished = onFinished; + setupFind([]); + + await liveList.update(); + + assert.calledOnce(onFinished); + }); + + it('calls onFinished event handler only on first finish', async () => { + const onFinished = stub(); + config.onFinished = onFinished; + setupFind([]); + + await liveList.update(); + await liveList.update(); + + assert.calledOnce(onFinished); + }); + }); +}); diff --git a/src/ts/tests/services/model.spec.ts b/src/ts/tests/services/model.spec.ts new file mode 100644 index 0000000..8cd78d1 --- /dev/null +++ b/src/ts/tests/services/model.spec.ts @@ -0,0 +1,233 @@ +import { stubClass, resetStubMethods } from '../lib'; +import { Router } from '@angular/router'; +import { HttpClient } from '@angular/common/http'; +import { expect } from 'chai'; +import { assert, match } from 'sinon'; +import { from } from 'rxjs'; +import { Model } from '../../components/services/model'; +import { ErrorReporter } from '../../components/services/errorReporter'; +import { NAME_ERROR } from '../../common/errors'; +import { StorageService } from '../../components/services/storageService'; + +describe.skip('Model', () => { + let http: any = stubClass(HttpClient); + let router = stubClass(Router); + let storage = stubClass(StorageService); + let errorReporter = stubClass(ErrorReporter); + let model: Model; + + beforeEach(async () => { + resetStubMethods(http, 'post', 'get'); + resetStubMethods(router); + resetStubMethods(storage); + resetStubMethods(errorReporter); + http.post.withArgs('/api1/account').returns(from([{ + id: 'accid', + name: 'accname', + settings: {}, + friends: [], + }])); + model = new Model(http as any, router as any, storage as any, errorReporter); + await model.accountPromise; + (global as any).location = { href: 'local-href' }; + }); + + describe('supporter', () => { + it('returns 0 if not supporter', () => { + expect(model.supporter).equal(0); + }); + + it('returns supporter level', () => { + model.account!.supporter = 2; + + expect(model.supporter).equal(2); + }); + }); + + describe('signOut()', () => { + it('makes sign out request', async () => { + http.post.withArgs('/auth/sign-out').returns(from([{}])); + + await model.signOut(); + await model.accountPromise; + + assert.calledWithMatch(http.post, '/auth/sign-out', {}, match.any); + }); + + it('re-fetches account details', async () => { + http.post.reset(); + http.post.withArgs('/auth/sign-out').returns(from([{}])); + http.post.withArgs('/api1/account').returns(from([null])); + + await model.signOut(); + await model.accountPromise; + + assert.calledWithMatch(http.post, '/api1/account', match.any, match.any); + }); + }); + + describe('updateAccount()', () => { + it('makes update request', async () => { + http.post.withArgs('/api/account-update').returns(from([{ name: 'bar' }])); + + await model.updateAccount({ name: 'bar', birthdate: '' }); + + assert.calledWithMatch(http.post, '/api/account-update', { account: { name: 'bar' } }, match.any); + }); + + it('updates account with returned result', async () => { + http.post.withArgs('/api/account-update').returns(from([{ name: 'bar' }])); + + await model.updateAccount({ name: 'bar', birthdate: '' }); + + expect(model.account!.name).equal('bar'); + }); + }); + + describe('saveSettings()', () => { + it('makes save settings request', async () => { + http.post.withArgs('/api/account-settings') + .returns(from([{ settings: { ignorePartyInvites: true } }])); + + await model.saveSettings({ ignorePartyInvites: true }); + + assert.calledWithMatch(http.post, '/api/account-settings', { settings: { ignorePartyInvites: true } }, match.any); + }); + + it('updates settings with returned result', async () => { + http.post.withArgs('/api/account-settings') + .returns(from([{ settings: { ignorePartyInvites: true } }])); + + await model.saveSettings({ ignorePartyInvites: true }); + + expect(model.account!.settings.ignorePartyInvites).true; + }); + }); + + describe('removeSite()', () => { + it('makes remove request', async () => { + http.post.withArgs('/api/remove-site').returns(from([{}])); + + await model.removeSite('siteid'); + + assert.calledWithMatch(http.post, '/api/remove-site', { siteId: 'siteid' }, match.any); + }); + + it('removes site from the list', async () => { + http.post.withArgs('/api/remove-site').returns(from([{}])); + const site = { id: 'siteid' } as any; + model.account!.sites = [site]; + + await model.removeSite('siteid'); + + expect(model.account!.sites).not.contain(site); + }); + }); + + describe('savePony()', () => { + it('makes save request', async () => { + http.post.withArgs('/api/pony/save').returns(from([{}])); + + await model.savePony({ + id: 'ponyid', + name: 'ponyname', + info: 'ponyinfo', + site: 'siteid', + tag: 'tagname', + hideSupport: true, + }); + + assert.calledWithMatch(http.post, '/api/pony/save', { + pony: { + id: 'ponyid', + name: 'ponyname', + info: 'ponyinfo', + site: 'siteid', + tag: 'tagname', + hideSupport: true, + }, + }, match.any); + }); + + it('rejects if name is invalid', async () => { + http.post.withArgs('/api/pony/save').returns(from([{}])); + + await expect(model.savePony({ id: '', name: '', info: 'ponyinfo' })).rejectedWith(NAME_ERROR); + }); + + it('rejects if already saving', async () => { + http.post.withArgs('/api/pony/save').returns(from([{}])); + + await Promise.all([ + model.savePony({ id: '', name: 'ponyname', info: 'ponyinfo' }), + expect(model.savePony({ id: '', name: '', info: 'ponyinfo' })).rejectedWith('Saving in progress'), + ]); + }); + }); + + describe('removePony()', () => { + it('makes remove request', async () => { + const ponyObject = { id: 'ponyid' } as any; + http.post.withArgs('/api/pony/remove').returns(from([{}])); + + await model.removePony(ponyObject); + + assert.calledWithMatch(http.post, '/api/pony/remove', { id: 'ponyid' }, match.any); + }); + }); + + describe('status()', () => { + it('fetches full server status', async () => { + const status = {}; + http.get.withArgs('/api2/game/status').returns(from([status])); + + const result = await model.status(false); + + assert.calledWithMatch(http.get, '/api2/game/status'); + expect(result).eql(status); + }); + }); + + describe('join()', () => { + it('fetches join result', async () => { + const state = {}; + http.post.withArgs('/api/game/join').returns(from([state])); + + const result = await model.join('serverid', 'ponyid'); + + assert.calledWithMatch(http.post, '/api/game/join', { + accountId: 'accid', + accountName: 'accname', + version: undefined, + serverId: 'serverid', + ponyId: 'ponyid', + url: 'local-href', + }, match.any); + expect(result).equal(state); + }); + + it('rejects if called when already pending', async () => { + http.post.withArgs('/api/game/join').returns(from([{}])); + + const promise1 = model.join('serverid', 'ponyid'); + const promise2 = model.join('serverid', 'ponyid'); + + await Promise.all([ + promise1, + expect(promise2).rejectedWith('Joining in progress'), + ]); + }); + + it('rejects if server is invalid', async () => { + http.post.withArgs('/api/game/join').returns(from([{}])); + + await expect(model.join('', 'ponyid')).rejectedWith('Invalid server ID'); + }); + + it('rejects if pony is invalid', async () => { + http.post.withArgs('/api/game/join').returns(from([{}])); + + await expect(model.join('serverid', '')).rejectedWith('Invalid pony ID'); + }); + }); +}); diff --git a/src/ts/tests/services/settingsService.spec.ts b/src/ts/tests/services/settingsService.spec.ts new file mode 100644 index 0000000..39da976 --- /dev/null +++ b/src/ts/tests/services/settingsService.spec.ts @@ -0,0 +1,105 @@ +import { stubClass, resetStubMethods } from '../lib'; +import { expect } from 'chai'; +import { assert, stub } from 'sinon'; +import { SettingsService } from '../../components/services/settingsService'; +import { StorageService } from '../../components/services/storageService'; +import { Model } from '../../components/services/model'; + +describe('SettingsService', () => { + let model = stubClass(Model); + let storage = stubClass(StorageService); + let service: SettingsService; + + beforeEach(() => { + resetStubMethods(model, 'saveSettings'); + service = new SettingsService(storage as any, model as any); + }); + + it('loads browser settings from storage', () => { + const settings = { foo: 'bar' }; + storage.getJSON.returns(settings); + + service = new SettingsService(storage as any, model as any); + + expect(service.browser).equal(settings); + }); + + it('returns account settings', () => { + const settings = { foo: 'bar' }; + model.account = { settings } as any; + + expect(service.account).equal(settings); + }); + + it('returns empty settings if account is empty', () => { + model.account = undefined as any; + + expect(service.account).eql({}); + }); + + it('update account settings field', async () => { + const settings = { foo: 'bar' }; + model.account = {} as any; + + await service.saveAccountSettings(settings as any); + + expect((model.account as any).settings).equal(settings); + }); + + it('does not update account settings field if account is empty', async () => { + const settings = { foo: 'bar' }; + model.account = undefined as any; + + await service.saveAccountSettings(settings as any); + + expect(model.account).undefined; + }); + + it('saves settings to model', async () => { + const settings = { foo: 'bar' }; + + await service.saveAccountSettings(settings as any); + + assert.calledWith(model.saveSettings, settings as any); + }); + + it('calls saving callback', async () => { + const settings = { foo: 'bar' }; + const save = stub().returns(true); + service.saving(save); + + await service.saveAccountSettings(settings as any); + + assert.calledOnce(save); + assert.notCalled(model.saveSettings); + }); + + it('saves settings to model if saving callback returns false', async () => { + const settings = { foo: 'bar' }; + const save = stub().returns(false); + service.saving(save); + + await service.saveAccountSettings(settings as any); + + assert.calledOnce(save); + assert.calledWith(model.saveSettings, settings as any); + }); + + it('saves current browser settings', () => { + const settings = { foo: 'bar' }; + service.browser = settings as any; + + service.saveBrowserSettings(); + + assert.calledWith(storage.setJSON, 'browser-settings', settings); + }); + + it('saves given browser settings', () => { + const settings = { foo: 'bar' }; + + service.saveBrowserSettings(settings as any); + + expect(service.browser).equal(settings); + assert.calledWith(storage.setJSON, 'browser-settings', settings); + }); +}); diff --git a/src/ts/tools/canvas-utils.ts b/src/ts/tools/canvas-utils.ts new file mode 100644 index 0000000..428bd50 --- /dev/null +++ b/src/ts/tools/canvas-utils.ts @@ -0,0 +1,283 @@ +import * as fs from 'fs'; +import { createCanvas, Image } from 'canvas'; +import { ExtCanvas, Point } from './types'; + +export function loadImage(filePath: string) { + const image = new Image(); + image.src = fs.readFileSync(filePath); + (image as any).currentSrc = filePath; + return image; +} + +export function createExtCanvas(width: number, height: number, info: string): ExtCanvas { + const canvas: ExtCanvas = createCanvas(width, height) as any; + canvas.info = info; + return canvas; +} + +export function imageToCanvas(image: HTMLImageElement): ExtCanvas { + const canvas = createExtCanvas(image.width, image.height, `imageToCanvas(${image.currentSrc})`); + canvas.getContext('2d')!.drawImage(image, 0, 0); + return canvas; +} + +export function cropCanvas(canvas: ExtCanvas | undefined, x: number, y: number, w: number, h: number): ExtCanvas { + if (Math.round(x) !== x || Math.round(y) !== y || Math.round(w) !== w || Math.round(h) !== h) { + throw new Error(`Invalid cropping dimentions (${x} ${y} ${w} ${h})`); + } + + const result = createExtCanvas(w, h, `${canvas ? canvas.info : 'from null'} (cropped ${x} ${y} ${w} ${h})`); + + if (canvas) { + w = Math.min(w, canvas.width - x); + h = Math.min(h, canvas.height - y); + result.getContext('2d')!.drawImage(canvas, x, y, w, h, 0, 0, w, h); + } + + return result; +} + +export function mirrorCanvas(canvas: ExtCanvas, offsetX = 0) { + const mirrored = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (mirrored)`); + const context = mirrored.getContext('2d')!; + context.translate(canvas.width, 0); + context.scale(-1, 1); + context.translate(offsetX, 0); + context.drawImage(canvas, 0, 0); + return mirrored; +} + +export function padCanvas(canvas: ExtCanvas, left: number, top: number, right = 0, bottom = 0, bg?: string) { + if (left === 0 && top === 0 && right === 0 && bottom === 0) + return canvas; + + const result = createExtCanvas( + canvas.width + left + right, canvas.height + top + bottom, `${canvas.info} (pad ${left} ${top} ${right} ${bottom})`); + const context = result.getContext('2d')!; + + if (bg) { + context.fillStyle = bg; + context.fillRect(0, 0, result.width, result.height); + } + + context.drawImage(canvas, left, top); + return result; +} + +export function clipCanvas(canvas: ExtCanvas, x: number, y: number, w: number, h: number) { + return padCanvas(cropCanvas(canvas, x, y, w, h), x, y, canvas.width - (w + x), canvas.height - (h + y)); +} + +export function mergeCanvases(...canvases: (ExtCanvas | undefined)[]): ExtCanvas { + const existing = canvases.filter(c => !!c) as ExtCanvas[]; + const { width, height } = existing[0]; + const result = createExtCanvas(width, height, existing.map(c => c.info).join(' + ')); + const context = result.getContext('2d')!; + existing.forEach(c => context.drawImage(c, 0, 0)); + return result; +} + +export function reverseMaskCanvas(canvas: ExtCanvas): ExtCanvas; +export function reverseMaskCanvas(canvas: ExtCanvas | undefined): ExtCanvas | undefined; +export function reverseMaskCanvas(canvas: ExtCanvas | undefined) { + if (!canvas) + return undefined; + + const result = createExtCanvas(canvas.width, canvas.height, `${canvas.info} reversed mask`); + const context = result.getContext('2d')!; + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + context.globalCompositeOperation = 'destination-out'; + context.drawImage(canvas, 0, 0); + return result; +} + +export function maskCanvas(canvas: ExtCanvas, mask: ExtCanvas): ExtCanvas; +export function maskCanvas(canvas: ExtCanvas | undefined, mask: ExtCanvas | undefined): ExtCanvas | undefined; +export function maskCanvas(canvas: ExtCanvas | undefined, mask: ExtCanvas | undefined) { + if (!canvas || !mask) + return undefined; + + const result = createExtCanvas(canvas.width, canvas.height, `${canvas.info} masked by ${mask.info}`); + const context = result.getContext('2d')!; + context.drawImage(canvas, 0, 0); + context.globalCompositeOperation = 'destination-in'; + context.drawImage(mask, 0, 0); + return result; +} + +export function colorCanvas(canvas: ExtCanvas, color: string): ExtCanvas; +export function colorCanvas(canvas: ExtCanvas | undefined, color: string): ExtCanvas | undefined; +export function colorCanvas(canvas: ExtCanvas | undefined, color: string): ExtCanvas | undefined { + const copy = copyCanvas(canvas); + + if (copy) { + const context = copy.getContext('2d')!; + context.globalCompositeOperation = 'source-in'; + context.fillStyle = color; + context.fillRect(0, 0, copy.width, copy.height); + } + + return copy; +} + +export function copyCanvas(canvas: ExtCanvas): ExtCanvas; +export function copyCanvas(canvas: ExtCanvas | undefined): ExtCanvas | undefined; +export function copyCanvas(canvas: ExtCanvas | undefined): ExtCanvas | undefined { + if (!canvas) + return undefined; + + const newCanvas = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (copy)`); + newCanvas.getContext('2d')!.drawImage(canvas, 0, 0); + return newCanvas; +} + +export function recolorCanvas(canvas: ExtCanvas, color: string) { + const result = createExtCanvas(canvas.width, canvas.height, `recolorCanvas(${canvas.info}, ${color})`); + const context = result.getContext('2d')!; + context.fillStyle = color; + context.fillRect(0, 0, result.width, result.height); + context.globalCompositeOperation = 'destination-in'; + context.drawImage(canvas, 0, 0); + return result; +} + +export function createColorCanvas(width: number, height: number, color: string) { + const canvas = createExtCanvas(width, height, `createColorCanvas(${color})`); + const context = canvas.getContext('2d')!; + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + return canvas; +} + +export function isCanvasEmpty(canvas: ExtCanvas | undefined): boolean { + if (canvas && canvas.width > 0 && canvas.height > 0) { + const context = canvas.getContext('2d')!; + const data = context.getImageData(0, 0, canvas.width, canvas.height); + const size = data.width * data.height * 4; + + for (let i = 0; i < size; i++) { + if (data.data[i] !== 0) { + return false; + } + } + } + + return true; +} + +export function saveCanvas(filePath: string, canvas: HTMLCanvasElement) { + fs.writeFileSync(filePath, canvas.toBuffer()); +} + +function getColorAt(d: Uint8ClampedArray, i: number) { + return ((d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]) >>> 0; +} + +export function forEachPixel(canvas: ExtCanvas, action: (color: number, x: number, y: number) => void) { + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + + for (let y = 0, i = 0; y < data.height; y++) { + for (let x = 0; x < data.width; x++ , i += 4) { + action(getColorAt(data.data, i), x, y); + } + } +} + +export function forEachPixelOf2Canvases( + canvas1: ExtCanvas, canvas2: ExtCanvas, action: (color1: number, color2: number, x: number, y: number) => void +) { + if (canvas1.width !== canvas2.width || canvas1.height !== canvas2.height) { + throw new Error('Canvas not the same size'); + } + + const data1 = canvas1.getContext('2d')!.getImageData(0, 0, canvas1.width, canvas1.height); + const data2 = canvas2.getContext('2d')!.getImageData(0, 0, canvas2.width, canvas2.height); + + for (let y = 0, i = 0; y < data1.height; y++) { + for (let x = 0; x < data1.width; x++ , i += 4) { + action(getColorAt(data1.data, i), getColorAt(data2.data, i), x, y); + } + } +} + +type MapColor = (color: number, x: number, y: number) => number; + +export function mapEachPixel(canvas: ExtCanvas, action: MapColor) { + const context = canvas.getContext('2d')!; + const data = context.getImageData(0, 0, canvas.width, canvas.height); + const d = data.data; + + for (let y = 0, i = 0; y < data.height; y++) { + for (let x = 0; x < data.width; x++ , i += 4) { + const c = ((d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]) >>> 0; + const out = action(c, x, y); + d[i] = (out >>> 24) & 0xff; + d[i + 1] = (out >>> 16) & 0xff; + d[i + 2] = (out >>> 8) & 0xff; + d[i + 3] = out & 0xff; + } + } + + context.putImageData(data, 0, 0); +} + +export function mapColors(canvas: ExtCanvas, map: MapColor): ExtCanvas; +export function mapColors(canvas: ExtCanvas | undefined, map: MapColor): ExtCanvas | undefined; +export function mapColors(canvas: ExtCanvas | undefined, map: MapColor): ExtCanvas | undefined { + const result = copyCanvas(canvas); + + if (result) { + mapEachPixel(result, map); + } + + return result; +} + +function compareTemplate(canvas: ImageData, template: ImageData, ox: number, oy: number) { + for (let y = 0; y < template.height; y++) { + for (let x = 0; x < template.width; x++) { + for (let i = 0; i < 4; i++) { + if (canvas.data[i + (x + ox) * 4 + (y + oy) * canvas.width * 4] !== template.data[i + x * 4 + y * template.width * 4]) { + return false; + } + } + } + } + + return true; +} + +export function findTemplate(canvas: HTMLCanvasElement, template: HTMLCanvasElement) { + const canvasData = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const templateData = template.getContext('2d')!.getImageData(0, 0, template.width, template.height); + const maxX = canvas.width - template.width; + const maxY = canvas.height - template.height; + + for (let y = 0; y <= maxY; y++) { + for (let x = 0; x <= maxX; x++) { + if (compareTemplate(canvasData, templateData, x, y)) { + return { x, y }; + } + } + } + + return null; +} + +export function offsetCanvas(canvas: ExtCanvas | undefined, { x, y }: Point) { + return canvas && padCanvas(canvas, x, y); +} + +export type CanvasGetter = (Canvas: ExtCanvas, col: number, row: number) => ExtCanvas; +export type ByIndexGetter = (Canvas: ExtCanvas, index: number) => ExtCanvas; + +export function cropAndPadByColRow( + x: number, y: number, w: number, h: number, dx: number, dy: number, padLeft = 0, padTop = 0 +): CanvasGetter { + return (canvas, col, row) => padCanvas(cropCanvas(canvas, x + dx * col, y + dy * row, w, h), padLeft, padTop); +} + +export function cropByIndex(get: CanvasGetter, perLine: number): ByIndexGetter { + return (canvas, i) => get(canvas, i % perLine, Math.floor(i / perLine)); +} diff --git a/src/ts/tools/common.ts b/src/ts/tools/common.ts new file mode 100644 index 0000000..a3063df --- /dev/null +++ b/src/ts/tools/common.ts @@ -0,0 +1,234 @@ +/// + +import * as fs from 'fs'; +import * as path from 'path'; +import { spawn } from 'child_process'; +import { reduce, flatten, map, dropRightWhile } from 'lodash'; +import { Layer, Psd, ExtCanvas, Sprite, Rect, Result, ColorExtra } from './types'; +import { ColorsOutput, imageToPalette } from './palette-utils'; +import { getSpriteRect } from './sprite-sheet'; +import { createColorCanvas, isCanvasEmpty } from './canvas-utils'; + +(global as any).DEVELOPMENT = true; + +export const TRANSPARENT = 0; +export const BLACK = 0x000000ff; +export const WHITE = 0xffffffff; +export const OUTLINE_COLOR = 0x9f9f9fff; +export const SHADE_COLOR = 0xccccccff; +export const LIGHT_SHADE_COLOR = 0xddddddff; +export const TEAR_COLOR = 0xc1eef0ff; +export const DARK_GRAY = 0x232728ff; +export const MOUTH_COLOR = 0x721946ff; +export const TONGUE_COLOR = 0xf39f4bff; +export const TEETH_COLOR = 0x8cffffff; +export const TEETH_SHADE_COLOR = 0x77d9d9ff; + +// TEMP: remove after adding palettes for effects +const holdPoofColors = [ + 0xffff47ff, 0xeaed58ff, 0xd8dc00ff, 0xff6741ff, 0xff0000ff, 0xff7af9ff, 0xff00ccff, 0xb67affff, + 0x9876ffff, 0x76a6ffff, 0x0097ffff, 0x00ff9bff, 0x76ed5cff, 0x28dc00ff, 0x76ed5cff, +]; + +export const defaultPalette = [ + TRANSPARENT, WHITE, BLACK, MOUTH_COLOR, TONGUE_COLOR, LIGHT_SHADE_COLOR, TEAR_COLOR, DARK_GRAY, + ...holdPoofColors, +]; + +export function cartesian(a: A[], b: B[]): [A, B][]; +export function cartesian(a: A[], b: B[], c: C[]): [A, B, C][]; +export function cartesian(...args: any[]) { + return reduce(args, (a, b) => flatten(map(a, x => map(b as any, y => x.concat([y])))), [[]]); +} + +export function mkdir(dirpath: string) { + try { + fs.mkdirSync(dirpath); + } catch { } +} + +const isDirectory = (dir: string) => fs.lstatSync(dir).isDirectory(); + +export const getDirectories = (dir: string) => fs.readdirSync(dir).map(name => path.join(dir, name)).filter(isDirectory); + +function findLayerByPath([name, ...child]: string[], layer: Layer | Psd | undefined): Layer | undefined { + return name ? findLayerByPath(child, layer && findByName(layer.children, name)) : layer; +} + +export function findLayer(path: string, layer: Layer | Psd | undefined): Layer | undefined { + return findLayerByPath(path.split('/'), layer); +} + +export function findLayerSafe(name: string, parent: Layer | Psd): Layer { + const layer = findLayer(name, parent); + + if (!layer) { + throw new Error(`Missing layer "${name}" in "${parent.info}"`); + } + + return layer; +} + +export function findByName(items: T[], name: string): T | undefined { + return items.find(i => i.name === name); +} + +export function findByIndex(items: T[], index: number) { + return items.find(i => i.index === index); +} + +export const nameMatches = (regex: RegExp) => (l: { name: string; }) => regex.test(l.name); + +export function compareNames(a: { name: string }, b: { name: string }) { + return a.name.localeCompare(b.name); +} + +export const time = (function () { + const start = Date.now(); + let last = start; + + return function (text: string) { + console.log(text, (Date.now() - last), 'ms'); + last = Date.now(); + return true; + }; +})(); + +export function spawnAsync(command: string, args?: string[]) { + return new Promise((resolve, reject) => { + spawn(command, args) + .on('error', (err: Error) => reject(err)) + .on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Non-zero return code for ${command} (${code})`))); + }); +} + +// canvas + +export function getCanvas(layer: Layer | undefined): ExtCanvas | undefined { + if (!layer) + return undefined; + + const canvas = layer.canvas; + + if (canvas) { + canvas.info = layer.info; + } + + return canvas; +} + +export function getCanvasSafe(layer: Layer): ExtCanvas { + const canvas = getCanvas(layer); + + if (!canvas) { + throw new Error(`Cannot find canvas in layer "${layer.info}"`); + } + + return canvas; +} + +export function getLayerCanvas(name: string, parent: Layer | Psd | undefined) { + return getCanvas(findLayer(name, parent)); +} + +export function getLayerCanvasSafe(name: string, parent: Layer | Psd): ExtCanvas { + return getCanvasSafe(findLayerSafe(name, parent)); +} + +export function parseWithNumber(name: string) { + const match = /(\d+)/.exec(name); + return parseInt(match ? match[1] : '0', 10); +} + +export const matcher = (regex: RegExp) => (text: string) => regex.test(text); + +const isArrayEmpty = (a: T[] | null) => !a || a.length === 0; +const nullForEmpty = (a: T[] | null) => isArrayEmpty(a) ? null : a; + +export function trimRight(items: ((T | null)[] | null)[]): ((T | null)[] | null)[] { + return dropRightWhile(items.map(nullForEmpty) as any, isArrayEmpty as any) as any; +} + +// sprites + +const redCanvas = createColorCanvas(1000, 1000, 'red'); + +export function addImage(images: HTMLCanvasElement[], canvas: HTMLCanvasElement) { + if (canvas) { + // TODO: remove duplicated + images.push(canvas); + return images.length - 1; + } else { + return 0; + } +} + +export function createSprite(index: number, image: HTMLCanvasElement, { w, h, x, y }: Rect) { + return { index, image, w, h, x: 0, y: 0, ox: x, oy: y }; +} + +const maxSpriteWidth = 500; +const maxSpriteHeight = 500; + +export function addSprite( + sprites: Sprite[], canvas?: ExtCanvas, pattern?: ExtCanvas, palette?: number[], out: ColorsOutput = {} +): number { + if (canvas) { + const rect = getSpriteRect(canvas, 0, 0, canvas.width, canvas.height); + + if (rect.w && rect.h) { + if (rect.w > maxSpriteWidth || rect.h > maxSpriteHeight) { + throw new Error(`Sprite too large (${rect.w}, ${rect.h}) from [${canvas.info}]`); + } + + const image = imageToPalette(rect, canvas, pattern || redCanvas, palette, out); + sprites.push(createSprite(sprites.length, image, rect)); + return sprites.length - 1; + } + } + + return 0; +} + +export function addSpriteWithColors( + sprites: Sprite[], colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean +): ColorExtra { + const out: ColorsOutput = { forceWhite }; + const color = addSprite(sprites, colorImage, patternImage, undefined, out); + return { color, colors: out.colors! }; +} + +export function getColorsCount(colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean): number { + const out: ColorsOutput = { forceWhite }; + addSprite([], colorImage, patternImage, undefined, out); + return out.colors!; +} + +export function createPixelSprites({ objects, objects2, images, sprites }: Result) { + const pixel = createColorCanvas(3, 3, 'white'); + objects['pixelRect'] = addImage(images, pixel); + objects2['pixelRect2'] = addSprite(sprites, pixel, undefined, defaultPalette); +} + +// layers + +export const compareLayers = (a: Layer, b: Layer) => parseWithNumber(a.name) - parseWithNumber(b.name); + +export function getPatternLayers(layer: Layer) { + return layer.children.filter(nameMatches(/^pattern/)).sort(compareLayers); +} + +export function getPatternCanvases(layer: Layer): ExtCanvas[] { + const canvases = getPatternLayers(layer).map(getCanvas); + return dropRightWhile(canvases, isCanvasEmpty) as ExtCanvas[]; +} + +export function clipPattern(color: ExtCanvas, pattern: ExtCanvas | undefined): ExtCanvas | undefined { + if (pattern) { + const ctx = pattern.getContext('2d')!; + ctx.globalCompositeOperation = 'destination-in'; + ctx.drawImage(color, 0, 0); + } + + return pattern; +} diff --git a/src/ts/tools/convert-tiles.ts b/src/ts/tools/convert-tiles.ts new file mode 100644 index 0000000..d1d0114 --- /dev/null +++ b/src/ts/tools/convert-tiles.ts @@ -0,0 +1,41 @@ +import { ExtCanvas } from './types'; +import { cropCanvas } from './canvas-utils'; + +const tileWidth = 32; +const tileHeight = 24; + +const cols = 10; +const tiles = [ + 47, 47, 0, 0, 13, 19, 21, 20, 15, 16, + 47, 47, 0, 0, 13, 13, 45, 22, 18, 17, + 9, 2, 2, 2, 10, 14, 14, 14, 35, 36, + 8, 5, null, 7, 4, 27, 26, 29, 37, 38, + 8, null, 46, null, 4, 28, 24, 30, 39, 40, + 8, 3, null, 1, 4, 23, 31, 32, 41, 42, + 12, 6, 6, 6, 11, 25, 33, 34, 43, 44, +]; + +interface Rev { + number: number; + index: number; + dstIndex: number; +} + +const revtiles: Rev[] = tiles + .map((number, index) => ({ number, index, dstIndex: 0 })) + .filter(x => x.number != null) as Rev[]; +revtiles.sort((a, b) => a.number - b.number); +revtiles.forEach((t, i) => t.dstIndex = i); + +export function tilesToSprites(canvas: ExtCanvas, spaceH: number, spaceV: number) { + return revtiles + .sort((a, b) => a.dstIndex - b.dstIndex) + .map(t => { + const srcIndex = t.index; + const srcCol = srcIndex % cols; + const srcRow = Math.floor(srcIndex / cols); + const srcX = spaceH + srcCol * (tileWidth + spaceH); + const srcY = spaceV + srcRow * (tileHeight + spaceV); + return cropCanvas(canvas, srcX, srcY, tileWidth, tileHeight); + }); +} diff --git a/src/ts/tools/create-font.ts b/src/ts/tools/create-font.ts new file mode 100644 index 0000000..641576c --- /dev/null +++ b/src/ts/tools/create-font.ts @@ -0,0 +1,385 @@ +import { cropCanvas } from './canvas-utils'; +import { compact } from 'lodash'; +import { ExtCanvas } from './types'; + +export const CHINESE = [ + '一丁七万丈三上下不与丐丑专且丕世丘丙业丛东丝丞両丢两严並丧个丫中', + '丰串临丸丹为主丼丽举乃久么义之乌乍乎乏乐乒乓乔乖乗乘乙九乞也习乡', + '书买乱乳乾亀了予争事二于亏云互五井亘亙亚些亜亞亟亡亢交亥亦产亨亩', + '享京亭亮亲亵人亿什仁仅仆仇今介仍从仏仑仓仔仕他仗付仙代令以仪们仮', + '仰仲件价任份仿企伊伍伎伏伐休众优伙会伝伞伟传伢伤伦伪伯估伴伶伸伺', + '似伽佃但位低住佐佑体何余佚佛作佟你佣佩佬佯佳併佼使侃侄來侈例侍侏', + '侑供依侠価侣侥侦侧侨侬侮侯侵侶便係促俄俊俏俐俗俘保俞俟俠信俣俨俩', + '俭修俯俱俳俵俸俺俾倉個倍倏倒倔倖倘候倚借倡倣値倦倩倪倫倭倶倹债值', + '倾假偉偌偎偏偕做停健偲側偵偶偷偽偿傀傅傍傑傘備傣储傩催傭傲傳債傷', + '傻傾僅働像僑僕僚僞僧僭僵價僻儀億儉儒償儡優儲儿兀允元兄充兆先光克', + '免兎児兑兒兔党兜兢入全八公六兮兰共关兴兵其具典兹养兼兽冀内円冈冉', + '冊册再冒冕冗写军农冠冢冤冥冨冬冯冰冲决冴况冶冷冻净凄准凉凋凌凍减', + '凑凛凜凝几凡凤処凧凪凭凯凰凱凳凶凸凹出击函凿刀刁刃分切刈刊刑划列', + '刘则刚创初删判別刨利别刮到制刷券刹刺刻刽剂剃則削剌前剑剔剖剛剣剤', + '剥剧剩剪副剰割創剿劇劈劉劍力劝办功加务劣动助努劫励劲劳労効劾势勁', + '勃勅勇勉勋勒動勘務勝募勢勤勧勲勳勺勾勿匀匁匂包匆匈匕化北匙匠匡匣', + '匪匮匹区医匾匿十千升午卉半华协卑卒卓協单卖南単博卜占卡卢卤卦卧卫', + '卯印危即却卵卷卸卿厂厄厅历厉压厌厕厘厚原厢厥厦厨厩厮厳去县参又叉', + '及友双反収发叔取受变叙叛叟叠叡叢口古句另叨叩只叫召叭叮可台叱史右', + '叶号司叹叼叽吁吃各吆合吉吊同名后吏吐向吓吕吗君吝吞吟吠否吧吨吩含', + '听吭吮启吱吴吵吸吹吻吼吾呀呂呃呆呈呉告呐呕呗员呛呜呟呢周呪呱味呵', + '呸呻呼命咀咄咆咋和咎咏咐咒咔咕咖咙咚咦咧咨咪咬咯咱咲咳咸咽哀品哄', + '哆哇哈哉响哎哑哒哗哝哟員哥哦哧哨哩哪哭哮哲哺哼哽唄唆唇唉唐唔唠唤', + '唧唬售唯唱唷唾啃啄商啊問啓啡啤啥啦啧啪啬啸啼喀喂喃善喇喉喊喋喔喘', + '喙喚喜喝喧喩喪喫喬單喰喳営喷喻喽嗅嗒嗓嗔嗖嗜嗟嗡嗣嗤嗦嗨嗯嗳嗽嘀', + '嘆嘈嘉嘎嘗嘘嘛嘟嘩嘱嘲嘴嘶嘻嘿噂噌噎噗噛噜噢器噩噪噬噴噶嚇嚎嚏嚓', + '嚣嚴嚷嚼囊囚四回因团団园困囱囲図围固国图圃圆圈國圏園圓團土圣圧在', + '圭地圳场圾址坂均坊坍坎坏坐坑块坚坛坝坞坟坠坡坤坦坪坯坷垂垃垄型垒', + '垢垣垦垫垮埃埋城埔埜域埠埴執培基埼堀堂堅堆堑堕堡堤堪堯堰報場堵堺', + '塀塁塊塌塑塔塗塘塙塚塞塩填塾境墅墓増墙墜增墟墨墩墳墾壁壇壊壌壑壕', + '壘壞壤士壬壮壯声壱売壳壶壷壽处备変复夏夕外多夜够夢大天太夫夭央失', + '头夷夸夹夺奂奄奇奈奉奋奎奏契奔奕奖套奚奠奢奥奧奨奪奬奮女奴奶奸她', + '好如妃妄妆妇妈妊妒妓妖妙妞妥妨妬妮妹妻妾姆姉姊始姐姑姓委姗姚姜姥', + '姦姨姪姫姬姻姿威娃娅娇娓娘娜娟娠娥娩娯娱娴娶娼婆婉婚婢婦婪婴婵婶', + '婷婿媒媚媛媲媳嫁嫂嫉嫌嫔嫖嫡嫣嫦嫩嬉嬢孃子孔孕字存孙孚孜孝孟季孤', + '学孩孫孰孵孽宁它宅宇守安宋完宏宕宗官宙定宛宜宝实実宠审客宣室宥宦', + '宪宫宮宰害宴宵家容宽宾宿寂寄寅密寇富寐寒寓寛寝寞察寡寢寥實寧寨審', + '寮寰寵寸对寺寻导対寿封専射将將專尉尊尋導小少尔尖尘尚尝尤尧尬尭就', + '尴尸尹尺尻尼尽尾尿局屁层居屈屉届屋屎屏屑展属屠屡層履屯山屹屿岁岂', + '岐岑岔岗岚岛岡岩岬岭岳岸峙峠峡峦峨峭峯峰島峻峽崇崎崔崖崚崛崩崭崽', + '嵋嵌嵐嵩嵯嶋嶺巅巌巍巖川州巡巢巣工左巧巨巩巫差己已巳巴巷巻巽巾币', + '市布帅帆师希帐帕帖帘帚帛帜帝帥带帧師席帮帯帰帳帶帷常帽幅幌幔幕幡', + '幢幣干平年并幸幹幻幼幽幾广庁広庄庆庇床序庐库应底店庙庚府庞废度座', + '庫庭庵庶康庸廃廉廊廓廖廟廣廳延廷建廻廿开弁异弃弄弊式弐弓弔引弗弘', + '弛弟张弥弦弧弩弯弱張強弹强弼弾彈彌归当录彗彙彝形彦彩彪彫彬彭彰影', + '彷役彻彼往征径待很徊律後徐徒従得徘徙從徠御復循微徳徴德徹徽心必忆', + '忌忍忏忒忖志忘忙応忠忡忧快忱念忽忿怀态怂怅怎怒怔怕怖怜思怠怡急怦', + '性怨怪怯总恃恆恋恍恐恒恕恙恚恢恣恤恥恨恩恪恬恭息恰恳恵恶恺恻恼恿', + '悄悉悌悍悔悖悚悟悠患悦您悩悪悬悯悲悴悸悻悼情惇惊惋惑惕惘惚惜惟惠', + '惡惣惦惧惨惩惫惬惭惮惯惰想惴惶惹惺愁愈愉意愕愚愛感愣愤愧愫愼愿慄', + '慈態慌慎慑慕慢慣慧慨慮慰慶慷憂憋憎憐憔憚憤憧憨憩憬憲憶憾懂懇懈應', + '懊懐懒懦懲懵懷懸懿戈戊戌戍戎戏成我戒或战戚戟戦截戮戯戰戲戳戴户戸', + '戻房所扁扇扈扉手才扎扑扒打扔払托扛扣执扩扫扬扭扮扯扰扱扳扶批扼找', + '承技抄抉把抑抒抓投抖抗折抚抛抜択抡抢护报抨披抬抱抵抹押抽抿拂担拆', + '拇拈拉拌拍拎拐拒拓拔拖拗拘拙拚招拜拝拟拠拡拢拣拥拦拧拨择括拭拯拱', + '拳拴拶拷拼拽拾拿持挂指按挑挖挙挚挛挝挟挠挡挣挤挥挨挪挫振挺挽挿捂', + '捅捆捉捍捎捏捐捕捗捜捞损捡换捣捧捨据捲捶捷捺捻掀掃授掉掌掏掐排掖', + '掘掛掠採探掣接控推掩措掬掲掳掴掷掺掻揃揉揍描提插揖揚換握揣揩揪揭', + '揮援揺揽搀搁搂搅損搏搐搓搔搖搜搞搬搭携搾摂摄摆摇摊摑摒摔摘摧摩摯', + '摸摹摺撂撃撇撑撒撕撚撞撤撩撫撬播撮撰撲撵撸撼擁擂擅擊操擎擒擞擢擦', + '擬攀攒攘攝攥攫支收攸改攻放政故效敌敍敏救敕敖敗教敛敝敞敢散敦敬数', + '敲整敵敷文斉斋斌斎斐斑斓斗料斜斟斡斤斥斧斩斬断斯新方於施旁旅旋族', + '旗无既日旦旧旨早旬旭旱时旷旺昂昆昇昊昌明昏易昔星映春昧昨昭是昴昼', + '显時晃晄晋晌晏晒晓晕晖晚晝晟晤晦晨晩普景晰晴晶智晾暁暂暄暇暉暑暖', + '暗暢暦暧暨暫暮暴曇曉曖曙曜曝曦曰曲曳更書曹曼曽曾替最月有朋服朔朕', + '朗望朝期朦木未末本札术朱朴朵机朽杀杂权杆杉李杏材村杖杜杞束杠条来', + '杨杭杯杰東杵杷松板极构枇枉析枕林枚果枝枠枢枣枪枫枭枯架枷柄柊柏某', + '柑染柔柘柚柜查柬柯柱柳柴柵査柾柿栃栄栅标栈栋栏树栓栖栗栞校栩株样', + '核根格栽桁桂桃桅框案桌桐桑桓桔桜桟桢档桥桦桧桨桩桶梁梅梓梗梛條梢', + '梦梧梨梭梯械梳梵梶检棄棉棋棍棒棕棘棚棟棠森棱棲棵棺椀椅椋植椎椒椛', + '検椭椰椿楊楓楔楕楚楞楠楢業楯極楷楼楽概榄榆榈榊榎榛榜榨榮榴榻槇構', + '槌槍槐様槙槛槻槽樂樊樋標樟模樣権横樫樱樵樹樺樽橄橋橘橙機橡橱檀檎', + '檐檜檢檬櫂櫓櫛櫻欄欠次欢欣欤欧欲欺欽款歇歉歌歎歓止正此步武歧歩歪', + '歯歳歴歹死歼殃殆殉殊残殖殡殴段殷殺殻殿毁毅毋母毎每毒毓比毕毗毘毙', + '毛毡毫毬毯氏民氓气氖気氛氟氢氣氦氧氨氮氯水氷永氾汀汁求汇汉汎汐汕', + '汗汚汛汝汞江池污汤汪汰汲汶汹決汽汾沁沂沃沈沉沌沐沓沖沙沛沟没沢沥', + '沦沧沪沫沮河沸油治沼沽沾沿況泄泉泊泌法泛泞泡波泣泥注泪泰泳泵泻泼', + '泽洁洋洒洗洛洞津洪洲洵洸活洼洽派流浄浅浆浇浊测济浏浑浒浓浙浚浜浦', + '浩浪浬浮浴海浸涂涅消涉涌涎涕涙涛涟涡涣涤润涧涨涩涯液涵涸涼淀淄淆', + '淋淌淑淖淘淡淤淨淫淮深淳淵混淹添清渇済渉渊渋渍渎渐渓渔渗渚減渝渠', + '渡渣渤渥渦温測渭港渲渴游渺湃湄湊湍湖湘湛湧湯湾湿満溃溅溉源準溜溝', + '溢溥溪溯溶溺滅滇滉滋滑滔滕滚滝滞满滤滥滨滩滯滴漁漂漆漉漏漓演漕漠', + '漢漣漩漪漫漬漱漳漸漾潇潔潘潜潟潤潦潭潮潰澁澄澈澎澜澡澪澱澳激濁濃', + '濒濕濠濡濫濯瀑瀕瀚瀛瀧瀬灌灘火灭灯灰灵灶灸灼災灾灿炉炊炎炒炕炖炙', + '炫炬炭炮炯炳炸点為炼炽烁烂烈烏烘烙烛烟烤烦烧烫烬热烹烽焉焕焘焙焚', + '無焦焰然焼煉煌煎煙煜煞煤照煩煮煽熄熊熏熔熙熟熬熱熹燃燈燎燒燕燥燦', + '燭燿爆爪爬爭爱爲爵父爷爸爹爽爾片版牌牒牙牛牟牡牢牧物牲牵特牺牽犀', + '犁犊犠犬犯状犹狂狄狈狐狒狗狙狞狠狡狩独狭狮狱狸狹狼猎猓猖猛猜猝猟', + '猩猪猫献猴猶猾猿獄獅獗獣獲獸玄率玉王玑玖玛玩玫环现玲玺玻珀珂珈珊', + '珍珑珠珥班現球琅理琉琏琐琛琢琥琦琪琳琴琵琶琼瑕瑙瑚瑛瑜瑞瑟瑠瑰瑳', + '瑶瑾璃璇璋璐璞璧環璽瓜瓢瓣瓦瓮瓶瓷甄甘甚甜生産甥用甩甫甭田由甲申', + '电男甸町画畅界畏畑畔留畜畝畠畢略番異畳畴畸畿疆疊疋疎疏疑疗疙疚疡', + '疣疤疫疮疯疲疵疹疼疾病症痉痊痒痔痕痘痛痢痩痪痰痴痹瘍瘟瘢瘤瘦瘩瘪', + '瘫瘸瘾療癌癒癖癫発登白百皂的皆皇皈皋皐皑皓皖皮皱皿盃盅盆盈益盎盏', + '盐监盒盔盖盗盘盛盜盟盡監盤目盯盲直相盼盾省眈眉看県眞真眠眨眩眯眶', + '眷眸眺眼着睁睐睑睛睡督睦睨睫睬睹睽睾睿瞄瞅瞎瞒瞟瞥瞧瞩瞪瞬瞭瞰瞳', + '瞻瞿矗矛矜矢矣知矩矫短矮矯石矶矿码砂砌砍研砕砖砥砦砧砰砲破砸砾础', + '硅硕硝硫硬确硯碁碉碌碍碎碑碓碗碘碟碧碩碰碱碳確碾磁磅磊磋磐磕磨磯', + '磷磺礁礎示礼社祀祁祇祈祉祐祕祖祝神祟祠祢祥票祭祯祷祸祺祿禀禁禄禅', + '禍禎福禧禪禮禰禱禹禺离禽禾秀私秃秉秋种科秒秘租秤秦秧秩积称秸移秽', + '稀程稍税稔稚稜稟稠稣種稲稳稷稻稼稽稿穀穂穆積穏穗穣穫穰穴究穷穹空', + '穿突窃窄窍窑窒窓窖窗窘窜窝窟窥窦窪窮窯窺窿立竖站竜竞竟章竣童竪竭', + '端競竹竺竿笃笆笈笋笑笔笙笛笠符笨第笹笺笼筆筈等筋筏筐筑筒答策筛筝', + '筵筷筹签简箇箋箍箔箕算管箫箭箱箸節範篇築篠篡篤篮篱篷簇簌簡簧簸簾', + '簿籍籠米类籽籾粉粋粒粗粘粛粟粤粥粧粪粮粱粹精糊糕糖糙糜糟糧糸系糾', + '紀約紅紊紋納紐純紗紘紙級紛素紡索紧紫紬累細紳紹紺終絃組絆経結絞絡', + '絢給絮統絵絶絹継続綜維綱網綴綸綺綻綾綿緊緋総緑緒線締編緩緯練緻縁', + '縄縛縞縣縦縫縮縱績繁繊繍織繕繡繫繭繰纂纏纖纠红纤约级纪纬纯纱纲纳', + '纵纶纷纸纹纺纽线练组绅细织终绊绍绎经绑绒结绕绘给绚绛络绝绞统绢绣', + '绥继绩绪绫续绮绯绰绳维绵绷绸综绽绿缀缄缅缆缉缎缓缔缕编缘缚缝缠缤', + '缩缪缭缮缰缴缶缸缺罐网罕罗罚罡罢罩罪置罰署罵罷羁羅羊羌美羔羚羞羡', + '群羨義羲羹羽翁翅翌翎習翔翘翟翠翩翰翻翼耀老考者耆而耍耐耕耗耳耶耸', + '耻耽耿聂聆聊聋职联聖聘聚聞聡聪聴職聽聿肃肆肇肉肋肌肖肘肚肛肝肠股', + '肢肤肥肩肪肮肯育肴肺肾肿胀胁胃胆背胎胖胚胜胞胡胤胥胧胰胱胳胴胶胸', + '胺能脂脅脆脇脈脉脊脏脐脑脓脖脚脩脯脱脳脸脹脾腊腋腎腐腑腓腔腕腥腫', + '腮腰腸腹腺腻腾腿膀膊膏膚膛膜膝膨膳膺臀臂臆臓臟臣臥臨自臭至致臻臼', + '舅舆與興舌舍舎舒舔舗舜舞舟航般舰舱舵舶舷船艇艘艦良艰色艳艶艺艾节', + '芋芒芙芜芝芥芦芬芭芯花芳芸芹芽苇苍苏苑苔苗苛苞苟若苦苯英苹苺茂范', + '茄茅茉茎茜茧茨茫茬茵茶茸茹荀荃荆草荐荒荔荘荡荣荧荫药荷荻荼莉莊莎', + '莞莫莱莲获莹莺莽菁菅菇菊菌菓菖菜菩菫華菱菲萃萄萊萌萍萎萝萠萤营萦', + '萧萨萩萬萱落葆葉著葛葡董葦葫葬葱葵葺蒂蒋蒐蒔蒙蒜蒲蒸蒼蓄蓉蓋蓑蓓', + '蓝蓦蓬蓮蔑蔓蔗蔚蔡蔣蔦蔬蔭蔵蔷蔼蔽蕃蕉蕊蕎蕗蕨蕪蕴蕾薄薇薗薙薛薦', + '薩薪薫薬薯藁藉藍藏藐藕藝藤藥藩藻蘇蘑蘭蘸虎虏虐虑虔虚虜虞虫虹虻虽', + '虾蚀蚁蚂蚊蚌蚕蛀蛇蛊蛋蛍蛙蛛蛟蛤蛮蛹蛾蜀蜂蜈蜒蜕蜗蜘蜜蜡蜷蜿蝇蝉', + '蝎蝗蝙蝠蝦蝴蝶螂螃螅融螺蟀蟆蟋蟒蟹蟾蠕蠟蠢血衅衆行衍術衔街衙衛衝', + '衞衡衣补表衫衬衰衷衿袁袄袅袈袋袍袒袖袜被袭袱袴裁裂装裏裔裕裘裙補', + '裝裟裡裤裳裴裸裹製裾褂複褐褒褚褥褪褶襄襖襟襲西要覆覇見規視覗覚覧', + '親観覽见观规觅视览觉觑角解触言訂訃計訊討訓託記訟訣訪設許訳訴診註', + '証詐詔評詞詠詢詣試詩詫詮詰話該詳詹誇誉誌認誓誕誘語誠誤説読誰課誼', + '調諄談請諏諒論諜諦諧諭諮諸諺諾謀謁謂謄謎謙講謝謠謡謹識譜警譬議譲', + '護讃讐讓计订认讥讨让讪训议讯记讲讳讶讷许讹论讼讽设访诀证诃评诅识', + '诈诉诊词诏译试诗诘诙诚诛话诞诠诡询诣该详诧诩诫诬语误诰诱诲说诵请', + '诸诺读诽课谀谁调谅谈谊谋谍谎谏谐谑谒谓谕谙谚谛谜谟谢谣谤谦谨谩谬', + '谭谱谴谷豁豆豊豚象豢豪豫豹貌貝貞負財貢貧貨販貪貫責貯貰貴買貸費貼', + '貿賀賃賄資賊賑賓賛賜賞賠賢賣賦質賭購贈贝贞负贡财责贤败账货质贩贪', + '贫贬购贮贯贰贱贴贵贷贸费贺贻贼贾贿赁赂赃资赋赌赎赏赐赓赔赖赘赚赛', + '赞赠赡赢赣赤赦赫走赳赴赵赶起趁超越趋趟趣足趴趾跃跄跋跌跑跚跛距跟', + '跡跤跨跪路跳践跷跺踉踊踌踏踝踞踢踩踪踱踵蹂蹄蹈蹊蹋蹑蹙蹟蹦蹬蹭蹲', + '蹴蹿躁躇躍躏身躬躯躲躺車軌軍軒軟転軸軽較載輔輝輩輪輯輸輿轄轉轍轟', + '车轧轨轩转轮软轰轲轴轻轼载轿较辄辅辆辈辉辐辑输辕辖辗辘辙辛辜辞辟', + '辣辨辩辫辰辱農边辺辻込辽达辿迁迂迄迅过迈迎运近返还这进远违连迟迢', + '迥迦迪迫迭述迷迸迹追退送适逃逆选逊逍透逐递逓途逗這通逛逝逞速造逢', + '連逮週進逵逸逻逼逾遁遂遅遇遊運遍過遏遐道達違遗遙遜遠遡遣遥適遭遮', + '遵遷選遺遼遽避邀邃還邑邓邢那邦邪邮邯邱邵邸邹邻郁郊郎郑郝郡部郭郵', + '郷都鄂鄙鄭酉酋酌配酎酒酔酝酢酣酥酪酬酮酱酵酶酷酸酿醇醉醋醍醐醒醜', + '醤醬醸釀采釈釉释里重野量金釘釜針釣釧鈍鈴鉄鉛鉢鉱鉴銀銃銅銑銘銚銭', + '鋒鋭鋳鋸鋼錆錐錘錠錦錫錬錯録鍋鍛鍬鍵鎌鎖鎧鎭鎮鏡鐘鑄鑑鑫针钉钊钓', + '钗钙钝钞钟钠钡钢钥钦钧钩钮钱钳钵钻钾铀铁铃铅铎铐铜铝铢铭铮铲银铸', + '铺链销锁锄锅锈锋锌锐错锚锡锢锣锤锥锦键锯锵锻镀镇镑镖镜镯镰镶長长', + '門閃閉開閏閑間関閣閤閥閲闇闘门闪闭问闯闲间闵闷闸闹闺闻闽阀阁阅阉', + '阎阐阑阔阙阜队阪阮阱防阳阴阵阶阻阿陀附际陆陇陈陋陌降限陕陛陡院陣', + '除陥陨险陪陰陳陵陶陷陸険陽隅隆隈隊隋階随隐隔隘隙際障隠隣隧險隶隷', + '隻隼难雀雁雄雅集雇雌雍雏雑雕雛雜離難雨雪雫雯雰雲雳零雷電雾需霄霆', + '震霉霊霍霎霓霖霜霞霧露霸霹青靖静靜非靠靡面革靳靴靶鞄鞅鞋鞍鞘鞠鞭', + '韓韦韧韩韬音韵韶韻響頁頂頃項順須頌預頑頒頓頗領頬頭頰頻頼題額顎顔', + '顕願顛類顧顯页顶顷项顺须顼顽顾顿颁颂预颅领颇颈颉颊颌颍颐频颓颖颗', + '题颚颜额颠颤風颯风飘飛飜飞食飢飯飲飼飽飾餅養餌餐餓館饗饥饬饭饮饰', + '饱饲饵饶饷饺饼饿馅馆馈馋馒首香馨馬馳馴駄駅駆駈駐駒駕駿騎騒験騰騷', + '驍驗驚马驭驮驯驰驱驳驴驶驹驻驼驾驿骂骄骆骇验骏骑骗骚骡骤骥骨骷骸', + '骼髄髅髓高髦髪髭髮鬓鬱鬼魁魂魄魅魏魔魚魯鮎鮮鯉鯛鯨鰯鱈鱒鱗鱼鲁鲍', + '鲜鲤鲨鲸鳃鳄鳌鳍鳖鳞鳥鳩鳳鳴鳶鴨鴻鵜鵬鶏鶴鷄鷗鷲鷹鷺鸟鸠鸡鸣鸥鸦', + '鸭鸯鸳鸽鸾鸿鹃鹅鹉鹊鹏鹤鹦鹰鹳鹿麒麓麗麟麦麹麺麻麽麿黄黎黏黑黒黔', + '默黙黛黝黯鼎鼓鼠鼻鼾齊齐齢齿龄龈龋龍龙龚龟 ', +].map(lengthChecker(32)).join(''); + +export const BASE_CHARS = [ + ' ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼', + ` !"#$%&'()*+,-./0123456789:;<=>?`, + '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_', + '`abcdefghijklmnopqrstuvwxyz{|}~⌂', +].map(lengthChecker(32)).join(''); + +export const CHARS = [ + ' ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼', + ` !"#$%&'()*+,-./0123456789:;<=>?`, + '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_', + '`abcdefghijklmnopqrstuvwxyz{|}~⌂', + 'ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒ', + 'áíóúñѪº¿®¬½¼¡«»АаБбВвГгДдЕеЁёЖж', + 'ЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦц', + 'ЧчШшЩщЪъЫыЬьЭэЮюЯяĄąĆćĘęŃńŚśŹźŻż', + 'ŁłÓÔŪū€·´°⚲⚥♡ß™ÁÃãÕõČčĎďĚěÍÚŇňŘř', + 'ŠšŤťŮůÝýŽžĹ弾ŔŕĞğİıŞşÀÈÌÒÙЀЁЂЃЄ', + 'ЅІЇЈЉЊЋЌЍЎЏѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤ', + 'ѥѰѱѲѳҊҋҌҍҎҏҐґ ĂăÂÎȘșȚțŊŋŐőŰűĀāĈĉ', + '■□▢▣▤▥▦▧▨▩▪▫▭▮▯▰▱△▴▵▶▷▸▹▻▽▾▿◀◁◂◃', + '◅◆◇◈◉◊◌◍◎●◐◑◒◓◔◕◖◗◚◛◜◝◞◟◠◡◢◣◤◥◦◧', + '◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿¸¾ÊËÏÐÛÞ', + 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩĊċĐđĒēĚě', + 'αβγδεζηθικλμνξοπρστυφχψωĖėĜĝĠġĢģ', + 'ĤĥĦħĨĩĪīĬĭĮįIJijĴĵĶķĸĻļĿŀŅņʼnŌōŎŏŒœ', + 'ŖŗŜŝŢţŦŧŨũŬŭŲųŴŵŶŷŸſ¢¤¥¦¨©¯±¹²³µ', + 'ð÷þĔĕ★☆✰✦✧卐卍❥ღஐϟ【】《》✿❀№♢♤♧✓✔✕✖✗✘', + 'ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただ', + 'ちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむ', + 'めもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ⦅⦆。「」、・ ̄ˊς', + 'ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダ', + 'チヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミム', + 'メモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ ', + '、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵 ', + '‐‑‒–—―‖‗‘’‚‛“”„‟‧⁇⁈⁉ ', + '⚧☿♁⚨⚩⚦⚢⚣⚤♔♕♚♛❣☀… ', + '👃🙂😵😠😐😑😆😟🙃 ', + ' ', + ' ', +].map(lengthChecker(32)).join(''); + +export const ROMAJI = [ + '\uff00!"#$%&'()*+,-./0123456789:;<=>?', + '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_', + '`abcdefghijklmnopqrstuvwxyz{|}~ ', +].map(lengthChecker(32)).join(''); + +export const EMOJI = [ + '😍😈👿🤔 ', + '❤💙💚💛💜🖤💔💖💗💕 ', + '🦋🕷🦇👻🐾🐈🐱😺😸😹😻😼😽🙀😿😾', + '🌸💮🌲🎄💐🌿🍎🍏🍊🏀🎃🍕🍬🍭🍡🌈', + '💎🥌🎁⛄❄🕯🎅🌠⭐🌟🎲✨⚡🔥🎵🎶', + '♈♉♊♋♌♍♎♏♐♑♒♓⛎👑☘🍀', + '🍪🥚🍐🥭🥕🍇⛏🍌 ', +].map(lengthChecker(16)).join(''); + +function isSurrogate(code: number) { + return code >= 0xd800 && code <= 0xdbff; +} + +function fromSurrogate(high: number, low: number) { + return ((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000; +} + +export function charsToCodes(text: string) { + const chars: number[] = []; + + for (let i = 0; i < text.length; i++) { + let code = text.charCodeAt(i); + + if (isSurrogate(code) && (i + 1) < text.length) { + code = fromSurrogate(code, text.charCodeAt(i + 1)); + i++; + } + + chars.push(code); + } + + return chars; +} + +export interface FontSprite { + code: number; + sprite: number; +} + +export function createFont( + canvas: ExtCanvas, w: number, h: number, addImage: (canvas: ExtCanvas) => number, + options: { noChinese?: boolean; mono?: number; onlyBase?: boolean; } = {}, +): FontSprite[] { + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const cols = canvas.width / w; + const baseCodes = charsToCodes(BASE_CHARS); + const charsCodes = charsToCodes(CHARS); + const chineseCodes = options.noChinese ? [] : charsToCodes(CHINESE); + const romajiCodes = charsToCodes(ROMAJI); + const codes = options.onlyBase ? [...baseCodes] : [...charsCodes, ...chineseCodes, ...romajiCodes]; + const baseCodesLength = charsCodes.length + chineseCodes.length; + const added = new Set(); + const arrows = new Set('↑↓'.split('').map(x => x.charCodeAt(0))); + + return compact(codes + .map((code, i) => { + if (i >= baseCodesLength) { + i = 32 + (i - baseCodesLength); + } + + const { x, y } = getXY(cols, i); + let { left, width } = getCharWidth(data, x, y, w, h); + const actualCode = i ? code : 0; + + if (arrows.has(code)) { + left -= 1; + width += 2; + } + + if (width !== 0 && options.mono !== undefined) { + left = 0; + width = options.mono; + } + + if (!width) { + return undefined; + } else if (added.has(actualCode)) { + // console.log('skipping character', actualCode); + return undefined; + } else { + const sprite = addImage(cropCanvas(canvas, x * w + left, y * h, width - left, h)); + added.add(actualCode); + return { code: actualCode, sprite }; + } + })) + .sort(compareFontSprite); +} + +export function createEmojis( + canvas: ExtCanvas, w: number, h: number, addImage: (canvas: ExtCanvas) => number +): FontSprite[] { + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const cols = canvas.width / w; + + return compact(charsToCodes(EMOJI) + .map((code, i) => { + const { x, y } = getXY(cols, i); + const { left, width } = getCharWidth(data, x, y, w, h); + + if (!code || code === 32) { + return undefined; + } + + if (!width) { + console.log('emoji not found in PSD', code, width); + return undefined; + } + + const sprite = addImage(cropCanvas(canvas, x * w + left, y * h, width - left, h)); + return { code, sprite }; + })) + .sort(compareFontSprite); +} + +export function fontSpritesToStringAndSprites(fontSprites: FontSprite[]): { chars: string; sprites: number[]; } { + const chars = fontSprites.map(s => String.fromCodePoint(s.code)).join(''); + const sprites = fontSprites.map(s => s.sprite); + return { chars, sprites }; +} + +function compareFontSprite(a: FontSprite, b: FontSprite) { + return a.code - b.code; +} + +function isColEmpty(data: ImageData, x: number, y: number, h: number) { + for (let yy = 0; yy < h; yy++) { + if (data.data[((y + yy) * data.width + x) * 4 + 3]) + return false; + } + + return true; +} + +function getXY(cols: number, i: number) { + return { x: i % cols, y: Math.floor(i / cols) }; +} + +function getCharWidth(data: ImageData, x: number, y: number, w: number, h: number) { + let left = 0; + let width = w; + + for (; left < w; left++) { + if (!isColEmpty(data, x * w + left, y * h, h)) { + break; + } + } + + for (; width > 0; width--) { + if (!isColEmpty(data, x * w + width - 1, y * h, h)) { + break; + } + } + + return { left, width }; +} + +function lengthChecker(expected: number) { + return function (line: string) { + const length = charsToCodes(line).length; + + if (length !== expected) + throw new Error(`Invalid line length (${length}/${expected}) in "${line}"`); + + return line; + }; +} diff --git a/src/ts/tools/create-sprites.ts b/src/ts/tools/create-sprites.ts new file mode 100644 index 0000000..eb31ae8 --- /dev/null +++ b/src/ts/tools/create-sprites.ts @@ -0,0 +1,1399 @@ +/* tslint:disable */ + +require('source-map-support').install(); + +(global as any).BETA = false; + +import * as fs from 'fs'; +import * as path from 'path'; +import { + max, range, flatten, mapValues, dropRightWhile, isEqual, uniq, times, compact, includes, toPairs, +} from 'lodash'; +import { + ColorShadow, Nose, Eye, Tree, Animation, ColorExtra, Button, Emote, Layer, Sprite, ExtCanvas, Psd, + Result, TileSprites +} from './types'; +import { + findLayer, mkdir, cartesian, findLayerSafe, getCanvas, getLayerCanvas, getLayerCanvasSafe, + getCanvasSafe, matcher, getDirectories, trimRight, addSprite, createSprite, + TRANSPARENT, BLACK, defaultPalette, WHITE, OUTLINE_COLOR, SHADE_COLOR, TONGUE_COLOR, MOUTH_COLOR, + TEETH_COLOR, TEETH_SHADE_COLOR, LIGHT_SHADE_COLOR, compareLayers, addImage, + addSpriteWithColors, createPixelSprites, nameMatches, getPatternCanvases, clipPattern +} from './common'; +import { createSpriteSheet, imageToSprite, saveSpriteSheet, saveCanvasAsRaw, saveSpriteSheetAsBinary } from './sprite-sheet'; +import { tilesToSprites } from './convert-tiles'; +import { createFont, FontSprite, createEmojis } from './create-font'; +import { + padCanvas, cropCanvas, mergeCanvases, colorCanvas, maskCanvas, saveCanvas, imageToCanvas, + isCanvasEmpty, forEachPixel, mapColors, createExtCanvas, forEachPixelOf2Canvases, + cropAndPadByColRow, cropByIndex, ByIndexGetter, reverseMaskCanvas, loadImage, mirrorCanvas, +} from './canvas-utils'; +import { openPsd, openPsdFiles } from './psd-utils'; +import { sheets, Sheet } from '../common/sheets'; +import { colorToCSS } from '../common/color'; +import { bitWriter } from '../common/bitUtils'; + +const head0Indices = [0, 1, 2, 4, 7, 8, 9, 10, 12, 13, 14, 16, 18, 19]; // regular +const head1Indices = [0, 1, 3, 5, 6, 8, 9, 11, 12, 13, 15, 17, 18, 19]; // clipped + +const MAX_PALETTE_SIZE = 128; +const { assetsPath } = require('../../../config.json'); +const rootPath = path.join(__dirname, '..', '..', '..'); +const sourcePath = path.join(assetsPath); +const generatedPath = path.join(rootPath, 'src', 'ts', 'generated'); +const outputPath = path.join(rootPath, 'tools', 'output'); +const destPath = path.join(rootPath, 'tools', 'output', 'images'); +const ponyPath = path.join(sourcePath, 'pony'); + +const shadowPalette = [TRANSPARENT, BLACK]; +const isPng = matcher(/\.png$/); +const getPngs = (directory: string) => fs.readdirSync(directory).filter(isPng); +const getFrames = (layers: Layer[]) => layers.filter(nameMatches(/^frame/)).sort(compareLayers); +const ponyPsd = (name: string) => openPsd(path.join(ponyPath, name)); + +function openPng(fileName: string) { + return imageToCanvas(loadImage(fileName)); +} + +function createPaletteFromList(canvases: ExtCanvas[]) { + return canvases.reduce((pal, can) => createPalette(can, pal), []); +} + +function createPaletteFromLayers(layers: Layer[]) { + return createPaletteFromList(layers.map(getCanvasSafe)); +} + +function addCMSprite(sprites: Sprite[], flip: boolean, ox = 43, oy = 49) { + const canvas = createExtCanvas(5, 5, 'cm'); + const context = canvas.getContext('2d')!; + const imageData = context.getImageData(0, 0, 5, 5); + + for (let i = 0, y = 0; y < 5; y++) { + for (let x = 0; x < 5; x++) { + imageData.data[i++] = flip ? (y * 5 + (4 - x)) : (y * 5 + x); + imageData.data[i++] = 255; + imageData.data[i++] = 0; + imageData.data[i++] = 255; + } + } + + context.putImageData(imageData, 0, 0); + sprites.push(createSprite(sprites.length, padCanvas(canvas, ox, oy), { x: ox, y: oy, w: 5, h: 5 })); + return sprites.length - 1; +} + +function createPalette(canvas: ExtCanvas, palette: number[] = []) { + forEachPixel(canvas, c => { + if (!includes(palette, c)) { + palette.push(c); + } + }); + + if (palette.length > MAX_PALETTE_SIZE) { + throw new Error(`Exceeded max palette size ${palette.length}/${MAX_PALETTE_SIZE} (${canvas.info})`); + } + + return palette.sort((a, b) => a - b); +} + +function splitButton(canvas: ExtCanvas, border: number) { + return { + topLeft: cropCanvas(canvas, 0, 0, border, border), + top: cropCanvas(canvas, border, 0, canvas.width - border * 2, border), + topRight: cropCanvas(canvas, canvas.width - border, 0, border, border), + left: cropCanvas(canvas, 0, border, border, canvas.height - border * 2), + bg: cropCanvas(canvas, border, border, canvas.width - border * 2, canvas.height - border * 2), + right: cropCanvas(canvas, canvas.width - border, border, border, canvas.height - border * 2), + bottomLeft: cropCanvas(canvas, 0, canvas.height - border, border, border), + bottom: cropCanvas(canvas, border, canvas.height - border, canvas.width - border * 2, border), + bottomRight: cropCanvas(canvas, canvas.width - border, canvas.height - border, border, border), + }; +} + +// main methods + +function getEyesFromPsd({ objects2, sprites }: Result, eyesPsd: Psd, irisesPsd: Psd) { + const eyeCount = 24; + const left = 20; + const top = 20; + const rightEyeWidth = 12; + const perLine = 10; + const h = 30, dx = 30, dy = 30; + const irisesCount = 8; + + const irises = colorCanvas(getLayerCanvasSafe('irises', irisesPsd), 'white'); + const whites = getLayerCanvasSafe('whites', eyesPsd); + const lineart = getLayerCanvasSafe('lineart', eyesPsd); + const eyeshadow = getLayerCanvasSafe('eyeshadow', eyesPsd); + const bases = [mergeCanvases(whites, lineart), ...findLayerSafe('eyelashes', eyesPsd).children + .sort(compareLayers) + .map(getCanvas) + .map(c => mergeCanvases(whites, lineart, c))]; + const shadow = colorCanvas(eyeshadow, 'white'); + const shine = mapColors(eyeshadow, c => c === 0xffffffff ? c : 0); + + const getRightEye = cropAndPadByColRow(left, top, rightEyeWidth, h, dx, dy, left, top); + const getLeftEye = cropAndPadByColRow(left + rightEyeWidth, top, 30 - rightEyeWidth, h, dx, dy, left + rightEyeWidth, top); + const getRight = cropByIndex(getRightEye, perLine); + const getLeft = cropByIndex(getLeftEye, perLine); + + // const mirrored = (get: ByIndexGetter): ByIndexGetter => (canvas, index) => mirrorCanvas(get(canvas, index), -15); + + const palette = [0, WHITE, BLACK]; // + + const getEye = (get: ByIndexGetter) => (i: number) => bases.map(base => { + const s = addSprite(sprites, get(shadow, i), undefined, palette); + + return { + base: addSprite(sprites, get(base, i), undefined, palette), + irises: range(0, irisesCount) + .map(j => maskCanvas(get(irises, j), get(whites, i))) + .map(canvas => addSprite(sprites, canvas, undefined, palette)), + shadow: s ? s : addSprite(sprites, get(shadow, 0), undefined, palette), + shine: s ? addSprite(sprites, get(shine, i), undefined, palette) : addSprite(sprites, get(shine, 0), undefined, palette), + }; + }); + + objects2['eyeRight: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(getRight))]; + objects2['eyeLeft: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(getLeft))]; + // objects2['eyeRight2: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(mirrored(getLeft)))]; +} + +function getBlushFromPsd({ sprites, objects2 }: Result, psd: Psd) { + objects2.blush = addSprite(sprites, getLayerCanvasSafe('color', psd)); +} + +function getPonyShadowsAndSelection({ sprites, objects2 }: Result, psd: Psd) { + const count = 5; + const shadow = colorCanvas(getLayerCanvasSafe('shadow', psd), 'white'); + const selection = getLayerCanvasSafe('selection', psd); + const crop = cropAndPadByColRow(0, 0, psd.width, 10, 0, 10); + + objects2.ponyShadows = times(count, i => crop(shadow, 0, i)).map(c => addSprite(sprites, c)); + objects2.ponySelections = times(count, i => crop(selection, 0, i)).map(c => addSprite(sprites, c)); +} + +function splitMuzzleMouth(canvas: ExtCanvas) { + const muzzleCanvas = mapColors(canvas, c => (c === WHITE || c === OUTLINE_COLOR || c === SHADE_COLOR) ? c : 0); + const mouthCanvas = mapColors(canvas, c => { + if (c === MOUTH_COLOR || c === TONGUE_COLOR) { + return c; + } else if (c === TEETH_COLOR) { + return WHITE; + } else if (c === TEETH_SHADE_COLOR) { + return LIGHT_SHADE_COLOR; + } else { + return 0; + } + }); + + return { mouthCanvas, muzzleCanvas }; +} + +function getMuzzlesFromPsd({ sprites, objects2 }: Result, psd: Psd) { + const columns = 5; + const typeCount = 2; + const expressionsPerLine = 5; + const expressionList = [ + 0, 1, 24, 4, 9, + 5, 2, 3, 8, 10, + 11, 12, 13, 14, 15, + 17, 18, 19, 20, 21, + 22, 23, 30, 32, 6, + 33, + ]; + + const { mouthCanvas, muzzleCanvas } = splitMuzzleMouth(getLayerCanvasSafe('muzzle', psd)); + const fangsCanvas = colorCanvas(getLayerCanvasSafe('fangs', psd), 'white'); + const noseCanvas = colorCanvas(getLayerCanvasSafe('nose', psd), 'white'); + const noseMuzzleCanvas = muzzleCanvas; // mergeCanvases(muzzleCanvas, noseCanvas); + const nosePatternCanvas = mergeCanvases(colorCanvas(muzzleCanvas, 'red'), colorCanvas(noseCanvas, '#00ff00')); + const getImage = cropAndPadByColRow(20, 20, 30, 30, 30, 30, 20, 20); + const muzzleIndices = [...range(0, typeCount), -1]; + + saveCanvas(path.join(outputPath, 'noseMuzzleCanvas.png'), noseMuzzleCanvas); + saveCanvas(path.join(outputPath, 'nosePatternCanvas.png'), nosePatternCanvas); + + // [expression][type][pattern] + objects2['noses: PonyNose[][][]'] = expressionList.map(expression => muzzleIndices.map(type => { + const x = expression % expressionsPerLine + (type > 0 ? type : 0) * columns; + const y = Math.floor(expression / expressionsPerLine); + const fangs = addSprite(sprites, getImage(fangsCanvas, x, y)) || 0; + const mouth = addSprite(sprites, getImage(mouthCanvas, x, y), undefined, defaultPalette); + + if (type < 0) { + const color = getImage(noseMuzzleCanvas, x, y); + const pattern = getImage(nosePatternCanvas, x, y); + return [{ ...addSpriteWithColors(sprites, color, pattern), mouth, fangs }]; + } else { + return [{ color: addSprite(sprites, getImage(muzzleCanvas, x, y)), colors: 3, mouth, fangs }]; + } + })); +} + +interface SetImportInfo { + name: string; + layerName: string; + mask?: string; + reverse?: boolean; + maskFile?: string; + mirror?: boolean; + mirrorOffsetX?: number; +} + +function getSetNameLayer(sheet: Sheet): SetImportInfo[] { + const layersWithSets = sheet.layers.filter(l => l.set); + + return [ + ...layersWithSets.map(l => ({ + name: l.set!, + layerName: l.name + })), + ...layersWithSets.filter(l => !!l.importMirrored).map(l => ({ + name: l.importMirrored!.fieldName!, + layerName: l.name, + mirror: true, + mirrorOffsetX: l.importMirrored!.offsetX, + })), + ]; +} + +function importSprites({ sprites, objects2 }: Result, sheet: Sheet) { + const psd = ponyPsd(`${sheet.file}.psd`); + const { width, height, offset, offsetY = 0, padLeft = 0, padTop = 20, wrap = 0, importOffsets } = sheet; + const setsWithEmpties = sheet.setsWithEmpties; + let frameCount = Math.floor(psd.width / offset); + let typeCount = Math.floor(psd.height / height); + let getImage = cropAndPadByColRow(padLeft, 0, width, height, offset, height, 10 + padLeft, padTop - offsetY); + let hasExtra = false; + + if (wrap) { + typeCount = typeCount * frameCount; + frameCount = 1; + const oldGet = getImage; + getImage = (canvas, _, type) => oldGet(canvas, type % wrap, Math.floor(type / wrap)); + } else if (sheet.single) { + typeCount = typeCount * frameCount; + frameCount = 1; + const oldGet = getImage; + getImage = (canvas, _, type) => oldGet(canvas, type, 0); + } + + const sets: SetImportInfo[] = [ + ...getSetNameLayer(sheet), + ...(sheet.masks || []), + ]; + + const maskFiles: any = {}; + + if (sheet.masks) { + compact(sheet.masks.map(m => m.maskFile)) + .forEach(file => maskFiles[file] = openPsd(path.join(ponyPath, file + '.psd'))); + } + + const animations = sets.map(({ layerName, name, mask, reverse, maskFile, mirror, mirrorOffsetX }) => { + const layer = findLayerSafe(layerName, psd); + let color = getLayerCanvasSafe('color', layer); + const extraCanvas = sheet.extra ? getLayerCanvas('extra', layer) : undefined; + const patterns = getPatternCanvases(layer); + + if (mask) { + const maskColor = getLayerCanvasSafe(mask, maskFiles[maskFile || ''] || psd); + const maskRepeated = createExtCanvas(psd.width, psd.height, `${maskColor.info} repeated`); + const maskContext = maskRepeated.getContext('2d')!; + + for (let i = 0; i < typeCount; i++) { + maskContext.drawImage(maskColor, 0, i * (height + offsetY)); + } + + color = maskCanvas(color, reverse ? reverseMaskCanvas(maskRepeated) : maskRepeated); + } + + // [frame][type][pattern] + const frames = trimRight(range(0, frameCount).map(frame => { + return trimRight(range(0, typeCount).map(type => { + const { x, y } = importOffsets && importOffsets[frame] || { x: 0, y: 0 }; + const getAndPadBase = (canvas: ExtCanvas) => padCanvas(getImage(canvas, frame, type), -x, -y); + const getAndPad = mirror ? (canvas: ExtCanvas) => mirrorCanvas(getAndPadBase(canvas), mirrorOffsetX) : getAndPadBase; + const accessoryFrame = getAndPad(color); + const extraFrame = extraCanvas && getAndPad(extraCanvas); + + if (isCanvasEmpty(accessoryFrame)) { + return null; + } else { + let extraProps: any = {}; + + if (extraFrame) { + const palette = createPalette(extraFrame); + const extra = addSprite(sprites, extraFrame, undefined, palette); + extraProps = { extra, palette }; + hasExtra = true; + } + + const patternCanvases = patterns + .map(getAndPad) + .map(pattern => clipPattern(accessoryFrame, pattern)); + + return dropRightWhile(patternCanvases, isCanvasEmpty) + .map(patternFrame => { + ...addSpriteWithColors(sprites, accessoryFrame, patternFrame), + ...extraProps + }); + } + })); + })); + + return { name, frames }; + }); + + // fill-in missing types + if (animations.length > 1) { + animations.forEach(({ frames }) => { + frames.forEach((types, i) => { + const typeCount = max(animations.map(({ frames }) => frames[i] ? frames[i]!.length : 0))!; + + while (types && types.length < typeCount) { + types.push([]); + } + }); + }); + } + + // fix pattern color counts + if (sheet.single) { + const groups = sheet.groups || [animations.map(a => a.name)]; + const colorCounts: number[][][] = groups.map(() => []); // [group][type][pattern] + + animations.forEach(({ name, frames }) => { + const gi = groups.findIndex(g => includes(g, name)); + const groupColorCounts = colorCounts[gi]; + + frames.forEach(types => { + (types || []).forEach((patterns, ti) => { + groupColorCounts[ti] = groupColorCounts[ti] || []; + + (patterns || []).forEach((pattern, pi) => { + if (pattern) { + groupColorCounts[ti][pi] = Math.max(pattern.colors, groupColorCounts[ti][pi] || 0); + } + }); + }); + }); + }); + + animations.forEach(({ name, frames }) => { + const gi = groups.findIndex(g => includes(g, name)); + const groupColorCounts = colorCounts[gi]; + + frames.forEach(types => { + (types || []).forEach((patterns, ti) => { + (patterns || []).forEach((pattern, pi) => { + if (pattern) { + pattern.colors = groupColorCounts[ti] && groupColorCounts[ti][pi] || 0; + } + }); + }); + }); + }); + } + + animations.map(({ name, frames }) => { + if (sheet.empties && setsWithEmpties && includes(setsWithEmpties, name)) { + for (let i = 0; i < frames.length; i++) { + frames[i] = frames[i] && frames[i]!.filter((_, j) => !includes(sheet.empties, j)); + } + } + + if (sheet.single) { + objects2[`${name}: StaticSprites${hasExtra ? 'Extra' : ''}`] = frames[0]; + } else { + objects2[`${name}: AnimatedSprites`] = frames; + } + }); +} + +function getTreesFromPsd({ sprites, objects2 }: Result, psd: Psd, name: string, palettes?: number[][]) { + const groups = psd.children.filter(c => c.children && c.children.length).sort(compareLayers); + const width = psd.width / groups.length; + const spr = (name: string, index: number, palette: number[], parent: Layer) => { + const canvas = getLayerCanvasSafe(name, parent); + const cropped = cropCanvas(canvas, width * index, 0, width, canvas.height); + return addSprite(sprites, cropped, undefined, palette); + }; + + const children = flatten(groups.map(c => c.children)); + const trunkPalettes = palettes || [createPaletteFromLayers(children.filter(l => /^(stump|trunk)$/.test(l.name)))]; + const crownPalettes = palettes || [createPaletteFromLayers(children.filter(l => /crown/.test(l.name)))]; + + const hasStumpWinter = children.some(l => /stump winter/.test(l.name)); + const stumpWinterPalettes = hasStumpWinter ? [createPaletteFromLayers(children.filter(l => /stump winter/.test(l.name)))] : []; + + groups.forEach((group, index) => { + objects2[`${name}Stump${index}`] = { + color: spr('stump', index, trunkPalettes[0], group), + //shadow: spr('stump shadow', index, shadowPalette, group), + palettes: trunkPalettes, + }; + + if (hasStumpWinter) { + objects2[`${name}StumpWinter${index}`] = { + color: spr('stump winter', index, stumpWinterPalettes[0], group), + //shadow: spr('stump shadow', index, shadowPalette, group), + palettes: stumpWinterPalettes, + }; + } + + if (findLayer('trunk', group)) { + objects2[`${name}Trunk${index}`] = { + color: spr('trunk', index, trunkPalettes[0], group), + palettes: trunkPalettes, + }; + } + + const crown = spr('crown', index, crownPalettes[0], group); + + for (let i = 0; i < groups.length; i++) { + objects2[`${name}Crown${index}_${i}`] = { + color: crown, + shadow: spr(`shadow ${index + 1}`, i, shadowPalette, groups[i]), + palettes: crownPalettes, + }; + } + }); +} + +function getTreesOrObjectFromPsd(result: Result, psd: Psd, name: string, palettes: number[][]) { + if (findLayer('color', psd)) { + const color = getLayerCanvas('color', psd); + const shadow = getLayerCanvas('shadow', psd); + addColorShadow(result, name, color, shadow, palettes); + } else { + getTreesFromPsd(result, psd, name, palettes); + } +} + +function getTreeStagesFromPsds(result: Result, psds: Psd[], name: string) { + const groups = range(1, 3).map(i => `tree ${i}`); + const layers = ['crown', 'trunk', 'stump']; + const palettes = createPalettes(psds, ['color', ...cartesian(groups, layers).map(([a, b]) => `${a}/${b}`)]); + + psds + .filter(psd => !isPalettePsd(psd)) + .forEach(psd => getTreesOrObjectFromPsd(result, psd, `${name}_${psd.name}`, palettes)); +} + +function getTreesFromPsds(result: Result, directory: string) { + getDirectories(directory) + .filter(dir => !/^_/.test(path.basename(dir))) + .forEach(dir => getTreeStagesFromPsds(result, openPsdFiles(dir, /\d+\.psd$/), path.basename(dir))); +} + +function addColorShadow( + { sprites, objects2 }: Result, name: string, color: ExtCanvas | undefined, + shadow: ExtCanvas | undefined, palettes: number[][] | undefined +) { + objects2[name] = { + color: addSprite(sprites, color, undefined, palettes && palettes[0]), + shadow: addSprite(sprites, shadow, undefined, shadowPalette), + palettes, + }; +} + +function getObjectFromPsd(result: Result, psd: Psd, name: string) { + const color = getLayerCanvas('color', psd); + const shadow = getLayerCanvas('shadow', psd); + const palettes = color ? [createPalette(color)] : undefined; + addColorShadow(result, name, color, shadow, palettes); +} + +function createOtherPalette(basePalette: number[], base: ExtCanvas, color: ExtCanvas, palette: number[]): number[] { + forEachPixelOf2Canvases(base, color, (b, c) => { + if (b !== c) { + const index = basePalette.indexOf(b); + + if (index === -1) { + throw new Error(`cannot find color in palette`); + } + + palette[index] = c; + } + }); + + return palette; +} + +function otherPaletteFromPsd(basePalette: number[], basePsd: Psd, palettePsd: Psd, palette: number[], layers: string[]) { + return layers.reduce((palette, layer) => { + const base = getLayerCanvas(layer, basePsd); + const pal = getLayerCanvas(layer, palettePsd); + return base && pal ? createOtherPalette(basePalette, base, pal, palette) : palette; + }, palette); +} + +function isPalettePsd(psd: Psd) { + return /^palette_/.test(psd.name); +} + +function getLayerCanvases(names: string[], psd: Psd) { + return compact(names.map(name => getLayerCanvas(name, psd))); +} + +function createPalettes(psds: Psd[], layers: string[]) { + const main = psds.filter(psd => !isPalettePsd(psd)); + const canvases = flatten(main.map(psd => getLayerCanvases(layers, psd))); + const palette = createPaletteFromList(canvases); + const otherPalettes = psds + .filter(isPalettePsd) + .map(psd => { + const [, index, name] = psd.name.split('_'); + return { index: +index, name, psd }; + }); + const paletteCount = max(otherPalettes.map(p => p.index)) || 0; + const other = range(1, paletteCount + 1) + .map(i => otherPalettes.filter(p => p.index === i)) + .filter(x => x.length) + .map(x => x.map(({ name, psd }) => ({ base: main.find(x => x.name === name)!, psd }))) + .map(x => x.reduce((pal, { base, psd }) => otherPaletteFromPsd(palette, base, psd, pal, layers), palette.slice())); + + return [palette, ...other]; +} + +function getObjectGroupFromPsd(result: Result, psds: Psd[], dir: string) { + const palettes = createPalettes(psds, ['color']); + + psds + .filter(psd => !isPalettePsd(psd)) + .forEach(psd => { + const color = getLayerCanvas('color', psd); + const shadow = getLayerCanvas('shadow', psd); + addColorShadow(result, `${dir}_${psd.name}`, color, shadow, palettes); + }); +} + +function getObjectsFromPsds(result: Result, directory: string) { + openPsdFiles(directory, /psd$/) + .forEach(psd => getObjectFromPsd(result, psd, psd.name)); + + getDirectories(directory) + .filter(dir => !/^_/.test(path.basename(dir))) + .forEach(dir => { + const files = openPsdFiles(dir, /psd$/) + .filter(psd => !/^_/.test(path.basename(psd.name))); + + getObjectGroupFromPsd(result, files, path.basename(dir)); + }); +} + +function createOtherSprites({ objects, images }: Result, directory: string) { + getPngs(directory).forEach(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + objects[name] = addImage(images, canvas); + }); +} + +function createOtherSpritesPalette({ objects2, sprites }: Result, directory: string) { + getPngs(directory).forEach(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + const palette = createPalette(canvas); + const color = addSprite(sprites, canvas, undefined, palette); + objects2[name + '_2'] = { color, palette }; + }); +} + +function createIcons({ objects2, sprites }: Result, directory: string) { + getPngs(directory).forEach(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + objects2[name] = addSprite(sprites, canvas, undefined, defaultPalette); + }); +} + +function createOtherSpritesAnimations({ objects, images }: Result, directory: string) { + openPsdFiles(directory).forEach(psd => { + const canvases = getFrames(psd.children).map(getCanvas); + const frames = canvases.map(c => c ? addImage(images, c) : 0); + objects[psd.name] = { frames }; + }); +} + +function createButtons({ objects, objects2, images, sprites }: Result, directory: string) { + getPngs(directory).forEach(f => { + const [, name, borderText] = /^(.+)-(\d+)$/.exec(path.basename(f, '.png')) as string[]; + const border = +borderText; + const canvas = openPng(path.join(directory, f)); + const canvases = splitButton(canvas, border); + objects[name] = { border, ...mapValues(canvases, c => addImage(images, c)) }; + + const palette = createPalette(canvas); + objects2[name + '_2'] = { border, palette, ...mapValues(canvases, c => addSprite(sprites, c, undefined, palette)) }; + }); +} + +const lightsPad = 4; + +function createLights({ objects, images }: Result, directory: string) { + return getPngs(directory).map(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + return objects[name] = addImage(images, padCanvas(canvas, lightsPad, lightsPad, lightsPad, lightsPad, 'black')); + }); +} + +function createAnimations({ objects2, sprites }: Result, directory: string) { + getPngs(directory).forEach(f => { + const [, name, w, h] = /^(.+)-(\d+)x(\d+)\.png$/.exec(f)!; + const canvas = imageToCanvas(loadImage(path.join(directory, f))); + const spriteWidth = canvas.width / +w; + const spriteHeight = canvas.height / +h; + const palette = createPalette(canvas); + const frames = cartesian(range(+w), range(+h)) + .map(([x, y]) => cropCanvas(canvas, x * spriteWidth, y * spriteHeight, spriteWidth, spriteHeight)) + .map(bitmap => addSprite(sprites, bitmap, undefined, palette)); + + objects2[name] = { frames, palette } as Animation; + }); + + openPsdFiles(directory).forEach(psd => { + const canvases = getFrames(psd.children).map(getCanvas); + const palette = createPaletteFromList(compact(canvases)); + const frames = canvases.map(c => addSprite(sprites, c, undefined, palette)); + const shadowLayer = findLayer('shadow', psd); + const shadow = shadowLayer ? addSprite(sprites, getCanvas(shadowLayer), undefined, shadowPalette) : undefined; + + objects2[psd.name] = { frames, palette, shadow } as Animation; + }); + + getDirectories(directory) + .filter(dir => !/^_/.test(path.basename(dir))) + .forEach(dir => { + const psds = openPsdFiles(dir); + const dirName = path.basename(dir); + const canvases = flatten(psds.map(psd => compact(psd.children.filter(x => /^frame/i.test(x.name)).map(x => getCanvas(x))))); + const palette = createPaletteFromList(canvases); + + for (const psd of psds) { + const canvases = getFrames(psd.children).map(getCanvas); + const frames = canvases.map(c => addSprite(sprites, c, undefined, palette)); + const shadowLayer = findLayer('shadow', psd); + const shadow = shadowLayer ? addSprite(sprites, getCanvas(shadowLayer), undefined, shadowPalette) : undefined; + + objects2[`${dirName}_${psd.name}`] = { frames, palette, shadow } as Animation; + } + }); +} + +function createEmoteAnimations({ objects2, sprites }: Result, directory: string) { + openPsdFiles(directory).forEach(psd => { + const frames = getFrames(psd.children) + .map(getCanvasSafe) + .map(c => addSprite(sprites, c, undefined, defaultPalette)); + + objects2[psd.name] = { frames, palette: [] } as Animation; + }); +} + +// .ts file generation + +const palettes = [defaultPalette]; + +function createObject(name: string, value: any) { + if (!/^[a-z_][a-z0-9_]*(: [a-z]+(\[\])*)?$/i.test(name)) { + throw new Error(`Invalid sprite name (${name})`); + } + + return `export const ${name} = ${obj(value, name, true)};\n`; +} + +function encodeArray(items: any[]) { + return `[${items.join(', ')}]`; +} + +function addPalette(palette: number[]) { + const index = palettes.findIndex(p => isEqual(p, palette)); + const paletteIndex = index === -1 ? (palettes.push(palette) - 1) : index; + return paletteIndex; +} + +function addPalettes(palettes: number[][]) { + return `[${palettes.map(addPalette).join(', ')}]`; +} + +function obj(value: any, name: string, indent = false): string { + if (value == null) { + return 'undefined'; + } else if (typeof value === 'string') { + return value; + } else if (typeof value === 'number') { + return `sprites[${value.toString()}]`; + } else if (Array.isArray(value)) { + if (/: StaticSprites(Extra)?$/.test(name)) { + // [type][pattern] + const types = value as (({ color: number; colors: number; extra?: number; palette?: number[]; } | null)[] | null)[]; + const key = name.replace(/: StaticSprites(Extra)?/, ''); + const lines: string[] = []; + // const typeCount = types.length; + // const patternCounts = types.map(type => type ? type.length : 0); + // const patternCountMax = max(patternCounts)!; + // const emptyLine = times(patternCountMax, () => 0); + // const colorsCounts = types.map(type => { + // type = type || []; + // // console.log(type); + // return [...type.map(p => p!.colors), ...emptyLine.slice(0, patternCountMax - type.length)]; + // }); + + // lines.push(`/* NEW */ export const ${key}SpritesArray = mapSprites2([\n${ + // types.map(patterns => ` ${ + // padArray(patterns, patternCountMax, { color: 0, colors: 0 }).map(x => x!.color).join(', ') + // },`).join('\n') + // }\n]);`); + + // lines.push(`/* NEW */ export const ${key}TypeCount = ${typeCount};`); + // lines.push(`/* NEW */ export const ${key}PatternCounts = [${patternCounts.join(', ')}];`); + // lines.push(`/* NEW */ export const ${key}PatternCountsMax = ${patternCountMax};`); + // lines.push(`/* NEW */ export const ${key}PatternColorCounts = [\n` + + // `${colorsCounts.map(counts => ` ${counts.join(', ')},`).join('\n')}` + + // `\n];`); + + if (/Extra$/.test(name)) { + lines.push(`export const ${key}Extra: StaticSprites = [\n${ + types.map(patterns => patterns ? + `\t[${patterns.map(x => x!.extra ? + `createColorPalette(${x!.extra}, [${addPalette(x!.palette!)}])` : + 'emptyColorPalette()').join(', ')}],` : + '\tundefined,' + ).join('\n')}\n];`); + } + + const items = `\n${value.map((x, i) => '\t' + obj(x, `${name}[${i}]`)).join(',\n')}\n`; + return `[${indent ? items : items.replace(/\t/g, '').replace(/\n/g, ' ').trim()}];` + + '\n' + lines.join('\n') + '\n'; + } else { + const items = `\n${value.map((x, i) => '\t' + obj(x, `${name}[${i}]`)).join(',\n')}\n`; + return `[${indent ? items : items.replace(/\t/g, '').replace(/\n/g, ' ').trim()}]`; + } + } else { + return createObj(value, name); + } +} + +function encodeColor(value: number) { + const alpha = value & 0xff; + + if (alpha === 0) { + return '0'; + } else if (alpha !== 0xff) { + return value.toString(16).padStart(8, '0'); + } else { + return (value >>> 8).toString(16).padStart(6, '0'); + } +} + +function createObj( + s: ColorShadow & Nose & Eye & Tree & Animation & ColorExtra & TileSprites & Button & Emote, name: string +) { + if (s.frames && s.palette && s.shadow) { + return `createAnimationShadow(${encodeArray(s.frames)}, ${s.shadow}, ${addPalette(s.palette)})`; + } else if (s.frames && s.palette) { + return `createAnimationPalette(${encodeArray(s.frames)}, ${addPalette(s.palette)})`; + } else if (s.frames) { + return `createAnimation(${encodeArray(s.frames)})`; + } else if (s.fangs != null) { + return `createNose(${s.color}, ${s.colors}, ${s.mouth}, ${s.fangs})`; + } else if (s.color && s.colors && s.extra && s.palette) { + return `createColorExtraPal(${s.color}, ${s.colors}, ${s.extra}, [${addPalette(s.palette)}])`; + } else if (s.color && s.colors) { + return `colorPal${s.colors}(${s.color})`; + } else if (s.base && s.irises != null) { + return `createEye(${s.base}, ${encodeArray(s.irises)}, ${s.shadow}, ${s.shine})`; + } else if (s.color && s.shadow && s.palettes) { + return `createColorShadowPalette(${s.color}, ${s.shadow}, ${addPalettes(s.palettes)})`; + } else if (s.color && s.palettes) { + return `createColorPalette(${s.color}, ${addPalettes(s.palettes)})`; + } else if (s.sprites && s.palettes) { + return `createSpritesPalette(${encodeArray(s.sprites)}, ${addPalettes(s.palettes)})`; + } else if (s.color && s.palette) { + return `/* no palettes */ createColorPalette(${s.color}, [${addPalette(s.palette)}])`; + } else if (s.color && s.shadow) { + return `createColorShadow(${s.color}, ${s.shadow})`; + } else if (s.color) { + return `createColor(${s.color})`; + } else if (s.shadow) { + return `createShadow(${s.shadow})`; + } else if (s.topLeft) { + return `createButton(${s.border}, ${s.topLeft}, ${s.top}, ${s.topRight}, ${s.left}, ${s.bg},` + + ` ${s.right}, ${s.bottomLeft}, ${s.bottom}, ${s.bottomRight})`; + } else if (s.name && s.sprite) { + return `createEmote('${s.name}', ${s.sprite})`; + } else { + throw new Error(`Failed '${name}' createSprite(${JSON.stringify(s)})`); + } +} + +function spriteType({ shade, layer }: Sprite) { + return shade ? (layer ? layer - 1 : (layer || 0)) : (3 + (layer || 0)); +} + +let minOX = 0, minOY = 0; +let maxOX = 0, maxOY = 0; +let maxW = 0, maxH = 0; + +function toHex(value: number): string { + const result = value.toString(16); + return result.length === 1 ? `0${result}` : result; +} +function encodeSprite(s: Sprite) { + if (s.x > 0xfff || s.y > 0xfff || s.ox > 0xff || s.oy > 0xff || s.w > 0x1ff || s.h > 0x1ff || spriteType(s) > 0x3f) { + throw new Error(`Invalid sprite (${s})`); + } + + const buffer = bitWriter(write => { + write(s.x, 12); + write(s.y, 12); + write(s.w, 9); + write(s.h, 9); + write(s.ox, 8); + write(s.oy, 8); + write(spriteType(s), 6); + }); + + if (buffer.length !== 8) { + throw new Error(`Invalid encoded sprite length (${buffer.length} !== 8)`); + } + + let result = ''; + + for (let i = 0; i < buffer.length; i++) { + result += toHex(buffer[i]); + } + + return result; +} + +function toSpritesArray(sprites: (Sprite | null)[]): string { + const index = sprites.findIndex((s, i) => !!i && (!s || !s.w || !s.h)); + + if (index !== -1) { + console.error(`Invalid sprite at ${index}`, sprites[index]); + throw new Error(`Invalid sprite at ${index}`); + } + + return compact(sprites) + .filter(s => s.w && s.h) + .map(s => { + maxW = Math.max(maxW, s.w); + maxH = Math.max(maxH, s.h); + minOX = Math.min(minOX, s.ox); + maxOX = Math.max(maxOX, s.ox); + minOY = Math.min(minOY, s.oy); + maxOY = Math.max(maxOY, s.oy); + return encodeSprite(s); // `\t${s.x}, ${s.y}, ${s.w}, ${s.h}, ${s.ox}, ${s.oy}, ${spriteType(s)},\n`; + }) + .join('') + .trim(); +} + +interface FontCharGroup { + firstCode: number; + lastCode: number; + sprites: FontSprite[]; +} + +function groupFont(fontSprites: FontSprite[], maxWaste: number) { + const groups: FontCharGroup[] = []; + let group: FontCharGroup | undefined = undefined; + + for (const codeSprite of fontSprites) { + if (!group || (codeSprite.code - group.lastCode) > maxWaste) { + group = { firstCode: codeSprite.code, lastCode: codeSprite.code, sprites: [] }; + groups.push(group); + } + + while ((codeSprite.code - group.lastCode) > 1) { + group.sprites.push({ sprite: 0, code: 0 }); + group.lastCode++; + } + + group.sprites.push(codeSprite); + group.lastCode = codeSprite.code; + } + + return groups; +} + +function createFontCode(name: string, fontSprites: FontSprite[], sprites: string) { + const groups = groupFont(fontSprites, 5); + + return `export const ${name} = createFont(${sprites}, [\n` + + `${groups.map(g => ` [${g.firstCode}, [${g.sprites.map(s => s.sprite).join(', ')}]],`).join('\n')}\n]);`; +} + +interface SpriteTSConfig { + spriteFileName: string; + paletteFileName: string; + paletteAlphaFileName: string; + sprites: (Sprite | null)[]; + paletteSprites: (Sprite | null)[]; + result: Result; + palettes: number[][]; + fonts: { [key: string]: FontSprite[]; }; + fontsPal: { [key: string]: FontSprite[]; }; + namedPalettes: { [key: string]: number; }; +} + +function createSpritesTS(dest: string, config: SpriteTSConfig) { + const { objects, objects2 } = config.result; + + let ts = fs.readFileSync(path.join(rootPath, 'src', 'ts', 'tools', 'sprites-template.ts'), 'utf8'); + ts = ts.replace(/export \{.+?\r\n/, ''); + ts = ts.replace('/*SPRITE_SHEET*/', `images/${config.spriteFileName}`); + ts = ts.replace('/*SPRITE_SHEET_PALETTE*/', `images/${config.paletteFileName}`); + ts = ts.replace('/*SPRITE_SHEET_PALETTE_ALPHA*/', `images/${config.paletteAlphaFileName}`); + ts = ts.replace('/*SPRITES*/', toSpritesArray(config.sprites)); + ts = ts.replace('/*SPRITES_PALETTE*/', toSpritesArray(config.paletteSprites)); + ts = ts.replace('/*FONTS*/', [ + Object.keys(config.fonts).map(key => createFontCode(key, config.fonts[key], 'sprites')).join('\n\n'), + Object.keys(config.fontsPal).map(key => createFontCode(key, config.fontsPal[key], 'sprites2')).join('\n\n'), + ].join('\n\n')); + + ts += Object.keys(objects).map(key => createObject(key, objects[key])).join(''); + ts += Object.keys(objects2).map(key => createObject(key, objects2[key])).join('').replace(/sprites/g, 'sprites2'); + + const colors = uniq(flatten(config.palettes)).sort((a, b) => a - b); + const palettesCode = config.palettes + .map(p => p.map(c => colors.indexOf(c)).join(', ')) + .map(p => `\t[${p}]`) + .join(',\n') + .trim(); + + ts = ts.replace('/*COLORS*/', colors.map(encodeColor).join(' ')); + ts = ts.replace('/*PALETTES*/', palettesCode); + ts = ts.replace('/*NAMED_PALETTES*/', toPairs(config.namedPalettes) + .map(([key, value]) => `export const ${key} = palettes[${value}];`).join('\n')); + ts = ts.replace('/*NAMED_SPRITES*/', `export const emptySprite = sprites[0];\nexport const emptySprite2 = sprites2[0];`); + + ts += ` +export const head0 = [ + undefined, + [[${head0Indices.join(', ')}].map(i => head[1]![0]![i])], +]; + +export const head1 = [ + undefined, + [[${head1Indices.join(', ')}].map(i => head[1]![0]![i])], +]; + `; + + fs.writeFileSync(dest, ts.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'), 'utf8'); +} + +function fixPixelRect(sprites: (Sprite | null)[], objects: any, srcName: string, dstName: string) { + const r = sprites[objects[srcName]]!; + sprites.push({ x: r.x + 1, y: r.y + 1, w: 1, h: 1, ox: 0, oy: 0, layer: r.layer, image: null as any }); + objects[dstName] = sprites.length - 1; +} + +export function getFramesFromPSD({ sprites }: Result, psd: Psd, padY = 5): any[] { + return findLayerSafe('frames', psd).children + .slice() + .sort(compareLayers) + .map(getCanvasSafe) + .map(canvas => { + const cropped = padCanvas(canvas, 0, padY); + const pattern = clipPattern(cropped, colorCanvas(cropped, 'red')); + return addSpriteWithColors(sprites, cropped, pattern); + }); +} + +// main + +function createResult(): Result { + return { + objects: {}, + objects2: {}, + images: [createExtCanvas(1, 1, 'empty')], + sprites: [{ image: createExtCanvas(1, 1, 'empty'), x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0 }], + }; +} + +function createPonySprites(result: Result) { + getEyesFromPsd(result, ponyPsd('eyes.psd'), ponyPsd('irises.psd')); + getMuzzlesFromPsd(result, ponyPsd('muzzles.psd')); + getBlushFromPsd(result, ponyPsd('blush.psd')); + getPonyShadowsAndSelection(result, ponyPsd('shadows.psd')); + + result.objects2.cms = addCMSprite(result.sprites, false); + result.objects2.cmsFlip = addCMSprite(result.sprites, true); + + sheets + .filter(s => 'name' in s && !!s.file && !s.skipImport) + .forEach(s => importSprites(result, s as Sheet)); + + // const bug = getFramesFromPSD(result, ponyPsd('fly-bug.psd')); + // result.objects2['wings: AnimatedSprites'].slice(3) + // .forEach((frame: any, i: number) => frame[1] = [pegasus[i]]); + + getTreesFromPsds(result, path.join(sourcePath, 'trees')); + getObjectsFromPsds(result, path.join(sourcePath, 'objects')); + + createButtons(result, path.join(sourcePath, 'buttons')); + createAnimations(result, path.join(sourcePath, 'animations')); + createOtherSprites(result, path.join(sourcePath, 'sprites')); + createOtherSpritesPalette(result, path.join(sourcePath, 'sprites-palette')); + createIcons(result, path.join(sourcePath, 'icons')); + createOtherSpritesAnimations(result, path.join(sourcePath, 'sprites-animations')); + createEmoteAnimations(result, path.join(sourcePath, 'emotes')); + createWalls(result, path.join(sourcePath, 'walls')); + + createPixelSprites(result); +} + +interface WallConfig { + thickness: number; + fullHeight: number; + halfHeight: number; + fullHeightVertical: number; + halfHeightVertical: number; +} + +function createWalls(result: Result, rootPath: string) { + createWall(result, 'wall_wood', openPsd(path.join(rootPath, 'wood.psd')), { + thickness: 8, fullHeight: 85, halfHeight: 22, fullHeightVertical: 97, halfHeightVertical: 34, + }); + + createWall(result, 'wall_stone', openPsd(path.join(rootPath, 'stone.psd')), { + thickness: 8, fullHeight: 85, halfHeight: 22, fullHeightVertical: 97, halfHeightVertical: 34, + }); +} + +function createWall(result: Result, name: string, psd: Psd, config: WallConfig) { + const full = getCanvasSafe(findLayerSafe('full', psd)); + const half = getCanvasSafe(findLayerSafe('half', psd)); + const palette = createPaletteFromList([full, half]); + const h0wall = 16; + const v0wall = 17; + const lcut = 18; + const rcut = 19; + const thickness = config.thickness; + const map: { x: number; w: number; vertical: boolean; }[] = []; + let offset = 0; + + function push(index: number, w: number, vertical = false) { + if (map[index]) { + throw new Error('Already taken'); + } + + map[index] = { x: offset, w, vertical }; + offset += w; + } + + function gap() { + offset++; + } + + function createSprites(canvas: ExtCanvas, y: number, height: number, verticalHeight: number) { + return map + .map(({ x, w, vertical }) => cropCanvas(canvas, x, y, w, vertical ? verticalHeight : height)) + .map(part => ({ palette, color: addSprite(result.sprites, part, undefined, palette) })); + } + + // 0b top right bottom left + push(0b0100, thickness); + push(h0wall, 32 - thickness); + push(0b0101, thickness); + push(0b0001, thickness); + gap(); + push(0b0000, thickness); + gap(); + push(0b0010, thickness); + gap(); + push(0b1000, thickness); + gap(); + push(0b1100, thickness); + push(0b1101, thickness); + push(0b1001, thickness); + gap(); + push(0b0110, thickness); + push(0b0111, thickness); + push(0b0011, thickness); + gap(); + push(0b1010, thickness); + gap(); + push(0b1110, thickness); + push(0b1111, thickness); + push(0b1011, thickness); + gap(); + push(v0wall, thickness, true); + gap(); + push(lcut, 32 - thickness); + push(rcut, 32 - thickness); + + result.objects2[`${name}_full`] = createSprites(full, 0, config.fullHeight, config.fullHeightVertical); + + map.pop(); + map.pop(); + + result.objects2[`${name}_half`] = createSprites( + half, config.fullHeight - config.halfHeight, config.halfHeight, config.halfHeightVertical); +} + +function createTileSprites({ sprites, objects2 }: Result) { + const basePath = path.join(sourcePath, 'tiles'); + const types = [ + { name: 'grassTiles', file: 'dirt-grass.png', space: 1, alts: ['dirt-grass-autumn.png'] }, + { name: 'snowTiles', file: 'dirt-snow.png', space: 1, alts: [] }, + { name: 'woodTiles', file: 'wood-tiles.png', space: 1, alts: [] }, + { name: 'stoneTiles', file: 'stone-tiles.png', space: 1, alts: [] }, + { name: 'stone2Tiles', file: 'stone2-tiles.png', space: 1, alts: [] }, + { + name: 'waterTiles1', file: 'dirt-water-1.png', space: 1, + alts: ['dirt-water-1-autumn.png', 'dirt-water-1-winter.png', 'dirt-water-1-cave.png'] + }, + { name: 'waterTiles2', file: 'dirt-water-2.png', space: 1, alts: [] }, + { name: 'waterTiles3', file: 'dirt-water-3.png', space: 1, alts: [] }, + { name: 'waterTiles4', file: 'dirt-water-4.png', space: 1, alts: [] }, + { name: 'iceTiles', file: 'dirt-ice.png', space: 0, alts: ['dirt-ice-autumn.png', 'dirt-ice-winter.png'] }, + { name: 'grassTilesNew', file: 'grass.png', space: 1, alts: [] }, + { name: 'snowOnIceTiles', file: 'ice-snow.png', space: 1, alts: [] }, + { name: 'caveTiles', file: 'dirt-stone-cave.png', space: 1, alts: [] }, + ]; + + types.forEach(({ name, file, alts, space }) => { + const tileSprites = tilesToSprites(openPng(path.join(basePath, file)), space, space); + const palette = createPaletteFromList(tileSprites); + const palettes = [palette, ...alts.map(altFile => { + const altSprites = tilesToSprites(openPng(path.join(basePath, altFile)), space, space); + const altPalette = palette.slice(); + return altSprites.reduce((pal, s, i) => createOtherPalette(palette, tileSprites[i], s, pal), altPalette); + })]; + + objects2[name] = { + palettes, + sprites: tileSprites.map(s => addSprite(sprites, s, undefined, palette)), + }; + }); + + const otherTiles: string[] = []; + + otherTiles.forEach(tile => { + const image = openPng(path.join(basePath, `${tile}.png`)); + const palette = createPaletteFromList([image]); + + objects2[`${tile}Tiles`] = { + palettes: [palette], + sprites: [addSprite(sprites, image, undefined, palette)], + }; + }); + + const cliffs = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass.png')); + const cliffsAutumn = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass-autumn.png')); + const cliffsWinter = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass-winter.png')); + const cliffsPalette = createPaletteFromList([cliffs]); + const cliffsPaletteAutumn = createOtherPalette(cliffsPalette, cliffs, cliffsAutumn, [...cliffsPalette]); + const cliffsPaletteWinter = createOtherPalette(cliffsPalette, cliffs, cliffsWinter, [...cliffsPalette]); + + const cave = openPng(path.join(sourcePath, 'tiles', 'cave-walls.png')); + const cavePalette = createPaletteFromList([cave]); + + createCliffs('cliffs_grass', cliffs, [cliffsPalette, cliffsPaletteAutumn, cliffsPaletteWinter]); + createCliffs('cave_walls', cave, [cavePalette]); + + function createCliffs(baseName: string, canvas: ExtCanvas, palettes: number[][]) { + function addCliffSprite(name: string, x: number, y: number, w = 1, h = 1) { + const color = addSprite(sprites, cropCanvas(canvas, 32 * x, 24 * y, 32 * w, 24 * h), undefined, palettes[0]); + objects2[`${baseName}_${name}`] = { color, palettes }; + } + + addCliffSprite('decal_1', 3, 1); + addCliffSprite('decal_2', 3, 2); + addCliffSprite('decal_3', 4, 2); + addCliffSprite('decal_l', 4, 0); + addCliffSprite('decal_r', 4, 1); + + addCliffSprite('top_nw', 1, 0); + addCliffSprite('top_n', 3, 0); + addCliffSprite('top_ne', 5, 0); + addCliffSprite('top_w', 1, 1); + addCliffSprite('top_e', 5, 1); + addCliffSprite('top_sw', 1, 2); + addCliffSprite('top_se', 5, 2); + addCliffSprite('top_s1', 2, 3); + addCliffSprite('top_s2', 3, 3); + addCliffSprite('top_s3', 4, 3); + addCliffSprite('mid_sw1', 1, 4); + addCliffSprite('mid_sw2', 1, 3); + addCliffSprite('mid_s1', 2, 4); + addCliffSprite('mid_s2', 3, 4); + addCliffSprite('mid_s3', 4, 4); + addCliffSprite('mid_se1', 5, 4); + addCliffSprite('mid_se2', 5, 3); + addCliffSprite('bot_sw', 1, 5); + addCliffSprite('bot_s1', 2, 5); + addCliffSprite('bot_s2', 3, 5); + addCliffSprite('bot_s3', 4, 5); + addCliffSprite('bot_se', 5, 5); + + addCliffSprite('top_sb', 2, 0); + addCliffSprite('mid_sb', 2, 1); + addCliffSprite('bot_sb', 2, 2); + + addCliffSprite('top_trim_left', 0, 1); + addCliffSprite('mid_trim_left', 0, 2); + addCliffSprite('bot_trim_left', 0, 4); + addCliffSprite('top_trim_right', 6, 1); + addCliffSprite('mid_trim_right', 6, 2); + addCliffSprite('bot_trim_right', 6, 4); + + // combined sections + addCliffSprite('sw', 1, 2, 1, 4); + addCliffSprite('s1', 2, 3, 1, 3); + addCliffSprite('s2', 3, 3, 1, 3); + addCliffSprite('s3', 4, 3, 1, 3); + addCliffSprite('sb', 2, 0, 1, 3); + addCliffSprite('se', 5, 2, 1, 4); + } +} + +function fontCanvas(name: string) { + const filePath = path.join(sourcePath, 'fonts', name); + const file = openPsd(filePath); + return getLayerCanvasSafe('color', file); +} + +function createStripedCanvas(canvas: ExtCanvas, colors: number[]) { + const stripes = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (stripes)`); + const context = stripes.getContext('2d')!; + + for (let y = 0; y < canvas.height; y++) { + context.fillStyle = colorToCSS(colors[y % colors.length]); + context.fillRect(0, y, canvas.width, 1); + } + + context.globalCompositeOperation = 'destination-in'; + context.drawImage(canvas, 0, 0); + return stripes; +} + +function bandedPalette(colors: number[], bands: number[]) { + return flatten(bands.map((t, i) => times(t, () => colors[i] >>> 0))); +} + +function bandedTextPalette(colors: number[]) { + return bandedPalette(colors, [3, 2, 3, 2]); +} + +function bandedTinyPalette(colors: number[]) { + return bandedPalette(colors, [2, 2, 2, 3]); +} + +const SUPPORTER1 = 0xf86754ff; +const SUPPORTER2_BANDS = [0xffdfc1ff, 0xffcd99ff, 0xff9f3bff, 0xd97e09ff]; +const SUPPORTER3_BANDS = [0xffffffff, 0xfffda4ff, 0xffea3bff, 0xfdbb0bff]; + +export function createSprites(log: boolean) { + mkdir(destPath); + mkdir(generatedPath); + + const result = createResult(); + + createPonySprites(result); + createTileSprites(result); + + const mainFont = fontCanvas('main.psd'); + const mainEmoji = fontCanvas('emoji.psd'); + const tinyFont = fontCanvas('tiny.psd'); + const monoFont = fontCanvas('mono.psd'); + const emojiPalette = createPalette(mainEmoji); + + const mainFontPalette = [TRANSPARENT, ...times(10, i => 0xff + i * 256)]; + const stripedMainFont = createStripedCanvas(mainFont, mainFontPalette.slice(1)); + const smallFontPalette = [TRANSPARENT, ...times(9, i => 0xff + i * 256)]; + const stripedSmallFont = createStripedCanvas(tinyFont, smallFontPalette.slice(1)); + const stripedMonoFont = createStripedCanvas(monoFont, smallFontPalette.slice(1)); + + const fontSprites = createFont(mainFont, 10, 10, + canvas => addImage(result.images, canvas), { noChinese: true }); + const emojiSprites = createEmojis(mainEmoji, 10, 10, + canvas => addImage(result.images, canvas)); + const smallFontSprites = createFont(tinyFont, 8, 9, + canvas => addImage(result.images, canvas), { noChinese: true }); + const monoFontSprites = createFont(monoFont, 8, 9, + canvas => addImage(result.images, canvas), { noChinese: true, mono: 4, onlyBase: true }); + + const fontSpritesPal = createFont(stripedMainFont, 10, 10, + canvas => addSprite(result.sprites, canvas, undefined, mainFontPalette)); + const emojiSpritesPal = createEmojis(mainEmoji, 10, 10, + canvas => addSprite(result.sprites, canvas, undefined, emojiPalette)); + const smallFontSpritesPal = createFont(stripedSmallFont, 8, 9, + canvas => addSprite(result.sprites, canvas, undefined, smallFontPalette), { noChinese: true }); + const monoFontSpritesPal = createFont(stripedMonoFont, 8, 9, + canvas => addSprite(result.sprites, canvas, undefined, smallFontPalette), { noChinese: true, mono: 4, onlyBase: true }); + + const lights = createLights(result, path.join(sourcePath, 'lights')); + + const ponySheet = createSpriteSheet('ponySheet', result.images.map(imageToSprite), log, 1024); + const ponySheet2 = createSpriteSheet('ponySheet2', result.sprites, log, 1024, 'black', true); + + fixPixelRect(ponySheet.sprites, result.objects, 'pixelRect', 'pixel'); + fixPixelRect(ponySheet2.sprites, result.objects2, 'pixelRect2', 'pixel2'); + + lights.map(i => ponySheet.sprites[i]).forEach(s => { + if (s) { + s.x += lightsPad; + s.y += lightsPad; + s.w -= lightsPad * 2; + s.h -= lightsPad * 2; + } + }); + + saveSpriteSheetAsBinary(path.join(generatedPath, 'pony.bin'), ponySheet.image); + + const spritesConfig: SpriteTSConfig = { + spriteFileName: saveSpriteSheet(path.join(destPath, 'pony.png'), ponySheet.image), + paletteFileName: saveSpriteSheet(path.join(destPath, 'pony2.png'), ponySheet2.image), + paletteAlphaFileName: saveSpriteSheet(path.join(destPath, 'pony2a.png'), ponySheet2.alpha), + sprites: ponySheet.sprites, + paletteSprites: ponySheet2.sprites, + result, + palettes, + fonts: { + font: fontSprites, + emoji: emojiSprites, + fontSmall: smallFontSprites, + fontMono: monoFontSprites, + }, + fontsPal: { + fontPal: fontSpritesPal, + emojiPal: emojiSpritesPal, + fontSmallPal: smallFontSpritesPal, + fontMonoPal: monoFontSpritesPal, + }, + namedPalettes: { + defaultPalette: 0, + emojiPalette: addPalette(emojiPalette), + // main + fontPalette: addPalette([TRANSPARENT, ...times(10, () => WHITE)]), + fontSupporter1Palette: addPalette([TRANSPARENT, ...times(10, () => SUPPORTER1)]), + fontSupporter2Palette: addPalette([TRANSPARENT, ...bandedTextPalette(SUPPORTER2_BANDS)]), + fontSupporter3Palette: addPalette([TRANSPARENT, ...bandedTextPalette(SUPPORTER3_BANDS)]), + // small + fontSmallPalette: addPalette([TRANSPARENT, ...times(9, () => WHITE)]), + fontSmallSupporter1Palette: addPalette([TRANSPARENT, ...times(9, () => SUPPORTER1)]), + fontSmallSupporter2Palette: addPalette([TRANSPARENT, ...bandedTinyPalette(SUPPORTER2_BANDS)]), + fontSmallSupporter3Palette: addPalette([TRANSPARENT, ...bandedTinyPalette(SUPPORTER3_BANDS)]), + }, + }; + + createSpritesTS(path.join(generatedPath, 'sprites.ts'), spritesConfig); + + // Other exports + saveCanvasAsRaw(path.join(outputPath, 'pony2.raw'), ponySheet2.image); +} + +if (require.main === module) { + const start = Date.now(); + createSprites(true); + const time = ((Date.now() - start) / 1000).toFixed(2); + console.log(`[sprites] done: ${time}s, stats: { w: ${0}-${maxW} h: ${0}-${maxH} ox: ${minOX}-${maxOX} oy: ${minOY}-${maxOY} }`); +} diff --git a/src/ts/tools/name-tester.ts b/src/ts/tools/name-tester.ts new file mode 100644 index 0000000..fdc5cba --- /dev/null +++ b/src/ts/tools/name-tester.ts @@ -0,0 +1,54 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { CHARS, ROMAJI, EMOJI, charsToCodes } from './create-font'; + +type Item = { name: string; }[]; + +const existing = new Set(); +const missing = new Map(); +const missingNames: string[] = []; +const rootPath = path.join(__dirname, '..', '..', '..', 'tools'); +const items: Item = JSON.parse(fs.readFileSync(path.join(rootPath, 'names.json'), 'utf8')); + +function isNonPrintableCharacter(code: number): boolean { + return code >= 0xfe00 && code <= 0xfe0f; +} + +function ignore(code: number): boolean { + return (code >= 0x0180 && code <= 0x024F) // Latin Extended-B + || (code >= 0x0600 && code <= 0x06FF) // Arabic + || (code >= 0x2719 && code <= 0x2721) // crosses + || (code >= 0x0300 && code <= 0x036F) // Combining Diacritical Marks + || (code >= 0x2200 && code <= 0x22FF) // Mathematical Operators + || (code >= 0x0E00 && code <= 0x0E7F) // Thai + || (code >= 0x0250 && code <= 0x02AF) // IPA Extensions + || (code >= 0x2460 && code <= 0x24FF) // Enclosed Alphanumerics + || (code >= 0x2300 && code <= 0x23FF) // Miscellaneous Technical + ; +} + +charsToCodes(CHARS + ROMAJI + EMOJI).forEach(code => existing.add(code)); + +items.forEach(({ name }) => { + const codes = charsToCodes(name); + const missingChars = codes.reduce((count, code) => { + if (existing.has(code) || isNonPrintableCharacter(code)) + return count; + + const current = missing.get(code) || 0; + missing.set(code, current + 1); + return count + 1; + }, 0); + + if (missingChars) { + missingNames.push(name); + } +}); + +const missingChars: [number, string, number][] = []; +missing.forEach((value, key) => missingChars.push([value, String.fromCodePoint(key), key])); +missingChars.sort(([a], [b]) => b - a); + +fs.writeFileSync(path.join(rootPath, 'output', 'missing-names.txt'), missingNames.join('\n'), 'utf8'); +fs.writeFileSync(path.join(rootPath, 'output', 'missing-chars.txt'), missingChars + .map(([a, b, c]) => `${a}: "${b}" (${c}) [U+${c.toString(16)}]${ignore(c) ? ' (ignore)' : ''}`).join('\n'), 'utf8'); diff --git a/src/ts/tools/palette-utils.ts b/src/ts/tools/palette-utils.ts new file mode 100644 index 0000000..50acbb2 --- /dev/null +++ b/src/ts/tools/palette-utils.ts @@ -0,0 +1,128 @@ +import { Rect, ExtCanvas } from './types'; +import { cropCanvas, createExtCanvas } from './canvas-utils'; + +const validPatternColors = [ + 255, // red + 255 << 8, // green + 255 | (255 << 8), // yellow + 255 << 16, // blue + 255 | (255 << 16), // purple + (255 << 8) | (255 << 16), // teal + 255 | (255 << 8) | (255 << 16), // white + 0, // black +]; + +const getAlpha = (alpha: number) => alpha < 15 ? false : (alpha > 250 ? true : null); +const isOutline = (shade: number) => shade <= 159; +const round255 = (n: number) => n > 200 ? 255 : (n < 50 ? 0 : n); +const rgbToColor = (r: number, g: number, b: number) => round255(r) | (round255(g) << 8) | (round255(b) << 16); +const findValidPatternColor = (r: number, g: number, b: number) => validPatternColors.indexOf(rgbToColor(r, g, b)); + +function getShade(shade: number) { + // outlines + // 135 - dark outline + // 159 - outline + + // fills + // 174 - dark shades + // 204 - shade + // 217 - dark fill + // 255 - fill + + if (shade > 130 && shade < 140) { // dark outline + return 135; + } else if (shade > 150 && shade < 165) { // outline + return 159; + } else if (shade > 165 && shade < 180) { // dark shade + return 174; + } else if (shade > 190 && shade < 210) { // shade + return 204; + } else if (shade > 210 && shade < 220) { // dark fill + return 217; + } else if (shade > 245) { // fill + return 255; + } else { + return -1; + } +} + +function getShadeForShading(shade: number) { + if (shade === 135) { // dark outline + return 204; + } else if (shade === 159) { // outline + return 255; + } else { + return shade; + } +} + +function pixel(data: Uint8ClampedArray, index: number) { + return data.slice(index, index + 4).join(', '); +} + +export interface ColorsOutput { + colors?: number; + forceWhite?: boolean; +} + +export function imageToPalette( + rect: Rect, image: ExtCanvas, pattern: ExtCanvas, palette: number[] | undefined, config: ColorsOutput +) { + const pat = cropCanvas(pattern, 0, 0, image.width, image.height); + const ctx = pat.getContext('2d')!; + ctx.globalCompositeOperation = 'destination-in'; + ctx.drawImage(image, 0, 0); + + const imageData = image.getContext('2d')!.getImageData(rect.x, rect.y, rect.w, rect.h); + const pattData = ctx.getImageData(rect.x, rect.y, rect.w, rect.h); + const length = imageData.width * imageData.height * 4; + const data = imageData.data; + const pdata = pattData.data; + + let maxIndex = 0; + + for (let i = 0; i < length; i += 4) { + const x = ((i / 4) % rect.w) + rect.x; + const y = Math.floor((i / 4) / rect.h) + rect.y; + + if (palette) { + const color = ((data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3]) >>> 0; + const index = palette.indexOf(color); + + if (index === -1) { + throw new Error(`Invalid color (palette) [${pixel(data, i)}] [0x${color.toString(16)}] (${x} ${y}) (${image.info})`); + } + + data[i] = index; + data[i + 1] = 255; + } else { + const alpha = getAlpha(data[i + 3]); + const shade = getShade(data[i]); + const index = findValidPatternColor(pdata[i], pdata[i + 1], pdata[i + 2]); + + if (index === -1) + throw new Error(`Invalid color (pattern) [${pixel(pdata, i)}] (${x} ${y}) (${pat.info})`); + if (alpha == null) + throw new Error(`Invalid color [${pixel(data, i)}] (${x} ${y}) (${image.info})`); + + const actualIndex = (index * 2) + (isOutline(shade) ? 2 : 1); + const shadeForShading = getShadeForShading(shade); + + data[i] = alpha ? actualIndex : 0; + data[i + 1] = (config.forceWhite && shade !== -1) ? 255 : (alpha ? shadeForShading : 255); + + if (alpha) { + maxIndex = Math.max(maxIndex, index); + } + } + + data[i + 2] = 0; + data[i + 3] = 255; + } + + config.colors = palette ? palette.length : ((maxIndex + 1) * 2 + 1); + + const result = createExtCanvas(image.width, image.height, `${image.info} (image to palette)`); + result.getContext('2d')!.putImageData(imageData, rect.x, rect.y); + return result; +} diff --git a/src/ts/tools/psd-utils.ts b/src/ts/tools/psd-utils.ts new file mode 100644 index 0000000..286a808 --- /dev/null +++ b/src/ts/tools/psd-utils.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Layer as PsdLayer, Psd as PsdFile, readPsd, initializeCanvas } from 'ag-psd'; +import { Layer, Psd } from './types'; +import { createExtCanvas } from './canvas-utils'; +import { matcher, parseWithNumber } from './common'; + +initializeCanvas((width, height) => createExtCanvas(width, height, 'loaded from psd')); + +export function openPsd(filePath: string) { + try { + const buffer = fs.readFileSync(filePath); + const name = path.basename(filePath, '.psd'); + const dir = path.basename(path.dirname(filePath)); + const psd = readPsd(buffer, { + skipCompositeImageData: true, + skipThumbnail: true, + throwForMissingFeatures: true, + logMissingFeatures: true, + }); + return toPsd(psd, name, dir); + } catch (e) { + console.error(`Failed to load: ${filePath}: ${e.message}`); + throw e; + } +} + +function toPsd({ width, height, children }: PsdFile, name: string, dir: string): Psd { + const info = `${dir}/${name}`; + + return { + dir, name, width, height, info, children: (children || []).map(c => toLayer(c, width, height, info)), + }; +} + +function toLayer({ name, canvas, left, top, children }: PsdLayer, width: number, height: number, parentInfo: string): Layer { + const info = `${parentInfo}/${name}`; + + return { + name: name || '', + info, + canvas: fixCanvas(canvas, width, height, left || 0, top || 0, info), + children: (children || []).map(c => toLayer(c, width, height, info)), + }; +} + +function fixCanvas( + canvas: HTMLCanvasElement | undefined, width: number, height: number, left: number, top: number, info: string +) { + if (!canvas) + return undefined; + + const result = createExtCanvas(width, height, info); + result.getContext('2d')!.drawImage(canvas, left, top); + return result; +} + +const isPsd = matcher(/\.psd$/); + +export const getPsds = (directory: string) => fs.readdirSync(directory).filter(isPsd); + +export function openPsdFiles(directory: string, match?: RegExp) { + return getPsds(directory) + .filter(f => match ? match.test(f) : true) + .sort((a, b) => parseWithNumber(a) - parseWithNumber(b)) + .map(f => path.join(directory, f)) + .map(openPsd); +} diff --git a/src/ts/tools/sprite-sheet.ts b/src/ts/tools/sprite-sheet.ts new file mode 100644 index 0000000..f420c48 --- /dev/null +++ b/src/ts/tools/sprite-sheet.ts @@ -0,0 +1,492 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { times } from 'lodash'; +import { Sprite, Rect } from './types'; +import { findByIndex } from './common'; +import { removeItem } from '../common/utils'; +import { createExtCanvas, saveCanvas } from './canvas-utils'; + +interface ExtSprite extends Sprite { + index?: number; + layers?: number; + duplicateOf?: ExtSprite; + overlays?: ExtSprite[]; + overlayedOn?: ExtSprite; + data?: ImageData; +} + +function isIdenticalSprite(a: ExtSprite | undefined, b: ExtSprite | undefined): boolean { + return !!(a && b && a.data && b.data && isIdenticalData(a.data, b.data)); +} + +function isIdenticalData(a: ImageData, b: ImageData) { + if (a.width !== b.width || a.height !== b.height) + return false; + + const length = (a.width * a.height * 4) | 0; + const adat = a.data; + const bdat = b.data; + + for (let i = 0; i < length; i = (i + 1) | 0) { + if (adat[i] !== bdat[i]) { + return false; + } + } + + return true; +} + +function isIdenticalChannel(a: Sprite | undefined, b: Sprite | undefined, channel: number) { + if (!a || !b || a.w !== b.w || a.h !== b.h) + return false; + + const adata = a.image.getContext('2d')!.getImageData(a.ox, a.oy, a.w, a.h); + const bdata = b.image.getContext('2d')!.getImageData(b.ox, b.oy, b.w, b.h); + const length = (adata.width * adata.height * 4) | 0; + const adat = adata.data; + const bdat = bdata.data; + + for (let i = channel | 0; i < length; i = (i + 4) | 0) { + if (adat[i] !== bdat[i]) { + return false; + } + } + + return true; +} + +function trimImageData(data: ImageData): Rect { + const width = data.width | 0; + const imageData = data.data; + + let top = 0; + let left = 0; + let right = data.width | 0; + let bottom = data.height | 0; + + function isEmpty(x: number, y: number) { + return imageData[((getIndex(x, y, width) << 2) + 3) | 0] === 0; + } + + function isRowEmpty(y: number) { + for (let x = left | 0; x < right; x = (x + 1) | 0) { + if (!isEmpty(x | 0, y | 0)) { + return false; + } + } + + return true; + } + + function isColEmpty(x: number) { + for (let y = top | 0; y < bottom; y = (y + 1) | 0) { + if (!isEmpty(x | 0, y | 0)) { + return false; + } + } + + return true; + } + + while (bottom > top && isRowEmpty(bottom - 1)) + bottom--; + while (right > left && isColEmpty(right - 1)) + right--; + while (top < bottom && isRowEmpty(top)) + top++; + while (left < right && isColEmpty(left)) + left++; + + return { y: top, x: left, w: right - left, h: bottom - top }; +} + +export function getSpriteRect(canvas: HTMLCanvasElement, x: number, y: number, w: number, h: number): Rect { + const data = canvas.getContext('2d')!.getImageData(x, y, w, h); + const rect = trimImageData(data); + return { x: x + rect.x, y: y + rect.y, w: rect.w, h: rect.h }; +} + +export function imageToSprite(image: HTMLCanvasElement, index: number): ExtSprite { + const { w, h, x, y } = getSpriteRect(image, 0, 0, image.width, image.height); + return { image, index, w, h, x: 0, y: 0, ox: x, oy: y }; +} + +function getIndex(x: number, y: number, outputWidth: number) { + return ((x | 0) + (((y | 0) * outputWidth) | 0)) | 0; +} + +function isEmpty(x: number, y: number, w: number, h: number, outputWidth: number, taken: Uint8Array) { + outputWidth = outputWidth | 0; + + if (((x + w) | 0) > outputWidth || ((y + h) | 0) > outputWidth) { + return false; + } + + for (let iy = 0; iy < h; iy++) { + for (let ix = 0; ix < w; ix++) { + if (taken[getIndex((ix + x) | 0, (iy + y) | 0, outputWidth)] !== 0) { + return false; + } + } + } + + return true; +} + +interface Line { + start: number; + length: number; +} + +interface Taken { + lines: Line[][]; + data: Uint8Array; // TODO: remove completely, just use lines +} + +function getFirstFree(outputWidth: number, width: number, height: number, { data, lines }: Taken) { + const maxY = (outputWidth - height) | 0; + + for (let y = 0; y < maxY; y = (y + 1) | 0) { + const spans = lines[y]; + + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const start = span.start | 0; + const end = (start + span.length - width) | 0; + + for (let x = start; x < end; x = (x + 1) | 0) { + if (isEmpty(x, y, width, height, outputWidth, data)) { + return { x, y, layer: 0 }; + } + } + } + } + + throw new Error(`Cannot find free space for (${width}, ${height}) [getFirstFree]`); +} + +function getFirstFreePacked(outputWidth: number, width: number, height: number, takens: Taken[]) { + const maxY = outputWidth - height; + + for (let layer = 0; layer < takens.length; layer = (layer + 1) | 0) { + const { data, lines } = takens[layer]; + + for (let y = 0; y < maxY; y = (y + 1) | 0) { + const spans = lines[y]; + + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const start = span.start | 0; + const end = (start + span.length - width) | 0; + + for (let x = start | 0; x < end; x = (x + 1) | 0) { + if (isEmpty(x, y, width, height, outputWidth, data)) { + return { x, y, layer }; + } + } + } + } + } + + throw new Error(`Cannot find free space for (${width}, ${height}) [getFirstFreePacked]`); +} + +function positionSprite(sprite: ExtSprite, outputWidth: number, taken: Taken[], pack: boolean) { + const layers = sprite.layers || 1; + const { x, y, layer } = (pack && layers === 1) ? + getFirstFreePacked(outputWidth, sprite.w, sprite.h, taken) : + getFirstFree(outputWidth, sprite.w, sprite.h, taken[0]); + + sprite.x = x; + sprite.y = y; + sprite.layer = layer; + + const w = sprite.w; + const right = x + w; + + for (let il = 0; il < layers; il++) { + const { data, lines } = taken[il + layer]; + + for (let iy = 0; iy < sprite.h; iy++) { + const yy = y + iy; + const spans = lines[yy]; + + for (let ix = 0; ix < sprite.w; ix++) { + const xx = x + ix; + data[getIndex(xx, yy, outputWidth)] = 1; + } + + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const start = span.start; + const length = span.length; + const end = start + length; + + if (start >= right) // right of span + break; + + if (end <= right) // left of span + continue; + + if (start === x) { + if (length === w) { // entire span + spans.splice(i, 1); + } else { // at the start of span + span.start += w; + span.length -= w; + } + } else { + if (end === right) { // at the end of span + span.length -= w; + } else { // in the middle of span + span.length = x - start; + spans.splice(i + 1, 0, { start: right, length: end - right }); + } + } + + // let start = span.start; + // let lineMoved = false; + + // for (let ix = 0; ix < sprite.w; ix++) { + // const xx = x + ix; + // data[getIndex(xx, yy, outputWidth)] = 1; + + // if (xx === start) { + // start++; + // lineMoved = true; + // } + // } + + // if (lineMoved) { + // for (let x = start; x < outputWidth && data[getIndex(x, yy, outputWidth)] !== 0; x++) { + // start++; + // } + + // span.start = start; + // span.length = outputWidth - start; + // } + } + } + } +} + +function hasShading(s: Sprite) { + const data = s.image.getContext('2d')!.getImageData(s.ox, s.oy, s.w, s.h); + + for (let y = 0; y < data.height; y++) { + for (let x = 0; x < data.width; x++) { + if (data.data[(x + y * data.width) * 4 + 1] !== 0xff) { + return true; + } + } + } + + return false; +} + +function getSpriteImageData(s: ExtSprite): ImageData | undefined { + if (!s.w || !s.h) + return undefined; + + const context = s.image.getContext('2d')!; + const { width, height, data } = context.getImageData(s.ox, s.oy, s.w, s.h); + return { width, height, data }; +} + +export function createSpriteSheet(name: string, images: ExtSprite[], log: boolean, size: number, bg?: string, pack = false) { + const maxLayers = 4; + const sprites = images.slice(); + + let outputWidth = size; + let maxY = 0; + let areaTaken = 0; + let deduplicated = 0; + let layered = 0; + let tooBig = sprites.find(s => s.w >= outputWidth); + + while (tooBig) { + throw new Error(`Sprite too large (${tooBig.w}x${tooBig.h}) in ${name} (${size}x${size})`); + } + + sprites + .forEach(s => s.data = getSpriteImageData(s)); + + sprites + .forEach((s, i) => { + if (s.w && s.h) { + for (let j = 0; j < i; j++) { + if (!s.duplicateOf && isIdenticalSprite(sprites[j], s)) { + s.duplicateOf = sprites[j]; + deduplicated++; + //console.log('duplicate', sprite.name, '==', sprites[j].name); + break; + } + } + } + }); + + sprites + .filter(s => !s.duplicateOf && s.w && s.h) + .forEach(s => { + s.shade = !pack || hasShading(s); + s.layers = s.shade ? 2 : 1; + }); + + if (pack) { + sprites + .filter(s => !s.duplicateOf && s.shade) + .reduce((pool, sprite) => { + const match = pool.find(d => isIdenticalChannel(sprite, d, 1)); + + if (match) { + match.overlays = match.overlays || []; + match.overlays.push(sprite); + sprite.layer = match.layers!; + sprite.overlayedOn = match; + match.layers = match.layers! + 1; + + if (match.layers >= maxLayers) { + removeItem(pool, match); + } + + layered++; + } else { + pool.push(sprite); + } + + return pool; + }, [] as ExtSprite[]); + } + + sprites.sort((a, b) => ((b.layers || 1) - (a.layers || 1)) || ((b.h * 1024 + b.w) - (a.h * 1024 + a.w))); + + maxY = 0; + areaTaken = 0; + + const taken: Taken[] = times(maxLayers, () => ({ + lines: times(outputWidth, () => [{ start: 0, length: outputWidth }]), + data: new Uint8Array(outputWidth * outputWidth), + })); + + sprites + .filter(s => !s.duplicateOf && !s.overlayedOn) + .forEach(s => { + try { + positionSprite(s, outputWidth, taken, pack); + maxY = Math.max(maxY, s.y + s.h); + areaTaken += s.w * s.h; + } catch (e) { + console.error(e); + } + }); + + if (maxY > outputWidth) { + throw new Error(`Exceeded sprite sheet size for ${name} (${size}x${size})`); + } + + if (log) { + const efficiency = (areaTaken * 100 / outputWidth / maxY).toFixed(); + + console.log( + `[sprites] [${name}] Packed ${sprites.length} sprites into ${outputWidth} x ${maxY} sheet, ` + + `${efficiency}% efficiency, ${deduplicated} deduplicated, ${layered} layered`); + } + + sprites + .filter(s => s.overlayedOn) + .forEach(s => { + s.x = s.overlayedOn!.x; + s.y = s.overlayedOn!.y; + }); + + sprites + .filter(s => s.duplicateOf) + .forEach(s => { + s.x = s.duplicateOf!.x; + s.y = s.duplicateOf!.y; + s.layer = s.duplicateOf!.layer; + s.shade = s.duplicateOf!.shade; + }); + + const image = createExtCanvas(outputWidth, outputWidth, 'sprite sheet image'); + const alpha = createExtCanvas(outputWidth, outputWidth, 'sprite sheet alpha'); + const context = image.getContext('2d')!; + const alphaContext = alpha.getContext('2d')!; + + if (bg) { + context.fillStyle = bg; + context.fillRect(0, 0, outputWidth, outputWidth); + alphaContext.fillStyle = bg; + alphaContext.fillRect(0, 0, outputWidth, outputWidth); + } + + if (pack) { + const data = context.getImageData(0, 0, image.width, image.height); + const alphaData = alphaContext.getImageData(0, 0, image.width, image.height); + + sprites + .filter(s => !s.duplicateOf && s.w && s.h) + .forEach(s => { + if (s.layer === 3) { + drawChannel(s.image, alphaData, 0, 0, s.ox, s.oy, s.x, s.y, s.w, s.h); + } else { + drawChannel(s.image, data, 0, s.layer || 0, s.ox, s.oy, s.x, s.y, s.w, s.h); + } + + if (s.shade) { + drawChannel(s.image, data, 1, 1, s.ox, s.oy, s.x, s.y, s.w, s.h); + } + }); + + context.putImageData(data, 0, 0); + alphaContext.putImageData(alphaData, 0, 0); + } else { + sprites + .filter(s => !s.duplicateOf && s.w && s.h) + .forEach(s => context.drawImage(s.image, s.ox, s.oy, s.w, s.h, s.x, s.y, s.w, s.h)); + } + + return { + sprites: images.map((_, index) => findByIndex(sprites, index) || null), + image, + alpha, + }; +} + +function drawChannel( + src: HTMLCanvasElement, dst: ImageData, srcChannel: number, dstChannel: number, + sx: number, sy: number, dx: number, dy: number, w: number, h: number +) { + const srcData = src.getContext('2d')!.getImageData(sx, sy, w, h); + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + dst.data[((x + dx) + (y + dy) * dst.width) * 4 + dstChannel] = + srcData.data[(x + y * srcData.width) * 4 + srcChannel]; + } + } +} + +export function saveSpriteSheet(filePath: string, canvas: HTMLCanvasElement) { + saveCanvas(filePath, canvas); + return path.basename(filePath); +} + +export function saveSpriteSheetAsBinary(filePath: string, canvas: HTMLCanvasElement) { + const context = canvas.getContext('2d')!; + const data = context.getImageData(0, 0, canvas.width, canvas.height); + fs.writeFileSync(filePath, Buffer.from(data.data.buffer)); +} + +export function saveCanvasAsRaw(filePath: string, canvas: HTMLCanvasElement) { + const buffer = Buffer.alloc(4 + 4 + 4 + 4 * canvas.width * canvas.height); + buffer.writeUInt8('R'.charCodeAt(0), 0); + buffer.writeUInt8('A'.charCodeAt(0), 1); + buffer.writeUInt8('W'.charCodeAt(0), 2); + buffer.writeUInt8(' '.charCodeAt(0), 3); + buffer.writeUInt32LE(canvas.width, 4); + buffer.writeUInt32LE(canvas.height, 8); + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + buffer.set(data.data, 12); + fs.writeFileSync(filePath, buffer); +} diff --git a/src/ts/tools/sprites-template.ts b/src/ts/tools/sprites-template.ts new file mode 100644 index 0000000..d6721fb --- /dev/null +++ b/src/ts/tools/sprites-template.ts @@ -0,0 +1,195 @@ +// generated file +/* tslint:disable */ + +import { + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, Shadow, ColorShadow, TileSprites, SpriteSheet +} from '../common/interfaces'; +import { parseSpriteColor } from '../common/utils'; +import { bitReaderCustom } from '../common/bitUtils'; + +// [frame][type][pattern] +export type StaticSprites = (ColorExtra[] | undefined)[]; +export type AnimatedSprites = (StaticSprites | undefined)[]; +export type StaticSpritesExtra = (ColorExtra[] | undefined)[]; +export type AnimatedSpritesExtra = (StaticSprites | undefined)[]; +export type PonyEyes = ((PonyEye | undefined)[] | undefined)[]; + +const sprites = createSprites('/*SPRITES*/'); + +const sprites2 = createSprites('/*SPRITES_PALETTE*/'); + +/*FONTS*/ + +const palettes: Uint32Array[] = createPalettes('/*COLORS*/', [ + /*PALETTES*/ +]); + +/*NAMED_SPRITES*/ + +/*NAMED_PALETTES*/ + +export const spriteSheets: SpriteSheet[] = [ + { + src: '/*SPRITE_SHEET*/', + data: undefined, texture: undefined, sprites: sprites, palette: false + }, + { + src: '/*SPRITE_SHEET_PALETTE*/', srcA: '/*SPRITE_SHEET_PALETTE_ALPHA*/', + data: undefined, texture: undefined, sprites: sprites2, palette: true + }, +]; + +export const normalSpriteSheet = spriteSheets[0]; +export const paletteSpriteSheet = spriteSheets[1]; + +export function createSprites(data: string): Sprite[] { + const sprites: Sprite[] = [ + { x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0, type: 0 }, + ]; + + let offset = 0; + const read = bitReaderCustom(() => { + const value = parseInt(data.substr(offset, 2), 16); + offset += 2; + return value; + }); + + while (offset < data.length) { + sprites.push({ + x: read(12), + y: read(12), + w: read(9), + h: read(9), + ox: read(8), + oy: read(8), + type: read(6), + }); + } + + return sprites; +} + +export function createFont(sprites: Sprite[], groups: [number, number[]][]) { + const chars: { code: number; sprite: Sprite; }[] = []; + + for (const [start, codes] of groups) { + for (let i = 0; i < codes.length; i++) { + if (codes[i]) { + chars.push({ code: start + i, sprite: sprites[codes[i]] }); + } + } + } + + return chars; +} + +export function createButton( + border: number, topLeft: number, top: number, topRight: number, left: number, bg: number, right: number, + bottomLeft: number, bottom: number, bottomRight: number +): SpriteBorder { + return { + border, + topLeft: sprites[topLeft], + top: sprites[top], + topRight: sprites[topRight], + left: sprites[left], + bg: sprites[bg], + right: sprites[right], + bottomLeft: sprites[bottomLeft], + bottom: sprites[bottom], + bottomRight: sprites[bottomRight] + }; +} + +export function mapSprites(frames: number[]) { + return frames.map(i => sprites[i]); +} + +export function mapSprites2(frames: number[]) { + return frames.map(i => sprites2[i]); +} + +export function createPalettes(colorsString: string, palettes: number[][]): Uint32Array[] { + const colors = colorsString.split(/ /g).map(parseSpriteColor); + + return palettes.map(palette => { + const result = new Uint32Array(palette.length); + + for (let i = 0; i < palette.length; i++) { + result[i] = colors[palette[i]] >>> 0; + } + + return result; + }); +} + +export function createColorPal(color: number, colors: number): ColorExtra { + return { color: sprites2[color], colors }; +} + +export function colorPal(colors: number) { + return (color: number) => createColorPal(color, colors); +} + +const colorPal3 = colorPal(3); +const colorPal5 = colorPal(5); +const colorPal7 = colorPal(7); +const colorPal9 = colorPal(9); +const colorPal11 = colorPal(11); +const colorPal13 = colorPal(13); +const colorPal17 = colorPal(17); + +export function getPalette(index: number) { + return palettes[index]; +} + +const emptyPalette = new Uint32Array(0); + +export function emptyColorPalette(): ColorExtra { + return { color: sprites2[0], palettes: [emptyPalette] }; +} + +export function createSpritesPalette(sprites: number[], paletteIndexes: number[]): TileSprites { + return { sprites: sprites.map(i => sprites2[i]), palettes: paletteIndexes.map(getPalette) }; +} + +export function createColorPalette(color: number, paletteIndexes: number[]): ColorExtra { + return { color: sprites2[color], palettes: paletteIndexes.map(getPalette) }; +} + +export function createColorExtraPal(color: number, colors: number, extra: number, paletteIndexes: number[]): ColorExtra { + return { color: sprites2[color], colors, extra: sprites2[extra], palettes: paletteIndexes.map(getPalette) }; +} + +export function createShadow(shadow: number): Shadow { + return { shadow: sprites2[shadow] }; +} + +export function createColorShadowPalette(color: number, shadow: number, paletteIndexes: number[]): ColorShadow { + return { color: sprites2[color], shadow: sprites2[shadow], palettes: paletteIndexes.map(getPalette) }; +} + +export function createNose(color: number, colors: number, mouth: number, fangs: number) { + return { color: sprites2[color], colors, mouth: sprites2[mouth], fangs: sprites2[fangs] }; +} + +export function createEye(base: number, irises: number[], shadow?: number, shine?: number): PonyEye { + return { base: sprites2[base], irises: mapSprites2(irises), shadow: sprites2[shadow || 0], shine: sprites2[shine || 0] }; +} + +export function createAnimation(frames: number[]) { + return { frames: mapSprites(frames) }; +} + +export function createAnimationPalette(frames: number[], palette: number) { + return { frames: mapSprites2(frames), palette: getPalette(palette) }; +} + +export function createAnimationShadow(frames: number[], shadow: number, palette: number) { + return { frames: mapSprites2(frames), shadow: sprites2[shadow], palette: getPalette(palette) }; +} + +export { + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, + colorPal3, colorPal5, colorPal7, colorPal9, colorPal11, colorPal13, colorPal17 +}; diff --git a/src/ts/tools/trigger.txt b/src/ts/tools/trigger.txt new file mode 100644 index 0000000..1de728f --- /dev/null +++ b/src/ts/tools/trigger.txt @@ -0,0 +1 @@ +save this file to trigger sprite regenerations in `gulp dev --sprites` mode diff --git a/src/ts/tools/types.ts b/src/ts/tools/types.ts new file mode 100644 index 0000000..48dad23 --- /dev/null +++ b/src/ts/tools/types.ts @@ -0,0 +1,112 @@ +export interface Psd { + dir: string; + name: string; + info: string; + width: number; + height: number; + children: Layer[]; +} + +export interface Layer { + name: string; + info: string; + children: Layer[]; + canvas?: ExtCanvas; +} + +export interface ExtCanvas extends HTMLCanvasElement { + info: string; +} + +export interface Sprite extends Rect { + ox: number; + oy: number; + image: HTMLCanvasElement; + shade?: boolean; + layer?: number; +} + +export interface Result { + objects: any; + objects2: any; + images: HTMLCanvasElement[]; + sprites: Sprite[]; + // palettes: number[][]; +} + +export interface ColorShadow { + color: number; + shadow: number; +} + +export interface Nose { + mouth: number; + muzzle: number; + fangs: number; +} + +export interface Eye { + base: number; + irises: number[]; + shadow: number; + shine: number; +} + +export interface Point { + x: number; + y: number; +} + +export interface Rect { + x: number; + y: number; + w: number; + h: number; +} + +export interface Tree { + stump: number; + trunk: number; + crown: number; + stumpShadow: number; + shadow: number; + palette: number[]; +} + +export interface Animation { + frames: number[]; + palette: number[]; + shadow?: number; +} + +export interface ColorExtra { + color: number; + colors: number; + extra?: number; + palette?: number[]; + palettes?: number[][]; +} + +export interface TileSprites { + sprites: number[]; + palettes: number[][]; +} + +export interface Button { + border: number; + topLeft: number; + top: number; + topRight: number; + left: number; + bg: number; + right: number; + bottomLeft: number; + bottom: number; + bottomRight: number; + palette?: number[]; +} + +export interface Emote { + name: string; + sprite: number; +} diff --git a/src/typings/my.d.ts b/src/typings/my.d.ts new file mode 100644 index 0000000..a0bda70 --- /dev/null +++ b/src/typings/my.d.ts @@ -0,0 +1,4 @@ +/// +/// +/// +/// diff --git a/src/typings/my/canvas.d.ts b/src/typings/my/canvas.d.ts new file mode 100644 index 0000000..c7d7cc1 --- /dev/null +++ b/src/typings/my/canvas.d.ts @@ -0,0 +1,22 @@ +interface HTMLCanvasElement { + toBuffer(callback: (err: any, buffer: Buffer) => void): void; + toBuffer(): Buffer; +} + +interface NodeCanvasImage extends HTMLImageElement { + src: any; +} + +interface NodeCanvas extends HTMLCanvasElement { + new(width?: number, height?: number): NodeCanvas; +} + +interface NodeStaticImage { + new(width?: number, height?: number): NodeCanvasImage; +} + +declare module "canvas" { + export function createCanvas(width: number, height: number): HTMLCanvasElement; + export type Canvas = HTMLCanvasElement; + export const Image: NodeStaticImage; +} diff --git a/src/typings/my/fix.d.ts b/src/typings/my/fix.d.ts new file mode 100644 index 0000000..34d6c5f --- /dev/null +++ b/src/typings/my/fix.d.ts @@ -0,0 +1,66 @@ +interface HTMLCanvasElement { + getContext(contextId: "webgl2", contextAttributes?: WebGLContextAttributes): WebGLRenderingContext | null; +} + +interface WebGLRenderingContext { + readonly MAX_ELEMENT_INDEX: number; +} + +interface Window { + readonly Notification: any; +} + +interface NodeModule { + hot?: { accept(): void; }; +} + +interface Promise { + finally(handler: () => any): Promise; +} + +declare const requestIdleCallback: (callback: () => void) => number; +declare const cancelIdleCallback: (idle: number) => void; + +declare const Zone: any; +declare const DEVELOPMENT: boolean; +declare const TOOLS: boolean; +declare const SERVER: boolean; +declare const BETA: boolean; +declare const TIMING: boolean; +declare const TESTS: boolean; + +declare module '*.json'; +declare module 'connect-mongo'; + +declare module 'passport-google-oauth20' { export const Strategy: any; } +declare module 'passport-twitter' { export const Strategy: any; } +declare module 'passport-facebook' { export const Strategy: any; } +declare module '@passport-next/passport-google-oauth2' { export const Strategy: any; } +declare module '@passport-next/passport-facebook' { export const Strategy: any; } +declare module 'passport-github2' { export const Strategy: any; } +declare module 'passport-vkontakte' { export const Strategy: any; } +declare module 'passport-yahoo-oauth' { export const Strategy: any; } +declare module 'passport-tumblr' { export const Strategy: any; } +declare module 'passport-deviantart' { export const Strategy: any; } +declare module 'passport-steam' { export const Strategy: any; } +declare module 'passport-patreon' { export const Strategy: any; } + +declare module 'timsort' { + export function sort(array: T[], compare: (a: T, b: T) => number): void; +} + +declare module 'color-convert' { + export const hex: { + lab(color: string): [number, number, number]; + }; +} + +declare module 'delta-e' { + export interface LAB { + L: number; + A: number; + B: number; + } + + export function getDeltaE00(a: LAB, b: LAB): number; +} diff --git a/src/typings/my/node-async.d.ts b/src/typings/my/node-async.d.ts new file mode 100644 index 0000000..e7c686a --- /dev/null +++ b/src/typings/my/node-async.d.ts @@ -0,0 +1,22 @@ +declare module "fs" { + import * as Promise from "bluebird"; + + export function writeFileAsync(filename: string, data: any): Promise; + export function writeFileAsync(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; } | string): Promise; + export function writeFileAsync(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; } | string): Promise; + export function appendFileAsync(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; } | string): Promise; + export function appendFileAsync(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; } | string): Promise; + export function appendFileAsync(filename: string, data: any): Promise; + export function readFileAsync(filename: string, encoding: string): Promise; + export function readFileAsync(filename: string, options: { encoding: string; flag?: string; }): Promise; + export function readFileAsync(filename: string, options: { flag?: string; }): Promise; + export function readFileAsync(filename: string): Promise; + export function readdirAsync(path: string): Promise; + export function unlinkAsync(path: string): Promise; + export function mkdirAsync(path: string): Promise; + export function mkdirAsync(path: string, mode: number): Promise; + export function mkdirAsync(path: string, mode: string): Promise; + export function renameAsync(oldPath: string, newPath: string): Promise; + export function statAsync(path: string): Promise; + export function rmdirAsync(path: string): Promise; +} diff --git a/src/typings/my/patreon.d.ts b/src/typings/my/patreon.d.ts new file mode 100644 index 0000000..d503e24 --- /dev/null +++ b/src/typings/my/patreon.d.ts @@ -0,0 +1,62 @@ +declare module 'patreon' { + export interface PatronDataRelation { + data: { + id: string; + type: string; + }; + links: { + related: string; + }; + } + + export interface PatronDataEntry { + attributes: { + title?: string; + description?: string; + amount_cents?: number; + created_at?: string; + declined_since?: string | null; + patron_pays_fees?: boolean; + pledge_cap_cents?: number; + total_historical_amount_cents?: number; + }; + id: string; + relationships: { + patron: PatronDataRelation; + reward: PatronDataRelation; + }; + type: string; + } + + export interface PatronData { + rawJson: { + data: PatronDataEntry[]; + included: PatronDataEntry[]; + links: { + next?: string; + }; + }; + } + + export interface PatreonClient { + (pathname: string): Promise; + getStore(): any; + setStore(store: { sync: () => void }): void; + } + + export interface PatreonTokens { + access_token: string; + refresh_token: string; + expires_in: string; + scope: string; + token_type: string; + } + + export interface PatreonOAuthClient { + getTokens(redirectCode: string, redirectUri: string): Promise; + refreshToken(refreshToken: string): Promise; + } + + export function patreon(accessToken: string): PatreonClient; + export function oauth(clientId: string, clientSecret: string): PatreonOAuthClient; +} diff --git a/tsconfig-aot-es.json b/tsconfig-aot-es.json new file mode 100644 index 0000000..e42df63 --- /dev/null +++ b/tsconfig-aot-es.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig-aot.json", + "compilerOptions": { + "target": "es2017", + "paths": { + "core-js/es6": ["src/fixes/empty.js"], + "lodash": ["src/ts/lodash.ts"] + } + } +} \ No newline at end of file diff --git a/tsconfig-aot.json b/tsconfig-aot.json new file mode 100644 index 0000000..6b8a53e --- /dev/null +++ b/tsconfig-aot.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "es5", + "module": "es2015", + "baseUrl": ".", + "preserveConstEnums": false, + "paths": { + "lodash": ["src/ts/lodash.ts"] + } + }, + "exclude": [ + "build", + "tools", + "src/scripts", + "src/ts/server", + "src/ts/tests", + "src/ts/tools", + "src/ts/experiments", + "node_modules" + ], + "angularCompilerOptions": { + "genDir": "src/ts", + "skipMetadataEmit": true, + "preserveWhitespaces": true + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8493a2c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": [ + "dom", + "es6" + ], + "module": "commonjs", + "moduleResolution": "node", + "importHelpers": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "declaration": false, + "noEmitOnError": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noImplicitReturns": true, + "strict": true, + "strictFunctionTypes": true, + "sourceMap": true, + "preserveConstEnums": true + }, + "exclude": [ + "build", + "tools", + "src/scripts", + "node_modules" + ] +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..62ae03b --- /dev/null +++ b/tslint.json @@ -0,0 +1,81 @@ +{ + "rules": { + "ban": [ + true, + "name", + "length", + "focus" + ], + "class-name": true, + "forin": true, + "indent": [ + true, + "tabs" + ], + "label-position": true, + "no-arg": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "adjacent-overload-signatures": true, + "no-internal-module": true, + "no-namespace": true, + "no-construct": true, + "no-consecutive-blank-lines": [ + true, + 2 + ], + "no-debugger": true, + "no-duplicate-variable": true, + "no-eval": true, + "no-trailing-whitespace": true, + "linebreak-style": [ + true, + "CRLF" + ], + "max-line-length": [ + true, + 130 + ], + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-whitespace" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-separator", + "check-type" + ], + "eofline": true, + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "array-type": [ + true, + "array" + ] + } +} \ No newline at end of file diff --git a/views/index.pug b/views/index.pug new file mode 100644 index 0000000..c2573ae --- /dev/null +++ b/views/index.pug @@ -0,0 +1,79 @@ +doctype html +html(lang="en") + head + meta(charset="utf-8") + meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no") + meta(name="description" content=description) + + if noindex + meta(name="robots" content="noindex, nofollow") + + base(href=base) + title=title + style!=inlineStyle + + link(rel="stylesheet" href=style) + + //- open graph + meta(property="og:title" content=title) + meta(property="og:site_name" content=title) + meta(property="og:type" content="website") + meta(property="og:url" content=host.replace(/\/$/, '')) + meta(property="og:description" content=description) + meta(property="og:image" content=logo) + if facebookAppId + meta(property="fb:app_id" content=facebookAppId) + + //- twitter card + if twitterLink + meta(name="twitter:card" content="summary") + meta(name="twitter:site" content=twitterLink.replace('https://twitter.com/', '@')) + meta(name="twitter:title" content=title) + meta(name="twitter:description" content=description) + meta(name="twitter:image" content=logo) + + //- favicons + link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png") + link(rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png") + link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png") + link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png") + link(rel="icon" type="image/png" sizes="194x194" href="/favicon-194x194.png") + link(rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png") + link(rel="manifest" href="/manifest.json?v2") + link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#d1a73c") + + //- other + meta(name="theme-color" content="#e6c966") + meta(name="apple-mobile-web-app-capable" content="yes") + + body( + data-key="kmncxb908z" + data-version=version + data-supporter-link=supporterLink + data-twitter-link=twitterLink + data-email=email + data-copyright=title + data-host=host + data-local=local + data-public=isPublic + data-sw=sw + data-token=token + data-wasm=wasm) + pony-town-app + #loading + img(src=loadingImage alt="") + div Loading + p.loading-error + | Could not load the game, #[a.loading-error-btn(href="/reset.html") reload] to continue. + + if oauthProviders + script#oauth-providers(type="application/json")!=JSON.stringify(oauthProviders) + + if socketOptions + script#socket-options(type="text/plain")!=socketOptions + + if production + script(src=script async) + else + script(src="http://localhost:8091/assets/scripts/vendor.js") + script(src="http://localhost:8091" + script.replace('/adm/', '/')) diff --git a/webpack.common.js b/webpack.common.js new file mode 100644 index 0000000..d7ccb58 --- /dev/null +++ b/webpack.common.js @@ -0,0 +1,74 @@ +const webpack = require('webpack'); +const path = require('path'); + +module.exports = { + context: path.join(__dirname, 'src'), + output: { + path: path.resolve(__dirname, 'build', 'assets', 'scripts'), + filename: '[name].js', + publicPath: '/assets/scripts/', + }, + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.js$/, + use: ['angular2-template-loader'], + exclude: [/node_modules/], + }, + { + test: /\.css$/, + use: ['raw-loader'], + }, + { + test: /\.html$/, + use: ['raw-loader'], + }, + { + test: /\.pug$/, + use: [ + 'raw-loader', + { + loader: 'pug-html-loader', + query: { doctype: 'html', plugins: require('pug-plugin-ng') }, + }, + ], + }, + { + test: /\.scss$/, + use: [ + 'raw-loader', + { + loader: 'postcss-loader', + options: { + ident: 'postcss', + plugins: loader => [ + require('autoprefixer')('last 2 versions'), + require('cssnano')({ discardComments: { removeAll: true } }), + ], + }, + }, + { + loader: 'sass-loader', + options: { includePaths: ['src/styles'] }, + }, + ], + }, + { + // Mark files inside `@angular/core` as using SystemJS style dynamic imports. + // Removing this will cause deprecation warnings to appear. + test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, + parser: { system: true }, // enable SystemJS + }, + ], + }, + plugins: [ + new webpack.ContextReplacementPlugin( + /\@angular(\\|\/)core(\\|\/)fesm5/, + path.resolve(__dirname, 'src', 'ts'), + {} + ), + ], +}; diff --git a/webpack.dev.js b/webpack.dev.js new file mode 100644 index 0000000..e3790ff --- /dev/null +++ b/webpack.dev.js @@ -0,0 +1,67 @@ +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const common = require('./webpack.common.js'); + +const compilerOptions = { + ...require('./tsconfig.json').compilerOptions, + target: 'es5', + module: 'es2015', +}; + +module.exports = merge(common, { + mode: 'development', + entry: { + 'bootstrap': './ts/bootstrap', + 'bootstrap-admin': './ts/bootstrap-admin', + 'bootstrap-tools': './ts/bootstrap-tools', + }, + devtool: 'cheap-eval-source-map', + devServer: { + host: '0.0.0.0', + hot: true, + historyApiFallback: true, + publicPath: '/assets/scripts/', + stats: 'minimal', + }, + stats: 'minimal', + output: { + pathinfo: false + }, + watchOptions: { + ignored: /node_modules/, + }, + module: { + rules: [ + { + test: /\.ts$/, + use: [ + { + loader: 'ts-loader', + options: { + compilerOptions, + }, + }, + 'angular2-template-loader', + ], + }, + ], + }, + optimization: { + removeAvailableModules: false, + removeEmptyChunks: false, + splitChunks: { + cacheGroups: { + commons: { + test: /[\\/]node_modules[\\/]/, + name: 'vendor', + chunks: 'all', + }, + }, + }, + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new webpack.NamedModulesPlugin(), + new webpack.DefinePlugin({ DEVELOPMENT: true, TOOLS: true, SERVER: false, BETA: true, TIMING: true, TESTS: false }), + ], +}); diff --git a/webpack.prod.js b/webpack.prod.js new file mode 100644 index 0000000..fe996ea --- /dev/null +++ b/webpack.prod.js @@ -0,0 +1,89 @@ +const path = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const WrapperPlugin = require('wrapper-webpack-plugin'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); +const { AngularCompilerPlugin } = require('@ngtools/webpack'); +const common = require('./webpack.common.js'); +const config = require('./config.json'); + +const analytics = config.analytics ? ` + (function(i, s, o, g, r, a, m) {i['GoogleAnalyticsObject']=r; i[r]=i[r]||function() { + (i[r].q=i[r].q||[]).push(arguments) }, i[r].l=1*new Date(); a=s.createElement(o), + m=s.getElementsByTagName(o)[0]; a.async=1; a.src=g; m.parentNode.insertBefore(a, m) + }) (window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); + ga('create', '${config.analytics.trackingID}', 'auto'); + ga('send', 'pageview'); +` : ``; + +const compilerOptions = require('./tsconfig-aot.json').compilerOptions; +const compilerOptionsES = require('./tsconfig-aot-es.json').compilerOptions; + +const scripts = [ + ['bootstrap', 'app/app.module#AppModule', 'assets', compilerOptions, 'tsconfig-aot.json', 5, false], + // ['bootstrap-es', 'app/app.module#AppModule', 'assets', compilerOptionsES, 'tsconfig-aot-es.json', 7, false], + ['bootstrap-admin', 'admin/admin.module#AdminAppModule', 'assets-admin', compilerOptions, 'tsconfig-aot.json', 5, true], + ['bootstrap-tools', 'tools/tools.module#ToolsAppModule', 'assets', compilerOptions, 'tsconfig-aot.json', 5, true], +]; + +function getScripts(args) { + const { analyze, main, admin } = args; + + if (analyze || main) { + return [scripts[0]]; + } else if (admin) { + return [scripts[2]]; + } else { + return scripts; + } +} + +module.exports = (args = {}) => + getScripts(args) + .map(([script, entry, outDir, compilerOptions, tsConfigPath, ecma, TOOLS]) => merge(common, { + mode: 'production', + performance: { + maxEntrypointSize: 10000000, + maxAssetSize: 10000000, + }, + entry: { + [script]: `./ts/${script}`, + }, + output: Object.assign({}, common.output, { + filename: '[name]-[chunkhash:10].js', + path: path.resolve(__dirname, 'build', outDir, 'scripts'), + }), + devtool: script === 'bootstrap' ? 'source-map' : false, + node: { Buffer: false }, + module: { + rules: [ + { + test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + use: [ + { loader: '@ngtools/webpack', options: { sourcemap: true, compilerOptions } }, + ], + }, + ], + }, + optimization: { + minimizer: [ + new UglifyJSPlugin({ + sourceMap: true, + uglifyOptions: { + ecma, + mangle: !args.debug, + output: { comments: false }, + compress: ecma !== 5 ? { inline: 1 } : {}, + }, + }), + ], + }, + plugins: [ + new AngularCompilerPlugin({ tsConfigPath, entryModule: `src/ts/components/${entry}` }), + (script === 'bootstrap' && !args.analyze) ? new WrapperPlugin({ test: /\.js$/, header: analytics }) : undefined, + args.analyze ? new BundleAnalyzerPlugin({ analyzerMode: 'static' }) : undefined, + new webpack.DefinePlugin({ DEVELOPMENT: false, TOOLS, SERVER: false, BETA: !!args.beta, TIMING: !!args.timing, TESTS: false }), + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + ].filter(x => x), + })); diff --git a/workbox-config.js b/workbox-config.js new file mode 100644 index 0000000..26402d1 --- /dev/null +++ b/workbox-config.js @@ -0,0 +1,45 @@ +const { hash } = require('./src/ts/generated/hash.json'); +const ignore = /^\/((a(pi(\d?|-tools)|uth|dm(in)?)|(bl|m)ep)|tools|reload)\/|\.html|^\/sw\.js$/; + +module.exports = { + globDirectory: 'build/', + globPatterns: [ + '**/*.{js,css,png,jpg,wasm}', + ], + globIgnores: [ + '**/assets-admin/**', + '**/bootstrap-es*', + '**/bootstrap-tools*', + '**/style-tools*', + '**/style-inline*', + '**/sw.min.js', + ], + dontCacheBustURLsMatching: /-[a-f0-9]{10}\./, + swDest: 'build/sw.js', + navigateFallback: '/', + navigateFallbackBlacklist: [ignore], + maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10 MB + // importWorkboxFrom: 'local', + skipWaiting: true, + clientsClaim: true, + templatedURLs: { + '/': hash, + }, + directoryIndex: '/', + runtimeCaching: [ + { + urlPattern: ignore, + handler: 'NetworkOnly', + }, + { + urlPattern: /^\/assets\/music/, + handler: 'CacheOnly', + options: { + cacheName: 'music-cache', + expiration: { + maxAgeSeconds: 1000 * 3600 * 24 * 30, // 30 days + }, + }, + }, + ], +};